Compare commits

..

No commits in common. "main" and "0.1.0" have entirely different histories.
main ... 0.1.0

118 changed files with 5787 additions and 15118 deletions

66
.github/workflows/master.yml vendored Normal file
View File

@ -0,0 +1,66 @@
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

60
.github/workflows/pr.yml vendored Normal file
View File

@ -0,0 +1,60 @@
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

View File

@ -1,29 +0,0 @@
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

View File

@ -1,181 +0,0 @@
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

2
.gitignore vendored
View File

@ -1,6 +1,4 @@
**/target **/target
.idea .idea
.vscode
.DS_Store
**/Cargo.lock **/Cargo.lock

View File

@ -39,7 +39,7 @@ cargo test --all-features --all
To build and open the documentation: To build and open the documentation:
```sh ```sh
cargo doc --all-features --lib --open cargo doc --lib --open
``` ```
Before performing the PR, once you have committed all changes, run: Before performing the PR, once you have committed all changes, run:

View File

@ -1,6 +1,6 @@
[package] [package]
name = "cloudevents-sdk" name = "cloudevents-sdk"
version = "0.8.0" version = "0.1.0"
authors = ["Francesco Guardiani <francescoguard@gmail.com>"] authors = ["Francesco Guardiani <francescoguard@gmail.com>"]
license-file = "LICENSE" license-file = "LICENSE"
edition = "2018" edition = "2018"
@ -11,95 +11,42 @@ repository = "https://github.com/cloudevents/sdk-rust"
exclude = [ exclude = [
".github/*" ".github/*"
] ]
categories = ["web-programming", "encoding", "data-structures"]
resolver = "2"
# Enable all features when building on docs.rs to show feature gated bindings # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[package.metadata.docs.rs]
all-features = true
rustdoc-args = ["--cfg", "docsrs"]
[lib]
name = "cloudevents"
[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] [dependencies]
serde = { version = "^1.0", features = ["derive"] } serde = { version = "^1.0", features = ["derive"] }
serde_json = "^1.0" serde_json = "^1.0"
serde-value = "^0.6"
chrono = { version = "^0.4", features = ["serde"] } chrono = { version = "^0.4", features = ["serde"] }
delegate-attr = "^0.3" delegate = "^0.4"
base64 = "^0.22" base64 = "^0.12"
url = { version = "^2.5", features = ["serde"] } url = { version = "^2.1", features = ["serde"] }
snafu = "^0.8" snafu = "^0.6"
bitflags = "^2.6" lazy_static = "1.4.0"
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] [target."cfg(not(target_arch = \"wasm32\"))".dependencies]
hostname = "^0.4" hostname = "^0.3"
uuid = { version = "^0.8", features = ["v4"] }
[target.'cfg(all(target_arch = "wasm32", target_os = "unknown"))'.dependencies] [target.'cfg(target_arch = "wasm32")'.dependencies]
web-sys = { version = "^0.3", features = ["Window", "Location"] } web-sys = { version = "^0.3", features = ["Window", "Location"] }
uuid = { version = "^0.8", features = ["v4", "wasm-bindgen"] }
[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] [dev-dependencies]
rstest = "0.23" rstest = "0.6"
claims = "0.8" claim = "0.3.1"
version-sync = "0.9.2"
serde_yaml = "^0.9"
rmp-serde = "1"
# runtime dev-deps [lib]
name = "cloudevents"
url = { version = "^2.1", features = ["serde"] } [workspace]
serde_json = { version = "^1.0" } members = [
chrono = { version = "^0.4", features = ["serde"] } ".",
mockito = "0.31.1" "cloudevents-sdk-actix-web",
mime = "0.3" "cloudevents-sdk-reqwest"
]
exclude = [
[target.'cfg(not(target_os = "wasi"))'.dev-dependencies] "example-projects/actix-web-example",
actix-rt = { version = "^2" } "example-projects/reqwest-wasm-example"
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",
] }

View File

@ -1,7 +0,0 @@
# 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)

6
OWNERS
View File

@ -1,6 +0,0 @@
admins:
- jcrossley3
- linuxbasic
- slinkydeveloper
- Lazzaretti
approvers:

View File

