├── .config └── nextest.toml ├── .devcontainer ├── Dockerfile ├── devcontainer.json └── post-create.sh ├── .env.oft-current ├── .env.oft-latest ├── .github ├── release.yml └── workflows │ ├── README.md │ ├── check-dependencies.yaml │ ├── check.yaml │ ├── latest-up-spec-compatibility.yaml │ ├── nightly.yaml │ └── release.yaml ├── .gitignore ├── .gitmodules ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── Cargo.lock ├── Cargo.toml ├── LICENSE ├── NOTICE.md ├── README.md ├── about.hbs ├── about.toml ├── build.rs ├── deny.toml ├── examples ├── simple_notify.rs ├── simple_publish.rs └── simple_rpc.rs ├── proto └── io │ └── cloudevents │ └── v1 │ └── cloudevents.proto ├── src ├── cloudevents.rs ├── communication.rs ├── communication │ ├── default_notifier.rs │ ├── default_pubsub.rs │ ├── in_memory_rpc_client.rs │ ├── in_memory_rpc_server.rs │ ├── notification.rs │ ├── pubsub.rs │ ├── rpc.rs │ ├── udiscovery_client.rs │ └── usubscription_client.rs ├── core.rs ├── core │ ├── udiscovery.rs │ ├── usubscription.rs │ └── utwin.rs ├── lib.rs ├── local_transport.rs ├── uattributes.rs ├── uattributes │ ├── uattributesvalidator.rs │ ├── upayloadformat.rs │ └── upriority.rs ├── umessage.rs ├── umessage │ ├── README.md │ ├── umessagebuilder.rs │ └── umessagetype.rs ├── uri.rs ├── ustatus.rs ├── utransport.rs └── uuid.rs └── tools ├── coverage.sh ├── fmt_clippy_doc.sh └── generate_test_coverage_report.sh /.config/nextest.toml: -------------------------------------------------------------------------------- 1 | [profile.ci] 2 | fail-fast = false 3 | leak-timeout = "200ms" 4 | 5 | [profile.ci.junit] 6 | path = "nextest-results.xml" 7 | -------------------------------------------------------------------------------- /.devcontainer/Dockerfile: -------------------------------------------------------------------------------- 1 | ################################################################################ 2 | # Copyright (c) 2023 Contributors to the Eclipse Foundation 3 | # 4 | # See the NOTICE file(s) distributed with this work for additional 5 | # information regarding copyright ownership. 6 | # 7 | # This program and the accompanying materials are made available under the 8 | # terms of the Apache License Version 2.0 which is available at 9 | # https://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # SPDX-License-Identifier: Apache-2.0 12 | ################################################################################ 13 | 14 | FROM mcr.microsoft.com/devcontainers/rust:latest 15 | ARG USERNAME=vscode 16 | ARG TARGETARCH 17 | 18 | RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \ 19 | && apt-get -y install \ 20 | curl \ 21 | gcc \ 22 | git \ 23 | openjdk-17-jre \ 24 | zip \ 25 | && \ 26 | rm -rf /var/lib/apt/lists/* 27 | 28 | # Download openfasttrace JARs and create shortcut oft executable 29 | ARG OFT_CORE_VERSION=4.1.0 30 | ARG OFT_ASCIIDOC_PLUGIN_VERSION=0.3.0 31 | ENV LIB_DIR=/opt/oft/lib 32 | RUN < /usr/local/bin/oft \ 39 | && chmod +x /usr/local/bin/oft 40 | 41 | # Before installing cargo tools, change to the user that will be used in the container later. 42 | # The reason is that cargo creates some cache, etc. folders with the correct group rustlang, but 43 | # without making them group writable. Easiest fix is to change to the correct user before the install, 44 | # so that the owner is correct from the start. 45 | USER ${USERNAME} 46 | 47 | # Install cargo cli tools 48 | RUN cargo install cargo-nextest cargo-deny cargo-tarpaulin --locked 49 | 50 | # Install cargo tools for cross compilation 51 | RUN rustup target add aarch64-unknown-linux-gnu \ 52 | && rustup toolchain install stable-aarch64-unknown-linux-gnu 53 | -------------------------------------------------------------------------------- /.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | // For format details, see https://aka.ms/devcontainer.json. For config options, see the 2 | // README at: https://github.com/devcontainers/templates/tree/main/src/rust 3 | { 4 | "name": "uProtocol-Rust language library", 5 | "build": { 6 | "dockerfile": "Dockerfile" 7 | }, 8 | "runArgs": [ 9 | "--privileged" 10 | ], 11 | // This is a workaround which is again necessary on MacOS 14.0, it looks like this bug is back: 12 | // https://github.com/microsoft/vscode-dev-containers/issues/1487#issuecomment-1143907307 13 | "mounts": [ 14 | "source=rust-volume,target=/rust-volume,type=volume" 15 | ], 16 | "postCreateCommand": "sudo .devcontainer/post-create.sh", 17 | "customizations": { 18 | "vscode": { 19 | // Set *default* container specific settings.json values on container create. 20 | "settings": { 21 | "lldb.executable": "/usr/bin/lldb", 22 | // VS Code don't watch files under ./target 23 | "files.watcherExclude": { 24 | "**/target/**": true 25 | }, 26 | "rust-analyzer.check.command": "clippy", 27 | "rust-analyzer.checkOnSave": true, 28 | "coverage-gutters.coverageBaseDir": "**", 29 | "coverage-gutters.coverageFileNames": [ 30 | "target/tarpaulin/lcov.info" 31 | ] 32 | }, 33 | // Add the IDs of extensions you want installed when the container is created. 34 | "extensions": [ 35 | "asciidoctor.asciidoctor-vscode", 36 | "bianxianyang.htmlplay", 37 | "bierner.markdown-mermaid", 38 | "bierner.markdown-preview-github-styles", 39 | "davidanson.vscode-markdownlint", 40 | "gxl.git-graph-3", 41 | "hediet.vscode-drawio", 42 | "linusu.auto-dark-mode", 43 | "mhutchie.git-graph", 44 | "ms-azuretools.vscode-docker", 45 | "rust-lang.rust-analyzer", 46 | "streetsidesoftware.code-spell-checker", 47 | "tamasfe.even-better-toml", 48 | "timonwong.shellcheck", 49 | "vadimcn.vscode-lldb", 50 | "yzhang.markdown-all-in-one" 51 | ] 52 | } 53 | }, 54 | "workspaceMount": "source=${localWorkspaceFolder},target=/workspace/up-rust/,type=bind", 55 | "workspaceFolder": "/workspace/up-rust/", 56 | "remoteUser": "vscode" 57 | } -------------------------------------------------------------------------------- /.devcontainer/post-create.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # This is a workaround which is againt necessary on MacOS 14.0, it looks like this bug is back: 4 | # https://github.com/microsoft/vscode-dev-containers/issues/1487#issuecomment-1143907307 5 | 6 | # grant permissions to mounted rust volume 7 | chown vscode:vscode /rust-volume 8 | 9 | # create /.cargo/config.toml in root folder 10 | mkdir /.cargo/ 11 | touch /.cargo/config.toml 12 | cat << EOF > /.cargo/config.toml 13 | [build] 14 | target-dir = "/rust-volume/target" 15 | EOF 16 | -------------------------------------------------------------------------------- /.env.oft-current: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2025 Contributors to the Eclipse Foundation 2 | # 3 | # See the NOTICE file(s) distributed with this work for additional 4 | # information regarding copyright ownership. 5 | # 6 | # This program and the accompanying materials are made available under the 7 | # terms of the Apache License Version 2.0 which is available at 8 | # https://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # SPDX-License-Identifier: Apache-2.0 11 | 12 | # shellcheck disable=SC2148,SC2034 13 | 14 | # The file patterns that specify the relevant parts of the uProtocol Specification 15 | # that this component is supposed to implement 16 | UP_SPEC_FILE_PATTERNS="up-spec/*.adoc up-spec/*.md up-spec/basics up-spec/up-l1/cloudevents.adoc up-spec/up-l2/api.adoc" 17 | 18 | # The file patterns that specify this component's resources which contain specification items 19 | # that cover the requirements 20 | COMPONENT_FILE_PATTERNS="*.adoc *.md *.rs .github examples src tests tools" 21 | 22 | OFT_FILE_PATTERNS="$UP_SPEC_FILE_PATTERNS $COMPONENT_FILE_PATTERNS" 23 | OFT_TAGS="" 24 | -------------------------------------------------------------------------------- /.env.oft-latest: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2025 Contributors to the Eclipse Foundation 2 | # 3 | # See the NOTICE file(s) distributed with this work for additional 4 | # information regarding copyright ownership. 5 | # 6 | # This program and the accompanying materials are made available under the 7 | # terms of the Apache License Version 2.0 which is available at 8 | # https://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # SPDX-License-Identifier: Apache-2.0 11 | 12 | # shellcheck disable=SC2148,SC2034 13 | 14 | # The file patterns that specify the relevant parts of the latest uProtocol Specification 15 | # that this component is supposed to implement 16 | UP_SPEC_FILE_PATTERNS="up-spec/*.adoc up-spec/*.md up-spec/basics up-spec/up-l1/README.adoc up-spec/up-l1/cloudevents.adoc up-spec/up-l2/api.adoc" 17 | 18 | # The file patterns that specify this component's resources which contain specification items 19 | # that cover the requirements 20 | COMPONENT_FILE_PATTERNS="*.adoc *.md *.rs .github examples src tests tools" 21 | 22 | OFT_FILE_PATTERNS="$UP_SPEC_FILE_PATTERNS $COMPONENT_FILE_PATTERNS" 23 | OFT_TAGS="_,LanguageLibrary" 24 | -------------------------------------------------------------------------------- /.github/release.yml: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2024 Contributors to the Eclipse Foundation 2 | # 3 | # See the NOTICE file(s) distributed with this work for additional 4 | # information regarding copyright ownership. 5 | # 6 | # This program and the accompanying materials are made available under the 7 | # terms of the Apache License Version 2.0 which is available at 8 | # https://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # SPDX-License-Identifier: Apache-2.0 11 | 12 | # This is the configuration used by GitHub for automatically creating release notes 13 | # from pull requests based on their labels 14 | # see https://docs.github.com/en/repositories/releasing-projects-on-github/automatically-generated-release-notes 15 | 16 | changelog: 17 | exclude: 18 | labels: 19 | - duplicate 20 | - wontfix 21 | - invalid 22 | authors: 23 | - octocat 24 | categories: 25 | - title: "🛠️ Breaking Changes" 26 | labels: 27 | - "breaking change" 28 | - title: "✨ Features" 29 | labels: 30 | - enhancement 31 | - title: "🐛 Bug Fixes" 32 | labels: 33 | - bug 34 | - title: "📚 Documentation" 35 | labels: 36 | - documentation 37 | - title: "Other Changes" 38 | labels: 39 | - "*" 40 | -------------------------------------------------------------------------------- /.github/workflows/README.md: -------------------------------------------------------------------------------- 1 | # Which automations do we run? 2 | 3 | This file is meant to provide an overview and explainer on what the up-rust workflow automation strategy is, and what the different workflow elements do. 4 | 5 | __A general note:__ All workflows will use the `stable` version of the Rust toolchain, unless the GitHub actions variable `RUST_TOOLCHAIN` is set to pin a specific Rust version (e.g. ```RUST_TOOLCHAIN=1.76.0```). 6 | 7 | At this time, there are three events that will initiate a workflow run: 8 | 9 | ## PRs and merges to main 10 | 11 | We want a comprehensive but also quick check&test workflow. This should be testing all relevant/obvious feature sets, run on all major OSes, and of course include all the Rust goodness around cargo check, fmt, clippy and so on. 12 | 13 | This is implemented in [`check.yaml`](check.yaml) 14 | 15 | ## Release publication 16 | 17 | We want exhaustive tests and all possible checks, as well as creation of license reports, collection of quality artifacts and publication to crates.io. This workflow pulls in other pieces like the build workflow. An actual release is triggered by pushing a tag that begins with 'v', else this workflow just generates and collects artifacts on workflow level. This will also publish to crates.io if the CRATES_TOKEN secret is set. 18 | 19 | This is implemented in [`release.yaml`](release.yaml) 20 | 21 | ## Nightly, out of everyone's way 22 | 23 | All the tests we can think of, however long they might take. For instance, we can build up-rust for different architectures - this might not really create many insights, but doesn't hurt to try either, and fits nicely into a nightly build schedule. 24 | 25 | This is implemented in [`nightly.yaml`](nightly.yaml) 26 | 27 | ## Further workflow modules 28 | 29 | In addition to the main workflows described above, there exist a number of modules that are used by these main workflows. They can also be run standalone, and are intendet to make composing the capabilities of our main workflows simpler. These are: 30 | 31 | - [`check-up-spec-compatibility.yaml`](check-up-spec-compatibility.yaml) - checks if the current main branch can be built against up-spec's main branch instead of its latest tag/release 32 | - [`coverage.yaml`](coverage.yaml) - collects test code coverage, and can optionally upload the results to codecov.io 33 | - Will publish coverage data to CodeCov if `${{ secrets.CODECOV_TOKEN }}` is set 34 | - outputs: download URL for the workflow-generated coverage info file 35 | - [`license-report.yaml`](license-report.yaml) - create a license report for `up-rust` and all its dependencies in html format 36 | - outputs: download URL for the workflow-generated license report 37 | - [`requirements-tracing.yaml`](requirements-tracing.yaml) - Run OpenFastTrace to verify that all requirements from uProtocol Specification are met 38 | - [`test-featurematrix.yaml`](test-featurematrix.yaml) - Test all feature combinations on a range of OS platforms 39 | - [`verify-msrv.yaml`](verify-msrv.yaml) - checks if the MSRV ('Minimum Supported Rust Version) declared in Cargo.toml is correct 40 | - [`x-build.yaml`](x-build.yaml) - Run release builds on multiple architecture targets 41 | -------------------------------------------------------------------------------- /.github/workflows/check-dependencies.yaml: -------------------------------------------------------------------------------- 1 | # ******************************************************************************** 2 | # Copyright (c) 2023 Contributors to the Eclipse Foundation 3 | # 4 | # See the NOTICE file(s) distributed with this work for additional 5 | # information regarding copyright ownership. 6 | # 7 | # This program and the accompanying materials are made available under the 8 | # terms of the Apache License Version 2.0 which is available at 9 | # https://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # SPDX-License-Identifier: Apache-2.0 12 | # *******************************************************************************/ 13 | 14 | name: Check 3rd party dependencies 15 | 16 | on: 17 | push: 18 | branches: [ main ] 19 | pull_request: 20 | paths: 21 | - "Cargo.*" 22 | workflow_call: 23 | workflow_dispatch: 24 | 25 | concurrency: 26 | group: ${{ github.ref }}-${{ github.workflow }} 27 | cancel-in-progress: true 28 | 29 | jobs: 30 | deps: 31 | name: "Check 3rd party licenses" 32 | runs-on: ubuntu-latest 33 | steps: 34 | - uses: actions/checkout@v4 35 | - name: Determine 3rd party dependencies 36 | working-directory: ${{github.workspace}} 37 | run: | 38 | cargo tree --all-features --locked -e no-build,no-dev --prefix none --no-dedupe \ 39 | | sort -u \ 40 | | grep -v '^[[:space:]]*$' \ 41 | | grep -v up-rust \ 42 | | sed -E 's|([^ ]+) v([^ ]+).*|crate/cratesio/-/\1/\2|' \ 43 | > DEPS.txt 44 | - name: Run Eclipse Dash Licenses tool 45 | uses: eclipse-uprotocol/ci-cd/.github/actions/run-eclipse-dash@main 46 | with: 47 | components-file: DEPS.txt 48 | -------------------------------------------------------------------------------- /.github/workflows/check.yaml: -------------------------------------------------------------------------------- 1 | # ******************************************************************************** 2 | # Copyright (c) 2024 Contributors to the Eclipse Foundation 3 | # 4 | # See the NOTICE file(s) distributed with this work for additional 5 | # information regarding copyright ownership. 6 | # 7 | # This program and the accompanying materials are made available under the 8 | # terms of the Apache License Version 2.0 which is available at 9 | # https://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # SPDX-License-Identifier: Apache-2.0 12 | # *******************************************************************************/ 13 | 14 | # Comprehensive combination of checks, linting, feature-checks, testing to be run on merge and on PR 15 | # Upload test results for potential re-use in publication workflow, returns the corresponding download URL as an output on workflow_call 16 | 17 | name: Cargo 18 | 19 | on: 20 | push: 21 | branches: 22 | - main 23 | pull_request: 24 | paths: 25 | - "src/**" 26 | - "Cargo.*" 27 | - "build.rs" 28 | - "deny.toml" 29 | workflow_call: 30 | outputs: 31 | test_results_url: 32 | description: "URL of the test results artifact" 33 | value: ${{ jobs.nextest.outputs.test_results_url }} 34 | workflow_dispatch: 35 | 36 | concurrency: 37 | group: ${{ github.ref }}-${{ github.workflow }} 38 | cancel-in-progress: true 39 | 40 | env: 41 | RUST_TOOLCHAIN: ${{ vars.RUST_TOOLCHAIN || 'stable' }} 42 | RUSTFLAGS: -Dwarnings 43 | CARGO_TERM_COLOR: always 44 | 45 | jobs: 46 | deny: 47 | runs-on: ubuntu-latest 48 | steps: 49 | - uses: actions/checkout@v4 50 | with: 51 | submodules: "recursive" 52 | - uses: EmbarkStudios/cargo-deny-action@v2 53 | with: 54 | rust-version: ${{ env.RUST_TOOLCHAIN }} 55 | command: check 56 | arguments: --all-features 57 | 58 | # [impl->req~up-language-ci-build~1] 59 | check: 60 | runs-on: ubuntu-latest 61 | steps: 62 | - uses: actions/checkout@v4 63 | with: 64 | submodules: "recursive" 65 | - uses: dtolnay/rust-toolchain@master 66 | with: 67 | toolchain: ${{ env.RUST_TOOLCHAIN }} 68 | - name: Run cargo check 69 | run: | 70 | cargo check --workspace --all-targets --all-features 71 | 72 | # [impl->req~up-language-ci-linter~1] 73 | fmt: 74 | runs-on: ubuntu-latest 75 | steps: 76 | - uses: actions/checkout@v4 77 | with: 78 | submodules: "recursive" 79 | - uses: dtolnay/rust-toolchain@master 80 | with: 81 | toolchain: ${{ env.RUST_TOOLCHAIN }} 82 | components: rustfmt 83 | - name: Run cargo fmt 84 | run: | 85 | cargo fmt --all --check 86 | 87 | # [impl->req~up-language-ci-linter~1] 88 | clippy: 89 | runs-on: ubuntu-latest 90 | steps: 91 | - uses: actions/checkout@v4 92 | with: 93 | submodules: "recursive" 94 | - uses: dtolnay/rust-toolchain@master 95 | with: 96 | toolchain: ${{ env.RUST_TOOLCHAIN }} 97 | components: clippy 98 | - name: Run cargo clippy 99 | run: | 100 | cargo clippy --version 101 | cargo clippy --all-targets --all-features --no-deps -- -W warnings -D warnings 102 | 103 | # [impl->req~up-language-ci-api-docs~1] 104 | docu: 105 | runs-on: ubuntu-latest 106 | env: 107 | RUSTDOCFLAGS: -Dwarnings 108 | steps: 109 | - uses: actions/checkout@v4 110 | with: 111 | submodules: "recursive" 112 | - uses: dtolnay/rust-toolchain@master 113 | with: 114 | toolchain: ${{ env.RUST_TOOLCHAIN }} 115 | - name: Run rustdoc 116 | run: | 117 | cargo doc --no-deps --all-features 118 | 119 | feature-check: 120 | # Comprehensive check on dependencies for all feature flag combinations, excluding development dependencies 121 | needs: check 122 | runs-on: ubuntu-latest 123 | steps: 124 | - uses: actions/checkout@v4 125 | with: 126 | submodules: "recursive" 127 | - uses: dtolnay/rust-toolchain@master 128 | with: 129 | toolchain: ${{ env.RUST_TOOLCHAIN }} 130 | - uses: Swatinem/rust-cache@v2 131 | - uses: taiki-e/install-action@cargo-hack 132 | - name: Run cargo hack powerset 133 | run: | 134 | cargo hack check --feature-powerset --no-dev-deps 135 | 136 | # [impl->req~up-language-ci-test~1] 137 | nextest: 138 | # Subset of feature-combos, on only one OS - more complete testing in test-featurematrix.yaml 139 | outputs: 140 | test_results_url: ${{ steps.test_results.outputs.artifact-url }} 141 | runs-on: ubuntu-latest 142 | env: 143 | NEXTEST_EXPERIMENTAL_LIBTEST_JSON: 1 144 | strategy: 145 | matrix: 146 | feature-flags: ["", "--no-default-features", "--all-features"] 147 | steps: 148 | - uses: actions/checkout@v4 149 | with: 150 | submodules: "recursive" 151 | - uses: dtolnay/rust-toolchain@master 152 | with: 153 | toolchain: ${{ env.RUST_TOOLCHAIN }} 154 | - uses: Swatinem/rust-cache@v2 155 | # install tool to convert cargo's JSON test output to JUNIT format 156 | - run: | 157 | cargo install cargo2junit 158 | # Using nextest because it's faster than built-in test 159 | - uses: taiki-e/install-action@nextest 160 | - name: Run cargo nextest 161 | run: | 162 | cargo nextest run ${{ matrix.feature-flags }} --profile ci 163 | - name: Run doctests 164 | run: | 165 | # we use tee to let cargo test print to the console 166 | RUSTC_BOOTSTRAP=1 cargo test --doc ${{ matrix.feature-flags }} -- -Z unstable-options --format json --report-time | tee target/doctest-results.json 167 | # write output to same directory as the one used by cargo nextest in order 168 | # to flatten the directory hierarchy when uploading the test results 169 | cat target/doctest-results.json | cargo2junit > target/nextest/ci/doctest-results.xml 170 | 171 | - name: Upload all-features test results artifact 172 | id: test_results 173 | if: matrix.feature-flags == '--all-features' 174 | uses: actions/upload-artifact@v4 175 | with: 176 | name: test-results 177 | # this will include the nextest and doctest result files in the archive's root folder 178 | path: target/nextest/ci/*-results.xml 179 | -------------------------------------------------------------------------------- /.github/workflows/latest-up-spec-compatibility.yaml: -------------------------------------------------------------------------------- 1 | # ******************************************************************************** 2 | # Copyright (c) 2024 Contributors to the Eclipse Foundation 3 | # 4 | # See the NOTICE file(s) distributed with this work for additional 5 | # information regarding copyright ownership. 6 | # 7 | # This program and the accompanying materials are made available under the 8 | # terms of the Apache License Version 2.0 which is available at 9 | # https://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # SPDX-License-Identifier: Apache-2.0 12 | # *******************************************************************************/ 13 | 14 | # Verifies that this crate can be built using the uProtocol Core API from up-spec's main branch. 15 | # Also performs requirements tracing using OpenFastTrace. The job fails if any of the two 16 | # activities fail. 17 | 18 | name: Latest uP Spec Compatibility 19 | 20 | on: 21 | schedule: 22 | - cron: '0 4 * * *' 23 | workflow_dispatch: 24 | 25 | concurrency: 26 | group: ${{ github.ref }}-${{ github.workflow }} 27 | cancel-in-progress: true 28 | 29 | env: 30 | RUST_TOOLCHAIN: ${{ vars.RUST_TOOLCHAIN || 'stable' }} 31 | RUSTFLAGS: -Dwarnings 32 | CARGO_TERM_COLOR: always 33 | 34 | jobs: 35 | requirements-tracing: 36 | runs-on: ubuntu-latest 37 | steps: 38 | - uses: actions/checkout@v4 39 | with: 40 | submodules: "recursive" 41 | - name: Fast-Forward to HEAD revision of uProtocol Spec main branch 42 | run: | 43 | cd "${{ github.workspace }}/up-spec" 44 | echo "Switching to up-spec/main branch ..." 45 | git checkout main 46 | echo "fast-forwarding to HEAD revision ..." 47 | git pull 48 | git status 49 | cd "${{ github.workspace }}" 50 | 51 | - name: "Determine OpenFastTrace file patterns from .env file" 52 | uses: xom9ikk/dotenv@v2.3.0 53 | with: 54 | mode: "oft-latest" 55 | load-mode: strict 56 | 57 | # run OpenFastTrace first because the action will always succeed and produce 58 | # a tracing report 59 | - name: Run OpenFastTrace 60 | id: run-oft 61 | uses: eclipse-uprotocol/ci-cd/.github/actions/run-oft@main 62 | with: 63 | file-patterns: "${{ env.OFT_FILE_PATTERNS }}" 64 | tags: "${{ env.OFT_TAGS_}}" 65 | 66 | # now try to build and run the tests which may fail if incomaptible changes 67 | # have been introduced into the uProtocol Core API 68 | - uses: dtolnay/rust-toolchain@master 69 | with: 70 | toolchain: ${{ env.RUST_TOOLCHAIN }} 71 | - uses: Swatinem/rust-cache@v2 72 | - uses: taiki-e/install-action@nextest 73 | - name: Run tests 74 | run: | 75 | # Using nextest because it's faster than built-in test 76 | cargo nextest run --all-features 77 | # but it cannot execute doc tests 78 | cargo test --doc --all-features 79 | 80 | # This step will only be run if the tests in the previous step have succeeded. 81 | # In that case, we use the exit code produced by the OFT run as the job's 82 | # overall outcome. This means that the job fails if the tests run successfully 83 | # but some of the requirements from up-spec are not covered. 84 | - name: Determine exit status 85 | env: 86 | OFT_EXIT_CODE: ${{ steps.run-oft.outputs.oft-exit-code }} 87 | run: | 88 | exit $OFT_EXIT_CODE 89 | -------------------------------------------------------------------------------- /.github/workflows/nightly.yaml: -------------------------------------------------------------------------------- 1 | # ******************************************************************************** 2 | # Copyright (c) 2024 Contributors to the Eclipse Foundation 3 | # 4 | # See the NOTICE file(s) distributed with this work for additional 5 | # information regarding copyright ownership. 6 | # 7 | # This program and the accompanying materials are made available under the 8 | # terms of the Apache License Version 2.0 which is available at 9 | # https://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # SPDX-License-Identifier: Apache-2.0 12 | # *******************************************************************************/ 13 | 14 | # Nightly-scheduled comprehensive test runs that would take too long for on-merge workflows 15 | # Note: running the coverage job would only make sense if we use that to upload coverage info to a public platform 16 | 17 | name: Nightly 18 | 19 | on: 20 | schedule: 21 | - cron: '0 3 * * *' 22 | workflow_dispatch: 23 | 24 | concurrency: 25 | group: ${{ github.ref }}-${{ github.workflow }} 26 | cancel-in-progress: true 27 | 28 | env: 29 | RUST_TOOLCHAIN: ${{ vars.RUST_TOOLCHAIN || 'stable' }} 30 | 31 | jobs: 32 | check-msrv: 33 | uses: eclipse-uprotocol/ci-cd/.github/workflows/rust-verify-msrv.yaml@main 34 | 35 | check-latest-deps: 36 | uses: eclipse-uprotocol/ci-cd/.github/workflows/rust-verify-latest-deps.yaml@main 37 | 38 | deny: 39 | uses: eclipse-uprotocol/ci-cd/.github/workflows/rust-deny-check.yaml@main 40 | with: 41 | arguments: --all-features --locked --exclude-dev 42 | 43 | # [impl->req~up-language-ci-test~1] 44 | test-all-features: 45 | uses: eclipse-uprotocol/ci-cd/.github/workflows/rust-test-featurematrix.yaml@main 46 | 47 | x-build: 48 | # The jury is still out on whether this actually adds any value, besides simply being possible... 49 | uses: eclipse-uprotocol/ci-cd/.github/workflows/rust-x-build.yaml@main 50 | 51 | coverage: 52 | uses: eclipse-uprotocol/ci-cd/.github/workflows/rust-coverage.yaml@main 53 | 54 | current-spec-compliance: 55 | uses: eclipse-uprotocol/ci-cd/.github/workflows/requirements-tracing.yaml@main 56 | with: 57 | env-file-suffix: "oft-current" 58 | -------------------------------------------------------------------------------- /.github/workflows/release.yaml: -------------------------------------------------------------------------------- 1 | # ******************************************************************************** 2 | # Copyright (c) 2024 Contributors to the Eclipse Foundation 3 | # 4 | # See the NOTICE file(s) distributed with this work for additional 5 | # information regarding copyright ownership. 6 | # 7 | # This program and the accompanying materials are made available under the 8 | # terms of the Apache License Version 2.0 which is available at 9 | # https://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # SPDX-License-Identifier: Apache-2.0 12 | # *******************************************************************************/ 13 | 14 | # Create artifacts for project releases 15 | 16 | name: Release 17 | 18 | on: 19 | push: 20 | tags: 21 | - v* 22 | 23 | concurrency: 24 | group: "release-${{ github.head_ref || github.ref }}" 25 | cancel-in-progress: true 26 | 27 | jobs: 28 | check: 29 | uses: ./.github/workflows/check.yaml 30 | 31 | check-msrv: 32 | uses: eclipse-uprotocol/ci-cd/.github/workflows/rust-verify-msrv.yaml@main 33 | 34 | coverage: 35 | uses: eclipse-uprotocol/ci-cd/.github/workflows/rust-coverage.yaml@main 36 | 37 | current-spec-compliance: 38 | uses: eclipse-uprotocol/ci-cd/.github/workflows/requirements-tracing.yaml@main 39 | with: 40 | env-file-suffix: "oft-current" 41 | 42 | licenses: 43 | # This works off the license declarations in dependent packages/crates, so if these declarations are wrong, this report will contain erroneous information 44 | uses: eclipse-uprotocol/ci-cd/.github/workflows/rust-license-report.yaml@main 45 | with: 46 | templates: "about.hbs" 47 | config: "about.toml" 48 | 49 | tag_release_artifacts: 50 | # This only runs if this workflow is initiated via a tag-push with pattern 'v*' 51 | if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v') 52 | name: collect v-tag release artifacts 53 | runs-on: ubuntu-latest 54 | needs: 55 | - check 56 | - check-msrv 57 | - coverage 58 | - current-spec-compliance 59 | - licenses 60 | permissions: write-all 61 | steps: 62 | - uses: actions/checkout@v4 63 | with: 64 | submodules: "recursive" 65 | 66 | - name: "Determine uProtocol Specification file patterns from .env file" 67 | uses: xom9ikk/dotenv@v2.3.0 68 | with: 69 | mode: oft-current 70 | load-mode: strict 71 | 72 | # License report - we later need the download_url output of the upload step 73 | - name: Download license report 74 | uses: actions/download-artifact@v4 75 | with: 76 | name: license-report 77 | path: dist/license/ 78 | - name: Upload license report to release 79 | uses: svenstaro/upload-release-action@v2 80 | id: upload_license_report 81 | with: 82 | repo_token: ${{ secrets.GITHUB_TOKEN }} 83 | file: dist/license/* 84 | file_glob: true 85 | tag: ${{ github.ref }} 86 | 87 | # Requirements Tracing report - we later need the download_url output of the upload step 88 | - name: Download requirements tracing report 89 | uses: actions/download-artifact@v4 90 | with: 91 | name: tracing-report-html 92 | path: dist/tracing/ 93 | - name: Upload requirements tracing report to release 94 | uses: svenstaro/upload-release-action@v2 95 | id: upload_requirements_tracing_report 96 | with: 97 | repo_token: ${{ secrets.GITHUB_TOKEN }} 98 | file: dist/tracing/* 99 | file_glob: true 100 | tag: ${{ github.ref }} 101 | 102 | # Test results - we later need the download_url output of the upload step 103 | - name: Download test report 104 | uses: actions/download-artifact@v4 105 | with: 106 | name: test-results 107 | path: dist/tests/ 108 | - name: Upload test report to release 109 | uses: svenstaro/upload-release-action@v2 110 | id: upload_test_report 111 | with: 112 | repo_token: ${{ secrets.GITHUB_TOKEN }} 113 | file: dist/tests/* 114 | file_glob: true 115 | tag: ${{ github.ref }} 116 | 117 | # Test coverage - we later need the download_url output of the upload step 118 | - name: Download test coverage 119 | uses: actions/download-artifact@v4 120 | with: 121 | name: code-coverage-html 122 | path: dist/codecov/ 123 | - name: Upload test coverage to release 124 | uses: svenstaro/upload-release-action@v2 125 | id: upload_test_coverage 126 | with: 127 | repo_token: ${{ secrets.GITHUB_TOKEN }} 128 | file: dist/codecov/* 129 | file_glob: true 130 | tag: ${{ github.ref }} 131 | 132 | # README - we later need the download_url output of the upload step 133 | - name: Upload README to release 134 | uses: svenstaro/upload-release-action@v2 135 | id: upload_readme 136 | with: 137 | repo_token: ${{ secrets.GITHUB_TOKEN }} 138 | file: README.md 139 | tag: ${{ github.ref }} 140 | 141 | - name: Gather uProtocol Specification documents 142 | shell: bash 143 | run: | 144 | tar cvz --file up-spec.tar.gz ${{ env.UP_SPEC_FILE_PATTERNS }} 145 | - name: Upload relevant uProtocol Spec files to release 146 | uses: svenstaro/upload-release-action@v2 147 | id: upload_up_spec 148 | with: 149 | repo_token: ${{ secrets.GITHUB_TOKEN }} 150 | file: up-spec.tar.gz 151 | tag: ${{ github.ref }} 152 | 153 | - name: Gets latest created release info 154 | id: latest_release_info 155 | uses: joutvhu/get-release@v1 156 | env: 157 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 158 | 159 | - name: Collect quality artifacts 160 | uses: eclipse-dash/quevee@v1 161 | id: quevee_manifest 162 | with: 163 | release_url: ${{ steps.latest_release_info.outputs.html_url }} 164 | artifacts_documentation: ${{ steps.upload_up_spec.outputs.browser_download_url }} 165 | artifacts_coding_guidelines: https://github.com/AnotherDaniel/up-rust/blob/3a3ddcc2ee49ca6d33bd15577f769575718c22e5/.github/workflows/check.yaml#L85 166 | artifacts_release_process: https://github.com/AnotherDaniel/up-rust/blob/3a3ddcc2ee49ca6d33bd15577f769575718c22e5/.github/workflows/release.yaml 167 | artifacts_readme: ${{ steps.upload_readme.outputs.browser_download_url }} 168 | artifacts_requirements: ${{ steps.upload_up_spec.outputs.browser_download_url }} 169 | artifacts_testing: ${{ steps.upload_test_report.outputs.browser_download_url }},${{ steps.upload_test_coverage.outputs.browser_download_url }},${{ steps.upload_requirements_tracing_report.outputs.browser_download_url }} 170 | 171 | - name: Upload manifest to release 172 | uses: svenstaro/upload-release-action@v2 173 | id: upload_quality_manifest 174 | with: 175 | repo_token: ${{ secrets.GITHUB_TOKEN }} 176 | file: ${{ steps.quevee_manifest.outputs.manifest_file }} 177 | tag: ${{ github.ref }} 178 | 179 | cargo-publish: 180 | name: publish to crates.io 181 | # This will publish to crates.io if secrets.CRATES_TOKEN is set in the workspace, otherwise do a dry-run 182 | runs-on: ubuntu-latest 183 | needs: 184 | - tag_release_artifacts 185 | env: 186 | CRATES_TOKEN: ${{ secrets.CRATES_IO_TOKEN }} 187 | steps: 188 | - uses: actions/checkout@v4 189 | with: 190 | submodules: "recursive" 191 | 192 | - if: env.CRATES_TOKEN == '' 193 | run: cargo publish --all-features --dry-run 194 | - if: env.CRATES_TOKEN != '' 195 | run: cargo publish --all-features --token ${CRATES_TOKEN} 196 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Generated by Cargo 2 | # will have compiled files and executables 3 | debug/ 4 | target/ 5 | 6 | # Remove Cargo.lock from gitignore if creating an executable, leave it for libraries 7 | # More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html 8 | #Cargo.lock 9 | 10 | # These are backup files generated by rustfmt 11 | **/*.rs.bk 12 | 13 | # MSVC Windows builds of rustc generate these, which store debugging information 14 | *.pdb 15 | 16 | .vscode/ 17 | .idea/ 18 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "up-spec"] 2 | path = up-spec 3 | url = git@github.com:eclipse-uprotocol/up-spec 4 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Code of Conduct 2 | 3 | The standard _Code of Conduct_ that all Eclipse projects must adhere to is defined in the following documents: 4 | 5 | - [Eclipse Code of Conduct](https://raw.githubusercontent.com/eclipse/.github/master/CODE_OF_CONDUCT.md) 6 | - [Eclipse Foundation Handbook](https://www.eclipse.org/projects/handbook/) 7 | - [Eclipse Committer Due Diligence Guidelines](https://www.eclipse.org/legal/committerguidelines.php) 8 | 9 | We (the committers) aim to run this project in an open and fair manner where contributions are encouraged from all companies and individuals. We aim for high quality, well documented and tested software that can be used in production environments. 10 | 11 | This document shall outline additional roles and responsibilities for committers and project leads to ensure the project is well maintained and supported to be useable in production environments. 12 | 13 | ## Committers 14 | 15 | [Eclipse-uProtocol Committers](https://www.eclipse.org/projects/handbook/#roles-cm) play a vital role to ensure contributions from others (and themselves) follow the vision and mission of the project as well. In this section we will outline how committers are nominated, retired, and their duties while in service. 16 | 17 | ### Duties 18 | 19 | * Contribute to specifications by providing feedback, code contributions in up-spec, up-core-api repos 20 | * Ensure all contributors (including themselves) adhere to this code of conduct, the Eclipse Foundation Handbook, and the vision & mission of the project 21 | * Make _significant_ code contributions to one or more repositories in the Eclipse-uProtocol project 22 | * Review and provide feedback to pull requests from other contributors 23 | * _Actively_ participate in weekly/bi-weekly project meetings 24 | 25 | ### Nomination 26 | 27 | Contributors are nominated by a uProtocol Committer when they meet the [Eclipse Committer Nomination Process](https://www.eclipse.org/projects/handbook/#elections-committer) requirements and are actively performing the duties of a committer mentioned above. 28 | 29 | ### Retirement 30 | 31 | Per [Eclipse Foundation Handbook](https://www.eclipse.org/projects/handbook/#elections-retire-cm), Committers may retire for one of the following reasons: 32 | 33 | * Their own volition 34 | * By the project lead (with supporting justification) 35 | 36 | Non-exhaustive examples for early retirement might be: 37 | * Inactivity over extended period of time 38 | * Repeated violations of this code of conduct (ex. obstructing progress during discussions/PRs without 39 | * valid justification, intentional damage of various repos/projects, etc...) 40 | 41 | All communication regarding committer nominations and retirement, *SHALL* be sent to the uprotocol-dev@eclipse.org mailing list. 42 | 43 | 44 | ## Project Lead 45 | 46 | In addition to the duties mentioned in [Eclipse Contributor Handbook](https://www.eclipse.org/projects/handbook/#roles-pl), project leads *MUST* also fulfill the Committer [Duties defined above](#duties). 47 | 48 | 49 | NOTE: Violation to this code of conduct should be reported to the [Eclipse Foundation Management Office (EMO)](https://gitlab.eclipse.org/eclipsefdn/emo-team/emo/-/issues) 50 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to Eclipse uProtocol 2 | 3 | Thanks for your interest in this project. Contributions are welcome! 4 | 5 | ## Developer resources 6 | 7 | Information regarding source code management, builds, coding standards, and 8 | more. 9 | 10 | 11 | 12 | The project maintains the following source code repositories 13 | 14 | 15 | 16 | ## Eclipse Contributor Agreement 17 | 18 | Before your contribution can be accepted by the project team contributors must 19 | electronically sign the Eclipse Contributor Agreement (ECA). 20 | 21 | 22 | 23 | Commits that are provided by non-committers must have a Signed-off-by field in 24 | the footer indicating that the author is aware of the terms by which the 25 | contribution has been provided to the project. The non-committer must 26 | additionally have an Eclipse Foundation account and must have a signed Eclipse 27 | Contributor Agreement (ECA) on file. 28 | 29 | For more information, please see the Eclipse Committer Handbook: 30 | 31 | 32 | ## Setting up a development environment 33 | 34 | You can use any development environment you like to contribute to the uProtocol Rust SDK. However, it is mandatory to use the Rust linter ('[clippy]()') for any pull requests you do. 35 | To set up VSCode to run clippy per default every time you save your code, have a look here: [How to use Clippy in VS Code with rust-analyzer?](https://users.rust-lang.org/t/how-to-use-clippy-in-vs-code-with-rust-analyzer/41881) 36 | 37 | Similarly, the project requests that markdown is formatted and linted properly - to help with this, it is recommended to use [markdown linters](https://marketplace.visualstudio.com/items?itemName=DavidAnson.vscode-markdownlint). 38 | 39 | During development, before submitting a PR, you can use `./tools/fmt_clippy_doc.sh` to run these checks on the workspace. 40 | 41 | There also exists a helper script in ./tools to generate test results and test code coverage reports. These reports are placed in the `./target/tarpaulin` directory. If you use VSCode with the [Coverage Gutters](https://marketplace.visualstudio.com/items?itemName=ryanluker.vscode-coverage-gutters) extension, you can enable display of code coverage information with these settings: 42 | 43 | ``` json 44 | "coverage-gutters.coverageBaseDir": "**", 45 | "coverage-gutters.coverageFileNames": [ 46 | "target/tarpaulin/lcov.info", 47 | ], 48 | ``` 49 | 50 | ## DevContainer 51 | 52 | All of these prerequisites are made available as a VSCode devcontainer, configured at the usual place (`.devcontainer`). 53 | 54 | ## Contact 55 | 56 | Contact the project developers via the project's "dev" list. 57 | 58 | 59 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | ################################################################################ 2 | # Copyright (c) 2023 Contributors to the Eclipse Foundation 3 | # 4 | # See the NOTICE file(s) distributed with this work for additional 5 | # information regarding copyright ownership. 6 | # 7 | # This program and the accompanying materials are made available under the 8 | # terms of the Apache License Version 2.0 which is available at 9 | # https://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # SPDX-License-Identifier: Apache-2.0 12 | ################################################################################ 13 | 14 | [package] 15 | categories = ["api-bindings"] 16 | description = "The Eclipse uProtocol Rust Language Library" 17 | edition = "2021" 18 | include = [ 19 | "/build.rs", 20 | "/Cargo.toml", 21 | "/examples/*", 22 | "/proto/*", 23 | "/README.md", 24 | "/src/*", 25 | "/up-spec/up-core-api/uprotocol/*", 26 | ] 27 | keywords = ["uProtocol", "SDK", "communication"] 28 | license = "Apache-2.0" 29 | name = "up-rust" 30 | readme = "README.md" 31 | repository = "https://github.com/eclipse-uprotocol/up-rust" 32 | rust-version = "1.82" 33 | version = "0.5.0" 34 | 35 | [features] 36 | default = ["communication"] 37 | cloudevents = [] 38 | communication = ["usubscription", "dep:thiserror", "tokio/sync", "tokio/time"] 39 | udiscovery = [] 40 | usubscription = [] 41 | utwin = [] 42 | util = ["tokio/sync"] 43 | test-util = ["mockall"] 44 | 45 | [dependencies] 46 | async-trait = { version = "0.1" } 47 | bytes = { version = "1.10" } 48 | mediatype = "0.19" 49 | mockall = { version = "0.13", optional = true } 50 | protobuf = { version = "3.7.2", features = ["with-bytes"] } 51 | rand = { version = "0.8.0" } 52 | thiserror = { version = "1.0.69", optional = true } 53 | tokio = { version = "1.44", default-features = false, optional = true } 54 | tracing = { version = "0.1", default-features = false, features = [ 55 | "log", 56 | "std", 57 | ] } 58 | uriparse = { version = "0.6" } 59 | uuid-simd = { version = "0.8", default-features = false, features = [ 60 | "std", 61 | "detect", 62 | ] } 63 | 64 | [build-dependencies] 65 | protobuf-codegen = { version = "3.7.2" } 66 | protoc-bin-vendored = { version = "3.1" } 67 | 68 | [dev-dependencies] 69 | mockall = "0.13" 70 | test-case = { version = "3.3" } 71 | tokio = { version = "1.44", default-features = false, features = [ 72 | "macros", 73 | "rt", 74 | "rt-multi-thread", 75 | "sync", 76 | "time", 77 | ] } 78 | 79 | [profile.release] 80 | opt-level = 3 81 | lto = "fat" 82 | codegen-units = 1 83 | 84 | [package.metadata.docs.rs] 85 | all-features = true 86 | 87 | [[example]] 88 | name = "simple_notify" 89 | required-features = ["communication", "util"] 90 | 91 | [[example]] 92 | name = "simple_publish" 93 | required-features = ["communication", "util"] 94 | 95 | [[example]] 96 | name = "simple_rpc" 97 | required-features = ["communication", "util"] 98 | 99 | [lints.rust] 100 | # this prevents cargo from complaining about code blocks 101 | # excluded from tarpaulin coverage checks 102 | unexpected_cfgs = { level = "warn", check-cfg = ['cfg(tarpaulin_include)'] } 103 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /NOTICE.md: -------------------------------------------------------------------------------- 1 | # Notices for Eclipse uProtocol Rust Language Library 2 | 3 | This content is produced and maintained by the [Eclipse uProtocol project](https://eclipse-uprotocol.github.io). 4 | 5 | ## Trademarks 6 | 7 | Eclipse uProtocol is a trademark of the Eclipse Foundation. Eclipse, and the Eclipse Logo are registered trademarks of the Eclipse Foundation. 8 | 9 | ## Copyright 10 | 11 | All content is the property of the respective authors or their employers. For more information regarding authorship of content, please consult the listed source code repository logs. 12 | 13 | ## Declared Project Licenses 14 | 15 | This program and the accompanying materials are made available under the terms of the Apache License, Version 2.0 which is available at . 16 | 17 | SPDX-License-Identifier: Apache-2.0 18 | 19 | ## 3rd Party Dependencies 20 | 21 | Please refer to [Cargo.toml](Cargo.toml) for the libraries this crate depends on. 22 | This create also includes content from the [CloudEvents Specification](https://github.com/cloudevents/spec). 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Eclipse uProtocol Rust library 2 | 3 | This is the [uProtocol v1.6.0-alpha.4 Language Library](https://github.com/eclipse-uprotocol/uprotocol-spec/blob/v1.6.0-alpha.4/languages.adoc) for the Rust programming language. 4 | 5 | The crate can be used to 6 | 7 | * implement uEntities that communicate with each other using the uProtocol [Communication Layer API](https://github.com/eclipse-uprotocol/up-spec/blob/v1.6.0-alpha.4/up-l2/api.adoc) over one of the supported transport protocols. 8 | * implement support for an additional transport protocol by means of implementing the [Transport Layer API](https://github.com/eclipse-uprotocol/up-spec/blob/v1.6.0-alpha.4/up-l1/README.adoc). 9 | 10 | ## Using the Crate 11 | 16 | The crate needs to be added to the `[dependencies]` section of the `Cargo.toml` file: 17 | 18 | ```toml 19 | [dependencies] 20 | up-rust = { version = "0.1" } 21 | ``` 22 | 23 | Most developers will want to use the Communication Level API and its default implementation 24 | which are provided by the `communication` module. 25 | 26 | ## Building from Source 27 | 32 | 33 | First, the repository needs to be cloned using: 34 | 35 | ```sh 36 | git clone --recurse-submodules git@github.com:eclipse-uprotocol/up-rust 37 | ``` 38 | 39 | The `--recurse-submodules` parameter is important to make sure that the git submodule referring to the uProtocol type definitions is being initialized in the workspace. The proto3 files contained in that submodule define uProtocol's basic types and are being compiled into Rust code as part of the build process. 40 | If the repository has already been cloned without the parameter, the submodule can be initialized manually using `git submodule update --init --recursive`. 41 | 42 | The crate can then be built using the [Cargo package manager](https://doc.rust-lang.org/cargo/) from the root folder: 43 | 49 | 50 | ```sh 51 | cargo build 52 | ``` 53 | 54 | The crate has some (optional) _features_ as documented in [lib.rs](src/lib.rs). 55 | 56 | VSCode can be instructed to build all features automatically by means of putting the following into `./vscode/settings.json`: 57 | 58 | ```json 59 | { 60 | "rust-analyzer.cargo.features": "all" 61 | } 62 | ``` 63 | 64 | ### Generating API Documentation 65 | 66 | The API documentation can be generated using 67 | 68 | ```sh 69 | cargo doc --no-deps --all-features --open 70 | ``` 71 | 72 | ## License 73 | 74 | The crate is published under the terms of the [Apache License 2.0](LICENSE). 75 | 76 | ## Contributing 77 | 78 | Contributions are more than welcome. Please refer to the [Contribution Guide](CONTRIBUTING.md). 79 | -------------------------------------------------------------------------------- /about.hbs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Third Party Licenses 9 | 41 | 42 | 43 | 44 |
45 |
46 |

