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
.idea
.vscode
.DS_Store
**/Cargo.lock

View File

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

View File

@ -1,6 +1,6 @@
[package]
name = "cloudevents-sdk"
version = "0.8.0"
version = "0.1.0"
authors = ["Francesco Guardiani <francescoguard@gmail.com>"]
license-file = "LICENSE"
edition = "2018"
@ -11,95 +11,42 @@ repository = "https://github.com/cloudevents/sdk-rust"
exclude = [
".github/*"
]
categories = ["web-programming", "encoding", "data-structures"]
resolver = "2"
# Enable all features when building on docs.rs to show feature gated bindings
[package.metadata.docs.rs]
all-features = true
rustdoc-args = ["--cfg", "docsrs"]
[lib]
name = "cloudevents"
[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"]
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
serde = { version = "^1.0", features = ["derive"] }
serde_json = "^1.0"
serde-value = "^0.6"
chrono = { version = "^0.4", features = ["serde"] }
delegate-attr = "^0.3"
base64 = "^0.22"
url = { version = "^2.5", features = ["serde"] }
snafu = "^0.8"
bitflags = "^2.6"
uuid = { version = "1", features = ["v4"] }
# runtime optional deps
actix-web = { version = "4", optional = true }
actix-http = { version = "3", optional = true }
reqwest-lib = { version = "^0.12", default-features = false, features = ["rustls-tls"], optional = true, package = "reqwest" }
rdkafka-lib = { version = "^0.37", features = ["cmake-build"], optional = true, package = "rdkafka" }
warp-lib = { version = "^0.3", optional = true, package = "warp" }
async-trait = { version = "^0.1", optional = true }
bytes = { version = "^1.0", optional = true }
futures = { version = "^0.3", optional = true, features = ["compat"]}
http = { version = "1.2", optional = true}
http-0-2 = { version = "0.2", optional = true, package = "http"}
axum-lib = { version = "^0.8", optional = true, package="axum"}
http-body-util = {version = "^0.1", optional = true}
poem-lib = { version = "^3.1", optional = true, package = "poem" }
nats-lib = { version = "0.25.0", optional = true, package = "nats" }
delegate = "^0.4"
base64 = "^0.12"
url = { version = "^2.1", features = ["serde"] }
snafu = "^0.6"
lazy_static = "1.4.0"
[target."cfg(not(target_arch = \"wasm32\"))".dependencies]
hostname = "^0.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"] }
[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 }
uuid = { version = "^0.8", features = ["v4", "wasm-bindgen"] }
[dev-dependencies]
rstest = "0.23"
claims = "0.8"
version-sync = "0.9.2"
serde_yaml = "^0.9"
rmp-serde = "1"
rstest = "0.6"
claim = "0.3.1"
# runtime dev-deps
[lib]
name = "cloudevents"
url = { version = "^2.1", features = ["serde"] }
serde_json = { version = "^1.0" }
chrono = { version = "^0.4", features = ["serde"] }
mockito = "0.31.1"
mime = "0.3"
[target.'cfg(not(target_os = "wasi"))'.dev-dependencies]
actix-rt = { version = "^2" }
tokio = { version = "^1.0", features = ["full"] }
tower = { version = "0.5", features = ["util"] }
[target.'cfg(all(target_arch = "wasm32", target_os = "wasi"))'.dev-dependencies]
tokio_wasi = { version = "1", features = [
"io-util",
"fs",
"net",
"time",
"rt",
"macros",
] }
[workspace]
members = [
".",
"cloudevents-sdk-actix-web",
"cloudevents-sdk-reqwest"
]
exclude = [
"example-projects/actix-web-example",
"example-projects/reqwest-wasm-example"
]

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.
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
| | [v0.3](https://github.com/cloudevents/spec/tree/v0.3) | [v1.0](https://github.com/cloudevents/spec/tree/v1.0) |
| :---------------------------: | :----------------------------------------------------------------------------: | :---------------------------------------------------------------------------------: |
| CloudEvents Core | ✓ | ✓ |
| AMQP Protocol Binding | ✕ | ✕ |
| AVRO Event Format | ✕ | ✕ |
| HTTP Protocol Binding | ✓ | ✓ |
| JSON Event Format | ✓ | ✓ |
| Kafka Protocol Binding | ✓ | ✓ |
| MQTT Protocol Binding | ✕ | ✕ |
| NATS Protocol Binding | ✓ | ✓ |
| Web hook | ✕ | ✕ |
| CloudEvents Core | :heavy_check_mark: | :heavy_check_mark: |
| AMQP Protocol Binding | :x: | :x: |
| AVRO Event Format | :x: | :x: |
| HTTP Protocol Binding | :heavy_check_mark: | :heavy_check_mark: |
| JSON Event Format | :heavy_check_mark: | :heavy_check_mark: |
| Kafka Protocol Binding | :x: | :x: |
| MQTT Protocol Binding | :x: | :x: |
| NATS Protocol Binding | :x: | :x: |
| Web hook | :x: | :x: |
## Crate Structure
## Crates
The core modules include definitions for the `Event` and
`EventBuilder` data structures, JSON serialization rules, and a
mechanism to support various Protocol Bindings, each of which is
enabled by a specific [feature flag]:
* `actix`: Integration with [actix](https://actix.rs/).
* `axum`: Integration with [axum](https://lib.rs/crates/axum).
* `warp`: Integration with [warp](https://github.com/seanmonstar/warp/).
* `reqwest`: Integration with [reqwest](https://github.com/seanmonstar/reqwest).
* `rdkafka`: Integration with [rdkafka](https://fede1024.github.io/rust-rdkafka).
* `nats`: Integration with [nats](https://github.com/nats-io/nats.rs)
This crate is continuously tested to work with GNU libc, WASM and musl
toolchains.
* `cloudevents-sdk`: Provides Event data structure, JSON Event format implementation. This module is tested to work with GNU libc, WASM and musl toolchains.
* `cloudevents-sdk-actix-web`: Integration with [Actix Web](https://github.com/actix/actix-web).
* `cloudevents-sdk-reqwest`: Integration with [reqwest](https://github.com/seanmonstar/reqwest).
## Get Started
To get started, add the dependency to `Cargo.toml`, optionally
enabling your Protocol Binding of choice:
To get started, add the dependency to `Cargo.toml`:
```toml
[dependencies]
cloudevents-sdk = { version = "0.8.0" }
cloudevents-sdk = "0.1.0"
```
Now you can start creating events:
```rust
use cloudevents::{EventBuilder, EventBuilderV10};
use cloudevents::EventBuilder;
use url::Url;
let event = EventBuilderV10::new()
let event = EventBuilder::v03()
.id("aaa")
.source(Url::parse("http://localhost").unwrap())
.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)
* [Axum Example](example-projects/axum-example)
* [Reqwest/WASM Example](example-projects/reqwest-wasm-example)
* [Kafka Example](example-projects/rdkafka-example)
* [Warp Example](example-projects/warp-example)
* [NATS Example](example-projects/nats-example)
## Development & Contributing
If you're interested in contributing to sdk-rust, look at [Contributing documentation](CONTRIBUTING.md)
## 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`
on slack).
Each SDK may have its own unique processes, tooling and guidelines, common
governance related material can be found in the
[CloudEvents `community`](https://github.com/cloudevents/spec/tree/master/community)
directory. In particular, in there you will find information concerning
how SDK projects are
[managed](https://github.com/cloudevents/spec/blob/master/community/SDK-GOVERNANCE.md),
[guidelines](https://github.com/cloudevents/spec/blob/master/community/SDK-maintainer-guidelines.md)
for how PR reviews and approval, and our
[Code of Conduct](https://github.com/cloudevents/spec/blob/master/community/GOVERNANCE.md#additional-information)
information.
If there is a security concern with one of the CloudEvents specifications, or
with one of the project's SDKs, please send an email to
[cncf-cloudevents-security@lists.cncf.io](mailto:cncf-cloudevents-security@lists.cncf.io).
[Crates badge]: https://img.shields.io/crates/v/cloudevents-sdk.svg
[crates.io]: https://crates.io/crates/cloudevents-sdk
[Docs badge]: https://docs.rs/cloudevents-sdk/badge.svg
[docs.rs]: https://docs.rs/cloudevents-sdk
[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)
[docs.rs]: https://docs.rs/cloudevents-sdk

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]
name = "actix-web-example"
version = "0.3.0"
version = "0.1.0"
authors = ["Francesco Guardiani <francescoguard@gmail.com>"]
edition = "2018"
[dependencies]
cloudevents-sdk = { path = "../..", features = ["actix"] }
actix-web = "4"
actix-cors = "^0.7"
cloudevents-sdk = { path = "../.." }
cloudevents-sdk-actix-web = { path = "../../cloudevents-sdk-actix-web" }
actix-web = "2"
actix-rt = "1"
actix-cors = "^0.2.0"
lazy_static = "1.4.0"
bytes = "^0.5"
futures = "^0.3"
serde_json = "^1.0"
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,41 +1,46 @@
use actix_web::{get, post, App, HttpServer};
use cloudevents::{Event, EventBuilder, EventBuilderV10};
use actix_web::{get, post, web, App, HttpRequest, HttpResponse, HttpServer};
use cloudevents::EventBuilder;
use url::Url;
use std::str::FromStr;
use serde_json::json;
#[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);
event
Ok(format!("{:?}", event))
}
#[get("/")]
async fn get_event() -> Event {
async fn get_event() -> Result<HttpResponse, actix_web::Error> {
let payload = json!({"hello": "world"});
EventBuilderV10::new()
.id("0001")
.ty("example.test")
.source("http://localhost/")
.data("application/json", payload)
.extension("someint", "10")
.build()
.unwrap()
Ok(cloudevents_sdk_actix_web::event_to_response(
EventBuilder::new()
.id("0001")
.ty("example.test")
.source(Url::from_str("http://localhost/").unwrap())
.data("application/json", payload)
.extension("someint", "10")
.build(),
HttpResponse::Ok()
).await?)
}
#[actix_web::main]
#[actix_rt::main]
async fn main() -> std::io::Result<()> {
std::env::set_var("RUST_LOG", "actix_server=info,actix_web=info");
env_logger::init();
HttpServer::new(|| {
App::new()
.wrap(actix_cors::Cors::permissive())
.wrap(actix_web::middleware::Logger::default())
.wrap(actix_cors::Cors::new().finish())
.service(post_event)
.service(get_event)
})
.bind("127.0.0.1:9000")?
.workers(1)
.run()
.await
.bind("127.0.0.1:9000")?
.workers(1)
.run()
.await
}

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]
name = "reqwest-wasm-example"
version = "0.3.0"
version = "0.1.0"
authors = ["Francesco Guardiani <francescoguard@gmail.com>"]
edition = "2018"
resolver = "2"
# Config mostly pulled from: https://github.com/rustwasm/wasm-bindgen/blob/master/examples/fetch/Cargo.toml
@ -11,10 +10,12 @@ resolver = "2"
crate-type = ["cdylib"]
[dependencies]
reqwest = "^0.12"
uuid = "1"
cloudevents-sdk = { path = "../..", features = ["reqwest"] }
reqwest = "0.10.4"
cloudevents-sdk = { path = "../.." }
cloudevents-sdk-reqwest = { path = "../../cloudevents-sdk-reqwest" }
url = { version = "^2.1" }
web-sys = { version = "0.3.39", features = ["Window", "Location"] }
wasm-bindgen-futures = "0.4.12"
wasm-bindgen = { version = "0.2.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
First, ensure you have [`wasm-pack` installed](https://rustwasm.github.io/wasm-pack/installer/)
Then install the dependencies:
Install the dependencies with:
npm install
And finally run the example:
Then build the example locally with:
npm run serve
You should see a form in your browser at http://localhost:8080. When
the form is submitted, a CloudEvent will be sent to the Target URL,
http://localhost:9000 by default, which is the default URL for the
[actix example](../actix-web-example). Fire it up in another terminal
to verify that the data is successfully sent and received.
and then visiting http://localhost:8080 in a browser should run the example!
Open the javascript console in the browser to see any helpful error
messages.
This example is loosely based off of [this
example](https://github.com/seanmonstar/reqwest/tree/master/examples/wasm_github_fetch).
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).

View File

@ -13,7 +13,7 @@
<div class="form-group">
<label class="col-md-4 control-label" for="event_target">Target</label>
<div class="col-md-4">
<input id="event_target" name="event_target" type="text" 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>
@ -22,7 +22,7 @@
<div class="form-group">
<label class="col-md-4 control-label" for="event_type">Event Type</label>
<div class="col-md-4">
<input id="event_type" name="event_type" type="text" 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>
@ -30,7 +30,7 @@
<div class="form-group">
<label class="col-md-4 control-label" for="event_datacontenttype">Event Data Content Type</label>
<div class="col-md-4">
<input id="event_datacontenttype" name="event_datacontenttype" type="text" 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>
@ -54,4 +54,4 @@
</form>
</body>
</html>
</html>

View File

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

File diff suppressed because it is too large Load Diff

View File

@ -1,21 +1,21 @@
{
"scripts": {
"build": "webpack",
"serve": "webpack serve"
"serve": "webpack-dev-server"
},
"devDependencies": {
"@wasm-tool/wasm-pack-plugin": "^1.4.0",
"css-loader": "^5.2.6",
"html-webpack-plugin": "^5.5.0",
"style-loader": "^2.0.0",
"@wasm-tool/wasm-pack-plugin": "^1.3.1",
"css-loader": "^3.5.3",
"html-webpack-plugin": "^3.2.0",
"style-loader": "^1.2.1",
"text-encoding": "^0.7.0",
"webpack": "^5.95.0",
"webpack-cli": "^4.8.0",
"webpack-dev-server": "^5.0.4"
"webpack": "^4.29.4",
"webpack-cli": "^3.1.1",
"webpack-dev-server": "^3.1.0"
},
"dependencies": {
"bootstrap": "^5.0.2",
"jquery": "^3.6.0",
"@popperjs/core": "^2.9.2"
"bootstrap": "^4.5.0",
"jquery": "^3.5.1",
"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 uuid::Uuid;
#[wasm_bindgen]
pub async fn run(
target: String,
ty: String,
datacontenttype: String,
data: String,
) -> Result<(), JsValue> {
let event = EventBuilderV10::new()
.id(&Uuid::new_v4().hyphenated().to_string())
pub async fn run(target: String, ty: String, datacontenttype: String, data: String) -> Result<(), String> {
let event = cloudevents::EventBuilder::new()
.ty(ty)
.source("http://localhost/")
.data(datacontenttype, data)
.build()
.unwrap();
.build();
println!("Going to send event: {:?}", event);
reqwest::Client::new()
.post(&target)
.event(event)
cloudevents_sdk_reqwest::event_to_request(event, reqwest::Client::new().post(&target))
.map_err(|e| e.to_string())?
.header("Access-Control-Allow-Origin", "*")
.send()
@ -30,4 +17,4 @@ pub async fn run(
.map_err(|e| e.to_string())?;
Ok(())
}
}

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

View File

@ -1,284 +1,32 @@
use super::Event;
use snafu::Snafu;
use super::{EventBuilderV03, EventBuilderV10};
/// Trait to implement a builder for [`Event`]:
/// Builder to create [`super::Event`]:
/// ```
/// use cloudevents::event::{EventBuilderV10, EventBuilder};
/// use cloudevents::EventBuilder;
/// use chrono::Utc;
/// use url::Url;
///
/// let event = EventBuilderV10::new()
/// let event = EventBuilder::v10()
/// .id("my_event.my_application")
/// .source("http://localhost:8080")
/// .ty("example.demo")
/// .source(Url::parse("http://localhost:8080").unwrap())
/// .time(Utc::now())
/// .build()
/// .unwrap();
/// .build();
/// ```
///
/// 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;
pub struct EventBuilder {}
/// Build [`Event`]
fn build(self) -> Result<Event, Error>;
}
/// Represents an error during build process
#[derive(Debug, Snafu, Clone)]
pub enum Error {
#[snafu(display("Missing required attribute {}", attribute_name))]
MissingRequiredAttribute { attribute_name: &'static str },
#[snafu(display(
"Error while setting attribute '{}' with timestamp type: {}",
attribute_name,
source
))]
ParseTimeError {
attribute_name: &'static str,
source: chrono::ParseError,
},
#[snafu(display(
"Error while setting attribute '{}' with uri type: {}",
attribute_name,
source
))]
ParseUrlError {
attribute_name: &'static str,
source: url::ParseError,
},
#[snafu(display(
"Invalid value setting attribute '{}' with uriref type",
attribute_name,
))]
InvalidUriRefError { attribute_name: &'static str },
}
#[cfg(test)]
mod tests {
use crate::test::fixtures;
use crate::Event;
use crate::EventBuilder;
use crate::EventBuilderV03;
use crate::EventBuilderV10;
use claims::*;
use rstest::rstest;
use serde_json::{json, Value};
use serde_yaml;
/// Test conversions
#[test]
fn v10_to_v03() {
let in_event = fixtures::v10::full_json_data();
let out_event = EventBuilderV03::from(in_event).build().unwrap();
assert_eq!(fixtures::v03::full_json_data(), out_event)
impl EventBuilder {
/// Creates a new builder for latest CloudEvents version
pub fn new() -> EventBuilderV10 {
return Self::v10();
}
#[test]
fn v03_to_v10() {
let in_event = fixtures::v03::full_json_data();
let out_event = EventBuilderV10::from(in_event).build().unwrap();
assert_eq!(fixtures::v10::full_json_data(), out_event)
/// Creates a new builder for CloudEvents V1.0
pub fn v10() -> EventBuilderV10 {
return EventBuilderV10::new();
}
/// Test YAML
/// This test checks if the usage of serde_json::Value makes the Deserialize implementation incompatible with
/// other Deserializers
#[test]
fn deserialize_yaml_should_succeed() {
let input = r#"
id: aaa
type: bbb
source: http://localhost
datacontenttype: application/json
data: true
specversion: "1.0"
"#;
let expected = EventBuilderV10::new()
.id("aaa")
.ty("bbb")
.source("http://localhost")
.data("application/json", serde_json::Value::Bool(true))
.build()
.unwrap();
let deserialize_result: Result<Event, serde_yaml::Error> = serde_yaml::from_str(input);
assert_ok!(&deserialize_result);
let deserialized = deserialize_result.unwrap();
assert_eq!(deserialized, expected)
}
/// Test Json
/// This test is a parametrized test that uses data from tests/test_data
#[rstest(
in_event,
out_json,
case::minimal_v03(fixtures::v03::minimal(), fixtures::v03::minimal_json()),
case::full_v03_no_data(fixtures::v03::full_no_data(), fixtures::v03::full_no_data_json()),
case::full_v03_with_json_data(
fixtures::v03::full_json_data(),
fixtures::v03::full_json_data_json()
),
case::full_v03_with_xml_string_data(
fixtures::v03::full_xml_string_data(),
fixtures::v03::full_xml_string_data_json()
),
case::full_v03_with_xml_base64_data(
fixtures::v03::full_xml_binary_data(),
fixtures::v03::full_xml_base64_data_json()
),
case::minimal_v10(fixtures::v10::minimal(), fixtures::v10::minimal_json()),
case::full_v10_no_data(fixtures::v10::full_no_data(), fixtures::v10::full_no_data_json()),
case::full_v10_with_json_data(
fixtures::v10::full_json_data(),
fixtures::v10::full_json_data_json()
),
case::full_v10_with_xml_string_data(
fixtures::v10::full_xml_string_data(),
fixtures::v10::full_xml_string_data_json()
),
case::full_v10_with_xml_base64_data(
fixtures::v10::full_xml_binary_data(),
fixtures::v10::full_xml_base64_data_json()
)
)]
fn serialize_should_succeed(in_event: Event, out_json: Value) {
// Event -> serde_json::Value
let serialize_result = serde_json::to_value(in_event.clone());
assert_ok!(&serialize_result);
let actual_json = serialize_result.unwrap();
assert_eq!(&actual_json, &out_json);
// serde_json::Value -> String
let actual_json_serialized = actual_json.to_string();
assert_eq!(actual_json_serialized, out_json.to_string());
// String -> Event
let deserialize_result: Result<Event, serde_json::Error> =
serde_json::from_str(&actual_json_serialized);
assert_ok!(&deserialize_result);
let deserialize_json = deserialize_result.unwrap();
assert_eq!(deserialize_json, in_event)
}
/// This test is a parametrized test that uses data from tests/test_data
#[rstest(
in_json,
out_event,
case::minimal_v03(fixtures::v03::minimal_json(), fixtures::v03::minimal()),
case::full_v03_no_data(fixtures::v03::full_no_data_json(), fixtures::v03::full_no_data()),
case::full_v03_with_json_data(
fixtures::v03::full_json_data_json(),
fixtures::v03::full_json_data()
),
case::full_v03_with_json_base64_data(
fixtures::v03::full_json_base64_data_json(),
fixtures::v03::full_json_data()
),
case::full_v03_with_xml_string_data(
fixtures::v03::full_xml_string_data_json(),
fixtures::v03::full_xml_string_data()
),
case::full_v03_with_xml_base64_data(
fixtures::v03::full_xml_base64_data_json(),
fixtures::v03::full_xml_binary_data()
),
case::minimal_v10(fixtures::v10::minimal_json(), fixtures::v10::minimal()),
case::full_v10_no_data(fixtures::v10::full_no_data_json(), fixtures::v10::full_no_data()),
case::full_v10_with_json_data(
fixtures::v10::full_json_data_json(),
fixtures::v10::full_json_data()
),
case::full_v10_with_json_base64_data(
fixtures::v10::full_json_base64_data_json(),
fixtures::v10::full_json_data()
),
case::full_v10_with_non_json_base64_data(
fixtures::v10::full_non_json_base64_data(),
fixtures::v10::full_non_json_data()
),
case::full_v10_with_xml_string_data(
fixtures::v10::full_xml_string_data_json(),
fixtures::v10::full_xml_string_data()
),
case::full_v10_with_xml_base64_data(
fixtures::v10::full_xml_base64_data_json(),
fixtures::v10::full_xml_binary_data()
)
)]
fn deserialize_json_should_succeed(in_json: Value, out_event: Event) {
let deserialize_result: Result<Event, serde_json::Error> = serde_json::from_value(in_json);
assert_ok!(&deserialize_result);
let deserialize_json = deserialize_result.unwrap();
assert_eq!(deserialize_json, out_event)
}
#[test]
fn deserialize_with_null_attribute() {
let in_json = json!({
"specversion" : "1.0",
"type" : "com.example.someevent",
"source" : "/mycontext",
"id" : "A234-1234-1234",
"time" : null,
"comexampleextension1" : "value",
"comexampleothervalue" : 5,
"datacontenttype" : "text/xml",
"data" : "<much wow=\"xml\"/>"
});
let out_event = EventBuilderV10::new()
.ty("com.example.someevent")
.source("/mycontext")
.id("A234-1234-1234")
.data("text/xml", "<much wow=\"xml\"/>")
.extension("comexampleextension1", "value")
.extension("comexampleothervalue", 5)
.build()
.unwrap();
let deserialize_result: Result<Event, serde_json::Error> = serde_json::from_value(in_json);
assert_ok!(&deserialize_result);
let deserialize_json = deserialize_result.unwrap();
assert_eq!(deserialize_json, out_event)
}
#[test]
fn deserialize_with_null_ext() {
let in_json = json!({
"specversion" : "1.0",
"type" : "com.example.someevent",
"source" : "/mycontext",
"id" : "A234-1234-1234",
"time" : "2018-04-05T17:31:00Z",
"comexampleextension1" : "value",
"comexampleothervalue" : 5,
"unsetextension": null,
"datacontenttype" : "text/xml",
"data" : "<much wow=\"xml\"/>"
});
let out_event = EventBuilderV10::new()
.ty("com.example.someevent")
.source("/mycontext")
.id("A234-1234-1234")
.time("2018-04-05T17:31:00Z")
.data("text/xml", "<much wow=\"xml\"/>")
.extension("comexampleextension1", "value")
.extension("comexampleothervalue", 5)
.build()
.unwrap();
let deserialize_result: Result<Event, serde_json::Error> = serde_json::from_value(in_json);
assert_ok!(&deserialize_result);
let deserialize_json = deserialize_result.unwrap();
assert_eq!(deserialize_json, out_event)
/// Creates a new builder for CloudEvents V0.3
pub fn v03() -> EventBuilderV03 {
return EventBuilderV03::new();
}
}

View File

@ -1,11 +1,7 @@
use serde_json::Value;
use std::convert::TryFrom;
use std::fmt;
use std::fmt::Formatter;
use std::str;
use std::convert::{Into, TryFrom};
/// 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 {
/// Event has a binary payload
Binary(Vec<u8>),
@ -15,31 +11,59 @@ pub enum Data {
Json(serde_json::Value),
}
impl Data {
/// Create a [`Data`] from a [`Into<Vec<u8>>`].
///
/// # Example
///
/// ```
/// use cloudevents::event::Data;
///
/// let value = Data::from_base64(b"dmFsdWU=").unwrap();
/// assert_eq!(value, Data::Binary(base64::decode("dmFsdWU=").unwrap()));
/// ```
///
/// [`AsRef<[u8]>`]: https://doc.rust-lang.org/std/convert/trait.AsRef.html
/// [`Data`]: enum.Data.html
pub fn from_base64<I>(i: I) -> Result<Self, base64::DecodeError>
where
I: AsRef<[u8]>,
{
Ok(base64::decode(&i)?.into())
}
pub fn from_binary<I>(content_type: Option<&str>, i: I) -> Result<Self, serde_json::Error>
where
I: AsRef<[u8]>,
{
let is_json = is_json_content_type(content_type.unwrap_or("application/json"));
if is_json {
serde_json::from_slice::<serde_json::Value>(i.as_ref()).map(Data::Json)
} else {
Ok(Data::Binary(i.as_ref().to_vec()))
}
}
}
pub(crate) fn is_json_content_type(ct: &str) -> bool {
ct.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 {
fn from(value: Value) -> Self {
Data::Json(value)
impl Into<Data> for serde_json::Value {
fn into(self) -> Data {
Data::Json(self)
}
}
impl From<Vec<u8>> for Data {
fn from(value: Vec<u8>) -> Self {
Data::Binary(value)
impl Into<Data> for Vec<u8> {
fn into(self) -> Data {
Data::Binary(self)
}
}
impl From<String> for Data {
fn from(value: String) -> Self {
Data::String(value)
}
}
impl From<&str> for Data {
fn from(value: &str) -> Self {
Data::String(String::from(value))
impl Into<Data> for String {
fn into(self) -> Data {
Data::String(self)
}
}
@ -60,7 +84,7 @@ impl TryFrom<Data> for Vec<u8> {
fn try_from(value: Data) -> Result<Self, Self::Error> {
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::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::fmt;
#[derive(PartialEq, Eq, Debug, Clone, Serialize, Deserialize)]
#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)]
#[serde(untagged)]
/// Represents all the possible [CloudEvents extension](https://github.com/cloudevents/spec/blob/master/spec.md#extension-context-attributes) values
pub enum ExtensionValue {
/// Represents a [`String`] value.
/// Represents a [`String`](std::string::String) value.
String(String),
/// Represents a [`bool`] value.
/// Represents a [`bool`](bool) value.
Boolean(bool),
/// Represents an integer [`i64`] value.
/// Represents an integer [`i64`](i64) value.
Integer(i64),
}
@ -60,13 +59,3 @@ impl ExtensionValue {
ExtensionValue::from(s.into())
}
}
impl fmt::Display for ExtensionValue {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
ExtensionValue::String(s) => f.write_str(s),
ExtensionValue::Boolean(b) => f.serialize_bool(*b),
ExtensionValue::Integer(i) => f.serialize_i64(*i),
}
}
}

View File

@ -3,99 +3,119 @@ use super::{
EventFormatSerializerV03, EventFormatSerializerV10,
};
use crate::event::{AttributesReader, ExtensionValue};
use base64::prelude::*;
use serde::de::{Error, IntoDeserializer};
use serde::de::{Error, IntoDeserializer, Unexpected};
use serde::{Deserialize, Deserializer, Serialize, Serializer};
use serde_json::{Map, Value};
use std::collections::HashMap;
use serde_value::Value;
use std::collections::{BTreeMap, HashMap};
macro_rules! parse_optional_field {
($map:ident, $name:literal, $value_variant:ident, $error:ty) => {
$map.remove($name)
.map(|val| match val {
Value::$value_variant(v) => Ok(v),
other => Err(<$error>::invalid_type(
crate::event::format::value_to_unexpected(&other),
&stringify!($value_variant),
)),
})
.transpose()
};
($map:ident, $name:literal, $value_variant:ident, $error:ty, $mapper:expr) => {
$map.remove($name)
.map(|val| match val {
Value::$value_variant(v) => $mapper(&v).map_err(|e| {
<$error>::invalid_value(
crate::event::format::value_to_unexpected(&Value::$value_variant(v)),
&e.to_string().as_str(),
)
}),
other => Err(<$error>::invalid_type(
crate::event::format::value_to_unexpected(&other),
&stringify!($value_variant),
)),
})
.transpose()
};
}
macro_rules! parse_field {
($value:expr, $target_type:ty, $error:ty) => {
<$target_type>::deserialize($value.into_deserializer()).map_err(<$error>::custom)
};
($value:expr, $target_type:ty, $error:ty, $mapper:expr) => {
<$target_type>::deserialize($value.into_deserializer())
.map_err(<$error>::custom)
.and_then(|v| $mapper(v).map_err(<$error>::custom))
};
}
macro_rules! extract_optional_field {
($map:ident, $name:literal, $target_type:ty, $error:ty) => {
$map.remove($name)
.filter(|v| !v.is_null())
.map(|v| parse_field!(v, $target_type, $error))
.transpose()
};
($map:ident, $name:literal, $target_type:ty, $error:ty, $mapper:expr) => {
$map.remove($name)
.filter(|v| !v.is_null())
.map(|v| parse_field!(v, $target_type, $error, $mapper))
.transpose()
};
}
macro_rules! extract_field {
($map:ident, $name:literal, $target_type:ty, $error:ty) => {
extract_optional_field!($map, $name, $target_type, $error)?
($map:ident, $name:literal, $value_variant:ident, $error:ty) => {
parse_optional_field!($map, $name, $value_variant, $error)?
.ok_or_else(|| <$error>::missing_field($name))
};
($map:ident, $name:literal, $target_type:ty, $error:ty, $mapper:expr) => {
extract_optional_field!($map, $name, $target_type, $error, $mapper)?
($map:ident, $name:literal, $value_variant:ident, $error:ty, $mapper:expr) => {
parse_optional_field!($map, $name, $value_variant, $error, $mapper)?
.ok_or_else(|| <$error>::missing_field($name))
};
}
pub fn parse_data_json<E: serde::de::Error>(v: Value) -> Result<Value, E> {
Value::deserialize(v.into_deserializer()).map_err(E::custom)
macro_rules! parse_data_json {
($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> {
parse_field!(v, String, E)
macro_rules! parse_data_string {
($in:ident, $error:ty) => {
match $in {
Value::String(s) => Ok(s),
other => Err(E::invalid_type(
crate::event::format::value_to_unexpected(&other),
&"a string",
)),
}
};
}
pub fn parse_data_base64<E: serde::de::Error>(v: Value) -> Result<Vec<u8>, E> {
parse_field!(v, String, E).and_then(|s| {
BASE64_STANDARD
.decode(s)
.map_err(|e| E::custom(format_args!("decode error `{}`", e)))
})
macro_rules! parse_json_data_base64 {
($in:ident, $error:ty) => {{
let data = parse_data_base64!($in, $error)?;
serde_json::from_slice(&data).map_err(|e| <$error>::custom(e))
}};
}
pub fn parse_data_base64_json<E: serde::de::Error>(v: Value) -> Result<Value, E> {
let data = parse_data_base64(v)?;
serde_json::from_slice(&data).map_err(E::custom)
macro_rules! parse_data_base64 {
($in:ident, $error:ty) => {
match $in {
Value::String(s) => base64::decode(&s).map_err(|e| {
<$error>::invalid_value(serde::de::Unexpected::Str(&s), &e.to_string().as_str())
}),
other => Err(E::invalid_type(
crate::event::format::value_to_unexpected(&other),
&"a string",
)),
}
};
}
pub(crate) trait EventFormatDeserializer {
fn deserialize_attributes<E: serde::de::Error>(
map: &mut Map<String, Value>,
map: &mut BTreeMap<String, Value>,
) -> Result<Attributes, E>;
fn deserialize_data<E: serde::de::Error>(
content_type: &str,
map: &mut Map<String, Value>,
map: &mut BTreeMap<String, Value>,
) -> 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 data = Self::deserialize_data(
attributes.datacontenttype().unwrap_or("application/json"),
attributes
.get_datacontenttype()
.unwrap_or("application/json"),
&mut map,
)?;
let extensions = map
.into_iter()
.filter(|v| !v.1.is_null())
.map(|(k, v)| {
Ok((
k,
ExtensionValue::deserialize(v.into_deserializer()).map_err(E::custom)?,
))
})
.collect::<Result<HashMap<String, ExtensionValue>, E>>()?;
.map(|(k, v)| Ok((k, ExtensionValue::deserialize(v.into_deserializer())?)))
.collect::<Result<HashMap<String, ExtensionValue>, serde_value::DeserializerError>>()
.map_err(|e| E::custom(e))?;
Ok(Event {
attributes,
@ -119,12 +139,20 @@ impl<'de> Deserialize<'de> for Event {
where
D: Deserializer<'de>,
{
let root_value = Value::deserialize(deserializer)?;
let mut map: Map<String, Value> =
Map::deserialize(root_value.into_deserializer()).map_err(D::Error::custom)?;
let map = match Value::deserialize(deserializer)? {
Value::Map(m) => Ok(m),
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),
"1.0" => EventFormatDeserializerV10::deserialize_event(map),
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,
StructuredSerializer,
};
use crate::{EventBuilder, EventBuilderV03, EventBuilderV10};
impl StructuredDeserializer for Event {
fn deserialize_structured<R, V: StructuredSerializer<R>>(self, visitor: V) -> Result<R> {
@ -17,7 +16,7 @@ impl StructuredDeserializer for Event {
impl BinaryDeserializer for Event {
fn deserialize_binary<R: Sized, V: BinarySerializer<R>>(self, mut visitor: V) -> Result<R> {
visitor = visitor.set_spec_version(self.specversion())?;
visitor = visitor.set_spec_version(self.get_specversion())?;
visitor = self.attributes.deserialize_attributes(visitor)?;
for (k, v) in self.extensions.into_iter() {
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>;
}
pub(crate) trait AttributesSerializer {
fn serialize_attribute(&mut self, name: &str, value: MessageAttributeValue) -> Result<()>;
}
impl AttributesDeserializer for Attributes {
fn deserialize_attributes<R: Sized, V: BinarySerializer<R>>(self, visitor: V) -> Result<V> {
match self {
@ -47,202 +50,50 @@ impl AttributesDeserializer for Attributes {
}
}
pub(crate) trait AttributesSerializer {
fn serialize_attribute(&mut self, name: &str, value: MessageAttributeValue) -> Result<()>;
}
#[derive(Debug)]
pub(crate) struct EventStructuredSerializer {}
impl StructuredSerializer<Event> for EventStructuredSerializer {
fn set_structured_event(self, bytes: Vec<u8>) -> Result<Event> {
Ok(serde_json::from_slice(&bytes)?)
impl AttributesSerializer for Attributes {
fn serialize_attribute(&mut self, name: &str, value: MessageAttributeValue) -> Result<()> {
match self {
Attributes::V03(v03) => v03.serialize_attribute(name, value),
Attributes::V10(v10) => v10.serialize_attribute(name, value),
}
}
}
#[derive(Debug)]
pub(crate) enum EventBinarySerializer {
V10(EventBuilderV10),
V03(EventBuilderV03),
}
impl EventBinarySerializer {
pub(crate) fn new() -> Self {
EventBinarySerializer::V10(EventBuilderV10::new())
impl StructuredSerializer<Event> for Event {
fn set_structured_event(mut self, bytes: Vec<u8>) -> Result<Event> {
let new_event: Event = serde_json::from_slice(&bytes)?;
self.attributes = new_event.attributes;
self.data = new_event.data;
self.extensions = new_event.extensions;
Ok(self)
}
}
impl BinarySerializer<Event> for EventBinarySerializer {
fn set_spec_version(self, spec_version: SpecVersion) -> Result<Self> {
Ok(match spec_version {
SpecVersion::V03 => EventBinarySerializer::V03(EventBuilderV03::new()),
SpecVersion::V10 => EventBinarySerializer::V10(EventBuilderV10::new()),
})
}
fn set_attribute(mut self, name: &str, value: MessageAttributeValue) -> Result<Self> {
match &mut self {
EventBinarySerializer::V03(eb) => eb.serialize_attribute(name, value)?,
EventBinarySerializer::V10(eb) => eb.serialize_attribute(name, value)?,
impl BinarySerializer<Event> for Event {
fn set_spec_version(mut self, spec_version: SpecVersion) -> Result<Self> {
match spec_version {
SpecVersion::V03 => self.attributes = self.attributes.clone().into_v03(),
SpecVersion::V10 => self.attributes = self.attributes.clone().into_v10(),
}
Ok(self)
}
fn set_extension(self, name: &str, value: MessageAttributeValue) -> Result<Self> {
Ok(match self {
EventBinarySerializer::V03(eb) => EventBinarySerializer::V03(eb.extension(name, value)),
EventBinarySerializer::V10(eb) => EventBinarySerializer::V10(eb.extension(name, value)),
})
fn set_attribute(mut self, name: &str, value: MessageAttributeValue) -> Result<Self> {
self.attributes.serialize_attribute(name, value)?;
Ok(self)
}
fn end_with_data(self, bytes: Vec<u8>) -> Result<Event> {
Ok(match self {
EventBinarySerializer::V03(eb) => {
eb.data_without_content_type(Data::Binary(bytes)).build()
}
EventBinarySerializer::V10(eb) => {
eb.data_without_content_type(Data::Binary(bytes)).build()
}
}?)
fn set_extension(mut self, name: &str, value: MessageAttributeValue) -> Result<Self> {
self.extensions.insert(name.to_string(), value.into());
Ok(self)
}
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> {
Ok(match self {
EventBinarySerializer::V03(eb) => eb.build(),
EventBinarySerializer::V10(eb) => eb.build(),
}?)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::message::Error;
use crate::test::fixtures;
use std::convert::TryInto;
#[test]
fn binary_deserializer_unrecognized_attribute_v03() {
assert_eq!(
Error::UnknownAttribute {
name: "dataschema".to_string()
}
.to_string(),
EventBinarySerializer::new()
.set_spec_version(SpecVersion::V03)
.unwrap()
.set_attribute("dataschema", MessageAttributeValue::Boolean(true))
.expect_err("Should return an error")
.to_string()
)
}
#[test]
fn binary_deserializer_missing_id() {
assert_eq!(
Error::EventBuilderError {
source: crate::event::EventBuilderError::MissingRequiredAttribute {
attribute_name: "id"
},
}
.to_string(),
EventBinarySerializer::new()
.set_spec_version(SpecVersion::V10)
.unwrap()
.end()
.unwrap_err()
.to_string()
)
}
#[test]
fn binary_deserializer_unrecognized_attribute_v10() {
assert_eq!(
Error::UnknownAttribute {
name: "schemaurl".to_string()
}
.to_string(),
EventBinarySerializer::new()
.set_spec_version(SpecVersion::V10)
.unwrap()
.set_attribute("schemaurl", MessageAttributeValue::Boolean(true))
.expect_err("Should return an error")
.to_string()
)
}
#[test]
fn message_v03_roundtrip_structured() -> Result<()> {
assert_eq!(
fixtures::v03::full_json_data(),
StructuredDeserializer::into_event(fixtures::v03::full_json_data())?
);
Ok(())
}
#[test]
fn message_v03_roundtrip_binary() -> Result<()> {
//TODO this code smells because we're missing a proper way in the public APIs
// to destructure an event and rebuild it
let wanna_be_expected = fixtures::v03::full_json_data();
let data: serde_json::Value = wanna_be_expected.data().unwrap().clone().try_into()?;
let bytes = serde_json::to_vec(&data)?;
let expected = EventBuilderV03::from(wanna_be_expected.clone())
.data(wanna_be_expected.datacontenttype().unwrap(), bytes)
.build()
.unwrap();
assert_eq!(
expected,
BinaryDeserializer::into_event(fixtures::v03::full_json_data())?
);
Ok(())
}
#[test]
fn message_v03_msgpack() {
let buff = rmp_serde::to_vec(&fixtures::v03::full_json_data()).unwrap();
let event = rmp_serde::from_slice::<Event>(buff.as_slice()).unwrap();
assert_eq!(event, fixtures::v03::full_json_data(),);
}
#[test]
fn message_v10_roundtrip_structured() -> Result<()> {
assert_eq!(
fixtures::v10::full_json_data(),
StructuredDeserializer::into_event(fixtures::v10::full_json_data())?
);
Ok(())
}
#[test]
fn message_v10_roundtrip_binary() -> Result<()> {
//TODO this code smells because we're missing a proper way in the public APIs
// to destructure an event and rebuild it
let wanna_be_expected = fixtures::v10::full_json_data();
let data: serde_json::Value = wanna_be_expected
.data()
.cloned()
.unwrap()
.try_into()
.unwrap();
let bytes = serde_json::to_vec(&data)?;
let expected = EventBuilderV10::from(wanna_be_expected.clone())
.data(wanna_be_expected.datacontenttype().unwrap(), bytes)
.build()
.unwrap();
assert_eq!(
expected,
BinaryDeserializer::into_event(fixtures::v10::full_json_data())?
);
Ok(())
}
#[test]
fn message_v10_msgpack() {
let buff = rmp_serde::to_vec(&fixtures::v10::full_json_data()).unwrap();
let event = rmp_serde::from_slice::<Event>(buff.as_slice()).unwrap();
assert_eq!(event, fixtures::v10::full_json_data(),);
Ok(self)
}
}

View File

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

View File

@ -1,37 +1,36 @@
use super::{v03, v10};
use lazy_static::lazy_static;
use serde::export::Formatter;
use std::collections::HashMap;
use std::convert::TryFrom;
use std::fmt;
use std::fmt::Formatter;
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)]
pub enum SpecVersion {
/// CloudEvents v0.3
V03,
/// CloudEvents v1.0
V10,
}
impl SpecVersion {
/// Returns the string representation of [`SpecVersion`].
#[inline]
pub fn as_str(&self) -> &str {
match self {
SpecVersion::V03 => "0.3",
SpecVersion::V10 => "1.0",
}
}
/// Get all attribute names for this [`SpecVersion`].
#[inline]
pub fn attribute_names(&self) -> &'static [&'static str] {
match self {
SpecVersion::V03 => &v03::ATTRIBUTE_NAMES,
SpecVersion::V10 => &v10::ATTRIBUTE_NAMES,
}
}
}
impl fmt::Display for SpecVersion {
@ -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)]
pub struct UnknownSpecVersion {
pub struct InvalidSpecVersion {
spec_version_value: String,
}
impl fmt::Display for UnknownSpecVersion {
impl fmt::Display for InvalidSpecVersion {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
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 {
type Error = UnknownSpecVersion;
type Error = InvalidSpecVersion;
fn try_from(value: &str) -> Result<Self, UnknownSpecVersion> {
fn try_from(value: &str) -> Result<Self, InvalidSpecVersion> {
match value {
"0.3" => Ok(SpecVersion::V03),
"1.0" => Ok(SpecVersion::V10),
_ => Err(UnknownSpecVersion {
_ => Err(InvalidSpecVersion {
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::{AttributesReader, AttributesV10, AttributesWriter, SpecVersion, UriReference};
use crate::message::{BinarySerializer, MessageAttributeValue};
use crate::event::attributes::{
default_hostname, AttributeValue, AttributesConverter, DataAttributesWriter,
};
use crate::event::AttributesV10;
use crate::event::{AttributesReader, AttributesWriter, SpecVersion};
use chrono::{DateTime, Utc};
use url::Url;
use uuid::Uuid;
pub(crate) const ATTRIBUTE_NAMES: [&str; 8] = [
pub(crate) const ATTRIBUTE_NAMES: [&'static str; 8] = [
"specversion",
"id",
"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)
#[derive(PartialEq, Eq, Debug, Clone)]
#[derive(PartialEq, Debug, Clone)]
pub struct Attributes {
pub(crate) id: String,
pub(crate) ty: String,
pub(crate) source: UriReference,
pub(crate) source: Url,
pub(crate) datacontenttype: Option<String>,
pub(crate) schemaurl: Option<Url>,
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(crate) attributes: &'a Attributes,
pub(crate) index: usize,
attributes: &'a Attributes,
index: usize,
}
impl<'a> Iterator for AttributesIntoIterator<'a> {
type Item = (&'a str, AttributeValue<'a>);
fn next(&mut self) -> Option<Self::Item> {
let result = match self.index {
0 => Some(("specversion", AttributeValue::SpecVersion(SpecVersion::V03))),
1 => Some(("id", AttributeValue::String(&self.attributes.id))),
2 => Some(("type", AttributeValue::String(&self.attributes.ty))),
3 => Some(("source", AttributeValue::URIRef(&self.attributes.source))),
4 => self
0 => Some(("id", AttributeValue::String(&self.attributes.id))),
1 => Some(("type", AttributeValue::String(&self.attributes.ty))),
2 => Some(("source", AttributeValue::URIRef(&self.attributes.source))),
3 => self
.attributes
.datacontenttype
.as_ref()
.map(|v| ("datacontenttype", AttributeValue::String(v))),
5 => self
4 => self
.attributes
.schemaurl
.as_ref()
.map(|v| ("schemaurl", AttributeValue::URI(v))),
6 => self
.map(|v| ("schemaurl", AttributeValue::URIRef(v))),
5 => self
.attributes
.subject
.as_ref()
.map(|v| ("subject", AttributeValue::String(v))),
7 => self
6 => self
.attributes
.time
.as_ref()
@ -85,69 +85,68 @@ impl<'a> Iterator for AttributesIntoIterator<'a> {
}
impl AttributesReader for Attributes {
fn id(&self) -> &str {
fn get_id(&self) -> &str {
&self.id
}
fn source(&self) -> &UriReference {
fn get_source(&self) -> &Url {
&self.source
}
fn specversion(&self) -> SpecVersion {
fn get_specversion(&self) -> SpecVersion {
SpecVersion::V03
}
fn ty(&self) -> &str {
fn get_type(&self) -> &str {
&self.ty
}
fn datacontenttype(&self) -> Option<&str> {
fn get_datacontenttype(&self) -> Option<&str> {
self.datacontenttype.as_deref()
}
fn dataschema(&self) -> Option<&Url> {
fn get_dataschema(&self) -> Option<&Url> {
self.schemaurl.as_ref()
}
fn subject(&self) -> Option<&str> {
fn get_subject(&self) -> Option<&str> {
self.subject.as_deref()
}
fn time(&self) -> Option<&DateTime<Utc>> {
fn get_time(&self) -> Option<&DateTime<Utc>> {
self.time.as_ref()
}
}
impl AttributesWriter for Attributes {
fn set_id(&mut self, id: impl Into<String>) -> String {
std::mem::replace(&mut self.id, id.into())
fn set_id(&mut self, id: impl Into<String>) {
self.id = id.into()
}
fn set_source(&mut self, source: impl Into<UriReference>) -> UriReference {
std::mem::replace(&mut self.source, source.into())
fn set_source(&mut self, source: impl Into<Url>) {
self.source = source.into()
}
fn set_type(&mut self, ty: impl Into<String>) -> String {
std::mem::replace(&mut self.ty, ty.into())
fn set_type(&mut self, ty: impl Into<String>) {
self.ty = ty.into()
}
fn set_subject(&mut self, subject: Option<impl Into<String>>) -> Option<String> {
std::mem::replace(&mut self.subject, subject.map(Into::into))
fn set_subject(&mut self, subject: Option<impl Into<String>>) {
self.subject = subject.map(Into::into)
}
fn set_time(&mut self, time: Option<impl Into<DateTime<Utc>>>) -> Option<DateTime<Utc>> {
std::mem::replace(&mut self.time, time.map(Into::into))
fn set_time(&mut self, time: Option<impl Into<DateTime<Utc>>>) {
self.time = time.map(Into::into)
}
}
impl DataAttributesWriter for Attributes {
fn set_datacontenttype(&mut self, datacontenttype: Option<impl Into<String>>) {
self.datacontenttype = datacontenttype.map(Into::into)
}
fn set_datacontenttype(
&mut self,
datacontenttype: Option<impl Into<String>>,
) -> Option<String> {
std::mem::replace(&mut self.datacontenttype, datacontenttype.map(Into::into))
}
fn set_dataschema(&mut self, dataschema: Option<impl Into<Url>>) -> Option<Url> {
std::mem::replace(&mut self.schemaurl, dataschema.map(Into::into))
fn set_dataschema(&mut self, dataschema: Option<impl Into<Url>>) {
self.schemaurl = dataschema.map(Into::into)
}
}
@ -156,11 +155,11 @@ impl Default for Attributes {
Attributes {
id: Uuid::new_v4().to_string(),
ty: "type".to_string(),
source: default_hostname().to_string(),
source: default_hostname(),
datacontenttype: None,
schemaurl: 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)]
mod tests {
use super::*;
use crate::test::fixtures;
use chrono::DateTime;
#[test]
fn iter_v03_test() {
let in_event = fixtures::v03::full_json_data();
let mut iter_v03 = in_event.iter_attributes();
assert_eq!(
("specversion", AttributeValue::SpecVersion(SpecVersion::V03)),
iter_v03.next().unwrap()
);
}
use chrono::NaiveDateTime;
#[test]
fn iterator_test_v03() {
let a = Attributes {
id: String::from("1"),
ty: String::from("someType"),
source: "https://example.net".into(),
source: Url::parse("https://example.net").unwrap(),
datacontenttype: None,
schemaurl: 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 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!(
("type", AttributeValue::String("someType")),
@ -260,7 +212,7 @@ mod tests {
assert_eq!(
(
"source",
AttributeValue::URIRef(&"https://example.net".to_string())
AttributeValue::URIRef(&Url::parse("https://example.net").unwrap())
),
b.next().unwrap()
);

View File

@ -1,68 +1,58 @@
use super::Attributes as AttributesV03;
use crate::event::{
Attributes, Data, Event, EventBuilderError, ExtensionValue, TryIntoTime, TryIntoUrl,
UriReference,
};
use crate::message::MessageAttributeValue;
use crate::event::{Attributes, AttributesWriter, Data, Event, ExtensionValue};
use chrono::{DateTime, Utc};
use std::collections::HashMap;
use std::convert::TryInto;
use url::Url;
/// Builder to create a CloudEvent V0.3
#[derive(Clone, Debug)]
pub struct EventBuilder {
id: Option<String>,
ty: Option<String>,
source: Option<UriReference>,
datacontenttype: Option<String>,
schemaurl: Option<Url>,
subject: Option<String>,
time: Option<DateTime<Utc>>,
data: Option<Data>,
extensions: HashMap<String, ExtensionValue>,
error: Option<EventBuilderError>,
event: Event,
}
impl EventBuilder {
pub fn id(mut self, id: impl Into<String>) -> Self {
self.id = Some(id.into());
self
pub fn from(event: Event) -> Self {
EventBuilder {
event: Event {
attributes: event.attributes.into_v03(),
data: event.data,
extensions: event.extensions,
},
}
}
pub fn source(mut self, source: impl Into<String>) -> Self {
let source = source.into();
if source.is_empty() {
self.error = Some(EventBuilderError::InvalidUriRefError {
attribute_name: "source",
});
} else {
self.source = Some(source);
pub fn new() -> Self {
EventBuilder {
event: Event {
attributes: Attributes::V03(AttributesV03::default()),
data: None,
extensions: HashMap::new(),
},
}
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 {
self.ty = Some(ty.into());
self
self.event.set_type(ty);
return self;
}
pub fn subject(mut self, subject: impl Into<String>) -> Self {
self.subject = Some(subject.into());
self
self.event.set_subject(Some(subject));
return self;
}
pub fn time(mut self, time: impl TryIntoTime) -> Self {
match time.into_time() {
Ok(u) => self.time = Some(u),
Err(e) => {
self.error = Some(EventBuilderError::ParseTimeError {
attribute_name: "time",
source: e,
})
}
};
self
pub fn time(mut self, time: impl Into<DateTime<Utc>>) -> Self {
self.event.set_time(Some(time));
return self;
}
pub fn extension(
@ -70,158 +60,40 @@ impl EventBuilder {
extension_name: &str,
extension_value: impl Into<ExtensionValue>,
) -> Self {
self.extensions
.insert(extension_name.to_owned(), extension_value.into());
self
}
pub(crate) fn data_without_content_type(mut self, data: impl Into<Data>) -> Self {
self.data = Some(data.into());
self
self.event.set_extension(extension_name, extension_value);
return self;
}
pub fn data(mut self, datacontenttype: impl Into<String>, data: impl Into<Data>) -> Self {
self.datacontenttype = Some(datacontenttype.into());
self.data = Some(data.into());
self
self.event.write_data(datacontenttype, data);
return self;
}
pub fn data_with_schema(
mut self,
datacontenttype: impl Into<String>,
schemaurl: impl TryIntoUrl,
schemaurl: impl Into<Url>,
data: impl Into<Data>,
) -> Self {
self.datacontenttype = Some(datacontenttype.into());
match schemaurl.into_url() {
Ok(u) => self.schemaurl = Some(u),
Err(e) => {
self.error = Some(EventBuilderError::ParseUrlError {
attribute_name: "schemaurl",
source: e,
})
}
};
self.data = Some(data.into());
self
}
}
impl From<Event> for EventBuilder {
fn from(event: Event) -> Self {
let attributes = match event.attributes.into_v03() {
Attributes::V03(attr) => attr,
// This branch is unreachable because into_v03() returns
// always a Attributes::V03
_ => unreachable!(),
};
EventBuilder {
id: Some(attributes.id),
ty: Some(attributes.ty),
source: Some(attributes.source),
datacontenttype: attributes.datacontenttype,
schemaurl: attributes.schemaurl,
subject: attributes.subject,
time: attributes.time,
data: event.data,
extensions: event.extensions,
error: None,
}
}
}
impl Default for EventBuilder {
fn default() -> Self {
Self::from(Event::default())
}
}
impl crate::event::builder::EventBuilder for EventBuilder {
fn new() -> Self {
EventBuilder {
id: None,
ty: None,
source: None,
datacontenttype: None,
schemaurl: None,
subject: None,
time: None,
data: None,
extensions: Default::default(),
error: None,
}
self.event
.write_data_with_schema(datacontenttype, schemaurl, data);
return self;
}
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(())
pub fn build(self) -> Event {
self.event
}
}
#[cfg(test)]
mod tests {
use crate::assert_match_pattern;
use chrono::{DateTime, Utc};
use crate::event::{
AttributesReader, EventBuilder, EventBuilderError, ExtensionValue, SpecVersion,
};
use crate::EventBuilderV03;
use std::convert::TryInto;
use url::Url;
use super::*;
use crate::event::{AttributesReader, SpecVersion};
#[test]
fn build_event() {
let id = "aaa";
let source = "http://localhost:8080";
let source = Url::parse("http://localhost:8080").unwrap();
let ty = "bbb";
let subject = "francesco";
let time: DateTime<Utc> = Utc::now();
@ -233,71 +105,30 @@ mod tests {
"hello": "world"
});
let mut event = EventBuilderV03::new()
let event = EventBuilder::new()
.id(id)
.source(source.to_string())
.source(source.clone())
.ty(ty)
.subject(subject)
.time(time)
.extension(extension_name, extension_value)
.data_with_schema(content_type, schema.clone(), data.clone())
.build()
.unwrap();
.build();
assert_eq!(SpecVersion::V03, event.specversion());
assert_eq!(id, event.id());
assert_eq!(source, event.source().clone());
assert_eq!(ty, event.ty());
assert_eq!(subject, event.subject().unwrap());
assert_eq!(time, event.time().unwrap().clone());
assert_eq!(SpecVersion::V03, event.get_specversion());
assert_eq!(id, event.get_id());
assert_eq!(source, event.get_source().clone());
assert_eq!(ty, event.get_type());
assert_eq!(subject, event.get_subject().unwrap());
assert_eq!(time, event.get_time().unwrap().clone());
assert_eq!(
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!(schema, event.dataschema().unwrap().clone());
assert_eq!(content_type, event.get_datacontenttype().unwrap());
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);
}
#[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 crate::event::data::is_json_content_type;
use crate::event::format::{
parse_data_base64, parse_data_base64_json, parse_data_json, parse_data_string,
};
use crate::event::{Data, ExtensionValue};
use base64::prelude::*;
use chrono::{DateTime, Utc};
use serde::de::IntoDeserializer;
use serde::ser::SerializeMap;
use serde::{Deserialize, Serializer};
use serde_json::{Map, Value};
use std::collections::HashMap;
use serde_value::Value;
use std::collections::{BTreeMap, HashMap};
use url::Url;
pub(crate) struct EventFormatDeserializer {}
impl crate::event::format::EventFormatDeserializer for EventFormatDeserializer {
fn deserialize_attributes<E: serde::de::Error>(
map: &mut Map<String, Value>,
map: &mut BTreeMap<String, Value>,
) -> Result<crate::event::Attributes, E> {
Ok(crate::event::Attributes::V03(Attributes {
id: extract_field!(map, "id", String, E)?,
ty: extract_field!(map, "type", String, E)?,
source: extract_field!(map, "source", String, E)?,
datacontenttype: extract_optional_field!(map, "datacontenttype", String, E)?,
schemaurl: extract_optional_field!(map, "schemaurl", String, E, |s: String| {
Url::parse(&s)
})?,
subject: extract_optional_field!(map, "subject", String, E)?,
time: extract_optional_field!(map, "time", String, E, |s: String| {
DateTime::parse_from_rfc3339(&s).map(DateTime::<Utc>::from)
})?,
id: parse_field!(map, "id", String, E)?,
ty: parse_field!(map, "type", String, E)?,
source: parse_field!(map, "source", String, E, Url::parse)?,
datacontenttype: parse_optional_field!(map, "datacontenttype", String, E)?,
schemaurl: parse_optional_field!(map, "schemaurl", String, E, Url::parse)?,
subject: parse_optional_field!(map, "subject", String, E)?,
time: parse_optional_field!(map, "time", String, E, |s| DateTime::parse_from_rfc3339(
s
)
.map(DateTime::<Utc>::from))?,
}))
}
fn deserialize_data<E: serde::de::Error>(
content_type: &str,
map: &mut Map<String, Value>,
map: &mut BTreeMap<String, Value>,
) -> Result<Option<Data>, E> {
let data = map.remove("data");
let is_base64 = map
.remove("datacontentencoding")
.map(String::deserialize)
.transpose()
.map_err(E::custom)?
.map_err(|e| E::custom(e))?
.map(|dce| dce.to_lowercase() == "base64")
.unwrap_or(false);
let is_json = is_json_content_type(content_type);
Ok(match (data, is_base64, is_json) {
(Some(d), false, true) => Some(Data::Json(parse_data_json(d)?)),
(Some(d), false, false) => Some(Data::String(parse_data_string(d)?)),
(Some(d), true, true) => Some(Data::Json(parse_data_base64_json(d)?)),
(Some(d), true, false) => Some(Data::Binary(parse_data_base64(d)?)),
(Some(d), false, true) => Some(Data::Json(parse_data_json!(d, E)?)),
(Some(d), false, false) => Some(Data::String(parse_data_string!(d, E)?)),
(Some(d), true, true) => Some(Data::Json(parse_json_data_base64!(d, E)?)),
(Some(d), true, false) => Some(Data::Binary(parse_data_base64!(d, E)?)),
(None, _, _) => None,
})
}
@ -69,19 +64,16 @@ impl<S: serde::Serializer> crate::event::format::EventFormatSerializer<S, Attrib
extensions: &HashMap<String, ExtensionValue>,
serializer: S,
) -> Result<<S as Serializer>::Ok, <S as Serializer>::Error> {
let num = 4
+ [
attributes.datacontenttype.is_some(),
attributes.schemaurl.is_some(),
attributes.subject.is_some(),
attributes.time.is_some(),
data.is_some(),
]
.iter()
.filter(|&b| *b)
.count()
+ extensions.len();
let num =
3 + if attributes.datacontenttype.is_some() {
1
} else {
0
} + if attributes.schemaurl.is_some() { 1 } else { 0 }
+ if attributes.subject.is_some() { 1 } else { 0 }
+ if attributes.time.is_some() { 1 } else { 0 }
+ if data.is_some() { 1 } else { 0 }
+ extensions.len();
let mut state = serializer.serialize_map(Some(num))?;
state.serialize_entry("specversion", "0.3")?;
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::String(s)) => state.serialize_entry("data", s)?,
Some(Data::Binary(v)) => {
state.serialize_entry("data", &BASE64_STANDARD.encode(v))?;
state.serialize_entry("data", &base64::encode(v))?;
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 builder;
mod format;
mod message;
pub(crate) use crate::event::v03::format::EventFormatDeserializer;
pub(crate) use crate::event::v03::format::EventFormatSerializer;
pub use attributes::Attributes;
pub(crate) use attributes::AttributesIntoIterator;
pub(crate) use attributes::ATTRIBUTE_NAMES;
pub use builder::EventBuilder;
pub(crate) use format::EventFormatDeserializer;
pub(crate) use format::EventFormatSerializer;

View File

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

View File

@ -1,68 +1,58 @@
use super::Attributes as AttributesV10;
use crate::event::{
Attributes, Data, Event, EventBuilderError, ExtensionValue, TryIntoTime, TryIntoUrl,
UriReference,
};
use crate::message::MessageAttributeValue;
use crate::event::{Attributes, AttributesWriter, Data, Event, ExtensionValue};
use chrono::{DateTime, Utc};
use std::collections::HashMap;
use std::convert::TryInto;
use url::Url;
/// Builder to create a CloudEvent V1.0
#[derive(Clone, Debug)]
pub struct EventBuilder {
id: Option<String>,
ty: Option<String>,
source: Option<UriReference>,
datacontenttype: Option<String>,
dataschema: Option<Url>,
subject: Option<String>,
time: Option<DateTime<Utc>>,
data: Option<Data>,
extensions: HashMap<String, ExtensionValue>,
error: Option<EventBuilderError>,
event: Event,
}
impl EventBuilder {
pub fn id(mut self, id: impl Into<String>) -> Self {
self.id = Some(id.into());
self
pub fn from(event: Event) -> Self {
EventBuilder {
event: Event {
attributes: event.attributes.into_v10(),
data: event.data,
extensions: event.extensions,
},
}
}
pub fn source(mut self, source: impl Into<String>) -> Self {
let source = source.into();
if source.is_empty() {
self.error = Some(EventBuilderError::InvalidUriRefError {
attribute_name: "source",
});
} else {
self.source = Some(source);
pub fn new() -> Self {
EventBuilder {
event: Event {
attributes: Attributes::V10(AttributesV10::default()),
data: None,
extensions: HashMap::new(),
},
}
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 {
self.ty = Some(ty.into());
self
self.event.set_type(ty);
return self;
}
pub fn subject(mut self, subject: impl Into<String>) -> Self {
self.subject = Some(subject.into());
self
self.event.set_subject(Some(subject));
return self;
}
pub fn time(mut self, time: impl TryIntoTime) -> Self {
match time.into_time() {
Ok(u) => self.time = Some(u),
Err(e) => {
self.error = Some(EventBuilderError::ParseTimeError {
attribute_name: "time",
source: e,
})
}
};
self
pub fn time(mut self, time: impl Into<DateTime<Utc>>) -> Self {
self.event.set_time(Some(time));
return self;
}
pub fn extension(
@ -70,158 +60,40 @@ impl EventBuilder {
extension_name: &str,
extension_value: impl Into<ExtensionValue>,
) -> Self {
self.extensions
.insert(extension_name.to_owned(), extension_value.into());
self
}
pub(crate) fn data_without_content_type(mut self, data: impl Into<Data>) -> Self {
self.data = Some(data.into());
self
self.event.set_extension(extension_name, extension_value);
return self;
}
pub fn data(mut self, datacontenttype: impl Into<String>, data: impl Into<Data>) -> Self {
self.datacontenttype = Some(datacontenttype.into());
self.data = Some(data.into());
self
self.event.write_data(datacontenttype, data);
return self;
}
pub fn data_with_schema(
mut self,
datacontenttype: impl Into<String>,
schemaurl: impl TryIntoUrl,
dataschema: impl Into<Url>,
data: impl Into<Data>,
) -> Self {
self.datacontenttype = Some(datacontenttype.into());
match schemaurl.into_url() {
Ok(u) => self.dataschema = Some(u),
Err(e) => {
self.error = Some(EventBuilderError::ParseUrlError {
attribute_name: "dataschema",
source: e,
})
}
};
self.data = Some(data.into());
self
}
}
impl From<Event> for EventBuilder {
fn from(event: Event) -> Self {
let attributes = match event.attributes.into_v10() {
Attributes::V10(attr) => attr,
// This branch is unreachable because into_v10() returns
// always a Attributes::V10
_ => unreachable!(),
};
EventBuilder {
id: Some(attributes.id),
ty: Some(attributes.ty),
source: Some(attributes.source),
datacontenttype: attributes.datacontenttype,
dataschema: attributes.dataschema,
subject: attributes.subject,
time: attributes.time,
data: event.data,
extensions: event.extensions,
error: None,
}
}
}
impl Default for EventBuilder {
fn default() -> Self {
Self::from(Event::default())
}
}
impl crate::event::builder::EventBuilder for EventBuilder {
fn new() -> Self {
EventBuilder {
id: None,
ty: None,
source: None,
datacontenttype: None,
dataschema: None,
subject: None,
time: None,
data: None,
extensions: Default::default(),
error: None,
}
self.event
.write_data_with_schema(datacontenttype, dataschema, data);
return self;
}
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(())
pub fn build(self) -> Event {
self.event
}
}
#[cfg(test)]
mod tests {
use crate::assert_match_pattern;
use chrono::{DateTime, Utc};
use crate::event::{
AttributesReader, EventBuilder, EventBuilderError, ExtensionValue, SpecVersion,
};
use crate::EventBuilderV10;
use std::convert::TryInto;
use url::Url;
use super::*;
use crate::event::{AttributesReader, SpecVersion};
#[test]
fn build_event() {
let id = "aaa";
let source = "http://localhost:8080";
let source = Url::parse("http://localhost:8080").unwrap();
let ty = "bbb";
let subject = "francesco";
let time: DateTime<Utc> = Utc::now();
@ -233,71 +105,30 @@ mod tests {
"hello": "world"
});
let mut event = EventBuilderV10::new()
let event = EventBuilder::new()
.id(id)
.source(source.to_string())
.source(source.clone())
.ty(ty)
.subject(subject)
.time(time)
.extension(extension_name, extension_value)
.data_with_schema(content_type, schema.clone(), data.clone())
.build()
.unwrap();
.build();
assert_eq!(SpecVersion::V10, event.specversion());
assert_eq!(id, event.id());
assert_eq!(source, event.source().clone());
assert_eq!(ty, event.ty());
assert_eq!(subject, event.subject().unwrap());
assert_eq!(time, event.time().unwrap().clone());
assert_eq!(SpecVersion::V10, event.get_specversion());
assert_eq!(id, event.get_id());
assert_eq!(source, event.get_source().clone());
assert_eq!(ty, event.get_type());
assert_eq!(subject, event.get_subject().unwrap());
assert_eq!(time, event.get_time().unwrap().clone());
assert_eq!(
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!(schema, event.dataschema().unwrap().clone());
assert_eq!(content_type, event.get_datacontenttype().unwrap());
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);
}
#[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 crate::event::data::is_json_content_type;
use crate::event::format::{
parse_data_base64, parse_data_base64_json, parse_data_json, parse_data_string,
};
use crate::event::{Data, ExtensionValue};
use base64::prelude::*;
use chrono::{DateTime, Utc};
use serde::de::IntoDeserializer;
use serde::ser::SerializeMap;
use serde::{Deserialize, Serializer};
use serde_json::{Map, Value};
use std::collections::HashMap;
use serde_value::Value;
use std::collections::{BTreeMap, HashMap};
use url::Url;
pub(crate) struct EventFormatDeserializer {}
impl crate::event::format::EventFormatDeserializer for EventFormatDeserializer {
fn deserialize_attributes<E: serde::de::Error>(
map: &mut Map<String, Value>,
map: &mut BTreeMap<String, Value>,
) -> Result<crate::event::Attributes, E> {
Ok(crate::event::Attributes::V10(Attributes {
id: extract_field!(map, "id", String, E)?,
ty: extract_field!(map, "type", String, E)?,
source: extract_field!(map, "source", String, E)?,
datacontenttype: extract_optional_field!(map, "datacontenttype", String, E)?,
dataschema: extract_optional_field!(map, "dataschema", String, E, |s: String| {
Url::parse(&s)
})?,
subject: extract_optional_field!(map, "subject", String, E)?,
time: extract_optional_field!(map, "time", String, E, |s: String| {
DateTime::parse_from_rfc3339(&s).map(DateTime::<Utc>::from)
})?,
id: parse_field!(map, "id", String, E)?,
ty: parse_field!(map, "type", String, E)?,
source: parse_field!(map, "source", String, E, Url::parse)?,
datacontenttype: parse_optional_field!(map, "datacontenttype", String, E)?,
dataschema: parse_optional_field!(map, "dataschema", String, E, Url::parse)?,
subject: parse_optional_field!(map, "subject", String, E)?,
time: parse_optional_field!(map, "time", String, E, |s| DateTime::parse_from_rfc3339(
s
)
.map(DateTime::<Utc>::from))?,
}))
}
fn deserialize_data<E: serde::de::Error>(
content_type: &str,
map: &mut Map<String, Value>,
map: &mut BTreeMap<String, Value>,
) -> Result<Option<Data>, E> {
let data = map.remove("data");
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);
Ok(match (data, data_base64, is_json) {
(Some(d), None, true) => Some(Data::Json(parse_data_json(d)?)),
(Some(d), None, false) => Some(Data::String(parse_data_string(d)?)),
(None, Some(d), true) => match parse_data_base64_json::<E>(d.to_owned()) {
Ok(x) => Some(Data::Json(x)),
Err(_) => Some(Data::Binary(parse_data_base64(d)?)),
},
(None, Some(d), false) => Some(Data::Binary(parse_data_base64(d)?)),
(Some(_), Some(_), _) => {
return Err(E::custom("Cannot have both data and data_base64 field"))
}
(Some(d), None, true) => Some(Data::Json(parse_data_json!(d, E)?)),
(Some(d), None, false) => Some(Data::String(parse_data_string!(d, E)?)),
(None, Some(d), true) => Some(Data::Json(parse_json_data_base64!(d, E)?)),
(None, Some(d), false) => Some(Data::Binary(parse_data_base64!(d, E)?)),
(Some(_), Some(_), _) => Err(E::custom("Cannot have both data and data_base64 field"))?,
(None, None, _) => None,
})
}
@ -70,19 +60,19 @@ impl<S: serde::Serializer> crate::event::format::EventFormatSerializer<S, Attrib
extensions: &HashMap<String, ExtensionValue>,
serializer: S,
) -> Result<<S as Serializer>::Ok, <S as Serializer>::Error> {
let num = 4
+ [
attributes.datacontenttype.is_some(),
attributes.dataschema.is_some(),
attributes.subject.is_some(),
attributes.time.is_some(),
data.is_some(),
]
.iter()
.filter(|&b| *b)
.count()
+ extensions.len();
let num =
3 + if attributes.datacontenttype.is_some() {
1
} else {
0
} + if attributes.dataschema.is_some() {
1
} else {
0
} + if attributes.subject.is_some() { 1 } else { 0 }
+ if attributes.time.is_some() { 1 } else { 0 }
+ if data.is_some() { 1 } else { 0 }
+ extensions.len();
let mut state = serializer.serialize_map(Some(num))?;
state.serialize_entry("specversion", "1.0")?;
state.serialize_entry("id", &attributes.id)?;
@ -103,9 +93,7 @@ impl<S: serde::Serializer> crate::event::format::EventFormatSerializer<S, Attrib
match data {
Some(Data::Json(j)) => state.serialize_entry("data", j)?,
Some(Data::String(s)) => state.serialize_entry("data", s)?,
Some(Data::Binary(v)) => {
state.serialize_entry("data_base64", &BASE64_STANDARD.encode(v))?
}
Some(Data::Binary(v)) => state.serialize_entry("data_base64", &base64::encode(v))?,
_ => (),
};
for (k, v) in extensions {

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