@ -2,70 +2,57 @@
This project implements the [CloudEvents](https://cloudevents.io/) Spec for Rust. This project implements the [CloudEvents](https://cloudevents.io/) Spec for Rust.
Note: This project is WIP under active development, hence all APIs are considered unstable. Note: This projecets is WIP under active development, hence all APIs are considered unstable.
## Spec support ## Spec support
| | [v0.3](https://github.com/cloudevents/spec/tree/v0.3) | [v1.0](https://github.com/cloudevents/spec/tree/v1.0) | | | [v0.3](https://github.com/cloudevents/spec/tree/v0.3) | [v1.0](https://github.com/cloudevents/spec/tree/v1.0) |
| :---------------------------: | :----------------------------------------------------------------------------: | :---------------------------------------------------------------------------------: | | :---------------------------: | :----------------------------------------------------------------------------: | :---------------------------------------------------------------------------------: |
| CloudEvents Core | ✓ | ✓ | | CloudEvents Core | :heavy_check_mark: | :heavy_check_mark: |
| AMQP Protocol Binding | ✕ | ✕ | | AMQP Protocol Binding | :x: | :x: |
| AVRO Event Format | ✕ | ✕ | | AVRO Event Format | :x: | :x: |
| HTTP Protocol Binding | ✓ | ✓ | | HTTP Protocol Binding | :heavy_check_mark: | :heavy_check_mark: |
| JSON Event Format | ✓ | ✓ | | JSON Event Format | :heavy_check_mark: | :heavy_check_mark: |
| Kafka Protocol Binding | ✓ | ✓ | | Kafka Protocol Binding | :x: | :x: |
| MQTT Protocol Binding | ✕ | ✕ | | MQTT Protocol Binding | :x: | :x: |
| NATS Protocol Binding | ✓ | ✓ | | NATS Protocol Binding | :x: | :x: |
| Web hook | ✕ | ✕ | | Web hook | :x: | :x: |
## Crate Structure ## Crates
The core modules include definitions for the `Event` and * `cloudevents-sdk`: Provides Event data structure, JSON Event format implementation. This module is tested to work with GNU libc, WASM and musl toolchains.
`EventBuilder` data structures, JSON serialization rules, and a * `cloudevents-sdk-actix-web`: Integration with [Actix Web](https://github.com/actix/actix-web).
mechanism to support various Protocol Bindings, each of which is * `cloudevents-sdk-reqwest`: Integration with [reqwest](https://github.com/seanmonstar/reqwest).
enabled by a specific [feature flag]:
* `actix`: Integration with [actix](https://actix.rs/).
* `axum`: Integration with [axum](https://lib.rs/crates/axum).
* `warp`: Integration with [warp](https://github.com/seanmonstar/warp/).
* `reqwest`: Integration with [reqwest](https://github.com/seanmonstar/reqwest).
* `rdkafka`: Integration with [rdkafka](https://fede1024.github.io/rust-rdkafka).
* `nats`: Integration with [nats](https://github.com/nats-io/nats.rs)
This crate is continuously tested to work with GNU libc, WASM and musl
toolchains.
## Get Started ## Get Started
To get started, add the dependency to `Cargo.toml`, optionally To get started, add the dependency to `Cargo.toml`:
enabling your Protocol Binding of choice:
```toml ```toml
[dependencies] cloudevents-sdk = "0.1.0"
cloudevents-sdk = { version = "0.8.0" }
``` ```
Now you can start creating events: Now you can start creating events:
```rust ```rust
use cloudevents::{EventBuilder, EventBuilderV10}; use cloudevents::EventBuilder;
use url::Url; use url::Url;
let event = EventBuilderV10::new() let event = EventBuilder::v03()
.id("aaa") .id("aaa")
.source(Url::parse("http://localhost").unwrap()) .source(Url::parse("http://localhost").unwrap())
.ty("example.demo") .ty("example.demo")
.build()?; .build();
``` ```
Checkout the examples using our integrations to learn how to send and receive events: Checkout the examples using our integrations with `actix-web` and `reqwest` to learn how to send and receive events:
* [Actix Web Example](example-projects/actix-web-example) * [Actix Web Example](example-projects/actix-web-example)
* [Axum Example](example-projects/axum-example)
* [Reqwest/WASM Example](example-projects/reqwest-wasm-example) * [Reqwest/WASM Example](example-projects/reqwest-wasm-example)
* [Kafka Example](example-projects/rdkafka-example)
* [Warp Example](example-projects/warp-example) ## Development & Contributing
* [NATS Example](example-projects/nats-example)
If you're interested in contributing to sdk-rust, look at [Contributing documentation](CONTRIBUTING.md)
## Community ## Community
@ -82,30 +69,7 @@ Checkout the examples using our integrations to learn how to send and receive ev
- Contact for additional information: Francesco Guardiani (`@slinkydeveloper` - Contact for additional information: Francesco Guardiani (`@slinkydeveloper`
on slack). on slack).
Each SDK may have its own unique processes, tooling and guidelines, common
governance related material can be found in the
[CloudEvents `community`](https://github.com/cloudevents/spec/tree/master/community)
directory. In particular, in there you will find information concerning
how SDK projects are
[managed](https://github.com/cloudevents/spec/blob/master/community/SDK-GOVERNANCE.md),
[guidelines](https://github.com/cloudevents/spec/blob/master/community/SDK-maintainer-guidelines.md)
for how PR reviews and approval, and our
[Code of Conduct](https://github.com/cloudevents/spec/blob/master/community/GOVERNANCE.md#additional-information)
information.
If there is a security concern with one of the CloudEvents specifications, or
with one of the project's SDKs, please send an email to
[cncf-cloudevents-security@lists.cncf.io](mailto:cncf-cloudevents-security@lists.cncf.io).
[Crates badge]: https://img.shields.io/crates/v/cloudevents-sdk.svg [Crates badge]: https://img.shields.io/crates/v/cloudevents-sdk.svg
[crates.io]: https://crates.io/crates/cloudevents-sdk [crates.io]: https://crates.io/crates/cloudevents-sdk
[Docs badge]: https://docs.rs/cloudevents-sdk/badge.svg [Docs badge]: https://docs.rs/cloudevents-sdk/badge.svg
[docs.rs]: https://docs.rs/cloudevents-sdk [docs.rs]: https://docs.rs/cloudevents-sdk
[feature flag]: https://doc.rust-lang.org/cargo/reference/manifest.html#the-features-section
## Additional SDK Resources
- [List of current active maintainers](MAINTAINERS.md)
- [How to contribute to the project](CONTRIBUTING.md)
- [SDK's License](LICENSE)
- [SDK's Release process](RELEASING.md)

View File

@ -1,10 +0,0 @@
# 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!

View File

@ -0,0 +1,23 @@
[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"] }

View File

@ -0,0 +1,70 @@
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");
}

View File

@ -0,0 +1,9 @@
#[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;

View File

@ -0,0 +1,172 @@
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);
}
}

View File

@ -0,0 +1,183 @@
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())
}
}

View File

@ -0,0 +1,27 @@
[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" }

View File

@ -0,0 +1,172 @@
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();
}
}

View File

@ -0,0 +1,206 @@
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);
}
}

View File

@ -0,0 +1,63 @@
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");
}

View File

@ -0,0 +1,9 @@
#[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;

View File

@ -1,13 +1,20 @@
[package] [package]
name = "actix-web-example" name = "actix-web-example"
version = "0.3.0" version = "0.1.0"
authors = ["Francesco Guardiani <francescoguard@gmail.com>"] authors = ["Francesco Guardiani <francescoguard@gmail.com>"]
edition = "2018" edition = "2018"
[dependencies] [dependencies]
cloudevents-sdk = { path = "../..", features = ["actix"] } cloudevents-sdk = { path = "../.." }
actix-web = "4" cloudevents-sdk-actix-web = { path = "../../cloudevents-sdk-actix-web" }
actix-cors = "^0.7" actix-web = "2"
actix-rt = "1"
actix-cors = "^0.2.0"
lazy_static = "1.4.0"
bytes = "^0.5"
futures = "^0.3"
serde_json = "^1.0" serde_json = "^1.0"
url = { version = "^2.1" } url = { version = "^2.1" }
env_logger = "^0.11" env_logger = "0.7.1"
[workspace]

View File

@ -1,23 +0,0 @@
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
```

View File

@ -1,36 +1,41 @@
use actix_web::{get, post, App, HttpServer}; use actix_web::{get, post, web, App, HttpRequest, HttpResponse, HttpServer};
use cloudevents::{Event, EventBuilder, EventBuilderV10}; use cloudevents::EventBuilder;
use url::Url;
use std::str::FromStr;
use serde_json::json; use serde_json::json;
#[post("/")] #[post("/")]
async fn post_event(event: Event) -> Event { 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?;
println!("Received Event: {:?}", event); println!("Received Event: {:?}", event);
event Ok(format!("{:?}", event))
} }
#[get("/")] #[get("/")]
async fn get_event() -> Event { async fn get_event() -> Result<HttpResponse, actix_web::Error> {
let payload = json!({"hello": "world"}); let payload = json!({"hello": "world"});
EventBuilderV10::new() Ok(cloudevents_sdk_actix_web::event_to_response(
EventBuilder::new()
.id("0001") .id("0001")
.ty("example.test") .ty("example.test")
.source("http://localhost/") .source(Url::from_str("http://localhost/").unwrap())
.data("application/json", payload) .data("application/json", payload)
.extension("someint", "10") .extension("someint", "10")
.build() .build(),
.unwrap() HttpResponse::Ok()
).await?)
} }
#[actix_web::main] #[actix_rt::main]
async fn main() -> std::io::Result<()> { async fn main() -> std::io::Result<()> {
std::env::set_var("RUST_LOG", "actix_server=info,actix_web=info"); std::env::set_var("RUST_LOG", "actix_server=info,actix_web=info");
env_logger::init(); env_logger::init();
HttpServer::new(|| { HttpServer::new(|| {
App::new() App::new()
.wrap(actix_cors::Cors::permissive())
.wrap(actix_web::middleware::Logger::default()) .wrap(actix_web::middleware::Logger::default())
.wrap(actix_cors::Cors::new().finish())
.service(post_event) .service(post_event)
.service(get_event) .service(get_event)
}) })

View File

@ -1,21 +0,0 @@
[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" }

View File

@ -1,23 +0,0 @@
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
```

View File

@ -1,108 +0,0 @@
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);
}
}

View File

@ -1,12 +0,0 @@
[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"

View File

@ -1,47 +0,0 @@
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(())
}

View File

@ -1,16 +0,0 @@
[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"] }

View File

@ -1,23 +0,0 @@
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
```

View File

@ -1,121 +0,0 @@
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()
);
}
}

View File

@ -1,19 +0,0 @@
[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"] }

View File

@ -1,162 +0,0 @@
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
}
_ => (),
};
}

View File

@ -1,9 +1,8 @@
[package] [package]
name = "reqwest-wasm-example" name = "reqwest-wasm-example"
version = "0.3.0" version = "0.1.0"
authors = ["Francesco Guardiani <francescoguard@gmail.com>"] authors = ["Francesco Guardiani <francescoguard@gmail.com>"]
edition = "2018" edition = "2018"
resolver = "2"
# Config mostly pulled from: https://github.com/rustwasm/wasm-bindgen/blob/master/examples/fetch/Cargo.toml # Config mostly pulled from: https://github.com/rustwasm/wasm-bindgen/blob/master/examples/fetch/Cargo.toml
@ -11,10 +10,12 @@ resolver = "2"
crate-type = ["cdylib"] crate-type = ["cdylib"]
[dependencies] [dependencies]
reqwest = "^0.12" reqwest = "0.10.4"
uuid = "1" cloudevents-sdk = { path = "../.." }
cloudevents-sdk = { path = "../..", features = ["reqwest"] } cloudevents-sdk-reqwest = { path = "../../cloudevents-sdk-reqwest" }
url = { version = "^2.1" } url = { version = "^2.1" }
web-sys = { version = "0.3.39", features = ["Window", "Location"] } web-sys = { version = "0.3.39", features = ["Window", "Location"] }
wasm-bindgen-futures = "0.4.12" wasm-bindgen-futures = "0.4.12"
wasm-bindgen = { version = "0.2.77", features = ["serde-serialize"] } wasm-bindgen = { version = "0.2.62", features = ["serde-serialize"] }
[workspace]

View File

@ -1,23 +1,13 @@
## Example usage of CLoudEvents sdk/Reqwest from WASM ## Example usage of CLoudEvents sdk/Reqwest from WASM
First, ensure you have [`wasm-pack` installed](https://rustwasm.github.io/wasm-pack/installer/) Install the dependencies with:
Then install the dependencies:
npm install npm install
And finally run the example: Then build the example locally with:
npm run serve npm run serve
You should see a form in your browser at http://localhost:8080. When and then visiting http://localhost:8080 in a browser should run the example!
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.
Open the javascript console in the browser to see any helpful error 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).
messages.
This example is loosely based off of [this
example](https://github.com/seanmonstar/reqwest/tree/master/examples/wasm_github_fetch).

View File

@ -13,7 +13,7 @@
<div class="form-group"> <div class="form-group">
<label class="col-md-4 control-label" for="event_target">Target</label> <label class="col-md-4 control-label" for="event_target">Target</label>
<div class="col-md-4"> <div class="col-md-4">
<input id="event_target" name="event_target" type="text" value="http://localhost:9000" class="form-control input-md" required=""> <input id="event_target" name="event_target" type="text" placeholder="http://localhost:9000" class="form-control input-md" required="">
</div> </div>
</div> </div>
@ -22,7 +22,7 @@
<div class="form-group"> <div class="form-group">
<label class="col-md-4 control-label" for="event_type">Event Type</label> <label class="col-md-4 control-label" for="event_type">Event Type</label>
<div class="col-md-4"> <div class="col-md-4">
<input id="event_type" name="event_type" type="text" value="example" class="form-control input-md" required=""> <input id="event_type" name="event_type" type="text" placeholder="example" class="form-control input-md" required="">
</div> </div>
</div> </div>
@ -30,7 +30,7 @@
<div class="form-group"> <div class="form-group">
<label class="col-md-4 control-label" for="event_datacontenttype">Event Data Content Type</label> <label class="col-md-4 control-label" for="event_datacontenttype">Event Data Content Type</label>
<div class="col-md-4"> <div class="col-md-4">
<input id="event_datacontenttype" name="event_datacontenttype" type="text" value="application/json" class="form-control input-md" required=""> <input id="event_datacontenttype" name="event_datacontenttype" type="text" placeholder="application/json" class="form-control input-md" required="">
</div> </div>
</div> </div>

View File

@ -1,11 +1,11 @@
import $ from 'jquery'; import $ from 'jquery';
import 'bootstrap';
import 'bootstrap/dist/css/bootstrap.min.css'; import 'bootstrap/dist/css/bootstrap.min.css';
import 'bootstrap';
import("./pkg").then(rustModule => { import("./pkg").then(rustModule => {
$(function() { $(document).ready(function () {
$("#send").on("click", function () { $("#send").click(function () {
let target = $("#event_target").val() let target = $("#event_target").val()
let ty = $("#event_type").val() let ty = $("#event_type").val()
let dataContentType = $("#event_datacontenttype").val() let dataContentType = $("#event_datacontenttype").val()

File diff suppressed because it is too large Load Diff

View File

@ -1,21 +1,21 @@
{ {
"scripts": { "scripts": {
"build": "webpack", "build": "webpack",
"serve": "webpack serve" "serve": "webpack-dev-server"
}, },
"devDependencies": { "devDependencies": {
"@wasm-tool/wasm-pack-plugin": "^1.4.0", "@wasm-tool/wasm-pack-plugin": "^1.3.1",
"css-loader": "^5.2.6", "css-loader": "^3.5.3",
"html-webpack-plugin": "^5.5.0", "html-webpack-plugin": "^3.2.0",
"style-loader": "^2.0.0", "style-loader": "^1.2.1",
"text-encoding": "^0.7.0", "text-encoding": "^0.7.0",
"webpack": "^5.95.0", "webpack": "^4.29.4",
"webpack-cli": "^4.8.0", "webpack-cli": "^3.1.1",
"webpack-dev-server": "^5.0.4" "webpack-dev-server": "^3.1.0"
}, },
"dependencies": { "dependencies": {
"bootstrap": "^5.0.2", "bootstrap": "^4.5.0",
"jquery": "^3.6.0", "jquery": "^3.5.1",
"@popperjs/core": "^2.9.2" "popper.js": "^1.16.1"
} }
} }

View File

@ -1,28 +1,15 @@
use cloudevents::binding::reqwest::RequestBuilderExt;
use cloudevents::{EventBuilder, EventBuilderV10};
use wasm_bindgen::prelude::*; use wasm_bindgen::prelude::*;
use uuid::Uuid;
#[wasm_bindgen] #[wasm_bindgen]
pub async fn run( pub async fn run(target: String, ty: String, datacontenttype: String, data: String) -> Result<(), String> {
target: String, let event = cloudevents::EventBuilder::new()
ty: String,
datacontenttype: String,
data: String,
) -> Result<(), JsValue> {
let event = EventBuilderV10::new()
.id(&Uuid::new_v4().hyphenated().to_string())
.ty(ty) .ty(ty)
.source("http://localhost/")
.data(datacontenttype, data) .data(datacontenttype, data)
.build() .build();
.unwrap();
println!("Going to send event: {:?}", event); println!("Going to send event: {:?}", event);
reqwest::Client::new() cloudevents_sdk_reqwest::event_to_request(event, reqwest::Client::new().post(&target))
.post(&target)
.event(event)
.map_err(|e| e.to_string())? .map_err(|e| e.to_string())?
.header("Access-Control-Allow-Origin", "*") .header("Access-Control-Allow-Origin", "*")
.send() .send()

View File

@ -1,12 +0,0 @@
[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"] }

View File

@ -1,17 +0,0 @@
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
```

View File

@ -1,13 +0,0 @@
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;
}

View File

@ -1,17 +0,0 @@
[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 = "*"

View File

@ -1,26 +0,0 @@
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
```

View File

@ -1,39 +0,0 @@
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(())
}
}

View File

@ -1,49 +0,0 @@
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)
}
}
}

View File

@ -1,83 +0,0 @@
//! 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;

View File

@ -1,156 +0,0 @@
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);
}
}

View File

@ -1,143 +0,0 @@
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);
}
}

View File

@ -1,109 +0,0 @@
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);
}
}

View File

@ -1,162 +0,0 @@
//! 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);
}
}

View File

@ -1,106 +0,0 @@
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);
}
}

View File

@ -1,42 +0,0 @@
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()),
}),
)
}

View File

@ -1,12 +0,0 @@
#[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>;
}

View File

@ -1,102 +0,0 @@
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
}
}
}

View File

@ -1,23 +0,0 @@
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()
}
}

View File

@ -1,75 +0,0 @@
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());
}
}

View File

@ -1,161 +0,0 @@
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())
);
}
}

View File

@ -1,44 +0,0 @@
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()),
}),
)
}

View File

@ -1,11 +0,0 @@
#[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>;
}

View File

@ -1,101 +0,0 @@
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
}
}
}

View File

@ -1,22 +0,0 @@
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()
}
}

View File

@ -1,73 +0,0 @@
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());
}
}

View File

@ -1,159 +0,0 @@
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())
);
}
}

View File

@ -1,79 +0,0 @@
//! 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),
})
};
}

View File

@ -1,77 +0,0 @@
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)
}
}

View File

@ -1,43 +0,0 @@
//! 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;

View File

@ -1,26 +0,0 @@
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 }),
}
}
}

View File

@ -1,84 +0,0 @@
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);
}
}

View File

@ -1,57 +0,0 @@
//! 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;

View File

@ -1,114 +0,0 @@
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);
}
}

View File

@ -1,234 +0,0 @@
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)
}
}

View File

@ -1,155 +0,0 @@
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>
{
}
}

View File

@ -1,63 +0,0 @@
//! 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;

View File

@ -1,225 +0,0 @@
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();
}
}

View File

@ -1,190 +0,0 @@
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);
}
}

View File

@ -1,40 +0,0 @@
//! 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;

View File

@ -1,124 +0,0 @@
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);
}
}

View File

@ -1,65 +0,0 @@
//! 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;

View File

@ -1,114 +0,0 @@
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);
}
}

View File

@ -1,48 +1,25 @@
use super::{ use super::{AttributesV03, AttributesV10, SpecVersion};
AttributesIntoIteratorV03, AttributesIntoIteratorV10, AttributesV03, AttributesV10,
ExtensionValue, SpecVersion, UriReference,
};
use base64::prelude::*;
use chrono::{DateTime, Utc}; use chrono::{DateTime, Utc};
use serde::Serializer;
use std::fmt; use std::fmt;
use url::Url; use url::Url;
/// Enum representing a borrowed value of a CloudEvent attribute. #[derive(Debug, PartialEq)]
/// This represents the types defined in the [CloudEvent spec type system](https://github.com/cloudevents/spec/blob/v1.0/spec.md#type-system)
#[derive(Debug, PartialEq, Eq)]
pub enum AttributeValue<'a> { pub enum AttributeValue<'a> {
Boolean(&'a bool),
Integer(&'a i64),
String(&'a str),
Binary(&'a [u8]),
URI(&'a Url),
URIRef(&'a UriReference),
Time(&'a DateTime<Utc>),
SpecVersion(SpecVersion), SpecVersion(SpecVersion),
} String(&'a str),
URI(&'a Url),
impl<'a> From<&'a ExtensionValue> for AttributeValue<'a> { URIRef(&'a Url),
fn from(ev: &'a ExtensionValue) -> Self { Time(&'a DateTime<Utc>),
match ev {
ExtensionValue::String(s) => AttributeValue::String(s),
ExtensionValue::Boolean(b) => AttributeValue::Boolean(b),
ExtensionValue::Integer(i) => AttributeValue::Integer(i),
}
}
} }
impl fmt::Display for AttributeValue<'_> { impl fmt::Display for AttributeValue<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self { match self {
AttributeValue::Boolean(b) => f.serialize_bool(**b),
AttributeValue::Integer(i) => f.serialize_i64(**i),
AttributeValue::String(s) => f.write_str(s),
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), AttributeValue::SpecVersion(s) => s.fmt(f),
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::Time(s) => f.write_str(&s.to_rfc3339()),
} }
} }
} }
@ -50,47 +27,35 @@ impl fmt::Display for AttributeValue<'_> {
/// Trait to get [CloudEvents Context attributes](https://github.com/cloudevents/spec/blob/master/spec.md#context-attributes). /// Trait to get [CloudEvents Context attributes](https://github.com/cloudevents/spec/blob/master/spec.md#context-attributes).
pub trait AttributesReader { pub trait AttributesReader {
/// Get the [id](https://github.com/cloudevents/spec/blob/master/spec.md#id). /// Get the [id](https://github.com/cloudevents/spec/blob/master/spec.md#id).
fn id(&self) -> &str; fn get_id(&self) -> &str;
/// Get the [source](https://github.com/cloudevents/spec/blob/master/spec.md#source-1). /// Get the [source](https://github.com/cloudevents/spec/blob/master/spec.md#source-1).
fn source(&self) -> &UriReference; fn get_source(&self) -> &Url;
/// Get the [specversion](https://github.com/cloudevents/spec/blob/master/spec.md#specversion). /// Get the [specversion](https://github.com/cloudevents/spec/blob/master/spec.md#specversion).
fn specversion(&self) -> SpecVersion; fn get_specversion(&self) -> SpecVersion;
/// Get the [type](https://github.com/cloudevents/spec/blob/master/spec.md#type). /// Get the [type](https://github.com/cloudevents/spec/blob/master/spec.md#type).
fn ty(&self) -> &str; fn get_type(&self) -> &str;
/// Get the [datacontenttype](https://github.com/cloudevents/spec/blob/master/spec.md#datacontenttype). /// Get the [datacontenttype](https://github.com/cloudevents/spec/blob/master/spec.md#datacontenttype).
fn datacontenttype(&self) -> Option<&str>; fn get_datacontenttype(&self) -> Option<&str>;
/// Get the [dataschema](https://github.com/cloudevents/spec/blob/master/spec.md#dataschema). /// Get the [dataschema](https://github.com/cloudevents/spec/blob/master/spec.md#dataschema).
fn dataschema(&self) -> Option<&Url>; fn get_dataschema(&self) -> Option<&Url>;
/// Get the [subject](https://github.com/cloudevents/spec/blob/master/spec.md#subject). /// Get the [subject](https://github.com/cloudevents/spec/blob/master/spec.md#subject).
fn subject(&self) -> Option<&str>; fn get_subject(&self) -> Option<&str>;
/// Get the [time](https://github.com/cloudevents/spec/blob/master/spec.md#time). /// Get the [time](https://github.com/cloudevents/spec/blob/master/spec.md#time).
fn time(&self) -> Option<&DateTime<Utc>>; fn get_time(&self) -> Option<&DateTime<Utc>>;
} }
/// Trait to set [CloudEvents Context attributes](https://github.com/cloudevents/spec/blob/master/spec.md#context-attributes). /// Trait to set [CloudEvents Context attributes](https://github.com/cloudevents/spec/blob/master/spec.md#context-attributes).
pub trait AttributesWriter { pub trait AttributesWriter {
/// Set the [id](https://github.com/cloudevents/spec/blob/master/spec.md#id). /// Set the [id](https://github.com/cloudevents/spec/blob/master/spec.md#id).
/// Returns the previous value. fn set_id(&mut self, id: impl Into<String>);
fn set_id(&mut self, id: impl Into<String>) -> String;
/// Set the [source](https://github.com/cloudevents/spec/blob/master/spec.md#source-1). /// Set the [source](https://github.com/cloudevents/spec/blob/master/spec.md#source-1).
/// Returns the previous value. fn set_source(&mut self, source: impl Into<Url>);
fn set_source(&mut self, source: impl Into<UriReference>) -> UriReference;
/// Set the [type](https://github.com/cloudevents/spec/blob/master/spec.md#type). /// Set the [type](https://github.com/cloudevents/spec/blob/master/spec.md#type).
/// Returns the previous value. fn set_type(&mut self, ty: impl Into<String>);
fn set_type(&mut self, ty: impl Into<String>) -> String;
/// Set the [subject](https://github.com/cloudevents/spec/blob/master/spec.md#subject). /// Set the [subject](https://github.com/cloudevents/spec/blob/master/spec.md#subject).
/// Returns the previous value. fn set_subject(&mut self, subject: Option<impl Into<String>>);
fn set_subject(&mut self, subject: Option<impl Into<String>>) -> Option<String>;
/// Set the [time](https://github.com/cloudevents/spec/blob/master/spec.md#time). /// Set the [time](https://github.com/cloudevents/spec/blob/master/spec.md#time).
/// Returns the previous value. 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>>;
/// Set the [datacontenttype](https://github.com/cloudevents/spec/blob/master/spec.md#datacontenttype).
/// Returns the previous value.
fn set_datacontenttype(&mut self, datacontenttype: Option<impl Into<String>>)
-> Option<String>;
/// Set the [dataschema](https://github.com/cloudevents/spec/blob/master/spec.md#dataschema).
/// Returns the previous value.
fn set_dataschema(&mut self, dataschema: Option<impl Into<Url>>) -> Option<Url>;
} }
pub(crate) trait AttributesConverter { pub(crate) trait AttributesConverter {
@ -98,134 +63,122 @@ pub(crate) trait AttributesConverter {
fn into_v10(self) -> AttributesV10; fn into_v10(self) -> AttributesV10;
} }
#[derive(PartialEq, Debug, Clone, Copy)] pub(crate) trait DataAttributesWriter {
pub(crate) enum AttributesIter<'a> { fn set_datacontenttype(&mut self, datacontenttype: Option<impl Into<String>>);
IterV03(AttributesIntoIteratorV03<'a>), fn set_dataschema(&mut self, dataschema: Option<impl Into<Url>>);
IterV10(AttributesIntoIteratorV10<'a>),
}
impl<'a> Iterator for AttributesIter<'a> {
type Item = (&'a str, AttributeValue<'a>);
fn next(&mut self) -> Option<Self::Item> {
match self {
AttributesIter::IterV03(a) => a.next(),
AttributesIter::IterV10(a) => a.next(),
}
}
} }
/// Union type representing one of the possible context attributes structs /// Union type representing one of the possible context attributes structs
#[derive(PartialEq, Eq, Debug, Clone)] #[derive(PartialEq, Debug, Clone)]
pub enum Attributes { pub enum Attributes {
V03(AttributesV03), V03(AttributesV03),
V10(AttributesV10), V10(AttributesV10),
} }
impl AttributesReader for Attributes { impl AttributesReader for Attributes {
fn id(&self) -> &str { fn get_id(&self) -> &str {
match self { match self {
Attributes::V03(a) => a.id(), Attributes::V03(a) => a.get_id(),
Attributes::V10(a) => a.id(), Attributes::V10(a) => a.get_id(),
} }
} }
fn source(&self) -> &UriReference { fn get_source(&self) -> &Url {
match self { match self {
Attributes::V03(a) => a.source(), Attributes::V03(a) => a.get_source(),
Attributes::V10(a) => a.source(), Attributes::V10(a) => a.get_source(),
} }
} }
fn specversion(&self) -> SpecVersion { fn get_specversion(&self) -> SpecVersion {
match self { match self {
Attributes::V03(a) => a.specversion(), Attributes::V03(a) => a.get_specversion(),
Attributes::V10(a) => a.specversion(), Attributes::V10(a) => a.get_specversion(),
} }
} }
fn ty(&self) -> &str { fn get_type(&self) -> &str {
match self { match self {
Attributes::V03(a) => a.ty(), Attributes::V03(a) => a.get_type(),
Attributes::V10(a) => a.ty(), Attributes::V10(a) => a.get_type(),
} }
} }
fn datacontenttype(&self) -> Option<&str> { fn get_datacontenttype(&self) -> Option<&str> {
match self { match self {
Attributes::V03(a) => a.datacontenttype(), Attributes::V03(a) => a.get_datacontenttype(),
Attributes::V10(a) => a.datacontenttype(), Attributes::V10(a) => a.get_datacontenttype(),
} }
} }
fn dataschema(&self) -> Option<&Url> { fn get_dataschema(&self) -> Option<&Url> {
match self { match self {
Attributes::V03(a) => a.dataschema(), Attributes::V03(a) => a.get_dataschema(),
Attributes::V10(a) => a.dataschema(), Attributes::V10(a) => a.get_dataschema(),
} }
} }
fn subject(&self) -> Option<&str> { fn get_subject(&self) -> Option<&str> {
match self { match self {
Attributes::V03(a) => a.subject(), Attributes::V03(a) => a.get_subject(),
Attributes::V10(a) => a.subject(), Attributes::V10(a) => a.get_subject(),
} }
} }
fn time(&self) -> Option<&DateTime<Utc>> { fn get_time(&self) -> Option<&DateTime<Utc>> {
match self { match self {
Attributes::V03(a) => a.time(), Attributes::V03(a) => a.get_time(),
Attributes::V10(a) => a.time(), Attributes::V10(a) => a.get_time(),
} }
} }
} }
impl AttributesWriter for Attributes { impl AttributesWriter for Attributes {
fn set_id(&mut self, id: impl Into<String>) -> String { fn set_id(&mut self, id: impl Into<String>) {
match self { match self {
Attributes::V03(a) => a.set_id(id), Attributes::V03(a) => a.set_id(id),
Attributes::V10(a) => a.set_id(id), Attributes::V10(a) => a.set_id(id),
} }
} }
fn set_source(&mut self, source: impl Into<UriReference>) -> UriReference { fn set_source(&mut self, source: impl Into<Url>) {
match self { match self {
Attributes::V03(a) => a.set_source(source), Attributes::V03(a) => a.set_source(source),
Attributes::V10(a) => a.set_source(source), Attributes::V10(a) => a.set_source(source),
} }
} }
fn set_type(&mut self, ty: impl Into<String>) -> String { fn set_type(&mut self, ty: impl Into<String>) {
match self { match self {
Attributes::V03(a) => a.set_type(ty), Attributes::V03(a) => a.set_type(ty),
Attributes::V10(a) => a.set_type(ty), Attributes::V10(a) => a.set_type(ty),
} }
} }
fn set_subject(&mut self, subject: Option<impl Into<String>>) -> Option<String> { fn set_subject(&mut self, subject: Option<impl Into<String>>) {
match self { match self {
Attributes::V03(a) => a.set_subject(subject), Attributes::V03(a) => a.set_subject(subject),
Attributes::V10(a) => a.set_subject(subject), Attributes::V10(a) => a.set_subject(subject),
} }
} }
fn set_time(&mut self, time: Option<impl Into<DateTime<Utc>>>) -> Option<DateTime<Utc>> { fn set_time(&mut self, time: Option<impl Into<DateTime<Utc>>>) {
match self { match self {
Attributes::V03(a) => a.set_time(time), Attributes::V03(a) => a.set_time(time),
Attributes::V10(a) => a.set_time(time), Attributes::V10(a) => a.set_time(time),
} }
} }
}
fn set_datacontenttype( impl DataAttributesWriter for Attributes {
&mut self, fn set_datacontenttype(&mut self, datacontenttype: Option<impl Into<String>>) {
datacontenttype: Option<impl Into<String>>,
) -> Option<String> {
match self { match self {
Attributes::V03(a) => a.set_datacontenttype(datacontenttype), Attributes::V03(a) => a.set_datacontenttype(datacontenttype),
Attributes::V10(a) => a.set_datacontenttype(datacontenttype), Attributes::V10(a) => a.set_datacontenttype(datacontenttype),
} }
} }
fn set_dataschema(&mut self, dataschema: Option<impl Into<Url>>) -> Option<Url> { fn set_dataschema(&mut self, dataschema: Option<impl Into<Url>>) {
match self { match self {
Attributes::V03(a) => a.set_dataschema(dataschema), Attributes::V03(a) => a.set_dataschema(dataschema),
Attributes::V10(a) => a.set_dataschema(dataschema), Attributes::V10(a) => a.set_dataschema(dataschema),
@ -234,25 +187,18 @@ impl AttributesWriter for Attributes {
} }
impl Attributes { impl Attributes {
pub(crate) fn into_v10(self) -> Self { pub fn into_v10(self) -> Self {
match self { match self {
Attributes::V03(v03) => Attributes::V10(v03.into_v10()), Attributes::V03(v03) => Attributes::V10(v03.into_v10()),
_ => self, _ => self,
} }
} }
pub(crate) fn into_v03(self) -> Self { pub fn into_v03(self) -> Self {
match self { match self {
Attributes::V10(v10) => Attributes::V03(v10.into_v03()), Attributes::V10(v10) => Attributes::V03(v10.into_v03()),
_ => self, _ => self,
} }
} }
pub(crate) fn iter(&self) -> impl Iterator<Item = (&str, AttributeValue)> {
match self {
Attributes::V03(a) => AttributesIter::IterV03(a.into_iter()),
Attributes::V10(a) => AttributesIter::IterV10(a.into_iter()),
}
}
} }
#[cfg(not(target_arch = "wasm32"))] #[cfg(not(target_arch = "wasm32"))]
@ -262,15 +208,16 @@ pub(crate) fn default_hostname() -> Url {
"http://{}", "http://{}",
hostname::get() hostname::get()
.ok() .ok()
.and_then(|s| s.into_string().ok()) .map(|s| s.into_string().ok())
.unwrap_or_else(|| "localhost".to_string()) .flatten()
.unwrap_or(String::from("localhost".to_string()))
) )
.as_ref(), .as_ref(),
) )
.unwrap() .unwrap()
} }
#[cfg(all(target_arch = "wasm32", target_os = "unknown"))] #[cfg(target_arch = "wasm32")]
pub(crate) fn default_hostname() -> Url { pub(crate) fn default_hostname() -> Url {
use std::str::FromStr; use std::str::FromStr;
@ -283,10 +230,3 @@ pub(crate) fn default_hostname() -> Url {
) )
.unwrap() .unwrap()
} }
#[cfg(all(target_arch = "wasm32", target_os = "wasi"))]
pub(crate) fn default_hostname() -> Url {
use std::str::FromStr;
Url::from_str("http://localhost").unwrap()
}

View File

@ -1,284 +1,32 @@
use super::Event; use super::{EventBuilderV03, EventBuilderV10};
use snafu::Snafu;
/// Trait to implement a builder for [`Event`]: /// Builder to create [`super::Event`]:
/// ``` /// ```
/// use cloudevents::event::{EventBuilderV10, EventBuilder}; /// use cloudevents::EventBuilder;
/// use chrono::Utc; /// use chrono::Utc;
/// use url::Url; /// use url::Url;
/// ///
/// let event = EventBuilderV10::new() /// let event = EventBuilder::v10()
/// .id("my_event.my_application") /// .id("my_event.my_application")
/// .source("http://localhost:8080") /// .source(Url::parse("http://localhost:8080").unwrap())
/// .ty("example.demo")
/// .time(Utc::now()) /// .time(Utc::now())
/// .build() /// .build();
/// .unwrap();
/// ``` /// ```
/// pub struct EventBuilder {}
/// You can create an [`EventBuilder`] starting from an existing [`Event`] using the [`From`] trait.
/// You can create a default [`EventBuilder`] setting default values for some attributes.
pub trait EventBuilder
where
Self: Clone + Sized + From<Event> + Default,
{
/// Create a new empty builder
fn new() -> Self;
/// Build [`Event`] impl EventBuilder {
fn build(self) -> Result<Event, Error>; /// Creates a new builder for latest CloudEvents version
pub fn new() -> EventBuilderV10 {
return Self::v10();
} }
/// Represents an error during build process /// Creates a new builder for CloudEvents V1.0
#[derive(Debug, Snafu, Clone)] pub fn v10() -> EventBuilderV10 {
pub enum Error { return EventBuilderV10::new();
#[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)] /// Creates a new builder for CloudEvents V0.3
mod tests { pub fn v03() -> EventBuilderV03 {
use crate::test::fixtures; return EventBuilderV03::new();
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)
}
#[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)
}
/// 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)
} }
} }

