diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000000000000000000000000000000000000..071ebc630500d685a1de0c09e261e3eb476ea5f9 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,2 @@ +pages/docs/img/2022_models/slides.pdf filter=lfs diff=lfs merge=lfs -text +pages/docs/img/2022_models/poster.pdf filter=lfs diff=lfs merge=lfs -text diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index a33e8f0fcddb2760ec9a9e83e337ccd5035da873..408c56d36930b08aed2118ed0148904cfd140cab 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -22,11 +22,13 @@ build: image: openjdk:11 stage: build script: - - ./gradlew --console=plain --no-daemon assemble jar + - ./gradlew --console=plain --no-daemon assemble testClasses jar artifacts: paths: - "ragconnect.base/build/libs/ragconnect-*.jar" - "ragconnect.base/src/gen" + - "ragconnect.tests/src/test/02-after-ragconnect" + - "ragconnect.tests/src/test/java-gen" expire_in: 1 week test: @@ -57,9 +59,9 @@ publish-dev: - "./gradlew setDevVersionForCI" - "./gradlew publish" except: - - master + - main -publish-master: +publish-main: image: openjdk:11 stage: publish needs: @@ -67,26 +69,26 @@ publish-master: script: - "./gradlew publish" only: - - master + - main ragdoc_build: image: name: "git-st.inf.tu-dresden.de:4567/jastadd/ragdoc-builder" - entrypoint: [""] stage: ragdoc_build needs: - build script: - JAVA_FILES=$(find ragconnect.base/src/ -name '*.java') + - echo $JAVA_FILES | wc -l - /ragdoc-builder/start-builder.sh -excludeGenerated -d data/ $JAVA_FILES artifacts: paths: - "data/" + expire_in: 1 week ragdoc_view: image: name: "git-st.inf.tu-dresden.de:4567/jastadd/ragdoc-view:relations" - entrypoint: [""] stage: ragdoc_view needs: - ragdoc_build @@ -95,10 +97,11 @@ ragdoc_view: - mkdir -p pages/docs/ragdoc - OUTPUT_DIR=$(pwd -P)/pages/docs/ragdoc - cd /ragdoc-view/src/ && rm -rf data && ln -s $DATA_DIR - - /ragdoc-view/build-view.sh --output-path=$OUTPUT_DIR + - BASE_HREF=/ragconnect/ragdoc/ /ragdoc-view/build-view.sh --output-path=$OUTPUT_DIR artifacts: paths: - "pages/docs/ragdoc" + expire_in: 1 week build_cloc: image: "alpine:latest" @@ -111,6 +114,7 @@ build_cloc: artifacts: paths: - pages/docs/cloc.md + expire_in: 1 week .pages-template: image: python:3.10.0-bullseye @@ -128,15 +132,16 @@ pages-dry-run: extends: .pages-template except: - dev - - master + - main pages: extends: .pages-template artifacts: paths: - public/ + expire_in: 1 week only: - - master + - main pages-dev: extends: .pages-template @@ -148,6 +153,7 @@ pages-dev: - public/ reports: dotenv: build.env + expire_in: 1 week only: - dev diff --git a/pages/docs/adding.md b/pages/docs/adding.md index e06e9fc190a42dba21bec50807508c1ccf0d4bfa..ec7d67090ba46d64cb2a6c24028fce7170edc12a 100644 --- a/pages/docs/adding.md +++ b/pages/docs/adding.md @@ -1,6 +1,7 @@ # Adding `RagConnect` to your project If you want to use `RagConnect`, either use the latest [pre-build version](#use-packaged-version) or clone the repository and [build it yourself](#build-from-source). +Either way, a task for [compiling RelAst specifications](#compiling-relast-specifications) has to be specified. ## Use packaged version @@ -25,7 +26,7 @@ configurations { } dependencies { - ragconnectClasspath group: 'de.tudresden.inf.st', name: 'ragconnect', version: '0.2.3' + ragconnectClasspath group: 'de.tudresden.inf.st', name: 'ragconnect', version: '{{ragconnect_version()}}' } ``` @@ -60,11 +61,11 @@ cd ragconnect ls ragconnect.base/build/libs/ ``` -This `ragconnect-<version>.jar` can then be copied to your project. +This `ragconnect-{{ragconnect_version()}}.jar` can then be copied to your project. Please note, that you can safely use `ragconnect.jar` as filename, because the version can always be printed using `java -jar path/to/ragconnect.jar --version`. ```bash -cp ragconnect.base/build/libs/ragconnect-<version>.jar ../your-project/libs/ragconnect.jar +cp ragconnect.base/build/libs/ragconnect-{{ragconnect_version()}}.jar ../your-project/libs/ragconnect.jar cd ../your-project/ ``` @@ -90,7 +91,7 @@ task ragConnect(type: JavaExec) { You might need to add another task for [compiling relast specifications](#compiling-relast-specifications). -# Compiling RelAst specifications +## Compiling RelAst specifications The task to compile `RagConnect` specifications is typically accompanied by a task to invoke the [RelAst compiler](http://relational-rags.eu/) and the [JastAdd gradle plugin](https://plugins.gradle.org/plugin/org.jastadd). Currently, the parameter `--useJastAddNames` is **required**, and it may cause incompatibilities if not set. diff --git a/pages/docs/changelog.md b/pages/docs/changelog.md index 79386ad968a22edb7c53c4dd0c5c253bdbf89280..004fd88622fd0e138486fd60dbf7c773b50fd9be 100644 --- a/pages/docs/changelog.md +++ b/pages/docs/changelog.md @@ -1,5 +1,17 @@ # Changelog +## 1.1.0 + +### Changes + +- Added new REST client handler ([#61](https://git-st.inf.tu-dresden.de/jastadd/ragconnect/-/issues/61)) + +### Development Changes + +- Bugfix: "error: variable handler is already defined" when using multiple protocols [#58](https://git-st.inf.tu-dresden.de/jastadd/ragconnect/-/issues/58) +- Bugfix: Inherited components of a type can not be chosen as port targets [#59](https://git-st.inf.tu-dresden.de/jastadd/ragconnect/-/issues/59) +- Update structure and design of documentation pages + ## 1.0.0 ### Changes diff --git a/pages/docs/compiler.md b/pages/docs/compiler.md index e89ad8fc8169e084c97736d6447c3466f1e2bc27..93b0bc0f48ec7e1e6083fe97236c5a8701057135 100644 --- a/pages/docs/compiler.md +++ b/pages/docs/compiler.md @@ -3,84 +3,49 @@ The compiler is JastAdd-compliant, i.e., it accepts all flags available for JastAdd, though there is no process how to chain pre-processors _yet_. Additional options are as follows. -| Name | Required (Default) | Description | -|---|---|---| -| `--rootNode` | Yes | Root node in the base grammar. | -| `--protocols` | No (`mqtt`) | Protocols to enable, currently available: `java` (experimental), `mqtt`, `rest`. | -| `--printYaml` | No (false) | Print out YAML instead of generating files. | -| `--verbose` | No (false) | Print more messages while compiling. | -| `--logReads` | No (false) | Enable logging for every received message. | -| `--logWrites` | No (false) | Enable logging for every sent message. | -| `--logIncremental` | No (false) | Enable logging for observer in incremental dependency tracking. | -| `--logTarget` | No (`console`) | Logging target to use, currently available: `console, slf4j`. | -| `--experimental-jastadd-329` | No (false) | Use tracing events `INC_FLUSH_START` and `INC_FLUSH_END` ([JastAdd issue #329][jastadd-issue-329]), see [section about automatic dependency tracking](using.md#dependency-tracking-automatically-derived). | -| `--incremental` | No (false) | Enables incremental dependency tracking (if `tracing` is also set appropriately). | -| `--tracing[=flush]` | No (false) | Enables incremental dependency tracking (if `incremental` is also set appropriately). | -| `--version` | No (false) | Print version info and exit (reused JastAdd option) | -| `--o` | No (`.`) | Output directory (reused JastAdd option) | +## Table with available options + +| Name | Required (Default) | Description | +|------------------------------|--------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `--rootNode` | Yes | Root node in the base grammar. | +| `--protocols` | No (`mqtt`) | Protocols to enable, one of `java` (experimental), `mqtt`, `rest`, see [Handlers][handlers] for details. | +| `--printYaml` | No (false) | Print out YAML instead of generating files. | +| `--verbose` | No (false) | Print more messages while compiling. | +| `--logReads` | No (false) | Enable logging for every received message. | +| `--logWrites` | No (false) | Enable logging for every sent message. | +| `--logIncremental` | No (false) | Enable logging for observer in incremental dependency tracking. | +| `--logTarget` | No (`console`) | Logging target to use, currently available: `console, slf4j`. | +| `--experimental-jastadd-329` | No (false) | Use tracing events `INC_FLUSH_START` and `INC_FLUSH_END` ([JastAdd issue #329][jastadd-issue-329]), see [section about automatic dependency tracking][automatic-dependency-tracking]. | +| `--incremental` | No (false) | Enables incremental dependency tracking (if `tracing` is also set appropriately). | +| `--tracing[=flush]` | No (false) | Enables incremental dependency tracking (if `incremental` is also set appropriately). | +| `--version` | No (false) | Print version info and exit (reused JastAdd option) | +| `--o` | No (`.`) | Output directory (reused JastAdd option) | All files to be processed have to be passed as arguments. Their type is deduced by the file extension (`ast` and `relast` for input grammars, `connect` and `ragconnect` for RagConnect definitions file). -# Additional software dependencies +## Additional software dependencies Using RagConnect itself does not introduce dependencies. However, depending on the selected protocols and/or used features, additional dependencies are required when using the generated code. -## Communication protocol characteristics +### Used features -### Java - -- Protocol identifier: `java` -- URI scheme: `java://<ignored-host>[:ignored-port]/<topic>` - - the value for host and port are always ignored, but are necessary to form a legal URI -- No required runtime dependencies -- Additional remarks: - - First leading slash not included in topic. - - Currently, the default mappings are applied, which requires a consumer to expect `byte[]` (instead of a more intuitive token or node value). This might change in future versions. - -### MQTT - -- Protocol identifier: `mqtt` -- URI scheme: `mqtt://<broker-host>[:port]/<topic>` -- Default port: 1883 -- Type for mapping definitions: `byte[]` -- Required runtime dependencies: - - `group: 'org.fusesource.mqtt-client', name: 'mqtt-client', version: '1.15'` -- Additional remarks: - - First leading slash not included in topic. - - Mqtt is selected by default, so this dependency therefore is required "by default". - - Might work with other versions of `org.fusesource.mqtt-client.mqtt.client` as well. - -### REST - -- Protocol identifier: `rest` -- URI scheme: `rest://localhost[:port]/<path>` -- Default port: 4567 -- Type for mapping definitions: `String` -- Required runtime dependencies: - - `group: 'com.sparkjava', name: 'spark-core', version: '2.9.3'` -- Additional remarks: - - Host is always `localhost`. - - Might work with newer versions of `com.sparkjava.spark-core` as well. - - For debugging, it is beneficial to include an implementation for [SLF4J][slf4j]. - -## Used features - -### Automatic dependency tracking +#### Automatic dependency tracking - Condition: When passing `--incremental` and `--tracing=flush` to RagConnect - Required runtime dependencies: _none_ - Required options for RelAST compiler: _none_ - Required options for JastAdd: - - `--incremental` - - `--tracing=flush` + - `--incremental=param` (enable incremental evaluation) + - `--tracing=flush` (enable tracing of events) + - `--cache=all` (set all attributes to be cached) - Remarks: - Other (additional) values passed to those two options must be equal (e.g., `--incremental=param` passed to RagConnect must be also passed to JastAdd) - Other values besides `flush` can be added to `--tracing` - [Feature description](using.md#dependency-tracking-automatically-derived) -### (Safer) Automatic dependency tracking +#### (Safer) Automatic dependency tracking - Condition: When passing `--experimental-jastadd-329` to RagConnect - Required runtime dependencies: _none_ @@ -90,7 +55,7 @@ However, depending on the selected protocols and/or used features, additional de - JastAdd version has to support `INC_FLUSH_START` and `INC_FLUSH_END` (i.e., has [issue #329][jastadd-issue-329] resolved) - [Feature description](using.md#dependency-tracking-automatically-derived) -### Tree/List Ports +#### Tree/List Ports - Condition: When using ports along with default mappings for subtrees - Required runtime dependencies: @@ -102,7 +67,7 @@ However, depending on the selected protocols and/or used features, additional de - Remarks: - [Feature description](using.md#an-advanced-example) -### Logging Target SLF4J +#### Logging Target SLF4J - Condition: When passing `--logTarget=slf4j` to RagConnect - Required runtime dependencies: @@ -113,4 +78,5 @@ However, depending on the selected protocols and/or used features, additional de - Additionally, a slf4j binding is required, see [the slf4j user manual][slf4j] [jastadd-issue-329]: https://bitbucket.org/jastadd/jastadd2/issues/329/add-event-for-completion-of-flush -[slf4j]: https://www.slf4j.org/manual.html +[automatic-dependency-tracking]: using.md#dependency-tracking-automatically-derived +[handlers]: handlers.md diff --git a/pages/docs/extending.md b/pages/docs/extending.md index 14fa8c8dbb525900f26c0ccbf9f7b45ac17544e5..4ed4ce1285b402a747f69f59580310a10c037a92 100644 --- a/pages/docs/extending.md +++ b/pages/docs/extending.md @@ -5,9 +5,9 @@ To add a new communication protocol, the following locations have to be changed ### Within `ragconnect.base/src/main/resources` {% raw %} -- Add a new handler `ABCHandler.jadd`, similar to the existing handlers. +- Add a new handler `ABCHandler.jadd`, similar to the existing handlers. A handler must have a constructor accepting a single String parameter, and must have a `close()` method cleaning up any held resources. - In `handler.mustache`, add further methods if needed for handler usage in the application code (similar to `{{rootNodeName}}.{{SetupWaitUntilReadyMethodName}}` for `mqtt`) -- In `receiveDefinition.mustache` and `sendDefinition.mustache`: add a new case in the switch statements defining the logic to happen upon connect and disconnect for both definitions. If the new protocol is close to a PUSH semantic, follow `mqtt`. If it is closer to PULL semantic, follow `rest`. +- In `receiveDefinition.mustache` and `sendDefinition.mustache`: add a new case in the switch statements defining the logic to happen upon connect and disconnect for both definitions (that are four distinct locations). If the new protocol is close to a PUSH semantic, follow `mqtt`. If it is closer to PULL semantic, follow `restClient`. {% endraw %} ### Within `ragconnect.base/src/main/jastadd` @@ -18,7 +18,7 @@ In `Handlers.jrag`: Add a new attribute `RagConnect.abcHandler()` returning the In `Compiler.java`: - Add a new choice for `--protocols` similar to the existing ones -- Add a newly constructed handler in `setConfiguration` with the needed fields (definition file name within `resources` directory, commonly `ABCHandler.jadd`; class name of the handler; unique name for the protocol; whether the handler is used, i.e., if it was given in `--protocols`) +- Add a newly constructed handler in `setConfiguration` with the needed fields (class name of the handler; unique name for the protocol (must be a valid Java identifier); whether the handler is used, i.e., if it was given in `--protocols`) Furthermore, new test cases are appreciated, see [below](#writing-tests). diff --git a/pages/docs/handlers.md b/pages/docs/handlers.md new file mode 100644 index 0000000000000000000000000000000000000000..f126ea126b73c7568f9cf753eb712d6c2f80ed8e --- /dev/null +++ b/pages/docs/handlers.md @@ -0,0 +1,70 @@ +# Communication Protocol Characteristics (Handlers) + +## Java (experimental) + +Uses Java methods to supply values (receive) and for callbacks (send). + +- Protocol identifier: `java` +- URI scheme: `java://<ignored-host>[:ignored-port]/<topic>` + - the value for host and port are always ignored, but are necessary to form a legal URI +- No required runtime dependencies +- Receive behaviour: Use the generated method `ragconnectJavaPush` to pass a value to the receiving port. +- Send behaviour: When the value to be sent changes, previously registered callbacks are invoked. +- Additional remarks: + - First leading slash not included in topic. + - Currently, the default mappings are applied, which requires a consumer to expect `byte[]` (instead of a more intuitive token or node value). This might change in future versions. + + +## MQTT + +Use an MQTT broker to receive and send messages. + +- Protocol identifier: `mqtt` +- URI scheme: `mqtt://<broker-host>[:port]/<topic>` +- Default port: 1883 +- Type for mapping definitions: `byte[]` +- Required runtime dependencies: + - `group: 'org.fusesource.mqtt-client', name: 'mqtt-client', version: '1.15'` +- Receive behaviour: Upon connection, instruct the MQTT broker to listen for messages on some topic and pass the value of those messages to the receiving port. +- Send behaviour: When the value to be sent changes, publish a message to the topic specified upon connection. +- Additional remarks: + - First leading slash not included in topic. + - Mqtt is selected by default, so this dependency therefore is required "by default". + - Might work with other versions of `org.fusesource.mqtt-client.mqtt.client` as well. + + +## REST Server + +Create a new REST server with its own target routes. + +- Protocol identifier: `rest` +- URI scheme: `rest://localhost[:port]/<path>` +- Default port: 4567 +- Type for mapping definitions: `byte[]` +- Required runtime dependencies: + - `group: 'com.sparkjava', name: 'spark-core', version: '2.9.3'` +- Receive behaviour: Upon connection, create a new PUT connection and pass the value of every call to this PUT route to the receiving port. +- Send behaviour: Upon connection, create a new GET connection and serve the latest value at this GET route. +- Additional remarks: + - Host is always `localhost`. + - Targets to be invoked need to replace `rest` with `http` + - Might work with newer versions of `com.sparkjava.spark-core` as well. + - For debugging, it is beneficial to include an implementation for [SLF4J][slf4j]. + + +## REST Client + +Invoke REST routes to fetch and send values. + +- Protocol identifier: `restClient` +- URI scheme: `restClient://localhost[:port]/<path>` +- Default port: 80 +- Type for mapping definitions: `byte[]` +- No required runtime dependencies +- Receive behaviour: Whenever the accessor is called, a GET request fetches the latest data and returns this data. +- Send behaviour: When the value to be sent changes, a PUT request sends this data. +- Additional remarks: + - Invoked target replaces `restClient` with `http` + - **Important constraint**: Receiving ports are only supported for tokens, since they interrupt the getter method! + +[slf4j]: https://www.slf4j.org/manual.html diff --git a/pages/docs/img/2022_acsos/architecture.png b/pages/docs/img/2022_acsos/architecture.png new file mode 100644 index 0000000000000000000000000000000000000000..dec5fddc467ee34a0df7645ea5a056f31441c61e Binary files /dev/null and b/pages/docs/img/2022_acsos/architecture.png differ diff --git a/pages/docs/img/2022_models/poster.pdf b/pages/docs/img/2022_models/poster.pdf new file mode 100644 index 0000000000000000000000000000000000000000..337666ceb491e1a1a0b1999a2342f77509416535 --- /dev/null +++ b/pages/docs/img/2022_models/poster.pdf @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:227796a5a16324465dab6c2cdf082e02f9b17c7fc3b97b7f1f1e05a8e5b38aa5 +size 5178029 diff --git a/pages/docs/img/2022_models/slides.pdf b/pages/docs/img/2022_models/slides.pdf new file mode 100644 index 0000000000000000000000000000000000000000..9df4a199dd964ce5e4825dceb320bbf555ef9606 --- /dev/null +++ b/pages/docs/img/2022_models/slides.pdf @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b257454ed44ae2b94b5373275984a77ca2ca9cf8e5829bc5a1f82d5f93fce18a +size 6713501 diff --git a/pages/docs/img/ST-black.png b/pages/docs/img/ST-black.png new file mode 100644 index 0000000000000000000000000000000000000000..1dff814753e73b1d10ddd456817e6d72ee7b82e2 Binary files /dev/null and b/pages/docs/img/ST-black.png differ diff --git a/pages/docs/inner-workings.md b/pages/docs/inner-workings.md index c3f88279d126ec6f4f878621c6eb0f0650061225..93103a23ed1e125700aad1eec61a9ec6f7f16037 100644 --- a/pages/docs/inner-workings.md +++ b/pages/docs/inner-workings.md @@ -20,11 +20,11 @@ The other main aspect (which is currently not really used) is `IntermediateToYAM This is used to generate a YAML file containing the data used by mustache. It can be used by the default mustache implementation together with the templates. -# Implementation Details +## Implementation Details In the following, details for special implementation topics are discussed. -## Forwarding +### Forwarding When a nonterminal is used in a send ports, it needs an implicit forwarding attribute to work, because only _computed elements_ can be sent. Since the nonterminal itself should be sent, the generated attribute simply returns this nonterminal. @@ -34,9 +34,9 @@ This way, the dependency tracking registers a dependency between structure and t The attribute (as well as any other generated element) is prefixed with `_ragconnect_` to avoid potential name conflicts with user-specified elements. -# Implementation Hints +## Implementation Hints -## Debugging Tests and Finding Bugs +### Debugging Tests and Finding Bugs To help with finding errors/bugs when tests fail, there are several things to find the correct spot. diff --git a/pages/docs/use_cases.md b/pages/docs/use_cases.md index 56c5af23a33a6aded02584dd4d1b37828dfdac4d..7e79e8839493ba3b4a54f9242de2bcbd224aebbf 100644 --- a/pages/docs/use_cases.md +++ b/pages/docs/use_cases.md @@ -1,5 +1,27 @@ # Use cases with `RagConnect` +## MODELS Paper (2022) - Codename 'Ros3Rag' + +In the paper [*"Incremental Causal Connection for Self-Adaptive Systems Based on Relational Reference Attribute Grammars"*](https://doi.org/10.1145/3550355.3552460), a [previous use case](#mpm4cps-paper-2020-codename-ros2rag) was extended to Collaborative, Teaching-Based Robotic Cells. + + + +This paper was presented on October, 26th 2022 in the technical track of the [MODELS 2022 conference](https://conf.researchr.org/home/models-2022) (Oct 23 - Oct 29). +There is an [artifact hosted at Zenodo](https://doi.org/10.5281/zenodo.7009758) containing all source code and the executable case study. +For more information, see the [presented slides](img/2022_models/slides.pdf) or the accompanied [poster](img/2022_models/poster.pdf). + +The repository with the used source code can be found at: <https://git-st.inf.tu-dresden.de/ceti/ros/models2022> + +## ACSOS Paper (2022) - Codename 'Motion Grammar Demo' + +In the paper [*"Specifying Reactive Robotic Applications +With Reference Attribute Motion Grammars"*](http://mg.relational-rags.eu), motion grammars (an older approach by [Dantham and Stilman](https://doi.org/10.1109/TRO.2013.2239553)) were implemented using Relational RAGs. + + + +This paper was presented on September, 21st 2022 during the [Posters and Demos session](https://2022.acsos.org/track/acsos-2022-posters-and-demos) of the [ACSOS 2022 conference](https://2022.acsos.org/). +For more information, see <http://mg.relational-rags.eu/>. + ## MPM4CPS Paper (2020) - Codename 'Ros2Rag' In the publication [*"Connecting conceptual models using Relational Reference Attribute Grammars"*](https://doi.org/10.1145/3417990.3421437), a use case involving a simulated robot arm and two different models connected to it was shown. @@ -28,22 +50,3 @@ docker-compose up rag_app # Terminal 3: Goal-Model docker-compose up rag_goal ``` - -## ACSOS Paper (2022) - Codename 'Motion Grammar Demo' - -In the paper [*"Specifying Reactive Robotic Applications -With Reference Attribute Motion Grammars"*](http://mg.relational-rags.eu), motion grammars (an older approach by [Dantham and Stilman](https://doi.org/10.1109/TRO.2013.2239553)) were implemented using Relational RAGs. -This paper will be presented on September, 21st 2022 during the [Posters and Demos session](https://2022.acsos.org/track/acsos-2022-posters-and-demos) of the [ACSOS 2022 conference](https://2022.acsos.org/). -For more information, see <http://mg.relational-rags.eu/>. - -## MODELS Paper (2022) - Codename 'Ros3Rag' - -In the paper [*"Incremental Causal Connection for Self-Adaptive Systems Based on Relational Reference Attribute Grammars"*](https://doi.org/10.1145/3550355.3552460), a [previous use case](#mpm4cps-paper-codename-ros2rag) was extended to Collaborative, Teaching-Based Robotic Cells. - - - -This paper will be presented in the technical track of the [MODELS 2022 conference](https://conf.researchr.org/home/models-2022) (Oct 23 - Oct 29). -There is an [artifact hosted at Zenodo](https://doi.org/10.5281/zenodo.7009758) containing all source code and the executable case study. -Furthermore, the presentation slides and a poster will be available soon. - -The repository with the used source code can be found at: <https://git-st.inf.tu-dresden.de/ceti/ros/models2022> diff --git a/pages/docs/using.md b/pages/docs/using.md index e9705465ab851344f0abda19acd75e1cad334cb4..9930e0e02cff757a217d1719746b53f969f584d0 100644 --- a/pages/docs/using.md +++ b/pages/docs/using.md @@ -4,7 +4,7 @@ The full example is available at <https://git-st.inf.tu-dresden.de/jastadd/ragco ## Preparation and Specification -The following examples are inspired by real [test cases](https://git-st.inf.tu-dresden.de/jastadd/ragconnect/-/tree/master/ragconnect.tests/src/test/01-input) read1write2 and tokenValueSend. +The following examples are inspired by real [test cases](https://git-st.inf.tu-dresden.de/jastadd/ragconnect/-/tree/main/ragconnect.tests/src/test/01-input) read1write2 and tokenValueSend. The idea is to have two non-terminals, where input information is received on one of them, and - after transformation - is sent out by both. Let's use the following grammar: @@ -127,11 +127,11 @@ Round maps float f to int {: ... connecting first could mean to store the first rounded value and not propagating this update, since no dependencies are set, and not propagating further updates leading to the same rounded value even after setting the dependencies. -# An advanced example +## An advanced example Non-terminal children can also be selected as ports (not only tokens). -## Normal Non-Terminal Children +### Normal Non-Terminal Children Receiving normal non-terminal children and optionals means to replace them with a new node deserialized from the received message. Sending them involves serializing a node, and sending this representation in a message. @@ -158,7 +158,7 @@ To process non-terminals, default mappings are provided for every non-terminal t They use the JSON serialization offered by the RelAST compiler, i.e., interpret the message as a `String`, deserialize the content reading the message as JSON, or vice versa. Additional dependencies are required to use this feature, as detailed in [the compiler section](compiler.md#treelist-ports). -## Receiving List Children +### Receiving List Children When receiving list children, there are a few more options to match the connection to given requirements. @@ -171,7 +171,7 @@ ReceiverRoot ::= A* ; Several options are possible (please also refer to the specification of the [connect DSL](dsl.md): -### (empty) +#### (empty) A message for a list port can be interpreted as a complete list (a sequence of nodes of type `A`) by not specifying any special keyword: @@ -179,7 +179,7 @@ A message for a list port can be interpreted as a complete list (a sequence of n receive ReceiverRoot.A ; ``` -### with add +#### with add Upon receiving the message, the deserialized list can also be appended to the existing list instead of replace the latter. This can be achieved using the keyword `with add` : @@ -188,7 +188,7 @@ This can be achieved using the keyword `with add` : receive with add ReceiverRoot.Alfa ; ``` -### indexed +#### indexed A message for a list port can also be interpreted as an element of this list. @@ -203,7 +203,7 @@ The list must have enough elements once a message is received. receiverRoot.connectA("<some-url>", 1); ``` -### indexed (wildcard) +#### indexed (wildcard) Similar to the `indexed` case above, messages are interpreted as an element of the list, but the connection can also be made using a "wildcard topic" and without an index. Then, once a message is received from a new concrete topic, the deserialized element will be appended to the list and this topic is associated with the index of the newly added element. @@ -223,7 +223,7 @@ assertEquals(receiverRoot.getAList(), list("1", "other")); assertEquals(receiverRoot.getAList(), list("new", "other")); ``` -### indexed + with add +#### indexed + with add Combining `indexed` and `with add` results in a connection, where messages are interpreted as elements of the list, and new elements are appended to the existing list. In that case, wildcard and non-wildcard connections behave in the same way, as no index has to be passed, and the element is always append at the end. @@ -245,7 +245,7 @@ assertEquals(receiverRoot.getAList(), list("1", "other")); assertEquals(receiverRoot.getAList(), list("1", "other", "new")); ``` -## Using attributes as port targets +### Using attributes as port targets As described in the [DSL specification](dsl.md), attributes can be used as port targets. They can only be used in send ports, and the return type of the attribute must be specified in the connect specification (because aspect files are not handled completely yet). diff --git a/pages/mkdocs.yml b/pages/mkdocs.yml index 8ff01eda0839ea0231b98a152a2820a50bef92c1..21b10b06e5bb470225199e1f3b7481e72fb55001 100644 --- a/pages/mkdocs.yml +++ b/pages/mkdocs.yml @@ -7,6 +7,7 @@ nav: - "Using RagConnect (by Example)": using.md - "RagConnect Specification Language": dsl.md - "Compiler options": compiler.md + - "Communication Protocols": handlers.md - "Use Cases": use_cases.md - "Inner workings": inner-workings.md - "Evaluation Metrics: Lines of Code": cloc.md @@ -15,12 +16,26 @@ nav: - "API documentation": ragdoc/index.html theme: - name: readthedocs - custom_dir: custom_theme/ + name: material + font: false + palette: + # Palette toggle for light mode + - scheme: default + toggle: + icon: material/brightness-7 + name: Switch to dark mode + # Palette toggle for dark mode + - scheme: slate + toggle: + icon: material/brightness-4 + name: Switch to light mode + favicon: img/ST-black.png + icon: + logo: material/connection markdown_extensions: - toc: - permalink: + permalink: true - admonition plugins: diff --git a/pages/requirements.txt b/pages/requirements.txt index 15f2ba17edb0e5afa8886d4a3cc191baf4f89248..25d7d3180e71c0bcdc15dc8bf7d53d08dec06173 100644 --- a/pages/requirements.txt +++ b/pages/requirements.txt @@ -1,6 +1,7 @@ -mkdocs==1.2.2 -mkdocs-git-revision-date-localized-plugin==0.10.3 -mkdocs-macros-plugin==0.6.3 -mike==1.1.2 -Jinja2==2.11.2 -MarkupSafe==1.1.1 +mkdocs==1.4.2 +mkdocs-git-revision-date-localized-plugin==1.1.0 +mkdocs-macros-plugin==0.7.0 +mkdocs-material==8.5.10 +mkdocs-material-extensions==1.1 +Jinja2==3.1.2 +MarkupSafe==2.1.1 diff --git a/ragconnect.base/build.gradle b/ragconnect.base/build.gradle index b00a4301c412901a14dd13199829a2ad7904eab1..7eceb8458788b01ba26a88116a2bf8e3a048eb56 100644 --- a/ragconnect.base/build.gradle +++ b/ragconnect.base/build.gradle @@ -249,7 +249,7 @@ task newVersion() { task setDevVersionForCI() { doFirst { def props = new Properties() - props['version'] = version + "-$System.env.CI_PIPELINE_IID" + props['version'] = version + "-dev-$System.env.CI_PIPELINE_IID" props.store(file(versionFile).newWriter(), null) } } diff --git a/ragconnect.base/src/main/jastadd/Analysis.jrag b/ragconnect.base/src/main/jastadd/Analysis.jrag index 79f37b38b3839982f784f1dd79b094a663aaa921..407efa363aa6208730add0899d650a2cd73a497a 100644 --- a/ragconnect.base/src/main/jastadd/Analysis.jrag +++ b/ragconnect.base/src/main/jastadd/Analysis.jrag @@ -98,10 +98,22 @@ aspect Analysis { eq ContextFreeTypePortTarget.hasAttributeResetMethod() = false; // --- needProxyToken --- - syn boolean TokenComponent.needProxyToken() = !getDependencySourceDefinitionList().isEmpty() || - getTokenPortTargetList().stream() - .map(PortTarget::containingPortDefinition) - .anyMatch(PortDefinition::shouldNotResetValue); + syn boolean TokenComponent.needProxyToken() { + for (Component comp : meOwnedByOthers()) { + TokenComponent tokenComp = comp.asTokenComponent(); + if (tokenComp.directNeedProxyToken()) { + return true; + } + } + return directNeedProxyToken(); + } + + syn boolean TokenComponent.directNeedProxyToken() { + return !getDependencySourceDefinitionList().isEmpty() || + getTokenPortTargetList().stream() + .map(PortTarget::containingPortDefinition) + .anyMatch(def -> def.shouldNotResetValue() || ragconnect().restClientHandler().getInUse()); + } // --- effectiveUsedAt --- coll Set<PortDefinition> MappingDefinition.effectiveUsedAt() diff --git a/ragconnect.base/src/main/jastadd/Handlers.jrag b/ragconnect.base/src/main/jastadd/Handlers.jrag index e64a8c5d0db5467f9a4b0d7451e8c38ab22a8bbf..8d891d5025877f387425803fa39e930a7b023314 100644 --- a/ragconnect.base/src/main/jastadd/Handlers.jrag +++ b/ragconnect.base/src/main/jastadd/Handlers.jrag @@ -2,6 +2,7 @@ aspect RagConnectHandlers { syn Handler RagConnect.javaHandler() = resolveHandlerByName("java"); syn Handler RagConnect.mqttHandler() = resolveHandlerByName("mqtt"); syn Handler RagConnect.restHandler() = resolveHandlerByName("rest"); + syn Handler RagConnect.restClientHandler() = resolveHandlerByName("restClient"); private Handler RagConnect.resolveHandlerByName(String uniqueName) { for (Handler handler : getHandlerList()) { diff --git a/ragconnect.base/src/main/jastadd/Intermediate.jadd b/ragconnect.base/src/main/jastadd/Intermediate.jadd index 97cc5148e9e6901d8a1b23cc965adbd57a7c0d44..ebf3b0af00dcb9f6acf7dd95cd658cdb0ead5a49 100644 --- a/ragconnect.base/src/main/jastadd/Intermediate.jadd +++ b/ragconnect.base/src/main/jastadd/Intermediate.jadd @@ -232,7 +232,8 @@ aspect MustacheMappingApplicationAndDefinition { eq MRelationSendDefinition.preemptiveReturn() = "return false;"; eq MTokenReceiveDefinition.firstInputVarName() = "message"; - eq MTokenReceiveDefinition.preemptiveExpectedValue() = getterMethodCall(); + // if receiverName variable is set, use internal getter instead to avoid StackOverflow + eq MTokenReceiveDefinition.preemptiveExpectedValue() = "(" + getPortDefinition().receiverName() + " != null ? get" + getPortDefinition().token().internalName() + "() : " + getterMethodCall() + ")"; eq MTokenReceiveDefinition.preemptiveReturn() = "return;"; eq MTokenSendDefinition.firstInputVarName() = getterMethodCall(); @@ -358,7 +359,14 @@ aspect MustacheRagConnect { eq ContextFreeTypePortTarget.impliedPortDefinitions() { JastAddList<PortDefinition> result = super.impliedPortDefinitions(); PortDefinition containingDef = containingPortDefinition(); + Set<TypeComponent> seenTypeComponents = new HashSet<>(); for (TypeComponent typeComponent : getTypeDecl().occurencesInProductionRules()) { + // only process each once + if (seenTypeComponents.contains(typeComponent)) { + continue; + } + seenTypeComponents.add(typeComponent); + List<PortDefinition> defsForTypeComponent = lookupGivenTypePortDefinitions(typeComponent); if (!defsForTypeComponent.stream().anyMatch(containingDef::matchesType)) { // there is no user-defined port definition for this typeComponent yet @@ -398,7 +406,7 @@ aspect MustacheReceiveAndSendAndHandleUri { syn String PortDefinition.connectParameterName() = "uriString"; syn String PortDefinition.disconnectMethodName() { - // if both (send and receive) are defined for an port, ensure methods with different names + // if both (send and receive) are defined for a port, ensure methods with different names String extra; if (getPortTarget().isTokenPortTarget()) { extra = lookupTokenPortDefinitions(token()).size() > 1 ? uniqueSuffix() : ""; @@ -518,6 +526,7 @@ aspect MustacheSendDefinition { syn boolean PortDefinition.relationPortWithListRole() = getPortTarget().relationPortWithListRole(); + syn String PortDefinition.receiverName() = getPortTarget().receiverName(); syn String PortDefinition.senderName() = getPortTarget().senderName(); syn java.util.List<SendIncrementalObserverEntry> PortDefinition.sendIncrementalObserverEntries() { @@ -576,6 +585,9 @@ containingPortDefinition().getIndexBasedListAccess()); syn boolean PortTarget.relationPortWithListRole() = false; eq RelationPortTarget.relationPortWithListRole() = getRole().isListRole(); + syn String PortTarget.receiverName() = ragconnect().internalRagConnectPrefix() + "_receiver_" + entityName(); + eq ContextFreeTypePortTarget.receiverName() = null; + syn String PortTarget.senderName() = ragconnect().internalRagConnectPrefix() + "_sender_" + entityName(); eq ContextFreeTypePortTarget.senderName() = null; @@ -625,17 +637,37 @@ containingPortDefinition().getIndexBasedListAccess()); aspect MustacheTokenComponent { // === TokenComponent === + syn java.util.List<DependencyDefinition> TokenComponent.dependencySourceDefinitionsOwnedByMe() { + java.util.List<DependencyDefinition> result = new java.util.ArrayList<>(); + result.addAll(getDependencySourceDefinitions()); + for (Component comp : meOwnedByOthers()) { + result.addAll(comp.asTokenComponent().getDependencySourceDefinitions()); + } + return result; + } + syn String TokenComponent.internalName() = needProxyToken() ? ragconnect().internalRagConnectPrefix() + getName() : getName(); syn String TokenComponent.javaType() = effectiveJavaTypeUse().prettyPrint(); + syn PortDefinition TokenComponent.normalTokenReceiveDef() { + for (Component comp : meOwnedByOthers()) { + PortDefinition maybeResult = comp.asTokenComponent().directNormalTokenReceiveDef(); + if (maybeResult != null) { + return maybeResult; + } + } + return directNormalTokenReceiveDef(); + } + syn PortDefinition TokenComponent.normalTokenSendDef() { - for (PortTarget target : getTokenPortTargetList()) { - if (target.isTokenPortTarget() && target.containingPortDefinition().shouldNotResetValue()) { - return target.containingPortDefinition(); + for (Component comp : meOwnedByOthers()) { + PortDefinition maybeResult = comp.asTokenComponent().directNormalTokenSendDef(); + if (maybeResult != null) { + return maybeResult; } } - return null; + return directNormalTokenSendDef(); } syn String TokenComponent.parentTypeName() = containingTypeDecl().getName(); @@ -663,6 +695,26 @@ aspect MustacheTokenComponent { } // > see MustacheSend for updateMethodName, writeMethodName + + // === attributes needed for computing above ones === + syn PortDefinition TokenComponent.directNormalTokenReceiveDef() { + for (PortTarget target : getTokenPortTargetList()) { + if (target.isTokenPortTarget() && !target.containingPortDefinition().getSend() && + ragconnect().restClientHandler().getInUse()) { + return target.containingPortDefinition(); + } + } + return null; + } + + syn PortDefinition TokenComponent.directNormalTokenSendDef() { + for (PortTarget target : getTokenPortTargetList()) { + if (target.isTokenPortTarget() && target.containingPortDefinition().shouldNotResetValue()) { + return target.containingPortDefinition(); + } + } + return null; + } } aspect MustacheTypeDecl { @@ -670,6 +722,10 @@ aspect MustacheTypeDecl { syn String TypeComponent.parentTypeName() = containingTypeDecl().getName(); syn String TypeComponent.disconnectMethodName() { List<TypePortTarget> typePortTargets = getTypePortTargets(); + while (typePortTargets.isEmpty() && getRealComponent() != null) { + // get for "real" typeComponent (of super-type) and use its getTypePortTargets + typePortTargets = getRealComponent().asTypeComponent().getTypePortTargets(); + } if (typePortTargets.isEmpty()) { return "MISSING_PORT"; } else { @@ -681,7 +737,7 @@ aspect MustacheTypeDecl { syn List<TypeComponent> TypeDecl.occurencesInProductionRules() { List<TypeComponent> result = new ArrayList<>(); for (TypeDecl typeDecl : program().typeDecls()) { - for (Component comp : typeDecl.getComponentList()) { + for (Component comp : typeDecl.allComponentsAsOwnedByMe()) { if (comp.isTypeComponent() && comp.asTypeComponent().getTypeDecl().equals(this)) { result.add(comp.asTypeComponent()); } diff --git a/ragconnect.base/src/main/jastadd/IntermediateToYAML.jrag b/ragconnect.base/src/main/jastadd/IntermediateToYAML.jrag index c247d9d0a69b2f548e04fe69adcf35620fd69255..c02518e3658395beffa69c3f5ba6983b826de62d 100644 --- a/ragconnect.base/src/main/jastadd/IntermediateToYAML.jrag +++ b/ragconnect.base/src/main/jastadd/IntermediateToYAML.jrag @@ -181,7 +181,8 @@ aspect IntermediateToYAML { result.put("DependencySourceDefinitions" , dependencySourceDefinitions); result.put("javaType" , javaType()); - result.put("normalTokenSendDef" , normalTokenSendDef().toYAML()); + result.put("normalTokenSendDef" , ASTNode.<PortDefinition,MappingElement>safeCall( + normalTokenSendDef(), def -> def.toYAML())); result.put("parentTypeName" , parentTypeName()); return result; } @@ -209,6 +210,38 @@ aspect IntermediateToYAML { protected StringElement ASTNode.sanitizeValueForYAML(String value) { return StringElement.of(value.replace("\"" , "\\\"").replace("\n" , "\\n")); } + + // FIXME: remove refine once fixed in upstream mustache + refine Printing protected StringBuilder KeyValuePair.prettyPrint(StringBuilder sb, boolean printIndent, String indent) { + if (printIndent) sb.append(indent); + if (isCollapsed()) { + sb.append("\""); + } + sb.append(getKey()); + if (isCollapsed()) { + sb.append("\""); + } + sb.append(":"); + if (getValue() == null) { + sb.append(" null"); + } else if (getValue().isComplexElement() && !getValue().isEmpty() && !getValue().isCollapsed()) { + sb.append("\n"); + getValue().prettyPrint(sb, true, indent + PRINT_INDENT); + } else { + sb.append(" "); + getValue().prettyPrint(sb, false, indent); + } + return sb; + } + + // FIXME: remove refine once fixed in upstream mustache + refine Helpers protected SimpleElement ComplexElement.makeStringElement(String value) { + // simple test, check for special characters + return containsAny(value, ":#,[{\"\n") ? + StringElement.of(value.replace("\n", "\\n").replace("\"", "\\\"")) : + ValueElement.of(value); + } + } aspect Navigation { diff --git a/ragconnect.base/src/main/jastadd/NameResolution.jrag b/ragconnect.base/src/main/jastadd/NameResolution.jrag index e470430d8f5d355cd90d4e79503ee431de994a18..18a35f7295b63225f7695c045604df713950fbe0 100644 --- a/ragconnect.base/src/main/jastadd/NameResolution.jrag +++ b/ragconnect.base/src/main/jastadd/NameResolution.jrag @@ -27,7 +27,7 @@ aspect RagConnectNameResolution { java.util.List<PortDefinition> result = new java.util.ArrayList<>(); for (PortDefinition def : onlyGiven ? givenPortDefinitionList() : allPortDefinitionList()) { PortTarget target = def.getPortTarget(); - if (target.isTypePortTarget() && target.asTypePortTarget().getType().equals(type)) { + if (target.isTypePortTarget() && target.asTypePortTarget().getType().matches(type) && target.asTypePortTarget().getType().containingTypeDecl().equals(type.containingTypeDecl())) { result.add(def); } } @@ -91,7 +91,7 @@ aspect RagConnectNameResolution { String childTypeName = id.substring(dotIndex + 1); TypeDecl type = program().resolveTypeDecl(parentTypeName); // iterate over components and find the matching typeComponent - for (Component comp : type.getComponentList()) { + for (Component comp : type.allComponentsAsOwnedByMe()) { if (comp.isTypeComponent() && comp.getName().equals(childTypeName)) { return comp.asTypeComponent(); } @@ -114,7 +114,7 @@ aspect RagConnectNameResolution { String childTypeName = id.substring(dotIndex + 1); TypeDecl type = program().resolveTypeDecl(parentTypeName); // iterate over components and find the matching typeComponent - for (Component comp : type.getComponentList()) { + for (Component comp : type.allComponentsAsOwnedByMe()) { if (comp.getName().equals(childTypeName)) { return comp; } @@ -138,7 +138,7 @@ aspect RagConnectNameResolution { String tokenName = id.substring(dotIndex + 1); TypeDecl type = program().resolveTypeDecl(typeName); // iterate over components and find the matching tokenComponent - for (Component comp : type.getComponentList()) { + for (Component comp : type.allComponentsAsOwnedByMe()) { if (comp.isTokenComponent() && comp.getName().equals(tokenName)) { return comp.asTokenComponent(); } @@ -175,10 +175,63 @@ aspect RagConnectNameResolution { } return null; } +} +aspect RelastNameResolution { syn boolean Role.matches(String typeName, String roleName) = false; eq NavigableRole.matches(String typeName, String roleName) { return getType().getName().equals(typeName) && getName().equals(roleName); } + /** Returns all components including inherited ones */ + syn List<Component> TypeDecl.allComponents() { + List<Component> result = new ArrayList<>(); + getComponentList().forEach(result::add); + if (hasSuperType()) { + mergeComponentLists(result, getSuperType().allComponents()); + } + return result; + } + + /** Returns same as allComponents() but for each component the containingTypeDecl will compute to this component */ + syn nta JastAddList<Component> TypeDecl.allComponentsAsOwnedByMe() { + JastAddList<Component> result = new JastAddList<>(); + for (Component comp : allComponents()) { + Component newComp = comp.treeCopy(); + newComp.setRealComponent(comp); + result.add(newComp); + } + return result; + } + + public static void TypeDecl.mergeComponentLists(List<Component> subTypeResult, List<Component> superTypeResult) { + for (int i = superTypeResult.size() - 1; i >= 0; i--) { + Component superTypeComponent = superTypeResult.get(i); + if (subTypeResult.stream().noneMatch(subTypeComponent -> subTypeComponent.matches(superTypeComponent))) { + subTypeResult.add(0, superTypeComponent); + } + } + } + + syn boolean Component.matches(Component other) = false; + eq TokenComponent.matches(Component other) { + if (!other.isTokenComponent()) { return false; } + return getName().equals(other.getName()) && (getJavaTypeUse() == null && other.asTokenComponent().getJavaTypeUse() == null || getJavaTypeUse().prettyPrint().equals(other.asTokenComponent().getJavaTypeUse().prettyPrint())); + } + eq TypeComponent.matches(Component other) = matchesNameAndType(other); + eq NormalComponent.matches(Component other) { + if (!other.isTypeComponent() || !other.asTypeComponent().isNormalComponent()) { return false; } + return super.matches(other); + } + eq ListComponent.matches(Component other) { + if (!other.isTypeComponent() || !other.asTypeComponent().isListComponent()) { return false; } + return super.matches(other); + } + eq OptComponent.matches(Component other) { + if (!other.isTypeComponent() || !other.asTypeComponent().isOptComponent()) { return false; } + return super.matches(other); + } + syn boolean TypeComponent.matchesNameAndType(Component other) { + return getName().equals(other.getName()) && getTypeDecl().equals(other.asTypeComponent().getTypeDecl()); + } } diff --git a/ragconnect.base/src/main/jastadd/Navigation.jrag b/ragconnect.base/src/main/jastadd/Navigation.jrag index 8b23c000fb8136af504d6dc452a42d5dd6ae82ca..ffa4596e54ca19295c1c886eecf2276e38e16497 100644 --- a/ragconnect.base/src/main/jastadd/Navigation.jrag +++ b/ragconnect.base/src/main/jastadd/Navigation.jrag @@ -175,4 +175,25 @@ aspect RagConnectNavigation { // --- isListComponent --- (defined in PP, but only on TypeComponent) syn boolean Component.isListComponent() = false; + + syn Set<Component> Component.meOwnedByOthers() { + Set<Component> result = new HashSet<>(); + Deque<TypeDecl> todo = new ArrayDeque<>(); + todo.add(containingTypeDecl()); + while (!todo.isEmpty()) { + TypeDecl current = todo.pop(); + todo.addAll(current.getSubTypeList()); + + for (Component comp : current.allComponentsAsOwnedByMe()) { + if (comp.matches(this)) { + result.add(comp); + } + } + } + return result; + } +} +aspect RelastNavigation { + // TODO only token-components from the nta "owned-by-me" must be added to Program.allTokenComponents + //TypeDecl contributes nta allComponentsAsOwnedByMe() to Program.allTokenComponents(); } diff --git a/ragconnect.base/src/main/jastadd/RagConnect.relast b/ragconnect.base/src/main/jastadd/RagConnect.relast index 7282bb3fda5771c3b1ea87a4bfae235c3950b640..60f08491d4ac721c4dedb7a37280b1cdcdd385a3 100644 --- a/ragconnect.base/src/main/jastadd/RagConnect.relast +++ b/ragconnect.base/src/main/jastadd/RagConnect.relast @@ -47,3 +47,5 @@ Configuration ::= <EvaluationCounter:boolean> <ExperimentalJastAdd329:boolean>; rel Configuration.RootNode -> TypeDecl ; + +rel Component.RealComponent? -> Component ; diff --git a/ragconnect.base/src/main/jastadd/Util.jadd b/ragconnect.base/src/main/jastadd/Util.jadd index 174f8cdb9e40db6f3b1edef519b31daade2c1bba..7c8f5e65ac603fdc38a49b1961286b566f097b54 100644 --- a/ragconnect.base/src/main/jastadd/Util.jadd +++ b/ragconnect.base/src/main/jastadd/Util.jadd @@ -4,6 +4,9 @@ aspect Util { if (s.isEmpty()) return ""; return Character.toUpperCase(s.charAt(0)) + s.substring(1); } + static <Input,Result> Result ASTNode.safeCall(Input input, java.util.function.Function<Input, Result> function) { + return input == null ? null : function.apply(input); + } protected T JastAddList.firstChild() { return getChild(0); } protected T JastAddList.lastChild() { return getChild(getNumChild() - 1); } diff --git a/ragconnect.base/src/main/java/org/jastadd/ragconnect/compiler/Compiler.java b/ragconnect.base/src/main/java/org/jastadd/ragconnect/compiler/Compiler.java index be81bfd4ea84123624e97c0047b85e7b29bda348..173404cee21fdb497276bc99c169dbe0c5864e58 100644 --- a/ragconnect.base/src/main/java/org/jastadd/ragconnect/compiler/Compiler.java +++ b/ragconnect.base/src/main/java/org/jastadd/ragconnect/compiler/Compiler.java @@ -39,6 +39,7 @@ public class Compiler extends AbstractCompiler { private static final String OPTION_PROTOCOL_JAVA = "java"; private static final String OPTION_PROTOCOL_MQTT = "mqtt"; private static final String OPTION_PROTOCOL_REST = "rest"; + private static final String OPTION_PROTOCOL_REST_CLIENT = "restClient"; public Compiler() { super("ragconnect", true); @@ -183,6 +184,7 @@ public class Compiler extends AbstractCompiler { .addDefaultValue(OPTION_PROTOCOL_MQTT, "Enable MQTT") .addAcceptedValue(OPTION_PROTOCOL_JAVA, "Enable Java (experimental)") .addAcceptedValue(OPTION_PROTOCOL_REST, "Enable REST") + .addAcceptedValue(OPTION_PROTOCOL_REST_CLIENT, "Enable REST client (experimental)") ); optionPrintYaml = addOption( new BooleanOption("printYaml", "Print out YAML instead of generating files and exit.") @@ -341,6 +343,8 @@ public class Compiler extends AbstractCompiler { ragConnect.addHandler(new Handler("JavaHandler", "java", optionProtocols.hasValue(OPTION_PROTOCOL_JAVA))); ragConnect.addHandler(new Handler("MqttServerHandler", "mqtt", optionProtocols.hasValue(OPTION_PROTOCOL_MQTT))); ragConnect.addHandler(new Handler("RestServerHandler", "rest", optionProtocols.hasValue(OPTION_PROTOCOL_REST))); + ragConnect.addHandler(new Handler("RestClientHandler", "restClient", + optionProtocols.hasValue(OPTION_PROTOCOL_REST_CLIENT))); } public String generateAspect(RagConnect ragConnect) { diff --git a/ragconnect.base/src/main/resources/RagConnectObserver.mustache b/ragconnect.base/src/main/resources/RagConnectObserver.mustache index 24ed61c1a2ed6ceabec733b45f76a51eeae5f1e0..4414782b3dd1eb6e3e93ce4fe90fc35462416b79 100644 --- a/ragconnect.base/src/main/resources/RagConnectObserver.mustache +++ b/ragconnect.base/src/main/resources/RagConnectObserver.mustache @@ -119,8 +119,8 @@ aspect RagConnectObserver { } RagConnectObserverStartEntry startEntry = startEntries.peekFirst(); if (node == startEntry.node && - attribute == startEntry.attributeString && - value == startEntry.flushIncToken) { + java.util.Objects.equals(attribute, startEntry.attributeString) && + java.util.Objects.equals(value, startEntry.flushIncToken)) { // create a copy of the queue to avoid entering this again causing an endless recursion RagConnectObserverEntry[] entriesToProcess = entryQueue.toArray(new RagConnectObserverEntry[entryQueue.size()]); entryQueue.clear(); diff --git a/ragconnect.base/src/main/resources/RestClientHandler.mustache b/ragconnect.base/src/main/resources/RestClientHandler.mustache new file mode 100644 index 0000000000000000000000000000000000000000..24307ef7c8e22274bf71929774a8f7e12521b216 --- /dev/null +++ b/ragconnect.base/src/main/resources/RestClientHandler.mustache @@ -0,0 +1,65 @@ +public class RestClientHandler { + private java.net.http.HttpClient httpClient; + public RestClientHandler(String name) { + httpClient = java.net.http.HttpClient.newHttpClient(); + } + + public RagConnectReceiver newReceiverFor(RagConnectToken connectToken, java.util.function.BiConsumer<String, byte[]> callback) { + java.net.URI target = java.net.URI.create(connectToken.uri.toString().replaceFirst("restClient", "http")); + java.net.http.HttpRequest httpRequest = java.net.http.HttpRequest.newBuilder(target) + .GET() + .build(); + return () -> { + byte[] rawInput; + try { + rawInput = fetchFrom(httpRequest); + } catch (Exception e) { + {{logException}}("Exception when fetching from " + target, e); + return; + } + callback.accept("", rawInput); + }; + } + + byte[] fetchFrom(java.net.http.HttpRequest httpRequest) throws java.io.IOException, InterruptedException { + return httpClient.send(httpRequest, java.net.http.HttpResponse.BodyHandlers.ofByteArray()).body(); + } + + public RestClientPreparedRequest preparePut(java.net.URI uri) { + java.net.URI target = java.net.URI.create(uri.toString().replaceFirst("restClient", "http")); + java.net.http.HttpRequest.Builder httpRequestBuilder = java.net.http.HttpRequest.newBuilder(target); + return new RestClientPreparedRequest(httpRequestBuilder); + } + + void sendRequestAsync(RestClientPreparedRequest preparedRequest, byte[] message) { + java.net.http.HttpRequest httpRequest = preparedRequest.builder + .PUT(java.net.http.HttpRequest.BodyPublishers.ofByteArray(message)) + .build(); + httpClient.sendAsync(httpRequest, java.net.http.HttpResponse.BodyHandlers.discarding()); + } + + void sendRequestSync(RestClientPreparedRequest preparedRequest, byte[] message) { + java.net.http.HttpRequest httpRequest = preparedRequest.builder + .PUT(java.net.http.HttpRequest.BodyPublishers.ofByteArray(message)) + .build(); + java.net.http.HttpResponse response; + try { + response = httpClient.send(httpRequest, java.net.http.HttpResponse.BodyHandlers.ofString()); + {{logDebug}}("Response for message to {{log_}} is {{log_}} {{log_}}", + httpRequest, response.statusCode(), response.body()); + } catch (java.io.IOException | InterruptedException e) { + {{logException}}("Exception while sending {{log_}}: {{log_}}", httpRequest, e); + } + } + + public void close() { + // empty + } +} + +public class RestClientPreparedRequest { + java.net.http.HttpRequest.Builder builder; + RestClientPreparedRequest(java.net.http.HttpRequest.Builder builder) { + this.builder = builder; + } +} diff --git a/ragconnect.base/src/main/resources/RestHandler.mustache b/ragconnect.base/src/main/resources/RestHandler.mustache index 25192af366bfd4da29ace5c11d661b8cc8fd0992..4e8f228712fd7b6e6ee3c66e72f85c8d8ac5dd9b 100644 --- a/ragconnect.base/src/main/resources/RestHandler.mustache +++ b/ragconnect.base/src/main/resources/RestHandler.mustache @@ -20,13 +20,13 @@ public class RestServerHandler { return handler; } - public boolean newPUTConnection(RagConnectToken connectToken, java.util.function.Consumer<String> callback) { - tokensForRemoval.put(connectToken, callback); - resolveHandler(connectToken.uri).newPUTConnection(connectToken.uri.getPath(), callback); + public boolean newPUTConnection(RagConnectToken connectToken, java.util.function.BiConsumer<String, byte[]> consumer) { + tokensForRemoval.put(connectToken, consumer); + resolveHandler(connectToken.uri).newPUTConnection(connectToken.uri.getPath(), consumer); return true; } - public boolean newGETConnection(RagConnectToken connectToken, SupplierWithException<String> supplier) { + public boolean newGETConnection(RagConnectToken connectToken, SupplierWithException<byte[]> supplier) { tokensForRemoval.put(connectToken, supplier); resolveHandler(connectToken.uri).newGETConnection(connectToken.uri.getPath(), supplier); return true; @@ -52,8 +52,8 @@ public class RestHandler { private int port; /** Dispatch knowledge */ - private final java.util.Map<String, java.util.List<java.util.function.Consumer<String>>> callbacks; - private final java.util.Map<String, SupplierWithException<String>> suppliers; + private final java.util.Map<String, java.util.List<java.util.function.BiConsumer<String, byte[]>>> callbacks; + private final java.util.Map<String, SupplierWithException<byte[]>> suppliers; public RestHandler() { this.port = DEFAULT_PORT; @@ -64,23 +64,24 @@ public class RestHandler { public RestHandler setPort(int port) { this.port = port; start(); + spark.Spark.path("", () -> spark.Spark.before((request, response) -> {{logDebug}}("Request on {{log_}}: {{log_}}", request.pathInfo(), request.bodyAsBytes()))); return this; } - public void newPUTConnection(String path, java.util.function.Consumer<String> callback) { + public void newPUTConnection(String path, java.util.function.BiConsumer<String, byte[]> callback) { if (callbacks.containsKey(path)) { callbacks.get(path).add(callback); } else { // setup path - java.util.List<java.util.function.Consumer<String>> callbackList = new java.util.ArrayList<>(); + java.util.List<java.util.function.BiConsumer<String, byte[]>> callbackList = new java.util.ArrayList<>(); callbackList.add(callback); callbacks.put(path, callbackList); spark.Spark.put(path, (request, response) -> { - String content = request.body(); + byte[] content = request.bodyAsBytes(); java.util.Set<String> errors = new java.util.HashSet<>(); - for (java.util.function.Consumer<String> f : callbackList) { + for (java.util.function.BiConsumer<String, byte[]> f : callbackList) { try { - f.accept(content); + f.accept(path, content); } catch (Exception e) { errors.add(e.getMessage()); } @@ -94,7 +95,7 @@ public class RestHandler { } } - public void newGETConnection(String path, SupplierWithException<String> supplier) { + public void newGETConnection(String path, SupplierWithException<byte[]> supplier) { if (suppliers.get(path) != null) { {{logWarn}}("Overriding existing supplier for '{{log_}}'", path); } diff --git a/ragconnect.base/src/main/resources/handler.mustache b/ragconnect.base/src/main/resources/handler.mustache index 75bd14ce26638ceeee267251988a01b516de0970..435a88c8c765ed35fe72caeca96ca1770b3f0968 100644 --- a/ragconnect.base/src/main/resources/handler.mustache +++ b/ragconnect.base/src/main/resources/handler.mustache @@ -46,6 +46,12 @@ aspect RagConnectHandler { {{/InUse}} {{/restHandler}} +{{#restClientHandler}} + {{#InUse}} + {{> RestClientHandler}} + {{/InUse}} +{{/restClientHandler}} + class RagConnectToken { static java.util.concurrent.atomic.AtomicLong counter = new java.util.concurrent.atomic.AtomicLong(0); final long id; @@ -191,4 +197,7 @@ aspect RagConnectHandler { java.util.Optional.ofNullable(publishers.get(index)).ifPresent(publisher -> publisher.setLastValue(value)); } } + + interface RagConnectReceiver extends Runnable { + } } diff --git a/ragconnect.base/src/main/resources/ragconnect.mustache b/ragconnect.base/src/main/resources/ragconnect.mustache index 5bd0166a46bb6f02be2df10a2bfd26b9cc63d508..a38ac9b3151227d92dffb83365eef8a1ab27f49e 100644 --- a/ragconnect.base/src/main/resources/ragconnect.mustache +++ b/ragconnect.base/src/main/resources/ragconnect.mustache @@ -91,6 +91,7 @@ aspect RagConnect { mayHaveRewrite(); // check if --incremental is active Object checkIncremental = inc_throwAway_visited; + {{! TODO maybe check for something like _ragconnect_mqttHandler_computed to see if --cache=all }} {{/configIncrementalOptionActive}} } } diff --git a/ragconnect.base/src/main/resources/ragconnectVersion.properties b/ragconnect.base/src/main/resources/ragconnectVersion.properties index 08b12cf3b90afcc13f9ba32d5b42877333390019..40de0279b4519083b5533bf5f57e846b89e1a213 100644 --- a/ragconnect.base/src/main/resources/ragconnectVersion.properties +++ b/ragconnect.base/src/main/resources/ragconnectVersion.properties @@ -1,2 +1,2 @@ -#Tue Sep 06 12:31:39 CEST 2022 -version=1.0.0 +#Tue Aug 29 15:29:04 CEST 2023 +version=1.1.0 diff --git a/ragconnect.base/src/main/resources/receiveDefinition.mustache b/ragconnect.base/src/main/resources/receiveDefinition.mustache index b2e05ce6b2ce246b5225b98eecc7779bf3b0cfa0..f1ca58d6a02ba1dcfd3b47cb62e1daa6c92fd195 100644 --- a/ragconnect.base/src/main/resources/receiveDefinition.mustache +++ b/ragconnect.base/src/main/resources/receiveDefinition.mustache @@ -1,3 +1,5 @@ +private RagConnectReceiver {{parentTypeName}}.{{receiverName}} = null; + {{#typeIsList}} {{#IndexBasedListAccess}} private int {{parentTypeName}}.{{resolveInListMethodName}}(String topic) { @@ -116,13 +118,18 @@ private boolean {{parentTypeName}}.{{internalConnectMethodName}}(String {{connec {{#restHandler}} {{#InUse}} case "rest": - success = {{attributeName}}().newPUTConnection(connectToken, input -> { - // TODO wildcard-topic not supported yet - consumer.accept("", input.getBytes()); - }); + success = {{attributeName}}().newPUTConnection(connectToken, consumer); break; {{/InUse}} {{/restHandler}} + {{#restClientHandler}} + {{#InUse}} + case "restClient": + {{receiverName}} = {{attributeName}}().newReceiverFor(connectToken, consumer); + success = {{receiverName}} != null; + break; + {{/InUse}} + {{/restClientHandler}} default: {{logError}}("Unknown protocol '{{log_}}'.", scheme); success = false; @@ -161,6 +168,15 @@ public boolean {{parentTypeName}}.{{disconnectMethodName}}(String {{connectParam break; {{/InUse}} {{/restHandler}} + {{#restClientHandler}} + {{#InUse}} + case "restClient": disconnectingMethod = token -> { + {{receiverName}} = null; + return true; + }; + break; + {{/InUse}} + {{/restClientHandler}} default: {{logError}}("Unknown protocol '{{log_}}' in '{{log_}}' for disconnecting {{parentTypeName}}.{{entityName}}", scheme, {{connectParameterName}}); diff --git a/ragconnect.base/src/main/resources/sendDefinition.mustache b/ragconnect.base/src/main/resources/sendDefinition.mustache index 9c7dac4fc7a9bccc449fcdfced102c4ba865fbb2..45fa4de131a4331827e2643d04d51757c61e1fb2 100644 --- a/ragconnect.base/src/main/resources/sendDefinition.mustache +++ b/ragconnect.base/src/main/resources/sendDefinition.mustache @@ -7,23 +7,24 @@ public boolean {{parentTypeName}}.{{connectMethodName}}(String {{connectParamete switch (scheme) { {{#javaHandler}} {{#InUse}} - case "java": - final JavaHandler handler = {{attributeName}}(); + case "java": { + final JavaHandler handler = {{attributeName}}(); - {{senderName}}.add(() -> { - handler.push(path, {{lastValueGetterCall}}); - }{{#IndexBasedListAccess}}, index{{/IndexBasedListAccess}}, connectToken); - {{updateMethodName}}(); - if (writeCurrentValue) { - {{writeMethodName}}({{#IndexBasedListAccess}}index, {{/IndexBasedListAccess}}connectToken); + {{senderName}}.add(() -> { + handler.push(path, {{lastValueGetterCall}}); + }{{#IndexBasedListAccess}}, index{{/IndexBasedListAccess}}, connectToken); + {{updateMethodName}}(); + if (writeCurrentValue) { + {{writeMethodName}}({{#IndexBasedListAccess}}index, {{/IndexBasedListAccess}}connectToken); + } + success = true; + break; } - success = true; - break; {{/InUse}} {{/javaHandler}} {{#mqttHandler}} {{#InUse}} - case "mqtt": + case "mqtt": { final MqttHandler handler = {{attributeName}}().resolveHandler(uri); final String topic = {{attributeName}}().extractTopic(uri); {{senderName}}.add(() -> { @@ -45,18 +46,36 @@ public boolean {{parentTypeName}}.{{connectMethodName}}(String {{connectParamete } success = true; break; + } {{/InUse}} {{/mqttHandler}} {{#restHandler}} {{#InUse}} - case "rest": + case "rest": { success = {{attributeName}}().newGETConnection(connectToken, () -> { {{updateMethodName}}({{#IndexBasedListAccess}}index{{/IndexBasedListAccess}}); - return new String({{lastValueGetterCall}}); + return {{lastValueGetterCall}}; }); break; + } {{/InUse}} {{/restHandler}} + {{#restClientHandler}} + {{#InUse}} + case "restClient": + RestClientPreparedRequest preparedRequest = {{attributeName}}().preparePut(uri); + {{senderName}}.add(() -> { + {{! sync or async could be selected via options, see issue #62 }} + {{attributeName}}().sendRequestSync(preparedRequest, {{lastValueGetterCall}}); + }{{#IndexBasedListAccess}}, index{{/IndexBasedListAccess}}, connectToken); + {{updateMethodName}}({{#IndexBasedListAccess}}index{{/IndexBasedListAccess}}); + if (writeCurrentValue) { + {{writeMethodName}}({{#IndexBasedListAccess}}index, {{/IndexBasedListAccess}}connectToken); + } + success = true; + break; + {{/InUse}} + {{/restClientHandler}} default: {{logError}}("Unknown protocol '{{log_}}'.", scheme); success = false; @@ -118,6 +137,13 @@ public boolean {{parentTypeName}}.{{disconnectMethodName}}(String {{connectParam break; {{/InUse}} {{/restHandler}} + {{#restClientHandler}} + {{#InUse}} + case "restClient": + disconnectingMethod = {{senderName}}::remove; + break; + {{/InUse}} + {{/restClientHandler}} default: {{logError}}("Unknown protocol '{{log_}}' in '{{log_}}' for disconnecting {{parentTypeName}}.{{entityName}}", scheme, {{connectParameterName}}); @@ -162,9 +188,6 @@ protected void {{parentTypeName}}.{{writeMethodName}}({{#IndexBasedListAccess}}i {{#needForwarding}} syn {{{forwardingType}}} {{parentTypeName}}.{{forwardingName}}({{#IndexBasedListAccess}}int index{{/IndexBasedListAccess}}) { {{#relationPortWithListRole}} -// for (var element : {{realGetterMethodCall}}) { -// element.{{touchedTerminalsMethodName}}(); -// } {{realGetterMethodCall}}.stream().forEach(element -> element.{{touchedTerminalsMethodName}}()); return {{realGetterMethodCall}}; {{/relationPortWithListRole}} diff --git a/ragconnect.base/src/main/resources/tokenComponent.mustache b/ragconnect.base/src/main/resources/tokenComponent.mustache index 12238f4428bf2ef79951015425cf45285cca624b..ee669d87bf128bcecdd2a2830586bd26df83921f 100644 --- a/ragconnect.base/src/main/resources/tokenComponent.mustache +++ b/ragconnect.base/src/main/resources/tokenComponent.mustache @@ -1,6 +1,6 @@ public {{parentTypeName}} {{parentTypeName}}.set{{Name}}({{javaType}} value) { set{{internalName}}(value); - {{#DependencySourceDefinitions}} + {{#dependencySourceDefinitionsOwnedByMe}} for ({{targetParentTypeName}} target : get{{internalRelationPrefix}}TargetList()) { {{#targetPortDefinition}} if (target.{{updateMethodName}}()) { @@ -8,7 +8,7 @@ public {{parentTypeName}} {{parentTypeName}}.set{{Name}}({{javaType}} value) { } {{/targetPortDefinition}} } - {{/DependencySourceDefinitions}} + {{/dependencySourceDefinitionsOwnedByMe}} {{#normalTokenSendDef}} if ({{updateMethodName}}()) { {{writeMethodName}}(); @@ -18,5 +18,10 @@ public {{parentTypeName}} {{parentTypeName}}.set{{Name}}({{javaType}} value) { } public {{javaType}} {{parentTypeName}}.get{{Name}}() { + {{#normalTokenReceiveDef}} + if ({{receiverName}} != null) { + {{receiverName}}.run(); + } + {{/normalTokenReceiveDef}} return get{{internalName}}(); } diff --git a/ragconnect.tests/build.gradle b/ragconnect.tests/build.gradle index c5e6f13b574d6e93ee635a265c6f0cfe9b718793..255091abca95394c7245be0d67564cd08d5a5328 100644 --- a/ragconnect.tests/build.gradle +++ b/ragconnect.tests/build.gradle @@ -244,7 +244,7 @@ task compileVia(type: RagConnectTest) { inputFiles = [file('src/test/01-input/via/Test.relast'), file('src/test/01-input/via/Test.connect')] rootNode = 'A' - protocols = ['mqtt', 'rest'] + protocols = ['mqtt', 'rest', 'java'] extraOptions = defaultRagConnectOptionsAnd() } relast { @@ -712,6 +712,28 @@ task compileJavaIncremental(type: RagConnectTest) { } } +// --- Test: rest-client-server --- +task compileRestClientServerTest(type: RagConnectTest) { + ragconnect { + outputDir = file('src/test/02-after-ragconnect/restClientServer') + inputFiles = [file('src/test/01-input/restClientServer/Test.relast'), + file('src/test/01-input/restClientServer/Test.connect')] + rootNode = 'Root' + protocols = ['rest','restClient'] + extraOptions = defaultRagConnectOptionsAnd(['--experimental-jastadd-329']) + } + relast { + useJastAddNames = true + grammarName = 'src/test/03-after-relast/restClientServer/restClientServer' + serializer = 'jackson' + } + jastadd { + jastAddList = 'JastAddList' + packageName = 'restClientServer.ast' + extraOptions = JASTADD_INCREMENTAL_OPTIONS_TRACING_FULL + } +} + // --- Task order --- classes.dependsOn(':ragconnect.base:jar') diff --git a/ragconnect.tests/src/test/01-input/passing/README.md b/ragconnect.tests/src/test/01-input/passing/README.md new file mode 100644 index 0000000000000000000000000000000000000000..15afc42285d80e7e205777af6333162100cb36dd --- /dev/null +++ b/ragconnect.tests/src/test/01-input/passing/README.md @@ -0,0 +1,4 @@ +# Passing + +This directory contains use case for RagConnect, that just need to pass compiling (no dedicated Java test). + diff --git a/ragconnect.tests/src/test/01-input/passing/inheritance/Test.connect b/ragconnect.tests/src/test/01-input/passing/inheritance/Test.connect new file mode 100644 index 0000000000000000000000000000000000000000..3b4690cd05838159de6e5c9be0e47808ca25f9ab --- /dev/null +++ b/ragconnect.tests/src/test/01-input/passing/inheritance/Test.connect @@ -0,0 +1,18 @@ +// both directions for SpecialA1 +send SpecialA1.Input ; +receive SpecialA1.Input ; + +send SpecialA1.Many ; +receive SpecialA1.Many ; + +send SpecialA1.Maybe ; +receive SpecialA1.Maybe ; + +send SpecialA1.B ; +receive SpecialA1.B ; + +// only one direction for SpecialA2 +send SpecialA2.Input ; +send SpecialA2.Many ; +send SpecialA2.Maybe ; +send SpecialA2.B ; diff --git a/ragconnect.tests/src/test/01-input/passing/inheritance/Test.relast b/ragconnect.tests/src/test/01-input/passing/inheritance/Test.relast new file mode 100644 index 0000000000000000000000000000000000000000..7de55c8642cbb307c615e7abe78833f7e7ff1a6c --- /dev/null +++ b/ragconnect.tests/src/test/01-input/passing/inheritance/Test.relast @@ -0,0 +1,5 @@ +Root ::= A ; +A ::= <Input:String> Many:B* [Maybe:B] B ; +SpecialA1 : A ; +SpecialA2 : A ; +B ::= ; diff --git a/ragconnect.tests/src/test/01-input/restClientServer/Test.connect b/ragconnect.tests/src/test/01-input/restClientServer/Test.connect new file mode 100644 index 0000000000000000000000000000000000000000..cbee90ac4a7e4bcc2209035a1e77b75abf16d2d2 --- /dev/null +++ b/ragconnect.tests/src/test/01-input/restClientServer/Test.connect @@ -0,0 +1,5 @@ +send SenderRoot.SimpleValue ; +receive ReceiverRoot.ReceivedValue ; + +send SenderRoot.SingleA ; +receive ReceiverRoot.SomeA ; diff --git a/ragconnect.tests/src/test/01-input/restClientServer/Test.relast b/ragconnect.tests/src/test/01-input/restClientServer/Test.relast new file mode 100644 index 0000000000000000000000000000000000000000..5c7bcdeb79b3df51ec142093e7b920b0e04e8ae4 --- /dev/null +++ b/ragconnect.tests/src/test/01-input/restClientServer/Test.relast @@ -0,0 +1,5 @@ +Root ::= SenderRoot ReceiverRoot; +SenderRoot ::= <SimpleValue:int> SingleA:A ; +ReceiverRoot ::= <ReceivedValue:int> SomeA:A ; +A ::= <Value> Inner ; +Inner ::= <InnerValue> ; diff --git a/ragconnect.tests/src/test/java/org/jastadd/ragconnect/tests/AbstractCompilerTest.java b/ragconnect.tests/src/test/java/org/jastadd/ragconnect/tests/AbstractCompilerTest.java new file mode 100644 index 0000000000000000000000000000000000000000..002f9815f9eb09b8b41d533c9f01d6dcc5f0e98f --- /dev/null +++ b/ragconnect.tests/src/test/java/org/jastadd/ragconnect/tests/AbstractCompilerTest.java @@ -0,0 +1,61 @@ +package org.jastadd.ragconnect.tests; + +import org.jastadd.ragconnect.tests.utils.TestUtils; +import org.junit.jupiter.api.BeforeEach; + +import java.io.File; +import java.io.IOException; +import java.nio.charset.Charset; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.jastadd.ragconnect.tests.utils.TestUtils.readFile; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * TODO: Add description. + * + * @author rschoene - Initial contribution + */ +public abstract class AbstractCompilerTest extends RagConnectTest { + + protected abstract String getDirectoryName(); + + protected String getOutputDirectory() { + return TestUtils.OUTPUT_DIRECTORY_PREFIX + getDirectoryName(); + } + + protected abstract String getDefaultGrammarName(); + + @BeforeEach + public void ensureOutputDirectory() { + File outputDirectory = new File(getOutputDirectory()); + assertTrue((outputDirectory.exists() && outputDirectory.isDirectory()) || outputDirectory.mkdir()); + } + + protected Path test(String inputDirectoryName, int expectedReturnValue, String rootNode, String... connectNames) throws IOException { + String grammarFile = Paths.get(inputDirectoryName, getDefaultGrammarName() + ".relast").toString(); + List<String> connectFiles = Arrays.stream(connectNames) + .map(connectName -> Paths.get(inputDirectoryName,connectName + ".connect").toString()) + .collect(Collectors.toList()); + return TestUtils.runCompiler(grammarFile, connectFiles, rootNode, + getDirectoryName(), expectedReturnValue); + } + + protected void testAndCompare(String inputDirectoryName, String expectedName, String rootNode, String... connectNames) throws IOException { + Path outPath = test(inputDirectoryName, 1, rootNode, connectNames); + + final String startOfErrorsPattern = "Errors:\n"; + String out = readFile(outPath, Charset.defaultCharset()); + assertThat(out).contains(startOfErrorsPattern); + out = out.substring(out.indexOf(startOfErrorsPattern) + startOfErrorsPattern.length()); + + TestUtils.assertLinesMatch(getDirectoryName(), expectedName, out); + + logger.info("ragconnect for " + expectedName + " returned:\n{}", out); + } +} diff --git a/ragconnect.tests/src/test/java/org/jastadd/ragconnect/tests/AttributeTest.java b/ragconnect.tests/src/test/java/org/jastadd/ragconnect/tests/AttributeTest.java index 7073e6d4f05d35ab7d42d84b0d1d31072b92efc2..ee1b7e55a72e571b21c2d3a427455a53775efabd 100644 --- a/ragconnect.tests/src/test/java/org/jastadd/ragconnect/tests/AttributeTest.java +++ b/ragconnect.tests/src/test/java/org/jastadd/ragconnect/tests/AttributeTest.java @@ -26,7 +26,6 @@ import static org.junit.jupiter.api.Assertions.*; * @author rschoene - Initial contribution */ @Tag("Incremental") -@Tag("New") public class AttributeTest extends AbstractMqttTest { private static final String TOPIC_WILDCARD = "attr/#"; diff --git a/ragconnect.tests/src/test/java/org/jastadd/ragconnect/tests/ErrorsTest.java b/ragconnect.tests/src/test/java/org/jastadd/ragconnect/tests/ErrorsTest.java index 4853b6a7e62f6e71878b37700a72fe2515655200..7f7e0cb2fbbaba2f6ce2995d9866ba10142e2039 100644 --- a/ragconnect.tests/src/test/java/org/jastadd/ragconnect/tests/ErrorsTest.java +++ b/ragconnect.tests/src/test/java/org/jastadd/ragconnect/tests/ErrorsTest.java @@ -1,65 +1,33 @@ package org.jastadd.ragconnect.tests; -import org.jastadd.ragconnect.tests.utils.TestUtils; -import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; -import java.io.File; import java.io.IOException; -import java.nio.charset.Charset; -import java.nio.file.Path; -import java.util.Arrays; -import java.util.List; -import java.util.stream.Collectors; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.jastadd.ragconnect.tests.utils.TestUtils.readFile; -import static org.junit.jupiter.api.Assertions.assertTrue; /** * Test error messages. * * @author rschoene - Initial contribution */ -public class ErrorsTest extends RagConnectTest { - - private static final String ERROR_DIRECTORY = "errors/"; - private static final String OUTPUT_DIRECTORY = TestUtils.OUTPUT_DIRECTORY_PREFIX + ERROR_DIRECTORY; +public class ErrorsTest extends AbstractCompilerTest { - private static final String DEFAULT_GRAMMAR_NAME = "Errors"; + @Override + protected String getDirectoryName() { + return "errors"; + } - @BeforeAll - public static void createOutputDirectory() { - File outputDirectory = new File(OUTPUT_DIRECTORY); - assertTrue((outputDirectory.exists() && outputDirectory.isDirectory()) || outputDirectory.mkdir()); + @Override + protected String getDefaultGrammarName() { + return "Errors"; } @Test void testStandardErrors() throws IOException { - test("Standard", "A", "Standard"); + testAndCompare(getDirectoryName(), "Standard", "A", "Standard"); } @Test void testTwoPartsErrors() throws IOException { - test("Part", "A", "Part1", "Part2"); + testAndCompare(getDirectoryName(), "Part", "A", "Part1", "Part2"); } - - @SuppressWarnings("SameParameterValue") - private void test(String expectedName, String rootNode, String... connectNames) throws IOException { - String grammarFile = ERROR_DIRECTORY + DEFAULT_GRAMMAR_NAME + ".relast"; - List<String> connectFiles = Arrays.stream(connectNames) - .map(connectName -> ERROR_DIRECTORY + connectName + ".connect") - .collect(Collectors.toList()); - Path outPath = TestUtils.runCompiler(grammarFile, connectFiles, rootNode, ERROR_DIRECTORY, 1); - - final String startOfErrorsPattern = "Errors:\n"; - String out = readFile(outPath, Charset.defaultCharset()); - assertThat(out).contains(startOfErrorsPattern); - out = out.substring(out.indexOf(startOfErrorsPattern) + startOfErrorsPattern.length()); - - TestUtils.assertLinesMatch(ERROR_DIRECTORY, expectedName, out); - - logger.info("ragconnect for " + expectedName + " returned:\n{}", out); - } - } diff --git a/ragconnect.tests/src/test/java/org/jastadd/ragconnect/tests/JavaTest.java b/ragconnect.tests/src/test/java/org/jastadd/ragconnect/tests/JavaTest.java index 81d2817c5a1453bb82728edb496d1b41042d31ba..dfac385b2161cf4fff96aad5d05985438a03f242 100644 --- a/ragconnect.tests/src/test/java/org/jastadd/ragconnect/tests/JavaTest.java +++ b/ragconnect.tests/src/test/java/org/jastadd/ragconnect/tests/JavaTest.java @@ -150,7 +150,10 @@ public class JavaTest extends RagConnectTest { model.ragconnectJavaPush(TOPIC_RECEIVE_NTA, ExposingASTNode.INSTANCE.aToBytes(createA("12"))); checker.put(TOPIC_RECEIVE_NTA, "12").check(); + } + @AfterEach + public void printEvaluationSummary() { System.out.println(model.ragconnectEvaluationCounterSummary()); } diff --git a/ragconnect.tests/src/test/java/org/jastadd/ragconnect/tests/PassingTest.java b/ragconnect.tests/src/test/java/org/jastadd/ragconnect/tests/PassingTest.java new file mode 100644 index 0000000000000000000000000000000000000000..5829c21d41f8ee0601a5218c33bb3d7165ae457b --- /dev/null +++ b/ragconnect.tests/src/test/java/org/jastadd/ragconnect/tests/PassingTest.java @@ -0,0 +1,33 @@ +package org.jastadd.ragconnect.tests; + +import org.junit.jupiter.api.Test; + +import java.io.IOException; +import java.nio.file.Paths; + +/** + * Use cases that just need to compile. + * + * @author rschoene - Initial contribution + */ +public class PassingTest extends AbstractCompilerTest { + @Override + protected String getDirectoryName() { + return "passing"; + } + + @Override + protected String getDefaultGrammarName() { + return "Test"; + } + + protected void run(String inputDirectoryName, String rootNode) throws IOException { + super.test(Paths.get("passing", inputDirectoryName).toString(), + 0, rootNode, getDefaultGrammarName()); + } + + @Test + public void testInheritance() throws IOException { + run("inheritance", "Root"); + } +} diff --git a/ragconnect.tests/src/test/java/org/jastadd/ragconnect/tests/RestClientServerTest.java b/ragconnect.tests/src/test/java/org/jastadd/ragconnect/tests/RestClientServerTest.java new file mode 100644 index 0000000000000000000000000000000000000000..bcfaea91118d671f8a65275e1abcf364862036a0 --- /dev/null +++ b/ragconnect.tests/src/test/java/org/jastadd/ragconnect/tests/RestClientServerTest.java @@ -0,0 +1,124 @@ +package org.jastadd.ragconnect.tests; + +import org.jastadd.ragconnect.tests.utils.TestChecker; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; +import restClientServer.ast.*; + +import java.io.IOException; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Testing RestClient. + * + * @author rschoene - Initial contribution + */ +public class RestClientServerTest extends RagConnectTest { + private Root root; + private TestChecker checker; + + private static final String TOPIC_NT_A_VALUE = "nt/a/value"; + private static final String TOPIC_NT_A_INNER_VALUE = "nt/a/inner/value"; + + @Test + public void testSimpleSenderRest() throws IOException { + root.getSenderRoot().setSimpleValue(1); + + assertThat(root.getReceiverRoot().connectReceivedValue("restClient://localhost:4567/serve-simple")).isTrue(); + assertThat(root.getSenderRoot().connectSimpleValue("rest://localhost:4567/serve-simple", true)).isTrue(); + + assertThat(root.getReceiverRoot().getReceivedValue()).isEqualTo(1); + + root.getSenderRoot().setSimpleValue(2); + assertThat(root.getReceiverRoot().getReceivedValue()).isEqualTo(2); + } + + @Test + public void testSimpleSenderRestClient() throws IOException { + root.getSenderRoot().setSimpleValue(1); + + assertThat(root.getReceiverRoot().connectReceivedValue("rest://localhost:4567/put-simple")).isTrue(); + assertThat(root.getSenderRoot().connectSimpleValue("restClient://localhost:4567/put-simple", true)).isTrue(); + + assertThat(root.getReceiverRoot().getReceivedValue()).isEqualTo(1); + + root.getSenderRoot().setSimpleValue(2); + assertThat(root.getReceiverRoot().getReceivedValue()).isEqualTo(2); + } + + @Test + public void testNonterminalSenderRest() throws IOException { + A a = new A().setValue("a1"); + a.setInner(new Inner().setInnerValue("1")); + root.getSenderRoot().setSingleA(a); + + checker.put(TOPIC_NT_A_VALUE, "a1") + .put(TOPIC_NT_A_INNER_VALUE, "1"); + + assertThat(root.getReceiverRoot().connectSomeA("rest://localhost:4567/put-nt")).isTrue(); + assertThat(root.getSenderRoot().connectSingleA("restClient://localhost:4567/put-nt", true)).isTrue(); + + communicateNonterminal(); + } + + @Test + @Disabled("Receiving nonterminals using restClient is not supported yet") + public void testNonterminalSenderRestClient() throws IOException { + A a = new A().setValue("a1"); + a.setInner(new Inner().setInnerValue("1")); + root.getSenderRoot().setSingleA(a); + + assertThat(root.getReceiverRoot().connectSomeA("restClient://localhost:4567/serve-nt")).isTrue(); + assertThat(root.getSenderRoot().connectSingleA("rest://localhost:4567/serve-nt", true)).isTrue(); + + communicateNonterminal(); + } + + private void communicateNonterminal() { + A a = root.getSenderRoot().getSingleA(); + + checker.check(); + + a.setValue("a23"); + checker.put(TOPIC_NT_A_VALUE, "a23").check(); + + a.getInner().setInnerValue("abc"); + checker.put(TOPIC_NT_A_INNER_VALUE, "abc").check(); + } + + @BeforeEach + public void createModel() { + root = new Root(); + root.setReceiverRoot(new ReceiverRoot()); + root.setSenderRoot(new SenderRoot()); + + checker = new TestChecker().setActualNumberOfValues(() -> 0); + checker.setCheckForString(TOPIC_NT_A_VALUE, (name, expected) -> + assertThat(someAValue()).describedAs(name).isEqualTo(expected)) + .setCheckForString(TOPIC_NT_A_INNER_VALUE, (name, expected) -> + assertThat(someAInnerValueOrNull()).describedAs(name).isEqualTo(expected)) + ; + } + + private String someAValue() { + if (root.getReceiverRoot().getSomeA() == null) { + return null; + } + return root.getReceiverRoot().getSomeA().getValue(); + } + + private String someAInnerValueOrNull() { + if (root.getReceiverRoot().getSomeA() == null || root.getReceiverRoot().getSomeA().getInner() == null) { + return null; + } + return root.getReceiverRoot().getSomeA().getInner().getInnerValue(); + } + + @AfterEach + public void close() { + root.ragconnectCloseConnections(); + } +} diff --git a/ragconnect.tests/src/test/java/org/jastadd/ragconnect/tests/tree/AbstractTreeTest.java b/ragconnect.tests/src/test/java/org/jastadd/ragconnect/tests/tree/AbstractTreeTest.java index 9b2e18f5a5fd075843b4022386408c944506f0be..c8f87c184a555fc65006e30a023992812ee94295 100644 --- a/ragconnect.tests/src/test/java/org/jastadd/ragconnect/tests/tree/AbstractTreeTest.java +++ b/ragconnect.tests/src/test/java/org/jastadd/ragconnect/tests/tree/AbstractTreeTest.java @@ -16,7 +16,6 @@ import static org.junit.jupiter.api.Assertions.*; * @author rschoene - Initial contribution */ @Tag("Tree") -@Tag("New") public abstract class AbstractTreeTest extends AbstractMqttTest { protected static final String TOPIC_ALFA = "alfa"; protected ReceiverData data;