Skip to content

Releases

v0.7.0

Choose a tag to compare

@filipsajdak filipsajdak released this 06 Nov 21:33
801c4a2

Events can travel over MQTT, and they can be written and read in the Protobuf event format.
ce::mqtt is a new binding (MQTT 5.0 structured and binary mode, MQTT 3.1.1 structured mode), and ce::protobuf_format is a new, hand-written event format that also works as structured mode in the HTTP, Kafka and NATS bindings.
Around the SDK, the coverage gate now counts branches, and the Go and Java SDKs check the Protobuf bytes and the MQTT messages the way they already check HTTP, Kafka and NATS.

This is a feature release (D-BUILD-5: before 1.0 a feature release bumps the minor version, as a break does).
The reason, as the owner ruled on 2026-10-27: it adds a new binding and a new format, each a capability of its own with its own requirements, entry points and interop suite, and D-BUILD-4 as it stood would have numbered it v0.6.2.

There are no breaking changes.
Everything below is an addition to ce::v4 or a change outside the library, and ce::v1 to ce::v4 keep every declaration and every documented behaviour they had at v0.6.1: python3 scripts/frozen_api_diff.py --base v0.6.1 finds no frozen declaration removed or changed (1,843 declarations compared).
The published to_message, from_message and from_payload return what they returned for every input, a message of the Protobuf content type included, and no namespace is added, so ce::v4 stays the current generation.

Added

  • An MQTT binding, structured mode (binding/mqtt.hpp, ce::mqtt, CR-0008 slice 1).
    An event travels as an MQTT 5.0 PUBLISH held in a ce::message: the JSON event as the payload, the Content Type property application/cloudevents+json as the header content-type, and no User Property written or read.
    ce::mqtt::to_message<Codec>(event, ce::content_mode::structured) and ce::mqtt::from_message<Codec>(message, options) are the 5.0 pair, with detect_content_mode; ce::mqtt::to_payload<Codec>(event) and from_payload<Codec>(payload_or_message, options) are the MQTT 3.1.1 pair, which has no properties, so the caller names the version by the function.
    A message with no CloudEvents Content Type is not_a_cloudevent, a malformed payload is the codec's error, a batch is refused with invalid_argument and so is a second header named content-type, and names match byte for byte.
    Binary mode is the next bullet.
    The binding is header-only, names no MQTT client library and adds the target ce::binding_mqtt, the module export ce::v4::mqtt and the fuzz target fuzz_mqtt_message; the Go SDK reads and writes the same structured messages (sixteen recorded goldens, no broker, sdk-java has no MQTT binding).
    It is an addition to ce::v4: python3 scripts/frozen_api_diff.py --base v0.6.1 finds no changed declaration.
    Use:

    auto publish = ce::mqtt::to_message<codec>(event, ce::content_mode::structured);
    auto received = ce::mqtt::from_message<codec>(*publish);
  • MQTT binary mode, with sdk-go's ce- prefix as an opt-in (ce::mqtt, CR-0008 slice 2).
    ce::mqtt::to_message<Codec>(event, ce::content_mode::binary_mode) writes an MQTT 5.0 PUBLISH whose payload is the data, whose Content Type property is datacontenttype and whose User Properties are every other attribute and extension; from_message<Codec> reads it.
    The names follow the binding text, unchanged (specversion, id), by default.
    Name the policy for the form the Go SDK writes and requires: to_message<Codec, ce::mqtt::prefixed_names>(...) and from_message<Codec, ce::mqtt::prefixed_names>(...) use ce-specversion, ce-id; unprefixed_names is the default and the concept is name_policy.
    A message with a CloudEvents Content Type is still structured; any other is binary when it carries the specversion User Property under the policy, and not_a_cloudevent otherwise, where an MQTT binding limited to structured mode would answer not_a_cloudevent for every such message.
    A User Property that is not an attribute name under the policy is ignored; a datacontenttype User Property must equal the Content Type property; values are well-formed UTF-8 without NUL and at most 65,535 bytes.
    The eight Go binary goldens recorded with slice 1 now decode under prefixed_names, are not_a_cloudevent under the default, and Go reads the binary messages this SDK writes under the policy.
    fuzz_mqtt_message covers binary mode under both policies, and the module also exports name_policy, unprefixed_names and prefixed_names.
    Use:

    auto for_go = ce::mqtt::to_message<codec, ce::mqtt::prefixed_names>(event, ce::content_mode::binary_mode);
    auto back = ce::mqtt::from_message<codec, ce::mqtt::prefixed_names>(*for_go);
  • The Protobuf event format: ce::protobuf_format, target ce::format_protobuf.
    Bytes to event and event to bytes for the CloudEvents v1.0.2 Protobuf format, one event or a batch, hand-written over the standard library and ce::core: no libprotobuf, no protoc, no generated code and no codec.
    encode, decode, encode_batch and decode_batch; decode_as<Codec>, which parses a JSON payload on request and holds no document; and decode_detailed and encode_proto_data, which carry the type URL of a proto_data payload beside the event.
    The field numbers are those of cloudevents.proto, not the prose of the specification.
    The writer follows the specification (text_data for text, the attributes in ascending order of name) and the reader accepts all three data fields, so the bytes of the Go and Java SDKs decode here and the bytes written here decode there.
    Decoding is strict, uses the existing error codes, and is bounded by protobuf::decode_options (16 MiB per event, 1024 attributes per event, 100,000 events per batch, all adjustable and checked before an allocation); a libFuzzer target and the goldens of both SDKs and of protoc back it.
    A decoded time is in UTC with the fewest of 0, 3, 6 or 9 fraction digits, and a Timestamp outside the range of ce::timestamp is invalid_timestamp.
    It is an addition to ce::v4 and changes no declaration of any generation (CR-0009, SWR-PROTO-0001 to SWR-PROTO-0018, SWR-PROTO-0024 to SWR-PROTO-0026); structured mode in the bindings is the next bullet.
    Use:

    auto bytes = ce::protobuf_format::encode(event);
    auto back = ce::protobuf_format::decode(*bytes);
  • Structured Protobuf in the HTTP, Kafka and NATS bindings (CR-0009 slice 2).
    ce::http::, ce::kafka:: and ce::nats:: each get to_message_protobuf(event), which writes the application/cloudevents+protobuf content type under the binding's own header spelling and the bytes of protobuf_format::encode, and from_message_protobuf(message, options), which returns not_a_cloudevent for a message whose media type, compared ignoring case and parameters, is another one.
    NATS also gets the payload form, to_payload_protobuf(event) and from_payload_protobuf(bytes, options), which always reads Protobuf.
    The functions are new names and not templates, there is no content mode and no moving variant, Kafka sets no key, and the published to_message, from_message and from_payload return what they returned for every input, a message of the Protobuf content type included.
    The Go and Java SDKs' HTTP and Kafka bindings write and read the same messages (thirteen events each, no server and no broker), and the fuzz target for the decoder drives the new functions.
    It is an addition to ce::v4 (SWR-PROTO-0019 to SWR-PROTO-0023).
    Use:

    auto record = ce::kafka::to_message_protobuf(event);
    auto back = ce::kafka::from_message_protobuf(*record);