View File

@ -1,11 +1,7 @@
use serde_json::Value; use std::convert::{Into, TryFrom};
use std::convert::TryFrom;
use std::fmt;
use std::fmt::Formatter;
use std::str;
/// Event [data attribute](https://github.com/cloudevents/spec/blob/master/spec.md#event-data) representation /// Event [data attribute](https://github.com/cloudevents/spec/blob/master/spec.md#event-data) representation
#[derive(PartialEq, Eq, Debug, Clone)] #[derive(Debug, PartialEq, Clone)]
pub enum Data { pub enum Data {
/// Event has a binary payload /// Event has a binary payload
Binary(Vec<u8>), Binary(Vec<u8>),
@ -15,31 +11,59 @@ pub enum Data {
Json(serde_json::Value), Json(serde_json::Value),
} }
impl Data {
/// Create a [`Data`] from a [`Into<Vec<u8>>`].
///
/// # Example
///
/// ```
/// use cloudevents::event::Data;
///
/// let value = Data::from_base64(b"dmFsdWU=").unwrap();
/// assert_eq!(value, Data::Binary(base64::decode("dmFsdWU=").unwrap()));
/// ```
///
/// [`AsRef<[u8]>`]: https://doc.rust-lang.org/std/convert/trait.AsRef.html
/// [`Data`]: enum.Data.html
pub fn from_base64<I>(i: I) -> Result<Self, base64::DecodeError>
where
I: AsRef<[u8]>,
{
Ok(base64::decode(&i)?.into())
}
pub fn from_binary<I>(content_type: Option<&str>, i: I) -> Result<Self, serde_json::Error>
where
I: AsRef<[u8]>,
{
let is_json = is_json_content_type(content_type.unwrap_or("application/json"));
if is_json {
serde_json::from_slice::<serde_json::Value>(i.as_ref()).map(Data::Json)
} else {
Ok(Data::Binary(i.as_ref().to_vec()))
}
}
}
pub(crate) fn is_json_content_type(ct: &str) -> bool { pub(crate) fn is_json_content_type(ct: &str) -> bool {
ct.starts_with("application/json") || ct.starts_with("text/json") || ct.ends_with("+json") ct == "application/json" || ct == "text/json" || ct.ends_with("+json")
} }
impl From<serde_json::Value> for Data { impl Into<Data> for serde_json::Value {
fn from(value: Value) -> Self { fn into(self) -> Data {
Data::Json(value) Data::Json(self)
} }
} }
impl From<Vec<u8>> for Data { impl Into<Data> for Vec<u8> {
fn from(value: Vec<u8>) -> Self { fn into(self) -> Data {
Data::Binary(value) Data::Binary(self)
} }
} }
impl From<String> for Data { impl Into<Data> for String {
fn from(value: String) -> Self { fn into(self) -> Data {
Data::String(value) Data::String(self)
}
}
impl From<&str> for Data {
fn from(value: &str) -> Self {
Data::String(String::from(value))
} }
} }
@ -60,7 +84,7 @@ impl TryFrom<Data> for Vec<u8> {
fn try_from(value: Data) -> Result<Self, Self::Error> { fn try_from(value: Data) -> Result<Self, Self::Error> {
match value { match value {
Data::Binary(v) => Ok(v), Data::Binary(v) => Ok(serde_json::from_slice(&v)?),
Data::Json(v) => Ok(serde_json::to_vec(&v)?), Data::Json(v) => Ok(serde_json::to_vec(&v)?),
Data::String(s) => Ok(s.into_bytes()), Data::String(s) => Ok(s.into_bytes()),
} }
@ -78,13 +102,3 @@ 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),
}
}
}

230
src/event/event.rs Normal file
View File

@ -0,0 +1,230 @@
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());
}
}

View File

@ -1,16 +1,15 @@
use serde::{Deserialize, Serialize, Serializer}; use serde::{Deserialize, Serialize};
use std::convert::From; use std::convert::From;
use std::fmt;
#[derive(PartialEq, Eq, Debug, Clone, Serialize, Deserialize)] #[derive(Debug, PartialEq, Clone, Serialize, Deserialize)]
#[serde(untagged)] #[serde(untagged)]
/// Represents all the possible [CloudEvents extension](https://github.com/cloudevents/spec/blob/master/spec.md#extension-context-attributes) values /// Represents all the possible [CloudEvents extension](https://github.com/cloudevents/spec/blob/master/spec.md#extension-context-attributes) values
pub enum ExtensionValue { pub enum ExtensionValue {
/// Represents a [`String`] value. /// Represents a [`String`](std::string::String) value.
String(String), String(String),
/// Represents a [`bool`] value. /// Represents a [`bool`](bool) value.
Boolean(bool), Boolean(bool),
/// Represents an integer [`i64`] value. /// Represents an integer [`i64`](i64) value.
Integer(i64), Integer(i64),
} }
@ -60,13 +59,3 @@ impl ExtensionValue {
ExtensionValue::from(s.into()) ExtensionValue::from(s.into())
} }
} }
impl fmt::Display for ExtensionValue {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
ExtensionValue::String(s) => f.write_str(s),
ExtensionValue::Boolean(b) => f.serialize_bool(*b),
ExtensionValue::Integer(i) => f.serialize_i64(*i),
}
}
}

