Compare commits
175 Commits
Author | SHA1 | Date |
---|---|---|
|
fa0aadb31d | |
|
3590617290 | |
|
5345ee3edc | |
|
2bd15dfe9d | |
|
897cd85c40 | |
|
09661ddaf7 | |
|
9b38aead8d | |
|
f9dde9daae | |
|
0f5f748685 | |
|
bf45f01602 | |
|
1f94433c0a | |
|
2f57c3ce36 | |
|
13c36fdbfe | |
|
a59c3f55a0 | |
|
1978ae16aa | |
|
c4a5443d19 | |
|
48b7e33cc2 | |
|
b719f70cee | |
|
22f54770c9 | |
|
a73743b06a | |
|
721c42c27c | |
|
933edbc883 | |
|
109d02d66a | |
|
957ef1aa6d | |
|
e001b9cd8b | |
|
e19431fc06 | |
|
9e10eaadf0 | |
|
4f265cd142 | |
|
77232941e4 | |
|
1e147eb560 | |
|
4a86973f22 | |
|
38469b245d | |
|
20fd82a651 | |
|
4acd162cb7 | |
|
c8454bce97 | |
|
eee1f82f8d | |
|
6148f2efd3 | |
|
bc1fc8e79c | |
|
ee987c9955 | |
|
e3a86d642a | |
|
e28424e198 | |
|
c6a84b3c5a | |
|
7c2ff41960 | |
|
c380078bf4 | |
|
b8487af97c | |
|
e2066527e6 | |
|
8dde763c06 | |
|
6848cb15bd | |
|
cb9d9ddf16 | |
|
9882d5c521 | |
|
8b77dd8c33 | |
|
6653e46059 | |
|
87c98aa26e | |
|
24aec9320c | |
|
515fa81a77 | |
|
2c5933bad4 | |
|
1f29640b50 | |
|
5a9f64868d | |
|
6af7d1e8ec | |
|
32b3134981 | |
|
785786f251 | |
|
2742de5307 | |
|
2b66c959d1 | |
|
ae83a69f7f | |
|
ba798f30cb | |
|
6d8e78bf99 | |
|
f3d106b3b1 | |
|
7f538a3f37 | |
|
70e3b6681b | |
|
65a4782853 | |
|
82f08f85ea | |
|
66b9bfde1b | |
|
127d0cec4b | |
|
e16e9667cf | |
|
05807fdf28 | |
|
8928803e67 | |
|
d987002173 | |
|
1e89203cbc | |
|
9480a0e944 | |
|
0741f2bf28 | |
|
62b895c025 | |
|
934915d910 | |
|
1e00f6fe04 | |
|
0aeebac010 | |
|
5cc2fddddd | |
|
733d568591 | |
|
6074b4db7e | |
|
b540542040 | |
|
c4e8780c79 | |
|
ca3ba3b88c | |
|
96c69d99da | |
|
211792f0f4 | |
|
2cae3f0b36 | |
|
bcb8363deb | |
|
589db8e5be | |
|
cd98c6705e | |
|
f6b45d1af7 | |
|
e4d3370656 | |
|
432259bd26 | |
|
12230429b8 | |
|
ceb034b813 | |
|
dc81cf6cad | |
|
1a9d0d46fc | |
|
c1715d75a4 | |
|
d45811604d | |
|
bf21c52869 | |
|
c57be0b99b | |
|
362492fa9a | |
|
4979c44532 | |
|
69f16a5144 | |
|
538b647804 | |
|
9055d71fb2 | |
|
51b49f1335 | |
|
935234a9cb | |
|
303ed92545 | |
|
a1358ea5b7 | |
|
dfe2bcce13 | |
|
62ca1cd7da | |
|
da40d3d563 | |
|
364ad7b41e | |
|
4cb40f98a4 | |
|
7f63bd74ec | |
|
6f5a767f19 | |
|
609c036ce5 | |
|
570a9ea488 | |
|
6ac6534f16 | |
|
579665f226 | |
|
5e5aca54be | |
|
d69ab904d1 | |
|
b5d95741c2 | |
|
099200c657 | |
|
6ec93db965 | |
|
80dad09f56 | |
|
3770f99e77 | |
|
0f9c9bd08f | |
|
321f04ea09 | |
|
3f9f024a6d | |
|
a9fbb8e0ea | |
|
e6500338c6 | |
|
4e3c023e4b | |
|
30367b7e54 | |
|
c253dfe06d | |
|
502c6d8ef7 | |
|
47d9c272b2 | |
|
6f58d63e72 | |
|
6133a6e67f | |
|
e330c6cc23 | |
|
6658a809e8 | |
|
cd8cddac18 | |
|
d1281e7fea | |
|
c4305e0713 | |
|
39af2d7ad0 | |
|
c926188d78 | |
|
1858a1caa5 | |
|
fbadb3300a | |
|
500f8e76e6 | |
|
2e66f6a46f | |
|
5e0067bbbc | |
|
bd7559c369 | |
|
71f5c38a05 | |
|
35b37e5a45 | |
|
4a70d506de | |
|
4700fa267f | |
|
3a56fcc641 | |
|
94a134c44e | |
|
25af32ee3a | |
|
e87605734e | |
|
fc3790a7bd | |
|
57f42cf753 | |
|
f6770b4591 | |
|
7c8206b4e1 | |
|
046fabc55b | |
|
f294cec0fe | |
|
b832b6bcf4 | |
|
2b91ee8d7a |
|
@ -1,66 +0,0 @@
|
||||||
name: Master
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- master
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
build:
|
|
||||||
name: Run tests on ${{ matrix.toolchain }} ${{ matrix.target }}
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
strategy:
|
|
||||||
matrix:
|
|
||||||
toolchain:
|
|
||||||
- stable
|
|
||||||
- nightly
|
|
||||||
target:
|
|
||||||
- x86_64-unknown-linux-gnu
|
|
||||||
- x86_64-unknown-linux-musl
|
|
||||||
- wasm32-unknown-unknown
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v2
|
|
||||||
- run: sudo apt-get update
|
|
||||||
if: matrix.target == 'x86_64-unknown-linux-musl'
|
|
||||||
- run: sudo apt-get install -y musl musl-dev musl-tools
|
|
||||||
if: matrix.target == 'x86_64-unknown-linux-musl'
|
|
||||||
- name: Cache cargo registry
|
|
||||||
uses: actions/cache@v1
|
|
||||||
with:
|
|
||||||
path: ~/.cargo/registry
|
|
||||||
key: ${{ runner.os }}-cargo-registry-${{ hashFiles('**/Cargo.lock') }}
|
|
||||||
- name: Cache cargo index
|
|
||||||
uses: actions/cache@v1
|
|
||||||
with:
|
|
||||||
path: ~/.cargo/git
|
|
||||||
key: ${{ runner.os }}-cargo-index-${{ hashFiles('**/Cargo.lock') }}
|
|
||||||
- name: Cache cargo build
|
|
||||||
uses: actions/cache@v1
|
|
||||||
with:
|
|
||||||
path: target
|
|
||||||
key: ${{ runner.os }}-cargo-build-target-${{ matrix.toolchain }}-${{ matrix.target }}-${{ hashFiles('**/Cargo.lock') }}
|
|
||||||
- uses: actions-rs/toolchain@v1
|
|
||||||
with:
|
|
||||||
toolchain: ${{ matrix.toolchain }}
|
|
||||||
target: ${{ matrix.target }}
|
|
||||||
override: true
|
|
||||||
- uses: actions-rs/cargo@v1
|
|
||||||
if: matrix.target != 'wasm32-unknown-unknown'
|
|
||||||
with:
|
|
||||||
command: build
|
|
||||||
toolchain: ${{ matrix.toolchain }}
|
|
||||||
args: --target ${{ matrix.target }} --all
|
|
||||||
- uses: actions-rs/cargo@v1
|
|
||||||
if: matrix.target != 'wasm32-unknown-unknown'
|
|
||||||
with:
|
|
||||||
command: test
|
|
||||||
toolchain: ${{ matrix.toolchain }}
|
|
||||||
args: --target ${{ matrix.target }} --all
|
|
||||||
|
|
||||||
# If wasm, then we don't need to compile --all
|
|
||||||
- uses: actions-rs/cargo@v1
|
|
||||||
if: matrix.target == 'wasm32-unknown-unknown'
|
|
||||||
with:
|
|
||||||
command: build
|
|
||||||
toolchain: ${{ matrix.toolchain }}
|
|
||||||
args: --target wasm32-unknown-unknown
|
|
|
@ -1,60 +0,0 @@
|
||||||
name: Pull Request
|
|
||||||
|
|
||||||
on:
|
|
||||||
pull_request:
|
|
||||||
branches:
|
|
||||||
- master
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
build:
|
|
||||||
name: Run tests
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v2
|
|
||||||
- name: Cache cargo registry
|
|
||||||
uses: actions/cache@v1
|
|
||||||
with:
|
|
||||||
path: ~/.cargo/registry
|
|
||||||
key: ${{ runner.os }}-cargo-registry-${{ hashFiles('**/Cargo.lock') }}
|
|
||||||
- name: Cache cargo index
|
|
||||||
uses: actions/cache@v1
|
|
||||||
with:
|
|
||||||
path: ~/.cargo/git
|
|
||||||
key: ${{ runner.os }}-cargo-index-${{ hashFiles('**/Cargo.lock') }}
|
|
||||||
- name: Cache cargo build
|
|
||||||
uses: actions/cache@v1
|
|
||||||
with:
|
|
||||||
path: target
|
|
||||||
key: ${{ runner.os }}-cargo-build-target-${{ hashFiles('**/Cargo.lock') }}
|
|
||||||
- uses: actions-rs/toolchain@v1
|
|
||||||
with:
|
|
||||||
profile: minimal
|
|
||||||
toolchain: stable
|
|
||||||
- uses: actions-rs/cargo@v1
|
|
||||||
with:
|
|
||||||
command: build
|
|
||||||
toolchain: stable
|
|
||||||
target: x86_64-unknown-linux-gnu
|
|
||||||
args: --all
|
|
||||||
- uses: actions-rs/cargo@v1
|
|
||||||
with:
|
|
||||||
command: test
|
|
||||||
toolchain: stable
|
|
||||||
target: x86_64-unknown-linux-gnu
|
|
||||||
args: --all
|
|
||||||
|
|
||||||
fmt:
|
|
||||||
name: Format check
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v2
|
|
||||||
- uses: actions-rs/toolchain@v1
|
|
||||||
with:
|
|
||||||
profile: minimal
|
|
||||||
toolchain: stable
|
|
||||||
override: true
|
|
||||||
components: rustfmt
|
|
||||||
- uses: actions-rs/cargo@v1
|
|
||||||
with:
|
|
||||||
command: fmt
|
|
||||||
args: --all -- --check
|
|
|
@ -0,0 +1,29 @@
|
||||||
|
name: Lints
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
lint:
|
||||||
|
name: Rust
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v2
|
||||||
|
- uses: actions-rs/toolchain@v1
|
||||||
|
with:
|
||||||
|
profile: minimal
|
||||||
|
toolchain: stable
|
||||||
|
override: true
|
||||||
|
components: clippy, rustfmt
|
||||||
|
- uses: actions-rs/cargo@v1
|
||||||
|
name: "Cargo fmt"
|
||||||
|
with:
|
||||||
|
command: fmt
|
||||||
|
args: --all -- --check
|
||||||
|
- uses: actions-rs/clippy-check@v1
|
||||||
|
name: "Cargo clippy"
|
||||||
|
with:
|
||||||
|
token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
args: --all-features
|
|
@ -0,0 +1,181 @@
|
||||||
|
name: Rust Tests
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
name: ${{ matrix.toolchain }} / ${{ matrix.target }}
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
toolchain:
|
||||||
|
- stable
|
||||||
|
- nightly
|
||||||
|
target:
|
||||||
|
- x86_64-unknown-linux-gnu
|
||||||
|
- x86_64-unknown-linux-musl
|
||||||
|
- wasm32-unknown-unknown
|
||||||
|
- wasm32-wasip1
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v2
|
||||||
|
|
||||||
|
# setup wasmedge
|
||||||
|
- run: curl -sSf https://raw.githubusercontent.com/WasmEdge/WasmEdge/master/utils/install.sh | sudo bash -s -- -p /usr/local
|
||||||
|
# Setup musl if needed
|
||||||
|
- run: sudo apt-get update
|
||||||
|
if: matrix.target == 'x86_64-unknown-linux-musl'
|
||||||
|
- run: sudo apt-get install -y musl musl-dev musl-tools cmake
|
||||||
|
if: matrix.target == 'x86_64-unknown-linux-musl'
|
||||||
|
# # Caching stuff
|
||||||
|
- uses: actions/cache@v2
|
||||||
|
with:
|
||||||
|
path: |
|
||||||
|
~/.cargo/bin/
|
||||||
|
~/.cargo/registry/index/
|
||||||
|
~/.cargo/registry/cache/
|
||||||
|
~/.cargo/git/db/
|
||||||
|
key: ${{ runner.os }}-cargo-deps-${{ hashFiles('**/Cargo.toml') }}
|
||||||
|
- uses: actions/cache@v2
|
||||||
|
with:
|
||||||
|
path: |
|
||||||
|
target/
|
||||||
|
key: ${{ runner.os }}-cargo-target-${{ matrix.toolchain }}-${{ matrix.target }}-${{ hashFiles('**/Cargo.toml') }}
|
||||||
|
- uses: actions-rs/toolchain@v1
|
||||||
|
with:
|
||||||
|
toolchain: ${{ matrix.toolchain }}
|
||||||
|
target: ${{ matrix.target }}
|
||||||
|
override: true
|
||||||
|
|
||||||
|
# If glibc, compile and test all
|
||||||
|
- uses: actions-rs/cargo@v1
|
||||||
|
name: "Build"
|
||||||
|
if: matrix.target == 'x86_64-unknown-linux-gnu'
|
||||||
|
with:
|
||||||
|
command: build
|
||||||
|
toolchain: ${{ matrix.toolchain }}
|
||||||
|
args: --target ${{ matrix.target }} --all-features
|
||||||
|
- uses: actions-rs/cargo@v1
|
||||||
|
name: "Test"
|
||||||
|
if: matrix.target == 'x86_64-unknown-linux-gnu'
|
||||||
|
with:
|
||||||
|
command: test
|
||||||
|
toolchain: ${{ matrix.toolchain }}
|
||||||
|
args: --target ${{ matrix.target }} --all-features
|
||||||
|
|
||||||
|
# If musl, compile and test all
|
||||||
|
- uses: actions-rs/cargo@v1
|
||||||
|
name: "Build"
|
||||||
|
if: matrix.target == 'x86_64-unknown-linux-musl'
|
||||||
|
with:
|
||||||
|
command: build
|
||||||
|
toolchain: ${{ matrix.toolchain }}
|
||||||
|
args: --target ${{ matrix.target }} --all-features
|
||||||
|
env:
|
||||||
|
CC: musl-gcc
|
||||||
|
CXX: g++
|
||||||
|
- uses: actions-rs/cargo@v1
|
||||||
|
name: "Test"
|
||||||
|
if: matrix.target == 'x86_64-unknown-linux-musl'
|
||||||
|
with:
|
||||||
|
command: test
|
||||||
|
toolchain: ${{ matrix.toolchain }}
|
||||||
|
args: --target ${{ matrix.target }} --all-features
|
||||||
|
env:
|
||||||
|
CC: musl-gcc
|
||||||
|
CXX: g++
|
||||||
|
|
||||||
|
- uses: actions-rs/cargo@v1
|
||||||
|
name: "Build"
|
||||||
|
if: matrix.target == 'wasm32-unknown-unknown'
|
||||||
|
with:
|
||||||
|
command: build
|
||||||
|
toolchain: ${{ matrix.toolchain }}
|
||||||
|
args: --target wasm32-unknown-unknown --features reqwest
|
||||||
|
- uses: actions-rs/cargo@v1
|
||||||
|
name: "Build"
|
||||||
|
if: matrix.target == 'wasm32-wasi'
|
||||||
|
with:
|
||||||
|
command: build
|
||||||
|
toolchain: ${{ matrix.toolchain }}
|
||||||
|
args: --target ${{ matrix.target }} --features "http-0-2-binding hyper-0-14 hyper_wasi"
|
||||||
|
- uses: actions-rs/cargo@v1
|
||||||
|
name: "Test"
|
||||||
|
if: matrix.target == 'wasm32-wasi'
|
||||||
|
with:
|
||||||
|
command: test
|
||||||
|
toolchain: ${{ matrix.toolchain }}
|
||||||
|
args: --target ${{ matrix.target }} --features "http-0-2-binding hyper-0-14 hyper_wasi"
|
||||||
|
env:
|
||||||
|
CARGO_TARGET_WASM32_WASI_RUNNER: wasmedge
|
||||||
|
# Build examples
|
||||||
|
- uses: actions-rs/cargo@v1
|
||||||
|
name: "Build wasi-example"
|
||||||
|
if: matrix.target == 'wasm32-wasi' && matrix.toolchain == 'stable'
|
||||||
|
with:
|
||||||
|
command: build
|
||||||
|
toolchain: ${{ matrix.toolchain }}
|
||||||
|
args: --target ${{ matrix.target }} --manifest-path ./example-projects/wasi-example/Cargo.toml
|
||||||
|
|
||||||
|
- uses: actions-rs/cargo@v1
|
||||||
|
name: "Build reqwest-wasm-example"
|
||||||
|
if: matrix.target == 'wasm32-unknown-unknown' && matrix.toolchain == 'stable'
|
||||||
|
with:
|
||||||
|
command: build
|
||||||
|
toolchain: ${{ matrix.toolchain }}
|
||||||
|
args: --target ${{ matrix.target }} --manifest-path ./example-projects/reqwest-wasm-example/Cargo.toml
|
||||||
|
|
||||||
|
- uses: actions-rs/cargo@v1
|
||||||
|
name: "Build rdkafka-example"
|
||||||
|
if: matrix.target == 'x86_64-unknown-linux-gnu' && matrix.toolchain == 'stable'
|
||||||
|
with:
|
||||||
|
command: build
|
||||||
|
toolchain: ${{ matrix.toolchain }}
|
||||||
|
args: --target ${{ matrix.target }} --manifest-path ./example-projects/rdkafka-example/Cargo.toml
|
||||||
|
|
||||||
|
- uses: actions-rs/cargo@v1
|
||||||
|
name: "Build actix-web-example"
|
||||||
|
if: matrix.target == 'x86_64-unknown-linux-gnu' && matrix.toolchain == 'stable'
|
||||||
|
with:
|
||||||
|
command: build
|
||||||
|
toolchain: ${{ matrix.toolchain }}
|
||||||
|
args: --target ${{ matrix.target }} --manifest-path ./example-projects/actix-web-example/Cargo.toml
|
||||||
|
|
||||||
|
- uses: actions-rs/cargo@v1
|
||||||
|
name: "Build warp-example"
|
||||||
|
if: matrix.target == 'x86_64-unknown-linux-gnu' && matrix.toolchain == 'stable'
|
||||||
|
with:
|
||||||
|
command: build
|
||||||
|
toolchain: ${{ matrix.toolchain }}
|
||||||
|
args: --target ${{ matrix.target }} --manifest-path ./example-projects/warp-example/Cargo.toml
|
||||||
|
|
||||||
|
- uses: actions-rs/cargo@v1
|
||||||
|
name: "Build axum-example"
|
||||||
|
if: matrix.target == 'x86_64-unknown-linux-gnu' && matrix.toolchain == 'stable'
|
||||||
|
with:
|
||||||
|
command: build
|
||||||
|
toolchain: ${{ matrix.toolchain }}
|
||||||
|
args: --target ${{ matrix.target }} --manifest-path ./example-projects/axum-example/Cargo.toml
|
||||||
|
|
||||||
|
- uses: actions-rs/cargo@v1
|
||||||
|
name: "Build poem-example"
|
||||||
|
if: matrix.target == 'x86_64-unknown-linux-gnu' && matrix.toolchain == 'stable'
|
||||||
|
with:
|
||||||
|
command: build
|
||||||
|
toolchain: ${{ matrix.toolchain }}
|
||||||
|
args: --target ${{ matrix.target }} --manifest-path ./example-projects/poem-example/Cargo.toml
|
||||||
|
|
||||||
|
- uses: actions-rs/cargo@v1
|
||||||
|
name: "Build nats-example"
|
||||||
|
if: matrix.target == 'x86_64-unknown-linux-gnu' && matrix.toolchain == 'stable'
|
||||||
|
with:
|
||||||
|
command: build
|
||||||
|
toolchain: ${{ matrix.toolchain }}
|
||||||
|
args: --target ${{ matrix.target }} --manifest-path ./example-projects/nats-example/Cargo.toml
|
||||||
|
|
|
@ -1,4 +1,6 @@
|
||||||
**/target
|
**/target
|
||||||
|
|
||||||
.idea
|
.idea
|
||||||
|
.vscode
|
||||||
|
.DS_Store
|
||||||
**/Cargo.lock
|
**/Cargo.lock
|
||||||
|
|
|
@ -39,7 +39,7 @@ cargo test --all-features --all
|
||||||
To build and open the documentation:
|
To build and open the documentation:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
cargo doc --lib --open
|
cargo doc --all-features --lib --open
|
||||||
```
|
```
|
||||||
|
|
||||||
Before performing the PR, once you have committed all changes, run:
|
Before performing the PR, once you have committed all changes, run:
|
||||||
|
|
123
Cargo.toml
123
Cargo.toml
|
@ -1,6 +1,6 @@
|
||||||
[package]
|
[package]
|
||||||
name = "cloudevents-sdk"
|
name = "cloudevents-sdk"
|
||||||
version = "0.1.0"
|
version = "0.8.0"
|
||||||
authors = ["Francesco Guardiani <francescoguard@gmail.com>"]
|
authors = ["Francesco Guardiani <francescoguard@gmail.com>"]
|
||||||
license-file = "LICENSE"
|
license-file = "LICENSE"
|
||||||
edition = "2018"
|
edition = "2018"
|
||||||
|
@ -11,42 +11,95 @@ repository = "https://github.com/cloudevents/sdk-rust"
|
||||||
exclude = [
|
exclude = [
|
||||||
".github/*"
|
".github/*"
|
||||||
]
|
]
|
||||||
|
categories = ["web-programming", "encoding", "data-structures"]
|
||||||
|
resolver = "2"
|
||||||
|
|
||||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
# Enable all features when building on docs.rs to show feature gated bindings
|
||||||
|
[package.metadata.docs.rs]
|
||||||
[dependencies]
|
all-features = true
|
||||||
serde = { version = "^1.0", features = ["derive"] }
|
rustdoc-args = ["--cfg", "docsrs"]
|
||||||
serde_json = "^1.0"
|
|
||||||
serde-value = "^0.6"
|
|
||||||
chrono = { version = "^0.4", features = ["serde"] }
|
|
||||||
delegate = "^0.4"
|
|
||||||
base64 = "^0.12"
|
|
||||||
url = { version = "^2.1", features = ["serde"] }
|
|
||||||
snafu = "^0.6"
|
|
||||||
lazy_static = "1.4.0"
|
|
||||||
|
|
||||||
[target."cfg(not(target_arch = \"wasm32\"))".dependencies]
|
|
||||||
hostname = "^0.3"
|
|
||||||
uuid = { version = "^0.8", features = ["v4"] }
|
|
||||||
|
|
||||||
[target.'cfg(target_arch = "wasm32")'.dependencies]
|
|
||||||
web-sys = { version = "^0.3", features = ["Window", "Location"] }
|
|
||||||
uuid = { version = "^0.8", features = ["v4", "wasm-bindgen"] }
|
|
||||||
|
|
||||||
[dev-dependencies]
|
|
||||||
rstest = "0.6"
|
|
||||||
claim = "0.3.1"
|
|
||||||
|
|
||||||
[lib]
|
[lib]
|
||||||
name = "cloudevents"
|
name = "cloudevents"
|
||||||
|
|
||||||
[workspace]
|
[features]
|
||||||
members = [
|
http-binding = ["async-trait", "bytes", "futures", "http"]
|
||||||
".",
|
http-0-2-binding = ["async-trait", "bytes", "futures", "http-0-2"]
|
||||||
"cloudevents-sdk-actix-web",
|
actix = ["actix-web", "actix-http", "async-trait", "bytes", "futures", "http-0-2"]
|
||||||
"cloudevents-sdk-reqwest"
|
reqwest = ["reqwest-lib", "async-trait", "bytes", "http", "uuid/js"]
|
||||||
]
|
rdkafka = ["rdkafka-lib", "bytes", "futures"]
|
||||||
exclude = [
|
warp = ["warp-lib", "bytes", "http-0-2", "http-body-util", "hyper-0-14"]
|
||||||
"example-projects/actix-web-example",
|
axum = ["bytes", "http", "hyper", "axum-lib", "http-body-util", "async-trait"]
|
||||||
"example-projects/reqwest-wasm-example"
|
poem = ["bytes", "http", "poem-lib", "hyper", "async-trait", "http-body-util", "futures"]
|
||||||
]
|
nats = ["nats-lib"]
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
serde = { version = "^1.0", features = ["derive"] }
|
||||||
|
serde_json = "^1.0"
|
||||||
|
chrono = { version = "^0.4", features = ["serde"] }
|
||||||
|
delegate-attr = "^0.3"
|
||||||
|
base64 = "^0.22"
|
||||||
|
url = { version = "^2.5", features = ["serde"] }
|
||||||
|
snafu = "^0.8"
|
||||||
|
bitflags = "^2.6"
|
||||||
|
uuid = { version = "1", features = ["v4"] }
|
||||||
|
|
||||||
|
# runtime optional deps
|
||||||
|
actix-web = { version = "4", optional = true }
|
||||||
|
actix-http = { version = "3", optional = true }
|
||||||
|
reqwest-lib = { version = "^0.12", default-features = false, features = ["rustls-tls"], optional = true, package = "reqwest" }
|
||||||
|
rdkafka-lib = { version = "^0.37", features = ["cmake-build"], optional = true, package = "rdkafka" }
|
||||||
|
warp-lib = { version = "^0.3", optional = true, package = "warp" }
|
||||||
|
async-trait = { version = "^0.1", optional = true }
|
||||||
|
bytes = { version = "^1.0", optional = true }
|
||||||
|
futures = { version = "^0.3", optional = true, features = ["compat"]}
|
||||||
|
http = { version = "1.2", optional = true}
|
||||||
|
http-0-2 = { version = "0.2", optional = true, package = "http"}
|
||||||
|
axum-lib = { version = "^0.8", optional = true, package="axum"}
|
||||||
|
http-body-util = {version = "^0.1", optional = true}
|
||||||
|
poem-lib = { version = "^3.1", optional = true, package = "poem" }
|
||||||
|
nats-lib = { version = "0.25.0", optional = true, package = "nats" }
|
||||||
|
|
||||||
|
[target."cfg(not(target_arch = \"wasm32\"))".dependencies]
|
||||||
|
hostname = "^0.4"
|
||||||
|
|
||||||
|
[target.'cfg(all(target_arch = "wasm32", target_os = "unknown"))'.dependencies]
|
||||||
|
web-sys = { version = "^0.3", features = ["Window", "Location"] }
|
||||||
|
|
||||||
|
[target.'cfg(not(target_os = "wasi"))'.dependencies]
|
||||||
|
hyper = { version = "^1.5", optional = true, package="hyper" }
|
||||||
|
hyper-0-14 = { version = "^0.14", optional = true, package = "hyper"}
|
||||||
|
|
||||||
|
[target.'cfg(all(target_arch = "wasm32", target_os = "wasi"))'.dependencies]
|
||||||
|
hyper_wasi = { version = "0.15", features = ["full"], optional = true }
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
|
rstest = "0.23"
|
||||||
|
claims = "0.8"
|
||||||
|
version-sync = "0.9.2"
|
||||||
|
serde_yaml = "^0.9"
|
||||||
|
rmp-serde = "1"
|
||||||
|
|
||||||
|
# runtime dev-deps
|
||||||
|
|
||||||
|
url = { version = "^2.1", features = ["serde"] }
|
||||||
|
serde_json = { version = "^1.0" }
|
||||||
|
chrono = { version = "^0.4", features = ["serde"] }
|
||||||
|
mockito = "0.31.1"
|
||||||
|
mime = "0.3"
|
||||||
|
|
||||||
|
|
||||||
|
[target.'cfg(not(target_os = "wasi"))'.dev-dependencies]
|
||||||
|
actix-rt = { version = "^2" }
|
||||||
|
tokio = { version = "^1.0", features = ["full"] }
|
||||||
|
tower = { version = "0.5", features = ["util"] }
|
||||||
|
|
||||||
|
[target.'cfg(all(target_arch = "wasm32", target_os = "wasi"))'.dev-dependencies]
|
||||||
|
tokio_wasi = { version = "1", features = [
|
||||||
|
"io-util",
|
||||||
|
"fs",
|
||||||
|
"net",
|
||||||
|
"time",
|
||||||
|
"rt",
|
||||||
|
"macros",
|
||||||
|
] }
|
||||||
|
|
|
@ -0,0 +1,7 @@
|
||||||
|
# Maintainers
|
||||||
|
|
||||||
|
Current active maintainers of this SDK:
|
||||||
|
|
||||||
|
- [Jim Crossley](https://github.com/jcrossley3)
|
||||||
|
- [Francesco Guardiani](https://github.com/slinkydeveloper)
|
||||||
|
- [Fabrizio Lazzaretti](https://github.com/Lazzaretti)
|
|
@ -0,0 +1,6 @@
|
||||||
|
admins:
|
||||||
|
- jcrossley3
|
||||||
|
- linuxbasic
|
||||||
|
- slinkydeveloper
|
||||||
|
- Lazzaretti
|
||||||
|
approvers:
|
86
README.md
86
README.md
|
@ -2,57 +2,70 @@
|
||||||
|
|
||||||
This project implements the [CloudEvents](https://cloudevents.io/) Spec for Rust.
|
This project implements the [CloudEvents](https://cloudevents.io/) Spec for Rust.
|
||||||
|
|
||||||
Note: This projecets is WIP under active development, hence all APIs are considered unstable.
|
Note: This project is WIP under active development, hence all APIs are considered unstable.
|
||||||
|
|
||||||
## Spec support
|
## Spec support
|
||||||
|
|
||||||
| | [v0.3](https://github.com/cloudevents/spec/tree/v0.3) | [v1.0](https://github.com/cloudevents/spec/tree/v1.0) |
|
| | [v0.3](https://github.com/cloudevents/spec/tree/v0.3) | [v1.0](https://github.com/cloudevents/spec/tree/v1.0) |
|
||||||
| :---------------------------: | :----------------------------------------------------------------------------: | :---------------------------------------------------------------------------------: |
|
| :---------------------------: | :----------------------------------------------------------------------------: | :---------------------------------------------------------------------------------: |
|
||||||
| CloudEvents Core | :heavy_check_mark: | :heavy_check_mark: |
|
| CloudEvents Core | ✓ | ✓ |
|
||||||
| AMQP Protocol Binding | :x: | :x: |
|
| AMQP Protocol Binding | ✕ | ✕ |
|
||||||
| AVRO Event Format | :x: | :x: |
|
| AVRO Event Format | ✕ | ✕ |
|
||||||
| HTTP Protocol Binding | :heavy_check_mark: | :heavy_check_mark: |
|
| HTTP Protocol Binding | ✓ | ✓ |
|
||||||
| JSON Event Format | :heavy_check_mark: | :heavy_check_mark: |
|
| JSON Event Format | ✓ | ✓ |
|
||||||
| Kafka Protocol Binding | :x: | :x: |
|
| Kafka Protocol Binding | ✓ | ✓ |
|
||||||
| MQTT Protocol Binding | :x: | :x: |
|
| MQTT Protocol Binding | ✕ | ✕ |
|
||||||
| NATS Protocol Binding | :x: | :x: |
|
| NATS Protocol Binding | ✓ | ✓ |
|
||||||
| Web hook | :x: | :x: |
|
| Web hook | ✕ | ✕ |
|
||||||
|
|
||||||
## Crates
|
## Crate Structure
|
||||||
|
|
||||||
* `cloudevents-sdk`: Provides Event data structure, JSON Event format implementation. This module is tested to work with GNU libc, WASM and musl toolchains.
|
The core modules include definitions for the `Event` and
|
||||||
* `cloudevents-sdk-actix-web`: Integration with [Actix Web](https://github.com/actix/actix-web).
|
`EventBuilder` data structures, JSON serialization rules, and a
|
||||||
* `cloudevents-sdk-reqwest`: Integration with [reqwest](https://github.com/seanmonstar/reqwest).
|
mechanism to support various Protocol Bindings, each of which is
|
||||||
|
enabled by a specific [feature flag]:
|
||||||
|
|
||||||
|
* `actix`: Integration with [actix](https://actix.rs/).
|
||||||
|
* `axum`: Integration with [axum](https://lib.rs/crates/axum).
|
||||||
|
* `warp`: Integration with [warp](https://github.com/seanmonstar/warp/).
|
||||||
|
* `reqwest`: Integration with [reqwest](https://github.com/seanmonstar/reqwest).
|
||||||
|
* `rdkafka`: Integration with [rdkafka](https://fede1024.github.io/rust-rdkafka).
|
||||||
|
* `nats`: Integration with [nats](https://github.com/nats-io/nats.rs)
|
||||||
|
|
||||||
|
This crate is continuously tested to work with GNU libc, WASM and musl
|
||||||
|
toolchains.
|
||||||
|
|
||||||
## Get Started
|
## Get Started
|
||||||
|
|
||||||
To get started, add the dependency to `Cargo.toml`:
|
To get started, add the dependency to `Cargo.toml`, optionally
|
||||||
|
enabling your Protocol Binding of choice:
|
||||||
|
|
||||||
```toml
|
```toml
|
||||||
cloudevents-sdk = "0.1.0"
|
[dependencies]
|
||||||
|
cloudevents-sdk = { version = "0.8.0" }
|
||||||
```
|
```
|
||||||
|
|
||||||
Now you can start creating events:
|
Now you can start creating events:
|
||||||
|
|
||||||
```rust
|
```rust
|
||||||
use cloudevents::EventBuilder;
|
use cloudevents::{EventBuilder, EventBuilderV10};
|
||||||
use url::Url;
|
use url::Url;
|
||||||
|
|
||||||
let event = EventBuilder::v03()
|
let event = EventBuilderV10::new()
|
||||||
.id("aaa")
|
.id("aaa")
|
||||||
.source(Url::parse("http://localhost").unwrap())
|
.source(Url::parse("http://localhost").unwrap())
|
||||||
.ty("example.demo")
|
.ty("example.demo")
|
||||||
.build();
|
.build()?;
|
||||||
```
|
```
|
||||||
|
|
||||||
Checkout the examples using our integrations with `actix-web` and `reqwest` to learn how to send and receive events:
|
Checkout the examples using our integrations to learn how to send and receive events:
|
||||||
|
|
||||||
* [Actix Web Example](example-projects/actix-web-example)
|
* [Actix Web Example](example-projects/actix-web-example)
|
||||||
|
* [Axum Example](example-projects/axum-example)
|
||||||
* [Reqwest/WASM Example](example-projects/reqwest-wasm-example)
|
* [Reqwest/WASM Example](example-projects/reqwest-wasm-example)
|
||||||
|
* [Kafka Example](example-projects/rdkafka-example)
|
||||||
## Development & Contributing
|
* [Warp Example](example-projects/warp-example)
|
||||||
|
* [NATS Example](example-projects/nats-example)
|
||||||
If you're interested in contributing to sdk-rust, look at [Contributing documentation](CONTRIBUTING.md)
|
|
||||||
|
|
||||||
## Community
|
## Community
|
||||||
|
|
||||||
|
@ -69,7 +82,30 @@ If you're interested in contributing to sdk-rust, look at [Contributing document
|
||||||
- Contact for additional information: Francesco Guardiani (`@slinkydeveloper`
|
- Contact for additional information: Francesco Guardiani (`@slinkydeveloper`
|
||||||
on slack).
|
on slack).
|
||||||
|
|
||||||
|
Each SDK may have its own unique processes, tooling and guidelines, common
|
||||||
|
governance related material can be found in the
|
||||||
|
[CloudEvents `community`](https://github.com/cloudevents/spec/tree/master/community)
|
||||||
|
directory. In particular, in there you will find information concerning
|
||||||
|
how SDK projects are
|
||||||
|
[managed](https://github.com/cloudevents/spec/blob/master/community/SDK-GOVERNANCE.md),
|
||||||
|
[guidelines](https://github.com/cloudevents/spec/blob/master/community/SDK-maintainer-guidelines.md)
|
||||||
|
for how PR reviews and approval, and our
|
||||||
|
[Code of Conduct](https://github.com/cloudevents/spec/blob/master/community/GOVERNANCE.md#additional-information)
|
||||||
|
information.
|
||||||
|
|
||||||
|
If there is a security concern with one of the CloudEvents specifications, or
|
||||||
|
with one of the project's SDKs, please send an email to
|
||||||
|
[cncf-cloudevents-security@lists.cncf.io](mailto:cncf-cloudevents-security@lists.cncf.io).
|
||||||
|
|
||||||
[Crates badge]: https://img.shields.io/crates/v/cloudevents-sdk.svg
|
[Crates badge]: https://img.shields.io/crates/v/cloudevents-sdk.svg
|
||||||
[crates.io]: https://crates.io/crates/cloudevents-sdk
|
[crates.io]: https://crates.io/crates/cloudevents-sdk
|
||||||
[Docs badge]: https://docs.rs/cloudevents-sdk/badge.svg
|
[Docs badge]: https://docs.rs/cloudevents-sdk/badge.svg
|
||||||
[docs.rs]: https://docs.rs/cloudevents-sdk
|
[docs.rs]: https://docs.rs/cloudevents-sdk
|
||||||
|
[feature flag]: https://doc.rust-lang.org/cargo/reference/manifest.html#the-features-section
|
||||||
|
|
||||||
|
## Additional SDK Resources
|
||||||
|
|
||||||
|
- [List of current active maintainers](MAINTAINERS.md)
|
||||||
|
- [How to contribute to the project](CONTRIBUTING.md)
|
||||||
|
- [SDK's License](LICENSE)
|
||||||
|
- [SDK's Release process](RELEASING.md)
|
||||||
|
|
|
@ -0,0 +1,10 @@
|
||||||
|
# How to create a Release
|
||||||
|
|
||||||
|
To create a new release, do the following:
|
||||||
|
- Bump the version in the README, lib.rs and cargo.toml
|
||||||
|
- Try to run `cargo test --all-features`, `cargo doc --all-features --lib` and
|
||||||
|
`cargo publish --dry-run`
|
||||||
|
- If none of the above commands fail, PR the changes and merge it
|
||||||
|
- Checkout `main` on your local machine and run `cargo publish`
|
||||||
|
- Once that is done, create the release in the Github UI (make sure it
|
||||||
|
creates the git tag as well) and that's it!
|
|
@ -1,23 +0,0 @@
|
||||||
[package]
|
|
||||||
name = "cloudevents-sdk-actix-web"
|
|
||||||
version = "0.1.0"
|
|
||||||
authors = ["Francesco Guardiani <francescoguard@gmail.com>"]
|
|
||||||
license-file = "../LICENSE"
|
|
||||||
edition = "2018"
|
|
||||||
description = "CloudEvents official Rust SDK - Actix-Web integration"
|
|
||||||
documentation = "https://docs.rs/cloudevents-sdk-actix-web"
|
|
||||||
repository = "https://github.com/cloudevents/sdk-rust"
|
|
||||||
|
|
||||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
|
||||||
|
|
||||||
[dependencies]
|
|
||||||
cloudevents-sdk = { version = "0.1.0", path = ".." }
|
|
||||||
actix-web = "2"
|
|
||||||
actix-rt = "1"
|
|
||||||
lazy_static = "1.4.0"
|
|
||||||
bytes = "^0.5"
|
|
||||||
futures = "^0.3"
|
|
||||||
serde_json = "^1.0"
|
|
||||||
|
|
||||||
[dev-dependencies]
|
|
||||||
url = { version = "^2.1", features = ["serde"] }
|
|
|
@ -1,70 +0,0 @@
|
||||||
use actix_web::http::header;
|
|
||||||
use actix_web::http::{HeaderName, HeaderValue};
|
|
||||||
use cloudevents::event::SpecVersion;
|
|
||||||
use lazy_static::lazy_static;
|
|
||||||
use std::collections::HashMap;
|
|
||||||
use std::str::FromStr;
|
|
||||||
|
|
||||||
macro_rules! unwrap_optional_header {
|
|
||||||
($headers:expr, $name:expr) => {
|
|
||||||
$headers
|
|
||||||
.get::<&'static HeaderName>(&$name)
|
|
||||||
.map(|a| header_value_to_str!(a))
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
macro_rules! header_value_to_str {
|
|
||||||
($header_value:expr) => {
|
|
||||||
$header_value
|
|
||||||
.to_str()
|
|
||||||
.map_err(|e| cloudevents::message::Error::Other {
|
|
||||||
source: Box::new(e),
|
|
||||||
})
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
macro_rules! str_to_header_value {
|
|
||||||
($header_value:expr) => {
|
|
||||||
HeaderValue::from_str($header_value).map_err(|e| cloudevents::message::Error::Other {
|
|
||||||
source: Box::new(e),
|
|
||||||
})
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
macro_rules! str_name_to_header {
|
|
||||||
($attribute:expr) => {
|
|
||||||
HeaderName::from_str($attribute).map_err(|e| cloudevents::message::Error::Other {
|
|
||||||
source: Box::new(e),
|
|
||||||
})
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
macro_rules! attribute_name_to_header {
|
|
||||||
($attribute:expr) => {
|
|
||||||
str_name_to_header!(&["ce-", $attribute].concat())
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
fn attributes_to_headers(
|
|
||||||
map: &HashMap<SpecVersion, &'static [&'static str]>,
|
|
||||||
) -> HashMap<&'static str, HeaderName> {
|
|
||||||
map.values()
|
|
||||||
.flat_map(|s| s.iter())
|
|
||||||
.map(|s| {
|
|
||||||
if *s == "datacontenttype" {
|
|
||||||
(*s, header::CONTENT_TYPE)
|
|
||||||
} else {
|
|
||||||
(*s, attribute_name_to_header!(s).unwrap())
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.collect()
|
|
||||||
}
|
|
||||||
|
|
||||||
lazy_static! {
|
|
||||||
pub(crate) static ref ATTRIBUTES_TO_HEADERS: HashMap<&'static str, HeaderName> =
|
|
||||||
attributes_to_headers(&cloudevents::event::SPEC_VERSION_ATTRIBUTES);
|
|
||||||
pub(crate) static ref SPEC_VERSION_HEADER: HeaderName =
|
|
||||||
HeaderName::from_static("ce-specversion");
|
|
||||||
pub(crate) static ref CLOUDEVENTS_JSON_HEADER: HeaderValue =
|
|
||||||
HeaderValue::from_static("application/cloudevents+json");
|
|
||||||
}
|
|
|
@ -1,9 +0,0 @@
|
||||||
#[macro_use]
|
|
||||||
mod headers;
|
|
||||||
mod server_request;
|
|
||||||
mod server_response;
|
|
||||||
|
|
||||||
pub use server_request::request_to_event;
|
|
||||||
pub use server_request::HttpRequestDeserializer;
|
|
||||||
pub use server_response::event_to_response;
|
|
||||||
pub use server_response::HttpResponseSerializer;
|
|
|
@ -1,172 +0,0 @@
|
||||||
use super::headers;
|
|
||||||
use actix_web::http::HeaderName;
|
|
||||||
use actix_web::web::{Bytes, BytesMut};
|
|
||||||
use actix_web::{web, HttpMessage, HttpRequest};
|
|
||||||
use cloudevents::event::SpecVersion;
|
|
||||||
use cloudevents::message::{
|
|
||||||
BinaryDeserializer, BinarySerializer, Encoding, MessageAttributeValue, MessageDeserializer,
|
|
||||||
Result, StructuredDeserializer, StructuredSerializer,
|
|
||||||
};
|
|
||||||
use cloudevents::{message, Event};
|
|
||||||
use futures::StreamExt;
|
|
||||||
use std::convert::TryFrom;
|
|
||||||
|
|
||||||
/// Wrapper for [`HttpRequest`] that implements [`MessageDeserializer`] trait
|
|
||||||
pub struct HttpRequestDeserializer<'a> {
|
|
||||||
req: &'a HttpRequest,
|
|
||||||
body: Bytes,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl HttpRequestDeserializer<'_> {
|
|
||||||
pub fn new(req: &HttpRequest, body: Bytes) -> HttpRequestDeserializer {
|
|
||||||
HttpRequestDeserializer { req, body }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'a> BinaryDeserializer for HttpRequestDeserializer<'a> {
|
|
||||||
fn deserialize_binary<R: Sized, V: BinarySerializer<R>>(self, mut visitor: V) -> Result<R> {
|
|
||||||
if self.encoding() != Encoding::BINARY {
|
|
||||||
return Err(message::Error::WrongEncoding {});
|
|
||||||
}
|
|
||||||
|
|
||||||
let spec_version = SpecVersion::try_from(
|
|
||||||
unwrap_optional_header!(self.req.headers(), headers::SPEC_VERSION_HEADER).unwrap()?,
|
|
||||||
)?;
|
|
||||||
|
|
||||||
visitor = visitor.set_spec_version(spec_version.clone())?;
|
|
||||||
|
|
||||||
let attributes = cloudevents::event::SPEC_VERSION_ATTRIBUTES
|
|
||||||
.get(&spec_version)
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
for (hn, hv) in
|
|
||||||
self.req.headers().iter().filter(|(hn, _)| {
|
|
||||||
headers::SPEC_VERSION_HEADER.ne(hn) && hn.as_str().starts_with("ce-")
|
|
||||||
})
|
|
||||||
{
|
|
||||||
let name = &hn.as_str()["ce-".len()..];
|
|
||||||
|
|
||||||
if attributes.contains(&name) {
|
|
||||||
visitor = visitor.set_attribute(
|
|
||||||
name,
|
|
||||||
MessageAttributeValue::String(String::from(header_value_to_str!(hv)?)),
|
|
||||||
)?
|
|
||||||
} else {
|
|
||||||
visitor = visitor.set_extension(
|
|
||||||
name,
|
|
||||||
MessageAttributeValue::String(String::from(header_value_to_str!(hv)?)),
|
|
||||||
)?
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(hv) = self.req.headers().get("content-type") {
|
|
||||||
visitor = visitor.set_attribute(
|
|
||||||
"datacontenttype",
|
|
||||||
MessageAttributeValue::String(String::from(header_value_to_str!(hv)?)),
|
|
||||||
)?
|
|
||||||
}
|
|
||||||
|
|
||||||
if self.body.len() != 0 {
|
|
||||||
visitor.end_with_data(self.body.to_vec())
|
|
||||||
} else {
|
|
||||||
visitor.end()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'a> StructuredDeserializer for HttpRequestDeserializer<'a> {
|
|
||||||
fn deserialize_structured<R: Sized, V: StructuredSerializer<R>>(self, visitor: V) -> Result<R> {
|
|
||||||
if self.encoding() != Encoding::STRUCTURED {
|
|
||||||
return Err(message::Error::WrongEncoding {});
|
|
||||||
}
|
|
||||||
visitor.set_structured_event(self.body.to_vec())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'a> MessageDeserializer for HttpRequestDeserializer<'a> {
|
|
||||||
fn encoding(&self) -> Encoding {
|
|
||||||
if self.req.content_type() == "application/cloudevents+json" {
|
|
||||||
Encoding::STRUCTURED
|
|
||||||
} else if self
|
|
||||||
.req
|
|
||||||
.headers()
|
|
||||||
.get::<&'static HeaderName>(&super::headers::SPEC_VERSION_HEADER)
|
|
||||||
.is_some()
|
|
||||||
{
|
|
||||||
Encoding::BINARY
|
|
||||||
} else {
|
|
||||||
Encoding::UNKNOWN
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Method to transform an incoming [`HttpRequest`] to [`Event`]
|
|
||||||
pub async fn request_to_event(
|
|
||||||
req: &HttpRequest,
|
|
||||||
mut payload: web::Payload,
|
|
||||||
) -> std::result::Result<Event, actix_web::error::Error> {
|
|
||||||
let mut bytes = BytesMut::new();
|
|
||||||
while let Some(item) = payload.next().await {
|
|
||||||
bytes.extend_from_slice(&item?);
|
|
||||||
}
|
|
||||||
MessageDeserializer::into_event(HttpRequestDeserializer::new(req, bytes.freeze()))
|
|
||||||
.map_err(actix_web::error::ErrorBadRequest)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use super::*;
|
|
||||||
use actix_web::test;
|
|
||||||
use url::Url;
|
|
||||||
|
|
||||||
use cloudevents::EventBuilder;
|
|
||||||
use serde_json::json;
|
|
||||||
use std::str::FromStr;
|
|
||||||
|
|
||||||
#[actix_rt::test]
|
|
||||||
async fn test_request() {
|
|
||||||
let expected = EventBuilder::new()
|
|
||||||
.id("0001")
|
|
||||||
.ty("example.test")
|
|
||||||
.source(Url::from_str("http://localhost").unwrap())
|
|
||||||
.extension("someint", "10")
|
|
||||||
.build();
|
|
||||||
|
|
||||||
let (req, payload) = test::TestRequest::post()
|
|
||||||
.header("ce-specversion", "1.0")
|
|
||||||
.header("ce-id", "0001")
|
|
||||||
.header("ce-type", "example.test")
|
|
||||||
.header("ce-source", "http://localhost")
|
|
||||||
.header("ce-someint", "10")
|
|
||||||
.to_http_parts();
|
|
||||||
|
|
||||||
let resp = request_to_event(&req, web::Payload(payload)).await.unwrap();
|
|
||||||
assert_eq!(expected, resp);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[actix_rt::test]
|
|
||||||
async fn test_request_with_full_data() {
|
|
||||||
let j = json!({"hello": "world"});
|
|
||||||
|
|
||||||
let expected = EventBuilder::new()
|
|
||||||
.id("0001")
|
|
||||||
.ty("example.test")
|
|
||||||
.source(Url::from_str("http://localhost").unwrap())
|
|
||||||
.data("application/json", j.clone())
|
|
||||||
.extension("someint", "10")
|
|
||||||
.build();
|
|
||||||
|
|
||||||
let (req, payload) = test::TestRequest::post()
|
|
||||||
.header("ce-specversion", "1.0")
|
|
||||||
.header("ce-id", "0001")
|
|
||||||
.header("ce-type", "example.test")
|
|
||||||
.header("ce-source", "http://localhost")
|
|
||||||
.header("ce-someint", "10")
|
|
||||||
.header("content-type", "application/json")
|
|
||||||
.set_json(&j)
|
|
||||||
.to_http_parts();
|
|
||||||
|
|
||||||
let resp = request_to_event(&req, web::Payload(payload)).await.unwrap();
|
|
||||||
assert_eq!(expected, resp);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,183 +0,0 @@
|
||||||
use super::headers;
|
|
||||||
use actix_web::dev::HttpResponseBuilder;
|
|
||||||
use actix_web::http::{HeaderName, HeaderValue};
|
|
||||||
use actix_web::HttpResponse;
|
|
||||||
use cloudevents::event::SpecVersion;
|
|
||||||
use cloudevents::message::{
|
|
||||||
BinaryDeserializer, BinarySerializer, MessageAttributeValue, Result, StructuredSerializer,
|
|
||||||
};
|
|
||||||
use cloudevents::Event;
|
|
||||||
use std::str::FromStr;
|
|
||||||
|
|
||||||
/// Wrapper for [`HttpResponseBuilder`] that implements [`StructuredSerializer`] and [`BinarySerializer`]
|
|
||||||
pub struct HttpResponseSerializer {
|
|
||||||
builder: HttpResponseBuilder,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl HttpResponseSerializer {
|
|
||||||
pub fn new(builder: HttpResponseBuilder) -> HttpResponseSerializer {
|
|
||||||
HttpResponseSerializer { builder }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl BinarySerializer<HttpResponse> for HttpResponseSerializer {
|
|
||||||
fn set_spec_version(mut self, spec_version: SpecVersion) -> Result<Self> {
|
|
||||||
self.builder.set_header(
|
|
||||||
headers::SPEC_VERSION_HEADER.clone(),
|
|
||||||
str_to_header_value!(spec_version.as_str())?,
|
|
||||||
);
|
|
||||||
Ok(self)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn set_attribute(mut self, name: &str, value: MessageAttributeValue) -> Result<Self> {
|
|
||||||
self.builder.set_header(
|
|
||||||
headers::ATTRIBUTES_TO_HEADERS.get(name).unwrap().clone(),
|
|
||||||
str_to_header_value!(value.to_string().as_str())?,
|
|
||||||
);
|
|
||||||
Ok(self)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn set_extension(mut self, name: &str, value: MessageAttributeValue) -> Result<Self> {
|
|
||||||
self.builder.set_header(
|
|
||||||
attribute_name_to_header!(name)?,
|
|
||||||
str_to_header_value!(value.to_string().as_str())?,
|
|
||||||
);
|
|
||||||
Ok(self)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn end_with_data(mut self, bytes: Vec<u8>) -> Result<HttpResponse> {
|
|
||||||
Ok(self.builder.body(bytes))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn end(mut self) -> Result<HttpResponse> {
|
|
||||||
Ok(self.builder.finish())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl StructuredSerializer<HttpResponse> for HttpResponseSerializer {
|
|
||||||
fn set_structured_event(mut self, bytes: Vec<u8>) -> Result<HttpResponse> {
|
|
||||||
Ok(self
|
|
||||||
.builder
|
|
||||||
.set_header(
|
|
||||||
actix_web::http::header::CONTENT_TYPE,
|
|
||||||
headers::CLOUDEVENTS_JSON_HEADER.clone(),
|
|
||||||
)
|
|
||||||
.body(bytes))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Method to fill an [`HttpResponseBuilder`] with an [`Event`]
|
|
||||||
pub async fn event_to_response(
|
|
||||||
event: Event,
|
|
||||||
response: HttpResponseBuilder,
|
|
||||||
) -> std::result::Result<HttpResponse, actix_web::error::Error> {
|
|
||||||
BinaryDeserializer::deserialize_binary(event, HttpResponseSerializer::new(response))
|
|
||||||
.map_err(actix_web::error::ErrorBadRequest)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use super::*;
|
|
||||||
use url::Url;
|
|
||||||
|
|
||||||
use actix_web::http::StatusCode;
|
|
||||||
use actix_web::test;
|
|
||||||
use cloudevents::EventBuilder;
|
|
||||||
use futures::TryStreamExt;
|
|
||||||
use serde_json::json;
|
|
||||||
use std::str::FromStr;
|
|
||||||
|
|
||||||
#[actix_rt::test]
|
|
||||||
async fn test_response() {
|
|
||||||
let input = EventBuilder::new()
|
|
||||||
.id("0001")
|
|
||||||
.ty("example.test")
|
|
||||||
.source(Url::from_str("http://localhost/").unwrap())
|
|
||||||
.extension("someint", "10")
|
|
||||||
.build();
|
|
||||||
|
|
||||||
let resp = event_to_response(input, HttpResponseBuilder::new(StatusCode::OK))
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
resp.headers()
|
|
||||||
.get("ce-specversion")
|
|
||||||
.unwrap()
|
|
||||||
.to_str()
|
|
||||||
.unwrap(),
|
|
||||||
"1.0"
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
resp.headers().get("ce-id").unwrap().to_str().unwrap(),
|
|
||||||
"0001"
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
resp.headers().get("ce-type").unwrap().to_str().unwrap(),
|
|
||||||
"example.test"
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
resp.headers().get("ce-source").unwrap().to_str().unwrap(),
|
|
||||||
"http://localhost/"
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
resp.headers().get("ce-someint").unwrap().to_str().unwrap(),
|
|
||||||
"10"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[actix_rt::test]
|
|
||||||
async fn test_response_with_full_data() {
|
|
||||||
let j = json!({"hello": "world"});
|
|
||||||
|
|
||||||
let input = EventBuilder::new()
|
|
||||||
.id("0001")
|
|
||||||
.ty("example.test")
|
|
||||||
.source(Url::from_str("http://localhost").unwrap())
|
|
||||||
.data("application/json", j.clone())
|
|
||||||
.extension("someint", "10")
|
|
||||||
.build();
|
|
||||||
|
|
||||||
let mut resp = event_to_response(input, HttpResponseBuilder::new(StatusCode::OK))
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
resp.headers()
|
|
||||||
.get("ce-specversion")
|
|
||||||
.unwrap()
|
|
||||||
.to_str()
|
|
||||||
.unwrap(),
|
|
||||||
"1.0"
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
resp.headers().get("ce-id").unwrap().to_str().unwrap(),
|
|
||||||
"0001"
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
resp.headers().get("ce-type").unwrap().to_str().unwrap(),
|
|
||||||
"example.test"
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
resp.headers().get("ce-source").unwrap().to_str().unwrap(),
|
|
||||||
"http://localhost/"
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
resp.headers()
|
|
||||||
.get("content-type")
|
|
||||||
.unwrap()
|
|
||||||
.to_str()
|
|
||||||
.unwrap(),
|
|
||||||
"application/json"
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
resp.headers().get("ce-someint").unwrap().to_str().unwrap(),
|
|
||||||
"10"
|
|
||||||
);
|
|
||||||
|
|
||||||
let bytes = test::load_stream(resp.take_body().into_stream())
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
assert_eq!(j.to_string().as_bytes(), bytes.as_ref())
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,27 +0,0 @@
|
||||||
[package]
|
|
||||||
name = "cloudevents-sdk-reqwest"
|
|
||||||
version = "0.1.0"
|
|
||||||
authors = ["Francesco Guardiani <francescoguard@gmail.com>"]
|
|
||||||
license-file = "../LICENSE"
|
|
||||||
edition = "2018"
|
|
||||||
description = "CloudEvents official Rust SDK - Reqwest integration"
|
|
||||||
documentation = "https://docs.rs/cloudevents-sdk-reqwest"
|
|
||||||
repository = "https://github.com/cloudevents/sdk-rust"
|
|
||||||
|
|
||||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
|
||||||
|
|
||||||
[dependencies]
|
|
||||||
cloudevents-sdk = { version = "0.1.0", path = ".." }
|
|
||||||
lazy_static = "1.4.0"
|
|
||||||
bytes = "^0.5"
|
|
||||||
serde_json = "^1.0"
|
|
||||||
|
|
||||||
[dependencies.reqwest]
|
|
||||||
version = "0.10.4"
|
|
||||||
default-features = false
|
|
||||||
features = ["rustls-tls"]
|
|
||||||
|
|
||||||
[dev-dependencies]
|
|
||||||
mockito = "0.25.1"
|
|
||||||
tokio = { version = "^0.2", features = ["full"] }
|
|
||||||
url = { version = "^2.1" }
|
|
|
@ -1,172 +0,0 @@
|
||||||
use super::headers;
|
|
||||||
use cloudevents::event::SpecVersion;
|
|
||||||
use cloudevents::message::{
|
|
||||||
BinaryDeserializer, BinarySerializer, MessageAttributeValue, Result, StructuredSerializer,
|
|
||||||
};
|
|
||||||
use cloudevents::Event;
|
|
||||||
use reqwest::RequestBuilder;
|
|
||||||
use std::str::FromStr;
|
|
||||||
|
|
||||||
/// Wrapper for [`RequestBuilder`] that implements [`StructuredSerializer`] & [`BinarySerializer`] traits
|
|
||||||
pub struct RequestSerializer {
|
|
||||||
req: RequestBuilder,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl RequestSerializer {
|
|
||||||
pub fn new(req: RequestBuilder) -> RequestSerializer {
|
|
||||||
RequestSerializer { req }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl BinarySerializer<RequestBuilder> for RequestSerializer {
|
|
||||||
fn set_spec_version(mut self, spec_version: SpecVersion) -> Result<Self> {
|
|
||||||
self.req = self
|
|
||||||
.req
|
|
||||||
.header(headers::SPEC_VERSION_HEADER.clone(), spec_version.as_str());
|
|
||||||
Ok(self)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn set_attribute(mut self, name: &str, value: MessageAttributeValue) -> Result<Self> {
|
|
||||||
self.req = self.req.header(
|
|
||||||
headers::ATTRIBUTES_TO_HEADERS.get(name).unwrap().clone(),
|
|
||||||
value.to_string(),
|
|
||||||
);
|
|
||||||
Ok(self)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn set_extension(mut self, name: &str, value: MessageAttributeValue) -> Result<Self> {
|
|
||||||
self.req = self
|
|
||||||
.req
|
|
||||||
.header(attribute_name_to_header!(name)?, value.to_string());
|
|
||||||
Ok(self)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn end_with_data(self, bytes: Vec<u8>) -> Result<RequestBuilder> {
|
|
||||||
Ok(self.req.body(bytes))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn end(self) -> Result<RequestBuilder> {
|
|
||||||
Ok(self.req)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl StructuredSerializer<RequestBuilder> for RequestSerializer {
|
|
||||||
fn set_structured_event(self, bytes: Vec<u8>) -> Result<RequestBuilder> {
|
|
||||||
Ok(self
|
|
||||||
.req
|
|
||||||
.header(
|
|
||||||
reqwest::header::CONTENT_TYPE,
|
|
||||||
headers::CLOUDEVENTS_JSON_HEADER.clone(),
|
|
||||||
)
|
|
||||||
.body(bytes))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Method to fill a [`RequestBuilder`] with an [`Event`]
|
|
||||||
pub fn event_to_request(event: Event, request_builder: RequestBuilder) -> Result<RequestBuilder> {
|
|
||||||
BinaryDeserializer::deserialize_binary(event, RequestSerializer::new(request_builder))
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use super::*;
|
|
||||||
use mockito::{mock, Matcher};
|
|
||||||
|
|
||||||
use cloudevents::message::StructuredDeserializer;
|
|
||||||
use cloudevents::EventBuilder;
|
|
||||||
use serde_json::json;
|
|
||||||
use url::Url;
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn test_request() {
|
|
||||||
let url = mockito::server_url();
|
|
||||||
let m = mock("POST", "/")
|
|
||||||
.match_header("ce-specversion", "1.0")
|
|
||||||
.match_header("ce-id", "0001")
|
|
||||||
.match_header("ce-type", "example.test")
|
|
||||||
.match_header("ce-source", "http://localhost/")
|
|
||||||
.match_header("ce-someint", "10")
|
|
||||||
.match_body(Matcher::Missing)
|
|
||||||
.create();
|
|
||||||
|
|
||||||
let input = EventBuilder::new()
|
|
||||||
.id("0001")
|
|
||||||
.ty("example.test")
|
|
||||||
.source(Url::from_str("http://localhost/").unwrap())
|
|
||||||
.extension("someint", "10")
|
|
||||||
.build();
|
|
||||||
|
|
||||||
let client = reqwest::Client::new();
|
|
||||||
event_to_request(input, client.post(&url))
|
|
||||||
.unwrap()
|
|
||||||
.send()
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
m.assert();
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn test_request_with_full_data() {
|
|
||||||
let j = json!({"hello": "world"});
|
|
||||||
|
|
||||||
let url = mockito::server_url();
|
|
||||||
let m = mock("POST", "/")
|
|
||||||
.match_header("ce-specversion", "1.0")
|
|
||||||
.match_header("ce-id", "0001")
|
|
||||||
.match_header("ce-type", "example.test")
|
|
||||||
.match_header("ce-source", "http://localhost/")
|
|
||||||
.match_header("content-type", "application/json")
|
|
||||||
.match_header("ce-someint", "10")
|
|
||||||
.match_body(Matcher::Exact(j.to_string()))
|
|
||||||
.create();
|
|
||||||
|
|
||||||
let input = EventBuilder::new()
|
|
||||||
.id("0001")
|
|
||||||
.ty("example.test")
|
|
||||||
.source(Url::from_str("http://localhost").unwrap())
|
|
||||||
.data("application/json", j.clone())
|
|
||||||
.extension("someint", "10")
|
|
||||||
.build();
|
|
||||||
|
|
||||||
let client = reqwest::Client::new();
|
|
||||||
event_to_request(input, client.post(&url))
|
|
||||||
.unwrap()
|
|
||||||
.send()
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
m.assert();
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn test_structured_request_with_full_data() {
|
|
||||||
let j = json!({"hello": "world"});
|
|
||||||
|
|
||||||
let input = EventBuilder::new()
|
|
||||||
.id("0001")
|
|
||||||
.ty("example.test")
|
|
||||||
.source(Url::from_str("http://localhost").unwrap())
|
|
||||||
.data("application/json", j.clone())
|
|
||||||
.extension("someint", "10")
|
|
||||||
.build();
|
|
||||||
|
|
||||||
let url = mockito::server_url();
|
|
||||||
let m = mock("POST", "/")
|
|
||||||
.match_header("content-type", "application/cloudevents+json")
|
|
||||||
.match_body(Matcher::Exact(serde_json::to_string(&input).unwrap()))
|
|
||||||
.create();
|
|
||||||
|
|
||||||
let client = reqwest::Client::new();
|
|
||||||
StructuredDeserializer::deserialize_structured(
|
|
||||||
input,
|
|
||||||
RequestSerializer::new(client.post(&url)),
|
|
||||||
)
|
|
||||||
.unwrap()
|
|
||||||
.send()
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
m.assert();
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,206 +0,0 @@
|
||||||
use super::headers;
|
|
||||||
use bytes::Bytes;
|
|
||||||
use cloudevents::event::SpecVersion;
|
|
||||||
use cloudevents::message::{
|
|
||||||
BinaryDeserializer, BinarySerializer, Encoding, Error, MessageAttributeValue,
|
|
||||||
MessageDeserializer, Result, StructuredDeserializer, StructuredSerializer,
|
|
||||||
};
|
|
||||||
use cloudevents::{message, Event};
|
|
||||||
use reqwest::header::{HeaderMap, HeaderName};
|
|
||||||
use reqwest::Response;
|
|
||||||
use std::convert::TryFrom;
|
|
||||||
|
|
||||||
/// Wrapper for [`Response`] that implements [`MessageDeserializer`] trait
|
|
||||||
pub struct ResponseDeserializer {
|
|
||||||
headers: HeaderMap,
|
|
||||||
body: Bytes,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ResponseDeserializer {
|
|
||||||
pub fn new(headers: HeaderMap, body: Bytes) -> ResponseDeserializer {
|
|
||||||
ResponseDeserializer { headers, body }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl BinaryDeserializer for ResponseDeserializer {
|
|
||||||
fn deserialize_binary<R: Sized, V: BinarySerializer<R>>(self, mut visitor: V) -> Result<R> {
|
|
||||||
if self.encoding() != Encoding::BINARY {
|
|
||||||
return Err(message::Error::WrongEncoding {});
|
|
||||||
}
|
|
||||||
|
|
||||||
let spec_version = SpecVersion::try_from(
|
|
||||||
unwrap_optional_header!(self.headers, headers::SPEC_VERSION_HEADER).unwrap()?,
|
|
||||||
)?;
|
|
||||||
|
|
||||||
visitor = visitor.set_spec_version(spec_version.clone())?;
|
|
||||||
|
|
||||||
let attributes = cloudevents::event::SPEC_VERSION_ATTRIBUTES
|
|
||||||
.get(&spec_version)
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
for (hn, hv) in self
|
|
||||||
.headers
|
|
||||||
.iter()
|
|
||||||
.filter(|(hn, _)| headers::SPEC_VERSION_HEADER.ne(hn) && hn.as_str().starts_with("ce-"))
|
|
||||||
{
|
|
||||||
let name = &hn.as_str()["ce-".len()..];
|
|
||||||
|
|
||||||
if attributes.contains(&name) {
|
|
||||||
visitor = visitor.set_attribute(
|
|
||||||
name,
|
|
||||||
MessageAttributeValue::String(String::from(header_value_to_str!(hv)?)),
|
|
||||||
)?
|
|
||||||
} else {
|
|
||||||
visitor = visitor.set_extension(
|
|
||||||
name,
|
|
||||||
MessageAttributeValue::String(String::from(header_value_to_str!(hv)?)),
|
|
||||||
)?
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(hv) = self.headers.get("content-type") {
|
|
||||||
visitor = visitor.set_attribute(
|
|
||||||
"datacontenttype",
|
|
||||||
MessageAttributeValue::String(String::from(header_value_to_str!(hv)?)),
|
|
||||||
)?
|
|
||||||
}
|
|
||||||
|
|
||||||
if self.body.len() != 0 {
|
|
||||||
visitor.end_with_data(self.body.to_vec())
|
|
||||||
} else {
|
|
||||||
visitor.end()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl StructuredDeserializer for ResponseDeserializer {
|
|
||||||
fn deserialize_structured<R: Sized, V: StructuredSerializer<R>>(self, visitor: V) -> Result<R> {
|
|
||||||
if self.encoding() != Encoding::STRUCTURED {
|
|
||||||
return Err(message::Error::WrongEncoding {});
|
|
||||||
}
|
|
||||||
visitor.set_structured_event(self.body.to_vec())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl MessageDeserializer for ResponseDeserializer {
|
|
||||||
fn encoding(&self) -> Encoding {
|
|
||||||
match (
|
|
||||||
unwrap_optional_header!(self.headers, reqwest::header::CONTENT_TYPE)
|
|
||||||
.map(|r| r.ok())
|
|
||||||
.flatten()
|
|
||||||
.map(|e| e.starts_with("application/cloudevents+json")),
|
|
||||||
self.headers
|
|
||||||
.get::<&'static HeaderName>(&headers::SPEC_VERSION_HEADER),
|
|
||||||
) {
|
|
||||||
(Some(true), _) => Encoding::STRUCTURED,
|
|
||||||
(_, Some(_)) => Encoding::BINARY,
|
|
||||||
_ => Encoding::UNKNOWN,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Method to transform an incoming [`Response`] to [`Event`]
|
|
||||||
pub async fn response_to_event(res: Response) -> Result<Event> {
|
|
||||||
let h = res.headers().to_owned();
|
|
||||||
let b = res.bytes().await.map_err(|e| Error::Other {
|
|
||||||
source: Box::new(e),
|
|
||||||
})?;
|
|
||||||
|
|
||||||
MessageDeserializer::into_event(ResponseDeserializer::new(h, b))
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use super::*;
|
|
||||||
use mockito::mock;
|
|
||||||
|
|
||||||
use cloudevents::EventBuilder;
|
|
||||||
use serde_json::json;
|
|
||||||
use std::str::FromStr;
|
|
||||||
use url::Url;
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn test_response() {
|
|
||||||
let url = mockito::server_url();
|
|
||||||
let _m = mock("GET", "/")
|
|
||||||
.with_status(200)
|
|
||||||
.with_header("ce-specversion", "1.0")
|
|
||||||
.with_header("ce-id", "0001")
|
|
||||||
.with_header("ce-type", "example.test")
|
|
||||||
.with_header("ce-source", "http://localhost")
|
|
||||||
.with_header("ce-someint", "10")
|
|
||||||
.create();
|
|
||||||
|
|
||||||
let expected = EventBuilder::new()
|
|
||||||
.id("0001")
|
|
||||||
.ty("example.test")
|
|
||||||
.source(Url::from_str("http://localhost").unwrap())
|
|
||||||
.extension("someint", "10")
|
|
||||||
.build();
|
|
||||||
|
|
||||||
let client = reqwest::Client::new();
|
|
||||||
let res = client.get(&url).send().await.unwrap();
|
|
||||||
|
|
||||||
let resp = response_to_event(res).await.unwrap();
|
|
||||||
assert_eq!(expected, resp);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn test_response_with_full_data() {
|
|
||||||
let j = json!({"hello": "world"});
|
|
||||||
|
|
||||||
let url = mockito::server_url();
|
|
||||||
let _m = mock("GET", "/")
|
|
||||||
.with_status(200)
|
|
||||||
.with_header("ce-specversion", "1.0")
|
|
||||||
.with_header("ce-id", "0001")
|
|
||||||
.with_header("ce-type", "example.test")
|
|
||||||
.with_header("ce-source", "http://localhost/")
|
|
||||||
.with_header("content-type", "application/json")
|
|
||||||
.with_header("ce-someint", "10")
|
|
||||||
.with_body(j.to_string())
|
|
||||||
.create();
|
|
||||||
|
|
||||||
let expected = EventBuilder::new()
|
|
||||||
.id("0001")
|
|
||||||
.ty("example.test")
|
|
||||||
.source(Url::from_str("http://localhost").unwrap())
|
|
||||||
.data("application/json", j.clone())
|
|
||||||
.extension("someint", "10")
|
|
||||||
.build();
|
|
||||||
|
|
||||||
let client = reqwest::Client::new();
|
|
||||||
let res = client.get(&url).send().await.unwrap();
|
|
||||||
|
|
||||||
let resp = response_to_event(res).await.unwrap();
|
|
||||||
assert_eq!(expected, resp);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn test_structured_response_with_full_data() {
|
|
||||||
let j = json!({"hello": "world"});
|
|
||||||
let expected = EventBuilder::new()
|
|
||||||
.id("0001")
|
|
||||||
.ty("example.test")
|
|
||||||
.source(Url::from_str("http://localhost").unwrap())
|
|
||||||
.data("application/json", j.clone())
|
|
||||||
.extension("someint", "10")
|
|
||||||
.build();
|
|
||||||
|
|
||||||
let url = mockito::server_url();
|
|
||||||
let _m = mock("GET", "/")
|
|
||||||
.with_status(200)
|
|
||||||
.with_header(
|
|
||||||
"content-type",
|
|
||||||
"application/cloudevents+json; charset=utf-8",
|
|
||||||
)
|
|
||||||
.with_body(serde_json::to_string(&expected).unwrap())
|
|
||||||
.create();
|
|
||||||
|
|
||||||
let client = reqwest::Client::new();
|
|
||||||
let res = client.get(&url).send().await.unwrap();
|
|
||||||
|
|
||||||
let resp = response_to_event(res).await.unwrap();
|
|
||||||
assert_eq!(expected, resp);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,63 +0,0 @@
|
||||||
use cloudevents::event::SpecVersion;
|
|
||||||
use lazy_static::lazy_static;
|
|
||||||
use reqwest::header::{HeaderName, HeaderValue};
|
|
||||||
use std::collections::HashMap;
|
|
||||||
use std::str::FromStr;
|
|
||||||
|
|
||||||
macro_rules! unwrap_optional_header {
|
|
||||||
($headers:expr, $name:expr) => {
|
|
||||||
$headers
|
|
||||||
.get::<&'static reqwest::header::HeaderName>(&$name)
|
|
||||||
.map(|a| header_value_to_str!(a))
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
macro_rules! header_value_to_str {
|
|
||||||
($header_value:expr) => {
|
|
||||||
$header_value
|
|
||||||
.to_str()
|
|
||||||
.map_err(|e| cloudevents::message::Error::Other {
|
|
||||||
source: Box::new(e),
|
|
||||||
})
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
macro_rules! str_name_to_header {
|
|
||||||
($attribute:expr) => {
|
|
||||||
reqwest::header::HeaderName::from_str($attribute).map_err(|e| {
|
|
||||||
cloudevents::message::Error::Other {
|
|
||||||
source: Box::new(e),
|
|
||||||
}
|
|
||||||
})
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
macro_rules! attribute_name_to_header {
|
|
||||||
($attribute:expr) => {
|
|
||||||
str_name_to_header!(&["ce-", $attribute].concat())
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
fn attributes_to_headers(
|
|
||||||
map: &HashMap<SpecVersion, &'static [&'static str]>,
|
|
||||||
) -> HashMap<&'static str, HeaderName> {
|
|
||||||
map.values()
|
|
||||||
.flat_map(|s| s.iter())
|
|
||||||
.map(|s| {
|
|
||||||
if *s == "datacontenttype" {
|
|
||||||
(*s, reqwest::header::CONTENT_TYPE)
|
|
||||||
} else {
|
|
||||||
(*s, attribute_name_to_header!(s).unwrap())
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.collect()
|
|
||||||
}
|
|
||||||
|
|
||||||
lazy_static! {
|
|
||||||
pub(crate) static ref ATTRIBUTES_TO_HEADERS: HashMap<&'static str, HeaderName> =
|
|
||||||
attributes_to_headers(&cloudevents::event::SPEC_VERSION_ATTRIBUTES);
|
|
||||||
pub(crate) static ref SPEC_VERSION_HEADER: HeaderName =
|
|
||||||
HeaderName::from_static("ce-specversion");
|
|
||||||
pub(crate) static ref CLOUDEVENTS_JSON_HEADER: HeaderValue =
|
|
||||||
HeaderValue::from_static("application/cloudevents+json");
|
|
||||||
}
|
|
|
@ -1,9 +0,0 @@
|
||||||
#[macro_use]
|
|
||||||
mod headers;
|
|
||||||
mod client_request;
|
|
||||||
mod client_response;
|
|
||||||
|
|
||||||
pub use client_request::event_to_request;
|
|
||||||
pub use client_request::RequestSerializer;
|
|
||||||
pub use client_response::response_to_event;
|
|
||||||
pub use client_response::ResponseDeserializer;
|
|
|
@ -1,20 +1,13 @@
|
||||||
[package]
|
[package]
|
||||||
name = "actix-web-example"
|
name = "actix-web-example"
|
||||||
version = "0.1.0"
|
version = "0.3.0"
|
||||||
authors = ["Francesco Guardiani <francescoguard@gmail.com>"]
|
authors = ["Francesco Guardiani <francescoguard@gmail.com>"]
|
||||||
edition = "2018"
|
edition = "2018"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
cloudevents-sdk = { path = "../.." }
|
cloudevents-sdk = { path = "../..", features = ["actix"] }
|
||||||
cloudevents-sdk-actix-web = { path = "../../cloudevents-sdk-actix-web" }
|
actix-web = "4"
|
||||||
actix-web = "2"
|
actix-cors = "^0.7"
|
||||||
actix-rt = "1"
|
|
||||||
actix-cors = "^0.2.0"
|
|
||||||
lazy_static = "1.4.0"
|
|
||||||
bytes = "^0.5"
|
|
||||||
futures = "^0.3"
|
|
||||||
serde_json = "^1.0"
|
serde_json = "^1.0"
|
||||||
url = { version = "^2.1" }
|
url = { version = "^2.1" }
|
||||||
env_logger = "0.7.1"
|
env_logger = "^0.11"
|
||||||
|
|
||||||
[workspace]
|
|
||||||
|
|
|
@ -0,0 +1,23 @@
|
||||||
|
To run the server:
|
||||||
|
|
||||||
|
```console
|
||||||
|
cargo run
|
||||||
|
```
|
||||||
|
|
||||||
|
To test a GET:
|
||||||
|
|
||||||
|
```console
|
||||||
|
curl http://localhost:9000
|
||||||
|
```
|
||||||
|
|
||||||
|
To test a POST:
|
||||||
|
|
||||||
|
```console
|
||||||
|
curl -d '{"hello": "world"}' \
|
||||||
|
-H'content-type: application/json' \
|
||||||
|
-H'ce-specversion: 1.0' \
|
||||||
|
-H'ce-id: 1' \
|
||||||
|
-H'ce-source: http://cloudevents.io' \
|
||||||
|
-H'ce-type: dev.knative.example' \
|
||||||
|
http://localhost:9000
|
||||||
|
```
|
|
@ -1,46 +1,41 @@
|
||||||
use actix_web::{get, post, web, App, HttpRequest, HttpResponse, HttpServer};
|
use actix_web::{get, post, App, HttpServer};
|
||||||
use cloudevents::EventBuilder;
|
use cloudevents::{Event, EventBuilder, EventBuilderV10};
|
||||||
use url::Url;
|
|
||||||
use std::str::FromStr;
|
|
||||||
use serde_json::json;
|
use serde_json::json;
|
||||||
|
|
||||||
#[post("/")]
|
#[post("/")]
|
||||||
async fn post_event(req: HttpRequest, payload: web::Payload) -> Result<String, actix_web::Error> {
|
async fn post_event(event: Event) -> Event {
|
||||||
let event = cloudevents_sdk_actix_web::request_to_event(&req, payload).await?;
|
|
||||||
println!("Received Event: {:?}", event);
|
println!("Received Event: {:?}", event);
|
||||||
Ok(format!("{:?}", event))
|
event
|
||||||
}
|
}
|
||||||
|
|
||||||
#[get("/")]
|
#[get("/")]
|
||||||
async fn get_event() -> Result<HttpResponse, actix_web::Error> {
|
async fn get_event() -> Event {
|
||||||
let payload = json!({"hello": "world"});
|
let payload = json!({"hello": "world"});
|
||||||
|
|
||||||
Ok(cloudevents_sdk_actix_web::event_to_response(
|
EventBuilderV10::new()
|
||||||
EventBuilder::new()
|
.id("0001")
|
||||||
.id("0001")
|
.ty("example.test")
|
||||||
.ty("example.test")
|
.source("http://localhost/")
|
||||||
.source(Url::from_str("http://localhost/").unwrap())
|
.data("application/json", payload)
|
||||||
.data("application/json", payload)
|
.extension("someint", "10")
|
||||||
.extension("someint", "10")
|
.build()
|
||||||
.build(),
|
.unwrap()
|
||||||
HttpResponse::Ok()
|
|
||||||
).await?)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[actix_rt::main]
|
#[actix_web::main]
|
||||||
async fn main() -> std::io::Result<()> {
|
async fn main() -> std::io::Result<()> {
|
||||||
std::env::set_var("RUST_LOG", "actix_server=info,actix_web=info");
|
std::env::set_var("RUST_LOG", "actix_server=info,actix_web=info");
|
||||||
env_logger::init();
|
env_logger::init();
|
||||||
|
|
||||||
HttpServer::new(|| {
|
HttpServer::new(|| {
|
||||||
App::new()
|
App::new()
|
||||||
|
.wrap(actix_cors::Cors::permissive())
|
||||||
.wrap(actix_web::middleware::Logger::default())
|
.wrap(actix_web::middleware::Logger::default())
|
||||||
.wrap(actix_cors::Cors::new().finish())
|
|
||||||
.service(post_event)
|
.service(post_event)
|
||||||
.service(get_event)
|
.service(get_event)
|
||||||
})
|
})
|
||||||
.bind("127.0.0.1:9000")?
|
.bind("127.0.0.1:9000")?
|
||||||
.workers(1)
|
.workers(1)
|
||||||
.run()
|
.run()
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,21 @@
|
||||||
|
[package]
|
||||||
|
name = "axum-example"
|
||||||
|
version = "0.3.0"
|
||||||
|
authors = ["Andrew Webber <andrewvwebber@googlemail.com>"]
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
cloudevents-sdk = { path = "../..", features = ["axum"] }
|
||||||
|
axum = "^0.8"
|
||||||
|
http = "^1.1"
|
||||||
|
tokio = { version = "^1", features = ["full"] }
|
||||||
|
tracing = "^0.1"
|
||||||
|
tracing-subscriber = "^0.3"
|
||||||
|
tower-http = { version = "^0.6", features = ["trace"] }
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
|
tower = { version = "^0.5", features = ["util"] }
|
||||||
|
serde = { version = "^1.0", features = ["derive"] }
|
||||||
|
serde_json = "^1.0"
|
||||||
|
chrono = { version = "^0.4", features = ["serde"] }
|
||||||
|
hyper = { version = "^1.4" }
|
|
@ -0,0 +1,23 @@
|
||||||
|
To run the server:
|
||||||
|
|
||||||
|
```console
|
||||||
|
cargo run
|
||||||
|
```
|
||||||
|
|
||||||
|
To test a GET:
|
||||||
|
|
||||||
|
```console
|
||||||
|
curl http://localhost:8080
|
||||||
|
```
|
||||||
|
|
||||||
|
To test a POST:
|
||||||
|
|
||||||
|
```console
|
||||||
|
curl -d '{"hello": "world"}' \
|
||||||
|
-H'content-type: application/json' \
|
||||||
|
-H'ce-specversion: 1.0' \
|
||||||
|
-H'ce-id: 1' \
|
||||||
|
-H'ce-source: http://cloudevents.io' \
|
||||||
|
-H'ce-type: dev.knative.example' \
|
||||||
|
http://localhost:8080
|
||||||
|
```
|
|
@ -0,0 +1,108 @@
|
||||||
|
use axum::{
|
||||||
|
routing::{get, post},
|
||||||
|
Router,
|
||||||
|
};
|
||||||
|
use cloudevents::Event;
|
||||||
|
use http::StatusCode;
|
||||||
|
use tower_http::trace::TraceLayer;
|
||||||
|
|
||||||
|
fn echo_app() -> Router {
|
||||||
|
Router::new()
|
||||||
|
.route("/", get(|| async { "hello from cloudevents server" }))
|
||||||
|
.route(
|
||||||
|
"/",
|
||||||
|
post(|event: Event| async move {
|
||||||
|
tracing::debug!("received cloudevent {}", &event);
|
||||||
|
(StatusCode::OK, event)
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.layer(TraceLayer::new_for_http())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::main]
|
||||||
|
async fn main() {
|
||||||
|
if std::env::var("RUST_LOG").is_err() {
|
||||||
|
std::env::set_var("RUST_LOG", "axum_example=debug,tower_http=debug")
|
||||||
|
}
|
||||||
|
tracing_subscriber::fmt::init();
|
||||||
|
let service = echo_app();
|
||||||
|
let listener = tokio::net::TcpListener::bind("0.0.0.0:8080").await.unwrap();
|
||||||
|
axum::serve(listener, service).await.unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
|
||||||
|
use super::echo_app;
|
||||||
|
|
||||||
|
use axum::{
|
||||||
|
body::Body,
|
||||||
|
http::{self, Request},
|
||||||
|
};
|
||||||
|
use chrono::Utc;
|
||||||
|
use hyper;
|
||||||
|
use serde_json::json;
|
||||||
|
use tower::ServiceExt; // for `app.oneshot()`
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn axum_mod_test() {
|
||||||
|
if std::env::var("RUST_LOG").is_err() {
|
||||||
|
std::env::set_var("RUST_LOG", "axum_example=debug,tower_http=debug")
|
||||||
|
}
|
||||||
|
tracing_subscriber::fmt::init();
|
||||||
|
|
||||||
|
let app = echo_app();
|
||||||
|
let time = Utc::now();
|
||||||
|
let j = json!({"hello": "world"});
|
||||||
|
let request = Request::builder()
|
||||||
|
.method(http::Method::POST)
|
||||||
|
.header("ce-specversion", "1.0")
|
||||||
|
.header("ce-id", "0001")
|
||||||
|
.header("ce-type", "example.test")
|
||||||
|
.header("ce-source", "http://localhost/")
|
||||||
|
.header("ce-someint", "10")
|
||||||
|
.header("ce-time", time.to_rfc3339())
|
||||||
|
.header("content-type", "application/json")
|
||||||
|
.body(Body::from(serde_json::to_vec(&j).unwrap()))
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let resp = app.oneshot(request).await.unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
resp.headers()
|
||||||
|
.get("ce-specversion")
|
||||||
|
.unwrap()
|
||||||
|
.to_str()
|
||||||
|
.unwrap(),
|
||||||
|
"1.0"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
resp.headers().get("ce-id").unwrap().to_str().unwrap(),
|
||||||
|
"0001"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
resp.headers().get("ce-type").unwrap().to_str().unwrap(),
|
||||||
|
"example.test"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
resp.headers().get("ce-source").unwrap().to_str().unwrap(),
|
||||||
|
"http://localhost/"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
resp.headers()
|
||||||
|
.get("content-type")
|
||||||
|
.unwrap()
|
||||||
|
.to_str()
|
||||||
|
.unwrap(),
|
||||||
|
"application/json"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
resp.headers().get("ce-someint").unwrap().to_str().unwrap(),
|
||||||
|
"10"
|
||||||
|
);
|
||||||
|
|
||||||
|
let (_, body) = resp.into_parts();
|
||||||
|
let body = hyper::body::to_bytes(body).await.unwrap();
|
||||||
|
|
||||||
|
assert_eq!(j.to_string().as_bytes(), body);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,12 @@
|
||||||
|
[package]
|
||||||
|
name = "nats-example"
|
||||||
|
version = "0.1.0"
|
||||||
|
authors = ["Jakub Noga <jakub.noga@softchameleon.io>"]
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
cloudevents-sdk = { path = "../..", features = ["nats"] }
|
||||||
|
serde_json = "^1.0"
|
||||||
|
nats = "^0.25"
|
|
@ -0,0 +1,47 @@
|
||||||
|
use std::{error::Error, thread};
|
||||||
|
|
||||||
|
use cloudevents::binding::nats::{MessageExt, NatsCloudEvent};
|
||||||
|
use cloudevents::{Event, EventBuilder, EventBuilderV10};
|
||||||
|
use serde_json::json;
|
||||||
|
|
||||||
|
/// First spin up a nats server i.e.
|
||||||
|
/// ```bash
|
||||||
|
/// docker run -p 4222:4222 -ti nats:latest
|
||||||
|
/// ```
|
||||||
|
fn main() -> Result<(), Box<dyn Error>> {
|
||||||
|
let nc = nats::connect("localhost:4222").unwrap();
|
||||||
|
|
||||||
|
let event = EventBuilderV10::new()
|
||||||
|
.id("123".to_string())
|
||||||
|
.ty("example.test")
|
||||||
|
.source("http://localhost/")
|
||||||
|
.data("application/json", json!({"hello": "world"}))
|
||||||
|
.build()
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let n_msg = NatsCloudEvent::from_event(event).unwrap();
|
||||||
|
|
||||||
|
let sub = nc.subscribe("test").unwrap();
|
||||||
|
|
||||||
|
let t = thread::spawn(move || -> Result<Event, String> {
|
||||||
|
match sub.next() {
|
||||||
|
Some(msg) => match msg.to_event() {
|
||||||
|
Ok(evt) => Ok(evt),
|
||||||
|
Err(e) => Err(e.to_string()),
|
||||||
|
},
|
||||||
|
None => Err("Unsubed or disconnected".to_string()),
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
nc.publish("test", n_msg)?;
|
||||||
|
|
||||||
|
let maybe_event = t.join().unwrap();
|
||||||
|
|
||||||
|
if let Ok(evt) = maybe_event {
|
||||||
|
println!("{}", evt.to_string());
|
||||||
|
} else {
|
||||||
|
println!("{}", maybe_event.unwrap_err().to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
|
@ -0,0 +1,16 @@
|
||||||
|
[package]
|
||||||
|
name = "poem-example"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
cloudevents-sdk = { path = "../..", features = ["poem"] }
|
||||||
|
tokio = { version = "1.13", features = ["macros", "rt-multi-thread"] }
|
||||||
|
tracing = "0.1"
|
||||||
|
poem = { version = "^3.0" }
|
||||||
|
tracing-subscriber = "0.3"
|
||||||
|
serde_json = "1.0"
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
|
chrono = { version = "0.4", features = ["serde"] }
|
||||||
|
|
|
@ -0,0 +1,23 @@
|
||||||
|
To run the server:
|
||||||
|
|
||||||
|
```console
|
||||||
|
cargo run
|
||||||
|
```
|
||||||
|
|
||||||
|
To test a GET:
|
||||||
|
|
||||||
|
```console
|
||||||
|
curl http://localhost:8080
|
||||||
|
```
|
||||||
|
|
||||||
|
To test a POST:
|
||||||
|
|
||||||
|
```console
|
||||||
|
curl -d '{"hello": "world"}' \
|
||||||
|
-H'content-type: application/json' \
|
||||||
|
-H'ce-specversion: 1.0' \
|
||||||
|
-H'ce-id: 1' \
|
||||||
|
-H'ce-source: http://cloudevents.io' \
|
||||||
|
-H'ce-type: dev.knative.example' \
|
||||||
|
http://localhost:8080
|
||||||
|
```
|
|
@ -0,0 +1,121 @@
|
||||||
|
use cloudevents::{Event, EventBuilder, EventBuilderV10};
|
||||||
|
use poem::error::InternalServerError;
|
||||||
|
use poem::listener::TcpListener;
|
||||||
|
use poem::middleware::Tracing;
|
||||||
|
use poem::{get, handler, Endpoint, EndpointExt, Response, Result, Route, Server};
|
||||||
|
use serde_json::json;
|
||||||
|
|
||||||
|
#[handler]
|
||||||
|
async fn get_event() -> Result<Event> {
|
||||||
|
let event = EventBuilderV10::new()
|
||||||
|
.id("1")
|
||||||
|
.source("url://example_response/")
|
||||||
|
.ty("example.ce")
|
||||||
|
.data(
|
||||||
|
"application/json",
|
||||||
|
json!({
|
||||||
|
"name": "John Doe",
|
||||||
|
"age": 43,
|
||||||
|
"phones": [
|
||||||
|
"+44 1234567",
|
||||||
|
"+44 2345678"
|
||||||
|
]
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.build()
|
||||||
|
.map_err(InternalServerError)?;
|
||||||
|
Ok(event)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[handler]
|
||||||
|
async fn post_event(event: Event) -> Event {
|
||||||
|
tracing::debug!("received cloudevent {}", &event);
|
||||||
|
event
|
||||||
|
}
|
||||||
|
|
||||||
|
fn echo_app() -> impl Endpoint<Output = Response> {
|
||||||
|
Route::new()
|
||||||
|
.at("/", get(get_event).post(post_event))
|
||||||
|
.with(Tracing)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::main]
|
||||||
|
async fn main() -> Result<(), std::io::Error> {
|
||||||
|
if std::env::var("RUST_LOG").is_err() {
|
||||||
|
std::env::set_var("RUST_LOG", "poem=debug")
|
||||||
|
}
|
||||||
|
tracing_subscriber::fmt::init();
|
||||||
|
|
||||||
|
let server = Server::new(TcpListener::bind("127.0.0.1:8080"));
|
||||||
|
server.run(echo_app()).await
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use chrono::Utc;
|
||||||
|
use poem::http::Method;
|
||||||
|
use poem::{Body, Request};
|
||||||
|
use serde_json::json;
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn poem_test() {
|
||||||
|
if std::env::var("RUST_LOG").is_err() {
|
||||||
|
std::env::set_var("RUST_LOG", "poem_example=debug")
|
||||||
|
}
|
||||||
|
tracing_subscriber::fmt::init();
|
||||||
|
|
||||||
|
let app = echo_app();
|
||||||
|
let time = Utc::now();
|
||||||
|
let j = json!({"hello": "world"});
|
||||||
|
let request = Request::builder()
|
||||||
|
.method(Method::POST)
|
||||||
|
.header("ce-specversion", "1.0")
|
||||||
|
.header("ce-id", "0001")
|
||||||
|
.header("ce-type", "example.test")
|
||||||
|
.header("ce-source", "http://localhost/")
|
||||||
|
.header("ce-someint", "10")
|
||||||
|
.header("ce-time", time.to_rfc3339())
|
||||||
|
.header("content-type", "application/json")
|
||||||
|
.body(Body::from_json(&j).unwrap());
|
||||||
|
|
||||||
|
let resp: Response = app.call(request).await.unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
resp.headers()
|
||||||
|
.get("ce-specversion")
|
||||||
|
.unwrap()
|
||||||
|
.to_str()
|
||||||
|
.unwrap(),
|
||||||
|
"1.0"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
resp.headers().get("ce-id").unwrap().to_str().unwrap(),
|
||||||
|
"0001"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
resp.headers().get("ce-type").unwrap().to_str().unwrap(),
|
||||||
|
"example.test"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
resp.headers().get("ce-source").unwrap().to_str().unwrap(),
|
||||||
|
"http://localhost/"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
resp.headers()
|
||||||
|
.get("content-type")
|
||||||
|
.unwrap()
|
||||||
|
.to_str()
|
||||||
|
.unwrap(),
|
||||||
|
"application/json"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
resp.headers().get("ce-someint").unwrap().to_str().unwrap(),
|
||||||
|
"10"
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
j.to_string().as_bytes(),
|
||||||
|
resp.into_body().into_vec().await.unwrap()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,19 @@
|
||||||
|
[package]
|
||||||
|
name = "rdkafka-example"
|
||||||
|
version = "0.4.0"
|
||||||
|
authors = ["Pranav Bhatt <adpranavb2000@gmail.com>"]
|
||||||
|
edition = "2018"
|
||||||
|
|
||||||
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
async-trait = "^0.1.33"
|
||||||
|
cloudevents-sdk = { path = "../..", features = ["rdkafka"] }
|
||||||
|
lazy_static = "1.4.0"
|
||||||
|
bytes = "^1.0"
|
||||||
|
url = { version = "^2.1", features = ["serde"] }
|
||||||
|
serde_json = "^1.0"
|
||||||
|
futures = "^0.3"
|
||||||
|
tokio = { version = "^1.0", features = ["full"] }
|
||||||
|
clap = "2.33.1"
|
||||||
|
rdkafka = { version = "^0.37", features = ["cmake-build"] }
|
|
@ -0,0 +1,162 @@
|
||||||
|
use clap::{App, Arg};
|
||||||
|
use futures::StreamExt;
|
||||||
|
use serde_json::json;
|
||||||
|
|
||||||
|
use cloudevents::{EventBuilder, EventBuilderV10};
|
||||||
|
use cloudevents::binding::rdkafka::{FutureRecordExt, MessageExt, MessageRecord};
|
||||||
|
|
||||||
|
use rdkafka::config::{ClientConfig, RDKafkaLogLevel};
|
||||||
|
use rdkafka::consumer::stream_consumer::StreamConsumer;
|
||||||
|
use rdkafka::consumer::{CommitMode, Consumer, DefaultConsumerContext};
|
||||||
|
use rdkafka::producer::{FutureProducer, FutureRecord};
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
|
// You need a running Kafka cluster to try out this example.
|
||||||
|
// With docker: docker run --rm --net=host -e ADV_HOST=localhost -e SAMPLEDATA=0 lensesio/fast-data-dev
|
||||||
|
|
||||||
|
async fn consume(brokers: &str, group_id: &str, topics: &[&str]) {
|
||||||
|
let consumer: StreamConsumer<DefaultConsumerContext> = ClientConfig::new()
|
||||||
|
.set("group.id", group_id)
|
||||||
|
.set("bootstrap.servers", brokers)
|
||||||
|
.set("enable.partition.eof", "false")
|
||||||
|
.set("session.timeout.ms", "6000")
|
||||||
|
.set("enable.auto.commit", "true")
|
||||||
|
//.set("statistics.interval.ms", "30000")
|
||||||
|
//.set("auto.offset.reset", "smallest")
|
||||||
|
.set_log_level(RDKafkaLogLevel::Debug)
|
||||||
|
.create_with_context(DefaultConsumerContext)
|
||||||
|
.expect("Consumer creation failed");
|
||||||
|
|
||||||
|
consumer
|
||||||
|
.subscribe(&topics.to_vec())
|
||||||
|
.expect("Can't subscribe to specified topics");
|
||||||
|
|
||||||
|
// consumer.stream() returns a stream. The stream can be used ot chain together expensive steps,
|
||||||
|
// such as complex computations on a thread pool or asynchronous IO.
|
||||||
|
let mut message_stream = consumer.stream();
|
||||||
|
|
||||||
|
while let Some(message) = message_stream.next().await {
|
||||||
|
match message {
|
||||||
|
Err(e) => println!("Kafka error: {}", e),
|
||||||
|
Ok(m) => {
|
||||||
|
let event = m.to_event().unwrap();
|
||||||
|
println!("Received Event: {:#?}", event);
|
||||||
|
|
||||||
|
consumer.commit_message(&m, CommitMode::Async).unwrap();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn produce(brokers: &str, topic_name: &str) {
|
||||||
|
let producer: &FutureProducer = &ClientConfig::new()
|
||||||
|
.set("bootstrap.servers", brokers)
|
||||||
|
.set("message.timeout.ms", "5000")
|
||||||
|
.create()
|
||||||
|
.expect("Producer creation error");
|
||||||
|
|
||||||
|
// This loop is non blocking: all messages will be sent one after the other, without waiting
|
||||||
|
// for the results.
|
||||||
|
let futures = (0..5)
|
||||||
|
.map(|i| async move {
|
||||||
|
// The send operation on the topic returns a future, which will be
|
||||||
|
// completed once the result or failure from Kafka is received.
|
||||||
|
let event = EventBuilderV10::new()
|
||||||
|
.id(i.to_string())
|
||||||
|
.ty("example.test")
|
||||||
|
.source("http://localhost/")
|
||||||
|
.data("application/json", json!({"hello": "world"}))
|
||||||
|
.build()
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
println!("Sending event: {:#?}", event);
|
||||||
|
|
||||||
|
let message_record =
|
||||||
|
MessageRecord::from_event(event).expect("error while serializing the event");
|
||||||
|
|
||||||
|
let delivery_status = producer
|
||||||
|
.send(
|
||||||
|
FutureRecord::to(topic_name)
|
||||||
|
.message_record(&message_record)
|
||||||
|
.key(&format!("Key {}", i)),
|
||||||
|
Duration::from_secs(10),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
// This will be executed when the result is received.
|
||||||
|
println!("Delivery status for message {} received", i);
|
||||||
|
delivery_status
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
|
// This loop will wait until all delivery statuses have been received.
|
||||||
|
for future in futures {
|
||||||
|
println!("Future completed. Result: {:?}", future.await);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::main]
|
||||||
|
async fn main() {
|
||||||
|
let selector = App::new("CloudEvents Kafka Example")
|
||||||
|
.version(option_env!("CARGO_PKG_VERSION").unwrap_or(""))
|
||||||
|
.about("select consumer or producer")
|
||||||
|
.arg(
|
||||||
|
Arg::with_name("mode")
|
||||||
|
.long("mode")
|
||||||
|
.help("enter \"consmer\" or \"producer\"")
|
||||||
|
.takes_value(true)
|
||||||
|
.possible_values(&["consumer", "producer"])
|
||||||
|
.required(true),
|
||||||
|
)
|
||||||
|
.arg(
|
||||||
|
Arg::with_name("topics")
|
||||||
|
.long("topics")
|
||||||
|
.help("Topic list")
|
||||||
|
.takes_value(true)
|
||||||
|
.multiple(true)
|
||||||
|
.requires_if("consumer", "mode"),
|
||||||
|
)
|
||||||
|
.arg(
|
||||||
|
Arg::with_name("topic")
|
||||||
|
.long("topic")
|
||||||
|
.help("Destination topic")
|
||||||
|
.takes_value(true)
|
||||||
|
.requires_if("producer", "mode"),
|
||||||
|
)
|
||||||
|
.arg(
|
||||||
|
Arg::with_name("brokers")
|
||||||
|
.short("b")
|
||||||
|
.long("brokers")
|
||||||
|
.help("Broker list in kafka format")
|
||||||
|
.takes_value(true)
|
||||||
|
.default_value("localhost:9092"),
|
||||||
|
)
|
||||||
|
.arg(
|
||||||
|
Arg::with_name("group-id")
|
||||||
|
.short("g")
|
||||||
|
.long("group-id")
|
||||||
|
.help("Consumer group id")
|
||||||
|
.takes_value(true)
|
||||||
|
.default_value("example_consumer_group_id"),
|
||||||
|
)
|
||||||
|
.get_matches();
|
||||||
|
|
||||||
|
match selector.value_of("mode").unwrap() {
|
||||||
|
"producer" => {
|
||||||
|
produce(
|
||||||
|
selector.value_of("brokers").unwrap(),
|
||||||
|
selector.value_of("topic").unwrap(),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
"consumer" => {
|
||||||
|
consume(
|
||||||
|
selector.value_of("brokers").unwrap(),
|
||||||
|
selector.value_of("group-id").unwrap(),
|
||||||
|
&selector.values_of("topics").unwrap().collect::<Vec<&str>>(),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
_ => (),
|
||||||
|
};
|
||||||
|
}
|
|
@ -1,8 +1,9 @@
|
||||||
[package]
|
[package]
|
||||||
name = "reqwest-wasm-example"
|
name = "reqwest-wasm-example"
|
||||||
version = "0.1.0"
|
version = "0.3.0"
|
||||||
authors = ["Francesco Guardiani <francescoguard@gmail.com>"]
|
authors = ["Francesco Guardiani <francescoguard@gmail.com>"]
|
||||||
edition = "2018"
|
edition = "2018"
|
||||||
|
resolver = "2"
|
||||||
|
|
||||||
# Config mostly pulled from: https://github.com/rustwasm/wasm-bindgen/blob/master/examples/fetch/Cargo.toml
|
# Config mostly pulled from: https://github.com/rustwasm/wasm-bindgen/blob/master/examples/fetch/Cargo.toml
|
||||||
|
|
||||||
|
@ -10,12 +11,10 @@ edition = "2018"
|
||||||
crate-type = ["cdylib"]
|
crate-type = ["cdylib"]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
reqwest = "0.10.4"
|
reqwest = "^0.12"
|
||||||
cloudevents-sdk = { path = "../.." }
|
uuid = "1"
|
||||||
cloudevents-sdk-reqwest = { path = "../../cloudevents-sdk-reqwest" }
|
cloudevents-sdk = { path = "../..", features = ["reqwest"] }
|
||||||
url = { version = "^2.1" }
|
url = { version = "^2.1" }
|
||||||
web-sys = { version = "0.3.39", features = ["Window", "Location"] }
|
web-sys = { version = "0.3.39", features = ["Window", "Location"] }
|
||||||
wasm-bindgen-futures = "0.4.12"
|
wasm-bindgen-futures = "0.4.12"
|
||||||
wasm-bindgen = { version = "0.2.62", features = ["serde-serialize"] }
|
wasm-bindgen = { version = "0.2.77", features = ["serde-serialize"] }
|
||||||
|
|
||||||
[workspace]
|
|
||||||
|
|
|
@ -1,13 +1,23 @@
|
||||||
## Example usage of CLoudEvents sdk/Reqwest from WASM
|
## Example usage of CLoudEvents sdk/Reqwest from WASM
|
||||||
|
|
||||||
Install the dependencies with:
|
First, ensure you have [`wasm-pack` installed](https://rustwasm.github.io/wasm-pack/installer/)
|
||||||
|
|
||||||
|
Then install the dependencies:
|
||||||
|
|
||||||
npm install
|
npm install
|
||||||
|
|
||||||
Then build the example locally with:
|
And finally run the example:
|
||||||
|
|
||||||
npm run serve
|
npm run serve
|
||||||
|
|
||||||
and then visiting http://localhost:8080 in a browser should run the example!
|
You should see a form in your browser at http://localhost:8080. When
|
||||||
|
the form is submitted, a CloudEvent will be sent to the Target URL,
|
||||||
|
http://localhost:9000 by default, which is the default URL for the
|
||||||
|
[actix example](../actix-web-example). Fire it up in another terminal
|
||||||
|
to verify that the data is successfully sent and received.
|
||||||
|
|
||||||
This example is loosely based off of [this example](https://github.com/rustwasm/wasm-bindgen/blob/master/examples/fetch/src/lib.rs), an example usage of `fetch` from `wasm-bindgen`, and from [reqwest repo](https://github.com/seanmonstar/reqwest/tree/master/examples/wasm_header).
|
Open the javascript console in the browser to see any helpful error
|
||||||
|
messages.
|
||||||
|
|
||||||
|
This example is loosely based off of [this
|
||||||
|
example](https://github.com/seanmonstar/reqwest/tree/master/examples/wasm_github_fetch).
|
||||||
|
|
|
@ -13,7 +13,7 @@
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label class="col-md-4 control-label" for="event_target">Target</label>
|
<label class="col-md-4 control-label" for="event_target">Target</label>
|
||||||
<div class="col-md-4">
|
<div class="col-md-4">
|
||||||
<input id="event_target" name="event_target" type="text" placeholder="http://localhost:9000" class="form-control input-md" required="">
|
<input id="event_target" name="event_target" type="text" value="http://localhost:9000" class="form-control input-md" required="">
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -22,7 +22,7 @@
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label class="col-md-4 control-label" for="event_type">Event Type</label>
|
<label class="col-md-4 control-label" for="event_type">Event Type</label>
|
||||||
<div class="col-md-4">
|
<div class="col-md-4">
|
||||||
<input id="event_type" name="event_type" type="text" placeholder="example" class="form-control input-md" required="">
|
<input id="event_type" name="event_type" type="text" value="example" class="form-control input-md" required="">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -30,7 +30,7 @@
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label class="col-md-4 control-label" for="event_datacontenttype">Event Data Content Type</label>
|
<label class="col-md-4 control-label" for="event_datacontenttype">Event Data Content Type</label>
|
||||||
<div class="col-md-4">
|
<div class="col-md-4">
|
||||||
<input id="event_datacontenttype" name="event_datacontenttype" type="text" placeholder="application/json" class="form-control input-md" required="">
|
<input id="event_datacontenttype" name="event_datacontenttype" type="text" value="application/json" class="form-control input-md" required="">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -54,4 +54,4 @@
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
@ -1,11 +1,11 @@
|
||||||
import $ from 'jquery';
|
import $ from 'jquery';
|
||||||
|
|
||||||
import 'bootstrap/dist/css/bootstrap.min.css';
|
|
||||||
import 'bootstrap';
|
import 'bootstrap';
|
||||||
|
import 'bootstrap/dist/css/bootstrap.min.css';
|
||||||
|
|
||||||
import("./pkg").then(rustModule => {
|
import("./pkg").then(rustModule => {
|
||||||
$(document).ready(function () {
|
$(function() {
|
||||||
$("#send").click(function () {
|
$("#send").on("click", function () {
|
||||||
let target = $("#event_target").val()
|
let target = $("#event_target").val()
|
||||||
let ty = $("#event_type").val()
|
let ty = $("#event_type").val()
|
||||||
let dataContentType = $("#event_datacontenttype").val()
|
let dataContentType = $("#event_datacontenttype").val()
|
||||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -1,21 +1,21 @@
|
||||||
{
|
{
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "webpack",
|
"build": "webpack",
|
||||||
"serve": "webpack-dev-server"
|
"serve": "webpack serve"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@wasm-tool/wasm-pack-plugin": "^1.3.1",
|
"@wasm-tool/wasm-pack-plugin": "^1.4.0",
|
||||||
"css-loader": "^3.5.3",
|
"css-loader": "^5.2.6",
|
||||||
"html-webpack-plugin": "^3.2.0",
|
"html-webpack-plugin": "^5.5.0",
|
||||||
"style-loader": "^1.2.1",
|
"style-loader": "^2.0.0",
|
||||||
"text-encoding": "^0.7.0",
|
"text-encoding": "^0.7.0",
|
||||||
"webpack": "^4.29.4",
|
"webpack": "^5.95.0",
|
||||||
"webpack-cli": "^3.1.1",
|
"webpack-cli": "^4.8.0",
|
||||||
"webpack-dev-server": "^3.1.0"
|
"webpack-dev-server": "^5.0.4"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"bootstrap": "^4.5.0",
|
"bootstrap": "^5.0.2",
|
||||||
"jquery": "^3.5.1",
|
"jquery": "^3.6.0",
|
||||||
"popper.js": "^1.16.1"
|
"@popperjs/core": "^2.9.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,15 +1,28 @@
|
||||||
|
use cloudevents::binding::reqwest::RequestBuilderExt;
|
||||||
|
use cloudevents::{EventBuilder, EventBuilderV10};
|
||||||
use wasm_bindgen::prelude::*;
|
use wasm_bindgen::prelude::*;
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
#[wasm_bindgen]
|
#[wasm_bindgen]
|
||||||
pub async fn run(target: String, ty: String, datacontenttype: String, data: String) -> Result<(), String> {
|
pub async fn run(
|
||||||
let event = cloudevents::EventBuilder::new()
|
target: String,
|
||||||
|
ty: String,
|
||||||
|
datacontenttype: String,
|
||||||
|
data: String,
|
||||||
|
) -> Result<(), JsValue> {
|
||||||
|
let event = EventBuilderV10::new()
|
||||||
|
.id(&Uuid::new_v4().hyphenated().to_string())
|
||||||
.ty(ty)
|
.ty(ty)
|
||||||
|
.source("http://localhost/")
|
||||||
.data(datacontenttype, data)
|
.data(datacontenttype, data)
|
||||||
.build();
|
.build()
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
println!("Going to send event: {:?}", event);
|
println!("Going to send event: {:?}", event);
|
||||||
|
|
||||||
cloudevents_sdk_reqwest::event_to_request(event, reqwest::Client::new().post(&target))
|
reqwest::Client::new()
|
||||||
|
.post(&target)
|
||||||
|
.event(event)
|
||||||
.map_err(|e| e.to_string())?
|
.map_err(|e| e.to_string())?
|
||||||
.header("Access-Control-Allow-Origin", "*")
|
.header("Access-Control-Allow-Origin", "*")
|
||||||
.send()
|
.send()
|
||||||
|
@ -17,4 +30,4 @@ pub async fn run(target: String, ty: String, datacontenttype: String, data: Stri
|
||||||
.map_err(|e| e.to_string())?;
|
.map_err(|e| e.to_string())?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,12 @@
|
||||||
|
[package]
|
||||||
|
name = "warp-example"
|
||||||
|
version = "0.3.0"
|
||||||
|
authors = ["Marko Milenković <milenkovicm@users.noreply.github.com>"]
|
||||||
|
edition = "2018"
|
||||||
|
categories = ["web-programming", "encoding"]
|
||||||
|
license-file = "../LICENSE"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
cloudevents-sdk = { path = "../..", features = ["warp"] }
|
||||||
|
warp = "^0.3"
|
||||||
|
tokio = { version = "^1.0", features = ["full"] }
|
|
@ -0,0 +1,17 @@
|
||||||
|
To run the server:
|
||||||
|
|
||||||
|
```console
|
||||||
|
cargo run
|
||||||
|
```
|
||||||
|
|
||||||
|
To test a POST:
|
||||||
|
|
||||||
|
```console
|
||||||
|
curl -d '{"hello": "world"}' \
|
||||||
|
-H'content-type: application/json' \
|
||||||
|
-H'ce-specversion: 1.0' \
|
||||||
|
-H'ce-id: 1' \
|
||||||
|
-H'ce-source: http://cloudevents.io' \
|
||||||
|
-H'ce-type: dev.knative.example' \
|
||||||
|
http://localhost:3030
|
||||||
|
```
|
|
@ -0,0 +1,13 @@
|
||||||
|
use cloudevents::binding::warp::{filter, reply};
|
||||||
|
use warp::Filter;
|
||||||
|
|
||||||
|
#[tokio::main]
|
||||||
|
async fn main() {
|
||||||
|
let routes = warp::any()
|
||||||
|
// extracting event from request
|
||||||
|
.and(filter::to_event())
|
||||||
|
// returning event back
|
||||||
|
.map(|event| reply::from_event(event));
|
||||||
|
|
||||||
|
warp::serve(routes).run(([127, 0, 0, 1], 3030)).await;
|
||||||
|
}
|
|
@ -0,0 +1,17 @@
|
||||||
|
[package]
|
||||||
|
name = "wasi-example"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
anyhow = "1.0"
|
||||||
|
cloudevents-sdk = { path = "../..", features = ["http-0-2-binding", "hyper_wasi", "hyper-0-14" ] }
|
||||||
|
hyper_wasi = { version = "0.15", features = ["full"] }
|
||||||
|
log = "0.4.21"
|
||||||
|
tokio_wasi = { version = "1", features = ["io-util", "fs", "net", "time", "rt", "macros"] }
|
||||||
|
serde_json = "^1.0"
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
|
bytes = "1.6.0"
|
||||||
|
http-body-util = "0.1.1"
|
||||||
|
chrono = "*"
|
|
@ -0,0 +1,26 @@
|
||||||
|
Install WASMEdge:
|
||||||
|
|
||||||
|
https://wasmedge.org/docs/start/install/
|
||||||
|
|
||||||
|
To run the server:
|
||||||
|
```console
|
||||||
|
cargo run --target wasm32-wasi
|
||||||
|
```
|
||||||
|
|
||||||
|
To test a GET:
|
||||||
|
|
||||||
|
```console
|
||||||
|
curl -sw '%{http_code}\n' http://localhost:9000/health/readiness
|
||||||
|
```
|
||||||
|
|
||||||
|
To test a POST:
|
||||||
|
|
||||||
|
```console
|
||||||
|
curl -d '{"name": "wasi-womble"}' \
|
||||||
|
-H'content-type: application/json' \
|
||||||
|
-H'ce-specversion: 1.0' \
|
||||||
|
-H'ce-id: 1' \
|
||||||
|
-H'ce-source: http://cloudevents.io' \
|
||||||
|
-H'ce-type: dev.knative.example' \
|
||||||
|
http://localhost:9000
|
||||||
|
```
|
|
@ -0,0 +1,39 @@
|
||||||
|
use cloudevents::{event::Data, Event, EventBuilder, EventBuilderV10};
|
||||||
|
use log::info;
|
||||||
|
use serde_json::{from_slice, from_str, json};
|
||||||
|
|
||||||
|
pub async fn handle_event(event: Event) -> Result<Event, anyhow::Error> {
|
||||||
|
info!("event: {}", event);
|
||||||
|
|
||||||
|
let input = match event.data() {
|
||||||
|
Some(Data::Binary(v)) => from_slice(v)?,
|
||||||
|
Some(Data::String(v)) => from_str(v)?,
|
||||||
|
Some(Data::Json(v)) => v.to_owned(),
|
||||||
|
None => json!({ "name": "default" }),
|
||||||
|
};
|
||||||
|
|
||||||
|
EventBuilderV10::from(event)
|
||||||
|
.source("func://handler")
|
||||||
|
.ty("func.example")
|
||||||
|
.data("application/json", json!({ "hello": input["name"] }))
|
||||||
|
.build()
|
||||||
|
.map_err(|err| err.into())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
#[tokio::test]
|
||||||
|
async fn post_test() -> Result<(), anyhow::Error> {
|
||||||
|
let reqevt = Event::default();
|
||||||
|
let respevt = handle_event(reqevt).await?;
|
||||||
|
let output = match respevt.data() {
|
||||||
|
Some(Data::Binary(v)) => from_slice(v)?,
|
||||||
|
Some(Data::String(v)) => from_str(v)?,
|
||||||
|
Some(Data::Json(v)) => v.to_owned(),
|
||||||
|
None => json!({ "name": "default" }),
|
||||||
|
};
|
||||||
|
assert_eq!(output, json!({ "hello": "default" }));
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,49 @@
|
||||||
|
use cloudevents::binding::http_0_2::builder::adapter::to_response;
|
||||||
|
use cloudevents::binding::http_0_2::to_event;
|
||||||
|
|
||||||
|
use hyper::service::{make_service_fn, service_fn};
|
||||||
|
use hyper::Server;
|
||||||
|
use hyper::{Body, Method, Request, Response, StatusCode};
|
||||||
|
use std::convert::Infallible;
|
||||||
|
use std::net::SocketAddr;
|
||||||
|
use std::result::Result;
|
||||||
|
|
||||||
|
mod handler;
|
||||||
|
|
||||||
|
#[allow(clippy::redundant_closure)]
|
||||||
|
#[tokio::main(flavor = "current_thread")]
|
||||||
|
async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
||||||
|
let addr = SocketAddr::from(([0, 0, 0, 0], 9000));
|
||||||
|
let make_svc = make_service_fn(|_| async move {
|
||||||
|
Ok::<_, Infallible>(service_fn(move |req| handle_request(req)))
|
||||||
|
});
|
||||||
|
let server = Server::bind(&addr).serve(make_svc);
|
||||||
|
if let Err(e) = server.await {
|
||||||
|
eprintln!("server error: {}", e);
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
async fn handle_request(
|
||||||
|
req: Request<Body>,
|
||||||
|
) -> Result<Response<Body>, anyhow::Error> {
|
||||||
|
match (req.method(), req.uri().path()) {
|
||||||
|
(&Method::POST, "/") => {
|
||||||
|
let headers = req.headers().clone();
|
||||||
|
let body_bytes = hyper::body::to_bytes(req.into_body()).await?;
|
||||||
|
let body = body_bytes.to_vec();
|
||||||
|
let reqevt = to_event(&headers, body)?;
|
||||||
|
let _respevt = handler::handle_event(reqevt).await?;
|
||||||
|
|
||||||
|
to_response(_respevt).map_err(|err| err.into())
|
||||||
|
}
|
||||||
|
(&Method::GET, "/health/readiness") => {
|
||||||
|
Ok(Response::new(Body::from("")))
|
||||||
|
}
|
||||||
|
(&Method::GET, "/health/liveness") => Ok(Response::new(Body::from(""))),
|
||||||
|
_ => {
|
||||||
|
let mut not_found = Response::default();
|
||||||
|
*not_found.status_mut() = StatusCode::NOT_FOUND;
|
||||||
|
Ok(not_found)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,83 @@
|
||||||
|
//! This module integrates the [cloudevents-sdk](https://docs.rs/cloudevents-sdk) with [Actix web](https://docs.rs/actix-web/) to easily send and receive CloudEvents.
|
||||||
|
//!
|
||||||
|
//! To deserialize an HTTP request as CloudEvent:
|
||||||
|
//!
|
||||||
|
//! ```
|
||||||
|
//! use cloudevents::Event;
|
||||||
|
//! use actix_web::post;
|
||||||
|
//!
|
||||||
|
//! #[post("/")]
|
||||||
|
//! async fn post_event(event: Event) -> Result<String, actix_web::Error> {
|
||||||
|
//! println!("Received Event: {:?}", event);
|
||||||
|
//! Ok(format!("{:?}", event))
|
||||||
|
//! }
|
||||||
|
//! ```
|
||||||
|
//!
|
||||||
|
//! For more complex applications, access the Payload directly:
|
||||||
|
//!
|
||||||
|
//! ```
|
||||||
|
//! use cloudevents::binding::actix::HttpRequestExt;
|
||||||
|
//! use actix_web::{HttpRequest, web, post};
|
||||||
|
//!
|
||||||
|
//! #[post("/")]
|
||||||
|
//! async fn post_event(req: HttpRequest, payload: web::Payload) -> Result<String, actix_web::Error> {
|
||||||
|
//! let event = req.to_event(payload).await?;
|
||||||
|
//! println!("Received Event: {:?}", event);
|
||||||
|
//! Ok(format!("{:?}", event))
|
||||||
|
//! }
|
||||||
|
//! ```
|
||||||
|
//!
|
||||||
|
//! To serialize a CloudEvent to an HTTP response:
|
||||||
|
//!
|
||||||
|
//! ```
|
||||||
|
//! use actix_web::get;
|
||||||
|
//! use cloudevents::{Event, EventBuilderV10, EventBuilder};
|
||||||
|
//! use serde_json::json;
|
||||||
|
//!
|
||||||
|
//! #[get("/")]
|
||||||
|
//! async fn get_event() -> Event {
|
||||||
|
//! let payload = json!({"hello": "world"});
|
||||||
|
//!
|
||||||
|
//! EventBuilderV10::new()
|
||||||
|
//! .id("0001")
|
||||||
|
//! .ty("example.test")
|
||||||
|
//! .source("http://localhost/")
|
||||||
|
//! .data("application/json", payload)
|
||||||
|
//! .extension("someint", "10")
|
||||||
|
//! .build()
|
||||||
|
//! .unwrap()
|
||||||
|
//! }
|
||||||
|
//! ```
|
||||||
|
//!
|
||||||
|
//! For more complex applications, use the HTTP response builder extension:
|
||||||
|
//!
|
||||||
|
//! ```
|
||||||
|
//! use cloudevents::binding::actix::HttpResponseBuilderExt;
|
||||||
|
//! use actix_web::{get, HttpResponse};
|
||||||
|
//! use cloudevents::{EventBuilderV10, EventBuilder};
|
||||||
|
//! use serde_json::json;
|
||||||
|
//!
|
||||||
|
//! #[get("/")]
|
||||||
|
//! async fn get_event() -> Result<HttpResponse, actix_web::Error> {
|
||||||
|
//! HttpResponse::Ok()
|
||||||
|
//! .event(
|
||||||
|
//! EventBuilderV10::new()
|
||||||
|
//! .id("0001")
|
||||||
|
//! .ty("example.test")
|
||||||
|
//! .source("http://localhost/")
|
||||||
|
//! .data("application/json", json!({"hello": "world"}))
|
||||||
|
//! .build()
|
||||||
|
//! .expect("No error while building the event"),
|
||||||
|
//! )
|
||||||
|
//! }
|
||||||
|
//! ```
|
||||||
|
|
||||||
|
#![deny(rustdoc::broken_intra_doc_links)]
|
||||||
|
|
||||||
|
mod server_request;
|
||||||
|
mod server_response;
|
||||||
|
|
||||||
|
pub use server_request::request_to_event;
|
||||||
|
pub use server_request::HttpRequestExt;
|
||||||
|
pub use server_response::event_to_response;
|
||||||
|
pub use server_response::HttpResponseBuilderExt;
|
|
@ -0,0 +1,156 @@
|
||||||
|
use crate::binding::http_0_2::{to_event, Headers};
|
||||||
|
use crate::Event;
|
||||||
|
use actix_web::dev::Payload;
|
||||||
|
use actix_web::web::BytesMut;
|
||||||
|
use actix_web::{web, HttpRequest};
|
||||||
|
use async_trait::async_trait;
|
||||||
|
use futures::{future::LocalBoxFuture, FutureExt, StreamExt};
|
||||||
|
use http::header::{AsHeaderName, HeaderName, HeaderValue};
|
||||||
|
use http_0_2 as http;
|
||||||
|
|
||||||
|
/// Implement Headers for the actix HeaderMap
|
||||||
|
impl<'a> Headers<'a> for actix_http::header::HeaderMap {
|
||||||
|
type Iterator = Box<dyn Iterator<Item = (&'a HeaderName, &'a HeaderValue)> + 'a>;
|
||||||
|
fn get<K: AsHeaderName>(&self, key: K) -> Option<&HeaderValue> {
|
||||||
|
self.get(key.as_str())
|
||||||
|
}
|
||||||
|
fn iter(&'a self) -> Self::Iterator {
|
||||||
|
Box::new(self.iter())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Method to transform an incoming [`HttpRequest`] to [`Event`].
|
||||||
|
pub async fn request_to_event(
|
||||||
|
req: &HttpRequest,
|
||||||
|
mut payload: web::Payload,
|
||||||
|
) -> std::result::Result<Event, actix_web::error::Error> {
|
||||||
|
let mut bytes = BytesMut::new();
|
||||||
|
while let Some(item) = payload.next().await {
|
||||||
|
bytes.extend_from_slice(&item?);
|
||||||
|
}
|
||||||
|
to_event(req.headers(), bytes.to_vec()).map_err(actix_web::error::ErrorBadRequest)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// So that an actix-web handler may take an Event parameter
|
||||||
|
impl actix_web::FromRequest for Event {
|
||||||
|
type Error = actix_web::Error;
|
||||||
|
type Future = LocalBoxFuture<'static, std::result::Result<Self, Self::Error>>;
|
||||||
|
|
||||||
|
fn from_request(r: &HttpRequest, p: &mut Payload) -> Self::Future {
|
||||||
|
let request = r.to_owned();
|
||||||
|
bytes::Bytes::from_request(&request, p)
|
||||||
|
.map(move |bytes| match bytes {
|
||||||
|
Ok(b) => to_event(request.headers(), b.to_vec())
|
||||||
|
.map_err(actix_web::error::ErrorBadRequest),
|
||||||
|
Err(e) => Err(e),
|
||||||
|
})
|
||||||
|
.boxed_local()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Extension Trait for [`HttpRequest`] which acts as a wrapper for the function [`request_to_event()`].
|
||||||
|
///
|
||||||
|
/// This trait is sealed and cannot be implemented for types outside of this crate.
|
||||||
|
#[async_trait(?Send)]
|
||||||
|
pub trait HttpRequestExt: private::Sealed {
|
||||||
|
/// Convert this [`HttpRequest`] into an [`Event`].
|
||||||
|
async fn to_event(
|
||||||
|
&self,
|
||||||
|
mut payload: web::Payload,
|
||||||
|
) -> std::result::Result<Event, actix_web::error::Error>;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait(?Send)]
|
||||||
|
impl HttpRequestExt for HttpRequest {
|
||||||
|
async fn to_event(
|
||||||
|
&self,
|
||||||
|
payload: web::Payload,
|
||||||
|
) -> std::result::Result<Event, actix_web::error::Error> {
|
||||||
|
request_to_event(self, payload).await
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mod private {
|
||||||
|
// Sealing the RequestExt
|
||||||
|
pub trait Sealed {}
|
||||||
|
impl Sealed for actix_web::HttpRequest {}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use actix_web::{test, FromRequest};
|
||||||
|
|
||||||
|
use crate::test::fixtures;
|
||||||
|
use serde_json::json;
|
||||||
|
|
||||||
|
async fn to_event(req: &HttpRequest, mut payload: Payload) -> Event {
|
||||||
|
web::Payload::from_request(req, &mut payload)
|
||||||
|
.then(|p| req.to_event(p.unwrap()))
|
||||||
|
.await
|
||||||
|
.unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[actix_rt::test]
|
||||||
|
async fn test_request() {
|
||||||
|
let expected = fixtures::v10::minimal_string_extension();
|
||||||
|
|
||||||
|
let (req, payload) = test::TestRequest::post()
|
||||||
|
.insert_header(("ce-specversion", "1.0"))
|
||||||
|
.insert_header(("ce-id", "0001"))
|
||||||
|
.insert_header(("ce-type", "test_event.test_application"))
|
||||||
|
.insert_header(("ce-source", "http://localhost/"))
|
||||||
|
.insert_header(("ce-someint", "10"))
|
||||||
|
.to_http_parts();
|
||||||
|
|
||||||
|
assert_eq!(expected, to_event(&req, payload).await);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[actix_rt::test]
|
||||||
|
async fn test_request_with_full_data() {
|
||||||
|
let expected = fixtures::v10::full_binary_json_data_string_extension();
|
||||||
|
|
||||||
|
let (req, payload) = test::TestRequest::post()
|
||||||
|
.insert_header(("ce-specversion", "1.0"))
|
||||||
|
.insert_header(("ce-id", "0001"))
|
||||||
|
.insert_header(("ce-type", "test_event.test_application"))
|
||||||
|
.insert_header(("ce-subject", "cloudevents-sdk"))
|
||||||
|
.insert_header(("ce-source", "http://localhost/"))
|
||||||
|
.insert_header(("ce-time", fixtures::time().to_rfc3339()))
|
||||||
|
.insert_header(("ce-string_ex", "val"))
|
||||||
|
.insert_header(("ce-int_ex", "10"))
|
||||||
|
.insert_header(("ce-bool_ex", "true"))
|
||||||
|
.insert_header(("content-type", "application/json"))
|
||||||
|
.set_json(fixtures::json_data())
|
||||||
|
.to_http_parts();
|
||||||
|
|
||||||
|
assert_eq!(expected, to_event(&req, payload).await);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[actix_rt::test]
|
||||||
|
async fn test_structured_request_with_full_data() {
|
||||||
|
let payload = json!({
|
||||||
|
"specversion": "1.0",
|
||||||
|
"id": "0001",
|
||||||
|
"type": "test_event.test_application",
|
||||||
|
"subject": "cloudevents-sdk",
|
||||||
|
"source": "http://localhost/",
|
||||||
|
"time": fixtures::time().to_rfc3339(),
|
||||||
|
"string_ex": "val",
|
||||||
|
"int_ex": "10",
|
||||||
|
"bool_ex": "true",
|
||||||
|
"datacontenttype": "application/json",
|
||||||
|
"data": fixtures::json_data()
|
||||||
|
});
|
||||||
|
let bytes = serde_json::to_string(&payload).expect("Failed to serialize test data to json");
|
||||||
|
|
||||||
|
let expected = fixtures::v10::full_json_data_string_extension();
|
||||||
|
|
||||||
|
let (req, payload) = test::TestRequest::post()
|
||||||
|
.insert_header(("content-type", "application/cloudevents+json"))
|
||||||
|
.set_payload(bytes)
|
||||||
|
.to_http_parts();
|
||||||
|
|
||||||
|
assert_eq!(expected, to_event(&req, payload).await);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,143 @@
|
||||||
|
use crate::binding::http_0_2::{Builder, Serializer};
|
||||||
|
use crate::message::{BinaryDeserializer, Result};
|
||||||
|
use crate::Event;
|
||||||
|
use actix_web::http::StatusCode;
|
||||||
|
use actix_web::{HttpRequest, HttpResponse, HttpResponseBuilder};
|
||||||
|
use http_0_2 as http;
|
||||||
|
|
||||||
|
impl Builder<HttpResponse> for HttpResponseBuilder {
|
||||||
|
fn header(&mut self, key: &str, value: http::header::HeaderValue) {
|
||||||
|
self.insert_header((key, value));
|
||||||
|
}
|
||||||
|
fn body(&mut self, bytes: Vec<u8>) -> Result<HttpResponse> {
|
||||||
|
Ok(HttpResponseBuilder::body(self, bytes))
|
||||||
|
}
|
||||||
|
fn finish(&mut self) -> Result<HttpResponse> {
|
||||||
|
Ok(HttpResponseBuilder::finish(self))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Method to fill an [`HttpResponseBuilder`] with an [`Event`].
|
||||||
|
pub fn event_to_response<T: Builder<HttpResponse> + 'static>(
|
||||||
|
event: Event,
|
||||||
|
response: T,
|
||||||
|
) -> std::result::Result<HttpResponse, actix_web::error::Error> {
|
||||||
|
BinaryDeserializer::deserialize_binary(event, Serializer::new(response))
|
||||||
|
.map_err(actix_web::error::ErrorBadRequest)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// So that an actix-web handler may return an Event
|
||||||
|
impl actix_web::Responder for Event {
|
||||||
|
type Body = actix_web::body::BoxBody;
|
||||||
|
fn respond_to(self, _: &HttpRequest) -> HttpResponse {
|
||||||
|
HttpResponse::build(StatusCode::OK).event(self).unwrap()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Extension Trait for [`HttpResponseBuilder`] which acts as a wrapper for the function [`event_to_response()`].
|
||||||
|
///
|
||||||
|
/// This trait is sealed and cannot be implemented for types outside of this crate.
|
||||||
|
pub trait HttpResponseBuilderExt: private::Sealed {
|
||||||
|
/// Fill this [`HttpResponseBuilder`] with an [`Event`].
|
||||||
|
fn event(self, event: Event) -> std::result::Result<HttpResponse, actix_web::Error>;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl HttpResponseBuilderExt for HttpResponseBuilder {
|
||||||
|
fn event(self, event: Event) -> std::result::Result<HttpResponse, actix_web::Error> {
|
||||||
|
event_to_response(event, self)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sealing the HttpResponseBuilderExt
|
||||||
|
mod private {
|
||||||
|
pub trait Sealed {}
|
||||||
|
impl Sealed for actix_web::HttpResponseBuilder {}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
use crate::test::fixtures;
|
||||||
|
use actix_web::http::StatusCode;
|
||||||
|
use actix_web::test;
|
||||||
|
|
||||||
|
#[actix_rt::test]
|
||||||
|
async fn test_response() {
|
||||||
|
let input = fixtures::v10::minimal_string_extension();
|
||||||
|
|
||||||
|
let resp = HttpResponseBuilder::new(StatusCode::OK)
|
||||||
|
.event(input)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
resp.headers()
|
||||||
|
.get("ce-specversion")
|
||||||
|
.unwrap()
|
||||||
|
.to_str()
|
||||||
|
.unwrap(),
|
||||||
|
"1.0"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
resp.headers().get("ce-id").unwrap().to_str().unwrap(),
|
||||||
|
"0001"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
resp.headers().get("ce-type").unwrap().to_str().unwrap(),
|
||||||
|
"test_event.test_application"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
resp.headers().get("ce-source").unwrap().to_str().unwrap(),
|
||||||
|
"http://localhost/"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
resp.headers().get("ce-someint").unwrap().to_str().unwrap(),
|
||||||
|
"10"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[actix_rt::test]
|
||||||
|
async fn test_response_with_full_data() {
|
||||||
|
let input = fixtures::v10::full_binary_json_data_string_extension();
|
||||||
|
|
||||||
|
let resp = HttpResponseBuilder::new(StatusCode::OK)
|
||||||
|
.event(input)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
resp.headers()
|
||||||
|
.get("ce-specversion")
|
||||||
|
.unwrap()
|
||||||
|
.to_str()
|
||||||
|
.unwrap(),
|
||||||
|
"1.0"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
resp.headers().get("ce-id").unwrap().to_str().unwrap(),
|
||||||
|
"0001"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
resp.headers().get("ce-type").unwrap().to_str().unwrap(),
|
||||||
|
"test_event.test_application"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
resp.headers().get("ce-source").unwrap().to_str().unwrap(),
|
||||||
|
"http://localhost/"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
resp.headers()
|
||||||
|
.get("content-type")
|
||||||
|
.unwrap()
|
||||||
|
.to_str()
|
||||||
|
.unwrap(),
|
||||||
|
"application/json"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
resp.headers().get("ce-int_ex").unwrap().to_str().unwrap(),
|
||||||
|
"10"
|
||||||
|
);
|
||||||
|
|
||||||
|
let sr = test::TestRequest::default().to_srv_response(resp);
|
||||||
|
assert_eq!(fixtures::json_data_binary(), test::read_body(sr).await);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,109 @@
|
||||||
|
use axum::body::Bytes;
|
||||||
|
use axum::extract::{FromRequest, Request};
|
||||||
|
use axum::response::Response;
|
||||||
|
use axum_lib as axum;
|
||||||
|
use http;
|
||||||
|
use http::StatusCode;
|
||||||
|
|
||||||
|
use crate::binding::http::to_event;
|
||||||
|
use crate::event::Event;
|
||||||
|
|
||||||
|
impl<S> FromRequest<S> for Event
|
||||||
|
where
|
||||||
|
Bytes: FromRequest<S>,
|
||||||
|
S: Send + Sync,
|
||||||
|
{
|
||||||
|
type Rejection = Response;
|
||||||
|
|
||||||
|
async fn from_request(req: Request, _state: &S) -> Result<Self, Self::Rejection> {
|
||||||
|
let (parts, body) = req.into_parts();
|
||||||
|
|
||||||
|
let body = axum::body::to_bytes(body, usize::MAX).await.map_err(|e| {
|
||||||
|
Response::builder()
|
||||||
|
.status(StatusCode::INTERNAL_SERVER_ERROR)
|
||||||
|
.body(axum::body::Body::from(e.to_string()))
|
||||||
|
.unwrap()
|
||||||
|
})?;
|
||||||
|
|
||||||
|
to_event(&parts.headers, body.to_vec()).map_err(|e| {
|
||||||
|
Response::builder()
|
||||||
|
.status(StatusCode::BAD_REQUEST)
|
||||||
|
.body(axum::body::Body::from(e.to_string()))
|
||||||
|
.unwrap()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use axum::body::Body;
|
||||||
|
use axum::extract::FromRequest;
|
||||||
|
use axum::http::{self, Request, StatusCode};
|
||||||
|
|
||||||
|
use crate::test::fixtures;
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn axum_test_request() {
|
||||||
|
let expected = fixtures::v10::minimal_string_extension();
|
||||||
|
|
||||||
|
let request = Request::builder()
|
||||||
|
.method(http::Method::POST)
|
||||||
|
.header("ce-specversion", "1.0")
|
||||||
|
.header("ce-id", "0001")
|
||||||
|
.header("ce-type", "test_event.test_application")
|
||||||
|
.header("ce-source", "http://localhost/")
|
||||||
|
.header("ce-someint", "10")
|
||||||
|
.body(Body::empty())
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let result = Event::from_request(request, &()).await.unwrap();
|
||||||
|
|
||||||
|
assert_eq!(expected, result);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn axum_test_bad_request() {
|
||||||
|
let request = Request::builder()
|
||||||
|
.method(http::Method::POST)
|
||||||
|
.header("ce-specversion", "BAD SPECIFICATION")
|
||||||
|
.header("ce-id", "0001")
|
||||||
|
.header("ce-type", "example.test")
|
||||||
|
.header("ce-source", "http://localhost/")
|
||||||
|
.header("ce-someint", "10")
|
||||||
|
.header("ce-time", fixtures::time().to_rfc3339())
|
||||||
|
.body(Body::empty())
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let result = Event::from_request(request, &()).await;
|
||||||
|
assert!(result.is_err());
|
||||||
|
let rejection = result.unwrap_err();
|
||||||
|
|
||||||
|
let reason = rejection.status();
|
||||||
|
assert_eq!(reason, StatusCode::BAD_REQUEST)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn axum_test_request_with_full_data() {
|
||||||
|
let expected = fixtures::v10::full_binary_json_data_string_extension();
|
||||||
|
|
||||||
|
let request = Request::builder()
|
||||||
|
.method(http::Method::POST)
|
||||||
|
.header("ce-specversion", "1.0")
|
||||||
|
.header("ce-id", "0001")
|
||||||
|
.header("ce-type", "test_event.test_application")
|
||||||
|
.header("ce-source", "http://localhost/")
|
||||||
|
.header("ce-subject", "cloudevents-sdk")
|
||||||
|
.header("content-type", "application/json")
|
||||||
|
.header("ce-string_ex", "val")
|
||||||
|
.header("ce-int_ex", "10")
|
||||||
|
.header("ce-bool_ex", "true")
|
||||||
|
.header("ce-time", &fixtures::time().to_rfc3339())
|
||||||
|
.body(Body::from(fixtures::json_data_binary()))
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let result = Event::from_request(request, &()).await.unwrap();
|
||||||
|
|
||||||
|
assert_eq!(expected, result);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,162 @@
|
||||||
|
//! This module integrates the [cloudevents-sdk](https://docs.rs/cloudevents-sdk) with [Axum web service framework](https://docs.rs/axum/)
|
||||||
|
//! to easily send and receive CloudEvents.
|
||||||
|
//!
|
||||||
|
//! To deserialize an HTTP request as CloudEvent
|
||||||
|
//!
|
||||||
|
//! To echo events:
|
||||||
|
//!
|
||||||
|
//! ```
|
||||||
|
//! use axum_lib as axum;
|
||||||
|
//! use axum::{
|
||||||
|
//! routing::{get, post},
|
||||||
|
//! Router,
|
||||||
|
//! };
|
||||||
|
//! use cloudevents::Event;
|
||||||
|
//! use http::StatusCode;
|
||||||
|
//!
|
||||||
|
//! fn app() -> Router {
|
||||||
|
//! Router::new()
|
||||||
|
//! .route("/", get(|| async { "hello from cloudevents server" }))
|
||||||
|
//! .route(
|
||||||
|
//! "/",
|
||||||
|
//! post(|event: Event| async move {
|
||||||
|
//! println!("received cloudevent {}", &event);
|
||||||
|
//! (StatusCode::OK, event)
|
||||||
|
//! }),
|
||||||
|
//! )
|
||||||
|
//! }
|
||||||
|
//!
|
||||||
|
//! ```
|
||||||
|
//!
|
||||||
|
//! To create event inside request handlers and send them as responses:
|
||||||
|
//!
|
||||||
|
//! ```
|
||||||
|
//! use axum_lib as axum;
|
||||||
|
//! use axum::{
|
||||||
|
//! routing::{get, post},
|
||||||
|
//! Router,
|
||||||
|
//! };
|
||||||
|
//! use cloudevents::{Event, EventBuilder, EventBuilderV10};
|
||||||
|
//! use http::StatusCode;
|
||||||
|
//! use serde_json::json;
|
||||||
|
//!
|
||||||
|
//! fn app() -> Router {
|
||||||
|
//! Router::new()
|
||||||
|
//! .route("/", get(|| async { "hello from cloudevents server" }))
|
||||||
|
//! .route(
|
||||||
|
//! "/",
|
||||||
|
//! post(|| async move {
|
||||||
|
//! let event = EventBuilderV10::new()
|
||||||
|
//! .id("1")
|
||||||
|
//! .source("url://example_response/")
|
||||||
|
//! .ty("example.ce")
|
||||||
|
//! .data(
|
||||||
|
//! mime::APPLICATION_JSON.to_string(),
|
||||||
|
//! json!({
|
||||||
|
//! "name": "John Doe",
|
||||||
|
//! "age": 43,
|
||||||
|
//! "phones": [
|
||||||
|
//! "+44 1234567",
|
||||||
|
//! "+44 2345678"
|
||||||
|
//! ]
|
||||||
|
//! }),
|
||||||
|
//! )
|
||||||
|
//! .build()
|
||||||
|
//! .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?;
|
||||||
|
//!
|
||||||
|
//! Ok::<Event, (StatusCode, String)>(event)
|
||||||
|
//! }),
|
||||||
|
//! )
|
||||||
|
//! }
|
||||||
|
//!
|
||||||
|
//! ```
|
||||||
|
|
||||||
|
pub mod extract;
|
||||||
|
pub mod response;
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
|
||||||
|
use axum_lib as axum;
|
||||||
|
|
||||||
|
use axum::{
|
||||||
|
body::Body,
|
||||||
|
http::{self, Request, StatusCode},
|
||||||
|
routing::{get, post},
|
||||||
|
Router,
|
||||||
|
};
|
||||||
|
use chrono::Utc;
|
||||||
|
use serde_json::json;
|
||||||
|
use tower::ServiceExt; // for `app.oneshot()`
|
||||||
|
|
||||||
|
use crate::Event;
|
||||||
|
|
||||||
|
fn echo_app() -> Router {
|
||||||
|
Router::new()
|
||||||
|
.route("/", get(|| async { "hello from cloudevents server" }))
|
||||||
|
.route(
|
||||||
|
"/",
|
||||||
|
post(|event: Event| async move {
|
||||||
|
println!("received cloudevent {}", &event);
|
||||||
|
(StatusCode::OK, event)
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn axum_mod_test() {
|
||||||
|
let app = echo_app();
|
||||||
|
let time = Utc::now();
|
||||||
|
let j = json!({"hello": "world"});
|
||||||
|
let request = Request::builder()
|
||||||
|
.method(http::Method::POST)
|
||||||
|
.header("ce-specversion", "1.0")
|
||||||
|
.header("ce-id", "0001")
|
||||||
|
.header("ce-type", "example.test")
|
||||||
|
.header("ce-source", "http://localhost/")
|
||||||
|
.header("ce-someint", "10")
|
||||||
|
.header("ce-time", time.to_rfc3339())
|
||||||
|
.header("content-type", "application/json")
|
||||||
|
.body(Body::from(serde_json::to_vec(&j).unwrap()))
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let resp = app.oneshot(request).await.unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
resp.headers()
|
||||||
|
.get("ce-specversion")
|
||||||
|
.unwrap()
|
||||||
|
.to_str()
|
||||||
|
.unwrap(),
|
||||||
|
"1.0"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
resp.headers().get("ce-id").unwrap().to_str().unwrap(),
|
||||||
|
"0001"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
resp.headers().get("ce-type").unwrap().to_str().unwrap(),
|
||||||
|
"example.test"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
resp.headers().get("ce-source").unwrap().to_str().unwrap(),
|
||||||
|
"http://localhost/"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
resp.headers()
|
||||||
|
.get("content-type")
|
||||||
|
.unwrap()
|
||||||
|
.to_str()
|
||||||
|
.unwrap(),
|
||||||
|
"application/json"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
resp.headers().get("ce-someint").unwrap().to_str().unwrap(),
|
||||||
|
"10"
|
||||||
|
);
|
||||||
|
|
||||||
|
let (_, body) = resp.into_parts();
|
||||||
|
let body = axum::body::to_bytes(body, usize::MAX).await.unwrap();
|
||||||
|
|
||||||
|
assert_eq!(j.to_string().as_bytes(), body);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,106 @@
|
||||||
|
use crate::binding::http::builder::adapter::to_response;
|
||||||
|
use crate::event::Event;
|
||||||
|
use axum::{body::Body, http::Response, response::IntoResponse};
|
||||||
|
use axum_lib as axum;
|
||||||
|
use http;
|
||||||
|
use http::{header, StatusCode};
|
||||||
|
|
||||||
|
impl IntoResponse for Event {
|
||||||
|
fn into_response(self) -> Response<Body> {
|
||||||
|
match to_response(self) {
|
||||||
|
Ok(resp) => {
|
||||||
|
let (parts, body) = resp.into_parts();
|
||||||
|
Response::from_parts(parts, Body::new(body))
|
||||||
|
}
|
||||||
|
Err(err) => Response::builder()
|
||||||
|
.status(StatusCode::INTERNAL_SERVER_ERROR)
|
||||||
|
.header(header::CONTENT_TYPE, "text/plain")
|
||||||
|
.body(Body::from(err.to_string()))
|
||||||
|
.unwrap(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
use crate::test::fixtures;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn axum_test_response() {
|
||||||
|
let input = fixtures::v10::minimal_string_extension();
|
||||||
|
|
||||||
|
let resp = input.into_response();
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
resp.headers()
|
||||||
|
.get("ce-specversion")
|
||||||
|
.unwrap()
|
||||||
|
.to_str()
|
||||||
|
.unwrap(),
|
||||||
|
"1.0"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
resp.headers().get("ce-id").unwrap().to_str().unwrap(),
|
||||||
|
"0001"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
resp.headers().get("ce-type").unwrap().to_str().unwrap(),
|
||||||
|
"test_event.test_application"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
resp.headers().get("ce-source").unwrap().to_str().unwrap(),
|
||||||
|
"http://localhost/"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
resp.headers().get("ce-someint").unwrap().to_str().unwrap(),
|
||||||
|
"10"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn axum_test_response_with_full_data() {
|
||||||
|
let input = fixtures::v10::full_binary_json_data_string_extension();
|
||||||
|
|
||||||
|
let resp = input.into_response();
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
resp.headers()
|
||||||
|
.get("ce-specversion")
|
||||||
|
.unwrap()
|
||||||
|
.to_str()
|
||||||
|
.unwrap(),
|
||||||
|
"1.0"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
resp.headers().get("ce-id").unwrap().to_str().unwrap(),
|
||||||
|
"0001"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
resp.headers().get("ce-type").unwrap().to_str().unwrap(),
|
||||||
|
"test_event.test_application"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
resp.headers().get("ce-source").unwrap().to_str().unwrap(),
|
||||||
|
"http://localhost/"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
resp.headers()
|
||||||
|
.get("content-type")
|
||||||
|
.unwrap()
|
||||||
|
.to_str()
|
||||||
|
.unwrap(),
|
||||||
|
"application/json"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
resp.headers().get("ce-int_ex").unwrap().to_str().unwrap(),
|
||||||
|
"10"
|
||||||
|
);
|
||||||
|
|
||||||
|
let (_, body) = resp.into_parts();
|
||||||
|
let body = axum::body::to_bytes(body, usize::MAX).await.unwrap();
|
||||||
|
|
||||||
|
assert_eq!(fixtures::json_data_binary(), body);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,42 @@
|
||||||
|
use bytes::Bytes;
|
||||||
|
use http::Response;
|
||||||
|
use http_body_util::Full;
|
||||||
|
use std::cell::Cell;
|
||||||
|
|
||||||
|
use crate::binding::http::{Builder, Serializer};
|
||||||
|
use crate::message::{BinaryDeserializer, Error, Result};
|
||||||
|
use crate::Event;
|
||||||
|
use std::convert::Infallible;
|
||||||
|
type BoxBody = http_body_util::combinators::UnsyncBoxBody<Bytes, Infallible>;
|
||||||
|
|
||||||
|
struct Adapter {
|
||||||
|
builder: Cell<http::response::Builder>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Builder<Response<BoxBody>> for Adapter {
|
||||||
|
fn header(&mut self, key: &str, value: http::header::HeaderValue) {
|
||||||
|
self.builder.set(self.builder.take().header(key, value));
|
||||||
|
}
|
||||||
|
|
||||||
|
fn body(&mut self, bytes: Vec<u8>) -> Result<Response<BoxBody>> {
|
||||||
|
self.builder
|
||||||
|
.take()
|
||||||
|
.body(BoxBody::new(Full::from(bytes)))
|
||||||
|
.map_err(|e| crate::message::Error::Other {
|
||||||
|
source: Box::new(e),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn finish(&mut self) -> Result<Response<BoxBody>> {
|
||||||
|
self.body(Vec::new())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn to_response(event: Event) -> std::result::Result<Response<BoxBody>, Error> {
|
||||||
|
BinaryDeserializer::deserialize_binary(
|
||||||
|
event,
|
||||||
|
Serializer::new(Adapter {
|
||||||
|
builder: Cell::new(http::Response::builder()),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
}
|
|
@ -0,0 +1,12 @@
|
||||||
|
#[cfg(feature = "hyper")]
|
||||||
|
pub mod adapter;
|
||||||
|
|
||||||
|
use crate::message::Result;
|
||||||
|
|
||||||
|
use http;
|
||||||
|
|
||||||
|
pub trait Builder<R> {
|
||||||
|
fn header(&mut self, key: &str, value: http::header::HeaderValue);
|
||||||
|
fn body(&mut self, bytes: Vec<u8>) -> Result<R>;
|
||||||
|
fn finish(&mut self) -> Result<R>;
|
||||||
|
}
|
|
@ -0,0 +1,102 @@
|
||||||
|
use super::{Headers, SPEC_VERSION_HEADER};
|
||||||
|
use crate::{
|
||||||
|
binding::CLOUDEVENTS_JSON_HEADER,
|
||||||
|
event::SpecVersion,
|
||||||
|
header_value_to_str, message,
|
||||||
|
message::{
|
||||||
|
BinaryDeserializer, BinarySerializer, Encoding, MessageAttributeValue, MessageDeserializer,
|
||||||
|
Result, StructuredDeserializer, StructuredSerializer,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
use http;
|
||||||
|
use std::convert::TryFrom;
|
||||||
|
|
||||||
|
pub struct Deserializer<'a, T: Headers<'a>> {
|
||||||
|
headers: &'a T,
|
||||||
|
body: Vec<u8>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a, T: Headers<'a>> Deserializer<'a, T> {
|
||||||
|
pub fn new(headers: &'a T, body: Vec<u8>) -> Deserializer<'a, T> {
|
||||||
|
Deserializer { headers, body }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a, T: Headers<'a>> BinaryDeserializer for Deserializer<'a, T> {
|
||||||
|
fn deserialize_binary<R: Sized, V: BinarySerializer<R>>(self, mut visitor: V) -> Result<R> {
|
||||||
|
if self.encoding() != Encoding::BINARY {
|
||||||
|
return Err(message::Error::WrongEncoding {});
|
||||||
|
}
|
||||||
|
|
||||||
|
let spec_version = SpecVersion::try_from(
|
||||||
|
self.headers
|
||||||
|
.get(SPEC_VERSION_HEADER)
|
||||||
|
.map(|a| header_value_to_str!(a))
|
||||||
|
.unwrap()?,
|
||||||
|
)?;
|
||||||
|
|
||||||
|
let attributes = spec_version.attribute_names();
|
||||||
|
|
||||||
|
visitor = visitor.set_spec_version(spec_version)?;
|
||||||
|
|
||||||
|
for (hn, hv) in self.headers.iter().filter(|(hn, _)| {
|
||||||
|
let key = hn.as_str();
|
||||||
|
SPEC_VERSION_HEADER.ne(key) && key.starts_with("ce-")
|
||||||
|
}) {
|
||||||
|
let name = &hn.as_str()["ce-".len()..];
|
||||||
|
|
||||||
|
if attributes.contains(&name) {
|
||||||
|
visitor = visitor.set_attribute(
|
||||||
|
name,
|
||||||
|
MessageAttributeValue::String(String::from(header_value_to_str!(hv)?)),
|
||||||
|
)?
|
||||||
|
} else {
|
||||||
|
visitor = visitor.set_extension(
|
||||||
|
name,
|
||||||
|
MessageAttributeValue::String(String::from(header_value_to_str!(hv)?)),
|
||||||
|
)?
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(hv) = self.headers.get(http::header::CONTENT_TYPE) {
|
||||||
|
visitor = visitor.set_attribute(
|
||||||
|
"datacontenttype",
|
||||||
|
MessageAttributeValue::String(String::from(header_value_to_str!(hv)?)),
|
||||||
|
)?
|
||||||
|
}
|
||||||
|
|
||||||
|
if !self.body.is_empty() {
|
||||||
|
visitor.end_with_data(self.body)
|
||||||
|
} else {
|
||||||
|
visitor.end()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a, T: Headers<'a>> StructuredDeserializer for Deserializer<'a, T> {
|
||||||
|
fn deserialize_structured<R: Sized, V: StructuredSerializer<R>>(self, visitor: V) -> Result<R> {
|
||||||
|
if self.encoding() != Encoding::STRUCTURED {
|
||||||
|
return Err(message::Error::WrongEncoding {});
|
||||||
|
}
|
||||||
|
visitor.set_structured_event(self.body)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a, T: Headers<'a>> MessageDeserializer for Deserializer<'a, T> {
|
||||||
|
fn encoding(&self) -> Encoding {
|
||||||
|
if self
|
||||||
|
.headers
|
||||||
|
.get(http::header::CONTENT_TYPE)
|
||||||
|
.and_then(|v| v.to_str().ok())
|
||||||
|
.filter(|&v| v.starts_with(CLOUDEVENTS_JSON_HEADER))
|
||||||
|
.is_some()
|
||||||
|
{
|
||||||
|
Encoding::STRUCTURED
|
||||||
|
} else if self.headers.get(SPEC_VERSION_HEADER).is_some() {
|
||||||
|
Encoding::BINARY
|
||||||
|
} else {
|
||||||
|
Encoding::UNKNOWN
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,23 @@
|
||||||
|
use http::header::{AsHeaderName, HeaderMap, HeaderName, HeaderValue};
|
||||||
|
|
||||||
|
use http;
|
||||||
|
|
||||||
|
/// Any http library should be able to use the
|
||||||
|
/// [`to_event`](super::to_event) function with an implementation of
|
||||||
|
/// this trait.
|
||||||
|
pub trait Headers<'a> {
|
||||||
|
type Iterator: Iterator<Item = (&'a HeaderName, &'a HeaderValue)>;
|
||||||
|
fn get<K: AsHeaderName>(&self, name: K) -> Option<&HeaderValue>;
|
||||||
|
fn iter(&'a self) -> Self::Iterator;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Implemention for the HeaderMap used by warp/reqwest
|
||||||
|
impl<'a> Headers<'a> for HeaderMap<HeaderValue> {
|
||||||
|
type Iterator = http::header::Iter<'a, HeaderValue>;
|
||||||
|
fn get<K: AsHeaderName>(&self, name: K) -> Option<&HeaderValue> {
|
||||||
|
self.get(name)
|
||||||
|
}
|
||||||
|
fn iter(&'a self) -> Self::Iterator {
|
||||||
|
self.iter()
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,75 @@
|
||||||
|
pub mod builder;
|
||||||
|
pub mod deserializer;
|
||||||
|
mod headers;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
message::{Error, MessageDeserializer},
|
||||||
|
Event,
|
||||||
|
};
|
||||||
|
use deserializer::Deserializer;
|
||||||
|
pub use headers::Headers;
|
||||||
|
mod serializer;
|
||||||
|
|
||||||
|
pub use builder::Builder;
|
||||||
|
use core::convert::TryFrom;
|
||||||
|
use http::Response;
|
||||||
|
|
||||||
|
use http;
|
||||||
|
pub use serializer::Serializer;
|
||||||
|
use std::convert::TryInto;
|
||||||
|
use std::fmt::Debug;
|
||||||
|
|
||||||
|
pub static SPEC_VERSION_HEADER: &str = "ce-specversion";
|
||||||
|
|
||||||
|
/// Turn a pile of HTTP headers and a body into a CloudEvent
|
||||||
|
pub fn to_event<'a, T: Headers<'a>>(
|
||||||
|
headers: &'a T,
|
||||||
|
body: Vec<u8>,
|
||||||
|
) -> std::result::Result<Event, Error> {
|
||||||
|
MessageDeserializer::into_event(Deserializer::new(headers, body))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn header_prefix(name: &str) -> String {
|
||||||
|
super::header_prefix("ce-", name)
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> TryFrom<Response<T>> for Event
|
||||||
|
where
|
||||||
|
T: TryInto<Vec<u8>>,
|
||||||
|
<T as TryInto<Vec<u8>>>::Error: Debug,
|
||||||
|
{
|
||||||
|
type Error = crate::message::Error;
|
||||||
|
|
||||||
|
fn try_from(response: Response<T>) -> Result<Self, Self::Error> {
|
||||||
|
let headers = response.headers().to_owned();
|
||||||
|
let body = T::try_into(response.into_body()).unwrap();
|
||||||
|
|
||||||
|
to_event(&headers, body)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use crate::test::fixtures;
|
||||||
|
use crate::Event;
|
||||||
|
use core::convert::TryFrom;
|
||||||
|
use http::Response;
|
||||||
|
|
||||||
|
use http;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_response_to_event() {
|
||||||
|
let event = fixtures::v10::minimal_string_extension();
|
||||||
|
|
||||||
|
let response = Response::builder()
|
||||||
|
.header("ce-id", fixtures::id())
|
||||||
|
.header("ce-source", fixtures::source())
|
||||||
|
.header("ce-type", fixtures::ty())
|
||||||
|
.header("ce-specversion", "1.0")
|
||||||
|
.header("ce-someint", "10")
|
||||||
|
.body(Vec::new())
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
assert_eq!(event, Event::try_from(response).unwrap());
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,161 @@
|
||||||
|
use std::{cell::RefCell, rc::Rc};
|
||||||
|
|
||||||
|
use crate::binding::http::builder::Builder;
|
||||||
|
use crate::binding::{
|
||||||
|
http::{header_prefix, SPEC_VERSION_HEADER},
|
||||||
|
CLOUDEVENTS_JSON_HEADER,
|
||||||
|
};
|
||||||
|
use crate::event::SpecVersion;
|
||||||
|
use crate::message::BinaryDeserializer;
|
||||||
|
use crate::message::{
|
||||||
|
BinarySerializer, Error, MessageAttributeValue, Result, StructuredSerializer,
|
||||||
|
};
|
||||||
|
use crate::Event;
|
||||||
|
use http::Request;
|
||||||
|
|
||||||
|
use http;
|
||||||
|
use std::convert::TryFrom;
|
||||||
|
use std::fmt::Debug;
|
||||||
|
|
||||||
|
macro_rules! str_to_header_value {
|
||||||
|
($header_value:expr) => {
|
||||||
|
http::header::HeaderValue::from_str(&$header_value.to_string()).map_err(|e| {
|
||||||
|
crate::message::Error::Other {
|
||||||
|
source: Box::new(e),
|
||||||
|
}
|
||||||
|
})
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Serializer<T> {
|
||||||
|
builder: Rc<RefCell<dyn Builder<T>>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> Serializer<T> {
|
||||||
|
pub fn new<B: Builder<T> + 'static>(delegate: B) -> Serializer<T> {
|
||||||
|
let builder = Rc::new(RefCell::new(delegate));
|
||||||
|
Serializer { builder }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> BinarySerializer<T> for Serializer<T> {
|
||||||
|
fn set_spec_version(self, spec_version: SpecVersion) -> Result<Self> {
|
||||||
|
self.builder
|
||||||
|
.borrow_mut()
|
||||||
|
.header(SPEC_VERSION_HEADER, str_to_header_value!(spec_version)?);
|
||||||
|
Ok(self)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_attribute(self, name: &str, value: MessageAttributeValue) -> Result<Self> {
|
||||||
|
self.builder
|
||||||
|
.borrow_mut()
|
||||||
|
.header(&header_prefix(name), str_to_header_value!(value)?);
|
||||||
|
Ok(self)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_extension(self, name: &str, value: MessageAttributeValue) -> Result<Self> {
|
||||||
|
self.builder
|
||||||
|
.borrow_mut()
|
||||||
|
.header(&header_prefix(name), str_to_header_value!(value)?);
|
||||||
|
Ok(self)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn end_with_data(self, bytes: Vec<u8>) -> Result<T> {
|
||||||
|
self.builder.borrow_mut().body(bytes)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn end(self) -> Result<T> {
|
||||||
|
self.builder.borrow_mut().finish()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> StructuredSerializer<T> for Serializer<T> {
|
||||||
|
fn set_structured_event(self, bytes: Vec<u8>) -> Result<T> {
|
||||||
|
let mut builder = self.builder.borrow_mut();
|
||||||
|
builder.header(
|
||||||
|
http::header::CONTENT_TYPE.as_str(),
|
||||||
|
http::HeaderValue::from_static(CLOUDEVENTS_JSON_HEADER),
|
||||||
|
);
|
||||||
|
builder.body(bytes)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> BinarySerializer<http::request::Request<Option<T>>> for http::request::Builder
|
||||||
|
where
|
||||||
|
T: TryFrom<Vec<u8>>,
|
||||||
|
<T as TryFrom<Vec<u8>>>::Error: Debug,
|
||||||
|
{
|
||||||
|
fn set_spec_version(mut self, sv: SpecVersion) -> Result<Self> {
|
||||||
|
self = self.header(SPEC_VERSION_HEADER, &sv.to_string());
|
||||||
|
Ok(self)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_attribute(mut self, name: &str, value: MessageAttributeValue) -> Result<Self> {
|
||||||
|
let key = &header_prefix(name);
|
||||||
|
self = self.header(key, &value.to_string());
|
||||||
|
Ok(self)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_extension(mut self, name: &str, value: MessageAttributeValue) -> Result<Self> {
|
||||||
|
let key = &header_prefix(name);
|
||||||
|
self = self.header(key, &value.to_string());
|
||||||
|
Ok(self)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn end_with_data(self, bytes: Vec<u8>) -> Result<http::request::Request<Option<T>>> {
|
||||||
|
let body = T::try_from(bytes).unwrap();
|
||||||
|
self.body(Some(body)).map_err(|e| Error::Other {
|
||||||
|
source: Box::new(e),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn end(self) -> Result<http::request::Request<Option<T>>> {
|
||||||
|
self.body(None).map_err(|e| Error::Other {
|
||||||
|
source: Box::new(e),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> TryFrom<Event> for Request<Option<T>>
|
||||||
|
where
|
||||||
|
T: TryFrom<Vec<u8>>,
|
||||||
|
<T as TryFrom<Vec<u8>>>::Error: Debug,
|
||||||
|
{
|
||||||
|
type Error = crate::message::Error;
|
||||||
|
|
||||||
|
fn try_from(event: Event) -> Result<Self> {
|
||||||
|
BinaryDeserializer::deserialize_binary(event, http::request::Builder::new())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use crate::test::fixtures;
|
||||||
|
use bytes::Bytes;
|
||||||
|
use http::Request;
|
||||||
|
|
||||||
|
use http;
|
||||||
|
use std::convert::TryFrom;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_event_to_http_request() {
|
||||||
|
let event = fixtures::v10::minimal_string_extension();
|
||||||
|
let request: Request<Option<Vec<u8>>> = Request::try_from(event).unwrap();
|
||||||
|
|
||||||
|
assert_eq!(request.headers()["ce-id"], "0001");
|
||||||
|
assert_eq!(request.headers()["ce-type"], "test_event.test_application");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_event_to_bytes_body() {
|
||||||
|
let event = fixtures::v10::full_binary_json_data_string_extension();
|
||||||
|
let request: Request<Option<Vec<u8>>> = Request::try_from(event).unwrap();
|
||||||
|
|
||||||
|
assert_eq!(request.headers()["ce-id"], "0001");
|
||||||
|
assert_eq!(request.headers()["ce-type"], "test_event.test_application");
|
||||||
|
assert_eq!(
|
||||||
|
request.body().as_ref().unwrap(),
|
||||||
|
&Bytes::from(fixtures::json_data().to_string())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,44 @@
|
||||||
|
use http::Response;
|
||||||
|
use http_0_2 as http;
|
||||||
|
use hyper::body::Body;
|
||||||
|
use std::cell::Cell;
|
||||||
|
|
||||||
|
#[cfg(not(target_os = "wasi"))]
|
||||||
|
use hyper_0_14 as hyper;
|
||||||
|
|
||||||
|
#[cfg(target_os = "wasi")]
|
||||||
|
use hyper;
|
||||||
|
|
||||||
|
use crate::binding::http_0_2::{Builder, Serializer};
|
||||||
|
use crate::message::{BinaryDeserializer, Error, Result};
|
||||||
|
use crate::Event;
|
||||||
|
|
||||||
|
struct Adapter {
|
||||||
|
builder: Cell<http::response::Builder>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Builder<Response<Body>> for Adapter {
|
||||||
|
fn header(&mut self, key: &str, value: http::header::HeaderValue) {
|
||||||
|
self.builder.set(self.builder.take().header(key, value));
|
||||||
|
}
|
||||||
|
fn body(&mut self, bytes: Vec<u8>) -> Result<Response<Body>> {
|
||||||
|
self.builder
|
||||||
|
.take()
|
||||||
|
.body(Body::from(bytes))
|
||||||
|
.map_err(|e| crate::message::Error::Other {
|
||||||
|
source: Box::new(e),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
fn finish(&mut self) -> Result<Response<Body>> {
|
||||||
|
self.body(Vec::new())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn to_response(event: Event) -> std::result::Result<Response<Body>, Error> {
|
||||||
|
BinaryDeserializer::deserialize_binary(
|
||||||
|
event,
|
||||||
|
Serializer::new(Adapter {
|
||||||
|
builder: Cell::new(http::Response::builder()),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
}
|
|
@ -0,0 +1,11 @@
|
||||||
|
#[cfg(feature = "hyper-0-14")]
|
||||||
|
pub mod adapter;
|
||||||
|
|
||||||
|
use crate::message::Result;
|
||||||
|
use http_0_2 as http;
|
||||||
|
|
||||||
|
pub trait Builder<R> {
|
||||||
|
fn header(&mut self, key: &str, value: http::header::HeaderValue);
|
||||||
|
fn body(&mut self, bytes: Vec<u8>) -> Result<R>;
|
||||||
|
fn finish(&mut self) -> Result<R>;
|
||||||
|
}
|
|
@ -0,0 +1,101 @@
|
||||||
|
use super::{Headers, SPEC_VERSION_HEADER};
|
||||||
|
use crate::{
|
||||||
|
binding::CLOUDEVENTS_JSON_HEADER,
|
||||||
|
event::SpecVersion,
|
||||||
|
header_value_to_str, message,
|
||||||
|
message::{
|
||||||
|
BinaryDeserializer, BinarySerializer, Encoding, MessageAttributeValue, MessageDeserializer,
|
||||||
|
Result, StructuredDeserializer, StructuredSerializer,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
use http_0_2 as http;
|
||||||
|
use std::convert::TryFrom;
|
||||||
|
|
||||||
|
pub struct Deserializer<'a, T: Headers<'a>> {
|
||||||
|
headers: &'a T,
|
||||||
|
body: Vec<u8>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a, T: Headers<'a>> Deserializer<'a, T> {
|
||||||
|
pub fn new(headers: &'a T, body: Vec<u8>) -> Deserializer<'a, T> {
|
||||||
|
Deserializer { headers, body }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a, T: Headers<'a>> BinaryDeserializer for Deserializer<'a, T> {
|
||||||
|
fn deserialize_binary<R: Sized, V: BinarySerializer<R>>(self, mut visitor: V) -> Result<R> {
|
||||||
|
if self.encoding() != Encoding::BINARY {
|
||||||
|
return Err(message::Error::WrongEncoding {});
|
||||||
|
}
|
||||||
|
|
||||||
|
let spec_version = SpecVersion::try_from(
|
||||||
|
self.headers
|
||||||
|
.get(SPEC_VERSION_HEADER)
|
||||||
|
.map(|a| header_value_to_str!(a))
|
||||||
|
.unwrap()?,
|
||||||
|
)?;
|
||||||
|
|
||||||
|
let attributes = spec_version.attribute_names();
|
||||||
|
|
||||||
|
visitor = visitor.set_spec_version(spec_version)?;
|
||||||
|
|
||||||
|
for (hn, hv) in self.headers.iter().filter(|(hn, _)| {
|
||||||
|
let key = hn.as_str();
|
||||||
|
SPEC_VERSION_HEADER.ne(key) && key.starts_with("ce-")
|
||||||
|
}) {
|
||||||
|
let name = &hn.as_str()["ce-".len()..];
|
||||||
|
|
||||||
|
if attributes.contains(&name) {
|
||||||
|
visitor = visitor.set_attribute(
|
||||||
|
name,
|
||||||
|
MessageAttributeValue::String(String::from(header_value_to_str!(hv)?)),
|
||||||
|
)?
|
||||||
|
} else {
|
||||||
|
visitor = visitor.set_extension(
|
||||||
|
name,
|
||||||
|
MessageAttributeValue::String(String::from(header_value_to_str!(hv)?)),
|
||||||
|
)?
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(hv) = self.headers.get(http::header::CONTENT_TYPE) {
|
||||||
|
visitor = visitor.set_attribute(
|
||||||
|
"datacontenttype",
|
||||||
|
MessageAttributeValue::String(String::from(header_value_to_str!(hv)?)),
|
||||||
|
)?
|
||||||
|
}
|
||||||
|
|
||||||
|
if !self.body.is_empty() {
|
||||||
|
visitor.end_with_data(self.body)
|
||||||
|
} else {
|
||||||
|
visitor.end()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a, T: Headers<'a>> StructuredDeserializer for Deserializer<'a, T> {
|
||||||
|
fn deserialize_structured<R: Sized, V: StructuredSerializer<R>>(self, visitor: V) -> Result<R> {
|
||||||
|
if self.encoding() != Encoding::STRUCTURED {
|
||||||
|
return Err(message::Error::WrongEncoding {});
|
||||||
|
}
|
||||||
|
visitor.set_structured_event(self.body)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a, T: Headers<'a>> MessageDeserializer for Deserializer<'a, T> {
|
||||||
|
fn encoding(&self) -> Encoding {
|
||||||
|
if self
|
||||||
|
.headers
|
||||||
|
.get(http::header::CONTENT_TYPE)
|
||||||
|
.and_then(|v| v.to_str().ok())
|
||||||
|
.filter(|&v| v.starts_with(CLOUDEVENTS_JSON_HEADER))
|
||||||
|
.is_some()
|
||||||
|
{
|
||||||
|
Encoding::STRUCTURED
|
||||||
|
} else if self.headers.get(SPEC_VERSION_HEADER).is_some() {
|
||||||
|
Encoding::BINARY
|
||||||
|
} else {
|
||||||
|
Encoding::UNKNOWN
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,22 @@
|
||||||
|
use http::header::{AsHeaderName, HeaderMap, HeaderName, HeaderValue};
|
||||||
|
use http_0_2 as http;
|
||||||
|
|
||||||
|
/// Any http library should be able to use the
|
||||||
|
/// [`to_event`](super::to_event) function with an implementation of
|
||||||
|
/// this trait.
|
||||||
|
pub trait Headers<'a> {
|
||||||
|
type Iterator: Iterator<Item = (&'a HeaderName, &'a HeaderValue)>;
|
||||||
|
fn get<K: AsHeaderName>(&self, name: K) -> Option<&HeaderValue>;
|
||||||
|
fn iter(&'a self) -> Self::Iterator;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Implemention for the HeaderMap used by warp/reqwest
|
||||||
|
impl<'a> Headers<'a> for HeaderMap<HeaderValue> {
|
||||||
|
type Iterator = http::header::Iter<'a, HeaderValue>;
|
||||||
|
fn get<K: AsHeaderName>(&self, name: K) -> Option<&HeaderValue> {
|
||||||
|
self.get(name)
|
||||||
|
}
|
||||||
|
fn iter(&'a self) -> Self::Iterator {
|
||||||
|
self.iter()
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,73 @@
|
||||||
|
pub mod builder;
|
||||||
|
pub mod deserializer;
|
||||||
|
mod headers;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
message::{Error, MessageDeserializer},
|
||||||
|
Event,
|
||||||
|
};
|
||||||
|
use deserializer::Deserializer;
|
||||||
|
pub use headers::Headers;
|
||||||
|
mod serializer;
|
||||||
|
|
||||||
|
pub use builder::Builder;
|
||||||
|
use core::convert::TryFrom;
|
||||||
|
use http::Response;
|
||||||
|
use http_0_2 as http;
|
||||||
|
pub use serializer::Serializer;
|
||||||
|
use std::convert::TryInto;
|
||||||
|
use std::fmt::Debug;
|
||||||
|
|
||||||
|
pub static SPEC_VERSION_HEADER: &str = "ce-specversion";
|
||||||
|
|
||||||
|
/// Turn a pile of HTTP headers and a body into a CloudEvent
|
||||||
|
pub fn to_event<'a, T: Headers<'a>>(
|
||||||
|
headers: &'a T,
|
||||||
|
body: Vec<u8>,
|
||||||
|
) -> std::result::Result<Event, Error> {
|
||||||
|
MessageDeserializer::into_event(Deserializer::new(headers, body))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn header_prefix(name: &str) -> String {
|
||||||
|
super::header_prefix("ce-", name)
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> TryFrom<Response<T>> for Event
|
||||||
|
where
|
||||||
|
T: TryInto<Vec<u8>>,
|
||||||
|
<T as TryInto<Vec<u8>>>::Error: Debug,
|
||||||
|
{
|
||||||
|
type Error = crate::message::Error;
|
||||||
|
|
||||||
|
fn try_from(response: Response<T>) -> Result<Self, Self::Error> {
|
||||||
|
let headers = response.headers().to_owned();
|
||||||
|
let body = T::try_into(response.into_body()).unwrap();
|
||||||
|
|
||||||
|
to_event(&headers, body)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use crate::test::fixtures;
|
||||||
|
use crate::Event;
|
||||||
|
use core::convert::TryFrom;
|
||||||
|
use http::Response;
|
||||||
|
use http_0_2 as http;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_response_to_event() {
|
||||||
|
let event = fixtures::v10::minimal_string_extension();
|
||||||
|
|
||||||
|
let response = Response::builder()
|
||||||
|
.header("ce-id", fixtures::id())
|
||||||
|
.header("ce-source", fixtures::source())
|
||||||
|
.header("ce-type", fixtures::ty())
|
||||||
|
.header("ce-specversion", "1.0")
|
||||||
|
.header("ce-someint", "10")
|
||||||
|
.body(Vec::new())
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
assert_eq!(event, Event::try_from(response).unwrap());
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,159 @@
|
||||||
|
use std::{cell::RefCell, rc::Rc};
|
||||||
|
|
||||||
|
use crate::binding::http_0_2::builder::Builder;
|
||||||
|
use crate::binding::{
|
||||||
|
http_0_2::{header_prefix, SPEC_VERSION_HEADER},
|
||||||
|
CLOUDEVENTS_JSON_HEADER,
|
||||||
|
};
|
||||||
|
use crate::event::SpecVersion;
|
||||||
|
use crate::message::BinaryDeserializer;
|
||||||
|
use crate::message::{
|
||||||
|
BinarySerializer, Error, MessageAttributeValue, Result, StructuredSerializer,
|
||||||
|
};
|
||||||
|
use crate::Event;
|
||||||
|
use http::Request;
|
||||||
|
use http_0_2 as http;
|
||||||
|
use std::convert::TryFrom;
|
||||||
|
use std::fmt::Debug;
|
||||||
|
|
||||||
|
macro_rules! str_to_header_value {
|
||||||
|
($header_value:expr) => {
|
||||||
|
http::header::HeaderValue::from_str(&$header_value.to_string()).map_err(|e| {
|
||||||
|
crate::message::Error::Other {
|
||||||
|
source: Box::new(e),
|
||||||
|
}
|
||||||
|
})
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Serializer<T> {
|
||||||
|
builder: Rc<RefCell<dyn Builder<T>>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> Serializer<T> {
|
||||||
|
pub fn new<B: Builder<T> + 'static>(delegate: B) -> Serializer<T> {
|
||||||
|
let builder = Rc::new(RefCell::new(delegate));
|
||||||
|
Serializer { builder }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> BinarySerializer<T> for Serializer<T> {
|
||||||
|
fn set_spec_version(self, spec_version: SpecVersion) -> Result<Self> {
|
||||||
|
self.builder
|
||||||
|
.borrow_mut()
|
||||||
|
.header(SPEC_VERSION_HEADER, str_to_header_value!(spec_version)?);
|
||||||
|
Ok(self)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_attribute(self, name: &str, value: MessageAttributeValue) -> Result<Self> {
|
||||||
|
self.builder
|
||||||
|
.borrow_mut()
|
||||||
|
.header(&header_prefix(name), str_to_header_value!(value)?);
|
||||||
|
Ok(self)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_extension(self, name: &str, value: MessageAttributeValue) -> Result<Self> {
|
||||||
|
self.builder
|
||||||
|
.borrow_mut()
|
||||||
|
.header(&header_prefix(name), str_to_header_value!(value)?);
|
||||||
|
Ok(self)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn end_with_data(self, bytes: Vec<u8>) -> Result<T> {
|
||||||
|
self.builder.borrow_mut().body(bytes)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn end(self) -> Result<T> {
|
||||||
|
self.builder.borrow_mut().finish()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> StructuredSerializer<T> for Serializer<T> {
|
||||||
|
fn set_structured_event(self, bytes: Vec<u8>) -> Result<T> {
|
||||||
|
let mut builder = self.builder.borrow_mut();
|
||||||
|
builder.header(
|
||||||
|
http::header::CONTENT_TYPE.as_str(),
|
||||||
|
http::HeaderValue::from_static(CLOUDEVENTS_JSON_HEADER),
|
||||||
|
);
|
||||||
|
builder.body(bytes)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> BinarySerializer<http::request::Request<Option<T>>> for http::request::Builder
|
||||||
|
where
|
||||||
|
T: TryFrom<Vec<u8>>,
|
||||||
|
<T as TryFrom<Vec<u8>>>::Error: Debug,
|
||||||
|
{
|
||||||
|
fn set_spec_version(mut self, sv: SpecVersion) -> Result<Self> {
|
||||||
|
self = self.header(SPEC_VERSION_HEADER, &sv.to_string());
|
||||||
|
Ok(self)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_attribute(mut self, name: &str, value: MessageAttributeValue) -> Result<Self> {
|
||||||
|
let key = &header_prefix(name);
|
||||||
|
self = self.header(key, &value.to_string());
|
||||||
|
Ok(self)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_extension(mut self, name: &str, value: MessageAttributeValue) -> Result<Self> {
|
||||||
|
let key = &header_prefix(name);
|
||||||
|
self = self.header(key, &value.to_string());
|
||||||
|
Ok(self)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn end_with_data(self, bytes: Vec<u8>) -> Result<http::request::Request<Option<T>>> {
|
||||||
|
let body = T::try_from(bytes).unwrap();
|
||||||
|
self.body(Some(body)).map_err(|e| Error::Other {
|
||||||
|
source: Box::new(e),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn end(self) -> Result<http::request::Request<Option<T>>> {
|
||||||
|
self.body(None).map_err(|e| Error::Other {
|
||||||
|
source: Box::new(e),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> TryFrom<Event> for Request<Option<T>>
|
||||||
|
where
|
||||||
|
T: TryFrom<Vec<u8>>,
|
||||||
|
<T as TryFrom<Vec<u8>>>::Error: Debug,
|
||||||
|
{
|
||||||
|
type Error = crate::message::Error;
|
||||||
|
|
||||||
|
fn try_from(event: Event) -> Result<Self> {
|
||||||
|
BinaryDeserializer::deserialize_binary(event, http::request::Builder::new())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use crate::test::fixtures;
|
||||||
|
use bytes::Bytes;
|
||||||
|
use http::Request;
|
||||||
|
use http_0_2 as http;
|
||||||
|
use std::convert::TryFrom;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_event_to_http_request() {
|
||||||
|
let event = fixtures::v10::minimal_string_extension();
|
||||||
|
let request: Request<Option<Vec<u8>>> = Request::try_from(event).unwrap();
|
||||||
|
|
||||||
|
assert_eq!(request.headers()["ce-id"], "0001");
|
||||||
|
assert_eq!(request.headers()["ce-type"], "test_event.test_application");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_event_to_bytes_body() {
|
||||||
|
let event = fixtures::v10::full_binary_json_data_string_extension();
|
||||||
|
let request: Request<Option<Vec<u8>>> = Request::try_from(event).unwrap();
|
||||||
|
|
||||||
|
assert_eq!(request.headers()["ce-id"], "0001");
|
||||||
|
assert_eq!(request.headers()["ce-type"], "test_event.test_application");
|
||||||
|
assert_eq!(
|
||||||
|
request.body().as_ref().unwrap(),
|
||||||
|
&Bytes::from(fixtures::json_data().to_string())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,79 @@
|
||||||
|
//! Provides protocol binding implementations for [`crate::Event`].
|
||||||
|
|
||||||
|
#[cfg_attr(docsrs, doc(cfg(feature = "actix")))]
|
||||||
|
#[cfg(feature = "actix")]
|
||||||
|
pub mod actix;
|
||||||
|
#[cfg_attr(docsrs, doc(cfg(feature = "axum")))]
|
||||||
|
#[cfg(feature = "axum")]
|
||||||
|
pub mod axum;
|
||||||
|
|
||||||
|
#[cfg_attr(
|
||||||
|
docsrs,
|
||||||
|
doc(cfg(any(
|
||||||
|
feature = "http-binding",
|
||||||
|
feature = "reqwest",
|
||||||
|
feature = "axum",
|
||||||
|
feature = "poem"
|
||||||
|
)))
|
||||||
|
)]
|
||||||
|
#[cfg(any(
|
||||||
|
feature = "http-binding",
|
||||||
|
feature = "reqwest",
|
||||||
|
feature = "axum",
|
||||||
|
feature = "poem"
|
||||||
|
))]
|
||||||
|
pub mod http;
|
||||||
|
|
||||||
|
#[cfg_attr(
|
||||||
|
docsrs,
|
||||||
|
doc(cfg(any(feature = "http-0-2-binding", feature = "actix", feature = "warp",)))
|
||||||
|
)]
|
||||||
|
#[cfg(any(feature = "http-0-2-binding", feature = "actix", feature = "warp",))]
|
||||||
|
pub mod http_0_2;
|
||||||
|
|
||||||
|
#[cfg_attr(docsrs, doc(cfg(feature = "nats")))]
|
||||||
|
#[cfg(feature = "nats")]
|
||||||
|
pub mod nats;
|
||||||
|
#[cfg_attr(docsrs, doc(cfg(feature = "poem")))]
|
||||||
|
#[cfg(feature = "poem")]
|
||||||
|
pub mod poem;
|
||||||
|
#[cfg_attr(docsrs, doc(cfg(feature = "rdkafka")))]
|
||||||
|
#[cfg(feature = "rdkafka")]
|
||||||
|
pub mod rdkafka;
|
||||||
|
#[cfg_attr(docsrs, doc(cfg(feature = "reqwest")))]
|
||||||
|
#[cfg(feature = "reqwest")]
|
||||||
|
pub mod reqwest;
|
||||||
|
#[cfg_attr(docsrs, doc(cfg(feature = "warp")))]
|
||||||
|
#[cfg(feature = "warp")]
|
||||||
|
pub mod warp;
|
||||||
|
|
||||||
|
#[cfg(feature = "rdkafka")]
|
||||||
|
pub(crate) mod kafka {
|
||||||
|
pub static SPEC_VERSION_HEADER: &str = "ce_specversion";
|
||||||
|
pub fn header_prefix(name: &str) -> String {
|
||||||
|
super::header_prefix("ce_", name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) static CLOUDEVENTS_JSON_HEADER: &str = "application/cloudevents+json";
|
||||||
|
pub(crate) static CLOUDEVENTS_BATCH_JSON_HEADER: &str = "application/cloudevents-batch+json";
|
||||||
|
pub(crate) static CONTENT_TYPE: &str = "content-type";
|
||||||
|
|
||||||
|
fn header_prefix(prefix: &str, name: &str) -> String {
|
||||||
|
if name == "datacontenttype" {
|
||||||
|
CONTENT_TYPE.to_string()
|
||||||
|
} else {
|
||||||
|
[prefix, name].concat()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[macro_export]
|
||||||
|
macro_rules! header_value_to_str {
|
||||||
|
($header_value:expr) => {
|
||||||
|
$header_value
|
||||||
|
.to_str()
|
||||||
|
.map_err(|e| $crate::message::Error::Other {
|
||||||
|
source: Box::new(e),
|
||||||
|
})
|
||||||
|
};
|
||||||
|
}
|
|
@ -0,0 +1,77 @@
|
||||||
|
use crate::{
|
||||||
|
message::{Result, StructuredDeserializer},
|
||||||
|
Event,
|
||||||
|
};
|
||||||
|
|
||||||
|
use nats_lib as nats;
|
||||||
|
|
||||||
|
impl StructuredDeserializer for nats::Message {
|
||||||
|
fn deserialize_structured<R: Sized, V: crate::message::StructuredSerializer<R>>(
|
||||||
|
self,
|
||||||
|
serializer: V,
|
||||||
|
) -> crate::message::Result<R> {
|
||||||
|
serializer.set_structured_event(self.data.to_vec())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Trait implemented by [`nats::Message`] to enable convenient deserialization to [`Event`]
|
||||||
|
///
|
||||||
|
/// Trait sealed <https://rust-lang.github.io/api-guidelines/future-proofing.html#sealed-traits-protect-against-downstream-implementations-c-sealed>
|
||||||
|
pub trait MessageExt: private::Sealed {
|
||||||
|
fn to_event(&self) -> Result<Event>;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MessageExt for nats::Message {
|
||||||
|
fn to_event(&self) -> Result<Event> {
|
||||||
|
StructuredDeserializer::into_event(self.to_owned())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mod private {
|
||||||
|
use nats_lib as nats;
|
||||||
|
|
||||||
|
// Sealing the MessageExt
|
||||||
|
pub trait Sealed {}
|
||||||
|
impl Sealed for nats::Message {}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use crate::test::fixtures;
|
||||||
|
use nats_lib as nats;
|
||||||
|
use serde_json::json;
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_structured_deserialize_v10() {
|
||||||
|
let expected = fixtures::v10::full_json_data_string_extension();
|
||||||
|
|
||||||
|
let nats_message = nats::Message::new(
|
||||||
|
"not_relevant",
|
||||||
|
None,
|
||||||
|
json!(expected).to_string().as_bytes(),
|
||||||
|
None,
|
||||||
|
);
|
||||||
|
|
||||||
|
let actual = nats_message.to_event().unwrap();
|
||||||
|
|
||||||
|
assert_eq!(expected, actual)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_structured_deserialize_v03() {
|
||||||
|
let expected = fixtures::v03::full_json_data();
|
||||||
|
|
||||||
|
let nats_message = nats::Message::new(
|
||||||
|
"not_relevant",
|
||||||
|
None,
|
||||||
|
json!(expected).to_string().as_bytes(),
|
||||||
|
None,
|
||||||
|
);
|
||||||
|
|
||||||
|
let actual = nats_message.to_event().unwrap();
|
||||||
|
|
||||||
|
assert_eq!(expected, actual)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,43 @@
|
||||||
|
//! This module provides bindings between [cloudevents-sdk](https://docs.rs/cloudevents-sdk) and [nats](https://docs.rs/nats)
|
||||||
|
//! ## Examples
|
||||||
|
//! Deserialize [nats::Message](https://docs.rs/nats/0.21.0/nats/struct.Message.html) into [Event](https://docs.rs/cloudevents-sdk/latest/cloudevents/event/struct.Event.html)
|
||||||
|
//! ```
|
||||||
|
//! use nats_lib as nats;
|
||||||
|
//! use cloudevents::binding::nats::MessageExt;
|
||||||
|
//!
|
||||||
|
//! fn consume() {
|
||||||
|
//! let nc = nats::connect("localhost:4222").unwrap();
|
||||||
|
//! let sub = nc.subscribe("test").unwrap();
|
||||||
|
//! let nats_message = sub.next().unwrap();
|
||||||
|
//! let cloud_event = nats_message.to_event().unwrap();
|
||||||
|
//!
|
||||||
|
//! println!("{}", cloud_event.to_string());
|
||||||
|
//! }
|
||||||
|
//! ```
|
||||||
|
//!
|
||||||
|
//! Serialize [Event](https://docs.rs/cloudevents-sdk/latest/cloudevents/event/struct.Event.html) into [NatsCloudEvent] and publish to nats subject
|
||||||
|
//! ```
|
||||||
|
//! use nats_lib as nats;
|
||||||
|
//! use cloudevents::binding::nats::NatsCloudEvent;
|
||||||
|
//! use cloudevents::{EventBuilder, EventBuilderV10, Event};
|
||||||
|
//! use serde_json::json;
|
||||||
|
//!
|
||||||
|
//! fn publish() {
|
||||||
|
//! let nc = nats::connect("localhost:4222").unwrap();
|
||||||
|
//!
|
||||||
|
//! let event = EventBuilderV10::new()
|
||||||
|
//! .id("123".to_string())
|
||||||
|
//! .ty("example.test")
|
||||||
|
//! .source("http://localhost/")
|
||||||
|
//! .data("application/json", json!({"hello": "world"}))
|
||||||
|
//! .build()
|
||||||
|
//! .unwrap();
|
||||||
|
//!
|
||||||
|
//! nc.publish("whatever.subject.you.like", NatsCloudEvent::from_event(event).unwrap()).unwrap();
|
||||||
|
//! }
|
||||||
|
//! ```
|
||||||
|
mod deserializer;
|
||||||
|
mod serializer;
|
||||||
|
|
||||||
|
pub use deserializer::MessageExt;
|
||||||
|
pub use serializer::NatsCloudEvent;
|
|
@ -0,0 +1,26 @@
|
||||||
|
use crate::{
|
||||||
|
message::{Error, Result},
|
||||||
|
Event,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Helper struct containing text data bytes of JSON serialized [Event]
|
||||||
|
///
|
||||||
|
/// Implements [`AsRef`] so it can be directly passed to [`nats::Connection`](https://docs.rs/nats/0.21.0/nats/struct.Connection.html) methods as payload.
|
||||||
|
pub struct NatsCloudEvent {
|
||||||
|
pub payload: Vec<u8>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AsRef<[u8]> for NatsCloudEvent {
|
||||||
|
fn as_ref(&self) -> &[u8] {
|
||||||
|
self.payload.as_ref()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl NatsCloudEvent {
|
||||||
|
pub fn from_event(event: Event) -> Result<Self> {
|
||||||
|
match serde_json::to_vec(&event) {
|
||||||
|
Ok(payload) => Ok(Self { payload }),
|
||||||
|
Err(e) => Err(Error::SerdeJsonError { source: e }),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,84 @@
|
||||||
|
use crate::binding::http::to_event;
|
||||||
|
use crate::Event;
|
||||||
|
|
||||||
|
use poem_lib::error::ResponseError;
|
||||||
|
use poem_lib::http::StatusCode;
|
||||||
|
use poem_lib::{FromRequest, Request, RequestBody, Result};
|
||||||
|
|
||||||
|
impl ResponseError for crate::message::Error {
|
||||||
|
fn status(&self) -> StatusCode {
|
||||||
|
StatusCode::BAD_REQUEST
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> FromRequest<'a> for Event {
|
||||||
|
async fn from_request(req: &'a Request, body: &mut RequestBody) -> Result<Self> {
|
||||||
|
Ok(to_event(req.headers(), body.take()?.into_vec().await?)?)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use crate::test::fixtures;
|
||||||
|
use poem_lib::http::Method;
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_request() {
|
||||||
|
let expected = fixtures::v10::minimal_string_extension();
|
||||||
|
|
||||||
|
let req = Request::builder()
|
||||||
|
.method(Method::POST)
|
||||||
|
.header("ce-specversion", "1.0")
|
||||||
|
.header("ce-id", "0001")
|
||||||
|
.header("ce-type", "test_event.test_application")
|
||||||
|
.header("ce-source", "http://localhost/")
|
||||||
|
.header("ce-someint", "10")
|
||||||
|
.finish();
|
||||||
|
let (req, mut body) = req.split();
|
||||||
|
let result = Event::from_request(&req, &mut body).await.unwrap();
|
||||||
|
|
||||||
|
assert_eq!(expected, result);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_bad_request() {
|
||||||
|
let req = Request::builder()
|
||||||
|
.method(Method::POST)
|
||||||
|
.header("ce-specversion", "BAD SPECIFICATION")
|
||||||
|
.header("ce-id", "0001")
|
||||||
|
.header("ce-type", "example.test")
|
||||||
|
.header("ce-source", "http://localhost/")
|
||||||
|
.header("ce-someint", "10")
|
||||||
|
.header("ce-time", fixtures::time().to_rfc3339())
|
||||||
|
.finish();
|
||||||
|
|
||||||
|
let (req, mut body) = req.split();
|
||||||
|
let resp = Event::from_request(&req, &mut body).await.err().unwrap();
|
||||||
|
assert_eq!(resp.status(), StatusCode::BAD_REQUEST);
|
||||||
|
assert_eq!(resp.to_string(), "Invalid specversion BAD SPECIFICATION");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_request_with_full_data() {
|
||||||
|
let expected = fixtures::v10::full_binary_json_data_string_extension();
|
||||||
|
|
||||||
|
let req = Request::builder()
|
||||||
|
.method(Method::POST)
|
||||||
|
.header("ce-specversion", "1.0")
|
||||||
|
.header("ce-id", "0001")
|
||||||
|
.header("ce-type", "test_event.test_application")
|
||||||
|
.header("ce-source", "http://localhost/")
|
||||||
|
.header("ce-subject", "cloudevents-sdk")
|
||||||
|
.header("content-type", "application/json")
|
||||||
|
.header("ce-string_ex", "val")
|
||||||
|
.header("ce-int_ex", "10")
|
||||||
|
.header("ce-bool_ex", "true")
|
||||||
|
.header("ce-time", fixtures::time().to_rfc3339())
|
||||||
|
.body(fixtures::json_data_binary());
|
||||||
|
let (req, mut body) = req.split();
|
||||||
|
let result = Event::from_request(&req, &mut body).await.unwrap();
|
||||||
|
|
||||||
|
assert_eq!(expected, result);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,57 @@
|
||||||
|
//! This module integrates the [cloudevents-sdk](https://docs.rs/cloudevents-sdk) with
|
||||||
|
//! [Poem](https://docs.rs/poem/) to easily send and receive CloudEvents.
|
||||||
|
//!
|
||||||
|
//! To deserialize an HTTP request as CloudEvent
|
||||||
|
//!
|
||||||
|
//! To echo events:
|
||||||
|
//!
|
||||||
|
//! ```rust
|
||||||
|
//! use cloudevents::Event;
|
||||||
|
//! use poem_lib as poem;
|
||||||
|
//! use poem::{handler, Route, post};
|
||||||
|
//!
|
||||||
|
//! #[handler]
|
||||||
|
//! async fn index(event: Event) -> Event {
|
||||||
|
//! println!("received cloudevent {}", &event);
|
||||||
|
//! event
|
||||||
|
//! }
|
||||||
|
//!
|
||||||
|
//! let app = Route::new().at("/", post(index));
|
||||||
|
//! ```
|
||||||
|
//!
|
||||||
|
//! To create event inside request handlers and send them as responses:
|
||||||
|
//!
|
||||||
|
//! ```rust
|
||||||
|
//! use cloudevents::{Event, EventBuilder, EventBuilderV10};
|
||||||
|
//! use poem_lib as poem;
|
||||||
|
//! use poem::{handler, Route, post, Result};
|
||||||
|
//! use poem::error::InternalServerError;
|
||||||
|
//! use serde_json::json;
|
||||||
|
//!
|
||||||
|
//! #[handler]
|
||||||
|
//! async fn index() -> Result<Event> {
|
||||||
|
//! let event = EventBuilderV10::new()
|
||||||
|
//! .id("1")
|
||||||
|
//! .source("url://example_response/")
|
||||||
|
//! .ty("example.ce")
|
||||||
|
//! .data(
|
||||||
|
//! mime::APPLICATION_JSON.to_string(),
|
||||||
|
//! json!({
|
||||||
|
//! "name": "John Doe",
|
||||||
|
//! "age": 43,
|
||||||
|
//! "phones": [
|
||||||
|
//! "+44 1234567",
|
||||||
|
//! "+44 2345678"
|
||||||
|
//! ]
|
||||||
|
//! }),
|
||||||
|
//! )
|
||||||
|
//! .build()
|
||||||
|
//! .map_err(InternalServerError)?;
|
||||||
|
//! Ok(event)
|
||||||
|
//! }
|
||||||
|
//!
|
||||||
|
//! let app = Route::new().at("/", post(index));
|
||||||
|
//! ```
|
||||||
|
|
||||||
|
mod extractor;
|
||||||
|
mod response;
|
|
@ -0,0 +1,114 @@
|
||||||
|
use crate::{AttributesReader, Data, Event};
|
||||||
|
|
||||||
|
use bytes::Bytes;
|
||||||
|
use poem_lib::http::StatusCode;
|
||||||
|
use poem_lib::{IntoResponse, Response};
|
||||||
|
|
||||||
|
impl IntoResponse for Event {
|
||||||
|
fn into_response(self) -> Response {
|
||||||
|
let mut builder = Response::builder().status(StatusCode::OK);
|
||||||
|
|
||||||
|
if let Some(dct) = self.datacontenttype() {
|
||||||
|
builder = builder.content_type(dct);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (key, value) in self.iter() {
|
||||||
|
builder = builder.header(format!("ce-{key}").as_str(), value.to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
match self.data {
|
||||||
|
Some(data) => match data {
|
||||||
|
Data::Binary(v) => builder.body(Bytes::copy_from_slice(v.as_slice())),
|
||||||
|
Data::String(s) => builder.body(s.clone()),
|
||||||
|
Data::Json(j) => match serde_json::to_string(&j) {
|
||||||
|
Ok(s) => builder.body(s),
|
||||||
|
Err(e) => Response::builder()
|
||||||
|
.status(StatusCode::INTERNAL_SERVER_ERROR)
|
||||||
|
.body(e.to_string()),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
None => builder.finish(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use crate::test::fixtures;
|
||||||
|
use poem_lib::IntoResponse;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_response() {
|
||||||
|
let input = fixtures::v10::minimal_string_extension();
|
||||||
|
|
||||||
|
let resp = input.into_response();
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
resp.headers()
|
||||||
|
.get("ce-specversion")
|
||||||
|
.unwrap()
|
||||||
|
.to_str()
|
||||||
|
.unwrap(),
|
||||||
|
"1.0"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
resp.headers().get("ce-id").unwrap().to_str().unwrap(),
|
||||||
|
"0001"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
resp.headers().get("ce-type").unwrap().to_str().unwrap(),
|
||||||
|
"test_event.test_application"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
resp.headers().get("ce-source").unwrap().to_str().unwrap(),
|
||||||
|
"http://localhost/"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
resp.headers().get("ce-someint").unwrap().to_str().unwrap(),
|
||||||
|
"10"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_response_with_full_data() {
|
||||||
|
let input = fixtures::v10::full_binary_json_data_string_extension();
|
||||||
|
|
||||||
|
let resp = input.into_response();
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
resp.headers()
|
||||||
|
.get("ce-specversion")
|
||||||
|
.unwrap()
|
||||||
|
.to_str()
|
||||||
|
.unwrap(),
|
||||||
|
"1.0"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
resp.headers().get("ce-id").unwrap().to_str().unwrap(),
|
||||||
|
"0001"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
resp.headers().get("ce-type").unwrap().to_str().unwrap(),
|
||||||
|
"test_event.test_application"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
resp.headers().get("ce-source").unwrap().to_str().unwrap(),
|
||||||
|
"http://localhost/"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
resp.headers()
|
||||||
|
.get("content-type")
|
||||||
|
.unwrap()
|
||||||
|
.to_str()
|
||||||
|
.unwrap(),
|
||||||
|
"application/json"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
resp.headers().get("ce-int_ex").unwrap().to_str().unwrap(),
|
||||||
|
"10"
|
||||||
|
);
|
||||||
|
|
||||||
|
let body = resp.into_body().into_vec().await.unwrap();
|
||||||
|
assert_eq!(fixtures::json_data_binary(), body);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,234 @@
|
||||||
|
use rdkafka_lib as rdkafka;
|
||||||
|
|
||||||
|
use crate::binding::{kafka::SPEC_VERSION_HEADER, CLOUDEVENTS_JSON_HEADER, CONTENT_TYPE};
|
||||||
|
use crate::event::SpecVersion;
|
||||||
|
use crate::message::{
|
||||||
|
BinaryDeserializer, BinarySerializer, Encoding, MessageAttributeValue, MessageDeserializer,
|
||||||
|
Result, StructuredDeserializer, StructuredSerializer,
|
||||||
|
};
|
||||||
|
use crate::{message, Event};
|
||||||
|
use rdkafka::message::{BorrowedMessage, Headers, Message, OwnedMessage};
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use std::convert::TryFrom;
|
||||||
|
use std::str;
|
||||||
|
|
||||||
|
/// Wrapper for [`Message`] that implements [`MessageDeserializer`] trait.
|
||||||
|
pub struct ConsumerRecordDeserializer {
|
||||||
|
pub(crate) headers: HashMap<String, Vec<u8>>,
|
||||||
|
pub(crate) payload: Option<Vec<u8>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ConsumerRecordDeserializer {
|
||||||
|
fn get_kafka_headers(message: &impl Message) -> Result<HashMap<String, Vec<u8>>> {
|
||||||
|
match message.headers() {
|
||||||
|
None => Err(crate::message::Error::WrongEncoding {}),
|
||||||
|
Some(headers) => Ok(headers
|
||||||
|
.iter()
|
||||||
|
.map(|h| (h.key.to_string(), Vec::from(h.value.unwrap())))
|
||||||
|
.collect()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn new(message: &impl Message) -> Result<ConsumerRecordDeserializer> {
|
||||||
|
Ok(ConsumerRecordDeserializer {
|
||||||
|
headers: Self::get_kafka_headers(message)?,
|
||||||
|
payload: message.payload().map(Vec::from),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl BinaryDeserializer for ConsumerRecordDeserializer {
|
||||||
|
fn deserialize_binary<R: Sized, V: BinarySerializer<R>>(mut self, mut visitor: V) -> Result<R> {
|
||||||
|
if self.encoding() != Encoding::BINARY {
|
||||||
|
return Err(message::Error::WrongEncoding {});
|
||||||
|
}
|
||||||
|
|
||||||
|
let spec_version = SpecVersion::try_from(
|
||||||
|
str::from_utf8(&self.headers.remove(SPEC_VERSION_HEADER).unwrap()).map_err(|e| {
|
||||||
|
crate::message::Error::Other {
|
||||||
|
source: Box::new(e),
|
||||||
|
}
|
||||||
|
})?,
|
||||||
|
)?;
|
||||||
|
|
||||||
|
let attributes = spec_version.attribute_names();
|
||||||
|
|
||||||
|
visitor = visitor.set_spec_version(spec_version)?;
|
||||||
|
|
||||||
|
if let Some(hv) = self.headers.remove(CONTENT_TYPE) {
|
||||||
|
visitor = visitor.set_attribute(
|
||||||
|
"datacontenttype",
|
||||||
|
MessageAttributeValue::String(String::from_utf8(hv).map_err(|e| {
|
||||||
|
crate::message::Error::Other {
|
||||||
|
source: Box::new(e),
|
||||||
|
}
|
||||||
|
})?),
|
||||||
|
)?
|
||||||
|
}
|
||||||
|
|
||||||
|
for (hn, hv) in self
|
||||||
|
.headers
|
||||||
|
.into_iter()
|
||||||
|
.filter(|(hn, _)| SPEC_VERSION_HEADER != *hn && hn.starts_with("ce_"))
|
||||||
|
{
|
||||||
|
let name = &hn["ce_".len()..];
|
||||||
|
|
||||||
|
if attributes.contains(&name) {
|
||||||
|
visitor = visitor.set_attribute(
|
||||||
|
name,
|
||||||
|
MessageAttributeValue::String(String::from_utf8(hv).map_err(|e| {
|
||||||
|
crate::message::Error::Other {
|
||||||
|
source: Box::new(e),
|
||||||
|
}
|
||||||
|
})?),
|
||||||
|
)?
|
||||||
|
} else {
|
||||||
|
visitor = visitor.set_extension(
|
||||||
|
name,
|
||||||
|
MessageAttributeValue::String(String::from_utf8(hv).map_err(|e| {
|
||||||
|
crate::message::Error::Other {
|
||||||
|
source: Box::new(e),
|
||||||
|
}
|
||||||
|
})?),
|
||||||
|
)?
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.payload.is_some() {
|
||||||
|
visitor.end_with_data(self.payload.unwrap())
|
||||||
|
} else {
|
||||||
|
visitor.end()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl StructuredDeserializer for ConsumerRecordDeserializer {
|
||||||
|
fn deserialize_structured<R: Sized, V: StructuredSerializer<R>>(self, visitor: V) -> Result<R> {
|
||||||
|
if self.encoding() != Encoding::STRUCTURED {
|
||||||
|
return Err(message::Error::WrongEncoding {});
|
||||||
|
}
|
||||||
|
visitor.set_structured_event(self.payload.unwrap())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MessageDeserializer for ConsumerRecordDeserializer {
|
||||||
|
fn encoding(&self) -> Encoding {
|
||||||
|
match (
|
||||||
|
self.headers
|
||||||
|
.get("content-type")
|
||||||
|
.and_then(|s| String::from_utf8(s.to_vec()).ok())
|
||||||
|
.map(|s| s.starts_with(CLOUDEVENTS_JSON_HEADER))
|
||||||
|
.unwrap_or(false),
|
||||||
|
self.headers.get(SPEC_VERSION_HEADER),
|
||||||
|
) {
|
||||||
|
(true, _) => Encoding::STRUCTURED,
|
||||||
|
(_, Some(_)) => Encoding::BINARY,
|
||||||
|
_ => Encoding::UNKNOWN,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Method to transform a [`Message`] to [`Event`].
|
||||||
|
pub fn record_to_event(msg: &impl Message) -> Result<Event> {
|
||||||
|
MessageDeserializer::into_event(ConsumerRecordDeserializer::new(msg)?)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Extension Trait for [`Message`] which acts as a wrapper for the function [`record_to_event()`].
|
||||||
|
///
|
||||||
|
/// This trait is sealed and cannot be implemented for types outside of this crate.
|
||||||
|
pub trait MessageExt: private::Sealed {
|
||||||
|
/// Generates [`Event`] from [`BorrowedMessage`].
|
||||||
|
fn to_event(&self) -> Result<Event>;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MessageExt for BorrowedMessage<'_> {
|
||||||
|
fn to_event(&self) -> Result<Event> {
|
||||||
|
record_to_event(self)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MessageExt for OwnedMessage {
|
||||||
|
fn to_event(&self) -> Result<Event> {
|
||||||
|
record_to_event(self)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mod private {
|
||||||
|
use rdkafka_lib as rdkafka;
|
||||||
|
|
||||||
|
// Sealing the MessageExt
|
||||||
|
pub trait Sealed {}
|
||||||
|
impl Sealed for rdkafka::message::OwnedMessage {}
|
||||||
|
impl Sealed for rdkafka::message::BorrowedMessage<'_> {}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use rdkafka_lib as rdkafka;
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
use crate::binding::rdkafka::kafka_producer_record::MessageRecord;
|
||||||
|
|
||||||
|
use crate::test::fixtures;
|
||||||
|
use crate::{EventBuilder, EventBuilderV10};
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_binary_record() {
|
||||||
|
let expected = fixtures::v10::minimal_string_extension();
|
||||||
|
|
||||||
|
// Since there is neither a way provided by rust-rdkafka to convert FutureProducer back into
|
||||||
|
// OwnedMessage or BorrowedMessage, nor is there a way to create a BorrowedMessage struct,
|
||||||
|
// the test uses OwnedMessage instead, which consumes the message instead of borrowing it like
|
||||||
|
// in the case of BorrowedMessage
|
||||||
|
|
||||||
|
let message_record = MessageRecord::from_event(
|
||||||
|
EventBuilderV10::new()
|
||||||
|
.id("0001")
|
||||||
|
.ty("test_event.test_application")
|
||||||
|
.source("http://localhost/")
|
||||||
|
.extension("someint", "10")
|
||||||
|
.build()
|
||||||
|
.unwrap(),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let owned_message = OwnedMessage::new(
|
||||||
|
message_record.payload,
|
||||||
|
Some(String::from("test key").into_bytes()),
|
||||||
|
String::from("test topic"),
|
||||||
|
rdkafka::message::Timestamp::NotAvailable,
|
||||||
|
10,
|
||||||
|
10,
|
||||||
|
Some(message_record.headers),
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_eq!(owned_message.to_event().unwrap(), expected)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_structured_record() {
|
||||||
|
let expected = fixtures::v10::full_json_data_string_extension();
|
||||||
|
|
||||||
|
// Since there is neither a way provided by rust-rdkafka to convert FutureProducer back into
|
||||||
|
// OwnedMessage or BorrowedMessage, nor is there a way to create a BorrowedMessage struct,
|
||||||
|
// the test uses OwnedMessage instead, which consumes the message instead of borrowing it like
|
||||||
|
// in the case of BorrowedMessage
|
||||||
|
|
||||||
|
let input = expected.clone();
|
||||||
|
|
||||||
|
let serialized_event =
|
||||||
|
StructuredDeserializer::deserialize_structured(input, MessageRecord::new()).unwrap();
|
||||||
|
|
||||||
|
let owned_message = OwnedMessage::new(
|
||||||
|
serialized_event.payload,
|
||||||
|
Some(String::from("test key").into_bytes()),
|
||||||
|
String::from("test topic"),
|
||||||
|
rdkafka::message::Timestamp::NotAvailable,
|
||||||
|
10,
|
||||||
|
10,
|
||||||
|
Some(serialized_event.headers),
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_eq!(owned_message.to_event().unwrap(), expected)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,155 @@
|
||||||
|
use rdkafka_lib as rdkafka;
|
||||||
|
|
||||||
|
use crate::binding::{
|
||||||
|
kafka::{header_prefix, SPEC_VERSION_HEADER},
|
||||||
|
CLOUDEVENTS_JSON_HEADER, CONTENT_TYPE,
|
||||||
|
};
|
||||||
|
use crate::event::SpecVersion;
|
||||||
|
use crate::message::{
|
||||||
|
BinaryDeserializer, BinarySerializer, MessageAttributeValue, Result, StructuredSerializer,
|
||||||
|
};
|
||||||
|
use crate::Event;
|
||||||
|
use rdkafka::message::{Header, OwnedHeaders, ToBytes};
|
||||||
|
use rdkafka::producer::{BaseRecord, FutureRecord};
|
||||||
|
|
||||||
|
/// This struct contains a serialized CloudEvent message in the Kafka shape.
|
||||||
|
/// Implements [`StructuredSerializer`] & [`BinarySerializer`] traits.
|
||||||
|
///
|
||||||
|
/// To instantiate a new `MessageRecord` from an [`Event`],
|
||||||
|
/// look at [`Self::from_event`] or use [`StructuredDeserializer::deserialize_structured`](crate::message::StructuredDeserializer::deserialize_structured)
|
||||||
|
/// or [`BinaryDeserializer::deserialize_binary`].
|
||||||
|
pub struct MessageRecord {
|
||||||
|
pub(crate) headers: OwnedHeaders,
|
||||||
|
pub(crate) payload: Option<Vec<u8>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MessageRecord {
|
||||||
|
/// Create a new empty [`MessageRecord`]
|
||||||
|
pub fn new() -> Self {
|
||||||
|
MessageRecord {
|
||||||
|
headers: OwnedHeaders::new(),
|
||||||
|
payload: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create a new [`MessageRecord`], filled with `event` serialized in binary mode.
|
||||||
|
pub fn from_event(event: Event) -> Result<Self> {
|
||||||
|
BinaryDeserializer::deserialize_binary(event, MessageRecord::new())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for MessageRecord {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::new()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl BinarySerializer<MessageRecord> for MessageRecord {
|
||||||
|
fn set_spec_version(mut self, sv: SpecVersion) -> Result<Self> {
|
||||||
|
let v = sv.to_string();
|
||||||
|
let header = Header {
|
||||||
|
key: SPEC_VERSION_HEADER,
|
||||||
|
value: Some(&v),
|
||||||
|
};
|
||||||
|
self.headers = self.headers.insert(header);
|
||||||
|
Ok(self)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_attribute(mut self, name: &str, value: MessageAttributeValue) -> Result<Self> {
|
||||||
|
let v = value.to_string();
|
||||||
|
let header = Header {
|
||||||
|
key: &header_prefix(name),
|
||||||
|
value: Some(&v),
|
||||||
|
};
|
||||||
|
self.headers = self.headers.insert(header);
|
||||||
|
Ok(self)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_extension(self, name: &str, value: MessageAttributeValue) -> Result<Self> {
|
||||||
|
self.set_attribute(name, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn end_with_data(mut self, bytes: Vec<u8>) -> Result<MessageRecord> {
|
||||||
|
self.payload = Some(bytes);
|
||||||
|
Ok(self)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn end(self) -> Result<MessageRecord> {
|
||||||
|
Ok(self)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl StructuredSerializer<MessageRecord> for MessageRecord {
|
||||||
|
fn set_structured_event(mut self, bytes: Vec<u8>) -> Result<MessageRecord> {
|
||||||
|
let header = Header {
|
||||||
|
key: CONTENT_TYPE,
|
||||||
|
value: Some(CLOUDEVENTS_JSON_HEADER),
|
||||||
|
};
|
||||||
|
self.headers = self.headers.insert(header);
|
||||||
|
|
||||||
|
self.payload = Some(bytes);
|
||||||
|
|
||||||
|
Ok(self)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Extension Trait for [`BaseRecord`] that fills the record with a [`MessageRecord`].
|
||||||
|
///
|
||||||
|
/// This trait is sealed and cannot be implemented for types outside of this crate.
|
||||||
|
pub trait BaseRecordExt<'a, K: ToBytes + ?Sized>: private::Sealed {
|
||||||
|
/// Fill this [`BaseRecord`] with a [`MessageRecord`].
|
||||||
|
fn message_record(
|
||||||
|
self,
|
||||||
|
message_record: &'a MessageRecord,
|
||||||
|
) -> Result<BaseRecord<'a, K, Vec<u8>>>;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a, K: ToBytes + ?Sized> BaseRecordExt<'a, K> for BaseRecord<'a, K, Vec<u8>> {
|
||||||
|
fn message_record(
|
||||||
|
mut self,
|
||||||
|
message_record: &'a MessageRecord,
|
||||||
|
) -> Result<BaseRecord<'a, K, Vec<u8>>> {
|
||||||
|
self = self.headers(message_record.headers.clone());
|
||||||
|
|
||||||
|
if let Some(s) = message_record.payload.as_ref() {
|
||||||
|
self = self.payload(s);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(self)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Extension Trait for [`FutureRecord`] that fills the record with a [`MessageRecord`].
|
||||||
|
///
|
||||||
|
/// This trait is sealed and cannot be implemented for types outside of this crate.
|
||||||
|
pub trait FutureRecordExt<'a, K: ToBytes + ?Sized>: private::Sealed {
|
||||||
|
/// Fill this [`FutureRecord`] with a [`MessageRecord`].
|
||||||
|
fn message_record(self, message_record: &'a MessageRecord) -> FutureRecord<'a, K, Vec<u8>>;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a, K: ToBytes + ?Sized> FutureRecordExt<'a, K> for FutureRecord<'a, K, Vec<u8>> {
|
||||||
|
fn message_record(mut self, message_record: &'a MessageRecord) -> FutureRecord<'a, K, Vec<u8>> {
|
||||||
|
self = self.headers(message_record.headers.clone());
|
||||||
|
|
||||||
|
if let Some(s) = message_record.payload.as_ref() {
|
||||||
|
self = self.payload(s);
|
||||||
|
}
|
||||||
|
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mod private {
|
||||||
|
use rdkafka_lib as rdkafka;
|
||||||
|
|
||||||
|
// Sealing the FutureRecordExt and BaseRecordExt
|
||||||
|
pub trait Sealed {}
|
||||||
|
impl<K: rdkafka::message::ToBytes + ?Sized, V: rdkafka::message::ToBytes> Sealed
|
||||||
|
for rdkafka::producer::FutureRecord<'_, K, V>
|
||||||
|
{
|
||||||
|
}
|
||||||
|
impl<K: rdkafka::message::ToBytes + ?Sized, V: rdkafka::message::ToBytes> Sealed
|
||||||
|
for rdkafka::producer::BaseRecord<'_, K, V>
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,63 @@
|
||||||
|
//! This library provides Kafka protocol bindings for CloudEvents
|
||||||
|
//! using the [rust-rdkafka](https://fede1024.github.io/rust-rdkafka) library.
|
||||||
|
//!
|
||||||
|
//! To produce Cloudevents:
|
||||||
|
//!
|
||||||
|
//! ```
|
||||||
|
//! # use rdkafka_lib as rdkafka;
|
||||||
|
//! use cloudevents::Event;
|
||||||
|
//! use rdkafka::producer::{FutureProducer, FutureRecord};
|
||||||
|
//! use rdkafka::util::Timeout;
|
||||||
|
//! use cloudevents::binding::rdkafka::{MessageRecord, FutureRecordExt};
|
||||||
|
//!
|
||||||
|
//! # async fn produce(producer: &FutureProducer, event: Event) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
//! let message_record = MessageRecord::from_event(event)?;
|
||||||
|
//!
|
||||||
|
//! producer.send(
|
||||||
|
//! FutureRecord::to("topic")
|
||||||
|
//! .key("some_event")
|
||||||
|
//! .message_record(&message_record),
|
||||||
|
//! Timeout::Never
|
||||||
|
//! ).await;
|
||||||
|
//! # Ok(())
|
||||||
|
//! # }
|
||||||
|
//!
|
||||||
|
//! ```
|
||||||
|
//!
|
||||||
|
//! To consume Cloudevents:
|
||||||
|
//!
|
||||||
|
//! ```
|
||||||
|
//! # use rdkafka_lib as rdkafka;
|
||||||
|
//! use rdkafka::consumer::{StreamConsumer, DefaultConsumerContext, Consumer, CommitMode};
|
||||||
|
//! use cloudevents::binding::rdkafka::MessageExt;
|
||||||
|
//! use futures::StreamExt;
|
||||||
|
//!
|
||||||
|
//! # async fn consume(consumer: StreamConsumer<DefaultConsumerContext>) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
//! let mut message_stream = consumer.stream();
|
||||||
|
//!
|
||||||
|
//! while let Some(message) = message_stream.next().await {
|
||||||
|
//! match message {
|
||||||
|
//! Err(e) => println!("Kafka error: {}", e),
|
||||||
|
//! Ok(m) => {
|
||||||
|
//! let event = m.to_event()?;
|
||||||
|
//! println!("Received Event: {}", event);
|
||||||
|
//! consumer.commit_message(&m, CommitMode::Async)?;
|
||||||
|
//! }
|
||||||
|
//! };
|
||||||
|
//! }
|
||||||
|
//! # Ok(())
|
||||||
|
//! # }
|
||||||
|
//! ```
|
||||||
|
|
||||||
|
#![deny(rustdoc::broken_intra_doc_links)]
|
||||||
|
|
||||||
|
mod kafka_consumer_record;
|
||||||
|
mod kafka_producer_record;
|
||||||
|
|
||||||
|
pub use kafka_consumer_record::record_to_event;
|
||||||
|
pub use kafka_consumer_record::ConsumerRecordDeserializer;
|
||||||
|
pub use kafka_consumer_record::MessageExt;
|
||||||
|
|
||||||
|
pub use kafka_producer_record::BaseRecordExt;
|
||||||
|
pub use kafka_producer_record::FutureRecordExt;
|
||||||
|
pub use kafka_producer_record::MessageRecord;
|
|
@ -0,0 +1,225 @@
|
||||||
|
use reqwest_lib as reqwest;
|
||||||
|
|
||||||
|
use crate::binding::{
|
||||||
|
http::{header_prefix, SPEC_VERSION_HEADER},
|
||||||
|
CLOUDEVENTS_BATCH_JSON_HEADER, CLOUDEVENTS_JSON_HEADER,
|
||||||
|
};
|
||||||
|
use crate::event::SpecVersion;
|
||||||
|
use crate::message::{
|
||||||
|
BinaryDeserializer, BinarySerializer, MessageAttributeValue, Result, StructuredSerializer,
|
||||||
|
};
|
||||||
|
use crate::Event;
|
||||||
|
use reqwest::RequestBuilder;
|
||||||
|
|
||||||
|
// TODO: Ideally, we'd only need to implement binding::http::Builder
|
||||||
|
// for reqwest::RequestBuilder here, but because the latter is a
|
||||||
|
// consuming builder, we'd need an intermediate struct similar to
|
||||||
|
// warp's to adapt that interface. Unfortunately, the reqwest builder
|
||||||
|
// doesn't implement the Default trait, so I can't use take() as
|
||||||
|
// warp's Adapter does, and I've yet to come up with another
|
||||||
|
// solution. So for now, we continue to implement BinarySerializer
|
||||||
|
// directly in here.
|
||||||
|
|
||||||
|
/// Wrapper for [`RequestBuilder`] that implements [`StructuredSerializer`] & [`BinarySerializer`] traits.
|
||||||
|
pub struct RequestSerializer {
|
||||||
|
req: RequestBuilder,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RequestSerializer {
|
||||||
|
pub fn new(req: RequestBuilder) -> RequestSerializer {
|
||||||
|
RequestSerializer { req }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl BinarySerializer<RequestBuilder> for RequestSerializer {
|
||||||
|
fn set_spec_version(mut self, spec_ver: SpecVersion) -> Result<Self> {
|
||||||
|
self.req = self.req.header(SPEC_VERSION_HEADER, spec_ver.to_string());
|
||||||
|
Ok(self)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_attribute(mut self, name: &str, value: MessageAttributeValue) -> Result<Self> {
|
||||||
|
let key = &header_prefix(name);
|
||||||
|
self.req = self.req.header(key, value.to_string());
|
||||||
|
Ok(self)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_extension(mut self, name: &str, value: MessageAttributeValue) -> Result<Self> {
|
||||||
|
let key = &header_prefix(name);
|
||||||
|
self.req = self.req.header(key, value.to_string());
|
||||||
|
Ok(self)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn end_with_data(self, bytes: Vec<u8>) -> Result<RequestBuilder> {
|
||||||
|
Ok(self.req.body(bytes))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn end(self) -> Result<RequestBuilder> {
|
||||||
|
Ok(self.req)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl StructuredSerializer<RequestBuilder> for RequestSerializer {
|
||||||
|
fn set_structured_event(self, bytes: Vec<u8>) -> Result<RequestBuilder> {
|
||||||
|
Ok(self
|
||||||
|
.req
|
||||||
|
.header(reqwest::header::CONTENT_TYPE, CLOUDEVENTS_JSON_HEADER)
|
||||||
|
.body(bytes))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Method to fill a [`RequestBuilder`] with an [`Event`].
|
||||||
|
pub fn event_to_request(event: Event, request_builder: RequestBuilder) -> Result<RequestBuilder> {
|
||||||
|
BinaryDeserializer::deserialize_binary(event, RequestSerializer::new(request_builder))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Method to fill a [`RequestBuilder`] with a batched [`Vec<Event>`].
|
||||||
|
pub fn events_to_request(
|
||||||
|
events: Vec<Event>,
|
||||||
|
request_builder: RequestBuilder,
|
||||||
|
) -> Result<RequestBuilder> {
|
||||||
|
let bytes = serde_json::to_vec(&events)?;
|
||||||
|
Ok(request_builder
|
||||||
|
.header(reqwest::header::CONTENT_TYPE, CLOUDEVENTS_BATCH_JSON_HEADER)
|
||||||
|
.body(bytes))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Extension Trait for [`RequestBuilder`] which acts as a wrapper for the function [`event_to_request()`].
|
||||||
|
///
|
||||||
|
/// This trait is sealed and cannot be implemented for types outside of this crate.
|
||||||
|
pub trait RequestBuilderExt: private::Sealed {
|
||||||
|
/// Write in this [`RequestBuilder`] the provided [`Event`]. Similar to invoking [`Event`].
|
||||||
|
fn event(self, event: Event) -> Result<RequestBuilder>;
|
||||||
|
/// Write in this [`RequestBuilder`] the provided batched [`Vec<Event>`].
|
||||||
|
fn events(self, events: Vec<Event>) -> Result<RequestBuilder>;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RequestBuilderExt for RequestBuilder {
|
||||||
|
fn event(self, event: Event) -> Result<RequestBuilder> {
|
||||||
|
event_to_request(event, self)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn events(self, events: Vec<Event>) -> Result<RequestBuilder> {
|
||||||
|
events_to_request(events, self)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sealing the RequestBuilderExt
|
||||||
|
mod private {
|
||||||
|
use reqwest_lib as reqwest;
|
||||||
|
|
||||||
|
pub trait Sealed {}
|
||||||
|
impl Sealed for reqwest::RequestBuilder {}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use mockito::Matcher;
|
||||||
|
use reqwest_lib as reqwest;
|
||||||
|
|
||||||
|
use crate::message::StructuredDeserializer;
|
||||||
|
use crate::test::fixtures;
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_request() {
|
||||||
|
let url = mockito::server_url();
|
||||||
|
let m = mockito::mock("POST", "/")
|
||||||
|
.match_header("ce-specversion", "1.0")
|
||||||
|
.match_header("ce-id", "0001")
|
||||||
|
.match_header("ce-type", "test_event.test_application")
|
||||||
|
.match_header("ce-source", "http://localhost/")
|
||||||
|
.match_header("ce-someint", "10")
|
||||||
|
.match_body(Matcher::Missing)
|
||||||
|
.create();
|
||||||
|
|
||||||
|
let input = fixtures::v10::minimal_string_extension();
|
||||||
|
|
||||||
|
let client = reqwest::Client::new();
|
||||||
|
client
|
||||||
|
.post(&url)
|
||||||
|
.event(input)
|
||||||
|
.unwrap()
|
||||||
|
.send()
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
m.assert();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_request_with_full_data() {
|
||||||
|
let url = mockito::server_url();
|
||||||
|
let m = mockito::mock("POST", "/")
|
||||||
|
.match_header("ce-specversion", "1.0")
|
||||||
|
.match_header("ce-id", "0001")
|
||||||
|
.with_header("ce-type", "test_event.test_application")
|
||||||
|
.with_header("ce-source", "http://localhost/")
|
||||||
|
.with_header("ce-subject", "cloudevents-sdk")
|
||||||
|
.with_header("content-type", "application/json")
|
||||||
|
.with_header("ce-string_ex", "val")
|
||||||
|
.with_header("ce-int_ex", "10")
|
||||||
|
.with_header("ce-bool_ex", "true")
|
||||||
|
.with_header("ce-time", &fixtures::time().to_rfc3339())
|
||||||
|
.match_body(Matcher::Exact(fixtures::json_data().to_string()))
|
||||||
|
.create();
|
||||||
|
|
||||||
|
let input = fixtures::v10::full_binary_json_data_string_extension();
|
||||||
|
|
||||||
|
let client = reqwest::Client::new();
|
||||||
|
|
||||||
|
client
|
||||||
|
.post(&url)
|
||||||
|
.event(input)
|
||||||
|
.unwrap()
|
||||||
|
.send()
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
m.assert();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_structured_request_with_full_data() {
|
||||||
|
let input = fixtures::v10::full_json_data_string_extension();
|
||||||
|
|
||||||
|
let url = mockito::server_url();
|
||||||
|
let m = mockito::mock("POST", "/")
|
||||||
|
.match_header("content-type", "application/cloudevents+json")
|
||||||
|
.match_body(Matcher::Exact(serde_json::to_string(&input).unwrap()))
|
||||||
|
.create();
|
||||||
|
|
||||||
|
let client = reqwest::Client::new();
|
||||||
|
StructuredDeserializer::deserialize_structured(
|
||||||
|
input,
|
||||||
|
RequestSerializer::new(client.post(&url)),
|
||||||
|
)
|
||||||
|
.unwrap()
|
||||||
|
.send()
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
m.assert();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_batched_request() {
|
||||||
|
let input = vec![fixtures::v10::full_json_data_string_extension()];
|
||||||
|
|
||||||
|
let url = mockito::server_url();
|
||||||
|
let m = mockito::mock("POST", "/")
|
||||||
|
.match_header("content-type", "application/cloudevents-batch+json")
|
||||||
|
.match_body(Matcher::Exact(serde_json::to_string(&input).unwrap()))
|
||||||
|
.create();
|
||||||
|
|
||||||
|
let client = reqwest::Client::new();
|
||||||
|
client
|
||||||
|
.post(&url)
|
||||||
|
.events(input)
|
||||||
|
.unwrap()
|
||||||
|
.send()
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
m.assert();
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,190 @@
|
||||||
|
use reqwest_lib as reqwest;
|
||||||
|
|
||||||
|
use crate::binding;
|
||||||
|
use crate::message::{Error, Result};
|
||||||
|
use crate::Event;
|
||||||
|
use async_trait::async_trait;
|
||||||
|
use http;
|
||||||
|
use http::header;
|
||||||
|
use reqwest::Response;
|
||||||
|
|
||||||
|
/// Method to transform an incoming [`Response`] to [`Event`].
|
||||||
|
pub async fn response_to_event(res: Response) -> Result<Event> {
|
||||||
|
let h = res.headers().to_owned();
|
||||||
|
let b = res.bytes().await.map_err(|e| Error::Other {
|
||||||
|
source: Box::new(e),
|
||||||
|
})?;
|
||||||
|
binding::http::to_event(&h, b.to_vec())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Method to transform an incoming [`Response`] to a batched [`Vec<Event>`]
|
||||||
|
pub async fn response_to_events(res: Response) -> Result<Vec<Event>> {
|
||||||
|
if res
|
||||||
|
.headers()
|
||||||
|
.get(header::CONTENT_TYPE)
|
||||||
|
.and_then(|v| v.to_str().ok())
|
||||||
|
.filter(|&v| v.starts_with(binding::CLOUDEVENTS_BATCH_JSON_HEADER))
|
||||||
|
.is_none()
|
||||||
|
{
|
||||||
|
return Err(Error::WrongEncoding {});
|
||||||
|
}
|
||||||
|
|
||||||
|
let bytes = res.bytes().await.map_err(|e| Error::Other {
|
||||||
|
source: Box::new(e),
|
||||||
|
})?;
|
||||||
|
|
||||||
|
Ok(serde_json::from_slice(&bytes)?)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Extension Trait for [`Response`] which acts as a wrapper for the function [`response_to_event()`].
|
||||||
|
///
|
||||||
|
/// This trait is sealed and cannot be implemented for types outside of this crate.
|
||||||
|
#[async_trait(?Send)]
|
||||||
|
pub trait ResponseExt: private::Sealed {
|
||||||
|
/// Convert this [`Response`] to [`Event`].
|
||||||
|
async fn into_event(self) -> Result<Event>;
|
||||||
|
/// Convert this [`Response`] to a batched [`Vec<Event>`].
|
||||||
|
async fn into_events(self) -> Result<Vec<Event>>;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait(?Send)]
|
||||||
|
impl ResponseExt for Response {
|
||||||
|
async fn into_event(self) -> Result<Event> {
|
||||||
|
response_to_event(self).await
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn into_events(self) -> Result<Vec<Event>> {
|
||||||
|
response_to_events(self).await
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sealing the ResponseExt
|
||||||
|
mod private {
|
||||||
|
use reqwest_lib as reqwest;
|
||||||
|
|
||||||
|
pub trait Sealed {}
|
||||||
|
impl Sealed for reqwest::Response {}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use reqwest_lib as reqwest;
|
||||||
|
use std::vec;
|
||||||
|
|
||||||
|
use crate::test::fixtures;
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_response() {
|
||||||
|
let url = mockito::server_url();
|
||||||
|
let _m = mockito::mock("GET", "/")
|
||||||
|
.with_status(200)
|
||||||
|
.with_header("ce-specversion", "1.0")
|
||||||
|
.with_header("ce-id", "0001")
|
||||||
|
.with_header("ce-type", "test_event.test_application")
|
||||||
|
.with_header("ce-source", "http://localhost/")
|
||||||
|
.with_header("ce-someint", "10")
|
||||||
|
.create();
|
||||||
|
|
||||||
|
let expected = fixtures::v10::minimal_string_extension();
|
||||||
|
|
||||||
|
let client = reqwest::Client::new();
|
||||||
|
let res = client
|
||||||
|
.get(&url)
|
||||||
|
.send()
|
||||||
|
.await
|
||||||
|
.unwrap()
|
||||||
|
.into_event()
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
assert_eq!(expected, res);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_response_with_full_data() {
|
||||||
|
let url = mockito::server_url();
|
||||||
|
let _m = mockito::mock("GET", "/")
|
||||||
|
.with_status(200)
|
||||||
|
.with_header("ce-specversion", "1.0")
|
||||||
|
.with_header("ce-id", "0001")
|
||||||
|
.with_header("ce-type", "test_event.test_application")
|
||||||
|
.with_header("ce-source", "http://localhost/")
|
||||||
|
.with_header("ce-subject", "cloudevents-sdk")
|
||||||
|
.with_header("content-type", "application/json")
|
||||||
|
.with_header("ce-string_ex", "val")
|
||||||
|
.with_header("ce-int_ex", "10")
|
||||||
|
.with_header("ce-bool_ex", "true")
|
||||||
|
.with_header("ce-time", &fixtures::time().to_rfc3339())
|
||||||
|
.with_body(fixtures::json_data().to_string())
|
||||||
|
.create();
|
||||||
|
|
||||||
|
let expected = fixtures::v10::full_binary_json_data_string_extension();
|
||||||
|
|
||||||
|
let client = reqwest::Client::new();
|
||||||
|
let res = client
|
||||||
|
.get(&url)
|
||||||
|
.send()
|
||||||
|
.await
|
||||||
|
.unwrap()
|
||||||
|
.into_event()
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
assert_eq!(expected, res);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_structured_response_with_full_data() {
|
||||||
|
let expected = fixtures::v10::full_json_data_string_extension();
|
||||||
|
|
||||||
|
let url = mockito::server_url();
|
||||||
|
let _m = mockito::mock("GET", "/")
|
||||||
|
.with_status(200)
|
||||||
|
.with_header(
|
||||||
|
"content-type",
|
||||||
|
"application/cloudevents+json; charset=utf-8",
|
||||||
|
)
|
||||||
|
.with_body(serde_json::to_string(&expected).unwrap())
|
||||||
|
.create();
|
||||||
|
|
||||||
|
let client = reqwest::Client::new();
|
||||||
|
let res = client
|
||||||
|
.get(&url)
|
||||||
|
.send()
|
||||||
|
.await
|
||||||
|
.unwrap()
|
||||||
|
.into_event()
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
assert_eq!(expected, res);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_batched_response() {
|
||||||
|
let expected = vec![fixtures::v10::full_json_data_string_extension()];
|
||||||
|
|
||||||
|
let url = mockito::server_url();
|
||||||
|
let _m = mockito::mock("GET", "/")
|
||||||
|
.with_status(200)
|
||||||
|
.with_header(
|
||||||
|
"content-type",
|
||||||
|
"application/cloudevents-batch+json; charset=utf-8",
|
||||||
|
)
|
||||||
|
.with_body(serde_json::to_string(&expected).unwrap())
|
||||||
|
.create();
|
||||||
|
|
||||||
|
let client = reqwest::Client::new();
|
||||||
|
let res = client
|
||||||
|
.get(&url)
|
||||||
|
.send()
|
||||||
|
.await
|
||||||
|
.unwrap()
|
||||||
|
.into_events()
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
assert_eq!(expected, res);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,40 @@
|
||||||
|
//! This module integrates the [cloudevents-sdk](https://docs.rs/cloudevents-sdk) with [reqwest](https://docs.rs/reqwest/) to easily send and receive CloudEvents.
|
||||||
|
//!
|
||||||
|
//! ```
|
||||||
|
//! # use reqwest_lib as reqwest;
|
||||||
|
//! use cloudevents::binding::reqwest::{RequestBuilderExt, ResponseExt};
|
||||||
|
//! use cloudevents::{EventBuilderV10, EventBuilder};
|
||||||
|
//! use serde_json::json;
|
||||||
|
//!
|
||||||
|
//! # async fn example() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
//! let client = reqwest::Client::new();
|
||||||
|
//!
|
||||||
|
//! // Prepare the event to send
|
||||||
|
//! let event_to_send = EventBuilderV10::new()
|
||||||
|
//! .id("0001")
|
||||||
|
//! .ty("example.test")
|
||||||
|
//! .source("http://localhost/")
|
||||||
|
//! .data("application/json", json!({"hello": "world"}))
|
||||||
|
//! .build()?;
|
||||||
|
//!
|
||||||
|
//! // Send request
|
||||||
|
//! let response = client.post("http://localhost")
|
||||||
|
//! .event(event_to_send)?
|
||||||
|
//! .send().await?;
|
||||||
|
//! // Parse response as event
|
||||||
|
//! let received_event = response
|
||||||
|
//! .into_event().await?;
|
||||||
|
//! # Ok(())
|
||||||
|
//! # }
|
||||||
|
//! ```
|
||||||
|
|
||||||
|
#![deny(rustdoc::broken_intra_doc_links)]
|
||||||
|
|
||||||
|
mod client_request;
|
||||||
|
mod client_response;
|
||||||
|
|
||||||
|
pub use client_request::event_to_request;
|
||||||
|
pub use client_request::RequestBuilderExt;
|
||||||
|
pub use client_request::RequestSerializer;
|
||||||
|
pub use client_response::response_to_event;
|
||||||
|
pub use client_response::ResponseExt;
|
|
@ -0,0 +1,124 @@
|
||||||
|
use warp_lib as warp;
|
||||||
|
|
||||||
|
use crate::binding::http_0_2 as http;
|
||||||
|
|
||||||
|
use crate::Event;
|
||||||
|
use warp::http::HeaderMap;
|
||||||
|
use warp::Filter;
|
||||||
|
use warp::Rejection;
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
#[allow(dead_code)]
|
||||||
|
pub struct EventFilterError {
|
||||||
|
error: crate::message::Error,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl warp::reject::Reject for EventFilterError {}
|
||||||
|
|
||||||
|
///
|
||||||
|
/// # Extracts [`crate::Event`] from incoming request
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// # use warp_lib as warp;
|
||||||
|
/// use cloudevents::binding::warp::filter::to_event;
|
||||||
|
/// use warp::Filter;
|
||||||
|
/// use warp::Reply;
|
||||||
|
///
|
||||||
|
/// let routes = warp::any()
|
||||||
|
/// .and(to_event())
|
||||||
|
/// .map(|event| {
|
||||||
|
/// // do something with the event
|
||||||
|
/// }
|
||||||
|
/// );
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
pub fn to_event() -> impl Filter<Extract = (Event,), Error = Rejection> + Copy {
|
||||||
|
warp::header::headers_cloned()
|
||||||
|
.and(warp::body::bytes())
|
||||||
|
.and_then(create_event)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn create_event(headers: HeaderMap, body: bytes::Bytes) -> Result<Event, Rejection> {
|
||||||
|
http::to_event(&headers, body.to_vec())
|
||||||
|
.map_err(|error| warp::reject::custom(EventFilterError { error }))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::to_event;
|
||||||
|
use crate::test::fixtures;
|
||||||
|
use std::convert::TryInto;
|
||||||
|
use warp::test;
|
||||||
|
use warp_lib as warp;
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_request() {
|
||||||
|
let expected = fixtures::v10::minimal_string_extension();
|
||||||
|
|
||||||
|
let result = test::request()
|
||||||
|
.method("POST")
|
||||||
|
.header("ce-specversion", "1.0")
|
||||||
|
.header("ce-id", "0001")
|
||||||
|
.header("ce-type", "test_event.test_application")
|
||||||
|
.header("ce-source", "http://localhost/")
|
||||||
|
.header("ce-someint", "10")
|
||||||
|
.filter(&to_event())
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
assert_eq!(expected, result);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_bad_request() {
|
||||||
|
let result = test::request()
|
||||||
|
.method("POST")
|
||||||
|
.header("ce-specversion", "BAD SPECIFICATION")
|
||||||
|
.header("ce-id", "0001")
|
||||||
|
.header("ce-type", "example.test")
|
||||||
|
.header("ce-source", "http://localhost/")
|
||||||
|
.header("ce-someint", "10")
|
||||||
|
.header("ce-time", fixtures::time().to_rfc3339())
|
||||||
|
.filter(&to_event())
|
||||||
|
.await;
|
||||||
|
|
||||||
|
assert!(result.is_err());
|
||||||
|
let rejection = result.unwrap_err();
|
||||||
|
|
||||||
|
let reason = rejection.find::<super::EventFilterError>().unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
reason.error.to_string(),
|
||||||
|
"Invalid specversion BAD SPECIFICATION"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_request_with_full_data() {
|
||||||
|
let expected = fixtures::v10::full_binary_json_data_string_extension();
|
||||||
|
|
||||||
|
let result = test::request()
|
||||||
|
.method("POST")
|
||||||
|
.header("ce-specversion", "1.0")
|
||||||
|
.header("ce-id", "0001")
|
||||||
|
.header("ce-type", "test_event.test_application")
|
||||||
|
.header("ce-source", "http://localhost/")
|
||||||
|
.header("ce-subject", "cloudevents-sdk")
|
||||||
|
.header("content-type", "application/json")
|
||||||
|
.header("ce-string_ex", "val")
|
||||||
|
.header("ce-int_ex", "10")
|
||||||
|
.header("ce-bool_ex", "true")
|
||||||
|
.header("ce-time", &fixtures::time().to_rfc3339())
|
||||||
|
.json(&fixtures::json_data())
|
||||||
|
.filter(&to_event())
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let mut event = result.clone();
|
||||||
|
let (_datacontenttype, _dataschema, data) = event.take_data();
|
||||||
|
let actual_payload: Vec<u8> = data.unwrap().try_into().unwrap();
|
||||||
|
let expected_payload: Vec<u8> = serde_json::to_vec(&fixtures::json_data()).unwrap();
|
||||||
|
assert_eq!(expected_payload, actual_payload);
|
||||||
|
|
||||||
|
assert_eq!(expected, result);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,65 @@
|
||||||
|
//! This module integrates the [cloudevents-sdk](https://docs.rs/cloudevents-sdk) with [Warp web service framework](https://docs.rs/warp/)
|
||||||
|
//! to easily send and receive CloudEvents.
|
||||||
|
//!
|
||||||
|
//! To deserialize an HTTP request as CloudEvent
|
||||||
|
//!
|
||||||
|
//! To echo events:
|
||||||
|
//!
|
||||||
|
//! ```
|
||||||
|
//! # use warp_lib as warp;
|
||||||
|
//! use warp::{Filter, Reply};
|
||||||
|
//! use cloudevents::binding::warp::reply::from_event;
|
||||||
|
//! use cloudevents::binding::warp::filter::to_event;
|
||||||
|
//!
|
||||||
|
//! let routes = warp::any()
|
||||||
|
//! // extracting event from request
|
||||||
|
//! .and(to_event())
|
||||||
|
//! // returning event back
|
||||||
|
//! .map(|event| from_event(event));
|
||||||
|
//!
|
||||||
|
//! warp::serve(routes).run(([127, 0, 0, 1], 3030));
|
||||||
|
//! ```
|
||||||
|
//!
|
||||||
|
//! To create event inside request handlers and send them as responses:
|
||||||
|
//!
|
||||||
|
//! ```
|
||||||
|
//! # use warp_lib as warp;
|
||||||
|
//! # use http_0_2 as http;
|
||||||
|
//! use cloudevents::{Event, EventBuilder, EventBuilderV10};
|
||||||
|
//! use http::StatusCode;
|
||||||
|
//! use serde_json::json;
|
||||||
|
//! use warp::{Filter, Reply};
|
||||||
|
//! use cloudevents::binding::warp::reply::from_event;
|
||||||
|
//!
|
||||||
|
//! let routes = warp::any().map(|| {
|
||||||
|
//! let event = EventBuilderV10::new()
|
||||||
|
//! .id("1")
|
||||||
|
//! .source("url://example_response/")
|
||||||
|
//! .ty("example.ce")
|
||||||
|
//! .data(
|
||||||
|
//! mime::APPLICATION_JSON.to_string(),
|
||||||
|
//! json!({
|
||||||
|
//! "name": "John Doe",
|
||||||
|
//! "age": 43,
|
||||||
|
//! "phones": [
|
||||||
|
//! "+44 1234567",
|
||||||
|
//! "+44 2345678"
|
||||||
|
//! ]
|
||||||
|
//! }),
|
||||||
|
//! )
|
||||||
|
//! .build();
|
||||||
|
//!
|
||||||
|
//! match event {
|
||||||
|
//! Ok(event) => from_event(event),
|
||||||
|
//! Err(e) => warp::reply::with_status(
|
||||||
|
//! e.to_string(),
|
||||||
|
//! StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
|
//! ).into_response(),
|
||||||
|
//! }
|
||||||
|
//! });
|
||||||
|
//!
|
||||||
|
//! warp::serve(routes).run(([127, 0, 0, 1], 3030));
|
||||||
|
//! ```
|
||||||
|
|
||||||
|
pub mod filter;
|
||||||
|
pub mod reply;
|
|
@ -0,0 +1,114 @@
|
||||||
|
use warp_lib as warp;
|
||||||
|
|
||||||
|
use crate::binding::http_0_2::builder::adapter::to_response;
|
||||||
|
|
||||||
|
use crate::Event;
|
||||||
|
use http::StatusCode;
|
||||||
|
use http_0_2 as http;
|
||||||
|
use hyper_0_14 as hyper;
|
||||||
|
use warp::reply::Response;
|
||||||
|
///
|
||||||
|
/// # Serializes [`crate::Event`] as a http response
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// # use warp_lib as warp;
|
||||||
|
/// use cloudevents::binding::warp::reply::from_event;
|
||||||
|
/// use cloudevents::Event;
|
||||||
|
/// use warp::Filter;
|
||||||
|
/// use warp::Reply;
|
||||||
|
///
|
||||||
|
/// let routes = warp::any()
|
||||||
|
/// .map(|| from_event(Event::default()));
|
||||||
|
/// ```
|
||||||
|
pub fn from_event(event: Event) -> Response {
|
||||||
|
match to_response(event) {
|
||||||
|
Ok(response) => response,
|
||||||
|
Err(e) => warp::http::response::Response::builder()
|
||||||
|
.status(StatusCode::INTERNAL_SERVER_ERROR)
|
||||||
|
.body(hyper::body::Body::from(e.to_string()))
|
||||||
|
.unwrap(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use crate::test::fixtures;
|
||||||
|
use hyper_0_14 as hyper;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_response() {
|
||||||
|
let input = fixtures::v10::minimal_string_extension();
|
||||||
|
|
||||||
|
let resp = super::from_event(input);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
resp.headers()
|
||||||
|
.get("ce-specversion")
|
||||||
|
.unwrap()
|
||||||
|
.to_str()
|
||||||
|
.unwrap(),
|
||||||
|
"1.0"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
resp.headers().get("ce-id").unwrap().to_str().unwrap(),
|
||||||
|
"0001"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
resp.headers().get("ce-type").unwrap().to_str().unwrap(),
|
||||||
|
"test_event.test_application"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
resp.headers().get("ce-source").unwrap().to_str().unwrap(),
|
||||||
|
"http://localhost/"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
resp.headers().get("ce-someint").unwrap().to_str().unwrap(),
|
||||||
|
"10"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_response_with_full_data() {
|
||||||
|
let input = fixtures::v10::full_binary_json_data_string_extension();
|
||||||
|
|
||||||
|
let resp = super::from_event(input);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
resp.headers()
|
||||||
|
.get("ce-specversion")
|
||||||
|
.unwrap()
|
||||||
|
.to_str()
|
||||||
|
.unwrap(),
|
||||||
|
"1.0"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
resp.headers().get("ce-id").unwrap().to_str().unwrap(),
|
||||||
|
"0001"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
resp.headers().get("ce-type").unwrap().to_str().unwrap(),
|
||||||
|
"test_event.test_application"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
resp.headers().get("ce-source").unwrap().to_str().unwrap(),
|
||||||
|
"http://localhost/"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
resp.headers()
|
||||||
|
.get("content-type")
|
||||||
|
.unwrap()
|
||||||
|
.to_str()
|
||||||
|
.unwrap(),
|
||||||
|
"application/json"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
resp.headers().get("ce-int_ex").unwrap().to_str().unwrap(),
|
||||||
|
"10"
|
||||||
|
);
|
||||||
|
|
||||||
|
let (_, body) = resp.into_parts();
|
||||||
|
let body = hyper::body::to_bytes(body).await.unwrap();
|
||||||
|
|
||||||
|
assert_eq!(fixtures::json_data_binary(), body);
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,25 +1,48 @@
|
||||||
use super::{AttributesV03, AttributesV10, SpecVersion};
|
use super::{
|
||||||
|
AttributesIntoIteratorV03, AttributesIntoIteratorV10, AttributesV03, AttributesV10,
|
||||||
|
ExtensionValue, SpecVersion, UriReference,
|
||||||
|
};
|
||||||
|
use base64::prelude::*;
|
||||||
use chrono::{DateTime, Utc};
|
use chrono::{DateTime, Utc};
|
||||||
|
use serde::Serializer;
|
||||||
use std::fmt;
|
use std::fmt;
|
||||||
use url::Url;
|
use url::Url;
|
||||||
|
|
||||||
#[derive(Debug, PartialEq)]
|
/// Enum representing a borrowed value of a CloudEvent attribute.
|
||||||
|
/// This represents the types defined in the [CloudEvent spec type system](https://github.com/cloudevents/spec/blob/v1.0/spec.md#type-system)
|
||||||
|
#[derive(Debug, PartialEq, Eq)]
|
||||||
pub enum AttributeValue<'a> {
|
pub enum AttributeValue<'a> {
|
||||||
SpecVersion(SpecVersion),
|
Boolean(&'a bool),
|
||||||
|
Integer(&'a i64),
|
||||||
String(&'a str),
|
String(&'a str),
|
||||||
|
Binary(&'a [u8]),
|
||||||
URI(&'a Url),
|
URI(&'a Url),
|
||||||
URIRef(&'a Url),
|
URIRef(&'a UriReference),
|
||||||
Time(&'a DateTime<Utc>),
|
Time(&'a DateTime<Utc>),
|
||||||
|
SpecVersion(SpecVersion),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> From<&'a ExtensionValue> for AttributeValue<'a> {
|
||||||
|
fn from(ev: &'a ExtensionValue) -> Self {
|
||||||
|
match ev {
|
||||||
|
ExtensionValue::String(s) => AttributeValue::String(s),
|
||||||
|
ExtensionValue::Boolean(b) => AttributeValue::Boolean(b),
|
||||||
|
ExtensionValue::Integer(i) => AttributeValue::Integer(i),
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl fmt::Display for AttributeValue<'_> {
|
impl fmt::Display for AttributeValue<'_> {
|
||||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
match self {
|
match self {
|
||||||
AttributeValue::SpecVersion(s) => s.fmt(f),
|
AttributeValue::Boolean(b) => f.serialize_bool(**b),
|
||||||
|
AttributeValue::Integer(i) => f.serialize_i64(**i),
|
||||||
AttributeValue::String(s) => f.write_str(s),
|
AttributeValue::String(s) => f.write_str(s),
|
||||||
AttributeValue::URI(s) => f.write_str(&s.as_str()),
|
AttributeValue::Binary(b) => f.write_str(&BASE64_STANDARD.encode(b)),
|
||||||
AttributeValue::URIRef(s) => f.write_str(&s.as_str()),
|
AttributeValue::URI(s) => f.write_str(s.as_str()),
|
||||||
|
AttributeValue::URIRef(s) => f.write_str(s.as_str()),
|
||||||
AttributeValue::Time(s) => f.write_str(&s.to_rfc3339()),
|
AttributeValue::Time(s) => f.write_str(&s.to_rfc3339()),
|
||||||
|
AttributeValue::SpecVersion(s) => s.fmt(f),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -27,35 +50,47 @@ impl fmt::Display for AttributeValue<'_> {
|
||||||
/// Trait to get [CloudEvents Context attributes](https://github.com/cloudevents/spec/blob/master/spec.md#context-attributes).
|
/// Trait to get [CloudEvents Context attributes](https://github.com/cloudevents/spec/blob/master/spec.md#context-attributes).
|
||||||
pub trait AttributesReader {
|
pub trait AttributesReader {
|
||||||
/// Get the [id](https://github.com/cloudevents/spec/blob/master/spec.md#id).
|
/// Get the [id](https://github.com/cloudevents/spec/blob/master/spec.md#id).
|
||||||
fn get_id(&self) -> &str;
|
fn id(&self) -> &str;
|
||||||
/// Get the [source](https://github.com/cloudevents/spec/blob/master/spec.md#source-1).
|
/// Get the [source](https://github.com/cloudevents/spec/blob/master/spec.md#source-1).
|
||||||
fn get_source(&self) -> &Url;
|
fn source(&self) -> &UriReference;
|
||||||
/// Get the [specversion](https://github.com/cloudevents/spec/blob/master/spec.md#specversion).
|
/// Get the [specversion](https://github.com/cloudevents/spec/blob/master/spec.md#specversion).
|
||||||
fn get_specversion(&self) -> SpecVersion;
|
fn specversion(&self) -> SpecVersion;
|
||||||
/// Get the [type](https://github.com/cloudevents/spec/blob/master/spec.md#type).
|
/// Get the [type](https://github.com/cloudevents/spec/blob/master/spec.md#type).
|
||||||
fn get_type(&self) -> &str;
|
fn ty(&self) -> &str;
|
||||||
/// Get the [datacontenttype](https://github.com/cloudevents/spec/blob/master/spec.md#datacontenttype).
|
/// Get the [datacontenttype](https://github.com/cloudevents/spec/blob/master/spec.md#datacontenttype).
|
||||||
fn get_datacontenttype(&self) -> Option<&str>;
|
fn datacontenttype(&self) -> Option<&str>;
|
||||||
/// Get the [dataschema](https://github.com/cloudevents/spec/blob/master/spec.md#dataschema).
|
/// Get the [dataschema](https://github.com/cloudevents/spec/blob/master/spec.md#dataschema).
|
||||||
fn get_dataschema(&self) -> Option<&Url>;
|
fn dataschema(&self) -> Option<&Url>;
|
||||||
/// Get the [subject](https://github.com/cloudevents/spec/blob/master/spec.md#subject).
|
/// Get the [subject](https://github.com/cloudevents/spec/blob/master/spec.md#subject).
|
||||||
fn get_subject(&self) -> Option<&str>;
|
fn subject(&self) -> Option<&str>;
|
||||||
/// Get the [time](https://github.com/cloudevents/spec/blob/master/spec.md#time).
|
/// Get the [time](https://github.com/cloudevents/spec/blob/master/spec.md#time).
|
||||||
fn get_time(&self) -> Option<&DateTime<Utc>>;
|
fn time(&self) -> Option<&DateTime<Utc>>;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Trait to set [CloudEvents Context attributes](https://github.com/cloudevents/spec/blob/master/spec.md#context-attributes).
|
/// Trait to set [CloudEvents Context attributes](https://github.com/cloudevents/spec/blob/master/spec.md#context-attributes).
|
||||||
pub trait AttributesWriter {
|
pub trait AttributesWriter {
|
||||||
/// Set the [id](https://github.com/cloudevents/spec/blob/master/spec.md#id).
|
/// Set the [id](https://github.com/cloudevents/spec/blob/master/spec.md#id).
|
||||||
fn set_id(&mut self, id: impl Into<String>);
|
/// Returns the previous value.
|
||||||
|
fn set_id(&mut self, id: impl Into<String>) -> String;
|
||||||
/// Set the [source](https://github.com/cloudevents/spec/blob/master/spec.md#source-1).
|
/// Set the [source](https://github.com/cloudevents/spec/blob/master/spec.md#source-1).
|
||||||
fn set_source(&mut self, source: impl Into<Url>);
|
/// Returns the previous value.
|
||||||
|
fn set_source(&mut self, source: impl Into<UriReference>) -> UriReference;
|
||||||
/// Set the [type](https://github.com/cloudevents/spec/blob/master/spec.md#type).
|
/// Set the [type](https://github.com/cloudevents/spec/blob/master/spec.md#type).
|
||||||
fn set_type(&mut self, ty: impl Into<String>);
|
/// Returns the previous value.
|
||||||
|
fn set_type(&mut self, ty: impl Into<String>) -> String;
|
||||||
/// Set the [subject](https://github.com/cloudevents/spec/blob/master/spec.md#subject).
|
/// Set the [subject](https://github.com/cloudevents/spec/blob/master/spec.md#subject).
|
||||||
fn set_subject(&mut self, subject: Option<impl Into<String>>);
|
/// Returns the previous value.
|
||||||
|
fn set_subject(&mut self, subject: Option<impl Into<String>>) -> Option<String>;
|
||||||
/// Set the [time](https://github.com/cloudevents/spec/blob/master/spec.md#time).
|
/// Set the [time](https://github.com/cloudevents/spec/blob/master/spec.md#time).
|
||||||
fn set_time(&mut self, time: Option<impl Into<DateTime<Utc>>>);
|
/// Returns the previous value.
|
||||||
|
fn set_time(&mut self, time: Option<impl Into<DateTime<Utc>>>) -> Option<DateTime<Utc>>;
|
||||||
|
/// Set the [datacontenttype](https://github.com/cloudevents/spec/blob/master/spec.md#datacontenttype).
|
||||||
|
/// Returns the previous value.
|
||||||
|
fn set_datacontenttype(&mut self, datacontenttype: Option<impl Into<String>>)
|
||||||
|
-> Option<String>;
|
||||||
|
/// Set the [dataschema](https://github.com/cloudevents/spec/blob/master/spec.md#dataschema).
|
||||||
|
/// Returns the previous value.
|
||||||
|
fn set_dataschema(&mut self, dataschema: Option<impl Into<Url>>) -> Option<Url>;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) trait AttributesConverter {
|
pub(crate) trait AttributesConverter {
|
||||||
|
@ -63,122 +98,134 @@ pub(crate) trait AttributesConverter {
|
||||||
fn into_v10(self) -> AttributesV10;
|
fn into_v10(self) -> AttributesV10;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) trait DataAttributesWriter {
|
#[derive(PartialEq, Debug, Clone, Copy)]
|
||||||
fn set_datacontenttype(&mut self, datacontenttype: Option<impl Into<String>>);
|
pub(crate) enum AttributesIter<'a> {
|
||||||
fn set_dataschema(&mut self, dataschema: Option<impl Into<Url>>);
|
IterV03(AttributesIntoIteratorV03<'a>),
|
||||||
|
IterV10(AttributesIntoIteratorV10<'a>),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> Iterator for AttributesIter<'a> {
|
||||||
|
type Item = (&'a str, AttributeValue<'a>);
|
||||||
|
fn next(&mut self) -> Option<Self::Item> {
|
||||||
|
match self {
|
||||||
|
AttributesIter::IterV03(a) => a.next(),
|
||||||
|
AttributesIter::IterV10(a) => a.next(),
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Union type representing one of the possible context attributes structs
|
/// Union type representing one of the possible context attributes structs
|
||||||
#[derive(PartialEq, Debug, Clone)]
|
#[derive(PartialEq, Eq, Debug, Clone)]
|
||||||
pub enum Attributes {
|
pub enum Attributes {
|
||||||
V03(AttributesV03),
|
V03(AttributesV03),
|
||||||
V10(AttributesV10),
|
V10(AttributesV10),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl AttributesReader for Attributes {
|
impl AttributesReader for Attributes {
|
||||||
fn get_id(&self) -> &str {
|
fn id(&self) -> &str {
|
||||||
match self {
|
match self {
|
||||||
Attributes::V03(a) => a.get_id(),
|
Attributes::V03(a) => a.id(),
|
||||||
Attributes::V10(a) => a.get_id(),
|
Attributes::V10(a) => a.id(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_source(&self) -> &Url {
|
fn source(&self) -> &UriReference {
|
||||||
match self {
|
match self {
|
||||||
Attributes::V03(a) => a.get_source(),
|
Attributes::V03(a) => a.source(),
|
||||||
Attributes::V10(a) => a.get_source(),
|
Attributes::V10(a) => a.source(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_specversion(&self) -> SpecVersion {
|
fn specversion(&self) -> SpecVersion {
|
||||||
match self {
|
match self {
|
||||||
Attributes::V03(a) => a.get_specversion(),
|
Attributes::V03(a) => a.specversion(),
|
||||||
Attributes::V10(a) => a.get_specversion(),
|
Attributes::V10(a) => a.specversion(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_type(&self) -> &str {
|
fn ty(&self) -> &str {
|
||||||
match self {
|
match self {
|
||||||
Attributes::V03(a) => a.get_type(),
|
Attributes::V03(a) => a.ty(),
|
||||||
Attributes::V10(a) => a.get_type(),
|
Attributes::V10(a) => a.ty(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_datacontenttype(&self) -> Option<&str> {
|
fn datacontenttype(&self) -> Option<&str> {
|
||||||
match self {
|
match self {
|
||||||
Attributes::V03(a) => a.get_datacontenttype(),
|
Attributes::V03(a) => a.datacontenttype(),
|
||||||
Attributes::V10(a) => a.get_datacontenttype(),
|
Attributes::V10(a) => a.datacontenttype(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_dataschema(&self) -> Option<&Url> {
|
fn dataschema(&self) -> Option<&Url> {
|
||||||
match self {
|
match self {
|
||||||
Attributes::V03(a) => a.get_dataschema(),
|
Attributes::V03(a) => a.dataschema(),
|
||||||
Attributes::V10(a) => a.get_dataschema(),
|
Attributes::V10(a) => a.dataschema(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_subject(&self) -> Option<&str> {
|
fn subject(&self) -> Option<&str> {
|
||||||
match self {
|
match self {
|
||||||
Attributes::V03(a) => a.get_subject(),
|
Attributes::V03(a) => a.subject(),
|
||||||
Attributes::V10(a) => a.get_subject(),
|
Attributes::V10(a) => a.subject(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_time(&self) -> Option<&DateTime<Utc>> {
|
fn time(&self) -> Option<&DateTime<Utc>> {
|
||||||
match self {
|
match self {
|
||||||
Attributes::V03(a) => a.get_time(),
|
Attributes::V03(a) => a.time(),
|
||||||
Attributes::V10(a) => a.get_time(),
|
Attributes::V10(a) => a.time(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl AttributesWriter for Attributes {
|
impl AttributesWriter for Attributes {
|
||||||
fn set_id(&mut self, id: impl Into<String>) {
|
fn set_id(&mut self, id: impl Into<String>) -> String {
|
||||||
match self {
|
match self {
|
||||||
Attributes::V03(a) => a.set_id(id),
|
Attributes::V03(a) => a.set_id(id),
|
||||||
Attributes::V10(a) => a.set_id(id),
|
Attributes::V10(a) => a.set_id(id),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn set_source(&mut self, source: impl Into<Url>) {
|
fn set_source(&mut self, source: impl Into<UriReference>) -> UriReference {
|
||||||
match self {
|
match self {
|
||||||
Attributes::V03(a) => a.set_source(source),
|
Attributes::V03(a) => a.set_source(source),
|
||||||
Attributes::V10(a) => a.set_source(source),
|
Attributes::V10(a) => a.set_source(source),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn set_type(&mut self, ty: impl Into<String>) {
|
fn set_type(&mut self, ty: impl Into<String>) -> String {
|
||||||
match self {
|
match self {
|
||||||
Attributes::V03(a) => a.set_type(ty),
|
Attributes::V03(a) => a.set_type(ty),
|
||||||
Attributes::V10(a) => a.set_type(ty),
|
Attributes::V10(a) => a.set_type(ty),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn set_subject(&mut self, subject: Option<impl Into<String>>) {
|
fn set_subject(&mut self, subject: Option<impl Into<String>>) -> Option<String> {
|
||||||
match self {
|
match self {
|
||||||
Attributes::V03(a) => a.set_subject(subject),
|
Attributes::V03(a) => a.set_subject(subject),
|
||||||
Attributes::V10(a) => a.set_subject(subject),
|
Attributes::V10(a) => a.set_subject(subject),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn set_time(&mut self, time: Option<impl Into<DateTime<Utc>>>) {
|
fn set_time(&mut self, time: Option<impl Into<DateTime<Utc>>>) -> Option<DateTime<Utc>> {
|
||||||
match self {
|
match self {
|
||||||
Attributes::V03(a) => a.set_time(time),
|
Attributes::V03(a) => a.set_time(time),
|
||||||
Attributes::V10(a) => a.set_time(time),
|
Attributes::V10(a) => a.set_time(time),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
impl DataAttributesWriter for Attributes {
|
fn set_datacontenttype(
|
||||||
fn set_datacontenttype(&mut self, datacontenttype: Option<impl Into<String>>) {
|
&mut self,
|
||||||
|
datacontenttype: Option<impl Into<String>>,
|
||||||
|
) -> Option<String> {
|
||||||
match self {
|
match self {
|
||||||
Attributes::V03(a) => a.set_datacontenttype(datacontenttype),
|
Attributes::V03(a) => a.set_datacontenttype(datacontenttype),
|
||||||
Attributes::V10(a) => a.set_datacontenttype(datacontenttype),
|
Attributes::V10(a) => a.set_datacontenttype(datacontenttype),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn set_dataschema(&mut self, dataschema: Option<impl Into<Url>>) {
|
fn set_dataschema(&mut self, dataschema: Option<impl Into<Url>>) -> Option<Url> {
|
||||||
match self {
|
match self {
|
||||||
Attributes::V03(a) => a.set_dataschema(dataschema),
|
Attributes::V03(a) => a.set_dataschema(dataschema),
|
||||||
Attributes::V10(a) => a.set_dataschema(dataschema),
|
Attributes::V10(a) => a.set_dataschema(dataschema),
|
||||||
|
@ -187,18 +234,25 @@ impl DataAttributesWriter for Attributes {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Attributes {
|
impl Attributes {
|
||||||
pub fn into_v10(self) -> Self {
|
pub(crate) fn into_v10(self) -> Self {
|
||||||
match self {
|
match self {
|
||||||
Attributes::V03(v03) => Attributes::V10(v03.into_v10()),
|
Attributes::V03(v03) => Attributes::V10(v03.into_v10()),
|
||||||
_ => self,
|
_ => self,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
pub fn into_v03(self) -> Self {
|
pub(crate) fn into_v03(self) -> Self {
|
||||||
match self {
|
match self {
|
||||||
Attributes::V10(v10) => Attributes::V03(v10.into_v03()),
|
Attributes::V10(v10) => Attributes::V03(v10.into_v03()),
|
||||||
_ => self,
|
_ => self,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) fn iter(&self) -> impl Iterator<Item = (&str, AttributeValue)> {
|
||||||
|
match self {
|
||||||
|
Attributes::V03(a) => AttributesIter::IterV03(a.into_iter()),
|
||||||
|
Attributes::V10(a) => AttributesIter::IterV10(a.into_iter()),
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(not(target_arch = "wasm32"))]
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
|
@ -208,16 +262,15 @@ pub(crate) fn default_hostname() -> Url {
|
||||||
"http://{}",
|
"http://{}",
|
||||||
hostname::get()
|
hostname::get()
|
||||||
.ok()
|
.ok()
|
||||||
.map(|s| s.into_string().ok())
|
.and_then(|s| s.into_string().ok())
|
||||||
.flatten()
|
.unwrap_or_else(|| "localhost".to_string())
|
||||||
.unwrap_or(String::from("localhost".to_string()))
|
|
||||||
)
|
)
|
||||||
.as_ref(),
|
.as_ref(),
|
||||||
)
|
)
|
||||||
.unwrap()
|
.unwrap()
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(target_arch = "wasm32")]
|
#[cfg(all(target_arch = "wasm32", target_os = "unknown"))]
|
||||||
pub(crate) fn default_hostname() -> Url {
|
pub(crate) fn default_hostname() -> Url {
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
|
|
||||||
|
@ -230,3 +283,10 @@ pub(crate) fn default_hostname() -> Url {
|
||||||
)
|
)
|
||||||
.unwrap()
|
.unwrap()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(all(target_arch = "wasm32", target_os = "wasi"))]
|
||||||
|
pub(crate) fn default_hostname() -> Url {
|
||||||
|
use std::str::FromStr;
|
||||||
|
|
||||||
|
Url::from_str("http://localhost").unwrap()
|
||||||
|
}
|
||||||
|
|
|
@ -1,32 +1,284 @@
|
||||||
use super::{EventBuilderV03, EventBuilderV10};
|
use super::Event;
|
||||||
|
use snafu::Snafu;
|
||||||
|
|
||||||
/// Builder to create [`super::Event`]:
|
/// Trait to implement a builder for [`Event`]:
|
||||||
/// ```
|
/// ```
|
||||||
/// use cloudevents::EventBuilder;
|
/// use cloudevents::event::{EventBuilderV10, EventBuilder};
|
||||||
/// use chrono::Utc;
|
/// use chrono::Utc;
|
||||||
/// use url::Url;
|
/// use url::Url;
|
||||||
///
|
///
|
||||||
/// let event = EventBuilder::v10()
|
/// let event = EventBuilderV10::new()
|
||||||
/// .id("my_event.my_application")
|
/// .id("my_event.my_application")
|
||||||
/// .source(Url::parse("http://localhost:8080").unwrap())
|
/// .source("http://localhost:8080")
|
||||||
|
/// .ty("example.demo")
|
||||||
/// .time(Utc::now())
|
/// .time(Utc::now())
|
||||||
/// .build();
|
/// .build()
|
||||||
|
/// .unwrap();
|
||||||
/// ```
|
/// ```
|
||||||
pub struct EventBuilder {}
|
///
|
||||||
|
/// You can create an [`EventBuilder`] starting from an existing [`Event`] using the [`From`] trait.
|
||||||
|
/// You can create a default [`EventBuilder`] setting default values for some attributes.
|
||||||
|
pub trait EventBuilder
|
||||||
|
where
|
||||||
|
Self: Clone + Sized + From<Event> + Default,
|
||||||
|
{
|
||||||
|
/// Create a new empty builder
|
||||||
|
fn new() -> Self;
|
||||||
|
|
||||||
impl EventBuilder {
|
/// Build [`Event`]
|
||||||
/// Creates a new builder for latest CloudEvents version
|
fn build(self) -> Result<Event, Error>;
|
||||||
pub fn new() -> EventBuilderV10 {
|
}
|
||||||
return Self::v10();
|
|
||||||
|
/// Represents an error during build process
|
||||||
|
#[derive(Debug, Snafu, Clone)]
|
||||||
|
pub enum Error {
|
||||||
|
#[snafu(display("Missing required attribute {}", attribute_name))]
|
||||||
|
MissingRequiredAttribute { attribute_name: &'static str },
|
||||||
|
#[snafu(display(
|
||||||
|
"Error while setting attribute '{}' with timestamp type: {}",
|
||||||
|
attribute_name,
|
||||||
|
source
|
||||||
|
))]
|
||||||
|
ParseTimeError {
|
||||||
|
attribute_name: &'static str,
|
||||||
|
source: chrono::ParseError,
|
||||||
|
},
|
||||||
|
#[snafu(display(
|
||||||
|
"Error while setting attribute '{}' with uri type: {}",
|
||||||
|
attribute_name,
|
||||||
|
source
|
||||||
|
))]
|
||||||
|
ParseUrlError {
|
||||||
|
attribute_name: &'static str,
|
||||||
|
source: url::ParseError,
|
||||||
|
},
|
||||||
|
#[snafu(display(
|
||||||
|
"Invalid value setting attribute '{}' with uriref type",
|
||||||
|
attribute_name,
|
||||||
|
))]
|
||||||
|
InvalidUriRefError { attribute_name: &'static str },
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use crate::test::fixtures;
|
||||||
|
use crate::Event;
|
||||||
|
use crate::EventBuilder;
|
||||||
|
use crate::EventBuilderV03;
|
||||||
|
use crate::EventBuilderV10;
|
||||||
|
use claims::*;
|
||||||
|
use rstest::rstest;
|
||||||
|
use serde_json::{json, Value};
|
||||||
|
use serde_yaml;
|
||||||
|
|
||||||
|
/// Test conversions
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn v10_to_v03() {
|
||||||
|
let in_event = fixtures::v10::full_json_data();
|
||||||
|
let out_event = EventBuilderV03::from(in_event).build().unwrap();
|
||||||
|
assert_eq!(fixtures::v03::full_json_data(), out_event)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Creates a new builder for CloudEvents V1.0
|
#[test]
|
||||||
pub fn v10() -> EventBuilderV10 {
|
fn v03_to_v10() {
|
||||||
return EventBuilderV10::new();
|
let in_event = fixtures::v03::full_json_data();
|
||||||
|
let out_event = EventBuilderV10::from(in_event).build().unwrap();
|
||||||
|
assert_eq!(fixtures::v10::full_json_data(), out_event)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Creates a new builder for CloudEvents V0.3
|
/// Test YAML
|
||||||
pub fn v03() -> EventBuilderV03 {
|
/// This test checks if the usage of serde_json::Value makes the Deserialize implementation incompatible with
|
||||||
return EventBuilderV03::new();
|
/// other Deserializers
|
||||||
|
#[test]
|
||||||
|
fn deserialize_yaml_should_succeed() {
|
||||||
|
let input = r#"
|
||||||
|
id: aaa
|
||||||
|
type: bbb
|
||||||
|
source: http://localhost
|
||||||
|
datacontenttype: application/json
|
||||||
|
data: true
|
||||||
|
specversion: "1.0"
|
||||||
|
"#;
|
||||||
|
|
||||||
|
let expected = EventBuilderV10::new()
|
||||||
|
.id("aaa")
|
||||||
|
.ty("bbb")
|
||||||
|
.source("http://localhost")
|
||||||
|
.data("application/json", serde_json::Value::Bool(true))
|
||||||
|
.build()
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let deserialize_result: Result<Event, serde_yaml::Error> = serde_yaml::from_str(input);
|
||||||
|
assert_ok!(&deserialize_result);
|
||||||
|
let deserialized = deserialize_result.unwrap();
|
||||||
|
assert_eq!(deserialized, expected)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Test Json
|
||||||
|
/// This test is a parametrized test that uses data from tests/test_data
|
||||||
|
#[rstest(
|
||||||
|
in_event,
|
||||||
|
out_json,
|
||||||
|
case::minimal_v03(fixtures::v03::minimal(), fixtures::v03::minimal_json()),
|
||||||
|
case::full_v03_no_data(fixtures::v03::full_no_data(), fixtures::v03::full_no_data_json()),
|
||||||
|
case::full_v03_with_json_data(
|
||||||
|
fixtures::v03::full_json_data(),
|
||||||
|
fixtures::v03::full_json_data_json()
|
||||||
|
),
|
||||||
|
case::full_v03_with_xml_string_data(
|
||||||
|
fixtures::v03::full_xml_string_data(),
|
||||||
|
fixtures::v03::full_xml_string_data_json()
|
||||||
|
),
|
||||||
|
case::full_v03_with_xml_base64_data(
|
||||||
|
fixtures::v03::full_xml_binary_data(),
|
||||||
|
fixtures::v03::full_xml_base64_data_json()
|
||||||
|
),
|
||||||
|
case::minimal_v10(fixtures::v10::minimal(), fixtures::v10::minimal_json()),
|
||||||
|
case::full_v10_no_data(fixtures::v10::full_no_data(), fixtures::v10::full_no_data_json()),
|
||||||
|
case::full_v10_with_json_data(
|
||||||
|
fixtures::v10::full_json_data(),
|
||||||
|
fixtures::v10::full_json_data_json()
|
||||||
|
),
|
||||||
|
case::full_v10_with_xml_string_data(
|
||||||
|
fixtures::v10::full_xml_string_data(),
|
||||||
|
fixtures::v10::full_xml_string_data_json()
|
||||||
|
),
|
||||||
|
case::full_v10_with_xml_base64_data(
|
||||||
|
fixtures::v10::full_xml_binary_data(),
|
||||||
|
fixtures::v10::full_xml_base64_data_json()
|
||||||
|
)
|
||||||
|
)]
|
||||||
|
fn serialize_should_succeed(in_event: Event, out_json: Value) {
|
||||||
|
// Event -> serde_json::Value
|
||||||
|
let serialize_result = serde_json::to_value(in_event.clone());
|
||||||
|
assert_ok!(&serialize_result);
|
||||||
|
let actual_json = serialize_result.unwrap();
|
||||||
|
assert_eq!(&actual_json, &out_json);
|
||||||
|
|
||||||
|
// serde_json::Value -> String
|
||||||
|
let actual_json_serialized = actual_json.to_string();
|
||||||
|
assert_eq!(actual_json_serialized, out_json.to_string());
|
||||||
|
|
||||||
|
// String -> Event
|
||||||
|
let deserialize_result: Result<Event, serde_json::Error> =
|
||||||
|
serde_json::from_str(&actual_json_serialized);
|
||||||
|
assert_ok!(&deserialize_result);
|
||||||
|
let deserialize_json = deserialize_result.unwrap();
|
||||||
|
assert_eq!(deserialize_json, in_event)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// This test is a parametrized test that uses data from tests/test_data
|
||||||
|
#[rstest(
|
||||||
|
in_json,
|
||||||
|
out_event,
|
||||||
|
case::minimal_v03(fixtures::v03::minimal_json(), fixtures::v03::minimal()),
|
||||||
|
case::full_v03_no_data(fixtures::v03::full_no_data_json(), fixtures::v03::full_no_data()),
|
||||||
|
case::full_v03_with_json_data(
|
||||||
|
fixtures::v03::full_json_data_json(),
|
||||||
|
fixtures::v03::full_json_data()
|
||||||
|
),
|
||||||
|
case::full_v03_with_json_base64_data(
|
||||||
|
fixtures::v03::full_json_base64_data_json(),
|
||||||
|
fixtures::v03::full_json_data()
|
||||||
|
),
|
||||||
|
case::full_v03_with_xml_string_data(
|
||||||
|
fixtures::v03::full_xml_string_data_json(),
|
||||||
|
fixtures::v03::full_xml_string_data()
|
||||||
|
),
|
||||||
|
case::full_v03_with_xml_base64_data(
|
||||||
|
fixtures::v03::full_xml_base64_data_json(),
|
||||||
|
fixtures::v03::full_xml_binary_data()
|
||||||
|
),
|
||||||
|
case::minimal_v10(fixtures::v10::minimal_json(), fixtures::v10::minimal()),
|
||||||
|
case::full_v10_no_data(fixtures::v10::full_no_data_json(), fixtures::v10::full_no_data()),
|
||||||
|
case::full_v10_with_json_data(
|
||||||
|
fixtures::v10::full_json_data_json(),
|
||||||
|
fixtures::v10::full_json_data()
|
||||||
|
),
|
||||||
|
case::full_v10_with_json_base64_data(
|
||||||
|
fixtures::v10::full_json_base64_data_json(),
|
||||||
|
fixtures::v10::full_json_data()
|
||||||
|
),
|
||||||
|
case::full_v10_with_non_json_base64_data(
|
||||||
|
fixtures::v10::full_non_json_base64_data(),
|
||||||
|
fixtures::v10::full_non_json_data()
|
||||||
|
),
|
||||||
|
case::full_v10_with_xml_string_data(
|
||||||
|
fixtures::v10::full_xml_string_data_json(),
|
||||||
|
fixtures::v10::full_xml_string_data()
|
||||||
|
),
|
||||||
|
case::full_v10_with_xml_base64_data(
|
||||||
|
fixtures::v10::full_xml_base64_data_json(),
|
||||||
|
fixtures::v10::full_xml_binary_data()
|
||||||
|
)
|
||||||
|
)]
|
||||||
|
fn deserialize_json_should_succeed(in_json: Value, out_event: Event) {
|
||||||
|
let deserialize_result: Result<Event, serde_json::Error> = serde_json::from_value(in_json);
|
||||||
|
assert_ok!(&deserialize_result);
|
||||||
|
let deserialize_json = deserialize_result.unwrap();
|
||||||
|
assert_eq!(deserialize_json, out_event)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn deserialize_with_null_attribute() {
|
||||||
|
let in_json = json!({
|
||||||
|
"specversion" : "1.0",
|
||||||
|
"type" : "com.example.someevent",
|
||||||
|
"source" : "/mycontext",
|
||||||
|
"id" : "A234-1234-1234",
|
||||||
|
"time" : null,
|
||||||
|
"comexampleextension1" : "value",
|
||||||
|
"comexampleothervalue" : 5,
|
||||||
|
"datacontenttype" : "text/xml",
|
||||||
|
"data" : "<much wow=\"xml\"/>"
|
||||||
|
});
|
||||||
|
|
||||||
|
let out_event = EventBuilderV10::new()
|
||||||
|
.ty("com.example.someevent")
|
||||||
|
.source("/mycontext")
|
||||||
|
.id("A234-1234-1234")
|
||||||
|
.data("text/xml", "<much wow=\"xml\"/>")
|
||||||
|
.extension("comexampleextension1", "value")
|
||||||
|
.extension("comexampleothervalue", 5)
|
||||||
|
.build()
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let deserialize_result: Result<Event, serde_json::Error> = serde_json::from_value(in_json);
|
||||||
|
assert_ok!(&deserialize_result);
|
||||||
|
let deserialize_json = deserialize_result.unwrap();
|
||||||
|
assert_eq!(deserialize_json, out_event)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn deserialize_with_null_ext() {
|
||||||
|
let in_json = json!({
|
||||||
|
"specversion" : "1.0",
|
||||||
|
"type" : "com.example.someevent",
|
||||||
|
"source" : "/mycontext",
|
||||||
|
"id" : "A234-1234-1234",
|
||||||
|
"time" : "2018-04-05T17:31:00Z",
|
||||||
|
"comexampleextension1" : "value",
|
||||||
|
"comexampleothervalue" : 5,
|
||||||
|
"unsetextension": null,
|
||||||
|
"datacontenttype" : "text/xml",
|
||||||
|
"data" : "<much wow=\"xml\"/>"
|
||||||
|
});
|
||||||
|
|
||||||
|
let out_event = EventBuilderV10::new()
|
||||||
|
.ty("com.example.someevent")
|
||||||
|
.source("/mycontext")
|
||||||
|
.id("A234-1234-1234")
|
||||||
|
.time("2018-04-05T17:31:00Z")
|
||||||
|
.data("text/xml", "<much wow=\"xml\"/>")
|
||||||
|
.extension("comexampleextension1", "value")
|
||||||
|
.extension("comexampleothervalue", 5)
|
||||||
|
.build()
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let deserialize_result: Result<Event, serde_json::Error> = serde_json::from_value(in_json);
|
||||||
|
assert_ok!(&deserialize_result);
|
||||||
|
let deserialize_json = deserialize_result.unwrap();
|
||||||
|
assert_eq!(deserialize_json, out_event)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,11 @@
|
||||||
use std::convert::{Into, TryFrom};
|
use serde_json::Value;
|
||||||
|
use std::convert::TryFrom;
|
||||||
|
use std::fmt;
|
||||||
|
use std::fmt::Formatter;
|
||||||
|
use std::str;
|
||||||
|
|
||||||
/// Event [data attribute](https://github.com/cloudevents/spec/blob/master/spec.md#event-data) representation
|
/// Event [data attribute](https://github.com/cloudevents/spec/blob/master/spec.md#event-data) representation
|
||||||
#[derive(Debug, PartialEq, Clone)]
|
#[derive(PartialEq, Eq, Debug, Clone)]
|
||||||
pub enum Data {
|
pub enum Data {
|
||||||
/// Event has a binary payload
|
/// Event has a binary payload
|
||||||
Binary(Vec<u8>),
|
Binary(Vec<u8>),
|
||||||
|
@ -11,59 +15,31 @@ pub enum Data {
|
||||||
Json(serde_json::Value),
|
Json(serde_json::Value),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Data {
|
|
||||||
/// Create a [`Data`] from a [`Into<Vec<u8>>`].
|
|
||||||
///
|
|
||||||
/// # Example
|
|
||||||
///
|
|
||||||
/// ```
|
|
||||||
/// use cloudevents::event::Data;
|
|
||||||
///
|
|
||||||
/// let value = Data::from_base64(b"dmFsdWU=").unwrap();
|
|
||||||
/// assert_eq!(value, Data::Binary(base64::decode("dmFsdWU=").unwrap()));
|
|
||||||
/// ```
|
|
||||||
///
|
|
||||||
/// [`AsRef<[u8]>`]: https://doc.rust-lang.org/std/convert/trait.AsRef.html
|
|
||||||
/// [`Data`]: enum.Data.html
|
|
||||||
pub fn from_base64<I>(i: I) -> Result<Self, base64::DecodeError>
|
|
||||||
where
|
|
||||||
I: AsRef<[u8]>,
|
|
||||||
{
|
|
||||||
Ok(base64::decode(&i)?.into())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn from_binary<I>(content_type: Option<&str>, i: I) -> Result<Self, serde_json::Error>
|
|
||||||
where
|
|
||||||
I: AsRef<[u8]>,
|
|
||||||
{
|
|
||||||
let is_json = is_json_content_type(content_type.unwrap_or("application/json"));
|
|
||||||
if is_json {
|
|
||||||
serde_json::from_slice::<serde_json::Value>(i.as_ref()).map(Data::Json)
|
|
||||||
} else {
|
|
||||||
Ok(Data::Binary(i.as_ref().to_vec()))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn is_json_content_type(ct: &str) -> bool {
|
pub(crate) fn is_json_content_type(ct: &str) -> bool {
|
||||||
ct == "application/json" || ct == "text/json" || ct.ends_with("+json")
|
ct.starts_with("application/json") || ct.starts_with("text/json") || ct.ends_with("+json")
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Into<Data> for serde_json::Value {
|
impl From<serde_json::Value> for Data {
|
||||||
fn into(self) -> Data {
|
fn from(value: Value) -> Self {
|
||||||
Data::Json(self)
|
Data::Json(value)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Into<Data> for Vec<u8> {
|
impl From<Vec<u8>> for Data {
|
||||||
fn into(self) -> Data {
|
fn from(value: Vec<u8>) -> Self {
|
||||||
Data::Binary(self)
|
Data::Binary(value)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Into<Data> for String {
|
impl From<String> for Data {
|
||||||
fn into(self) -> Data {
|
fn from(value: String) -> Self {
|
||||||
Data::String(self)
|
Data::String(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<&str> for Data {
|
||||||
|
fn from(value: &str) -> Self {
|
||||||
|
Data::String(String::from(value))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -84,7 +60,7 @@ impl TryFrom<Data> for Vec<u8> {
|
||||||
|
|
||||||
fn try_from(value: Data) -> Result<Self, Self::Error> {
|
fn try_from(value: Data) -> Result<Self, Self::Error> {
|
||||||
match value {
|
match value {
|
||||||
Data::Binary(v) => Ok(serde_json::from_slice(&v)?),
|
Data::Binary(v) => Ok(v),
|
||||||
Data::Json(v) => Ok(serde_json::to_vec(&v)?),
|
Data::Json(v) => Ok(serde_json::to_vec(&v)?),
|
||||||
Data::String(s) => Ok(s.into_bytes()),
|
Data::String(s) => Ok(s.into_bytes()),
|
||||||
}
|
}
|
||||||
|
@ -102,3 +78,13 @@ impl TryFrom<Data> for String {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl fmt::Display for Data {
|
||||||
|
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
|
||||||
|
match self {
|
||||||
|
Data::Binary(vec) => write!(f, "Binary data: {:?}", str::from_utf8(vec).unwrap()),
|
||||||
|
Data::String(s) => write!(f, "String data: {}", s),
|
||||||
|
Data::Json(j) => write!(f, "Json data: {}", j),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -1,230 +0,0 @@
|
||||||
use super::{
|
|
||||||
Attributes, AttributesReader, AttributesV10, AttributesWriter, Data, ExtensionValue,
|
|
||||||
SpecVersion,
|
|
||||||
};
|
|
||||||
use crate::event::attributes::DataAttributesWriter;
|
|
||||||
use chrono::{DateTime, Utc};
|
|
||||||
use delegate::delegate;
|
|
||||||
use std::collections::HashMap;
|
|
||||||
use std::convert::TryFrom;
|
|
||||||
use url::Url;
|
|
||||||
|
|
||||||
/// Data structure that represents a [CloudEvent](https://github.com/cloudevents/spec/blob/master/spec.md).
|
|
||||||
/// It provides methods to get the attributes through [`AttributesReader`]
|
|
||||||
/// and write them through [`AttributesWriter`].
|
|
||||||
/// It also provides methods to read and write the [event data](https://github.com/cloudevents/spec/blob/master/spec.md#event-data).
|
|
||||||
///
|
|
||||||
/// You can build events using [`super::EventBuilder`]
|
|
||||||
/// ```
|
|
||||||
/// use cloudevents::Event;
|
|
||||||
/// use cloudevents::event::AttributesReader;
|
|
||||||
///
|
|
||||||
/// // Create an event using the Default trait
|
|
||||||
/// let mut e = Event::default();
|
|
||||||
/// e.write_data(
|
|
||||||
/// "application/json",
|
|
||||||
/// serde_json::json!({"hello": "world"})
|
|
||||||
/// );
|
|
||||||
///
|
|
||||||
/// // Print the event id
|
|
||||||
/// println!("Event id: {}", e.get_id());
|
|
||||||
///
|
|
||||||
/// // Get the event data
|
|
||||||
/// let data: serde_json::Value = e.try_get_data().unwrap().unwrap();
|
|
||||||
/// println!("Event data: {}", data)
|
|
||||||
/// ```
|
|
||||||
#[derive(PartialEq, Debug, Clone)]
|
|
||||||
pub struct Event {
|
|
||||||
pub(crate) attributes: Attributes,
|
|
||||||
pub(crate) data: Option<Data>,
|
|
||||||
pub(crate) extensions: HashMap<String, ExtensionValue>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl AttributesReader for Event {
|
|
||||||
delegate! {
|
|
||||||
to self.attributes {
|
|
||||||
fn get_id(&self) -> &str;
|
|
||||||
fn get_source(&self) -> &Url;
|
|
||||||
fn get_specversion(&self) -> SpecVersion;
|
|
||||||
fn get_type(&self) -> &str;
|
|
||||||
fn get_datacontenttype(&self) -> Option<&str>;
|
|
||||||
fn get_dataschema(&self) -> Option<&Url>;
|
|
||||||
fn get_subject(&self) -> Option<&str>;
|
|
||||||
fn get_time(&self) -> Option<&DateTime<Utc>>;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl AttributesWriter for Event {
|
|
||||||
delegate! {
|
|
||||||
to self.attributes {
|
|
||||||
fn set_id(&mut self, id: impl Into<String>);
|
|
||||||
fn set_source(&mut self, source: impl Into<Url>);
|
|
||||||
fn set_type(&mut self, ty: impl Into<String>);
|
|
||||||
fn set_subject(&mut self, subject: Option<impl Into<String>>);
|
|
||||||
fn set_time(&mut self, time: Option<impl Into<DateTime<Utc>>>);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for Event {
|
|
||||||
fn default() -> Self {
|
|
||||||
Event {
|
|
||||||
attributes: Attributes::V10(AttributesV10::default()),
|
|
||||||
data: None,
|
|
||||||
extensions: HashMap::default(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Event {
|
|
||||||
/// Remove `data`, `dataschema` and `datacontenttype` from this `Event`
|
|
||||||
pub fn remove_data(&mut self) {
|
|
||||||
self.data = None;
|
|
||||||
self.attributes.set_dataschema(None as Option<Url>);
|
|
||||||
self.attributes.set_datacontenttype(None as Option<String>);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Write `data` into this `Event` with the specified `datacontenttype`.
|
|
||||||
///
|
|
||||||
/// ```
|
|
||||||
/// use cloudevents::Event;
|
|
||||||
/// use serde_json::json;
|
|
||||||
/// use std::convert::Into;
|
|
||||||
///
|
|
||||||
/// let mut e = Event::default();
|
|
||||||
/// e.write_data("application/json", json!({}))
|
|
||||||
/// ```
|
|
||||||
pub fn write_data(&mut self, datacontenttype: impl Into<String>, data: impl Into<Data>) {
|
|
||||||
self.attributes.set_datacontenttype(Some(datacontenttype));
|
|
||||||
self.attributes.set_dataschema(None as Option<Url>);
|
|
||||||
self.data = Some(data.into());
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Write `data` into this `Event` with the specified `datacontenttype` and `dataschema`.
|
|
||||||
///
|
|
||||||
/// ```
|
|
||||||
/// use cloudevents::Event;
|
|
||||||
/// use serde_json::json;
|
|
||||||
/// use std::convert::Into;
|
|
||||||
/// use url::Url;
|
|
||||||
///
|
|
||||||
/// let mut e = Event::default();
|
|
||||||
/// e.write_data_with_schema(
|
|
||||||
/// "application/json",
|
|
||||||
/// Url::parse("http://myapplication.com/schema").unwrap(),
|
|
||||||
/// json!({})
|
|
||||||
/// )
|
|
||||||
/// ```
|
|
||||||
pub fn write_data_with_schema(
|
|
||||||
&mut self,
|
|
||||||
datacontenttype: impl Into<String>,
|
|
||||||
dataschema: impl Into<Url>,
|
|
||||||
data: impl Into<Data>,
|
|
||||||
) {
|
|
||||||
self.attributes.set_datacontenttype(Some(datacontenttype));
|
|
||||||
self.attributes.set_dataschema(Some(dataschema));
|
|
||||||
self.data = Some(data.into());
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get `data` from this `Event`
|
|
||||||
pub fn get_data<T: Sized + From<Data>>(&self) -> Option<T> {
|
|
||||||
match self.data.as_ref() {
|
|
||||||
Some(d) => Some(T::from(d.clone())),
|
|
||||||
None => None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Try to get `data` from this `Event`
|
|
||||||
pub fn try_get_data<T: Sized + TryFrom<Data>>(&self) -> Result<Option<T>, T::Error> {
|
|
||||||
match self.data.as_ref() {
|
|
||||||
Some(d) => Some(T::try_from(d.clone())),
|
|
||||||
None => None,
|
|
||||||
}
|
|
||||||
.transpose()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Transform this `Event` into the content of `data`
|
|
||||||
pub fn into_data<T: Sized + TryFrom<Data>>(self) -> Result<Option<T>, T::Error> {
|
|
||||||
match self.data {
|
|
||||||
Some(d) => Some(T::try_from(d)),
|
|
||||||
None => None,
|
|
||||||
}
|
|
||||||
.transpose()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get the [extension](https://github.com/cloudevents/spec/blob/master/spec.md#extension-context-attributes) named `extension_name`
|
|
||||||
pub fn get_extension(&self, extension_name: &str) -> Option<&ExtensionValue> {
|
|
||||||
self.extensions.get(extension_name)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get all the [extensions](https://github.com/cloudevents/spec/blob/master/spec.md#extension-context-attributes)
|
|
||||||
pub fn get_extensions(&self) -> Vec<(&str, &ExtensionValue)> {
|
|
||||||
self.extensions
|
|
||||||
.iter()
|
|
||||||
.map(|(k, v)| (k.as_str(), v))
|
|
||||||
.collect()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Set the [extension](https://github.com/cloudevents/spec/blob/master/spec.md#extension-context-attributes) named `extension_name` with `extension_value`
|
|
||||||
pub fn set_extension<'name, 'event: 'name>(
|
|
||||||
&'event mut self,
|
|
||||||
extension_name: &'name str,
|
|
||||||
extension_value: impl Into<ExtensionValue>,
|
|
||||||
) {
|
|
||||||
self.extensions
|
|
||||||
.insert(extension_name.to_owned(), extension_value.into());
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Remove the [extension](https://github.com/cloudevents/spec/blob/master/spec.md#extension-context-attributes) named `extension_name`
|
|
||||||
pub fn remove_extension<'name, 'event: 'name>(
|
|
||||||
&'event mut self,
|
|
||||||
extension_name: &'name str,
|
|
||||||
) -> Option<ExtensionValue> {
|
|
||||||
self.extensions.remove(extension_name)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use super::*;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn try_get_data_json() {
|
|
||||||
let expected_data = serde_json::json!({
|
|
||||||
"hello": "world"
|
|
||||||
});
|
|
||||||
|
|
||||||
let mut e = Event::default();
|
|
||||||
e.write_data_with_schema(
|
|
||||||
"application/json",
|
|
||||||
Url::parse("http://localhost:8080/schema").unwrap(),
|
|
||||||
expected_data.clone(),
|
|
||||||
);
|
|
||||||
|
|
||||||
let data: serde_json::Value = e.try_get_data().unwrap().unwrap();
|
|
||||||
assert_eq!(expected_data, data);
|
|
||||||
assert_eq!("application/json", e.get_datacontenttype().unwrap());
|
|
||||||
assert_eq!(
|
|
||||||
&Url::parse("http://localhost:8080/schema").unwrap(),
|
|
||||||
e.get_dataschema().unwrap()
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn remove_data() {
|
|
||||||
let mut e = Event::default();
|
|
||||||
e.write_data(
|
|
||||||
"application/json",
|
|
||||||
serde_json::json!({
|
|
||||||
"hello": "world"
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
e.remove_data();
|
|
||||||
|
|
||||||
assert!(e.try_get_data::<serde_json::Value>().unwrap().is_none());
|
|
||||||
assert!(e.get_dataschema().is_none());
|
|
||||||
assert!(e.get_datacontenttype().is_none());
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,15 +1,16 @@
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize, Serializer};
|
||||||
use std::convert::From;
|
use std::convert::From;
|
||||||
|
use std::fmt;
|
||||||
|
|
||||||
#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)]
|
#[derive(PartialEq, Eq, Debug, Clone, Serialize, Deserialize)]
|
||||||
#[serde(untagged)]
|
#[serde(untagged)]
|
||||||
/// Represents all the possible [CloudEvents extension](https://github.com/cloudevents/spec/blob/master/spec.md#extension-context-attributes) values
|
/// Represents all the possible [CloudEvents extension](https://github.com/cloudevents/spec/blob/master/spec.md#extension-context-attributes) values
|
||||||
pub enum ExtensionValue {
|
pub enum ExtensionValue {
|
||||||
/// Represents a [`String`](std::string::String) value.
|
/// Represents a [`String`] value.
|
||||||
String(String),
|
String(String),
|
||||||
/// Represents a [`bool`](bool) value.
|
/// Represents a [`bool`] value.
|
||||||
Boolean(bool),
|
Boolean(bool),
|
||||||
/// Represents an integer [`i64`](i64) value.
|
/// Represents an integer [`i64`] value.
|
||||||
Integer(i64),
|
Integer(i64),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -59,3 +60,13 @@ impl ExtensionValue {
|
||||||
ExtensionValue::from(s.into())
|
ExtensionValue::from(s.into())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl fmt::Display for ExtensionValue {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
match self {
|
||||||
|
ExtensionValue::String(s) => f.write_str(s),
|
||||||
|
ExtensionValue::Boolean(b) => f.serialize_bool(*b),
|
||||||
|
ExtensionValue::Integer(i) => f.serialize_i64(*i),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -3,119 +3,99 @@ use super::{
|
||||||
EventFormatSerializerV03, EventFormatSerializerV10,
|
EventFormatSerializerV03, EventFormatSerializerV10,
|
||||||
};
|
};
|
||||||
use crate::event::{AttributesReader, ExtensionValue};
|
use crate::event::{AttributesReader, ExtensionValue};
|
||||||
use serde::de::{Error, IntoDeserializer, Unexpected};
|
use base64::prelude::*;
|
||||||
|
use serde::de::{Error, IntoDeserializer};
|
||||||
use serde::{Deserialize, Deserializer, Serialize, Serializer};
|
use serde::{Deserialize, Deserializer, Serialize, Serializer};
|
||||||
use serde_value::Value;
|
use serde_json::{Map, Value};
|
||||||
use std::collections::{BTreeMap, HashMap};
|
use std::collections::HashMap;
|
||||||
|
|
||||||
macro_rules! parse_optional_field {
|
|
||||||
($map:ident, $name:literal, $value_variant:ident, $error:ty) => {
|
|
||||||
$map.remove($name)
|
|
||||||
.map(|val| match val {
|
|
||||||
Value::$value_variant(v) => Ok(v),
|
|
||||||
other => Err(<$error>::invalid_type(
|
|
||||||
crate::event::format::value_to_unexpected(&other),
|
|
||||||
&stringify!($value_variant),
|
|
||||||
)),
|
|
||||||
})
|
|
||||||
.transpose()
|
|
||||||
};
|
|
||||||
|
|
||||||
($map:ident, $name:literal, $value_variant:ident, $error:ty, $mapper:expr) => {
|
|
||||||
$map.remove($name)
|
|
||||||
.map(|val| match val {
|
|
||||||
Value::$value_variant(v) => $mapper(&v).map_err(|e| {
|
|
||||||
<$error>::invalid_value(
|
|
||||||
crate::event::format::value_to_unexpected(&Value::$value_variant(v)),
|
|
||||||
&e.to_string().as_str(),
|
|
||||||
)
|
|
||||||
}),
|
|
||||||
other => Err(<$error>::invalid_type(
|
|
||||||
crate::event::format::value_to_unexpected(&other),
|
|
||||||
&stringify!($value_variant),
|
|
||||||
)),
|
|
||||||
})
|
|
||||||
.transpose()
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
macro_rules! parse_field {
|
macro_rules! parse_field {
|
||||||
($map:ident, $name:literal, $value_variant:ident, $error:ty) => {
|
($value:expr, $target_type:ty, $error:ty) => {
|
||||||
parse_optional_field!($map, $name, $value_variant, $error)?
|
<$target_type>::deserialize($value.into_deserializer()).map_err(<$error>::custom)
|
||||||
|
};
|
||||||
|
|
||||||
|
($value:expr, $target_type:ty, $error:ty, $mapper:expr) => {
|
||||||
|
<$target_type>::deserialize($value.into_deserializer())
|
||||||
|
.map_err(<$error>::custom)
|
||||||
|
.and_then(|v| $mapper(v).map_err(<$error>::custom))
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
macro_rules! extract_optional_field {
|
||||||
|
($map:ident, $name:literal, $target_type:ty, $error:ty) => {
|
||||||
|
$map.remove($name)
|
||||||
|
.filter(|v| !v.is_null())
|
||||||
|
.map(|v| parse_field!(v, $target_type, $error))
|
||||||
|
.transpose()
|
||||||
|
};
|
||||||
|
|
||||||
|
($map:ident, $name:literal, $target_type:ty, $error:ty, $mapper:expr) => {
|
||||||
|
$map.remove($name)
|
||||||
|
.filter(|v| !v.is_null())
|
||||||
|
.map(|v| parse_field!(v, $target_type, $error, $mapper))
|
||||||
|
.transpose()
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
macro_rules! extract_field {
|
||||||
|
($map:ident, $name:literal, $target_type:ty, $error:ty) => {
|
||||||
|
extract_optional_field!($map, $name, $target_type, $error)?
|
||||||
.ok_or_else(|| <$error>::missing_field($name))
|
.ok_or_else(|| <$error>::missing_field($name))
|
||||||
};
|
};
|
||||||
|
|
||||||
($map:ident, $name:literal, $value_variant:ident, $error:ty, $mapper:expr) => {
|
($map:ident, $name:literal, $target_type:ty, $error:ty, $mapper:expr) => {
|
||||||
parse_optional_field!($map, $name, $value_variant, $error, $mapper)?
|
extract_optional_field!($map, $name, $target_type, $error, $mapper)?
|
||||||
.ok_or_else(|| <$error>::missing_field($name))
|
.ok_or_else(|| <$error>::missing_field($name))
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
macro_rules! parse_data_json {
|
pub fn parse_data_json<E: serde::de::Error>(v: Value) -> Result<Value, E> {
|
||||||
($in:ident, $error:ty) => {
|
Value::deserialize(v.into_deserializer()).map_err(E::custom)
|
||||||
Ok(serde_json::Value::deserialize($in.into_deserializer())
|
|
||||||
.map_err(|e| <$error>::custom(e))?)
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
macro_rules! parse_data_string {
|
pub fn parse_data_string<E: serde::de::Error>(v: Value) -> Result<String, E> {
|
||||||
($in:ident, $error:ty) => {
|
parse_field!(v, String, E)
|
||||||
match $in {
|
|
||||||
Value::String(s) => Ok(s),
|
|
||||||
other => Err(E::invalid_type(
|
|
||||||
crate::event::format::value_to_unexpected(&other),
|
|
||||||
&"a string",
|
|
||||||
)),
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
macro_rules! parse_json_data_base64 {
|
pub fn parse_data_base64<E: serde::de::Error>(v: Value) -> Result<Vec<u8>, E> {
|
||||||
($in:ident, $error:ty) => {{
|
parse_field!(v, String, E).and_then(|s| {
|
||||||
let data = parse_data_base64!($in, $error)?;
|
BASE64_STANDARD
|
||||||
serde_json::from_slice(&data).map_err(|e| <$error>::custom(e))
|
.decode(s)
|
||||||
}};
|
.map_err(|e| E::custom(format_args!("decode error `{}`", e)))
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
macro_rules! parse_data_base64 {
|
pub fn parse_data_base64_json<E: serde::de::Error>(v: Value) -> Result<Value, E> {
|
||||||
($in:ident, $error:ty) => {
|
let data = parse_data_base64(v)?;
|
||||||
match $in {
|
serde_json::from_slice(&data).map_err(E::custom)
|
||||||
Value::String(s) => base64::decode(&s).map_err(|e| {
|
|
||||||
<$error>::invalid_value(serde::de::Unexpected::Str(&s), &e.to_string().as_str())
|
|
||||||
}),
|
|
||||||
other => Err(E::invalid_type(
|
|
||||||
crate::event::format::value_to_unexpected(&other),
|
|
||||||
&"a string",
|
|
||||||
)),
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) trait EventFormatDeserializer {
|
pub(crate) trait EventFormatDeserializer {
|
||||||
fn deserialize_attributes<E: serde::de::Error>(
|
fn deserialize_attributes<E: serde::de::Error>(
|
||||||
map: &mut BTreeMap<String, Value>,
|
map: &mut Map<String, Value>,
|
||||||
) -> Result<Attributes, E>;
|
) -> Result<Attributes, E>;
|
||||||
|
|
||||||
fn deserialize_data<E: serde::de::Error>(
|
fn deserialize_data<E: serde::de::Error>(
|
||||||
content_type: &str,
|
content_type: &str,
|
||||||
map: &mut BTreeMap<String, Value>,
|
map: &mut Map<String, Value>,
|
||||||
) -> Result<Option<Data>, E>;
|
) -> Result<Option<Data>, E>;
|
||||||
|
|
||||||
fn deserialize_event<E: serde::de::Error>(
|
fn deserialize_event<E: serde::de::Error>(mut map: Map<String, Value>) -> Result<Event, E> {
|
||||||
mut map: BTreeMap<String, Value>,
|
|
||||||
) -> Result<Event, E> {
|
|
||||||
let attributes = Self::deserialize_attributes(&mut map)?;
|
let attributes = Self::deserialize_attributes(&mut map)?;
|
||||||
let data = Self::deserialize_data(
|
let data = Self::deserialize_data(
|
||||||
attributes
|
attributes.datacontenttype().unwrap_or("application/json"),
|
||||||
.get_datacontenttype()
|
|
||||||
.unwrap_or("application/json"),
|
|
||||||
&mut map,
|
&mut map,
|
||||||
)?;
|
)?;
|
||||||
let extensions = map
|
let extensions = map
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|(k, v)| Ok((k, ExtensionValue::deserialize(v.into_deserializer())?)))
|
.filter(|v| !v.1.is_null())
|
||||||
.collect::<Result<HashMap<String, ExtensionValue>, serde_value::DeserializerError>>()
|
.map(|(k, v)| {
|
||||||
.map_err(|e| E::custom(e))?;
|
Ok((
|
||||||
|
k,
|
||||||
|
ExtensionValue::deserialize(v.into_deserializer()).map_err(E::custom)?,
|
||||||
|
))
|
||||||
|
})
|
||||||
|
.collect::<Result<HashMap<String, ExtensionValue>, E>>()?;
|
||||||
|
|
||||||
Ok(Event {
|
Ok(Event {
|
||||||
attributes,
|
attributes,
|
||||||
|
@ -139,20 +119,12 @@ impl<'de> Deserialize<'de> for Event {
|
||||||
where
|
where
|
||||||
D: Deserializer<'de>,
|
D: Deserializer<'de>,
|
||||||
{
|
{
|
||||||
let map = match Value::deserialize(deserializer)? {
|
let root_value = Value::deserialize(deserializer)?;
|
||||||
Value::Map(m) => Ok(m),
|
let mut map: Map<String, Value> =
|
||||||
v => Err(Error::invalid_type(value_to_unexpected(&v), &"a map")),
|
Map::deserialize(root_value.into_deserializer()).map_err(D::Error::custom)?;
|
||||||
}?;
|
|
||||||
|
|
||||||
let mut map: BTreeMap<String, Value> = map
|
match extract_field!(map, "specversion", String, <D as Deserializer<'de>>::Error)?.as_str()
|
||||||
.into_iter()
|
{
|
||||||
.map(|(k, v)| match k {
|
|
||||||
Value::String(s) => Ok((s, v)),
|
|
||||||
k => Err(Error::invalid_type(value_to_unexpected(&k), &"a string")),
|
|
||||||
})
|
|
||||||
.collect::<Result<BTreeMap<String, Value>, <D as Deserializer<'de>>::Error>>()?;
|
|
||||||
|
|
||||||
match parse_field!(map, "specversion", String, <D as Deserializer<'de>>::Error)?.as_str() {
|
|
||||||
"0.3" => EventFormatDeserializerV03::deserialize_event(map),
|
"0.3" => EventFormatDeserializerV03::deserialize_event(map),
|
||||||
"1.0" => EventFormatDeserializerV10::deserialize_event(map),
|
"1.0" => EventFormatDeserializerV10::deserialize_event(map),
|
||||||
s => Err(D::Error::unknown_variant(
|
s => Err(D::Error::unknown_variant(
|
||||||
|
@ -178,28 +150,3 @@ impl Serialize for Event {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// This should be provided by the Value package itself
|
|
||||||
pub(crate) fn value_to_unexpected(v: &Value) -> Unexpected {
|
|
||||||
match v {
|
|
||||||
Value::Bool(b) => serde::de::Unexpected::Bool(*b),
|
|
||||||
Value::U8(n) => serde::de::Unexpected::Unsigned(*n as u64),
|
|
||||||
Value::U16(n) => serde::de::Unexpected::Unsigned(*n as u64),
|
|
||||||
Value::U32(n) => serde::de::Unexpected::Unsigned(*n as u64),
|
|
||||||
Value::U64(n) => serde::de::Unexpected::Unsigned(*n),
|
|
||||||
Value::I8(n) => serde::de::Unexpected::Signed(*n as i64),
|
|
||||||
Value::I16(n) => serde::de::Unexpected::Signed(*n as i64),
|
|
||||||
Value::I32(n) => serde::de::Unexpected::Signed(*n as i64),
|
|
||||||
Value::I64(n) => serde::de::Unexpected::Signed(*n),
|
|
||||||
Value::F32(n) => serde::de::Unexpected::Float(*n as f64),
|
|
||||||
Value::F64(n) => serde::de::Unexpected::Float(*n),
|
|
||||||
Value::Char(c) => serde::de::Unexpected::Char(*c),
|
|
||||||
Value::String(s) => serde::de::Unexpected::Str(s),
|
|
||||||
Value::Unit => serde::de::Unexpected::Unit,
|
|
||||||
Value::Option(_) => serde::de::Unexpected::Option,
|
|
||||||
Value::Newtype(_) => serde::de::Unexpected::NewtypeStruct,
|
|
||||||
Value::Seq(_) => serde::de::Unexpected::Seq,
|
|
||||||
Value::Map(_) => serde::de::Unexpected::Map,
|
|
||||||
Value::Bytes(b) => serde::de::Unexpected::Bytes(b),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -6,6 +6,7 @@ use crate::message::{
|
||||||
BinaryDeserializer, BinarySerializer, MessageAttributeValue, Result, StructuredDeserializer,
|
BinaryDeserializer, BinarySerializer, MessageAttributeValue, Result, StructuredDeserializer,
|
||||||
StructuredSerializer,
|
StructuredSerializer,
|
||||||
};
|
};
|
||||||
|
use crate::{EventBuilder, EventBuilderV03, EventBuilderV10};
|
||||||
|
|
||||||
impl StructuredDeserializer for Event {
|
impl StructuredDeserializer for Event {
|
||||||
fn deserialize_structured<R, V: StructuredSerializer<R>>(self, visitor: V) -> Result<R> {
|
fn deserialize_structured<R, V: StructuredSerializer<R>>(self, visitor: V) -> Result<R> {
|
||||||
|
@ -16,7 +17,7 @@ impl StructuredDeserializer for Event {
|
||||||
|
|
||||||
impl BinaryDeserializer for Event {
|
impl BinaryDeserializer for Event {
|
||||||
fn deserialize_binary<R: Sized, V: BinarySerializer<R>>(self, mut visitor: V) -> Result<R> {
|
fn deserialize_binary<R: Sized, V: BinarySerializer<R>>(self, mut visitor: V) -> Result<R> {
|
||||||
visitor = visitor.set_spec_version(self.get_specversion())?;
|
visitor = visitor.set_spec_version(self.specversion())?;
|
||||||
visitor = self.attributes.deserialize_attributes(visitor)?;
|
visitor = self.attributes.deserialize_attributes(visitor)?;
|
||||||
for (k, v) in self.extensions.into_iter() {
|
for (k, v) in self.extensions.into_iter() {
|
||||||
visitor = visitor.set_extension(&k, v.into())?;
|
visitor = visitor.set_extension(&k, v.into())?;
|
||||||
|
@ -37,10 +38,6 @@ pub(crate) trait AttributesDeserializer {
|
||||||
fn deserialize_attributes<R: Sized, V: BinarySerializer<R>>(self, visitor: V) -> Result<V>;
|
fn deserialize_attributes<R: Sized, V: BinarySerializer<R>>(self, visitor: V) -> Result<V>;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) trait AttributesSerializer {
|
|
||||||
fn serialize_attribute(&mut self, name: &str, value: MessageAttributeValue) -> Result<()>;
|
|
||||||
}
|
|
||||||
|
|
||||||
impl AttributesDeserializer for Attributes {
|
impl AttributesDeserializer for Attributes {
|
||||||
fn deserialize_attributes<R: Sized, V: BinarySerializer<R>>(self, visitor: V) -> Result<V> {
|
fn deserialize_attributes<R: Sized, V: BinarySerializer<R>>(self, visitor: V) -> Result<V> {
|
||||||
match self {
|
match self {
|
||||||
|
@ -50,50 +47,202 @@ impl AttributesDeserializer for Attributes {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl AttributesSerializer for Attributes {
|
pub(crate) trait AttributesSerializer {
|
||||||
fn serialize_attribute(&mut self, name: &str, value: MessageAttributeValue) -> Result<()> {
|
fn serialize_attribute(&mut self, name: &str, value: MessageAttributeValue) -> Result<()>;
|
||||||
match self {
|
}
|
||||||
Attributes::V03(v03) => v03.serialize_attribute(name, value),
|
|
||||||
Attributes::V10(v10) => v10.serialize_attribute(name, value),
|
#[derive(Debug)]
|
||||||
}
|
pub(crate) struct EventStructuredSerializer {}
|
||||||
|
|
||||||
|
impl StructuredSerializer<Event> for EventStructuredSerializer {
|
||||||
|
fn set_structured_event(self, bytes: Vec<u8>) -> Result<Event> {
|
||||||
|
Ok(serde_json::from_slice(&bytes)?)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl StructuredSerializer<Event> for Event {
|
#[derive(Debug)]
|
||||||
fn set_structured_event(mut self, bytes: Vec<u8>) -> Result<Event> {
|
pub(crate) enum EventBinarySerializer {
|
||||||
let new_event: Event = serde_json::from_slice(&bytes)?;
|
V10(EventBuilderV10),
|
||||||
self.attributes = new_event.attributes;
|
V03(EventBuilderV03),
|
||||||
self.data = new_event.data;
|
}
|
||||||
self.extensions = new_event.extensions;
|
|
||||||
Ok(self)
|
impl EventBinarySerializer {
|
||||||
|
pub(crate) fn new() -> Self {
|
||||||
|
EventBinarySerializer::V10(EventBuilderV10::new())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl BinarySerializer<Event> for Event {
|
impl BinarySerializer<Event> for EventBinarySerializer {
|
||||||
fn set_spec_version(mut self, spec_version: SpecVersion) -> Result<Self> {
|
fn set_spec_version(self, spec_version: SpecVersion) -> Result<Self> {
|
||||||
match spec_version {
|
Ok(match spec_version {
|
||||||
SpecVersion::V03 => self.attributes = self.attributes.clone().into_v03(),
|
SpecVersion::V03 => EventBinarySerializer::V03(EventBuilderV03::new()),
|
||||||
SpecVersion::V10 => self.attributes = self.attributes.clone().into_v10(),
|
SpecVersion::V10 => EventBinarySerializer::V10(EventBuilderV10::new()),
|
||||||
}
|
})
|
||||||
Ok(self)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn set_attribute(mut self, name: &str, value: MessageAttributeValue) -> Result<Self> {
|
fn set_attribute(mut self, name: &str, value: MessageAttributeValue) -> Result<Self> {
|
||||||
self.attributes.serialize_attribute(name, value)?;
|
match &mut self {
|
||||||
|
EventBinarySerializer::V03(eb) => eb.serialize_attribute(name, value)?,
|
||||||
|
EventBinarySerializer::V10(eb) => eb.serialize_attribute(name, value)?,
|
||||||
|
}
|
||||||
Ok(self)
|
Ok(self)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn set_extension(mut self, name: &str, value: MessageAttributeValue) -> Result<Self> {
|
fn set_extension(self, name: &str, value: MessageAttributeValue) -> Result<Self> {
|
||||||
self.extensions.insert(name.to_string(), value.into());
|
Ok(match self {
|
||||||
Ok(self)
|
EventBinarySerializer::V03(eb) => EventBinarySerializer::V03(eb.extension(name, value)),
|
||||||
|
EventBinarySerializer::V10(eb) => EventBinarySerializer::V10(eb.extension(name, value)),
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn end_with_data(mut self, bytes: Vec<u8>) -> Result<Event> {
|
fn end_with_data(self, bytes: Vec<u8>) -> Result<Event> {
|
||||||
self.data = Some(Data::from_binary(self.get_datacontenttype(), bytes)?);
|
Ok(match self {
|
||||||
Ok(self)
|
EventBinarySerializer::V03(eb) => {
|
||||||
|
eb.data_without_content_type(Data::Binary(bytes)).build()
|
||||||
|
}
|
||||||
|
EventBinarySerializer::V10(eb) => {
|
||||||
|
eb.data_without_content_type(Data::Binary(bytes)).build()
|
||||||
|
}
|
||||||
|
}?)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn end(self) -> Result<Event> {
|
fn end(self) -> Result<Event> {
|
||||||
Ok(self)
|
Ok(match self {
|
||||||
|
EventBinarySerializer::V03(eb) => eb.build(),
|
||||||
|
EventBinarySerializer::V10(eb) => eb.build(),
|
||||||
|
}?)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use crate::message::Error;
|
||||||
|
use crate::test::fixtures;
|
||||||
|
use std::convert::TryInto;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn binary_deserializer_unrecognized_attribute_v03() {
|
||||||
|
assert_eq!(
|
||||||
|
Error::UnknownAttribute {
|
||||||
|
name: "dataschema".to_string()
|
||||||
|
}
|
||||||
|
.to_string(),
|
||||||
|
EventBinarySerializer::new()
|
||||||
|
.set_spec_version(SpecVersion::V03)
|
||||||
|
.unwrap()
|
||||||
|
.set_attribute("dataschema", MessageAttributeValue::Boolean(true))
|
||||||
|
.expect_err("Should return an error")
|
||||||
|
.to_string()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn binary_deserializer_missing_id() {
|
||||||
|
assert_eq!(
|
||||||
|
Error::EventBuilderError {
|
||||||
|
source: crate::event::EventBuilderError::MissingRequiredAttribute {
|
||||||
|
attribute_name: "id"
|
||||||
|
},
|
||||||
|
}
|
||||||
|
.to_string(),
|
||||||
|
EventBinarySerializer::new()
|
||||||
|
.set_spec_version(SpecVersion::V10)
|
||||||
|
.unwrap()
|
||||||
|
.end()
|
||||||
|
.unwrap_err()
|
||||||
|
.to_string()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn binary_deserializer_unrecognized_attribute_v10() {
|
||||||
|
assert_eq!(
|
||||||
|
Error::UnknownAttribute {
|
||||||
|
name: "schemaurl".to_string()
|
||||||
|
}
|
||||||
|
.to_string(),
|
||||||
|
EventBinarySerializer::new()
|
||||||
|
.set_spec_version(SpecVersion::V10)
|
||||||
|
.unwrap()
|
||||||
|
.set_attribute("schemaurl", MessageAttributeValue::Boolean(true))
|
||||||
|
.expect_err("Should return an error")
|
||||||
|
.to_string()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn message_v03_roundtrip_structured() -> Result<()> {
|
||||||
|
assert_eq!(
|
||||||
|
fixtures::v03::full_json_data(),
|
||||||
|
StructuredDeserializer::into_event(fixtures::v03::full_json_data())?
|
||||||
|
);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn message_v03_roundtrip_binary() -> Result<()> {
|
||||||
|
//TODO this code smells because we're missing a proper way in the public APIs
|
||||||
|
// to destructure an event and rebuild it
|
||||||
|
let wanna_be_expected = fixtures::v03::full_json_data();
|
||||||
|
let data: serde_json::Value = wanna_be_expected.data().unwrap().clone().try_into()?;
|
||||||
|
let bytes = serde_json::to_vec(&data)?;
|
||||||
|
let expected = EventBuilderV03::from(wanna_be_expected.clone())
|
||||||
|
.data(wanna_be_expected.datacontenttype().unwrap(), bytes)
|
||||||
|
.build()
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
expected,
|
||||||
|
BinaryDeserializer::into_event(fixtures::v03::full_json_data())?
|
||||||
|
);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn message_v03_msgpack() {
|
||||||
|
let buff = rmp_serde::to_vec(&fixtures::v03::full_json_data()).unwrap();
|
||||||
|
let event = rmp_serde::from_slice::<Event>(buff.as_slice()).unwrap();
|
||||||
|
assert_eq!(event, fixtures::v03::full_json_data(),);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn message_v10_roundtrip_structured() -> Result<()> {
|
||||||
|
assert_eq!(
|
||||||
|
fixtures::v10::full_json_data(),
|
||||||
|
StructuredDeserializer::into_event(fixtures::v10::full_json_data())?
|
||||||
|
);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn message_v10_roundtrip_binary() -> Result<()> {
|
||||||
|
//TODO this code smells because we're missing a proper way in the public APIs
|
||||||
|
// to destructure an event and rebuild it
|
||||||
|
let wanna_be_expected = fixtures::v10::full_json_data();
|
||||||
|
let data: serde_json::Value = wanna_be_expected
|
||||||
|
.data()
|
||||||
|
.cloned()
|
||||||
|
.unwrap()
|
||||||
|
.try_into()
|
||||||
|
.unwrap();
|
||||||
|
let bytes = serde_json::to_vec(&data)?;
|
||||||
|
let expected = EventBuilderV10::from(wanna_be_expected.clone())
|
||||||
|
.data(wanna_be_expected.datacontenttype().unwrap(), bytes)
|
||||||
|
.build()
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
expected,
|
||||||
|
BinaryDeserializer::into_event(fixtures::v10::full_json_data())?
|
||||||
|
);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn message_v10_msgpack() {
|
||||||
|
let buff = rmp_serde::to_vec(&fixtures::v10::full_json_data()).unwrap();
|
||||||
|
let event = rmp_serde::from_slice::<Event>(buff.as_slice()).unwrap();
|
||||||
|
assert_eq!(event, fixtures::v10::full_json_data(),);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
270
src/event/mod.rs
270
src/event/mod.rs
|
@ -1,26 +1,31 @@
|
||||||
|
//! Provides [`Event`] data structure, [`EventBuilder`] and other facilities to work with [`Event`].
|
||||||
|
|
||||||
mod attributes;
|
mod attributes;
|
||||||
mod builder;
|
mod builder;
|
||||||
mod data;
|
mod data;
|
||||||
mod event;
|
|
||||||
mod extensions;
|
mod extensions;
|
||||||
#[macro_use]
|
#[macro_use]
|
||||||
mod format;
|
mod format;
|
||||||
mod message;
|
mod message;
|
||||||
mod spec_version;
|
mod spec_version;
|
||||||
|
mod types;
|
||||||
|
|
||||||
pub use attributes::Attributes;
|
pub use attributes::Attributes;
|
||||||
pub use attributes::{AttributesReader, AttributesWriter};
|
pub use attributes::{AttributeValue, AttributesReader, AttributesWriter};
|
||||||
|
pub use builder::Error as EventBuilderError;
|
||||||
pub use builder::EventBuilder;
|
pub use builder::EventBuilder;
|
||||||
pub use data::Data;
|
pub use data::Data;
|
||||||
pub use event::Event;
|
|
||||||
pub use extensions::ExtensionValue;
|
pub use extensions::ExtensionValue;
|
||||||
pub use spec_version::InvalidSpecVersion;
|
pub(crate) use message::EventBinarySerializer;
|
||||||
|
pub(crate) use message::EventStructuredSerializer;
|
||||||
pub use spec_version::SpecVersion;
|
pub use spec_version::SpecVersion;
|
||||||
pub use spec_version::ATTRIBUTE_NAMES as SPEC_VERSION_ATTRIBUTES;
|
pub use spec_version::UnknownSpecVersion;
|
||||||
|
pub use types::{TryIntoTime, TryIntoUrl, UriReference};
|
||||||
|
|
||||||
mod v03;
|
mod v03;
|
||||||
|
|
||||||
pub use v03::Attributes as AttributesV03;
|
pub use v03::Attributes as AttributesV03;
|
||||||
|
pub(crate) use v03::AttributesIntoIterator as AttributesIntoIteratorV03;
|
||||||
pub use v03::EventBuilder as EventBuilderV03;
|
pub use v03::EventBuilder as EventBuilderV03;
|
||||||
pub(crate) use v03::EventFormatDeserializer as EventFormatDeserializerV03;
|
pub(crate) use v03::EventFormatDeserializer as EventFormatDeserializerV03;
|
||||||
pub(crate) use v03::EventFormatSerializer as EventFormatSerializerV03;
|
pub(crate) use v03::EventFormatSerializer as EventFormatSerializerV03;
|
||||||
|
@ -28,6 +33,261 @@ pub(crate) use v03::EventFormatSerializer as EventFormatSerializerV03;
|
||||||
mod v10;
|
mod v10;
|
||||||
|
|
||||||
pub use v10::Attributes as AttributesV10;
|
pub use v10::Attributes as AttributesV10;
|
||||||
|
pub(crate) use v10::AttributesIntoIterator as AttributesIntoIteratorV10;
|
||||||
pub use v10::EventBuilder as EventBuilderV10;
|
pub use v10::EventBuilder as EventBuilderV10;
|
||||||
pub(crate) use v10::EventFormatDeserializer as EventFormatDeserializerV10;
|
pub(crate) use v10::EventFormatDeserializer as EventFormatDeserializerV10;
|
||||||
pub(crate) use v10::EventFormatSerializer as EventFormatSerializerV10;
|
pub(crate) use v10::EventFormatSerializer as EventFormatSerializerV10;
|
||||||
|
|
||||||
|
use chrono::{DateTime, Utc};
|
||||||
|
use delegate_attr::delegate;
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use std::fmt;
|
||||||
|
use url::Url;
|
||||||
|
|
||||||
|
/// Data structure that represents a [CloudEvent](https://github.com/cloudevents/spec/blob/master/spec.md).
|
||||||
|
/// It provides methods to get the attributes through [`AttributesReader`]
|
||||||
|
/// and write them through [`AttributesWriter`].
|
||||||
|
/// It also provides methods to read and write the [event data](https://github.com/cloudevents/spec/blob/master/spec.md#event-data).
|
||||||
|
///
|
||||||
|
/// You can build events using [`super::EventBuilder`]
|
||||||
|
/// ```
|
||||||
|
/// use cloudevents::*;
|
||||||
|
/// use std::convert::TryInto;
|
||||||
|
///
|
||||||
|
/// # use std::error::Error;
|
||||||
|
/// # fn main() -> Result<(), Box<dyn Error>> {
|
||||||
|
/// // Create an event using the Default trait
|
||||||
|
/// let mut e = Event::default();
|
||||||
|
/// e.set_data(
|
||||||
|
/// "application/json",
|
||||||
|
/// serde_json::json!({"hello": "world"})
|
||||||
|
/// );
|
||||||
|
///
|
||||||
|
/// // Print the event id
|
||||||
|
/// println!("Event id: {}", e.id());
|
||||||
|
///
|
||||||
|
/// // Get the event data
|
||||||
|
/// let data: Option<Data> = e.data().cloned();
|
||||||
|
/// match data {
|
||||||
|
/// Some(d) => println!("{}", d),
|
||||||
|
/// None => println!("No event data")
|
||||||
|
/// }
|
||||||
|
/// # Ok(())
|
||||||
|
/// # }
|
||||||
|
/// ```
|
||||||
|
#[derive(PartialEq, Eq, Debug, Clone)]
|
||||||
|
pub struct Event {
|
||||||
|
pub(crate) attributes: Attributes,
|
||||||
|
pub(crate) data: Option<Data>,
|
||||||
|
pub(crate) extensions: HashMap<String, ExtensionValue>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[delegate(self.attributes)]
|
||||||
|
impl AttributesReader for Event {
|
||||||
|
fn id(&self) -> &str {}
|
||||||
|
fn source(&self) -> &UriReference {}
|
||||||
|
fn specversion(&self) -> SpecVersion {}
|
||||||
|
fn ty(&self) -> &str {}
|
||||||
|
fn datacontenttype(&self) -> Option<&str> {}
|
||||||
|
fn dataschema(&self) -> Option<&Url> {}
|
||||||
|
fn subject(&self) -> Option<&str> {}
|
||||||
|
fn time(&self) -> Option<&DateTime<Utc>> {}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[delegate(self.attributes)]
|
||||||
|
impl AttributesWriter for Event {
|
||||||
|
fn set_id(&mut self, id: impl Into<String>) -> String {}
|
||||||
|
fn set_source(&mut self, source: impl Into<UriReference>) -> UriReference {}
|
||||||
|
fn set_type(&mut self, ty: impl Into<String>) -> String {}
|
||||||
|
fn set_subject(&mut self, subject: Option<impl Into<String>>) -> Option<String> {}
|
||||||
|
fn set_time(&mut self, time: Option<impl Into<DateTime<Utc>>>) -> Option<DateTime<Utc>> {}
|
||||||
|
fn set_datacontenttype(
|
||||||
|
&mut self,
|
||||||
|
datacontenttype: Option<impl Into<String>>,
|
||||||
|
) -> Option<String> {
|
||||||
|
}
|
||||||
|
fn set_dataschema(&mut self, dataschema: Option<impl Into<Url>>) -> Option<Url> {}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for Event {
|
||||||
|
fn default() -> Self {
|
||||||
|
Event {
|
||||||
|
attributes: Attributes::V10(AttributesV10::default()),
|
||||||
|
data: None,
|
||||||
|
extensions: HashMap::default(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Display for Event {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
writeln!(f, "CloudEvent:")?;
|
||||||
|
self.iter()
|
||||||
|
.try_for_each(|(name, val)| writeln!(f, " {}: '{}'", name, val))?;
|
||||||
|
match self.data() {
|
||||||
|
Some(data) => write!(f, " {}", data)?,
|
||||||
|
None => write!(f, " No data")?,
|
||||||
|
}
|
||||||
|
writeln!(f)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Event {
|
||||||
|
/// Returns an [`Iterator`] for all the available [CloudEvents Context attributes](https://github.com/cloudevents/spec/blob/master/spec.md#context-attributes) and extensions.
|
||||||
|
/// Same as chaining [`Event::iter_attributes()`] and [`Event::iter_extensions()`]
|
||||||
|
pub fn iter(&self) -> impl Iterator<Item = (&str, AttributeValue)> {
|
||||||
|
self.iter_attributes()
|
||||||
|
.chain(self.extensions.iter().map(|(k, v)| (k.as_str(), v.into())))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns an [`Iterator`] for all the available [CloudEvents Context attributes](https://github.com/cloudevents/spec/blob/master/spec.md#context-attributes), excluding extensions.
|
||||||
|
/// This iterator does not contain the `data` field.
|
||||||
|
pub fn iter_attributes(&self) -> impl Iterator<Item = (&str, AttributeValue)> {
|
||||||
|
self.attributes.iter()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get all the [extensions](https://github.com/cloudevents/spec/blob/master/spec.md#extension-context-attributes)
|
||||||
|
pub fn iter_extensions(&self) -> impl Iterator<Item = (&str, &ExtensionValue)> {
|
||||||
|
self.extensions.iter().map(|(k, v)| (k.as_str(), v))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get `data` from this `Event`
|
||||||
|
pub fn data(&self) -> Option<&Data> {
|
||||||
|
self.data.as_ref()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Take (`datacontenttype`, `dataschema`, `data`) from this event, leaving these fields empty
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// use cloudevents::Event;
|
||||||
|
/// use serde_json::json;
|
||||||
|
/// use std::convert::Into;
|
||||||
|
///
|
||||||
|
/// let mut e = Event::default();
|
||||||
|
/// e.set_data("application/json", json!({}));
|
||||||
|
///
|
||||||
|
/// let (datacontenttype, dataschema, data) = e.take_data();
|
||||||
|
/// ```
|
||||||
|
pub fn take_data(&mut self) -> (Option<String>, Option<Url>, Option<Data>) {
|
||||||
|
(
|
||||||
|
self.attributes.set_datacontenttype(None as Option<String>),
|
||||||
|
self.attributes.set_dataschema(None as Option<Url>),
|
||||||
|
self.data.take(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set `data` into this `Event` with the specified `datacontenttype`.
|
||||||
|
/// Returns the previous value of `datacontenttype` and `data`.
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// use cloudevents::Event;
|
||||||
|
/// use serde_json::json;
|
||||||
|
/// use std::convert::Into;
|
||||||
|
///
|
||||||
|
/// let mut e = Event::default();
|
||||||
|
/// let (old_datacontenttype, old_data) = e.set_data("application/json", json!({}));
|
||||||
|
/// ```
|
||||||
|
pub fn set_data(
|
||||||
|
&mut self,
|
||||||
|
datacontenttype: impl Into<String>,
|
||||||
|
data: impl Into<Data>,
|
||||||
|
) -> (Option<String>, Option<Data>) {
|
||||||
|
(
|
||||||
|
self.attributes.set_datacontenttype(Some(datacontenttype)),
|
||||||
|
std::mem::replace(&mut self.data, Some(data.into())),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set `data` into this `Event`, without checking if there is a `datacontenttype`.
|
||||||
|
/// Returns the previous value of `data`.
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// use cloudevents::Event;
|
||||||
|
/// use serde_json::json;
|
||||||
|
/// use std::convert::Into;
|
||||||
|
///
|
||||||
|
/// let mut e = Event::default();
|
||||||
|
/// let old_data = e.set_data_unchecked(json!({}));
|
||||||
|
/// ```
|
||||||
|
pub fn set_data_unchecked(&mut self, data: impl Into<Data>) -> Option<Data> {
|
||||||
|
std::mem::replace(&mut self.data, Some(data.into()))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the [extension](https://github.com/cloudevents/spec/blob/master/spec.md#extension-context-attributes) named `extension_name`
|
||||||
|
pub fn extension(&self, extension_name: &str) -> Option<&ExtensionValue> {
|
||||||
|
self.extensions.get(extension_name)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set the [extension](https://github.com/cloudevents/spec/blob/master/spec.md#extension-context-attributes) named `extension_name` with `extension_value`
|
||||||
|
pub fn set_extension<'name, 'event: 'name>(
|
||||||
|
&'event mut self,
|
||||||
|
extension_name: &'name str,
|
||||||
|
extension_value: impl Into<ExtensionValue>,
|
||||||
|
) {
|
||||||
|
self.extensions
|
||||||
|
.insert(extension_name.to_owned(), extension_value.into());
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Remove the [extension](https://github.com/cloudevents/spec/blob/master/spec.md#extension-context-attributes) named `extension_name`
|
||||||
|
pub fn remove_extension<'name, 'event: 'name>(
|
||||||
|
&'event mut self,
|
||||||
|
extension_name: &'name str,
|
||||||
|
) -> Option<ExtensionValue> {
|
||||||
|
self.extensions.remove(extension_name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn take_data() {
|
||||||
|
let mut e = Event::default();
|
||||||
|
e.set_data(
|
||||||
|
"application/json",
|
||||||
|
serde_json::json!({
|
||||||
|
"hello": "world"
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
let (datacontenttype, dataschema, data) = e.take_data();
|
||||||
|
|
||||||
|
assert!(datacontenttype.is_some());
|
||||||
|
assert!(dataschema.is_none());
|
||||||
|
assert!(data.is_some());
|
||||||
|
|
||||||
|
assert!(e.data().is_none());
|
||||||
|
assert!(e.dataschema().is_none());
|
||||||
|
assert!(e.datacontenttype().is_none());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn set_id() {
|
||||||
|
let mut e = Event::default();
|
||||||
|
e.set_id("001");
|
||||||
|
|
||||||
|
assert_eq!(e.set_id("002"), String::from("001"));
|
||||||
|
assert_eq!(e.id(), "002")
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn iter() {
|
||||||
|
let mut e = Event::default();
|
||||||
|
e.set_extension("aaa", "bbb");
|
||||||
|
e.set_data(
|
||||||
|
"application/json",
|
||||||
|
serde_json::json!({
|
||||||
|
"hello": "world"
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
let mut v: HashMap<&str, AttributeValue> = e.iter().collect();
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
v.remove("specversion"),
|
||||||
|
Some(AttributeValue::SpecVersion(SpecVersion::V10))
|
||||||
|
);
|
||||||
|
assert_eq!(v.remove("aaa"), Some(AttributeValue::String("bbb")))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -1,36 +1,37 @@
|
||||||
use super::{v03, v10};
|
use super::{v03, v10};
|
||||||
use lazy_static::lazy_static;
|
|
||||||
use serde::export::Formatter;
|
|
||||||
use std::collections::HashMap;
|
|
||||||
use std::convert::TryFrom;
|
use std::convert::TryFrom;
|
||||||
use std::fmt;
|
use std::fmt;
|
||||||
|
use std::fmt::Formatter;
|
||||||
|
|
||||||
lazy_static! {
|
pub(crate) const SPEC_VERSIONS: [&str; 2] = ["0.3", "1.0"];
|
||||||
/// Lazily initialized map that contains all the context attribute names per [`SpecVersion`]
|
|
||||||
pub static ref ATTRIBUTE_NAMES: HashMap<SpecVersion, &'static [&'static str]> = {
|
|
||||||
let mut m = HashMap::new();
|
|
||||||
m.insert(SpecVersion::V03, &v03::ATTRIBUTE_NAMES[..]);
|
|
||||||
m.insert(SpecVersion::V10, &v10::ATTRIBUTE_NAMES[..]);
|
|
||||||
m
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) const SPEC_VERSIONS: [&'static str; 2] = ["0.3", "1.0"];
|
/// CloudEvent specification version.
|
||||||
|
|
||||||
/// CloudEvent specification version
|
|
||||||
#[derive(PartialEq, Eq, Hash, Debug, Clone)]
|
#[derive(PartialEq, Eq, Hash, Debug, Clone)]
|
||||||
pub enum SpecVersion {
|
pub enum SpecVersion {
|
||||||
|
/// CloudEvents v0.3
|
||||||
V03,
|
V03,
|
||||||
|
/// CloudEvents v1.0
|
||||||
V10,
|
V10,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl SpecVersion {
|
impl SpecVersion {
|
||||||
|
/// Returns the string representation of [`SpecVersion`].
|
||||||
|
#[inline]
|
||||||
pub fn as_str(&self) -> &str {
|
pub fn as_str(&self) -> &str {
|
||||||
match self {
|
match self {
|
||||||
SpecVersion::V03 => "0.3",
|
SpecVersion::V03 => "0.3",
|
||||||
SpecVersion::V10 => "1.0",
|
SpecVersion::V10 => "1.0",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Get all attribute names for this [`SpecVersion`].
|
||||||
|
#[inline]
|
||||||
|
pub fn attribute_names(&self) -> &'static [&'static str] {
|
||||||
|
match self {
|
||||||
|
SpecVersion::V03 => &v03::ATTRIBUTE_NAMES,
|
||||||
|
SpecVersion::V10 => &v10::ATTRIBUTE_NAMES,
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl fmt::Display for SpecVersion {
|
impl fmt::Display for SpecVersion {
|
||||||
|
@ -39,28 +40,28 @@ impl fmt::Display for SpecVersion {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Error representing an invalid [`SpecVersion`] string identifier
|
/// Error representing an unknown [`SpecVersion`] string identifier
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct InvalidSpecVersion {
|
pub struct UnknownSpecVersion {
|
||||||
spec_version_value: String,
|
spec_version_value: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl fmt::Display for InvalidSpecVersion {
|
impl fmt::Display for UnknownSpecVersion {
|
||||||
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
|
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
|
||||||
write!(f, "Invalid specversion {}", self.spec_version_value)
|
write!(f, "Invalid specversion {}", self.spec_version_value)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl std::error::Error for InvalidSpecVersion {}
|
impl std::error::Error for UnknownSpecVersion {}
|
||||||
|
|
||||||
impl TryFrom<&str> for SpecVersion {
|
impl TryFrom<&str> for SpecVersion {
|
||||||
type Error = InvalidSpecVersion;
|
type Error = UnknownSpecVersion;
|
||||||
|
|
||||||
fn try_from(value: &str) -> Result<Self, InvalidSpecVersion> {
|
fn try_from(value: &str) -> Result<Self, UnknownSpecVersion> {
|
||||||
match value {
|
match value {
|
||||||
"0.3" => Ok(SpecVersion::V03),
|
"0.3" => Ok(SpecVersion::V03),
|
||||||
"1.0" => Ok(SpecVersion::V10),
|
"1.0" => Ok(SpecVersion::V10),
|
||||||
_ => Err(InvalidSpecVersion {
|
_ => Err(UnknownSpecVersion {
|
||||||
spec_version_value: value.to_string(),
|
spec_version_value: value.to_string(),
|
||||||
}),
|
}),
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,60 @@
|
||||||
|
use chrono::{DateTime, Utc};
|
||||||
|
use url::Url;
|
||||||
|
|
||||||
|
/// Trait to define conversion to [`Url`]
|
||||||
|
pub trait TryIntoUrl {
|
||||||
|
fn into_url(self) -> Result<Url, url::ParseError>;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TryIntoUrl for Url {
|
||||||
|
fn into_url(self) -> Result<Url, url::ParseError> {
|
||||||
|
Ok(self)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TryIntoUrl for &str {
|
||||||
|
fn into_url(self) -> Result<Url, url::ParseError> {
|
||||||
|
Url::parse(self)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TryIntoUrl for String {
|
||||||
|
fn into_url(self) -> Result<Url, url::ParseError> {
|
||||||
|
self.as_str().into_url()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Trait to define conversion to [`DateTime`]
|
||||||
|
pub trait TryIntoTime {
|
||||||
|
fn into_time(self) -> Result<DateTime<Utc>, chrono::ParseError>;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TryIntoTime for DateTime<Utc> {
|
||||||
|
fn into_time(self) -> Result<DateTime<Utc>, chrono::ParseError> {
|
||||||
|
Ok(self)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TryIntoTime for &str {
|
||||||
|
fn into_time(self) -> Result<DateTime<Utc>, chrono::ParseError> {
|
||||||
|
Ok(DateTime::<Utc>::from(DateTime::parse_from_rfc3339(self)?))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TryIntoTime for String {
|
||||||
|
fn into_time(self) -> Result<DateTime<Utc>, chrono::ParseError> {
|
||||||
|
self.as_str().into_time()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The URI-reference type.
|
||||||
|
///
|
||||||
|
/// The URI reference can be a URI, or just a relative path.
|
||||||
|
///
|
||||||
|
/// As the [`url::Url`] type can only represent an absolute URL, we are falling back to a string
|
||||||
|
/// here.
|
||||||
|
///
|
||||||
|
/// Also see:
|
||||||
|
/// * <https://github.com/cloudevents/spec/blob/v1.0.1/spec.md#type-system>
|
||||||
|
/// * <https://tools.ietf.org/html/rfc3986#section-4.1>
|
||||||
|
pub type UriReference = String;
|
|
@ -1,13 +1,11 @@
|
||||||
use crate::event::attributes::{
|
use crate::event::attributes::{default_hostname, AttributeValue, AttributesConverter};
|
||||||
default_hostname, AttributeValue, AttributesConverter, DataAttributesWriter,
|
use crate::event::{AttributesReader, AttributesV10, AttributesWriter, SpecVersion, UriReference};
|
||||||
};
|
use crate::message::{BinarySerializer, MessageAttributeValue};
|
||||||
use crate::event::AttributesV10;
|
|
||||||
use crate::event::{AttributesReader, AttributesWriter, SpecVersion};
|
|
||||||
use chrono::{DateTime, Utc};
|
use chrono::{DateTime, Utc};
|
||||||
use url::Url;
|
use url::Url;
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
pub(crate) const ATTRIBUTE_NAMES: [&'static str; 8] = [
|
pub(crate) const ATTRIBUTE_NAMES: [&str; 8] = [
|
||||||
"specversion",
|
"specversion",
|
||||||
"id",
|
"id",
|
||||||
"type",
|
"type",
|
||||||
|
@ -19,11 +17,11 @@ pub(crate) const ATTRIBUTE_NAMES: [&'static str; 8] = [
|
||||||
];
|
];
|
||||||
|
|
||||||
/// Data structure representing [CloudEvents V0.3 context attributes](https://github.com/cloudevents/spec/blob/v0.3/spec.md#context-attributes)
|
/// Data structure representing [CloudEvents V0.3 context attributes](https://github.com/cloudevents/spec/blob/v0.3/spec.md#context-attributes)
|
||||||
#[derive(PartialEq, Debug, Clone)]
|
#[derive(PartialEq, Eq, Debug, Clone)]
|
||||||
pub struct Attributes {
|
pub struct Attributes {
|
||||||
pub(crate) id: String,
|
pub(crate) id: String,
|
||||||
pub(crate) ty: String,
|
pub(crate) ty: String,
|
||||||
pub(crate) source: Url,
|
pub(crate) source: UriReference,
|
||||||
pub(crate) datacontenttype: Option<String>,
|
pub(crate) datacontenttype: Option<String>,
|
||||||
pub(crate) schemaurl: Option<Url>,
|
pub(crate) schemaurl: Option<Url>,
|
||||||
pub(crate) subject: Option<String>,
|
pub(crate) subject: Option<String>,
|
||||||
|
@ -42,34 +40,36 @@ impl<'a> IntoIterator for &'a Attributes {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(PartialEq, Debug, Clone, Copy)]
|
||||||
pub struct AttributesIntoIterator<'a> {
|
pub struct AttributesIntoIterator<'a> {
|
||||||
attributes: &'a Attributes,
|
pub(crate) attributes: &'a Attributes,
|
||||||
index: usize,
|
pub(crate) index: usize,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> Iterator for AttributesIntoIterator<'a> {
|
impl<'a> Iterator for AttributesIntoIterator<'a> {
|
||||||
type Item = (&'a str, AttributeValue<'a>);
|
type Item = (&'a str, AttributeValue<'a>);
|
||||||
fn next(&mut self) -> Option<Self::Item> {
|
fn next(&mut self) -> Option<Self::Item> {
|
||||||
let result = match self.index {
|
let result = match self.index {
|
||||||
0 => Some(("id", AttributeValue::String(&self.attributes.id))),
|
0 => Some(("specversion", AttributeValue::SpecVersion(SpecVersion::V03))),
|
||||||
1 => Some(("type", AttributeValue::String(&self.attributes.ty))),
|
1 => Some(("id", AttributeValue::String(&self.attributes.id))),
|
||||||
2 => Some(("source", AttributeValue::URIRef(&self.attributes.source))),
|
2 => Some(("type", AttributeValue::String(&self.attributes.ty))),
|
||||||
3 => self
|
3 => Some(("source", AttributeValue::URIRef(&self.attributes.source))),
|
||||||
|
4 => self
|
||||||
.attributes
|
.attributes
|
||||||
.datacontenttype
|
.datacontenttype
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.map(|v| ("datacontenttype", AttributeValue::String(v))),
|
.map(|v| ("datacontenttype", AttributeValue::String(v))),
|
||||||
4 => self
|
5 => self
|
||||||
.attributes
|
.attributes
|
||||||
.schemaurl
|
.schemaurl
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.map(|v| ("schemaurl", AttributeValue::URIRef(v))),
|
.map(|v| ("schemaurl", AttributeValue::URI(v))),
|
||||||
5 => self
|
6 => self
|
||||||
.attributes
|
.attributes
|
||||||
.subject
|
.subject
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.map(|v| ("subject", AttributeValue::String(v))),
|
.map(|v| ("subject", AttributeValue::String(v))),
|
||||||
6 => self
|
7 => self
|
||||||
.attributes
|
.attributes
|
||||||
.time
|
.time
|
||||||
.as_ref()
|
.as_ref()
|
||||||
|
@ -85,68 +85,69 @@ impl<'a> Iterator for AttributesIntoIterator<'a> {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl AttributesReader for Attributes {
|
impl AttributesReader for Attributes {
|
||||||
fn get_id(&self) -> &str {
|
fn id(&self) -> &str {
|
||||||
&self.id
|
&self.id
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_source(&self) -> &Url {
|
fn source(&self) -> &UriReference {
|
||||||
&self.source
|
&self.source
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_specversion(&self) -> SpecVersion {
|
fn specversion(&self) -> SpecVersion {
|
||||||
SpecVersion::V03
|
SpecVersion::V03
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_type(&self) -> &str {
|
fn ty(&self) -> &str {
|
||||||
&self.ty
|
&self.ty
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_datacontenttype(&self) -> Option<&str> {
|
fn datacontenttype(&self) -> Option<&str> {
|
||||||
self.datacontenttype.as_deref()
|
self.datacontenttype.as_deref()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_dataschema(&self) -> Option<&Url> {
|
fn dataschema(&self) -> Option<&Url> {
|
||||||
self.schemaurl.as_ref()
|
self.schemaurl.as_ref()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_subject(&self) -> Option<&str> {
|
fn subject(&self) -> Option<&str> {
|
||||||
self.subject.as_deref()
|
self.subject.as_deref()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_time(&self) -> Option<&DateTime<Utc>> {
|
fn time(&self) -> Option<&DateTime<Utc>> {
|
||||||
self.time.as_ref()
|
self.time.as_ref()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl AttributesWriter for Attributes {
|
impl AttributesWriter for Attributes {
|
||||||
fn set_id(&mut self, id: impl Into<String>) {
|
fn set_id(&mut self, id: impl Into<String>) -> String {
|
||||||
self.id = id.into()
|
std::mem::replace(&mut self.id, id.into())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn set_source(&mut self, source: impl Into<Url>) {
|
fn set_source(&mut self, source: impl Into<UriReference>) -> UriReference {
|
||||||
self.source = source.into()
|
std::mem::replace(&mut self.source, source.into())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn set_type(&mut self, ty: impl Into<String>) {
|
fn set_type(&mut self, ty: impl Into<String>) -> String {
|
||||||
self.ty = ty.into()
|
std::mem::replace(&mut self.ty, ty.into())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn set_subject(&mut self, subject: Option<impl Into<String>>) {
|
fn set_subject(&mut self, subject: Option<impl Into<String>>) -> Option<String> {
|
||||||
self.subject = subject.map(Into::into)
|
std::mem::replace(&mut self.subject, subject.map(Into::into))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn set_time(&mut self, time: Option<impl Into<DateTime<Utc>>>) {
|
fn set_time(&mut self, time: Option<impl Into<DateTime<Utc>>>) -> Option<DateTime<Utc>> {
|
||||||
self.time = time.map(Into::into)
|
std::mem::replace(&mut self.time, time.map(Into::into))
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl DataAttributesWriter for Attributes {
|
|
||||||
fn set_datacontenttype(&mut self, datacontenttype: Option<impl Into<String>>) {
|
|
||||||
self.datacontenttype = datacontenttype.map(Into::into)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn set_dataschema(&mut self, dataschema: Option<impl Into<Url>>) {
|
fn set_datacontenttype(
|
||||||
self.schemaurl = dataschema.map(Into::into)
|
&mut self,
|
||||||
|
datacontenttype: Option<impl Into<String>>,
|
||||||
|
) -> Option<String> {
|
||||||
|
std::mem::replace(&mut self.datacontenttype, datacontenttype.map(Into::into))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_dataschema(&mut self, dataschema: Option<impl Into<Url>>) -> Option<Url> {
|
||||||
|
std::mem::replace(&mut self.schemaurl, dataschema.map(Into::into))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -155,11 +156,11 @@ impl Default for Attributes {
|
||||||
Attributes {
|
Attributes {
|
||||||
id: Uuid::new_v4().to_string(),
|
id: Uuid::new_v4().to_string(),
|
||||||
ty: "type".to_string(),
|
ty: "type".to_string(),
|
||||||
source: default_hostname(),
|
source: default_hostname().to_string(),
|
||||||
datacontenttype: None,
|
datacontenttype: None,
|
||||||
schemaurl: None,
|
schemaurl: None,
|
||||||
subject: None,
|
subject: None,
|
||||||
time: None,
|
time: Some(Utc::now()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -182,28 +183,75 @@ impl AttributesConverter for Attributes {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl crate::event::message::AttributesDeserializer for super::Attributes {
|
||||||
|
fn deserialize_attributes<R: Sized, V: BinarySerializer<R>>(
|
||||||
|
self,
|
||||||
|
mut visitor: V,
|
||||||
|
) -> crate::message::Result<V> {
|
||||||
|
visitor = visitor.set_attribute("id", MessageAttributeValue::String(self.id))?;
|
||||||
|
visitor = visitor.set_attribute("type", MessageAttributeValue::String(self.ty))?;
|
||||||
|
visitor = visitor.set_attribute("source", MessageAttributeValue::UriRef(self.source))?;
|
||||||
|
if self.datacontenttype.is_some() {
|
||||||
|
visitor = visitor.set_attribute(
|
||||||
|
"datacontenttype",
|
||||||
|
MessageAttributeValue::String(self.datacontenttype.unwrap()),
|
||||||
|
)?;
|
||||||
|
}
|
||||||
|
if self.schemaurl.is_some() {
|
||||||
|
visitor = visitor.set_attribute(
|
||||||
|
"schemaurl",
|
||||||
|
MessageAttributeValue::Uri(self.schemaurl.unwrap()),
|
||||||
|
)?;
|
||||||
|
}
|
||||||
|
if self.subject.is_some() {
|
||||||
|
visitor = visitor.set_attribute(
|
||||||
|
"subject",
|
||||||
|
MessageAttributeValue::String(self.subject.unwrap()),
|
||||||
|
)?;
|
||||||
|
}
|
||||||
|
if self.time.is_some() {
|
||||||
|
visitor = visitor
|
||||||
|
.set_attribute("time", MessageAttributeValue::DateTime(self.time.unwrap()))?;
|
||||||
|
}
|
||||||
|
Ok(visitor)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
use chrono::NaiveDateTime;
|
use crate::test::fixtures;
|
||||||
|
use chrono::DateTime;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn iter_v03_test() {
|
||||||
|
let in_event = fixtures::v03::full_json_data();
|
||||||
|
let mut iter_v03 = in_event.iter_attributes();
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
("specversion", AttributeValue::SpecVersion(SpecVersion::V03)),
|
||||||
|
iter_v03.next().unwrap()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn iterator_test_v03() {
|
fn iterator_test_v03() {
|
||||||
let a = Attributes {
|
let a = Attributes {
|
||||||
id: String::from("1"),
|
id: String::from("1"),
|
||||||
ty: String::from("someType"),
|
ty: String::from("someType"),
|
||||||
source: Url::parse("https://example.net").unwrap(),
|
source: "https://example.net".into(),
|
||||||
datacontenttype: None,
|
datacontenttype: None,
|
||||||
schemaurl: None,
|
schemaurl: None,
|
||||||
subject: None,
|
subject: None,
|
||||||
time: Some(DateTime::<Utc>::from_utc(
|
time: DateTime::from_timestamp(61, 0),
|
||||||
NaiveDateTime::from_timestamp(61, 0),
|
|
||||||
Utc,
|
|
||||||
)),
|
|
||||||
};
|
};
|
||||||
let b = &mut a.into_iter();
|
let b = &mut a.into_iter();
|
||||||
let time = DateTime::<Utc>::from_utc(NaiveDateTime::from_timestamp(61, 0), Utc);
|
let time = DateTime::from_timestamp(61, 0).unwrap();
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
("specversion", AttributeValue::SpecVersion(SpecVersion::V03)),
|
||||||
|
b.next().unwrap()
|
||||||
|
);
|
||||||
assert_eq!(("id", AttributeValue::String("1")), b.next().unwrap());
|
assert_eq!(("id", AttributeValue::String("1")), b.next().unwrap());
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
("type", AttributeValue::String("someType")),
|
("type", AttributeValue::String("someType")),
|
||||||
|
@ -212,7 +260,7 @@ mod tests {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
(
|
(
|
||||||
"source",
|
"source",
|
||||||
AttributeValue::URIRef(&Url::parse("https://example.net").unwrap())
|
AttributeValue::URIRef(&"https://example.net".to_string())
|
||||||
),
|
),
|
||||||
b.next().unwrap()
|
b.next().unwrap()
|
||||||
);
|
);
|
||||||
|
|
|
@ -1,58 +1,68 @@
|
||||||
use super::Attributes as AttributesV03;
|
use super::Attributes as AttributesV03;
|
||||||
use crate::event::{Attributes, AttributesWriter, Data, Event, ExtensionValue};
|
use crate::event::{
|
||||||
|
Attributes, Data, Event, EventBuilderError, ExtensionValue, TryIntoTime, TryIntoUrl,
|
||||||
|
UriReference,
|
||||||
|
};
|
||||||
|
use crate::message::MessageAttributeValue;
|
||||||
use chrono::{DateTime, Utc};
|
use chrono::{DateTime, Utc};
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
use std::convert::TryInto;
|
||||||
use url::Url;
|
use url::Url;
|
||||||
|
|
||||||
/// Builder to create a CloudEvent V0.3
|
/// Builder to create a CloudEvent V0.3
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
pub struct EventBuilder {
|
pub struct EventBuilder {
|
||||||
event: Event,
|
id: Option<String>,
|
||||||
|
ty: Option<String>,
|
||||||
|
source: Option<UriReference>,
|
||||||
|
datacontenttype: Option<String>,
|
||||||
|
schemaurl: Option<Url>,
|
||||||
|
subject: Option<String>,
|
||||||
|
time: Option<DateTime<Utc>>,
|
||||||
|
data: Option<Data>,
|
||||||
|
extensions: HashMap<String, ExtensionValue>,
|
||||||
|
error: Option<EventBuilderError>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl EventBuilder {
|
impl EventBuilder {
|
||||||
pub fn from(event: Event) -> Self {
|
|
||||||
EventBuilder {
|
|
||||||
event: Event {
|
|
||||||
attributes: event.attributes.into_v03(),
|
|
||||||
data: event.data,
|
|
||||||
extensions: event.extensions,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn new() -> Self {
|
|
||||||
EventBuilder {
|
|
||||||
event: Event {
|
|
||||||
attributes: Attributes::V03(AttributesV03::default()),
|
|
||||||
data: None,
|
|
||||||
extensions: HashMap::new(),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn id(mut self, id: impl Into<String>) -> Self {
|
pub fn id(mut self, id: impl Into<String>) -> Self {
|
||||||
self.event.set_id(id);
|
self.id = Some(id.into());
|
||||||
return self;
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn source(mut self, source: impl Into<Url>) -> Self {
|
pub fn source(mut self, source: impl Into<String>) -> Self {
|
||||||
self.event.set_source(source);
|
let source = source.into();
|
||||||
return self;
|
if source.is_empty() {
|
||||||
|
self.error = Some(EventBuilderError::InvalidUriRefError {
|
||||||
|
attribute_name: "source",
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
self.source = Some(source);
|
||||||
|
}
|
||||||
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn ty(mut self, ty: impl Into<String>) -> Self {
|
pub fn ty(mut self, ty: impl Into<String>) -> Self {
|
||||||
self.event.set_type(ty);
|
self.ty = Some(ty.into());
|
||||||
return self;
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn subject(mut self, subject: impl Into<String>) -> Self {
|
pub fn subject(mut self, subject: impl Into<String>) -> Self {
|
||||||
self.event.set_subject(Some(subject));
|
self.subject = Some(subject.into());
|
||||||
return self;
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn time(mut self, time: impl Into<DateTime<Utc>>) -> Self {
|
pub fn time(mut self, time: impl TryIntoTime) -> Self {
|
||||||
self.event.set_time(Some(time));
|
match time.into_time() {
|
||||||
return self;
|
Ok(u) => self.time = Some(u),
|
||||||
|
Err(e) => {
|
||||||
|
self.error = Some(EventBuilderError::ParseTimeError {
|
||||||
|
attribute_name: "time",
|
||||||
|
source: e,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
};
|
||||||
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn extension(
|
pub fn extension(
|
||||||
|
@ -60,40 +70,158 @@ impl EventBuilder {
|
||||||
extension_name: &str,
|
extension_name: &str,
|
||||||
extension_value: impl Into<ExtensionValue>,
|
extension_value: impl Into<ExtensionValue>,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
self.event.set_extension(extension_name, extension_value);
|
self.extensions
|
||||||
return self;
|
.insert(extension_name.to_owned(), extension_value.into());
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn data_without_content_type(mut self, data: impl Into<Data>) -> Self {
|
||||||
|
self.data = Some(data.into());
|
||||||
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn data(mut self, datacontenttype: impl Into<String>, data: impl Into<Data>) -> Self {
|
pub fn data(mut self, datacontenttype: impl Into<String>, data: impl Into<Data>) -> Self {
|
||||||
self.event.write_data(datacontenttype, data);
|
self.datacontenttype = Some(datacontenttype.into());
|
||||||
return self;
|
self.data = Some(data.into());
|
||||||
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn data_with_schema(
|
pub fn data_with_schema(
|
||||||
mut self,
|
mut self,
|
||||||
datacontenttype: impl Into<String>,
|
datacontenttype: impl Into<String>,
|
||||||
schemaurl: impl Into<Url>,
|
schemaurl: impl TryIntoUrl,
|
||||||
data: impl Into<Data>,
|
data: impl Into<Data>,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
self.event
|
self.datacontenttype = Some(datacontenttype.into());
|
||||||
.write_data_with_schema(datacontenttype, schemaurl, data);
|
match schemaurl.into_url() {
|
||||||
return self;
|
Ok(u) => self.schemaurl = Some(u),
|
||||||
|
Err(e) => {
|
||||||
|
self.error = Some(EventBuilderError::ParseUrlError {
|
||||||
|
attribute_name: "schemaurl",
|
||||||
|
source: e,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
};
|
||||||
|
self.data = Some(data.into());
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<Event> for EventBuilder {
|
||||||
|
fn from(event: Event) -> Self {
|
||||||
|
let attributes = match event.attributes.into_v03() {
|
||||||
|
Attributes::V03(attr) => attr,
|
||||||
|
// This branch is unreachable because into_v03() returns
|
||||||
|
// always a Attributes::V03
|
||||||
|
_ => unreachable!(),
|
||||||
|
};
|
||||||
|
|
||||||
|
EventBuilder {
|
||||||
|
id: Some(attributes.id),
|
||||||
|
ty: Some(attributes.ty),
|
||||||
|
source: Some(attributes.source),
|
||||||
|
datacontenttype: attributes.datacontenttype,
|
||||||
|
schemaurl: attributes.schemaurl,
|
||||||
|
subject: attributes.subject,
|
||||||
|
time: attributes.time,
|
||||||
|
data: event.data,
|
||||||
|
extensions: event.extensions,
|
||||||
|
error: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for EventBuilder {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::from(Event::default())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl crate::event::builder::EventBuilder for EventBuilder {
|
||||||
|
fn new() -> Self {
|
||||||
|
EventBuilder {
|
||||||
|
id: None,
|
||||||
|
ty: None,
|
||||||
|
source: None,
|
||||||
|
datacontenttype: None,
|
||||||
|
schemaurl: None,
|
||||||
|
subject: None,
|
||||||
|
time: None,
|
||||||
|
data: None,
|
||||||
|
extensions: Default::default(),
|
||||||
|
error: None,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn build(self) -> Event {
|
fn build(self) -> Result<Event, EventBuilderError> {
|
||||||
self.event
|
match self.error {
|
||||||
|
Some(e) => Err(e),
|
||||||
|
None => Ok(Event {
|
||||||
|
attributes: Attributes::V03(AttributesV03 {
|
||||||
|
id: self.id.ok_or(EventBuilderError::MissingRequiredAttribute {
|
||||||
|
attribute_name: "id",
|
||||||
|
})?,
|
||||||
|
ty: self.ty.ok_or(EventBuilderError::MissingRequiredAttribute {
|
||||||
|
attribute_name: "type",
|
||||||
|
})?,
|
||||||
|
source: self
|
||||||
|
.source
|
||||||
|
.ok_or(EventBuilderError::MissingRequiredAttribute {
|
||||||
|
attribute_name: "source",
|
||||||
|
})?,
|
||||||
|
datacontenttype: self.datacontenttype,
|
||||||
|
schemaurl: self.schemaurl,
|
||||||
|
subject: self.subject,
|
||||||
|
time: self.time,
|
||||||
|
}),
|
||||||
|
data: self.data,
|
||||||
|
extensions: self.extensions,
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl crate::event::message::AttributesSerializer for EventBuilder {
|
||||||
|
fn serialize_attribute(
|
||||||
|
&mut self,
|
||||||
|
name: &str,
|
||||||
|
value: MessageAttributeValue,
|
||||||
|
) -> crate::message::Result<()> {
|
||||||
|
match name {
|
||||||
|
"id" => self.id = Some(value.to_string()),
|
||||||
|
"type" => self.ty = Some(value.to_string()),
|
||||||
|
"source" => self.source = Some(value.to_string()),
|
||||||
|
"datacontenttype" => self.datacontenttype = Some(value.to_string()),
|
||||||
|
"schemaurl" => self.schemaurl = Some(value.try_into()?),
|
||||||
|
"subject" => self.subject = Some(value.to_string()),
|
||||||
|
"time" => self.time = Some(value.try_into()?),
|
||||||
|
_ => {
|
||||||
|
return Err(crate::message::Error::UnknownAttribute {
|
||||||
|
name: name.to_string(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
};
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
|
||||||
use crate::event::{AttributesReader, SpecVersion};
|
use crate::assert_match_pattern;
|
||||||
|
use chrono::{DateTime, Utc};
|
||||||
|
|
||||||
|
use crate::event::{
|
||||||
|
AttributesReader, EventBuilder, EventBuilderError, ExtensionValue, SpecVersion,
|
||||||
|
};
|
||||||
|
use crate::EventBuilderV03;
|
||||||
|
use std::convert::TryInto;
|
||||||
|
use url::Url;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn build_event() {
|
fn build_event() {
|
||||||
let id = "aaa";
|
let id = "aaa";
|
||||||
let source = Url::parse("http://localhost:8080").unwrap();
|
let source = "http://localhost:8080";
|
||||||
let ty = "bbb";
|
let ty = "bbb";
|
||||||
let subject = "francesco";
|
let subject = "francesco";
|
||||||
let time: DateTime<Utc> = Utc::now();
|
let time: DateTime<Utc> = Utc::now();
|
||||||
|
@ -105,30 +233,71 @@ mod tests {
|
||||||
"hello": "world"
|
"hello": "world"
|
||||||
});
|
});
|
||||||
|
|
||||||
let event = EventBuilder::new()
|
let mut event = EventBuilderV03::new()
|
||||||
.id(id)
|
.id(id)
|
||||||
.source(source.clone())
|
.source(source.to_string())
|
||||||
.ty(ty)
|
.ty(ty)
|
||||||
.subject(subject)
|
.subject(subject)
|
||||||
.time(time)
|
.time(time)
|
||||||
.extension(extension_name, extension_value)
|
.extension(extension_name, extension_value)
|
||||||
.data_with_schema(content_type, schema.clone(), data.clone())
|
.data_with_schema(content_type, schema.clone(), data.clone())
|
||||||
.build();
|
.build()
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
assert_eq!(SpecVersion::V03, event.get_specversion());
|
assert_eq!(SpecVersion::V03, event.specversion());
|
||||||
assert_eq!(id, event.get_id());
|
assert_eq!(id, event.id());
|
||||||
assert_eq!(source, event.get_source().clone());
|
assert_eq!(source, event.source().clone());
|
||||||
assert_eq!(ty, event.get_type());
|
assert_eq!(ty, event.ty());
|
||||||
assert_eq!(subject, event.get_subject().unwrap());
|
assert_eq!(subject, event.subject().unwrap());
|
||||||
assert_eq!(time, event.get_time().unwrap().clone());
|
assert_eq!(time, event.time().unwrap().clone());
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
ExtensionValue::from(extension_value),
|
ExtensionValue::from(extension_value),
|
||||||
event.get_extension(extension_name).unwrap().clone()
|
event.extension(extension_name).unwrap().clone()
|
||||||
);
|
);
|
||||||
assert_eq!(content_type, event.get_datacontenttype().unwrap());
|
assert_eq!(content_type, event.datacontenttype().unwrap());
|
||||||
assert_eq!(schema, event.get_dataschema().unwrap().clone());
|
assert_eq!(schema, event.dataschema().unwrap().clone());
|
||||||
|
|
||||||
let event_data: serde_json::Value = event.try_get_data().unwrap().unwrap();
|
let event_data: serde_json::Value = event.take_data().2.unwrap().try_into().unwrap();
|
||||||
assert_eq!(data, event_data);
|
assert_eq!(data, event_data);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn source_valid_relative_url() {
|
||||||
|
let res = EventBuilderV03::new()
|
||||||
|
.id("id1")
|
||||||
|
.source("/source") // relative URL
|
||||||
|
.ty("type")
|
||||||
|
.build();
|
||||||
|
assert_match_pattern!(res, Ok(_));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn build_missing_id() {
|
||||||
|
let res = EventBuilderV03::new()
|
||||||
|
.source("http://localhost:8080")
|
||||||
|
.build();
|
||||||
|
assert_match_pattern!(
|
||||||
|
res,
|
||||||
|
Err(EventBuilderError::MissingRequiredAttribute {
|
||||||
|
attribute_name: "id"
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn source_invalid_url() {
|
||||||
|
let res = EventBuilderV03::new().source("").build();
|
||||||
|
assert_match_pattern!(
|
||||||
|
res,
|
||||||
|
Err(EventBuilderError::InvalidUriRefError {
|
||||||
|
attribute_name: "source",
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn default_builds() {
|
||||||
|
let res = EventBuilderV03::default().build();
|
||||||
|
assert_match_pattern!(res, Ok(_));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,53 +1,58 @@
|
||||||
use super::Attributes;
|
use super::Attributes;
|
||||||
use crate::event::data::is_json_content_type;
|
use crate::event::data::is_json_content_type;
|
||||||
|
use crate::event::format::{
|
||||||
|
parse_data_base64, parse_data_base64_json, parse_data_json, parse_data_string,
|
||||||
|
};
|
||||||
use crate::event::{Data, ExtensionValue};
|
use crate::event::{Data, ExtensionValue};
|
||||||
|
use base64::prelude::*;
|
||||||
use chrono::{DateTime, Utc};
|
use chrono::{DateTime, Utc};
|
||||||
use serde::de::IntoDeserializer;
|
use serde::de::IntoDeserializer;
|
||||||
use serde::ser::SerializeMap;
|
use serde::ser::SerializeMap;
|
||||||
use serde::{Deserialize, Serializer};
|
use serde::{Deserialize, Serializer};
|
||||||
use serde_value::Value;
|
use serde_json::{Map, Value};
|
||||||
use std::collections::{BTreeMap, HashMap};
|
use std::collections::HashMap;
|
||||||
use url::Url;
|
use url::Url;
|
||||||
|
|
||||||
pub(crate) struct EventFormatDeserializer {}
|
pub(crate) struct EventFormatDeserializer {}
|
||||||
|
|
||||||
impl crate::event::format::EventFormatDeserializer for EventFormatDeserializer {
|
impl crate::event::format::EventFormatDeserializer for EventFormatDeserializer {
|
||||||
fn deserialize_attributes<E: serde::de::Error>(
|
fn deserialize_attributes<E: serde::de::Error>(
|
||||||
map: &mut BTreeMap<String, Value>,
|
map: &mut Map<String, Value>,
|
||||||
) -> Result<crate::event::Attributes, E> {
|
) -> Result<crate::event::Attributes, E> {
|
||||||
Ok(crate::event::Attributes::V03(Attributes {
|
Ok(crate::event::Attributes::V03(Attributes {
|
||||||
id: parse_field!(map, "id", String, E)?,
|
id: extract_field!(map, "id", String, E)?,
|
||||||
ty: parse_field!(map, "type", String, E)?,
|
ty: extract_field!(map, "type", String, E)?,
|
||||||
source: parse_field!(map, "source", String, E, Url::parse)?,
|
source: extract_field!(map, "source", String, E)?,
|
||||||
datacontenttype: parse_optional_field!(map, "datacontenttype", String, E)?,
|
datacontenttype: extract_optional_field!(map, "datacontenttype", String, E)?,
|
||||||
schemaurl: parse_optional_field!(map, "schemaurl", String, E, Url::parse)?,
|
schemaurl: extract_optional_field!(map, "schemaurl", String, E, |s: String| {
|
||||||
subject: parse_optional_field!(map, "subject", String, E)?,
|
Url::parse(&s)
|
||||||
time: parse_optional_field!(map, "time", String, E, |s| DateTime::parse_from_rfc3339(
|
})?,
|
||||||
s
|
subject: extract_optional_field!(map, "subject", String, E)?,
|
||||||
)
|
time: extract_optional_field!(map, "time", String, E, |s: String| {
|
||||||
.map(DateTime::<Utc>::from))?,
|
DateTime::parse_from_rfc3339(&s).map(DateTime::<Utc>::from)
|
||||||
|
})?,
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn deserialize_data<E: serde::de::Error>(
|
fn deserialize_data<E: serde::de::Error>(
|
||||||
content_type: &str,
|
content_type: &str,
|
||||||
map: &mut BTreeMap<String, Value>,
|
map: &mut Map<String, Value>,
|
||||||
) -> Result<Option<Data>, E> {
|
) -> Result<Option<Data>, E> {
|
||||||
let data = map.remove("data");
|
let data = map.remove("data");
|
||||||
let is_base64 = map
|
let is_base64 = map
|
||||||
.remove("datacontentencoding")
|
.remove("datacontentencoding")
|
||||||
.map(String::deserialize)
|
.map(String::deserialize)
|
||||||
.transpose()
|
.transpose()
|
||||||
.map_err(|e| E::custom(e))?
|
.map_err(E::custom)?
|
||||||
.map(|dce| dce.to_lowercase() == "base64")
|
.map(|dce| dce.to_lowercase() == "base64")
|
||||||
.unwrap_or(false);
|
.unwrap_or(false);
|
||||||
let is_json = is_json_content_type(content_type);
|
let is_json = is_json_content_type(content_type);
|
||||||
|
|
||||||
Ok(match (data, is_base64, is_json) {
|
Ok(match (data, is_base64, is_json) {
|
||||||
(Some(d), false, true) => Some(Data::Json(parse_data_json!(d, E)?)),
|
(Some(d), false, true) => Some(Data::Json(parse_data_json(d)?)),
|
||||||
(Some(d), false, false) => Some(Data::String(parse_data_string!(d, E)?)),
|
(Some(d), false, false) => Some(Data::String(parse_data_string(d)?)),
|
||||||
(Some(d), true, true) => Some(Data::Json(parse_json_data_base64!(d, E)?)),
|
(Some(d), true, true) => Some(Data::Json(parse_data_base64_json(d)?)),
|
||||||
(Some(d), true, false) => Some(Data::Binary(parse_data_base64!(d, E)?)),
|
(Some(d), true, false) => Some(Data::Binary(parse_data_base64(d)?)),
|
||||||
(None, _, _) => None,
|
(None, _, _) => None,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -64,16 +69,19 @@ impl<S: serde::Serializer> crate::event::format::EventFormatSerializer<S, Attrib
|
||||||
extensions: &HashMap<String, ExtensionValue>,
|
extensions: &HashMap<String, ExtensionValue>,
|
||||||
serializer: S,
|
serializer: S,
|
||||||
) -> Result<<S as Serializer>::Ok, <S as Serializer>::Error> {
|
) -> Result<<S as Serializer>::Ok, <S as Serializer>::Error> {
|
||||||
let num =
|
let num = 4
|
||||||
3 + if attributes.datacontenttype.is_some() {
|
+ [
|
||||||
1
|
attributes.datacontenttype.is_some(),
|
||||||
} else {
|
attributes.schemaurl.is_some(),
|
||||||
0
|
attributes.subject.is_some(),
|
||||||
} + if attributes.schemaurl.is_some() { 1 } else { 0 }
|
attributes.time.is_some(),
|
||||||
+ if attributes.subject.is_some() { 1 } else { 0 }
|
data.is_some(),
|
||||||
+ if attributes.time.is_some() { 1 } else { 0 }
|
]
|
||||||
+ if data.is_some() { 1 } else { 0 }
|
.iter()
|
||||||
+ extensions.len();
|
.filter(|&b| *b)
|
||||||
|
.count()
|
||||||
|
+ extensions.len();
|
||||||
|
|
||||||
let mut state = serializer.serialize_map(Some(num))?;
|
let mut state = serializer.serialize_map(Some(num))?;
|
||||||
state.serialize_entry("specversion", "0.3")?;
|
state.serialize_entry("specversion", "0.3")?;
|
||||||
state.serialize_entry("id", &attributes.id)?;
|
state.serialize_entry("id", &attributes.id)?;
|
||||||
|
@ -95,7 +103,7 @@ impl<S: serde::Serializer> crate::event::format::EventFormatSerializer<S, Attrib
|
||||||
Some(Data::Json(j)) => state.serialize_entry("data", j)?,
|
Some(Data::Json(j)) => state.serialize_entry("data", j)?,
|
||||||
Some(Data::String(s)) => state.serialize_entry("data", s)?,
|
Some(Data::String(s)) => state.serialize_entry("data", s)?,
|
||||||
Some(Data::Binary(v)) => {
|
Some(Data::Binary(v)) => {
|
||||||
state.serialize_entry("data", &base64::encode(v))?;
|
state.serialize_entry("data", &BASE64_STANDARD.encode(v))?;
|
||||||
state.serialize_entry("datacontentencoding", "base64")?;
|
state.serialize_entry("datacontentencoding", "base64")?;
|
||||||
}
|
}
|
||||||
_ => (),
|
_ => (),
|
||||||
|
|
|
@ -1,53 +0,0 @@
|
||||||
use crate::message::{BinarySerializer, Error, MessageAttributeValue, Result};
|
|
||||||
use std::convert::TryInto;
|
|
||||||
|
|
||||||
impl crate::event::message::AttributesDeserializer for super::Attributes {
|
|
||||||
fn deserialize_attributes<R: Sized, V: BinarySerializer<R>>(self, mut visitor: V) -> Result<V> {
|
|
||||||
visitor = visitor.set_attribute("id", MessageAttributeValue::String(self.id))?;
|
|
||||||
visitor = visitor.set_attribute("type", MessageAttributeValue::String(self.ty))?;
|
|
||||||
visitor = visitor.set_attribute("source", MessageAttributeValue::UriRef(self.source))?;
|
|
||||||
if self.datacontenttype.is_some() {
|
|
||||||
visitor = visitor.set_attribute(
|
|
||||||
"datacontenttype",
|
|
||||||
MessageAttributeValue::String(self.datacontenttype.unwrap()),
|
|
||||||
)?;
|
|
||||||
}
|
|
||||||
if self.schemaurl.is_some() {
|
|
||||||
visitor = visitor.set_attribute(
|
|
||||||
"schemaurl",
|
|
||||||
MessageAttributeValue::Uri(self.schemaurl.unwrap()),
|
|
||||||
)?;
|
|
||||||
}
|
|
||||||
if self.subject.is_some() {
|
|
||||||
visitor = visitor.set_attribute(
|
|
||||||
"subject",
|
|
||||||
MessageAttributeValue::String(self.subject.unwrap()),
|
|
||||||
)?;
|
|
||||||
}
|
|
||||||
if self.time.is_some() {
|
|
||||||
visitor = visitor
|
|
||||||
.set_attribute("time", MessageAttributeValue::DateTime(self.time.unwrap()))?;
|
|
||||||
}
|
|
||||||
Ok(visitor)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl crate::event::message::AttributesSerializer for super::Attributes {
|
|
||||||
fn serialize_attribute(&mut self, name: &str, value: MessageAttributeValue) -> Result<()> {
|
|
||||||
match name {
|
|
||||||
"id" => self.id = value.to_string(),
|
|
||||||
"type" => self.ty = value.to_string(),
|
|
||||||
"source" => self.source = value.try_into()?,
|
|
||||||
"datacontenttype" => self.datacontenttype = Some(value.to_string()),
|
|
||||||
"schemaurl" => self.schemaurl = Some(value.try_into()?),
|
|
||||||
"subject" => self.subject = Some(value.to_string()),
|
|
||||||
"time" => self.time = Some(value.try_into()?),
|
|
||||||
_ => {
|
|
||||||
return Err(Error::UnrecognizedAttributeName {
|
|
||||||
name: name.to_string(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
};
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,10 +1,10 @@
|
||||||
mod attributes;
|
mod attributes;
|
||||||
mod builder;
|
mod builder;
|
||||||
mod format;
|
mod format;
|
||||||
mod message;
|
|
||||||
|
|
||||||
pub(crate) use crate::event::v03::format::EventFormatDeserializer;
|
|
||||||
pub(crate) use crate::event::v03::format::EventFormatSerializer;
|
|
||||||
pub use attributes::Attributes;
|
pub use attributes::Attributes;
|
||||||
|
pub(crate) use attributes::AttributesIntoIterator;
|
||||||
pub(crate) use attributes::ATTRIBUTE_NAMES;
|
pub(crate) use attributes::ATTRIBUTE_NAMES;
|
||||||
pub use builder::EventBuilder;
|
pub use builder::EventBuilder;
|
||||||
|
pub(crate) use format::EventFormatDeserializer;
|
||||||
|
pub(crate) use format::EventFormatSerializer;
|
||||||
|
|
|
@ -1,12 +1,12 @@
|
||||||
use crate::event::attributes::{
|
use crate::event::attributes::{default_hostname, AttributeValue, AttributesConverter};
|
||||||
default_hostname, AttributeValue, AttributesConverter, DataAttributesWriter,
|
use crate::event::{AttributesReader, AttributesV03, AttributesWriter, SpecVersion, UriReference};
|
||||||
};
|
use crate::message::{BinarySerializer, MessageAttributeValue};
|
||||||
use crate::event::{AttributesReader, AttributesV03, AttributesWriter, SpecVersion};
|
|
||||||
use chrono::{DateTime, Utc};
|
use chrono::{DateTime, Utc};
|
||||||
|
use core::fmt::Debug;
|
||||||
use url::Url;
|
use url::Url;
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
pub(crate) const ATTRIBUTE_NAMES: [&'static str; 8] = [
|
pub(crate) const ATTRIBUTE_NAMES: [&str; 8] = [
|
||||||
"specversion",
|
"specversion",
|
||||||
"id",
|
"id",
|
||||||
"type",
|
"type",
|
||||||
|
@ -18,11 +18,11 @@ pub(crate) const ATTRIBUTE_NAMES: [&'static str; 8] = [
|
||||||
];
|
];
|
||||||
|
|
||||||
/// Data structure representing [CloudEvents V1.0 context attributes](https://github.com/cloudevents/spec/blob/v1.0/spec.md#context-attributes)
|
/// Data structure representing [CloudEvents V1.0 context attributes](https://github.com/cloudevents/spec/blob/v1.0/spec.md#context-attributes)
|
||||||
#[derive(PartialEq, Debug, Clone)]
|
#[derive(PartialEq, Eq, Debug, Clone)]
|
||||||
pub struct Attributes {
|
pub struct Attributes {
|
||||||
pub(crate) id: String,
|
pub(crate) id: String,
|
||||||
pub(crate) ty: String,
|
pub(crate) ty: String,
|
||||||
pub(crate) source: Url,
|
pub(crate) source: UriReference,
|
||||||
pub(crate) datacontenttype: Option<String>,
|
pub(crate) datacontenttype: Option<String>,
|
||||||
pub(crate) dataschema: Option<Url>,
|
pub(crate) dataschema: Option<Url>,
|
||||||
pub(crate) subject: Option<String>,
|
pub(crate) subject: Option<String>,
|
||||||
|
@ -41,34 +41,36 @@ impl<'a> IntoIterator for &'a Attributes {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(PartialEq, Debug, Clone, Copy)]
|
||||||
pub struct AttributesIntoIterator<'a> {
|
pub struct AttributesIntoIterator<'a> {
|
||||||
attributes: &'a Attributes,
|
pub(crate) attributes: &'a Attributes,
|
||||||
index: usize,
|
pub(crate) index: usize,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> Iterator for AttributesIntoIterator<'a> {
|
impl<'a> Iterator for AttributesIntoIterator<'a> {
|
||||||
type Item = (&'a str, AttributeValue<'a>);
|
type Item = (&'a str, AttributeValue<'a>);
|
||||||
fn next(&mut self) -> Option<Self::Item> {
|
fn next(&mut self) -> Option<Self::Item> {
|
||||||
let result = match self.index {
|
let result = match self.index {
|
||||||
0 => Some(("id", AttributeValue::String(&self.attributes.id))),
|
0 => Some(("specversion", AttributeValue::SpecVersion(SpecVersion::V10))),
|
||||||
1 => Some(("type", AttributeValue::String(&self.attributes.ty))),
|
1 => Some(("id", AttributeValue::String(&self.attributes.id))),
|
||||||
2 => Some(("source", AttributeValue::URIRef(&self.attributes.source))),
|
2 => Some(("type", AttributeValue::String(&self.attributes.ty))),
|
||||||
3 => self
|
3 => Some(("source", AttributeValue::URIRef(&self.attributes.source))),
|
||||||
|
4 => self
|
||||||
.attributes
|
.attributes
|
||||||
.datacontenttype
|
.datacontenttype
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.map(|v| ("datacontenttype", AttributeValue::String(v))),
|
.map(|v| ("datacontenttype", AttributeValue::String(v))),
|
||||||
4 => self
|
5 => self
|
||||||
.attributes
|
.attributes
|
||||||
.dataschema
|
.dataschema
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.map(|v| ("dataschema", AttributeValue::URI(v))),
|
.map(|v| ("dataschema", AttributeValue::URI(v))),
|
||||||
5 => self
|
6 => self
|
||||||
.attributes
|
.attributes
|
||||||
.subject
|
.subject
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.map(|v| ("subject", AttributeValue::String(v))),
|
.map(|v| ("subject", AttributeValue::String(v))),
|
||||||
6 => self
|
7 => self
|
||||||
.attributes
|
.attributes
|
||||||
.time
|
.time
|
||||||
.as_ref()
|
.as_ref()
|
||||||
|
@ -84,68 +86,69 @@ impl<'a> Iterator for AttributesIntoIterator<'a> {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl AttributesReader for Attributes {
|
impl AttributesReader for Attributes {
|
||||||
fn get_id(&self) -> &str {
|
fn id(&self) -> &str {
|
||||||
&self.id
|
&self.id
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_source(&self) -> &Url {
|
fn source(&self) -> &UriReference {
|
||||||
&self.source
|
&self.source
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_specversion(&self) -> SpecVersion {
|
fn specversion(&self) -> SpecVersion {
|
||||||
SpecVersion::V10
|
SpecVersion::V10
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_type(&self) -> &str {
|
fn ty(&self) -> &str {
|
||||||
&self.ty
|
&self.ty
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_datacontenttype(&self) -> Option<&str> {
|
fn datacontenttype(&self) -> Option<&str> {
|
||||||
self.datacontenttype.as_deref()
|
self.datacontenttype.as_deref()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_dataschema(&self) -> Option<&Url> {
|
fn dataschema(&self) -> Option<&Url> {
|
||||||
self.dataschema.as_ref()
|
self.dataschema.as_ref()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_subject(&self) -> Option<&str> {
|
fn subject(&self) -> Option<&str> {
|
||||||
self.subject.as_deref()
|
self.subject.as_deref()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_time(&self) -> Option<&DateTime<Utc>> {
|
fn time(&self) -> Option<&DateTime<Utc>> {
|
||||||
self.time.as_ref()
|
self.time.as_ref()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl AttributesWriter for Attributes {
|
impl AttributesWriter for Attributes {
|
||||||
fn set_id(&mut self, id: impl Into<String>) {
|
fn set_id(&mut self, id: impl Into<String>) -> String {
|
||||||
self.id = id.into()
|
std::mem::replace(&mut self.id, id.into())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn set_source(&mut self, source: impl Into<Url>) {
|
fn set_source(&mut self, source: impl Into<UriReference>) -> UriReference {
|
||||||
self.source = source.into()
|
std::mem::replace(&mut self.source, source.into())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn set_type(&mut self, ty: impl Into<String>) {
|
fn set_type(&mut self, ty: impl Into<String>) -> String {
|
||||||
self.ty = ty.into()
|
std::mem::replace(&mut self.ty, ty.into())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn set_subject(&mut self, subject: Option<impl Into<String>>) {
|
fn set_subject(&mut self, subject: Option<impl Into<String>>) -> Option<String> {
|
||||||
self.subject = subject.map(Into::into)
|
std::mem::replace(&mut self.subject, subject.map(Into::into))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn set_time(&mut self, time: Option<impl Into<DateTime<Utc>>>) {
|
fn set_time(&mut self, time: Option<impl Into<DateTime<Utc>>>) -> Option<DateTime<Utc>> {
|
||||||
self.time = time.map(Into::into)
|
std::mem::replace(&mut self.time, time.map(Into::into))
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl DataAttributesWriter for Attributes {
|
|
||||||
fn set_datacontenttype(&mut self, datacontenttype: Option<impl Into<String>>) {
|
|
||||||
self.datacontenttype = datacontenttype.map(Into::into)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn set_dataschema(&mut self, dataschema: Option<impl Into<Url>>) {
|
fn set_datacontenttype(
|
||||||
self.dataschema = dataschema.map(Into::into)
|
&mut self,
|
||||||
|
datacontenttype: Option<impl Into<String>>,
|
||||||
|
) -> Option<String> {
|
||||||
|
std::mem::replace(&mut self.datacontenttype, datacontenttype.map(Into::into))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_dataschema(&mut self, dataschema: Option<impl Into<Url>>) -> Option<Url> {
|
||||||
|
std::mem::replace(&mut self.dataschema, dataschema.map(Into::into))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -154,15 +157,49 @@ impl Default for Attributes {
|
||||||
Attributes {
|
Attributes {
|
||||||
id: Uuid::new_v4().to_string(),
|
id: Uuid::new_v4().to_string(),
|
||||||
ty: "type".to_string(),
|
ty: "type".to_string(),
|
||||||
source: default_hostname(),
|
source: default_hostname().to_string(),
|
||||||
datacontenttype: None,
|
datacontenttype: None,
|
||||||
dataschema: None,
|
dataschema: None,
|
||||||
subject: None,
|
subject: None,
|
||||||
time: None,
|
time: Some(Utc::now()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl crate::event::message::AttributesDeserializer for super::Attributes {
|
||||||
|
fn deserialize_attributes<R: Sized, V: BinarySerializer<R>>(
|
||||||
|
self,
|
||||||
|
mut visitor: V,
|
||||||
|
) -> crate::message::Result<V> {
|
||||||
|
visitor = visitor.set_attribute("id", MessageAttributeValue::String(self.id))?;
|
||||||
|
visitor = visitor.set_attribute("type", MessageAttributeValue::String(self.ty))?;
|
||||||
|
visitor = visitor.set_attribute("source", MessageAttributeValue::UriRef(self.source))?;
|
||||||
|
if self.datacontenttype.is_some() {
|
||||||
|
visitor = visitor.set_attribute(
|
||||||
|
"datacontenttype",
|
||||||
|
MessageAttributeValue::String(self.datacontenttype.unwrap()),
|
||||||
|
)?;
|
||||||
|
}
|
||||||
|
if self.dataschema.is_some() {
|
||||||
|
visitor = visitor.set_attribute(
|
||||||
|
"dataschema",
|
||||||
|
MessageAttributeValue::Uri(self.dataschema.unwrap()),
|
||||||
|
)?;
|
||||||
|
}
|
||||||
|
if self.subject.is_some() {
|
||||||
|
visitor = visitor.set_attribute(
|
||||||
|
"subject",
|
||||||
|
MessageAttributeValue::String(self.subject.unwrap()),
|
||||||
|
)?;
|
||||||
|
}
|
||||||
|
if self.time.is_some() {
|
||||||
|
visitor = visitor
|
||||||
|
.set_attribute("time", MessageAttributeValue::DateTime(self.time.unwrap()))?;
|
||||||
|
}
|
||||||
|
Ok(visitor)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl AttributesConverter for Attributes {
|
impl AttributesConverter for Attributes {
|
||||||
fn into_v10(self) -> Self {
|
fn into_v10(self) -> Self {
|
||||||
self
|
self
|
||||||
|
@ -184,25 +221,37 @@ impl AttributesConverter for Attributes {
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
use chrono::NaiveDateTime;
|
use crate::test::fixtures;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn iter_v10_test() {
|
||||||
|
let in_event = fixtures::v10::full_no_data();
|
||||||
|
let mut iter_v10 = in_event.iter_attributes();
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
("specversion", AttributeValue::SpecVersion(SpecVersion::V10)),
|
||||||
|
iter_v10.next().unwrap()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn iterator_test_v10() {
|
fn iterator_test_v10() {
|
||||||
let a = Attributes {
|
let a = Attributes {
|
||||||
id: String::from("1"),
|
id: String::from("1"),
|
||||||
ty: String::from("someType"),
|
ty: String::from("someType"),
|
||||||
source: Url::parse("https://example.net").unwrap(),
|
source: "https://example.net".into(),
|
||||||
datacontenttype: None,
|
datacontenttype: None,
|
||||||
dataschema: None,
|
dataschema: None,
|
||||||
subject: None,
|
subject: None,
|
||||||
time: Some(DateTime::<Utc>::from_utc(
|
time: DateTime::from_timestamp(61, 0),
|
||||||
NaiveDateTime::from_timestamp(61, 0),
|
|
||||||
Utc,
|
|
||||||
)),
|
|
||||||
};
|
};
|
||||||
let b = &mut a.into_iter();
|
let b = &mut a.into_iter();
|
||||||
let time = DateTime::<Utc>::from_utc(NaiveDateTime::from_timestamp(61, 0), Utc);
|
let time = DateTime::from_timestamp(61, 0).unwrap();
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
("specversion", AttributeValue::SpecVersion(SpecVersion::V10)),
|
||||||
|
b.next().unwrap()
|
||||||
|
);
|
||||||
assert_eq!(("id", AttributeValue::String("1")), b.next().unwrap());
|
assert_eq!(("id", AttributeValue::String("1")), b.next().unwrap());
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
("type", AttributeValue::String("someType")),
|
("type", AttributeValue::String("someType")),
|
||||||
|
@ -211,7 +260,7 @@ mod tests {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
(
|
(
|
||||||
"source",
|
"source",
|
||||||
AttributeValue::URIRef(&Url::parse("https://example.net").unwrap())
|
AttributeValue::URIRef(&"https://example.net".to_string())
|
||||||
),
|
),
|
||||||
b.next().unwrap()
|
b.next().unwrap()
|
||||||
);
|
);
|
||||||
|
|
|
@ -1,58 +1,68 @@
|
||||||
use super::Attributes as AttributesV10;
|
use super::Attributes as AttributesV10;
|
||||||
use crate::event::{Attributes, AttributesWriter, Data, Event, ExtensionValue};
|
use crate::event::{
|
||||||
|
Attributes, Data, Event, EventBuilderError, ExtensionValue, TryIntoTime, TryIntoUrl,
|
||||||
|
UriReference,
|
||||||
|
};
|
||||||
|
use crate::message::MessageAttributeValue;
|
||||||
use chrono::{DateTime, Utc};
|
use chrono::{DateTime, Utc};
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
use std::convert::TryInto;
|
||||||
use url::Url;
|
use url::Url;
|
||||||
|
|
||||||
/// Builder to create a CloudEvent V1.0
|
/// Builder to create a CloudEvent V1.0
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
pub struct EventBuilder {
|
pub struct EventBuilder {
|
||||||
event: Event,
|
id: Option<String>,
|
||||||
|
ty: Option<String>,
|
||||||
|
source: Option<UriReference>,
|
||||||
|
datacontenttype: Option<String>,
|
||||||
|
dataschema: Option<Url>,
|
||||||
|
subject: Option<String>,
|
||||||
|
time: Option<DateTime<Utc>>,
|
||||||
|
data: Option<Data>,
|
||||||
|
extensions: HashMap<String, ExtensionValue>,
|
||||||
|
error: Option<EventBuilderError>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl EventBuilder {
|
impl EventBuilder {
|
||||||
pub fn from(event: Event) -> Self {
|
|
||||||
EventBuilder {
|
|
||||||
event: Event {
|
|
||||||
attributes: event.attributes.into_v10(),
|
|
||||||
data: event.data,
|
|
||||||
extensions: event.extensions,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn new() -> Self {
|
|
||||||
EventBuilder {
|
|
||||||
event: Event {
|
|
||||||
attributes: Attributes::V10(AttributesV10::default()),
|
|
||||||
data: None,
|
|
||||||
extensions: HashMap::new(),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn id(mut self, id: impl Into<String>) -> Self {
|
pub fn id(mut self, id: impl Into<String>) -> Self {
|
||||||
self.event.set_id(id);
|
self.id = Some(id.into());
|
||||||
return self;
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn source(mut self, source: impl Into<Url>) -> Self {
|
pub fn source(mut self, source: impl Into<String>) -> Self {
|
||||||
self.event.set_source(source);
|
let source = source.into();
|
||||||
return self;
|
if source.is_empty() {
|
||||||
|
self.error = Some(EventBuilderError::InvalidUriRefError {
|
||||||
|
attribute_name: "source",
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
self.source = Some(source);
|
||||||
|
}
|
||||||
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn ty(mut self, ty: impl Into<String>) -> Self {
|
pub fn ty(mut self, ty: impl Into<String>) -> Self {
|
||||||
self.event.set_type(ty);
|
self.ty = Some(ty.into());
|
||||||
return self;
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn subject(mut self, subject: impl Into<String>) -> Self {
|
pub fn subject(mut self, subject: impl Into<String>) -> Self {
|
||||||
self.event.set_subject(Some(subject));
|
self.subject = Some(subject.into());
|
||||||
return self;
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn time(mut self, time: impl Into<DateTime<Utc>>) -> Self {
|
pub fn time(mut self, time: impl TryIntoTime) -> Self {
|
||||||
self.event.set_time(Some(time));
|
match time.into_time() {
|
||||||
return self;
|
Ok(u) => self.time = Some(u),
|
||||||
|
Err(e) => {
|
||||||
|
self.error = Some(EventBuilderError::ParseTimeError {
|
||||||
|
attribute_name: "time",
|
||||||
|
source: e,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
};
|
||||||
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn extension(
|
pub fn extension(
|
||||||
|
@ -60,40 +70,158 @@ impl EventBuilder {
|
||||||
extension_name: &str,
|
extension_name: &str,
|
||||||
extension_value: impl Into<ExtensionValue>,
|
extension_value: impl Into<ExtensionValue>,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
self.event.set_extension(extension_name, extension_value);
|
self.extensions
|
||||||
return self;
|
.insert(extension_name.to_owned(), extension_value.into());
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn data_without_content_type(mut self, data: impl Into<Data>) -> Self {
|
||||||
|
self.data = Some(data.into());
|
||||||
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn data(mut self, datacontenttype: impl Into<String>, data: impl Into<Data>) -> Self {
|
pub fn data(mut self, datacontenttype: impl Into<String>, data: impl Into<Data>) -> Self {
|
||||||
self.event.write_data(datacontenttype, data);
|
self.datacontenttype = Some(datacontenttype.into());
|
||||||
return self;
|
self.data = Some(data.into());
|
||||||
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn data_with_schema(
|
pub fn data_with_schema(
|
||||||
mut self,
|
mut self,
|
||||||
datacontenttype: impl Into<String>,
|
datacontenttype: impl Into<String>,
|
||||||
dataschema: impl Into<Url>,
|
schemaurl: impl TryIntoUrl,
|
||||||
data: impl Into<Data>,
|
data: impl Into<Data>,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
self.event
|
self.datacontenttype = Some(datacontenttype.into());
|
||||||
.write_data_with_schema(datacontenttype, dataschema, data);
|
match schemaurl.into_url() {
|
||||||
return self;
|
Ok(u) => self.dataschema = Some(u),
|
||||||
|
Err(e) => {
|
||||||
|
self.error = Some(EventBuilderError::ParseUrlError {
|
||||||
|
attribute_name: "dataschema",
|
||||||
|
source: e,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
};
|
||||||
|
self.data = Some(data.into());
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<Event> for EventBuilder {
|
||||||
|
fn from(event: Event) -> Self {
|
||||||
|
let attributes = match event.attributes.into_v10() {
|
||||||
|
Attributes::V10(attr) => attr,
|
||||||
|
// This branch is unreachable because into_v10() returns
|
||||||
|
// always a Attributes::V10
|
||||||
|
_ => unreachable!(),
|
||||||
|
};
|
||||||
|
|
||||||
|
EventBuilder {
|
||||||
|
id: Some(attributes.id),
|
||||||
|
ty: Some(attributes.ty),
|
||||||
|
source: Some(attributes.source),
|
||||||
|
datacontenttype: attributes.datacontenttype,
|
||||||
|
dataschema: attributes.dataschema,
|
||||||
|
subject: attributes.subject,
|
||||||
|
time: attributes.time,
|
||||||
|
data: event.data,
|
||||||
|
extensions: event.extensions,
|
||||||
|
error: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for EventBuilder {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::from(Event::default())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl crate::event::builder::EventBuilder for EventBuilder {
|
||||||
|
fn new() -> Self {
|
||||||
|
EventBuilder {
|
||||||
|
id: None,
|
||||||
|
ty: None,
|
||||||
|
source: None,
|
||||||
|
datacontenttype: None,
|
||||||
|
dataschema: None,
|
||||||
|
subject: None,
|
||||||
|
time: None,
|
||||||
|
data: None,
|
||||||
|
extensions: Default::default(),
|
||||||
|
error: None,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn build(self) -> Event {
|
fn build(self) -> Result<Event, EventBuilderError> {
|
||||||
self.event
|
match self.error {
|
||||||
|
Some(e) => Err(e),
|
||||||
|
None => Ok(Event {
|
||||||
|
attributes: Attributes::V10(AttributesV10 {
|
||||||
|
id: self.id.ok_or(EventBuilderError::MissingRequiredAttribute {
|
||||||
|
attribute_name: "id",
|
||||||
|
})?,
|
||||||
|
ty: self.ty.ok_or(EventBuilderError::MissingRequiredAttribute {
|
||||||
|
attribute_name: "type",
|
||||||
|
})?,
|
||||||
|
source: self
|
||||||
|
.source
|
||||||
|
.ok_or(EventBuilderError::MissingRequiredAttribute {
|
||||||
|
attribute_name: "source",
|
||||||
|
})?,
|
||||||
|
datacontenttype: self.datacontenttype,
|
||||||
|
dataschema: self.dataschema,
|
||||||
|
subject: self.subject,
|
||||||
|
time: self.time,
|
||||||
|
}),
|
||||||
|
data: self.data,
|
||||||
|
extensions: self.extensions,
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl crate::event::message::AttributesSerializer for EventBuilder {
|
||||||
|
fn serialize_attribute(
|
||||||
|
&mut self,
|
||||||
|
name: &str,
|
||||||
|
value: MessageAttributeValue,
|
||||||
|
) -> crate::message::Result<()> {
|
||||||
|
match name {
|
||||||
|
"id" => self.id = Some(value.to_string()),
|
||||||
|
"type" => self.ty = Some(value.to_string()),
|
||||||
|
"source" => self.source = Some(value.to_string()),
|
||||||
|
"datacontenttype" => self.datacontenttype = Some(value.to_string()),
|
||||||
|
"dataschema" => self.dataschema = Some(value.try_into()?),
|
||||||
|
"subject" => self.subject = Some(value.to_string()),
|
||||||
|
"time" => self.time = Some(value.try_into()?),
|
||||||
|
_ => {
|
||||||
|
return Err(crate::message::Error::UnknownAttribute {
|
||||||
|
name: name.to_string(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
|
||||||
use crate::event::{AttributesReader, SpecVersion};
|
use crate::assert_match_pattern;
|
||||||
|
use chrono::{DateTime, Utc};
|
||||||
|
|
||||||
|
use crate::event::{
|
||||||
|
AttributesReader, EventBuilder, EventBuilderError, ExtensionValue, SpecVersion,
|
||||||
|
};
|
||||||
|
use crate::EventBuilderV10;
|
||||||
|
use std::convert::TryInto;
|
||||||
|
use url::Url;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn build_event() {
|
fn build_event() {
|
||||||
let id = "aaa";
|
let id = "aaa";
|
||||||
let source = Url::parse("http://localhost:8080").unwrap();
|
let source = "http://localhost:8080";
|
||||||
let ty = "bbb";
|
let ty = "bbb";
|
||||||
let subject = "francesco";
|
let subject = "francesco";
|
||||||
let time: DateTime<Utc> = Utc::now();
|
let time: DateTime<Utc> = Utc::now();
|
||||||
|
@ -105,30 +233,71 @@ mod tests {
|
||||||
"hello": "world"
|
"hello": "world"
|
||||||
});
|
});
|
||||||
|
|
||||||
let event = EventBuilder::new()
|
let mut event = EventBuilderV10::new()
|
||||||
.id(id)
|
.id(id)
|
||||||
.source(source.clone())
|
.source(source.to_string())
|
||||||
.ty(ty)
|
.ty(ty)
|
||||||
.subject(subject)
|
.subject(subject)
|
||||||
.time(time)
|
.time(time)
|
||||||
.extension(extension_name, extension_value)
|
.extension(extension_name, extension_value)
|
||||||
.data_with_schema(content_type, schema.clone(), data.clone())
|
.data_with_schema(content_type, schema.clone(), data.clone())
|
||||||
.build();
|
.build()
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
assert_eq!(SpecVersion::V10, event.get_specversion());
|
assert_eq!(SpecVersion::V10, event.specversion());
|
||||||
assert_eq!(id, event.get_id());
|
assert_eq!(id, event.id());
|
||||||
assert_eq!(source, event.get_source().clone());
|
assert_eq!(source, event.source().clone());
|
||||||
assert_eq!(ty, event.get_type());
|
assert_eq!(ty, event.ty());
|
||||||
assert_eq!(subject, event.get_subject().unwrap());
|
assert_eq!(subject, event.subject().unwrap());
|
||||||
assert_eq!(time, event.get_time().unwrap().clone());
|
assert_eq!(time, event.time().unwrap().clone());
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
ExtensionValue::from(extension_value),
|
ExtensionValue::from(extension_value),
|
||||||
event.get_extension(extension_name).unwrap().clone()
|
event.extension(extension_name).unwrap().clone()
|
||||||
);
|
);
|
||||||
assert_eq!(content_type, event.get_datacontenttype().unwrap());
|
assert_eq!(content_type, event.datacontenttype().unwrap());
|
||||||
assert_eq!(schema, event.get_dataschema().unwrap().clone());
|
assert_eq!(schema, event.dataschema().unwrap().clone());
|
||||||
|
|
||||||
let event_data: serde_json::Value = event.try_get_data().unwrap().unwrap();
|
let event_data: serde_json::Value = event.take_data().2.unwrap().try_into().unwrap();
|
||||||
assert_eq!(data, event_data);
|
assert_eq!(data, event_data);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn source_valid_relative_url() {
|
||||||
|
let res = EventBuilderV10::new()
|
||||||
|
.id("id1")
|
||||||
|
.source("/source") // relative URL
|
||||||
|
.ty("type")
|
||||||
|
.build();
|
||||||
|
assert_match_pattern!(res, Ok(_));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn build_missing_id() {
|
||||||
|
let res = EventBuilderV10::new()
|
||||||
|
.source("http://localhost:8080")
|
||||||
|
.build();
|
||||||
|
assert_match_pattern!(
|
||||||
|
res,
|
||||||
|
Err(EventBuilderError::MissingRequiredAttribute {
|
||||||
|
attribute_name: "id"
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn source_invalid_url() {
|
||||||
|
let res = EventBuilderV10::new().source("").build();
|
||||||
|
assert_match_pattern!(
|
||||||
|
res,
|
||||||
|
Err(EventBuilderError::InvalidUriRefError {
|
||||||
|
attribute_name: "source",
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn default_builds() {
|
||||||
|
let res = EventBuilderV10::default().build();
|
||||||
|
assert_match_pattern!(res, Ok(_));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,37 +1,42 @@
|
||||||
use super::Attributes;
|
use super::Attributes;
|
||||||
use crate::event::data::is_json_content_type;
|
use crate::event::data::is_json_content_type;
|
||||||
|
use crate::event::format::{
|
||||||
|
parse_data_base64, parse_data_base64_json, parse_data_json, parse_data_string,
|
||||||
|
};
|
||||||
use crate::event::{Data, ExtensionValue};
|
use crate::event::{Data, ExtensionValue};
|
||||||
|
use base64::prelude::*;
|
||||||
use chrono::{DateTime, Utc};
|
use chrono::{DateTime, Utc};
|
||||||
use serde::de::IntoDeserializer;
|
use serde::de::IntoDeserializer;
|
||||||
use serde::ser::SerializeMap;
|
use serde::ser::SerializeMap;
|
||||||
use serde::{Deserialize, Serializer};
|
use serde::{Deserialize, Serializer};
|
||||||
use serde_value::Value;
|
use serde_json::{Map, Value};
|
||||||
use std::collections::{BTreeMap, HashMap};
|
use std::collections::HashMap;
|
||||||
use url::Url;
|
use url::Url;
|
||||||
|
|
||||||
pub(crate) struct EventFormatDeserializer {}
|
pub(crate) struct EventFormatDeserializer {}
|
||||||
|
|
||||||
impl crate::event::format::EventFormatDeserializer for EventFormatDeserializer {
|
impl crate::event::format::EventFormatDeserializer for EventFormatDeserializer {
|
||||||
fn deserialize_attributes<E: serde::de::Error>(
|
fn deserialize_attributes<E: serde::de::Error>(
|
||||||
map: &mut BTreeMap<String, Value>,
|
map: &mut Map<String, Value>,
|
||||||
) -> Result<crate::event::Attributes, E> {
|
) -> Result<crate::event::Attributes, E> {
|
||||||
Ok(crate::event::Attributes::V10(Attributes {
|
Ok(crate::event::Attributes::V10(Attributes {
|
||||||
id: parse_field!(map, "id", String, E)?,
|
id: extract_field!(map, "id", String, E)?,
|
||||||
ty: parse_field!(map, "type", String, E)?,
|
ty: extract_field!(map, "type", String, E)?,
|
||||||
source: parse_field!(map, "source", String, E, Url::parse)?,
|
source: extract_field!(map, "source", String, E)?,
|
||||||
datacontenttype: parse_optional_field!(map, "datacontenttype", String, E)?,
|
datacontenttype: extract_optional_field!(map, "datacontenttype", String, E)?,
|
||||||
dataschema: parse_optional_field!(map, "dataschema", String, E, Url::parse)?,
|
dataschema: extract_optional_field!(map, "dataschema", String, E, |s: String| {
|
||||||
subject: parse_optional_field!(map, "subject", String, E)?,
|
Url::parse(&s)
|
||||||
time: parse_optional_field!(map, "time", String, E, |s| DateTime::parse_from_rfc3339(
|
})?,
|
||||||
s
|
subject: extract_optional_field!(map, "subject", String, E)?,
|
||||||
)
|
time: extract_optional_field!(map, "time", String, E, |s: String| {
|
||||||
.map(DateTime::<Utc>::from))?,
|
DateTime::parse_from_rfc3339(&s).map(DateTime::<Utc>::from)
|
||||||
|
})?,
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn deserialize_data<E: serde::de::Error>(
|
fn deserialize_data<E: serde::de::Error>(
|
||||||
content_type: &str,
|
content_type: &str,
|
||||||
map: &mut BTreeMap<String, Value>,
|
map: &mut Map<String, Value>,
|
||||||
) -> Result<Option<Data>, E> {
|
) -> Result<Option<Data>, E> {
|
||||||
let data = map.remove("data");
|
let data = map.remove("data");
|
||||||
let data_base64 = map.remove("data_base64");
|
let data_base64 = map.remove("data_base64");
|
||||||
|
@ -39,11 +44,16 @@ impl crate::event::format::EventFormatDeserializer for EventFormatDeserializer {
|
||||||
let is_json = is_json_content_type(content_type);
|
let is_json = is_json_content_type(content_type);
|
||||||
|
|
||||||
Ok(match (data, data_base64, is_json) {
|
Ok(match (data, data_base64, is_json) {
|
||||||
(Some(d), None, true) => Some(Data::Json(parse_data_json!(d, E)?)),
|
(Some(d), None, true) => Some(Data::Json(parse_data_json(d)?)),
|
||||||
(Some(d), None, false) => Some(Data::String(parse_data_string!(d, E)?)),
|
(Some(d), None, false) => Some(Data::String(parse_data_string(d)?)),
|
||||||
(None, Some(d), true) => Some(Data::Json(parse_json_data_base64!(d, E)?)),
|
(None, Some(d), true) => match parse_data_base64_json::<E>(d.to_owned()) {
|
||||||
(None, Some(d), false) => Some(Data::Binary(parse_data_base64!(d, E)?)),
|
Ok(x) => Some(Data::Json(x)),
|
||||||
(Some(_), Some(_), _) => Err(E::custom("Cannot have both data and data_base64 field"))?,
|
Err(_) => Some(Data::Binary(parse_data_base64(d)?)),
|
||||||
|
},
|
||||||
|
(None, Some(d), false) => Some(Data::Binary(parse_data_base64(d)?)),
|
||||||
|
(Some(_), Some(_), _) => {
|
||||||
|
return Err(E::custom("Cannot have both data and data_base64 field"))
|
||||||
|
}
|
||||||
(None, None, _) => None,
|
(None, None, _) => None,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -60,19 +70,19 @@ impl<S: serde::Serializer> crate::event::format::EventFormatSerializer<S, Attrib
|
||||||
extensions: &HashMap<String, ExtensionValue>,
|
extensions: &HashMap<String, ExtensionValue>,
|
||||||
serializer: S,
|
serializer: S,
|
||||||
) -> Result<<S as Serializer>::Ok, <S as Serializer>::Error> {
|
) -> Result<<S as Serializer>::Ok, <S as Serializer>::Error> {
|
||||||
let num =
|
let num = 4
|
||||||
3 + if attributes.datacontenttype.is_some() {
|
+ [
|
||||||
1
|
attributes.datacontenttype.is_some(),
|
||||||
} else {
|
attributes.dataschema.is_some(),
|
||||||
0
|
attributes.subject.is_some(),
|
||||||
} + if attributes.dataschema.is_some() {
|
attributes.time.is_some(),
|
||||||
1
|
data.is_some(),
|
||||||
} else {
|
]
|
||||||
0
|
.iter()
|
||||||
} + if attributes.subject.is_some() { 1 } else { 0 }
|
.filter(|&b| *b)
|
||||||
+ if attributes.time.is_some() { 1 } else { 0 }
|
.count()
|
||||||
+ if data.is_some() { 1 } else { 0 }
|
+ extensions.len();
|
||||||
+ extensions.len();
|
|
||||||
let mut state = serializer.serialize_map(Some(num))?;
|
let mut state = serializer.serialize_map(Some(num))?;
|
||||||
state.serialize_entry("specversion", "1.0")?;
|
state.serialize_entry("specversion", "1.0")?;
|
||||||
state.serialize_entry("id", &attributes.id)?;
|
state.serialize_entry("id", &attributes.id)?;
|
||||||
|
@ -93,7 +103,9 @@ impl<S: serde::Serializer> crate::event::format::EventFormatSerializer<S, Attrib
|
||||||
match data {
|
match data {
|
||||||
Some(Data::Json(j)) => state.serialize_entry("data", j)?,
|
Some(Data::Json(j)) => state.serialize_entry("data", j)?,
|
||||||
Some(Data::String(s)) => state.serialize_entry("data", s)?,
|
Some(Data::String(s)) => state.serialize_entry("data", s)?,
|
||||||
Some(Data::Binary(v)) => state.serialize_entry("data_base64", &base64::encode(v))?,
|
Some(Data::Binary(v)) => {
|
||||||
|
state.serialize_entry("data_base64", &BASE64_STANDARD.encode(v))?
|
||||||
|
}
|
||||||
_ => (),
|
_ => (),
|
||||||
};
|
};
|
||||||
for (k, v) in extensions {
|
for (k, v) in extensions {
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue