Compare commits

...

175 Commits
0.1.0 ... main

Author SHA1 Message Date
Fabrizio Lazzaretti fa0aadb31d
Update MAINTAINERS.md - Remove Linus (#251)
Remove @linuxbasic as discussed with him.

Signed-off-by: Fabrizio Lazzaretti <fabrizio@lazzaretti.me>
2025-01-09 11:24:03 +01:00
Fabrizio Lazzaretti 3590617290 Bump version to 0.8.0
change to 0.9.0 in a pull request was wrong, last released version is 0.7.0

Signed-off-by: Fabrizio Lazzaretti <fabrizio@lazzaretti.me>
2025-01-07 08:50:34 -05:00
Omar Zabala-Ferrera 5345ee3edc Updating axum.
Signed-off-by: Omar Zabala-Ferrera <ozf-dev@pm.me>
2025-01-06 11:02:39 -05:00
Omar Zabala-Ferrera 2bd15dfe9d Update rdkafka and and http.
Also fix base64 deprecation warning.

Signed-off-by: Omar Zabala-Ferrera <73452461+ozabalaferrera@users.noreply.github.com>
2025-01-06 11:02:39 -05:00
Omar Zabala-Ferrera 897cd85c40
Trying resolver fix, again. (#248)
Signed-off-by: Omar Zabala-Ferrera <73452461+ozabalaferrera@users.noreply.github.com>
2024-12-03 15:49:56 +01:00
Omar Zabala-Ferrera 09661ddaf7
Upgrade dependencies, including http and hyper, where possible. (#233)
* Upgrade axum.
Breaks docs.

Signed-off-by: Omar Zabala-Ferrera <73452461+ozabalaferrera@users.noreply.github.com>

* Upgrade several dependencies.
delegate-attr, base64, snafu, bitflags, hostname, and serde_yaml.

Signed-off-by: Omar Zabala-Ferrera <73452461+ozabalaferrera@users.noreply.github.com>

* Change target wasm32-wasi to wasm32-wasip1.

Signed-off-by: Omar Zabala-Ferrera <73452461+ozabalaferrera@users.noreply.github.com>

---------

Signed-off-by: Omar Zabala-Ferrera <73452461+ozabalaferrera@users.noreply.github.com>
2024-12-02 17:45:10 +01:00
Bobby Calderwood 9b38aead8d
Add AttributeValue::Binary; align order of AttributeValue with spec (#238)
Signed-off-by: Bobby Calderwood <8336+bobby@users.noreply.github.com>
2024-12-02 17:42:03 +01:00
dependabot[bot] f9dde9daae
Bump webpack in /example-projects/reqwest-wasm-example (#241)
Bumps [webpack](https://github.com/webpack/webpack) from 5.76.0 to 5.95.0.
- [Release notes](https://github.com/webpack/webpack/releases)
- [Commits](https://github.com/webpack/webpack/compare/v5.76.0...v5.95.0)

---
updated-dependencies:
- dependency-name: webpack
  dependency-type: direct:development
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-10-30 20:42:39 +02:00
dependabot[bot] 0f5f748685
Bump cookie and express in /example-projects/reqwest-wasm-example (#239)
Bumps [cookie](https://github.com/jshttp/cookie) and [express](https://github.com/expressjs/express). These dependencies needed to be updated together.

Updates `cookie` from 0.6.0 to 0.7.1
- [Release notes](https://github.com/jshttp/cookie/releases)
- [Commits](https://github.com/jshttp/cookie/compare/v0.6.0...v0.7.1)

Updates `express` from 4.19.2 to 4.21.1
- [Release notes](https://github.com/expressjs/express/releases)
- [Changelog](https://github.com/expressjs/express/blob/4.21.1/History.md)
- [Commits](https://github.com/expressjs/express/compare/4.19.2...4.21.1)

---
updated-dependencies:
- dependency-name: cookie
  dependency-type: indirect
- dependency-name: express
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-10-30 20:31:51 +02:00
dependabot[bot] bf45f01602
Bump braces in /example-projects/reqwest-wasm-example (#231)
Bumps [braces](https://github.com/micromatch/braces) from 3.0.2 to 3.0.3.
- [Changelog](https://github.com/micromatch/braces/blob/master/CHANGELOG.md)
- [Commits](https://github.com/micromatch/braces/compare/3.0.2...3.0.3)

---
updated-dependencies:
- dependency-name: braces
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-06-20 10:50:44 +02:00
dependabot[bot] 1f94433c0a
Bump ws from 8.16.0 to 8.17.1 in /example-projects/reqwest-wasm-example (#232)
Bumps [ws](https://github.com/websockets/ws) from 8.16.0 to 8.17.1.
- [Release notes](https://github.com/websockets/ws/releases)
- [Commits](https://github.com/websockets/ws/compare/8.16.0...8.17.1)

---
updated-dependencies:
- dependency-name: ws
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-06-19 11:02:18 +02:00
Anton Whalley 2f57c3ce36
WASI example (#228)
* feat: wasi-example

Signed-off-by: Anton Whalley <anton@venshare.com>

* fix: remove line end

Signed-off-by: Anton Whalley <anton@venshare.com>

---------

Signed-off-by: Anton Whalley <anton@venshare.com>
2024-05-10 15:06:08 +02:00
Anton Whalley 13c36fdbfe
WASI support (#202)
* fix: use different claim for wasi

Signed-off-by: Anton Whalley <anton@venshare.com>

* fix: remove main hyper

Signed-off-by: Anton Whalley <anton@venshare.com>

* fix: ignore runtimes docs in wasi test

Signed-off-by: Anton Whalley <anton@venshare.com>

* fix: reference attributed rust-claim

Signed-off-by: Anton Whalley <anton@venshare.com>

* fix: use supported claims

Signed-off-by: Anthony Whalley <anton@venshare.com>

* fix: update ticks

Signed-off-by: Anton Whalley <anton@venshare.com>

* test: wasi target_os and hyper conditional

Signed-off-by: Anton Whalley <anton@venshare.com>

* fix: revert toml format and param for target

Signed-off-by: Anton Whalley <anton@venshare.com>

* fix: remove formatting of cargo file

Signed-off-by: Fabrizio Lazzaretti <fabrizio@lazzaretti.me>

---------

Signed-off-by: Anton Whalley <anton@venshare.com>
Signed-off-by: Anthony Whalley <anton@venshare.com>
Signed-off-by: Fabrizio Lazzaretti <fabrizio@lazzaretti.me>
Co-authored-by: Fabrizio Lazzaretti <fabrizio@lazzaretti.me>
2024-04-30 20:05:41 +02:00
Davide Petilli a59c3f55a0
Update rdkafka-lib version to ^0.36 (#226)
* Update rdkafka-lib version to ^0.36

The rdkafka-lib version being used has been updated from ^0.29 to ^0.36. This update in the package version is necessary for better compatibility with the latest kafka features.

Signed-off-by: Davide Petilli <davide@petilli.me>

* Upgrade version of rdkafka library example

Signed-off-by: Davide Petilli <davide@petilli.me>

---------

Signed-off-by: Davide Petilli <davide@petilli.me>
2024-04-20 12:13:43 +02:00
Dirk Rusche 1978ae16aa
remove unnecessary clones (#224)
Signed-off-by: Dirk Rusche <dirk@rusche.me>
2024-04-19 10:36:38 +02:00
dependabot[bot] c4a5443d19
Bump express in /example-projects/reqwest-wasm-example (#223)
Bumps [express](https://github.com/expressjs/express) from 4.18.2 to 4.19.2.
- [Release notes](https://github.com/expressjs/express/releases)
- [Changelog](https://github.com/expressjs/express/blob/master/History.md)
- [Commits](https://github.com/expressjs/express/compare/4.18.2...4.19.2)

---
updated-dependencies:
- dependency-name: express
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-04-02 13:29:29 +02:00
dependabot[bot] 48b7e33cc2
Bump follow-redirects in /example-projects/reqwest-wasm-example (#221)
Bumps [follow-redirects](https://github.com/follow-redirects/follow-redirects) from 1.15.4 to 1.15.6.
- [Release notes](https://github.com/follow-redirects/follow-redirects/releases)
- [Commits](https://github.com/follow-redirects/follow-redirects/compare/v1.15.4...v1.15.6)

---
updated-dependencies:
- dependency-name: follow-redirects
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-03-24 16:29:43 +01:00
dependabot[bot] b719f70cee Bump webpack-dev-middleware and webpack-dev-server
Bumps [webpack-dev-middleware](https://github.com/webpack/webpack-dev-middleware) to 7.1.1 and updates ancestor dependency [webpack-dev-server](https://github.com/webpack/webpack-dev-server). These dependencies need to be updated together.


Updates `webpack-dev-middleware` from 3.7.3 to 7.1.1
- [Release notes](https://github.com/webpack/webpack-dev-middleware/releases)
- [Changelog](https://github.com/webpack/webpack-dev-middleware/blob/master/CHANGELOG.md)
- [Commits](https://github.com/webpack/webpack-dev-middleware/compare/v3.7.3...v7.1.1)

Updates `webpack-dev-server` from 3.11.2 to 5.0.4
- [Release notes](https://github.com/webpack/webpack-dev-server/releases)
- [Changelog](https://github.com/webpack/webpack-dev-server/blob/master/CHANGELOG.md)
- [Commits](https://github.com/webpack/webpack-dev-server/compare/v3.11.2...v5.0.4)

---
updated-dependencies:
- dependency-name: webpack-dev-middleware
  dependency-type: indirect
- dependency-name: webpack-dev-server
  dependency-type: direct:development
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-03-22 10:39:11 -04:00
dependabot[bot] 22f54770c9 Bump ip from 1.1.5 to 1.1.9 in /example-projects/reqwest-wasm-example
Bumps [ip](https://github.com/indutny/node-ip) from 1.1.5 to 1.1.9.
- [Commits](https://github.com/indutny/node-ip/compare/v1.1.5...v1.1.9)

---
updated-dependencies:
- dependency-name: ip
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-02-21 09:09:42 -05:00
dependabot[bot] a73743b06a
Bump follow-redirects in /example-projects/reqwest-wasm-example (#219)
Bumps [follow-redirects](https://github.com/follow-redirects/follow-redirects) from 1.14.8 to 1.15.4.
- [Release notes](https://github.com/follow-redirects/follow-redirects/releases)
- [Commits](https://github.com/follow-redirects/follow-redirects/compare/v1.14.8...v1.15.4)

---
updated-dependencies:
- dependency-name: follow-redirects
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-01-29 13:34:41 +01:00
dependabot[bot] 721c42c27c Bump postcss in /example-projects/reqwest-wasm-example
Bumps [postcss](https://github.com/postcss/postcss) from 8.3.5 to 8.4.31.
- [Release notes](https://github.com/postcss/postcss/releases)
- [Changelog](https://github.com/postcss/postcss/blob/main/CHANGELOG.md)
- [Commits](https://github.com/postcss/postcss/compare/8.3.5...8.4.31)

---
updated-dependencies:
- dependency-name: postcss
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-10-18 15:44:04 -04:00
Jim Crossley 933edbc883 Simplify warp doctest in order to pass with latest v0.3.6
Signed-off-by: Jim Crossley <jim@crossleys.org>
2023-10-18 13:41:49 -04:00
Jim Crossley 109d02d66a
revert cache change and pin warp dep to 0.3.5
Something about 0.3.6 breaks the doc test

Signed-off-by: Jim Crossley <jim@crossleys.org>
2023-10-18 12:49:54 -04:00
Jim Crossley 957ef1aa6d
ci: see if caching is causing build failures
Signed-off-by: Jim Crossley <jim@crossleys.org>
2023-10-18 09:58:45 -04:00
Doug Davis e001b9cd8b add link to our security mailing list
Signed-off-by: Doug Davis <dug@microsoft.com>
2023-10-16 10:46:41 -04:00
Doug Davis e19431fc06 Governance docs per CE PR 1226
Signed-off-by: Doug Davis <dug@microsoft.com>
2023-09-28 11:22:23 -04:00
Doug Davis 9e10eaadf0 add some missing governance docs
Signed-off-by: Doug Davis <dug@microsoft.com>
2023-09-28 10:43:03 -04:00
dependabot[bot] 4f265cd142
Bump webpack in /example-projects/reqwest-wasm-example (#208)
Bumps [webpack](https://github.com/webpack/webpack) from 5.75.0 to 5.76.0.
- [Release notes](https://github.com/webpack/webpack/releases)
- [Commits](https://github.com/webpack/webpack/compare/v5.75.0...v5.76.0)

---
updated-dependencies:
- dependency-name: webpack
  dependency-type: direct:development
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-03-22 22:09:19 +01:00
Kirin Xiao 77232941e4 Apply suggestions from code review
Signed-off-by: Kirin Xiao <xiaoqilin82@gmail.com>

Co-authored-by: Jim Crossley <jim@crossleys.org>
2023-03-08 08:33:12 -05:00
xiaoqilin 1e147eb560 fix dev-dependencies, bump axum version
Signed-off-by: xiaoqilin <xiaoqilin@bytedance.com>
2023-03-08 08:33:12 -05:00
Lazzaretti 4a86973f22
Prep for 0.7.0 (#203)
Signed-off-by: Fabrizio Lazzaretti <fabrizio@lazzaretti.me>
2023-02-24 12:38:50 +01:00
Mark Mandel 38469b245d Batch Event implementation for reqwest bindings
Added `events(Vec<Event>)` to `RequestBuilderExt` to provide a batched
set of Events to send to an HTTP endpoint, and
`into_events() -> Result<Vec<Event>>` to ResponseExt to parse a batched
Event response.

I deliberately kept things simple, as I thought this would be a good
place to start with Batch support throughout the SDK, and the
implementation was simple enough, that there didn't seem to be much
opportunity for reusable libraries across the SDK.
That could be changed as more Batch support is provided across the SDK,
and opportunities for code reuse present themselves.

Signed-off-by: Mark Mandel <markmandel@google.com>
2023-01-03 17:43:08 -05:00
Jim Crossley 20fd82a651
Merge pull request #201 from cloudevents/dependabot/npm_and_yarn/example-projects/reqwest-wasm-example/json5-and-webpack-and-html-webpack-plugin-2.2.3
Bump json5, webpack and html-webpack-plugin in /example-projects/reqwest-wasm-example
2023-01-03 13:30:30 -05:00
dependabot[bot] 4acd162cb7
Bump json5, webpack and html-webpack-plugin
Bumps [json5](https://github.com/json5/json5) to 2.2.3 and updates ancestor dependencies [json5](https://github.com/json5/json5), [webpack](https://github.com/webpack/webpack) and [html-webpack-plugin](https://github.com/jantimon/html-webpack-plugin). These dependencies need to be updated together.


Updates `json5` from 2.2.0 to 2.2.3
- [Release notes](https://github.com/json5/json5/releases)
- [Changelog](https://github.com/json5/json5/blob/main/CHANGELOG.md)
- [Commits](https://github.com/json5/json5/compare/v2.2.0...v2.2.3)

Updates `webpack` from 4.46.0 to 5.75.0
- [Release notes](https://github.com/webpack/webpack/releases)
- [Commits](https://github.com/webpack/webpack/compare/v4.46.0...v5.75.0)

Updates `html-webpack-plugin` from 4.5.2 to 5.5.0
- [Release notes](https://github.com/jantimon/html-webpack-plugin/releases)
- [Changelog](https://github.com/jantimon/html-webpack-plugin/blob/main/CHANGELOG.md)
- [Commits](https://github.com/jantimon/html-webpack-plugin/compare/v4.5.2...v5.5.0)

---
updated-dependencies:
- dependency-name: json5
  dependency-type: indirect
- dependency-name: webpack
  dependency-type: direct:development
- dependency-name: html-webpack-plugin
  dependency-type: direct:development
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-01-02 12:02:21 +00:00
dependabot[bot] c8454bce97
Bump express in /example-projects/reqwest-wasm-example (#199)
Bumps [express](https://github.com/expressjs/express) from 4.17.1 to 4.18.2.
- [Release notes](https://github.com/expressjs/express/releases)
- [Changelog](https://github.com/expressjs/express/blob/master/History.md)
- [Commits](https://github.com/expressjs/express/compare/4.17.1...4.18.2)

---
updated-dependencies:
- dependency-name: express
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-12-07 13:55:27 -05:00
dependabot[bot] eee1f82f8d
Bump minimatch in /example-projects/reqwest-wasm-example (#198)
Bumps [minimatch](https://github.com/isaacs/minimatch) from 3.0.4 to 3.1.2.
- [Release notes](https://github.com/isaacs/minimatch/releases)
- [Changelog](https://github.com/isaacs/minimatch/blob/main/changelog.md)
- [Commits](https://github.com/isaacs/minimatch/compare/v3.0.4...v3.1.2)

---
updated-dependencies:
- dependency-name: minimatch
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-12-07 13:55:10 -05:00
dependabot[bot] 6148f2efd3
Bump qs and express in /example-projects/reqwest-wasm-example (#197)
Bumps [qs](https://github.com/ljharb/qs) and [express](https://github.com/expressjs/express). These dependencies needed to be updated together.

Updates `qs` from 6.7.0 to 6.11.0
- [Release notes](https://github.com/ljharb/qs/releases)
- [Changelog](https://github.com/ljharb/qs/blob/main/CHANGELOG.md)
- [Commits](https://github.com/ljharb/qs/compare/v6.7.0...v6.11.0)

Updates `express` from 4.17.1 to 4.18.2
- [Release notes](https://github.com/expressjs/express/releases)
- [Changelog](https://github.com/expressjs/express/blob/master/History.md)
- [Commits](https://github.com/expressjs/express/compare/4.17.1...4.18.2)

---
updated-dependencies:
- dependency-name: qs
  dependency-type: indirect
- dependency-name: express
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-12-07 13:54:51 -05:00
dependabot[bot] bc1fc8e79c
Bump decode-uri-component in /example-projects/reqwest-wasm-example (#196)
Bumps [decode-uri-component](https://github.com/SamVerschueren/decode-uri-component) from 0.2.0 to 0.2.2.
- [Release notes](https://github.com/SamVerschueren/decode-uri-component/releases)
- [Commits](https://github.com/SamVerschueren/decode-uri-component/compare/v0.2.0...v0.2.2)

---
updated-dependencies:
- dependency-name: decode-uri-component
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-12-07 13:29:25 -05:00
dependabot[bot] ee987c9955
Bump loader-utils in /example-projects/reqwest-wasm-example (#195)
Bumps [loader-utils](https://github.com/webpack/loader-utils) from 1.4.1 to 1.4.2.
- [Release notes](https://github.com/webpack/loader-utils/releases)
- [Changelog](https://github.com/webpack/loader-utils/blob/v1.4.2/CHANGELOG.md)
- [Commits](https://github.com/webpack/loader-utils/compare/v1.4.1...v1.4.2)

---
updated-dependencies:
- dependency-name: loader-utils
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-11-23 17:47:18 +01:00
dependabot[bot] e3a86d642a
Bump loader-utils in /example-projects/reqwest-wasm-example (#194)
Bumps [loader-utils](https://github.com/webpack/loader-utils) from 1.4.0 to 1.4.1.
- [Release notes](https://github.com/webpack/loader-utils/releases)
- [Changelog](https://github.com/webpack/loader-utils/blob/v1.4.1/CHANGELOG.md)
- [Commits](https://github.com/webpack/loader-utils/compare/v1.4.0...v1.4.1)

---
updated-dependencies:
- dependency-name: loader-utils
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-11-08 11:21:45 -05:00
Jim Crossley e28424e198
Prep for 0.6.0 release (#193)
Signed-off-by: Jim Crossley <jim@crossleys.org>
2022-11-07 18:33:03 -05:00
Jim Crossley c6a84b3c5a Address clippy warnings
Signed-off-by: Jim Crossley <jim@crossleys.org>
2022-11-04 10:34:13 -04:00
Jim Crossley 7c2ff41960
Bump to latest rdkafka 0.29 (#192)
* Bump to latest rdkafka 0.29

* Updated example

Apps will be required to bump their dep to 0.29, too.

* Using the new iter() fn for message headers
2022-11-04 10:32:45 -04:00
Jim Crossley c380078bf4
Fix off-by-one bug in Event serializer (#191)
Fixes #189

Added breaking tests for both V03 and V10 to confirm bug and changed
the logic slightly to clarify intent

Signed-off-by: Jim Crossley <jim@crossleys.org>
2022-09-26 12:14:57 -04:00
minghuaw b8487af97c
Show feature gated bindings in documentaion (#187)
* show feature gated bindings in docsrs

* moved crate root docsrs feature

* fixed cargo fmt check

* removed files that should go with another PR

Signed-off-by: minghuaw <michael.wu1107@gmail.com>
2022-08-24 17:30:43 -04:00
dependabot[bot] e2066527e6 Bump terser in /example-projects/reqwest-wasm-example
Bumps [terser](https://github.com/terser/terser) from 4.8.0 to 4.8.1.
- [Release notes](https://github.com/terser/terser/releases)
- [Changelog](https://github.com/terser/terser/blob/master/CHANGELOG.md)
- [Commits](https://github.com/terser/terser/commits)

---
updated-dependencies:
- dependency-name: terser
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-07-22 16:04:55 -04:00
Jakub Noga 8dde763c06
New feature: NATS bindings (#185)
* feat: add NATS protocol bindings

Signed-off-by: Jakub Noga <jakub.noga@softchameleon.io>

* chore: run cargo fix & fmt

Signed-off-by: Jakub Noga <jakub.noga@softchameleon.io>

* fix: issues with docs

Signed-off-by: Jakub Noga <jakub.noga@softchameleon.io>

* Apply suggestions from code review

Co-authored-by: Lazzaretti <fabrizio@lazzaretti.me>

Signed-off-by: Jakub Noga <jakub.noga@softchameleon.io>

* feat: apply suggestions from code review

Signed-off-by: Jakub Noga <jakub.noga@softchameleon.io>

* feat: add test for v0.3 deserialization

Signed-off-by: Jakub Noga <jakub.noga@softchameleon.io>

* chore: run cargo fmt

Signed-off-by: Fabrizio Lazzaretti <fabrizio@lazzaretti.me>

Co-authored-by: Jakub Noga <jakub.noga@softchameleon.io>
Co-authored-by: Lazzaretti <fabrizio@lazzaretti.me>
2022-07-12 21:51:04 +02:00
Lazzaretti 6848cb15bd
Merge pull request #177 from cloudevents/dependabot/npm_and_yarn/example-projects/reqwest-wasm-example/minimist-1.2.6
Bump minimist from 1.2.5 to 1.2.6 in /example-projects/reqwest-wasm-example
2022-07-09 13:37:40 +02:00
dependabot[bot] cb9d9ddf16
Bump minimist in /example-projects/reqwest-wasm-example
Bumps [minimist](https://github.com/substack/minimist) from 1.2.5 to 1.2.6.
- [Release notes](https://github.com/substack/minimist/releases)
- [Commits](https://github.com/substack/minimist/compare/1.2.5...1.2.6)

---
updated-dependencies:
- dependency-name: minimist
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-07-09 11:22:30 +00:00
Jim Crossley 9882d5c521 Fix reqwest build by enabling js feature
Per https://docs.rs/getrandom/latest/getrandom/#webassembly-support

Signed-off-by: Jim Crossley <jim@crossleys.org>
2022-06-14 10:52:44 -04:00
Jim Crossley 8b77dd8c33 Bump uuid dep to version 1
Fixes #183

Signed-off-by: Jim Crossley <jim@crossleys.org>
2022-06-14 10:52:44 -04:00
Jim Crossley 6653e46059 Fix automated tests master->main
Signed-off-by: Jim Crossley <jim@crossleys.org>
2022-06-13 16:02:26 -04:00
dependabot[bot] 87c98aa26e
Bump eventsource in /example-projects/reqwest-wasm-example (#181)
Bumps [eventsource](https://github.com/EventSource/eventsource) from 1.1.0 to 1.1.1.
- [Release notes](https://github.com/EventSource/eventsource/releases)
- [Changelog](https://github.com/EventSource/eventsource/blob/master/HISTORY.md)
- [Commits](https://github.com/EventSource/eventsource/compare/v1.1.0...v1.1.1)

---
updated-dependencies:
- dependency-name: eventsource
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-06-02 09:54:00 -04:00
Yoshiki Kudo 24aec9320c
Support for Axum v0.5.x (#179)
* Support for Axum x0.5.x

* Upgrade axum v0.5 for example-project

Signed-off-by: Yoshiki Kudo <actionstar619@yahoo.co.jp>
2022-05-03 13:47:04 -04:00
dependabot[bot] 515fa81a77
Bump async from 2.6.3 to 2.6.4 in /example-projects/reqwest-wasm-example (#178)
Bumps [async](https://github.com/caolan/async) from 2.6.3 to 2.6.4.
- [Release notes](https://github.com/caolan/async/releases)
- [Changelog](https://github.com/caolan/async/blob/v2.6.4/CHANGELOG.md)
- [Commits](https://github.com/caolan/async/compare/v2.6.3...v2.6.4)

---
updated-dependencies:
- dependency-name: async
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-05-02 10:25:42 -04:00
Jim Crossley 2c5933bad4
Prep for 0.5.0 release (#176)
Signed-off-by: Jim Crossley <jim@crossleys.org>
2022-03-16 23:45:46 -04:00
dependabot[bot] 1f29640b50
Bump url-parse in /example-projects/reqwest-wasm-example (#175)
Bumps [url-parse](https://github.com/unshiftio/url-parse) from 1.5.7 to 1.5.10.
- [Release notes](https://github.com/unshiftio/url-parse/releases)
- [Commits](https://github.com/unshiftio/url-parse/compare/1.5.7...1.5.10)

---
updated-dependencies:
- dependency-name: url-parse
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-03-16 17:56:24 -04:00
Jim Crossley 5a9f64868d
Update to actix-web 4 (#167)
* Compatiblity with actix-web 4

Fixes #114

Main challenge here was no longer being able to to construct
web::Payload instances from dev::Payload instances. Instead, we now
invoke the web::Payload FromRequest impl directly.

Also adapted to the latest upstream test and body redesign.

Macros are featurized now so enabled default features for the doc
tests to pass.

Signed-off-by: Jim Crossley <jim@crossleys.org>
2022-02-28 13:02:00 -05:00
dependabot[bot] 6af7d1e8ec
Bump nth-check in /example-projects/reqwest-wasm-example (#174)
Bumps [nth-check](https://github.com/fb55/nth-check) from 2.0.0 to 2.0.1.
- [Release notes](https://github.com/fb55/nth-check/releases)
- [Commits](https://github.com/fb55/nth-check/compare/v2.0.0...v2.0.1)

---
updated-dependencies:
- dependency-name: nth-check
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-02-28 12:23:33 -05:00
dependabot[bot] 32b3134981
Bump nanoid in /example-projects/reqwest-wasm-example (#173)
Bumps [nanoid](https://github.com/ai/nanoid) from 3.1.23 to 3.3.1.
- [Release notes](https://github.com/ai/nanoid/releases)
- [Changelog](https://github.com/ai/nanoid/blob/main/CHANGELOG.md)
- [Commits](https://github.com/ai/nanoid/compare/3.1.23...3.3.1)

---
updated-dependencies:
- dependency-name: nanoid
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-02-28 12:23:08 -05:00
dependabot[bot] 785786f251
Bump follow-redirects in /example-projects/reqwest-wasm-example (#171)
Bumps [follow-redirects](https://github.com/follow-redirects/follow-redirects) from 1.14.1 to 1.14.8.
- [Release notes](https://github.com/follow-redirects/follow-redirects/releases)
- [Commits](https://github.com/follow-redirects/follow-redirects/compare/v1.14.1...v1.14.8)

---
updated-dependencies:
- dependency-name: follow-redirects
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-02-28 12:19:06 -05:00
dependabot[bot] 2742de5307
Bump url-parse in /example-projects/reqwest-wasm-example (#172)
Bumps [url-parse](https://github.com/unshiftio/url-parse) from 1.5.1 to 1.5.7.
- [Release notes](https://github.com/unshiftio/url-parse/releases)
- [Commits](https://github.com/unshiftio/url-parse/compare/1.5.1...1.5.7)

---
updated-dependencies:
- dependency-name: url-parse
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-02-21 09:11:47 -05:00
Roman Kudryashov 2b66c959d1
Bump rdkafka version (#170)
Signed-off-by: Roman Kudryashov <rskudryashov@gmail.com>
2022-02-05 12:07:41 -05:00
Andrew ae83a69f7f
Axum 0.4.0 (#168)
* axum 0.4.0

Signed-off-by: andrew webber (personal) <andrewvwebber@googlemail.com>
2022-01-17 09:23:12 -05:00
Jim Crossley ba798f30cb Fix latest breaking change from poem
Pin to latest version to prevent this in the future

Signed-off-by: Jim Crossley <jim@crossleys.org>
2022-01-14 18:51:03 -05:00
Jim Crossley 6d8e78bf99 Fix clippy warnings 2022-01-14 16:14:46 -05:00
Jim Crossley f3d106b3b1
Account for poem 1.2 breaking changes (#166)
* Account for poem 1.2 breaking changes

* Update the poem example, too

Signed-off-by: Jim Crossley <jim@crossleys.org>
2022-01-06 00:37:49 -05:00
Jim Crossley 7f538a3f37 Pin poem version to fix CI
Signed-off-by: Jim Crossley <jim@crossleys.org>
2021-12-15 09:33:57 -05:00
Jim Crossley 70e3b6681b Fix TryFrom<Data> impl for Vec<u8>
Fixes #163

Signed-off-by: Jim Crossley <jim@crossleys.org>
2021-12-15 09:33:57 -05:00
Jim Crossley 65a4782853
Merge pull request #159 from sunli829/master
New feature: cloudevents-poem
2021-11-13 13:00:26 -05:00
Jim Crossley 82f08f85ea Ignore useless warning
Signed-off-by: Jim Crossley <jim@crossleys.org>
2021-11-12 09:13:35 -05:00
Jim Crossley 66b9bfde1b De-macro-fy data parsing
The code is more legible without them, I think. I'm not sure their
complexity is justified.

Signed-off-by: Jim Crossley <jim@crossleys.org>
2021-11-12 09:13:35 -05:00
Sunli 127d0cec4b Update poem example
Signed-off-by: Sunli <scott_s829@163.com>
2021-11-12 11:38:02 +08:00
Sunli e16e9667cf Add poem-example
Signed-off-by: Sunli <scott_s829@163.com>
2021-11-11 09:34:05 +08:00
Jim Crossley 05807fdf28 Don't fail to deserialize data_base64 even if datacontenttype lies
This fixes #160

From the spec: "When a CloudEvent is deserialized from JSON, the
presence of the data_base64 member clearly indicates that the value is
a Base64 encoded binary data, which the deserializer MUST decode into
a binary runtime data type. The deserializer MAY further interpret
this binary data according to the datacontenttype."
https://github.com/cloudevents/spec/blob/master/cloudevents/formats/json-format.md#312-payload-deserialization

Signed-off-by: Jim Crossley <jim@crossleys.org>
2021-11-10 13:06:24 -05:00
Sunli 8928803e67 Remove useless `in_band_lifetimes` feature
Signed-off-by: Sunli <scott_s829@163.com>
2021-11-10 11:42:45 +08:00
Sunli d987002173 New feature: cloudevents-poem
Signed-off-by: Sunli <scott_s829@163.com>
2021-11-07 10:29:16 +08:00
Jim Crossley 1e89203cbc Make the reqwest/wasm example a little more helpful 2021-10-27 23:18:04 -04:00
Jim Crossley 9480a0e944 Add some example help 2021-10-20 21:46:44 -04:00
Dejan Bosanac 0741f2bf28 Add support for core http crate binding
Signed-off-by: Dejan Bosanac <dejan@sensatic.net>
2021-10-08 13:23:28 -04:00
Dejan Bosanac 62b895c025 Display utf8 data as plain text
Signed-off-by: Dejan Bosanac <dejan@sensatic.net>
2021-10-04 18:11:43 -04:00
Jim Crossley 934915d910 Fix broken doc links warnings 2021-09-10 13:30:53 -04:00
Dejan Bosanac 1e00f6fe04 Update reqwest wasm example so it compiles and run properly again
Signed-off-by: Dejan Bosanac <dejan@sensatic.net>
2021-09-10 10:56:14 -04:00
Jim Crossley 0aeebac010 Refactor Builder Adapter common to both warp and axum
Resulted in some minor code re-org as well

Signed-off-by: Jim Crossley <jim@crossleys.org>
2021-09-09 13:27:05 -04:00
Dejan Bosanac 5cc2fddddd Use defined fixtures in tests
Signed-off-by: Dejan Bosanac <dejan@sensatic.net>
2021-09-02 09:22:15 -04:00
Jim Crossley 733d568591 Minor docs re Axum 2021-08-31 15:56:43 -04:00
andrew webber (personal) 6074b4db7e Add Axum binding
Signed-off-by: andrew webber (personal) <andrewvwebber@googlemail.com>
2021-08-31 15:40:04 -04:00
Jim Crossley b540542040 Factor common response Builder trait out for actix & warp
This proved very difficult due to the different ownership behavior
between each builder, i.e. warp is a consuming builder, actix isn't.

With the use of the Rc/Cell/RefCell, I worry that this is now more
complex than it was, and for all I know, I've introduced a memory leak
somewhere. :)

Since the reqwest builder is also consuming, it should be able to
follow the same "adapter" pattern as warp. Unfortunately, the reqwest
builder doesn't implement the Default trait, so I can't use take() and
I've yet to come up with another solution.

Since 2/3 of the builders are consumers, it's possible we might
simplify the code by having the new Builder trait reflect that,
i.e. using self instead of &self in the fn params. We'll investigate
that next.

For now, I'm just happy to have 2 builders sharing some formerly
redundant behavior.

Signed-off-by: Jim Crossley <jim@crossleys.org>
2021-08-25 16:43:27 -04:00
Jim Crossley c4e8780c79 Encapsulate shared event deserialization behind a Headers trait
Both warp and reqwest use the HeaderMap from the http crate. Actix has
its own. Fortunately, both contain (HeaderName, HeaderValue) tuples.

Further, actix uses a conflicting version of the bytes crate, so I
store a Vec<u8> instead of a Bytes member in the Deserializer
struct. Not sure if that's a problem, but the tests pass. :)

We use an associated type in the Headers trait to facilitate static
dispatch for warp/reqwest since their concrete iterator is public, but
the actix Iter struct is private, so we use a Box for its impl.

We're using AsHeaderName for the get() param to avoid having to call
as_str() on any header constants, but of course actix uses its own
AsName trait, which isn't public, so we must call as_str() for the
passed header name in its impl.

Signed-off-by: Jim Crossley <jim@crossleys.org>
2021-08-25 16:27:32 -04:00
Jim Crossley ca3ba3b88c Move binding/http mod into its own file
Signed-off-by: Jim Crossley <jim@crossleys.org>
2021-08-17 11:41:13 -04:00
Dejan Bosanac 96c69d99da Move test_data to main crate
Signed-off-by: Dejan Bosanac <dejan@sensatic.net>
2021-07-29 08:58:45 -04:00
Jim Crossley 211792f0f4
Refactor redundant header logic to a shared lib (#146)
* Refactor redundant header logic to a shared lib

* Remove some unused code

* Share macro between actix and warp, eliminating actix header mod

Fixes #145

Signed-off-by: Jim Crossley <jim@crossleys.org>
2021-07-23 14:00:26 -04:00
Francesco Guardiani 2cae3f0b36
Bumps + fix version_sync crate (#143)
Signed-off-by: Francesco Guardiani <francescoguard@gmail.com>
2021-07-07 15:15:48 +02:00
Francesco Guardiani bcb8363deb
Fix null context attributes in json (#142)
* Fix null extensions and fields in json

Signed-off-by: Francesco Guardiani <francescoguard@gmail.com>

* fmt

Signed-off-by: Francesco Guardiani <francescoguard@gmail.com>
2021-07-07 14:49:49 +02:00
Francesco Guardiani 589db8e5be
Renamed features (#140)
* Renamed features
Moved all bindings under a binding crate

Signed-off-by: Francesco Guardiani <francescoguard@gmail.com>

* Cargo fmt

Signed-off-by: Francesco Guardiani <francescoguard@gmail.com>

* Now this should build

Signed-off-by: Francesco Guardiani <francescoguard@gmail.com>

* Use the new cache plugin

Signed-off-by: Francesco Guardiani <francescoguard@gmail.com>

* Fix

Signed-off-by: Francesco Guardiani <francescoguard@gmail.com>

* Fix the build

Signed-off-by: Francesco Guardiani <francescoguard@gmail.com>

* Fixed doc links

Signed-off-by: Francesco Guardiani <francescoguard@gmail.com>

* Change link

Signed-off-by: Francesco Guardiani <francescoguard@gmail.com>
2021-07-07 10:47:30 +02:00
Dejan Bosanac cd98c6705e
Add Actix structured test and clean up a bit (#141)
Signed-off-by: Dejan Bosanac <dejan@sensatic.net>
2021-07-06 15:53:40 +02:00
Jim Crossley f6b45d1af7
Fix broken link (#138)
Signed-off-by: Jim Crossley <jim@crossleys.org>
2021-06-30 16:54:50 +02:00
Jim Crossley e4d3370656
Update README to reflect new structure (#137)
Signed-off-by: Jim Crossley <jim@crossleys.org>
2021-06-30 16:21:30 +02:00
Jim Crossley 432259bd26
De-workspace-ification (#135)
Now that we've refactored the protocol bindings from crates to
feature-guarded modules (PR #131), we can remove the workspaces for
those crates.

Signed-off-by: Jim Crossley <jim@crossleys.org>
2021-06-30 14:13:41 +02:00
Francesco Guardiani 12230429b8
Merge pull request #136 from jcrossley3/feature-docs
Document feature flags
2021-06-30 08:56:34 +02:00
Jim Crossley ceb034b813 Document feature flags
Signed-off-by: Jim Crossley <jim@crossleys.org>
2021-06-29 19:23:44 -04:00
Francesco Guardiani dc81cf6cad
Merge pull request #132 from jcrossley3/actix-impls
Implement actix-web FromRequest and Responder
2021-06-29 09:31:09 +02:00
Francesco Guardiani 1a9d0d46fc
Merge pull request #134 from jcrossley3/clippy
Fix clippy warnings
2021-06-29 09:29:37 +02:00
Jim Crossley c1715d75a4 Fix clippy warnings
Signed-off-by: Jim Crossley <jim@crossleys.org>
2021-06-28 11:53:47 -04:00
Jim Crossley d45811604d Implement actix-web FromRequest and Responder
Fixes #130

I'm not entirely sure why this works, but the compiler seems to like
it! :D

The example is intentionally as simple as it gets, but a "real" app
should probably return Result<Event, Error> from its handlers.

Signed-off-by: Jim Crossley <jim@crossleys.org>
2021-06-28 11:21:32 -04:00
Francesco Guardiani bf21c52869
Merge pull request #133 from slinkydeveloper/fix_node_example
Node example now builds
2021-06-28 17:14:54 +02:00
slinkydeveloper c57be0b99b Node example now builds
Signed-off-by: Francesco Guardiani <francescoguard@gmail.com>
2021-06-28 16:39:33 +02:00
Francesco Guardiani 362492fa9a
Merge pull request #131 from jcrossley3/single-crate
Refactor runtime crates into feature-guarded modules
2021-06-25 16:51:45 +02:00
Jim Crossley 4979c44532 Build/Test --all-features for action matrix
For the wasm32 build, we replace --package <<redundant>> with --features cloudevents-reqwest

Signed-off-by: Jim Crossley <jim@crossleys.org>
2021-06-25 10:12:23 -04:00
Jim Crossley 69f16a5144 Fixed docs
Verified `cargo doc --all-features` works

Signed-off-by: Jim Crossley <jim@crossleys.org>
2021-06-25 10:12:23 -04:00
Jim Crossley 538b647804 New feature: cloudevents-warp
Conditionally compile warp module when enabled

Signed-off-by: Jim Crossley <jim@crossleys.org>
2021-06-25 10:12:23 -04:00
Jim Crossley 9055d71fb2 New feature: cloudevents-rdkafka
Conditionally compile rdkafka module when enabled

Signed-off-by: Jim Crossley <jim@crossleys.org>
2021-06-25 10:12:23 -04:00
Jim Crossley 51b49f1335 New feature: cloudevents-reqwest
Conditionally compile reqwest module when enabled

This resulted in a naming conflict between my desired feature name,
"reqwest", and the optional dependency itself. So I adopted the
convention of prefixing the features with "cloudevents-".

Signed-off-by: Jim Crossley <jim@crossleys.org>
2021-06-25 10:12:23 -04:00
Jim Crossley 935234a9cb New feature: cloudevents-actix
Conditionally compile actix module when enabled

Mostly straightforward, though I don't particularly love that dev-deps
can't be optional: https://github.com/rust-lang/cargo/issues/1596

Signed-off-by: Jim Crossley <jim@crossleys.org>
2021-06-25 10:12:23 -04:00
Matthias Wessendorf 303ed92545
💄 Use stream() instead of deprecated start() (#126)
Signed-off-by: Matthias Wessendorf <mwessend@redhat.com>
2021-05-24 09:23:07 +02:00
dependabot[bot] a1358ea5b7
Bump url-parse in /example-projects/reqwest-wasm-example (#122)
Bumps [url-parse](https://github.com/unshiftio/url-parse) from 1.4.7 to 1.5.1.
- [Release notes](https://github.com/unshiftio/url-parse/releases)
- [Commits](https://github.com/unshiftio/url-parse/compare/1.4.7...1.5.1)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-05-17 11:05:15 +02:00
Jens Reimann dfe2bcce13
Fix tests after refactoring URIRef (#116)
Signed-off-by: Jens Reimann <jreimann@redhat.com>
2021-04-21 11:09:24 +02:00
Jens Reimann 62ca1cd7da
[#106] Allowing handling URI-ref types (#115)
Signed-off-by: Jens Reimann <jreimann@redhat.com>
2021-03-05 09:20:52 +01:00
Francesco Guardiani da40d3d563
Tokio 1.0 release bumps! (#113)
Signed-off-by: Francesco Guardiani <francescoguard@gmail.com>
2021-02-08 16:39:11 +01:00
Francesco Guardiani 364ad7b41e
Release 0.3.1 bumps (#111)
Signed-off-by: Francesco Guardiani <francescoguard@gmail.com>
2021-01-18 18:13:34 +01:00
Andrew 4cb40f98a4
Latest serde breaks cloudevents-sdk (#110)
serde::export::Formatter -> std::fmt::Formatter
Signed-off-by: andrew webber (personal) <andrewvwebber@googlemail.com>
2021-01-12 19:10:41 +01:00
dependabot[bot] 7f63bd74ec
Bump ini from 1.3.5 to 1.3.8 in /example-projects/reqwest-wasm-example (#108)
Bumps [ini](https://github.com/isaacs/ini) from 1.3.5 to 1.3.8.
- [Release notes](https://github.com/isaacs/ini/releases)
- [Commits](https://github.com/isaacs/ini/compare/v1.3.5...v1.3.8)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2020-12-14 08:56:56 +01:00
Francesco Guardiani 6f5a767f19
Removing serde-value (#107)
* Removed serde_value

Signed-off-by: Francesco Guardiani <francescoguard@gmail.com>

* clippy + fix

Signed-off-by: Francesco Guardiani <francescoguard@gmail.com>

* cargo fmt

Signed-off-by: Francesco Guardiani <francescoguard@gmail.com>
2020-12-02 18:33:51 +01:00
tottoto 609c036ce5
Fix typo (#101)
Signed-off-by: tottoto <tottotodev@gmail.com>
2020-11-06 10:39:41 +01:00
Francesco Guardiani 570a9ea488
Fix lint warnings (#100)
* Fix clippy warnings

Signed-off-by: Francesco Guardiani <francescoguard@gmail.com>

* Cargo fmt

Signed-off-by: Francesco Guardiani <francescoguard@gmail.com>
2020-11-06 10:39:10 +01:00
tottoto 6ac6534f16
Change unused dependency to dev_dependency (#103)
Signed-off-by: tottoto <tottotodev@gmail.com>
2020-11-06 10:38:42 +01:00
tottoto 579665f226
Remove actix_rt dependency in actix_web example (#102)
Signed-off-by: tottoto <tottotodev@gmail.com>
2020-11-06 09:35:39 +01:00
Marko Milenković 5e5aca54be
Add support for `seanmonstar/warp` web server framework (#97)
Initial take on cloud events support for `seanmonstar/warp`

Signed-off-by: Marko Milenković <milenkovicm@users.noreply.github.com>
2020-11-04 12:12:55 +01:00
Francesco Guardiani d69ab904d1
Release bumps (#98)
Signed-off-by: Francesco Guardiani <francescoguard@gmail.com>
2020-11-02 09:35:39 +01:00
Lazzaretti b5d95741c2
Merge pull request #96 from slinkydeveloper/actions_update
CI Improvements
2020-11-02 09:06:12 +01:00
slinkydeveloper 099200c657 Newline
Signed-off-by: Francesco Guardiani <francescoguard@gmail.com>
2020-11-02 08:56:51 +01:00
slinkydeveloper 6ec93db965 Fix again kafka dep
Signed-off-by: Francesco Guardiani <francescoguard@gmail.com>
2020-11-02 08:46:32 +01:00
slinkydeveloper 80dad09f56 Nit
Signed-off-by: Francesco Guardiani <francescoguard@gmail.com>
2020-11-02 08:22:14 +01:00
slinkydeveloper 3770f99e77 Using clippy action
Removed build-examples job

Signed-off-by: Francesco Guardiani <francescoguard@gmail.com>
2020-11-02 08:22:14 +01:00
slinkydeveloper 0f9c9bd08f More cosmetics
Signed-off-by: Francesco Guardiani <francescoguard@gmail.com>
2020-11-02 08:22:14 +01:00
slinkydeveloper 321f04ea09 Fail fast false + cosmetics
Signed-off-by: Francesco Guardiani <francescoguard@gmail.com>
2020-11-02 08:22:14 +01:00
slinkydeveloper 3f9f024a6d Yaml indentation....
Signed-off-by: Francesco Guardiani <francescoguard@gmail.com>
2020-11-02 08:22:14 +01:00
slinkydeveloper a9fbb8e0ea Updates on actions
Signed-off-by: Francesco Guardiani <francescoguard@gmail.com>
2020-11-02 08:22:14 +01:00
Lazzaretti e6500338c6
Merge pull request #95 from slinkydeveloper/docs
Another round of docs fix + cleanups
2020-11-01 08:40:47 +01:00
Francesco Guardiani 4e3c023e4b Cargo fmt
Signed-off-by: Francesco Guardiani <francescoguard@gmail.com>
2020-10-30 10:03:12 +01:00
slinkydeveloper 30367b7e54 Added html_root_url as specified in https://rust-lang.github.io/api-guidelines/documentation.html#c-html-root
Signed-off-by: Francesco Guardiani <francescoguard@gmail.com>
2020-10-30 09:53:49 +01:00
slinkydeveloper c253dfe06d Fixed rdkafka example
Signed-off-by: Francesco Guardiani <francescoguard@gmail.com>
2020-10-30 08:37:35 +01:00
Francesco Guardiani 502c6d8ef7 Cargo fmt
Signed-off-by: Francesco Guardiani <francescoguard@gmail.com>
2020-10-29 19:15:19 +01:00
slinkydeveloper 47d9c272b2 Fixed rdkafka example
Signed-off-by: Francesco Guardiani <francescoguard@gmail.com>
2020-10-29 19:14:37 +01:00
slinkydeveloper 6f58d63e72 Fixed actix-web-example
Signed-off-by: Francesco Guardiani <francescoguard@gmail.com>
2020-10-29 19:14:19 +01:00
slinkydeveloper 6133a6e67f Docs in cloudevents-sdk-reqwest
Signed-off-by: Francesco Guardiani <francescoguard@gmail.com>
2020-10-29 19:14:07 +01:00
slinkydeveloper e330c6cc23 Docs in cloudevents-sdk-rdkafka
Signed-off-by: Francesco Guardiani <francescoguard@gmail.com>
2020-10-29 19:13:54 +01:00
slinkydeveloper 6658a809e8 Docs in cloudevents-sdk-actix-web
Signed-off-by: Francesco Guardiani <francescoguard@gmail.com>
2020-10-29 19:13:34 +01:00
slinkydeveloper cd8cddac18 Docs in cloudevents-sdk crate
Signed-off-by: Francesco Guardiani <francescoguard@gmail.com>
2020-10-29 19:13:10 +01:00
Francesco Guardiani d1281e7fea
Align with C-COMMON-TRAITS criteria (#91)
* Fix C-COMMON-TRAITS

Signed-off-by: Francesco Guardiani <francescoguard@gmail.com>

* Fix C-COMMON-TRAITS

Signed-off-by: Francesco Guardiani <francescoguard@gmail.com>
2020-10-27 14:10:25 +01:00
Francesco Guardiani c4305e0713
Modify data writer APIs on Event (#92)
* Modify setters apis

Signed-off-by: Francesco Guardiani <francescoguard@gmail.com>

* Cargo fmt

Signed-off-by: Francesco Guardiani <francescoguard@gmail.com>

* Switched into impls in from

Signed-off-by: Francesco Guardiani <francescoguard@gmail.com>

* fmt

Signed-off-by: Francesco Guardiani <francescoguard@gmail.com>
2020-10-27 14:10:18 +01:00
Francesco Guardiani 39af2d7ad0
Renamed error variants of message::Error (#90)
* Renamed 2 error variants to be a little bit more consistent (from https://rust-lang.github.io/api-guidelines/naming.html#c-word-order)

Signed-off-by: Francesco Guardiani <francescoguard@gmail.com>

* message::Error implements Send + Sync

Signed-off-by: Francesco Guardiani <francescoguard@gmail.com>
2020-10-21 12:04:42 +02:00
Francesco Guardiani c926188d78
Cleanup to follow C-GETTER (#88)
* Cleanup to follow C-GETTER

Signed-off-by: Francesco Guardiani <francescoguard@gmail.com>

* Cleanup to follow C-GETTER
Implemented Debug in event::Data
Exposing event::Data in main cloudevents export
Fixed rebase errors with previous pr

Signed-off-by: Francesco Guardiani <francescoguard@gmail.com>

* cargo fmt

Signed-off-by: Francesco Guardiani <francescoguard@gmail.com>
2020-10-21 12:04:32 +02:00
Francesco Guardiani 1858a1caa5
Add iter() in cloudevents::event::Attributes (#89)
* Add iter in Attributes

Signed-off-by: Francesco Guardiani <francescoguard@gmail.com>

* Removed bad import

Signed-off-by: Francesco Guardiani <francescoguard@gmail.com>
2020-10-19 09:29:15 +02:00
Francesco Guardiani fbadb3300a
Actix 3 dump (#85)
* Actix 3 dump

Signed-off-by: Francesco Guardiani <francescoguard@gmail.com>

* fmt check

Signed-off-by: Francesco Guardiani <francescoguard@gmail.com>
2020-10-12 19:26:00 +02:00
Francesco Guardiani 500f8e76e6
Cleanup to follow C-CONV (#87)
Signed-off-by: Francesco Guardiani <francescoguard@gmail.com>
2020-10-12 15:47:22 +02:00
Francesco Guardiani 2e66f6a46f
Now set_attribute returns the previous attribute value (#86)
remove_data renamed to take_data and now returns the previous data value

Signed-off-by: Francesco Guardiani <francescoguard@gmail.com>
2020-10-12 13:45:58 +02:00
Pranav Bhatt 5e0067bbbc
fixed musl conflict (#81)
Signed-off-by: adpranavb2000@gmail.com <adpranavb2000@gmail.com>
2020-09-04 10:19:17 +02:00
Xidorn Quan bd7559c369
Use delegate-attr to simplify code (#80)
Signed-off-by: Xidorn Quan <me@upsuper.org>
2020-08-31 14:31:52 +02:00
Doug Davis 71f5c38a05
add coc ref (#78)
Signed-off-by: Doug Davis <dug@us.ibm.com>
2020-08-11 17:12:21 +02:00
Francesco Guardiani 35b37e5a45
Version bump (#74)
Signed-off-by: Francesco Guardiani <francescoguard@gmail.com>
2020-08-06 15:42:56 +02:00
Francesco Guardiani 4a70d506de
Disabled test rdkafka crate with musl (#76)
Enabled reqwest module with wasm

Signed-off-by: Francesco Guardiani <francescoguard@gmail.com>
2020-08-06 15:35:59 +02:00
Francesco Guardiani 4700fa267f
Improved docs in integrations crates (#71)
Signed-off-by: Francesco Guardiani <francescoguard@gmail.com>
2020-08-06 14:56:31 +02:00
Pranav Bhatt 3a56fcc641
Implementing Kafka Protocol Binding for CloudEvents using rdkafka-rust (#60)
Signed-off-by: adpranavb2000@gmail.com <adpranavb2000@gmail.com>

Co-authored-by: slinkydeveloper <francescoguard@gmail.com>
2020-08-06 14:51:51 +02:00
Francesco Guardiani 94a134c44e
Refactored iterator methods of Event (#66)
* Refactored a bit the iterator entry point

Signed-off-by: Francesco Guardiani <francescoguard@gmail.com>

* cargo fmt

Signed-off-by: Francesco Guardiani <francescoguard@gmail.com>
2020-08-04 08:26:18 +02:00
dependabot[bot] 25af32ee3a
Bump elliptic in /example-projects/reqwest-wasm-example (#65)
Bumps [elliptic](https://github.com/indutny/elliptic) from 6.5.2 to 6.5.3.
- [Release notes](https://github.com/indutny/elliptic/releases)
- [Commits](https://github.com/indutny/elliptic/compare/v6.5.2...v6.5.3)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2020-08-03 08:34:45 +02:00
Francesco Guardiani e87605734e
Proper BinarySerializer & StructuredSerializer implementations for Event (#61)
* WIP

Signed-off-by: Francesco Guardiani <francescoguard@gmail.com>

* Progress

Signed-off-by: Francesco Guardiani <francescoguard@gmail.com>

* Seems like everything works

Signed-off-by: Francesco Guardiani <francescoguard@gmail.com>

* fmt'ed more stuff

Signed-off-by: Francesco Guardiani <francescoguard@gmail.com>
2020-07-28 09:52:58 +02:00
dependabot[bot] fc3790a7bd
Bump lodash in /example-projects/reqwest-wasm-example (#59)
Bumps [lodash](https://github.com/lodash/lodash) from 4.17.15 to 4.17.19.
- [Release notes](https://github.com/lodash/lodash/releases)
- [Commits](https://github.com/lodash/lodash/compare/4.17.15...4.17.19)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2020-07-21 08:54:35 +02:00
Pranav Bhatt 57f42cf753
Improve api (#58)
* wip improve_api

Signed-off-by: Pranav Bhatt <adpranavb2000@gmail.com>

* improved cloudevent-sdk-reqwest api

Signed-off-by: Pranav Bhatt <adpranavb2000@gmail.com>

Signed-off-by: Pranav Bhatt <adpranavb2000@gmail.com>

* improve_api finalise#1

Signed-off-by: Pranav Bhatt <adpranavb2000@gmail.com>

Signed-off-by: Pranav Bhatt <adpranavb2000@gmail.com>

* improve_api(reqwest) finalise#2

Signed-off-by: Pranav Bhatt <adpranavb2000@gmail.com>

Signed-off-by: Pranav Bhatt <adpranavb2000@gmail.com>

* improve_api(reqwest) finalise#2

Signed-off-by: Pranav Bhatt <adpranavb2000@gmail.com>

Signed-off-by: Pranav Bhatt <adpranavb2000@gmail.com>

* tested api calls within rust

Signed-off-by: Pranav Bhatt <adpranavb2000@gmail.com>

* tested api calls within rust#2

Signed-off-by: Pranav Bhatt <adpranavb2000@gmail.com>

* improve_api actix finalise#1

Signed-off-by: Pranav Bhatt <adpranavb2000@gmail.com>

* improve_api actix finalise#2

Signed-off-by: Pranav Bhatt <adpranavb2000@gmail.com>

* added documentation for actix api modifications

Signed-off-by: Pranav Bhatt <adpranavb2000@gmail.com>

* added documentation for actix api modifications#2

Signed-off-by: Pranav Bhatt <adpranavb2000@gmail.com>

* improve_api actix-web finalise#3

Signed-off-by: Pranav Bhatt <adpranavb2000@gmail.com>

* minor fixes for improve_api(actix)

Signed-off-by: Pranav Bhatt <adpranavb2000@gmail.com>
2020-06-24 09:35:49 +02:00
Pranav Bhatt f6770b4591
Improve api of module cloudevents-sdk-reqwest (#57)
* wip improve_api

Signed-off-by: Pranav Bhatt <adpranavb2000@gmail.com>

* improved cloudevent-sdk-reqwest api

Signed-off-by: Pranav Bhatt <adpranavb2000@gmail.com>

Signed-off-by: Pranav Bhatt <adpranavb2000@gmail.com>

* improve_api finalise#1

Signed-off-by: Pranav Bhatt <adpranavb2000@gmail.com>

Signed-off-by: Pranav Bhatt <adpranavb2000@gmail.com>

* improve_api(reqwest) finalise#2

Signed-off-by: Pranav Bhatt <adpranavb2000@gmail.com>

* improve_api(reqwest) finalise#2

Signed-off-by: Pranav Bhatt <adpranavb2000@gmail.com>

Signed-off-by: Pranav Bhatt <adpranavb2000@gmail.com>

* improve_api(reqwest) finalise#2

Signed-off-by: Pranav Bhatt <adpranavb2000@gmail.com>

Signed-off-by: Pranav Bhatt <adpranavb2000@gmail.com>

* improve_api(reqwest) finalise#3

Signed-off-by: Pranav Bhatt <adpranavb2000@gmail.com>

* improve_api(reqwest) finalise#4

Signed-off-by: Pranav Bhatt <adpranavb2000@gmail.com>

* issue with example

Signed-off-by: Pranav Bhatt <adpranavb2000@gmail.com>

* tested api calls within rust

Signed-off-by: Pranav Bhatt <adpranavb2000@gmail.com>

* tested api calls within rust#2

Signed-off-by: Pranav Bhatt <adpranavb2000@gmail.com>

* improve_api reqwest finalise

Signed-off-by: Pranav Bhatt <adpranavb2000@gmail.com>
2020-06-22 16:27:05 +02:00
dependabot[bot] 7c8206b4e1
Bump websocket-extensions in /example-projects/reqwest-wasm-example (#56)
Bumps [websocket-extensions](https://github.com/faye/websocket-extensions-node) from 0.1.3 to 0.1.4.
- [Release notes](https://github.com/faye/websocket-extensions-node/releases)
- [Changelog](https://github.com/faye/websocket-extensions-node/blob/master/CHANGELOG.md)
- [Commits](https://github.com/faye/websocket-extensions-node/compare/0.1.3...0.1.4)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2020-06-08 12:14:36 +02:00
Pranav Bhatt 046fabc55b
Expose iter (#55)
* exposed iterator via enum

Signed-off-by: Pranav Bhatt <adpranavb2000@gmail.com>

* exposed iterator

Signed-off-by: Pranav Bhatt <adpranavb2000@gmail.com>

* exposed iterator(finalise#1)

Signed-off-by: Pranav Bhatt <adpranavb2000@gmail.com>

* resolving pull request issues #1

Signed-off-by: Pranav Bhatt <adpranavb2000@gmail.com>

* resolving pull request issues #2

Signed-off-by: Pranav Bhatt <adpranavb2000@gmail.com>

* resolving pull request issues #3

Signed-off-by: Pranav Bhatt <adpranavb2000@gmail.com>

* Copy trait issue

Signed-off-by: Pranav Bhatt <adpranavb2000@gmail.com>

* Attributes Iterator finalise #2

Signed-off-by: Pranav Bhatt <adpranavb2000@gmail.com>

* Attributes Iterator finalise #3

Signed-off-by: Pranav Bhatt <adpranavb2000@gmail.com>

* Attributes Iterator finalise #4

Signed-off-by: Pranav Bhatt <adpranavb2000@gmail.com>

* Attributes Iterator finalise #5

Signed-off-by: Pranav Bhatt <adpranavb2000@gmail.com>
2020-06-05 16:45:47 +02:00
Francesco Guardiani f294cec0fe
Fixed all readmes (#51)
Signed-off-by: Francesco Guardiani <francescoguard@gmail.com>
2020-05-29 14:04:44 +02:00
Francesco Guardiani b832b6bcf4
Redesigned EventBuilder (#53)
* Progress

Signed-off-by: Francesco Guardiani <francescoguard@gmail.com>

* Builder finished

Signed-off-by: Francesco Guardiani <francescoguard@gmail.com>

* Fixed all integrations

Signed-off-by: Francesco Guardiani <francescoguard@gmail.com>

* Fmt'ed

Signed-off-by: Francesco Guardiani <francescoguard@gmail.com>

* Fmt'ed part 2

Signed-off-by: Francesco Guardiani <francescoguard@gmail.com>

* Fixed tests in reqwest integration

Signed-off-by: Francesco Guardiani <francescoguard@gmail.com>

* fmt'ed again

Signed-off-by: Francesco Guardiani <francescoguard@gmail.com>
2020-05-26 15:35:30 +02:00
Francesco Guardiani 2b91ee8d7a
Removed cloudevents::event::SPEC_VERSION_ATTRIBUTES (#52)
* Removed cloudevents::event::SPEC_VERSION_ATTRIBUTES once and for all

Signed-off-by: Francesco Guardiani <francescoguard@gmail.com>

* cargo fmt

Signed-off-by: Francesco Guardiani <francescoguard@gmail.com>
2020-05-26 15:28:51 +02:00
118 changed files with 15115 additions and 5784 deletions

View File

@ -1,66 +0,0 @@
name: Master
on:
push:
branches:
- master
jobs:
build:
name: Run tests on ${{ matrix.toolchain }} ${{ matrix.target }}
runs-on: ubuntu-latest
strategy:
matrix:
toolchain:
- stable
- nightly
target:
- x86_64-unknown-linux-gnu
- x86_64-unknown-linux-musl
- wasm32-unknown-unknown
steps:
- uses: actions/checkout@v2
- run: sudo apt-get update
if: matrix.target == 'x86_64-unknown-linux-musl'
- run: sudo apt-get install -y musl musl-dev musl-tools
if: matrix.target == 'x86_64-unknown-linux-musl'
- name: Cache cargo registry
uses: actions/cache@v1
with:
path: ~/.cargo/registry
key: ${{ runner.os }}-cargo-registry-${{ hashFiles('**/Cargo.lock') }}
- name: Cache cargo index
uses: actions/cache@v1
with:
path: ~/.cargo/git
key: ${{ runner.os }}-cargo-index-${{ hashFiles('**/Cargo.lock') }}
- name: Cache cargo build
uses: actions/cache@v1
with:
path: target
key: ${{ runner.os }}-cargo-build-target-${{ matrix.toolchain }}-${{ matrix.target }}-${{ hashFiles('**/Cargo.lock') }}
- uses: actions-rs/toolchain@v1
with:
toolchain: ${{ matrix.toolchain }}
target: ${{ matrix.target }}
override: true
- uses: actions-rs/cargo@v1
if: matrix.target != 'wasm32-unknown-unknown'
with:
command: build
toolchain: ${{ matrix.toolchain }}
args: --target ${{ matrix.target }} --all
- uses: actions-rs/cargo@v1
if: matrix.target != 'wasm32-unknown-unknown'
with:
command: test
toolchain: ${{ matrix.toolchain }}
args: --target ${{ matrix.target }} --all
# If wasm, then we don't need to compile --all
- uses: actions-rs/cargo@v1
if: matrix.target == 'wasm32-unknown-unknown'
with:
command: build
toolchain: ${{ matrix.toolchain }}
args: --target wasm32-unknown-unknown

View File

@ -1,60 +0,0 @@
name: Pull Request
on:
pull_request:
branches:
- master
jobs:
build:
name: Run tests
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Cache cargo registry
uses: actions/cache@v1
with:
path: ~/.cargo/registry
key: ${{ runner.os }}-cargo-registry-${{ hashFiles('**/Cargo.lock') }}
- name: Cache cargo index
uses: actions/cache@v1
with:
path: ~/.cargo/git
key: ${{ runner.os }}-cargo-index-${{ hashFiles('**/Cargo.lock') }}
- name: Cache cargo build
uses: actions/cache@v1
with:
path: target
key: ${{ runner.os }}-cargo-build-target-${{ hashFiles('**/Cargo.lock') }}
- uses: actions-rs/toolchain@v1
with:
profile: minimal
toolchain: stable
- uses: actions-rs/cargo@v1
with:
command: build
toolchain: stable
target: x86_64-unknown-linux-gnu
args: --all
- uses: actions-rs/cargo@v1
with:
command: test
toolchain: stable
target: x86_64-unknown-linux-gnu
args: --all
fmt:
name: Format check
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions-rs/toolchain@v1
with:
profile: minimal
toolchain: stable
override: true
components: rustfmt
- uses: actions-rs/cargo@v1
with:
command: fmt
args: --all -- --check

View File

@ -0,0 +1,29 @@
name: Lints
on:
pull_request:
branches:
- main
jobs:
lint:
name: Rust
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions-rs/toolchain@v1
with:
profile: minimal
toolchain: stable
override: true
components: clippy, rustfmt
- uses: actions-rs/cargo@v1
name: "Cargo fmt"
with:
command: fmt
args: --all -- --check
- uses: actions-rs/clippy-check@v1
name: "Cargo clippy"
with:
token: ${{ secrets.GITHUB_TOKEN }}
args: --all-features

181
.github/workflows/rust_tests.yml vendored Normal file
View File

@ -0,0 +1,181 @@
name: Rust Tests
on:
pull_request:
branches:
- main
push:
branches:
- main
jobs:
build:
name: ${{ matrix.toolchain }} / ${{ matrix.target }}
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
toolchain:
- stable
- nightly
target:
- x86_64-unknown-linux-gnu
- x86_64-unknown-linux-musl
- wasm32-unknown-unknown
- wasm32-wasip1
steps:
- uses: actions/checkout@v2
# setup wasmedge
- run: curl -sSf https://raw.githubusercontent.com/WasmEdge/WasmEdge/master/utils/install.sh | sudo bash -s -- -p /usr/local
# Setup musl if needed
- run: sudo apt-get update
if: matrix.target == 'x86_64-unknown-linux-musl'
- run: sudo apt-get install -y musl musl-dev musl-tools cmake
if: matrix.target == 'x86_64-unknown-linux-musl'
# # Caching stuff
- uses: actions/cache@v2
with:
path: |
~/.cargo/bin/
~/.cargo/registry/index/
~/.cargo/registry/cache/
~/.cargo/git/db/
key: ${{ runner.os }}-cargo-deps-${{ hashFiles('**/Cargo.toml') }}
- uses: actions/cache@v2
with:
path: |
target/
key: ${{ runner.os }}-cargo-target-${{ matrix.toolchain }}-${{ matrix.target }}-${{ hashFiles('**/Cargo.toml') }}
- uses: actions-rs/toolchain@v1
with:
toolchain: ${{ matrix.toolchain }}
target: ${{ matrix.target }}
override: true
# If glibc, compile and test all
- uses: actions-rs/cargo@v1
name: "Build"
if: matrix.target == 'x86_64-unknown-linux-gnu'
with:
command: build
toolchain: ${{ matrix.toolchain }}
args: --target ${{ matrix.target }} --all-features
- uses: actions-rs/cargo@v1
name: "Test"
if: matrix.target == 'x86_64-unknown-linux-gnu'
with:
command: test
toolchain: ${{ matrix.toolchain }}
args: --target ${{ matrix.target }} --all-features
# If musl, compile and test all
- uses: actions-rs/cargo@v1
name: "Build"
if: matrix.target == 'x86_64-unknown-linux-musl'
with:
command: build
toolchain: ${{ matrix.toolchain }}
args: --target ${{ matrix.target }} --all-features
env:
CC: musl-gcc
CXX: g++
- uses: actions-rs/cargo@v1
name: "Test"
if: matrix.target == 'x86_64-unknown-linux-musl'
with:
command: test
toolchain: ${{ matrix.toolchain }}
args: --target ${{ matrix.target }} --all-features
env:
CC: musl-gcc
CXX: g++
- uses: actions-rs/cargo@v1
name: "Build"
if: matrix.target == 'wasm32-unknown-unknown'
with:
command: build
toolchain: ${{ matrix.toolchain }}
args: --target wasm32-unknown-unknown --features reqwest
- uses: actions-rs/cargo@v1
name: "Build"
if: matrix.target == 'wasm32-wasi'
with:
command: build
toolchain: ${{ matrix.toolchain }}
args: --target ${{ matrix.target }} --features "http-0-2-binding hyper-0-14 hyper_wasi"
- uses: actions-rs/cargo@v1
name: "Test"
if: matrix.target == 'wasm32-wasi'
with:
command: test
toolchain: ${{ matrix.toolchain }}
args: --target ${{ matrix.target }} --features "http-0-2-binding hyper-0-14 hyper_wasi"
env:
CARGO_TARGET_WASM32_WASI_RUNNER: wasmedge
# Build examples
- uses: actions-rs/cargo@v1
name: "Build wasi-example"
if: matrix.target == 'wasm32-wasi' && matrix.toolchain == 'stable'
with:
command: build
toolchain: ${{ matrix.toolchain }}
args: --target ${{ matrix.target }} --manifest-path ./example-projects/wasi-example/Cargo.toml
- uses: actions-rs/cargo@v1
name: "Build reqwest-wasm-example"
if: matrix.target == 'wasm32-unknown-unknown' && matrix.toolchain == 'stable'
with:
command: build
toolchain: ${{ matrix.toolchain }}
args: --target ${{ matrix.target }} --manifest-path ./example-projects/reqwest-wasm-example/Cargo.toml
- uses: actions-rs/cargo@v1
name: "Build rdkafka-example"
if: matrix.target == 'x86_64-unknown-linux-gnu' && matrix.toolchain == 'stable'
with:
command: build
toolchain: ${{ matrix.toolchain }}
args: --target ${{ matrix.target }} --manifest-path ./example-projects/rdkafka-example/Cargo.toml
- uses: actions-rs/cargo@v1
name: "Build actix-web-example"
if: matrix.target == 'x86_64-unknown-linux-gnu' && matrix.toolchain == 'stable'
with:
command: build
toolchain: ${{ matrix.toolchain }}
args: --target ${{ matrix.target }} --manifest-path ./example-projects/actix-web-example/Cargo.toml
- uses: actions-rs/cargo@v1
name: "Build warp-example"
if: matrix.target == 'x86_64-unknown-linux-gnu' && matrix.toolchain == 'stable'
with:
command: build
toolchain: ${{ matrix.toolchain }}
args: --target ${{ matrix.target }} --manifest-path ./example-projects/warp-example/Cargo.toml
- uses: actions-rs/cargo@v1
name: "Build axum-example"
if: matrix.target == 'x86_64-unknown-linux-gnu' && matrix.toolchain == 'stable'
with:
command: build
toolchain: ${{ matrix.toolchain }}
args: --target ${{ matrix.target }} --manifest-path ./example-projects/axum-example/Cargo.toml
- uses: actions-rs/cargo@v1
name: "Build poem-example"
if: matrix.target == 'x86_64-unknown-linux-gnu' && matrix.toolchain == 'stable'
with:
command: build
toolchain: ${{ matrix.toolchain }}
args: --target ${{ matrix.target }} --manifest-path ./example-projects/poem-example/Cargo.toml
- uses: actions-rs/cargo@v1
name: "Build nats-example"
if: matrix.target == 'x86_64-unknown-linux-gnu' && matrix.toolchain == 'stable'
with:
command: build
toolchain: ${{ matrix.toolchain }}
args: --target ${{ matrix.target }} --manifest-path ./example-projects/nats-example/Cargo.toml

2
.gitignore vendored
View File

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

7
MAINTAINERS.md Normal file
View File

@ -0,0 +1,7 @@
# Maintainers
Current active maintainers of this SDK:
- [Jim Crossley](https://github.com/jcrossley3)
- [Francesco Guardiani](https://github.com/slinkydeveloper)
- [Fabrizio Lazzaretti](https://github.com/Lazzaretti)

6
OWNERS Normal file
View File

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

View File

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

10
RELEASING.md Normal file
View File

@ -0,0 +1,10 @@
# How to create a Release
To create a new release, do the following:
- Bump the version in the README, lib.rs and cargo.toml
- Try to run `cargo test --all-features`, `cargo doc --all-features --lib` and
`cargo publish --dry-run`
- If none of the above commands fail, PR the changes and merge it
- Checkout `main` on your local machine and run `cargo publish`
- Once that is done, create the release in the Github UI (make sure it
creates the git tag as well) and that's it!

View File

@ -1,23 +0,0 @@
[package]
name = "cloudevents-sdk-actix-web"
version = "0.1.0"
authors = ["Francesco Guardiani <francescoguard@gmail.com>"]
license-file = "../LICENSE"
edition = "2018"
description = "CloudEvents official Rust SDK - Actix-Web integration"
documentation = "https://docs.rs/cloudevents-sdk-actix-web"
repository = "https://github.com/cloudevents/sdk-rust"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
cloudevents-sdk = { version = "0.1.0", path = ".." }
actix-web = "2"
actix-rt = "1"
lazy_static = "1.4.0"
bytes = "^0.5"
futures = "^0.3"
serde_json = "^1.0"
[dev-dependencies]
url = { version = "^2.1", features = ["serde"] }

View File

@ -1,70 +0,0 @@
use actix_web::http::header;
use actix_web::http::{HeaderName, HeaderValue};
use cloudevents::event::SpecVersion;
use lazy_static::lazy_static;
use std::collections::HashMap;
use std::str::FromStr;
macro_rules! unwrap_optional_header {
($headers:expr, $name:expr) => {
$headers
.get::<&'static HeaderName>(&$name)
.map(|a| header_value_to_str!(a))
};
}
macro_rules! header_value_to_str {
($header_value:expr) => {
$header_value
.to_str()
.map_err(|e| cloudevents::message::Error::Other {
source: Box::new(e),
})
};
}
macro_rules! str_to_header_value {
($header_value:expr) => {
HeaderValue::from_str($header_value).map_err(|e| cloudevents::message::Error::Other {
source: Box::new(e),
})
};
}
macro_rules! str_name_to_header {
($attribute:expr) => {
HeaderName::from_str($attribute).map_err(|e| cloudevents::message::Error::Other {
source: Box::new(e),
})
};
}
macro_rules! attribute_name_to_header {
($attribute:expr) => {
str_name_to_header!(&["ce-", $attribute].concat())
};
}
fn attributes_to_headers(
map: &HashMap<SpecVersion, &'static [&'static str]>,
) -> HashMap<&'static str, HeaderName> {
map.values()
.flat_map(|s| s.iter())
.map(|s| {
if *s == "datacontenttype" {
(*s, header::CONTENT_TYPE)
} else {
(*s, attribute_name_to_header!(s).unwrap())
}
})
.collect()
}
lazy_static! {
pub(crate) static ref ATTRIBUTES_TO_HEADERS: HashMap<&'static str, HeaderName> =
attributes_to_headers(&cloudevents::event::SPEC_VERSION_ATTRIBUTES);
pub(crate) static ref SPEC_VERSION_HEADER: HeaderName =
HeaderName::from_static("ce-specversion");
pub(crate) static ref CLOUDEVENTS_JSON_HEADER: HeaderValue =
HeaderValue::from_static("application/cloudevents+json");
}

View File

@ -1,9 +0,0 @@
#[macro_use]
mod headers;
mod server_request;
mod server_response;
pub use server_request::request_to_event;
pub use server_request::HttpRequestDeserializer;
pub use server_response::event_to_response;
pub use server_response::HttpResponseSerializer;

View File

@ -1,172 +0,0 @@
use super::headers;
use actix_web::http::HeaderName;
use actix_web::web::{Bytes, BytesMut};
use actix_web::{web, HttpMessage, HttpRequest};
use cloudevents::event::SpecVersion;
use cloudevents::message::{
BinaryDeserializer, BinarySerializer, Encoding, MessageAttributeValue, MessageDeserializer,
Result, StructuredDeserializer, StructuredSerializer,
};
use cloudevents::{message, Event};
use futures::StreamExt;
use std::convert::TryFrom;
/// Wrapper for [`HttpRequest`] that implements [`MessageDeserializer`] trait
pub struct HttpRequestDeserializer<'a> {
req: &'a HttpRequest,
body: Bytes,
}
impl HttpRequestDeserializer<'_> {
pub fn new(req: &HttpRequest, body: Bytes) -> HttpRequestDeserializer {
HttpRequestDeserializer { req, body }
}
}
impl<'a> BinaryDeserializer for HttpRequestDeserializer<'a> {
fn deserialize_binary<R: Sized, V: BinarySerializer<R>>(self, mut visitor: V) -> Result<R> {
if self.encoding() != Encoding::BINARY {
return Err(message::Error::WrongEncoding {});
}
let spec_version = SpecVersion::try_from(
unwrap_optional_header!(self.req.headers(), headers::SPEC_VERSION_HEADER).unwrap()?,
)?;
visitor = visitor.set_spec_version(spec_version.clone())?;
let attributes = cloudevents::event::SPEC_VERSION_ATTRIBUTES
.get(&spec_version)
.unwrap();
for (hn, hv) in
self.req.headers().iter().filter(|(hn, _)| {
headers::SPEC_VERSION_HEADER.ne(hn) && hn.as_str().starts_with("ce-")
})
{
let name = &hn.as_str()["ce-".len()..];
if attributes.contains(&name) {
visitor = visitor.set_attribute(
name,
MessageAttributeValue::String(String::from(header_value_to_str!(hv)?)),
)?
} else {
visitor = visitor.set_extension(
name,
MessageAttributeValue::String(String::from(header_value_to_str!(hv)?)),
)?
}
}
if let Some(hv) = self.req.headers().get("content-type") {
visitor = visitor.set_attribute(
"datacontenttype",
MessageAttributeValue::String(String::from(header_value_to_str!(hv)?)),
)?
}
if self.body.len() != 0 {
visitor.end_with_data(self.body.to_vec())
} else {
visitor.end()
}
}
}
impl<'a> StructuredDeserializer for HttpRequestDeserializer<'a> {
fn deserialize_structured<R: Sized, V: StructuredSerializer<R>>(self, visitor: V) -> Result<R> {
if self.encoding() != Encoding::STRUCTURED {
return Err(message::Error::WrongEncoding {});
}
visitor.set_structured_event(self.body.to_vec())
}
}
impl<'a> MessageDeserializer for HttpRequestDeserializer<'a> {
fn encoding(&self) -> Encoding {
if self.req.content_type() == "application/cloudevents+json" {
Encoding::STRUCTURED
} else if self
.req
.headers()
.get::<&'static HeaderName>(&super::headers::SPEC_VERSION_HEADER)
.is_some()
{
Encoding::BINARY
} else {
Encoding::UNKNOWN
}
}
}
/// Method to transform an incoming [`HttpRequest`] to [`Event`]
pub async fn request_to_event(
req: &HttpRequest,
mut payload: web::Payload,
) -> std::result::Result<Event, actix_web::error::Error> {
let mut bytes = BytesMut::new();
while let Some(item) = payload.next().await {
bytes.extend_from_slice(&item?);
}
MessageDeserializer::into_event(HttpRequestDeserializer::new(req, bytes.freeze()))
.map_err(actix_web::error::ErrorBadRequest)
}
#[cfg(test)]
mod tests {
use super::*;
use actix_web::test;
use url::Url;
use cloudevents::EventBuilder;
use serde_json::json;
use std::str::FromStr;
#[actix_rt::test]
async fn test_request() {
let expected = EventBuilder::new()
.id("0001")
.ty("example.test")
.source(Url::from_str("http://localhost").unwrap())
.extension("someint", "10")
.build();
let (req, payload) = test::TestRequest::post()
.header("ce-specversion", "1.0")
.header("ce-id", "0001")
.header("ce-type", "example.test")
.header("ce-source", "http://localhost")
.header("ce-someint", "10")
.to_http_parts();
let resp = request_to_event(&req, web::Payload(payload)).await.unwrap();
assert_eq!(expected, resp);
}
#[actix_rt::test]
async fn test_request_with_full_data() {
let j = json!({"hello": "world"});
let expected = EventBuilder::new()
.id("0001")
.ty("example.test")
.source(Url::from_str("http://localhost").unwrap())
.data("application/json", j.clone())
.extension("someint", "10")
.build();
let (req, payload) = test::TestRequest::post()
.header("ce-specversion", "1.0")
.header("ce-id", "0001")
.header("ce-type", "example.test")
.header("ce-source", "http://localhost")
.header("ce-someint", "10")
.header("content-type", "application/json")
.set_json(&j)
.to_http_parts();
let resp = request_to_event(&req, web::Payload(payload)).await.unwrap();
assert_eq!(expected, resp);
}
}

View File

@ -1,183 +0,0 @@
use super::headers;
use actix_web::dev::HttpResponseBuilder;
use actix_web::http::{HeaderName, HeaderValue};
use actix_web::HttpResponse;
use cloudevents::event::SpecVersion;
use cloudevents::message::{
BinaryDeserializer, BinarySerializer, MessageAttributeValue, Result, StructuredSerializer,
};
use cloudevents::Event;
use std::str::FromStr;
/// Wrapper for [`HttpResponseBuilder`] that implements [`StructuredSerializer`] and [`BinarySerializer`]
pub struct HttpResponseSerializer {
builder: HttpResponseBuilder,
}
impl HttpResponseSerializer {
pub fn new(builder: HttpResponseBuilder) -> HttpResponseSerializer {
HttpResponseSerializer { builder }
}
}
impl BinarySerializer<HttpResponse> for HttpResponseSerializer {
fn set_spec_version(mut self, spec_version: SpecVersion) -> Result<Self> {
self.builder.set_header(
headers::SPEC_VERSION_HEADER.clone(),
str_to_header_value!(spec_version.as_str())?,
);
Ok(self)
}
fn set_attribute(mut self, name: &str, value: MessageAttributeValue) -> Result<Self> {
self.builder.set_header(
headers::ATTRIBUTES_TO_HEADERS.get(name).unwrap().clone(),
str_to_header_value!(value.to_string().as_str())?,
);
Ok(self)
}
fn set_extension(mut self, name: &str, value: MessageAttributeValue) -> Result<Self> {
self.builder.set_header(
attribute_name_to_header!(name)?,
str_to_header_value!(value.to_string().as_str())?,
);
Ok(self)
}
fn end_with_data(mut self, bytes: Vec<u8>) -> Result<HttpResponse> {
Ok(self.builder.body(bytes))
}
fn end(mut self) -> Result<HttpResponse> {
Ok(self.builder.finish())
}
}
impl StructuredSerializer<HttpResponse> for HttpResponseSerializer {
fn set_structured_event(mut self, bytes: Vec<u8>) -> Result<HttpResponse> {
Ok(self
.builder
.set_header(
actix_web::http::header::CONTENT_TYPE,
headers::CLOUDEVENTS_JSON_HEADER.clone(),
)
.body(bytes))
}
}
/// Method to fill an [`HttpResponseBuilder`] with an [`Event`]
pub async fn event_to_response(
event: Event,
response: HttpResponseBuilder,
) -> std::result::Result<HttpResponse, actix_web::error::Error> {
BinaryDeserializer::deserialize_binary(event, HttpResponseSerializer::new(response))
.map_err(actix_web::error::ErrorBadRequest)
}
#[cfg(test)]
mod tests {
use super::*;
use url::Url;
use actix_web::http::StatusCode;
use actix_web::test;
use cloudevents::EventBuilder;
use futures::TryStreamExt;
use serde_json::json;
use std::str::FromStr;
#[actix_rt::test]
async fn test_response() {
let input = EventBuilder::new()
.id("0001")
.ty("example.test")
.source(Url::from_str("http://localhost/").unwrap())
.extension("someint", "10")
.build();
let resp = event_to_response(input, HttpResponseBuilder::new(StatusCode::OK))
.await
.unwrap();
assert_eq!(
resp.headers()
.get("ce-specversion")
.unwrap()
.to_str()
.unwrap(),
"1.0"
);
assert_eq!(
resp.headers().get("ce-id").unwrap().to_str().unwrap(),
"0001"
);
assert_eq!(
resp.headers().get("ce-type").unwrap().to_str().unwrap(),
"example.test"
);
assert_eq!(
resp.headers().get("ce-source").unwrap().to_str().unwrap(),
"http://localhost/"
);
assert_eq!(
resp.headers().get("ce-someint").unwrap().to_str().unwrap(),
"10"
);
}
#[actix_rt::test]
async fn test_response_with_full_data() {
let j = json!({"hello": "world"});
let input = EventBuilder::new()
.id("0001")
.ty("example.test")
.source(Url::from_str("http://localhost").unwrap())
.data("application/json", j.clone())
.extension("someint", "10")
.build();
let mut resp = event_to_response(input, HttpResponseBuilder::new(StatusCode::OK))
.await
.unwrap();
assert_eq!(
resp.headers()
.get("ce-specversion")
.unwrap()
.to_str()
.unwrap(),
"1.0"
);
assert_eq!(
resp.headers().get("ce-id").unwrap().to_str().unwrap(),
"0001"
);
assert_eq!(
resp.headers().get("ce-type").unwrap().to_str().unwrap(),
"example.test"
);
assert_eq!(
resp.headers().get("ce-source").unwrap().to_str().unwrap(),
"http://localhost/"
);
assert_eq!(
resp.headers()
.get("content-type")
.unwrap()
.to_str()
.unwrap(),
"application/json"
);
assert_eq!(
resp.headers().get("ce-someint").unwrap().to_str().unwrap(),
"10"
);
let bytes = test::load_stream(resp.take_body().into_stream())
.await
.unwrap();
assert_eq!(j.to_string().as_bytes(), bytes.as_ref())
}
}

View File

@ -1,27 +0,0 @@
[package]
name = "cloudevents-sdk-reqwest"
version = "0.1.0"
authors = ["Francesco Guardiani <francescoguard@gmail.com>"]
license-file = "../LICENSE"
edition = "2018"
description = "CloudEvents official Rust SDK - Reqwest integration"
documentation = "https://docs.rs/cloudevents-sdk-reqwest"
repository = "https://github.com/cloudevents/sdk-rust"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
cloudevents-sdk = { version = "0.1.0", path = ".." }
lazy_static = "1.4.0"
bytes = "^0.5"
serde_json = "^1.0"
[dependencies.reqwest]
version = "0.10.4"
default-features = false
features = ["rustls-tls"]
[dev-dependencies]
mockito = "0.25.1"
tokio = { version = "^0.2", features = ["full"] }
url = { version = "^2.1" }

View File

@ -1,172 +0,0 @@
use super::headers;
use cloudevents::event::SpecVersion;
use cloudevents::message::{
BinaryDeserializer, BinarySerializer, MessageAttributeValue, Result, StructuredSerializer,
};
use cloudevents::Event;
use reqwest::RequestBuilder;
use std::str::FromStr;
/// Wrapper for [`RequestBuilder`] that implements [`StructuredSerializer`] & [`BinarySerializer`] traits
pub struct RequestSerializer {
req: RequestBuilder,
}
impl RequestSerializer {
pub fn new(req: RequestBuilder) -> RequestSerializer {
RequestSerializer { req }
}
}
impl BinarySerializer<RequestBuilder> for RequestSerializer {
fn set_spec_version(mut self, spec_version: SpecVersion) -> Result<Self> {
self.req = self
.req
.header(headers::SPEC_VERSION_HEADER.clone(), spec_version.as_str());
Ok(self)
}
fn set_attribute(mut self, name: &str, value: MessageAttributeValue) -> Result<Self> {
self.req = self.req.header(
headers::ATTRIBUTES_TO_HEADERS.get(name).unwrap().clone(),
value.to_string(),
);
Ok(self)
}
fn set_extension(mut self, name: &str, value: MessageAttributeValue) -> Result<Self> {
self.req = self
.req
.header(attribute_name_to_header!(name)?, value.to_string());
Ok(self)
}
fn end_with_data(self, bytes: Vec<u8>) -> Result<RequestBuilder> {
Ok(self.req.body(bytes))
}
fn end(self) -> Result<RequestBuilder> {
Ok(self.req)
}
}
impl StructuredSerializer<RequestBuilder> for RequestSerializer {
fn set_structured_event(self, bytes: Vec<u8>) -> Result<RequestBuilder> {
Ok(self
.req
.header(
reqwest::header::CONTENT_TYPE,
headers::CLOUDEVENTS_JSON_HEADER.clone(),
)
.body(bytes))
}
}
/// Method to fill a [`RequestBuilder`] with an [`Event`]
pub fn event_to_request(event: Event, request_builder: RequestBuilder) -> Result<RequestBuilder> {
BinaryDeserializer::deserialize_binary(event, RequestSerializer::new(request_builder))
}
#[cfg(test)]
mod tests {
use super::*;
use mockito::{mock, Matcher};
use cloudevents::message::StructuredDeserializer;
use cloudevents::EventBuilder;
use serde_json::json;
use url::Url;
#[tokio::test]
async fn test_request() {
let url = mockito::server_url();
let m = mock("POST", "/")
.match_header("ce-specversion", "1.0")
.match_header("ce-id", "0001")
.match_header("ce-type", "example.test")
.match_header("ce-source", "http://localhost/")
.match_header("ce-someint", "10")
.match_body(Matcher::Missing)
.create();
let input = EventBuilder::new()
.id("0001")
.ty("example.test")
.source(Url::from_str("http://localhost/").unwrap())
.extension("someint", "10")
.build();
let client = reqwest::Client::new();
event_to_request(input, client.post(&url))
.unwrap()
.send()
.await
.unwrap();
m.assert();
}
#[tokio::test]
async fn test_request_with_full_data() {
let j = json!({"hello": "world"});
let url = mockito::server_url();
let m = mock("POST", "/")
.match_header("ce-specversion", "1.0")
.match_header("ce-id", "0001")
.match_header("ce-type", "example.test")
.match_header("ce-source", "http://localhost/")
.match_header("content-type", "application/json")
.match_header("ce-someint", "10")
.match_body(Matcher::Exact(j.to_string()))
.create();
let input = EventBuilder::new()
.id("0001")
.ty("example.test")
.source(Url::from_str("http://localhost").unwrap())
.data("application/json", j.clone())
.extension("someint", "10")
.build();
let client = reqwest::Client::new();
event_to_request(input, client.post(&url))
.unwrap()
.send()
.await
.unwrap();
m.assert();
}
#[tokio::test]
async fn test_structured_request_with_full_data() {
let j = json!({"hello": "world"});
let input = EventBuilder::new()
.id("0001")
.ty("example.test")
.source(Url::from_str("http://localhost").unwrap())
.data("application/json", j.clone())
.extension("someint", "10")
.build();
let url = mockito::server_url();
let m = mock("POST", "/")
.match_header("content-type", "application/cloudevents+json")
.match_body(Matcher::Exact(serde_json::to_string(&input).unwrap()))
.create();
let client = reqwest::Client::new();
StructuredDeserializer::deserialize_structured(
input,
RequestSerializer::new(client.post(&url)),
)
.unwrap()
.send()
.await
.unwrap();
m.assert();
}
}

View File

@ -1,206 +0,0 @@
use super::headers;
use bytes::Bytes;
use cloudevents::event::SpecVersion;
use cloudevents::message::{
BinaryDeserializer, BinarySerializer, Encoding, Error, MessageAttributeValue,
MessageDeserializer, Result, StructuredDeserializer, StructuredSerializer,
};
use cloudevents::{message, Event};
use reqwest::header::{HeaderMap, HeaderName};
use reqwest::Response;
use std::convert::TryFrom;
/// Wrapper for [`Response`] that implements [`MessageDeserializer`] trait
pub struct ResponseDeserializer {
headers: HeaderMap,
body: Bytes,
}
impl ResponseDeserializer {
pub fn new(headers: HeaderMap, body: Bytes) -> ResponseDeserializer {
ResponseDeserializer { headers, body }
}
}
impl BinaryDeserializer for ResponseDeserializer {
fn deserialize_binary<R: Sized, V: BinarySerializer<R>>(self, mut visitor: V) -> Result<R> {
if self.encoding() != Encoding::BINARY {
return Err(message::Error::WrongEncoding {});
}
let spec_version = SpecVersion::try_from(
unwrap_optional_header!(self.headers, headers::SPEC_VERSION_HEADER).unwrap()?,
)?;
visitor = visitor.set_spec_version(spec_version.clone())?;
let attributes = cloudevents::event::SPEC_VERSION_ATTRIBUTES
.get(&spec_version)
.unwrap();
for (hn, hv) in self
.headers
.iter()
.filter(|(hn, _)| headers::SPEC_VERSION_HEADER.ne(hn) && hn.as_str().starts_with("ce-"))
{
let name = &hn.as_str()["ce-".len()..];
if attributes.contains(&name) {
visitor = visitor.set_attribute(
name,
MessageAttributeValue::String(String::from(header_value_to_str!(hv)?)),
)?
} else {
visitor = visitor.set_extension(
name,
MessageAttributeValue::String(String::from(header_value_to_str!(hv)?)),
)?
}
}
if let Some(hv) = self.headers.get("content-type") {
visitor = visitor.set_attribute(
"datacontenttype",
MessageAttributeValue::String(String::from(header_value_to_str!(hv)?)),
)?
}
if self.body.len() != 0 {
visitor.end_with_data(self.body.to_vec())
} else {
visitor.end()
}
}
}
impl StructuredDeserializer for ResponseDeserializer {
fn deserialize_structured<R: Sized, V: StructuredSerializer<R>>(self, visitor: V) -> Result<R> {
if self.encoding() != Encoding::STRUCTURED {
return Err(message::Error::WrongEncoding {});
}
visitor.set_structured_event(self.body.to_vec())
}
}
impl MessageDeserializer for ResponseDeserializer {
fn encoding(&self) -> Encoding {
match (
unwrap_optional_header!(self.headers, reqwest::header::CONTENT_TYPE)
.map(|r| r.ok())
.flatten()
.map(|e| e.starts_with("application/cloudevents+json")),
self.headers
.get::<&'static HeaderName>(&headers::SPEC_VERSION_HEADER),
) {
(Some(true), _) => Encoding::STRUCTURED,
(_, Some(_)) => Encoding::BINARY,
_ => Encoding::UNKNOWN,
}
}
}
/// Method to transform an incoming [`Response`] to [`Event`]
pub async fn response_to_event(res: Response) -> Result<Event> {
let h = res.headers().to_owned();
let b = res.bytes().await.map_err(|e| Error::Other {
source: Box::new(e),
})?;
MessageDeserializer::into_event(ResponseDeserializer::new(h, b))
}
#[cfg(test)]
mod tests {
use super::*;
use mockito::mock;
use cloudevents::EventBuilder;
use serde_json::json;
use std::str::FromStr;
use url::Url;
#[tokio::test]
async fn test_response() {
let url = mockito::server_url();
let _m = mock("GET", "/")
.with_status(200)
.with_header("ce-specversion", "1.0")
.with_header("ce-id", "0001")
.with_header("ce-type", "example.test")
.with_header("ce-source", "http://localhost")
.with_header("ce-someint", "10")
.create();
let expected = EventBuilder::new()
.id("0001")
.ty("example.test")
.source(Url::from_str("http://localhost").unwrap())
.extension("someint", "10")
.build();
let client = reqwest::Client::new();
let res = client.get(&url).send().await.unwrap();
let resp = response_to_event(res).await.unwrap();
assert_eq!(expected, resp);
}
#[tokio::test]
async fn test_response_with_full_data() {
let j = json!({"hello": "world"});
let url = mockito::server_url();
let _m = mock("GET", "/")
.with_status(200)
.with_header("ce-specversion", "1.0")
.with_header("ce-id", "0001")
.with_header("ce-type", "example.test")
.with_header("ce-source", "http://localhost/")
.with_header("content-type", "application/json")
.with_header("ce-someint", "10")
.with_body(j.to_string())
.create();
let expected = EventBuilder::new()
.id("0001")
.ty("example.test")
.source(Url::from_str("http://localhost").unwrap())
.data("application/json", j.clone())
.extension("someint", "10")
.build();
let client = reqwest::Client::new();
let res = client.get(&url).send().await.unwrap();
let resp = response_to_event(res).await.unwrap();
assert_eq!(expected, resp);
}
#[tokio::test]
async fn test_structured_response_with_full_data() {
let j = json!({"hello": "world"});
let expected = EventBuilder::new()
.id("0001")
.ty("example.test")
.source(Url::from_str("http://localhost").unwrap())
.data("application/json", j.clone())
.extension("someint", "10")
.build();
let url = mockito::server_url();
let _m = mock("GET", "/")
.with_status(200)
.with_header(
"content-type",
"application/cloudevents+json; charset=utf-8",
)
.with_body(serde_json::to_string(&expected).unwrap())
.create();
let client = reqwest::Client::new();
let res = client.get(&url).send().await.unwrap();
let resp = response_to_event(res).await.unwrap();
assert_eq!(expected, resp);
}
}

View File

@ -1,63 +0,0 @@
use cloudevents::event::SpecVersion;
use lazy_static::lazy_static;
use reqwest::header::{HeaderName, HeaderValue};
use std::collections::HashMap;
use std::str::FromStr;
macro_rules! unwrap_optional_header {
($headers:expr, $name:expr) => {
$headers
.get::<&'static reqwest::header::HeaderName>(&$name)
.map(|a| header_value_to_str!(a))
};
}
macro_rules! header_value_to_str {
($header_value:expr) => {
$header_value
.to_str()
.map_err(|e| cloudevents::message::Error::Other {
source: Box::new(e),
})
};
}
macro_rules! str_name_to_header {
($attribute:expr) => {
reqwest::header::HeaderName::from_str($attribute).map_err(|e| {
cloudevents::message::Error::Other {
source: Box::new(e),
}
})
};
}
macro_rules! attribute_name_to_header {
($attribute:expr) => {
str_name_to_header!(&["ce-", $attribute].concat())
};
}
fn attributes_to_headers(
map: &HashMap<SpecVersion, &'static [&'static str]>,
) -> HashMap<&'static str, HeaderName> {
map.values()
.flat_map(|s| s.iter())
.map(|s| {
if *s == "datacontenttype" {
(*s, reqwest::header::CONTENT_TYPE)
} else {
(*s, attribute_name_to_header!(s).unwrap())
}
})
.collect()
}
lazy_static! {
pub(crate) static ref ATTRIBUTES_TO_HEADERS: HashMap<&'static str, HeaderName> =
attributes_to_headers(&cloudevents::event::SPEC_VERSION_ATTRIBUTES);
pub(crate) static ref SPEC_VERSION_HEADER: HeaderName =
HeaderName::from_static("ce-specversion");
pub(crate) static ref CLOUDEVENTS_JSON_HEADER: HeaderValue =
HeaderValue::from_static("application/cloudevents+json");
}

View File

@ -1,9 +0,0 @@
#[macro_use]
mod headers;
mod client_request;
mod client_response;
pub use client_request::event_to_request;
pub use client_request::RequestSerializer;
pub use client_response::response_to_event;
pub use client_response::ResponseDeserializer;

View File

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

View File

@ -0,0 +1,23 @@
To run the server:
```console
cargo run
```
To test a GET:
```console
curl http://localhost:9000
```
To test a POST:
```console
curl -d '{"hello": "world"}' \
-H'content-type: application/json' \
-H'ce-specversion: 1.0' \
-H'ce-id: 1' \
-H'ce-source: http://cloudevents.io' \
-H'ce-type: dev.knative.example' \
http://localhost:9000
```

View File

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

View File

@ -0,0 +1,21 @@
[package]
name = "axum-example"
version = "0.3.0"
authors = ["Andrew Webber <andrewvwebber@googlemail.com>"]
edition = "2021"
[dependencies]
cloudevents-sdk = { path = "../..", features = ["axum"] }
axum = "^0.8"
http = "^1.1"
tokio = { version = "^1", features = ["full"] }
tracing = "^0.1"
tracing-subscriber = "^0.3"
tower-http = { version = "^0.6", features = ["trace"] }
[dev-dependencies]
tower = { version = "^0.5", features = ["util"] }
serde = { version = "^1.0", features = ["derive"] }
serde_json = "^1.0"
chrono = { version = "^0.4", features = ["serde"] }
hyper = { version = "^1.4" }

View File

@ -0,0 +1,23 @@
To run the server:
```console
cargo run
```
To test a GET:
```console
curl http://localhost:8080
```
To test a POST:
```console
curl -d '{"hello": "world"}' \
-H'content-type: application/json' \
-H'ce-specversion: 1.0' \
-H'ce-id: 1' \
-H'ce-source: http://cloudevents.io' \
-H'ce-type: dev.knative.example' \
http://localhost:8080
```

View File

@ -0,0 +1,108 @@
use axum::{
routing::{get, post},
Router,
};
use cloudevents::Event;
use http::StatusCode;
use tower_http::trace::TraceLayer;
fn echo_app() -> Router {
Router::new()
.route("/", get(|| async { "hello from cloudevents server" }))
.route(
"/",
post(|event: Event| async move {
tracing::debug!("received cloudevent {}", &event);
(StatusCode::OK, event)
}),
)
.layer(TraceLayer::new_for_http())
}
#[tokio::main]
async fn main() {
if std::env::var("RUST_LOG").is_err() {
std::env::set_var("RUST_LOG", "axum_example=debug,tower_http=debug")
}
tracing_subscriber::fmt::init();
let service = echo_app();
let listener = tokio::net::TcpListener::bind("0.0.0.0:8080").await.unwrap();
axum::serve(listener, service).await.unwrap();
}
#[cfg(test)]
mod tests {
use super::echo_app;
use axum::{
body::Body,
http::{self, Request},
};
use chrono::Utc;
use hyper;
use serde_json::json;
use tower::ServiceExt; // for `app.oneshot()`
#[tokio::test]
async fn axum_mod_test() {
if std::env::var("RUST_LOG").is_err() {
std::env::set_var("RUST_LOG", "axum_example=debug,tower_http=debug")
}
tracing_subscriber::fmt::init();
let app = echo_app();
let time = Utc::now();
let j = json!({"hello": "world"});
let request = Request::builder()
.method(http::Method::POST)
.header("ce-specversion", "1.0")
.header("ce-id", "0001")
.header("ce-type", "example.test")
.header("ce-source", "http://localhost/")
.header("ce-someint", "10")
.header("ce-time", time.to_rfc3339())
.header("content-type", "application/json")
.body(Body::from(serde_json::to_vec(&j).unwrap()))
.unwrap();
let resp = app.oneshot(request).await.unwrap();
assert_eq!(
resp.headers()
.get("ce-specversion")
.unwrap()
.to_str()
.unwrap(),
"1.0"
);
assert_eq!(
resp.headers().get("ce-id").unwrap().to_str().unwrap(),
"0001"
);
assert_eq!(
resp.headers().get("ce-type").unwrap().to_str().unwrap(),
"example.test"
);
assert_eq!(
resp.headers().get("ce-source").unwrap().to_str().unwrap(),
"http://localhost/"
);
assert_eq!(
resp.headers()
.get("content-type")
.unwrap()
.to_str()
.unwrap(),
"application/json"
);
assert_eq!(
resp.headers().get("ce-someint").unwrap().to_str().unwrap(),
"10"
);
let (_, body) = resp.into_parts();
let body = hyper::body::to_bytes(body).await.unwrap();
assert_eq!(j.to_string().as_bytes(), body);
}
}

View File

@ -0,0 +1,12 @@
[package]
name = "nats-example"
version = "0.1.0"
authors = ["Jakub Noga <jakub.noga@softchameleon.io>"]
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
cloudevents-sdk = { path = "../..", features = ["nats"] }
serde_json = "^1.0"
nats = "^0.25"

View File

@ -0,0 +1,47 @@
use std::{error::Error, thread};
use cloudevents::binding::nats::{MessageExt, NatsCloudEvent};
use cloudevents::{Event, EventBuilder, EventBuilderV10};
use serde_json::json;
/// First spin up a nats server i.e.
/// ```bash
/// docker run -p 4222:4222 -ti nats:latest
/// ```
fn main() -> Result<(), Box<dyn Error>> {
let nc = nats::connect("localhost:4222").unwrap();
let event = EventBuilderV10::new()
.id("123".to_string())
.ty("example.test")
.source("http://localhost/")
.data("application/json", json!({"hello": "world"}))
.build()
.unwrap();
let n_msg = NatsCloudEvent::from_event(event).unwrap();
let sub = nc.subscribe("test").unwrap();
let t = thread::spawn(move || -> Result<Event, String> {
match sub.next() {
Some(msg) => match msg.to_event() {
Ok(evt) => Ok(evt),
Err(e) => Err(e.to_string()),
},
None => Err("Unsubed or disconnected".to_string()),
}
});
nc.publish("test", n_msg)?;
let maybe_event = t.join().unwrap();
if let Ok(evt) = maybe_event {
println!("{}", evt.to_string());
} else {
println!("{}", maybe_event.unwrap_err().to_string());
}
Ok(())
}

View File

@ -0,0 +1,16 @@
[package]
name = "poem-example"
version = "0.1.0"
edition = "2021"
[dependencies]
cloudevents-sdk = { path = "../..", features = ["poem"] }
tokio = { version = "1.13", features = ["macros", "rt-multi-thread"] }
tracing = "0.1"
poem = { version = "^3.0" }
tracing-subscriber = "0.3"
serde_json = "1.0"
[dev-dependencies]
chrono = { version = "0.4", features = ["serde"] }

View File

@ -0,0 +1,23 @@
To run the server:
```console
cargo run
```
To test a GET:
```console
curl http://localhost:8080
```
To test a POST:
```console
curl -d '{"hello": "world"}' \
-H'content-type: application/json' \
-H'ce-specversion: 1.0' \
-H'ce-id: 1' \
-H'ce-source: http://cloudevents.io' \
-H'ce-type: dev.knative.example' \
http://localhost:8080
```

View File

@ -0,0 +1,121 @@
use cloudevents::{Event, EventBuilder, EventBuilderV10};
use poem::error::InternalServerError;
use poem::listener::TcpListener;
use poem::middleware::Tracing;
use poem::{get, handler, Endpoint, EndpointExt, Response, Result, Route, Server};
use serde_json::json;
#[handler]
async fn get_event() -> Result<Event> {
let event = EventBuilderV10::new()
.id("1")
.source("url://example_response/")
.ty("example.ce")
.data(
"application/json",
json!({
"name": "John Doe",
"age": 43,
"phones": [
"+44 1234567",
"+44 2345678"
]
}),
)
.build()
.map_err(InternalServerError)?;
Ok(event)
}
#[handler]
async fn post_event(event: Event) -> Event {
tracing::debug!("received cloudevent {}", &event);
event
}
fn echo_app() -> impl Endpoint<Output = Response> {
Route::new()
.at("/", get(get_event).post(post_event))
.with(Tracing)
}
#[tokio::main]
async fn main() -> Result<(), std::io::Error> {
if std::env::var("RUST_LOG").is_err() {
std::env::set_var("RUST_LOG", "poem=debug")
}
tracing_subscriber::fmt::init();
let server = Server::new(TcpListener::bind("127.0.0.1:8080"));
server.run(echo_app()).await
}
#[cfg(test)]
mod tests {
use super::*;
use chrono::Utc;
use poem::http::Method;
use poem::{Body, Request};
use serde_json::json;
#[tokio::test]
async fn poem_test() {
if std::env::var("RUST_LOG").is_err() {
std::env::set_var("RUST_LOG", "poem_example=debug")
}
tracing_subscriber::fmt::init();
let app = echo_app();
let time = Utc::now();
let j = json!({"hello": "world"});
let request = Request::builder()
.method(Method::POST)
.header("ce-specversion", "1.0")
.header("ce-id", "0001")
.header("ce-type", "example.test")
.header("ce-source", "http://localhost/")
.header("ce-someint", "10")
.header("ce-time", time.to_rfc3339())
.header("content-type", "application/json")
.body(Body::from_json(&j).unwrap());
let resp: Response = app.call(request).await.unwrap();
assert_eq!(
resp.headers()
.get("ce-specversion")
.unwrap()
.to_str()
.unwrap(),
"1.0"
);
assert_eq!(
resp.headers().get("ce-id").unwrap().to_str().unwrap(),
"0001"
);
assert_eq!(
resp.headers().get("ce-type").unwrap().to_str().unwrap(),
"example.test"
);
assert_eq!(
resp.headers().get("ce-source").unwrap().to_str().unwrap(),
"http://localhost/"
);
assert_eq!(
resp.headers()
.get("content-type")
.unwrap()
.to_str()
.unwrap(),
"application/json"
);
assert_eq!(
resp.headers().get("ce-someint").unwrap().to_str().unwrap(),
"10"
);
assert_eq!(
j.to_string().as_bytes(),
resp.into_body().into_vec().await.unwrap()
);
}
}

View File

@ -0,0 +1,19 @@
[package]
name = "rdkafka-example"
version = "0.4.0"
authors = ["Pranav Bhatt <adpranavb2000@gmail.com>"]
edition = "2018"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
async-trait = "^0.1.33"
cloudevents-sdk = { path = "../..", features = ["rdkafka"] }
lazy_static = "1.4.0"
bytes = "^1.0"
url = { version = "^2.1", features = ["serde"] }
serde_json = "^1.0"
futures = "^0.3"
tokio = { version = "^1.0", features = ["full"] }
clap = "2.33.1"
rdkafka = { version = "^0.37", features = ["cmake-build"] }

View File

@ -0,0 +1,162 @@
use clap::{App, Arg};
use futures::StreamExt;
use serde_json::json;
use cloudevents::{EventBuilder, EventBuilderV10};
use cloudevents::binding::rdkafka::{FutureRecordExt, MessageExt, MessageRecord};
use rdkafka::config::{ClientConfig, RDKafkaLogLevel};
use rdkafka::consumer::stream_consumer::StreamConsumer;
use rdkafka::consumer::{CommitMode, Consumer, DefaultConsumerContext};
use rdkafka::producer::{FutureProducer, FutureRecord};
use std::time::Duration;
// You need a running Kafka cluster to try out this example.
// With docker: docker run --rm --net=host -e ADV_HOST=localhost -e SAMPLEDATA=0 lensesio/fast-data-dev
async fn consume(brokers: &str, group_id: &str, topics: &[&str]) {
let consumer: StreamConsumer<DefaultConsumerContext> = ClientConfig::new()
.set("group.id", group_id)
.set("bootstrap.servers", brokers)
.set("enable.partition.eof", "false")
.set("session.timeout.ms", "6000")
.set("enable.auto.commit", "true")
//.set("statistics.interval.ms", "30000")
//.set("auto.offset.reset", "smallest")
.set_log_level(RDKafkaLogLevel::Debug)
.create_with_context(DefaultConsumerContext)
.expect("Consumer creation failed");
consumer
.subscribe(&topics.to_vec())
.expect("Can't subscribe to specified topics");
// consumer.stream() returns a stream. The stream can be used ot chain together expensive steps,
// such as complex computations on a thread pool or asynchronous IO.
let mut message_stream = consumer.stream();
while let Some(message) = message_stream.next().await {
match message {
Err(e) => println!("Kafka error: {}", e),
Ok(m) => {
let event = m.to_event().unwrap();
println!("Received Event: {:#?}", event);
consumer.commit_message(&m, CommitMode::Async).unwrap();
}
};
}
}
async fn produce(brokers: &str, topic_name: &str) {
let producer: &FutureProducer = &ClientConfig::new()
.set("bootstrap.servers", brokers)
.set("message.timeout.ms", "5000")
.create()
.expect("Producer creation error");
// This loop is non blocking: all messages will be sent one after the other, without waiting
// for the results.
let futures = (0..5)
.map(|i| async move {
// The send operation on the topic returns a future, which will be
// completed once the result or failure from Kafka is received.
let event = EventBuilderV10::new()
.id(i.to_string())
.ty("example.test")
.source("http://localhost/")
.data("application/json", json!({"hello": "world"}))
.build()
.unwrap();
println!("Sending event: {:#?}", event);
let message_record =
MessageRecord::from_event(event).expect("error while serializing the event");
let delivery_status = producer
.send(
FutureRecord::to(topic_name)
.message_record(&message_record)
.key(&format!("Key {}", i)),
Duration::from_secs(10),
)
.await;
// This will be executed when the result is received.
println!("Delivery status for message {} received", i);
delivery_status
})
.collect::<Vec<_>>();
// This loop will wait until all delivery statuses have been received.
for future in futures {
println!("Future completed. Result: {:?}", future.await);
}
}
#[tokio::main]
async fn main() {
let selector = App::new("CloudEvents Kafka Example")
.version(option_env!("CARGO_PKG_VERSION").unwrap_or(""))
.about("select consumer or producer")
.arg(
Arg::with_name("mode")
.long("mode")
.help("enter \"consmer\" or \"producer\"")
.takes_value(true)
.possible_values(&["consumer", "producer"])
.required(true),
)
.arg(
Arg::with_name("topics")
.long("topics")
.help("Topic list")
.takes_value(true)
.multiple(true)
.requires_if("consumer", "mode"),
)
.arg(
Arg::with_name("topic")
.long("topic")
.help("Destination topic")
.takes_value(true)
.requires_if("producer", "mode"),
)
.arg(
Arg::with_name("brokers")
.short("b")
.long("brokers")
.help("Broker list in kafka format")
.takes_value(true)
.default_value("localhost:9092"),
)
.arg(
Arg::with_name("group-id")
.short("g")
.long("group-id")
.help("Consumer group id")
.takes_value(true)
.default_value("example_consumer_group_id"),
)
.get_matches();
match selector.value_of("mode").unwrap() {
"producer" => {
produce(
selector.value_of("brokers").unwrap(),
selector.value_of("topic").unwrap(),
)
.await
}
"consumer" => {
consume(
selector.value_of("brokers").unwrap(),
selector.value_of("group-id").unwrap(),
&selector.values_of("topics").unwrap().collect::<Vec<&str>>(),
)
.await
}
_ => (),
};
}

View File

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

View File

@ -1,13 +1,23 @@
## Example usage of CLoudEvents sdk/Reqwest from WASM
Install the dependencies with:
First, ensure you have [`wasm-pack` installed](https://rustwasm.github.io/wasm-pack/installer/)
Then install the dependencies:
npm install
Then build the example locally with:
And finally run the example:
npm run serve
and then visiting http://localhost:8080 in a browser should run the example!
You should see a form in your browser at http://localhost:8080. When
the form is submitted, a CloudEvent will be sent to the Target URL,
http://localhost:9000 by default, which is the default URL for the
[actix example](../actix-web-example). Fire it up in another terminal
to verify that the data is successfully sent and received.
This example is loosely based off of [this example](https://github.com/rustwasm/wasm-bindgen/blob/master/examples/fetch/src/lib.rs), an example usage of `fetch` from `wasm-bindgen`, and from [reqwest repo](https://github.com/seanmonstar/reqwest/tree/master/examples/wasm_header).
Open the javascript console in the browser to see any helpful error
messages.
This example is loosely based off of [this
example](https://github.com/seanmonstar/reqwest/tree/master/examples/wasm_github_fetch).

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

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

@ -0,0 +1,12 @@
[package]
name = "warp-example"
version = "0.3.0"
authors = ["Marko Milenković <milenkovicm@users.noreply.github.com>"]
edition = "2018"
categories = ["web-programming", "encoding"]
license-file = "../LICENSE"
[dependencies]
cloudevents-sdk = { path = "../..", features = ["warp"] }
warp = "^0.3"
tokio = { version = "^1.0", features = ["full"] }

View File

@ -0,0 +1,17 @@
To run the server:
```console
cargo run
```
To test a POST:
```console
curl -d '{"hello": "world"}' \
-H'content-type: application/json' \
-H'ce-specversion: 1.0' \
-H'ce-id: 1' \
-H'ce-source: http://cloudevents.io' \
-H'ce-type: dev.knative.example' \
http://localhost:3030
```

View File

@ -0,0 +1,13 @@
use cloudevents::binding::warp::{filter, reply};
use warp::Filter;
#[tokio::main]
async fn main() {
let routes = warp::any()
// extracting event from request
.and(filter::to_event())
// returning event back
.map(|event| reply::from_event(event));
warp::serve(routes).run(([127, 0, 0, 1], 3030)).await;
}

View File

@ -0,0 +1,17 @@
[package]
name = "wasi-example"
version = "0.1.0"
edition = "2021"
[dependencies]
anyhow = "1.0"
cloudevents-sdk = { path = "../..", features = ["http-0-2-binding", "hyper_wasi", "hyper-0-14" ] }
hyper_wasi = { version = "0.15", features = ["full"] }
log = "0.4.21"
tokio_wasi = { version = "1", features = ["io-util", "fs", "net", "time", "rt", "macros"] }
serde_json = "^1.0"
[dev-dependencies]
bytes = "1.6.0"
http-body-util = "0.1.1"
chrono = "*"

View File

@ -0,0 +1,26 @@
Install WASMEdge:
https://wasmedge.org/docs/start/install/
To run the server:
```console
cargo run --target wasm32-wasi
```
To test a GET:
```console
curl -sw '%{http_code}\n' http://localhost:9000/health/readiness
```
To test a POST:
```console
curl -d '{"name": "wasi-womble"}' \
-H'content-type: application/json' \
-H'ce-specversion: 1.0' \
-H'ce-id: 1' \
-H'ce-source: http://cloudevents.io' \
-H'ce-type: dev.knative.example' \
http://localhost:9000
```

View File

@ -0,0 +1,39 @@
use cloudevents::{event::Data, Event, EventBuilder, EventBuilderV10};
use log::info;
use serde_json::{from_slice, from_str, json};
pub async fn handle_event(event: Event) -> Result<Event, anyhow::Error> {
info!("event: {}", event);
let input = match event.data() {
Some(Data::Binary(v)) => from_slice(v)?,
Some(Data::String(v)) => from_str(v)?,
Some(Data::Json(v)) => v.to_owned(),
None => json!({ "name": "default" }),
};
EventBuilderV10::from(event)
.source("func://handler")
.ty("func.example")
.data("application/json", json!({ "hello": input["name"] }))
.build()
.map_err(|err| err.into())
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn post_test() -> Result<(), anyhow::Error> {
let reqevt = Event::default();
let respevt = handle_event(reqevt).await?;
let output = match respevt.data() {
Some(Data::Binary(v)) => from_slice(v)?,
Some(Data::String(v)) => from_str(v)?,
Some(Data::Json(v)) => v.to_owned(),
None => json!({ "name": "default" }),
};
assert_eq!(output, json!({ "hello": "default" }));
Ok(())
}
}

View File

@ -0,0 +1,49 @@
use cloudevents::binding::http_0_2::builder::adapter::to_response;
use cloudevents::binding::http_0_2::to_event;
use hyper::service::{make_service_fn, service_fn};
use hyper::Server;
use hyper::{Body, Method, Request, Response, StatusCode};
use std::convert::Infallible;
use std::net::SocketAddr;
use std::result::Result;
mod handler;
#[allow(clippy::redundant_closure)]
#[tokio::main(flavor = "current_thread")]
async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
let addr = SocketAddr::from(([0, 0, 0, 0], 9000));
let make_svc = make_service_fn(|_| async move {
Ok::<_, Infallible>(service_fn(move |req| handle_request(req)))
});
let server = Server::bind(&addr).serve(make_svc);
if let Err(e) = server.await {
eprintln!("server error: {}", e);
}
Ok(())
}
async fn handle_request(
req: Request<Body>,
) -> Result<Response<Body>, anyhow::Error> {
match (req.method(), req.uri().path()) {
(&Method::POST, "/") => {
let headers = req.headers().clone();
let body_bytes = hyper::body::to_bytes(req.into_body()).await?;
let body = body_bytes.to_vec();
let reqevt = to_event(&headers, body)?;
let _respevt = handler::handle_event(reqevt).await?;
to_response(_respevt).map_err(|err| err.into())
}
(&Method::GET, "/health/readiness") => {
Ok(Response::new(Body::from("")))
}
(&Method::GET, "/health/liveness") => Ok(Response::new(Body::from(""))),
_ => {
let mut not_found = Response::default();
*not_found.status_mut() = StatusCode::NOT_FOUND;
Ok(not_found)
}
}
}

83
src/binding/actix/mod.rs Normal file
View File

@ -0,0 +1,83 @@
//! This module integrates the [cloudevents-sdk](https://docs.rs/cloudevents-sdk) with [Actix web](https://docs.rs/actix-web/) to easily send and receive CloudEvents.
//!
//! To deserialize an HTTP request as CloudEvent:
//!
//! ```
//! use cloudevents::Event;
//! use actix_web::post;
//!
//! #[post("/")]
//! async fn post_event(event: Event) -> Result<String, actix_web::Error> {
//! println!("Received Event: {:?}", event);
//! Ok(format!("{:?}", event))
//! }
//! ```
//!
//! For more complex applications, access the Payload directly:
//!
//! ```
//! use cloudevents::binding::actix::HttpRequestExt;
//! use actix_web::{HttpRequest, web, post};
//!
//! #[post("/")]
//! async fn post_event(req: HttpRequest, payload: web::Payload) -> Result<String, actix_web::Error> {
//! let event = req.to_event(payload).await?;
//! println!("Received Event: {:?}", event);
//! Ok(format!("{:?}", event))
//! }
//! ```
//!
//! To serialize a CloudEvent to an HTTP response:
//!
//! ```
//! use actix_web::get;
//! use cloudevents::{Event, EventBuilderV10, EventBuilder};
//! use serde_json::json;
//!
//! #[get("/")]
//! async fn get_event() -> Event {
//! let payload = json!({"hello": "world"});
//!
//! EventBuilderV10::new()
//! .id("0001")
//! .ty("example.test")
//! .source("http://localhost/")
//! .data("application/json", payload)
//! .extension("someint", "10")
//! .build()
//! .unwrap()
//! }
//! ```
//!
//! For more complex applications, use the HTTP response builder extension:
//!
//! ```
//! use cloudevents::binding::actix::HttpResponseBuilderExt;
//! use actix_web::{get, HttpResponse};
//! use cloudevents::{EventBuilderV10, EventBuilder};
//! use serde_json::json;
//!
//! #[get("/")]
//! async fn get_event() -> Result<HttpResponse, actix_web::Error> {
//! HttpResponse::Ok()
//! .event(
//! EventBuilderV10::new()
//! .id("0001")
//! .ty("example.test")
//! .source("http://localhost/")
//! .data("application/json", json!({"hello": "world"}))
//! .build()
//! .expect("No error while building the event"),
//! )
//! }
//! ```
#![deny(rustdoc::broken_intra_doc_links)]
mod server_request;
mod server_response;
pub use server_request::request_to_event;
pub use server_request::HttpRequestExt;
pub use server_response::event_to_response;
pub use server_response::HttpResponseBuilderExt;

View File

@ -0,0 +1,156 @@
use crate::binding::http_0_2::{to_event, Headers};
use crate::Event;
use actix_web::dev::Payload;
use actix_web::web::BytesMut;
use actix_web::{web, HttpRequest};
use async_trait::async_trait;
use futures::{future::LocalBoxFuture, FutureExt, StreamExt};
use http::header::{AsHeaderName, HeaderName, HeaderValue};
use http_0_2 as http;
/// Implement Headers for the actix HeaderMap
impl<'a> Headers<'a> for actix_http::header::HeaderMap {
type Iterator = Box<dyn Iterator<Item = (&'a HeaderName, &'a HeaderValue)> + 'a>;
fn get<K: AsHeaderName>(&self, key: K) -> Option<&HeaderValue> {
self.get(key.as_str())
}
fn iter(&'a self) -> Self::Iterator {
Box::new(self.iter())
}
}
/// Method to transform an incoming [`HttpRequest`] to [`Event`].
pub async fn request_to_event(
req: &HttpRequest,
mut payload: web::Payload,
) -> std::result::Result<Event, actix_web::error::Error> {
let mut bytes = BytesMut::new();
while let Some(item) = payload.next().await {
bytes.extend_from_slice(&item?);
}
to_event(req.headers(), bytes.to_vec()).map_err(actix_web::error::ErrorBadRequest)
}
/// So that an actix-web handler may take an Event parameter
impl actix_web::FromRequest for Event {
type Error = actix_web::Error;
type Future = LocalBoxFuture<'static, std::result::Result<Self, Self::Error>>;
fn from_request(r: &HttpRequest, p: &mut Payload) -> Self::Future {
let request = r.to_owned();
bytes::Bytes::from_request(&request, p)
.map(move |bytes| match bytes {
Ok(b) => to_event(request.headers(), b.to_vec())
.map_err(actix_web::error::ErrorBadRequest),
Err(e) => Err(e),
})
.boxed_local()
}
}
/// Extension Trait for [`HttpRequest`] which acts as a wrapper for the function [`request_to_event()`].
///
/// This trait is sealed and cannot be implemented for types outside of this crate.
#[async_trait(?Send)]
pub trait HttpRequestExt: private::Sealed {
/// Convert this [`HttpRequest`] into an [`Event`].
async fn to_event(
&self,
mut payload: web::Payload,
) -> std::result::Result<Event, actix_web::error::Error>;
}
#[async_trait(?Send)]
impl HttpRequestExt for HttpRequest {
async fn to_event(
&self,
payload: web::Payload,
) -> std::result::Result<Event, actix_web::error::Error> {
request_to_event(self, payload).await
}
}
mod private {
// Sealing the RequestExt
pub trait Sealed {}
impl Sealed for actix_web::HttpRequest {}
}
#[cfg(test)]
mod tests {
use super::*;
use actix_web::{test, FromRequest};
use crate::test::fixtures;
use serde_json::json;
async fn to_event(req: &HttpRequest, mut payload: Payload) -> Event {
web::Payload::from_request(req, &mut payload)
.then(|p| req.to_event(p.unwrap()))
.await
.unwrap()
}
#[actix_rt::test]
async fn test_request() {
let expected = fixtures::v10::minimal_string_extension();
let (req, payload) = test::TestRequest::post()
.insert_header(("ce-specversion", "1.0"))
.insert_header(("ce-id", "0001"))
.insert_header(("ce-type", "test_event.test_application"))
.insert_header(("ce-source", "http://localhost/"))
.insert_header(("ce-someint", "10"))
.to_http_parts();
assert_eq!(expected, to_event(&req, payload).await);
}
#[actix_rt::test]
async fn test_request_with_full_data() {
let expected = fixtures::v10::full_binary_json_data_string_extension();
let (req, payload) = test::TestRequest::post()
.insert_header(("ce-specversion", "1.0"))
.insert_header(("ce-id", "0001"))
.insert_header(("ce-type", "test_event.test_application"))
.insert_header(("ce-subject", "cloudevents-sdk"))
.insert_header(("ce-source", "http://localhost/"))
.insert_header(("ce-time", fixtures::time().to_rfc3339()))
.insert_header(("ce-string_ex", "val"))
.insert_header(("ce-int_ex", "10"))
.insert_header(("ce-bool_ex", "true"))
.insert_header(("content-type", "application/json"))
.set_json(fixtures::json_data())
.to_http_parts();
assert_eq!(expected, to_event(&req, payload).await);
}
#[actix_rt::test]
async fn test_structured_request_with_full_data() {
let payload = json!({
"specversion": "1.0",
"id": "0001",
"type": "test_event.test_application",
"subject": "cloudevents-sdk",
"source": "http://localhost/",
"time": fixtures::time().to_rfc3339(),
"string_ex": "val",
"int_ex": "10",
"bool_ex": "true",
"datacontenttype": "application/json",
"data": fixtures::json_data()
});
let bytes = serde_json::to_string(&payload).expect("Failed to serialize test data to json");
let expected = fixtures::v10::full_json_data_string_extension();
let (req, payload) = test::TestRequest::post()
.insert_header(("content-type", "application/cloudevents+json"))
.set_payload(bytes)
.to_http_parts();
assert_eq!(expected, to_event(&req, payload).await);
}
}

View File

@ -0,0 +1,143 @@
use crate::binding::http_0_2::{Builder, Serializer};
use crate::message::{BinaryDeserializer, Result};
use crate::Event;
use actix_web::http::StatusCode;
use actix_web::{HttpRequest, HttpResponse, HttpResponseBuilder};
use http_0_2 as http;
impl Builder<HttpResponse> for HttpResponseBuilder {
fn header(&mut self, key: &str, value: http::header::HeaderValue) {
self.insert_header((key, value));
}
fn body(&mut self, bytes: Vec<u8>) -> Result<HttpResponse> {
Ok(HttpResponseBuilder::body(self, bytes))
}
fn finish(&mut self) -> Result<HttpResponse> {
Ok(HttpResponseBuilder::finish(self))
}
}
/// Method to fill an [`HttpResponseBuilder`] with an [`Event`].
pub fn event_to_response<T: Builder<HttpResponse> + 'static>(
event: Event,
response: T,
) -> std::result::Result<HttpResponse, actix_web::error::Error> {
BinaryDeserializer::deserialize_binary(event, Serializer::new(response))
.map_err(actix_web::error::ErrorBadRequest)
}
/// So that an actix-web handler may return an Event
impl actix_web::Responder for Event {
type Body = actix_web::body::BoxBody;
fn respond_to(self, _: &HttpRequest) -> HttpResponse {
HttpResponse::build(StatusCode::OK).event(self).unwrap()
}
}
/// Extension Trait for [`HttpResponseBuilder`] which acts as a wrapper for the function [`event_to_response()`].
///
/// This trait is sealed and cannot be implemented for types outside of this crate.
pub trait HttpResponseBuilderExt: private::Sealed {
/// Fill this [`HttpResponseBuilder`] with an [`Event`].
fn event(self, event: Event) -> std::result::Result<HttpResponse, actix_web::Error>;
}
impl HttpResponseBuilderExt for HttpResponseBuilder {
fn event(self, event: Event) -> std::result::Result<HttpResponse, actix_web::Error> {
event_to_response(event, self)
}
}
// Sealing the HttpResponseBuilderExt
mod private {
pub trait Sealed {}
impl Sealed for actix_web::HttpResponseBuilder {}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::test::fixtures;
use actix_web::http::StatusCode;
use actix_web::test;
#[actix_rt::test]
async fn test_response() {
let input = fixtures::v10::minimal_string_extension();
let resp = HttpResponseBuilder::new(StatusCode::OK)
.event(input)
.unwrap();
assert_eq!(
resp.headers()
.get("ce-specversion")
.unwrap()
.to_str()
.unwrap(),
"1.0"
);
assert_eq!(
resp.headers().get("ce-id").unwrap().to_str().unwrap(),
"0001"
);
assert_eq!(
resp.headers().get("ce-type").unwrap().to_str().unwrap(),
"test_event.test_application"
);
assert_eq!(
resp.headers().get("ce-source").unwrap().to_str().unwrap(),
"http://localhost/"
);
assert_eq!(
resp.headers().get("ce-someint").unwrap().to_str().unwrap(),
"10"
);
}
#[actix_rt::test]
async fn test_response_with_full_data() {
let input = fixtures::v10::full_binary_json_data_string_extension();
let resp = HttpResponseBuilder::new(StatusCode::OK)
.event(input)
.unwrap();
assert_eq!(
resp.headers()
.get("ce-specversion")
.unwrap()
.to_str()
.unwrap(),
"1.0"
);
assert_eq!(
resp.headers().get("ce-id").unwrap().to_str().unwrap(),
"0001"
);
assert_eq!(
resp.headers().get("ce-type").unwrap().to_str().unwrap(),
"test_event.test_application"
);
assert_eq!(
resp.headers().get("ce-source").unwrap().to_str().unwrap(),
"http://localhost/"
);
assert_eq!(
resp.headers()
.get("content-type")
.unwrap()
.to_str()
.unwrap(),
"application/json"
);
assert_eq!(
resp.headers().get("ce-int_ex").unwrap().to_str().unwrap(),
"10"
);
let sr = test::TestRequest::default().to_srv_response(resp);
assert_eq!(fixtures::json_data_binary(), test::read_body(sr).await);
}
}

109
src/binding/axum/extract.rs Normal file
View File

@ -0,0 +1,109 @@
use axum::body::Bytes;
use axum::extract::{FromRequest, Request};
use axum::response::Response;
use axum_lib as axum;
use http;
use http::StatusCode;
use crate::binding::http::to_event;
use crate::event::Event;
impl<S> FromRequest<S> for Event
where
Bytes: FromRequest<S>,
S: Send + Sync,
{
type Rejection = Response;
async fn from_request(req: Request, _state: &S) -> Result<Self, Self::Rejection> {
let (parts, body) = req.into_parts();
let body = axum::body::to_bytes(body, usize::MAX).await.map_err(|e| {
Response::builder()
.status(StatusCode::INTERNAL_SERVER_ERROR)
.body(axum::body::Body::from(e.to_string()))
.unwrap()
})?;
to_event(&parts.headers, body.to_vec()).map_err(|e| {
Response::builder()
.status(StatusCode::BAD_REQUEST)
.body(axum::body::Body::from(e.to_string()))
.unwrap()
})
}
}
#[cfg(test)]
mod tests {
use super::*;
use axum::body::Body;
use axum::extract::FromRequest;
use axum::http::{self, Request, StatusCode};
use crate::test::fixtures;
#[tokio::test]
async fn axum_test_request() {
let expected = fixtures::v10::minimal_string_extension();
let request = Request::builder()
.method(http::Method::POST)
.header("ce-specversion", "1.0")
.header("ce-id", "0001")
.header("ce-type", "test_event.test_application")
.header("ce-source", "http://localhost/")
.header("ce-someint", "10")
.body(Body::empty())
.unwrap();
let result = Event::from_request(request, &()).await.unwrap();
assert_eq!(expected, result);
}
#[tokio::test]
async fn axum_test_bad_request() {
let request = Request::builder()
.method(http::Method::POST)
.header("ce-specversion", "BAD SPECIFICATION")
.header("ce-id", "0001")
.header("ce-type", "example.test")
.header("ce-source", "http://localhost/")
.header("ce-someint", "10")
.header("ce-time", fixtures::time().to_rfc3339())
.body(Body::empty())
.unwrap();
let result = Event::from_request(request, &()).await;
assert!(result.is_err());
let rejection = result.unwrap_err();
let reason = rejection.status();
assert_eq!(reason, StatusCode::BAD_REQUEST)
}
#[tokio::test]
async fn axum_test_request_with_full_data() {
let expected = fixtures::v10::full_binary_json_data_string_extension();
let request = Request::builder()
.method(http::Method::POST)
.header("ce-specversion", "1.0")
.header("ce-id", "0001")
.header("ce-type", "test_event.test_application")
.header("ce-source", "http://localhost/")
.header("ce-subject", "cloudevents-sdk")
.header("content-type", "application/json")
.header("ce-string_ex", "val")
.header("ce-int_ex", "10")
.header("ce-bool_ex", "true")
.header("ce-time", &fixtures::time().to_rfc3339())
.body(Body::from(fixtures::json_data_binary()))
.unwrap();
let result = Event::from_request(request, &()).await.unwrap();
assert_eq!(expected, result);
}
}

162
src/binding/axum/mod.rs Normal file
View File

@ -0,0 +1,162 @@
//! This module integrates the [cloudevents-sdk](https://docs.rs/cloudevents-sdk) with [Axum web service framework](https://docs.rs/axum/)
//! to easily send and receive CloudEvents.
//!
//! To deserialize an HTTP request as CloudEvent
//!
//! To echo events:
//!
//! ```
//! use axum_lib as axum;
//! use axum::{
//! routing::{get, post},
//! Router,
//! };
//! use cloudevents::Event;
//! use http::StatusCode;
//!
//! fn app() -> Router {
//! Router::new()
//! .route("/", get(|| async { "hello from cloudevents server" }))
//! .route(
//! "/",
//! post(|event: Event| async move {
//! println!("received cloudevent {}", &event);
//! (StatusCode::OK, event)
//! }),
//! )
//! }
//!
//! ```
//!
//! To create event inside request handlers and send them as responses:
//!
//! ```
//! use axum_lib as axum;
//! use axum::{
//! routing::{get, post},
//! Router,
//! };
//! use cloudevents::{Event, EventBuilder, EventBuilderV10};
//! use http::StatusCode;
//! use serde_json::json;
//!
//! fn app() -> Router {
//! Router::new()
//! .route("/", get(|| async { "hello from cloudevents server" }))
//! .route(
//! "/",
//! post(|| async move {
//! let event = EventBuilderV10::new()
//! .id("1")
//! .source("url://example_response/")
//! .ty("example.ce")
//! .data(
//! mime::APPLICATION_JSON.to_string(),
//! json!({
//! "name": "John Doe",
//! "age": 43,
//! "phones": [
//! "+44 1234567",
//! "+44 2345678"
//! ]
//! }),
//! )
//! .build()
//! .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?;
//!
//! Ok::<Event, (StatusCode, String)>(event)
//! }),
//! )
//! }
//!
//! ```
pub mod extract;
pub mod response;
#[cfg(test)]
mod tests {
use axum_lib as axum;
use axum::{
body::Body,
http::{self, Request, StatusCode},
routing::{get, post},
Router,
};
use chrono::Utc;
use serde_json::json;
use tower::ServiceExt; // for `app.oneshot()`
use crate::Event;
fn echo_app() -> Router {
Router::new()
.route("/", get(|| async { "hello from cloudevents server" }))
.route(
"/",
post(|event: Event| async move {
println!("received cloudevent {}", &event);
(StatusCode::OK, event)
}),
)
}
#[tokio::test]
async fn axum_mod_test() {
let app = echo_app();
let time = Utc::now();
let j = json!({"hello": "world"});
let request = Request::builder()
.method(http::Method::POST)
.header("ce-specversion", "1.0")
.header("ce-id", "0001")
.header("ce-type", "example.test")
.header("ce-source", "http://localhost/")
.header("ce-someint", "10")
.header("ce-time", time.to_rfc3339())
.header("content-type", "application/json")
.body(Body::from(serde_json::to_vec(&j).unwrap()))
.unwrap();
let resp = app.oneshot(request).await.unwrap();
assert_eq!(
resp.headers()
.get("ce-specversion")
.unwrap()
.to_str()
.unwrap(),
"1.0"
);
assert_eq!(
resp.headers().get("ce-id").unwrap().to_str().unwrap(),
"0001"
);
assert_eq!(
resp.headers().get("ce-type").unwrap().to_str().unwrap(),
"example.test"
);
assert_eq!(
resp.headers().get("ce-source").unwrap().to_str().unwrap(),
"http://localhost/"
);
assert_eq!(
resp.headers()
.get("content-type")
.unwrap()
.to_str()
.unwrap(),
"application/json"
);
assert_eq!(
resp.headers().get("ce-someint").unwrap().to_str().unwrap(),
"10"
);
let (_, body) = resp.into_parts();
let body = axum::body::to_bytes(body, usize::MAX).await.unwrap();
assert_eq!(j.to_string().as_bytes(), body);
}
}

View File

@ -0,0 +1,106 @@
use crate::binding::http::builder::adapter::to_response;
use crate::event::Event;
use axum::{body::Body, http::Response, response::IntoResponse};
use axum_lib as axum;
use http;
use http::{header, StatusCode};
impl IntoResponse for Event {
fn into_response(self) -> Response<Body> {
match to_response(self) {
Ok(resp) => {
let (parts, body) = resp.into_parts();
Response::from_parts(parts, Body::new(body))
}
Err(err) => Response::builder()
.status(StatusCode::INTERNAL_SERVER_ERROR)
.header(header::CONTENT_TYPE, "text/plain")
.body(Body::from(err.to_string()))
.unwrap(),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::test::fixtures;
#[test]
fn axum_test_response() {
let input = fixtures::v10::minimal_string_extension();
let resp = input.into_response();
assert_eq!(
resp.headers()
.get("ce-specversion")
.unwrap()
.to_str()
.unwrap(),
"1.0"
);
assert_eq!(
resp.headers().get("ce-id").unwrap().to_str().unwrap(),
"0001"
);
assert_eq!(
resp.headers().get("ce-type").unwrap().to_str().unwrap(),
"test_event.test_application"
);
assert_eq!(
resp.headers().get("ce-source").unwrap().to_str().unwrap(),
"http://localhost/"
);
assert_eq!(
resp.headers().get("ce-someint").unwrap().to_str().unwrap(),
"10"
);
}
#[tokio::test]
async fn axum_test_response_with_full_data() {
let input = fixtures::v10::full_binary_json_data_string_extension();
let resp = input.into_response();
assert_eq!(
resp.headers()
.get("ce-specversion")
.unwrap()
.to_str()
.unwrap(),
"1.0"
);
assert_eq!(
resp.headers().get("ce-id").unwrap().to_str().unwrap(),
"0001"
);
assert_eq!(
resp.headers().get("ce-type").unwrap().to_str().unwrap(),
"test_event.test_application"
);
assert_eq!(
resp.headers().get("ce-source").unwrap().to_str().unwrap(),
"http://localhost/"
);
assert_eq!(
resp.headers()
.get("content-type")
.unwrap()
.to_str()
.unwrap(),
"application/json"
);
assert_eq!(
resp.headers().get("ce-int_ex").unwrap().to_str().unwrap(),
"10"
);
let (_, body) = resp.into_parts();
let body = axum::body::to_bytes(body, usize::MAX).await.unwrap();
assert_eq!(fixtures::json_data_binary(), body);
}
}

View File

@ -0,0 +1,42 @@
use bytes::Bytes;
use http::Response;
use http_body_util::Full;
use std::cell::Cell;
use crate::binding::http::{Builder, Serializer};
use crate::message::{BinaryDeserializer, Error, Result};
use crate::Event;
use std::convert::Infallible;
type BoxBody = http_body_util::combinators::UnsyncBoxBody<Bytes, Infallible>;
struct Adapter {
builder: Cell<http::response::Builder>,
}
impl Builder<Response<BoxBody>> for Adapter {
fn header(&mut self, key: &str, value: http::header::HeaderValue) {
self.builder.set(self.builder.take().header(key, value));
}
fn body(&mut self, bytes: Vec<u8>) -> Result<Response<BoxBody>> {
self.builder
.take()
.body(BoxBody::new(Full::from(bytes)))
.map_err(|e| crate::message::Error::Other {
source: Box::new(e),
})
}
fn finish(&mut self) -> Result<Response<BoxBody>> {
self.body(Vec::new())
}
}
pub fn to_response(event: Event) -> std::result::Result<Response<BoxBody>, Error> {
BinaryDeserializer::deserialize_binary(
event,
Serializer::new(Adapter {
builder: Cell::new(http::Response::builder()),
}),
)
}

View File

@ -0,0 +1,12 @@
#[cfg(feature = "hyper")]
pub mod adapter;
use crate::message::Result;
use http;
pub trait Builder<R> {
fn header(&mut self, key: &str, value: http::header::HeaderValue);
fn body(&mut self, bytes: Vec<u8>) -> Result<R>;
fn finish(&mut self) -> Result<R>;
}

View File

@ -0,0 +1,102 @@
use super::{Headers, SPEC_VERSION_HEADER};
use crate::{
binding::CLOUDEVENTS_JSON_HEADER,
event::SpecVersion,
header_value_to_str, message,
message::{
BinaryDeserializer, BinarySerializer, Encoding, MessageAttributeValue, MessageDeserializer,
Result, StructuredDeserializer, StructuredSerializer,
},
};
use http;
use std::convert::TryFrom;
pub struct Deserializer<'a, T: Headers<'a>> {
headers: &'a T,
body: Vec<u8>,
}
impl<'a, T: Headers<'a>> Deserializer<'a, T> {
pub fn new(headers: &'a T, body: Vec<u8>) -> Deserializer<'a, T> {
Deserializer { headers, body }
}
}
impl<'a, T: Headers<'a>> BinaryDeserializer for Deserializer<'a, T> {
fn deserialize_binary<R: Sized, V: BinarySerializer<R>>(self, mut visitor: V) -> Result<R> {
if self.encoding() != Encoding::BINARY {
return Err(message::Error::WrongEncoding {});
}
let spec_version = SpecVersion::try_from(
self.headers
.get(SPEC_VERSION_HEADER)
.map(|a| header_value_to_str!(a))
.unwrap()?,
)?;
let attributes = spec_version.attribute_names();
visitor = visitor.set_spec_version(spec_version)?;
for (hn, hv) in self.headers.iter().filter(|(hn, _)| {
let key = hn.as_str();
SPEC_VERSION_HEADER.ne(key) && key.starts_with("ce-")
}) {
let name = &hn.as_str()["ce-".len()..];
if attributes.contains(&name) {
visitor = visitor.set_attribute(
name,
MessageAttributeValue::String(String::from(header_value_to_str!(hv)?)),
)?
} else {
visitor = visitor.set_extension(
name,
MessageAttributeValue::String(String::from(header_value_to_str!(hv)?)),
)?
}
}
if let Some(hv) = self.headers.get(http::header::CONTENT_TYPE) {
visitor = visitor.set_attribute(
"datacontenttype",
MessageAttributeValue::String(String::from(header_value_to_str!(hv)?)),
)?
}
if !self.body.is_empty() {
visitor.end_with_data(self.body)
} else {
visitor.end()
}
}
}
impl<'a, T: Headers<'a>> StructuredDeserializer for Deserializer<'a, T> {
fn deserialize_structured<R: Sized, V: StructuredSerializer<R>>(self, visitor: V) -> Result<R> {
if self.encoding() != Encoding::STRUCTURED {
return Err(message::Error::WrongEncoding {});
}
visitor.set_structured_event(self.body)
}
}
impl<'a, T: Headers<'a>> MessageDeserializer for Deserializer<'a, T> {
fn encoding(&self) -> Encoding {
if self
.headers
.get(http::header::CONTENT_TYPE)
.and_then(|v| v.to_str().ok())
.filter(|&v| v.starts_with(CLOUDEVENTS_JSON_HEADER))
.is_some()
{
Encoding::STRUCTURED
} else if self.headers.get(SPEC_VERSION_HEADER).is_some() {
Encoding::BINARY
} else {
Encoding::UNKNOWN
}
}
}

View File

@ -0,0 +1,23 @@
use http::header::{AsHeaderName, HeaderMap, HeaderName, HeaderValue};
use http;
/// Any http library should be able to use the
/// [`to_event`](super::to_event) function with an implementation of
/// this trait.
pub trait Headers<'a> {
type Iterator: Iterator<Item = (&'a HeaderName, &'a HeaderValue)>;
fn get<K: AsHeaderName>(&self, name: K) -> Option<&HeaderValue>;
fn iter(&'a self) -> Self::Iterator;
}
/// Implemention for the HeaderMap used by warp/reqwest
impl<'a> Headers<'a> for HeaderMap<HeaderValue> {
type Iterator = http::header::Iter<'a, HeaderValue>;
fn get<K: AsHeaderName>(&self, name: K) -> Option<&HeaderValue> {
self.get(name)
}
fn iter(&'a self) -> Self::Iterator {
self.iter()
}
}

75
src/binding/http/mod.rs Normal file
View File

@ -0,0 +1,75 @@
pub mod builder;
pub mod deserializer;
mod headers;
use crate::{
message::{Error, MessageDeserializer},
Event,
};
use deserializer::Deserializer;
pub use headers::Headers;
mod serializer;
pub use builder::Builder;
use core::convert::TryFrom;
use http::Response;
use http;
pub use serializer::Serializer;
use std::convert::TryInto;
use std::fmt::Debug;
pub static SPEC_VERSION_HEADER: &str = "ce-specversion";
/// Turn a pile of HTTP headers and a body into a CloudEvent
pub fn to_event<'a, T: Headers<'a>>(
headers: &'a T,
body: Vec<u8>,
) -> std::result::Result<Event, Error> {
MessageDeserializer::into_event(Deserializer::new(headers, body))
}
pub fn header_prefix(name: &str) -> String {
super::header_prefix("ce-", name)
}
impl<T> TryFrom<Response<T>> for Event
where
T: TryInto<Vec<u8>>,
<T as TryInto<Vec<u8>>>::Error: Debug,
{
type Error = crate::message::Error;
fn try_from(response: Response<T>) -> Result<Self, Self::Error> {
let headers = response.headers().to_owned();
let body = T::try_into(response.into_body()).unwrap();
to_event(&headers, body)
}
}
#[cfg(test)]
mod tests {
use crate::test::fixtures;
use crate::Event;
use core::convert::TryFrom;
use http::Response;
use http;
#[test]
fn test_response_to_event() {
let event = fixtures::v10::minimal_string_extension();
let response = Response::builder()
.header("ce-id", fixtures::id())
.header("ce-source", fixtures::source())
.header("ce-type", fixtures::ty())
.header("ce-specversion", "1.0")
.header("ce-someint", "10")
.body(Vec::new())
.unwrap();
assert_eq!(event, Event::try_from(response).unwrap());
}
}

View File

@ -0,0 +1,161 @@
use std::{cell::RefCell, rc::Rc};
use crate::binding::http::builder::Builder;
use crate::binding::{
http::{header_prefix, SPEC_VERSION_HEADER},
CLOUDEVENTS_JSON_HEADER,
};
use crate::event::SpecVersion;
use crate::message::BinaryDeserializer;
use crate::message::{
BinarySerializer, Error, MessageAttributeValue, Result, StructuredSerializer,
};
use crate::Event;
use http::Request;
use http;
use std::convert::TryFrom;
use std::fmt::Debug;
macro_rules! str_to_header_value {
($header_value:expr) => {
http::header::HeaderValue::from_str(&$header_value.to_string()).map_err(|e| {
crate::message::Error::Other {
source: Box::new(e),
}
})
};
}
pub struct Serializer<T> {
builder: Rc<RefCell<dyn Builder<T>>>,
}
impl<T> Serializer<T> {
pub fn new<B: Builder<T> + 'static>(delegate: B) -> Serializer<T> {
let builder = Rc::new(RefCell::new(delegate));
Serializer { builder }
}
}
impl<T> BinarySerializer<T> for Serializer<T> {
fn set_spec_version(self, spec_version: SpecVersion) -> Result<Self> {
self.builder
.borrow_mut()
.header(SPEC_VERSION_HEADER, str_to_header_value!(spec_version)?);
Ok(self)
}
fn set_attribute(self, name: &str, value: MessageAttributeValue) -> Result<Self> {
self.builder
.borrow_mut()
.header(&header_prefix(name), str_to_header_value!(value)?);
Ok(self)
}
fn set_extension(self, name: &str, value: MessageAttributeValue) -> Result<Self> {
self.builder
.borrow_mut()
.header(&header_prefix(name), str_to_header_value!(value)?);
Ok(self)
}
fn end_with_data(self, bytes: Vec<u8>) -> Result<T> {
self.builder.borrow_mut().body(bytes)
}
fn end(self) -> Result<T> {
self.builder.borrow_mut().finish()
}
}
impl<T> StructuredSerializer<T> for Serializer<T> {
fn set_structured_event(self, bytes: Vec<u8>) -> Result<T> {
let mut builder = self.builder.borrow_mut();
builder.header(
http::header::CONTENT_TYPE.as_str(),
http::HeaderValue::from_static(CLOUDEVENTS_JSON_HEADER),
);
builder.body(bytes)
}
}
impl<T> BinarySerializer<http::request::Request<Option<T>>> for http::request::Builder
where
T: TryFrom<Vec<u8>>,
<T as TryFrom<Vec<u8>>>::Error: Debug,
{
fn set_spec_version(mut self, sv: SpecVersion) -> Result<Self> {
self = self.header(SPEC_VERSION_HEADER, &sv.to_string());
Ok(self)
}
fn set_attribute(mut self, name: &str, value: MessageAttributeValue) -> Result<Self> {
let key = &header_prefix(name);
self = self.header(key, &value.to_string());
Ok(self)
}
fn set_extension(mut self, name: &str, value: MessageAttributeValue) -> Result<Self> {
let key = &header_prefix(name);
self = self.header(key, &value.to_string());
Ok(self)
}
fn end_with_data(self, bytes: Vec<u8>) -> Result<http::request::Request<Option<T>>> {
let body = T::try_from(bytes).unwrap();
self.body(Some(body)).map_err(|e| Error::Other {
source: Box::new(e),
})
}
fn end(self) -> Result<http::request::Request<Option<T>>> {
self.body(None).map_err(|e| Error::Other {
source: Box::new(e),
})
}
}
impl<T> TryFrom<Event> for Request<Option<T>>
where
T: TryFrom<Vec<u8>>,
<T as TryFrom<Vec<u8>>>::Error: Debug,
{
type Error = crate::message::Error;
fn try_from(event: Event) -> Result<Self> {
BinaryDeserializer::deserialize_binary(event, http::request::Builder::new())
}
}
#[cfg(test)]
mod tests {
use crate::test::fixtures;
use bytes::Bytes;
use http::Request;
use http;
use std::convert::TryFrom;
#[test]
fn test_event_to_http_request() {
let event = fixtures::v10::minimal_string_extension();
let request: Request<Option<Vec<u8>>> = Request::try_from(event).unwrap();
assert_eq!(request.headers()["ce-id"], "0001");
assert_eq!(request.headers()["ce-type"], "test_event.test_application");
}
#[test]
fn test_event_to_bytes_body() {
let event = fixtures::v10::full_binary_json_data_string_extension();
let request: Request<Option<Vec<u8>>> = Request::try_from(event).unwrap();
assert_eq!(request.headers()["ce-id"], "0001");
assert_eq!(request.headers()["ce-type"], "test_event.test_application");
assert_eq!(
request.body().as_ref().unwrap(),
&Bytes::from(fixtures::json_data().to_string())
);
}
}

View File

@ -0,0 +1,44 @@
use http::Response;
use http_0_2 as http;
use hyper::body::Body;
use std::cell::Cell;
#[cfg(not(target_os = "wasi"))]
use hyper_0_14 as hyper;
#[cfg(target_os = "wasi")]
use hyper;
use crate::binding::http_0_2::{Builder, Serializer};
use crate::message::{BinaryDeserializer, Error, Result};
use crate::Event;
struct Adapter {
builder: Cell<http::response::Builder>,
}
impl Builder<Response<Body>> for Adapter {
fn header(&mut self, key: &str, value: http::header::HeaderValue) {
self.builder.set(self.builder.take().header(key, value));
}
fn body(&mut self, bytes: Vec<u8>) -> Result<Response<Body>> {
self.builder
.take()
.body(Body::from(bytes))
.map_err(|e| crate::message::Error::Other {
source: Box::new(e),
})
}
fn finish(&mut self) -> Result<Response<Body>> {
self.body(Vec::new())
}
}
pub fn to_response(event: Event) -> std::result::Result<Response<Body>, Error> {
BinaryDeserializer::deserialize_binary(
event,
Serializer::new(Adapter {
builder: Cell::new(http::Response::builder()),
}),
)
}

View File

@ -0,0 +1,11 @@
#[cfg(feature = "hyper-0-14")]
pub mod adapter;
use crate::message::Result;
use http_0_2 as http;
pub trait Builder<R> {
fn header(&mut self, key: &str, value: http::header::HeaderValue);
fn body(&mut self, bytes: Vec<u8>) -> Result<R>;
fn finish(&mut self) -> Result<R>;
}

View File

@ -0,0 +1,101 @@
use super::{Headers, SPEC_VERSION_HEADER};
use crate::{
binding::CLOUDEVENTS_JSON_HEADER,
event::SpecVersion,
header_value_to_str, message,
message::{
BinaryDeserializer, BinarySerializer, Encoding, MessageAttributeValue, MessageDeserializer,
Result, StructuredDeserializer, StructuredSerializer,
},
};
use http_0_2 as http;
use std::convert::TryFrom;
pub struct Deserializer<'a, T: Headers<'a>> {
headers: &'a T,
body: Vec<u8>,
}
impl<'a, T: Headers<'a>> Deserializer<'a, T> {
pub fn new(headers: &'a T, body: Vec<u8>) -> Deserializer<'a, T> {
Deserializer { headers, body }
}
}
impl<'a, T: Headers<'a>> BinaryDeserializer for Deserializer<'a, T> {
fn deserialize_binary<R: Sized, V: BinarySerializer<R>>(self, mut visitor: V) -> Result<R> {
if self.encoding() != Encoding::BINARY {
return Err(message::Error::WrongEncoding {});
}
let spec_version = SpecVersion::try_from(
self.headers
.get(SPEC_VERSION_HEADER)
.map(|a| header_value_to_str!(a))
.unwrap()?,
)?;
let attributes = spec_version.attribute_names();
visitor = visitor.set_spec_version(spec_version)?;
for (hn, hv) in self.headers.iter().filter(|(hn, _)| {
let key = hn.as_str();
SPEC_VERSION_HEADER.ne(key) && key.starts_with("ce-")
}) {
let name = &hn.as_str()["ce-".len()..];
if attributes.contains(&name) {
visitor = visitor.set_attribute(
name,
MessageAttributeValue::String(String::from(header_value_to_str!(hv)?)),
)?
} else {
visitor = visitor.set_extension(
name,
MessageAttributeValue::String(String::from(header_value_to_str!(hv)?)),
)?
}
}
if let Some(hv) = self.headers.get(http::header::CONTENT_TYPE) {
visitor = visitor.set_attribute(
"datacontenttype",
MessageAttributeValue::String(String::from(header_value_to_str!(hv)?)),
)?
}
if !self.body.is_empty() {
visitor.end_with_data(self.body)
} else {
visitor.end()
}
}
}
impl<'a, T: Headers<'a>> StructuredDeserializer for Deserializer<'a, T> {
fn deserialize_structured<R: Sized, V: StructuredSerializer<R>>(self, visitor: V) -> Result<R> {
if self.encoding() != Encoding::STRUCTURED {
return Err(message::Error::WrongEncoding {});
}
visitor.set_structured_event(self.body)
}
}
impl<'a, T: Headers<'a>> MessageDeserializer for Deserializer<'a, T> {
fn encoding(&self) -> Encoding {
if self
.headers
.get(http::header::CONTENT_TYPE)
.and_then(|v| v.to_str().ok())
.filter(|&v| v.starts_with(CLOUDEVENTS_JSON_HEADER))
.is_some()
{
Encoding::STRUCTURED
} else if self.headers.get(SPEC_VERSION_HEADER).is_some() {
Encoding::BINARY
} else {
Encoding::UNKNOWN
}
}
}

View File

@ -0,0 +1,22 @@
use http::header::{AsHeaderName, HeaderMap, HeaderName, HeaderValue};
use http_0_2 as http;
/// Any http library should be able to use the
/// [`to_event`](super::to_event) function with an implementation of
/// this trait.
pub trait Headers<'a> {
type Iterator: Iterator<Item = (&'a HeaderName, &'a HeaderValue)>;
fn get<K: AsHeaderName>(&self, name: K) -> Option<&HeaderValue>;
fn iter(&'a self) -> Self::Iterator;
}
/// Implemention for the HeaderMap used by warp/reqwest
impl<'a> Headers<'a> for HeaderMap<HeaderValue> {
type Iterator = http::header::Iter<'a, HeaderValue>;
fn get<K: AsHeaderName>(&self, name: K) -> Option<&HeaderValue> {
self.get(name)
}
fn iter(&'a self) -> Self::Iterator {
self.iter()
}
}

View File

@ -0,0 +1,73 @@
pub mod builder;
pub mod deserializer;
mod headers;
use crate::{
message::{Error, MessageDeserializer},
Event,
};
use deserializer::Deserializer;
pub use headers::Headers;
mod serializer;
pub use builder::Builder;
use core::convert::TryFrom;
use http::Response;
use http_0_2 as http;
pub use serializer::Serializer;
use std::convert::TryInto;
use std::fmt::Debug;
pub static SPEC_VERSION_HEADER: &str = "ce-specversion";
/// Turn a pile of HTTP headers and a body into a CloudEvent
pub fn to_event<'a, T: Headers<'a>>(
headers: &'a T,
body: Vec<u8>,
) -> std::result::Result<Event, Error> {
MessageDeserializer::into_event(Deserializer::new(headers, body))
}
pub fn header_prefix(name: &str) -> String {
super::header_prefix("ce-", name)
}
impl<T> TryFrom<Response<T>> for Event
where
T: TryInto<Vec<u8>>,
<T as TryInto<Vec<u8>>>::Error: Debug,
{
type Error = crate::message::Error;
fn try_from(response: Response<T>) -> Result<Self, Self::Error> {
let headers = response.headers().to_owned();
let body = T::try_into(response.into_body()).unwrap();
to_event(&headers, body)
}
}
#[cfg(test)]
mod tests {
use crate::test::fixtures;
use crate::Event;
use core::convert::TryFrom;
use http::Response;
use http_0_2 as http;
#[test]
fn test_response_to_event() {
let event = fixtures::v10::minimal_string_extension();
let response = Response::builder()
.header("ce-id", fixtures::id())
.header("ce-source", fixtures::source())
.header("ce-type", fixtures::ty())
.header("ce-specversion", "1.0")
.header("ce-someint", "10")
.body(Vec::new())
.unwrap();
assert_eq!(event, Event::try_from(response).unwrap());
}
}

View File

@ -0,0 +1,159 @@
use std::{cell::RefCell, rc::Rc};
use crate::binding::http_0_2::builder::Builder;
use crate::binding::{
http_0_2::{header_prefix, SPEC_VERSION_HEADER},
CLOUDEVENTS_JSON_HEADER,
};
use crate::event::SpecVersion;
use crate::message::BinaryDeserializer;
use crate::message::{
BinarySerializer, Error, MessageAttributeValue, Result, StructuredSerializer,
};
use crate::Event;
use http::Request;
use http_0_2 as http;
use std::convert::TryFrom;
use std::fmt::Debug;
macro_rules! str_to_header_value {
($header_value:expr) => {
http::header::HeaderValue::from_str(&$header_value.to_string()).map_err(|e| {
crate::message::Error::Other {
source: Box::new(e),
}
})
};
}
pub struct Serializer<T> {
builder: Rc<RefCell<dyn Builder<T>>>,
}
impl<T> Serializer<T> {
pub fn new<B: Builder<T> + 'static>(delegate: B) -> Serializer<T> {
let builder = Rc::new(RefCell::new(delegate));
Serializer { builder }
}
}
impl<T> BinarySerializer<T> for Serializer<T> {
fn set_spec_version(self, spec_version: SpecVersion) -> Result<Self> {
self.builder
.borrow_mut()
.header(SPEC_VERSION_HEADER, str_to_header_value!(spec_version)?);
Ok(self)
}
fn set_attribute(self, name: &str, value: MessageAttributeValue) -> Result<Self> {
self.builder
.borrow_mut()
.header(&header_prefix(name), str_to_header_value!(value)?);
Ok(self)
}
fn set_extension(self, name: &str, value: MessageAttributeValue) -> Result<Self> {
self.builder
.borrow_mut()
.header(&header_prefix(name), str_to_header_value!(value)?);
Ok(self)
}
fn end_with_data(self, bytes: Vec<u8>) -> Result<T> {
self.builder.borrow_mut().body(bytes)
}
fn end(self) -> Result<T> {
self.builder.borrow_mut().finish()
}
}
impl<T> StructuredSerializer<T> for Serializer<T> {
fn set_structured_event(self, bytes: Vec<u8>) -> Result<T> {
let mut builder = self.builder.borrow_mut();
builder.header(
http::header::CONTENT_TYPE.as_str(),
http::HeaderValue::from_static(CLOUDEVENTS_JSON_HEADER),
);
builder.body(bytes)
}
}
impl<T> BinarySerializer<http::request::Request<Option<T>>> for http::request::Builder
where
T: TryFrom<Vec<u8>>,
<T as TryFrom<Vec<u8>>>::Error: Debug,
{
fn set_spec_version(mut self, sv: SpecVersion) -> Result<Self> {
self = self.header(SPEC_VERSION_HEADER, &sv.to_string());
Ok(self)
}
fn set_attribute(mut self, name: &str, value: MessageAttributeValue) -> Result<Self> {
let key = &header_prefix(name);
self = self.header(key, &value.to_string());
Ok(self)
}
fn set_extension(mut self, name: &str, value: MessageAttributeValue) -> Result<Self> {
let key = &header_prefix(name);
self = self.header(key, &value.to_string());
Ok(self)
}
fn end_with_data(self, bytes: Vec<u8>) -> Result<http::request::Request<Option<T>>> {
let body = T::try_from(bytes).unwrap();
self.body(Some(body)).map_err(|e| Error::Other {
source: Box::new(e),
})
}
fn end(self) -> Result<http::request::Request<Option<T>>> {
self.body(None).map_err(|e| Error::Other {
source: Box::new(e),
})
}
}
impl<T> TryFrom<Event> for Request<Option<T>>
where
T: TryFrom<Vec<u8>>,
<T as TryFrom<Vec<u8>>>::Error: Debug,
{
type Error = crate::message::Error;
fn try_from(event: Event) -> Result<Self> {
BinaryDeserializer::deserialize_binary(event, http::request::Builder::new())
}
}
#[cfg(test)]
mod tests {
use crate::test::fixtures;
use bytes::Bytes;
use http::Request;
use http_0_2 as http;
use std::convert::TryFrom;
#[test]
fn test_event_to_http_request() {
let event = fixtures::v10::minimal_string_extension();
let request: Request<Option<Vec<u8>>> = Request::try_from(event).unwrap();
assert_eq!(request.headers()["ce-id"], "0001");
assert_eq!(request.headers()["ce-type"], "test_event.test_application");
}
#[test]
fn test_event_to_bytes_body() {
let event = fixtures::v10::full_binary_json_data_string_extension();
let request: Request<Option<Vec<u8>>> = Request::try_from(event).unwrap();
assert_eq!(request.headers()["ce-id"], "0001");
assert_eq!(request.headers()["ce-type"], "test_event.test_application");
assert_eq!(
request.body().as_ref().unwrap(),
&Bytes::from(fixtures::json_data().to_string())
);
}
}

79
src/binding/mod.rs Normal file
View File

@ -0,0 +1,79 @@
//! Provides protocol binding implementations for [`crate::Event`].
#[cfg_attr(docsrs, doc(cfg(feature = "actix")))]
#[cfg(feature = "actix")]
pub mod actix;
#[cfg_attr(docsrs, doc(cfg(feature = "axum")))]
#[cfg(feature = "axum")]
pub mod axum;
#[cfg_attr(
docsrs,
doc(cfg(any(
feature = "http-binding",
feature = "reqwest",
feature = "axum",
feature = "poem"
)))
)]
#[cfg(any(
feature = "http-binding",
feature = "reqwest",
feature = "axum",
feature = "poem"
))]
pub mod http;
#[cfg_attr(
docsrs,
doc(cfg(any(feature = "http-0-2-binding", feature = "actix", feature = "warp",)))
)]
#[cfg(any(feature = "http-0-2-binding", feature = "actix", feature = "warp",))]
pub mod http_0_2;
#[cfg_attr(docsrs, doc(cfg(feature = "nats")))]
#[cfg(feature = "nats")]
pub mod nats;
#[cfg_attr(docsrs, doc(cfg(feature = "poem")))]
#[cfg(feature = "poem")]
pub mod poem;
#[cfg_attr(docsrs, doc(cfg(feature = "rdkafka")))]
#[cfg(feature = "rdkafka")]
pub mod rdkafka;
#[cfg_attr(docsrs, doc(cfg(feature = "reqwest")))]
#[cfg(feature = "reqwest")]
pub mod reqwest;
#[cfg_attr(docsrs, doc(cfg(feature = "warp")))]
#[cfg(feature = "warp")]
pub mod warp;
#[cfg(feature = "rdkafka")]
pub(crate) mod kafka {
pub static SPEC_VERSION_HEADER: &str = "ce_specversion";
pub fn header_prefix(name: &str) -> String {
super::header_prefix("ce_", name)
}
}
pub(crate) static CLOUDEVENTS_JSON_HEADER: &str = "application/cloudevents+json";
pub(crate) static CLOUDEVENTS_BATCH_JSON_HEADER: &str = "application/cloudevents-batch+json";
pub(crate) static CONTENT_TYPE: &str = "content-type";
fn header_prefix(prefix: &str, name: &str) -> String {
if name == "datacontenttype" {
CONTENT_TYPE.to_string()
} else {
[prefix, name].concat()
}
}
#[macro_export]
macro_rules! header_value_to_str {
($header_value:expr) => {
$header_value
.to_str()
.map_err(|e| $crate::message::Error::Other {
source: Box::new(e),
})
};
}

View File

@ -0,0 +1,77 @@
use crate::{
message::{Result, StructuredDeserializer},
Event,
};
use nats_lib as nats;
impl StructuredDeserializer for nats::Message {
fn deserialize_structured<R: Sized, V: crate::message::StructuredSerializer<R>>(
self,
serializer: V,
) -> crate::message::Result<R> {
serializer.set_structured_event(self.data.to_vec())
}
}
/// Trait implemented by [`nats::Message`] to enable convenient deserialization to [`Event`]
///
/// Trait sealed <https://rust-lang.github.io/api-guidelines/future-proofing.html#sealed-traits-protect-against-downstream-implementations-c-sealed>
pub trait MessageExt: private::Sealed {
fn to_event(&self) -> Result<Event>;
}
impl MessageExt for nats::Message {
fn to_event(&self) -> Result<Event> {
StructuredDeserializer::into_event(self.to_owned())
}
}
mod private {
use nats_lib as nats;
// Sealing the MessageExt
pub trait Sealed {}
impl Sealed for nats::Message {}
}
#[cfg(test)]
mod tests {
use crate::test::fixtures;
use nats_lib as nats;
use serde_json::json;
use super::*;
#[test]
fn test_structured_deserialize_v10() {
let expected = fixtures::v10::full_json_data_string_extension();
let nats_message = nats::Message::new(
"not_relevant",
None,
json!(expected).to_string().as_bytes(),
None,
);
let actual = nats_message.to_event().unwrap();
assert_eq!(expected, actual)
}
#[test]
fn test_structured_deserialize_v03() {
let expected = fixtures::v03::full_json_data();
let nats_message = nats::Message::new(
"not_relevant",
None,
json!(expected).to_string().as_bytes(),
None,
);
let actual = nats_message.to_event().unwrap();
assert_eq!(expected, actual)
}
}

43
src/binding/nats/mod.rs Normal file
View File

@ -0,0 +1,43 @@
//! This module provides bindings between [cloudevents-sdk](https://docs.rs/cloudevents-sdk) and [nats](https://docs.rs/nats)
//! ## Examples
//! Deserialize [nats::Message](https://docs.rs/nats/0.21.0/nats/struct.Message.html) into [Event](https://docs.rs/cloudevents-sdk/latest/cloudevents/event/struct.Event.html)
//! ```
//! use nats_lib as nats;
//! use cloudevents::binding::nats::MessageExt;
//!
//! fn consume() {
//! let nc = nats::connect("localhost:4222").unwrap();
//! let sub = nc.subscribe("test").unwrap();
//! let nats_message = sub.next().unwrap();
//! let cloud_event = nats_message.to_event().unwrap();
//!
//! println!("{}", cloud_event.to_string());
//! }
//! ```
//!
//! Serialize [Event](https://docs.rs/cloudevents-sdk/latest/cloudevents/event/struct.Event.html) into [NatsCloudEvent] and publish to nats subject
//! ```
//! use nats_lib as nats;
//! use cloudevents::binding::nats::NatsCloudEvent;
//! use cloudevents::{EventBuilder, EventBuilderV10, Event};
//! use serde_json::json;
//!
//! fn publish() {
//! let nc = nats::connect("localhost:4222").unwrap();
//!
//! let event = EventBuilderV10::new()
//! .id("123".to_string())
//! .ty("example.test")
//! .source("http://localhost/")
//! .data("application/json", json!({"hello": "world"}))
//! .build()
//! .unwrap();
//!
//! nc.publish("whatever.subject.you.like", NatsCloudEvent::from_event(event).unwrap()).unwrap();
//! }
//! ```
mod deserializer;
mod serializer;
pub use deserializer::MessageExt;
pub use serializer::NatsCloudEvent;

View File

@ -0,0 +1,26 @@
use crate::{
message::{Error, Result},
Event,
};
/// Helper struct containing text data bytes of JSON serialized [Event]
///
/// Implements [`AsRef`] so it can be directly passed to [`nats::Connection`](https://docs.rs/nats/0.21.0/nats/struct.Connection.html) methods as payload.
pub struct NatsCloudEvent {
pub payload: Vec<u8>,
}
impl AsRef<[u8]> for NatsCloudEvent {
fn as_ref(&self) -> &[u8] {
self.payload.as_ref()
}
}
impl NatsCloudEvent {
pub fn from_event(event: Event) -> Result<Self> {
match serde_json::to_vec(&event) {
Ok(payload) => Ok(Self { payload }),
Err(e) => Err(Error::SerdeJsonError { source: e }),
}
}
}

View File

@ -0,0 +1,84 @@
use crate::binding::http::to_event;
use crate::Event;
use poem_lib::error::ResponseError;
use poem_lib::http::StatusCode;
use poem_lib::{FromRequest, Request, RequestBody, Result};
impl ResponseError for crate::message::Error {
fn status(&self) -> StatusCode {
StatusCode::BAD_REQUEST
}
}
impl<'a> FromRequest<'a> for Event {
async fn from_request(req: &'a Request, body: &mut RequestBody) -> Result<Self> {
Ok(to_event(req.headers(), body.take()?.into_vec().await?)?)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::test::fixtures;
use poem_lib::http::Method;
#[tokio::test]
async fn test_request() {
let expected = fixtures::v10::minimal_string_extension();
let req = Request::builder()
.method(Method::POST)
.header("ce-specversion", "1.0")
.header("ce-id", "0001")
.header("ce-type", "test_event.test_application")
.header("ce-source", "http://localhost/")
.header("ce-someint", "10")
.finish();
let (req, mut body) = req.split();
let result = Event::from_request(&req, &mut body).await.unwrap();
assert_eq!(expected, result);
}
#[tokio::test]
async fn test_bad_request() {
let req = Request::builder()
.method(Method::POST)
.header("ce-specversion", "BAD SPECIFICATION")
.header("ce-id", "0001")
.header("ce-type", "example.test")
.header("ce-source", "http://localhost/")
.header("ce-someint", "10")
.header("ce-time", fixtures::time().to_rfc3339())
.finish();
let (req, mut body) = req.split();
let resp = Event::from_request(&req, &mut body).await.err().unwrap();
assert_eq!(resp.status(), StatusCode::BAD_REQUEST);
assert_eq!(resp.to_string(), "Invalid specversion BAD SPECIFICATION");
}
#[tokio::test]
async fn test_request_with_full_data() {
let expected = fixtures::v10::full_binary_json_data_string_extension();
let req = Request::builder()
.method(Method::POST)
.header("ce-specversion", "1.0")
.header("ce-id", "0001")
.header("ce-type", "test_event.test_application")
.header("ce-source", "http://localhost/")
.header("ce-subject", "cloudevents-sdk")
.header("content-type", "application/json")
.header("ce-string_ex", "val")
.header("ce-int_ex", "10")
.header("ce-bool_ex", "true")
.header("ce-time", fixtures::time().to_rfc3339())
.body(fixtures::json_data_binary());
let (req, mut body) = req.split();
let result = Event::from_request(&req, &mut body).await.unwrap();
assert_eq!(expected, result);
}
}

57
src/binding/poem/mod.rs Normal file
View File

@ -0,0 +1,57 @@
//! This module integrates the [cloudevents-sdk](https://docs.rs/cloudevents-sdk) with
//! [Poem](https://docs.rs/poem/) to easily send and receive CloudEvents.
//!
//! To deserialize an HTTP request as CloudEvent
//!
//! To echo events:
//!
//! ```rust
//! use cloudevents::Event;
//! use poem_lib as poem;
//! use poem::{handler, Route, post};
//!
//! #[handler]
//! async fn index(event: Event) -> Event {
//! println!("received cloudevent {}", &event);
//! event
//! }
//!
//! let app = Route::new().at("/", post(index));
//! ```
//!
//! To create event inside request handlers and send them as responses:
//!
//! ```rust
//! use cloudevents::{Event, EventBuilder, EventBuilderV10};
//! use poem_lib as poem;
//! use poem::{handler, Route, post, Result};
//! use poem::error::InternalServerError;
//! use serde_json::json;
//!
//! #[handler]
//! async fn index() -> Result<Event> {
//! let event = EventBuilderV10::new()
//! .id("1")
//! .source("url://example_response/")
//! .ty("example.ce")
//! .data(
//! mime::APPLICATION_JSON.to_string(),
//! json!({
//! "name": "John Doe",
//! "age": 43,
//! "phones": [
//! "+44 1234567",
//! "+44 2345678"
//! ]
//! }),
//! )
//! .build()
//! .map_err(InternalServerError)?;
//! Ok(event)
//! }
//!
//! let app = Route::new().at("/", post(index));
//! ```
mod extractor;
mod response;

View File

@ -0,0 +1,114 @@
use crate::{AttributesReader, Data, Event};
use bytes::Bytes;
use poem_lib::http::StatusCode;
use poem_lib::{IntoResponse, Response};
impl IntoResponse for Event {
fn into_response(self) -> Response {
let mut builder = Response::builder().status(StatusCode::OK);
if let Some(dct) = self.datacontenttype() {
builder = builder.content_type(dct);
}
for (key, value) in self.iter() {
builder = builder.header(format!("ce-{key}").as_str(), value.to_string());
}
match self.data {
Some(data) => match data {
Data::Binary(v) => builder.body(Bytes::copy_from_slice(v.as_slice())),
Data::String(s) => builder.body(s.clone()),
Data::Json(j) => match serde_json::to_string(&j) {
Ok(s) => builder.body(s),
Err(e) => Response::builder()
.status(StatusCode::INTERNAL_SERVER_ERROR)
.body(e.to_string()),
},
},
None => builder.finish(),
}
}
}
#[cfg(test)]
mod tests {
use crate::test::fixtures;
use poem_lib::IntoResponse;
#[test]
fn test_response() {
let input = fixtures::v10::minimal_string_extension();
let resp = input.into_response();
assert_eq!(
resp.headers()
.get("ce-specversion")
.unwrap()
.to_str()
.unwrap(),
"1.0"
);
assert_eq!(
resp.headers().get("ce-id").unwrap().to_str().unwrap(),
"0001"
);
assert_eq!(
resp.headers().get("ce-type").unwrap().to_str().unwrap(),
"test_event.test_application"
);
assert_eq!(
resp.headers().get("ce-source").unwrap().to_str().unwrap(),
"http://localhost/"
);
assert_eq!(
resp.headers().get("ce-someint").unwrap().to_str().unwrap(),
"10"
);
}
#[tokio::test]
async fn test_response_with_full_data() {
let input = fixtures::v10::full_binary_json_data_string_extension();
let resp = input.into_response();
assert_eq!(
resp.headers()
.get("ce-specversion")
.unwrap()
.to_str()
.unwrap(),
"1.0"
);
assert_eq!(
resp.headers().get("ce-id").unwrap().to_str().unwrap(),
"0001"
);
assert_eq!(
resp.headers().get("ce-type").unwrap().to_str().unwrap(),
"test_event.test_application"
);
assert_eq!(
resp.headers().get("ce-source").unwrap().to_str().unwrap(),
"http://localhost/"
);
assert_eq!(
resp.headers()
.get("content-type")
.unwrap()
.to_str()
.unwrap(),
"application/json"
);
assert_eq!(
resp.headers().get("ce-int_ex").unwrap().to_str().unwrap(),
"10"
);
let body = resp.into_body().into_vec().await.unwrap();
assert_eq!(fixtures::json_data_binary(), body);
}
}

View File

@ -0,0 +1,234 @@
use rdkafka_lib as rdkafka;
use crate::binding::{kafka::SPEC_VERSION_HEADER, CLOUDEVENTS_JSON_HEADER, CONTENT_TYPE};
use crate::event::SpecVersion;
use crate::message::{
BinaryDeserializer, BinarySerializer, Encoding, MessageAttributeValue, MessageDeserializer,
Result, StructuredDeserializer, StructuredSerializer,
};
use crate::{message, Event};
use rdkafka::message::{BorrowedMessage, Headers, Message, OwnedMessage};
use std::collections::HashMap;
use std::convert::TryFrom;
use std::str;
/// Wrapper for [`Message`] that implements [`MessageDeserializer`] trait.
pub struct ConsumerRecordDeserializer {
pub(crate) headers: HashMap<String, Vec<u8>>,
pub(crate) payload: Option<Vec<u8>>,
}
impl ConsumerRecordDeserializer {
fn get_kafka_headers(message: &impl Message) -> Result<HashMap<String, Vec<u8>>> {
match message.headers() {
None => Err(crate::message::Error::WrongEncoding {}),
Some(headers) => Ok(headers
.iter()
.map(|h| (h.key.to_string(), Vec::from(h.value.unwrap())))
.collect()),
}
}
pub fn new(message: &impl Message) -> Result<ConsumerRecordDeserializer> {
Ok(ConsumerRecordDeserializer {
headers: Self::get_kafka_headers(message)?,
payload: message.payload().map(Vec::from),
})
}
}
impl BinaryDeserializer for ConsumerRecordDeserializer {
fn deserialize_binary<R: Sized, V: BinarySerializer<R>>(mut self, mut visitor: V) -> Result<R> {
if self.encoding() != Encoding::BINARY {
return Err(message::Error::WrongEncoding {});
}
let spec_version = SpecVersion::try_from(
str::from_utf8(&self.headers.remove(SPEC_VERSION_HEADER).unwrap()).map_err(|e| {
crate::message::Error::Other {
source: Box::new(e),
}
})?,
)?;
let attributes = spec_version.attribute_names();
visitor = visitor.set_spec_version(spec_version)?;
if let Some(hv) = self.headers.remove(CONTENT_TYPE) {
visitor = visitor.set_attribute(
"datacontenttype",
MessageAttributeValue::String(String::from_utf8(hv).map_err(|e| {
crate::message::Error::Other {
source: Box::new(e),
}
})?),
)?
}
for (hn, hv) in self
.headers
.into_iter()
.filter(|(hn, _)| SPEC_VERSION_HEADER != *hn && hn.starts_with("ce_"))
{
let name = &hn["ce_".len()..];
if attributes.contains(&name) {
visitor = visitor.set_attribute(
name,
MessageAttributeValue::String(String::from_utf8(hv).map_err(|e| {
crate::message::Error::Other {
source: Box::new(e),
}
})?),
)?
} else {
visitor = visitor.set_extension(
name,
MessageAttributeValue::String(String::from_utf8(hv).map_err(|e| {
crate::message::Error::Other {
source: Box::new(e),
}
})?),
)?
}
}
if self.payload.is_some() {
visitor.end_with_data(self.payload.unwrap())
} else {
visitor.end()
}
}
}
impl StructuredDeserializer for ConsumerRecordDeserializer {
fn deserialize_structured<R: Sized, V: StructuredSerializer<R>>(self, visitor: V) -> Result<R> {
if self.encoding() != Encoding::STRUCTURED {
return Err(message::Error::WrongEncoding {});
}
visitor.set_structured_event(self.payload.unwrap())
}
}
impl MessageDeserializer for ConsumerRecordDeserializer {
fn encoding(&self) -> Encoding {
match (
self.headers
.get("content-type")
.and_then(|s| String::from_utf8(s.to_vec()).ok())
.map(|s| s.starts_with(CLOUDEVENTS_JSON_HEADER))
.unwrap_or(false),
self.headers.get(SPEC_VERSION_HEADER),
) {
(true, _) => Encoding::STRUCTURED,
(_, Some(_)) => Encoding::BINARY,
_ => Encoding::UNKNOWN,
}
}
}
/// Method to transform a [`Message`] to [`Event`].
pub fn record_to_event(msg: &impl Message) -> Result<Event> {
MessageDeserializer::into_event(ConsumerRecordDeserializer::new(msg)?)
}
/// Extension Trait for [`Message`] which acts as a wrapper for the function [`record_to_event()`].
///
/// This trait is sealed and cannot be implemented for types outside of this crate.
pub trait MessageExt: private::Sealed {
/// Generates [`Event`] from [`BorrowedMessage`].
fn to_event(&self) -> Result<Event>;
}
impl MessageExt for BorrowedMessage<'_> {
fn to_event(&self) -> Result<Event> {
record_to_event(self)
}
}
impl MessageExt for OwnedMessage {
fn to_event(&self) -> Result<Event> {
record_to_event(self)
}
}
mod private {
use rdkafka_lib as rdkafka;
// Sealing the MessageExt
pub trait Sealed {}
impl Sealed for rdkafka::message::OwnedMessage {}
impl Sealed for rdkafka::message::BorrowedMessage<'_> {}
}
#[cfg(test)]
mod tests {
use rdkafka_lib as rdkafka;
use super::*;
use crate::binding::rdkafka::kafka_producer_record::MessageRecord;
use crate::test::fixtures;
use crate::{EventBuilder, EventBuilderV10};
#[test]
fn test_binary_record() {
let expected = fixtures::v10::minimal_string_extension();
// Since there is neither a way provided by rust-rdkafka to convert FutureProducer back into
// OwnedMessage or BorrowedMessage, nor is there a way to create a BorrowedMessage struct,
// the test uses OwnedMessage instead, which consumes the message instead of borrowing it like
// in the case of BorrowedMessage
let message_record = MessageRecord::from_event(
EventBuilderV10::new()
.id("0001")
.ty("test_event.test_application")
.source("http://localhost/")
.extension("someint", "10")
.build()
.unwrap(),
)
.unwrap();
let owned_message = OwnedMessage::new(
message_record.payload,
Some(String::from("test key").into_bytes()),
String::from("test topic"),
rdkafka::message::Timestamp::NotAvailable,
10,
10,
Some(message_record.headers),
);
assert_eq!(owned_message.to_event().unwrap(), expected)
}
#[test]
fn test_structured_record() {
let expected = fixtures::v10::full_json_data_string_extension();
// Since there is neither a way provided by rust-rdkafka to convert FutureProducer back into
// OwnedMessage or BorrowedMessage, nor is there a way to create a BorrowedMessage struct,
// the test uses OwnedMessage instead, which consumes the message instead of borrowing it like
// in the case of BorrowedMessage
let input = expected.clone();
let serialized_event =
StructuredDeserializer::deserialize_structured(input, MessageRecord::new()).unwrap();
let owned_message = OwnedMessage::new(
serialized_event.payload,
Some(String::from("test key").into_bytes()),
String::from("test topic"),
rdkafka::message::Timestamp::NotAvailable,
10,
10,
Some(serialized_event.headers),
);
assert_eq!(owned_message.to_event().unwrap(), expected)
}
}

View File

@ -0,0 +1,155 @@
use rdkafka_lib as rdkafka;
use crate::binding::{
kafka::{header_prefix, SPEC_VERSION_HEADER},
CLOUDEVENTS_JSON_HEADER, CONTENT_TYPE,
};
use crate::event::SpecVersion;
use crate::message::{
BinaryDeserializer, BinarySerializer, MessageAttributeValue, Result, StructuredSerializer,
};
use crate::Event;
use rdkafka::message::{Header, OwnedHeaders, ToBytes};
use rdkafka::producer::{BaseRecord, FutureRecord};
/// This struct contains a serialized CloudEvent message in the Kafka shape.
/// Implements [`StructuredSerializer`] & [`BinarySerializer`] traits.
///
/// To instantiate a new `MessageRecord` from an [`Event`],
/// look at [`Self::from_event`] or use [`StructuredDeserializer::deserialize_structured`](crate::message::StructuredDeserializer::deserialize_structured)
/// or [`BinaryDeserializer::deserialize_binary`].
pub struct MessageRecord {
pub(crate) headers: OwnedHeaders,
pub(crate) payload: Option<Vec<u8>>,
}
impl MessageRecord {
/// Create a new empty [`MessageRecord`]
pub fn new() -> Self {
MessageRecord {
headers: OwnedHeaders::new(),
payload: None,
}
}
/// Create a new [`MessageRecord`], filled with `event` serialized in binary mode.
pub fn from_event(event: Event) -> Result<Self> {
BinaryDeserializer::deserialize_binary(event, MessageRecord::new())
}
}
impl Default for MessageRecord {
fn default() -> Self {
Self::new()
}
}
impl BinarySerializer<MessageRecord> for MessageRecord {
fn set_spec_version(mut self, sv: SpecVersion) -> Result<Self> {
let v = sv.to_string();
let header = Header {
key: SPEC_VERSION_HEADER,
value: Some(&v),
};
self.headers = self.headers.insert(header);
Ok(self)
}
fn set_attribute(mut self, name: &str, value: MessageAttributeValue) -> Result<Self> {
let v = value.to_string();
let header = Header {
key: &header_prefix(name),
value: Some(&v),
};
self.headers = self.headers.insert(header);
Ok(self)
}
fn set_extension(self, name: &str, value: MessageAttributeValue) -> Result<Self> {
self.set_attribute(name, value)
}
fn end_with_data(mut self, bytes: Vec<u8>) -> Result<MessageRecord> {
self.payload = Some(bytes);
Ok(self)
}
fn end(self) -> Result<MessageRecord> {
Ok(self)
}
}
impl StructuredSerializer<MessageRecord> for MessageRecord {
fn set_structured_event(mut self, bytes: Vec<u8>) -> Result<MessageRecord> {
let header = Header {
key: CONTENT_TYPE,
value: Some(CLOUDEVENTS_JSON_HEADER),
};
self.headers = self.headers.insert(header);
self.payload = Some(bytes);
Ok(self)
}
}
/// Extension Trait for [`BaseRecord`] that fills the record with a [`MessageRecord`].
///
/// This trait is sealed and cannot be implemented for types outside of this crate.
pub trait BaseRecordExt<'a, K: ToBytes + ?Sized>: private::Sealed {
/// Fill this [`BaseRecord`] with a [`MessageRecord`].
fn message_record(
self,
message_record: &'a MessageRecord,
) -> Result<BaseRecord<'a, K, Vec<u8>>>;
}
impl<'a, K: ToBytes + ?Sized> BaseRecordExt<'a, K> for BaseRecord<'a, K, Vec<u8>> {
fn message_record(
mut self,
message_record: &'a MessageRecord,
) -> Result<BaseRecord<'a, K, Vec<u8>>> {
self = self.headers(message_record.headers.clone());
if let Some(s) = message_record.payload.as_ref() {
self = self.payload(s);
}
Ok(self)
}
}
/// Extension Trait for [`FutureRecord`] that fills the record with a [`MessageRecord`].
///
/// This trait is sealed and cannot be implemented for types outside of this crate.
pub trait FutureRecordExt<'a, K: ToBytes + ?Sized>: private::Sealed {
/// Fill this [`FutureRecord`] with a [`MessageRecord`].
fn message_record(self, message_record: &'a MessageRecord) -> FutureRecord<'a, K, Vec<u8>>;
}
impl<'a, K: ToBytes + ?Sized> FutureRecordExt<'a, K> for FutureRecord<'a, K, Vec<u8>> {
fn message_record(mut self, message_record: &'a MessageRecord) -> FutureRecord<'a, K, Vec<u8>> {
self = self.headers(message_record.headers.clone());
if let Some(s) = message_record.payload.as_ref() {
self = self.payload(s);
}
self
}
}
mod private {
use rdkafka_lib as rdkafka;
// Sealing the FutureRecordExt and BaseRecordExt
pub trait Sealed {}
impl<K: rdkafka::message::ToBytes + ?Sized, V: rdkafka::message::ToBytes> Sealed
for rdkafka::producer::FutureRecord<'_, K, V>
{
}
impl<K: rdkafka::message::ToBytes + ?Sized, V: rdkafka::message::ToBytes> Sealed
for rdkafka::producer::BaseRecord<'_, K, V>
{
}
}

View File

@ -0,0 +1,63 @@
//! This library provides Kafka protocol bindings for CloudEvents
//! using the [rust-rdkafka](https://fede1024.github.io/rust-rdkafka) library.
//!
//! To produce Cloudevents:
//!
//! ```
//! # use rdkafka_lib as rdkafka;
//! use cloudevents::Event;
//! use rdkafka::producer::{FutureProducer, FutureRecord};
//! use rdkafka::util::Timeout;
//! use cloudevents::binding::rdkafka::{MessageRecord, FutureRecordExt};
//!
//! # async fn produce(producer: &FutureProducer, event: Event) -> Result<(), Box<dyn std::error::Error>> {
//! let message_record = MessageRecord::from_event(event)?;
//!
//! producer.send(
//! FutureRecord::to("topic")
//! .key("some_event")
//! .message_record(&message_record),
//! Timeout::Never
//! ).await;
//! # Ok(())
//! # }
//!
//! ```
//!
//! To consume Cloudevents:
//!
//! ```
//! # use rdkafka_lib as rdkafka;
//! use rdkafka::consumer::{StreamConsumer, DefaultConsumerContext, Consumer, CommitMode};
//! use cloudevents::binding::rdkafka::MessageExt;
//! use futures::StreamExt;
//!
//! # async fn consume(consumer: StreamConsumer<DefaultConsumerContext>) -> Result<(), Box<dyn std::error::Error>> {
//! let mut message_stream = consumer.stream();
//!
//! while let Some(message) = message_stream.next().await {
//! match message {
//! Err(e) => println!("Kafka error: {}", e),
//! Ok(m) => {
//! let event = m.to_event()?;
//! println!("Received Event: {}", event);
//! consumer.commit_message(&m, CommitMode::Async)?;
//! }
//! };
//! }
//! # Ok(())
//! # }
//! ```
#![deny(rustdoc::broken_intra_doc_links)]
mod kafka_consumer_record;
mod kafka_producer_record;
pub use kafka_consumer_record::record_to_event;
pub use kafka_consumer_record::ConsumerRecordDeserializer;
pub use kafka_consumer_record::MessageExt;
pub use kafka_producer_record::BaseRecordExt;
pub use kafka_producer_record::FutureRecordExt;
pub use kafka_producer_record::MessageRecord;

View File

@ -0,0 +1,225 @@
use reqwest_lib as reqwest;
use crate::binding::{
http::{header_prefix, SPEC_VERSION_HEADER},
CLOUDEVENTS_BATCH_JSON_HEADER, CLOUDEVENTS_JSON_HEADER,
};
use crate::event::SpecVersion;
use crate::message::{
BinaryDeserializer, BinarySerializer, MessageAttributeValue, Result, StructuredSerializer,
};
use crate::Event;
use reqwest::RequestBuilder;
// TODO: Ideally, we'd only need to implement binding::http::Builder
// for reqwest::RequestBuilder here, but because the latter is a
// consuming builder, we'd need an intermediate struct similar to
// warp's to adapt that interface. Unfortunately, the reqwest builder
// doesn't implement the Default trait, so I can't use take() as
// warp's Adapter does, and I've yet to come up with another
// solution. So for now, we continue to implement BinarySerializer
// directly in here.
/// Wrapper for [`RequestBuilder`] that implements [`StructuredSerializer`] & [`BinarySerializer`] traits.
pub struct RequestSerializer {
req: RequestBuilder,
}
impl RequestSerializer {
pub fn new(req: RequestBuilder) -> RequestSerializer {
RequestSerializer { req }
}
}
impl BinarySerializer<RequestBuilder> for RequestSerializer {
fn set_spec_version(mut self, spec_ver: SpecVersion) -> Result<Self> {
self.req = self.req.header(SPEC_VERSION_HEADER, spec_ver.to_string());
Ok(self)
}
fn set_attribute(mut self, name: &str, value: MessageAttributeValue) -> Result<Self> {
let key = &header_prefix(name);
self.req = self.req.header(key, value.to_string());
Ok(self)
}
fn set_extension(mut self, name: &str, value: MessageAttributeValue) -> Result<Self> {
let key = &header_prefix(name);
self.req = self.req.header(key, value.to_string());
Ok(self)
}
fn end_with_data(self, bytes: Vec<u8>) -> Result<RequestBuilder> {
Ok(self.req.body(bytes))
}
fn end(self) -> Result<RequestBuilder> {
Ok(self.req)
}
}
impl StructuredSerializer<RequestBuilder> for RequestSerializer {
fn set_structured_event(self, bytes: Vec<u8>) -> Result<RequestBuilder> {
Ok(self
.req
.header(reqwest::header::CONTENT_TYPE, CLOUDEVENTS_JSON_HEADER)
.body(bytes))
}
}
/// Method to fill a [`RequestBuilder`] with an [`Event`].
pub fn event_to_request(event: Event, request_builder: RequestBuilder) -> Result<RequestBuilder> {
BinaryDeserializer::deserialize_binary(event, RequestSerializer::new(request_builder))
}
/// Method to fill a [`RequestBuilder`] with a batched [`Vec<Event>`].
pub fn events_to_request(
events: Vec<Event>,
request_builder: RequestBuilder,
) -> Result<RequestBuilder> {
let bytes = serde_json::to_vec(&events)?;
Ok(request_builder
.header(reqwest::header::CONTENT_TYPE, CLOUDEVENTS_BATCH_JSON_HEADER)
.body(bytes))
}
/// Extension Trait for [`RequestBuilder`] which acts as a wrapper for the function [`event_to_request()`].
///
/// This trait is sealed and cannot be implemented for types outside of this crate.
pub trait RequestBuilderExt: private::Sealed {
/// Write in this [`RequestBuilder`] the provided [`Event`]. Similar to invoking [`Event`].
fn event(self, event: Event) -> Result<RequestBuilder>;
/// Write in this [`RequestBuilder`] the provided batched [`Vec<Event>`].
fn events(self, events: Vec<Event>) -> Result<RequestBuilder>;
}
impl RequestBuilderExt for RequestBuilder {
fn event(self, event: Event) -> Result<RequestBuilder> {
event_to_request(event, self)
}
fn events(self, events: Vec<Event>) -> Result<RequestBuilder> {
events_to_request(events, self)
}
}
// Sealing the RequestBuilderExt
mod private {
use reqwest_lib as reqwest;
pub trait Sealed {}
impl Sealed for reqwest::RequestBuilder {}
}
#[cfg(test)]
mod tests {
use super::*;
use mockito::Matcher;
use reqwest_lib as reqwest;
use crate::message::StructuredDeserializer;
use crate::test::fixtures;
#[tokio::test]
async fn test_request() {
let url = mockito::server_url();
let m = mockito::mock("POST", "/")
.match_header("ce-specversion", "1.0")
.match_header("ce-id", "0001")
.match_header("ce-type", "test_event.test_application")
.match_header("ce-source", "http://localhost/")
.match_header("ce-someint", "10")
.match_body(Matcher::Missing)
.create();
let input = fixtures::v10::minimal_string_extension();
let client = reqwest::Client::new();
client
.post(&url)
.event(input)
.unwrap()
.send()
.await
.unwrap();
m.assert();
}
#[tokio::test]
async fn test_request_with_full_data() {
let url = mockito::server_url();
let m = mockito::mock("POST", "/")
.match_header("ce-specversion", "1.0")
.match_header("ce-id", "0001")
.with_header("ce-type", "test_event.test_application")
.with_header("ce-source", "http://localhost/")
.with_header("ce-subject", "cloudevents-sdk")
.with_header("content-type", "application/json")
.with_header("ce-string_ex", "val")
.with_header("ce-int_ex", "10")
.with_header("ce-bool_ex", "true")
.with_header("ce-time", &fixtures::time().to_rfc3339())
.match_body(Matcher::Exact(fixtures::json_data().to_string()))
.create();
let input = fixtures::v10::full_binary_json_data_string_extension();
let client = reqwest::Client::new();
client
.post(&url)
.event(input)
.unwrap()
.send()
.await
.unwrap();
m.assert();
}
#[tokio::test]
async fn test_structured_request_with_full_data() {
let input = fixtures::v10::full_json_data_string_extension();
let url = mockito::server_url();
let m = mockito::mock("POST", "/")
.match_header("content-type", "application/cloudevents+json")
.match_body(Matcher::Exact(serde_json::to_string(&input).unwrap()))
.create();
let client = reqwest::Client::new();
StructuredDeserializer::deserialize_structured(
input,
RequestSerializer::new(client.post(&url)),
)
.unwrap()
.send()
.await
.unwrap();
m.assert();
}
#[tokio::test]
async fn test_batched_request() {
let input = vec![fixtures::v10::full_json_data_string_extension()];
let url = mockito::server_url();
let m = mockito::mock("POST", "/")
.match_header("content-type", "application/cloudevents-batch+json")
.match_body(Matcher::Exact(serde_json::to_string(&input).unwrap()))
.create();
let client = reqwest::Client::new();
client
.post(&url)
.events(input)
.unwrap()
.send()
.await
.unwrap();
m.assert();
}
}

View File

@ -0,0 +1,190 @@
use reqwest_lib as reqwest;
use crate::binding;
use crate::message::{Error, Result};
use crate::Event;
use async_trait::async_trait;
use http;
use http::header;
use reqwest::Response;
/// Method to transform an incoming [`Response`] to [`Event`].
pub async fn response_to_event(res: Response) -> Result<Event> {
let h = res.headers().to_owned();
let b = res.bytes().await.map_err(|e| Error::Other {
source: Box::new(e),
})?;
binding::http::to_event(&h, b.to_vec())
}
/// Method to transform an incoming [`Response`] to a batched [`Vec<Event>`]
pub async fn response_to_events(res: Response) -> Result<Vec<Event>> {
if res
.headers()
.get(header::CONTENT_TYPE)
.and_then(|v| v.to_str().ok())
.filter(|&v| v.starts_with(binding::CLOUDEVENTS_BATCH_JSON_HEADER))
.is_none()
{
return Err(Error::WrongEncoding {});
}
let bytes = res.bytes().await.map_err(|e| Error::Other {
source: Box::new(e),
})?;
Ok(serde_json::from_slice(&bytes)?)
}
/// Extension Trait for [`Response`] which acts as a wrapper for the function [`response_to_event()`].
///
/// This trait is sealed and cannot be implemented for types outside of this crate.
#[async_trait(?Send)]
pub trait ResponseExt: private::Sealed {
/// Convert this [`Response`] to [`Event`].
async fn into_event(self) -> Result<Event>;
/// Convert this [`Response`] to a batched [`Vec<Event>`].
async fn into_events(self) -> Result<Vec<Event>>;
}
#[async_trait(?Send)]
impl ResponseExt for Response {
async fn into_event(self) -> Result<Event> {
response_to_event(self).await
}
async fn into_events(self) -> Result<Vec<Event>> {
response_to_events(self).await
}
}
// Sealing the ResponseExt
mod private {
use reqwest_lib as reqwest;
pub trait Sealed {}
impl Sealed for reqwest::Response {}
}
#[cfg(test)]
mod tests {
use super::*;
use reqwest_lib as reqwest;
use std::vec;
use crate::test::fixtures;
#[tokio::test]
async fn test_response() {
let url = mockito::server_url();
let _m = mockito::mock("GET", "/")
.with_status(200)
.with_header("ce-specversion", "1.0")
.with_header("ce-id", "0001")
.with_header("ce-type", "test_event.test_application")
.with_header("ce-source", "http://localhost/")
.with_header("ce-someint", "10")
.create();
let expected = fixtures::v10::minimal_string_extension();
let client = reqwest::Client::new();
let res = client
.get(&url)
.send()
.await
.unwrap()
.into_event()
.await
.unwrap();
assert_eq!(expected, res);
}
#[tokio::test]
async fn test_response_with_full_data() {
let url = mockito::server_url();
let _m = mockito::mock("GET", "/")
.with_status(200)
.with_header("ce-specversion", "1.0")
.with_header("ce-id", "0001")
.with_header("ce-type", "test_event.test_application")
.with_header("ce-source", "http://localhost/")
.with_header("ce-subject", "cloudevents-sdk")
.with_header("content-type", "application/json")
.with_header("ce-string_ex", "val")
.with_header("ce-int_ex", "10")
.with_header("ce-bool_ex", "true")
.with_header("ce-time", &fixtures::time().to_rfc3339())
.with_body(fixtures::json_data().to_string())
.create();
let expected = fixtures::v10::full_binary_json_data_string_extension();
let client = reqwest::Client::new();
let res = client
.get(&url)
.send()
.await
.unwrap()
.into_event()
.await
.unwrap();
assert_eq!(expected, res);
}
#[tokio::test]
async fn test_structured_response_with_full_data() {
let expected = fixtures::v10::full_json_data_string_extension();
let url = mockito::server_url();
let _m = mockito::mock("GET", "/")
.with_status(200)
.with_header(
"content-type",
"application/cloudevents+json; charset=utf-8",
)
.with_body(serde_json::to_string(&expected).unwrap())
.create();
let client = reqwest::Client::new();
let res = client
.get(&url)
.send()
.await
.unwrap()
.into_event()
.await
.unwrap();
assert_eq!(expected, res);
}
#[tokio::test]
async fn test_batched_response() {
let expected = vec![fixtures::v10::full_json_data_string_extension()];
let url = mockito::server_url();
let _m = mockito::mock("GET", "/")
.with_status(200)
.with_header(
"content-type",
"application/cloudevents-batch+json; charset=utf-8",
)
.with_body(serde_json::to_string(&expected).unwrap())
.create();
let client = reqwest::Client::new();
let res = client
.get(&url)
.send()
.await
.unwrap()
.into_events()
.await
.unwrap();
assert_eq!(expected, res);
}
}

View File

@ -0,0 +1,40 @@
//! This module integrates the [cloudevents-sdk](https://docs.rs/cloudevents-sdk) with [reqwest](https://docs.rs/reqwest/) to easily send and receive CloudEvents.
//!
//! ```
//! # use reqwest_lib as reqwest;
//! use cloudevents::binding::reqwest::{RequestBuilderExt, ResponseExt};
//! use cloudevents::{EventBuilderV10, EventBuilder};
//! use serde_json::json;
//!
//! # async fn example() -> Result<(), Box<dyn std::error::Error>> {
//! let client = reqwest::Client::new();
//!
//! // Prepare the event to send
//! let event_to_send = EventBuilderV10::new()
//! .id("0001")
//! .ty("example.test")
//! .source("http://localhost/")
//! .data("application/json", json!({"hello": "world"}))
//! .build()?;
//!
//! // Send request
//! let response = client.post("http://localhost")
//! .event(event_to_send)?
//! .send().await?;
//! // Parse response as event
//! let received_event = response
//! .into_event().await?;
//! # Ok(())
//! # }
//! ```
#![deny(rustdoc::broken_intra_doc_links)]
mod client_request;
mod client_response;
pub use client_request::event_to_request;
pub use client_request::RequestBuilderExt;
pub use client_request::RequestSerializer;
pub use client_response::response_to_event;
pub use client_response::ResponseExt;

124
src/binding/warp/filter.rs Normal file
View File

@ -0,0 +1,124 @@
use warp_lib as warp;
use crate::binding::http_0_2 as http;
use crate::Event;
use warp::http::HeaderMap;
use warp::Filter;
use warp::Rejection;
#[derive(Debug)]
#[allow(dead_code)]
pub struct EventFilterError {
error: crate::message::Error,
}
impl warp::reject::Reject for EventFilterError {}
///
/// # Extracts [`crate::Event`] from incoming request
///
/// ```
/// # use warp_lib as warp;
/// use cloudevents::binding::warp::filter::to_event;
/// use warp::Filter;
/// use warp::Reply;
///
/// let routes = warp::any()
/// .and(to_event())
/// .map(|event| {
/// // do something with the event
/// }
/// );
/// ```
///
pub fn to_event() -> impl Filter<Extract = (Event,), Error = Rejection> + Copy {
warp::header::headers_cloned()
.and(warp::body::bytes())
.and_then(create_event)
}
async fn create_event(headers: HeaderMap, body: bytes::Bytes) -> Result<Event, Rejection> {
http::to_event(&headers, body.to_vec())
.map_err(|error| warp::reject::custom(EventFilterError { error }))
}
#[cfg(test)]
mod tests {
use super::to_event;
use crate::test::fixtures;
use std::convert::TryInto;
use warp::test;
use warp_lib as warp;
#[tokio::test]
async fn test_request() {
let expected = fixtures::v10::minimal_string_extension();
let result = test::request()
.method("POST")
.header("ce-specversion", "1.0")
.header("ce-id", "0001")
.header("ce-type", "test_event.test_application")
.header("ce-source", "http://localhost/")
.header("ce-someint", "10")
.filter(&to_event())
.await
.unwrap();
assert_eq!(expected, result);
}
#[tokio::test]
async fn test_bad_request() {
let result = test::request()
.method("POST")
.header("ce-specversion", "BAD SPECIFICATION")
.header("ce-id", "0001")
.header("ce-type", "example.test")
.header("ce-source", "http://localhost/")
.header("ce-someint", "10")
.header("ce-time", fixtures::time().to_rfc3339())
.filter(&to_event())
.await;
assert!(result.is_err());
let rejection = result.unwrap_err();
let reason = rejection.find::<super::EventFilterError>().unwrap();
assert_eq!(
reason.error.to_string(),
"Invalid specversion BAD SPECIFICATION"
)
}
#[tokio::test]
async fn test_request_with_full_data() {
let expected = fixtures::v10::full_binary_json_data_string_extension();
let result = test::request()
.method("POST")
.header("ce-specversion", "1.0")
.header("ce-id", "0001")
.header("ce-type", "test_event.test_application")
.header("ce-source", "http://localhost/")
.header("ce-subject", "cloudevents-sdk")
.header("content-type", "application/json")
.header("ce-string_ex", "val")
.header("ce-int_ex", "10")
.header("ce-bool_ex", "true")
.header("ce-time", &fixtures::time().to_rfc3339())
.json(&fixtures::json_data())
.filter(&to_event())
.await
.unwrap();
let mut event = result.clone();
let (_datacontenttype, _dataschema, data) = event.take_data();
let actual_payload: Vec<u8> = data.unwrap().try_into().unwrap();
let expected_payload: Vec<u8> = serde_json::to_vec(&fixtures::json_data()).unwrap();
assert_eq!(expected_payload, actual_payload);
assert_eq!(expected, result);
}
}

65
src/binding/warp/mod.rs Normal file
View File

@ -0,0 +1,65 @@
//! This module integrates the [cloudevents-sdk](https://docs.rs/cloudevents-sdk) with [Warp web service framework](https://docs.rs/warp/)
//! to easily send and receive CloudEvents.
//!
//! To deserialize an HTTP request as CloudEvent
//!
//! To echo events:
//!
//! ```
//! # use warp_lib as warp;
//! use warp::{Filter, Reply};
//! use cloudevents::binding::warp::reply::from_event;
//! use cloudevents::binding::warp::filter::to_event;
//!
//! let routes = warp::any()
//! // extracting event from request
//! .and(to_event())
//! // returning event back
//! .map(|event| from_event(event));
//!
//! warp::serve(routes).run(([127, 0, 0, 1], 3030));
//! ```
//!
//! To create event inside request handlers and send them as responses:
//!
//! ```
//! # use warp_lib as warp;
//! # use http_0_2 as http;
//! use cloudevents::{Event, EventBuilder, EventBuilderV10};
//! use http::StatusCode;
//! use serde_json::json;
//! use warp::{Filter, Reply};
//! use cloudevents::binding::warp::reply::from_event;
//!
//! let routes = warp::any().map(|| {
//! let event = EventBuilderV10::new()
//! .id("1")
//! .source("url://example_response/")
//! .ty("example.ce")
//! .data(
//! mime::APPLICATION_JSON.to_string(),
//! json!({
//! "name": "John Doe",
//! "age": 43,
//! "phones": [
//! "+44 1234567",
//! "+44 2345678"
//! ]
//! }),
//! )
//! .build();
//!
//! match event {
//! Ok(event) => from_event(event),
//! Err(e) => warp::reply::with_status(
//! e.to_string(),
//! StatusCode::INTERNAL_SERVER_ERROR,
//! ).into_response(),
//! }
//! });
//!
//! warp::serve(routes).run(([127, 0, 0, 1], 3030));
//! ```
pub mod filter;
pub mod reply;

114
src/binding/warp/reply.rs Normal file
View File

@ -0,0 +1,114 @@
use warp_lib as warp;
use crate::binding::http_0_2::builder::adapter::to_response;
use crate::Event;
use http::StatusCode;
use http_0_2 as http;
use hyper_0_14 as hyper;
use warp::reply::Response;
///
/// # Serializes [`crate::Event`] as a http response
///
/// ```
/// # use warp_lib as warp;
/// use cloudevents::binding::warp::reply::from_event;
/// use cloudevents::Event;
/// use warp::Filter;
/// use warp::Reply;
///
/// let routes = warp::any()
/// .map(|| from_event(Event::default()));
/// ```
pub fn from_event(event: Event) -> Response {
match to_response(event) {
Ok(response) => response,
Err(e) => warp::http::response::Response::builder()
.status(StatusCode::INTERNAL_SERVER_ERROR)
.body(hyper::body::Body::from(e.to_string()))
.unwrap(),
}
}
#[cfg(test)]
mod tests {
use crate::test::fixtures;
use hyper_0_14 as hyper;
#[test]
fn test_response() {
let input = fixtures::v10::minimal_string_extension();
let resp = super::from_event(input);
assert_eq!(
resp.headers()
.get("ce-specversion")
.unwrap()
.to_str()
.unwrap(),
"1.0"
);
assert_eq!(
resp.headers().get("ce-id").unwrap().to_str().unwrap(),
"0001"
);
assert_eq!(
resp.headers().get("ce-type").unwrap().to_str().unwrap(),
"test_event.test_application"
);
assert_eq!(
resp.headers().get("ce-source").unwrap().to_str().unwrap(),
"http://localhost/"
);
assert_eq!(
resp.headers().get("ce-someint").unwrap().to_str().unwrap(),
"10"
);
}
#[tokio::test]
async fn test_response_with_full_data() {
let input = fixtures::v10::full_binary_json_data_string_extension();
let resp = super::from_event(input);
assert_eq!(
resp.headers()
.get("ce-specversion")
.unwrap()
.to_str()
.unwrap(),
"1.0"
);
assert_eq!(
resp.headers().get("ce-id").unwrap().to_str().unwrap(),
"0001"
);
assert_eq!(
resp.headers().get("ce-type").unwrap().to_str().unwrap(),
"test_event.test_application"
);
assert_eq!(
resp.headers().get("ce-source").unwrap().to_str().unwrap(),
"http://localhost/"
);
assert_eq!(
resp.headers()
.get("content-type")
.unwrap()
.to_str()
.unwrap(),
"application/json"
);
assert_eq!(
resp.headers().get("ce-int_ex").unwrap().to_str().unwrap(),
"10"
);
let (_, body) = resp.into_parts();
let body = hyper::body::to_bytes(body).await.unwrap();
assert_eq!(fixtures::json_data_binary(), body);
}
}

View File

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

View File

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

View File

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

View File

@ -1,230 +0,0 @@
use super::{
Attributes, AttributesReader, AttributesV10, AttributesWriter, Data, ExtensionValue,
SpecVersion,
};
use crate::event::attributes::DataAttributesWriter;
use chrono::{DateTime, Utc};
use delegate::delegate;
use std::collections::HashMap;
use std::convert::TryFrom;
use url::Url;
/// Data structure that represents a [CloudEvent](https://github.com/cloudevents/spec/blob/master/spec.md).
/// It provides methods to get the attributes through [`AttributesReader`]
/// and write them through [`AttributesWriter`].
/// It also provides methods to read and write the [event data](https://github.com/cloudevents/spec/blob/master/spec.md#event-data).
///
/// You can build events using [`super::EventBuilder`]
/// ```
/// use cloudevents::Event;
/// use cloudevents::event::AttributesReader;
///
/// // Create an event using the Default trait
/// let mut e = Event::default();
/// e.write_data(
/// "application/json",
/// serde_json::json!({"hello": "world"})
/// );
///
/// // Print the event id
/// println!("Event id: {}", e.get_id());
///
/// // Get the event data
/// let data: serde_json::Value = e.try_get_data().unwrap().unwrap();
/// println!("Event data: {}", data)
/// ```
#[derive(PartialEq, Debug, Clone)]
pub struct Event {
pub(crate) attributes: Attributes,
pub(crate) data: Option<Data>,
pub(crate) extensions: HashMap<String, ExtensionValue>,
}
impl AttributesReader for Event {
delegate! {
to self.attributes {
fn get_id(&self) -> &str;
fn get_source(&self) -> &Url;
fn get_specversion(&self) -> SpecVersion;
fn get_type(&self) -> &str;
fn get_datacontenttype(&self) -> Option<&str>;
fn get_dataschema(&self) -> Option<&Url>;
fn get_subject(&self) -> Option<&str>;
fn get_time(&self) -> Option<&DateTime<Utc>>;
}
}
}
impl AttributesWriter for Event {
delegate! {
to self.attributes {
fn set_id(&mut self, id: impl Into<String>);
fn set_source(&mut self, source: impl Into<Url>);
fn set_type(&mut self, ty: impl Into<String>);
fn set_subject(&mut self, subject: Option<impl Into<String>>);
fn set_time(&mut self, time: Option<impl Into<DateTime<Utc>>>);
}
}
}
impl Default for Event {
fn default() -> Self {
Event {
attributes: Attributes::V10(AttributesV10::default()),
data: None,
extensions: HashMap::default(),
}
}
}
impl Event {
/// Remove `data`, `dataschema` and `datacontenttype` from this `Event`
pub fn remove_data(&mut self) {
self.data = None;
self.attributes.set_dataschema(None as Option<Url>);
self.attributes.set_datacontenttype(None as Option<String>);
}
/// Write `data` into this `Event` with the specified `datacontenttype`.
///
/// ```
/// use cloudevents::Event;
/// use serde_json::json;
/// use std::convert::Into;
///
/// let mut e = Event::default();
/// e.write_data("application/json", json!({}))
/// ```
pub fn write_data(&mut self, datacontenttype: impl Into<String>, data: impl Into<Data>) {
self.attributes.set_datacontenttype(Some(datacontenttype));
self.attributes.set_dataschema(None as Option<Url>);
self.data = Some(data.into());
}
/// Write `data` into this `Event` with the specified `datacontenttype` and `dataschema`.
///
/// ```
/// use cloudevents::Event;
/// use serde_json::json;
/// use std::convert::Into;
/// use url::Url;
///
/// let mut e = Event::default();
/// e.write_data_with_schema(
/// "application/json",
/// Url::parse("http://myapplication.com/schema").unwrap(),
/// json!({})
/// )
/// ```
pub fn write_data_with_schema(
&mut self,
datacontenttype: impl Into<String>,
dataschema: impl Into<Url>,
data: impl Into<Data>,
) {
self.attributes.set_datacontenttype(Some(datacontenttype));
self.attributes.set_dataschema(Some(dataschema));
self.data = Some(data.into());
}
/// Get `data` from this `Event`
pub fn get_data<T: Sized + From<Data>>(&self) -> Option<T> {
match self.data.as_ref() {
Some(d) => Some(T::from(d.clone())),
None => None,
}
}
/// Try to get `data` from this `Event`
pub fn try_get_data<T: Sized + TryFrom<Data>>(&self) -> Result<Option<T>, T::Error> {
match self.data.as_ref() {
Some(d) => Some(T::try_from(d.clone())),
None => None,
}
.transpose()
}
/// Transform this `Event` into the content of `data`
pub fn into_data<T: Sized + TryFrom<Data>>(self) -> Result<Option<T>, T::Error> {
match self.data {
Some(d) => Some(T::try_from(d)),
None => None,
}
.transpose()
}
/// Get the [extension](https://github.com/cloudevents/spec/blob/master/spec.md#extension-context-attributes) named `extension_name`
pub fn get_extension(&self, extension_name: &str) -> Option<&ExtensionValue> {
self.extensions.get(extension_name)
}
/// Get all the [extensions](https://github.com/cloudevents/spec/blob/master/spec.md#extension-context-attributes)
pub fn get_extensions(&self) -> Vec<(&str, &ExtensionValue)> {
self.extensions
.iter()
.map(|(k, v)| (k.as_str(), v))
.collect()
}
/// Set the [extension](https://github.com/cloudevents/spec/blob/master/spec.md#extension-context-attributes) named `extension_name` with `extension_value`
pub fn set_extension<'name, 'event: 'name>(
&'event mut self,
extension_name: &'name str,
extension_value: impl Into<ExtensionValue>,
) {
self.extensions
.insert(extension_name.to_owned(), extension_value.into());
}
/// Remove the [extension](https://github.com/cloudevents/spec/blob/master/spec.md#extension-context-attributes) named `extension_name`
pub fn remove_extension<'name, 'event: 'name>(
&'event mut self,
extension_name: &'name str,
) -> Option<ExtensionValue> {
self.extensions.remove(extension_name)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn try_get_data_json() {
let expected_data = serde_json::json!({
"hello": "world"
});
let mut e = Event::default();
e.write_data_with_schema(
"application/json",
Url::parse("http://localhost:8080/schema").unwrap(),
expected_data.clone(),
);
let data: serde_json::Value = e.try_get_data().unwrap().unwrap();
assert_eq!(expected_data, data);
assert_eq!("application/json", e.get_datacontenttype().unwrap());
assert_eq!(
&Url::parse("http://localhost:8080/schema").unwrap(),
e.get_dataschema().unwrap()
)
}
#[test]
fn remove_data() {
let mut e = Event::default();
e.write_data(
"application/json",
serde_json::json!({
"hello": "world"
}),
);
e.remove_data();
assert!(e.try_get_data::<serde_json::Value>().unwrap().is_none());
assert!(e.get_dataschema().is_none());
assert!(e.get_datacontenttype().is_none());
}
}

View File

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

View File

@ -3,119 +3,99 @@ use super::{
EventFormatSerializerV03, EventFormatSerializerV10,
};
use crate::event::{AttributesReader, ExtensionValue};
use serde::de::{Error, IntoDeserializer, Unexpected};
use base64::prelude::*;
use serde::de::{Error, IntoDeserializer};
use serde::{Deserialize, Deserializer, Serialize, Serializer};
use serde_value::Value;
use std::collections::{BTreeMap, HashMap};
macro_rules! parse_optional_field {
($map:ident, $name:literal, $value_variant:ident, $error:ty) => {
$map.remove($name)
.map(|val| match val {
Value::$value_variant(v) => Ok(v),
other => Err(<$error>::invalid_type(
crate::event::format::value_to_unexpected(&other),
&stringify!($value_variant),
)),
})
.transpose()
};
($map:ident, $name:literal, $value_variant:ident, $error:ty, $mapper:expr) => {
$map.remove($name)
.map(|val| match val {
Value::$value_variant(v) => $mapper(&v).map_err(|e| {
<$error>::invalid_value(
crate::event::format::value_to_unexpected(&Value::$value_variant(v)),
&e.to_string().as_str(),
)
}),
other => Err(<$error>::invalid_type(
crate::event::format::value_to_unexpected(&other),
&stringify!($value_variant),
)),
})
.transpose()
};
}
use serde_json::{Map, Value};
use std::collections::HashMap;
macro_rules! parse_field {
($map:ident, $name:literal, $value_variant:ident, $error:ty) => {
parse_optional_field!($map, $name, $value_variant, $error)?
($value:expr, $target_type:ty, $error:ty) => {
<$target_type>::deserialize($value.into_deserializer()).map_err(<$error>::custom)
};
($value:expr, $target_type:ty, $error:ty, $mapper:expr) => {
<$target_type>::deserialize($value.into_deserializer())
.map_err(<$error>::custom)
.and_then(|v| $mapper(v).map_err(<$error>::custom))
};
}
macro_rules! extract_optional_field {
($map:ident, $name:literal, $target_type:ty, $error:ty) => {
$map.remove($name)
.filter(|v| !v.is_null())
.map(|v| parse_field!(v, $target_type, $error))
.transpose()
};
($map:ident, $name:literal, $target_type:ty, $error:ty, $mapper:expr) => {
$map.remove($name)
.filter(|v| !v.is_null())
.map(|v| parse_field!(v, $target_type, $error, $mapper))
.transpose()
};
}
macro_rules! extract_field {
($map:ident, $name:literal, $target_type:ty, $error:ty) => {
extract_optional_field!($map, $name, $target_type, $error)?
.ok_or_else(|| <$error>::missing_field($name))
};
($map:ident, $name:literal, $value_variant:ident, $error:ty, $mapper:expr) => {
parse_optional_field!($map, $name, $value_variant, $error, $mapper)?
($map:ident, $name:literal, $target_type:ty, $error:ty, $mapper:expr) => {
extract_optional_field!($map, $name, $target_type, $error, $mapper)?
.ok_or_else(|| <$error>::missing_field($name))
};
}
macro_rules! parse_data_json {
($in:ident, $error:ty) => {
Ok(serde_json::Value::deserialize($in.into_deserializer())
.map_err(|e| <$error>::custom(e))?)
};
pub fn parse_data_json<E: serde::de::Error>(v: Value) -> Result<Value, E> {
Value::deserialize(v.into_deserializer()).map_err(E::custom)
}
macro_rules! parse_data_string {
($in:ident, $error:ty) => {
match $in {
Value::String(s) => Ok(s),
other => Err(E::invalid_type(
crate::event::format::value_to_unexpected(&other),
&"a string",
)),
}
};
pub fn parse_data_string<E: serde::de::Error>(v: Value) -> Result<String, E> {
parse_field!(v, String, E)
}
macro_rules! parse_json_data_base64 {
($in:ident, $error:ty) => {{
let data = parse_data_base64!($in, $error)?;
serde_json::from_slice(&data).map_err(|e| <$error>::custom(e))
}};
pub fn parse_data_base64<E: serde::de::Error>(v: Value) -> Result<Vec<u8>, E> {
parse_field!(v, String, E).and_then(|s| {
BASE64_STANDARD
.decode(s)
.map_err(|e| E::custom(format_args!("decode error `{}`", e)))
})
}
macro_rules! parse_data_base64 {
($in:ident, $error:ty) => {
match $in {
Value::String(s) => base64::decode(&s).map_err(|e| {
<$error>::invalid_value(serde::de::Unexpected::Str(&s), &e.to_string().as_str())
}),
other => Err(E::invalid_type(
crate::event::format::value_to_unexpected(&other),
&"a string",
)),
}
};
pub fn parse_data_base64_json<E: serde::de::Error>(v: Value) -> Result<Value, E> {
let data = parse_data_base64(v)?;
serde_json::from_slice(&data).map_err(E::custom)
}
pub(crate) trait EventFormatDeserializer {
fn deserialize_attributes<E: serde::de::Error>(
map: &mut BTreeMap<String, Value>,
map: &mut Map<String, Value>,
) -> Result<Attributes, E>;
fn deserialize_data<E: serde::de::Error>(
content_type: &str,
map: &mut BTreeMap<String, Value>,
map: &mut Map<String, Value>,
) -> Result<Option<Data>, E>;
fn deserialize_event<E: serde::de::Error>(
mut map: BTreeMap<String, Value>,
) -> Result<Event, E> {
fn deserialize_event<E: serde::de::Error>(mut map: Map<String, Value>) -> Result<Event, E> {
let attributes = Self::deserialize_attributes(&mut map)?;
let data = Self::deserialize_data(
attributes
.get_datacontenttype()
.unwrap_or("application/json"),
attributes.datacontenttype().unwrap_or("application/json"),
&mut map,
)?;
let extensions = map
.into_iter()
.map(|(k, v)| Ok((k, ExtensionValue::deserialize(v.into_deserializer())?)))
.collect::<Result<HashMap<String, ExtensionValue>, serde_value::DeserializerError>>()
.map_err(|e| E::custom(e))?;
.filter(|v| !v.1.is_null())
.map(|(k, v)| {
Ok((
k,
ExtensionValue::deserialize(v.into_deserializer()).map_err(E::custom)?,
))
})
.collect::<Result<HashMap<String, ExtensionValue>, E>>()?;
Ok(Event {
attributes,
@ -139,20 +119,12 @@ impl<'de> Deserialize<'de> for Event {
where
D: Deserializer<'de>,
{
let map = match Value::deserialize(deserializer)? {
Value::Map(m) => Ok(m),
v => Err(Error::invalid_type(value_to_unexpected(&v), &"a map")),
}?;
let root_value = Value::deserialize(deserializer)?;
let mut map: Map<String, Value> =
Map::deserialize(root_value.into_deserializer()).map_err(D::Error::custom)?;
let mut map: BTreeMap<String, Value> = map
.into_iter()
.map(|(k, v)| match k {
Value::String(s) => Ok((s, v)),
k => Err(Error::invalid_type(value_to_unexpected(&k), &"a string")),
})
.collect::<Result<BTreeMap<String, Value>, <D as Deserializer<'de>>::Error>>()?;
match parse_field!(map, "specversion", String, <D as Deserializer<'de>>::Error)?.as_str() {
match extract_field!(map, "specversion", String, <D as Deserializer<'de>>::Error)?.as_str()
{
"0.3" => EventFormatDeserializerV03::deserialize_event(map),
"1.0" => EventFormatDeserializerV10::deserialize_event(map),
s => Err(D::Error::unknown_variant(
@ -178,28 +150,3 @@ impl Serialize for Event {
}
}
}
// This should be provided by the Value package itself
pub(crate) fn value_to_unexpected(v: &Value) -> Unexpected {
match v {
Value::Bool(b) => serde::de::Unexpected::Bool(*b),
Value::U8(n) => serde::de::Unexpected::Unsigned(*n as u64),
Value::U16(n) => serde::de::Unexpected::Unsigned(*n as u64),
Value::U32(n) => serde::de::Unexpected::Unsigned(*n as u64),
Value::U64(n) => serde::de::Unexpected::Unsigned(*n),
Value::I8(n) => serde::de::Unexpected::Signed(*n as i64),
Value::I16(n) => serde::de::Unexpected::Signed(*n as i64),
Value::I32(n) => serde::de::Unexpected::Signed(*n as i64),
Value::I64(n) => serde::de::Unexpected::Signed(*n),
Value::F32(n) => serde::de::Unexpected::Float(*n as f64),
Value::F64(n) => serde::de::Unexpected::Float(*n),
Value::Char(c) => serde::de::Unexpected::Char(*c),
Value::String(s) => serde::de::Unexpected::Str(s),
Value::Unit => serde::de::Unexpected::Unit,
Value::Option(_) => serde::de::Unexpected::Option,
Value::Newtype(_) => serde::de::Unexpected::NewtypeStruct,
Value::Seq(_) => serde::de::Unexpected::Seq,
Value::Map(_) => serde::de::Unexpected::Map,
Value::Bytes(b) => serde::de::Unexpected::Bytes(b),
}
}

View File

@ -6,6 +6,7 @@ use crate::message::{
BinaryDeserializer, BinarySerializer, MessageAttributeValue, Result, StructuredDeserializer,
StructuredSerializer,
};
use crate::{EventBuilder, EventBuilderV03, EventBuilderV10};
impl StructuredDeserializer for Event {
fn deserialize_structured<R, V: StructuredSerializer<R>>(self, visitor: V) -> Result<R> {
@ -16,7 +17,7 @@ impl StructuredDeserializer for Event {
impl BinaryDeserializer for Event {
fn deserialize_binary<R: Sized, V: BinarySerializer<R>>(self, mut visitor: V) -> Result<R> {
visitor = visitor.set_spec_version(self.get_specversion())?;
visitor = visitor.set_spec_version(self.specversion())?;
visitor = self.attributes.deserialize_attributes(visitor)?;
for (k, v) in self.extensions.into_iter() {
visitor = visitor.set_extension(&k, v.into())?;
@ -37,10 +38,6 @@ pub(crate) trait AttributesDeserializer {
fn deserialize_attributes<R: Sized, V: BinarySerializer<R>>(self, visitor: V) -> Result<V>;
}
pub(crate) trait AttributesSerializer {
fn serialize_attribute(&mut self, name: &str, value: MessageAttributeValue) -> Result<()>;
}
impl AttributesDeserializer for Attributes {
fn deserialize_attributes<R: Sized, V: BinarySerializer<R>>(self, visitor: V) -> Result<V> {
match self {
@ -50,50 +47,202 @@ impl AttributesDeserializer for Attributes {
}
}
impl AttributesSerializer for Attributes {
fn serialize_attribute(&mut self, name: &str, value: MessageAttributeValue) -> Result<()> {
match self {
Attributes::V03(v03) => v03.serialize_attribute(name, value),
Attributes::V10(v10) => v10.serialize_attribute(name, value),
}
pub(crate) trait AttributesSerializer {
fn serialize_attribute(&mut self, name: &str, value: MessageAttributeValue) -> Result<()>;
}
#[derive(Debug)]
pub(crate) struct EventStructuredSerializer {}
impl StructuredSerializer<Event> for EventStructuredSerializer {
fn set_structured_event(self, bytes: Vec<u8>) -> Result<Event> {
Ok(serde_json::from_slice(&bytes)?)
}
}
impl StructuredSerializer<Event> for Event {
fn set_structured_event(mut self, bytes: Vec<u8>) -> Result<Event> {
let new_event: Event = serde_json::from_slice(&bytes)?;
self.attributes = new_event.attributes;
self.data = new_event.data;
self.extensions = new_event.extensions;
Ok(self)
#[derive(Debug)]
pub(crate) enum EventBinarySerializer {
V10(EventBuilderV10),
V03(EventBuilderV03),
}
impl EventBinarySerializer {
pub(crate) fn new() -> Self {
EventBinarySerializer::V10(EventBuilderV10::new())
}
}
impl BinarySerializer<Event> for Event {
fn set_spec_version(mut self, spec_version: SpecVersion) -> Result<Self> {
match spec_version {
SpecVersion::V03 => self.attributes = self.attributes.clone().into_v03(),
SpecVersion::V10 => self.attributes = self.attributes.clone().into_v10(),
}
Ok(self)
impl BinarySerializer<Event> for EventBinarySerializer {
fn set_spec_version(self, spec_version: SpecVersion) -> Result<Self> {
Ok(match spec_version {
SpecVersion::V03 => EventBinarySerializer::V03(EventBuilderV03::new()),
SpecVersion::V10 => EventBinarySerializer::V10(EventBuilderV10::new()),
})
}
fn set_attribute(mut self, name: &str, value: MessageAttributeValue) -> Result<Self> {
self.attributes.serialize_attribute(name, value)?;
match &mut self {
EventBinarySerializer::V03(eb) => eb.serialize_attribute(name, value)?,
EventBinarySerializer::V10(eb) => eb.serialize_attribute(name, value)?,
}
Ok(self)
}
fn set_extension(mut self, name: &str, value: MessageAttributeValue) -> Result<Self> {
self.extensions.insert(name.to_string(), value.into());
Ok(self)
fn set_extension(self, name: &str, value: MessageAttributeValue) -> Result<Self> {
Ok(match self {
EventBinarySerializer::V03(eb) => EventBinarySerializer::V03(eb.extension(name, value)),
EventBinarySerializer::V10(eb) => EventBinarySerializer::V10(eb.extension(name, value)),
})
}
fn end_with_data(mut self, bytes: Vec<u8>) -> Result<Event> {
self.data = Some(Data::from_binary(self.get_datacontenttype(), bytes)?);
Ok(self)
fn end_with_data(self, bytes: Vec<u8>) -> Result<Event> {
Ok(match self {
EventBinarySerializer::V03(eb) => {
eb.data_without_content_type(Data::Binary(bytes)).build()
}
EventBinarySerializer::V10(eb) => {
eb.data_without_content_type(Data::Binary(bytes)).build()
}
}?)
}
fn end(self) -> Result<Event> {
Ok(self)
Ok(match self {
EventBinarySerializer::V03(eb) => eb.build(),
EventBinarySerializer::V10(eb) => eb.build(),
}?)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::message::Error;
use crate::test::fixtures;
use std::convert::TryInto;
#[test]
fn binary_deserializer_unrecognized_attribute_v03() {
assert_eq!(
Error::UnknownAttribute {
name: "dataschema".to_string()
}
.to_string(),
EventBinarySerializer::new()
.set_spec_version(SpecVersion::V03)
.unwrap()
.set_attribute("dataschema", MessageAttributeValue::Boolean(true))
.expect_err("Should return an error")
.to_string()
)
}
#[test]
fn binary_deserializer_missing_id() {
assert_eq!(
Error::EventBuilderError {
source: crate::event::EventBuilderError::MissingRequiredAttribute {
attribute_name: "id"
},
}
.to_string(),
EventBinarySerializer::new()
.set_spec_version(SpecVersion::V10)
.unwrap()
.end()
.unwrap_err()
.to_string()
)
}
#[test]
fn binary_deserializer_unrecognized_attribute_v10() {
assert_eq!(
Error::UnknownAttribute {
name: "schemaurl".to_string()
}
.to_string(),
EventBinarySerializer::new()
.set_spec_version(SpecVersion::V10)
.unwrap()
.set_attribute("schemaurl", MessageAttributeValue::Boolean(true))
.expect_err("Should return an error")
.to_string()
)
}
#[test]
fn message_v03_roundtrip_structured() -> Result<()> {
assert_eq!(
fixtures::v03::full_json_data(),
StructuredDeserializer::into_event(fixtures::v03::full_json_data())?
);
Ok(())
}
#[test]
fn message_v03_roundtrip_binary() -> Result<()> {
//TODO this code smells because we're missing a proper way in the public APIs
// to destructure an event and rebuild it
let wanna_be_expected = fixtures::v03::full_json_data();
let data: serde_json::Value = wanna_be_expected.data().unwrap().clone().try_into()?;
let bytes = serde_json::to_vec(&data)?;
let expected = EventBuilderV03::from(wanna_be_expected.clone())
.data(wanna_be_expected.datacontenttype().unwrap(), bytes)
.build()
.unwrap();
assert_eq!(
expected,
BinaryDeserializer::into_event(fixtures::v03::full_json_data())?
);
Ok(())
}
#[test]
fn message_v03_msgpack() {
let buff = rmp_serde::to_vec(&fixtures::v03::full_json_data()).unwrap();
let event = rmp_serde::from_slice::<Event>(buff.as_slice()).unwrap();
assert_eq!(event, fixtures::v03::full_json_data(),);
}
#[test]
fn message_v10_roundtrip_structured() -> Result<()> {
assert_eq!(
fixtures::v10::full_json_data(),
StructuredDeserializer::into_event(fixtures::v10::full_json_data())?
);
Ok(())
}
#[test]
fn message_v10_roundtrip_binary() -> Result<()> {
//TODO this code smells because we're missing a proper way in the public APIs
// to destructure an event and rebuild it
let wanna_be_expected = fixtures::v10::full_json_data();
let data: serde_json::Value = wanna_be_expected
.data()
.cloned()
.unwrap()
.try_into()
.unwrap();
let bytes = serde_json::to_vec(&data)?;
let expected = EventBuilderV10::from(wanna_be_expected.clone())
.data(wanna_be_expected.datacontenttype().unwrap(), bytes)
.build()
.unwrap();
assert_eq!(
expected,
BinaryDeserializer::into_event(fixtures::v10::full_json_data())?
);
Ok(())
}
#[test]
fn message_v10_msgpack() {
let buff = rmp_serde::to_vec(&fixtures::v10::full_json_data()).unwrap();
let event = rmp_serde::from_slice::<Event>(buff.as_slice()).unwrap();
assert_eq!(event, fixtures::v10::full_json_data(),);
}
}

View File

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

View File

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

60
src/event/types.rs Normal file
View File

@ -0,0 +1,60 @@
use chrono::{DateTime, Utc};
use url::Url;
/// Trait to define conversion to [`Url`]
pub trait TryIntoUrl {
fn into_url(self) -> Result<Url, url::ParseError>;
}
impl TryIntoUrl for Url {
fn into_url(self) -> Result<Url, url::ParseError> {
Ok(self)
}
}
impl TryIntoUrl for &str {
fn into_url(self) -> Result<Url, url::ParseError> {
Url::parse(self)
}
}
impl TryIntoUrl for String {
fn into_url(self) -> Result<Url, url::ParseError> {
self.as_str().into_url()
}
}
/// Trait to define conversion to [`DateTime`]
pub trait TryIntoTime {
fn into_time(self) -> Result<DateTime<Utc>, chrono::ParseError>;
}
impl TryIntoTime for DateTime<Utc> {
fn into_time(self) -> Result<DateTime<Utc>, chrono::ParseError> {
Ok(self)
}
}
impl TryIntoTime for &str {
fn into_time(self) -> Result<DateTime<Utc>, chrono::ParseError> {
Ok(DateTime::<Utc>::from(DateTime::parse_from_rfc3339(self)?))
}
}
impl TryIntoTime for String {
fn into_time(self) -> Result<DateTime<Utc>, chrono::ParseError> {
self.as_str().into_time()
}
}
/// The URI-reference type.
///
/// The URI reference can be a URI, or just a relative path.
///
/// As the [`url::Url`] type can only represent an absolute URL, we are falling back to a string
/// here.
///
/// Also see:
/// * <https://github.com/cloudevents/spec/blob/v1.0.1/spec.md#type-system>
/// * <https://tools.ietf.org/html/rfc3986#section-4.1>
pub type UriReference = String;

View File

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

View File

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

View File

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

View File

@ -1,53 +0,0 @@
use crate::message::{BinarySerializer, Error, MessageAttributeValue, Result};
use std::convert::TryInto;
impl crate::event::message::AttributesDeserializer for super::Attributes {
fn deserialize_attributes<R: Sized, V: BinarySerializer<R>>(self, mut visitor: V) -> Result<V> {
visitor = visitor.set_attribute("id", MessageAttributeValue::String(self.id))?;
visitor = visitor.set_attribute("type", MessageAttributeValue::String(self.ty))?;
visitor = visitor.set_attribute("source", MessageAttributeValue::UriRef(self.source))?;
if self.datacontenttype.is_some() {
visitor = visitor.set_attribute(
"datacontenttype",
MessageAttributeValue::String(self.datacontenttype.unwrap()),
)?;
}
if self.schemaurl.is_some() {
visitor = visitor.set_attribute(
"schemaurl",
MessageAttributeValue::Uri(self.schemaurl.unwrap()),
)?;
}
if self.subject.is_some() {
visitor = visitor.set_attribute(
"subject",
MessageAttributeValue::String(self.subject.unwrap()),
)?;
}
if self.time.is_some() {
visitor = visitor
.set_attribute("time", MessageAttributeValue::DateTime(self.time.unwrap()))?;
}
Ok(visitor)
}
}
impl crate::event::message::AttributesSerializer for super::Attributes {
fn serialize_attribute(&mut self, name: &str, value: MessageAttributeValue) -> Result<()> {
match name {
"id" => self.id = value.to_string(),
"type" => self.ty = value.to_string(),
"source" => self.source = value.try_into()?,
"datacontenttype" => self.datacontenttype = Some(value.to_string()),
"schemaurl" => self.schemaurl = Some(value.try_into()?),
"subject" => self.subject = Some(value.to_string()),
"time" => self.time = Some(value.try_into()?),
_ => {
return Err(Error::UnrecognizedAttributeName {
name: name.to_string(),
})
}
};
Ok(())
}
}

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

View File

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

View File

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

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