Tooling

None of this changes what the library does.

  • Branch coverage is gated (CR-0007, SWR-SEC-0007, D-SEC-5): the coverage floor job fails a pull request whose branch coverage, with the compiler's exception edges excluded, falls below CE_COVERAGE_BRANCH_FLOOR (86), next to the unchanged 90% line floor.
    The floor is valid for one toolchain, so the job asserts gcovr 7.0 and gcov-13 and fails before measuring, naming both versions, if the image ships others.
    scripts/check_coverage_gate.py and its cases (coverage_gate_tools under ctest) fail on a dropped flag, a literal floor or a floating toolchain.
    The floor is raised by a person, in the pull request that raises the measurement by a whole point, and never lowered to let a release go out.
    Tests were added for the untested error paths of JSON decode, header values, typed payloads and the payload scanner (#110).
  • The versioning rule (D-BUILD-5, docs/RELEASE.md): before 1.0 a break and a feature release each bump the minor version, any other release the patch; the owner decides that a release is a feature release and the release pull request states the reason.
  • The delivered_in check is part of the spec gate (SWR-BUILD-0016, D-SPEC-5): every pull request runs derive_delivered_in.py --check with the full history, so a requirement that a tag delivered cannot stay approved.
    The gate is red between a tag and the merge of the post-tag pull request, by design, so that pull request is merged first.
  • The vcpkg port is proved against a release (vcpkg-released workflow, scripts/check_vcpkg_release_guard.py, SWR-ADOPT-0009): vcpkg downloads the archive of the tag without CLOUDEVENTS_CPP_SOURCE_DIR, refuses a hash that does not match, and the job also checks that a wrong hash is refused.
    It skips, with a notice, while the tag is missing or the hash is still the previous release's.
  • Interop covers MQTT and Protobuf (interop/mqtt, interop/protobuf, SWR-MQTT-0014, SWR-PROTO-0016, SWR-PROTO-0023, D-INTEROP-6): the interop job verifies the Go SDK's MQTT messages and the Go and Java SDKs' Protobuf bytes and HTTP and Kafka messages in both directions, without a broker or a server.
  • Two fuzz targets, fuzz_mqtt_message and fuzz_protobuf_format (whi...
Read more

v0.6.1

Choose a tag to compare

@filipsajdak filipsajdak released this 21 Oct 19:48
bb8175b

A forwarder can keep a binary-mode JSON body unparsed, and a receiver that is done with its message can move a non-JSON binary-mode body instead of copying it.
Around the SDK, a gate now fails a pull request that changes a frozen declaration, delivered_in is derived from the tags, Kafka records are checked against the Go and Java SDKs, vcpkg and Conan recipes are built in CI, and an advisory mutation report runs on pull requests.

There are no breaking changes, so this is a patch release (D-BUILD-4: before 1.0 a break bumps the minor version, an addition or a fix the patch).
Everything below is an addition to ce::v4 or a change outside the library, and ce::v1 to ce::v4 keep every declaration and every documented behaviour they had at v0.6.0: python3 scripts/frozen_api_diff.py --base v0.6.0 finds no frozen declaration removed or changed.
Two additions have a consequence that the rule accepts (CR-0005, rule 4 of docs/SPEC.md section 3):

  • json::decode_options gains a trailing member, so its size grows from 8 to 16 bytes and a structured binding auto [limit] = options; stops compiling.
    Every call and every initializer written for v0.6.0 ({.retain_document_up_to = n}, {n}, {}) compiles and behaves as before.
  • decode_options is shared with ce::v3, so a v3 call accepts the member and ignores it.

The moving entry points are new names, so no existing call changes.

Added

  • json::decode_options{.parse_binary_body = false} keeps a binary-mode JSON body unparsed.
    A receiver that never reads the payload, such as a forwarder or router, no longer pays to parse it: from_message of the HTTP, Kafka and NATS bindings stores the body's exact bytes as json_text, without parsing or validating them, whatever their length and the retention limit.
    v0.6.0 parses that body on receive, which costs 15% to 54% more instructions (RapidJSON +15%, Boost.JSON +18%, Glaze +29%, nlohmann +54%) and 8 to 27 more allocations on the HTTP full-event path, by codec; measured by the perf job (x86-64, g++-14), the receive of the full event over HTTP costs 35.6k instructions and 10 allocations (675 bytes) with the option, against 41.5k to 55.7k instructions, 16 to 37 allocations and 1.1 to 2.2 KiB with the parse (RapidJSON -14%, Boost.JSON -16%, Glaze -24%, nlohmann -36% in instructions); Kafka and NATS show the same shape.
    The member is a trailing bool that defaults to true, so every call and every initializer written before compiles and behaves as it did.
    It applies to binary mode only: structured and batched decode, nats::from_payload and json_format::decode accept it and ignore it, as does every ce::v3 entry point.
    With it set the caller takes over validation: a body that is not JSON is accepted as it came.
    Use:

    auto passed_on = ce::http::from_message<codec>(request, {.parse_binary_body = false});

    It is an addition to ce::v4, so it needs no new namespace (CR-0005, SWR-BIND-0011 to SWR-BIND-0013).

  • Moving entry points: from_message_moving takes a non-JSON binary-mode body instead of copying it.
    A receiver that is done with its message, such as a gateway or a forwarder of images, archives or protobuf, hands it over with std::move, and the call adopts the body's buffer as the event's ce::binary payload: one allocation and one copy of the body fewer.
    Six new names, in ce::v4 only, each taking the message (or the payload) by rvalue reference and otherwise identical to its copying sibling: http::from_message_moving<Codec, Values>, kafka::from_message_moving<Codec>, nats::from_message_moving<Codec>, binding::read_body_moving<Codec>, and, for symmetry, http::from_batch_message_moving<Codec> and nats::from_payload_moving<Codec> (which takes a std::string&&).
    A body is moved only when it is non-empty, binary-mode, and its media type is absent or not one is_json_content_type accepts.
    After a successful call that moved the body, message.body.size() == 0 and message.body.capacity() == 0 (the buffer is released), and header_fields is unchanged; that is a permanent promise, not an implementation detail.
    Every other body takes the copying path exactly as the copying entry point does and leaves the message byte for byte intact: a JSON body (parsed or not, whatever decode_options say), a structured message, a batch, a nats payload string and an empty body, which keeps whatever capacity it had.
    A failed call leaves the message unchanged for every body, so it can still go to a dead-letter queue: the body is taken only after the event has passed its final validation.
    The copying entry points are unchanged and never modify their message, with std::move or without (SWR-BIND-0017).
    A JSON body saves nothing from a move, because the parse reads the bytes and a std::string cannot adopt a std::vector<std::byte>; for a forwarder of JSON events the opt-in above is the saving.
    Use:

    auto received = ce::kafka::from_message_moving<codec>(std::move(record.value));

    Measured by the perf job (x86-64, g++-14, nlohmann as the codec argument, the receive alone) against the copying receive of the same body: a 64 KiB application/octet-stream body costs 84% to 88% fewer instructions (HTTP 77.4k to 11.6k, Kafka 74.6k to 8.9k, NATS 77.9k to 12.3k), one allocation fewer of four and 99.9% fewer allocated bytes (65.6 KB to under 100 B).
    A 256 B body costs about the same instructions (HTTP -0.7%, Kafka -0.9%, NATS +0.6%), against an upper bound of 5% to 8% estimated before the code existed: building the event without the payload and moving the media type out and back costs nearly what the 256 B copy did.
    It still makes one allocation fewer of four and allocates 72% to 75% fewer bytes, so the saving is in memory and in large bodies, not in instructions at small sizes.
    It is an addition to ce::v4, and ce::v3 has none of the new names (CR-0005 stage 2, SWR-BIND-0014 to SWR-BIND-0018, SWR-HTTP-0018, SWR-HTTP-0019, SWR-KAFKA-0010, SWR-NATS-0005, SWR-NATS-0006).

Tooling

None of this changes what the library does.

  • A frozen-API gate (scripts/frozen_api_diff.py, the frozen-api workflow): clang parses every public header at the latest v* tag and in the tree, and a pull request that removes or changes a declaration of ce::v1 to ce::v4 fails with the declaration named.
    A declaration that moved between headers or a body that was fixed passes, and so does a defaulted member appended to an aggregate (D-BIND-3).
    Exit 2 means it did not run, which is not a pass (SWR-BUILD-0015, D-BUILD-4).
    docs/RELEASE.md carries the versioning rule it supports: a break goes to a new generation and bumps the minor version, an addition or a fix bumps the patch.
  • delivered_in is derived from the release tags, not typed (spec/tools/derive_delivered_in.py, SWR-BUILD-0016, D-SPEC-4).
    --write runs once the tag exists, and --check reports what it would change; 195 of 222 requirements read implemented with a tag, and the 27 changed or added since v0.6.0 stay approved until the next tag holds them.
  • Kafka records are checked against sdk-go and sdk-java the way HTTP and NATS documents already were: both SDKs decode every record this one writes, in binary and structured mode, and this one decodes theirs, in the ctest suite and in the interop job (SWR-SEC-0008, D-INTEROP-5).
  • vcpkg and Conan recipes (packaging/vcpkg/ports/cloudevents-cpp, packaging/conan), each built and tested against test/consumer in the packaging workflow.
    The nlohmann codec is the default feature or option; RapidJSON is the other one CI builds, and Boost.JSON is declared and not built there.
    Install from a clone with vcpkg install cloudevents-cpp --overlay-ports=... or conan create packaging/conan; neither is submitted to the vcpkg registry or to ConanCenter, and that is not decided.
    No CMake change: the build already looked CTRE up with find_package before it fetched it, and the recipes make a missed lookup an error.
    The consumer gained a round-trip program that runs the library through the installed package, and a check fails a release that leaves the vcpkg manifest on the old version (SWR-ADOPT-0006 to SWR-ADOPT-0008, D-ADOPT-2).
  • An advisory mutation report (scripts/mutation.sh, the mutation workflow): Mull mutates the lines a pull request changes and the report is commented on the pull request.
    It never blocks a merge.
    Tests were added for what it found surviving in the timestamp, the result<T> polyfill and the v4 document.
  • The perf job measures forty-four operations, up from twenty-three: a non-JSON binary-mode receive (empty, 256 B and 64 KiB, over HTTP, Kafka and NATS), the same for a text body over HTTP, the receive that leaves a binary-mode body unparsed, and the moving receive.

Measured

  • Coverage, from the coverage floor job on main at 48f0eb1, the merge of the last change before this release (run 39632669677; GCC 13, C++20, Debug): lines 96.2% (4456 of 4630), functions 91.9% (2143 of 2331), branches 60.9% (4958 of 8146), against a floor of 90% lines.
  • CI toolchains are those of v0.6.0: GCC 13 at C++20 and C++23, Clang 16 with libstdc++ 13, Clang 17 with libc++ 17 at C++23, AppleClang at C++20 and C++23, MSVC 19 (Windows 2022), a C++26 static-reflection job on GCC 15 (GCC 14 if 15 is unavailable; allowed to fail), plus the forced polyfill, exceptions disabled, no default codec, RapidJSON alone, Boost.JSON alone, every codec on Linux and macOS at C++20 and C++23, ASan with UBSan and TSan on Clang 17, the clang-tidy gate on Homebrew LLVM, install-and-consume, the examples, and interop with the Go and Java SDKs, now including Kafka records.
    New since v0.6.0: the frozen-api workflow (Clang 18 against the latest tag), the packaging workflow (g++-14; the vcpkg port and the Conan recipe, each in three v...
Read more

v0.6.0

Choose a tag to compare

@filipsajdak filipsajdak released this 11 Oct 01:27
483690c

ce::v4 opens: a json_document can move, a typed write refuses a media type that is not JSON, and a JSON body received in binary mode keeps the document it was parsed into.
Each of those changes behaviour that v0.5.0 documented, and a documented behaviour is part of the API, so they ship as a fourth API generation, ce::v4, which is the inline namespace.
ce::v3 stays what v0.5.0 published, declarations and behaviour both, and the opt-in header <cloudevents/v3_conversion.hpp> moves events across.

Breaking

Every change below is in ce::v4; ce::v3 behaves as v0.5.0 did.

  • ce:: now names v4. ce::event, ce::json_document, ce::json_format, ce::http, ce::kafka, ce::nats and ce::decode_as are the v4 entities.
    To keep v0.5.0 code compiling unchanged, spell the v3 namespace and include the v3/ copy of each header that has one (core.hpp, format/json_format.hpp, format/typed_payload.hpp and the four binding headers):

    #include <cloudevents/v3/binding/http.hpp>
    
    using namespace ce::v3::literals;
    auto order = ce::v3::event::builder{.id = "A1"_id, .source = "/orders"_source,
                                        .type = "com.example.placed"_type}.build();
    auto request = ce::v3::http::to_message<ce::v3::codec::nlohmann_codec>(
        *order, ce::v3::content_mode::binary_mode);

    Code that names only shared entities, such as an attribute type, message, json::decode_options or a codec, needs no change.
    To exchange events with code that moved to v4, include <cloudevents/v3_conversion.hpp> and call ce::v4::from_v3 and ce::v4::to_v3.

  • Moving a ce::v4::json_document leaves it reading as JSON null.
    In v0.5.0 a move copied the document, so the source still held the same DOM, and ce::v3::json_document still does.
    In v4 a move hands the DOM over without touching the reference count, and the moved-from document is a valid, never-empty document that reads as null: dump() returns null, get<Codec>() returns nullptr for every codec, and it compares equal only to another moved-from document.
    v0.5.0 documented the copy, so this is a break, and it is why v4 exists.
    Migrate: copy the document instead of moving it where you read the source afterwards, or stay on ce::v3.

  • set_data keeps a JSON media type and refuses any other.
    In v3 set_data<T, Codec> set datacontenttype to application/json whatever the event declared.
    In v4 it sets application/json only when the event declares none, keeps a media type is_json_content_type accepts (text/json and +json types included), and refuses any other with errc::type_mismatch naming datacontenttype, leaving the event unchanged, as encode_as already did.
    set_data and event_of::set_data return [[nodiscard]] result<void>.
    Migrate: check the result (if (auto stored = ce::set_data<T, Codec>(event, value); !stored) { ... }); if an event declares a non-JSON type that you now want replaced, call event.set_data(ce::json_document::make<Codec>(ce::to_json_value<Codec>(value)), "application/json"_mediatype) yourself.

  • event_of::with_data returns [[nodiscard]] result<event_of>, because it can be refused for the same reason.
    Migrate: unwrap it (view->data()).

  • A JSON body received in binary mode is a json_document.
    In v3 the HTTP, Kafka and NATS from_message stored a binary-mode body under a JSON media type as unparsed json_text.
    In v4 from_message<Codec> parses it with the codec, under the rules of structured decode: a payload within the 16 KiB retention limit is a json_document, one above it is json_text holding the body's bytes exactly as received, and a non-empty body that is not valid JSON fails with errc::parse_error.
    An empty body is still no payload, whatever its media type, and other media types are still ce::binary or std::string as before.
    binding::read_body takes the codec and the decode options and returns a result<data_t>.
    The parse costs instructions and allocations on receive that v3 spent only when the payload was read.
    Receive plus one typed read (from_message then data_as) costs the same as in v3, within about 2% in instructions, with equal allocations and slightly fewer retained bytes, because the read reuses the document instead of parsing the text again; each further read is cheaper.
    A consumer that never reads the payload, such as a forwarder or router, pays the parse for nothing: on the HTTP full-event path the receive alone costs 15% to 54% more instructions (RapidJSON +15%, Boost.JSON +18%, Glaze +29%, nlohmann +54%) and 8 to 27 more allocations, by codec.
    No option skips the parse today: json::decode_options{.retain_document_up_to = 0} keeps the body as json_text instead of a document, but the body is still parsed to validate it (SWR-BIND-0007, SWR-BIND-0008), so it saves the retained DOM and not the parse.
    Migrate: code that matched json_text on a received binary-mode event reads the document (data_as, get<Codec>(), dump()) or sets .retain_document_up_to = 0 to keep text; code that relied on a malformed body being accepted now gets parse_error from from_message.

  • json_format drops seven public helpers.
    required_text, optional_text, read_required, read_optional, read_time, read_context_attributes and read_extensions are not declared by ce::v4::json_format<Codec>.
    Nothing in the SDK called them, no suite tested them, and a generation's first tag is the only moment surface can go. ce::v3::json_format keeps all seven with their v0.5.0 signatures.
    Migrate: read a whole event with decode or from_value, and a typed payload with decode_as or from_value_as; code that needs one of the helpers itself spells ce::v3::json_format<Codec> and includes <cloudevents/v3/format/json_format.hpp>.

Generations

  • ce::v4 is the inline namespace, so ce::event and ce::json_document now name the v4 classes. v0.6.0 is its first tag.
  • ce::v3 holds the v0.5.0 surface.
    The headers that mention event, data_t or json_document are copied under include/cloudevents/v3/: core.hpp, format/json_format.hpp, format/typed_payload.hpp and the four binding headers.
    A v3 declaration does not change, and neither does its documented behaviour; v3 takes defect fixes only.
    ce::v3::json_document declares copy operations only, as v0.5.0 did, so moving it copies it and the source keeps its DOM.
  • What v3 shared stays shared, now with v4 too: the attribute types, timestamp, message, the literals, the typed extensions, errc, error, result, the codecs and their concepts, base64 and the describe seam are one type through ce::, ce::v4:: and ce::v3::.
    So are json::decode_options, now declared in format/decode_options.hpp, and the model behind a json_document.
  • The optional module exports ce::v4 only.
  • Events convert between the generations.
    ce::v4::from_v3 and ce::v4::to_v3, declared in the opt-in header <cloudevents/v3_conversion.hpp>, take an event by const& or && and return the other generation's event, not a result, because no conversion can fail.
    A json_document converts in constant time, because both generations' documents point at one model: a conversion copies or moves that pointer and never a DOM.
    to_v3 of a moved-from v4 document yields a v3 document on the same moved-from model, which dumps null.
    No other SDK header includes the conversions, and the module does not export them, so import cloudevents; never brings in the v3 event model.
  • Decode options reach every binding entry point that decodes JSON (an addition).
    from_message<Codec> of HTTP, Kafka and NATS, http::from_batch_message<Codec> and nats::from_payload<Codec> take a defaulted last json::decode_options and apply it in every mode they read, so existing calls compile unchanged.
    json_format::from_value and from_value_as take none: they receive a DOM you already hold.

Faster

  • A codec may take over the strings the encoder builds.
    A codec that provides adopt_string(std::string&&) -> value receives the RFC 3339 text of timestamps and the base64 text of binary data by move instead of by view; ce::json::string_adopting_codec<C> detects it.
    It is optional, so a v0.5.0 codec keeps compiling and keeps the copy. nlohmann_codec provides it, and the ce::v3 and ce::v4 encoders both use it.
    Measured by the perf job: one allocation fewer in encode_full on nlohmann (65 to 64), and 100 fewer in a 100-event batch encode on nlohmann (2,824 to 2,724) and on Glaze (715 to 615). Boost.JSON and RapidJSON cannot take over a std::string, so they are unchanged.
  • A moved ce::v4::json_document touches no reference count.
    Move construction goes from one atomic read-modify-write instruction to none, and move assignment from two to one.
    Every decode and batch moves events, so they stop counting references.
  • An error is forwarded by move through one cold helper, detail::forward_failure, instead of being copied.
    It changes no result and no success path: the first version, which moved inline at every site, made encode_full/rapidjson.shipped run 2.09 percent more instructions in the perf job, and the cold helper left it at +0.40 percent.
  • Message bodies are copied in bulk, not one push_back per byte, in to_bytes and to_text. It is a body-only fix that every generation takes (D-CODEC-3). Measured by the perf job (x86_64, g++-14), instructions per operation:
    HTTP structured encode 7.4 percent fewer on nlohmann (74,500 to 68,971) and 13.0 to 15.0 percent fewer on the other codecs, HTTP binary decode 4.2 percent fewer, HTTP binary encode 4.3, Kafka binary decode 5.1 and NATS binary decode 5.2. Allocations are unchanged.
  • A body that owns a std::string instead of a std::vector<std::byte> was measured and declined: it would save at mo...
Read more

v0.5.0

Choose a tag to compare

@filipsajdak filipsajdak released this 27 Sep 11:30
973a106

An event keeps the JSON document its decoder built, so a typed read or write
parses the payload at most once and never serialises it to text in between.
That changes data_t, so it ships as a third API generation, ce::v3, which is
the inline namespace. ce::v2 keeps what v0.4.0 published, so v0.4.0 code has a
way to keep compiling.

Three generations

  • ce::v3 is the inline namespace, so ce::event now names the v3 class.
  • ce::v2 holds the v0.4.0 surface.
    The headers that mention event or data_t are copied under include/cloudevents/v2/: core.hpp, format/json_format.hpp, format/typed_payload.hpp and the four binding headers.
    A v2 declaration does not change; v2 takes defect fixes only.
  • The attribute types are shared by v2 and v3: id, source, timestamp, message, the literals, the typed extensions and the rest of attributes.hpp are declared once, in ce::v2, and are one type through ce::, ce::v2:: and ce::v3::.
  • What v0.3.0 already shared stays shared: errc, error, result, the three codecs, base64 and the describe seam are one type through all three generations.
  • v3 codecs provide equal, copy, extract, for_each_mutable_element and identity.
    ce::v3::json::json_codec adds five members to the v1 concept: C::equal(const value&, const value&) -> bool, JSON value equality with object members in any order; C::copy(const value&) -> value, a deep copy; C::extract(value& object, std::string_view key) -> value, which moves a member out of an object and returns null for a key find would not return; C::for_each_mutable_element(value& array, F visit), which hands each element to visit as a value&; and C::identity, a non-empty static constexpr std::string_view naming the codec.
    The three in-tree codecs gain all five, and keep their v1 declarations; their identities are io.cloudevents.cpp.codec.nlohmann, io.cloudevents.cpp.codec.boost_json and io.cloudevents.cpp.codec.rapidjson.
    A third-party codec must add them to serve ce::v3, with an identity that is a reverse-DNS name under a domain its author controls and that no other codec uses.
    It keeps serving ce::v1 and ce::v2 unchanged, since their concept is the v1 one.
  • ce::json_document holds a parsed JSON document in core.hpp, which still names no codec type.
    json_document::make<Codec>(dom) builds one, get<Codec>() returns the DOM to a codec declaring the identity of the one that built it and nullptr to any other, and dump() serialises it.
    Documents compare as JSON values, across codecs too.
    Copies share one immutable DOM, so a document can be copied, compared and read from several threads at once.
    A document is never empty: it has no default constructor, and moving one copies it.
  • data_t has a fifth alternative, json_document.
    Code that visits data_t exhaustively stops compiling against ce::v3 until it handles it; that is the migration.
    An event holding json_text never equals one holding json_document, even for the same JSON value, so equality stays transitive.
  • A structured decode keeps the payload as a json_document built by the decoding codec, where v2 gave json_text.
    decode and decode_batch move the data member out of the document they parsed, so the payload is neither serialised nor copied; from_value copies it.
    A binary-mode body, a non-JSON string payload and data_base64 decode as before.
  • ce::json::decode_options{.retain_document_up_to = 16 * 1024} bounds what one event can pin: input longer than the limit keeps its payload as json_text, and 0 always does.
    The default is 16 KiB, named decode_options::default_retention_limit: a retained document measured up to 3.3 times the bytes of its text, so larger payloads stay text unless the caller raises the limit.
    decode and decode_batch take it. A batch keeps documents when its text is at most the limit times its number of events, and otherwise every event in it keeps text.
  • A payload kept as text above the limit is the input's own text of the data member, without the whitespace around it, rather than the codec's serialisation of it.
    Copying the slice avoids the serialisation, which measured 25 to 49 percent more instructions and 58 to 210 percent more allocated bytes in decode_large.
    The decoder falls back to the codec's serialisation when a top-level member name carries an escape, when data appears twice, or when the input holds something strict JSON does not allow; a batch decides per element.
    The text keeps the sender's spelling, so the same JSON formatted differently no longer compares equal as json_text after decode.
  • Encoding a json_document built by the encoding codec copies its DOM, with no serialisation and no parse.
    A document from another codec converts through the building codec's text.
  • data_as<T, Codec> reads a document built by Codec directly, through get<Codec>(), with no serialisation and no parse.
    A document from another codec still converts through text.
  • set_data<T, Codec> stores a json_document built by Codec, where v2 stored json_text.
    An event written by set_data therefore equals the same payload held as a document, never as text.
  • Typed entry points parse and write once.
    ce::decode_as<T, Codec>(text, options) returns ce::decoded<T>{.event, .payload}: the event decode returns, and the payload read from the parsed data member.
    ce::decode_batch_as does the same for a batch and fails the whole batch when one payload does not read as T; ce::from_value_as reads a document the caller parsed.
    ce::encode_as<T, Codec>(event, payload) writes the payload's DOM under data without changing the event, writing application/json when datacontenttype is absent and refusing a non-JSON one with type_mismatch.
    They fail as data_as does when the payload is absent (missing_required_attribute) or is not JSON (type_mismatch).
    The optional module exports them, and json::decode_options.
  • The optional module exports ce::v3 only.

CR-0003 and ADR-0010 record why: the v0.5.0 event model adds an alternative to data_t, and SPEC section 3 rule 4 sends a breaking change to a new generation.

Staying on v0.4.0 code

ce::X is now the v3 entity.
To keep the v0.4.0 declarations, spell ce::v2::X and include the v2/ copy of each header that has one:

#include <cloudevents/v2/binding/http.hpp>

using namespace ce::v2::literals;
auto order = ce::v2::event::builder{.id = "A1"_id, .source = "/orders"_source,
                                    .type = "com.example.placed"_type}.build();
auto request = ce::v2::http::to_message<ce::v2::codec::nlohmann_codec>(
    *order, ce::v2::content_mode::binary_mode);

Code that names only shared entities, such as an attribute type or a codec, needs no change.

Moving to ce::v3

  • data_t has a fifth alternative. Add a json_document case wherever you visit data_t; document.dump() gives its compact text.
    Code that read std::get<ce::json_text>(event.data()).raw after a structured decode now finds a json_document: read it with document.get<Codec>() for the DOM, or document.dump() for text.
    Code that compares a decoded event with one built from json_text now compares unequal; build the expected event with a json_document, or compare the payloads by value.
  • A codec you wrote needs five more members to serve ce::v3: equal, copy, extract, for_each_mutable_element and a static constexpr std::string_view identity, as listed above.
    static_assert(ce::json::json_codec<MyCodec>) fails until all five are present.
    Choose an identity under a domain you control; two codecs sharing one would hand each other's DOM to the wrong type.
    Without the five members the codec still serves ce::v1 and ce::v2 unchanged.
  • decode_options bounds what a decoded event pins, 16 KiB by default.
    decode, decode_batch, decode_as and decode_batch_as take ce::json::decode_options as a second argument; omitting it keeps a document for input up to decode_options::default_retention_limit (16 KiB) and text above it.
    Pass {.retain_document_up_to = 0} to keep the v2 behaviour of always decoding to text. The text is now the sender's own spelling of the payload, where v2 gave the codec's serialisation.
    Raise the limit when large payloads are read as typed values and memory allows it.
  • set_data<T, Codec> stores a document. Code that read std::get<ce::json_text>(event.data()) after set_data now finds a json_document; read the payload with data_as, or the text with dump().
  • Use the typed entry points. Replace decode followed by data_as with decode_as, decode_batch followed by a loop of data_as with decode_batch_as, and set_data on a copy followed by encode with encode_as, to save a parse or a serialisation.

Tooling

  • Every pull request is measured against main and against budgets.
    The perf workflow gates on instructions under Callgrind, heap allocations and the bytes a decoded event retains, per operation and codec, and comments the table on the pull request.
    Wall time and binary size are reported.
    Each merge to main is recorded on the bench-data branch.
    docs/PERFORMANCE.md explains the table and how to accept a deliberate cost.
  • The bench's Boost.JSON codec builds its values with parentheses, so it works with Boost before 1.84.

Faster, in every generation

  • A decode reads a document in one pass over its members, instead of one find per reserved attribute and a second walk for the extensions.
    Measured with bench/codec_bench before the perf job existed: 7 to 20 percent less CPU time for an event-sized document on all four bench codecs, 7 to 14 percent for a batch of 100.
  • The bindings stop copying bytes the SDK already holds.
    A s...
Read more

v0.4.0

Choose a tag to compare

@filipsajdak filipsajdak released this 23 Sep 14:57
b06a4c6

A second API generation, ce::v2, in which an invalid CloudEvent cannot be
written down. ce::v1 keeps what v0.3.0 published, so existing code has a way
to keep compiling.

Two generations

  • ce::v2 is the inline namespace, so ce::event now names the v2 class.
  • ce::v1 holds the v0.3.0 surface, under include/cloudevents/v1/: the
    aggregate event, validate(), headers, and the format and bindings built
    on them. A v1 declaration does not change; v1 takes defect fixes only.
  • What did not change is shared: errc, error, result, the codec
    concept, the three codecs, base64 and the describe seam are one type through
    ce::, ce::v1:: and ce::v2::. A codec written for v0.3.0 serves both.
  • The optional module exports ce::v2 only.

CR-0002 and ADR-0009 record why: SPEC section 3 rule 4 sends breaking changes to
a new generation, and v0.4.0's event model is one.

Staying on v0.3.0 code

Spell ce::v1:: and include the v1/ headers. Nothing else changes:

#include <cloudevents/v1/binding/http.hpp>

const ce::v1::event order{.id = "A1", .source = "/orders", .type = "com.example.placed"};
auto request = ce::v1::http::to_message<ce::v1::codec::nlohmann_codec>(
    order, ce::v1::content_mode::binary_mode);

Moving to ce::v2

v0.3.0 (ce::v1) v0.4.0 (ce::v2)
ce::event{.id = "1", .source = "/s", .type = "t", .subject = "x"} ce::event{"1"_id, "/s"_source, "t"_type, {.subject = "x"_subject}}
a runtime string assigned to a member ce::id::make(text) and its siblings, which return a result
if (auto ok = e.validate(); !ok) nothing to call: construction refuses what validate() refused
e.id, e.subject e.id(), e.subject(), returning the validated types
e.set_extension("name", value) returning result<void> e.set_extension("name"_ext, value), which cannot fail
e.datacontenttype = ...; e.data = ...; e.set_data(payload, "application/json"_mediatype)
ce::headers ce::raw_headers; ce::headers is now the type that refuses a repeated field
binding::read_attributes returning an event returns an event::builder; build() reports a missing attribute
binding::read_body(body, event&) read_body(body, media_type) returns the payload
ce::set_data<T, Codec> returning result<void> returns nothing, because it cannot fail
timestamp::fractional_digits as std::uint8_t a fraction_digits, refusing a count above nine

A literal is checked when the program compiles: ""_id does not build.
docs/GUIDE.md covers the whole v2 surface.

Fixed, in both generations

  • A timestamp with more than nine fractional digits divided by zero when
    rendered. v2 cannot hold one; v1 renders nine.
  • The RapidJSON codec handed a null pointer to RapidJSON for an empty string.
  • The result<void> polyfill answered error() on a success with errc{0}
    where std::expected leaves it undefined; a wrong-branch read now aborts.
  • base64 accepted any run of padding, so "QQ======" decoded.
  • A message carrying one ce- attribute twice decoded to whichever came last.
    It is now refused with invalid_argument.
  • A prefixed datacontenttype on HTTP or Kafka was accepted as an extension and
    then refused as a reserved name. It is now refused as invalid_argument.
  • The HTTP percent-encoding policy now refuses ill-formed UTF-8 on the way out,
    as it always did on the way in.

Added

  • docs/GUIDE.md, the user guide. Every C++ block in it is compiled and run in
    CI. CONTRIBUTING.md and SECURITY.md.
  • A clang-tidy gate over the public headers, including the magic-number checks.
  • check_references.py checks the // spec: markers in the headers against
    each requirement's satisfied_by, in both directions.

Removed

  • The Doxygen reference. The headers carry // spec: markers instead of doc
    comments, and the guide is the documentation.

Measured on the release candidate

  • Line coverage 95.8% (2416 of 2522), function coverage 92.5%, branch coverage
    60.3%, against a floor of 90% lines.
  • CI: GCC 13 at C++20 and C++23, Clang 16 with libstdc++ 13, Clang 17 with
    libc++ 17, AppleClang at C++20 and C++23, MSVC 19, GCC 16 with C++26 static
    reflection, plus the forced polyfill, exceptions disabled, no default codec,
    every codec on Linux and macOS, ASan with UBSan, and install-and-consume.
  • 45 CTest entries per preset: 27 for ce::v2 and 18 for ce::v1, of which
    16 are the v0.3.0 suites unchanged.

Known limitations

  • libc++ 17 and 18 implement std::format without defining __cpp_lib_format;
    the SDK carves them out by version (D-CI-1).
  • Clang 16 cannot compile libstdc++ 13 or 14 in C++23 mode, so the C++23 Clang
    job uses libc++ (D-CI-1).
  • The module interface is usable, with constraints on what an importing
    translation unit may also include (D-MODULE-1), and it exports ce::v2 only.
  • Given the same non-JSON payload, the Java SDK writes data_base64 where Go
    and this SDK write a JSON string. Both are legal (D-INTEROP-1).
  • HTTP binary mode percent-encodes header values as the binding requires; the Go
    and Java SDKs do not, so talking to them needs ce::http::literal_values
    (D-HTTP-1).
  • A ce::v1::timestamp can still be given more than nine fractional digits by
    hand. The renderer is guarded; the type is v0.3.0's and does not change.

v0.3.0

Choose a tag to compare

@filipsajdak filipsajdak released this 21 Sep 14:07
2f7ce0a

v0.3.0

NATS binary mode, and the reason it was missing.

All three bindings now support binary mode: HTTP, Kafka and NATS.

Added

  • ce::nats::to_message / from_message. Binary mode maps each attribute
    to a header named for it with a ce- prefix, percent-encoded by HTTP's rule.
    It needs a server at NATS 2.2 or later, which is where the protocol gained
    message headers.
  • ce::nats::detect_content_mode. The binding inverts HTTP's default: a
    CloudEvents content type means structured, and anything else means binary,
    including no content type at all.

to_payload and from_payload are unchanged and not deprecated. They remain
the right API for a server before 2.2, which cannot carry a header at all, and
ce::v1 is frozen against removal.

Two rules that differ from every other binding here

  • datacontenttype is not special. It maps to ce-datacontenttype like any
    other attribute, leaving Content-Type to mean that a message is structured.
    The shared binding core gained content_type_is_attribute for this,
    detected rather than required, so a binding_traits type written before
    it still satisfies the concept unchanged, including one outside this
    repository.
  • No batch mode, which the binding does not define.

Why this was missing

SWR-NATS-0001 forbade binary mode, quoting the v1.0.2 binding: "the NATS
protocol does not support custom message headers, necessary for binary mode".
NATS 2.2 introduced headers in 2021, and the binding now says "Every compliant
implementation SHOULD support both structured and binary modes".

The requirement was written against a frozen tag, and the tag moved. Nothing in
this repository's gates would catch that: they check a requirement is traceable
and verified, not that the document it was derived from still says the same
thing. Worth knowing if you rely on the traceability here.

Following the specification rather than the only implementation

sdk-go's protocol/nats_jetstream is the only other header-based NATS
mapping and differs from the binding on three rules: content-type unprefixed
instead of ce-datacontenttype, binary detected by ce-specversion presence
rather than content-type absence, and no percent-encoding where the binding
requires it. The third is the same defect as cloudevents/spec#1397 in a
second binding. Reported as cloudevents/sdk-go#1334.

This SDK follows the binding and deliberately ships no compatibility mode for
the divergence: no other SDK has a NATS binary mode yet, so there is nothing to
be compatible with, and adding one would make this the third behaviour rather
than the second.

JetStream

There is no JetStream binding in the CloudEvents specification, so there is
nothing named to implement. JetStream carries the same subjects, headers and
payloads as core NATS; persisting and delivering them is the client's, as
moving any message onto a wire is.

Verified on this commit

  • Seven fuzz targets, ten minutes each, no crash, leak or timeout. The NATS
    target now drives both decode paths.
  • AddressSanitizer and UndefinedBehaviorSanitizer clean over the whole suite.
  • 150 requirements lint and trace; 435 test suites resolve with no warnings.
  • Nine local presets and the full CI matrix, judged by exit code at each stage.
  • Go and Java accepted every document this SDK produced.

Known cost

Binary mode follows the NATS binding on main, marked 1.0.3-wip, while the
rest of the SDK implements v1.0.2. If it changes before 1.0.3 is cut, this
follows it (D-NATS-1).

Upgrading from v0.2.0

Nothing is removed and nothing changes behaviour, so code written against
v0.2.0 compiles and behaves as it did.

v0.2.0

Choose a tag to compare

@filipsajdak filipsajdak released this 21 Sep 12:25
9410bd3

v0.2.0

Three JSON codecs to choose between, three protocol bindings, and the cross-SDK
testing that found a defect in the first binding.

Additive throughout: ce::v1 gained entities and lost none, so code written
against v0.1.0 compiles unchanged.

Added

  • Codecs. ce::codec_rapidjson and ce::codec_boost_json beside nlohmann.
    CE_CODECS takes any set, or none; CE_DEFAULT_CODEC still works and now
    subtracts from the list. README.md says which to choose, from what bench/
    measured.
  • Kafka binding (ce::kafka): binary and structured mode, ce_ prefix,
    byte-exact record header keys, unescaped UTF-8 values, to_record with an
    opt-in partitionkey_mapper. No batch mode, which the binding spec does not
    define.
  • NATS binding (ce::nats): structured mode and the JSON event format,
    which is all the protocol supports. No subject is derived.
  • ce::binding: what the three bindings share, parameterised by a
    binding_traits type, so a fourth is its traits plus its entry points.
  • ce::http::literal_values: an opt-in value policy for talking to the Go
    and Java SDKs. The default still percent-encodes, as the binding spec
    requires.
  • headers::find_exact / contains_exact / set_exact, byte-exact beside
    the case-insensitive originals.
  • ce::to_bytes / ce::to_text are public.

Fixed

  • nlohmann_codec::as_int reports out_of_range for an unsigned integer past
    INT64_MAX rather than reinterpreting it. 2^63 used to arrive as a negative
    int32 that the format layer accepted.
  • Surrogate pairs combine into one UTF-8 character.
  • Boost.JSON values are constructed with parentheses; braces selected the
    initializer_list overload on Boost 1.83 and not on 1.92.

Interoperability, measured 2026-09-21

  • The HTTP binding spec requires percent-encoded header values. sdk-go
    v2.15.2 and sdk-java implement neither encoding nor decoding; sdk-csharp
    does.
    A conformant sender is therefore silently misread by Go and Java: an
    event whose subject is a b arrives as a%20b. Name
    ce::http::literal_values when talking to them. docs/DECISIONS.md D-HTTP-1
    has the tables, cloudevents/spec#1397 the cross-SDK picture.
  • An event that round-trips through the Go SDK loses its UTC offset. Go
    rewrites 2026-09-20T12:34:56+02:00 to ...10:34:56Z. The instant survives,
    the text does not, and ce::timestamp compares by value, so the same moment
    can compare unequal.
  • Of 21 binding-level cases run through sdk-go, 14 agree. The four that differ
    are in interop/audit/README.md, with which specification says who is right.

Verified on this commit

  • Coverage 95.9% (1160 of 1209 lines), floor 90%.
  • Seven fuzz targets, 649 to 678 seconds each, no crash, leak or timeout.
  • AddressSanitizer and UndefinedBehaviorSanitizer clean over the whole suite.
  • 150 requirements lint and trace; 434 test suites resolve with no warnings.
  • Go and Java accepted every document this SDK produced.
  • Built and tested on gcc-13 (C++20 and C++23), clang-16 (C++20, libstdc++ 13),
    clang-17 (C++23, libc++), AppleClang (C++20 and C++23), MSVC (C++20), GCC 16
    with -freflection for C++26 static reflection, and g++-14 for the forced
    result<T> polyfill and -fno-exceptions builds.

Known limitations

  • libc++ 17 and 18 implement std::format without defining
    __cpp_lib_format; the SDK carves them out by version (D-CI-1).
  • Clang 16 cannot compile libstdc++ 13 or 14 in C++23 mode, so the C++23 Clang
    job uses libc++ (D-CI-1).
  • The module interface is usable, with constraints on what an importing
    translation unit may also include (D-MODULE-1).
  • Given the same non-JSON payload, the Java SDK writes data_base64 where Go
    and this SDK write a JSON string. Both are legal (D-INTEROP-1).
  • Boost.JSON requires exceptions; the build refuses it under -fno-exceptions
    at configure time.

cloudevents-cpp v0.1.0

Choose a tag to compare

@filipsajdak filipsajdak released this 20 Sep 16:23
f19976f

CloudEvents v1.0.2 for C++20: the core event model, the JSON event format, the HTTP protocol binding, and a typed layer over both.

Header-only, no exceptions, no network I/O. The SDK names no HTTP type and picks no JSON library for you — ce::json::json_codec is a concept, and an nlohmann codec ships beside it.

This tag freezes the ce::v1 API. A breaking change after it introduces a new version namespace (SWR-BUILD-0006).

#include <cloudevents/binding/http.hpp>
#include <cloudevents/codec/nlohmann.hpp>

using codec = ce::codec::nlohmann_codec;

ce::event order{
    .id = "A234-1234-1234",
    .source = ce::uri_ref{"https://example.test/orders"},
    .type = "com.example.order.placed",
};
order.datacontenttype = "application/json";
order.data = ce::json_text{.raw = R"({"total":42})"};

auto request = ce::http::to_message<codec>(order, ce::content_mode::binary_mode);

Measured on this commit, not asserted

Line coverage 95.9% (1021 of 1065)
Fuzzing 5 targets, 10 minutes each, 338.3M executions, clean
Sanitizers full suite under ASan and UBSan, clean
Interoperability the Go and Java SDKs accept every document this SDK produces, and their goldens decode here
Conformance all 19 examples from the CloudEvents core, JSON format and HTTP binding documents, stored verbatim
Requirements 122, every one with test evidence that resolves

Per-target fuzz executions: timestamp 186.4M, base64 123.9M, json_decode 6.3M, http_message 14.2M, typed 7.4M.

Toolchains exercised

GCC 13 and 16, Clang 16 with libstdc++ 13, Clang 17 with libc++ 17, AppleClang, MSVC 19. C++20 and C++23, with and without std::expected, with and without exceptions, with and without the default codec, plus C++26 static reflection.

What is in it

  • Three content modes — binary, structured and batched, both directions, over a transport-neutral ce::message.
  • Typed extensions — the five documented CloudEvents extensions as structs. event.get<ce::ext::tracing>() restores the declared field type even when the wire form threw it away, which HTTP binary mode always does.
  • Typed payloads — describe your own struct once: set_data<order>(event, value), data_as<order>(event), event_of<order, codec>.
  • Your JSON library — a codec is a struct of statics over your DOM; examples/custom_codec.cpp is a complete one.
  • Four targets with a strictly downward dependency graph. ce::core depends on CTRE alone, checked by a test built against the installed package.

Installing

cmake -S . -B build && cmake --build build --target install
find_package(cloudevents REQUIRED)                            # core, format, binding
find_package(cloudevents REQUIRED COMPONENTS codec_nlohmann)  # and the codec

Known limitations

Each recorded in docs/DECISIONS.md with the measurement behind it.

  • D-CI-1 — libc++ 17 and 18 implement std::format without defining __cpp_lib_format, so the SDK carves them out by version. Clang 16 cannot compile libstdc++ 13 or 14 in C++23 mode at all.
  • D-MODULE-1 — cloudevents.cppm is usable, but an importing translation unit may not also include a standard header its global module fragment absorbed. It stays out of the default build.
  • D-INTEROP-1 — given the same non-JSON payload, the Java SDK writes data_base64 where Go and this SDK write a JSON string. Both are legal. Compare bytes, not representation.
  • D-ADOPT-1 — nlohmann is looked up softly, so find_package(cloudevents) works without it. Ask for the codec_nlohmann component when you need the codec.

Licence

Apache-2.0, matching the other CloudEvents SDKs.