diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 5915882..8169398 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -129,8 +129,8 @@ jobs: NEXT_VERSION: ${{ steps.version.outputs.next_version }} run: | set -euo pipefail - perl -0pi -e 's/(\[package\][\s\S]*?\nversion = ")[^"]+(")/$1$ENV{NEXT_VERSION}$2/' Cargo.toml - perl -0pi -e 's/(\[package\][\s\S]*?\nversion = ")[^"]+(")/$1$ENV{NEXT_VERSION}$2/' crates/tinydocs-module/Cargo.toml + perl -0pi -e 's/(\[workspace\.package\][\s\S]*?\nversion = ")[^"]+(")/$1$ENV{NEXT_VERSION}$2/' Cargo.toml + cargo update -p tinydocs-bus --precise "$NEXT_VERSION" cargo update -p "$CRATE_NAME" --precise "$NEXT_VERSION" - name: Commit version bump and tag @@ -140,12 +140,21 @@ jobs: set -euo pipefail git config user.name "github-actions[bot]" git config user.email "41898282+github-actions[bot]@users.noreply.github.com" - git add Cargo.toml Cargo.lock crates/tinydocs-module/Cargo.toml + git add Cargo.toml Cargo.lock git commit -m "Release ${RELEASE_TAG}" git tag -a "${RELEASE_TAG}" -m "Release ${RELEASE_TAG}" - - name: Package crate - run: cargo package --locked --package tinydocs + - name: Package and publish the bus contract + run: | + set -euo pipefail + cargo package --locked --package tinydocs-bus + cargo publish --locked --package tinydocs-bus + + - name: Package and publish crate + run: | + set -euo pipefail + cargo package --locked --package tinydocs + cargo publish --locked --package tinydocs - name: Package TinyBus source and module SDK run: | @@ -162,6 +171,7 @@ jobs: with: name: source-packages path: | + target/package/tinydocs-bus-${{ steps.version.outputs.next_version }}.crate target/package/${{ steps.version.outputs.crate_name }}-${{ steps.version.outputs.next_version }}.crate target/package/tinybus-source-*.tar.gz if-no-files-found: error diff --git a/AGENTS.md b/AGENTS.md index 957873b..cd213e3 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -32,13 +32,13 @@ This is a Rust 2024 library crate rooted at `Cargo.toml`. ```text src/ ├── lib.rs # crate docs + the entire public re-export surface -├── error/mod.rs # crate-wide `Error` and `Result` └── / # one directory per feature area ├── mod.rs # module docs, wiring, smallest useful public API ├── types.rs # substantial type definitions └── test.rs # module-local unit tests tests/ # integration tests against the public API only examples/ # runnable, compiled-in-CI usage examples +crates/tinydocs-bus/ # TinyBus wire contract: names, payload types, errors crates/tinydocs-module/ # private TinyBus cdylib adapter vendor/tinybus/ # pinned TinyBus source; optional until wired by a project docs/ @@ -64,8 +64,9 @@ missing module. Prefer many small modules that each do one thing well over few broad ones. Keep public exports centralized in `src/lib.rs` so downstream users have one -predictable surface. Put shared error variants in `src/error/mod.rs` and return -the crate-wide `Result` from fallible public APIs. +predictable surface. The shared error variants live in `tinydocs-bus` because +`DocumentSpec::validate` is an inherent method on a contract-owned type; re- +export its `Error` and `Result` from `src/lib.rs` for the primary API. ## Build And Test @@ -114,8 +115,9 @@ Use standard `rustfmt` output and Rust 2024 idioms. Do not hand-format around ### Errors -- One crate-wide `Error` enum in `src/error/mod.rs`, built with `thiserror`. -- Fallible public functions return `Result`, the crate alias. +- One crate-wide `Error` enum in `crates/tinydocs-bus/src/error/mod.rs`, built with + `thiserror` and re-exported by `tinydocs`. +- Fallible public functions return `Result`, the re-exported crate alias. - Add a specific variant instead of stuffing context into a string; error messages are lowercase, without trailing punctuation. - Do not `unwrap()`, `expect()`, or `panic!` in library code paths. They are diff --git a/Cargo.lock b/Cargo.lock index 542b333..0ea7f46 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -37,56 +37,6 @@ dependencies = [ "memchr", ] -[[package]] -name = "anstream" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d" -dependencies = [ - "anstyle", - "anstyle-parse", - "anstyle-query", - "anstyle-wincon", - "colorchoice", - "is_terminal_polyfill", - "utf8parse", -] - -[[package]] -name = "anstyle" -version = "1.0.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" - -[[package]] -name = "anstyle-parse" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e" -dependencies = [ - "utf8parse", -] - -[[package]] -name = "anstyle-query" -version = "1.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" -dependencies = [ - "windows-sys 0.61.2", -] - -[[package]] -name = "anstyle-wincon" -version = "3.0.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" -dependencies = [ - "anstyle", - "once_cell_polyfill", - "windows-sys 0.61.2", -] - [[package]] name = "anyhow" version = "1.0.104" @@ -266,9 +216,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.4.2" +version = "1.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d262e149917187838d5b42777c8253bcb64500067342904e7d429499a6f277e" +checksum = "0ad534f4357a5264cce5019c989cf66a4f0dc4e0d1b1d15f8aacec0ff7360273" dependencies = [ "find-msvc-tools", "jobserver", @@ -309,58 +259,12 @@ dependencies = [ "inout", ] -[[package]] -name = "clap" -version = "4.6.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "473c7e07f409a8d772161724aa8db6a765a2532a70f9667eeb7b49d3d02fbdca" -dependencies = [ - "clap_builder", - "clap_derive", -] - -[[package]] -name = "clap_builder" -version = "4.6.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b48fea5a88e9ae728a2dcbedbfc0e730f7d60da42e1cb049a83c9fb8b789889" -dependencies = [ - "anstream", - "anstyle", - "clap_lex", - "strsim", -] - -[[package]] -name = "clap_derive" -version = "4.6.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d012d2b9d65aca7f18f4d9878a045bc17899bba951561ba5ec3c2ba1eed9a061" -dependencies = [ - "heck", - "proc-macro2", - "quote", - "syn 3.0.3", -] - -[[package]] -name = "clap_lex" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9" - [[package]] name = "color_quant" version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b" -[[package]] -name = "colorchoice" -version = "1.0.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" - [[package]] name = "constant_time_eq" version = "0.1.5" @@ -387,9 +291,9 @@ dependencies = [ [[package]] name = "crc32fast" -version = "1.5.0" +version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" +checksum = "8498c871161e1742aaa9d52551b2d6ebdd4c3d45a3be423e3728f33b955be550" dependencies = [ "cfg-if", ] @@ -468,7 +372,7 @@ dependencies = [ "serde", "serde_json", "smallvec", - "thiserror 2.0.20", + "thiserror", "zip 8.6.0", ] @@ -568,9 +472,9 @@ dependencies = [ [[package]] name = "find-msvc-tools" -version = "0.1.10" +version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26b73573e6edcd2af0cdf47bd6cb58f0b3839491263c314eaad1ccf24430e1de" +checksum = "d45db016d36b838f563236e9193d0ee6ce38f3f68b6c94e914b4929c96bbb890" [[package]] name = "flate2" @@ -600,21 +504,21 @@ dependencies = [ [[package]] name = "futures-core" -version = "0.3.33" +version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2cd50c473c80f6d7c3670a752354b8e569b1a7cbfdc0419ec88e5edad85e0dc7" +checksum = "92d699e522242e69e3003b94ecc1f960f3a5e015aa7c5d7486e65ad01dd94f5e" [[package]] name = "futures-task" -version = "0.3.33" +version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b231ed28831efb4a61a08580c4bc233ec56bc009f4cd8f52da2c3cb97df0c109" +checksum = "cd417de3d1d015fc3bfd2b1ea46dfc7bab72ef86f1cc7cc9c78e728b34a6d1fd" [[package]] name = "futures-util" -version = "0.3.33" +version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a77a90a256fce34da66415271e30f94ee91c57b04b8a2c042d9cf3220179deaa" +checksum = "0d50a92467f8ba5dd6e3ee5d4bd04d73ab2e4e1c44474a0674821dfce14b79bc" dependencies = [ "futures-core", "futures-task", @@ -696,12 +600,6 @@ version = "0.17.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" -[[package]] -name = "heck" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" - [[package]] name = "hmac" version = "0.12.1" @@ -729,9 +627,9 @@ checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" [[package]] name = "image" -version = "0.25.9" +version = "0.25.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6506c6c10786659413faa717ceebcb8f70731c0a60cbae39795fdf114519c1a" +checksum = "85ab80394333c02fe689eaf900ab500fbd0c2213da414687ebf995a65d5a6104" dependencies = [ "bytemuck", "byteorder-lite", @@ -741,8 +639,8 @@ dependencies = [ "num-traits", "png", "tiff", - "zune-core 0.5.3", - "zune-jpeg 0.5.15", + "zune-core", + "zune-jpeg", ] [[package]] @@ -765,12 +663,6 @@ dependencies = [ "generic-array", ] -[[package]] -name = "is_terminal_polyfill" -version = "1.70.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" - [[package]] name = "itoa" version = "1.0.18" @@ -830,9 +722,9 @@ checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" [[package]] name = "log" -version = "0.4.33" +version = "0.4.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad" +checksum = "f9f8bd3e56ce4dfc153cf470fffbfa98c7620958b312ca5c3a4b8d5181fd13c6" [[package]] name = "lopdf" @@ -856,7 +748,7 @@ dependencies = [ "rangemap", "sha2", "stringprep", - "thiserror 2.0.20", + "thiserror", "ttf-parser", "weezl", ] @@ -889,9 +781,9 @@ dependencies = [ [[package]] name = "moxcms" -version = "0.7.11" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac9557c559cd6fc9867e122e20d2cbefc9ca29d80d027a8e39310920ed2f0a97" +checksum = "bb85c154ba489f01b25c0d36ae69a87e4a1c73a72631fc6c0eb6dde34a73e44b" dependencies = [ "num-traits", "pxfm", @@ -927,12 +819,6 @@ version = "1.21.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" -[[package]] -name = "once_cell_polyfill" -version = "1.70.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" - [[package]] name = "password-hash" version = "0.4.2" @@ -975,20 +861,18 @@ dependencies = [ [[package]] name = "pdfrs" -version = "0.1.9" +version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd2c58cc563c54ee2dc0dacce61bf686529d792c4b76f031cbcfe8e3f7ecdeb0" +checksum = "f5b88e8a50ad5ca1dcd50cfdd17fbfc3159b804a206497c61f30083460200fe4" dependencies = [ "aes", "anyhow", - "base64 0.22.1", "cbc", - "clap", "flate2", + "getrandom 0.3.4", "md-5", "regex", "serde", - "serde_json", "sha2", "subsetter", "syntect", @@ -1009,9 +893,9 @@ checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" [[package]] name = "pkg-config" -version = "0.3.33" +version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e" +checksum = "f6b464fbc74e149a392436b17d523f769e057cb6877f6a5c4618bc6f11800548" [[package]] name = "plist" @@ -1068,12 +952,12 @@ checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" [[package]] name = "ppt-rs" -version = "0.2.24" +version = "0.2.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed6af693d661395ff3464eac5f7cee1df674d082ce84da94c6819cc799fee929" +checksum = "eecd6e36aee13fd7e01d5e12c06be703906a51a41b5b0612a1af8569b18d7df4" dependencies = [ "pdfrs", - "thiserror 1.0.69", + "thiserror", "uuid", "xml-rs", "zip 0.6.6", @@ -1156,9 +1040,9 @@ checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69" [[package]] name = "rangemap" -version = "1.7.1" +version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "973443cf09a9c8656b574a866ab68dfa19f0867d0340648c7d2f6a71b8a8ea68" +checksum = "a611d15b50743feb4c76b7d03edcb0e64f399c26961e4efe6975bc398be6aa3d" [[package]] name = "read-fonts" @@ -1258,9 +1142,9 @@ dependencies = [ [[package]] name = "rustls-webpki" -version = "0.103.13" +version = "0.103.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61c429a8649f110dddef65e2a5ad240f747e85f7758a6bccc7e5777bd33f756e" +checksum = "f3c3cf1d8b1e7d4927e2d154c3fcb02979afb9939629c62cd9048d4f07b60ac2" dependencies = [ "ring", "rustls-pki-types", @@ -1401,12 +1285,6 @@ dependencies = [ "unicode-properties", ] -[[package]] -name = "strsim" -version = "0.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" - [[package]] name = "subsetter" version = "0.2.6" @@ -1463,7 +1341,7 @@ dependencies = [ "serde", "serde_derive", "serde_json", - "thiserror 2.0.20", + "thiserror", "walkdir", "yaml-rust", ] @@ -1492,33 +1370,13 @@ dependencies = [ "windows-sys 0.61.2", ] -[[package]] -name = "thiserror" -version = "1.0.69" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" -dependencies = [ - "thiserror-impl 1.0.69", -] - [[package]] name = "thiserror" version = "2.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec86235f5fcc2a73650310756d2ac5b138a5780bbbdfae3eeccec992c435ba4f" dependencies = [ - "thiserror-impl 2.0.20", -] - -[[package]] -name = "thiserror-impl" -version = "1.0.69" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.119", + "thiserror-impl", ] [[package]] @@ -1534,16 +1392,16 @@ dependencies = [ [[package]] name = "tiff" -version = "0.10.3" +version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af9605de7fee8d9551863fd692cce7637f548dbd9db9180fcc07ccc6d26c336f" +checksum = "b63feaf3343d35b6ca4d50483f94843803b0f51634937cc2ec519fc32232bc52" dependencies = [ "fax", "flate2", "half", "quick-error", "weezl", - "zune-jpeg 0.4.21", + "zune-jpeg", ] [[package]] @@ -1586,7 +1444,7 @@ dependencies = [ "serde_json", "tar", "tempfile", - "thiserror 2.0.20", + "thiserror", "tinybus-macros", "tokio", "toml", @@ -1623,12 +1481,20 @@ dependencies = [ "docx-rs", "pdf-extract", "ppt-rs", - "serde", "serde_json", - "thiserror 2.0.20", + "tinydocs-bus", "zip 8.6.0", ] +[[package]] +name = "tinydocs-bus" +version = "0.1.13" +dependencies = [ + "serde", + "serde_json", + "thiserror", +] + [[package]] name = "tinydocs-module" version = "0.1.13" @@ -1638,10 +1504,11 @@ dependencies = [ "serde", "serde_json", "sha2", - "thiserror 2.0.20", + "thiserror", "tinybus", "tinybus-module", "tinydocs", + "tinydocs-bus", "tokio", ] @@ -1849,17 +1716,11 @@ version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b8c0a043c9540bae7c578c88f91dda8bd82e59ae27c21baca69c8b191aaf5a6e" -[[package]] -name = "utf8parse" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" - [[package]] name = "uuid" -version = "1.24.0" +version = "1.25.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf3923a6f5c4c6382e0b653c4117f48d631ea17f38ed86e2a828e6f7412f5239" +checksum = "f053576934f05a761a402421fbbe3d425d9366f75f978806a037b3ca481abecc" dependencies = [ "getrandom 0.4.3", "js-sys", @@ -2166,7 +2027,7 @@ dependencies = [ "flate2", "indexmap", "memchr", - "thiserror 2.0.20", + "thiserror", "zopfli", ] @@ -2237,32 +2098,17 @@ dependencies = [ "pkg-config", ] -[[package]] -name = "zune-core" -version = "0.4.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f423a2c17029964870cfaabb1f13dfab7d092a62a29a89264f4d36990ca414a" - [[package]] name = "zune-core" version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d56377fd46368984a170bc5aac5567e52ca5da874caa60bea39fcbca78fb658b" -[[package]] -name = "zune-jpeg" -version = "0.4.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29ce2c8a9384ad323cf564b67da86e21d3cfdff87908bc1223ed5c99bc792713" -dependencies = [ - "zune-core 0.4.12", -] - [[package]] name = "zune-jpeg" version = "0.5.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "27bc9d5b815bc103f142aa054f561d9187d191692ec7c2d1e2b4737f8dbd7296" dependencies = [ - "zune-core 0.5.3", + "zune-core", ] diff --git a/Cargo.toml b/Cargo.toml index 4090260..acae9d1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "tinydocs" -version = "0.1.13" +version.workspace = true edition = "2024" rust-version = "1.88" license = "GPL-3.0-only" @@ -22,19 +22,19 @@ exclude = [ ] [workspace] -members = ["crates/tinydocs-module"] -default-members = [".", "crates/tinydocs-module"] +members = ["crates/tinydocs-bus", "crates/tinydocs-module"] +default-members = [".", "crates/tinydocs-bus", "crates/tinydocs-module"] exclude = ["vendor/tinybus"] resolver = "3" +[workspace.package] +# Releases update this sole workspace version before packaging every crate. +version = "0.1.13" + [dependencies] -# Derive macros for the crate-wide error type in `src/error/mod.rs`. Every -# dependency entry should carry a comment like this one saying why it is here. -thiserror = "2" -# The document spec types are the wire contract a host exposes to an LLM as a -# JSON tool schema, so they derive Serialize/Deserialize here rather than -# forcing every host to re-declare them. -serde = { version = "1", features = ["derive"] } +# The dependency-light TinyBus contract owns payload types and errors. Re-export +# it so library and bus callers use the exact same values. +tinydocs-bus = { version = "0.1.13", path = "crates/tinydocs-bus" } # OOXML `.docx` synthesis. Optional: exclusive to the `docx` feature so a host # that only needs extraction does not pull the writer stack. docx-rs = { version = "0.4.20", optional = true } diff --git a/README.md b/README.md index 43023c6..26b3dec 100644 --- a/README.md +++ b/README.md @@ -114,8 +114,13 @@ compiling. ## TinyBus module +`tinydocs-bus` is the dependency-light contract crate for TinyBus hosts. It +contains the bus identity, member names, versioning, and `DocumentSpec` types, +but no TinyBus transport or document writer. `tinydocs` re-exports the same +types, so existing `tinydocs::docx` callers remain source-compatible. + The private `tinydocs-module` workspace crate builds TinyDocs as a trusted -in-process TinyBus module while keeping the published library bus-agnostic: +in-process TinyBus module: ```sh cargo build --release --package tinydocs-module @@ -190,14 +195,6 @@ way, so the contract and its validation survive any combination. ```text src/ ├── lib.rs # crate docs + the entire public re-export surface -├── error/ -│ ├── mod.rs # crate-wide `Error` and `Result` -│ └── test.rs -├── spec/ # wire contracts — ungated, serde only -│ ├── mod.rs # re-export surface -│ ├── document/ # `DocumentSpec`, `DocumentSection`, limits, `validate` -│ ├── presentation/ # `PresentationSpec`, `SlideSpec`, `SlideImage`, limits -│ └── image/ # `ImageFormat` — PNG/JPEG sniffing + header measurement ├── docx/ │ ├── mod.rs # `generate` — the `WordprocessingML` mapping │ └── test.rs @@ -210,6 +207,7 @@ src/ tests/ └── public_api.rs # integration tests against the public API only crates/ +├── tinydocs-bus/ # TinyBus names, types, errors, and wire contract └── tinydocs-module/ # private TinyBus cdylib adapter + loader E2E test examples/ └── basic.rs # compiled and linted in CI diff --git a/crates/tinydocs-bus/Cargo.toml b/crates/tinydocs-bus/Cargo.toml new file mode 100644 index 0000000..ffc6dc9 --- /dev/null +++ b/crates/tinydocs-bus/Cargo.toml @@ -0,0 +1,25 @@ +[package] +name = "tinydocs-bus" +version.workspace = true +edition = "2024" +rust-version = "1.88" +license = "GPL-3.0-only" +description = "TinyBus wire contract for TinyDocs: names, payload types, and errors." +repository = "https://github.com/tinyhumansai/tinydocs" +documentation = "https://docs.rs/tinydocs-bus" +readme = "README.md" +keywords = ["docx", "tinybus", "document", "agent"] +categories = ["data-structures"] + +[dependencies] +# Document specifications cross the TinyBus boundary as typed Serde payloads. +serde = { version = "1", features = ["derive"] } +# The contract owns structured validation and generation error variants. +thiserror = "2" + +[dev-dependencies] +# Contract tests ensure the JSON vocabulary stays compatible for bus hosts. +serde_json = "1" + +[lints] +workspace = true diff --git a/crates/tinydocs-bus/README.md b/crates/tinydocs-bus/README.md new file mode 100644 index 0000000..81651cb --- /dev/null +++ b/crates/tinydocs-bus/README.md @@ -0,0 +1,16 @@ +# tinydocs-bus + +The transport-free TinyBus contract for TinyDocs: its well-known bus identity, +member names, contract version, shared errors, and every document payload type +that crosses the bus boundary. + +`tinydocs-bus` deliberately does not depend on `tinybus`, an async runtime, or +the OOXML writer. A host that only calls the module can depend on this crate to +construct `DocumentSpec` values without compiling the document implementation +or the loadable module. `tinydocs` depends on and re-exports the same types, so +`tinydocs::docx::DocumentSpec` and `tinydocs_bus::spec::DocumentSpec` are one +type, not compatible-looking duplicates. + +The module serves the `METHODS` at `BUS_NAME` and `OBJECT_PATH`. Keep changes +here backward compatible or advance `CONTRACT_VERSION` according to the +documented compatibility rule. diff --git a/src/error/mod.rs b/crates/tinydocs-bus/src/error/mod.rs similarity index 100% rename from src/error/mod.rs rename to crates/tinydocs-bus/src/error/mod.rs diff --git a/crates/tinydocs-bus/src/error/test.rs b/crates/tinydocs-bus/src/error/test.rs new file mode 100644 index 0000000..8fc3762 --- /dev/null +++ b/crates/tinydocs-bus/src/error/test.rs @@ -0,0 +1,36 @@ +//! Unit tests for the shared `TinyDocs` error contract. + +#![allow(clippy::panic)] + +use super::Error; + +#[test] +fn long_details_are_truncated_without_splitting_utf8() { + let raw = "🦀".repeat(Error::MAX_DETAIL_CHARS * 2); + let Error::GenerationFailed { detail } = Error::generation_failed(&raw) else { + panic!("expected GenerationFailed"); + }; + assert_eq!(detail.chars().count(), Error::MAX_DETAIL_CHARS); + assert!(detail.ends_with("[…truncated]")); +} + +#[test] +fn extraction_errors_use_the_shared_truncation_bound() { + let raw = "x".repeat(Error::MAX_DETAIL_CHARS * 2); + let Error::ExtractionFailed { detail } = Error::extraction_failed(&raw) else { + panic!("expected ExtractionFailed"); + }; + assert_eq!(detail.chars().count(), Error::MAX_DETAIL_CHARS); +} + +#[test] +fn invalid_input_preserves_the_field_path() { + let error = Error::invalid_input("sections[2].bullets[0]", "must be ≤ 10 chars"); + assert_eq!( + error, + Error::InvalidInput { + field: "sections[2].bullets[0]".to_string(), + reason: "must be ≤ 10 chars".to_string(), + } + ); +} diff --git a/crates/tinydocs-bus/src/lib.rs b/crates/tinydocs-bus/src/lib.rs new file mode 100644 index 0000000..c7f776e --- /dev/null +++ b/crates/tinydocs-bus/src/lib.rs @@ -0,0 +1,23 @@ +//! The transport-free `TinyBus` wire contract for `TinyDocs`. +//! +//! A `TinyBus` host loads the `tinydocs-module` dynamic library, but it cannot +//! import Rust types from that binary. This crate supplies the shared +//! vocabulary: bus identity, member names, versioning, and the serializable +//! document specification. It deliberately has no dependency on `tinybus`, an +//! async runtime, or document generation. +//! +//! `tinydocs` depends on and re-exports these types, so +//! `tinydocs::docx::DocumentSpec` and [`spec::DocumentSpec`] are identical. + +pub mod error; +pub mod names; +pub mod spec; +pub mod version; + +pub use error::{Error, Result}; +pub use names::{BUS_NAME, METHODS, OBJECT_PATH}; +pub use spec::{ + DocumentSection, DocumentSpec, ImageFormat, PresentationSpec, SlideImage, SlideSpec, + WirePresentationSpec, WireSlideImage, WireSlideSpec, +}; +pub use version::{CONTRACT_VERSION, is_compatible}; diff --git a/crates/tinydocs-bus/src/names.rs b/crates/tinydocs-bus/src/names.rs new file mode 100644 index 0000000..af0e444 --- /dev/null +++ b/crates/tinydocs-bus/src/names.rs @@ -0,0 +1,30 @@ +//! `TinyDocs` bus identity and member names. + +/// Well-known bus name exported by the `TinyDocs` module. +pub const BUS_NAME: &str = "ai.tinyhumans.tinydocs.Documents"; + +/// Object path served by the `TinyDocs` module. +pub const OBJECT_PATH: &str = "/ai/tinyhumans/tinydocs/Documents"; + +/// One constant per method name on [`BUS_NAME`]. +pub mod methods { + /// `GenerateDocx` — generate a complete DOCX payload. + pub const GENERATE_DOCX: &str = "GenerateDocx"; + /// `GeneratePptx` — generate a complete PPTX payload. + pub const GENERATE_PPTX: &str = "GeneratePptx"; + /// `ExtractText` — extract text from a streamed PDF. + pub const EXTRACT_TEXT: &str = "ExtractText"; + /// `ReadOutput` — read a bounded base64-encoded output chunk. + pub const READ_OUTPUT: &str = "ReadOutput"; + /// `ReleaseOutput` — release a held output. + pub const RELEASE_OUTPUT: &str = "ReleaseOutput"; +} + +/// All method names in the declaration order used by the module interface. +pub const METHODS: [&str; 5] = [ + methods::GENERATE_DOCX, + methods::GENERATE_PPTX, + methods::EXTRACT_TEXT, + methods::READ_OUTPUT, + methods::RELEASE_OUTPUT, +]; diff --git a/src/spec/document/mod.rs b/crates/tinydocs-bus/src/spec/document/mod.rs similarity index 100% rename from src/spec/document/mod.rs rename to crates/tinydocs-bus/src/spec/document/mod.rs diff --git a/src/spec/document/test.rs b/crates/tinydocs-bus/src/spec/document/test.rs similarity index 100% rename from src/spec/document/test.rs rename to crates/tinydocs-bus/src/spec/document/test.rs diff --git a/src/spec/image/mod.rs b/crates/tinydocs-bus/src/spec/image/mod.rs similarity index 100% rename from src/spec/image/mod.rs rename to crates/tinydocs-bus/src/spec/image/mod.rs diff --git a/src/spec/image/test.rs b/crates/tinydocs-bus/src/spec/image/test.rs similarity index 100% rename from src/spec/image/test.rs rename to crates/tinydocs-bus/src/spec/image/test.rs diff --git a/src/spec/mod.rs b/crates/tinydocs-bus/src/spec/mod.rs similarity index 100% rename from src/spec/mod.rs rename to crates/tinydocs-bus/src/spec/mod.rs diff --git a/src/spec/presentation/mod.rs b/crates/tinydocs-bus/src/spec/presentation/mod.rs similarity index 100% rename from src/spec/presentation/mod.rs rename to crates/tinydocs-bus/src/spec/presentation/mod.rs diff --git a/src/spec/presentation/test.rs b/crates/tinydocs-bus/src/spec/presentation/test.rs similarity index 100% rename from src/spec/presentation/test.rs rename to crates/tinydocs-bus/src/spec/presentation/test.rs diff --git a/src/spec/presentation/wire.rs b/crates/tinydocs-bus/src/spec/presentation/wire.rs similarity index 100% rename from src/spec/presentation/wire.rs rename to crates/tinydocs-bus/src/spec/presentation/wire.rs diff --git a/crates/tinydocs-bus/src/version.rs b/crates/tinydocs-bus/src/version.rs new file mode 100644 index 0000000..8472dbd --- /dev/null +++ b/crates/tinydocs-bus/src/version.rs @@ -0,0 +1,13 @@ +//! Wire-contract versioning for `TinyDocs` hosts. + +/// Current `TinyDocs` wire-contract version. +pub const CONTRACT_VERSION: u32 = 2; + +/// Returns whether a host requiring `required` can bind to this contract. +/// +/// The first contract revision supports only exact-version bindings. A future +/// backward-compatible revision may widen this rule deliberately. +#[must_use] +pub const fn is_compatible(required: u32) -> bool { + required == CONTRACT_VERSION +} diff --git a/crates/tinydocs-module/Cargo.toml b/crates/tinydocs-module/Cargo.toml index ca2d47b..5b2879d 100644 --- a/crates/tinydocs-module/Cargo.toml +++ b/crates/tinydocs-module/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "tinydocs-module" -version = "0.1.13" +version.workspace = true edition = "2024" rust-version = "1.88" license = "GPL-3.0-only" @@ -14,6 +14,8 @@ crate-type = ["rlib", "cdylib"] [dependencies] # The pure document library remains independently publishable and bus-agnostic. tinydocs = { path = "../..", default-features = false, features = ["docx", "pptx", "pdf"] } +# The transport-free vocabulary shared with every TinyBus host. +tinydocs-bus = { path = "../tinydocs-bus" } # TinyBus provides the typed service interface and dynamic module host ABI. tinybus = { version = "0.1.0", path = "../../vendor/tinybus/crates/tinybus", default-features = false, features = ["macros", "modules"] } # The module-side SDK owns its runtime and exports the stable C entrypoints. diff --git a/crates/tinydocs-module/src/lib.rs b/crates/tinydocs-module/src/lib.rs index 43fe84c..fb61bbe 100644 --- a/crates/tinydocs-module/src/lib.rs +++ b/crates/tinydocs-module/src/lib.rs @@ -8,4 +8,8 @@ pub mod outputs; mod service; pub use outputs::{OutputError, OutputRef, OutputStore, hex_digest}; -pub use service::{BUS_NAME, OBJECT_PATH, WirePresentationSpec, WireSlideImage, WireSlideSpec}; +pub use tinydocs_bus::{ + BUS_NAME, CONTRACT_VERSION, DocumentSection, DocumentSpec, Error, ImageFormat, METHODS, + OBJECT_PATH, PresentationSpec, Result, SlideImage, SlideSpec, WirePresentationSpec, + WireSlideImage, WireSlideSpec, is_compatible, +}; diff --git a/crates/tinydocs-module/src/service/mod.rs b/crates/tinydocs-module/src/service/mod.rs index 93ada22..3ad2fa0 100644 --- a/crates/tinydocs-module/src/service/mod.rs +++ b/crates/tinydocs-module/src/service/mod.rs @@ -55,16 +55,11 @@ use tinybus::{Connection, Error as BusError, Result as BusResult}; use tinydocs::spec::presentation::MAX_IMAGE_BYTES; use tinydocs::spec::{DocumentSpec, PresentationSpec, SlideImage, SlideSpec}; use tinydocs::{Error, pdf, pptx}; +use tinydocs_bus::{BUS_NAME, OBJECT_PATH}; use crate::outputs::{OutputError, OutputRef, OutputStore}; -pub use tinydocs::spec::{WirePresentationSpec, WireSlideImage, WireSlideSpec}; - -/// Well-known name and interface exported by the `TinyDocs` module. -pub const BUS_NAME: &str = "ai.tinyhumans.tinydocs.Documents"; - -/// Object path exported by the `TinyDocs` module. -pub const OBJECT_PATH: &str = "/ai/tinyhumans/tinydocs/Documents"; +use tinydocs_bus::WirePresentationSpec; const INVALID_INPUT_ERROR: &str = "ai.tinyhumans.tinydocs.Error.InvalidInput"; const GENERATION_FAILED_ERROR: &str = "ai.tinyhumans.tinydocs.Error.GenerationFailed"; @@ -88,6 +83,7 @@ struct Documents { // they await anything. `unused_async` can never be actionable in this block. #[allow( clippy::unused_async, + clippy::unused_async_trait_impl, reason = "tinybus::interface requires every method to be `async fn`" )] #[tinybus::interface(name = "ai.tinyhumans.tinydocs.Documents")] diff --git a/crates/tinydocs-module/src/service/test.rs b/crates/tinydocs-module/src/service/test.rs index 2a23516..112a4a0 100644 --- a/crates/tinydocs-module/src/service/test.rs +++ b/crates/tinydocs-module/src/service/test.rs @@ -12,6 +12,7 @@ #![allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)] use tinybus::Interface; +use tinydocs_bus::{BUS_NAME, METHODS, OBJECT_PATH, WireSlideImage, WireSlideSpec}; use super::*; use crate::outputs::hex_digest; @@ -73,6 +74,7 @@ async fn dispatch_members_match_the_manifest_exactly() { members, declared, "the interface impl and the module_export! methods list have drifted" ); + assert_eq!(METHODS, DECLARED_METHODS); } #[test] diff --git a/crates/tinydocs-module/tests/module_e2e.rs b/crates/tinydocs-module/tests/module_e2e.rs index bb29cb4..0bbd834 100644 --- a/crates/tinydocs-module/tests/module_e2e.rs +++ b/crates/tinydocs-module/tests/module_e2e.rs @@ -19,17 +19,11 @@ use tinybus::Connection; use tinybus::broker::Broker; use tinybus::module::{ModuleHost, ModuleState}; use tinybus::transport::memory::MemoryBus; -use tinydocs::spec::{DocumentSection, DocumentSpec}; -use tinydocs_module::{BUS_NAME, OBJECT_PATH, OutputRef, hex_digest}; +use tinydocs_bus::{BUS_NAME, DocumentSection, DocumentSpec, METHODS, OBJECT_PATH, names::methods}; +use tinydocs_module::{OutputRef, hex_digest}; /// Every method the manifest must declare, in order. -const EXPECTED_METHODS: &[&str] = &[ - "GenerateDocx", - "GeneratePptx", - "ExtractText", - "ReadOutput", - "ReleaseOutput", -]; +const EXPECTED_METHODS: &[&str] = &METHODS; /// Chunk size for reading outputs back. /// @@ -94,7 +88,6 @@ fn admit_module() -> ( assert_eq!(loaded.name, "tinydocs-module"); assert_eq!(loaded.manifest.bus_name.as_str(), BUS_NAME); assert_eq!(loaded.manifest.object_path.as_str(), OBJECT_PATH); - let declared: Vec<&str> = loaded .manifest .provides @@ -139,7 +132,7 @@ async fn wait_until_serving(client: &Connection) { async fn generates_a_docx(proxy: &tinybus::Proxy) { let handle: OutputRef = proxy .call( - "GenerateDocx", + methods::GENERATE_DOCX, (DocumentSpec { title: "TinyBus E2E".to_string(), author: Some("TinyDocs".to_string()), diff --git a/docs/specs/tinybus-module.md b/docs/specs/tinybus-module.md index 5934468..1889a02 100644 --- a/docs/specs/tinybus-module.md +++ b/docs/specs/tinybus-module.md @@ -28,10 +28,13 @@ production. ## Behavior -The private `tinydocs-module` workspace crate depends on the public library's -`docx`, `pptx` and `pdf` features and builds as a `cdylib`. This separation keeps -unpublished, vendored TinyBus packages out of the crates.io package manifest. The -module claims `ai.tinyhumans.tinydocs.Documents`, serves the object path +The `tinydocs-bus` workspace crate is the transport-free wire contract: bus +identity, member names, contract version, payload types, and shared errors. A +host can depend on it without compiling TinyBus or a format writer. The public +`tinydocs` crate re-exports those exact values, while the private +`tinydocs-module` crate consumes the contract directly and builds as a `cdylib`. +This separation keeps vendored TinyBus packages out of the crates.io package +manifest. The module claims `ai.tinyhumans.tinydocs.Documents`, serves the object path `/ai/tinyhumans/tinydocs/Documents`, and exports five methods: ```text @@ -78,6 +81,9 @@ second fully-declared interface is not expressible without a TinyBus change. ## Invariants and constraints - The vendored TinyBus gitlink is the ABI source of truth. +- `tinydocs-bus` contains no transport or document-generation dependency. +- `tinydocs::docx::DocumentSpec` and `tinydocs_bus::spec::DocumentSpec` are + the same type, not structural copies. - Manifest methods and generated dispatch members must remain identical. - No Rust value crosses the dynamic-library ABI boundary. - The native artifact must match the host target and TinyBus compatibility diff --git a/src/error/test.rs b/src/error/test.rs deleted file mode 100644 index 7bc5815..0000000 --- a/src/error/test.rs +++ /dev/null @@ -1,54 +0,0 @@ -//! Unit tests for the crate-wide error type. - -#![allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)] - -use super::Error; - -#[test] -fn short_details_are_left_intact() { - let err = Error::generation_failed("boom"); - assert_eq!( - err, - Error::GenerationFailed { - detail: "boom".to_string() - } - ); -} - -#[test] -fn long_details_are_truncated_with_a_suffix() { - let raw = "x".repeat(Error::MAX_DETAIL_CHARS * 2); - let Error::GenerationFailed { detail } = Error::generation_failed(&raw) else { - panic!("expected GenerationFailed"); - }; - assert_eq!(detail.chars().count(), Error::MAX_DETAIL_CHARS); - assert!(detail.ends_with("[…truncated]")); -} - -#[test] -fn truncation_never_splits_a_multi_byte_character() { - // Every character is 4 bytes, so a byte-based truncation would panic or - // produce invalid UTF-8. Counting characters keeps the boundary valid. - let raw = "🦀".repeat(Error::MAX_DETAIL_CHARS * 2); - let detail = Error::truncate_detail(&raw); - assert_eq!(detail.chars().count(), Error::MAX_DETAIL_CHARS); - assert!(detail.starts_with('🦀')); -} - -#[test] -fn detail_at_exactly_the_cap_is_not_truncated() { - let raw = "y".repeat(Error::MAX_DETAIL_CHARS); - assert_eq!(Error::truncate_detail(&raw), raw); -} - -#[test] -fn invalid_input_carries_the_field_path_verbatim() { - let err = Error::invalid_input("sections[2].bullets[0]", "must be ≤ 10 chars"); - assert_eq!( - err, - Error::InvalidInput { - field: "sections[2].bullets[0]".to_string(), - reason: "must be ≤ 10 chars".to_string(), - } - ); -} diff --git a/src/lib.rs b/src/lib.rs index 0e64fbc..70a7d98 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -27,6 +27,7 @@ //! - [`spec`] — the typed document specs and their validation. Compiled in //! every build, including `--no-default-features`, so a host whose synthesis //! happens elsewhere still shares one definition of the wire contract. +//! - [`Error`] and [`Result`] — the shared document and bus error contract. #![cfg_attr( feature = "docx", doc = "- [`docx`] — `.docx` (OOXML `WordprocessingML`) synthesis." @@ -83,9 +84,7 @@ //! - `pdf` (default) — `.pdf` text extraction via `pdf-extract`, which also //! drops its font and `PostScript` parsing stack. -mod error; - -pub mod spec; +pub use tinydocs_bus::spec; #[cfg(feature = "docx")] pub mod docx; @@ -96,4 +95,4 @@ pub mod pptx; #[cfg(feature = "pdf")] pub mod pdf; -pub use error::{Error, Result}; +pub use tinydocs_bus::{Error, Result}; diff --git a/src/pptx/test.rs b/src/pptx/test.rs index 446ad2e..0c528c4 100644 --- a/src/pptx/test.rs +++ b/src/pptx/test.rs @@ -10,7 +10,24 @@ use super::{PresentationSpec, SlideImage, SlideSpec, build_slides, fit_within, generate}; use crate::Error; -use crate::spec::image::test::png; + +/// Minimal PNG bytes with the requested dimensions in its `IHDR` header. +fn png(width: u32, height: u32) -> Vec { + let mut out = vec![0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A]; + out.extend_from_slice(&13u32.to_be_bytes()); + out.extend_from_slice(b"IHDR"); + out.extend_from_slice(&width.to_be_bytes()); + out.extend_from_slice(&height.to_be_bytes()); + out.extend_from_slice(&[0x08, 0x06, 0x00, 0x00, 0x00]); + out.extend_from_slice(&[0x00, 0x00, 0x00, 0x00]); + out.extend_from_slice(&0u32.to_be_bytes()); + out.extend_from_slice(b"IDAT"); + out.extend_from_slice(&[0x00, 0x00, 0x00, 0x00]); + out.extend_from_slice(&0u32.to_be_bytes()); + out.extend_from_slice(b"IEND"); + out.extend_from_slice(&[0xAE, 0x42, 0x60, 0x82]); + out +} fn slide() -> SlideSpec { SlideSpec { diff --git a/tests/public_api.rs b/tests/public_api.rs index c271662..87b6424 100644 --- a/tests/public_api.rs +++ b/tests/public_api.rs @@ -65,3 +65,10 @@ fn limits_are_visible_to_consumers() { invalid.title = "t".repeat(docx::MAX_TEXT_CHARS + 1); assert!(invalid.validate().is_err()); } + +#[test] +fn document_spec_is_the_bus_contract_type() { + fn accepts_bus_spec(_: tinydocs_bus::spec::DocumentSpec) {} + + accepts_bus_spec(spec()); +}