View File

@ -3,99 +3,119 @@ use super::{
EventFormatSerializerV03, EventFormatSerializerV10, EventFormatSerializerV03, EventFormatSerializerV10,
}; };
use crate::event::{AttributesReader, ExtensionValue}; use crate::event::{AttributesReader, ExtensionValue};
use base64::prelude::*; use serde::de::{Error, IntoDeserializer, Unexpected};
use serde::de::{Error, IntoDeserializer};
use serde::{Deserialize, Deserializer, Serialize, Serializer}; use serde::{Deserialize, Deserializer, Serialize, Serializer};
use serde_json::{Map, Value}; use serde_value::Value;
use std::collections::HashMap; 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()
};
}
macro_rules! parse_field { macro_rules! parse_field {
($value:expr, $target_type:ty, $error:ty) => { ($map:ident, $name:literal, $value_variant:ident, $error:ty) => {
<$target_type>::deserialize($value.into_deserializer()).map_err(<$error>::custom) parse_optional_field!($map, $name, $value_variant, $error)?
};
($value:expr, $target_type:ty, $error:ty, $mapper:expr) => {
<$target_type>::deserialize($value.into_deserializer())
.map_err(<$error>::custom)
.and_then(|v| $mapper(v).map_err(<$error>::custom))
};
}
macro_rules! extract_optional_field {
($map:ident, $name:literal, $target_type:ty, $error:ty) => {
$map.remove($name)
.filter(|v| !v.is_null())
.map(|v| parse_field!(v, $target_type, $error))
.transpose()
};
($map:ident, $name:literal, $target_type:ty, $error:ty, $mapper:expr) => {
$map.remove($name)
.filter(|v| !v.is_null())
.map(|v| parse_field!(v, $target_type, $error, $mapper))
.transpose()
};
}
macro_rules! extract_field {
($map:ident, $name:literal, $target_type:ty, $error:ty) => {
extract_optional_field!($map, $name, $target_type, $error)?
.ok_or_else(|| <$error>::missing_field($name)) .ok_or_else(|| <$error>::missing_field($name))
}; };
($map:ident, $name:literal, $target_type:ty, $error:ty, $mapper:expr) => { ($map:ident, $name:literal, $value_variant:ident, $error:ty, $mapper:expr) => {
extract_optional_field!($map, $name, $target_type, $error, $mapper)? parse_optional_field!($map, $name, $value_variant, $error, $mapper)?
.ok_or_else(|| <$error>::missing_field($name)) .ok_or_else(|| <$error>::missing_field($name))
}; };
} }
pub fn parse_data_json<E: serde::de::Error>(v: Value) -> Result<Value, E> { macro_rules! parse_data_json {
Value::deserialize(v.into_deserializer()).map_err(E::custom) ($in:ident, $error:ty) => {
Ok(serde_json::Value::deserialize($in.into_deserializer())
.map_err(|e| <$error>::custom(e))?)
};
} }
pub fn parse_data_string<E: serde::de::Error>(v: Value) -> Result<String, E> { macro_rules! parse_data_string {
parse_field!(v, String, E) ($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_base64<E: serde::de::Error>(v: Value) -> Result<Vec<u8>, E> { macro_rules! parse_json_data_base64 {
parse_field!(v, String, E).and_then(|s| { ($in:ident, $error:ty) => {{
BASE64_STANDARD let data = parse_data_base64!($in, $error)?;
.decode(s) serde_json::from_slice(&data).map_err(|e| <$error>::custom(e))
.map_err(|e| E::custom(format_args!("decode error `{}`", e))) }};
})
} }
pub fn parse_data_base64_json<E: serde::de::Error>(v: Value) -> Result<Value, E> { macro_rules! parse_data_base64 {
let data = parse_data_base64(v)?; ($in:ident, $error:ty) => {
serde_json::from_slice(&data).map_err(E::custom) 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(crate) trait EventFormatDeserializer { pub(crate) trait EventFormatDeserializer {
fn deserialize_attributes<E: serde::de::Error>( fn deserialize_attributes<E: serde::de::Error>(
map: &mut Map<String, Value>, map: &mut BTreeMap<String, Value>,
) -> Result<Attributes, E>; ) -> Result<Attributes, E>;
fn deserialize_data<E: serde::de::Error>( fn deserialize_data<E: serde::de::Error>(
content_type: &str, content_type: &str,
map: &mut Map<String, Value>, map: &mut BTreeMap<String, Value>,
) -> Result<Option<Data>, E>; ) -> Result<Option<Data>, E>;
fn deserialize_event<E: serde::de::Error>(mut map: Map<String, Value>) -> Result<Event, E> { fn deserialize_event<E: serde::de::Error>(
mut map: BTreeMap<String, Value>,
) -> Result<Event, E> {
let attributes = Self::deserialize_attributes(&mut map)?; let attributes = Self::deserialize_attributes(&mut map)?;
let data = Self::deserialize_data( let data = Self::deserialize_data(
attributes.datacontenttype().unwrap_or("application/json"), attributes
.get_datacontenttype()
.unwrap_or("application/json"),
&mut map, &mut map,
)?; )?;
let extensions = map let extensions = map
.into_iter() .into_iter()
.filter(|v| !v.1.is_null()) .map(|(k, v)| Ok((k, ExtensionValue::deserialize(v.into_deserializer())?)))
.map(|(k, v)| { .collect::<Result<HashMap<String, ExtensionValue>, serde_value::DeserializerError>>()
Ok(( .map_err(|e| E::custom(e))?;
k,
ExtensionValue::deserialize(v.into_deserializer()).map_err(E::custom)?,
))
})
.collect::<Result<HashMap<String, ExtensionValue>, E>>()?;
Ok(Event { Ok(Event {
attributes, attributes,
@ -119,12 +139,20 @@ impl<'de> Deserialize<'de> for Event {
where where
D: Deserializer<'de>, D: Deserializer<'de>,
{ {
let root_value = Value::deserialize(deserializer)?; let map = match Value::deserialize(deserializer)? {
let mut map: Map<String, Value> = Value::Map(m) => Ok(m),
Map::deserialize(root_value.into_deserializer()).map_err(D::Error::custom)?; v => Err(Error::invalid_type(value_to_unexpected(&v), &"a map")),
}?;
match extract_field!(map, "specversion", String, <D as Deserializer<'de>>::Error)?.as_str() 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() {
"0.3" => EventFormatDeserializerV03::deserialize_event(map), "0.3" => EventFormatDeserializerV03::deserialize_event(map),
"1.0" => EventFormatDeserializerV10::deserialize_event(map), "1.0" => EventFormatDeserializerV10::deserialize_event(map),
s => Err(D::Error::unknown_variant( s => Err(D::Error::unknown_variant(
@ -150,3 +178,28 @@ 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),
}
}

View File

@ -6,7 +6,6 @@ use crate::message::{
BinaryDeserializer, BinarySerializer, MessageAttributeValue, Result, StructuredDeserializer, BinaryDeserializer, BinarySerializer, MessageAttributeValue, Result, StructuredDeserializer,
StructuredSerializer, StructuredSerializer,
}; };
use crate::{EventBuilder, EventBuilderV03, EventBuilderV10};
impl StructuredDeserializer for Event { impl StructuredDeserializer for Event {
fn deserialize_structured<R, V: StructuredSerializer<R>>(self, visitor: V) -> Result<R> { fn deserialize_structured<R, V: StructuredSerializer<R>>(self, visitor: V) -> Result<R> {
@ -17,7 +16,7 @@ impl StructuredDeserializer for Event {
impl BinaryDeserializer for Event { impl BinaryDeserializer for Event {
fn deserialize_binary<R: Sized, V: BinarySerializer<R>>(self, mut visitor: V) -> Result<R> { fn deserialize_binary<R: Sized, V: BinarySerializer<R>>(self, mut visitor: V) -> Result<R> {
visitor = visitor.set_spec_version(self.specversion())?; visitor = visitor.set_spec_version(self.get_specversion())?;
visitor = self.attributes.deserialize_attributes(visitor)?; visitor = self.attributes.deserialize_attributes(visitor)?;
for (k, v) in self.extensions.into_iter() { for (k, v) in self.extensions.into_iter() {
visitor = visitor.set_extension(&k, v.into())?; visitor = visitor.set_extension(&k, v.into())?;
@ -38,6 +37,10 @@ pub(crate) trait AttributesDeserializer {
fn deserialize_attributes<R: Sized, V: BinarySerializer<R>>(self, visitor: V) -> Result<V>; fn deserialize_attributes<R: Sized, V: BinarySerializer<R>>(self, visitor: V) -> Result<V>;
} }
pub(crate) trait AttributesSerializer {
fn serialize_attribute(&mut self, name: &str, value: MessageAttributeValue) -> Result<()>;
}
impl AttributesDeserializer for Attributes { impl AttributesDeserializer for Attributes {
fn deserialize_attributes<R: Sized, V: BinarySerializer<R>>(self, visitor: V) -> Result<V> { fn deserialize_attributes<R: Sized, V: BinarySerializer<R>>(self, visitor: V) -> Result<V> {
match self { match self {
@ -47,202 +50,50 @@ impl AttributesDeserializer for Attributes {
} }
} }
pub(crate) trait AttributesSerializer { impl AttributesSerializer for Attributes {
fn serialize_attribute(&mut self, name: &str, value: MessageAttributeValue) -> Result<()>; fn serialize_attribute(&mut self, name: &str, value: MessageAttributeValue) -> Result<()> {
match self {
Attributes::V03(v03) => v03.serialize_attribute(name, value),
Attributes::V10(v10) => v10.serialize_attribute(name, value),
} }
#[derive(Debug)]
pub(crate) struct EventStructuredSerializer {}
impl StructuredSerializer<Event> for EventStructuredSerializer {
fn set_structured_event(self, bytes: Vec<u8>) -> Result<Event> {
Ok(serde_json::from_slice(&bytes)?)
} }
} }
#[derive(Debug)] impl StructuredSerializer<Event> for Event {
pub(crate) enum EventBinarySerializer { fn set_structured_event(mut self, bytes: Vec<u8>) -> Result<Event> {
V10(EventBuilderV10), let new_event: Event = serde_json::from_slice(&bytes)?;
V03(EventBuilderV03), self.attributes = new_event.attributes;
} self.data = new_event.data;
self.extensions = new_event.extensions;
impl EventBinarySerializer { Ok(self)
pub(crate) fn new() -> Self {
EventBinarySerializer::V10(EventBuilderV10::new())
} }
} }
impl BinarySerializer<Event> for EventBinarySerializer { impl BinarySerializer<Event> for Event {
fn set_spec_version(self, spec_version: SpecVersion) -> Result<Self> { fn set_spec_version(mut self, spec_version: SpecVersion) -> Result<Self> {
Ok(match spec_version { match spec_version {
SpecVersion::V03 => EventBinarySerializer::V03(EventBuilderV03::new()), SpecVersion::V03 => self.attributes = self.attributes.clone().into_v03(),
SpecVersion::V10 => EventBinarySerializer::V10(EventBuilderV10::new()), SpecVersion::V10 => self.attributes = self.attributes.clone().into_v10(),
})
}
fn set_attribute(mut self, name: &str, value: MessageAttributeValue) -> Result<Self> {
match &mut self {
EventBinarySerializer::V03(eb) => eb.serialize_attribute(name, value)?,
EventBinarySerializer::V10(eb) => eb.serialize_attribute(name, value)?,
} }
Ok(self) Ok(self)
} }
fn set_extension(self, name: &str, value: MessageAttributeValue) -> Result<Self> { fn set_attribute(mut self, name: &str, value: MessageAttributeValue) -> Result<Self> {
Ok(match self { self.attributes.serialize_attribute(name, value)?;
EventBinarySerializer::V03(eb) => EventBinarySerializer::V03(eb.extension(name, value)), Ok(self)
EventBinarySerializer::V10(eb) => EventBinarySerializer::V10(eb.extension(name, value)),
})
} }
fn end_with_data(self, bytes: Vec<u8>) -> Result<Event> { fn set_extension(mut self, name: &str, value: MessageAttributeValue) -> Result<Self> {
Ok(match self { self.extensions.insert(name.to_string(), value.into());
EventBinarySerializer::V03(eb) => { Ok(self)
eb.data_without_content_type(Data::Binary(bytes)).build()
} }
EventBinarySerializer::V10(eb) => {
eb.data_without_content_type(Data::Binary(bytes)).build() 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(self) -> Result<Event> { fn end(self) -> Result<Event> {
Ok(match self { Ok(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(),);
} }
} }

View File

@ -1,31 +1,26 @@
//! Provides [`Event`] data structure, [`EventBuilder`] and other facilities to work with [`Event`].
mod attributes; mod attributes;
mod builder; mod builder;
mod data; mod data;
mod event;
mod extensions; mod extensions;
#[macro_use] #[macro_use]
mod format; mod format;
mod message; mod message;
mod spec_version; mod spec_version;
mod types;
pub use attributes::Attributes; pub use attributes::Attributes;
pub use attributes::{AttributeValue, AttributesReader, AttributesWriter}; pub use attributes::{AttributesReader, AttributesWriter};
pub use builder::Error as EventBuilderError;
pub use builder::EventBuilder; pub use builder::EventBuilder;
pub use data::Data; pub use data::Data;
pub use event::Event;
pub use extensions::ExtensionValue; pub use extensions::ExtensionValue;
pub(crate) use message::EventBinarySerializer; pub use spec_version::InvalidSpecVersion;
pub(crate) use message::EventStructuredSerializer;
pub use spec_version::SpecVersion; pub use spec_version::SpecVersion;
pub use spec_version::UnknownSpecVersion; pub use spec_version::ATTRIBUTE_NAMES as SPEC_VERSION_ATTRIBUTES;
pub use types::{TryIntoTime, TryIntoUrl, UriReference};
mod v03; mod v03;
pub use v03::Attributes as AttributesV03; pub use v03::Attributes as AttributesV03;
pub(crate) use v03::AttributesIntoIterator as AttributesIntoIteratorV03;
pub use v03::EventBuilder as EventBuilderV03; pub use v03::EventBuilder as EventBuilderV03;
pub(crate) use v03::EventFormatDeserializer as EventFormatDeserializerV03; pub(crate) use v03::EventFormatDeserializer as EventFormatDeserializerV03;
pub(crate) use v03::EventFormatSerializer as EventFormatSerializerV03; pub(crate) use v03::EventFormatSerializer as EventFormatSerializerV03;
@ -33,261 +28,6 @@ pub(crate) use v03::EventFormatSerializer as EventFormatSerializerV03;
mod v10; mod v10;
pub use v10::Attributes as AttributesV10; pub use v10::Attributes as AttributesV10;
pub(crate) use v10::AttributesIntoIterator as AttributesIntoIteratorV10;
pub use v10::EventBuilder as EventBuilderV10; pub use v10::EventBuilder as EventBuilderV10;
pub(crate) use v10::EventFormatDeserializer as EventFormatDeserializerV10; pub(crate) use v10::EventFormatDeserializer as EventFormatDeserializerV10;
pub(crate) use v10::EventFormatSerializer as EventFormatSerializerV10; pub(crate) use v10::EventFormatSerializer as EventFormatSerializerV10;
use chrono::{DateTime, Utc};
use delegate_attr::delegate;
use std::collections::HashMap;
use std::fmt;
use url::Url;
/// Data structure that represents a [CloudEvent](https://github.com/cloudevents/spec/blob/master/spec.md).
/// It provides methods to get the attributes through [`AttributesReader`]
/// and write them through [`AttributesWriter`].
/// It also provides methods to read and write the [event data](https://github.com/cloudevents/spec/blob/master/spec.md#event-data).
///
/// You can build events using [`super::EventBuilder`]
/// ```
/// use cloudevents::*;
/// use std::convert::TryInto;
///
/// # use std::error::Error;
/// # fn main() -> Result<(), Box<dyn Error>> {
/// // Create an event using the Default trait
/// let mut e = Event::default();
/// e.set_data(
/// "application/json",
/// serde_json::json!({"hello": "world"})
/// );
///
/// // Print the event id
/// println!("Event id: {}", e.id());
///
/// // Get the event data
/// let data: Option<Data> = e.data().cloned();
/// match data {
/// Some(d) => println!("{}", d),
/// None => println!("No event data")
/// }
/// # Ok(())
/// # }
/// ```
#[derive(PartialEq, Eq, Debug, Clone)]
pub struct Event {
pub(crate) attributes: Attributes,
pub(crate) data: Option<Data>,
pub(crate) extensions: HashMap<String, ExtensionValue>,
}
#[delegate(self.attributes)]
impl AttributesReader for Event {
fn id(&self) -> &str {}
fn source(&self) -> &UriReference {}
fn specversion(&self) -> SpecVersion {}
fn ty(&self) -> &str {}
fn datacontenttype(&self) -> Option<&str> {}
fn dataschema(&self) -> Option<&Url> {}
fn subject(&self) -> Option<&str> {}
fn time(&self) -> Option<&DateTime<Utc>> {}
}
#[delegate(self.attributes)]
impl AttributesWriter for Event {
fn set_id(&mut self, id: impl Into<String>) -> String {}
fn set_source(&mut self, source: impl Into<UriReference>) -> UriReference {}
fn set_type(&mut self, ty: impl Into<String>) -> String {}
fn set_subject(&mut self, subject: Option<impl Into<String>>) -> Option<String> {}
fn set_time(&mut self, time: Option<impl Into<DateTime<Utc>>>) -> Option<DateTime<Utc>> {}
fn set_datacontenttype(
&mut self,
datacontenttype: Option<impl Into<String>>,
) -> Option<String> {
}
fn set_dataschema(&mut self, dataschema: Option<impl Into<Url>>) -> Option<Url> {}
}
impl Default for Event {
fn default() -> Self {
Event {
attributes: Attributes::V10(AttributesV10::default()),
data: None,
extensions: HashMap::default(),
}
}
}
impl fmt::Display for Event {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
writeln!(f, "CloudEvent:")?;
self.iter()
.try_for_each(|(name, val)| writeln!(f, " {}: '{}'", name, val))?;
match self.data() {
Some(data) => write!(f, " {}", data)?,
None => write!(f, " No data")?,
}
writeln!(f)
}
}
impl Event {
/// Returns an [`Iterator`] for all the available [CloudEvents Context attributes](https://github.com/cloudevents/spec/blob/master/spec.md#context-attributes) and extensions.
/// Same as chaining [`Event::iter_attributes()`] and [`Event::iter_extensions()`]
pub fn iter(&self) -> impl Iterator<Item = (&str, AttributeValue)> {
self.iter_attributes()
.chain(self.extensions.iter().map(|(k, v)| (k.as_str(), v.into())))
}
/// Returns an [`Iterator`] for all the available [CloudEvents Context attributes](https://github.com/cloudevents/spec/blob/master/spec.md#context-attributes), excluding extensions.
/// This iterator does not contain the `data` field.
pub fn iter_attributes(&self) -> impl Iterator<Item = (&str, AttributeValue)> {
self.attributes.iter()
}
/// Get all the [extensions](https://github.com/cloudevents/spec/blob/master/spec.md#extension-context-attributes)
pub fn iter_extensions(&self) -> impl Iterator<Item = (&str, &ExtensionValue)> {
self.extensions.iter().map(|(k, v)| (k.as_str(), v))
}
/// Get `data` from this `Event`
pub fn data(&self) -> Option<&Data> {
self.data.as_ref()
}
/// Take (`datacontenttype`, `dataschema`, `data`) from this event, leaving these fields empty
///
/// ```
/// use cloudevents::Event;
/// use serde_json::json;
/// use std::convert::Into;
///
/// let mut e = Event::default();
/// e.set_data("application/json", json!({}));
///
/// let (datacontenttype, dataschema, data) = e.take_data();
/// ```
pub fn take_data(&mut self) -> (Option<String>, Option<Url>, Option<Data>) {
(
self.attributes.set_datacontenttype(None as Option<String>),
self.attributes.set_dataschema(None as Option<Url>),
self.data.take(),
)
}
/// Set `data` into this `Event` with the specified `datacontenttype`.
/// Returns the previous value of `datacontenttype` and `data`.
///
/// ```
/// use cloudevents::Event;
/// use serde_json::json;
/// use std::convert::Into;
///
/// let mut e = Event::default();
/// let (old_datacontenttype, old_data) = e.set_data("application/json", json!({}));
/// ```
pub fn set_data(
&mut self,
datacontenttype: impl Into<String>,
data: impl Into<Data>,
) -> (Option<String>, Option<Data>) {
(
self.attributes.set_datacontenttype(Some(datacontenttype)),
std::mem::replace(&mut self.data, Some(data.into())),
)
}
/// Set `data` into this `Event`, without checking if there is a `datacontenttype`.
/// Returns the previous value of `data`.
///
/// ```
/// use cloudevents::Event;
/// use serde_json::json;
/// use std::convert::Into;
///
/// let mut e = Event::default();
/// let old_data = e.set_data_unchecked(json!({}));
/// ```
pub fn set_data_unchecked(&mut self, data: impl Into<Data>) -> Option<Data> {
std::mem::replace(&mut self.data, Some(data.into()))
}
/// Get the [extension](https://github.com/cloudevents/spec/blob/master/spec.md#extension-context-attributes) named `extension_name`
pub fn extension(&self, extension_name: &str) -> Option<&ExtensionValue> {
self.extensions.get(extension_name)
}
/// Set the [extension](https://github.com/cloudevents/spec/blob/master/spec.md#extension-context-attributes) named `extension_name` with `extension_value`
pub fn set_extension<'name, 'event: 'name>(
&'event mut self,
extension_name: &'name str,
extension_value: impl Into<ExtensionValue>,
) {
self.extensions
.insert(extension_name.to_owned(), extension_value.into());
}
/// Remove the [extension](https://github.com/cloudevents/spec/blob/master/spec.md#extension-context-attributes) named `extension_name`
pub fn remove_extension<'name, 'event: 'name>(
&'event mut self,
extension_name: &'name str,
) -> Option<ExtensionValue> {
self.extensions.remove(extension_name)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn take_data() {
let mut e = Event::default();
e.set_data(
"application/json",
serde_json::json!({
"hello": "world"
}),
);
let (datacontenttype, dataschema, data) = e.take_data();
assert!(datacontenttype.is_some());
assert!(dataschema.is_none());
assert!(data.is_some());
assert!(e.data().is_none());
assert!(e.dataschema().is_none());
assert!(e.datacontenttype().is_none());
}
#[test]
fn set_id() {
let mut e = Event::default();
e.set_id("001");
assert_eq!(e.set_id("002"), String::from("001"));
assert_eq!(e.id(), "002")
}
#[test]
fn iter() {
let mut e = Event::default();
e.set_extension("aaa", "bbb");
e.set_data(
"application/json",
serde_json::json!({
"hello": "world"
}),
);
let mut v: HashMap<&str, AttributeValue> = e.iter().collect();
assert_eq!(
v.remove("specversion"),
Some(AttributeValue::SpecVersion(SpecVersion::V10))
);
assert_eq!(v.remove("aaa"), Some(AttributeValue::String("bbb")))
}
}

View File

@ -1,37 +1,36 @@
use super::{v03, v10}; use super::{v03, v10};
use lazy_static::lazy_static;
use serde::export::Formatter;
use std::collections::HashMap;
use std::convert::TryFrom; use std::convert::TryFrom;
use std::fmt; use std::fmt;
use std::fmt::Formatter;
pub(crate) const SPEC_VERSIONS: [&str; 2] = ["0.3", "1.0"]; 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
};
}
/// CloudEvent specification version. pub(crate) const SPEC_VERSIONS: [&'static str; 2] = ["0.3", "1.0"];
/// CloudEvent specification version
#[derive(PartialEq, Eq, Hash, Debug, Clone)] #[derive(PartialEq, Eq, Hash, Debug, Clone)]
pub enum SpecVersion { pub enum SpecVersion {
/// CloudEvents v0.3
V03, V03,
/// CloudEvents v1.0
V10, V10,
} }
impl SpecVersion { impl SpecVersion {
/// Returns the string representation of [`SpecVersion`].
#[inline]
pub fn as_str(&self) -> &str { pub fn as_str(&self) -> &str {
match self { match self {
SpecVersion::V03 => "0.3", SpecVersion::V03 => "0.3",
SpecVersion::V10 => "1.0", SpecVersion::V10 => "1.0",
} }
} }
/// Get all attribute names for this [`SpecVersion`].
#[inline]
pub fn attribute_names(&self) -> &'static [&'static str] {
match self {
SpecVersion::V03 => &v03::ATTRIBUTE_NAMES,
SpecVersion::V10 => &v10::ATTRIBUTE_NAMES,
}
}
} }
impl fmt::Display for SpecVersion { impl fmt::Display for SpecVersion {
@ -40,28 +39,28 @@ impl fmt::Display for SpecVersion {
} }
} }
/// Error representing an unknown [`SpecVersion`] string identifier /// Error representing an invalid [`SpecVersion`] string identifier
#[derive(Debug)] #[derive(Debug)]
pub struct UnknownSpecVersion { pub struct InvalidSpecVersion {
spec_version_value: String, spec_version_value: String,
} }
impl fmt::Display for UnknownSpecVersion { impl fmt::Display for InvalidSpecVersion {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
write!(f, "Invalid specversion {}", self.spec_version_value) write!(f, "Invalid specversion {}", self.spec_version_value)
} }
} }
impl std::error::Error for UnknownSpecVersion {} impl std::error::Error for InvalidSpecVersion {}
impl TryFrom<&str> for SpecVersion { impl TryFrom<&str> for SpecVersion {
type Error = UnknownSpecVersion; type Error = InvalidSpecVersion;
fn try_from(value: &str) -> Result<Self, UnknownSpecVersion> { fn try_from(value: &str) -> Result<Self, InvalidSpecVersion> {
match value { match value {
"0.3" => Ok(SpecVersion::V03), "0.3" => Ok(SpecVersion::V03),
"1.0" => Ok(SpecVersion::V10), "1.0" => Ok(SpecVersion::V10),
_ => Err(UnknownSpecVersion { _ => Err(InvalidSpecVersion {
spec_version_value: value.to_string(), spec_version_value: value.to_string(),
}), }),
} }

View File

@ -1,60 +0,0 @@
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;

View File

@ -1,11 +1,13 @@
use crate::event::attributes::{default_hostname, AttributeValue, AttributesConverter}; use crate::event::attributes::{
use crate::event::{AttributesReader, AttributesV10, AttributesWriter, SpecVersion, UriReference}; default_hostname, AttributeValue, AttributesConverter, DataAttributesWriter,
use crate::message::{BinarySerializer, MessageAttributeValue}; };
use crate::event::AttributesV10;
use crate::event::{AttributesReader, AttributesWriter, SpecVersion};
use chrono::{DateTime, Utc}; use chrono::{DateTime, Utc};
use url::Url; use url::Url;
use uuid::Uuid; use uuid::Uuid;
pub(crate) const ATTRIBUTE_NAMES: [&str; 8] = [ pub(crate) const ATTRIBUTE_NAMES: [&'static str; 8] = [
"specversion", "specversion",
"id", "id",
"type", "type",
@ -17,11 +19,11 @@ pub(crate) const ATTRIBUTE_NAMES: [&str; 8] = [
]; ];
/// Data structure representing [CloudEvents V0.3 context attributes](https://github.com/cloudevents/spec/blob/v0.3/spec.md#context-attributes) /// Data structure representing [CloudEvents V0.3 context attributes](https://github.com/cloudevents/spec/blob/v0.3/spec.md#context-attributes)
#[derive(PartialEq, Eq, Debug, Clone)] #[derive(PartialEq, Debug, Clone)]
pub struct Attributes { pub struct Attributes {
pub(crate) id: String, pub(crate) id: String,
pub(crate) ty: String, pub(crate) ty: String,
pub(crate) source: UriReference, pub(crate) source: Url,
pub(crate) datacontenttype: Option<String>, pub(crate) datacontenttype: Option<String>,
pub(crate) schemaurl: Option<Url>, pub(crate) schemaurl: Option<Url>,
pub(crate) subject: Option<String>, pub(crate) subject: Option<String>,
@ -40,36 +42,34 @@ impl<'a> IntoIterator for &'a Attributes {
} }
} }
#[derive(PartialEq, Debug, Clone, Copy)]
pub struct AttributesIntoIterator<'a> { pub struct AttributesIntoIterator<'a> {
pub(crate) attributes: &'a Attributes, attributes: &'a Attributes,
pub(crate) index: usize, index: usize,
} }
impl<'a> Iterator for AttributesIntoIterator<'a> { impl<'a> Iterator for AttributesIntoIterator<'a> {
type Item = (&'a str, AttributeValue<'a>); type Item = (&'a str, AttributeValue<'a>);
fn next(&mut self) -> Option<Self::Item> { fn next(&mut self) -> Option<Self::Item> {
let result = match self.index { let result = match self.index {
0 => Some(("specversion", AttributeValue::SpecVersion(SpecVersion::V03))), 0 => Some(("id", AttributeValue::String(&self.attributes.id))),
1 => Some(("id", AttributeValue::String(&self.attributes.id))), 1 => Some(("type", AttributeValue::String(&self.attributes.ty))),
2 => Some(("type", AttributeValue::String(&self.attributes.ty))), 2 => Some(("source", AttributeValue::URIRef(&self.attributes.source))),
3 => Some(("source", AttributeValue::URIRef(&self.attributes.source))), 3 => self
4 => self
.attributes .attributes
.datacontenttype .datacontenttype
.as_ref() .as_ref()
.map(|v| ("datacontenttype", AttributeValue::String(v))), .map(|v| ("datacontenttype", AttributeValue::String(v))),
5 => self 4 => self
.attributes .attributes
.schemaurl .schemaurl
.as_ref() .as_ref()
.map(|v| ("schemaurl", AttributeValue::URI(v))), .map(|v| ("schemaurl", AttributeValue::URIRef(v))),
6 => self 5 => self
.attributes .attributes
.subject .subject
.as_ref() .as_ref()
.map(|v| ("subject", AttributeValue::String(v))), .map(|v| ("subject", AttributeValue::String(v))),
7 => self 6 => self
.attributes .attributes
.time .time
.as_ref() .as_ref()
@ -85,69 +85,68 @@ impl<'a> Iterator for AttributesIntoIterator<'a> {
} }
impl AttributesReader for Attributes { impl AttributesReader for Attributes {
fn id(&self) -> &str { fn get_id(&self) -> &str {
&self.id &self.id
} }
fn source(&self) -> &UriReference { fn get_source(&self) -> &Url {
&self.source &self.source
} }
fn specversion(&self) -> SpecVersion { fn get_specversion(&self) -> SpecVersion {
SpecVersion::V03 SpecVersion::V03
} }
fn ty(&self) -> &str { fn get_type(&self) -> &str {
&self.ty &self.ty
} }
fn datacontenttype(&self) -> Option<&str> { fn get_datacontenttype(&self) -> Option<&str> {
self.datacontenttype.as_deref() self.datacontenttype.as_deref()
} }
fn dataschema(&self) -> Option<&Url> { fn get_dataschema(&self) -> Option<&Url> {
self.schemaurl.as_ref() self.schemaurl.as_ref()
} }
fn subject(&self) -> Option<&str> { fn get_subject(&self) -> Option<&str> {
self.subject.as_deref() self.subject.as_deref()
} }
fn time(&self) -> Option<&DateTime<Utc>> { fn get_time(&self) -> Option<&DateTime<Utc>> {
self.time.as_ref() self.time.as_ref()
} }
} }
impl AttributesWriter for Attributes { impl AttributesWriter for Attributes {
fn set_id(&mut self, id: impl Into<String>) -> String { fn set_id(&mut self, id: impl Into<String>) {
std::mem::replace(&mut self.id, id.into()) self.id = id.into()
} }
fn set_source(&mut self, source: impl Into<UriReference>) -> UriReference { fn set_source(&mut self, source: impl Into<Url>) {
std::mem::replace(&mut self.source, source.into()) self.source = source.into()
} }
fn set_type(&mut self, ty: impl Into<String>) -> String { fn set_type(&mut self, ty: impl Into<String>) {
std::mem::replace(&mut self.ty, ty.into()) self.ty = ty.into()
} }
fn set_subject(&mut self, subject: Option<impl Into<String>>) -> Option<String> { fn set_subject(&mut self, subject: Option<impl Into<String>>) {
std::mem::replace(&mut self.subject, subject.map(Into::into)) self.subject = subject.map(Into::into)
} }
fn set_time(&mut self, time: Option<impl Into<DateTime<Utc>>>) -> Option<DateTime<Utc>> { fn set_time(&mut self, time: Option<impl Into<DateTime<Utc>>>) {
std::mem::replace(&mut self.time, time.map(Into::into)) self.time = time.map(Into::into)
}
} }
fn set_datacontenttype( impl DataAttributesWriter for Attributes {
&mut self, fn set_datacontenttype(&mut self, datacontenttype: Option<impl Into<String>>) {
datacontenttype: Option<impl Into<String>>, self.datacontenttype = datacontenttype.map(Into::into)
) -> Option<String> {
std::mem::replace(&mut self.datacontenttype, datacontenttype.map(Into::into))
} }
fn set_dataschema(&mut self, dataschema: Option<impl Into<Url>>) -> Option<Url> { fn set_dataschema(&mut self, dataschema: Option<impl Into<Url>>) {
std::mem::replace(&mut self.schemaurl, dataschema.map(Into::into)) self.schemaurl = dataschema.map(Into::into)
} }
} }
@ -156,11 +155,11 @@ impl Default for Attributes {
Attributes { Attributes {
id: Uuid::new_v4().to_string(), id: Uuid::new_v4().to_string(),
ty: "type".to_string(), ty: "type".to_string(),
source: default_hostname().to_string(), source: default_hostname(),
datacontenttype: None, datacontenttype: None,
schemaurl: None, schemaurl: None,
subject: None, subject: None,
time: Some(Utc::now()), time: None,
} }
} }
} }
@ -183,75 +182,28 @@ impl AttributesConverter for Attributes {
} }
} }
impl crate::event::message::AttributesDeserializer for super::Attributes {
fn deserialize_attributes<R: Sized, V: BinarySerializer<R>>(
self,
mut visitor: V,
) -> crate::message::Result<V> {
visitor = visitor.set_attribute("id", MessageAttributeValue::String(self.id))?;
visitor = visitor.set_attribute("type", MessageAttributeValue::String(self.ty))?;
visitor = visitor.set_attribute("source", MessageAttributeValue::UriRef(self.source))?;
if self.datacontenttype.is_some() {
visitor = visitor.set_attribute(
"datacontenttype",
MessageAttributeValue::String(self.datacontenttype.unwrap()),
)?;
}
if self.schemaurl.is_some() {
visitor = visitor.set_attribute(
"schemaurl",
MessageAttributeValue::Uri(self.schemaurl.unwrap()),
)?;
}
if self.subject.is_some() {
visitor = visitor.set_attribute(
"subject",
MessageAttributeValue::String(self.subject.unwrap()),
)?;
}
if self.time.is_some() {
visitor = visitor
.set_attribute("time", MessageAttributeValue::DateTime(self.time.unwrap()))?;
}
Ok(visitor)
}
}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
use crate::test::fixtures; use chrono::NaiveDateTime;
use chrono::DateTime;
#[test]
fn iter_v03_test() {
let in_event = fixtures::v03::full_json_data();
let mut iter_v03 = in_event.iter_attributes();
assert_eq!(
("specversion", AttributeValue::SpecVersion(SpecVersion::V03)),
iter_v03.next().unwrap()
);
}
#[test] #[test]
fn iterator_test_v03() { fn iterator_test_v03() {
let a = Attributes { let a = Attributes {
id: String::from("1"), id: String::from("1"),
ty: String::from("someType"), ty: String::from("someType"),
source: "https://example.net".into(), source: Url::parse("https://example.net").unwrap(),
datacontenttype: None, datacontenttype: None,
schemaurl: None, schemaurl: None,
subject: None, subject: None,
time: DateTime::from_timestamp(61, 0), time: Some(DateTime::<Utc>::from_utc(
NaiveDateTime::from_timestamp(61, 0),
Utc,
)),
}; };
let b = &mut a.into_iter(); let b = &mut a.into_iter();
let time = DateTime::from_timestamp(61, 0).unwrap(); let time = DateTime::<Utc>::from_utc(NaiveDateTime::from_timestamp(61, 0), Utc);
assert_eq!(
("specversion", AttributeValue::SpecVersion(SpecVersion::V03)),
b.next().unwrap()
);
assert_eq!(("id", AttributeValue::String("1")), b.next().unwrap()); assert_eq!(("id", AttributeValue::String("1")), b.next().unwrap());
assert_eq!( assert_eq!(
("type", AttributeValue::String("someType")), ("type", AttributeValue::String("someType")),
@ -260,7 +212,7 @@ mod tests {
assert_eq!( assert_eq!(
( (
"source", "source",
AttributeValue::URIRef(&"https://example.net".to_string()) AttributeValue::URIRef(&Url::parse("https://example.net").unwrap())
), ),
b.next().unwrap() b.next().unwrap()
); );

View File

@ -1,68 +1,58 @@
use super::Attributes as AttributesV03; use super::Attributes as AttributesV03;
use crate::event::{ use crate::event::{Attributes, AttributesWriter, Data, Event, ExtensionValue};
Attributes, Data, Event, EventBuilderError, ExtensionValue, TryIntoTime, TryIntoUrl,
UriReference,
};
use crate::message::MessageAttributeValue;
use chrono::{DateTime, Utc}; use chrono::{DateTime, Utc};
use std::collections::HashMap; use std::collections::HashMap;
use std::convert::TryInto;
use url::Url; use url::Url;
/// Builder to create a CloudEvent V0.3 /// Builder to create a CloudEvent V0.3
#[derive(Clone, Debug)]
pub struct EventBuilder { pub struct EventBuilder {
id: Option<String>, event: Event,
ty: Option<String>,
source: Option<UriReference>,
datacontenttype: Option<String>,
schemaurl: Option<Url>,
subject: Option<String>,
time: Option<DateTime<Utc>>,
data: Option<Data>,
extensions: HashMap<String, ExtensionValue>,
error: Option<EventBuilderError>,
} }
impl EventBuilder { impl EventBuilder {
pub fn id(mut self, id: impl Into<String>) -> Self { pub fn from(event: Event) -> Self {
self.id = Some(id.into()); EventBuilder {
self event: Event {
attributes: event.attributes.into_v03(),
data: event.data,
extensions: event.extensions,
},
}
} }
pub fn source(mut self, source: impl Into<String>) -> Self { pub fn new() -> Self {
let source = source.into(); EventBuilder {
if source.is_empty() { event: Event {
self.error = Some(EventBuilderError::InvalidUriRefError { attributes: Attributes::V03(AttributesV03::default()),
attribute_name: "source", data: None,
}); extensions: HashMap::new(),
} else { },
self.source = Some(source);
} }
self }
pub fn id(mut self, id: impl Into<String>) -> Self {
self.event.set_id(id);
return self;
}
pub fn source(mut self, source: impl Into<Url>) -> Self {
self.event.set_source(source);
return self;
} }
pub fn ty(mut self, ty: impl Into<String>) -> Self { pub fn ty(mut self, ty: impl Into<String>) -> Self {
self.ty = Some(ty.into()); self.event.set_type(ty);
self return self;
} }
pub fn subject(mut self, subject: impl Into<String>) -> Self { pub fn subject(mut self, subject: impl Into<String>) -> Self {
self.subject = Some(subject.into()); self.event.set_subject(Some(subject));
self return self;
} }
pub fn time(mut self, time: impl TryIntoTime) -> Self { pub fn time(mut self, time: impl Into<DateTime<Utc>>) -> Self {
match time.into_time() { self.event.set_time(Some(time));
Ok(u) => self.time = Some(u), return self;
Err(e) => {
self.error = Some(EventBuilderError::ParseTimeError {
attribute_name: "time",
source: e,
})
}
};
self
} }
pub fn extension( pub fn extension(
@ -70,158 +60,40 @@ impl EventBuilder {
extension_name: &str, extension_name: &str,
extension_value: impl Into<ExtensionValue>, extension_value: impl Into<ExtensionValue>,
) -> Self { ) -> Self {
self.extensions self.event.set_extension(extension_name, extension_value);
.insert(extension_name.to_owned(), extension_value.into()); return self;
self
}
pub(crate) fn data_without_content_type(mut self, data: impl Into<Data>) -> Self {
self.data = Some(data.into());
self
} }
pub fn data(mut self, datacontenttype: impl Into<String>, data: impl Into<Data>) -> Self { pub fn data(mut self, datacontenttype: impl Into<String>, data: impl Into<Data>) -> Self {
self.datacontenttype = Some(datacontenttype.into()); self.event.write_data(datacontenttype, data);
self.data = Some(data.into()); return self;
self
} }
pub fn data_with_schema( pub fn data_with_schema(
mut self, mut self,
datacontenttype: impl Into<String>, datacontenttype: impl Into<String>,
schemaurl: impl TryIntoUrl, schemaurl: impl Into<Url>,
data: impl Into<Data>, data: impl Into<Data>,
) -> Self { ) -> Self {
self.datacontenttype = Some(datacontenttype.into()); self.event
match schemaurl.into_url() { .write_data_with_schema(datacontenttype, schemaurl, data);
Ok(u) => self.schemaurl = Some(u), return self;
Err(e) => {
self.error = Some(EventBuilderError::ParseUrlError {
attribute_name: "schemaurl",
source: e,
})
}
};
self.data = Some(data.into());
self
}
} }
impl From<Event> for EventBuilder { pub fn build(self) -> Event {
fn from(event: Event) -> Self { self.event
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,
}
}
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)] #[cfg(test)]
mod tests { mod tests {
use super::*;
use crate::assert_match_pattern; use crate::event::{AttributesReader, SpecVersion};
use chrono::{DateTime, Utc};
use crate::event::{
AttributesReader, EventBuilder, EventBuilderError, ExtensionValue, SpecVersion,
};
use crate::EventBuilderV03;
use std::convert::TryInto;
use url::Url;
#[test] #[test]
fn build_event() { fn build_event() {
let id = "aaa"; let id = "aaa";
let source = "http://localhost:8080"; let source = Url::parse("http://localhost:8080").unwrap();
let ty = "bbb"; let ty = "bbb";
let subject = "francesco"; let subject = "francesco";
let time: DateTime<Utc> = Utc::now(); let time: DateTime<Utc> = Utc::now();
@ -233,71 +105,30 @@ mod tests {
"hello": "world" "hello": "world"
}); });
let mut event = EventBuilderV03::new() let event = EventBuilder::new()
.id(id) .id(id)
.source(source.to_string()) .source(source.clone())
.ty(ty) .ty(ty)
.subject(subject) .subject(subject)
.time(time) .time(time)
.extension(extension_name, extension_value) .extension(extension_name, extension_value)
.data_with_schema(content_type, schema.clone(), data.clone()) .data_with_schema(content_type, schema.clone(), data.clone())
.build() .build();
.unwrap();
assert_eq!(SpecVersion::V03, event.specversion()); assert_eq!(SpecVersion::V03, event.get_specversion());
assert_eq!(id, event.id()); assert_eq!(id, event.get_id());
assert_eq!(source, event.source().clone()); assert_eq!(source, event.get_source().clone());
assert_eq!(ty, event.ty()); assert_eq!(ty, event.get_type());
assert_eq!(subject, event.subject().unwrap()); assert_eq!(subject, event.get_subject().unwrap());
assert_eq!(time, event.time().unwrap().clone()); assert_eq!(time, event.get_time().unwrap().clone());
assert_eq!( assert_eq!(
ExtensionValue::from(extension_value), ExtensionValue::from(extension_value),
event.extension(extension_name).unwrap().clone() event.get_extension(extension_name).unwrap().clone()
); );
assert_eq!(content_type, event.datacontenttype().unwrap()); assert_eq!(content_type, event.get_datacontenttype().unwrap());
assert_eq!(schema, event.dataschema().unwrap().clone()); assert_eq!(schema, event.get_dataschema().unwrap().clone());
let event_data: serde_json::Value = event.take_data().2.unwrap().try_into().unwrap(); let event_data: serde_json::Value = event.try_get_data().unwrap().unwrap();
assert_eq!(data, event_data); assert_eq!(data, event_data);
} }
#[test]
fn source_valid_relative_url() {
let res = EventBuilderV03::new()
.id("id1")
.source("/source") // relative URL
.ty("type")
.build();
assert_match_pattern!(res, Ok(_));
}
#[test]
fn build_missing_id() {
let res = EventBuilderV03::new()
.source("http://localhost:8080")
.build();
assert_match_pattern!(
res,
Err(EventBuilderError::MissingRequiredAttribute {
attribute_name: "id"
})
);
}
#[test]
fn source_invalid_url() {
let res = EventBuilderV03::new().source("").build();
assert_match_pattern!(
res,
Err(EventBuilderError::InvalidUriRefError {
attribute_name: "source",
})
);
}
#[test]
fn default_builds() {
let res = EventBuilderV03::default().build();
assert_match_pattern!(res, Ok(_));
}
} }

View File

@ -1,58 +1,53 @@
use super::Attributes; use super::Attributes;
use crate::event::data::is_json_content_type; use crate::event::data::is_json_content_type;
use crate::event::format::{
parse_data_base64, parse_data_base64_json, parse_data_json, parse_data_string,
};
use crate::event::{Data, ExtensionValue}; use crate::event::{Data, ExtensionValue};
use base64::prelude::*;
use chrono::{DateTime, Utc}; use chrono::{DateTime, Utc};
use serde::de::IntoDeserializer; use serde::de::IntoDeserializer;
use serde::ser::SerializeMap; use serde::ser::SerializeMap;
use serde::{Deserialize, Serializer}; use serde::{Deserialize, Serializer};
use serde_json::{Map, Value}; use serde_value::Value;
use std::collections::HashMap; use std::collections::{BTreeMap, HashMap};
use url::Url; use url::Url;
pub(crate) struct EventFormatDeserializer {} pub(crate) struct EventFormatDeserializer {}
impl crate::event::format::EventFormatDeserializer for EventFormatDeserializer { impl crate::event::format::EventFormatDeserializer for EventFormatDeserializer {
fn deserialize_attributes<E: serde::de::Error>( fn deserialize_attributes<E: serde::de::Error>(
map: &mut Map<String, Value>, map: &mut BTreeMap<String, Value>,
) -> Result<crate::event::Attributes, E> { ) -> Result<crate::event::Attributes, E> {
Ok(crate::event::Attributes::V03(Attributes { Ok(crate::event::Attributes::V03(Attributes {
id: extract_field!(map, "id", String, E)?, id: parse_field!(map, "id", String, E)?,
ty: extract_field!(map, "type", String, E)?, ty: parse_field!(map, "type", String, E)?,
source: extract_field!(map, "source", String, E)?, source: parse_field!(map, "source", String, E, Url::parse)?,
datacontenttype: extract_optional_field!(map, "datacontenttype", String, E)?, datacontenttype: parse_optional_field!(map, "datacontenttype", String, E)?,
schemaurl: extract_optional_field!(map, "schemaurl", String, E, |s: String| { schemaurl: parse_optional_field!(map, "schemaurl", String, E, Url::parse)?,
Url::parse(&s) subject: parse_optional_field!(map, "subject", String, E)?,
})?, time: parse_optional_field!(map, "time", String, E, |s| DateTime::parse_from_rfc3339(
subject: extract_optional_field!(map, "subject", String, E)?, s
time: extract_optional_field!(map, "time", String, E, |s: String| { )
DateTime::parse_from_rfc3339(&s).map(DateTime::<Utc>::from) .map(DateTime::<Utc>::from))?,
})?,
})) }))
} }
fn deserialize_data<E: serde::de::Error>( fn deserialize_data<E: serde::de::Error>(
content_type: &str, content_type: &str,
map: &mut Map<String, Value>, map: &mut BTreeMap<String, Value>,
) -> Result<Option<Data>, E> { ) -> Result<Option<Data>, E> {
let data = map.remove("data"); let data = map.remove("data");
let is_base64 = map let is_base64 = map
.remove("datacontentencoding") .remove("datacontentencoding")
.map(String::deserialize) .map(String::deserialize)
.transpose() .transpose()
.map_err(E::custom)? .map_err(|e| E::custom(e))?
.map(|dce| dce.to_lowercase() == "base64") .map(|dce| dce.to_lowercase() == "base64")
.unwrap_or(false); .unwrap_or(false);
let is_json = is_json_content_type(content_type); let is_json = is_json_content_type(content_type);
Ok(match (data, is_base64, is_json) { Ok(match (data, is_base64, is_json) {
(Some(d), false, true) => Some(Data::Json(parse_data_json(d)?)), (Some(d), false, true) => Some(Data::Json(parse_data_json!(d, E)?)),
(Some(d), false, false) => Some(Data::String(parse_data_string(d)?)), (Some(d), false, false) => Some(Data::String(parse_data_string!(d, E)?)),
(Some(d), true, true) => Some(Data::Json(parse_data_base64_json(d)?)), (Some(d), true, true) => Some(Data::Json(parse_json_data_base64!(d, E)?)),
(Some(d), true, false) => Some(Data::Binary(parse_data_base64(d)?)), (Some(d), true, false) => Some(Data::Binary(parse_data_base64!(d, E)?)),
(None, _, _) => None, (None, _, _) => None,
}) })
} }
@ -69,19 +64,16 @@ impl<S: serde::Serializer> crate::event::format::EventFormatSerializer<S, Attrib
extensions: &HashMap<String, ExtensionValue>, extensions: &HashMap<String, ExtensionValue>,
serializer: S, serializer: S,
) -> Result<<S as Serializer>::Ok, <S as Serializer>::Error> { ) -> Result<<S as Serializer>::Ok, <S as Serializer>::Error> {
let num = 4 let num =
+ [ 3 + if attributes.datacontenttype.is_some() {
attributes.datacontenttype.is_some(), 1
attributes.schemaurl.is_some(), } else {
attributes.subject.is_some(), 0
attributes.time.is_some(), } + if attributes.schemaurl.is_some() { 1 } else { 0 }
data.is_some(), + if attributes.subject.is_some() { 1 } else { 0 }
] + if attributes.time.is_some() { 1 } else { 0 }
.iter() + if data.is_some() { 1 } else { 0 }
.filter(|&b| *b)
.count()
+ extensions.len(); + extensions.len();
let mut state = serializer.serialize_map(Some(num))?; let mut state = serializer.serialize_map(Some(num))?;
state.serialize_entry("specversion", "0.3")?; state.serialize_entry("specversion", "0.3")?;
state.serialize_entry("id", &attributes.id)?; state.serialize_entry("id", &attributes.id)?;
@ -103,7 +95,7 @@ impl<S: serde::Serializer> crate::event::format::EventFormatSerializer<S, Attrib
Some(Data::Json(j)) => state.serialize_entry("data", j)?, Some(Data::Json(j)) => state.serialize_entry("data", j)?,
Some(Data::String(s)) => state.serialize_entry("data", s)?, Some(Data::String(s)) => state.serialize_entry("data", s)?,
Some(Data::Binary(v)) => { Some(Data::Binary(v)) => {
state.serialize_entry("data", &BASE64_STANDARD.encode(v))?; state.serialize_entry("data", &base64::encode(v))?;
state.serialize_entry("datacontentencoding", "base64")?; state.serialize_entry("datacontentencoding", "base64")?;
} }
_ => (), _ => (),

53
src/event/v03/message.rs Normal file
View File

@ -0,0 +1,53 @@
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(())
}
}

View File

@ -1,10 +1,10 @@
mod attributes; mod attributes;
mod builder; mod builder;
mod format; mod format;
mod message;
pub(crate) use crate::event::v03::format::EventFormatDeserializer;
pub(crate) use crate::event::v03::format::EventFormatSerializer;
pub use attributes::Attributes; pub use attributes::Attributes;
pub(crate) use attributes::AttributesIntoIterator;
pub(crate) use attributes::ATTRIBUTE_NAMES; pub(crate) use attributes::ATTRIBUTE_NAMES;
pub use builder::EventBuilder; pub use builder::EventBuilder;
pub(crate) use format::EventFormatDeserializer;
pub(crate) use format::EventFormatSerializer;

View File

@ -1,12 +1,12 @@
use crate::event::attributes::{default_hostname, AttributeValue, AttributesConverter}; use crate::event::attributes::{
use crate::event::{AttributesReader, AttributesV03, AttributesWriter, SpecVersion, UriReference}; default_hostname, AttributeValue, AttributesConverter, DataAttributesWriter,
use crate::message::{BinarySerializer, MessageAttributeValue}; };
use crate::event::{AttributesReader, AttributesV03, AttributesWriter, SpecVersion};
use chrono::{DateTime, Utc}; use chrono::{DateTime, Utc};
use core::fmt::Debug;
use url::Url; use url::Url;
use uuid::Uuid; use uuid::Uuid;
pub(crate) const ATTRIBUTE_NAMES: [&str; 8] = [ pub(crate) const ATTRIBUTE_NAMES: [&'static str; 8] = [
"specversion", "specversion",
"id", "id",
"type", "type",
@ -18,11 +18,11 @@ pub(crate) const ATTRIBUTE_NAMES: [&str; 8] = [
]; ];
/// Data structure representing [CloudEvents V1.0 context attributes](https://github.com/cloudevents/spec/blob/v1.0/spec.md#context-attributes) /// Data structure representing [CloudEvents V1.0 context attributes](https://github.com/cloudevents/spec/blob/v1.0/spec.md#context-attributes)
#[derive(PartialEq, Eq, Debug, Clone)] #[derive(PartialEq, Debug, Clone)]
pub struct Attributes { pub struct Attributes {
pub(crate) id: String, pub(crate) id: String,
pub(crate) ty: String, pub(crate) ty: String,
pub(crate) source: UriReference, pub(crate) source: Url,
pub(crate) datacontenttype: Option<String>, pub(crate) datacontenttype: Option<String>,
pub(crate) dataschema: Option<Url>, pub(crate) dataschema: Option<Url>,
pub(crate) subject: Option<String>, pub(crate) subject: Option<String>,
@ -41,36 +41,34 @@ impl<'a> IntoIterator for &'a Attributes {
} }
} }
#[derive(PartialEq, Debug, Clone, Copy)]
pub struct AttributesIntoIterator<'a> { pub struct AttributesIntoIterator<'a> {
pub(crate) attributes: &'a Attributes, attributes: &'a Attributes,
pub(crate) index: usize, index: usize,
} }
impl<'a> Iterator for AttributesIntoIterator<'a> { impl<'a> Iterator for AttributesIntoIterator<'a> {
type Item = (&'a str, AttributeValue<'a>); type Item = (&'a str, AttributeValue<'a>);
fn next(&mut self) -> Option<Self::Item> { fn next(&mut self) -> Option<Self::Item> {
let result = match self.index { let result = match self.index {
0 => Some(("specversion", AttributeValue::SpecVersion(SpecVersion::V10))), 0 => Some(("id", AttributeValue::String(&self.attributes.id))),
1 => Some(("id", AttributeValue::String(&self.attributes.id))), 1 => Some(("type", AttributeValue::String(&self.attributes.ty))),
2 => Some(("type", AttributeValue::String(&self.attributes.ty))), 2 => Some(("source", AttributeValue::URIRef(&self.attributes.source))),
3 => Some(("source", AttributeValue::URIRef(&self.attributes.source))), 3 => self
4 => self
.attributes .attributes
.datacontenttype .datacontenttype
.as_ref() .as_ref()
.map(|v| ("datacontenttype", AttributeValue::String(v))), .map(|v| ("datacontenttype", AttributeValue::String(v))),
5 => self 4 => self
.attributes .attributes
.dataschema .dataschema
.as_ref() .as_ref()
.map(|v| ("dataschema", AttributeValue::URI(v))), .map(|v| ("dataschema", AttributeValue::URI(v))),
6 => self 5 => self
.attributes .attributes
.subject .subject
.as_ref() .as_ref()
.map(|v| ("subject", AttributeValue::String(v))), .map(|v| ("subject", AttributeValue::String(v))),
7 => self 6 => self
.attributes .attributes
.time .time
.as_ref() .as_ref()
@ -86,69 +84,68 @@ impl<'a> Iterator for AttributesIntoIterator<'a> {
} }
impl AttributesReader for Attributes { impl AttributesReader for Attributes {
fn id(&self) -> &str { fn get_id(&self) -> &str {
&self.id &self.id
} }
fn source(&self) -> &UriReference { fn get_source(&self) -> &Url {
&self.source &self.source
} }
fn specversion(&self) -> SpecVersion { fn get_specversion(&self) -> SpecVersion {
SpecVersion::V10 SpecVersion::V10
} }
fn ty(&self) -> &str { fn get_type(&self) -> &str {
&self.ty &self.ty
} }
fn datacontenttype(&self) -> Option<&str> { fn get_datacontenttype(&self) -> Option<&str> {
self.datacontenttype.as_deref() self.datacontenttype.as_deref()
} }
fn dataschema(&self) -> Option<&Url> { fn get_dataschema(&self) -> Option<&Url> {
self.dataschema.as_ref() self.dataschema.as_ref()
} }
fn subject(&self) -> Option<&str> { fn get_subject(&self) -> Option<&str> {
self.subject.as_deref() self.subject.as_deref()
} }
fn time(&self) -> Option<&DateTime<Utc>> { fn get_time(&self) -> Option<&DateTime<Utc>> {
self.time.as_ref() self.time.as_ref()
} }
} }
impl AttributesWriter for Attributes { impl AttributesWriter for Attributes {
fn set_id(&mut self, id: impl Into<String>) -> String { fn set_id(&mut self, id: impl Into<String>) {
std::mem::replace(&mut self.id, id.into()) self.id = id.into()
} }
fn set_source(&mut self, source: impl Into<UriReference>) -> UriReference { fn set_source(&mut self, source: impl Into<Url>) {
std::mem::replace(&mut self.source, source.into()) self.source = source.into()
} }
fn set_type(&mut self, ty: impl Into<String>) -> String { fn set_type(&mut self, ty: impl Into<String>) {
std::mem::replace(&mut self.ty, ty.into()) self.ty = ty.into()
} }
fn set_subject(&mut self, subject: Option<impl Into<String>>) -> Option<String> { fn set_subject(&mut self, subject: Option<impl Into<String>>) {
std::mem::replace(&mut self.subject, subject.map(Into::into)) self.subject = subject.map(Into::into)
} }
fn set_time(&mut self, time: Option<impl Into<DateTime<Utc>>>) -> Option<DateTime<Utc>> { fn set_time(&mut self, time: Option<impl Into<DateTime<Utc>>>) {
std::mem::replace(&mut self.time, time.map(Into::into)) self.time = time.map(Into::into)
}
} }
fn set_datacontenttype( impl DataAttributesWriter for Attributes {
&mut self, fn set_datacontenttype(&mut self, datacontenttype: Option<impl Into<String>>) {
datacontenttype: Option<impl Into<String>>, self.datacontenttype = datacontenttype.map(Into::into)
) -> Option<String> {
std::mem::replace(&mut self.datacontenttype, datacontenttype.map(Into::into))
} }
fn set_dataschema(&mut self, dataschema: Option<impl Into<Url>>) -> Option<Url> { fn set_dataschema(&mut self, dataschema: Option<impl Into<Url>>) {
std::mem::replace(&mut self.dataschema, dataschema.map(Into::into)) self.dataschema = dataschema.map(Into::into)
} }
} }
@ -157,49 +154,15 @@ impl Default for Attributes {
Attributes { Attributes {
id: Uuid::new_v4().to_string(), id: Uuid::new_v4().to_string(),
ty: "type".to_string(), ty: "type".to_string(),
source: default_hostname().to_string(), source: default_hostname(),
datacontenttype: None, datacontenttype: None,
dataschema: None, dataschema: None,
subject: None, subject: None,
time: Some(Utc::now()), time: None,
} }
} }
} }
impl crate::event::message::AttributesDeserializer for super::Attributes {
fn deserialize_attributes<R: Sized, V: BinarySerializer<R>>(
self,
mut visitor: V,
) -> crate::message::Result<V> {
visitor = visitor.set_attribute("id", MessageAttributeValue::String(self.id))?;
visitor = visitor.set_attribute("type", MessageAttributeValue::String(self.ty))?;
visitor = visitor.set_attribute("source", MessageAttributeValue::UriRef(self.source))?;
if self.datacontenttype.is_some() {
visitor = visitor.set_attribute(
"datacontenttype",
MessageAttributeValue::String(self.datacontenttype.unwrap()),
)?;
}
if self.dataschema.is_some() {
visitor = visitor.set_attribute(
"dataschema",
MessageAttributeValue::Uri(self.dataschema.unwrap()),
)?;
}
if self.subject.is_some() {
visitor = visitor.set_attribute(
"subject",
MessageAttributeValue::String(self.subject.unwrap()),
)?;
}
if self.time.is_some() {
visitor = visitor
.set_attribute("time", MessageAttributeValue::DateTime(self.time.unwrap()))?;
}
Ok(visitor)
}
}
impl AttributesConverter for Attributes { impl AttributesConverter for Attributes {
fn into_v10(self) -> Self { fn into_v10(self) -> Self {
self self
@ -221,37 +184,25 @@ impl AttributesConverter for Attributes {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
use crate::test::fixtures; use chrono::NaiveDateTime;
#[test]
fn iter_v10_test() {
let in_event = fixtures::v10::full_no_data();
let mut iter_v10 = in_event.iter_attributes();
assert_eq!(
("specversion", AttributeValue::SpecVersion(SpecVersion::V10)),
iter_v10.next().unwrap()
);
}
#[test] #[test]
fn iterator_test_v10() { fn iterator_test_v10() {
let a = Attributes { let a = Attributes {
id: String::from("1"), id: String::from("1"),
ty: String::from("someType"), ty: String::from("someType"),
source: "https://example.net".into(), source: Url::parse("https://example.net").unwrap(),
datacontenttype: None, datacontenttype: None,
dataschema: None, dataschema: None,
subject: None, subject: None,
time: DateTime::from_timestamp(61, 0), time: Some(DateTime::<Utc>::from_utc(
NaiveDateTime::from_timestamp(61, 0),
Utc,
)),
}; };
let b = &mut a.into_iter(); let b = &mut a.into_iter();
let time = DateTime::from_timestamp(61, 0).unwrap(); let time = DateTime::<Utc>::from_utc(NaiveDateTime::from_timestamp(61, 0), Utc);
assert_eq!(
("specversion", AttributeValue::SpecVersion(SpecVersion::V10)),
b.next().unwrap()
);
assert_eq!(("id", AttributeValue::String("1")), b.next().unwrap()); assert_eq!(("id", AttributeValue::String("1")), b.next().unwrap());
assert_eq!( assert_eq!(
("type", AttributeValue::String("someType")), ("type", AttributeValue::String("someType")),
@ -260,7 +211,7 @@ mod tests {
assert_eq!( assert_eq!(
( (
"source", "source",
AttributeValue::URIRef(&"https://example.net".to_string()) AttributeValue::URIRef(&Url::parse("https://example.net").unwrap())
), ),
b.next().unwrap() b.next().unwrap()
); );

View File

@ -1,68 +1,58 @@
use super::Attributes as AttributesV10; use super::Attributes as AttributesV10;
use crate::event::{ use crate::event::{Attributes, AttributesWriter, Data, Event, ExtensionValue};
Attributes, Data, Event, EventBuilderError, ExtensionValue, TryIntoTime, TryIntoUrl,
UriReference,
};
use crate::message::MessageAttributeValue;
use chrono::{DateTime, Utc}; use chrono::{DateTime, Utc};
use std::collections::HashMap; use std::collections::HashMap;
use std::convert::TryInto;
use url::Url; use url::Url;
/// Builder to create a CloudEvent V1.0 /// Builder to create a CloudEvent V1.0
#[derive(Clone, Debug)]
pub struct EventBuilder { pub struct EventBuilder {
id: Option<String>, event: Event,
ty: Option<String>,
source: Option<UriReference>,
datacontenttype: Option<String>,
dataschema: Option<Url>,
subject: Option<String>,
time: Option<DateTime<Utc>>,
data: Option<Data>,
extensions: HashMap<String, ExtensionValue>,
error: Option<EventBuilderError>,
} }
impl EventBuilder { impl EventBuilder {
pub fn id(mut self, id: impl Into<String>) -> Self { pub fn from(event: Event) -> Self {
self.id = Some(id.into()); EventBuilder {
self event: Event {
attributes: event.attributes.into_v10(),
data: event.data,
extensions: event.extensions,
},
}
} }
pub fn source(mut self, source: impl Into<String>) -> Self { pub fn new() -> Self {
let source = source.into(); EventBuilder {
if source.is_empty() { event: Event {
self.error = Some(EventBuilderError::InvalidUriRefError { attributes: Attributes::V10(AttributesV10::default()),
attribute_name: "source", data: None,
}); extensions: HashMap::new(),
} else { },
self.source = Some(source);
} }
self }
pub fn id(mut self, id: impl Into<String>) -> Self {
self.event.set_id(id);
return self;
}
pub fn source(mut self, source: impl Into<Url>) -> Self {
self.event.set_source(source);
return self;
} }
pub fn ty(mut self, ty: impl Into<String>) -> Self { pub fn ty(mut self, ty: impl Into<String>) -> Self {
self.ty = Some(ty.into()); self.event.set_type(ty);
self return self;
} }
pub fn subject(mut self, subject: impl Into<String>) -> Self { pub fn subject(mut self, subject: impl Into<String>) -> Self {
self.subject = Some(subject.into()); self.event.set_subject(Some(subject));
self return self;
} }
pub fn time(mut self, time: impl TryIntoTime) -> Self { pub fn time(mut self, time: impl Into<DateTime<Utc>>) -> Self {
match time.into_time() { self.event.set_time(Some(time));
Ok(u) => self.time = Some(u), return self;
Err(e) => {
self.error = Some(EventBuilderError::ParseTimeError {
attribute_name: "time",
source: e,
})
}
};
self
} }
pub fn extension( pub fn extension(
@ -70,158 +60,40 @@ impl EventBuilder {
extension_name: &str, extension_name: &str,
extension_value: impl Into<ExtensionValue>, extension_value: impl Into<ExtensionValue>,
) -> Self { ) -> Self {
self.extensions self.event.set_extension(extension_name, extension_value);
.insert(extension_name.to_owned(), extension_value.into()); return self;
self
}
pub(crate) fn data_without_content_type(mut self, data: impl Into<Data>) -> Self {
self.data = Some(data.into());
self
} }
pub fn data(mut self, datacontenttype: impl Into<String>, data: impl Into<Data>) -> Self { pub fn data(mut self, datacontenttype: impl Into<String>, data: impl Into<Data>) -> Self {
self.datacontenttype = Some(datacontenttype.into()); self.event.write_data(datacontenttype, data);
self.data = Some(data.into()); return self;
self
} }
pub fn data_with_schema( pub fn data_with_schema(
mut self, mut self,
datacontenttype: impl Into<String>, datacontenttype: impl Into<String>,
schemaurl: impl TryIntoUrl, dataschema: impl Into<Url>,
data: impl Into<Data>, data: impl Into<Data>,
) -> Self { ) -> Self {
self.datacontenttype = Some(datacontenttype.into()); self.event
match schemaurl.into_url() { .write_data_with_schema(datacontenttype, dataschema, data);
Ok(u) => self.dataschema = Some(u), return self;
Err(e) => {
self.error = Some(EventBuilderError::ParseUrlError {
attribute_name: "dataschema",
source: e,
})
}
};
self.data = Some(data.into());
self
}
} }
impl From<Event> for EventBuilder { pub fn build(self) -> Event {
fn from(event: Event) -> Self { self.event
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,
}
}
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)] #[cfg(test)]
mod tests { mod tests {
use super::*;
use crate::assert_match_pattern; use crate::event::{AttributesReader, SpecVersion};
use chrono::{DateTime, Utc};
use crate::event::{
AttributesReader, EventBuilder, EventBuilderError, ExtensionValue, SpecVersion,
};
use crate::EventBuilderV10;
use std::convert::TryInto;
use url::Url;
#[test] #[test]
fn build_event() { fn build_event() {
let id = "aaa"; let id = "aaa";
let source = "http://localhost:8080"; let source = Url::parse("http://localhost:8080").unwrap();
let ty = "bbb"; let ty = "bbb";
let subject = "francesco"; let subject = "francesco";
let time: DateTime<Utc> = Utc::now(); let time: DateTime<Utc> = Utc::now();
@ -233,71 +105,30 @@ mod tests {
"hello": "world" "hello": "world"
}); });
let mut event = EventBuilderV10::new() let event = EventBuilder::new()
.id(id) .id(id)
.source(source.to_string()) .source(source.clone())
.ty(ty) .ty(ty)
.subject(subject) .subject(subject)
.time(time) .time(time)
.extension(extension_name, extension_value) .extension(extension_name, extension_value)
.data_with_schema(content_type, schema.clone(), data.clone()) .data_with_schema(content_type, schema.clone(), data.clone())
.build() .build();
.unwrap();
assert_eq!(SpecVersion::V10, event.specversion()); assert_eq!(SpecVersion::V10, event.get_specversion());
assert_eq!(id, event.id()); assert_eq!(id, event.get_id());
assert_eq!(source, event.source().clone()); assert_eq!(source, event.get_source().clone());
assert_eq!(ty, event.ty()); assert_eq!(ty, event.get_type());
assert_eq!(subject, event.subject().unwrap()); assert_eq!(subject, event.get_subject().unwrap());
assert_eq!(time, event.time().unwrap().clone()); assert_eq!(time, event.get_time().unwrap().clone());
assert_eq!( assert_eq!(
ExtensionValue::from(extension_value), ExtensionValue::from(extension_value),
event.extension(extension_name).unwrap().clone() event.get_extension(extension_name).unwrap().clone()
); );
assert_eq!(content_type, event.datacontenttype().unwrap()); assert_eq!(content_type, event.get_datacontenttype().unwrap());
assert_eq!(schema, event.dataschema().unwrap().clone()); assert_eq!(schema, event.get_dataschema().unwrap().clone());
let event_data: serde_json::Value = event.take_data().2.unwrap().try_into().unwrap(); let event_data: serde_json::Value = event.try_get_data().unwrap().unwrap();
assert_eq!(data, event_data); assert_eq!(data, event_data);
} }
#[test]
fn source_valid_relative_url() {
let res = EventBuilderV10::new()
.id("id1")
.source("/source") // relative URL
.ty("type")
.build();
assert_match_pattern!(res, Ok(_));
}
#[test]
fn build_missing_id() {
let res = EventBuilderV10::new()
.source("http://localhost:8080")
.build();
assert_match_pattern!(
res,
Err(EventBuilderError::MissingRequiredAttribute {
attribute_name: "id"
})
);
}
#[test]
fn source_invalid_url() {
let res = EventBuilderV10::new().source("").build();
assert_match_pattern!(
res,
Err(EventBuilderError::InvalidUriRefError {
attribute_name: "source",
})
);
}
#[test]
fn default_builds() {
let res = EventBuilderV10::default().build();
assert_match_pattern!(res, Ok(_));
}
} }

View File

@ -1,42 +1,37 @@
use super::Attributes; use super::Attributes;
use crate::event::data::is_json_content_type; use crate::event::data::is_json_content_type;
use crate::event::format::{
parse_data_base64, parse_data_base64_json, parse_data_json, parse_data_string,
};
use crate::event::{Data, ExtensionValue}; use crate::event::{Data, ExtensionValue};
use base64::prelude::*;
use chrono::{DateTime, Utc}; use chrono::{DateTime, Utc};
use serde::de::IntoDeserializer; use serde::de::IntoDeserializer;
use serde::ser::SerializeMap; use serde::ser::SerializeMap;
use serde::{Deserialize, Serializer}; use serde::{Deserialize, Serializer};
use serde_json::{Map, Value}; use serde_value::Value;
use std::collections::HashMap; use std::collections::{BTreeMap, HashMap};
use url::Url; use url::Url;
pub(crate) struct EventFormatDeserializer {} pub(crate) struct EventFormatDeserializer {}
impl crate::event::format::EventFormatDeserializer for EventFormatDeserializer { impl crate::event::format::EventFormatDeserializer for EventFormatDeserializer {
fn deserialize_attributes<E: serde::de::Error>( fn deserialize_attributes<E: serde::de::Error>(
map: &mut Map<String, Value>, map: &mut BTreeMap<String, Value>,
) -> Result<crate::event::Attributes, E> { ) -> Result<crate::event::Attributes, E> {
Ok(crate::event::Attributes::V10(Attributes { Ok(crate::event::Attributes::V10(Attributes {
id: extract_field!(map, "id", String, E)?, id: parse_field!(map, "id", String, E)?,
ty: extract_field!(map, "type", String, E)?, ty: parse_field!(map, "type", String, E)?,
source: extract_field!(map, "source", String, E)?, source: parse_field!(map, "source", String, E, Url::parse)?,
datacontenttype: extract_optional_field!(map, "datacontenttype", String, E)?, datacontenttype: parse_optional_field!(map, "datacontenttype", String, E)?,
dataschema: extract_optional_field!(map, "dataschema", String, E, |s: String| { dataschema: parse_optional_field!(map, "dataschema", String, E, Url::parse)?,
Url::parse(&s) subject: parse_optional_field!(map, "subject", String, E)?,
})?, time: parse_optional_field!(map, "time", String, E, |s| DateTime::parse_from_rfc3339(
subject: extract_optional_field!(map, "subject", String, E)?, s
time: extract_optional_field!(map, "time", String, E, |s: String| { )
DateTime::parse_from_rfc3339(&s).map(DateTime::<Utc>::from) .map(DateTime::<Utc>::from))?,
})?,
})) }))
} }
fn deserialize_data<E: serde::de::Error>( fn deserialize_data<E: serde::de::Error>(
content_type: &str, content_type: &str,
map: &mut Map<String, Value>, map: &mut BTreeMap<String, Value>,
) -> Result<Option<Data>, E> { ) -> Result<Option<Data>, E> {
let data = map.remove("data"); let data = map.remove("data");
let data_base64 = map.remove("data_base64"); let data_base64 = map.remove("data_base64");
@ -44,16 +39,11 @@ impl crate::event::format::EventFormatDeserializer for EventFormatDeserializer {
let is_json = is_json_content_type(content_type); let is_json = is_json_content_type(content_type);
Ok(match (data, data_base64, is_json) { Ok(match (data, data_base64, is_json) {
(Some(d), None, true) => Some(Data::Json(parse_data_json(d)?)), (Some(d), None, true) => Some(Data::Json(parse_data_json!(d, E)?)),
(Some(d), None, false) => Some(Data::String(parse_data_string(d)?)), (Some(d), None, false) => Some(Data::String(parse_data_string!(d, E)?)),
(None, Some(d), true) => match parse_data_base64_json::<E>(d.to_owned()) { (None, Some(d), true) => Some(Data::Json(parse_json_data_base64!(d, E)?)),
Ok(x) => Some(Data::Json(x)), (None, Some(d), false) => Some(Data::Binary(parse_data_base64!(d, E)?)),
Err(_) => Some(Data::Binary(parse_data_base64(d)?)), (Some(_), Some(_), _) => Err(E::custom("Cannot have both data and data_base64 field"))?,
},
(None, Some(d), false) => Some(Data::Binary(parse_data_base64(d)?)),
(Some(_), Some(_), _) => {
return Err(E::custom("Cannot have both data and data_base64 field"))
}
(None, None, _) => None, (None, None, _) => None,
}) })
} }
@ -70,19 +60,19 @@ impl<S: serde::Serializer> crate::event::format::EventFormatSerializer<S, Attrib
extensions: &HashMap<String, ExtensionValue>, extensions: &HashMap<String, ExtensionValue>,
serializer: S, serializer: S,
) -> Result<<S as Serializer>::Ok, <S as Serializer>::Error> { ) -> Result<<S as Serializer>::Ok, <S as Serializer>::Error> {
let num = 4 let num =
+ [ 3 + if attributes.datacontenttype.is_some() {
attributes.datacontenttype.is_some(), 1
attributes.dataschema.is_some(), } else {
attributes.subject.is_some(), 0
attributes.time.is_some(), } + if attributes.dataschema.is_some() {
data.is_some(), 1
] } else {
.iter() 0
.filter(|&b| *b) } + if attributes.subject.is_some() { 1 } else { 0 }
.count() + if attributes.time.is_some() { 1 } else { 0 }
+ if data.is_some() { 1 } else { 0 }
+ extensions.len(); + extensions.len();
let mut state = serializer.serialize_map(Some(num))?; let mut state = serializer.serialize_map(Some(num))?;
state.serialize_entry("specversion", "1.0")?; state.serialize_entry("specversion", "1.0")?;
state.serialize_entry("id", &attributes.id)?; state.serialize_entry("id", &attributes.id)?;
@ -103,9 +93,7 @@ impl<S: serde::Serializer> crate::event::format::EventFormatSerializer<S, Attrib
match data { match data {
Some(Data::Json(j)) => state.serialize_entry("data", j)?, Some(Data::Json(j)) => state.serialize_entry("data", j)?,
Some(Data::String(s)) => state.serialize_entry("data", s)?, Some(Data::String(s)) => state.serialize_entry("data", s)?,
Some(Data::Binary(v)) => { Some(Data::Binary(v)) => state.serialize_entry("data_base64", &base64::encode(v))?,
state.serialize_entry("data_base64", &BASE64_STANDARD.encode(v))?
}
_ => (), _ => (),
}; };
for (k, v) in extensions { for (k, v) in extensions {

Some files were not shown because too many files have changed in this diff Show More