Third Party Licenses

47 |

48 | This page lists the licenses of the projects used in Eclipse uProtocol up-rust. Any licensing information listed here is based on 49 | the declarations made by individual Rust crates used by up-rust, and is reported as-is without further validation or additional checking. 50 |

51 |
52 | 53 |

Overview of licenses:

54 |
    55 | {{#each overview}} 56 |
  • {{name}} ({{count}})
  • 57 | {{/each}} 58 |
59 | 60 |

All license text:

61 |
    62 | {{#each licenses}} 63 |
  • 64 |

    {{name}}

    65 |

    Used by:

    66 | 71 |
    {{text}}
    72 |
  • 73 | {{/each}} 74 |
75 |
76 | 77 | 78 | 79 | -------------------------------------------------------------------------------- /about.toml: -------------------------------------------------------------------------------- 1 | # Config file for cargo about 2 | # For all options see https://embarkstudios.github.io/cargo-about/cli/generate/config.html 3 | 4 | # If you add a license in the following section also consider changing deny.toml 5 | accepted = [ 6 | "Apache-2.0", 7 | "MIT" 8 | ] 9 | 10 | targets = [ 11 | "x86_64-unknown-linux-musl", 12 | "aarch64-unknown-linux-musl" 13 | ] 14 | 15 | ignore-build-dependencies = true 16 | ignore-dev-dependencies = true 17 | 18 | [unicode-ident] 19 | accepted = ["Unicode-3.0"] 20 | 21 | [protobuf.clarify] 22 | license = "MIT" 23 | 24 | [[protobuf.clarify.files]] 25 | path = "LICENSE.txt" 26 | checksum = "7f2fa80a60e84f8dc0747abb0e42342f83bded04a20461a636b47c0331b92ddf" 27 | 28 | [protobuf-support.clarify] 29 | license = "MIT" 30 | 31 | [[protobuf-support.clarify.files]] 32 | path = "LICENSE.txt" 33 | checksum = "7f2fa80a60e84f8dc0747abb0e42342f83bded04a20461a636b47c0331b92ddf" 34 | -------------------------------------------------------------------------------- /build.rs: -------------------------------------------------------------------------------- 1 | /******************************************************************************** 2 | * Copyright (c) 2023 Contributors to the Eclipse Foundation 3 | * 4 | * See the NOTICE file(s) distributed with this work for additional 5 | * information regarding copyright ownership. 6 | * 7 | * This program and the accompanying materials are made available under the 8 | * terms of the Apache License Version 2.0 which is available at 9 | * https://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * SPDX-License-Identifier: Apache-2.0 12 | ********************************************************************************/ 13 | 14 | use protobuf_codegen::Customize; 15 | 16 | const UPROTOCOL_BASE_URI: &str = "up-spec/up-core-api/"; 17 | 18 | fn main() -> Result<(), Box> { 19 | let files = vec![ 20 | // uProtocol-project proto definitions 21 | format!("{}uprotocol/uoptions.proto", UPROTOCOL_BASE_URI), 22 | // [impl->req~uuid-proto~1] 23 | format!("{}uprotocol/v1/uuid.proto", UPROTOCOL_BASE_URI), 24 | // [impl->req~uri-data-model-proto~1] 25 | format!("{}uprotocol/v1/uri.proto", UPROTOCOL_BASE_URI), 26 | format!("{}uprotocol/v1/uattributes.proto", UPROTOCOL_BASE_URI), 27 | format!("{}uprotocol/v1/ucode.proto", UPROTOCOL_BASE_URI), 28 | format!("{}uprotocol/v1/umessage.proto", UPROTOCOL_BASE_URI), 29 | format!("{}uprotocol/v1/ustatus.proto", UPROTOCOL_BASE_URI), 30 | // not used in the SDK yet, but for completeness sake 31 | format!("{}uprotocol/v1/file.proto", UPROTOCOL_BASE_URI), 32 | // optional up-core-api features 33 | #[cfg(feature = "udiscovery")] 34 | format!( 35 | "{}uprotocol/core/udiscovery/v3/udiscovery.proto", 36 | UPROTOCOL_BASE_URI 37 | ), 38 | #[cfg(feature = "usubscription")] 39 | format!( 40 | "{}uprotocol/core/usubscription/v3/usubscription.proto", 41 | UPROTOCOL_BASE_URI 42 | ), 43 | #[cfg(feature = "utwin")] 44 | format!("{}uprotocol/core/utwin/v2/utwin.proto", UPROTOCOL_BASE_URI), 45 | ]; 46 | 47 | protobuf_codegen::Codegen::new() 48 | .protoc() 49 | // use vendored protoc instead of relying on user provided protobuf installation 50 | .protoc_path(&protoc_bin_vendored::protoc_bin_path().unwrap()) 51 | .customize(Customize::default().tokio_bytes(true)) 52 | .include(UPROTOCOL_BASE_URI) 53 | .inputs(files.as_slice()) 54 | .cargo_out_dir("uprotocol") 55 | .run_from_script(); 56 | 57 | #[cfg(feature = "cloudevents")] 58 | protobuf_codegen::Codegen::new() 59 | .protoc() 60 | // use vendored protoc instead of relying on user provided protobuf installation 61 | .protoc_path(&protoc_bin_vendored::protoc_bin_path().unwrap()) 62 | .include("proto") 63 | .inputs(["proto/io/cloudevents/v1/cloudevents.proto"]) 64 | .cargo_out_dir("cloudevents") 65 | .run_from_script(); 66 | 67 | Ok(()) 68 | } 69 | -------------------------------------------------------------------------------- /deny.toml: -------------------------------------------------------------------------------- 1 | # ******************************************************************************** 2 | # Copyright (c) 2023 Contributors to the Eclipse Foundation 3 | # 4 | # See the NOTICE file(s) distributed with this work for additional 5 | # information regarding copyright ownership. 6 | # 7 | # This program and the accompanying materials are made available under the 8 | # terms of the Apache License Version 2.0 which is available at 9 | # https://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # SPDX-License-Identifier: Apache-2.0 12 | # *******************************************************************************/ 13 | 14 | # Config file for cargo deny 15 | # For all options see https://github.com/EmbarkStudios/cargo-deny/blob/main/deny.template.toml 16 | 17 | # If you add a license in the following section also consider changing about.toml 18 | [licenses] 19 | allow = ["Apache-2.0", "MIT"] 20 | private = { ignore = true } 21 | exceptions = [ 22 | { name = "unicode-ident", allow = [ 23 | "Unicode-3.0", 24 | ] }, 25 | ] 26 | unused-allowed-license = "deny" 27 | 28 | [bans] 29 | multiple-versions = "deny" 30 | wildcards = "deny" 31 | skip-tree = [ 32 | { crate = "windows-sys", reason = "a foundational crate for many that bumps far too frequently to ever have a shared version" }, 33 | ] 34 | 35 | [advisories] 36 | ignore = [ 37 | { id = "RUSTSEC-2025-0023", reason = "this crate does not use tokio::sync::broadcast" } 38 | ] 39 | -------------------------------------------------------------------------------- /examples/simple_notify.rs: -------------------------------------------------------------------------------- 1 | /******************************************************************************** 2 | * Copyright (c) 2024 Contributors to the Eclipse Foundation 3 | * 4 | * See the NOTICE file(s) distributed with this work for additional 5 | * information regarding copyright ownership. 6 | * 7 | * This program and the accompanying materials are made available under the 8 | * terms of the Apache License Version 2.0 which is available at 9 | * https://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * SPDX-License-Identifier: Apache-2.0 12 | ********************************************************************************/ 13 | 14 | use std::sync::Arc; 15 | 16 | use protobuf::well_known_types::wrappers::StringValue; 17 | use up_rust::{ 18 | communication::{CallOptions, Notifier, SimpleNotifier, UPayload}, 19 | local_transport::LocalTransport, 20 | LocalUriProvider, StaticUriProvider, UListener, UMessage, 21 | }; 22 | 23 | struct ConsolePrinter {} 24 | 25 | #[async_trait::async_trait] 26 | impl UListener for ConsolePrinter { 27 | async fn on_receive(&self, msg: UMessage) { 28 | if let Ok(payload) = msg.extract_protobuf::() { 29 | println!("received notification: {}", payload.value); 30 | } 31 | } 32 | } 33 | 34 | #[tokio::main] 35 | pub async fn main() -> Result<(), Box> { 36 | const ORIGIN_RESOURCE_ID: u16 = 0xd100; 37 | 38 | let uri_provider = Arc::new(StaticUriProvider::new("my-vehicle", 0xa34b, 0x01)); 39 | let transport = Arc::new(LocalTransport::default()); 40 | let notifier = SimpleNotifier::new(transport, uri_provider.clone()); 41 | let topic = uri_provider.get_resource_uri(ORIGIN_RESOURCE_ID); 42 | let listener = Arc::new(ConsolePrinter {}); 43 | 44 | notifier.start_listening(&topic, listener.clone()).await?; 45 | 46 | let value = StringValue { 47 | value: "Hello".to_string(), 48 | ..Default::default() 49 | }; 50 | let payload = UPayload::try_from_protobuf(value)?; 51 | notifier 52 | .notify( 53 | ORIGIN_RESOURCE_ID, 54 | &uri_provider.get_source_uri(), 55 | CallOptions::for_notification(None, None, None), 56 | Some(payload), 57 | ) 58 | .await?; 59 | 60 | notifier.stop_listening(&topic, listener).await?; 61 | Ok(()) 62 | } 63 | -------------------------------------------------------------------------------- /examples/simple_publish.rs: -------------------------------------------------------------------------------- 1 | /******************************************************************************** 2 | * Copyright (c) 2024 Contributors to the Eclipse Foundation 3 | * 4 | * See the NOTICE file(s) distributed with this work for additional 5 | * information regarding copyright ownership. 6 | * 7 | * This program and the accompanying materials are made available under the 8 | * terms of the Apache License Version 2.0 which is available at 9 | * https://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * SPDX-License-Identifier: Apache-2.0 12 | ********************************************************************************/ 13 | 14 | use std::sync::Arc; 15 | 16 | use protobuf::well_known_types::wrappers::StringValue; 17 | use up_rust::{ 18 | communication::{CallOptions, Publisher, SimplePublisher, UPayload}, 19 | local_transport::LocalTransport, 20 | LocalUriProvider, StaticUriProvider, UListener, UMessage, UTransport, 21 | }; 22 | 23 | struct ConsolePrinter {} 24 | 25 | #[async_trait::async_trait] 26 | impl UListener for ConsolePrinter { 27 | async fn on_receive(&self, msg: UMessage) { 28 | if let Ok(payload) = msg.extract_protobuf::() { 29 | println!("received event: {}", payload.value); 30 | } 31 | } 32 | } 33 | 34 | #[tokio::main] 35 | pub async fn main() -> Result<(), Box> { 36 | const ORIGIN_RESOURCE_ID: u16 = 0xb4c1; 37 | let uri_provider = Arc::new(StaticUriProvider::new("my-vehicle", 0xa34b, 0x01)); 38 | let transport = Arc::new(LocalTransport::default()); 39 | let publisher = SimplePublisher::new(transport.clone(), uri_provider.clone()); 40 | let listener = Arc::new(ConsolePrinter {}); 41 | 42 | transport 43 | .register_listener( 44 | &uri_provider.get_resource_uri(ORIGIN_RESOURCE_ID), 45 | None, 46 | listener.clone(), 47 | ) 48 | .await?; 49 | 50 | let value = StringValue { 51 | value: "Hello".to_string(), 52 | ..Default::default() 53 | }; 54 | let payload = UPayload::try_from_protobuf(value)?; 55 | publisher 56 | .publish( 57 | ORIGIN_RESOURCE_ID, 58 | CallOptions::for_publish(None, None, None), 59 | Some(payload), 60 | ) 61 | .await?; 62 | 63 | transport 64 | .unregister_listener( 65 | &uri_provider.get_resource_uri(ORIGIN_RESOURCE_ID), 66 | None, 67 | listener, 68 | ) 69 | .await?; 70 | 71 | Ok(()) 72 | } 73 | -------------------------------------------------------------------------------- /examples/simple_rpc.rs: -------------------------------------------------------------------------------- 1 | /******************************************************************************** 2 | * Copyright (c) 2024 Contributors to the Eclipse Foundation 3 | * 4 | * See the NOTICE file(s) distributed with this work for additional 5 | * information regarding copyright ownership. 6 | * 7 | * This program and the accompanying materials are made available under the 8 | * terms of the Apache License Version 2.0 which is available at 9 | * https://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * SPDX-License-Identifier: Apache-2.0 12 | ********************************************************************************/ 13 | 14 | /*! 15 | This example illustrates how client code can use the _Communication Level API_ to invoke a 16 | service operation. It also shows how the corresponding service provider can be implemented. 17 | */ 18 | use std::sync::Arc; 19 | 20 | use protobuf::well_known_types::wrappers::StringValue; 21 | use up_rust::{ 22 | communication::{ 23 | CallOptions, InMemoryRpcClient, InMemoryRpcServer, RequestHandler, RpcClient, RpcServer, 24 | ServiceInvocationError, UPayload, 25 | }, 26 | local_transport::LocalTransport, 27 | LocalUriProvider, StaticUriProvider, UAttributes, 28 | }; 29 | 30 | struct EchoOperation {} 31 | 32 | #[async_trait::async_trait] 33 | impl RequestHandler for EchoOperation { 34 | async fn handle_request( 35 | &self, 36 | _resource_id: u16, 37 | _message_attributes: &UAttributes, 38 | request_payload: Option, 39 | ) -> Result, ServiceInvocationError> { 40 | if let Some(req_payload) = request_payload { 41 | Ok(Some(req_payload)) 42 | } else { 43 | Err(ServiceInvocationError::InvalidArgument( 44 | "request has no payload".to_string(), 45 | )) 46 | } 47 | } 48 | } 49 | 50 | #[tokio::main] 51 | pub async fn main() -> Result<(), Box> { 52 | const METHOD_RESOURCE_ID: u16 = 0x00a0; 53 | let uri_provider = Arc::new(StaticUriProvider::new("my-vehicle", 0xa34b, 0x01)); 54 | let transport = Arc::new(LocalTransport::default()); 55 | 56 | // create the RpcServer using the local transport 57 | let rpc_server = InMemoryRpcServer::new(transport.clone(), uri_provider.clone()); 58 | // and register an endpoint for the service operation 59 | let echo_op = Arc::new(EchoOperation {}); 60 | 61 | rpc_server 62 | .register_endpoint(None, METHOD_RESOURCE_ID, echo_op.clone()) 63 | .await?; 64 | 65 | // now create an RpcClient attached to the same local transport 66 | let rpc_client = InMemoryRpcClient::new(transport, uri_provider.clone()).await?; 67 | // and invoke the service operation without any payload 68 | match rpc_client 69 | .invoke_method( 70 | uri_provider.get_resource_uri(METHOD_RESOURCE_ID), 71 | CallOptions::for_rpc_request(1_000, None, None, None), 72 | None, // no payload 73 | ) 74 | .await 75 | { 76 | Err(ServiceInvocationError::InvalidArgument(msg)) => { 77 | println!("service returned expected error: {}", msg) 78 | } 79 | _ => panic!("expected service to return an Invalid Argument error"), 80 | } 81 | 82 | // now invoke the operaiton with a message in the request payload 83 | let value = StringValue { 84 | value: "Hello".to_string(), 85 | ..Default::default() 86 | }; 87 | let payload = UPayload::try_from_protobuf(value)?; 88 | // and make sure that the response contains a message in the payload 89 | match rpc_client 90 | .invoke_method( 91 | uri_provider.get_resource_uri(METHOD_RESOURCE_ID), 92 | CallOptions::for_rpc_request(1_000, None, None, None), 93 | Some(payload), 94 | ) 95 | .await 96 | { 97 | Ok(Some(payload)) => { 98 | let value = payload.extract_protobuf::()?; 99 | println!("service returned message: {}", value.value); 100 | } 101 | _ => panic!("expected service to return response message"), 102 | } 103 | 104 | // and finally unregister the endpoint 105 | rpc_server 106 | .unregister_endpoint(None, METHOD_RESOURCE_ID, echo_op) 107 | .await?; 108 | 109 | Ok(()) 110 | } 111 | -------------------------------------------------------------------------------- /proto/io/cloudevents/v1/cloudevents.proto: -------------------------------------------------------------------------------- 1 | /******************************************************************************** 2 | * Copyright (c) 2024 Contributors to the Eclipse Foundation 3 | * 4 | * See the NOTICE file(s) distributed with this work for additional 5 | * information regarding copyright ownership. 6 | * 7 | * This program and the accompanying materials are made available under the 8 | * terms of the Apache License Version 2.0 which is available at 9 | * https://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * SPDX-License-Identifier: Apache-2.0 12 | * 13 | * CloudEvent Protobuf Format 14 | * 15 | * - Required context attributes are explicity represented. 16 | * - Optional and Extension context attributes are carried in a map structure. 17 | * - Data may be represented as binary, text, or protobuf messages. 18 | * 19 | * This file is a verbatim copy of 20 | * https://github.com/cloudevents/spec/blob/v1.0.2/cloudevents/formats/cloudevents.proto 21 | */ 22 | 23 | syntax = "proto3"; 24 | 25 | package io.cloudevents.v1; 26 | 27 | import "google/protobuf/any.proto"; 28 | import "google/protobuf/timestamp.proto"; 29 | 30 | option csharp_namespace = "CloudNative.CloudEvents.V1"; 31 | option go_package = "cloudevents.io/genproto/v1"; 32 | option java_package = "io.cloudevents.v1.proto"; 33 | option java_multiple_files = true; 34 | option php_namespace = "Io\\CloudEvents\\V1\\Proto"; 35 | option ruby_package = "Io::CloudEvents::V1::Proto"; 36 | 37 | message CloudEvent { 38 | 39 | // -- CloudEvent Context Attributes 40 | 41 | // Required Attributes 42 | string id = 1; 43 | string source = 2; // URI-reference 44 | string spec_version = 3; 45 | string type = 4; 46 | 47 | // Optional & Extension Attributes 48 | map attributes = 5; 49 | 50 | // -- CloudEvent Data (Bytes, Text, or Proto) 51 | oneof data { 52 | bytes binary_data = 6; 53 | string text_data = 7; 54 | google.protobuf.Any proto_data = 8; 55 | } 56 | 57 | /** 58 | * The CloudEvent specification defines 59 | * seven attribute value types... 60 | */ 61 | 62 | message CloudEventAttributeValue { 63 | 64 | oneof attr { 65 | bool ce_boolean = 1; 66 | int32 ce_integer = 2; 67 | string ce_string = 3; 68 | bytes ce_bytes = 4; 69 | string ce_uri = 5; 70 | string ce_uri_ref = 6; 71 | google.protobuf.Timestamp ce_timestamp = 7; 72 | } 73 | } 74 | } 75 | 76 | /** 77 | * CloudEvent Protobuf Batch Format 78 | * 79 | */ 80 | 81 | message CloudEventBatch { 82 | repeated CloudEvent events = 1; 83 | } 84 | -------------------------------------------------------------------------------- /src/communication.rs: -------------------------------------------------------------------------------- 1 | /******************************************************************************** 2 | * Copyright (c) 2024 Contributors to the Eclipse Foundation 3 | * 4 | * See the NOTICE file(s) distributed with this work for additional 5 | * information regarding copyright ownership. 6 | * 7 | * This program and the accompanying materials are made available under the 8 | * terms of the Apache License Version 2.0 which is available at 9 | * https://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * SPDX-License-Identifier: Apache-2.0 12 | ********************************************************************************/ 13 | 14 | use bytes::Bytes; 15 | use protobuf::{well_known_types::any::Any, Message, MessageFull}; 16 | use std::{error::Error, fmt::Display}; 17 | 18 | pub use default_notifier::SimpleNotifier; 19 | #[cfg(feature = "usubscription")] 20 | pub use default_pubsub::{InMemorySubscriber, SimplePublisher}; 21 | pub use in_memory_rpc_client::InMemoryRpcClient; 22 | pub use in_memory_rpc_server::InMemoryRpcServer; 23 | #[cfg(any(test, feature = "test-util"))] 24 | pub use notification::MockNotifier; 25 | pub use notification::{NotificationError, Notifier}; 26 | #[cfg(any(test, feature = "test-util"))] 27 | pub use pubsub::MockSubscriptionChangeHandler; 28 | #[cfg(feature = "usubscription")] 29 | pub use pubsub::{PubSubError, Publisher, Subscriber}; 30 | #[cfg(any(test, feature = "test-util"))] 31 | pub use rpc::{MockRequestHandler, MockRpcClient, MockRpcServerImpl}; 32 | pub use rpc::{RequestHandler, RpcClient, RpcServer, ServiceInvocationError}; 33 | #[cfg(feature = "udiscovery")] 34 | pub use udiscovery_client::RpcClientUDiscovery; 35 | #[cfg(feature = "usubscription")] 36 | pub use usubscription_client::RpcClientUSubscription; 37 | 38 | use crate::{ 39 | umessage::{self, UMessageError}, 40 | UCode, UMessage, UMessageBuilder, UPayloadFormat, UPriority, UStatus, UUID, 41 | }; 42 | 43 | mod default_notifier; 44 | mod default_pubsub; 45 | mod in_memory_rpc_client; 46 | mod in_memory_rpc_server; 47 | mod notification; 48 | #[cfg(feature = "usubscription")] 49 | mod pubsub; 50 | mod rpc; 51 | #[cfg(feature = "udiscovery")] 52 | mod udiscovery_client; 53 | #[cfg(feature = "usubscription")] 54 | mod usubscription_client; 55 | 56 | /// An error indicating a problem with registering or unregistering a message listener. 57 | #[derive(Clone, Debug)] 58 | pub enum RegistrationError { 59 | /// Indicates that a listener for a given address already exists. 60 | AlreadyExists, 61 | /// Indicates that the maximum number of listeners supported by the Transport Layer implementation 62 | /// has already been registered. 63 | MaxListenersExceeded, 64 | /// Indicates that no listener is registered for given pattern URIs. 65 | NoSuchListener, 66 | /// Indicates that the underlying Transport Layer implementation does not support registration and 67 | /// notification of message handlers. 68 | PushDeliveryMethodNotSupported, 69 | /// Indicates that some of the given filters are inappropriate in this context. 70 | InvalidFilter(String), 71 | /// Indicates a generic error. 72 | Unknown(UStatus), 73 | } 74 | 75 | impl Display for RegistrationError { 76 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 77 | match self { 78 | RegistrationError::AlreadyExists => { 79 | f.write_str("a listener for the given filter criteria already exists") 80 | } 81 | RegistrationError::MaxListenersExceeded => { 82 | f.write_str("maximum number of listeners has been reached") 83 | } 84 | RegistrationError::NoSuchListener => { 85 | f.write_str("no listener registered for given pattern") 86 | } 87 | RegistrationError::PushDeliveryMethodNotSupported => f.write_str( 88 | "the underlying transport implementation does not support the push delivery method", 89 | ), 90 | RegistrationError::InvalidFilter(msg) => { 91 | f.write_fmt(format_args!("invalid filter(s): {}", msg)) 92 | } 93 | RegistrationError::Unknown(status) => f.write_fmt(format_args!( 94 | "error un-/registering listener: {}", 95 | status.get_message() 96 | )), 97 | } 98 | } 99 | } 100 | 101 | impl Error for RegistrationError {} 102 | 103 | impl From for RegistrationError { 104 | fn from(value: UStatus) -> Self { 105 | match value.code.enum_value() { 106 | Ok(UCode::ALREADY_EXISTS) => RegistrationError::AlreadyExists, 107 | Ok(UCode::NOT_FOUND) => RegistrationError::NoSuchListener, 108 | Ok(UCode::RESOURCE_EXHAUSTED) => RegistrationError::MaxListenersExceeded, 109 | Ok(UCode::UNIMPLEMENTED) => RegistrationError::PushDeliveryMethodNotSupported, 110 | Ok(UCode::INVALID_ARGUMENT) => RegistrationError::InvalidFilter(value.get_message()), 111 | _ => RegistrationError::Unknown(value), 112 | } 113 | } 114 | } 115 | 116 | /// General options that clients might want to specify when sending a uProtocol message. 117 | #[derive(Clone, Debug, PartialEq)] 118 | pub struct CallOptions { 119 | ttl: u32, 120 | message_id: Option, 121 | token: Option, 122 | priority: Option, 123 | } 124 | 125 | impl CallOptions { 126 | /// Creates new call options for an RPC Request. 127 | /// 128 | /// # Arguments 129 | /// 130 | /// * `ttl` - The message's time-to-live in milliseconds. 131 | /// * `message_id` - The identifier to use for the message or `None` to use a generated identifier. 132 | /// * `token` - The token to use for authenticating to infrastructure and service endpoints. 133 | /// * `priority` - The message's priority or `None` to use the default priority for RPC Requests. 134 | /// 135 | /// # Returns 136 | /// 137 | /// Options suitable for invoking an RPC method. 138 | /// 139 | /// # Examples 140 | /// 141 | /// ```rust 142 | /// use up_rust::{UPriority, UUID, communication::CallOptions}; 143 | /// 144 | /// let uuid = UUID::new(); 145 | /// let options = CallOptions::for_rpc_request(15_000, Some(uuid.clone()), Some("token".to_string()), Some(UPriority::UPRIORITY_CS6)); 146 | /// assert_eq!(options.ttl(), 15_000); 147 | /// assert_eq!(options.message_id(), Some(uuid)); 148 | /// assert_eq!(options.token(), Some("token".to_string())); 149 | /// assert_eq!(options.priority(), Some(UPriority::UPRIORITY_CS6)); 150 | /// ``` 151 | pub fn for_rpc_request( 152 | ttl: u32, 153 | message_id: Option, 154 | token: Option, 155 | priority: Option, 156 | ) -> Self { 157 | CallOptions { 158 | ttl, 159 | message_id, 160 | token, 161 | priority, 162 | } 163 | } 164 | 165 | /// Creates new call options for a Notification message. 166 | /// 167 | /// # Arguments 168 | /// 169 | /// * `ttl` - The message's time-to-live in milliseconds. 170 | /// * `message_id` - The identifier to use for the message or `None` to use a generated identifier. 171 | /// * `priority` - The message's priority or `None` to use the default priority for Notifications. 172 | /// 173 | /// # Returns 174 | /// 175 | /// Options suitable for sending a Notification. 176 | /// 177 | /// # Examples 178 | /// 179 | /// ```rust 180 | /// use up_rust::{UPriority, UUID, communication::CallOptions}; 181 | /// 182 | /// let uuid = UUID::new(); 183 | /// let options = CallOptions::for_notification(Some(15_000), Some(uuid.clone()), Some(UPriority::UPRIORITY_CS2)); 184 | /// assert_eq!(options.ttl(), 15_000); 185 | /// assert_eq!(options.message_id(), Some(uuid)); 186 | /// assert_eq!(options.priority(), Some(UPriority::UPRIORITY_CS2)); 187 | /// ``` 188 | pub fn for_notification( 189 | ttl: Option, 190 | message_id: Option, 191 | priority: Option, 192 | ) -> Self { 193 | CallOptions { 194 | ttl: ttl.unwrap_or(0), 195 | message_id, 196 | token: None, 197 | priority, 198 | } 199 | } 200 | 201 | /// Creates new call options for a Publish message. 202 | /// 203 | /// # Arguments 204 | /// 205 | /// * `ttl` - The message's time-to-live in milliseconds or `None` if the message should not expire at all. 206 | /// * `message_id` - The identifier to use for the message or `None` to use a generated identifier. 207 | /// * `priority` - The message's priority or `None` to use the default priority for Publish messages. 208 | /// 209 | /// # Returns 210 | /// 211 | /// Options suitable for sending a Publish message. 212 | /// 213 | /// # Examples 214 | /// 215 | /// ```rust 216 | /// use up_rust::{UPriority, UUID, communication::CallOptions}; 217 | /// 218 | /// let uuid = UUID::new(); 219 | /// let options = CallOptions::for_publish(Some(15_000), Some(uuid.clone()), Some(UPriority::UPRIORITY_CS2)); 220 | /// assert_eq!(options.ttl(), 15_000); 221 | /// assert_eq!(options.message_id(), Some(uuid)); 222 | /// assert_eq!(options.priority(), Some(UPriority::UPRIORITY_CS2)); 223 | /// ``` 224 | pub fn for_publish( 225 | ttl: Option, 226 | message_id: Option, 227 | priority: Option, 228 | ) -> Self { 229 | CallOptions { 230 | ttl: ttl.unwrap_or(0), 231 | message_id, 232 | token: None, 233 | priority, 234 | } 235 | } 236 | 237 | /// Gets the message's time-to-live in milliseconds. 238 | pub fn ttl(&self) -> u32 { 239 | self.ttl 240 | } 241 | 242 | /// Gets the identifier to use for the message. 243 | pub fn message_id(&self) -> Option { 244 | self.message_id.clone() 245 | } 246 | 247 | /// Gets the token to use for authenticating to infrastructure and service endpoints. 248 | pub fn token(&self) -> Option { 249 | self.token.clone() 250 | } 251 | 252 | /// Gets the message's priority. 253 | pub fn priority(&self) -> Option { 254 | self.priority 255 | } 256 | } 257 | 258 | /// A wrapper around (raw) message payload data and the corresponding payload format. 259 | #[derive(Clone, Debug, PartialEq)] 260 | pub struct UPayload { 261 | payload_format: UPayloadFormat, 262 | payload: Bytes, 263 | } 264 | 265 | impl UPayload { 266 | /// Creates a new payload for some data. 267 | /// 268 | /// # Examples 269 | /// 270 | /// ```rust 271 | /// use up_rust::UPayloadFormat; 272 | /// use up_rust::communication::UPayload; 273 | /// 274 | /// let data: Vec = vec![0x00_u8, 0x01_u8, 0x02_u8]; 275 | /// let payload = UPayload::new(data, UPayloadFormat::UPAYLOAD_FORMAT_RAW); 276 | /// assert_eq!(payload.payload_format(), UPayloadFormat::UPAYLOAD_FORMAT_RAW); 277 | /// assert_eq!(payload.payload().len(), 3); 278 | /// ``` 279 | pub fn new>(payload: T, payload_format: UPayloadFormat) -> Self { 280 | UPayload { 281 | payload_format, 282 | payload: payload.into(), 283 | } 284 | } 285 | 286 | /// Creates a new UPayload from a protobuf message. 287 | /// 288 | /// The resulting payload will have `UPayloadType::UPAYLOAD_FORMAT_PROTOBUF_WRAPPED_IN_ANY`. 289 | /// 290 | /// # Errors 291 | /// 292 | /// Returns an error if the given message cannot be serialized to bytes. 293 | /// 294 | /// # Examples 295 | /// 296 | /// ```rust 297 | /// use up_rust::{communication::UPayload, UPayloadFormat}; 298 | /// use protobuf::{well_known_types::wrappers::StringValue}; 299 | /// 300 | /// let mut data = StringValue::new(); 301 | /// data.value = "hello world".to_string(); 302 | /// assert!(UPayload::try_from_protobuf(data).is_ok_and(|pl| 303 | /// pl.payload_format() == UPayloadFormat::UPAYLOAD_FORMAT_PROTOBUF_WRAPPED_IN_ANY 304 | /// && pl.payload().len() > 0)); 305 | /// ``` 306 | pub fn try_from_protobuf(message: M) -> Result 307 | where 308 | M: MessageFull, 309 | { 310 | Any::pack(&message) 311 | .and_then(|any| any.write_to_bytes()) 312 | .map(|buf| UPayload::new(buf, UPayloadFormat::UPAYLOAD_FORMAT_PROTOBUF_WRAPPED_IN_ANY)) 313 | .map_err(UMessageError::DataSerializationError) 314 | } 315 | 316 | /// Gets the payload format. 317 | /// 318 | /// # Returns 319 | /// 320 | /// payload value of `UPayload`. 321 | pub fn payload_format(&self) -> UPayloadFormat { 322 | self.payload_format 323 | } 324 | 325 | /// Gets the payload data. 326 | /// 327 | /// Note that this consumes the payload. 328 | pub fn payload(self) -> Bytes { 329 | self.payload 330 | } 331 | 332 | /// Extracts the protobuf `Message` contained in payload. 333 | /// 334 | /// This function is used to extract strongly-typed data from a `UPayload` object, 335 | /// taking into account the payload format (will only succeed if payload format is 336 | /// `UPayloadFormat::UPAYLOAD_FORMAT_PROTOBUF` or `UPayloadFormat::UPAYLOAD_FORMAT_PROTOBUF_WRAPPED_IN_ANY`) 337 | /// 338 | /// # Type Parameters 339 | /// 340 | /// * `T`: The target type of the data to be unpacked. 341 | /// 342 | /// # Returns 343 | /// 344 | /// * `Ok(T)`: The deserialized protobuf `Message` contained in the payload. 345 | /// 346 | /// # Errors 347 | /// 348 | /// * Err(`UMessageError`) if the unpacking process fails, for example if the payload could 349 | /// not be deserialized into the target type `T`. 350 | /// 351 | /// 352 | /// # Examples 353 | /// 354 | /// ```rust 355 | /// use up_rust::{communication::UPayload, UPayloadFormat}; 356 | /// use protobuf::{well_known_types::wrappers::StringValue}; 357 | /// 358 | /// let mut data = StringValue::new(); 359 | /// data.value = "hello world".to_string(); 360 | /// let payload = UPayload::try_from_protobuf(data).expect("should be able to create UPayload from StringValue"); 361 | /// 362 | /// let string_value: StringValue = payload.extract_protobuf().expect("should be able to extract StringValue from UPayload"); 363 | /// assert_eq!(string_value.value, *"hello world"); 364 | /// ``` 365 | pub fn extract_protobuf(&self) -> Result { 366 | umessage::deserialize_protobuf_bytes(&self.payload, &self.payload_format) 367 | } 368 | } 369 | 370 | /// Moves all common call options into the given message builder. 371 | /// 372 | /// In particular, the following options are moved: 373 | /// * ttl 374 | /// * message ID 375 | /// * priority 376 | pub(crate) fn apply_common_options( 377 | call_options: CallOptions, 378 | message_builder: &mut UMessageBuilder, 379 | ) { 380 | message_builder.with_ttl(call_options.ttl); 381 | if let Some(v) = call_options.message_id { 382 | message_builder.with_message_id(v); 383 | } 384 | if let Some(v) = call_options.priority { 385 | message_builder.with_priority(v); 386 | } 387 | } 388 | 389 | /// Creates a message with given payload from a builder. 390 | pub(crate) fn build_message( 391 | message_builder: &mut UMessageBuilder, 392 | payload: Option, 393 | ) -> Result { 394 | if let Some(pl) = payload { 395 | let format = pl.payload_format(); 396 | message_builder.build_with_payload(pl.payload, format) 397 | } else { 398 | message_builder.build() 399 | } 400 | } 401 | -------------------------------------------------------------------------------- /src/communication/default_notifier.rs: -------------------------------------------------------------------------------- 1 | /******************************************************************************** 2 | * Copyright (c) 2024 Contributors to the Eclipse Foundation 3 | * 4 | * See the NOTICE file(s) distributed with this work for additional 5 | * information regarding copyright ownership. 6 | * 7 | * This program and the accompanying materials are made available under the 8 | * terms of the Apache License Version 2.0 which is available at 9 | * https://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * SPDX-License-Identifier: Apache-2.0 12 | ********************************************************************************/ 13 | 14 | // [impl->req~up-language-comm-api-default-impl~1] 15 | 16 | use std::sync::Arc; 17 | 18 | use async_trait::async_trait; 19 | 20 | use crate::{LocalUriProvider, UListener, UMessageBuilder, UTransport, UUri}; 21 | 22 | use super::{ 23 | apply_common_options, build_message, CallOptions, NotificationError, Notifier, 24 | RegistrationError, UPayload, 25 | }; 26 | 27 | /// A [`Notifier`] that uses the uProtocol Transport Layer API to send and receive 28 | /// notifications to/from (other) uEntities. 29 | pub struct SimpleNotifier { 30 | transport: Arc, 31 | uri_provider: Arc, 32 | } 33 | 34 | impl SimpleNotifier { 35 | /// Creates a new Notifier for a given transport. 36 | /// 37 | /// # Arguments 38 | /// 39 | /// * `transport` - The uProtocol Transport Layer implementation to use for sending and receiving notification messages. 40 | /// * `uri_provider` - The helper for creating URIs that represent local resources. 41 | pub fn new(transport: Arc, uri_provider: Arc) -> Self { 42 | SimpleNotifier { 43 | transport, 44 | uri_provider, 45 | } 46 | } 47 | } 48 | 49 | #[async_trait] 50 | impl Notifier for SimpleNotifier { 51 | async fn notify( 52 | &self, 53 | resource_id: u16, 54 | destination: &UUri, 55 | call_options: CallOptions, 56 | payload: Option, 57 | ) -> Result<(), NotificationError> { 58 | let mut builder = UMessageBuilder::notification( 59 | self.uri_provider.get_resource_uri(resource_id), 60 | destination.to_owned(), 61 | ); 62 | apply_common_options(call_options, &mut builder); 63 | let msg = build_message(&mut builder, payload) 64 | .map_err(|e| NotificationError::InvalidArgument(e.to_string()))?; 65 | self.transport 66 | .send(msg) 67 | .await 68 | .map_err(NotificationError::NotifyError) 69 | } 70 | 71 | async fn start_listening( 72 | &self, 73 | topic: &UUri, 74 | listener: Arc, 75 | ) -> Result<(), RegistrationError> { 76 | topic 77 | .verify_no_wildcards() 78 | .map_err(|e| RegistrationError::InvalidFilter(e.to_string()))?; 79 | self.transport 80 | .register_listener(topic, Some(&self.uri_provider.get_source_uri()), listener) 81 | .await 82 | .map_err(RegistrationError::from) 83 | } 84 | 85 | async fn stop_listening( 86 | &self, 87 | topic: &UUri, 88 | listener: Arc, 89 | ) -> Result<(), RegistrationError> { 90 | topic 91 | .verify_no_wildcards() 92 | .map_err(|e| RegistrationError::InvalidFilter(e.to_string()))?; 93 | self.transport 94 | .unregister_listener(topic, Some(&self.uri_provider.get_source_uri()), listener) 95 | .await 96 | .map_err(RegistrationError::from) 97 | } 98 | } 99 | 100 | #[cfg(test)] 101 | mod tests { 102 | 103 | // [utest->req~up-language-comm-api-default-impl~1] 104 | 105 | use super::*; 106 | 107 | use protobuf::well_known_types::wrappers::StringValue; 108 | 109 | use crate::{ 110 | utransport::{MockTransport, MockUListener}, 111 | StaticUriProvider, UCode, UPriority, UStatus, UUri, UUID, 112 | }; 113 | 114 | fn new_uri_provider() -> Arc { 115 | Arc::new(StaticUriProvider::new("", 0x0005, 0x02)) 116 | } 117 | 118 | #[tokio::test] 119 | async fn test_start_stop_listening_rejects_wildcard_topic() { 120 | let mut transport = MockTransport::new(); 121 | transport.expect_do_register_listener().never(); 122 | let uri_provider = new_uri_provider(); 123 | let notifier = SimpleNotifier::new(Arc::new(transport), uri_provider); 124 | 125 | let invalid_topic = UUri::try_from("up://my-vin/A15B/1/FFFF").unwrap(); 126 | let mut listener = MockUListener::new(); 127 | listener.expect_on_receive().never(); 128 | let wrapped_listener = Arc::new(listener); 129 | 130 | let result = notifier 131 | .start_listening(&invalid_topic, wrapped_listener.clone()) 132 | .await; 133 | assert!(result.is_err_and(|e| matches!(e, RegistrationError::InvalidFilter(_)))); 134 | 135 | let result = notifier 136 | .stop_listening(&invalid_topic, wrapped_listener) 137 | .await; 138 | assert!(result.is_err_and(|e| matches!(e, RegistrationError::InvalidFilter(_)))); 139 | } 140 | 141 | #[tokio::test] 142 | async fn test_start_listening_succeeds() { 143 | let uri_provider = new_uri_provider(); 144 | let topic = UUri::try_from("up://my-vin/A15B/1/B10F").unwrap(); 145 | let expected_source_filter = topic.clone(); 146 | let expected_sink_filter = uri_provider.get_source_uri(); 147 | let mut transport = MockTransport::new(); 148 | transport 149 | .expect_do_register_listener() 150 | .once() 151 | .withf(move |source_filter, sink_filter, _listener| { 152 | source_filter == &expected_source_filter 153 | && *sink_filter == Some(&expected_sink_filter) 154 | }) 155 | .return_const(Ok(())); 156 | let notifier = SimpleNotifier::new(Arc::new(transport), uri_provider); 157 | 158 | let mut listener = MockUListener::new(); 159 | listener.expect_on_receive().never(); 160 | let result = notifier.start_listening(&topic, Arc::new(listener)).await; 161 | assert!(result.is_ok()); 162 | } 163 | 164 | #[tokio::test] 165 | async fn test_stop_listening_succeeds() { 166 | let uri_provider = new_uri_provider(); 167 | let topic = UUri::try_from("up://my-vin/A15B/1/B10F").unwrap(); 168 | let expected_source_filter = topic.clone(); 169 | let expected_sink_filter = uri_provider.get_source_uri(); 170 | let mut transport = MockTransport::new(); 171 | transport 172 | .expect_do_unregister_listener() 173 | .once() 174 | .withf(move |source_filter, sink_filter, _listener| { 175 | source_filter == &expected_source_filter 176 | && *sink_filter == Some(&expected_sink_filter) 177 | }) 178 | .return_const(Ok(())); 179 | let notifier = SimpleNotifier::new(Arc::new(transport), uri_provider); 180 | 181 | let mut listener = MockUListener::new(); 182 | listener.expect_on_receive().never(); 183 | let result = notifier.stop_listening(&topic, Arc::new(listener)).await; 184 | assert!(result.is_ok()); 185 | } 186 | 187 | #[tokio::test] 188 | async fn test_publish_succeeds() { 189 | let message_id = UUID::build(); 190 | let uri_provider = new_uri_provider(); 191 | let destination = UUri::try_from("up://other-vin/A15B/1/0").unwrap(); 192 | let expected_message_id = message_id.clone(); 193 | let expected_sink = destination.clone(); 194 | let expected_source = uri_provider.get_resource_uri(0xB10F); 195 | let mut transport = MockTransport::new(); 196 | transport 197 | .expect_do_send() 198 | .once() 199 | .withf(move |message| { 200 | let Ok(payload) = message.extract_protobuf::() else { 201 | return false; 202 | }; 203 | message.is_notification() 204 | && message.id_unchecked() == &expected_message_id 205 | && message.source_unchecked() == &expected_source 206 | && message.sink_unchecked() == &expected_sink 207 | && message.ttl_unchecked() == 10_000 208 | && message.priority_unchecked() == UPriority::UPRIORITY_CS2 209 | && payload.value == *"Hello" 210 | }) 211 | .return_const(Ok(())); 212 | let notifier = SimpleNotifier::new(Arc::new(transport), uri_provider); 213 | 214 | let mut v = StringValue::new(); 215 | v.value = "Hello".to_string(); 216 | let payload = UPayload::try_from_protobuf(v).unwrap(); 217 | let options = CallOptions::for_notification( 218 | Some(10_000), 219 | Some(message_id), 220 | Some(UPriority::UPRIORITY_CS2), 221 | ); 222 | let result = notifier 223 | .notify(0xB10F, &destination, options, Some(payload)) 224 | .await; 225 | assert!(result.is_ok()); 226 | } 227 | 228 | #[tokio::test] 229 | async fn test_publish_fails_for_transport_error() { 230 | let uri_provider = new_uri_provider(); 231 | let destination = UUri::try_from("up://other-vin/A15B/1/0").unwrap(); 232 | let mut transport = MockTransport::new(); 233 | transport 234 | .expect_do_send() 235 | .once() 236 | .return_const(Err(UStatus::fail_with_code( 237 | crate::UCode::UNAVAILABLE, 238 | "connection lost", 239 | ))); 240 | let notifier = SimpleNotifier::new(Arc::new(transport), uri_provider); 241 | 242 | let options = CallOptions::for_notification(None, None, None); 243 | let result = notifier.notify(0xB10F, &destination, options, None).await; 244 | assert!(result.is_err_and(|e| match e { 245 | NotificationError::NotifyError(status) => status.get_code() == UCode::UNAVAILABLE, 246 | _ => false, 247 | })); 248 | } 249 | 250 | #[tokio::test] 251 | async fn test_publish_fails_for_invalid_destination() { 252 | let uri_provider = new_uri_provider(); 253 | // destination has resource ID != 0 254 | let destination = UUri::try_from("up://other-vin/A15B/1/10").unwrap(); 255 | let mut transport = MockTransport::new(); 256 | transport.expect_do_send().never(); 257 | let notifier = SimpleNotifier::new(Arc::new(transport), uri_provider); 258 | 259 | let options = CallOptions::for_notification(None, None, None); 260 | let result = notifier.notify(0xB10F, &destination, options, None).await; 261 | assert!(result.is_err_and(|e| matches!(e, NotificationError::InvalidArgument(_)))); 262 | } 263 | 264 | #[tokio::test] 265 | async fn test_publish_fails_for_invalid_resource_id() { 266 | let uri_provider = new_uri_provider(); 267 | let destination = UUri::try_from("up://other-vin/A15B/1/0").unwrap(); 268 | let mut transport = MockTransport::new(); 269 | transport.expect_do_send().never(); 270 | let notifier = SimpleNotifier::new(Arc::new(transport), uri_provider); 271 | 272 | let options = CallOptions::for_notification(None, None, None); 273 | // resource ID of origin address must not be 0 274 | let result = notifier.notify(0x0000, &destination, options, None).await; 275 | assert!(result.is_err_and(|e| matches!(e, NotificationError::InvalidArgument(_)))); 276 | } 277 | } 278 | -------------------------------------------------------------------------------- /src/communication/notification.rs: -------------------------------------------------------------------------------- 1 | /******************************************************************************** 2 | * Copyright (c) 2024 Contributors to the Eclipse Foundation 3 | * 4 | * See the NOTICE file(s) distributed with this work for additional 5 | * information regarding copyright ownership. 6 | * 7 | * This program and the accompanying materials are made available under the 8 | * terms of the Apache License Version 2.0 which is available at 9 | * https://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * SPDX-License-Identifier: Apache-2.0 12 | ********************************************************************************/ 13 | 14 | use std::{error::Error, fmt::Display, sync::Arc}; 15 | 16 | use async_trait::async_trait; 17 | 18 | use crate::communication::RegistrationError; 19 | use crate::{UListener, UStatus, UUri}; 20 | 21 | use super::{CallOptions, UPayload}; 22 | 23 | /// An error indicating a problem with sending a notification to another uEntity. 24 | // [impl->req~up-language-comm-api~1] 25 | #[derive(Debug)] 26 | pub enum NotificationError { 27 | /// Indicates that the given message cannot be sent because it is not a [valid Notification message](crate::NotificationValidator). 28 | InvalidArgument(String), 29 | /// Indicates an unspecific error that occurred at the Transport Layer while trying to send a notification. 30 | NotifyError(UStatus), 31 | } 32 | 33 | #[cfg(not(tarpaulin_include))] 34 | impl Display for NotificationError { 35 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 36 | match self { 37 | NotificationError::InvalidArgument(s) => f.write_str(s.as_str()), 38 | NotificationError::NotifyError(s) => { 39 | f.write_fmt(format_args!("failed to send notification: {}", s)) 40 | } 41 | } 42 | } 43 | } 44 | 45 | impl Error for NotificationError {} 46 | 47 | /// A client for sending Notification messages to a uEntity. 48 | /// 49 | /// Please refer to the 50 | /// [Communication Layer API Specifications](https://github.com/eclipse-uprotocol/up-spec/blob/main/up-l2/api.adoc). 51 | // [impl->req~up-language-comm-api~1] 52 | #[cfg_attr(any(test, feature = "test-util"), mockall::automock)] 53 | #[async_trait] 54 | pub trait Notifier: Send + Sync { 55 | /// Sends a notification to a uEntity. 56 | /// 57 | /// # Arguments 58 | /// 59 | /// * `resource_id` - The (local) resource identifier representing the origin of the notification. 60 | /// * `destination` - A URI representing the uEntity that the notification should be sent to. 61 | /// * `call_options` - Options to include in the notification message. 62 | /// * `payload` - The payload to include in the notification message. 63 | /// 64 | /// # Errors 65 | /// 66 | /// Returns an error if the given message is not a valid 67 | /// [uProtocol Notification message](`crate::NotificationValidator`). 68 | async fn notify( 69 | &self, 70 | resource_id: u16, 71 | destination: &UUri, 72 | call_options: CallOptions, 73 | payload: Option, 74 | ) -> Result<(), NotificationError>; 75 | 76 | /// Starts listening to a notification topic. 77 | /// 78 | /// More than one handler can be registered for the same topic. 79 | /// The same handler can be registered for multiple topics. 80 | /// 81 | /// # Arguments 82 | /// 83 | /// * `topic` - The topic to listen to. The topic must not contain any wildcards. 84 | /// * `listener` - The handler to invoke for each notification that has been sent on the topic. 85 | /// 86 | /// # Errors 87 | /// 88 | /// Returns an error if the listener cannot be registered. 89 | async fn start_listening( 90 | &self, 91 | topic: &UUri, 92 | listener: Arc, 93 | ) -> Result<(), RegistrationError>; 94 | 95 | /// Deregisters a previously [registered handler](`Self::start_listening`) for listening to notifications. 96 | /// 97 | /// # Arguments 98 | /// 99 | /// * `topic` - The topic that the handler had been registered for. 100 | /// * `listener` - The handler to unregister. 101 | /// 102 | /// # Errors 103 | /// 104 | /// Returns an error if the listener cannot be unregistered. 105 | async fn stop_listening( 106 | &self, 107 | topic: &UUri, 108 | listener: Arc, 109 | ) -> Result<(), RegistrationError>; 110 | } 111 | -------------------------------------------------------------------------------- /src/communication/pubsub.rs: -------------------------------------------------------------------------------- 1 | /******************************************************************************** 2 | * Copyright (c) 2024 Contributors to the Eclipse Foundation 3 | * 4 | * See the NOTICE file(s) distributed with this work for additional 5 | * information regarding copyright ownership. 6 | * 7 | * This program and the accompanying materials are made available under the 8 | * terms of the Apache License Version 2.0 which is available at 9 | * https://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * SPDX-License-Identifier: Apache-2.0 12 | ********************************************************************************/ 13 | 14 | use std::{error::Error, fmt::Display, sync::Arc}; 15 | 16 | use async_trait::async_trait; 17 | 18 | use crate::communication::RegistrationError; 19 | use crate::core::usubscription::SubscriptionStatus; 20 | use crate::{UListener, UStatus, UUri}; 21 | 22 | use super::{CallOptions, UPayload}; 23 | 24 | /// An error indicating a problem with publishing a message to a topic. 25 | // [impl->req~up-language-comm-api~1] 26 | #[derive(Debug)] 27 | pub enum PubSubError { 28 | /// Indicates that the given message cannot be sent because it is not a [valid Publish message](crate::PublishValidator). 29 | InvalidArgument(String), 30 | /// Indicates an unspecific error that occurred at the Transport Layer while trying to publish a message. 31 | PublishError(UStatus), 32 | } 33 | 34 | #[cfg(not(tarpaulin_include))] 35 | impl Display for PubSubError { 36 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 37 | match self { 38 | PubSubError::InvalidArgument(s) => f.write_str(s.as_str()), 39 | PubSubError::PublishError(s) => { 40 | f.write_fmt(format_args!("failed to publish message: {}", s)) 41 | } 42 | } 43 | } 44 | } 45 | 46 | impl Error for PubSubError {} 47 | 48 | /// A client for publishing messages to a topic. 49 | /// 50 | /// Please refer to the 51 | /// [Communication Layer API Specifications](https://github.com/eclipse-uprotocol/up-spec/blob/main/up-l2/api.adoc). 52 | // [impl->req~up-language-comm-api~1] 53 | #[async_trait] 54 | pub trait Publisher: Send + Sync { 55 | /// Publishes a message to a topic. 56 | /// 57 | /// # Arguments 58 | /// 59 | /// * `resource_id` - The (local) resource ID of the topic to publish to. 60 | /// * `call_options` - Options to include in the published message. 61 | /// * `payload` - Payload to include in the published message. 62 | /// 63 | /// # Errors 64 | /// 65 | /// Returns an error if the message could not be published. 66 | async fn publish( 67 | &self, 68 | resource_id: u16, 69 | call_options: CallOptions, 70 | payload: Option, 71 | ) -> Result<(), PubSubError>; 72 | } 73 | 74 | // [impl->req~up-language-comm-api~1] 75 | #[cfg_attr(any(test, feature = "test-util"), mockall::automock)] 76 | pub trait SubscriptionChangeHandler: Send + Sync { 77 | /// Invoked for each update to the subscription status for a given topic. 78 | /// 79 | /// Implementations must not block the current thread. 80 | /// 81 | /// # Arguments 82 | /// 83 | /// * `topic` - The topic for which the subscription status has changed. 84 | /// * `status` - The new status of the subscription. 85 | fn on_subscription_change(&self, topic: UUri, new_status: SubscriptionStatus); 86 | } 87 | 88 | /// A client for subscribing to topics. 89 | /// 90 | /// Please refer to the 91 | /// [Communication Layer API Specifications](https://github.com/eclipse-uprotocol/up-spec/blob/main/up-l2/api.adoc). 92 | // [impl->req~up-language-comm-api~1] 93 | #[async_trait] 94 | pub trait Subscriber: Send + Sync { 95 | /// Registers a handler to invoke for messages that have been published to a given topic. 96 | /// 97 | /// More than one handler can be registered for the same topic. 98 | /// The same handler can be registered for multiple topics. 99 | /// 100 | /// # Arguments 101 | /// 102 | /// * `topic` - The topic to subscribe to. The topic must not contain any wildcards. 103 | /// * `handler` - The handler to invoke for each message that has been published to the topic. 104 | /// * `subscription_change_handler` - A handler to invoke for any subscription state changes for 105 | /// the given topic. 106 | /// 107 | /// # Errors 108 | /// 109 | /// Returns an error if the listener cannot be registered. 110 | async fn subscribe( 111 | &self, 112 | topic: &UUri, 113 | handler: Arc, 114 | subscription_change_handler: Option>, 115 | ) -> Result<(), RegistrationError>; 116 | 117 | /// Deregisters a previously [registered handler](`Self::subscribe`). 118 | /// 119 | /// # Arguments 120 | /// 121 | /// * `topic` - The topic that the handler had been registered for. 122 | /// * `handler` - The handler to unregister. 123 | /// 124 | /// # Errors 125 | /// 126 | /// Returns an error if the listener cannot be unregistered. 127 | async fn unsubscribe( 128 | &self, 129 | topic: &UUri, 130 | handler: Arc, 131 | ) -> Result<(), RegistrationError>; 132 | } 133 | -------------------------------------------------------------------------------- /src/communication/rpc.rs: -------------------------------------------------------------------------------- 1 | /******************************************************************************** 2 | * Copyright (c) 2024 Contributors to the Eclipse Foundation 3 | * 4 | * See the NOTICE file(s) distributed with this work for additional 5 | * information regarding copyright ownership. 6 | * 7 | * This program and the accompanying materials are made available under the 8 | * terms of the Apache License Version 2.0 which is available at 9 | * https://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * SPDX-License-Identifier: Apache-2.0 12 | ********************************************************************************/ 13 | 14 | use std::sync::Arc; 15 | use thiserror::Error; 16 | 17 | use async_trait::async_trait; 18 | use protobuf::MessageFull; 19 | 20 | use crate::communication::RegistrationError; 21 | use crate::{UAttributes, UCode, UStatus, UUri}; 22 | 23 | use super::{CallOptions, UPayload}; 24 | 25 | /// An error indicating a problem with invoking a (remote) service operation. 26 | // [impl->req~up-language-comm-api~1] 27 | #[derive(Clone, Error, Debug)] 28 | pub enum ServiceInvocationError { 29 | /// Indicates that the calling uE requested to add/create something that already exists. 30 | #[error("entity already exists: {0}")] 31 | AlreadyExists(String), 32 | /// Indicates that a request's time-to-live (TTL) has expired. 33 | /// 34 | /// Note that this only means that the reply to the request has not been received in time. The request 35 | /// may still have been processed by the (remote) service provider. 36 | #[error("request timed out")] 37 | DeadlineExceeded, 38 | /// Indicates that the service provider is in a state that prevents it from handling the request. 39 | #[error("failed precondition: {0}")] 40 | FailedPrecondition(String), 41 | /// Indicates that a serious but unspecified internal error has occurred while sending/processing the request. 42 | #[error("internal error: {0}")] 43 | Internal(String), 44 | /// Indicates that the request cannot be processed because some of its parameters are invalid, e.g. not properly formatted. 45 | #[error("invalid argument: {0}")] 46 | InvalidArgument(String), 47 | /// Indicates that the requested entity was not found. 48 | #[error("no such entity: {0}")] 49 | NotFound(String), 50 | /// Indicates that the calling uE is authenticated but does not have the required authority to invoke the method. 51 | #[error("permission denied: {0}")] 52 | PermissionDenied(String), 53 | /// Indicates that some of the resources required for processing the request have been exhausted, e.g. disk space, number of API calls. 54 | #[error("resource exhausted: {0}")] 55 | ResourceExhausted(String), 56 | /// Indicates an unspecific error that occurred at the Transport Layer while trying to publish a message. 57 | #[error("unknown error: {0}")] 58 | RpcError(UStatus), 59 | /// Indicates that the calling uE could not be authenticated properly. 60 | #[error("unauthenticated")] 61 | Unauthenticated, 62 | /// Indicates that some of the resources required for processing the request are currently unavailable. 63 | #[error("resource unavailable: {0}")] 64 | Unavailable(String), 65 | /// Indicates that part or all of the invoked operation has not been implemented yet. 66 | #[error("unimplemented: {0}")] 67 | Unimplemented(String), 68 | } 69 | 70 | impl From for ServiceInvocationError { 71 | fn from(value: UStatus) -> Self { 72 | match value.code.enum_value() { 73 | Ok(UCode::ALREADY_EXISTS) => ServiceInvocationError::AlreadyExists(value.get_message()), 74 | Ok(UCode::DEADLINE_EXCEEDED) => ServiceInvocationError::DeadlineExceeded, 75 | Ok(UCode::FAILED_PRECONDITION) => { 76 | ServiceInvocationError::FailedPrecondition(value.get_message()) 77 | } 78 | Ok(UCode::INTERNAL) => ServiceInvocationError::Internal(value.get_message()), 79 | Ok(UCode::INVALID_ARGUMENT) => { 80 | ServiceInvocationError::InvalidArgument(value.get_message()) 81 | } 82 | Ok(UCode::NOT_FOUND) => ServiceInvocationError::NotFound(value.get_message()), 83 | Ok(UCode::PERMISSION_DENIED) => { 84 | ServiceInvocationError::PermissionDenied(value.get_message()) 85 | } 86 | Ok(UCode::RESOURCE_EXHAUSTED) => { 87 | ServiceInvocationError::ResourceExhausted(value.get_message()) 88 | } 89 | Ok(UCode::UNAUTHENTICATED) => ServiceInvocationError::Unauthenticated, 90 | Ok(UCode::UNAVAILABLE) => ServiceInvocationError::Unavailable(value.get_message()), 91 | Ok(UCode::UNIMPLEMENTED) => ServiceInvocationError::Unimplemented(value.get_message()), 92 | _ => ServiceInvocationError::RpcError(value), 93 | } 94 | } 95 | } 96 | 97 | impl From for UStatus { 98 | fn from(value: ServiceInvocationError) -> Self { 99 | match value { 100 | ServiceInvocationError::AlreadyExists(msg) => { 101 | UStatus::fail_with_code(UCode::ALREADY_EXISTS, msg) 102 | } 103 | ServiceInvocationError::DeadlineExceeded => { 104 | UStatus::fail_with_code(UCode::DEADLINE_EXCEEDED, "request timed out") 105 | } 106 | ServiceInvocationError::FailedPrecondition(msg) => { 107 | UStatus::fail_with_code(UCode::FAILED_PRECONDITION, msg) 108 | } 109 | ServiceInvocationError::Internal(msg) => UStatus::fail_with_code(UCode::INTERNAL, msg), 110 | ServiceInvocationError::InvalidArgument(msg) => { 111 | UStatus::fail_with_code(UCode::INVALID_ARGUMENT, msg) 112 | } 113 | ServiceInvocationError::NotFound(msg) => UStatus::fail_with_code(UCode::NOT_FOUND, msg), 114 | ServiceInvocationError::PermissionDenied(msg) => { 115 | UStatus::fail_with_code(UCode::PERMISSION_DENIED, msg) 116 | } 117 | ServiceInvocationError::ResourceExhausted(msg) => { 118 | UStatus::fail_with_code(UCode::RESOURCE_EXHAUSTED, msg) 119 | } 120 | ServiceInvocationError::Unauthenticated => { 121 | UStatus::fail_with_code(UCode::UNAUTHENTICATED, "client must authenticate") 122 | } 123 | ServiceInvocationError::Unavailable(msg) => { 124 | UStatus::fail_with_code(UCode::UNAVAILABLE, msg) 125 | } 126 | ServiceInvocationError::Unimplemented(msg) => { 127 | UStatus::fail_with_code(UCode::UNIMPLEMENTED, msg) 128 | } 129 | _ => UStatus::fail_with_code(UCode::UNKNOWN, "unknown"), 130 | } 131 | } 132 | } 133 | 134 | /// A client for performing Remote Procedure Calls (RPC) on (other) uEntities. 135 | /// 136 | /// Please refer to the 137 | /// [Communication Layer API specification](https://github.com/eclipse-uprotocol/up-spec/blob/main/up-l2/api.adoc) 138 | /// for details. 139 | // [impl->req~up-language-comm-api~1] 140 | #[cfg_attr(any(test, feature = "test-util"), mockall::automock)] 141 | #[async_trait] 142 | pub trait RpcClient: Send + Sync { 143 | /// Invokes a method on a service. 144 | /// 145 | /// # Arguments 146 | /// 147 | /// * `method` - The URI representing the method to invoke. 148 | /// * `call_options` - Options to include in the request message. 149 | /// * `payload` - The (optional) payload to include in the request message. 150 | /// 151 | /// # Returns 152 | /// 153 | /// The payload returned by the service operation. 154 | /// 155 | /// # Errors 156 | /// 157 | /// Returns an error if invocation fails or the given arguments cannot be turned into a valid RPC Request message. 158 | async fn invoke_method( 159 | &self, 160 | method: UUri, 161 | call_options: CallOptions, 162 | payload: Option, 163 | ) -> Result, ServiceInvocationError>; 164 | } 165 | 166 | impl dyn RpcClient { 167 | /// Invokes a method on a service using and returning proto-generated `Message` objects. 168 | /// 169 | /// # Arguments 170 | /// 171 | /// * `method` - The URI representing the method to invoke. 172 | /// * `call_options` - Options to include in the request message. 173 | /// * `request_message` - The protobuf `Message` to include in the request message. 174 | /// 175 | /// # Returns 176 | /// 177 | /// The payload returned by the service operation as a protobuf `Message`. 178 | /// 179 | /// # Errors 180 | /// 181 | /// Returns an error if invocation fails, the given arguments cannot be turned into a valid RPC Request message, 182 | /// result protobuf deserialization fails, or result payload is empty. 183 | pub async fn invoke_proto_method( 184 | &self, 185 | method: UUri, 186 | call_options: CallOptions, 187 | request_message: T, 188 | ) -> Result 189 | where 190 | T: MessageFull, 191 | R: MessageFull, 192 | { 193 | let payload = UPayload::try_from_protobuf(request_message) 194 | .map_err(|e| ServiceInvocationError::InvalidArgument(e.to_string()))?; 195 | 196 | let result = self 197 | .invoke_method(method, call_options, Some(payload)) 198 | .await?; 199 | 200 | if let Some(result) = result { 201 | UPayload::extract_protobuf::(&result) 202 | .map_err(|e| ServiceInvocationError::InvalidArgument(e.to_string())) 203 | } else { 204 | Err(ServiceInvocationError::InvalidArgument( 205 | "No payload".to_string(), 206 | )) 207 | } 208 | } 209 | } 210 | 211 | /// A handler for processing incoming RPC requests. 212 | /// 213 | // [impl->req~up-language-comm-api~1] 214 | #[cfg_attr(any(test, feature = "test-util"), mockall::automock)] 215 | #[async_trait] 216 | pub trait RequestHandler: Send + Sync { 217 | /// Handles a request to invoke a method with given input parameters. 218 | /// 219 | /// Implementations MUST NOT block the calling thread. Long running 220 | /// computations should be performed on a separate worker thread, yielding 221 | /// on the calling thread. 222 | /// 223 | /// # Arguments 224 | /// 225 | /// * `resource_id` - The resource identifier of the method to invoke. 226 | /// * `message_attributes` - Any metadata that is associated with the request message. 227 | /// * `request_payload` - The raw payload that contains the input data for the method. 228 | /// 229 | /// # Returns 230 | /// 231 | /// the output data generated by the method. 232 | /// 233 | /// # Errors 234 | /// 235 | /// Returns an error if the request could not be processed successfully. 236 | async fn handle_request( 237 | &self, 238 | resource_id: u16, 239 | message_attributes: &UAttributes, 240 | request_payload: Option, 241 | ) -> Result, ServiceInvocationError>; 242 | } 243 | 244 | /// A server for exposing Remote Procedure Call (RPC) endpoints. 245 | /// 246 | /// Please refer to the 247 | /// [Communication Layer API specification](https://github.com/eclipse-uprotocol/up-spec/blob/main/up-l2/api.adoc) 248 | /// for details. 249 | // [impl->req~up-language-comm-api~1] 250 | #[async_trait] 251 | pub trait RpcServer { 252 | /// Registers an endpoint for RPC requests. 253 | /// 254 | /// Note that only a single endpoint can be registered for a given resource ID. 255 | /// However, the same request handler can be registered for multiple endpoints. 256 | /// 257 | /// # Arguments 258 | /// 259 | /// * `origin_filter` - A pattern defining origin addresses to accept requests from. If `None`, requests 260 | /// will be accepted from all sources. 261 | /// * `resource_id` - The resource identifier of the (local) method to accept requests for. 262 | /// * `request_handler` - The handler to invoke for each incoming request. 263 | /// 264 | /// # Errors 265 | /// 266 | /// Returns an error if the listener cannot be registered or if a listener has already been registered 267 | /// for the given resource ID. 268 | async fn register_endpoint( 269 | &self, 270 | origin_filter: Option<&UUri>, 271 | resource_id: u16, 272 | request_handler: Arc, 273 | ) -> Result<(), RegistrationError>; 274 | 275 | /// Deregisters a previously [registered endpoint](Self::register_endpoint). 276 | /// 277 | /// # Arguments 278 | /// 279 | /// * `origin_filter` - The origin pattern that the endpoint had been registered for. 280 | /// * `resource_id` - The (local) resource identifier that the endpoint had been registered for. 281 | /// * `request_handler` - The handler to unregister. 282 | /// 283 | /// # Errors 284 | /// 285 | /// Returns an error if the listener cannot be unregistered. 286 | async fn unregister_endpoint( 287 | &self, 288 | origin_filter: Option<&UUri>, 289 | resource_id: u16, 290 | request_handler: Arc, 291 | ) -> Result<(), RegistrationError>; 292 | } 293 | 294 | #[cfg(not(tarpaulin_include))] 295 | #[cfg(any(test, feature = "test-util"))] 296 | mockall::mock! { 297 | /// This extra struct is necessary in order to comply with mockall's requirements regarding the parameter lifetimes 298 | /// see 299 | pub RpcServerImpl { 300 | pub async fn do_register_endpoint<'a>(&'a self, origin_filter: Option<&'a UUri>, resource_id: u16, request_handler: Arc) -> Result<(), RegistrationError>; 301 | pub async fn do_unregister_endpoint<'a>(&'a self, origin_filter: Option<&'a UUri>, resource_id: u16, request_handler: Arc) -> Result<(), RegistrationError>; 302 | } 303 | } 304 | 305 | #[cfg(not(tarpaulin_include))] 306 | #[cfg(any(test, feature = "test-util"))] 307 | #[async_trait] 308 | /// This delegates the invocation of the UTransport functions to the mocked functions of the Transport struct. 309 | /// see 310 | impl RpcServer for MockRpcServerImpl { 311 | async fn register_endpoint( 312 | &self, 313 | origin_filter: Option<&UUri>, 314 | resource_id: u16, 315 | request_handler: Arc, 316 | ) -> Result<(), RegistrationError> { 317 | self.do_register_endpoint(origin_filter, resource_id, request_handler) 318 | .await 319 | } 320 | async fn unregister_endpoint( 321 | &self, 322 | origin_filter: Option<&UUri>, 323 | resource_id: u16, 324 | request_handler: Arc, 325 | ) -> Result<(), RegistrationError> { 326 | self.do_unregister_endpoint(origin_filter, resource_id, request_handler) 327 | .await 328 | } 329 | } 330 | 331 | #[cfg(test)] 332 | mod tests { 333 | use std::sync::Arc; 334 | 335 | use protobuf::well_known_types::wrappers::StringValue; 336 | 337 | use crate::{communication::CallOptions, UUri}; 338 | 339 | use super::*; 340 | 341 | #[tokio::test] 342 | async fn test_invoke_proto_method_fails_for_unexpected_return_type() { 343 | let mut rpc_client = MockRpcClient::new(); 344 | rpc_client 345 | .expect_invoke_method() 346 | .once() 347 | .returning(|_method, _options, _payload| { 348 | let error = UStatus::fail_with_code(UCode::INTERNAL, "internal error"); 349 | let response_payload = UPayload::try_from_protobuf(error).unwrap(); 350 | Ok(Some(response_payload)) 351 | }); 352 | let client: Arc = Arc::new(rpc_client); 353 | let mut request = StringValue::new(); 354 | request.value = "hello".to_string(); 355 | let result = client 356 | .invoke_proto_method::( 357 | UUri::try_from_parts("", 0x1000, 0x01, 0x0001).unwrap(), 358 | CallOptions::for_rpc_request(5_000, None, None, None), 359 | request, 360 | ) 361 | .await; 362 | assert!(result.is_err_and(|e| matches!(e, ServiceInvocationError::InvalidArgument(_)))); 363 | } 364 | 365 | #[tokio::test] 366 | async fn test_invoke_proto_method_fails_for_missing_response_payload() { 367 | let mut rpc_client = MockRpcClient::new(); 368 | rpc_client 369 | .expect_invoke_method() 370 | .once() 371 | .return_const(Ok(None)); 372 | let client: Arc = Arc::new(rpc_client); 373 | let mut request = StringValue::new(); 374 | request.value = "hello".to_string(); 375 | let result = client 376 | .invoke_proto_method::( 377 | UUri::try_from_parts("", 0x1000, 0x01, 0x0001).unwrap(), 378 | CallOptions::for_rpc_request(5_000, None, None, None), 379 | request, 380 | ) 381 | .await; 382 | assert!(result.is_err_and(|e| matches!(e, ServiceInvocationError::InvalidArgument(_)))); 383 | } 384 | } 385 | -------------------------------------------------------------------------------- /src/communication/udiscovery_client.rs: -------------------------------------------------------------------------------- 1 | /******************************************************************************** 2 | * Copyright (c) 2024 Contributors to the Eclipse Foundation 3 | * 4 | * See the NOTICE file(s) distributed with this work for additional 5 | * information regarding copyright ownership. 6 | * 7 | * This program and the accompanying materials are made available under the 8 | * terms of the Apache License Version 2.0 which is available at 9 | * https://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * SPDX-License-Identifier: Apache-2.0 12 | ********************************************************************************/ 13 | use std::sync::Arc; 14 | 15 | use async_trait::async_trait; 16 | 17 | use crate::{ 18 | core::udiscovery::{ 19 | udiscovery_uri, FindServicesRequest, FindServicesResponse, GetServiceTopicsRequest, 20 | GetServiceTopicsResponse, ServiceTopicInfo, UDiscovery, RESOURCE_ID_FIND_SERVICES, 21 | RESOURCE_ID_GET_SERVICE_TOPICS, 22 | }, 23 | UStatus, UUri, 24 | }; 25 | 26 | use super::{CallOptions, RpcClient}; 27 | 28 | /// A [`UDiscovery`] client implementation for invoking operations of a local uDiscovery service. 29 | /// 30 | /// The client requires an [`RpcClient`] for performing the remote procedure calls. 31 | pub struct RpcClientUDiscovery { 32 | rpc_client: Arc, 33 | } 34 | 35 | impl RpcClientUDiscovery { 36 | /// Creates a new uDiscovery client for a given transport. 37 | /// 38 | /// # Arguments 39 | /// 40 | /// * `rpc_client` - The client to use for performing the remote procedure calls on the service. 41 | pub fn new(rpc_client: Arc) -> Self { 42 | RpcClientUDiscovery { rpc_client } 43 | } 44 | 45 | fn default_call_options() -> CallOptions { 46 | CallOptions::for_rpc_request(5_000, None, None, None) 47 | } 48 | } 49 | 50 | #[async_trait] 51 | impl UDiscovery for RpcClientUDiscovery { 52 | async fn find_services( 53 | &self, 54 | uri_pattern: UUri, 55 | recursive: bool, 56 | ) -> Result, UStatus> { 57 | let request_message = FindServicesRequest { 58 | uri: Some(uri_pattern).into(), 59 | recursive, 60 | ..Default::default() 61 | }; 62 | self.rpc_client 63 | .invoke_proto_method::<_, FindServicesResponse>( 64 | udiscovery_uri(RESOURCE_ID_FIND_SERVICES), 65 | Self::default_call_options(), 66 | request_message, 67 | ) 68 | .await 69 | .map(|response_message| { 70 | response_message 71 | .uris 72 | .as_ref() 73 | .map_or(vec![], |batch| batch.uris.to_owned()) 74 | }) 75 | .map_err(UStatus::from) 76 | } 77 | 78 | async fn get_service_topics( 79 | &self, 80 | topic_pattern: UUri, 81 | recursive: bool, 82 | ) -> Result, UStatus> { 83 | let request_message = GetServiceTopicsRequest { 84 | topic: Some(topic_pattern).into(), 85 | recursive, 86 | ..Default::default() 87 | }; 88 | self.rpc_client 89 | .invoke_proto_method::<_, GetServiceTopicsResponse>( 90 | udiscovery_uri(RESOURCE_ID_GET_SERVICE_TOPICS), 91 | Self::default_call_options(), 92 | request_message, 93 | ) 94 | .await 95 | .map(|response_message| response_message.topics.to_owned()) 96 | .map_err(UStatus::from) 97 | } 98 | } 99 | 100 | #[cfg(test)] 101 | mod tests { 102 | use mockall::Sequence; 103 | 104 | use super::*; 105 | use crate::{ 106 | communication::{rpc::MockRpcClient, UPayload}, 107 | up_core_api::uri::UUriBatch, 108 | UCode, UUri, 109 | }; 110 | use std::sync::Arc; 111 | 112 | #[tokio::test] 113 | async fn test_find_services_invokes_rpc_client() { 114 | let service_pattern_uri = UUri::try_from_parts("other", 0xFFFF_D5A3, 0x01, 0xFFFF).unwrap(); 115 | let request = FindServicesRequest { 116 | uri: Some(service_pattern_uri.clone()).into(), 117 | ..Default::default() 118 | }; 119 | let expected_request = request.clone(); 120 | let mut rpc_client = MockRpcClient::new(); 121 | let mut seq = Sequence::new(); 122 | rpc_client 123 | .expect_invoke_method() 124 | .once() 125 | .in_sequence(&mut seq) 126 | .withf(|method, _options, payload| { 127 | method == &udiscovery_uri(RESOURCE_ID_FIND_SERVICES) && payload.is_some() 128 | }) 129 | .return_const(Err(crate::communication::ServiceInvocationError::Internal( 130 | "internal error".to_string(), 131 | ))); 132 | rpc_client 133 | .expect_invoke_method() 134 | .once() 135 | .in_sequence(&mut seq) 136 | .withf(move |method, _options, payload| { 137 | let request = payload 138 | .to_owned() 139 | .unwrap() 140 | .extract_protobuf::() 141 | .unwrap(); 142 | request == expected_request && method == &udiscovery_uri(RESOURCE_ID_FIND_SERVICES) 143 | }) 144 | .returning(move |_method, _options, _payload| { 145 | let response = FindServicesResponse { 146 | uris: Some(UUriBatch { 147 | uris: vec![UUri::try_from_parts("other", 0x0004_D5A3, 0x01, 0xD3FE) 148 | .expect("failed to create query result")], 149 | ..Default::default() 150 | }) 151 | .into(), 152 | ..Default::default() 153 | }; 154 | Ok(Some(UPayload::try_from_protobuf(response).unwrap())) 155 | }); 156 | 157 | let udiscovery_client = RpcClientUDiscovery::new(Arc::new(rpc_client)); 158 | 159 | assert!(udiscovery_client 160 | .find_services(service_pattern_uri.clone(), false) 161 | .await 162 | .is_err_and(|e| e.get_code() == UCode::INTERNAL)); 163 | assert!(udiscovery_client 164 | .find_services(service_pattern_uri.clone(), false) 165 | .await 166 | .is_ok_and(|result| result.len() == 1 && service_pattern_uri.matches(&result[0]))); 167 | } 168 | 169 | #[tokio::test] 170 | async fn test_get_service_topics_invokes_rpc_client() { 171 | let topic_pattern_uri = UUri::try_from_parts("*", 0xFFFF_D5A3, 0x01, 0xFFFF).unwrap(); 172 | let request = GetServiceTopicsRequest { 173 | topic: Some(topic_pattern_uri.clone()).into(), 174 | ..Default::default() 175 | }; 176 | let expected_request = request.clone(); 177 | let mut rpc_client = MockRpcClient::new(); 178 | let mut seq = Sequence::new(); 179 | rpc_client 180 | .expect_invoke_method() 181 | .once() 182 | .in_sequence(&mut seq) 183 | .withf(|method, _options, payload| { 184 | method == &udiscovery_uri(RESOURCE_ID_GET_SERVICE_TOPICS) && payload.is_some() 185 | }) 186 | .return_const(Err(crate::communication::ServiceInvocationError::Internal( 187 | "internal error".to_string(), 188 | ))); 189 | rpc_client 190 | .expect_invoke_method() 191 | .once() 192 | .in_sequence(&mut seq) 193 | .withf(move |method, _options, payload| { 194 | let request = payload 195 | .to_owned() 196 | .unwrap() 197 | .extract_protobuf::() 198 | .unwrap(); 199 | request == expected_request 200 | && method == &udiscovery_uri(RESOURCE_ID_GET_SERVICE_TOPICS) 201 | }) 202 | .returning(move |_method, _options, _payload| { 203 | let topic_info = ServiceTopicInfo { 204 | topic: Some( 205 | UUri::try_from_parts("other", 0x0004_D5A3, 0x01, 0xD3FE) 206 | .expect("failed to create query result"), 207 | ) 208 | .into(), 209 | ttl: 600, 210 | info: None.into(), 211 | ..Default::default() 212 | }; 213 | let response = GetServiceTopicsResponse { 214 | topics: vec![topic_info], 215 | ..Default::default() 216 | }; 217 | Ok(Some(UPayload::try_from_protobuf(response).unwrap())) 218 | }); 219 | 220 | let udiscovery_client = RpcClientUDiscovery::new(Arc::new(rpc_client)); 221 | 222 | assert!(udiscovery_client 223 | .get_service_topics(topic_pattern_uri.clone(), false) 224 | .await 225 | .is_err_and(|e| e.get_code() == UCode::INTERNAL)); 226 | assert!(udiscovery_client 227 | .get_service_topics(topic_pattern_uri.clone(), false) 228 | .await 229 | .is_ok_and(|result| result.len() == 1 230 | && topic_pattern_uri.matches(result[0].topic.as_ref().unwrap()))); 231 | } 232 | } 233 | -------------------------------------------------------------------------------- /src/core.rs: -------------------------------------------------------------------------------- 1 | /******************************************************************************** 2 | * Copyright (c) 2024 Contributors to the Eclipse Foundation 3 | * 4 | * See the NOTICE file(s) distributed with this work for additional 5 | * information regarding copyright ownership. 6 | * 7 | * This program and the accompanying materials are made available under the 8 | * terms of the Apache License Version 2.0 which is available at 9 | * https://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * SPDX-License-Identifier: Apache-2.0 12 | ********************************************************************************/ 13 | 14 | #[cfg(feature = "usubscription")] 15 | pub mod usubscription; 16 | 17 | // Types not used by up_rust, but re-exported to up_rust users, keeping them in their respective submodules 18 | #[cfg(feature = "udiscovery")] 19 | pub mod udiscovery; 20 | #[cfg(feature = "utwin")] 21 | pub mod utwin; 22 | -------------------------------------------------------------------------------- /src/core/udiscovery.rs: -------------------------------------------------------------------------------- 1 | /******************************************************************************** 2 | * Copyright (c) 2024 Contributors to the Eclipse Foundation 3 | * 4 | * See the NOTICE file(s) distributed with this work for additional 5 | * information regarding copyright ownership. 6 | * 7 | * This program and the accompanying materials are made available under the 8 | * terms of the Apache License Version 2.0 which is available at 9 | * https://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * SPDX-License-Identifier: Apache-2.0 12 | ********************************************************************************/ 13 | 14 | use async_trait::async_trait; 15 | #[cfg(test)] 16 | use mockall::automock; 17 | 18 | pub use crate::up_core_api::udiscovery::{ 19 | FindServicesRequest, FindServicesResponse, GetServiceTopicsRequest, GetServiceTopicsResponse, 20 | ServiceTopicInfo, 21 | }; 22 | use crate::{UStatus, UUri}; 23 | 24 | /// The uEntity (type) identifier of the uDiscovery service. 25 | pub const UDISCOVERY_TYPE_ID: u32 = 0x0000_0001; 26 | /// The (latest) major version of the uDiscovery service. 27 | pub const UDISCOVERY_VERSION_MAJOR: u8 = 0x03; 28 | /// The resource identifier of uDiscovery's _find services_ operation. 29 | pub const RESOURCE_ID_FIND_SERVICES: u16 = 0x0001; 30 | /// The resource identifier of uDiscovery's _get service topics_ operation. 31 | pub const RESOURCE_ID_GET_SERVICE_TOPICS: u16 = 0x0002; 32 | 33 | /// Gets a UUri referring to one of the local uDiscovery service's resources. 34 | /// 35 | /// # Examples 36 | /// 37 | /// ```rust 38 | /// use up_rust::core::udiscovery; 39 | /// 40 | /// let uuri = udiscovery::udiscovery_uri(udiscovery::RESOURCE_ID_FIND_SERVICES); 41 | /// assert_eq!(uuri.resource_id, 0x0001); 42 | /// ``` 43 | pub fn udiscovery_uri(resource_id: u16) -> UUri { 44 | UUri::try_from_parts( 45 | "", 46 | UDISCOVERY_TYPE_ID, 47 | UDISCOVERY_VERSION_MAJOR, 48 | resource_id, 49 | ) 50 | .unwrap() 51 | } 52 | 53 | /// The uProtocol Application Layer client interface to the uDiscovery service. 54 | /// 55 | /// Please refer to the [uDiscovery service specification](https://github.com/eclipse-uprotocol/up-spec/blob/v1.6.0-alpha.4/up-l3/udiscovery/v3/client.adoc) 56 | /// for details. 57 | #[cfg_attr(test, automock)] 58 | #[async_trait] 59 | pub trait UDiscovery: Send + Sync { 60 | /// Finds service instances based on search criteria. 61 | /// 62 | /// # Parameters 63 | /// 64 | /// * `uri_pattern` - The URI pattern to use for looking up service instances. 65 | /// * `recursive` - Flag indicating whether the service should extend the search to its parent uDiscovery node. 66 | /// 67 | /// # Returns 68 | /// 69 | /// The service instances matching the given search criteria. 70 | async fn find_services(&self, uri_pattern: UUri, recursive: bool) 71 | -> Result, UStatus>; 72 | 73 | /// Gets information about topic(s) that a service (instance) publishes messages to. 74 | /// 75 | /// # Parameters 76 | /// 77 | /// * `topic_pattern` - The URI pattern to use for looking up topic information. 78 | /// * `recursive` - Flag indicating whether the service should extend the search to its parent uDiscovery node. 79 | /// 80 | /// # Returns 81 | /// 82 | /// The topics. 83 | async fn get_service_topics( 84 | &self, 85 | topic_pattern: UUri, 86 | recursive: bool, 87 | ) -> Result, UStatus>; 88 | } 89 | -------------------------------------------------------------------------------- /src/core/usubscription.rs: -------------------------------------------------------------------------------- 1 | /******************************************************************************** 2 | * Copyright (c) 2024 Contributors to the Eclipse Foundation 3 | * 4 | * See the NOTICE file(s) distributed with this work for additional 5 | * information regarding copyright ownership. 6 | * 7 | * This program and the accompanying materials are made available under the 8 | * terms of the Apache License Version 2.0 which is available at 9 | * https://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * SPDX-License-Identifier: Apache-2.0 12 | ********************************************************************************/ 13 | 14 | use async_trait::async_trait; 15 | use core::hash::{Hash, Hasher}; 16 | #[cfg(test)] 17 | use mockall::automock; 18 | 19 | pub use crate::up_core_api::usubscription::{ 20 | fetch_subscriptions_request::Request, subscription_status::State, EventDeliveryConfig, 21 | FetchSubscribersRequest, FetchSubscribersResponse, FetchSubscriptionsRequest, 22 | FetchSubscriptionsResponse, NotificationsRequest, NotificationsResponse, SubscribeAttributes, 23 | SubscriberInfo, Subscription, SubscriptionRequest, SubscriptionResponse, SubscriptionStatus, 24 | UnsubscribeRequest, UnsubscribeResponse, Update, 25 | }; 26 | 27 | use crate::{UStatus, UUri}; 28 | 29 | impl Hash for SubscriberInfo { 30 | /// Creates a hash value based on the URI property. 31 | /// 32 | /// # Examples 33 | /// 34 | /// ```rust 35 | /// use std::hash::{DefaultHasher, Hash, Hasher}; 36 | /// use up_rust::UUri; 37 | /// use up_rust::core::usubscription::SubscriberInfo; 38 | /// 39 | /// let mut hasher = DefaultHasher::new(); 40 | /// let info = SubscriberInfo { 41 | /// uri: Some(UUri::try_from_parts("", 0x1000, 0x01, 0x9a00).unwrap()).into(), 42 | /// ..Default::default() 43 | /// }; 44 | /// 45 | /// info.hash(&mut hasher); 46 | /// let hash_one = hasher.finish(); 47 | /// 48 | /// let mut hasher = DefaultHasher::new(); 49 | /// let info = SubscriberInfo { 50 | /// uri: Some(UUri::try_from_parts("", 0x1000, 0x02, 0xf100).unwrap()).into(), 51 | /// ..Default::default() 52 | /// }; 53 | /// 54 | /// info.hash(&mut hasher); 55 | /// let hash_two = hasher.finish(); 56 | /// 57 | /// assert_ne!(hash_one, hash_two); 58 | /// ``` 59 | fn hash(&self, state: &mut H) { 60 | self.uri.hash(state); 61 | } 62 | } 63 | 64 | impl Eq for SubscriberInfo {} 65 | 66 | /// Checks if a given [`SubscriberInfo`] contains any information. 67 | /// 68 | /// # Returns 69 | /// 70 | /// `true` if the given instance is equal to [`SubscriberInfo::default`], `false` otherwise. 71 | /// 72 | /// # Examples 73 | /// 74 | /// ```rust 75 | /// use up_rust::UUri; 76 | /// use up_rust::core::usubscription::SubscriberInfo; 77 | /// 78 | /// let mut info = SubscriberInfo::default(); 79 | /// assert!(info.is_empty()); 80 | /// 81 | /// info.uri = Some(UUri::try_from_parts("", 0x1000, 0x01, 0x9a00).unwrap()).into(); 82 | /// assert!(!info.is_empty()); 83 | /// ``` 84 | impl SubscriberInfo { 85 | pub fn is_empty(&self) -> bool { 86 | self.eq(&SubscriberInfo::default()) 87 | } 88 | } 89 | 90 | impl SubscriptionResponse { 91 | /// Checks if this `SubscriptionResponse` is in a specific state (`usubscription::State``). 92 | /// 93 | /// Returns `true` if SubscriptionResponse contains a valid SubscriptionStatus, which has a 94 | /// state property that is equal to state passed as argument. 95 | /// 96 | /// # Examples 97 | /// 98 | /// ```rust 99 | /// use up_rust::core::usubscription::{SubscriptionResponse, SubscriptionStatus, State}; 100 | /// 101 | /// let subscription_response = SubscriptionResponse { 102 | /// status: Some(SubscriptionStatus { 103 | /// state: State::SUBSCRIBED.into(), 104 | /// ..Default::default() 105 | /// }).into(), 106 | /// ..Default::default() 107 | /// }; 108 | /// assert!(subscription_response.is_state(State::SUBSCRIBED)); 109 | /// ``` 110 | pub fn is_state(&self, state: State) -> bool { 111 | self.status 112 | .as_ref() 113 | .is_some_and(|ss| ss.state.enum_value().is_ok_and(|s| s.eq(&state))) 114 | } 115 | } 116 | 117 | /// The uEntity (type) identifier of the uSubscription service. 118 | pub const USUBSCRIPTION_TYPE_ID: u32 = 0x0000_0000; 119 | /// The (latest) major version of the uSubscription service. 120 | pub const USUBSCRIPTION_VERSION_MAJOR: u8 = 0x03; 121 | /// The resource identifier of uSubscription's _subscribe_ operation. 122 | pub const RESOURCE_ID_SUBSCRIBE: u16 = 0x0001; 123 | /// The resource identifier of uSubscription's _unsubscribe_ operation. 124 | pub const RESOURCE_ID_UNSUBSCRIBE: u16 = 0x0002; 125 | /// The resource identifier of uSubscription's _fetch subscriptions_ operation. 126 | pub const RESOURCE_ID_FETCH_SUBSCRIPTIONS: u16 = 0x0003; 127 | /// The resource identifier of uSubscription's _register for notifications_ operation. 128 | pub const RESOURCE_ID_REGISTER_FOR_NOTIFICATIONS: u16 = 0x0006; 129 | /// The resource identifier of uSubscription's _unregister for notifications_ operation. 130 | pub const RESOURCE_ID_UNREGISTER_FOR_NOTIFICATIONS: u16 = 0x0007; 131 | /// The resource identifier of uSubscription's _fetch subscribers_ operation. 132 | pub const RESOURCE_ID_FETCH_SUBSCRIBERS: u16 = 0x0008; 133 | 134 | /// The resource identifier of uSubscription's _subscription change_ topic. 135 | pub const RESOURCE_ID_SUBSCRIPTION_CHANGE: u16 = 0x8000; 136 | 137 | /// Gets a UUri referring to one of the local uSubscription service's resources. 138 | /// 139 | /// # Examples 140 | /// 141 | /// ```rust 142 | /// use up_rust::core::usubscription; 143 | /// 144 | /// let uuri = usubscription::usubscription_uri(usubscription::RESOURCE_ID_SUBSCRIBE); 145 | /// assert_eq!(uuri.resource_id, 0x0001); 146 | /// ``` 147 | pub fn usubscription_uri(resource_id: u16) -> UUri { 148 | UUri::try_from_parts( 149 | "", 150 | USUBSCRIPTION_TYPE_ID, 151 | USUBSCRIPTION_VERSION_MAJOR, 152 | resource_id, 153 | ) 154 | .unwrap() 155 | } 156 | 157 | /// The uProtocol Application Layer client interface to the uSubscription service. 158 | /// 159 | /// Please refer to the [uSubscription service specification](https://github.com/eclipse-uprotocol/up-spec/blob/main/up-l3/usubscription/v3/README.adoc) 160 | /// for details. 161 | #[cfg_attr(test, automock)] 162 | #[async_trait] 163 | pub trait USubscription: Send + Sync { 164 | /// Subscribe to a topic, using a [`SubscriptionRequest`] 165 | /// 166 | /// # Parameters 167 | /// 168 | /// * `subscription_request` - A request to subscribe 169 | /// 170 | /// # Returns 171 | /// 172 | /// * [`SubscriptionResponse`] detailing if subscription was successful with other metadata 173 | async fn subscribe( 174 | &self, 175 | subscription_request: SubscriptionRequest, 176 | ) -> Result; 177 | 178 | /// Unsubscribe to a topic, using an [`UnsubscribeRequest`] 179 | /// 180 | /// # Parameters 181 | /// 182 | /// * `unsubscribe_request` - A request to unsubscribe 183 | /// 184 | /// # Returns 185 | /// 186 | /// * [`UStatus`] detailing if unsubscription was successful and if not why not 187 | async fn unsubscribe(&self, unsubscribe_request: UnsubscribeRequest) -> Result<(), UStatus>; 188 | 189 | /// Fetch all subscriptions for a given topic or subscriber contained inside a [`FetchSubscriptionsRequest`] 190 | /// 191 | /// # Parameters 192 | /// 193 | /// * `fetch_subscriptions_request` - A request to fetch subscriptions given a topic or subscriber 194 | /// 195 | /// # Returns 196 | /// 197 | /// * [`FetchSubscriptionsResponse`] detailing the zero or more subscriptions' info 198 | async fn fetch_subscriptions( 199 | &self, 200 | fetch_subscriptions_request: FetchSubscriptionsRequest, 201 | ) -> Result; 202 | 203 | /// Register for notifications relevant to a given topic inside a [`NotificationsRequest`] 204 | /// changing in subscription status. 205 | /// 206 | /// # Parameters 207 | /// 208 | /// * `notifications_register_request` - A request to receive changes to subscription status 209 | /// 210 | /// # Returns 211 | /// 212 | /// * [`UStatus`] detailing if notification registration was successful and if not why not 213 | async fn register_for_notifications( 214 | &self, 215 | notifications_register_request: NotificationsRequest, 216 | ) -> Result<(), UStatus>; 217 | 218 | /// Unregister for notifications relevant to a given topic inside a [`NotificationsRequest`] 219 | /// changing in subscription status. 220 | /// 221 | /// # Parameters 222 | /// 223 | /// * `notifications_unregister_request` - A request to no longer receive changes to subscription status 224 | /// 225 | /// # Returns 226 | /// 227 | /// * [`UStatus`] detailing if notification unregistration was successful and if not why not 228 | async fn unregister_for_notifications( 229 | &self, 230 | notifications_unregister_request: NotificationsRequest, 231 | ) -> Result<(), UStatus>; 232 | 233 | /// Fetch a list of subscribers that are currently subscribed to a given topic in a [`FetchSubscribersRequest`] 234 | /// 235 | /// # Parameters 236 | /// 237 | /// * `fetch_subscribers_request` - Request containing topic for which we'd like all subscribers' info 238 | /// 239 | /// # Returns 240 | /// 241 | /// * [`FetchSubscriptionsResponse`] detailing subscriber info for the provided topic 242 | async fn fetch_subscribers( 243 | &self, 244 | fetch_subscribers_request: FetchSubscribersRequest, 245 | ) -> Result; 246 | } 247 | -------------------------------------------------------------------------------- /src/core/utwin.rs: -------------------------------------------------------------------------------- 1 | /******************************************************************************** 2 | * Copyright (c) 2024 Contributors to the Eclipse Foundation 3 | * 4 | * See the NOTICE file(s) distributed with this work for additional 5 | * information regarding copyright ownership. 6 | * 7 | * This program and the accompanying materials are made available under the 8 | * terms of the Apache License Version 2.0 which is available at 9 | * https://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * SPDX-License-Identifier: Apache-2.0 12 | ********************************************************************************/ 13 | 14 | pub use crate::up_core_api::utwin::{GetLastMessagesResponse, MessageResponse}; 15 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | /******************************************************************************** 2 | * Copyright (c) 2023 Contributors to the Eclipse Foundation 3 | * 4 | * See the NOTICE file(s) distributed with this work for additional 5 | * information regarding copyright ownership. 6 | * 7 | * This program and the accompanying materials are made available under the 8 | * terms of the Apache License Version 2.0 which is available at 9 | * https://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * SPDX-License-Identifier: Apache-2.0 12 | ********************************************************************************/ 13 | 14 | /*! 15 | up-rust is the [Eclipse uProtocol™ Language Library](https://github.com/eclipse-uprotocol/up-spec/blob/v1.6.0-alpha.4/languages.adoc) for the 16 | Rust programming language. 17 | 18 | This crate can be used to 19 | 20 | * implement uEntities that communicate with each other using the uProtocol [Communication Layer API](https://github.com/eclipse-uprotocol/up-spec/blob/v1.6.0-alpha.4/up-l2/api.adoc) 21 | over one of the supported transport protocols. 22 | * implement support for an additional transport protocol by means of implementing the [Transport Layer API](https://github.com/eclipse-uprotocol/up-spec/blob/v1.6.0-alpha.4/up-l1/README.adoc). 23 | 24 | ## Library contents 25 | 26 | * `communication` module, which defines uProtocol's Communication Layer API for publishing and subscribing to topics and invoking RPC methods. 27 | It also contains a default implementation employing the Transport Layer API. 28 | * `uattributes` module, with uProtocol message attribute types and validators 29 | * `umessage` module, which defines the uProtocol core message type and provides related convenience functionality 30 | * `upayload` module, which defines payload representation for uProtocol messages 31 | * `uri` module, providing convenience wrappers for creation and validation of uProtocol-style resource identifiers 32 | * `ustatus` module, which provides uProtocol types for representing status and status codes 33 | * `utransport` module, as an interface contract between uProtocol and specific transport protocol implementations 34 | * `uuid` module, which generates and validates UUIDs as per the uProtocol specification 35 | 36 | For user convenience, all of these modules export their types on up_rust top-level, except for (future) optional features. 37 | 38 | ## Features 39 | 40 | * `cloudevents` enables support for mapping UMessages to/from CloudEvents using Protobuf Format according to the 41 | [uProtocol specification](https://github.com/eclipse-uprotocol/up-spec/blob/v1.6.0-alpha.4/up-l1/cloudevents.adoc). 42 | 43 | * `communication` enables support for the [Communication Layer API](https://github.com/eclipse-uprotocol/up-spec/blob/v1.6.0-alpha.4/up-l2/api.adoc) and its 44 | default implementation on top of the [Transport Layer API](https://github.com/eclipse-uprotocol/up-spec/blob/v1.6.0-alpha.4/up-l1/README.adoc). 45 | Enabled by default. 46 | * `udiscovery` enables support for types required to interact with [uDiscovery service](https://raw.githubusercontent.com/eclipse-uprotocol/up-spec/v1.6.0-alpha.4/up-l3/udiscovery/v3/README.adoc) 47 | implementations. 48 | * `usubscription` enables support for types required to interact with [uSubscription service](https://raw.githubusercontent.com/eclipse-uprotocol/up-spec/v1.6.0-alpha.4/up-l3/usubscription/v3/README.adoc) 49 | implementations. Enabled by default. 50 | * `utwin` enables support for types required to interact with [uTwin service](https://raw.githubusercontent.com/eclipse-uprotocol/up-spec/v1.6.0-alpha.4/up-l3/utwin/v3/README.adoc) 51 | implementations. 52 | * `test-util` provides some useful mock implementations for testing. In particular, provides mock implementations of UTransport and Communication Layer API traits which make implementing unit tests a lot easier. 53 | * `util` provides some useful helper structs. In particular, provides a local, in-memory UTransport for exchanging messages within a single process. This transport is also used by the examples illustrating usage of the Communication Layer API. 54 | 55 | ## References 56 | 57 | * [uProtocol Specification](https://github.com/eclipse-uprotocol/up-spec/tree/v1.6.0-alpha.4) 58 | 59 | */ 60 | 61 | // up_core_api types used and augmented by up_rust - symbols re-exported to toplevel, errors are module-specific 62 | #[cfg(feature = "cloudevents")] 63 | mod cloudevents; 64 | #[cfg(feature = "cloudevents")] 65 | pub use cloudevents::{CloudEvent, CONTENT_TYPE_CLOUDEVENTS_PROTOBUF}; 66 | 67 | #[cfg(feature = "communication")] 68 | pub mod communication; 69 | 70 | #[cfg(feature = "util")] 71 | pub mod local_transport; 72 | 73 | mod uattributes; 74 | pub use uattributes::{ 75 | NotificationValidator, PublishValidator, RequestValidator, ResponseValidator, UAttributes, 76 | UAttributesError, UAttributesValidator, UAttributesValidators, UMessageType, UPayloadFormat, 77 | UPriority, 78 | }; 79 | 80 | mod umessage; 81 | pub use umessage::{UMessage, UMessageBuilder, UMessageError}; 82 | 83 | mod uri; 84 | pub use uri::{UUri, UUriError}; 85 | 86 | mod ustatus; 87 | pub use ustatus::{UCode, UStatus}; 88 | 89 | mod utransport; 90 | pub use utransport::{ 91 | ComparableListener, LocalUriProvider, StaticUriProvider, UListener, UTransport, 92 | }; 93 | #[cfg(feature = "test-util")] 94 | pub use utransport::{MockLocalUriProvider, MockTransport, MockUListener}; 95 | 96 | mod uuid; 97 | pub use uuid::UUID; 98 | 99 | // protoc-generated stubs, see build.rs 100 | mod up_core_api { 101 | include!(concat!(env!("OUT_DIR"), "/uprotocol/mod.rs")); 102 | } 103 | 104 | // Types from up_core_api that we're not re-exporting for now (might change if need arises) 105 | // pub use up_core_api::file; 106 | // pub use up_core_api::uprotocol_options; 107 | pub mod core; 108 | -------------------------------------------------------------------------------- /src/local_transport.rs: -------------------------------------------------------------------------------- 1 | /******************************************************************************** 2 | * Copyright (c) 2024 Contributors to the Eclipse Foundation 3 | * 4 | * See the NOTICE file(s) distributed with this work for additional 5 | * information regarding copyright ownership. 6 | * 7 | * This program and the accompanying materials are made available under the 8 | * terms of the Apache License Version 2.0 which is available at 9 | * https://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * SPDX-License-Identifier: Apache-2.0 12 | ********************************************************************************/ 13 | 14 | /*! 15 | Provides a local UTransport which can be used for connecting uEntities running in the same 16 | process. 17 | */ 18 | 19 | use std::{collections::HashSet, sync::Arc}; 20 | 21 | use tokio::sync::RwLock; 22 | 23 | use crate::{ComparableListener, UListener, UMessage, UStatus, UTransport, UUri}; 24 | 25 | #[derive(Eq, PartialEq, Hash)] 26 | struct RegisteredListener { 27 | source_filter: UUri, 28 | sink_filter: Option, 29 | listener: ComparableListener, 30 | } 31 | 32 | impl RegisteredListener { 33 | fn matches(&self, source: &UUri, sink: Option<&UUri>) -> bool { 34 | if !self.source_filter.matches(source) { 35 | return false; 36 | } 37 | 38 | if let Some(pattern) = &self.sink_filter { 39 | sink.is_some_and(|candidate_sink| pattern.matches(candidate_sink)) 40 | } else { 41 | sink.is_none() 42 | } 43 | } 44 | fn matches_msg(&self, msg: &UMessage) -> bool { 45 | if let Some(source) = msg 46 | .attributes 47 | .as_ref() 48 | .and_then(|attribs| attribs.source.as_ref()) 49 | { 50 | self.matches( 51 | source, 52 | msg.attributes 53 | .as_ref() 54 | .and_then(|attribs| attribs.sink.as_ref()), 55 | ) 56 | } else { 57 | false 58 | } 59 | } 60 | async fn on_receive(&self, msg: UMessage) { 61 | self.listener.on_receive(msg).await 62 | } 63 | } 64 | 65 | /// A [`UTransport`] that can be used to exchange messages within a single process. 66 | /// 67 | /// A message sent via [`UTransport::send`] will be dispatched to all registered listeners that 68 | /// match the message's source and sink filters. 69 | #[derive(Default)] 70 | pub struct LocalTransport { 71 | listeners: RwLock>, 72 | } 73 | 74 | impl LocalTransport { 75 | async fn dispatch(&self, message: UMessage) { 76 | let listeners = self.listeners.read().await; 77 | for listener in listeners.iter() { 78 | if listener.matches_msg(&message) { 79 | listener.on_receive(message.clone()).await; 80 | } 81 | } 82 | } 83 | } 84 | 85 | #[async_trait::async_trait] 86 | impl UTransport for LocalTransport { 87 | async fn send(&self, message: UMessage) -> Result<(), UStatus> { 88 | self.dispatch(message).await; 89 | Ok(()) 90 | } 91 | 92 | async fn register_listener( 93 | &self, 94 | source_filter: &UUri, 95 | sink_filter: Option<&UUri>, 96 | listener: Arc, 97 | ) -> Result<(), UStatus> { 98 | let registered_listener = RegisteredListener { 99 | source_filter: source_filter.to_owned(), 100 | sink_filter: sink_filter.map(|u| u.to_owned()), 101 | listener: ComparableListener::new(listener), 102 | }; 103 | let mut listeners = self.listeners.write().await; 104 | if listeners.contains(®istered_listener) { 105 | Err(UStatus::fail_with_code( 106 | crate::UCode::ALREADY_EXISTS, 107 | "listener already registered for filters", 108 | )) 109 | } else { 110 | listeners.insert(registered_listener); 111 | Ok(()) 112 | } 113 | } 114 | 115 | async fn unregister_listener( 116 | &self, 117 | source_filter: &UUri, 118 | sink_filter: Option<&UUri>, 119 | listener: Arc, 120 | ) -> Result<(), UStatus> { 121 | let registered_listener = RegisteredListener { 122 | source_filter: source_filter.to_owned(), 123 | sink_filter: sink_filter.map(|u| u.to_owned()), 124 | listener: ComparableListener::new(listener), 125 | }; 126 | let mut listeners = self.listeners.write().await; 127 | if listeners.remove(®istered_listener) { 128 | Ok(()) 129 | } else { 130 | Err(UStatus::fail_with_code( 131 | crate::UCode::NOT_FOUND, 132 | "no such listener registered for filters", 133 | )) 134 | } 135 | } 136 | } 137 | 138 | #[cfg(test)] 139 | mod tests { 140 | use super::*; 141 | use crate::{utransport::MockUListener, LocalUriProvider, StaticUriProvider, UMessageBuilder}; 142 | 143 | #[tokio::test] 144 | async fn test_send_dispatches_to_matching_listener() { 145 | const RESOURCE_ID: u16 = 0xa1b3; 146 | let mut listener = MockUListener::new(); 147 | listener.expect_on_receive().once().return_const(()); 148 | let listener_ref = Arc::new(listener); 149 | let uri_provider = StaticUriProvider::new("my-vehicle", 0x100d, 0x02); 150 | let transport = LocalTransport::default(); 151 | 152 | transport 153 | .register_listener( 154 | &uri_provider.get_resource_uri(RESOURCE_ID), 155 | None, 156 | listener_ref.clone(), 157 | ) 158 | .await 159 | .unwrap(); 160 | let _ = transport 161 | .send( 162 | UMessageBuilder::publish(uri_provider.get_resource_uri(RESOURCE_ID)) 163 | .build() 164 | .unwrap(), 165 | ) 166 | .await; 167 | 168 | transport 169 | .unregister_listener( 170 | &uri_provider.get_resource_uri(RESOURCE_ID), 171 | None, 172 | listener_ref, 173 | ) 174 | .await 175 | .unwrap(); 176 | let _ = transport 177 | .send( 178 | UMessageBuilder::publish(uri_provider.get_resource_uri(RESOURCE_ID)) 179 | .build() 180 | .unwrap(), 181 | ) 182 | .await; 183 | } 184 | 185 | #[tokio::test] 186 | async fn test_send_does_not_dispatch_to_non_matching_listener() { 187 | const RESOURCE_ID: u16 = 0xa1b3; 188 | let mut listener = MockUListener::new(); 189 | listener.expect_on_receive().never().return_const(()); 190 | let listener_ref = Arc::new(listener); 191 | let uri_provider = StaticUriProvider::new("my-vehicle", 0x100d, 0x02); 192 | let transport = LocalTransport::default(); 193 | 194 | transport 195 | .register_listener( 196 | &uri_provider.get_resource_uri(RESOURCE_ID + 10), 197 | None, 198 | listener_ref.clone(), 199 | ) 200 | .await 201 | .unwrap(); 202 | let _ = transport 203 | .send( 204 | UMessageBuilder::publish(uri_provider.get_resource_uri(RESOURCE_ID)) 205 | .build() 206 | .unwrap(), 207 | ) 208 | .await; 209 | 210 | transport 211 | .unregister_listener( 212 | &uri_provider.get_resource_uri(RESOURCE_ID + 10), 213 | None, 214 | listener_ref, 215 | ) 216 | .await 217 | .unwrap(); 218 | let _ = transport 219 | .send( 220 | UMessageBuilder::publish(uri_provider.get_resource_uri(RESOURCE_ID)) 221 | .build() 222 | .unwrap(), 223 | ) 224 | .await; 225 | } 226 | } 227 | -------------------------------------------------------------------------------- /src/uattributes.rs: -------------------------------------------------------------------------------- 1 | /******************************************************************************** 2 | * Copyright (c) 2023 Contributors to the Eclipse Foundation 3 | * 4 | * See the NOTICE file(s) distributed with this work for additional 5 | * information regarding copyright ownership. 6 | * 7 | * This program and the accompanying materials are made available under the 8 | * terms of the Apache License Version 2.0 which is available at 9 | * https://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * SPDX-License-Identifier: Apache-2.0 12 | ********************************************************************************/ 13 | 14 | mod uattributesvalidator; 15 | mod upayloadformat; 16 | mod upriority; 17 | 18 | use std::time::SystemTime; 19 | 20 | pub use uattributesvalidator::*; 21 | pub use upriority::*; 22 | 23 | pub use crate::up_core_api::uattributes::*; 24 | use crate::UUID; 25 | 26 | const UPRIORITY_DEFAULT: UPriority = UPriority::UPRIORITY_CS1; 27 | 28 | #[derive(Debug)] 29 | pub enum UAttributesError { 30 | ValidationError(String), 31 | ParsingError(String), 32 | } 33 | 34 | impl UAttributesError { 35 | pub fn validation_error(message: T) -> UAttributesError 36 | where 37 | T: Into, 38 | { 39 | Self::ValidationError(message.into()) 40 | } 41 | 42 | pub fn parsing_error(message: T) -> UAttributesError 43 | where 44 | T: Into, 45 | { 46 | Self::ParsingError(message.into()) 47 | } 48 | } 49 | 50 | impl std::fmt::Display for UAttributesError { 51 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 52 | match self { 53 | Self::ValidationError(e) => f.write_fmt(format_args!("Validation failure: {}", e)), 54 | Self::ParsingError(e) => f.write_fmt(format_args!("Parsing error: {}", e)), 55 | } 56 | } 57 | } 58 | 59 | impl std::error::Error for UAttributesError {} 60 | 61 | impl UAttributes { 62 | /// Checks if a given priority class is the default priority class. 63 | /// 64 | /// Messages that do not have a priority class set explicity, are assigned to 65 | /// the default prioritiy class. 66 | pub(crate) fn is_default_priority(prio: UPriority) -> bool { 67 | // [impl->dsn~up-attributes-priority~1] 68 | prio == UPRIORITY_DEFAULT 69 | } 70 | 71 | /// Checks if these are the attributes for a Publish message. 72 | /// 73 | /// # Examples 74 | /// 75 | /// ```rust 76 | /// use up_rust::{UAttributes, UMessageType}; 77 | /// 78 | /// let attribs = UAttributes { 79 | /// type_: UMessageType::UMESSAGE_TYPE_PUBLISH.into(), 80 | /// ..Default::default() 81 | /// }; 82 | /// assert!(attribs.is_publish()); 83 | /// ``` 84 | pub fn is_publish(&self) -> bool { 85 | self.type_.enum_value() == Ok(UMessageType::UMESSAGE_TYPE_PUBLISH) 86 | } 87 | 88 | /// Checks if these are the attributes for an RPC Request message. 89 | /// 90 | /// # Examples 91 | /// 92 | /// ```rust 93 | /// use up_rust::{UAttributes, UMessageType}; 94 | /// 95 | /// let attribs = UAttributes { 96 | /// type_: UMessageType::UMESSAGE_TYPE_REQUEST.into(), 97 | /// ..Default::default() 98 | /// }; 99 | /// assert!(attribs.is_request()); 100 | /// ``` 101 | pub fn is_request(&self) -> bool { 102 | self.type_.enum_value() == Ok(UMessageType::UMESSAGE_TYPE_REQUEST) 103 | } 104 | 105 | /// Checks if these are the attributes for an RPC Response message. 106 | /// 107 | /// # Examples 108 | /// 109 | /// ```rust 110 | /// use up_rust::{UAttributes, UMessageType}; 111 | /// 112 | /// let attribs = UAttributes { 113 | /// type_: UMessageType::UMESSAGE_TYPE_RESPONSE.into(), 114 | /// ..Default::default() 115 | /// }; 116 | /// assert!(attribs.is_response()); 117 | /// ``` 118 | pub fn is_response(&self) -> bool { 119 | self.type_.enum_value() == Ok(UMessageType::UMESSAGE_TYPE_RESPONSE) 120 | } 121 | 122 | /// Checks if these are the attributes for a Notification message. 123 | /// 124 | /// # Examples 125 | /// 126 | /// ```rust 127 | /// use up_rust::{UAttributes, UMessageType}; 128 | /// 129 | /// let attribs = UAttributes { 130 | /// type_: UMessageType::UMESSAGE_TYPE_NOTIFICATION.into(), 131 | /// ..Default::default() 132 | /// }; 133 | /// assert!(attribs.is_notification()); 134 | /// ``` 135 | pub fn is_notification(&self) -> bool { 136 | self.type_.enum_value() == Ok(UMessageType::UMESSAGE_TYPE_NOTIFICATION) 137 | } 138 | 139 | /// Checks if the message that is described by these attributes should be considered expired. 140 | /// 141 | /// # Errors 142 | /// 143 | /// Returns an error if [`Self::ttl`] (time-to-live) contains a value greater than 0, but 144 | /// * the message has expired according to the timestamp extracted from [`Self::id`] and the time-to-live value, or 145 | /// * the current system time cannot be determined. 146 | pub fn check_expired(&self) -> Result<(), UAttributesError> { 147 | let ttl = match self.ttl { 148 | Some(t) if t > 0 => u64::from(t), 149 | // [impl->dsn~up-attributes-ttl~1] 150 | _ => return Ok(()), 151 | }; 152 | 153 | // [impl->dsn~up-attributes-ttl-timeout~1] 154 | if let Some(creation_time) = self.id.as_ref().and_then(UUID::get_time) { 155 | let delta = match SystemTime::now().duration_since(SystemTime::UNIX_EPOCH) { 156 | Ok(duration) => { 157 | if let Ok(duration) = u64::try_from(duration.as_millis()) { 158 | duration - creation_time 159 | } else { 160 | return Err(UAttributesError::validation_error( 161 | "Invalid system time: too far in the future", 162 | )); 163 | } 164 | } 165 | Err(e) => return Err(UAttributesError::validation_error(e.to_string())), 166 | }; 167 | if delta >= ttl { 168 | return Err(UAttributesError::validation_error("message is expired")); 169 | } 170 | } 171 | Ok(()) 172 | } 173 | } 174 | 175 | #[cfg(test)] 176 | mod tests { 177 | use std::ops::Sub; 178 | use std::time::{Duration, UNIX_EPOCH}; 179 | 180 | use super::*; 181 | use test_case::test_case; 182 | 183 | /// Creates a UUID n ms in the past. 184 | /// 185 | /// # Note 186 | /// 187 | /// For internal testing purposes only. For end-users, please use [`UUID::build()`] 188 | fn build_n_ms_in_past(n_ms_in_past: u64) -> UUID { 189 | let duration_since_unix_epoch = SystemTime::now() 190 | .duration_since(UNIX_EPOCH) 191 | .expect("current system time is set to a point in time before UNIX Epoch"); 192 | UUID::build_for_timestamp( 193 | duration_since_unix_epoch.sub(Duration::from_millis(n_ms_in_past)), 194 | ) 195 | } 196 | 197 | #[test_case(None, None, false; "for message without ID nor TTL")] 198 | // [utest->dsn~up-attributes-ttl~1] 199 | #[test_case(None, Some(0), false; "for message without ID with TTL 0")] 200 | #[test_case(None, Some(500), false; "for message without ID with TTL")] 201 | #[test_case(Some(build_n_ms_in_past(1000)), None, false; "for message with ID without TTL")] 202 | // [utest->dsn~up-attributes-ttl~1] 203 | #[test_case(Some(build_n_ms_in_past(1000)), Some(0), false; "for message with ID and TTL 0")] 204 | // [utest->dsn~up-attributes-ttl-timeout~1] 205 | #[test_case(Some(build_n_ms_in_past(1000)), Some(500), true; "for message with ID and expired TTL")] 206 | // [utest->dsn~up-attributes-ttl-timeout~1] 207 | #[test_case(Some(build_n_ms_in_past(1000)), Some(2000), false; "for message with ID and non-expired TTL")] 208 | fn test_is_expired(id: Option, ttl: Option, should_be_expired: bool) { 209 | let attributes = UAttributes { 210 | type_: UMessageType::UMESSAGE_TYPE_NOTIFICATION.into(), 211 | priority: UPriority::UPRIORITY_CS1.into(), 212 | id: id.into(), 213 | ttl, 214 | ..Default::default() 215 | }; 216 | 217 | assert!(attributes.check_expired().is_err() == should_be_expired); 218 | } 219 | } 220 | -------------------------------------------------------------------------------- /src/uattributes/upayloadformat.rs: -------------------------------------------------------------------------------- 1 | /******************************************************************************** 2 | * Copyright (c) 2023 Contributors to the Eclipse Foundation 3 | * 4 | * See the NOTICE file(s) distributed with this work for additional 5 | * information regarding copyright ownership. 6 | * 7 | * This program and the accompanying materials are made available under the 8 | * terms of the Apache License Version 2.0 which is available at 9 | * https://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * SPDX-License-Identifier: Apache-2.0 12 | ********************************************************************************/ 13 | 14 | use crate::up_core_api::uattributes::UPayloadFormat; 15 | use mediatype::MediaType; 16 | use protobuf::EnumFull; 17 | 18 | #[derive(Debug)] 19 | pub enum UPayloadError { 20 | SerializationError(String), 21 | MediatypeProblem, 22 | } 23 | 24 | impl UPayloadError { 25 | pub fn serialization_error(message: T) -> UPayloadError 26 | where 27 | T: Into, 28 | { 29 | Self::SerializationError(message.into()) 30 | } 31 | 32 | pub fn mediatype_error() -> UPayloadError { 33 | Self::MediatypeProblem 34 | } 35 | } 36 | 37 | impl std::fmt::Display for UPayloadError { 38 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 39 | match self { 40 | Self::SerializationError(e) => f.write_fmt(format_args!("Serialization error: {}", e)), 41 | Self::MediatypeProblem => { 42 | f.write_fmt(format_args!("Mediatype problem unsupported or malformed")) 43 | } 44 | } 45 | } 46 | } 47 | 48 | impl std::error::Error for UPayloadError {} 49 | 50 | impl UPayloadFormat { 51 | /// Gets the payload format that corresponds to a given media type. 52 | /// 53 | /// # Errors 54 | /// 55 | /// Returns an error if the given string is not a valid media type string or is unsupported by uProtocol. 56 | /// 57 | /// # Examples 58 | /// 59 | /// ```rust 60 | /// use up_rust::UPayloadFormat; 61 | /// 62 | /// let parse_attempt = UPayloadFormat::from_media_type("application/json; charset=utf-8"); 63 | /// assert!(parse_attempt.is_ok()); 64 | /// assert_eq!(parse_attempt.unwrap(), UPayloadFormat::UPAYLOAD_FORMAT_JSON); 65 | /// 66 | /// let parse_attempt = UPayloadFormat::from_media_type("application/unsupported"); 67 | /// assert!(parse_attempt.is_err()); 68 | /// ``` 69 | pub fn from_media_type(media_type_string: &str) -> Result { 70 | if let Ok(media_type) = MediaType::parse(media_type_string) { 71 | let descriptor = UPayloadFormat::enum_descriptor(); 72 | return descriptor 73 | .values() 74 | .find_map(|desc| { 75 | let proto_desc = desc.proto(); 76 | 77 | crate::up_core_api::uoptions::exts::mime_type 78 | .get(proto_desc.options.get_or_default()) 79 | .and_then(|mime_type_option_value| { 80 | if let Ok(enum_mime_type) = MediaType::parse(&mime_type_option_value) { 81 | if enum_mime_type.ty == media_type.ty 82 | && enum_mime_type.subty == media_type.subty 83 | { 84 | return desc.cast::(); 85 | } 86 | } 87 | None 88 | }) 89 | }) 90 | .ok_or(UPayloadError::mediatype_error()); 91 | } 92 | Err(UPayloadError::mediatype_error()) 93 | } 94 | 95 | /// Gets the media type corresponding to this payload format. 96 | /// 97 | /// # Returns 98 | /// 99 | /// None if the payload format is [`UPayloadFormat::UPAYLOAD_FORMAT_UNSPECIFIED`]. 100 | /// 101 | /// # Examples 102 | /// 103 | /// ```rust 104 | /// use up_rust::UPayloadFormat; 105 | /// 106 | /// assert_eq!(UPayloadFormat::UPAYLOAD_FORMAT_JSON.to_media_type().unwrap(), "application/json"); 107 | /// assert!(UPayloadFormat::UPAYLOAD_FORMAT_UNSPECIFIED.to_media_type().is_none()); 108 | /// ``` 109 | pub fn to_media_type(self) -> Option { 110 | let desc = self.descriptor(); 111 | let desc_proto = desc.proto(); 112 | crate::up_core_api::uoptions::exts::mime_type.get(desc_proto.options.get_or_default()) 113 | } 114 | } 115 | 116 | #[cfg(test)] 117 | mod tests { 118 | use super::*; 119 | 120 | use test_case::test_case; 121 | 122 | #[test_case("application/json", Ok(UPayloadFormat::UPAYLOAD_FORMAT_JSON); "map from JSON")] 123 | #[test_case( 124 | "application/json; charset=utf-8", 125 | Ok(UPayloadFormat::UPAYLOAD_FORMAT_JSON); 126 | "map from JSON with parameter" 127 | )] 128 | #[test_case("application/protobuf", Ok(UPayloadFormat::UPAYLOAD_FORMAT_PROTOBUF); "map from PROTOBUF")] 129 | #[test_case( 130 | "application/x-protobuf", 131 | Ok(UPayloadFormat::UPAYLOAD_FORMAT_PROTOBUF_WRAPPED_IN_ANY); "map from PROTOBUF_WRAPPED" 132 | )] 133 | #[test_case("application/octet-stream", Ok(UPayloadFormat::UPAYLOAD_FORMAT_RAW); "map from RAW")] 134 | #[test_case("application/x-someip", Ok(UPayloadFormat::UPAYLOAD_FORMAT_SOMEIP); "map from SOMEIP")] 135 | #[test_case( 136 | "application/x-someip_tlv", 137 | Ok(UPayloadFormat::UPAYLOAD_FORMAT_SOMEIP_TLV); "map from SOMEIP_TLV" 138 | )] 139 | #[test_case("text/plain", Ok(UPayloadFormat::UPAYLOAD_FORMAT_TEXT); "map from TEXT")] 140 | #[test_case("application/unsupported; foo=bar", Err(UPayloadError::mediatype_error()); "fail for unsupported media type")] 141 | fn test_from_media_type( 142 | media_type: &str, 143 | expected_format: Result, 144 | ) { 145 | let parsing_result = UPayloadFormat::from_media_type(media_type); 146 | assert!(parsing_result.is_ok() == expected_format.is_ok()); 147 | if let Ok(format) = expected_format { 148 | assert_eq!(format, parsing_result.unwrap()); 149 | } 150 | } 151 | 152 | #[test_case(UPayloadFormat::UPAYLOAD_FORMAT_JSON, Some("application/json".to_string()); "map JSON format to media type")] 153 | #[test_case(UPayloadFormat::UPAYLOAD_FORMAT_PROTOBUF, Some("application/protobuf".to_string()); "map PROTOBUF format to media type")] 154 | #[test_case(UPayloadFormat::UPAYLOAD_FORMAT_PROTOBUF_WRAPPED_IN_ANY, Some("application/x-protobuf".to_string()); "map PROTOBUF_WRAPPED format to media type")] 155 | #[test_case(UPayloadFormat::UPAYLOAD_FORMAT_RAW, Some("application/octet-stream".to_string()); "map RAW format to media type")] 156 | #[test_case(UPayloadFormat::UPAYLOAD_FORMAT_SOMEIP, Some("application/x-someip".to_string()); "map SOMEIP format to media type")] 157 | #[test_case(UPayloadFormat::UPAYLOAD_FORMAT_SOMEIP_TLV, Some("application/x-someip_tlv".to_string()); "map SOMEIP_TLV format to media type")] 158 | #[test_case(UPayloadFormat::UPAYLOAD_FORMAT_TEXT, Some("text/plain".to_string()); "map TEXT format to media type")] 159 | #[test_case(UPayloadFormat::UPAYLOAD_FORMAT_UNSPECIFIED, None; "map UNSPECIFIED format to None")] 160 | fn test_to_media_type(format: UPayloadFormat, expected_media_type: Option) { 161 | assert_eq!(format.to_media_type(), expected_media_type); 162 | } 163 | } 164 | -------------------------------------------------------------------------------- /src/uattributes/upriority.rs: -------------------------------------------------------------------------------- 1 | /******************************************************************************** 2 | * Copyright (c) 2023 Contributors to the Eclipse Foundation 3 | * 4 | * See the NOTICE file(s) distributed with this work for additional 5 | * information regarding copyright ownership. 6 | * 7 | * This program and the accompanying materials are made available under the 8 | * terms of the Apache License Version 2.0 which is available at 9 | * https://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * SPDX-License-Identifier: Apache-2.0 12 | ********************************************************************************/ 13 | 14 | use protobuf::EnumFull; 15 | 16 | use crate::uattributes::UAttributesError; 17 | pub use crate::up_core_api::uattributes::UPriority; 18 | use crate::up_core_api::uoptions::exts::ce_name; 19 | 20 | impl UPriority { 21 | /// Encodes this priority to a string. 22 | /// 23 | /// The encoding of priorities to strings is defined in the 24 | /// [uProtocol Core API](https://github.com/eclipse-uprotocol/up-core-api/blob/main/uprotocol/uattributes.proto). 25 | /// 26 | /// # Examples 27 | /// 28 | /// ``` 29 | /// use up_rust::UPriority; 30 | /// 31 | /// assert_eq!(UPriority::UPRIORITY_CS2.to_priority_code(), "CS2"); 32 | /// ``` 33 | pub fn to_priority_code(self) -> String { 34 | let desc = self.descriptor(); 35 | let desc_proto = desc.proto(); 36 | ce_name 37 | .get(desc_proto.options.get_or_default()) 38 | .unwrap_or_default() 39 | } 40 | 41 | /// Gets the priority for a string. 42 | /// 43 | /// The encoding of priorities to strings is defined in the 44 | /// [uProtocol Core API](https://github.com/eclipse-uprotocol/up-core-api/blob/main/uprotocol/uattributes.proto). 45 | /// 46 | /// # Errors 47 | /// 48 | /// Returns an error if the given string does not match a supported priority. 49 | /// 50 | /// # Examples 51 | /// 52 | /// ``` 53 | /// use up_rust::UPriority; 54 | /// 55 | /// let priority = UPriority::try_from_priority_code("CS2").unwrap(); 56 | /// assert_eq!(priority, UPriority::UPRIORITY_CS2); 57 | /// 58 | /// assert!(UPriority::try_from_priority_code("not-supported").is_err()); 59 | /// ``` 60 | pub fn try_from_priority_code(code: T) -> Result 61 | where 62 | T: Into, 63 | { 64 | let prio: String = code.into(); 65 | Self::enum_descriptor() 66 | .values() 67 | .find_map(|desc| { 68 | let proto_desc = desc.proto(); 69 | 70 | ce_name 71 | .get(proto_desc.options.get_or_default()) 72 | .and_then(|prio_option_value| { 73 | if prio_option_value.eq(prio.as_str()) { 74 | desc.cast::() 75 | } else { 76 | None 77 | } 78 | }) 79 | }) 80 | .ok_or_else(|| UAttributesError::parsing_error(format!("unknown priority [{}]", prio))) 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /src/umessage/README.md: -------------------------------------------------------------------------------- 1 | # uProtocol Transport Interface & Data Model 2 | 3 | ## Overview 4 | 5 | The purpose of this module is to provide an implementation of [uTransport API & Data Model](https://github.com/eclipse-uprotocol/uprotocol-spec/blob/main/up-l1/README.adoc). The transport API is used by all uE developers to send and receive messages across any transport. The interface is to be implemented by communication transport developers (i.e. developing a uTransport for SOME/IP, DDS, Zenoh, MQTT, etc...). 6 | -------------------------------------------------------------------------------- /src/umessage/umessagetype.rs: -------------------------------------------------------------------------------- 1 | /******************************************************************************** 2 | * Copyright (c) 2023 Contributors to the Eclipse Foundation 3 | * 4 | * See the NOTICE file(s) distributed with this work for additional 5 | * information regarding copyright ownership. 6 | * 7 | * This program and the accompanying materials are made available under the 8 | * terms of the Apache License Version 2.0 which is available at 9 | * https://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * SPDX-License-Identifier: Apache-2.0 12 | ********************************************************************************/ 13 | 14 | use protobuf::EnumFull; 15 | 16 | use crate::uattributes::UAttributesError; 17 | pub use crate::up_core_api::uattributes::UMessageType; 18 | use crate::up_core_api::uoptions::exts::ce_name; 19 | 20 | impl UMessageType { 21 | /// Gets this message type's CloudEvent type name. 22 | /// 23 | /// # Returns 24 | /// 25 | /// The value to use for the *type* property when mapping to a CloudEvent. 26 | pub fn to_cloudevent_type(&self) -> String { 27 | let desc = self.descriptor(); 28 | let desc_proto = desc.proto(); 29 | ce_name 30 | .get(desc_proto.options.get_or_default()) 31 | .unwrap_or_default() 32 | } 33 | 34 | /// Gets the message type for a CloudEvent type name. 35 | /// 36 | /// # Errors 37 | /// 38 | /// Returns a [`UAttributesError::ParsingError`] if the given name does not match 39 | /// any of the supported message types. 40 | pub fn try_from_cloudevent_type>(value: S) -> Result { 41 | let type_string = value.into(); 42 | 43 | Self::enum_descriptor() 44 | .values() 45 | .find_map(|desc| { 46 | let proto_desc = desc.proto(); 47 | 48 | ce_name 49 | .get(proto_desc.options.get_or_default()) 50 | .and_then(|prio_option_value| { 51 | if prio_option_value.eq(type_string.as_str()) { 52 | desc.cast::() 53 | } else { 54 | None 55 | } 56 | }) 57 | }) 58 | .ok_or_else(|| { 59 | UAttributesError::parsing_error(format!("unknown message type: {}", type_string)) 60 | }) 61 | } 62 | } 63 | 64 | #[cfg(test)] 65 | mod tests { 66 | use test_case::test_case; 67 | 68 | use crate::{UAttributesError, UMessageType}; 69 | 70 | const TYPE_PUBLISH: &str = "up-pub.v1"; 71 | const TYPE_NOTIFICATION: &str = "up-not.v1"; 72 | const TYPE_REQUEST: &str = "up-req.v1"; 73 | const TYPE_RESPONSE: &str = "up-res.v1"; 74 | 75 | #[test_case(UMessageType::UMESSAGE_TYPE_PUBLISH, TYPE_PUBLISH; "for PUBLISH")] 76 | #[test_case(UMessageType::UMESSAGE_TYPE_NOTIFICATION, TYPE_NOTIFICATION; "for NOTIFICATION")] 77 | #[test_case(UMessageType::UMESSAGE_TYPE_REQUEST, TYPE_REQUEST; "for REQUEST")] 78 | #[test_case(UMessageType::UMESSAGE_TYPE_RESPONSE, TYPE_RESPONSE; "for RESPONSE")] 79 | fn test_to_cloudevent_type(message_type: UMessageType, expected_ce_name: &str) { 80 | assert_eq!(message_type.to_cloudevent_type(), expected_ce_name); 81 | } 82 | 83 | #[test_case(TYPE_PUBLISH, Some(UMessageType::UMESSAGE_TYPE_PUBLISH); "succeeds for PUBLISH")] 84 | #[test_case(TYPE_NOTIFICATION, Some(UMessageType::UMESSAGE_TYPE_NOTIFICATION); "succeeds for NOTIFICATION")] 85 | #[test_case(TYPE_REQUEST, Some(UMessageType::UMESSAGE_TYPE_REQUEST); "succeeds for REQUEST")] 86 | #[test_case(TYPE_RESPONSE, Some(UMessageType::UMESSAGE_TYPE_RESPONSE); "succeeds for RESPONSE")] 87 | #[test_case("foo.bar", None; "fails for unknown type")] 88 | fn test_try_from_cloudevent_type( 89 | cloudevent_type: &str, 90 | expected_message_type: Option, 91 | ) { 92 | let result = UMessageType::try_from_cloudevent_type(cloudevent_type); 93 | assert!(result.is_ok() == expected_message_type.is_some()); 94 | if expected_message_type.is_some() { 95 | assert_eq!(result.unwrap(), expected_message_type.unwrap()) 96 | } else { 97 | assert!(matches!( 98 | result.unwrap_err(), 99 | UAttributesError::ParsingError(_msg) 100 | )) 101 | } 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /src/ustatus.rs: -------------------------------------------------------------------------------- 1 | /******************************************************************************** 2 | * Copyright (c) 2023 Contributors to the Eclipse Foundation 3 | * 4 | * See the NOTICE file(s) distributed with this work for additional 5 | * information regarding copyright ownership. 6 | * 7 | * This program and the accompanying materials are made available under the 8 | * terms of the Apache License Version 2.0 which is available at 9 | * https://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * SPDX-License-Identifier: Apache-2.0 12 | ********************************************************************************/ 13 | 14 | use std::error::Error; 15 | 16 | pub use crate::up_core_api::ucode::UCode; 17 | pub use crate::up_core_api::ustatus::UStatus; 18 | 19 | impl UStatus { 20 | /// Creates a status representing a success. 21 | /// 22 | /// # Examples 23 | /// 24 | /// ```rust 25 | /// use up_rust::{UCode, UStatus}; 26 | /// 27 | /// let status = UStatus::ok(); 28 | /// assert_eq!(status.code.unwrap(), UCode::OK); 29 | /// ``` 30 | pub fn ok() -> Self { 31 | UStatus { 32 | code: UCode::OK.into(), 33 | ..Default::default() 34 | } 35 | } 36 | 37 | /// Creates a status representing a failure. 38 | /// 39 | /// # Examples 40 | /// 41 | /// ```rust 42 | /// use up_rust::UStatus; 43 | /// 44 | /// let status = UStatus::fail("something went wrong"); 45 | /// assert_eq!(status.message.unwrap(), "something went wrong"); 46 | /// ``` 47 | pub fn fail>(msg: M) -> Self { 48 | UStatus { 49 | code: UCode::UNKNOWN.into(), 50 | message: Some(msg.into()), 51 | ..Default::default() 52 | } 53 | } 54 | 55 | /// Creates a status representing a failure. 56 | /// 57 | /// # Examples 58 | /// 59 | /// ```rust 60 | /// use up_rust::{UCode, UStatus}; 61 | /// 62 | /// let status = UStatus::fail_with_code(UCode::DATA_LOSS, "something went wrong"); 63 | /// assert_eq!(status.code.unwrap(), UCode::DATA_LOSS); 64 | /// assert_eq!(status.message.unwrap(), "something went wrong"); 65 | /// ``` 66 | pub fn fail_with_code>(code: UCode, msg: M) -> Self { 67 | UStatus { 68 | code: code.into(), 69 | message: Some(msg.into()), 70 | ..Default::default() 71 | } 72 | } 73 | 74 | /// Checks if this status represents a failure. 75 | /// 76 | /// # Examples 77 | /// 78 | /// ```rust 79 | /// use up_rust::UStatus; 80 | /// 81 | /// let failed_status = UStatus::fail("something went wrong"); 82 | /// assert!(failed_status.is_failed()); 83 | /// 84 | /// let succeeded_status = UStatus::ok(); 85 | /// assert!(!succeeded_status.is_failed()); 86 | /// ``` 87 | pub fn is_failed(&self) -> bool { 88 | self.get_code() != UCode::OK 89 | } 90 | 91 | /// Checks if this status represents a success. 92 | /// 93 | /// # Examples 94 | /// 95 | /// ```rust 96 | /// use up_rust::UStatus; 97 | /// 98 | /// let succeeded_status = UStatus::ok(); 99 | /// assert!(succeeded_status.is_success()); 100 | /// 101 | /// let failed_status = UStatus::fail("something went wrong"); 102 | /// assert!(!failed_status.is_success()); 103 | /// ``` 104 | pub fn is_success(&self) -> bool { 105 | self.get_code() == UCode::OK 106 | } 107 | 108 | /// Gets this status' error message. 109 | /// 110 | /// # Returns 111 | /// 112 | /// an empty string if this instance has been created without a message, 113 | /// i.e. not using one of its factory functions. 114 | /// 115 | /// # Examples 116 | /// 117 | /// ```rust 118 | /// use up_rust::UStatus; 119 | /// 120 | /// let failed_status = UStatus::fail("my error message"); 121 | /// assert_eq!(failed_status.get_message(), "my error message"); 122 | /// 123 | /// let succeeded_status = UStatus::ok(); 124 | /// assert!(succeeded_status.get_message().is_empty()); 125 | /// ``` 126 | pub fn get_message(&self) -> String { 127 | match self.message.as_ref() { 128 | Some(msg) => msg.to_owned(), 129 | None => String::default(), 130 | } 131 | } 132 | 133 | /// Gets this status' error code. 134 | /// 135 | /// # Returns 136 | /// 137 | /// [`UCode::UNKNOWN`] if this status has been created without providing an error code. 138 | /// 139 | /// # Examples 140 | /// 141 | /// ```rust 142 | /// use up_rust::{UCode, UStatus}; 143 | /// 144 | /// let status = UStatus::fail("my error message"); 145 | /// assert_eq!(status.get_code(), UCode::UNKNOWN); 146 | /// 147 | /// let status_with_code = UStatus::fail_with_code(UCode::INTERNAL, "my error message"); 148 | /// assert_eq!(status_with_code.get_code(), UCode::INTERNAL); 149 | /// ``` 150 | pub fn get_code(&self) -> UCode { 151 | self.code.enum_value_or_default() 152 | } 153 | } 154 | 155 | impl Error for UStatus {} 156 | 157 | #[cfg(test)] 158 | mod tests { 159 | use super::*; 160 | 161 | use protobuf::{Enum, EnumOrUnknown}; 162 | 163 | #[test] 164 | fn test_is_failed() { 165 | assert!(!UStatus { 166 | ..Default::default() 167 | } 168 | .is_failed()); 169 | UCode::VALUES.iter().for_each(|code| { 170 | let ustatus = UStatus { 171 | code: EnumOrUnknown::from(*code), 172 | ..Default::default() 173 | }; 174 | assert_eq!(ustatus.is_failed(), *code != UCode::OK); 175 | }); 176 | } 177 | 178 | #[test] 179 | fn test_is_success() { 180 | assert!(UStatus { 181 | ..Default::default() 182 | } 183 | .is_success()); 184 | UCode::VALUES.iter().for_each(|code| { 185 | let ustatus = UStatus { 186 | code: EnumOrUnknown::from(*code), 187 | ..Default::default() 188 | }; 189 | assert_eq!(ustatus.is_success(), *code == UCode::OK); 190 | }); 191 | } 192 | } 193 | -------------------------------------------------------------------------------- /src/uuid.rs: -------------------------------------------------------------------------------- 1 | /******************************************************************************** 2 | * Copyright (c) 2023 Contributors to the Eclipse Foundation 3 | * 4 | * See the NOTICE file(s) distributed with this work for additional 5 | * information regarding copyright ownership. 6 | * 7 | * This program and the accompanying materials are made available under the 8 | * terms of the Apache License Version 2.0 which is available at 9 | * https://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * SPDX-License-Identifier: Apache-2.0 12 | ********************************************************************************/ 13 | 14 | use rand::RngCore; 15 | use std::time::{Duration, SystemTime}; 16 | use std::{hash::Hash, str::FromStr}; 17 | 18 | pub use crate::up_core_api::uuid::UUID; 19 | 20 | use uuid_simd::{AsciiCase, Out}; 21 | 22 | const BITMASK_VERSION: u64 = 0b1111 << 12; 23 | const VERSION_7: u64 = 0b0111 << 12; 24 | const BITMASK_VARIANT: u64 = 0b11 << 62; 25 | const VARIANT_RFC4122: u64 = 0b10 << 62; 26 | 27 | fn is_correct_version(msb: u64) -> bool { 28 | msb & BITMASK_VERSION == VERSION_7 29 | } 30 | 31 | fn is_correct_variant(lsb: u64) -> bool { 32 | lsb & BITMASK_VARIANT == VARIANT_RFC4122 33 | } 34 | 35 | #[derive(Debug)] 36 | pub struct UuidConversionError { 37 | message: String, 38 | } 39 | 40 | impl UuidConversionError { 41 | pub fn new>(message: T) -> UuidConversionError { 42 | UuidConversionError { 43 | message: message.into(), 44 | } 45 | } 46 | } 47 | 48 | impl std::fmt::Display for UuidConversionError { 49 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 50 | write!(f, "Error converting Uuid: {}", self.message) 51 | } 52 | } 53 | 54 | impl std::error::Error for UuidConversionError {} 55 | 56 | impl UUID { 57 | /// Creates a new UUID from a byte array. 58 | /// 59 | /// # Arguments 60 | /// 61 | /// `bytes` - the byte array 62 | /// 63 | /// # Returns 64 | /// 65 | /// a uProtocol [`UUID`] with the given timestamp and random values. 66 | /// 67 | /// # Errors 68 | /// 69 | /// Returns an error if the given bytes contain an invalid version and/or variant identifier. 70 | pub(crate) fn from_bytes(bytes: &[u8; 16]) -> Result { 71 | let mut msb = [0_u8; 8]; 72 | let mut lsb = [0_u8; 8]; 73 | msb.copy_from_slice(&bytes[..8]); 74 | lsb.copy_from_slice(&bytes[8..]); 75 | Self::from_u64_pair(u64::from_be_bytes(msb), u64::from_be_bytes(lsb)) 76 | } 77 | 78 | /// Creates a new UUID from a high/low value pair. 79 | /// 80 | /// NOTE: This function does *not* check if the given bytes represent a [valid uProtocol UUID](Self::is_uprotocol_uuid). 81 | /// It should therefore only be used in cases where the bytes passed in are known to be valid. 82 | /// 83 | /// # Arguments 84 | /// 85 | /// `msb` - the most significant 8 bytes 86 | /// `lsb` - the least significant 8 bytes 87 | /// 88 | /// # Returns 89 | /// 90 | /// a uProtocol [`UUID`] with the given timestamp and random values. 91 | pub(crate) fn from_bytes_unchecked(msb: [u8; 8], lsb: [u8; 8]) -> Self { 92 | UUID { 93 | msb: u64::from_be_bytes(msb), 94 | lsb: u64::from_be_bytes(lsb), 95 | ..Default::default() 96 | } 97 | } 98 | 99 | /// Creates a new UUID from a high/low value pair. 100 | /// 101 | /// # Arguments 102 | /// 103 | /// `msb` - the most significant 8 bytes 104 | /// `lsb` - the least significant 8 bytes 105 | /// 106 | /// # Returns 107 | /// 108 | /// a uProtocol [`UUID`] with the given timestamp and random values. 109 | /// 110 | /// # Errors 111 | /// 112 | /// Returns an error if the given bytes contain an invalid version and/or variant identifier. 113 | // [impl->dsn~uuid-spec~1] 114 | pub(crate) fn from_u64_pair(msb: u64, lsb: u64) -> Result { 115 | if !is_correct_version(msb) { 116 | return Err(UuidConversionError::new("not a v7 UUID")); 117 | } 118 | if !is_correct_variant(lsb) { 119 | return Err(UuidConversionError::new("not an RFC4122 UUID")); 120 | } 121 | Ok(UUID { 122 | msb, 123 | lsb, 124 | ..Default::default() 125 | }) 126 | } 127 | 128 | // [impl->dsn~uuid-spec~1] 129 | pub(crate) fn build_for_timestamp(duration_since_unix_epoch: Duration) -> UUID { 130 | let timestamp_millis = u64::try_from(duration_since_unix_epoch.as_millis()) 131 | .expect("system time is set to a time too far in the future"); 132 | // fill upper 48 bits with timestamp 133 | let mut msb = (timestamp_millis << 16).to_be_bytes(); 134 | // fill remaining bits with random bits 135 | rand::thread_rng().fill_bytes(&mut msb[6..]); 136 | // set version (7) 137 | msb[6] = msb[6] & 0b00001111 | 0b01110000; 138 | 139 | let mut lsb = [0u8; 8]; 140 | // fill lsb with random bits 141 | rand::thread_rng().fill_bytes(&mut lsb); 142 | // set variant (RFC4122) 143 | lsb[0] = lsb[0] & 0b00111111 | 0b10000000; 144 | Self::from_bytes_unchecked(msb, lsb) 145 | } 146 | 147 | /// Creates a new UUID that can be used for uProtocol messages. 148 | /// 149 | /// # Panics 150 | /// 151 | /// if the system clock is set to an instant before the UNIX Epoch. 152 | /// 153 | /// # Examples 154 | /// 155 | /// ``` 156 | /// use up_rust::UUID; 157 | /// 158 | /// let uuid = UUID::build(); 159 | /// assert!(uuid.is_uprotocol_uuid()); 160 | /// ``` 161 | // [impl->dsn~uuid-spec~1] 162 | // [utest->dsn~uuid-spec~1] 163 | pub fn build() -> UUID { 164 | let duration_since_unix_epoch = SystemTime::UNIX_EPOCH 165 | .elapsed() 166 | .expect("current system time is set to a point in time before UNIX Epoch"); 167 | Self::build_for_timestamp(duration_since_unix_epoch) 168 | } 169 | 170 | /// Serializes this UUID to a hyphenated string as defined by 171 | /// [RFC 4122, Section 3](https://www.rfc-editor.org/rfc/rfc4122.html#section-3) 172 | /// using lower case characters. 173 | /// 174 | /// # Examples 175 | /// 176 | /// ```rust 177 | /// use up_rust::UUID; 178 | /// 179 | /// // timestamp = 1, ver = 0b0111 180 | /// let msb = 0x0000000000017000_u64; 181 | /// // variant = 0b10, random = 0x0010101010101a1a 182 | /// let lsb = 0x8010101010101a1a_u64; 183 | /// let uuid = UUID { msb, lsb, ..Default::default() }; 184 | /// assert_eq!(uuid.to_hyphenated_string(), "00000000-0001-7000-8010-101010101a1a"); 185 | /// ``` 186 | pub fn to_hyphenated_string(&self) -> String { 187 | let mut bytes = [0_u8; 16]; 188 | bytes[..8].clone_from_slice(self.msb.to_be_bytes().as_slice()); 189 | bytes[8..].clone_from_slice(self.lsb.to_be_bytes().as_slice()); 190 | let mut out_bytes = [0_u8; 36]; 191 | let out = 192 | uuid_simd::format_hyphenated(&bytes, Out::from_mut(&mut out_bytes), AsciiCase::Lower); 193 | String::from_utf8(out.to_vec()).unwrap() 194 | } 195 | 196 | /// Returns the point in time that this UUID has been created at. 197 | /// 198 | /// # Returns 199 | /// 200 | /// The number of milliseconds since UNIX EPOCH if this UUID is a uProtocol UUID, 201 | /// or [`Option::None`] otherwise. 202 | /// 203 | /// # Examples 204 | /// 205 | /// ```rust 206 | /// use up_rust::UUID; 207 | /// 208 | /// // timestamp = 0x018D548EA8E0 (Monday, 29 January 2024, 9:30:52 AM GMT) 209 | /// // ver = 0b0111 210 | /// let msb = 0x018D548EA8E07000u64; 211 | /// // variant = 0b10 212 | /// let lsb = 0x8000000000000000u64; 213 | /// let creation_time = UUID { msb, lsb, ..Default::default() }.get_time(); 214 | /// assert_eq!(creation_time.unwrap(), 0x018D548EA8E0_u64); 215 | /// 216 | /// // timestamp = 1, (invalid) ver = 0b1100 217 | /// let msb = 0x000000000001C000u64; 218 | /// // variant = 0b10 219 | /// let lsb = 0x8000000000000000u64; 220 | /// let creation_time = UUID { msb, lsb, ..Default::default() }.get_time(); 221 | /// assert!(creation_time.is_none()); 222 | /// ``` 223 | // [impl->dsn~uuid-spec~1] 224 | // [utest->dsn~uuid-spec~1] 225 | pub fn get_time(&self) -> Option { 226 | if self.is_uprotocol_uuid() { 227 | // the timestamp is contained in the 48 most significant bits 228 | Some(self.msb >> 16) 229 | } else { 230 | None 231 | } 232 | } 233 | 234 | /// Checks if this is a valid uProtocol UUID. 235 | /// 236 | /// # Returns 237 | /// 238 | /// `true` if this UUID meets the formal requirements defined by the 239 | /// [uProtocol spec](https://github.com/eclipse-uprotocol/uprotocol-spec). 240 | /// 241 | /// # Examples 242 | /// 243 | /// ```rust 244 | /// use up_rust::UUID; 245 | /// 246 | /// // timestamp = 1, ver = 0b0111 247 | /// let msb = 0x0000000000017000u64; 248 | /// // variant = 0b10 249 | /// let lsb = 0x8000000000000000u64; 250 | /// assert!(UUID { msb, lsb, ..Default::default() }.is_uprotocol_uuid()); 251 | /// 252 | /// // timestamp = 1, (invalid) ver = 0b1100 253 | /// let msb = 0x000000000001C000u64; 254 | /// // variant = 0b10 255 | /// let lsb = 0x8000000000000000u64; 256 | /// assert!(!UUID { msb, lsb, ..Default::default() }.is_uprotocol_uuid()); 257 | /// 258 | /// // timestamp = 1, ver = 0b0111 259 | /// let msb = 0x0000000000017000u64; 260 | /// // (invalid) variant = 0b01 261 | /// let lsb = 0x4000000000000000u64; 262 | /// assert!(!UUID { msb, lsb, ..Default::default() }.is_uprotocol_uuid()); 263 | /// ``` 264 | // [impl->dsn~uuid-spec~1] 265 | // [utest->dsn~uuid-spec~1] 266 | pub fn is_uprotocol_uuid(&self) -> bool { 267 | is_correct_version(self.msb) && is_correct_variant(self.lsb) 268 | } 269 | } 270 | 271 | impl Eq for UUID {} 272 | 273 | impl Hash for UUID { 274 | fn hash(&self, state: &mut H) { 275 | let bytes = (self.msb, self.lsb); 276 | bytes.hash(state) 277 | } 278 | } 279 | 280 | impl From for String { 281 | fn from(value: UUID) -> Self { 282 | Self::from(&value) 283 | } 284 | } 285 | 286 | impl From<&UUID> for String { 287 | fn from(value: &UUID) -> Self { 288 | value.to_hyphenated_string() 289 | } 290 | } 291 | 292 | impl FromStr for UUID { 293 | type Err = UuidConversionError; 294 | 295 | /// Parses a string into a UUID. 296 | /// 297 | /// # Returns 298 | /// 299 | /// a uProtocol [`UUID`] based on the bytes encoded in the string. 300 | /// 301 | /// # Errors 302 | /// 303 | /// Returns an error 304 | /// * if the given string does not represent a UUID as defined by 305 | /// [RFC 4122, Section 3](https://www.rfc-editor.org/rfc/rfc4122.html#section-3), or 306 | /// * if the bytes encoded in the string contain an invalid version and/or variant identifier. 307 | /// 308 | /// # Examples 309 | /// 310 | /// ```rust 311 | /// use up_rust::UUID; 312 | /// 313 | /// // parsing a valid uProtocol UUID succeeds 314 | /// let parsing_attempt = "00000000-0001-7000-8010-101010101a1A".parse::(); 315 | /// assert!(parsing_attempt.is_ok()); 316 | /// let uuid = parsing_attempt.unwrap(); 317 | /// assert!(uuid.is_uprotocol_uuid()); 318 | /// assert_eq!(uuid.msb, 0x0000000000017000_u64); 319 | /// assert_eq!(uuid.lsb, 0x8010101010101a1a_u64); 320 | /// 321 | /// // parsing an invalid UUID fails 322 | /// assert!("a1a2a3a4-b1b2-c1c2-d1d2-d3d4d5d6d7d8" 323 | /// .parse::() 324 | /// .is_err()); 325 | /// 326 | /// // parsing a string that is not a UUID fails 327 | /// assert!("this-is-not-a-UUID" 328 | /// .parse::() 329 | /// .is_err()); 330 | /// ``` 331 | fn from_str(uuid_str: &str) -> Result { 332 | let mut uuid = [0u8; 16]; 333 | uuid_simd::parse_hyphenated(uuid_str.as_bytes(), Out::from_mut(&mut uuid)) 334 | .map_err(|err| UuidConversionError::new(err.to_string())) 335 | .and_then(|bytes| UUID::from_bytes(bytes)) 336 | } 337 | } 338 | 339 | #[cfg(test)] 340 | mod tests { 341 | 342 | use protobuf::Message; 343 | 344 | use super::*; 345 | 346 | // [utest->dsn~uuid-spec~1] 347 | #[test] 348 | fn test_from_u64_pair() { 349 | // timestamp = 1, ver = 0b0111 350 | let msb = 0x0000000000017000_u64; 351 | // variant = 0b10 352 | let lsb = 0x8000000000000000_u64; 353 | let conversion_attempt = UUID::from_u64_pair(msb, lsb); 354 | assert!(conversion_attempt.is_ok()); 355 | let uuid = conversion_attempt.unwrap(); 356 | assert!(uuid.is_uprotocol_uuid()); 357 | assert_eq!(uuid.get_time(), Some(0x1_u64)); 358 | 359 | // timestamp = 1, (invalid) ver = 0b0000 360 | let msb = 0x0000000000010000_u64; 361 | // variant= 0b10 362 | let lsb = 0x80000000000000ab_u64; 363 | assert!(UUID::from_u64_pair(msb, lsb).is_err()); 364 | 365 | // timestamp = 1, ver = 0b0111 366 | let msb = 0x0000000000017000_u64; 367 | // (invalid) variant= 0b00 368 | let lsb = 0x00000000000000ab_u64; 369 | assert!(UUID::from_u64_pair(msb, lsb).is_err()); 370 | } 371 | 372 | // [utest->dsn~uuid-spec~1] 373 | #[test] 374 | fn test_from_bytes() { 375 | // timestamp = 1, ver = 0b0111, variant = 0b10 376 | let bytes: [u8; 16] = [ 377 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x70, 0x00, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 378 | 0x00, 0x00, 379 | ]; 380 | let conversion_attempt = UUID::from_bytes(&bytes); 381 | assert!(conversion_attempt.is_ok()); 382 | let uuid = conversion_attempt.unwrap(); 383 | assert!(uuid.is_uprotocol_uuid()); 384 | assert_eq!(uuid.get_time(), Some(0x1_u64)); 385 | } 386 | 387 | #[test] 388 | fn test_into_string() { 389 | // timestamp = 1, ver = 0b0111 390 | let msb = 0x0000000000017000_u64; 391 | // variant = 0b10, random = 0x0010101010101a1a 392 | let lsb = 0x8010101010101a1a_u64; 393 | let uuid = UUID { 394 | msb, 395 | lsb, 396 | ..Default::default() 397 | }; 398 | 399 | assert_eq!(String::from(&uuid), "00000000-0001-7000-8010-101010101a1a"); 400 | assert_eq!(String::from(uuid), "00000000-0001-7000-8010-101010101a1a"); 401 | } 402 | 403 | // [utest->req~uuid-proto~1] 404 | #[test] 405 | fn test_protobuf_serialization() { 406 | let uuid = UUID::build(); 407 | let bytes = uuid.write_to_bytes().unwrap(); 408 | let deserialized_uuid = UUID::parse_from_bytes(bytes.as_slice()).unwrap(); 409 | assert_eq!(uuid, deserialized_uuid); 410 | } 411 | } 412 | -------------------------------------------------------------------------------- /tools/coverage.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # This runs cargo test and creates test coverage data, as well as a test result report, currently this requires the 'nightly' rust toolchain. 3 | # Run this in the project root, and cargo2junit and grcov binaries (do `cargo install cargo2junit grcov`) 4 | # Result files will be placed in ./reports 5 | 6 | PROJECT_NAME_UNDERSCORE="uprotocol_sdk" 7 | RUSTFLAGS="--cfg uuid_unstable -Zprofile -Ccodegen-units=1 -Copt-level=0 -Clink-dead-code -Coverflow-checks=off -Zpanic_abort_tests -Cpanic=abort" 8 | RUSTDOCFLAGS="-Cpanic=abort" 9 | TEMP=`mktemp --directory` 10 | 11 | 12 | mkdir -p $TEMP/reports 13 | 14 | cargo test $CARGO_OPTIONS -- -Z unstable-options --format json | cargo2junit > $TEMP/results.xml 15 | zip -0 $TEMP/ccov.zip `find . \( -name "$PROJECT_NAME_UNDERSCORE*.gc*" \) -print` 16 | grcov $TEMP/ccov.zip -s . -t lcov --llvm --ignore-not-existing --ignore "/*" --ignore "tests/*" --ignore "target/*" -o $TEMP/lcov.info 17 | genhtml $TEMP/lcov.info --output-directory $TEMP/out 18 | 19 | rm $TEMP/ccov.zip 20 | mv -r $TEMP/* ./reports 21 | rm -fr $TEMP -------------------------------------------------------------------------------- /tools/fmt_clippy_doc.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | echo "Running cargo fmt --check" 4 | cargo fmt --all --check 5 | 6 | echo "" 7 | echo "Running cargo clippy" 8 | cargo clippy --all-targets --all-features --no-deps -- -W warnings -D warnings 9 | 10 | echo "" 11 | echo "Running cargo doc" 12 | cargo doc --no-deps --all-features 13 | -------------------------------------------------------------------------------- /tools/generate_test_coverage_report.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | ################################################################################ 4 | # Copyright (c) 2023 Contributors to the Eclipse Foundation 5 | # 6 | # See the NOTICE file(s) distributed with this work for additional 7 | # information regarding copyright ownership. 8 | # 9 | # This program and the accompanying materials are made available under the 10 | # terms of the Apache License Version 2.0 which is available at 11 | # https://www.apache.org/licenses/LICENSE-2.0 12 | # 13 | # SPDX-License-Identifier: Apache-2.0 14 | ################################################################################ 15 | 16 | # we want both html and lcov output formats 17 | cargo tarpaulin -o lcov -o html --output-dir target/tarpaulin 18 | --------------------------------------------------------------------------------