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
|
||||
|
||||
.idea
|
||||
.vscode
|
||||
.DS_Store
|
||||
**/Cargo.lock
|
||||
|
|
|
@ -39,7 +39,7 @@ cargo test --all-features --all
|
|||
To build and open the documentation:
|
||||
|
||||
```sh
|
||||
cargo doc --lib --open
|
||||
cargo doc --all-features --lib --open
|
||||
```
|
||||
|
||||
Before performing the PR, once you have committed all changes, run:
|
||||
|
|
123
Cargo.toml
123
Cargo.toml
|
@ -1,6 +1,6 @@
|
|||
[package]
|
||||
name = "cloudevents-sdk"
|
||||
version = "0.1.0"
|
||||
version = "0.8.0"
|
||||
authors = ["Francesco Guardiani <francescoguard@gmail.com>"]
|
||||
license-file = "LICENSE"
|
||||
edition = "2018"
|
||||
|
@ -11,42 +11,95 @@ repository = "https://github.com/cloudevents/sdk-rust"
|
|||
exclude = [
|
||||
".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
|
||||
|
||||
[dependencies]
|
||||
serde = { version = "^1.0", features = ["derive"] }
|
||||
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"
|
||||
# Enable all features when building on docs.rs to show feature gated bindings
|
||||
[package.metadata.docs.rs]
|
||||
all-features = true
|
||||
rustdoc-args = ["--cfg", "docsrs"]
|
||||
|
||||
[lib]
|
||||
name = "cloudevents"
|
||||
|
||||
[workspace]
|
||||
members = [
|
||||
".",
|
||||
"cloudevents-sdk-actix-web",
|
||||
"cloudevents-sdk-reqwest"
|
||||
]
|
||||
exclude = [
|
||||
"example-projects/actix-web-example",
|
||||
"example-projects/reqwest-wasm-example"
|
||||
]
|
||||
[features]
|
||||
http-binding = ["async-trait", "bytes", "futures", "http"]
|
||||
http-0-2-binding = ["async-trait", "bytes", "futures", "http-0-2"]
|
||||
actix = ["actix-web", "actix-http", "async-trait", "bytes", "futures", "http-0-2"]
|
||||
reqwest = ["reqwest-lib", "async-trait", "bytes", "http", "uuid/js"]
|
||||
rdkafka = ["rdkafka-lib", "bytes", "futures"]
|
||||
warp = ["warp-lib", "bytes", "http-0-2", "http-body-util", "hyper-0-14"]
|
||||
axum = ["bytes", "http", "hyper", "axum-lib", "http-body-util", "async-trait"]
|
||||
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.
|
||||
|
||||
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
|
||||
|
||||
| | [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: |
|
||||
| AMQP Protocol Binding | :x: | :x: |
|
||||
| AVRO Event Format | :x: | :x: |
|
||||
| HTTP Protocol Binding | :heavy_check_mark: | :heavy_check_mark: |
|
||||
| JSON Event Format | :heavy_check_mark: | :heavy_check_mark: |
|
||||
| Kafka Protocol Binding | :x: | :x: |
|
||||
| MQTT Protocol Binding | :x: | :x: |
|
||||
| NATS Protocol Binding | :x: | :x: |
|
||||
| Web hook | :x: | :x: |
|
||||
| CloudEvents Core | ✓ | ✓ |
|
||||
| AMQP Protocol Binding | ✕ | ✕ |
|
||||
| AVRO Event Format | ✕ | ✕ |
|
||||
| HTTP Protocol Binding | ✓ | ✓ |
|
||||
| JSON Event Format | ✓ | ✓ |
|
||||
| Kafka Protocol Binding | ✓ | ✓ |
|
||||
| MQTT Protocol Binding | ✕ | ✕ |
|
||||
| NATS Protocol Binding | ✓ | ✓ |
|
||||
| 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.
|
||||
* `cloudevents-sdk-actix-web`: Integration with [Actix Web](https://github.com/actix/actix-web).
|
||||
* `cloudevents-sdk-reqwest`: Integration with [reqwest](https://github.com/seanmonstar/reqwest).
|
||||
The core modules include definitions for the `Event` and
|
||||
`EventBuilder` data structures, JSON serialization rules, and a
|
||||
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
|
||||
|
||||
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
|
||||
cloudevents-sdk = "0.1.0"
|
||||
[dependencies]
|
||||
cloudevents-sdk = { version = "0.8.0" }
|
||||
```
|
||||
|
||||
Now you can start creating events:
|
||||
|
||||
```rust
|
||||
use cloudevents::EventBuilder;
|
||||
use cloudevents::{EventBuilder, EventBuilderV10};
|
||||
use url::Url;
|
||||
|
||||
let event = EventBuilder::v03()
|
||||
let event = EventBuilderV10::new()
|
||||
.id("aaa")
|
||||
.source(Url::parse("http://localhost").unwrap())
|
||||
.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)
|
||||
* [Axum Example](example-projects/axum-example)
|
||||
* [Reqwest/WASM Example](example-projects/reqwest-wasm-example)
|
||||
|
||||
## Development & Contributing
|
||||
|
||||
If you're interested in contributing to sdk-rust, look at [Contributing documentation](CONTRIBUTING.md)
|
||||
* [Kafka Example](example-projects/rdkafka-example)
|
||||
* [Warp Example](example-projects/warp-example)
|
||||
* [NATS Example](example-projects/nats-example)
|
||||
|
||||
## 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`
|
||||
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.io]: https://crates.io/crates/cloudevents-sdk
|
||||
[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]
|
||||
name = "actix-web-example"
|
||||
version = "0.1.0"
|
||||
version = "0.3.0"
|
||||
authors = ["Francesco Guardiani <francescoguard@gmail.com>"]
|
||||
edition = "2018"
|
||||
|
||||
[dependencies]
|
||||
cloudevents-sdk = { path = "../.." }
|
||||
cloudevents-sdk-actix-web = { path = "../../cloudevents-sdk-actix-web" }
|
||||
actix-web = "2"
|
||||
actix-rt = "1"
|
||||
actix-cors = "^0.2.0"
|
||||
lazy_static = "1.4.0"
|
||||
bytes = "^0.5"
|
||||
futures = "^0.3"
|
||||
cloudevents-sdk = { path = "../..", features = ["actix"] }
|
||||
actix-web = "4"
|
||||
actix-cors = "^0.7"
|
||||
serde_json = "^1.0"
|
||||
url = { version = "^2.1" }
|
||||
env_logger = "0.7.1"
|
||||
|
||||
[workspace]
|
||||
env_logger = "^0.11"
|
||||
|
|
|
@ -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 cloudevents::EventBuilder;
|
||||
use url::Url;
|
||||
use std::str::FromStr;
|
||||
use actix_web::{get, post, App, HttpServer};
|
||||
use cloudevents::{Event, EventBuilder, EventBuilderV10};
|
||||
use serde_json::json;
|
||||
|
||||
#[post("/")]
|
||||
async fn post_event(req: HttpRequest, payload: web::Payload) -> Result<String, actix_web::Error> {
|
||||
let event = cloudevents_sdk_actix_web::request_to_event(&req, payload).await?;
|
||||
async fn post_event(event: Event) -> Event {
|
||||
println!("Received Event: {:?}", event);
|
||||
Ok(format!("{:?}", event))
|
||||
event
|
||||
}
|
||||
|
||||
#[get("/")]
|
||||
async fn get_event() -> Result<HttpResponse, actix_web::Error> {
|
||||
async fn get_event() -> Event {
|
||||
let payload = json!({"hello": "world"});
|
||||
|
||||
Ok(cloudevents_sdk_actix_web::event_to_response(
|
||||
EventBuilder::new()
|
||||
.id("0001")
|
||||
.ty("example.test")
|
||||
.source(Url::from_str("http://localhost/").unwrap())
|
||||
.data("application/json", payload)
|
||||
.extension("someint", "10")
|
||||
.build(),
|
||||
HttpResponse::Ok()
|
||||
).await?)
|
||||
EventBuilderV10::new()
|
||||
.id("0001")
|
||||
.ty("example.test")
|
||||
.source("http://localhost/")
|
||||
.data("application/json", payload)
|
||||
.extension("someint", "10")
|
||||
.build()
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
#[actix_rt::main]
|
||||
#[actix_web::main]
|
||||
async fn main() -> std::io::Result<()> {
|
||||
std::env::set_var("RUST_LOG", "actix_server=info,actix_web=info");
|
||||
env_logger::init();
|
||||
|
||||
HttpServer::new(|| {
|
||||
App::new()
|
||||
.wrap(actix_cors::Cors::permissive())
|
||||
.wrap(actix_web::middleware::Logger::default())
|
||||
.wrap(actix_cors::Cors::new().finish())
|
||||
.service(post_event)
|
||||
.service(get_event)
|
||||
})
|
||||
.bind("127.0.0.1:9000")?
|
||||
.workers(1)
|
||||
.run()
|
||||
.await
|
||||
.bind("127.0.0.1:9000")?
|
||||
.workers(1)
|
||||
.run()
|
||||
.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]
|
||||
name = "reqwest-wasm-example"
|
||||
version = "0.1.0"
|
||||
version = "0.3.0"
|
||||
authors = ["Francesco Guardiani <francescoguard@gmail.com>"]
|
||||
edition = "2018"
|
||||
resolver = "2"
|
||||
|
||||
# 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"]
|
||||
|
||||
[dependencies]
|
||||
reqwest = "0.10.4"
|
||||
cloudevents-sdk = { path = "../.." }
|
||||
cloudevents-sdk-reqwest = { path = "../../cloudevents-sdk-reqwest" }
|
||||
reqwest = "^0.12"
|
||||
uuid = "1"
|
||||
cloudevents-sdk = { path = "../..", features = ["reqwest"] }
|
||||
url = { version = "^2.1" }
|
||||
web-sys = { version = "0.3.39", features = ["Window", "Location"] }
|
||||
wasm-bindgen-futures = "0.4.12"
|
||||
wasm-bindgen = { version = "0.2.62", features = ["serde-serialize"] }
|
||||
|
||||
[workspace]
|
||||
wasm-bindgen = { version = "0.2.77", features = ["serde-serialize"] }
|
||||
|
|
|
@ -1,13 +1,23 @@
|
|||
## 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
|
||||
|
||||
Then build the example locally with:
|
||||
And finally run the example:
|
||||
|
||||
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">
|
||||
<label class="col-md-4 control-label" for="event_target">Target</label>
|
||||
<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>
|
||||
|
@ -22,7 +22,7 @@
|
|||
<div class="form-group">
|
||||
<label class="col-md-4 control-label" for="event_type">Event Type</label>
|
||||
<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>
|
||||
|
||||
|
@ -30,7 +30,7 @@
|
|||
<div class="form-group">
|
||||
<label class="col-md-4 control-label" for="event_datacontenttype">Event Data Content Type</label>
|
||||
<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>
|
||||
|
||||
|
@ -54,4 +54,4 @@
|
|||
</form>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
</html>
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
import $ from 'jquery';
|
||||
|
||||
import 'bootstrap/dist/css/bootstrap.min.css';
|
||||
import 'bootstrap';
|
||||
import 'bootstrap/dist/css/bootstrap.min.css';
|
||||
|
||||
import("./pkg").then(rustModule => {
|
||||
$(document).ready(function () {
|
||||
$("#send").click(function () {
|
||||
$(function() {
|
||||
$("#send").on("click", function () {
|
||||
let target = $("#event_target").val()
|
||||
let ty = $("#event_type").val()
|
||||
let dataContentType = $("#event_datacontenttype").val()
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -1,21 +1,21 @@
|
|||
{
|
||||
"scripts": {
|
||||
"build": "webpack",
|
||||
"serve": "webpack-dev-server"
|
||||
"serve": "webpack serve"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@wasm-tool/wasm-pack-plugin": "^1.3.1",
|
||||
"css-loader": "^3.5.3",
|
||||
"html-webpack-plugin": "^3.2.0",
|
||||
"style-loader": "^1.2.1",
|
||||
"@wasm-tool/wasm-pack-plugin": "^1.4.0",
|
||||
"css-loader": "^5.2.6",
|
||||
"html-webpack-plugin": "^5.5.0",
|
||||
"style-loader": "^2.0.0",
|
||||
"text-encoding": "^0.7.0",
|
||||
"webpack": "^4.29.4",
|
||||
"webpack-cli": "^3.1.1",
|
||||
"webpack-dev-server": "^3.1.0"
|
||||
"webpack": "^5.95.0",
|
||||
"webpack-cli": "^4.8.0",
|
||||
"webpack-dev-server": "^5.0.4"
|
||||
},
|
||||
"dependencies": {
|
||||
"bootstrap": "^4.5.0",
|
||||
"jquery": "^3.5.1",
|
||||
"popper.js": "^1.16.1"
|
||||
"bootstrap": "^5.0.2",
|
||||
"jquery": "^3.6.0",
|
||||
"@popperjs/core": "^2.9.2"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,15 +1,28 @@
|
|||
use cloudevents::binding::reqwest::RequestBuilderExt;
|
||||
use cloudevents::{EventBuilder, EventBuilderV10};
|
||||
use wasm_bindgen::prelude::*;
|
||||
use uuid::Uuid;
|
||||
|
||||
#[wasm_bindgen]
|
||||
pub async fn run(target: String, ty: String, datacontenttype: String, data: String) -> Result<(), String> {
|
||||
let event = cloudevents::EventBuilder::new()
|
||||
pub async fn run(
|
||||
target: String,
|
||||
ty: String,
|
||||
datacontenttype: String,
|
||||
data: String,
|
||||
) -> Result<(), JsValue> {
|
||||
let event = EventBuilderV10::new()
|
||||
.id(&Uuid::new_v4().hyphenated().to_string())
|
||||
.ty(ty)
|
||||
.source("http://localhost/")
|
||||
.data(datacontenttype, data)
|
||||
.build();
|
||||
.build()
|
||||
.unwrap();
|
||||
|
||||
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())?
|
||||
.header("Access-Control-Allow-Origin", "*")
|
||||
.send()
|
||||
|
@ -17,4 +30,4 @@ pub async fn run(target: String, ty: String, datacontenttype: String, data: Stri
|
|||
.map_err(|e| e.to_string())?;
|
||||
|
||||
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 serde::Serializer;
|
||||
use std::fmt;
|
||||
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> {
|
||||
SpecVersion(SpecVersion),
|
||||
Boolean(&'a bool),
|
||||
Integer(&'a i64),
|
||||
String(&'a str),
|
||||
Binary(&'a [u8]),
|
||||
URI(&'a Url),
|
||||
URIRef(&'a Url),
|
||||
URIRef(&'a UriReference),
|
||||
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<'_> {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
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::URI(s) => f.write_str(&s.as_str()),
|
||||
AttributeValue::URIRef(s) => f.write_str(&s.as_str()),
|
||||
AttributeValue::Binary(b) => f.write_str(&BASE64_STANDARD.encode(b)),
|
||||
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::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).
|
||||
pub trait AttributesReader {
|
||||
/// 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).
|
||||
fn get_source(&self) -> &Url;
|
||||
fn source(&self) -> &UriReference;
|
||||
/// 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).
|
||||
fn get_type(&self) -> &str;
|
||||
fn ty(&self) -> &str;
|
||||
/// 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).
|
||||
fn get_dataschema(&self) -> Option<&Url>;
|
||||
fn dataschema(&self) -> Option<&Url>;
|
||||
/// 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).
|
||||
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).
|
||||
pub trait AttributesWriter {
|
||||
/// 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).
|
||||
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).
|
||||
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).
|
||||
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).
|
||||
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 {
|
||||
|
@ -63,122 +98,134 @@ pub(crate) trait AttributesConverter {
|
|||
fn into_v10(self) -> AttributesV10;
|
||||
}
|
||||
|
||||
pub(crate) trait DataAttributesWriter {
|
||||
fn set_datacontenttype(&mut self, datacontenttype: Option<impl Into<String>>);
|
||||
fn set_dataschema(&mut self, dataschema: Option<impl Into<Url>>);
|
||||
#[derive(PartialEq, Debug, Clone, Copy)]
|
||||
pub(crate) enum AttributesIter<'a> {
|
||||
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
|
||||
#[derive(PartialEq, Debug, Clone)]
|
||||
#[derive(PartialEq, Eq, Debug, Clone)]
|
||||
pub enum Attributes {
|
||||
V03(AttributesV03),
|
||||
V10(AttributesV10),
|
||||
}
|
||||
|
||||
impl AttributesReader for Attributes {
|
||||
fn get_id(&self) -> &str {
|
||||
fn id(&self) -> &str {
|
||||
match self {
|
||||
Attributes::V03(a) => a.get_id(),
|
||||
Attributes::V10(a) => a.get_id(),
|
||||
Attributes::V03(a) => a.id(),
|
||||
Attributes::V10(a) => a.id(),
|
||||
}
|
||||
}
|
||||
|
||||
fn get_source(&self) -> &Url {
|
||||
fn source(&self) -> &UriReference {
|
||||
match self {
|
||||
Attributes::V03(a) => a.get_source(),
|
||||
Attributes::V10(a) => a.get_source(),
|
||||
Attributes::V03(a) => a.source(),
|
||||
Attributes::V10(a) => a.source(),
|
||||
}
|
||||
}
|
||||
|
||||
fn get_specversion(&self) -> SpecVersion {
|
||||
fn specversion(&self) -> SpecVersion {
|
||||
match self {
|
||||
Attributes::V03(a) => a.get_specversion(),
|
||||
Attributes::V10(a) => a.get_specversion(),
|
||||
Attributes::V03(a) => a.specversion(),
|
||||
Attributes::V10(a) => a.specversion(),
|
||||
}
|
||||
}
|
||||
|
||||
fn get_type(&self) -> &str {
|
||||
fn ty(&self) -> &str {
|
||||
match self {
|
||||
Attributes::V03(a) => a.get_type(),
|
||||
Attributes::V10(a) => a.get_type(),
|
||||
Attributes::V03(a) => a.ty(),
|
||||
Attributes::V10(a) => a.ty(),
|
||||
}
|
||||
}
|
||||
|
||||
fn get_datacontenttype(&self) -> Option<&str> {
|
||||
fn datacontenttype(&self) -> Option<&str> {
|
||||
match self {
|
||||
Attributes::V03(a) => a.get_datacontenttype(),
|
||||
Attributes::V10(a) => a.get_datacontenttype(),
|
||||
Attributes::V03(a) => a.datacontenttype(),
|
||||
Attributes::V10(a) => a.datacontenttype(),
|
||||
}
|
||||
}
|
||||
|
||||
fn get_dataschema(&self) -> Option<&Url> {
|
||||
fn dataschema(&self) -> Option<&Url> {
|
||||
match self {
|
||||
Attributes::V03(a) => a.get_dataschema(),
|
||||
Attributes::V10(a) => a.get_dataschema(),
|
||||
Attributes::V03(a) => a.dataschema(),
|
||||
Attributes::V10(a) => a.dataschema(),
|
||||
}
|
||||
}
|
||||
|
||||
fn get_subject(&self) -> Option<&str> {
|
||||
fn subject(&self) -> Option<&str> {
|
||||
match self {
|
||||
Attributes::V03(a) => a.get_subject(),
|
||||
Attributes::V10(a) => a.get_subject(),
|
||||
Attributes::V03(a) => a.subject(),
|
||||
Attributes::V10(a) => a.subject(),
|
||||
}
|
||||
}
|
||||
|
||||
fn get_time(&self) -> Option<&DateTime<Utc>> {
|
||||
fn time(&self) -> Option<&DateTime<Utc>> {
|
||||
match self {
|
||||
Attributes::V03(a) => a.get_time(),
|
||||
Attributes::V10(a) => a.get_time(),
|
||||
Attributes::V03(a) => a.time(),
|
||||
Attributes::V10(a) => a.time(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
Attributes::V03(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 {
|
||||
Attributes::V03(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 {
|
||||
Attributes::V03(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 {
|
||||
Attributes::V03(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 {
|
||||
Attributes::V03(a) => a.set_time(time),
|
||||
Attributes::V10(a) => a.set_time(time),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl DataAttributesWriter for Attributes {
|
||||
fn set_datacontenttype(&mut self, datacontenttype: Option<impl Into<String>>) {
|
||||
fn set_datacontenttype(
|
||||
&mut self,
|
||||
datacontenttype: Option<impl Into<String>>,
|
||||
) -> Option<String> {
|
||||
match self {
|
||||
Attributes::V03(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 {
|
||||
Attributes::V03(a) => a.set_dataschema(dataschema),
|
||||
Attributes::V10(a) => a.set_dataschema(dataschema),
|
||||
|
@ -187,18 +234,25 @@ impl DataAttributesWriter for Attributes {
|
|||
}
|
||||
|
||||
impl Attributes {
|
||||
pub fn into_v10(self) -> Self {
|
||||
pub(crate) fn into_v10(self) -> Self {
|
||||
match self {
|
||||
Attributes::V03(v03) => Attributes::V10(v03.into_v10()),
|
||||
_ => self,
|
||||
}
|
||||
}
|
||||
pub fn into_v03(self) -> Self {
|
||||
pub(crate) fn into_v03(self) -> Self {
|
||||
match self {
|
||||
Attributes::V10(v10) => Attributes::V03(v10.into_v03()),
|
||||
_ => 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"))]
|
||||
|
@ -208,16 +262,15 @@ pub(crate) fn default_hostname() -> Url {
|
|||
"http://{}",
|
||||
hostname::get()
|
||||
.ok()
|
||||
.map(|s| s.into_string().ok())
|
||||
.flatten()
|
||||
.unwrap_or(String::from("localhost".to_string()))
|
||||
.and_then(|s| s.into_string().ok())
|
||||
.unwrap_or_else(|| "localhost".to_string())
|
||||
)
|
||||
.as_ref(),
|
||||
)
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
#[cfg(all(target_arch = "wasm32", target_os = "unknown"))]
|
||||
pub(crate) fn default_hostname() -> Url {
|
||||
use std::str::FromStr;
|
||||
|
||||
|
@ -230,3 +283,10 @@ pub(crate) fn default_hostname() -> Url {
|
|||
)
|
||||
.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 url::Url;
|
||||
///
|
||||
/// let event = EventBuilder::v10()
|
||||
/// let event = EventBuilderV10::new()
|
||||
/// .id("my_event.my_application")
|
||||
/// .source(Url::parse("http://localhost:8080").unwrap())
|
||||
/// .source("http://localhost:8080")
|
||||
/// .ty("example.demo")
|
||||
/// .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 {
|
||||
/// Creates a new builder for latest CloudEvents version
|
||||
pub fn new() -> EventBuilderV10 {
|
||||
return Self::v10();
|
||||
/// Build [`Event`]
|
||||
fn build(self) -> Result<Event, Error>;
|
||||
}
|
||||
|
||||
/// 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
|
||||
pub fn v10() -> EventBuilderV10 {
|
||||
return EventBuilderV10::new();
|
||||
#[test]
|
||||
fn v03_to_v10() {
|
||||
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
|
||||
pub fn v03() -> EventBuilderV03 {
|
||||
return EventBuilderV03::new();
|
||||
/// Test YAML
|
||||
/// This test checks if the usage of serde_json::Value makes the Deserialize implementation incompatible with
|
||||
/// 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
|
||||
#[derive(Debug, PartialEq, Clone)]
|
||||
#[derive(PartialEq, Eq, Debug, Clone)]
|
||||
pub enum Data {
|
||||
/// Event has a binary payload
|
||||
Binary(Vec<u8>),
|
||||
|
@ -11,59 +15,31 @@ pub enum Data {
|
|||
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 {
|
||||
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 {
|
||||
fn into(self) -> Data {
|
||||
Data::Json(self)
|
||||
impl From<serde_json::Value> for Data {
|
||||
fn from(value: Value) -> Self {
|
||||
Data::Json(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl Into<Data> for Vec<u8> {
|
||||
fn into(self) -> Data {
|
||||
Data::Binary(self)
|
||||
impl From<Vec<u8>> for Data {
|
||||
fn from(value: Vec<u8>) -> Self {
|
||||
Data::Binary(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl Into<Data> for String {
|
||||
fn into(self) -> Data {
|
||||
Data::String(self)
|
||||
impl From<String> for Data {
|
||||
fn from(value: 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> {
|
||||
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::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::fmt;
|
||||
|
||||
#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)]
|
||||
#[derive(PartialEq, Eq, Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(untagged)]
|
||||
/// Represents all the possible [CloudEvents extension](https://github.com/cloudevents/spec/blob/master/spec.md#extension-context-attributes) values
|
||||
pub enum ExtensionValue {
|
||||
/// Represents a [`String`](std::string::String) value.
|
||||
/// Represents a [`String`] value.
|
||||
String(String),
|
||||
/// Represents a [`bool`](bool) value.
|
||||
/// Represents a [`bool`] value.
|
||||
Boolean(bool),
|
||||
/// Represents an integer [`i64`](i64) value.
|
||||
/// Represents an integer [`i64`] value.
|
||||
Integer(i64),
|
||||
}
|
||||
|
||||
|
@ -59,3 +60,13 @@ impl ExtensionValue {
|
|||
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,
|
||||
};
|
||||
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_value::Value;
|
||||
use std::collections::{BTreeMap, 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()
|
||||
};
|
||||
}
|
||||
use serde_json::{Map, Value};
|
||||
use std::collections::HashMap;
|
||||
|
||||
macro_rules! parse_field {
|
||||
($map:ident, $name:literal, $value_variant:ident, $error:ty) => {
|
||||
parse_optional_field!($map, $name, $value_variant, $error)?
|
||||
($value:expr, $target_type:ty, $error:ty) => {
|
||||
<$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))
|
||||
};
|
||||
|
||||
($map:ident, $name:literal, $value_variant:ident, $error:ty, $mapper:expr) => {
|
||||
parse_optional_field!($map, $name, $value_variant, $error, $mapper)?
|
||||
($map:ident, $name:literal, $target_type:ty, $error:ty, $mapper:expr) => {
|
||||
extract_optional_field!($map, $name, $target_type, $error, $mapper)?
|
||||
.ok_or_else(|| <$error>::missing_field($name))
|
||||
};
|
||||
}
|
||||
|
||||
macro_rules! parse_data_json {
|
||||
($in:ident, $error:ty) => {
|
||||
Ok(serde_json::Value::deserialize($in.into_deserializer())
|
||||
.map_err(|e| <$error>::custom(e))?)
|
||||
};
|
||||
pub fn parse_data_json<E: serde::de::Error>(v: Value) -> Result<Value, E> {
|
||||
Value::deserialize(v.into_deserializer()).map_err(E::custom)
|
||||
}
|
||||
|
||||
macro_rules! parse_data_string {
|
||||
($in:ident, $error:ty) => {
|
||||
match $in {
|
||||
Value::String(s) => Ok(s),
|
||||
other => Err(E::invalid_type(
|
||||
crate::event::format::value_to_unexpected(&other),
|
||||
&"a string",
|
||||
)),
|
||||
}
|
||||
};
|
||||
pub fn parse_data_string<E: serde::de::Error>(v: Value) -> Result<String, E> {
|
||||
parse_field!(v, String, E)
|
||||
}
|
||||
|
||||
macro_rules! parse_json_data_base64 {
|
||||
($in:ident, $error:ty) => {{
|
||||
let data = parse_data_base64!($in, $error)?;
|
||||
serde_json::from_slice(&data).map_err(|e| <$error>::custom(e))
|
||||
}};
|
||||
pub fn parse_data_base64<E: serde::de::Error>(v: Value) -> Result<Vec<u8>, E> {
|
||||
parse_field!(v, String, E).and_then(|s| {
|
||||
BASE64_STANDARD
|
||||
.decode(s)
|
||||
.map_err(|e| E::custom(format_args!("decode error `{}`", e)))
|
||||
})
|
||||
}
|
||||
|
||||
macro_rules! parse_data_base64 {
|
||||
($in:ident, $error:ty) => {
|
||||
match $in {
|
||||
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 fn parse_data_base64_json<E: serde::de::Error>(v: Value) -> Result<Value, E> {
|
||||
let data = parse_data_base64(v)?;
|
||||
serde_json::from_slice(&data).map_err(E::custom)
|
||||
}
|
||||
|
||||
pub(crate) trait EventFormatDeserializer {
|
||||
fn deserialize_attributes<E: serde::de::Error>(
|
||||
map: &mut BTreeMap<String, Value>,
|
||||
map: &mut Map<String, Value>,
|
||||
) -> Result<Attributes, E>;
|
||||
|
||||
fn deserialize_data<E: serde::de::Error>(
|
||||
content_type: &str,
|
||||
map: &mut BTreeMap<String, Value>,
|
||||
map: &mut Map<String, Value>,
|
||||
) -> Result<Option<Data>, E>;
|
||||
|
||||
fn deserialize_event<E: serde::de::Error>(
|
||||
mut map: BTreeMap<String, Value>,
|
||||
) -> Result<Event, E> {
|
||||
fn deserialize_event<E: serde::de::Error>(mut map: Map<String, Value>) -> Result<Event, E> {
|
||||
let attributes = Self::deserialize_attributes(&mut map)?;
|
||||
let data = Self::deserialize_data(
|
||||
attributes
|
||||
.get_datacontenttype()
|
||||
.unwrap_or("application/json"),
|
||||
attributes.datacontenttype().unwrap_or("application/json"),
|
||||
&mut map,
|
||||
)?;
|
||||
let extensions = map
|
||||
.into_iter()
|
||||
.map(|(k, v)| Ok((k, ExtensionValue::deserialize(v.into_deserializer())?)))
|
||||
.collect::<Result<HashMap<String, ExtensionValue>, serde_value::DeserializerError>>()
|
||||
.map_err(|e| E::custom(e))?;
|
||||
.filter(|v| !v.1.is_null())
|
||||
.map(|(k, v)| {
|
||||
Ok((
|
||||
k,
|
||||
ExtensionValue::deserialize(v.into_deserializer()).map_err(E::custom)?,
|
||||
))
|
||||
})
|
||||
.collect::<Result<HashMap<String, ExtensionValue>, E>>()?;
|
||||
|
||||
Ok(Event {
|
||||
attributes,
|
||||
|
@ -139,20 +119,12 @@ impl<'de> Deserialize<'de> for Event {
|
|||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
let map = match Value::deserialize(deserializer)? {
|
||||
Value::Map(m) => Ok(m),
|
||||
v => Err(Error::invalid_type(value_to_unexpected(&v), &"a map")),
|
||||
}?;
|
||||
let root_value = Value::deserialize(deserializer)?;
|
||||
let mut map: Map<String, Value> =
|
||||
Map::deserialize(root_value.into_deserializer()).map_err(D::Error::custom)?;
|
||||
|
||||
let mut map: BTreeMap<String, Value> = map
|
||||
.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() {
|
||||
match extract_field!(map, "specversion", String, <D as Deserializer<'de>>::Error)?.as_str()
|
||||
{
|
||||
"0.3" => EventFormatDeserializerV03::deserialize_event(map),
|
||||
"1.0" => EventFormatDeserializerV10::deserialize_event(map),
|
||||
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,
|
||||
StructuredSerializer,
|
||||
};
|
||||
use crate::{EventBuilder, EventBuilderV03, EventBuilderV10};
|
||||
|
||||
impl StructuredDeserializer for Event {
|
||||
fn deserialize_structured<R, V: StructuredSerializer<R>>(self, visitor: V) -> Result<R> {
|
||||
|
@ -16,7 +17,7 @@ impl StructuredDeserializer for Event {
|
|||
|
||||
impl BinaryDeserializer for Event {
|
||||
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)?;
|
||||
for (k, v) in self.extensions.into_iter() {
|
||||
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>;
|
||||
}
|
||||
|
||||
pub(crate) trait AttributesSerializer {
|
||||
fn serialize_attribute(&mut self, name: &str, value: MessageAttributeValue) -> Result<()>;
|
||||
}
|
||||
|
||||
impl AttributesDeserializer for Attributes {
|
||||
fn deserialize_attributes<R: Sized, V: BinarySerializer<R>>(self, visitor: V) -> Result<V> {
|
||||
match self {
|
||||
|
@ -50,50 +47,202 @@ impl AttributesDeserializer for Attributes {
|
|||
}
|
||||
}
|
||||
|
||||
impl AttributesSerializer for Attributes {
|
||||
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),
|
||||
}
|
||||
pub(crate) trait AttributesSerializer {
|
||||
fn serialize_attribute(&mut self, name: &str, value: MessageAttributeValue) -> Result<()>;
|
||||
}
|
||||
|
||||
#[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 {
|
||||
fn set_structured_event(mut self, bytes: Vec<u8>) -> Result<Event> {
|
||||
let new_event: Event = serde_json::from_slice(&bytes)?;
|
||||
self.attributes = new_event.attributes;
|
||||
self.data = new_event.data;
|
||||
self.extensions = new_event.extensions;
|
||||
Ok(self)
|
||||
#[derive(Debug)]
|
||||
pub(crate) enum EventBinarySerializer {
|
||||
V10(EventBuilderV10),
|
||||
V03(EventBuilderV03),
|
||||
}
|
||||
|
||||
impl EventBinarySerializer {
|
||||
pub(crate) fn new() -> Self {
|
||||
EventBinarySerializer::V10(EventBuilderV10::new())
|
||||
}
|
||||
}
|
||||
|
||||
impl BinarySerializer<Event> for Event {
|
||||
fn set_spec_version(mut self, spec_version: SpecVersion) -> Result<Self> {
|
||||
match spec_version {
|
||||
SpecVersion::V03 => self.attributes = self.attributes.clone().into_v03(),
|
||||
SpecVersion::V10 => self.attributes = self.attributes.clone().into_v10(),
|
||||
}
|
||||
Ok(self)
|
||||
impl BinarySerializer<Event> for EventBinarySerializer {
|
||||
fn set_spec_version(self, spec_version: SpecVersion) -> Result<Self> {
|
||||
Ok(match spec_version {
|
||||
SpecVersion::V03 => EventBinarySerializer::V03(EventBuilderV03::new()),
|
||||
SpecVersion::V10 => EventBinarySerializer::V10(EventBuilderV10::new()),
|
||||
})
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
fn set_extension(mut self, name: &str, value: MessageAttributeValue) -> Result<Self> {
|
||||
self.extensions.insert(name.to_string(), value.into());
|
||||
Ok(self)
|
||||
fn set_extension(self, name: &str, value: MessageAttributeValue) -> Result<Self> {
|
||||
Ok(match 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> {
|
||||
self.data = Some(Data::from_binary(self.get_datacontenttype(), bytes)?);
|
||||
Ok(self)
|
||||
fn end_with_data(self, bytes: Vec<u8>) -> Result<Event> {
|
||||
Ok(match 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> {
|
||||
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 builder;
|
||||
mod data;
|
||||
mod event;
|
||||
mod extensions;
|
||||
#[macro_use]
|
||||
mod format;
|
||||
mod message;
|
||||
mod spec_version;
|
||||
mod types;
|
||||
|
||||
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 data::Data;
|
||||
pub use event::Event;
|
||||
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::ATTRIBUTE_NAMES as SPEC_VERSION_ATTRIBUTES;
|
||||
pub use spec_version::UnknownSpecVersion;
|
||||
pub use types::{TryIntoTime, TryIntoUrl, UriReference};
|
||||
|
||||
mod v03;
|
||||
|
||||
pub use v03::Attributes as AttributesV03;
|
||||
pub(crate) use v03::AttributesIntoIterator as AttributesIntoIteratorV03;
|
||||
pub use v03::EventBuilder as EventBuilderV03;
|
||||
pub(crate) use v03::EventFormatDeserializer as EventFormatDeserializerV03;
|
||||
pub(crate) use v03::EventFormatSerializer as EventFormatSerializerV03;
|
||||
|
@ -28,6 +33,261 @@ pub(crate) use v03::EventFormatSerializer as EventFormatSerializerV03;
|
|||
mod v10;
|
||||
|
||||
pub use v10::Attributes as AttributesV10;
|
||||
pub(crate) use v10::AttributesIntoIterator as AttributesIntoIteratorV10;
|
||||
pub use v10::EventBuilder as EventBuilderV10;
|
||||
pub(crate) use v10::EventFormatDeserializer as EventFormatDeserializerV10;
|
||||
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 lazy_static::lazy_static;
|
||||
use serde::export::Formatter;
|
||||
use std::collections::HashMap;
|
||||
use std::convert::TryFrom;
|
||||
use std::fmt;
|
||||
use std::fmt::Formatter;
|
||||
|
||||
lazy_static! {
|
||||
/// 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: [&str; 2] = ["0.3", "1.0"];
|
||||
|
||||
pub(crate) const SPEC_VERSIONS: [&'static str; 2] = ["0.3", "1.0"];
|
||||
|
||||
/// CloudEvent specification version
|
||||
/// CloudEvent specification version.
|
||||
#[derive(PartialEq, Eq, Hash, Debug, Clone)]
|
||||
pub enum SpecVersion {
|
||||
/// CloudEvents v0.3
|
||||
V03,
|
||||
/// CloudEvents v1.0
|
||||
V10,
|
||||
}
|
||||
|
||||
impl SpecVersion {
|
||||
/// Returns the string representation of [`SpecVersion`].
|
||||
#[inline]
|
||||
pub fn as_str(&self) -> &str {
|
||||
match self {
|
||||
SpecVersion::V03 => "0.3",
|
||||
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 {
|
||||
|
@ -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)]
|
||||
pub struct InvalidSpecVersion {
|
||||
pub struct UnknownSpecVersion {
|
||||
spec_version_value: String,
|
||||
}
|
||||
|
||||
impl fmt::Display for InvalidSpecVersion {
|
||||
impl fmt::Display for UnknownSpecVersion {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
|
||||
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 {
|
||||
type Error = InvalidSpecVersion;
|
||||
type Error = UnknownSpecVersion;
|
||||
|
||||
fn try_from(value: &str) -> Result<Self, InvalidSpecVersion> {
|
||||
fn try_from(value: &str) -> Result<Self, UnknownSpecVersion> {
|
||||
match value {
|
||||
"0.3" => Ok(SpecVersion::V03),
|
||||
"1.0" => Ok(SpecVersion::V10),
|
||||
_ => Err(InvalidSpecVersion {
|
||||
_ => Err(UnknownSpecVersion {
|
||||
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::{
|
||||
default_hostname, AttributeValue, AttributesConverter, DataAttributesWriter,
|
||||
};
|
||||
use crate::event::AttributesV10;
|
||||
use crate::event::{AttributesReader, AttributesWriter, SpecVersion};
|
||||
use crate::event::attributes::{default_hostname, AttributeValue, AttributesConverter};
|
||||
use crate::event::{AttributesReader, AttributesV10, AttributesWriter, SpecVersion, UriReference};
|
||||
use crate::message::{BinarySerializer, MessageAttributeValue};
|
||||
use chrono::{DateTime, Utc};
|
||||
use url::Url;
|
||||
use uuid::Uuid;
|
||||
|
||||
pub(crate) const ATTRIBUTE_NAMES: [&'static str; 8] = [
|
||||
pub(crate) const ATTRIBUTE_NAMES: [&str; 8] = [
|
||||
"specversion",
|
||||
"id",
|
||||
"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)
|
||||
#[derive(PartialEq, Debug, Clone)]
|
||||
#[derive(PartialEq, Eq, Debug, Clone)]
|
||||
pub struct Attributes {
|
||||
pub(crate) id: String,
|
||||
pub(crate) ty: String,
|
||||
pub(crate) source: Url,
|
||||
pub(crate) source: UriReference,
|
||||
pub(crate) datacontenttype: Option<String>,
|
||||
pub(crate) schemaurl: Option<Url>,
|
||||
pub(crate) subject: Option<String>,
|
||||
|
@ -42,34 +40,36 @@ impl<'a> IntoIterator for &'a Attributes {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(PartialEq, Debug, Clone, Copy)]
|
||||
pub struct AttributesIntoIterator<'a> {
|
||||
attributes: &'a Attributes,
|
||||
index: usize,
|
||||
pub(crate) attributes: &'a Attributes,
|
||||
pub(crate) index: usize,
|
||||
}
|
||||
|
||||
impl<'a> Iterator for AttributesIntoIterator<'a> {
|
||||
type Item = (&'a str, AttributeValue<'a>);
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
let result = match self.index {
|
||||
0 => Some(("id", AttributeValue::String(&self.attributes.id))),
|
||||
1 => Some(("type", AttributeValue::String(&self.attributes.ty))),
|
||||
2 => Some(("source", AttributeValue::URIRef(&self.attributes.source))),
|
||||
3 => self
|
||||
0 => Some(("specversion", AttributeValue::SpecVersion(SpecVersion::V03))),
|
||||
1 => Some(("id", AttributeValue::String(&self.attributes.id))),
|
||||
2 => Some(("type", AttributeValue::String(&self.attributes.ty))),
|
||||
3 => Some(("source", AttributeValue::URIRef(&self.attributes.source))),
|
||||
4 => self
|
||||
.attributes
|
||||
.datacontenttype
|
||||
.as_ref()
|
||||
.map(|v| ("datacontenttype", AttributeValue::String(v))),
|
||||
4 => self
|
||||
5 => self
|
||||
.attributes
|
||||
.schemaurl
|
||||
.as_ref()
|
||||
.map(|v| ("schemaurl", AttributeValue::URIRef(v))),
|
||||
5 => self
|
||||
.map(|v| ("schemaurl", AttributeValue::URI(v))),
|
||||
6 => self
|
||||
.attributes
|
||||
.subject
|
||||
.as_ref()
|
||||
.map(|v| ("subject", AttributeValue::String(v))),
|
||||
6 => self
|
||||
7 => self
|
||||
.attributes
|
||||
.time
|
||||
.as_ref()
|
||||
|
@ -85,68 +85,69 @@ impl<'a> Iterator for AttributesIntoIterator<'a> {
|
|||
}
|
||||
|
||||
impl AttributesReader for Attributes {
|
||||
fn get_id(&self) -> &str {
|
||||
fn id(&self) -> &str {
|
||||
&self.id
|
||||
}
|
||||
|
||||
fn get_source(&self) -> &Url {
|
||||
fn source(&self) -> &UriReference {
|
||||
&self.source
|
||||
}
|
||||
|
||||
fn get_specversion(&self) -> SpecVersion {
|
||||
fn specversion(&self) -> SpecVersion {
|
||||
SpecVersion::V03
|
||||
}
|
||||
|
||||
fn get_type(&self) -> &str {
|
||||
fn ty(&self) -> &str {
|
||||
&self.ty
|
||||
}
|
||||
|
||||
fn get_datacontenttype(&self) -> Option<&str> {
|
||||
fn datacontenttype(&self) -> Option<&str> {
|
||||
self.datacontenttype.as_deref()
|
||||
}
|
||||
|
||||
fn get_dataschema(&self) -> Option<&Url> {
|
||||
fn dataschema(&self) -> Option<&Url> {
|
||||
self.schemaurl.as_ref()
|
||||
}
|
||||
|
||||
fn get_subject(&self) -> Option<&str> {
|
||||
fn subject(&self) -> Option<&str> {
|
||||
self.subject.as_deref()
|
||||
}
|
||||
|
||||
fn get_time(&self) -> Option<&DateTime<Utc>> {
|
||||
fn time(&self) -> Option<&DateTime<Utc>> {
|
||||
self.time.as_ref()
|
||||
}
|
||||
}
|
||||
|
||||
impl AttributesWriter for Attributes {
|
||||
fn set_id(&mut self, id: impl Into<String>) {
|
||||
self.id = id.into()
|
||||
fn set_id(&mut self, id: impl Into<String>) -> String {
|
||||
std::mem::replace(&mut self.id, id.into())
|
||||
}
|
||||
|
||||
fn set_source(&mut self, source: impl Into<Url>) {
|
||||
self.source = source.into()
|
||||
fn set_source(&mut self, source: impl Into<UriReference>) -> UriReference {
|
||||
std::mem::replace(&mut self.source, source.into())
|
||||
}
|
||||
|
||||
fn set_type(&mut self, ty: impl Into<String>) {
|
||||
self.ty = ty.into()
|
||||
fn set_type(&mut self, ty: impl Into<String>) -> String {
|
||||
std::mem::replace(&mut self.ty, ty.into())
|
||||
}
|
||||
|
||||
fn set_subject(&mut self, subject: Option<impl Into<String>>) {
|
||||
self.subject = subject.map(Into::into)
|
||||
fn set_subject(&mut self, subject: Option<impl Into<String>>) -> Option<String> {
|
||||
std::mem::replace(&mut self.subject, subject.map(Into::into))
|
||||
}
|
||||
|
||||
fn set_time(&mut self, time: Option<impl Into<DateTime<Utc>>>) {
|
||||
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_time(&mut self, time: Option<impl Into<DateTime<Utc>>>) -> Option<DateTime<Utc>> {
|
||||
std::mem::replace(&mut self.time, time.map(Into::into))
|
||||
}
|
||||
|
||||
fn set_dataschema(&mut self, dataschema: Option<impl Into<Url>>) {
|
||||
self.schemaurl = dataschema.map(Into::into)
|
||||
fn set_datacontenttype(
|
||||
&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 {
|
||||
id: Uuid::new_v4().to_string(),
|
||||
ty: "type".to_string(),
|
||||
source: default_hostname(),
|
||||
source: default_hostname().to_string(),
|
||||
datacontenttype: None,
|
||||
schemaurl: 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)]
|
||||
mod tests {
|
||||
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]
|
||||
fn iterator_test_v03() {
|
||||
let a = Attributes {
|
||||
id: String::from("1"),
|
||||
ty: String::from("someType"),
|
||||
source: Url::parse("https://example.net").unwrap(),
|
||||
source: "https://example.net".into(),
|
||||
datacontenttype: None,
|
||||
schemaurl: None,
|
||||
subject: None,
|
||||
time: Some(DateTime::<Utc>::from_utc(
|
||||
NaiveDateTime::from_timestamp(61, 0),
|
||||
Utc,
|
||||
)),
|
||||
time: DateTime::from_timestamp(61, 0),
|
||||
};
|
||||
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!(
|
||||
("type", AttributeValue::String("someType")),
|
||||
|
@ -212,7 +260,7 @@ mod tests {
|
|||
assert_eq!(
|
||||
(
|
||||
"source",
|
||||
AttributeValue::URIRef(&Url::parse("https://example.net").unwrap())
|
||||
AttributeValue::URIRef(&"https://example.net".to_string())
|
||||
),
|
||||
b.next().unwrap()
|
||||
);
|
||||
|
|
|
@ -1,58 +1,68 @@
|
|||
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 std::collections::HashMap;
|
||||
use std::convert::TryInto;
|
||||
use url::Url;
|
||||
|
||||
/// Builder to create a CloudEvent V0.3
|
||||
#[derive(Clone, Debug)]
|
||||
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 {
|
||||
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 {
|
||||
self.event.set_id(id);
|
||||
return self;
|
||||
self.id = Some(id.into());
|
||||
self
|
||||
}
|
||||
|
||||
pub fn source(mut self, source: impl Into<Url>) -> Self {
|
||||
self.event.set_source(source);
|
||||
return self;
|
||||
pub fn source(mut self, source: impl Into<String>) -> Self {
|
||||
let source = source.into();
|
||||
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 {
|
||||
self.event.set_type(ty);
|
||||
return self;
|
||||
self.ty = Some(ty.into());
|
||||
self
|
||||
}
|
||||
|
||||
pub fn subject(mut self, subject: impl Into<String>) -> Self {
|
||||
self.event.set_subject(Some(subject));
|
||||
return self;
|
||||
self.subject = Some(subject.into());
|
||||
self
|
||||
}
|
||||
|
||||
pub fn time(mut self, time: impl Into<DateTime<Utc>>) -> Self {
|
||||
self.event.set_time(Some(time));
|
||||
return self;
|
||||
pub fn time(mut self, time: impl TryIntoTime) -> Self {
|
||||
match time.into_time() {
|
||||
Ok(u) => self.time = Some(u),
|
||||
Err(e) => {
|
||||
self.error = Some(EventBuilderError::ParseTimeError {
|
||||
attribute_name: "time",
|
||||
source: e,
|
||||
})
|
||||
}
|
||||
};
|
||||
self
|
||||
}
|
||||
|
||||
pub fn extension(
|
||||
|
@ -60,40 +70,158 @@ impl EventBuilder {
|
|||
extension_name: &str,
|
||||
extension_value: impl Into<ExtensionValue>,
|
||||
) -> Self {
|
||||
self.event.set_extension(extension_name, extension_value);
|
||||
return self;
|
||||
self.extensions
|
||||
.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 {
|
||||
self.event.write_data(datacontenttype, data);
|
||||
return self;
|
||||
self.datacontenttype = Some(datacontenttype.into());
|
||||
self.data = Some(data.into());
|
||||
self
|
||||
}
|
||||
|
||||
pub fn data_with_schema(
|
||||
mut self,
|
||||
datacontenttype: impl Into<String>,
|
||||
schemaurl: impl Into<Url>,
|
||||
schemaurl: impl TryIntoUrl,
|
||||
data: impl Into<Data>,
|
||||
) -> Self {
|
||||
self.event
|
||||
.write_data_with_schema(datacontenttype, schemaurl, data);
|
||||
return self;
|
||||
self.datacontenttype = Some(datacontenttype.into());
|
||||
match schemaurl.into_url() {
|
||||
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 {
|
||||
self.event
|
||||
fn build(self) -> Result<Event, EventBuilderError> {
|
||||
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)]
|
||||
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]
|
||||
fn build_event() {
|
||||
let id = "aaa";
|
||||
let source = Url::parse("http://localhost:8080").unwrap();
|
||||
let source = "http://localhost:8080";
|
||||
let ty = "bbb";
|
||||
let subject = "francesco";
|
||||
let time: DateTime<Utc> = Utc::now();
|
||||
|
@ -105,30 +233,71 @@ mod tests {
|
|||
"hello": "world"
|
||||
});
|
||||
|
||||
let event = EventBuilder::new()
|
||||
let mut event = EventBuilderV03::new()
|
||||
.id(id)
|
||||
.source(source.clone())
|
||||
.source(source.to_string())
|
||||
.ty(ty)
|
||||
.subject(subject)
|
||||
.time(time)
|
||||
.extension(extension_name, extension_value)
|
||||
.data_with_schema(content_type, schema.clone(), data.clone())
|
||||
.build();
|
||||
.build()
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(SpecVersion::V03, event.get_specversion());
|
||||
assert_eq!(id, event.get_id());
|
||||
assert_eq!(source, event.get_source().clone());
|
||||
assert_eq!(ty, event.get_type());
|
||||
assert_eq!(subject, event.get_subject().unwrap());
|
||||
assert_eq!(time, event.get_time().unwrap().clone());
|
||||
assert_eq!(SpecVersion::V03, event.specversion());
|
||||
assert_eq!(id, event.id());
|
||||
assert_eq!(source, event.source().clone());
|
||||
assert_eq!(ty, event.ty());
|
||||
assert_eq!(subject, event.subject().unwrap());
|
||||
assert_eq!(time, event.time().unwrap().clone());
|
||||
assert_eq!(
|
||||
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!(schema, event.get_dataschema().unwrap().clone());
|
||||
assert_eq!(content_type, event.datacontenttype().unwrap());
|
||||
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);
|
||||
}
|
||||
|
||||
#[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 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 base64::prelude::*;
|
||||
use chrono::{DateTime, Utc};
|
||||
use serde::de::IntoDeserializer;
|
||||
use serde::ser::SerializeMap;
|
||||
use serde::{Deserialize, Serializer};
|
||||
use serde_value::Value;
|
||||
use std::collections::{BTreeMap, HashMap};
|
||||
use serde_json::{Map, Value};
|
||||
use std::collections::HashMap;
|
||||
use url::Url;
|
||||
|
||||
pub(crate) struct EventFormatDeserializer {}
|
||||
|
||||
impl crate::event::format::EventFormatDeserializer for EventFormatDeserializer {
|
||||
fn deserialize_attributes<E: serde::de::Error>(
|
||||
map: &mut BTreeMap<String, Value>,
|
||||
map: &mut Map<String, Value>,
|
||||
) -> Result<crate::event::Attributes, E> {
|
||||
Ok(crate::event::Attributes::V03(Attributes {
|
||||
id: parse_field!(map, "id", String, E)?,
|
||||
ty: parse_field!(map, "type", String, E)?,
|
||||
source: parse_field!(map, "source", String, E, Url::parse)?,
|
||||
datacontenttype: parse_optional_field!(map, "datacontenttype", String, E)?,
|
||||
schemaurl: parse_optional_field!(map, "schemaurl", String, E, Url::parse)?,
|
||||
subject: parse_optional_field!(map, "subject", String, E)?,
|
||||
time: parse_optional_field!(map, "time", String, E, |s| DateTime::parse_from_rfc3339(
|
||||
s
|
||||
)
|
||||
.map(DateTime::<Utc>::from))?,
|
||||
id: extract_field!(map, "id", String, E)?,
|
||||
ty: extract_field!(map, "type", String, E)?,
|
||||
source: extract_field!(map, "source", String, E)?,
|
||||
datacontenttype: extract_optional_field!(map, "datacontenttype", String, E)?,
|
||||
schemaurl: extract_optional_field!(map, "schemaurl", String, E, |s: String| {
|
||||
Url::parse(&s)
|
||||
})?,
|
||||
subject: extract_optional_field!(map, "subject", String, E)?,
|
||||
time: extract_optional_field!(map, "time", String, E, |s: String| {
|
||||
DateTime::parse_from_rfc3339(&s).map(DateTime::<Utc>::from)
|
||||
})?,
|
||||
}))
|
||||
}
|
||||
|
||||
fn deserialize_data<E: serde::de::Error>(
|
||||
content_type: &str,
|
||||
map: &mut BTreeMap<String, Value>,
|
||||
map: &mut Map<String, Value>,
|
||||
) -> Result<Option<Data>, E> {
|
||||
let data = map.remove("data");
|
||||
let is_base64 = map
|
||||
.remove("datacontentencoding")
|
||||
.map(String::deserialize)
|
||||
.transpose()
|
||||
.map_err(|e| E::custom(e))?
|
||||
.map_err(E::custom)?
|
||||
.map(|dce| dce.to_lowercase() == "base64")
|
||||
.unwrap_or(false);
|
||||
let is_json = is_json_content_type(content_type);
|
||||
|
||||
Ok(match (data, is_base64, is_json) {
|
||||
(Some(d), false, true) => Some(Data::Json(parse_data_json!(d, E)?)),
|
||||
(Some(d), false, false) => Some(Data::String(parse_data_string!(d, E)?)),
|
||||
(Some(d), true, true) => Some(Data::Json(parse_json_data_base64!(d, E)?)),
|
||||
(Some(d), true, false) => Some(Data::Binary(parse_data_base64!(d, E)?)),
|
||||
(Some(d), false, true) => Some(Data::Json(parse_data_json(d)?)),
|
||||
(Some(d), false, false) => Some(Data::String(parse_data_string(d)?)),
|
||||
(Some(d), true, true) => Some(Data::Json(parse_data_base64_json(d)?)),
|
||||
(Some(d), true, false) => Some(Data::Binary(parse_data_base64(d)?)),
|
||||
(None, _, _) => None,
|
||||
})
|
||||
}
|
||||
|
@ -64,16 +69,19 @@ impl<S: serde::Serializer> crate::event::format::EventFormatSerializer<S, Attrib
|
|||
extensions: &HashMap<String, ExtensionValue>,
|
||||
serializer: S,
|
||||
) -> Result<<S as Serializer>::Ok, <S as Serializer>::Error> {
|
||||
let num =
|
||||
3 + if attributes.datacontenttype.is_some() {
|
||||
1
|
||||
} else {
|
||||
0
|
||||
} + if attributes.schemaurl.is_some() { 1 } else { 0 }
|
||||
+ if attributes.subject.is_some() { 1 } else { 0 }
|
||||
+ if attributes.time.is_some() { 1 } else { 0 }
|
||||
+ if data.is_some() { 1 } else { 0 }
|
||||
+ extensions.len();
|
||||
let num = 4
|
||||
+ [
|
||||
attributes.datacontenttype.is_some(),
|
||||
attributes.schemaurl.is_some(),
|
||||
attributes.subject.is_some(),
|
||||
attributes.time.is_some(),
|
||||
data.is_some(),
|
||||
]
|
||||
.iter()
|
||||
.filter(|&b| *b)
|
||||
.count()
|
||||
+ extensions.len();
|
||||
|
||||
let mut state = serializer.serialize_map(Some(num))?;
|
||||
state.serialize_entry("specversion", "0.3")?;
|
||||
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::String(s)) => state.serialize_entry("data", s)?,
|
||||
Some(Data::Binary(v)) => {
|
||||
state.serialize_entry("data", &base64::encode(v))?;
|
||||
state.serialize_entry("data", &BASE64_STANDARD.encode(v))?;
|
||||
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 builder;
|
||||
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(crate) use attributes::AttributesIntoIterator;
|
||||
pub(crate) use attributes::ATTRIBUTE_NAMES;
|
||||
pub use builder::EventBuilder;
|
||||
pub(crate) use format::EventFormatDeserializer;
|
||||
pub(crate) use format::EventFormatSerializer;
|
||||
|
|
|
@ -1,12 +1,12 @@
|
|||
use crate::event::attributes::{
|
||||
default_hostname, AttributeValue, AttributesConverter, DataAttributesWriter,
|
||||
};
|
||||
use crate::event::{AttributesReader, AttributesV03, AttributesWriter, SpecVersion};
|
||||
use crate::event::attributes::{default_hostname, AttributeValue, AttributesConverter};
|
||||
use crate::event::{AttributesReader, AttributesV03, AttributesWriter, SpecVersion, UriReference};
|
||||
use crate::message::{BinarySerializer, MessageAttributeValue};
|
||||
use chrono::{DateTime, Utc};
|
||||
use core::fmt::Debug;
|
||||
use url::Url;
|
||||
use uuid::Uuid;
|
||||
|
||||
pub(crate) const ATTRIBUTE_NAMES: [&'static str; 8] = [
|
||||
pub(crate) const ATTRIBUTE_NAMES: [&str; 8] = [
|
||||
"specversion",
|
||||
"id",
|
||||
"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)
|
||||
#[derive(PartialEq, Debug, Clone)]
|
||||
#[derive(PartialEq, Eq, Debug, Clone)]
|
||||
pub struct Attributes {
|
||||
pub(crate) id: String,
|
||||
pub(crate) ty: String,
|
||||
pub(crate) source: Url,
|
||||
pub(crate) source: UriReference,
|
||||
pub(crate) datacontenttype: Option<String>,
|
||||
pub(crate) dataschema: Option<Url>,
|
||||
pub(crate) subject: Option<String>,
|
||||
|
@ -41,34 +41,36 @@ impl<'a> IntoIterator for &'a Attributes {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(PartialEq, Debug, Clone, Copy)]
|
||||
pub struct AttributesIntoIterator<'a> {
|
||||
attributes: &'a Attributes,
|
||||
index: usize,
|
||||
pub(crate) attributes: &'a Attributes,
|
||||
pub(crate) index: usize,
|
||||
}
|
||||
|
||||
impl<'a> Iterator for AttributesIntoIterator<'a> {
|
||||
type Item = (&'a str, AttributeValue<'a>);
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
let result = match self.index {
|
||||
0 => Some(("id", AttributeValue::String(&self.attributes.id))),
|
||||
1 => Some(("type", AttributeValue::String(&self.attributes.ty))),
|
||||
2 => Some(("source", AttributeValue::URIRef(&self.attributes.source))),
|
||||
3 => self
|
||||
0 => Some(("specversion", AttributeValue::SpecVersion(SpecVersion::V10))),
|
||||
1 => Some(("id", AttributeValue::String(&self.attributes.id))),
|
||||
2 => Some(("type", AttributeValue::String(&self.attributes.ty))),
|
||||
3 => Some(("source", AttributeValue::URIRef(&self.attributes.source))),
|
||||
4 => self
|
||||
.attributes
|
||||
.datacontenttype
|
||||
.as_ref()
|
||||
.map(|v| ("datacontenttype", AttributeValue::String(v))),
|
||||
4 => self
|
||||
5 => self
|
||||
.attributes
|
||||
.dataschema
|
||||
.as_ref()
|
||||
.map(|v| ("dataschema", AttributeValue::URI(v))),
|
||||
5 => self
|
||||
6 => self
|
||||
.attributes
|
||||
.subject
|
||||
.as_ref()
|
||||
.map(|v| ("subject", AttributeValue::String(v))),
|
||||
6 => self
|
||||
7 => self
|
||||
.attributes
|
||||
.time
|
||||
.as_ref()
|
||||
|
@ -84,68 +86,69 @@ impl<'a> Iterator for AttributesIntoIterator<'a> {
|
|||
}
|
||||
|
||||
impl AttributesReader for Attributes {
|
||||
fn get_id(&self) -> &str {
|
||||
fn id(&self) -> &str {
|
||||
&self.id
|
||||
}
|
||||
|
||||
fn get_source(&self) -> &Url {
|
||||
fn source(&self) -> &UriReference {
|
||||
&self.source
|
||||
}
|
||||
|
||||
fn get_specversion(&self) -> SpecVersion {
|
||||
fn specversion(&self) -> SpecVersion {
|
||||
SpecVersion::V10
|
||||
}
|
||||
|
||||
fn get_type(&self) -> &str {
|
||||
fn ty(&self) -> &str {
|
||||
&self.ty
|
||||
}
|
||||
|
||||
fn get_datacontenttype(&self) -> Option<&str> {
|
||||
fn datacontenttype(&self) -> Option<&str> {
|
||||
self.datacontenttype.as_deref()
|
||||
}
|
||||
|
||||
fn get_dataschema(&self) -> Option<&Url> {
|
||||
fn dataschema(&self) -> Option<&Url> {
|
||||
self.dataschema.as_ref()
|
||||
}
|
||||
|
||||
fn get_subject(&self) -> Option<&str> {
|
||||
fn subject(&self) -> Option<&str> {
|
||||
self.subject.as_deref()
|
||||
}
|
||||
|
||||
fn get_time(&self) -> Option<&DateTime<Utc>> {
|
||||
fn time(&self) -> Option<&DateTime<Utc>> {
|
||||
self.time.as_ref()
|
||||
}
|
||||
}
|
||||
|
||||
impl AttributesWriter for Attributes {
|
||||
fn set_id(&mut self, id: impl Into<String>) {
|
||||
self.id = id.into()
|
||||
fn set_id(&mut self, id: impl Into<String>) -> String {
|
||||
std::mem::replace(&mut self.id, id.into())
|
||||
}
|
||||
|
||||
fn set_source(&mut self, source: impl Into<Url>) {
|
||||
self.source = source.into()
|
||||
fn set_source(&mut self, source: impl Into<UriReference>) -> UriReference {
|
||||
std::mem::replace(&mut self.source, source.into())
|
||||
}
|
||||
|
||||
fn set_type(&mut self, ty: impl Into<String>) {
|
||||
self.ty = ty.into()
|
||||
fn set_type(&mut self, ty: impl Into<String>) -> String {
|
||||
std::mem::replace(&mut self.ty, ty.into())
|
||||
}
|
||||
|
||||
fn set_subject(&mut self, subject: Option<impl Into<String>>) {
|
||||
self.subject = subject.map(Into::into)
|
||||
fn set_subject(&mut self, subject: Option<impl Into<String>>) -> Option<String> {
|
||||
std::mem::replace(&mut self.subject, subject.map(Into::into))
|
||||
}
|
||||
|
||||
fn set_time(&mut self, time: Option<impl Into<DateTime<Utc>>>) {
|
||||
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_time(&mut self, time: Option<impl Into<DateTime<Utc>>>) -> Option<DateTime<Utc>> {
|
||||
std::mem::replace(&mut self.time, time.map(Into::into))
|
||||
}
|
||||
|
||||
fn set_dataschema(&mut self, dataschema: Option<impl Into<Url>>) {
|
||||
self.dataschema = dataschema.map(Into::into)
|
||||
fn set_datacontenttype(
|
||||
&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 {
|
||||
id: Uuid::new_v4().to_string(),
|
||||
ty: "type".to_string(),
|
||||
source: default_hostname(),
|
||||
source: default_hostname().to_string(),
|
||||
datacontenttype: None,
|
||||
dataschema: 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 {
|
||||
fn into_v10(self) -> Self {
|
||||
self
|
||||
|
@ -184,25 +221,37 @@ impl AttributesConverter for Attributes {
|
|||
#[cfg(test)]
|
||||
mod tests {
|
||||
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]
|
||||
fn iterator_test_v10() {
|
||||
let a = Attributes {
|
||||
id: String::from("1"),
|
||||
ty: String::from("someType"),
|
||||
source: Url::parse("https://example.net").unwrap(),
|
||||
source: "https://example.net".into(),
|
||||
datacontenttype: None,
|
||||
dataschema: None,
|
||||
subject: None,
|
||||
time: Some(DateTime::<Utc>::from_utc(
|
||||
NaiveDateTime::from_timestamp(61, 0),
|
||||
Utc,
|
||||
)),
|
||||
time: DateTime::from_timestamp(61, 0),
|
||||
};
|
||||
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!(
|
||||
("type", AttributeValue::String("someType")),
|
||||
|
@ -211,7 +260,7 @@ mod tests {
|
|||
assert_eq!(
|
||||
(
|
||||
"source",
|
||||
AttributeValue::URIRef(&Url::parse("https://example.net").unwrap())
|
||||
AttributeValue::URIRef(&"https://example.net".to_string())
|
||||
),
|
||||
b.next().unwrap()
|
||||
);
|
||||
|
|
|
@ -1,58 +1,68 @@
|
|||
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 std::collections::HashMap;
|
||||
use std::convert::TryInto;
|
||||
use url::Url;
|
||||
|
||||
/// Builder to create a CloudEvent V1.0
|
||||
#[derive(Clone, Debug)]
|
||||
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 {
|
||||
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 {
|
||||
self.event.set_id(id);
|
||||
return self;
|
||||
self.id = Some(id.into());
|
||||
self
|
||||
}
|
||||
|
||||
pub fn source(mut self, source: impl Into<Url>) -> Self {
|
||||
self.event.set_source(source);
|
||||
return self;
|
||||
pub fn source(mut self, source: impl Into<String>) -> Self {
|
||||
let source = source.into();
|
||||
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 {
|
||||
self.event.set_type(ty);
|
||||
return self;
|
||||
self.ty = Some(ty.into());
|
||||
self
|
||||
}
|
||||
|
||||
pub fn subject(mut self, subject: impl Into<String>) -> Self {
|
||||
self.event.set_subject(Some(subject));
|
||||
return self;
|
||||
self.subject = Some(subject.into());
|
||||
self
|
||||
}
|
||||
|
||||
pub fn time(mut self, time: impl Into<DateTime<Utc>>) -> Self {
|
||||
self.event.set_time(Some(time));
|
||||
return self;
|
||||
pub fn time(mut self, time: impl TryIntoTime) -> Self {
|
||||
match time.into_time() {
|
||||
Ok(u) => self.time = Some(u),
|
||||
Err(e) => {
|
||||
self.error = Some(EventBuilderError::ParseTimeError {
|
||||
attribute_name: "time",
|
||||
source: e,
|
||||
})
|
||||
}
|
||||
};
|
||||
self
|
||||
}
|
||||
|
||||
pub fn extension(
|
||||
|
@ -60,40 +70,158 @@ impl EventBuilder {
|
|||
extension_name: &str,
|
||||
extension_value: impl Into<ExtensionValue>,
|
||||
) -> Self {
|
||||
self.event.set_extension(extension_name, extension_value);
|
||||
return self;
|
||||
self.extensions
|
||||
.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 {
|
||||
self.event.write_data(datacontenttype, data);
|
||||
return self;
|
||||
self.datacontenttype = Some(datacontenttype.into());
|
||||
self.data = Some(data.into());
|
||||
self
|
||||
}
|
||||
|
||||
pub fn data_with_schema(
|
||||
mut self,
|
||||
datacontenttype: impl Into<String>,
|
||||
dataschema: impl Into<Url>,
|
||||
schemaurl: impl TryIntoUrl,
|
||||
data: impl Into<Data>,
|
||||
) -> Self {
|
||||
self.event
|
||||
.write_data_with_schema(datacontenttype, dataschema, data);
|
||||
return self;
|
||||
self.datacontenttype = Some(datacontenttype.into());
|
||||
match schemaurl.into_url() {
|
||||
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 {
|
||||
self.event
|
||||
fn build(self) -> Result<Event, EventBuilderError> {
|
||||
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)]
|
||||
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]
|
||||
fn build_event() {
|
||||
let id = "aaa";
|
||||
let source = Url::parse("http://localhost:8080").unwrap();
|
||||
let source = "http://localhost:8080";
|
||||
let ty = "bbb";
|
||||
let subject = "francesco";
|
||||
let time: DateTime<Utc> = Utc::now();
|
||||
|
@ -105,30 +233,71 @@ mod tests {
|
|||
"hello": "world"
|
||||
});
|
||||
|
||||
let event = EventBuilder::new()
|
||||
let mut event = EventBuilderV10::new()
|
||||
.id(id)
|
||||
.source(source.clone())
|
||||
.source(source.to_string())
|
||||
.ty(ty)
|
||||
.subject(subject)
|
||||
.time(time)
|
||||
.extension(extension_name, extension_value)
|
||||
.data_with_schema(content_type, schema.clone(), data.clone())
|
||||
.build();
|
||||
.build()
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(SpecVersion::V10, event.get_specversion());
|
||||
assert_eq!(id, event.get_id());
|
||||
assert_eq!(source, event.get_source().clone());
|
||||
assert_eq!(ty, event.get_type());
|
||||
assert_eq!(subject, event.get_subject().unwrap());
|
||||
assert_eq!(time, event.get_time().unwrap().clone());
|
||||
assert_eq!(SpecVersion::V10, event.specversion());
|
||||
assert_eq!(id, event.id());
|
||||
assert_eq!(source, event.source().clone());
|
||||
assert_eq!(ty, event.ty());
|
||||
assert_eq!(subject, event.subject().unwrap());
|
||||
assert_eq!(time, event.time().unwrap().clone());
|
||||
assert_eq!(
|
||||
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!(schema, event.get_dataschema().unwrap().clone());
|
||||
assert_eq!(content_type, event.datacontenttype().unwrap());
|
||||
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);
|
||||
}
|
||||
|
||||
#[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 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 base64::prelude::*;
|
||||
use chrono::{DateTime, Utc};
|
||||
use serde::de::IntoDeserializer;
|
||||
use serde::ser::SerializeMap;
|
||||
use serde::{Deserialize, Serializer};
|
||||
use serde_value::Value;
|
||||
use std::collections::{BTreeMap, HashMap};
|
||||
use serde_json::{Map, Value};
|
||||
use std::collections::HashMap;
|
||||
use url::Url;
|
||||
|
||||
pub(crate) struct EventFormatDeserializer {}
|
||||
|
||||
impl crate::event::format::EventFormatDeserializer for EventFormatDeserializer {
|
||||
fn deserialize_attributes<E: serde::de::Error>(
|
||||
map: &mut BTreeMap<String, Value>,
|
||||
map: &mut Map<String, Value>,
|
||||
) -> Result<crate::event::Attributes, E> {
|
||||
Ok(crate::event::Attributes::V10(Attributes {
|
||||
id: parse_field!(map, "id", String, E)?,
|
||||
ty: parse_field!(map, "type", String, E)?,
|
||||
source: parse_field!(map, "source", String, E, Url::parse)?,
|
||||
datacontenttype: parse_optional_field!(map, "datacontenttype", String, E)?,
|
||||
dataschema: parse_optional_field!(map, "dataschema", String, E, Url::parse)?,
|
||||
subject: parse_optional_field!(map, "subject", String, E)?,
|
||||
time: parse_optional_field!(map, "time", String, E, |s| DateTime::parse_from_rfc3339(
|
||||
s
|
||||
)
|
||||
.map(DateTime::<Utc>::from))?,
|
||||
id: extract_field!(map, "id", String, E)?,
|
||||
ty: extract_field!(map, "type", String, E)?,
|
||||
source: extract_field!(map, "source", String, E)?,
|
||||
datacontenttype: extract_optional_field!(map, "datacontenttype", String, E)?,
|
||||
dataschema: extract_optional_field!(map, "dataschema", String, E, |s: String| {
|
||||
Url::parse(&s)
|
||||
})?,
|
||||
subject: extract_optional_field!(map, "subject", String, E)?,
|
||||
time: extract_optional_field!(map, "time", String, E, |s: String| {
|
||||
DateTime::parse_from_rfc3339(&s).map(DateTime::<Utc>::from)
|
||||
})?,
|
||||
}))
|
||||
}
|
||||
|
||||
fn deserialize_data<E: serde::de::Error>(
|
||||
content_type: &str,
|
||||
map: &mut BTreeMap<String, Value>,
|
||||
map: &mut Map<String, Value>,
|
||||
) -> Result<Option<Data>, E> {
|
||||
let data = map.remove("data");
|
||||
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);
|
||||
|
||||
Ok(match (data, data_base64, is_json) {
|
||||
(Some(d), None, true) => Some(Data::Json(parse_data_json!(d, E)?)),
|
||||
(Some(d), None, false) => Some(Data::String(parse_data_string!(d, E)?)),
|
||||
(None, Some(d), true) => Some(Data::Json(parse_json_data_base64!(d, E)?)),
|
||||
(None, Some(d), false) => Some(Data::Binary(parse_data_base64!(d, E)?)),
|
||||
(Some(_), Some(_), _) => Err(E::custom("Cannot have both data and data_base64 field"))?,
|
||||
(Some(d), None, true) => Some(Data::Json(parse_data_json(d)?)),
|
||||
(Some(d), None, false) => Some(Data::String(parse_data_string(d)?)),
|
||||
(None, Some(d), true) => match parse_data_base64_json::<E>(d.to_owned()) {
|
||||
Ok(x) => Some(Data::Json(x)),
|
||||
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,
|
||||
})
|
||||
}
|
||||
|
@ -60,19 +70,19 @@ impl<S: serde::Serializer> crate::event::format::EventFormatSerializer<S, Attrib
|
|||
extensions: &HashMap<String, ExtensionValue>,
|
||||
serializer: S,
|
||||
) -> Result<<S as Serializer>::Ok, <S as Serializer>::Error> {
|
||||
let num =
|
||||
3 + if attributes.datacontenttype.is_some() {
|
||||
1
|
||||
} else {
|
||||
0
|
||||
} + if attributes.dataschema.is_some() {
|
||||
1
|
||||
} else {
|
||||
0
|
||||
} + if attributes.subject.is_some() { 1 } else { 0 }
|
||||
+ if attributes.time.is_some() { 1 } else { 0 }
|
||||
+ if data.is_some() { 1 } else { 0 }
|
||||
+ extensions.len();
|
||||
let num = 4
|
||||
+ [
|
||||
attributes.datacontenttype.is_some(),
|
||||
attributes.dataschema.is_some(),
|
||||
attributes.subject.is_some(),
|
||||
attributes.time.is_some(),
|
||||
data.is_some(),
|
||||
]
|
||||
.iter()
|
||||
.filter(|&b| *b)
|
||||
.count()
|
||||
+ extensions.len();
|
||||
|
||||
let mut state = serializer.serialize_map(Some(num))?;
|
||||
state.serialize_entry("specversion", "1.0")?;
|
||||
state.serialize_entry("id", &attributes.id)?;
|
||||
|
@ -93,7 +103,9 @@ impl<S: serde::Serializer> crate::event::format::EventFormatSerializer<S, Attrib
|
|||
match data {
|
||||
Some(Data::Json(j)) => state.serialize_entry("data", j)?,
|
||||
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 {
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue