diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c59842f1..9df783d4 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -30,7 +30,8 @@ jobs: - uses: actions/checkout@v4 - uses: dtolnay/rust-toolchain@master with: - toolchain: ${{ env.RUST_TOOLCHAIN }} + # Pin lint diagnostics; build and tests continue to target stable Rust. + toolchain: "1.96.0" components: rustfmt, clippy - uses: Swatinem/rust-cache@v2 - name: System libraries (librdkafka native build + TLS/SASL) @@ -139,10 +140,16 @@ jobs: steps: - uses: actions/checkout@v4 - name: WeasyPrint system libraries - run: sudo apt-get update && sudo apt-get install -y libpango-1.0-0 libpangocairo-1.0-0 libcairo2 libgdk-pixbuf-2.0-0 libffi-dev + run: sudo apt-get update && sudo apt-get install -y libpango-1.0-0 libpangocairo-1.0-0 libcairo2 libgdk-pixbuf-2.0-0 libffi-dev poppler-utils - name: book toolchain (venv) run: | python3 -m venv docs/book/.venv - docs/book/.venv/bin/pip install --quiet weasyprint markdown pygments pyyaml - - name: build PDF + EPUB - run: docs/book/build-book.sh + docs/book/.venv/bin/pip install --quiet -r docs/book/requirements.txt + - name: cover integration tests + run: docs/book/build-book.sh --test + - name: build English and Spanish PDF + EPUB + env: + BOOK_OUTPUT_DIR: ${{ runner.temp }}/rust-book-editions + run: | + docs/book/build-book.sh + BOOK_CONFIG=book-es.yaml docs/book/build-book.sh diff --git a/README.md b/README.md index 6e9a0060..f6ee8aee 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,11 @@

- Firefly Framework for Rust + + + rsfly — Firefly for Rust +

-

Firefly Framework for Rust

+

rsfly — Firefly for Rust

Spring Boot for Rust — a production-grade platform for building @@ -28,7 +31,7 @@ Changelog

-> **Read the book — [Firefly Framework for Rust](docs/book/)** — the canonical, +> **Read the book — [rsfly by example](docs/book/)** — the canonical, > best-in-class guide: a punchy [Quickstart](docs/book/src/02-quickstart.md) > (zero to a running reactive endpoint in minutes), the keystone > [Reactive Model](docs/book/src/05-reactive-model.md) chapter (`Mono`/`Flux`), @@ -36,9 +39,14 @@ > chapter, and full chapters on configuration, persistence, DDD, CQRS, EDA, event > sourcing, sagas, HTTP clients, security, observability, testing, and > production. Build it locally with `mdbook build docs/book` and open -> `docs/book/book/index.html`, or read the offline editions: -> [`docs/book/dist/firefly-rust-by-example.pdf`](docs/book/dist/firefly-rust-by-example.pdf) -> and [`.epub`](docs/book/dist/firefly-rust-by-example.epub). +> `docs/book/book/index.html`, or download the redesigned offline editions: +> English [PDF](https://github.com/fireflyframework/fireflyframework-rust/releases/download/books-2026.10.08/firefly-rust-by-example.pdf) +> · [EPUB](https://github.com/fireflyframework/fireflyframework-rust/releases/download/books-2026.10.08/firefly-rust-by-example.epub). +> Español [PDF](https://github.com/fireflyframework/fireflyframework-rust/releases/download/books-2026.10.08/firefly-rust-by-example-es.pdf) +> · [EPUB](https://github.com/fireflyframework/fireflyframework-rust/releases/download/books-2026.10.08/firefly-rust-by-example-es.epub). + +**rsfly** is the product name. Existing `firefly` / `firefly-*` crate names, +imports, APIs and repository links remain unchanged. > **Two headline wins.** (1) A **Spring-Boot-for-Rust > developer experience**: add the one [`firefly`](crates/firefly/README.md) diff --git a/assets/architecture.svg b/assets/architecture.svg index bd505f0b..fb8dfa2e 100644 --- a/assets/architecture.svg +++ b/assets/architecture.svg @@ -4,29 +4,29 @@ font-family="Avenir Next,Avenir,Helvetica Neue,Helvetica,Arial,sans-serif"> - + - + - + - + - + - - + + - + - + @@ -36,94 +36,94 @@ - Architecture at a glance - fireflyframework-rust - - One dependency in; four strictly-layered tiers building left to right; a reactive core at the base. + Architecture at a glance + rsfly - Rust framework by Firefly + + One dependency in; four strictly-layered tiers building left to right; a reactive core at the base. - - - + + + - THE FRONT DOOR - firefly + firefly-macros - one dependency · use firefly::prelude::*; - declarative macros · stable __rt contract + THE FRONT DOOR + firefly + firefly-macros + one dependency · use firefly::prelude::*; + declarative macros · stable __rt contract - + - - + + - 1 - FOUNDATIONAL - reactive base · cross-cutting - + 1 + FOUNDATIONAL + reactive base · cross-cutting + kernelweb configvalidators containeri18n - + utils, session + + utils, session - - + + - 2 - PLATFORM - engines · defines ports - + 2 + PLATFORM + engines · defines ports + cqrseda eventsourcingorchestration cachesecurity - + observability, … + + observability, … - - + + - 3 - ADAPTERS - implement the ports - + 3 + ADAPTERS + implement the ports + data-sqlxdata-mongodb eda-kafkacache-redis idp-* · ecm-*notifications-* - + client, webhooks + + client, webhooks - - + + - 4 - STARTERS - compose · ship - + 4 + STARTERS + compose · ship + starter-corestarter-web starter-domainstarter-data admincli - + backoffice + + backoffice - + - - - - firefly-reactive - the Mono / Flux reactive core every tier is built on - tokio · axum · async-native + + + + firefly-reactive + the Mono / Flux reactive core every tier is built on + tokio · axum · async-native diff --git a/assets/banner.svg b/assets/banner.svg index 9634663e..de11600d 100644 --- a/assets/banner.svg +++ b/assets/banner.svg @@ -1,84 +1 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Event-Driven Rust Microservices - reactive · one-dependency · tokio + axum · async-native - +rsfly for Rustrsfly - Rust framework by Firefly \ No newline at end of file diff --git a/assets/brand/banner-dark.svg b/assets/brand/banner-dark.svg new file mode 100644 index 00000000..de11600d --- /dev/null +++ b/assets/brand/banner-dark.svg @@ -0,0 +1 @@ +rsfly for Rustrsfly - Rust framework by Firefly \ No newline at end of file diff --git a/assets/brand/banner-light.svg b/assets/brand/banner-light.svg new file mode 100644 index 00000000..b1f2fd97 --- /dev/null +++ b/assets/brand/banner-light.svg @@ -0,0 +1 @@ +rsfly for Rustrsfly - Rust framework by Firefly \ No newline at end of file diff --git a/assets/brand/banner.png b/assets/brand/banner.png new file mode 100644 index 00000000..ce59697a Binary files /dev/null and b/assets/brand/banner.png differ diff --git a/assets/brand/logo-dark.svg b/assets/brand/logo-dark.svg new file mode 100644 index 00000000..54cdb004 --- /dev/null +++ b/assets/brand/logo-dark.svg @@ -0,0 +1 @@ +rsfly - Rust framework by Firefly \ No newline at end of file diff --git a/assets/brand/logo-light.svg b/assets/brand/logo-light.svg new file mode 100644 index 00000000..17c73c52 --- /dev/null +++ b/assets/brand/logo-light.svg @@ -0,0 +1 @@ +rsfly - Rust framework by Firefly \ No newline at end of file diff --git a/docs/book/BUILDING.md b/docs/book/BUILDING.md new file mode 100644 index 00000000..1460938f --- /dev/null +++ b/docs/book/BUILDING.md @@ -0,0 +1,88 @@ +# Building the branded book editions + +Both editions use the collection title **rsfly by example**. The English subtitle +is “Composing typed services with Tokio, Axum and SQLx”; the Spanish subtitle is +“Servicios tipados con Tokio, Axum y SQLx”. The product name is rsfly; existing +crate names, imports, technical examples and repository URLs remain unchanged. + +The English and Spanish editions share the original chapters and technical examples. +Each edition has a localized front and back cover from the Firefly Framework Brand +Kit, `11-Books/rust/`. The cover source of truth is that kit; this repository stores +the exported artwork used to build the books. `art/brand-assets.json` records the +imported files and their SHA-256 digests. + +The PDF uses outlined SVG artwork at the existing 7.5 × 9.25 inch trim size. +CairoSVG renders the two cover pages as vectors, preserving translucent gradients; +they replace the reserved cover pages without changing interior pagination. Cover +pages have no running header or page number. The EPUB uses the corresponding PNG +artwork for reader compatibility, with a front-cover image declaration, localized +alternative text and separate front/back documents at the beginning/end of its +reading order. The back cover is not a table-of-contents chapter. + +## Toolchain + +Create an isolated Python 3.12 environment under `docs/book/.venv` and install +`docs/book/requirements.txt`. The requirements pin the renderer and its Python +dependencies, including the PDF inspection library used by the tests. WeasyPrint +also needs Pango/Cairo/GObject: on macOS use the Homebrew libraries; on Linux the +CI workflow lists the system packages. Poppler supplies `pdftoppm` for +rendered gradient regression tests. `build-book.sh` configures the macOS loader +path. Pandoc, LaTeX and mdBook are not part of the PDF/EPUB build. + +```sh +python3.12 -m venv docs/book/.venv +docs/book/.venv/bin/pip install -r docs/book/requirements.txt +docs/book/build-book.sh --test +BOOK_OUTPUT_DIR=/absolute/path/to/book-editions docs/book/build-book.sh +BOOK_CONFIG=book-es.yaml BOOK_OUTPUT_DIR=/absolute/path/to/book-editions docs/book/build-book.sh +``` + +The output directory contains `firefly-rust-by-example.{pdf,epub}` and +`firefly-rust-by-example-es.{pdf,epub}`. Use `BOOK_OUTPUT_DIR` for a new edition: +the default `docs/book/dist/` contains tracked historical release artifacts and +must not be replaced incidentally. `--pdf` and `--epub` select one format. + +The existing body typography uses system-font fallbacks; the outlined cover SVGs +do not depend on locally installed cover fonts. Inspect both complete PDFs after +building on the selected release host, including front/back pages, chapter +openings, page boundaries and the English/Spanish accented characters. Also open +each EPUB in a reader and check its cover, final page and navigation. + +## Updating cover artwork + +Copy the approved `cover`, `cover-es`, `back-cover` and `back-cover-es` SVG/PNG +pairs from the canonical kit into `docs/book/art/`, then refresh the provenance +digests. `book.yaml` and `book-es.yaml` select each edition's artwork, title labels +and alternative text. The build fails if a configured cover asset is missing. +`gen_openers.py` regenerates chapter openers only and does not own the cover files. + +## Book-only publication + +Book publication is separate from the framework version. The existing release +workflow is triggered by `v*.*.*` tags and publishes the historical committed +distribution; it is not the publisher for this design edition. A reviewed +`books-2026.10.08` release can attach the four newly built artifacts with +`latest=false`, preserving framework tags, package versions and existing assets. +Include the exact source commit, artwork provenance and artifact SHA-256 digests +in its release notes. No book build command publishes or uploads files. + +## Interior identity and diagram sources + +The approved rsfly gradient-y lockups in `art/rsfly-light.svg` and +`art/rsfly-dark.svg` come from Brand Kit `12-Frameworks/rust/`. +`build/brand.py` embeds their existing outlines; it never redraws the mark. +Chapter openers and part dividers use this same identity. Diagram footers reserve +space below the existing drawing, leaving technical coordinates and labels intact. +The inline figures in the English and Spanish manuscripts are maintained in the +manuscripts; `build/gen_diagrams.py` owns the standalone diagram previews. + +`theme/tokens.css` uses the kit's charcoal, paper, amber, gold, ink and neutral +colors. Success, warning and failure indicators retain distinct accessible +semantic treatments. Syntax highlighting keeps its language-token distinctions. +Root README and book landing headers use the matching canonical family assets +from `assets/brand/`; technical identifiers and example code remain unchanged. + +For PDF interiors, the renderer uses the canonical PNG version of each small +rsfly lockup to preserve the approved alpha gradient. Source SVG diagrams and +EPUB interiors retain the original vector lockups. This avoids WeasyPrint +flattening that gradient without changing the technical drawings. diff --git a/docs/book/README.md b/docs/book/README.md new file mode 100644 index 00000000..67b7fde3 --- /dev/null +++ b/docs/book/README.md @@ -0,0 +1,16 @@ +

+ + + rsfly — Firefly for Rust + +

+ +# rsfly by example + +Composing typed services with Tokio, Axum and SQLx. + +Read the [English manuscript](src/SUMMARY.md) or [Spanish manuscript](src-es/SUMMARY.md). +The book connects application composition, HTTP, domain modeling, persistence and distributed work through one running service. + +See [Building the book](BUILDING.md) for the PDF/EPUB editions and artwork provenance. +Existing `firefly` crate names, APIs and technical examples are unchanged. diff --git a/docs/book/art/back-cover-es.png b/docs/book/art/back-cover-es.png new file mode 100644 index 00000000..2b3896fe Binary files /dev/null and b/docs/book/art/back-cover-es.png differ diff --git a/docs/book/art/back-cover-es.svg b/docs/book/art/back-cover-es.svg new file mode 100644 index 00000000..182a7aae --- /dev/null +++ b/docs/book/art/back-cover-es.svg @@ -0,0 +1 @@ +rsfly - ContraportadaFramework by Firefly \ No newline at end of file diff --git a/docs/book/art/back-cover.png b/docs/book/art/back-cover.png new file mode 100644 index 00000000..76aa5464 Binary files /dev/null and b/docs/book/art/back-cover.png differ diff --git a/docs/book/art/back-cover.svg b/docs/book/art/back-cover.svg new file mode 100644 index 00000000..aafd3b6a --- /dev/null +++ b/docs/book/art/back-cover.svg @@ -0,0 +1 @@ +rsfly - Back coverFramework by Firefly \ No newline at end of file diff --git a/docs/book/art/brand-assets.json b/docs/book/art/brand-assets.json new file mode 100644 index 00000000..6df7722e --- /dev/null +++ b/docs/book/art/brand-assets.json @@ -0,0 +1,67 @@ +{ + "edition": "books-2026.10.08", + "source": "Firefly Framework Brand Kit / 11-Books/rust", + "frameworkSourceCommit": "e18e12e207921bf06cb42cf81c883160b45b15a2", + "dimensions": { + "width": 1500, + "height": 1850, + "pdfTrim": "7.5in \u00d7 9.25in" + }, + "typography": "Outlined Manrope", + "mark": "Canonical rsfly gradient-y wordmark and Firefly Framework identity", + "assets": [ + { + "file": "back-cover-es.png", + "sha256": "7e2076e980d95fc3607d43b778a8f117d1565c46df6444e61ae509be28b118f5" + }, + { + "file": "back-cover-es.svg", + "sha256": "036487d60198e560ec8026dd8204ce154941228c5cfda98d0f66bf0c94be03d7" + }, + { + "file": "back-cover.png", + "sha256": "1881c4fc5e142efa605c09de26b31aa7066b2e8fbf057b80e52fcc1c1e13a786" + }, + { + "file": "back-cover.svg", + "sha256": "f5498638b860fc9ca5124769b99d1ef3099f46dd86c865661054f51c6b99b029" + }, + { + "file": "cover-es.png", + "sha256": "d1f2403beadb7c9eee21fb4489e14d3ee5cc70c9340f326c2f4b4445cf787e55" + }, + { + "file": "cover-es.svg", + "sha256": "603f3c0048644a784d21cc0a0926f1a126651715bced3eef6d0bb3bebb5008d6" + }, + { + "file": "cover.png", + "sha256": "8c31fdbe7638a8adaff94e89aa5e53cdfb04aa509f866fbab89b0b8ca6498f91" + }, + { + "file": "cover.svg", + "sha256": "382e4bd6042a0e8bb562eb733e0c501cffad4ce7645951dae0b29a821d1ec63a" + }, + { + "file": "firefly-logo.svg", + "sha256": "d696c438ba9cb8d00e4830e5b62b9c756e23374f28c8d5244ab0e20df3a452ee" + }, + { + "file": "rsfly-dark.png", + "sha256": "151ebd37f0dedcc47b5480392aec552f56aa2667c8046224aa63433fcd196d7b" + }, + { + "file": "rsfly-dark.svg", + "sha256": "e76ffb35c3891eaf804516dcda616dcb3e5d17ab1604159bcd8027ed84d75772" + }, + { + "file": "rsfly-light.png", + "sha256": "2661e4826df55cbf89991a8cd8fdeff814465fcbfc7a854a3b7c02fe5906f702" + }, + { + "file": "rsfly-light.svg", + "sha256": "d696c438ba9cb8d00e4830e5b62b9c756e23374f28c8d5244ab0e20df3a452ee" + } + ], + "interiorSource": "Firefly Framework Brand Kit / 12-Frameworks/rust" +} diff --git a/docs/book/art/cover-es.png b/docs/book/art/cover-es.png new file mode 100644 index 00000000..9e546872 Binary files /dev/null and b/docs/book/art/cover-es.png differ diff --git a/docs/book/art/cover-es.svg b/docs/book/art/cover-es.svg new file mode 100644 index 00000000..e31912dc --- /dev/null +++ b/docs/book/art/cover-es.svg @@ -0,0 +1 @@ +rsfly by exampleFramework by Firefly \ No newline at end of file diff --git a/docs/book/art/cover.png b/docs/book/art/cover.png new file mode 100644 index 00000000..fbaab4a3 Binary files /dev/null and b/docs/book/art/cover.png differ diff --git a/docs/book/art/cover.svg b/docs/book/art/cover.svg index 26a98ce8..60e8dcc7 100644 --- a/docs/book/art/cover.svg +++ b/docs/book/art/cover.svg @@ -1,142 +1 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - FIREFLY SOFTWARE FOUNDATION - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - for Rust - by Example - - - - - - Event-Driven Rust Microservices - with the Firefly Framework - - - - - - - FIREFLY - · tokio + axum · async-native Rust - - +rsfly by exampleFramework by Firefly \ No newline at end of file diff --git a/docs/book/art/diagrams/config-precedence.svg b/docs/book/art/diagrams/config-precedence.svg index c6e25208..687d5001 100644 --- a/docs/book/art/diagrams/config-precedence.svg +++ b/docs/book/art/diagrams/config-precedence.svg @@ -1,17 +1,17 @@ - -defaultsStaticSource - -base YAMLapplication.yaml - -profile YAMLapplication-prod.yaml - -environmentFIREFLY_* - -CLI flagsFlagSource -merged left → right · last write wins -beats nothing -beats everything -an env override beats a YAML file; a CLI flag beats both - +defaultsStaticSource + +base YAMLapplication.yaml + +profile YAMLapplication-prod.yaml + +environmentFIREFLY_* + +CLI flagsFlagSource +merged left → right · last write wins +beats nothing +beats everything +an env override beats a YAML file; a CLI flag beats both +rsfly - Rust framework by Firefly diff --git a/docs/book/art/diagrams/cqrs-dispatch.svg b/docs/book/art/diagrams/cqrs-dispatch.svg index 08e1f661..07b179ae 100644 --- a/docs/book/art/diagrams/cqrs-dispatch.svg +++ b/docs/book/art/diagrams/cqrs-dispatch.svg @@ -1,17 +1,17 @@ - -send / query a message - -msg ↦ TypeIdmatched to a handler -middleware chain - -V - -C - -Q -V = Validation · C = Correlation · Q = QueryCache - -your handlerCommand or Query - +send / query a message + +msg ↦ TypeIdmatched to a handler +middleware chain + +V + +C + +Q +V = Validation · C = Correlation · Q = QueryCache + +your handlerCommand or Query +rsfly - Rust framework by Firefly diff --git a/docs/book/art/diagrams/di-graph.svg b/docs/book/art/diagrams/di-graph.svg index acc2152a..ed4f1f38 100644 --- a/docs/book/art/diagrams/di-graph.svg +++ b/docs/book/art/diagrams/di-graph.svg @@ -1,15 +1,15 @@ - - -Container · scan() wires beans in dependency order -WalletApi#[derive(Controller)] -Ledger#[derive(Service)] -ReadModel#[derive(Component)] -EventStore#[derive(Repository)] -Brokerport — #[autowired] -autowired -autowired - - - + +Container · scan() wires beans in dependency order +WalletApi#[derive(Controller)] +Ledger#[derive(Service)] +ReadModel#[derive(Component)] +EventStore#[derive(Repository)] +Brokerport — #[autowired] +autowired +autowired + + +rsfly - Rust framework by Firefly diff --git a/docs/book/art/diagrams/dual-port.svg b/docs/book/art/diagrams/dual-port.svg index 6b471632..d7fe8c53 100644 --- a/docs/book/art/diagrams/dual-port.svg +++ b/docs/book/art/diagrams/dual-port.svg @@ -1,9 +1,9 @@ - -Public API :8080client-facing -Management :8081operator-facing -#[rest_controller]your routesSecurityJWT · roles · sessionsRFC 9457 404problem+json fallback -/actuator/*health · info · metrics/adminself-hosted dashboard/swagger-ui · /redoc/v3/api-docs -FIREFLY_SERVER_ADDR · FIREFLY_MANAGEMENT_ADDR override the binds - +Public API :8080client-facing +Management :8081operator-facing +#[rest_controller]your routesSecurityJWT · roles · sessionsRFC 9457 404problem+json fallback +/actuator/*health · info · metrics/adminself-hosted dashboard/swagger-ui · /redoc/v3/api-docs +FIREFLY_SERVER_ADDR · FIREFLY_MANAGEMENT_ADDR override the binds +rsfly - Rust framework by Firefly diff --git a/docs/book/art/diagrams/event-sourcing.svg b/docs/book/art/diagrams/event-sourcing.svg index 6c5fb238..bc77832d 100644 --- a/docs/book/art/diagrams/event-sourcing.svg +++ b/docs/book/art/diagrams/event-sourcing.svg @@ -1,15 +1,15 @@ - -write path -CommandDeposit { amount }raise(event)→ uncommitted []append(events)optimistic concurrency -event stream (append-only) -+100WalletOpened - -+50MoneyDeposited - -−30MoneyWithdrawn -append -fold / replay -current statebalance = 120 - +write path +CommandDeposit { amount }raise(event)→ uncommitted []append(events)optimistic concurrency +event stream (append-only) ++100WalletOpened + ++50MoneyDeposited + +−30MoneyWithdrawn +append +fold / replay +current statebalance = 120 +rsfly - Rust framework by Firefly diff --git a/docs/book/art/diagrams/four-tier.svg b/docs/book/art/diagrams/four-tier.svg index 6a217bc7..8fb8310a 100644 --- a/docs/book/art/diagrams/four-tier.svg +++ b/docs/book/art/diagrams/four-tier.svg @@ -1,53 +1,53 @@ - -firefly + firefly-macrosone dependency · use firefly::prelude::*; - - -Tier 1 -Foundational -kernel -reactive -web -config -container -i18n - - - - -Tier 2 -Platform -cqrs -eda -event-sourcing -orchestration -cache -security - - - - -Tier 3 -Adapters -data-sqlx -data-mongodb -eda-kafka -cache-redis -idp-* -notif-* - - - - -Tier 4 -Starters -starter-core -starter-web -starter-domain -starter-data -admin -cli - -firefly-reactivethe Mono / Flux core every tier rests on (tokio · axum) - +firefly + firefly-macrosone dependency · use firefly::prelude::*; + + +Tier 1 +Foundational +kernel +reactive +web +config +container +i18n + + + + +Tier 2 +Platform +cqrs +eda +event-sourcing +orchestration +cache +security + + + + +Tier 3 +Adapters +data-sqlx +data-mongodb +eda-kafka +cache-redis +idp-* +notif-* + + + + +Tier 4 +Starters +starter-core +starter-web +starter-domain +starter-data +admin +cli + +firefly-reactivethe Mono / Flux core every tier rests on (tokio · axum) +rsfly - Rust framework by Firefly diff --git a/docs/book/art/diagrams/layered-crates.svg b/docs/book/art/diagrams/layered-crates.svg index f592e8e2..0a367bd3 100644 --- a/docs/book/art/diagrams/layered-crates.svg +++ b/docs/book/art/diagrams/layered-crates.svg @@ -1,17 +1,17 @@ - --interfacesDTOs · the public contract - -depends on --models@Entity · @Repository · @Bean - -depends on --core@Service · @Mapper · @Component - -depends on --web@RestController · the binary --sdktyped client - -→ -interfaces - +-interfacesDTOs · the public contract + +depends on +-models@Entity · @Repository · @Bean + +depends on +-core@Service · @Mapper · @Component + +depends on +-web@RestController · the binary +-sdktyped client + +→ -interfaces +rsfly - Rust framework by Firefly diff --git a/docs/book/art/diagrams/macros.svg b/docs/book/art/diagrams/macros.svg index 57f2a64a..3875df3c 100644 --- a/docs/book/art/diagrams/macros.svg +++ b/docs/book/art/diagrams/macros.svg @@ -1,21 +1,21 @@ - -you write -the macro generates -#[derive(Command)] - -the Message impl (kind, validate, cache_ttl) -#[derive(Schema)] - -an OpenAPI schema (appears in /v3/api-docs) -#[derive(DomainEvent)] - -EVENT_TYPE + to_domain_event -#[rest_controller] - -a Controller bean + WalletApi::routes(state) -#[derive(Service)] - -a scanned bean with #[autowired] fields - +you write +the macro generates +#[derive(Command)] + +the Message impl (kind, validate, cache_ttl) +#[derive(Schema)] + +an OpenAPI schema (appears in /v3/api-docs) +#[derive(DomainEvent)] + +EVENT_TYPE + to_domain_event +#[rest_controller] + +a Controller bean + WalletApi::routes(state) +#[derive(Service)] + +a scanned bean with #[autowired] fields +rsfly - Rust framework by Firefly diff --git a/docs/book/art/diagrams/openapi.svg b/docs/book/art/diagrams/openapi.svg index 59783940..e40ff79e 100644 --- a/docs/book/art/diagrams/openapi.svg +++ b/docs/book/art/diagrams/openapi.svg @@ -1,21 +1,21 @@ - -#[rest_controller]routes + status codes -#[derive(Schema)]DTO component schemas - - - - - - - - - -openapi.json -(/v3/api-docs) - - -Swagger UI -ReDoc - +#[rest_controller]routes + status codes +#[derive(Schema)]DTO component schemas + + + + + + + + + +openapi.json +(/v3/api-docs) + + +Swagger UI +ReDoc +rsfly - Rust framework by Firefly diff --git a/docs/book/art/diagrams/reactive.svg b/docs/book/art/diagrams/reactive.svg index 67beccab..07d16c17 100644 --- a/docs/book/art/diagrams/reactive.svg +++ b/docs/book/art/diagrams/reactive.svg @@ -1,24 +1,24 @@ - -Mono<T> -0 or 1 item, then complete - - - - -just(v) -complete - -Flux<T> -0..N items, then complete - - - - - - - - -map · filter · flat_map - +Mono<T> +0 or 1 item, then complete + + + + +just(v) +complete + +Flux<T> +0..N items, then complete + + + + + + + + +map · filter · flat_map +rsfly - Rust framework by Firefly diff --git a/docs/book/art/diagrams/request-lifecycle.svg b/docs/book/art/diagrams/request-lifecycle.svg index 1c621e6b..4bf5bf65 100644 --- a/docs/book/art/diagrams/request-lifecycle.svg +++ b/docs/book/art/diagrams/request-lifecycle.svg @@ -1,11 +1,11 @@ - -inbound HTTP request - -ProblemLayererrors → problem+jsonTraceContextLayerW3C traceparent in / outCorrelationLayerensure-or-generate idContentNegotiationLayerAccept → JSON / XML - -#[rest_controller]your handler runs -outermost -innermost - +inbound HTTP request + +ProblemLayererrors → problem+jsonTraceContextLayerW3C traceparent in / outCorrelationLayerensure-or-generate idContentNegotiationLayerAccept → JSON / XML + +#[rest_controller]your handler runs +outermost +innermost +rsfly - Rust framework by Firefly diff --git a/docs/book/art/diagrams/saga-compensation.svg b/docs/book/art/diagrams/saga-compensation.svg index 8a8b975b..a3a41be1 100644 --- a/docs/book/art/diagrams/saga-compensation.svg +++ b/docs/book/art/diagrams/saga-compensation.svg @@ -1,11 +1,11 @@ - -forward: dependency-ordered steps -debitwithdraw(amount)creditdeposit(amount)notifypublish event -may fail - - -compensate — reverse order -a compensation is a forward undo, not a database rollback - +forward: dependency-ordered steps +debitwithdraw(amount)creditdeposit(amount)notifypublish event +may fail + + +compensate — reverse order +a compensation is a forward undo, not a database rollback +rsfly - Rust framework by Firefly diff --git a/docs/book/art/diagrams/tcc.svg b/docs/book/art/diagrams/tcc.svg index 27bbd090..bf3a98cd 100644 --- a/docs/book/art/diagrams/tcc.svg +++ b/docs/book/art/diagrams/tcc.svg @@ -1,21 +1,21 @@ - -Try -reserve -Confirm -on all-tried -Cancel -on a try failure -source -withdraw (hold) -(none — held) -deposit (release) -dest -verify exists -deposit (capture) -(none — nothing held) - -all tried → confirm -any try fails → cancel tried in reverse - +Try +reserve +Confirm +on all-tried +Cancel +on a try failure +source +withdraw (hold) +(none — held) +deposit (release) +dest +verify exists +deposit (capture) +(none — nothing held) + +all tried → confirm +any try fails → cancel tried in reverse +rsfly - Rust framework by Firefly diff --git a/docs/book/art/diagrams/workflow-dag.svg b/docs/book/art/diagrams/workflow-dag.svg index 5a809276..9e70121b 100644 --- a/docs/book/art/diagrams/workflow-dag.svg +++ b/docs/book/art/diagrams/workflow-dag.svg @@ -1,10 +1,10 @@ - -parallel layer -balance-checkfunds_ok: bool -limit-checkwithin_limit: bool -approvedepends_on both - - - +parallel layer +balance-checkfunds_ok: bool +limit-checkwithin_limit: bool +approvedepends_on both + + +rsfly - Rust framework by Firefly diff --git a/docs/book/art/firefly-logo.svg b/docs/book/art/firefly-logo.svg index 59754b4b..17c73c52 100644 --- a/docs/book/art/firefly-logo.svg +++ b/docs/book/art/firefly-logo.svg @@ -1,15 +1 @@ - - - - - - - - - - - - - +rsfly - Rust framework by Firefly \ No newline at end of file diff --git a/docs/book/art/openers/appa.svg b/docs/book/art/openers/appa.svg index 1852e1a5..4e3455fb 100644 --- a/docs/book/art/openers/appa.svg +++ b/docs/book/art/openers/appa.svg @@ -1 +1 @@ -APPENDIX AAPPENDIX ASpring BootFirefly@Component → #[component]@Transactional → #[transactional] +APPENDIX AAPPENDIX ASpring BootFirefly@Component → #[component]@Transactional → #[transactional]rsfly - Rust framework by Firefly diff --git a/docs/book/art/openers/appb.svg b/docs/book/art/openers/appb.svg index 684631e9..e9c25441 100644 --- a/docs/book/art/openers/appb.svg +++ b/docs/book/art/openers/appb.svg @@ -1 +1 @@ -APPENDIX AMODULE INDEXfirefly-corefirefly-edafirefly-cqrs +APPENDIX AMODULE INDEXfirefly-corefirefly-edafirefly-cqrsrsfly - Rust framework by Firefly diff --git a/docs/book/art/openers/ch01.svg b/docs/book/art/openers/ch01.svg index 911241f1..e5fbecb5 100644 --- a/docs/book/art/openers/ch01.svg +++ b/docs/book/art/openers/ch01.svg @@ -1 +1 @@ -CHAPTER 1WHY FIREFLYaxum?sqlx?DI?Firefly Core +CHAPTER 1WHY FIREFLYaxum?sqlx?DI?Firefly Corersfly - Rust framework by Firefly diff --git a/docs/book/art/openers/ch02.svg b/docs/book/art/openers/ch02.svg index 0fd3f424..d3cb1f1e 100644 --- a/docs/book/art/openers/ch02.svg +++ b/docs/book/art/openers/ch02.svg @@ -1 +1 @@ -CHAPTER 2QUICKSTARTcargo newCore::newcargo run8081 ↑ +CHAPTER 2QUICKSTARTcargo newCore::newcargo run8081 ↑rsfly - Rust framework by Firefly diff --git a/docs/book/art/openers/ch03.svg b/docs/book/art/openers/ch03.svg index 06e6e1e5..491f257b 100644 --- a/docs/book/art/openers/ch03.svg +++ b/docs/book/art/openers/ch03.svg @@ -1 +1 @@ -CHAPTER 3CONFIGURATIONdefaultprofile: devenv / secretsSettings +CHAPTER 3CONFIGURATIONdefaultprofile: devenv / secretsSettingsrsfly - Rust framework by Firefly diff --git a/docs/book/art/openers/ch04.svg b/docs/book/art/openers/ch04.svg index 8b365b51..b81089a6 100644 --- a/docs/book/art/openers/ch04.svg +++ b/docs/book/art/openers/ch04.svg @@ -1 +1 @@ -CHAPTER 4DEPENDENCY INJECTION#[component]Arc<dyn Port>@autowiredlifecycleContext +CHAPTER 4DEPENDENCY INJECTION#[component]Arc<dyn Port>@autowiredlifecycleContextrsfly - Rust framework by Firefly diff --git a/docs/book/art/openers/ch04b.svg b/docs/book/art/openers/ch04b.svg index 92d7179c..21a79631 100644 --- a/docs/book/art/openers/ch04b.svg +++ b/docs/book/art/openers/ch04b.svg @@ -1 +1 @@ -CHAPTER 6BOOTSTRAPnew().run()web + CQRSscan beansadmin + docs +CHAPTER 6BOOTSTRAPnew().run()web + CQRSscan beansadmin + docsrsfly - Rust framework by Firefly diff --git a/docs/book/art/openers/ch05.svg b/docs/book/art/openers/ch05.svg index b2c1d1d0..2b159efe 100644 --- a/docs/book/art/openers/ch05.svg +++ b/docs/book/art/openers/ch05.svg @@ -1 +1 @@ -CHAPTER 5WIRINGCorecachebrokercompose() +CHAPTER 5WIRINGCorecachebrokercompose()rsfly - Rust framework by Firefly diff --git a/docs/book/art/openers/ch06.svg b/docs/book/art/openers/ch06.svg index 286de8d0..9c835065 100644 --- a/docs/book/art/openers/ch06.svg +++ b/docs/book/art/openers/ch06.svg @@ -1 +1 @@ -CHAPTER 7MONO & FLUXMono<T>Flux<T>▶ +CHAPTER 7MONO & FLUXMono<T>Flux<T>▶rsfly - Rust framework by Firefly diff --git a/docs/book/art/openers/ch06a.svg b/docs/book/art/openers/ch06a.svg index f9a0daa5..3795568b 100644 --- a/docs/book/art/openers/ch06a.svg +++ b/docs/book/art/openers/ch06a.svg @@ -1 +1 @@ -CHAPTER 9OPENAPI#[rest_controller]#[derive(Schema)]openapi.jsonSwagger · ReDoc +CHAPTER 9OPENAPI#[rest_controller]#[derive(Schema)]openapi.jsonSwagger · ReDocrsfly - Rust framework by Firefly diff --git a/docs/book/art/openers/ch07.svg b/docs/book/art/openers/ch07.svg index 6c9002b9..e9d9ca0d 100644 --- a/docs/book/art/openers/ch07.svg +++ b/docs/book/art/openers/ch07.svg @@ -1 +1 @@ -CHAPTER 8HTTP APIGET /walletsPOST /walletsRouter200 OK +CHAPTER 8HTTP APIGET /walletsPOST /walletsRouter200 OKrsfly - Rust framework by Firefly diff --git a/docs/book/art/openers/ch08.svg b/docs/book/art/openers/ch08.svg index c85da559..f53831a8 100644 --- a/docs/book/art/openers/ch08.svg +++ b/docs/book/art/openers/ch08.svg @@ -1 +1 @@ -CHAPTER 10PERSISTENCERepositoryfind · save · delete +CHAPTER 10PERSISTENCERepositoryfind · save · deletersfly - Rust framework by Firefly diff --git a/docs/book/art/openers/ch09.svg b/docs/book/art/openers/ch09.svg index 169b4e78..e7089bdd 100644 --- a/docs/book/art/openers/ch09.svg +++ b/docs/book/art/openers/ch09.svg @@ -1 +1 @@ -CHAPTER 11DOMAIN MODELMoneyWalletIdinvariantdomain eventWallet aggregate +CHAPTER 11DOMAIN MODELMoneyWalletIdinvariantdomain eventWallet aggregatersfly - Rust framework by Firefly diff --git a/docs/book/art/openers/ch10.svg b/docs/book/art/openers/ch10.svg index 2eb69202..8885fe3f 100644 --- a/docs/book/art/openers/ch10.svg +++ b/docs/book/art/openers/ch10.svg @@ -1 +1 @@ -CHAPTER 12CQRSDepositCommandBalanceQueryBus +CHAPTER 12CQRSDepositCommandBalanceQueryBusrsfly - Rust framework by Firefly diff --git a/docs/book/art/openers/ch11.svg b/docs/book/art/openers/ch11.svg index 257a4d70..b3b0454e 100644 --- a/docs/book/art/openers/ch11.svg +++ b/docs/book/art/openers/ch11.svg @@ -1 +1 @@ -CHAPTER 13EVENTS & MESSAGINGKafkaRabbitMQlistenerprojectionEventBus +CHAPTER 13EVENTS & MESSAGINGKafkaRabbitMQlistenerprojectionEventBusrsfly - Rust framework by Firefly diff --git a/docs/book/art/openers/ch12.svg b/docs/book/art/openers/ch12.svg index 50971fc9..109ec0f9 100644 --- a/docs/book/art/openers/ch12.svg +++ b/docs/book/art/openers/ch12.svg @@ -1 +1 @@ -CHAPTER 14EVENT SOURCING+10-3+5= balance 12replay the stream +CHAPTER 14EVENT SOURCING+10-3+5= balance 12replay the streamrsfly - Rust framework by Firefly diff --git a/docs/book/art/openers/ch13.svg b/docs/book/art/openers/ch13.svg index ea12185f..1bbc3c57 100644 --- a/docs/book/art/openers/ch13.svg +++ b/docs/book/art/openers/ch13.svg @@ -1 +1 @@ -CHAPTER 15HTTP CLIENTSLumenPayments APIWebClient · retry · breaker +CHAPTER 15HTTP CLIENTSLumenPayments APIWebClient · retry · breakerrsfly - Rust framework by Firefly diff --git a/docs/book/art/openers/ch14.svg b/docs/book/art/openers/ch14.svg index 810718ff..3e6dbe57 100644 --- a/docs/book/art/openers/ch14.svg +++ b/docs/book/art/openers/ch14.svg @@ -1 +1 @@ -CHAPTER 16EXPERIENCE TIERmobileBFFwalletsledgerfx +CHAPTER 16EXPERIENCE TIERmobileBFFwalletsledgerfxrsfly - Rust framework by Firefly diff --git a/docs/book/art/openers/ch15.svg b/docs/book/art/openers/ch15.svg index bd160ef1..bd3c8eab 100644 --- a/docs/book/art/openers/ch15.svg +++ b/docs/book/art/openers/ch15.svg @@ -1 +1 @@ -CHAPTER 17SAGASdebitcreditnotifycompensate +CHAPTER 17SAGASdebitcreditnotifycompensatersfly - Rust framework by Firefly diff --git a/docs/book/art/openers/ch16.svg b/docs/book/art/openers/ch16.svg index ab6c30a6..c4f89a4a 100644 --- a/docs/book/art/openers/ch16.svg +++ b/docs/book/art/openers/ch16.svg @@ -1 +1 @@ -CHAPTER 19SECURITYJWT#[secure]roles +CHAPTER 19SECURITYJWT#[secure]rolesrsfly - Rust framework by Firefly diff --git a/docs/book/art/openers/ch17.svg b/docs/book/art/openers/ch17.svg index dc4c15a7..4defa01e 100644 --- a/docs/book/art/openers/ch17.svg +++ b/docs/book/art/openers/ch17.svg @@ -1 +1 @@ -CHAPTER 20OBSERVABILITY/health/metricstraces +CHAPTER 20OBSERVABILITY/health/metricstracesrsfly - Rust framework by Firefly diff --git a/docs/book/art/openers/ch18.svg b/docs/book/art/openers/ch18.svg index 5207fd8a..c37f0c84 100644 --- a/docs/book/art/openers/ch18.svg +++ b/docs/book/art/openers/ch18.svg @@ -1 +1 @@ -CHAPTER 21CACHINGrequestCacheResiliencecache hitretry · breaker +CHAPTER 21CACHINGrequestCacheResiliencecache hitretry · breakerrsfly - Rust framework by Firefly diff --git a/docs/book/art/openers/ch19.svg b/docs/book/art/openers/ch19.svg index 7850e316..6a85798f 100644 --- a/docs/book/art/openers/ch19.svg +++ b/docs/book/art/openers/ch19.svg @@ -1 +1 @@ -CHAPTER 22SCHEDULING@schedulednotifywebhook +CHAPTER 22SCHEDULING@schedulednotifywebhookrsfly - Rust framework by Firefly diff --git a/docs/book/art/openers/ch20.svg b/docs/book/art/openers/ch20.svg index 3b9ec410..f571b520 100644 --- a/docs/book/art/openers/ch20.svg +++ b/docs/book/art/openers/ch20.svg @@ -1 +1 @@ -CHAPTER 23MACROS#[..]#[handler]#[route]#[saga_step] +CHAPTER 23MACROS#[..]#[handler]#[route]#[saga_step]rsfly - Rust framework by Firefly diff --git a/docs/book/art/openers/ch21.svg b/docs/book/art/openers/ch21.svg index 7718249a..0b4072d0 100644 --- a/docs/book/art/openers/ch21.svg +++ b/docs/book/art/openers/ch21.svg @@ -1 +1 @@ -CHAPTER 24TESTINGStepVerifierTestcontainersall greenreal infra +CHAPTER 24TESTINGStepVerifierTestcontainersall greenreal infrarsfly - Rust framework by Firefly diff --git a/docs/book/art/openers/ch22.svg b/docs/book/art/openers/ch22.svg index 1490829a..de3112ca 100644 --- a/docs/book/art/openers/ch22.svg +++ b/docs/book/art/openers/ch22.svg @@ -1 +1 @@ -CHAPTER 25THE CLI$ firefly new$ firefly run$ firefly migrate ▮ +CHAPTER 25THE CLI$ firefly new$ firefly run$ firefly migrate ▮rsfly - Rust framework by Firefly diff --git a/docs/book/art/openers/ch22b.svg b/docs/book/art/openers/ch22b.svg index d389d7f7..0dafb63e 100644 --- a/docs/book/art/openers/ch22b.svg +++ b/docs/book/art/openers/ch22b.svg @@ -1 +1 @@ -CHAPTER 18LAYERED SERVICES…-interfaces…-models…-core…-web…-sdk +CHAPTER 18LAYERED SERVICES…-interfaces…-models…-core…-web…-sdkrsfly - Rust framework by Firefly diff --git a/docs/book/art/openers/ch23.svg b/docs/book/art/openers/ch23.svg index 45dec7c1..e2e69d5e 100644 --- a/docs/book/art/openers/ch23.svg +++ b/docs/book/art/openers/ch23.svg @@ -1 +1 @@ -CHAPTER 26PRODUCTIONbuildimageship12-factork8s · health · graceful +CHAPTER 26PRODUCTIONbuildimageship12-factork8s · health · gracefulrsfly - Rust framework by Firefly diff --git a/docs/book/art/rsfly-dark.png b/docs/book/art/rsfly-dark.png new file mode 100644 index 00000000..858c9c65 Binary files /dev/null and b/docs/book/art/rsfly-dark.png differ diff --git a/docs/book/art/rsfly-dark.svg b/docs/book/art/rsfly-dark.svg new file mode 100644 index 00000000..54cdb004 --- /dev/null +++ b/docs/book/art/rsfly-dark.svg @@ -0,0 +1 @@ +rsfly - Rust framework by Firefly \ No newline at end of file diff --git a/docs/book/art/rsfly-light.png b/docs/book/art/rsfly-light.png new file mode 100644 index 00000000..869a473e Binary files /dev/null and b/docs/book/art/rsfly-light.png differ diff --git a/docs/book/art/rsfly-light.svg b/docs/book/art/rsfly-light.svg new file mode 100644 index 00000000..17c73c52 --- /dev/null +++ b/docs/book/art/rsfly-light.svg @@ -0,0 +1 @@ +rsfly - Rust framework by Firefly \ No newline at end of file diff --git a/docs/book/book-es.yaml b/docs/book/book-es.yaml index 7bccbeb7..ab69fef3 100644 --- a/docs/book/book-es.yaml +++ b/docs/book/book-es.yaml @@ -1,13 +1,18 @@ -# Edición en español (ES) — misma estructura que book.yaml; arte/openers compartidos. +# Edición en español (ES): cubiertas localizadas y capítulos originales. # Construir con: BOOK_CONFIG=book-es.yaml bash docs/book/build-book.sh -title: Firefly para Rust con Ejemplos -subtitle: Microservicios reactivos y orientados a eventos con el framework Firefly +title: rsfly by example +subtitle: Servicios tipados con Tokio, Axum y SQLx author: Firefly Software Foundation publisher: Firefly Software Foundation language: es identifier: urn:uuid:5b2e9c70-1a4d-4f88-bc31-7e0a6d2f9c14 rights: Copyright (c) 2026 Firefly Software Foundation. Licensed under Apache-2.0. -cover_svg: art/cover.svg +cover_svg: art/cover-es.svg +cover_image: art/cover-es.png +cover_alt: "Portada de rsfly by example: servicios tipados con Tokio, Axum y SQLx." +back_cover_svg: art/back-cover-es.svg +back_cover_image: art/back-cover-es.png +back_cover_alt: "Contraportada de rsfly by example: servicios Rust conectados mediante una arquitectura de aplicación compartida." trim_width: 7.5in trim_height: 9.25in src_dir: src-es @@ -191,6 +196,8 @@ back: [] pdf_name: firefly-rust-by-example-es.pdf epub_name: firefly-rust-by-example-es.epub labels: + cover: Portada + back_cover: Contraportada chapter: Capítulo appendix: Apéndice contents: Contenido diff --git a/docs/book/book.toml b/docs/book/book.toml index 07967084..bf4650d7 100644 --- a/docs/book/book.toml +++ b/docs/book/book.toml @@ -1,5 +1,5 @@ [book] -title = "Firefly Framework for Rust" +title = "rsfly by example" authors = ["Firefly Framework contributors"] description = "The canonical guide to building reactive, event-driven, resilient microservices on Rust with the Firefly Framework." language = "en" diff --git a/docs/book/book.yaml b/docs/book/book.yaml index 2334ee4e..1dcaa34d 100644 --- a/docs/book/book.yaml +++ b/docs/book/book.yaml @@ -1,11 +1,16 @@ -title: "Firefly for Rust by Example" -subtitle: "Event-Driven Rust Microservices with the Firefly Framework" +title: "rsfly by example" +subtitle: "Composing typed services with Tokio, Axum and SQLx" author: "Firefly Software Foundation" publisher: "Firefly Software Foundation" language: "en" identifier: "urn:uuid:9f3c7a14-2b6d-4e51-a8c9-f1efa11ce026" rights: "Copyright (c) 2026 Firefly Software Foundation. Licensed under Apache-2.0." cover_svg: "art/cover.svg" +cover_image: "art/cover.png" +cover_alt: "Front cover of rsfly by example: composing typed services with Tokio, Axum and SQLx." +back_cover_svg: "art/back-cover.svg" +back_cover_image: "art/back-cover.png" +back_cover_alt: "Back cover of rsfly by example: build connected Rust services around a shared application architecture." trim_width: "7.5in" trim_height: "9.25in" diff --git a/docs/book/build-book.py b/docs/book/build-book.py index 63235c17..906bbb53 100755 --- a/docs/book/build-book.py +++ b/docs/book/build-book.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -"""build-book.py — entry point for the designed *Firefly for Rust by Example* +"""build-book.py — entry point for the designed *rsfly by example* PDF + EPUB pipeline. This is a thin launcher: it ensures Homebrew's gobject/pango/cairo are on the diff --git a/docs/book/build-book.sh b/docs/book/build-book.sh index d0c02c0c..4e12b486 100755 --- a/docs/book/build-book.sh +++ b/docs/book/build-book.sh @@ -1,6 +1,6 @@ #!/usr/bin/env bash # -# build-book.sh — render the designed *Firefly for Rust by Example* PDF + EPUB. +# build-book.sh — render the designed *rsfly by example* PDF + EPUB. # # The pipeline is a WeasyPrint build (build/build.py) driven by book.yaml: # 1. Read book.yaml -> front matter, parts, chapters (each with an opener SVG). @@ -13,6 +13,7 @@ # docs/book/build-book.sh # build both PDF and EPUB # docs/book/build-book.sh --pdf # PDF only # docs/book/build-book.sh --epub # EPUB only +# docs/book/build-book.sh --test # cover integration tests # # Requires the project venv at docs/book/.venv (weasyprint, markdown, pygments, # pyyaml) and Homebrew's pango/cairo/gobject under /opt/homebrew/lib. @@ -29,16 +30,17 @@ export DYLD_FALLBACK_LIBRARY_PATH="${BREW_PREFIX}/lib:/usr/local/lib:${DYLD_FALL if [ ! -x "${VENV_PY}" ]; then echo "error: book venv not found at ${VENV_PY}" >&2 echo " create it with: python3 -m venv ${BOOK_DIR}/.venv && \\" >&2 - echo " ${BOOK_DIR}/.venv/bin/pip install weasyprint markdown pygments pyyaml" >&2 + echo " ${BOOK_DIR}/.venv/bin/pip install -r ${BOOK_DIR}/requirements.txt" >&2 exit 1 fi case "${1:-}" in + --test) exec "${VENV_PY}" -m unittest discover -s "${BOOK_DIR}/tests" -v ;; --pdf|--epub|"") ;; - *) echo "usage: build-book.sh [--pdf|--epub]" >&2; exit 2 ;; + *) echo "usage: build-book.sh [--pdf|--epub|--test]" >&2; exit 2 ;; esac -# (Re)generate the cover + chapter-opener art so it always matches the manifest. +# This generator owns chapter openers only. Branded covers are imported assets. "${VENV_PY}" "${BOOK_DIR}/build/gen_openers.py" exec "${VENV_PY}" "${BOOK_DIR}/build/build.py" "$@" diff --git a/docs/book/build/brand.py b/docs/book/build/brand.py new file mode 100644 index 00000000..c613fa30 --- /dev/null +++ b/docs/book/build/brand.py @@ -0,0 +1,26 @@ +"""Embed canonical rsfly artwork without redrawing its outlined wordmark.""" +from pathlib import Path +import re + +ART = Path(__file__).resolve().parents[1] / "art" + + +def lockup(x, y, width, height, *, theme="light", key="brand"): + svg = (ART / f"rsfly-{theme}.svg").read_text(encoding="utf-8") + for identifier in re.findall(r'\bid="([^"]+)"', svg): + svg = svg.replace(f'id="{identifier}"', f'id="{key}-{identifier}"') + svg = svg.replace(f'url(#{identifier})', f'url(#{key}-{identifier})') + svg = svg.replace(f'href="#{identifier}"', f'href="#{key}-{identifier}"') + svg = re.sub(r'\s(?:width|height)="[^"]+"', "", svg, count=2) + return svg.replace("", 1)[0] + footer + "" diff --git a/docs/book/build/build.py b/docs/book/build/build.py index ab492769..43ab5cad 100644 --- a/docs/book/build/build.py +++ b/docs/book/build/build.py @@ -1,4 +1,4 @@ -"""Build *Firefly for Rust by Example* into a designed PDF + EPUB from book.yaml. +"""Build *rsfly by example* into a designed PDF + EPUB from book.yaml. Pipeline (ported and adapted from the PyFly book build): 1. Read book.yaml -> front matter, parts, chapters (each referencing a src/ @@ -26,6 +26,7 @@ import md # noqa: E402 (to set md.LANG for localized callout labels) from md import render_markdown # noqa: E402 from epub import EpubBuilder, Doc # noqa: E402 +from brand import lockup from pdf import render_pdf # noqa: E402 BOOK = Path(__file__).resolve().parents[1] @@ -59,6 +60,41 @@ def _inline_svg(path: Path) -> str: return re.sub(r'^<\?xml[^>]*\?>\s*', "", svg) +def _covers(cfg: dict) -> dict[str, dict]: + covers = {} + for cid, prefix, label in (("cover", "cover", "Cover"), + ("back-cover", "back_cover", "Back cover")): + svg_name = cfg.get(f"{prefix}_svg", "art/cover.svg" if cid == "cover" else None) + if svg_name is None: + continue + svg = BOOK / svg_name + image = BOOK / cfg.get(f"{prefix}_image", svg_name) + for path in (svg, image): + if not path.is_file(): + raise FileNotFoundError(f"Configured {cid} asset is missing: {path}") + title = cfg.get("labels", {}).get(prefix, label) + covers[cid] = {"id": cid, "title": title, "svg": svg, "image": image, + "alt": cfg.get(f"{prefix}_alt", f"{title}: {cfg['title']}")} + return covers + + +def _add_epub_cover(epub: EpubBuilder, cover: dict) -> None: + cid = cover["id"] + href = f"art/{cid}{cover['image'].suffix}" + epub.add_file(cover["image"], href, f"{cid}-img", + properties="cover-image" if cid == "cover" else "") + alt = escape(cover["alt"], {'"': """}) + epub.add_doc(Doc(id=cid, title=cover["title"], + xhtml_body=f'{alt}', + in_nav=False, kind=cid)) + + +def _pdf_cover(cover: dict) -> str: + alt = escape(cover["alt"], {'"': """}) + return (f'') + + def _front_class(fid: str) -> str: return {"title": "frontmatter title-page", "copyright": "frontmatter copyright-page", @@ -183,33 +219,7 @@ def _toc_html(items: list[dict], *, href_fmt: str) -> str: def _divider_html(eyebrow: str, ptitle: str) -> str: eb = f'{escape(eyebrow)}' if eyebrow else "" - # A dark medallion holding the glowing firefly — the same motif as the - # cover and the chapter-opener panels, so the parts read as one family. - glyph = ('' - '' - '' - '' - '' - '' - '' - '' - '' - '' - '' - '' - '' - '' - '' - '' - '' - '' - '' - '' - '' - '' - '') + glyph = lockup(0, 0, 180, 130, key="part-rsfly").replace('{glyph}{eb}' f'

{escape(ptitle)}

') @@ -219,6 +229,7 @@ def main() -> int: # e.g. BOOK_CONFIG=book-es.yaml for the Spanish edition. cfg_name = os.environ.get("BOOK_CONFIG", "book.yaml") cfg = yaml.safe_load((BOOK / cfg_name).read_text()) + covers = _covers(cfg) # Localize structural labels + callout labels for this edition's language. md.LANG = cfg.get("language", "en") LABELS.update(cfg.get("labels", {})) @@ -233,20 +244,16 @@ def main() -> int: do_pdf = only in ("", "--pdf") do_epub = only in ("", "--epub") - DIST.mkdir(parents=True, exist_ok=True) - cover_svg = BOOK / cfg.get("cover_svg", "art/cover.svg") + dist = Path(os.environ.get("BOOK_OUTPUT_DIR", str(DIST))).expanduser() + dist.mkdir(parents=True, exist_ok=True) # ---- EPUB ---- if do_epub: epub = EpubBuilder(title=cfg["title"], author=cfg["author"], language=cfg["language"], identifier=cfg["identifier"], css=css_text) - if cover_svg.exists(): - epub.add_file(cover_svg, "art/cover.svg", "cover-img", properties="cover-image") - epub.add_doc(Doc(id="cover", title="Cover", - xhtml_body='
' - 'Cover
', - in_nav=False, kind="front")) + if "cover" in covers: + _add_epub_cover(epub, covers["cover"]) for it in items: if it["kind"] == "toc": body = _toc_html(items, href_fmt="{cid}.xhtml") @@ -265,13 +272,15 @@ def main() -> int: epub.add_doc(Doc(id=it["id"], title=it["title"], xhtml_body=body, in_nav=True, kind="chapter", part=it.get("part"), num=it.get("num"))) - epub.build(DIST / epub_name) + if "back-cover" in covers: + _add_epub_cover(epub, covers["back-cover"]) + epub.build(dist / epub_name) # ---- PDF (single concatenated document) ---- if do_pdf: parts_html: list[str] = [] - if cover_svg.exists(): - parts_html.append(f'
{_inline_svg(cover_svg)}
') + if "cover" in covers: + parts_html.append(_pdf_cover(covers["cover"])) for it in items: if it["kind"] == "toc": body = _toc_html(items, href_fmt="#{cid}") @@ -285,18 +294,22 @@ def main() -> int: else: # chapter body = _chapter_body(it) parts_html.append(f'
{body}
') - full = ("" + if "back-cover" in covers: + parts_html.append(_pdf_cover(covers["back-cover"])) + full = (f'' + f'{escape(cfg["title"])}' + "\n".join(parts_html) + "") render_pdf(full, base_url=BOOK, css_paths=[THEME / "tokens.css", THEME / "pygments.css", THEME / "book.css", THEME / "print.css"], - out=DIST / pdf_name) + out=dist / pdf_name, + cover_pages={cid: cover["svg"] for cid, cover in covers.items()}) nch = sum(1 for it in items if it["kind"] == "chapter") nfr = sum(1 for it in items if it["kind"] == "front") ndv = sum(1 for it in items if it["kind"] == "divider") print(f"Built {nch} chapter(s) + {nfr} front-matter + {ndv} part divider(s) " - f"+ cover + TOC -> {'PDF ' if do_pdf else ''}{'EPUB' if do_epub else ''} in {DIST}") + f"+ {len(covers)} covers + TOC -> {'PDF ' if do_pdf else ''}{'EPUB' if do_epub else ''} in {dist}") return 0 diff --git a/docs/book/build/epub.py b/docs/book/build/epub.py index 17540676..6a97fec5 100644 --- a/docs/book/build/epub.py +++ b/docs/book/build/epub.py @@ -60,13 +60,15 @@ def add_file(self, path: Path, href: str, aid: str, properties: str = "") -> Ass def _xhtml(self, d: Doc) -> str: links = "\n".join(f'' for i in range(len(self.css))) - sec_class = {"toc": "toc", "divider": "part-divider", - "front": "frontmatter"}.get(d.kind, "chapter") + sec_class = {"toc": "toc", "divider": "part-divider", "front": "frontmatter", + "cover": "cover-page", "back-cover": "cover-page back-cover"}.get(d.kind, "chapter") + semantics = {"cover": ' epub:type="cover"', + "back-cover": ' epub:type="backmatter"'}.get(d.kind, "") return (f'\n' f'\n' f'{escape(d.title)}\n{links}\n\n' - f'
' + f'
' f'{d.xhtml_body}
\n\n') def _nav(self) -> str: diff --git a/docs/book/build/gen_diagrams.py b/docs/book/build/gen_diagrams.py index de6b32fd..a919bd58 100644 --- a/docs/book/build/gen_diagrams.py +++ b/docs/book/build/gen_diagrams.py @@ -1,27 +1,14 @@ -"""Generate the on-brand inline concept diagrams for *Firefly for Rust by Example*. +"""Generate the on-brand inline concept diagrams for *rsfly by example*. These are the in-text technical figures (NOT the chapter openers — see ``gen_openers.py`` for those). Each diagram is a self-contained ``
`` holding one ```` plus a ``
``, matching the markup the CQRS, event-sourcing and saga chapters already embed. -Design language (shared with the cover, the openers, and ``theme/tokens.css``): - - * Cards are rounded rects ``rx=9..10``, cream fill ``#fdf6ea`` (or ``#fffaf0``), - stroke ``#e0cda8`` width ``1.5``. Accent/highlight cards use fill ``#fff6e6`` - stroke ``#e0b96a``. - * Connectors are rust ``#d4793a`` ``stroke-width=3`` with arrowheads drawn as - explicit ```` triangles (never ````). - * Titles ``#2a1d10`` / ``#3a2a1c`` in the sans stack; sub-labels ``#7a6450`` - smaller; code/type tokens in the mono stack. Gold accents ``#f6a821`` / - ``#ffc24a`` used sparingly. - -WeasyPrint SVG constraints (violating these breaks PDF rendering): - - * solid fills ONLY — no gradients, no ````, no ```` in shared - ````; - * arrowheads are explicit polygons; - * every ``viewBox`` is set and every SVG is self-contained (no external fonts). +Design language uses the approved Firefly charcoal/paper/amber/gold palette. +Technical shapes, labels and connector coordinates are preserved. A separate +footer embeds the canonical rsfly gradient-y lockup. Diagram arrows retain +explicit triangle heads for reliable WeasyPrint rendering. The COMPUTED-LAYOUT primitives (``card``/``chip``/``arrow``/``lane``/``label``) and the auto-positioning helpers (``flow_row``/``stack``/``grid``/``lanes``) keep @@ -35,31 +22,32 @@ import math from pathlib import Path +from brand import diagram_footer from xml.sax.saxutils import escape as _xml_escape ART = Path(__file__).resolve().parents[1] / "art" / "diagrams" # --- palette (mirrors tokens.css + the existing inline figures) -------------- -FIELD = "#fdf6ea" # cream card fill -FIELD2 = "#fffaf0" # lighter cream card fill -ACCENT = "#fff6e6" # highlight/accent card fill -ACCENT_S = "#e0b96a" # highlight/accent card stroke -CARD_S = "#e0cda8" # default card stroke -RUST = "#d4793a" # connectors, accent stroke -RUST_D = "#b5531f" # deeper rust (arrowheads/emphasis) -AMBER = "#f6a821" -AMBER_B = "#ffc24a" -GREEN = "#1f8a4c" # query / success accent (matches admin "green" badges) +FIELD = "#f3f1eb" # cream card fill +FIELD2 = "#ffffff" # lighter cream card fill +ACCENT = "#fff1dd" # highlight/accent card fill +ACCENT_S = "#855414" # highlight/accent card stroke +CARD_S = "#d8d4ca" # default card stroke +RUST = "#ffb34a" # connectors, accent stroke +RUST_D = "#855414" # deeper rust (arrowheads/emphasis) +AMBER = "#ffb34a" +AMBER_B = "#ffb34a" +GREEN = "#52613d" # query / success accent (matches admin "green" badges) GREEN_BG = "#ecf9f0" -BLUE = "#2563c9" # command / note accent (matches admin "blue" badges) +BLUE = "#456578" # command / note accent (matches admin "blue" badges) BLUE_BG = "#eef4ff" -RED = "#b03a2e" # failure / compensation (matches existing saga figs) +RED = "#a83c32" # failure / compensation (matches existing saga figs) RED_BG = "#fdecea" -TITLE = "#2a1d10" # card titles -TITLE2 = "#3a2a1c" # secondary titles -SUB = "#7a6450" # sub-labels -LANE_BG = "#f7ecd8" # swimlane background -LANE_S = "#e6d4b0" # swimlane stroke +TITLE = "#10110f" # card titles +TITLE2 = "#272820" # secondary titles +SUB = "#62645b" # sub-labels +LANE_BG = "#e9e7df" # swimlane background +LANE_S = "#d8d4ca" # swimlane stroke FONT = "Avenir Next,Avenir,Helvetica Neue,Helvetica,Arial,sans-serif" MONO = "SF Mono,JetBrains Mono,Menlo,Consolas,monospace" @@ -100,7 +88,7 @@ def card(x, y, w, h, title, *, sub=None, mono=False, accent=False, out = [ # soft drop shadow plate, then the card f'', + f'rx="9" fill="#bfb8ab" opacity="0.22"/>', f'', f'`` markup the book embeds. ``caption`` may contain inline HTML (e.g. ``
`` spans).""" - return ( + markup = ( '
\n' f'\n' f'
{caption}
\n' '
\n') + start, end = markup.index("") + len("") + return markup[:start] + diagram_footer(markup[start:end], key="figure-rsfly") + markup[end:] + def _bare(fig_html: str) -> str: """Strip the ``
``/``
`` wrapper, leaving the bare ```` for the preview files written to art/diagrams/.""" start = fig_html.index("") + len("") + end = fig_html.rindex("") + len("") return fig_html[start:end] + "\n" @@ -381,7 +372,7 @@ def fig_di_graph(): b = [] # the container plate behind everything b.append(f'') + f'fill="#f3f1eb" stroke="{LANE_S}" stroke-width="1.3"/>') b.append(label(36, 36, "Container · scan() wires beans in dependency order", size=12, weight="700", fill=TITLE2, anchor="start")) # controller at top, two collaborators below, two ports at the bottom @@ -630,7 +621,7 @@ def fig_tcc(): ("(none — nothing held)", RED, RED_BG)])] for ri, (rname, cells) in enumerate(rows): ry = 60 + ri * 74 - b.append(label(20, ry + 28, rname, size=11.5, weight="700", fill="#8a6d3b", + b.append(label(20, ry + 28, rname, size=11.5, weight="700", fill="#62645b", anchor="start")) for ci, (txt, stroke, fill) in enumerate(cells): x = cx[ci] - cw / 2 @@ -772,7 +763,7 @@ def fig_openapi(): b.append(arrow(224, 175, 318, 130, label=None)) # spec card with little "lines" b.append(f'') + f'fill="#bfb8ab" opacity="0.22"/>') b.append(f'') b.append(f'') diff --git a/docs/book/build/gen_openers.py b/docs/book/build/gen_openers.py index b9cfe471..5d226d56 100644 --- a/docs/book/build/gen_openers.py +++ b/docs/book/build/gen_openers.py @@ -1,16 +1,12 @@ -"""Generate on-brand chapter-opener SVGs for *Firefly for Rust by Example*. +"""Generate rsfly chapter openers with the canonical gradient-y family lockup. -Each opener is a 720x300 banner sharing one visual language with the cover: -a warm cream diagram field on the left, and a deep night-sky panel on the -right holding a glowing **firefly** emblem (the same bioluminescent motif as -the cover, not a literal gear). The emblem + palette are constant so the set -reads as a family; the left-hand glyph differs per chapter so each opener -previews that chapter's idea. - -Run: python build/gen_openers.py (writes art/openers/*.svg) +The left-hand technical glyphs retain their original coordinates. The right +panel uses the approved outlined artwork imported from the Firefly Brand Kit. +Run: python build/gen_openers.py (writes art/openers/*.svg). """ from __future__ import annotations from pathlib import Path +from brand import lockup from xml.sax.saxutils import escape as _xml_escape ART = Path(__file__).resolve().parents[1] / "art" / "openers" @@ -22,22 +18,22 @@ def esc(s: str) -> str: return _xml_escape(str(s)) # ---- palette --------------------------------------------------------------- -FIELD = "#fdf6ea" # warm cream banner field -FIELD2 = "#f7ecd8" # slightly deeper cream -PANEL1 = "#0e1217" # night-sky panel, cool top -PANEL2 = "#16100b" # night-sky panel, warm bottom -AMBER = "#f6a821" -AMBER_B = "#ffc24a" # bright amber -AMBER_D = "#c97e10" -RUST = "#d4793a" -RUST_D = "#b5531f" -INK = "#3a2a1c" # dark text / dark fills on the cream field -MUTED = "#9a8163" -GEAR = "#e8923f" -NODE = "#fffaf0" # light card on the cream field -CREAM = "#f3e7cf" # light text on the dark panel -GREEN = "#1f8a4c" -GREEN_B = "#bfe26a" # bioluminescent green-gold accent (matches the cover) +FIELD = "#f3f1eb" # warm cream banner field +FIELD2 = "#e9e7df" # slightly deeper cream +PANEL1 = "#10110f" # night-sky panel, cool top +PANEL2 = "#10110f" # night-sky panel, warm bottom +AMBER = "#ffb34a" +AMBER_B = "#ffb34a" # bright amber +AMBER_D = "#855414" +RUST = "#ffb34a" +RUST_D = "#855414" +INK = "#272820" # dark text / dark fills on the cream field +MUTED = "#62645b" +GEAR = "#ffb34a" +NODE = "#ffffff" # light card on the cream field +CREAM = "#f3f1eb" # light text on the dark panel +GREEN = "#52613d" +GREEN_B = "#7a8460" # olive control accent W, H = 720, 300 # the night-sky panel on the right @@ -77,41 +73,8 @@ def defs() -> str: def emblem() -> str: - """The constant glowing-firefly emblem on the night-sky panel (right).""" - cx, cy = 601, 150 - return ( - f'' - # ambient bloom on the panel - f'' - f'' - # light trail curving in from lower-left (two strokes = a taper) - f'' - f'' - # the firefly, gently tilted for life - '' - f'' - f'' - # wings, swept back and translucent - f'' - f'' - # glowing abdomen - f'' - f'' - # dark thorax + head with an amber rim - f'' - f'' - # antennae - f'' - f'' - '' - # satellite motes (one bioluminescent green, matching the cover) - f'' - f'' - f'' - '' - ) + """Canonical outlined rsfly lockup on the existing dark panel.""" + return lockup(506, 82, 184, 133, theme="dark", key="opener-rsfly") def frame(num: str, kicker: str) -> str: @@ -124,7 +87,7 @@ def frame(num: str, kicker: str) -> str: f'' # a few distant motes inside the panel - f'' + f'' f'' f'' f'' @@ -142,7 +105,7 @@ def frame(num: str, kicker: str) -> str: def card(x, y, w, label, fill=NODE, stroke=RUST, tcol=INK, fs=14): return ( f'' - f'' + f'' f'' f'' f'' - f'{esc(label)}' ) @@ -196,7 +159,7 @@ def s_choice(): # why firefly — chaos -> cohesion + card(120, 110, 96, "sqlx?", NODE, MUTED, MUTED, 13) + card(56, 164, 96, "DI?", NODE, MUTED, MUTED, 13)) return (cards + arrow(232, 130, 296, 130) - + card(308, 108, 150, "Firefly Core", "url(#grh)", RUST_D, "#16110c", 16) + + card(308, 108, 150, "Firefly Core", "url(#grh)", RUST_D, "#10110f", 16) + spark(250, 72) + spark(280, 178, 5, AMBER, 0.8)) @@ -218,12 +181,12 @@ def s_config(): for lbl, y in layers: out.append(card(56, y, 200, lbl, NODE, RUST, INK, 13)) out.append(arrow(150, 174, 150, 206)) - out.append(card(96, 208, 120, "Settings", "url(#grh)", RUST_D, "#16110c", 14)) + out.append(card(96, 208, 120, "Settings", "url(#grh)", RUST_D, "#10110f", 14)) return "".join(out) def s_di(): - hub = card(120, 120, 120, "Context", "url(#grh)", RUST_D, "#16110c", 14) + hub = card(120, 120, 120, "Context", "url(#grh)", RUST_D, "#10110f", 14) beans = (chip(40, 58, "#[component]") + chip(40, 188, "Arc") + chip(300, 60, "@autowired") + chip(300, 188, "lifecycle")) spokes = (f'' @@ -235,7 +198,7 @@ def s_di(): def s_wiring(): - return (card(52, 70, 104, "Core", "url(#grh)", RUST_D, "#16110c", 14) + return (card(52, 70, 104, "Core", "url(#grh)", RUST_D, "#10110f", 14) + card(52, 130, 104, "cache", NODE, RUST, INK, 13) + card(52, 190, 104, "broker", NODE, RUST, INK, 13) + arrow(160, 90, 220, 130) + arrow(160, 150, 220, 150) + arrow(160, 210, 220, 170) @@ -260,7 +223,7 @@ def s_reactive(): def s_http(): return (chip(48, 70, "GET /wallets") + chip(48, 134, "POST /wallets") + arrow(196, 130, 256, 130) - + card(266, 108, 120, "Router", "url(#grh)", RUST_D, "#16110c", 14) + + card(266, 108, 120, "Router", "url(#grh)", RUST_D, "#10110f", 14) + chip(300, 196, "200 OK", AMBER)) @@ -275,7 +238,7 @@ def s_persist(): def s_ddd(): - agg = card(170, 116, 150, "Wallet aggregate", "url(#grh)", RUST_D, "#16110c", 13) + agg = card(170, 116, 150, "Wallet aggregate", "url(#grh)", RUST_D, "#10110f", 13) vos = (chip(40, 70, "Money") + chip(40, 196, "WalletId") + chip(330, 70, "invariant") + chip(330, 196, "domain event")) return vos + agg @@ -285,12 +248,12 @@ def s_cqrs(): return (card(48, 64, 150, "DepositCommand", NODE, RUST, INK, 13) + card(48, 172, 150, "BalanceQuery", NODE, RUST, INK, 13) + arrow(202, 84, 262, 126) + arrow(202, 192, 262, 150) - + card(272, 108, 110, "Bus", "url(#grh)", RUST_D, "#16110c", 15) + + card(272, 108, 110, "Bus", "url(#grh)", RUST_D, "#10110f", 15) + arrow(384, 130, 410, 130)) def s_eda(): - hub = card(160, 120, 120, "EventBus", "url(#grh)", RUST_D, "#16110c", 13) + hub = card(160, 120, 120, "EventBus", "url(#grh)", RUST_D, "#10110f", 13) nodes = (chip(40, 64, "Kafka") + chip(40, 200, "RabbitMQ") + chip(320, 66, "listener") + chip(316, 196, "projection")) spokes = (f'' @@ -305,12 +268,12 @@ def s_es(): dots = "".join(card(40 + i*70, 116, 56, e, NODE, RUST, INK, 12) for i, e in enumerate(["+10", "-3", "+5"])) return (dots + arrow(220, 136, 248, 136) - + card(258, 110, 120, "= balance 12", "url(#grh)", RUST_D, "#16110c", 13) + + card(258, 110, 120, "= balance 12", "url(#grh)", RUST_D, "#10110f", 13) + chip(40, 188, "replay the stream")) def s_clients(): - return (card(48, 116, 120, "Lumen", "url(#grh)", RUST_D, "#16110c", 14) + return (card(48, 116, 120, "Lumen", "url(#grh)", RUST_D, "#10110f", 14) + arrow(172, 136, 250, 136) + card(260, 116, 130, "Payments API", NODE, RUST, INK, 13) + chip(60, 190, "WebClient · retry · breaker")) @@ -318,7 +281,7 @@ def s_clients(): def s_bff(): return (chip(40, 130, "mobile") + arrow(120, 144, 170, 144) - + card(180, 120, 110, "BFF", "url(#grh)", RUST_D, "#16110c", 15) + + card(180, 120, 110, "BFF", "url(#grh)", RUST_D, "#10110f", 15) + arrow(292, 122, 330, 96) + arrow(292, 144, 340, 144) + arrow(292, 166, 330, 192) + chip(338, 80, "wallets") + chip(348, 130, "ledger") + chip(338, 180, "fx")) @@ -358,7 +321,7 @@ def s_observe(): def s_cache(): return (chip(48, 80, "request") + arrow(132, 94, 180, 94) - + card(190, 72, 110, "Cache", "url(#grh)", RUST_D, "#16110c", 14) + + card(190, 72, 110, "Cache", "url(#grh)", RUST_D, "#10110f", 14) + card(190, 150, 110, "Resilience", NODE, RUST, INK, 13) + chip(316, 80, "cache hit") + chip(316, 162, "retry · breaker")) @@ -383,13 +346,13 @@ def s_macros(): def s_testing(): return (card(48, 116, 130, "StepVerifier", NODE, RUST, INK, 13) + arrow(182, 136, 244, 136) - + card(254, 116, 120, "Testcontainers", "url(#grh)", RUST_D, "#16110c", 12) + + card(254, 116, 120, "Testcontainers", "url(#grh)", RUST_D, "#10110f", 12) + chip(60, 70, "all green", AMBER) + chip(60, 190, "real infra")) def s_cli(): term = (f'' + f'fill="#10110f"/>' f'' f'$ firefly new' f'$ firefly run' @@ -402,14 +365,14 @@ def s_prod(): + arrow(160, 136, 184, 136) + card(192, 116, 110, "image", NODE, RUST, INK, 13) + arrow(304, 136, 328, 136) - + card(336, 116, 60, "ship", "url(#grh)", RUST_D, "#16110c", 13) + + card(336, 116, 60, "ship", "url(#grh)", RUST_D, "#10110f", 13) + chip(60, 70, "12-factor") + chip(180, 196, "k8s · health · graceful")) def s_appa(): return (card(40, 96, 130, "Spring Boot", NODE, MUTED, MUTED, 13) + arrow(174, 116, 234, 116) - + card(244, 96, 130, "Firefly", "url(#grh)", RUST_D, "#16110c", 14) + + card(244, 96, 130, "Firefly", "url(#grh)", RUST_D, "#10110f", 14) + chip(60, 178, "@Component → #[component]") + chip(60, 214, "@Transactional → #[transactional]")) @@ -423,7 +386,7 @@ def s_appb(): def s_bootstrap(): # FireflyApplication::new("lumen").run() ignites the whole stack: one call # on the left fans out into the services the framework discovers & wires. - call = (card(48, 132, 150, "new().run()", "url(#grh)", RUST_D, "#16110c", 14) + call = (card(48, 132, 150, "new().run()", "url(#grh)", RUST_D, "#10110f", 14) + spark(60, 90, 5, AMBER, 0.85) + spark(178, 102, 4, AMBER_B, 0.8)) fans = (arrow(202, 142, 270, 78) + arrow(202, 150, 274, 150) + arrow(202, 158, 270, 222)) pieces = (chip(280, 64, "web + CQRS") + chip(284, 136, "scan beans") @@ -436,7 +399,7 @@ def s_openapi(): sources = (chip(40, 70, "#[rest_controller]") + chip(40, 196, "#[derive(Schema)]")) funnel = (arrow(196, 92, 256, 132) + arrow(196, 210, 256, 156)) doc = (f'' - f'' + f'' f'' # a little "spec" sheet: title bar + lines @@ -457,7 +420,7 @@ def s_layered(): y = 60 + i * 38 fill = "url(#grh)" if i == 4 else NODE stroke = RUST_D if i == 4 else RUST - tcol = "#16110c" if i == 4 else INK + tcol = "#10110f" if i == 4 else INK # slight left inset per layer to read as a stack rows.append(card(44 + i * 6, y, 300 - i * 12, c, fill, stroke, tcol, 13)) return "".join(rows) + spark(420, 96, 5, AMBER, 0.8) + spark(404, 196, 4, AMBER_B, 0.7) diff --git a/docs/book/build/md.py b/docs/book/build/md.py index 25445e32..e829ac50 100644 --- a/docs/book/build/md.py +++ b/docs/book/build/md.py @@ -1,4 +1,4 @@ -"""Markdown -> HTML for *Firefly for Rust by Example*. +"""Markdown -> HTML for *rsfly by example*. The src/ chapters are authored in mdBook-flavoured Markdown, so this renderer is tuned to that dialect rather than python-markdown's admonition syntax: diff --git a/docs/book/build/pdf.py b/docs/book/build/pdf.py index d855a92e..55e3ffb9 100644 --- a/docs/book/build/pdf.py +++ b/docs/book/build/pdf.py @@ -5,13 +5,60 @@ invoking Python, so the import here succeeds. """ from __future__ import annotations +from io import BytesIO +import base64 from pathlib import Path +import re +import cairosvg +from pypdf import PdfReader, PdfWriter from weasyprint import HTML, CSS -def render_pdf(full_html: str, base_url: Path, css_paths: list[Path], out: Path) -> Path: +def _brand_fidelity(html: str) -> str: + """Use canonical PNGs for small lockups affected by WeasyPrint's SVG gradients.""" + stack, replacements = [], [] + for match in re.finditer(r'<(/?)svg\b[^>]*>', html): + if not match.group(1): + stack.append((match.start(), match.end())) + else: + start, body_start = stack.pop() + body = html[body_start:match.start()] + if not body.startswith('rsfly - Rust framework by Firefly'): + continue + theme = "dark" if 'rsfly - Rust framework by Firefly' + f'') + replacements.append((body_start, match.start(), replacement)) + for start, end, replacement in reversed(replacements): + html = html[:start] + replacement + html[end:] + return html + + +def render_pdf(full_html: str, base_url: Path, css_paths: list[Path], out: Path, + cover_pages: dict[str, Path] | None = None) -> Path: out = Path(out) out.parent.mkdir(parents=True, exist_ok=True) - HTML(string=full_html, base_url=str(base_url)).write_pdf( + HTML(string=_brand_fidelity(full_html), base_url=str(base_url)).write_pdf( str(out), stylesheets=[CSS(filename=str(p)) for p in css_paths]) + if cover_pages: + # Cairo preserves translucent SVG gradients that WeasyPrint flattens. + # Replace the already reserved cover pages; body pagination stays intact. + writer = PdfWriter(clone_from=str(out)) + for side, path in cover_pages.items(): + index = 0 if side == "cover" else len(writer.pages) - 1 + original = writer.pages[index] + vector = cairosvg.svg2pdf( + url=str(path), output_width=float(original.mediabox.width) * 4 / 3, + output_height=float(original.mediabox.height) * 4 / 3) + page = PdfReader(BytesIO(vector)).pages[0] + writer.remove_page(index) + writer.insert_page(page, index) + buffer = BytesIO() + writer.write(buffer) + out.write_bytes(buffer.getvalue()) return out diff --git a/docs/book/requirements.txt b/docs/book/requirements.txt new file mode 100644 index 00000000..12b41e3f --- /dev/null +++ b/docs/book/requirements.txt @@ -0,0 +1,20 @@ +brotli==1.2.0 +cairocffi==1.7.1 +cairosvg==2.8.2 +cffi==2.1.1 +cssselect2==0.10.1 +defusedxml==0.7.1 +fonttools==4.66.1 +markdown==3.8.2 +pillow==12.3.0 +pycparser==3.1 +pydyf==0.12.1 +pygments==2.19.2 +pypdf==6.1.1 +pyphen==0.18.1 +pyyaml==6.0.2 +tinycss2==1.5.1 +tinyhtml5==2.1.0 +weasyprint==66.0 +webencodings==0.6.1 +zopfli==0.4.3 diff --git a/docs/book/src-es/00-front/00-preface.md b/docs/book/src-es/00-front/00-preface.md index ca2f4cd7..2012d16d 100644 --- a/docs/book/src-es/00-front/00-preface.md +++ b/docs/book/src-es/00-front/00-preface.md @@ -1,5 +1,7 @@ ## Prefacio +**rsfly** es el nombre de producto de Firefly para Rust. Esta edición conserva los nombres de crates, imports y API de `firefly` utilizados en los ejemplos. + Rust te ofrece concurrencia sin miedo, abstracciones de coste cero y un compilador que se niega a publicar una condición de carrera de datos. Lo que no te da es *cohesión*. Cada nuevo servicio de back-office obliga a la misma cascada de decisiones antes de escribir una sola línea de lógica de negocio: qué capa HTTP, qué planteamiento de base de datos, cómo cablear las dependencias, cómo gestionar la configuración, los errores, los identificadores de correlación, las métricas y el apagado ordenado. **Firefly** cambia eso. Es un framework dogmático, de convención sobre configuración, que toma esas decisiones transversales una sola vez, de modo que todos los servicios comparten un mismo idioma — construido desde cero para Rust 1.88+ sobre `tokio` y `axum`. Este libro enseña Firefly **mediante ejemplos**. Construyes una aplicación real desde un crate vacío hasta un servicio asegurado, observable y basado en event sourcing — haciendo concreto cada concepto antes de pasar al siguiente. El código de estas páginas no es pseudocódigo ilustrativo: cada listado es una porción de un **proyecto real que compila, arranca y supera sus pruebas** contra la versión 26.6.x del framework. Cada fragmento se ha extraído del ejemplo en ejecución y se ha verificado contra las API de los crates, de modo que lo que lees es lo que realmente funciona. Cuando un listado se desvía de la fuente, la compilación del ejemplo se rompe y una prueba falla — esa es la garantía que respalda cada listado de este libro. diff --git a/docs/book/src-es/00-front/00-title.md b/docs/book/src-es/00-front/00-title.md index 773c420f..47360239 100644 --- a/docs/book/src-es/00-front/00-title.md +++ b/docs/book/src-es/00-front/00-title.md @@ -1,5 +1,7 @@ -# Firefly para Rust con ejemplos {.chtitle} +rsfly - Rust framework by Firefly -### Microservicios Rust orientados a eventos con el framework Firefly +# rsfly by example {.chtitle} + +### Servicios tipados con Tokio, Axum y SQLx **Firefly Software Foundation** diff --git a/docs/book/src-es/01-why-firefly.md b/docs/book/src-es/01-why-firefly.md index 21253eca..62e63438 100644 --- a/docs/book/src-es/01-why-firefly.md +++ b/docs/book/src-es/01-why-firefly.md @@ -287,59 +287,59 @@ de Cargo impone la estratificación. Rara vez nombras estos crates directamente y *qué* capítulo del libro la desbloquea.
- -firefly + firefly-macrosone dependency · use firefly::prelude::*; - - -Tier 1 -Foundational -kernel -reactive -web -config -container -i18n - - - - -Tier 2 -Platform -cqrs -eda -event-sourcing -orchestration -cache -security - - - - -Tier 3 -Adapters -data-sqlx -data-mongodb -eda-kafka -cache-redis -idp-* -notif-* - - - - -Tier 4 -Starters -starter-core -starter-web -starter-domain -starter-data -admin -cli - -firefly-reactivethe Mono / Flux core every tier rests on (tokio · axum) - +firefly + firefly-macrosone dependency · use firefly::prelude::*; + + +Tier 1 +Foundational +kernel +reactive +web +config +container +i18n + + + + +Tier 2 +Platform +cqrs +eda +event-sourcing +orchestration +cache +security + + + + +Tier 3 +Adapters +data-sqlx +data-mongodb +eda-kafka +cache-redis +idp-* +notif-* + + + + +Tier 4 +Starters +starter-core +starter-web +starter-domain +starter-data +admin +cli + +firefly-reactivethe Mono / Flux core every tier rests on (tokio · axum) +rsfly - Rust framework by Firefly
Las cuatro capas. Un servicio depende únicamente de la fachada firefly (la puerta de entrada). Las capas se construyen de izquierda a derecha: el vocabulario Fundacional, los motores de Plataforma que definen los puertos, los Adaptadores que los implementan y los Starters que componen y distribuyen, cada uno dependiendo solo de las capas a su izquierda, todos apoyados sobre el núcleo firefly-reactive.
diff --git a/docs/book/src-es/02-quickstart.md b/docs/book/src-es/02-quickstart.md index b67c1005..bbd34066 100644 --- a/docs/book/src-es/02-quickstart.md +++ b/docs/book/src-es/02-quickstart.md @@ -337,15 +337,15 @@ ahora: la red pública.
- -Public API :8080client-facing -Management :8081operator-facing -#[rest_controller]your routesSecurityJWT · roles · sessionsRFC 9457 404problem+json fallback -/actuator/*health · info · metrics/adminself-hosted dashboard/swagger-ui · /redoc/v3/api-docs -FIREFLY_SERVER_ADDR · FIREFLY_MANAGEMENT_ADDR override the binds - +Public API :8080client-facing +Management :8081operator-facing +#[rest_controller]your routesSecurityJWT · roles · sessionsRFC 9457 404problem+json fallback +/actuator/*health · info · metrics/adminself-hosted dashboard/swagger-ui · /redoc/v3/api-docs +FIREFLY_SERVER_ADDR · FIREFLY_MANAGEMENT_ADDR override the binds +rsfly - Rust framework by Firefly
Dos listeners, un proceso. La API pública (:8080) sirve tus controladores, la seguridad y el fallback 404 de RFC 9457; la superficie de gestión (:8081) sirve el actuator, el panel /admin autoalojado y la documentación OpenAPI, de modo que los endpoints operativos nunca se filtran a la red pública.
- **`FIREFLY_SERVER_ADDR` / `FIREFLY_MANAGEMENT_ADDR`** sobrescriben las direcciones diff --git a/docs/book/src-es/03-configuration.md b/docs/book/src-es/03-configuration.md index edc4359b..c649e7f8 100644 --- a/docs/book/src-es/03-configuration.md +++ b/docs/book/src-es/03-configuration.md @@ -286,23 +286,23 @@ tanto, sobrescriben a las más bajas. | 5 | Flags de CLI — `FlagSource::new().set("web.addr", "0.0.0.0:80")` | todo |
- -defaultsStaticSource - -base YAMLapplication.yaml - -profile YAMLapplication-prod.yaml - -environmentFIREFLY_* - -CLI flagsFlagSource -merged left → right · last write wins -beats nothing -beats everything -an env override beats a YAML file; a CLI flag beats both - +defaultsStaticSource + +base YAMLapplication.yaml + +profile YAMLapplication-prod.yaml + +environmentFIREFLY_* + +CLI flagsFlagSource +merged left → right · last write wins +beats nothing +beats everything +an env override beats a YAML file; a CLI flag beats both +rsfly - Rust framework by Firefly
Layered::new(...) fusiona sus sources de izquierda a derecha y gana la última escritura. Los defaults se sitúan los primeros y no vencen a nada; un YAML base vence a los defaults; una superposición de perfil vence a la base; el entorno vence a los archivos YAML; y un flag de CLI vence a todo: un único artefacto, desplegable en cualquier lugar.
diff --git a/docs/book/src-es/04a-dependency-injection.md b/docs/book/src-es/04a-dependency-injection.md index 7faa8f5e..d8e38f46 100644 --- a/docs/book/src-es/04a-dependency-injection.md +++ b/docs/book/src-es/04a-dependency-injection.md @@ -115,21 +115,21 @@ lugar de un pánico en lo más profundo de la primera petición cuando falta alg > ese grafo en mente; cada paso de abajo cablea una arista de él.
- - -Container · scan() wires beans in dependency order -WalletApi#[derive(Controller)] -Ledger#[derive(Service)] -ReadModel#[derive(Component)] -EventStore#[derive(Repository)] -Brokerport — #[autowired] -autowired -autowired - - - + +Container · scan() wires beans in dependency order +WalletApi#[derive(Controller)] +Ledger#[derive(Service)] +ReadModel#[derive(Component)] +EventStore#[derive(Repository)] +Brokerport — #[autowired] +autowired +autowired + + +rsfly - Rust framework by Firefly
El contenedor escanea los beans de estereotipo y los cablea en orden de dependencias. WalletApi autocablea el Ledger y el ReadModel; el Ledger autocablea los puertos EventStore y Broker — sin raíz de composición a mano.
diff --git a/docs/book/src-es/05-reactive-model.md b/docs/book/src-es/05-reactive-model.md index e5e56478..455ca252 100644 --- a/docs/book/src-es/05-reactive-model.md +++ b/docs/book/src-es/05-reactive-model.md @@ -163,30 +163,30 @@ el resto están ahí para cuando un caso límite los necesite. Constructores de `Mono`:
- -Mono<T> -0 or 1 item, then complete - - - - -just(v) -complete - -Flux<T> -0..N items, then complete - - - - - - - - -map · filter · flat_map - +Mono<T> +0 or 1 item, then complete + + + + +just(v) +complete + +Flux<T> +0..N items, then complete + + + + + + + + +map · filter · flat_map +rsfly - Rust framework by Firefly
Los dos tipos de retorno reactivos. Un Mono<T> emite como mucho un elemento (Ok(Some)), o ninguno (Ok(None)), y luego completa; un Flux<T> emite un stream de cero o más elementos. Ambos cortocircuitan ante un Err(FireflyError) terminal.
diff --git a/docs/book/src-es/06-first-http-api.md b/docs/book/src-es/06-first-http-api.md index bfd1f556..1b979f45 100644 --- a/docs/book/src-es/06-first-http-api.md +++ b/docs/book/src-es/06-first-http-api.md @@ -327,17 +327,17 @@ GET /api/v1/wallets/wlt_1 Accept: application/xml ```
- -inbound HTTP request - -ProblemLayererrors → problem+jsonTraceContextLayerW3C traceparent in / outCorrelationLayerensure-or-generate idContentNegotiationLayerAccept → JSON / XML - -#[rest_controller]your handler runs -outermost -innermost - +inbound HTTP request + +ProblemLayererrors → problem+jsonTraceContextLayerW3C traceparent in / outCorrelationLayerensure-or-generate idContentNegotiationLayerAccept → JSON / XML + +#[rest_controller]your handler runs +outermost +innermost +rsfly - Rust framework by Firefly
La pila de capas por defecto, de la más externa a la más interna (algunas capas opcionales —CORS, cabeceras de seguridad, métricas— se omiten). ProblemLayer envuelve todo, de modo que cualquier error se desenrolla hasta una respuesta RFC 9457 application/problem+json; el contexto de traza y la correlación se abren antes de que se ejecute tu handler; la negociación de contenido se sitúa más cerca de las rutas.
diff --git a/docs/book/src-es/06a-openapi.md b/docs/book/src-es/06a-openapi.md index 534e12a3..30a6804e 100644 --- a/docs/book/src-es/06a-openapi.md +++ b/docs/book/src-es/06a-openapi.md @@ -92,27 +92,27 @@ el puerto de **gestión**, por defecto son: | `/redoc` | ReDoc, apuntando a la especificación |
- -#[rest_controller]routes + status codes -#[derive(Schema)]DTO component schemas - - - - - - - - - -openapi.json -(/v3/api-docs) - - -Swagger UI -ReDoc - +#[rest_controller]routes + status codes +#[derive(Schema)]DTO component schemas + + + + + + + + + +openapi.json +(/v3/api-docs) + + +Swagger UI +ReDoc +rsfly - Rust framework by Firefly
Sin paso de generación de código, sin framework de anotaciones. En el arranque, FireflyApplication recolecta los atributos de enrutamiento y cada tipo con #[derive(Schema)] en un único documento OpenAPI 3.1 (servido en /v3/api-docs en el puerto de gestión) y apunta a él Swagger UI y ReDoc.
diff --git a/docs/book/src-es/09-cqrs.md b/docs/book/src-es/09-cqrs.md index b8164dd7..af1ecfad 100644 --- a/docs/book/src-es/09-cqrs.md +++ b/docs/book/src-es/09-cqrs.md @@ -466,23 +466,23 @@ registrado = más externo: | `QueryCache::middleware()` | memoiza los resultados de mensajes cuyo `cache_ttl` es `Some`; instalado cuando existe un bean `QueryCache` |
- -send / query a message - -msg ↦ TypeIdmatched to a handler -middleware chain - -V - -C - -Q -V = Validation · C = Correlation · Q = QueryCache - -your handlerCommand or Query - +send / query a message + +msg ↦ TypeIdmatched to a handler +middleware chain + +V + +C + +Q +V = Validation · C = Correlation · Q = QueryCache + +your handlerCommand or Query +rsfly - Rust framework by Firefly
Un mensaje se empareja con su manejador mediante TypeId, y luego recorre la cadena de middleware registrada —Validation como la más externa, después Correlation, después QueryCache— antes de que el manejador se ejecute. El ámbito de correlación se abre antes de la capa de caché, de modo que todo lo que esta registra lleva el id.
@@ -609,44 +609,44 @@ abierto antes de que se ejecute la capa de caché, de modo que cualquier cosa qu la caché registre lleva el id también:
- - send / query a message - + send / query a message + - - + msg ↦ TypeId - + - middleware chain + middleware chain - - + V - - + C - - + Q - + - + V = ValidationMiddleware C = Correlation Q = QueryCache - + - - your handler - + + your handler +rsfly - Rust framework by Firefly
El framework registra primero ValidationMiddleware (la más externa), luego CorrelationMiddleware y después QueryCache: el ámbito de correlación se abre antes de que se ejecute la capa de caché, de modo que todo lo que esta registra lleva el id.
diff --git a/docs/book/src-es/11-event-sourcing.md b/docs/book/src-es/11-event-sourcing.md index 2a49a39c..3f6b6825 100644 --- a/docs/book/src-es/11-event-sourcing.md +++ b/docs/book/src-es/11-event-sourcing.md @@ -126,21 +126,21 @@ de forma duradera bajo concurrencia optimista; una carga posterior **pliega** del capítulo es uno de estos tres movimientos.
- -write path -CommandDeposit { amount }raise(event)→ uncommitted []append(events)optimistic concurrency -event stream (append-only) -+100WalletOpened - -+50MoneyDeposited - -−30MoneyWithdrawn -append -fold / replay -current statebalance = 120 - +write path +CommandDeposit { amount }raise(event)→ uncommitted []append(events)optimistic concurrency +event stream (append-only) ++100WalletOpened + ++50MoneyDeposited + +−30MoneyWithdrawn +append +fold / replay +current statebalance = 120 +rsfly - Rust framework by Firefly
Tres movimientos. Un comando hace raise de un evento sobre el agregado; EventStore::append persiste los eventos no confirmados bajo concurrencia optimista; una carga posterior hace fold de todo el stream de solo anexado de vuelta al estado actual — los eventos son la fuente de verdad, el estado es derivado.
diff --git a/docs/book/src-es/12-sagas.md b/docs/book/src-es/12-sagas.md index 145f2e13..cc5ce9ba 100644 --- a/docs/book/src-es/12-sagas.md +++ b/docs/book/src-es/12-sagas.md @@ -120,17 +120,17 @@ que pueda tener éxito antes de que falle uno posterior. "Reabonar al origen" es de reembolso auditable.
- -forward: dependency-ordered steps -debitwithdraw(amount)creditdeposit(amount)notifypublish event -may fail - - -compensate — reverse order -a compensation is a forward undo, not a database rollback - +forward: dependency-ordered steps +debitwithdraw(amount)creditdeposit(amount)notifypublish event +may fail + + +compensate — reverse order +a compensation is a forward undo, not a database rollback +rsfly - Rust framework by Firefly
Una saga ejecuta sus pasos en orden de dependencias. Si un paso falla, el motor ejecuta las compensaciones de los pasos ya completados en orden inverso — aquí un credit fallido reembolsa el debit. Una compensación es una acción hacia delante que deshace, no un rollback de base de datos.
@@ -575,16 +575,16 @@ condición de salto — el nodo se omite cuando el predicado es falso) y WorkflowError>`.
- -parallel layer -balance-checkfunds_ok: bool -limit-checkwithin_limit: bool -approvedepends_on both - - - +parallel layer +balance-checkfunds_ok: bool +limit-checkwithin_limit: bool +approvedepends_on both + + +rsfly - Rust framework by Firefly
Un workflow es un DAG de pasos. balance-check y limit-check no tienen dependencia entre sí, así que se ejecutan en la misma capa paralela; approve espera a ambos y consume sus veredictos.
@@ -820,27 +820,27 @@ publica bajo su nombre, de modo que confirm y cancel pueden leerlo vía genera `Tcc::tcc(self: Arc)` y `run(self, input) -> Result<(), TccError>`.
- -Try -reserve -Confirm -on all-tried -Cancel -on a try failure -source -withdraw (hold) -(none — held) -deposit (release) -dest -verify exists -deposit (capture) -(none — nothing held) - -all tried → confirm -any try fails → cancel tried in reverse - +Try +reserve +Confirm +on all-tried +Cancel +on a try failure +source +withdraw (hold) +(none — held) +deposit (release) +dest +verify exists +deposit (capture) +(none — nothing held) + +all tried → confirm +any try fails → cancel tried in reverse +rsfly - Rust framework by Firefly
Try / Confirm / Cancel. El Try de cada participante reserva; una vez que todos han intentado, Confirm captura; si algún Try falla, el motor Cancela los participantes ya intentados en orden inverso. El origen retiene fondos en el Try y los libera en el Cancel; el destino captura en el Confirm.
diff --git a/docs/book/src-es/21-declarative-macros.md b/docs/book/src-es/21-declarative-macros.md index 01a64627..ce006ac1 100644 --- a/docs/book/src-es/21-declarative-macros.md +++ b/docs/book/src-es/21-declarative-macros.md @@ -188,27 +188,27 @@ aterriza y lo que genera. | `#[firefly::tcc]` + `#[participant]` | `tcc_transfer.rs` | un `run` de TCC que conduce el try / confirm / cancel de cada participante |
- -you write -the macro generates -#[derive(Command)] - -the Message impl (kind, validate, cache_ttl) -#[derive(Schema)] - -an OpenAPI schema (appears in /v3/api-docs) -#[derive(DomainEvent)] - -EVENT_TYPE + to_domain_event -#[rest_controller] - -a Controller bean + WalletApi::routes(state) -#[derive(Service)] - -a scanned bean with #[autowired] fields - +you write +the macro generates +#[derive(Command)] + +the Message impl (kind, validate, cache_ttl) +#[derive(Schema)] + +an OpenAPI schema (appears in /v3/api-docs) +#[derive(DomainEvent)] + +EVENT_TYPE + to_domain_event +#[rest_controller] + +a Controller bean + WalletApi::routes(state) +#[derive(Service)] + +a scanned bean with #[autowired] fields +rsfly - Rust framework by Firefly
Declarativo, en tiempo de compilación. Cada atributo o derive se expande al cableado que de otro modo escribirías a mano: un impl de Message, un esquema OpenAPI, un discriminador de evento, un constructor routes() de controlador o un bean escaneado con campos autowired — generado por firefly-macros, no por un paso de codegen que ejecutes.
diff --git a/docs/book/src-es/22-layered-microservices.md b/docs/book/src-es/22-layered-microservices.md index 142753f4..fb674627 100644 --- a/docs/book/src-es/22-layered-microservices.md +++ b/docs/book/src-es/22-layered-microservices.md @@ -102,23 +102,23 @@ serde = { workspace = true } Las flechas de dependencia apuntan estrictamente **hacia adentro**:
- --interfacesDTOs · the public contract - -depends on --models@Entity · @Repository · @Bean - -depends on --core@Service · @Mapper · @Component - -depends on --web@RestController · the binary --sdktyped client - -→ -interfaces - +-interfacesDTOs · the public contract + +depends on +-models@Entity · @Repository · @Bean + +depends on +-core@Service · @Mapper · @Component + +depends on +-web@RestController · the binary +-sdktyped client + +→ -interfaces +rsfly - Rust framework by Firefly
Cinco crates compilados por separado. Las dependencias apuntan estrictamente hacia adentro: -web conoce a -core, que conoce a -models, que conoce a -interfaces — y el crate del contrato no conoce a nadie. -sdk depende solo de -interfaces, de modo que un llamador enlaza los DTOs sin el código de persistencia ni el web.
diff --git a/docs/book/src/00-front/00-preface.md b/docs/book/src/00-front/00-preface.md index 77b3927d..e6b6df42 100644 --- a/docs/book/src/00-front/00-preface.md +++ b/docs/book/src/00-front/00-preface.md @@ -1,5 +1,7 @@ ## Preface +**rsfly** is the product name for Firefly for Rust. This edition keeps the existing `firefly` crate names, imports and APIs used throughout the examples. + Rust gives you fearless concurrency, zero-cost abstractions, and a compiler that refuses to ship a data race. What it does not give you is *cohesion*. Every new back-office service forces the same cascade of decisions before a single line of business logic is written: which HTTP layer, which database story, how to wire dependencies, how to handle configuration, errors, correlation IDs, metrics, and graceful shutdown. **Firefly** changes that. It is an opinionated, convention-over-configuration framework that makes those cross-cutting decisions once, so every service shares one idiom — built from the ground up for Rust 1.88+ on `tokio` and `axum`. This book teaches Firefly **by example**. You build one real application from an empty crate to a secured, observable, event-sourced service — making every concept concrete before moving to the next. The code in these pages is not illustrative pseudocode: every listing is a slice of a **real project that compiles, boots, and passes its tests** against the framework at version 26.6.x. Each snippet was lifted from the running sample and checked against the crate APIs, so what you read is what actually works. When a listing drifts from the source, the sample's build breaks and a test fails — that is the guarantee behind every listing in this book. diff --git a/docs/book/src/00-front/00-title.md b/docs/book/src/00-front/00-title.md index 1320dcef..916f6fa6 100644 --- a/docs/book/src/00-front/00-title.md +++ b/docs/book/src/00-front/00-title.md @@ -1,5 +1,7 @@ -# Firefly for Rust by Example {.chtitle} +rsfly - Rust framework by Firefly -### Event-Driven Rust Microservices with the Firefly Framework +# rsfly by example {.chtitle} + +### Composing typed services with Tokio, Axum and SQLx **Firefly Software Foundation** diff --git a/docs/book/src/01-why-firefly.md b/docs/book/src/01-why-firefly.md index 94782485..bfdbc170 100644 --- a/docs/book/src/01-why-firefly.md +++ b/docs/book/src/01-why-firefly.md @@ -262,59 +262,59 @@ rarely name these crates directly — the facade re-exports them — but knowing shape tells you where each capability lives, and *which* book chapter unlocks it.
- -firefly + firefly-macrosone dependency · use firefly::prelude::*; - - -Tier 1 -Foundational -kernel -reactive -web -config -container -i18n - - - - -Tier 2 -Platform -cqrs -eda -event-sourcing -orchestration -cache -security - - - - -Tier 3 -Adapters -data-sqlx -data-mongodb -eda-kafka -cache-redis -idp-* -notif-* - - - - -Tier 4 -Starters -starter-core -starter-web -starter-domain -starter-data -admin -cli - -firefly-reactivethe Mono / Flux core every tier rests on (tokio · axum) - +firefly + firefly-macrosone dependency · use firefly::prelude::*; + + +Tier 1 +Foundational +kernel +reactive +web +config +container +i18n + + + + +Tier 2 +Platform +cqrs +eda +event-sourcing +orchestration +cache +security + + + + +Tier 3 +Adapters +data-sqlx +data-mongodb +eda-kafka +cache-redis +idp-* +notif-* + + + + +Tier 4 +Starters +starter-core +starter-web +starter-domain +starter-data +admin +cli + +firefly-reactivethe Mono / Flux core every tier rests on (tokio · axum) +rsfly - Rust framework by Firefly
The four tiers. A service depends only on the firefly facade (the front door). The tiers build left to right — Foundational vocabulary, Platform engines that define ports, Adapters that implement them, Starters that compose and ship — each depending only on the tiers to its left, all resting on the firefly-reactive core.
diff --git a/docs/book/src/02-quickstart.md b/docs/book/src/02-quickstart.md index f2fbdabf..771f5ebf 100644 --- a/docs/book/src/02-quickstart.md +++ b/docs/book/src/02-quickstart.md @@ -314,15 +314,15 @@ A few properties recur in every chapter, so notice them now: network.
- -Public API :8080client-facing -Management :8081operator-facing -#[rest_controller]your routesSecurityJWT · roles · sessionsRFC 9457 404problem+json fallback -/actuator/*health · info · metrics/adminself-hosted dashboard/swagger-ui · /redoc/v3/api-docs -FIREFLY_SERVER_ADDR · FIREFLY_MANAGEMENT_ADDR override the binds - +Public API :8080client-facing +Management :8081operator-facing +#[rest_controller]your routesSecurityJWT · roles · sessionsRFC 9457 404problem+json fallback +/actuator/*health · info · metrics/adminself-hosted dashboard/swagger-ui · /redoc/v3/api-docs +FIREFLY_SERVER_ADDR · FIREFLY_MANAGEMENT_ADDR override the binds +rsfly - Rust framework by Firefly
Two listeners, one process. The public API (:8080) serves your controllers, security and the RFC 9457 404 fallback; the management surface (:8081) serves the actuator, the self-hosted /admin dashboard and the OpenAPI docs — so operational endpoints never leak onto the public network.
- **`FIREFLY_SERVER_ADDR` / `FIREFLY_MANAGEMENT_ADDR`** override the bind diff --git a/docs/book/src/03-configuration.md b/docs/book/src/03-configuration.md index 8db19a41..c9a4720c 100644 --- a/docs/book/src/03-configuration.md +++ b/docs/book/src/03-configuration.md @@ -269,23 +269,23 @@ below sit later in the list and therefore override lower ones. | 5 | CLI flags — `FlagSource::new().set("web.addr", "0.0.0.0:80")` | everything |
- -defaultsStaticSource - -base YAMLapplication.yaml - -profile YAMLapplication-prod.yaml - -environmentFIREFLY_* - -CLI flagsFlagSource -merged left → right · last write wins -beats nothing -beats everything -an env override beats a YAML file; a CLI flag beats both - +defaultsStaticSource + +base YAMLapplication.yaml + +profile YAMLapplication-prod.yaml + +environmentFIREFLY_* + +CLI flagsFlagSource +merged left → right · last write wins +beats nothing +beats everything +an env override beats a YAML file; a CLI flag beats both +rsfly - Rust framework by Firefly
Layered::new(...) merges its sources left to right and the last write wins. Defaults sit earliest and beat nothing; a base YAML beats defaults; a profile overlay beats the base; environment beats YAML files; and a CLI flag beats everything — one artifact, deployable everywhere.
diff --git a/docs/book/src/04a-dependency-injection.md b/docs/book/src/04a-dependency-injection.md index ade81470..2feb969f 100644 --- a/docs/book/src/04a-dependency-injection.md +++ b/docs/book/src/04a-dependency-injection.md @@ -110,21 +110,21 @@ first request when something is missing. > graph in mind; every step below wires one edge of it.
- - -Container · scan() wires beans in dependency order -WalletApi#[derive(Controller)] -Ledger#[derive(Service)] -ReadModel#[derive(Component)] -EventStore#[derive(Repository)] -Brokerport — #[autowired] -autowired -autowired - - - + +Container · scan() wires beans in dependency order +WalletApi#[derive(Controller)] +Ledger#[derive(Service)] +ReadModel#[derive(Component)] +EventStore#[derive(Repository)] +Brokerport — #[autowired] +autowired +autowired + + +rsfly - Rust framework by Firefly
The container scans the stereotype beans and wires them in dependency order. WalletApi autowires the Ledger and ReadModel; the Ledger autowires the EventStore and Broker ports — no composition root by hand.
diff --git a/docs/book/src/05-reactive-model.md b/docs/book/src/05-reactive-model.md index f4b58131..f20d39a9 100644 --- a/docs/book/src/05-reactive-model.md +++ b/docs/book/src/05-reactive-model.md @@ -151,30 +151,30 @@ the rest are there when an edge case needs them. `Mono` constructors:
- -Mono<T> -0 or 1 item, then complete - - - - -just(v) -complete - -Flux<T> -0..N items, then complete - - - - - - - - -map · filter · flat_map - +Mono<T> +0 or 1 item, then complete + + + + +just(v) +complete + +Flux<T> +0..N items, then complete + + + + + + + + +map · filter · flat_map +rsfly - Rust framework by Firefly
The two reactive return types. A Mono<T> emits at most one item (Ok(Some)), or none (Ok(None)), then completes; a Flux<T> emits a stream of zero-or-more items. Both short-circuit on a terminal Err(FireflyError).
diff --git a/docs/book/src/06-first-http-api.md b/docs/book/src/06-first-http-api.md index 1c77816f..3eed39e7 100644 --- a/docs/book/src/06-first-http-api.md +++ b/docs/book/src/06-first-http-api.md @@ -319,17 +319,17 @@ GET /api/v1/wallets/wlt_1 Accept: application/xml ```
- -inbound HTTP request - -ProblemLayererrors → problem+jsonTraceContextLayerW3C traceparent in / outCorrelationLayerensure-or-generate idContentNegotiationLayerAccept → JSON / XML - -#[rest_controller]your handler runs -outermost -innermost - +inbound HTTP request + +ProblemLayererrors → problem+jsonTraceContextLayerW3C traceparent in / outCorrelationLayerensure-or-generate idContentNegotiationLayerAccept → JSON / XML + +#[rest_controller]your handler runs +outermost +innermost +rsfly - Rust framework by Firefly
The default layer stack, outermost first (some optional layers — CORS, security headers, metrics — are elided). ProblemLayer wraps everything so any error unwinds to an RFC 9457 application/problem+json response; trace-context and correlation open before your handler runs; content negotiation sits closest to the routes.
diff --git a/docs/book/src/06a-openapi.md b/docs/book/src/06a-openapi.md index d113798e..27d68e71 100644 --- a/docs/book/src/06a-openapi.md +++ b/docs/book/src/06a-openapi.md @@ -83,27 +83,27 @@ port, default to: | `/redoc` | ReDoc, pointed at the spec |
- -#[rest_controller]routes + status codes -#[derive(Schema)]DTO component schemas - - - - - - - - - -openapi.json -(/v3/api-docs) - - -Swagger UI -ReDoc - +#[rest_controller]routes + status codes +#[derive(Schema)]DTO component schemas + + + + + + + + + +openapi.json +(/v3/api-docs) + + +Swagger UI +ReDoc +rsfly - Rust framework by Firefly
No codegen step, no annotation framework. At boot FireflyApplication harvests the routing attributes and every #[derive(Schema)] type into one OpenAPI 3.1 document (served at /v3/api-docs on the management port) and points Swagger UI and ReDoc at it.
diff --git a/docs/book/src/09-cqrs.md b/docs/book/src/09-cqrs.md index 681688c5..c5a3b0a3 100644 --- a/docs/book/src/09-cqrs.md +++ b/docs/book/src/09-cqrs.md @@ -439,23 +439,23 @@ automatically (a fourth, authorization, arrives at the HTTP edge with | `QueryCache::middleware()` | memoises results for messages whose `cache_ttl` is `Some` — installed when a `QueryCache` bean exists |
- -send / query a message - -msg ↦ TypeIdmatched to a handler -middleware chain - -V - -C - -Q -V = Validation · C = Correlation · Q = QueryCache - -your handlerCommand or Query - +send / query a message + +msg ↦ TypeIdmatched to a handler +middleware chain + +V + +C + +Q +V = Validation · C = Correlation · Q = QueryCache + +your handlerCommand or Query +rsfly - Rust framework by Firefly
A message is matched to its handler by TypeId, then runs the registered middleware chain — Validation outermost, then Correlation, then QueryCache — before the handler executes. The correlation scope opens before the cache layer, so everything it logs carries the id.
@@ -575,44 +575,44 @@ already open before the cache layer runs, so anything the cache logs carries the id too:
- - send / query a message - + send / query a message + - - + msg ↦ TypeId - + - middleware chain + middleware chain - - + V - - + C - - + Q - + - + V = ValidationMiddleware C = Correlation Q = QueryCache - + - - your handler - + + your handler +rsfly - Rust framework by Firefly
The framework registers ValidationMiddleware first (outermost), then CorrelationMiddleware, then QueryCache: the correlation scope opens before the cache layer runs, so everything it logs carries the id.
diff --git a/docs/book/src/11-event-sourcing.md b/docs/book/src/11-event-sourcing.md index e22909f7..f5493c23 100644 --- a/docs/book/src/11-event-sourcing.md +++ b/docs/book/src/11-event-sourcing.md @@ -119,21 +119,21 @@ concurrency; a later load **folds** the stream back into current state. Hold thi cycle in mind — every API in the chapter is one of these three moves.
- -write path -CommandDeposit { amount }raise(event)→ uncommitted []append(events)optimistic concurrency -event stream (append-only) -+100WalletOpened - -+50MoneyDeposited - -−30MoneyWithdrawn -append -fold / replay -current statebalance = 120 - +write path +CommandDeposit { amount }raise(event)→ uncommitted []append(events)optimistic concurrency +event stream (append-only) ++100WalletOpened + ++50MoneyDeposited + +−30MoneyWithdrawn +append +fold / replay +current statebalance = 120 +rsfly - Rust framework by Firefly
Three moves. A command raises an event onto the aggregate; EventStore::append persists the uncommitted events under optimistic concurrency; a later load folds the whole append-only stream back into the current state — the events are the source of truth, the state is derived.
diff --git a/docs/book/src/12-sagas.md b/docs/book/src/12-sagas.md index 98bb78b3..cedb30e7 100644 --- a/docs/book/src/12-sagas.md +++ b/docs/book/src/12-sagas.md @@ -113,17 +113,17 @@ one fails. "Re-credit the source" is a brand-new `deposit` that restores the balance, and it leaves an auditable refund event behind.
- -forward: dependency-ordered steps -debitwithdraw(amount)creditdeposit(amount)notifypublish event -may fail - - -compensate — reverse order -a compensation is a forward undo, not a database rollback - +forward: dependency-ordered steps +debitwithdraw(amount)creditdeposit(amount)notifypublish event +may fail + + +compensate — reverse order +a compensation is a forward undo, not a database rollback +rsfly - Rust framework by Firefly
A saga runs its steps in dependency order. If a step fails, the engine runs the already-completed steps' compensations in reverse order — here a failed credit refunds the debit. A compensation is a forward action that undoes, not a database rollback.
@@ -558,16 +558,16 @@ macro generates `Workflow::workflow(self: Arc)` and `run(self, input) -> Result<(), WorkflowError>`.
- -parallel layer -balance-checkfunds_ok: bool -limit-checkwithin_limit: bool -approvedepends_on both - - - +parallel layer +balance-checkfunds_ok: bool +limit-checkwithin_limit: bool +approvedepends_on both + + +rsfly - Rust framework by Firefly
A workflow is a DAG of steps. balance-check and limit-check have no dependency on each other, so they run in the same parallel layer; approve waits for both and consumes their verdicts.
@@ -796,27 +796,27 @@ name, so confirm and cancel can read it via `#[from_step("")]`. and `run(self, input) -> Result<(), TccError>`.
- -Try -reserve -Confirm -on all-tried -Cancel -on a try failure -source -withdraw (hold) -(none — held) -deposit (release) -dest -verify exists -deposit (capture) -(none — nothing held) - -all tried → confirm -any try fails → cancel tried in reverse - +Try +reserve +Confirm +on all-tried +Cancel +on a try failure +source +withdraw (hold) +(none — held) +deposit (release) +dest +verify exists +deposit (capture) +(none — nothing held) + +all tried → confirm +any try fails → cancel tried in reverse +rsfly - Rust framework by Firefly
Try / Confirm / Cancel. Every participant's Try reserves; once all have tried, Confirm captures; if any Try fails, the engine Cancels the already-tried participants in reverse order. The source holds funds on Try and releases them on Cancel; the destination captures on Confirm.
diff --git a/docs/book/src/21-declarative-macros.md b/docs/book/src/21-declarative-macros.md index ec4593b5..159ecf6b 100644 --- a/docs/book/src/21-declarative-macros.md +++ b/docs/book/src/21-declarative-macros.md @@ -175,27 +175,27 @@ generates. | `#[firefly::tcc]` + `#[participant]` | `tcc_transfer.rs` | a TCC `run` driving each participant's try / confirm / cancel |
- -you write -the macro generates -#[derive(Command)] - -the Message impl (kind, validate, cache_ttl) -#[derive(Schema)] - -an OpenAPI schema (appears in /v3/api-docs) -#[derive(DomainEvent)] - -EVENT_TYPE + to_domain_event -#[rest_controller] - -a Controller bean + WalletApi::routes(state) -#[derive(Service)] - -a scanned bean with #[autowired] fields - +you write +the macro generates +#[derive(Command)] + +the Message impl (kind, validate, cache_ttl) +#[derive(Schema)] + +an OpenAPI schema (appears in /v3/api-docs) +#[derive(DomainEvent)] + +EVENT_TYPE + to_domain_event +#[rest_controller] + +a Controller bean + WalletApi::routes(state) +#[derive(Service)] + +a scanned bean with #[autowired] fields +rsfly - Rust framework by Firefly
Declarative, compile-time. Each attribute or derive expands to the wiring you would otherwise hand-write: a Message impl, an OpenAPI schema, an event discriminator, a controller routes() builder, or a scanned bean with autowired fields — generated by firefly-macros, not by a codegen step you run.
diff --git a/docs/book/src/22-layered-microservices.md b/docs/book/src/22-layered-microservices.md index 311fca01..0d184175 100644 --- a/docs/book/src/22-layered-microservices.md +++ b/docs/book/src/22-layered-microservices.md @@ -95,23 +95,23 @@ serde = { workspace = true } The dependency arrows run strictly **inward**:
- --interfacesDTOs · the public contract - -depends on --models@Entity · @Repository · @Bean - -depends on --core@Service · @Mapper · @Component - -depends on --web@RestController · the binary --sdktyped client - -→ -interfaces - +-interfacesDTOs · the public contract + +depends on +-models@Entity · @Repository · @Bean + +depends on +-core@Service · @Mapper · @Component + +depends on +-web@RestController · the binary +-sdktyped client + +→ -interfaces +rsfly - Rust framework by Firefly
Five separately-compiled crates. Dependencies run strictly inward: -web knows -core, which knows -models, which knows -interfaces — and the contract crate knows nobody. -sdk depends only on -interfaces, so a caller links the DTOs without the persistence or web code.
diff --git a/docs/book/tests/test_covers.py b/docs/book/tests/test_covers.py new file mode 100644 index 00000000..a9e1bcfa --- /dev/null +++ b/docs/book/tests/test_covers.py @@ -0,0 +1,206 @@ +"""Integration tests for localized covers in the real book renderer.""" +from __future__ import annotations + +from contextlib import ExitStack, redirect_stdout +import importlib.util +import io +import os +from pathlib import Path +import subprocess +import tempfile +import unittest +from unittest.mock import patch +import xml.etree.ElementTree as ET +import zipfile + +from pypdf import PdfReader +from PIL import Image +import yaml + +BOOK = Path(__file__).resolve().parents[1] +SPEC = importlib.util.spec_from_file_location("book_build", BOOK / "build" / "build.py") +builder = importlib.util.module_from_spec(SPEC) +SPEC.loader.exec_module(builder) + +OPF = {"opf": "http://www.idpf.org/2007/opf", "dc": "http://purl.org/dc/elements/1.1/"} +XHTML = {"x": "http://www.w3.org/1999/xhtml"} + + +class CoverIntegrationTests(unittest.TestCase): + def setUp(self): + self.directory = tempfile.TemporaryDirectory() + self.addCleanup(self.directory.cleanup) + self.root = Path(self.directory.name) + (self.root / "src").mkdir() + (self.root / "art").mkdir() + (self.root / "src" / "chapter.md").write_text("# Chapter\n\nBusiness content stays here.\n") + for language in ("en", "es"): + for side, color in (("front", "#ed312f"), ("back", "#125cc4")): + (self.root / "art" / f"{side}-{language}.svg").write_text( + '' + f'' + f'{side.upper()}_{language.upper()}' + '' + ) + + def manifest(self, language="en"): + return { + "title": "A test book", "author": "Test Publisher", "language": language, + "identifier": "urn:uuid:9161672f-86cd-455d-a9a8-13b6c0fdfd89", + "cover_svg": f"art/front-{language}.svg", + "back_cover_svg": f"art/back-{language}.svg", + "cover_alt": "Portada de prueba" if language == "es" else 'Test "front" cover & architecture', + "back_cover_alt": "Contraportada de prueba" if language == "es" else "Test back cover", + "labels": {"cover": "Portada", "back_cover": "Contraportada"} if language == "es" else {}, + "front": [], + "parts": [{"title": "Part I — Test", "chapters": [ + {"id": "chapter", "file": "chapter.md", "num": 1, "title": "Chapter"} + ]}], + } + + def build(self, cfg, format="--epub", output=None): + (self.root / "book.yaml").write_text(yaml.safe_dump(cfg)) + with ExitStack() as stack: + stack.enter_context(patch.object(builder, "BOOK", self.root)) + stack.enter_context(patch.object(builder, "DIST", self.root / "dist")) + stack.enter_context(patch.object(builder.sys, "argv", ["build.py", format])) + stack.enter_context(patch.dict(os.environ, {"BOOK_CONFIG": "book.yaml"}, clear=False)) + if output is not None: + stack.enter_context(patch.dict(os.environ, {"BOOK_OUTPUT_DIR": str(output)})) + else: + stack.enter_context(patch.dict(os.environ, {"BOOK_OUTPUT_DIR": str(self.root / "dist")})) + stack.enter_context(redirect_stdout(io.StringIO())) + self.assertEqual(builder.main(), 0) + return output or self.root / "dist" + + def test_epub_localizes_both_cover_documents_and_ends_spine_with_back_cover(self): + for language in ("en", "es"): + with self.subTest(language=language): + cfg = self.manifest(language) + out = self.build(cfg) + with zipfile.ZipFile(out / "firefly-rust-by-example.epub") as archive: + opf = ET.fromstring(archive.read("OEBPS/content.opf")) + spine = [item.attrib["idref"] for item in opf.findall("opf:spine/opf:itemref", OPF)] + self.assertEqual(spine[0], "cover") + self.assertEqual(spine[-1], "back-cover") + self.assertEqual(opf.find("opf:metadata/dc:language", OPF).text, language) + covers = [item for item in opf.findall("opf:manifest/opf:item", OPF) + if item.attrib.get("properties") == "cover-image"] + self.assertEqual(len(covers), 1) + self.assertEqual(archive.read("OEBPS/" + covers[0].attrib["href"]), + (self.root / cfg["cover_svg"]).read_bytes()) + for doc, alt in (("cover", cfg["cover_alt"]), ("back-cover", cfg["back_cover_alt"])): + page = ET.fromstring(archive.read(f"OEBPS/{doc}.xhtml")) + self.assertEqual(page.attrib["lang"], language) + img = page.find(".//x:img", XHTML) + self.assertEqual(img.attrib["alt"], alt) + self.assertIn("OEBPS/" + img.attrib["src"], archive.namelist()) + section = page.find(".//x:section", XHTML) + self.assertEqual(section.attrib["{http://www.idpf.org/2007/ops}type"], + "cover" if doc == "cover" else "backmatter") + nav = archive.read("OEBPS/nav.xhtml").decode() + self.assertNotIn('href="back-cover.xhtml"', nav) + + def test_epub_uses_configured_png_cover_images(self): + cfg = self.manifest() + for prefix, color in (("cover", "red"), ("back_cover", "blue")): + path = self.root / "art" / f"{prefix}.png" + Image.new("RGB", (30, 37), color).save(path) + cfg[f"{prefix}_image"] = str(path) + out = self.build(cfg) + with zipfile.ZipFile(out / "firefly-rust-by-example.epub") as archive: + opf = ET.fromstring(archive.read("OEBPS/content.opf")) + for cid, prefix in (("cover", "cover"), ("back-cover", "back_cover")): + item = opf.find(f"opf:manifest/opf:item[@id='{cid}-img']", OPF) + self.assertEqual(item.attrib["media-type"], "image/png") + self.assertEqual(archive.read("OEBPS/" + item.attrib["href"]), + Path(cfg[f"{prefix}_image"]).read_bytes()) + + def test_pdf_has_full_trim_front_and_back_pages_without_running_furniture(self): + out = self.build(self.manifest("es"), format="--pdf") + pdf = PdfReader(out / "firefly-rust-by-example.pdf") + self.assertEqual(pdf.pages[0].extract_text().strip(), "FRONT_ES") + self.assertEqual(pdf.pages[-1].extract_text().strip(), "BACK_ES") + for page in (pdf.pages[0], pdf.pages[-1]): + self.assertEqual(float(page.mediabox.width), 540) + self.assertEqual(float(page.mediabox.height), 666) + self.assertIn("Business content stays here.", "\n".join(p.extract_text() for p in pdf.pages)) + + def test_output_directory_preserves_existing_distribution(self): + legacy = self.root / "dist" + legacy.mkdir() + artifact = legacy / "firefly-rust-by-example.epub" + artifact.write_bytes(b"historical release artifact") + out = self.build(self.manifest(), output=self.root / "new-edition") + self.assertTrue((out / artifact.name).is_file()) + self.assertEqual(artifact.read_bytes(), b"historical release artifact") + + def test_title_page_keeps_collection_name_lowercase_without_running_title(self): + cfg = self.manifest() + cfg["title"] = "rsfly by example" + cfg["front"] = [ + {"id": "title", "file": "title.md", "nav": False}, + {"id": "copyright", "file": "copyright.md", "nav": False}, + ] + (self.root / "src" / "title.md").write_text("# rsfly by example {.chtitle}\n") + (self.root / "src" / "copyright.md").write_text("Copyright 2026.\n") + out = self.build(cfg, format="--pdf") + pdf = PdfReader(out / "firefly-rust-by-example.pdf") + pages = [page.extract_text() for page in pdf.pages] + self.assertIn("rsfly by example", "\n".join(pages)) + self.assertNotIn("RSFLY BY EXAMPLE", "\n".join(pages)) + + def test_interior_generators_use_the_official_family_lockup(self): + import gen_openers + import gen_diagrams + self.assertIn("rsfly", gen_openers.emblem()) + self.assertNotIn("' + + lockup(0, 0, 335.52, 242, theme="dark") + '', + BOOK, [], out) + preview = self.root / "lockup-preview" + subprocess.run(["pdftoppm", "-scale-to-x", "1000", "-scale-to-y", "721", + "-png", "-singlefile", str(out), str(preview)], + check=True, capture_output=True) + with Image.open(preview.with_suffix(".png")) as actual, Image.open(BOOK / "art/rsfly-dark.png") as logo: + expected = Image.new("RGBA", logo.size, "#10110f") + expected.alpha_composite(logo.convert("RGBA")) + observed = actual.convert("RGB").getpixel((726, 188)) + reference = expected.convert("RGB").getpixel((726, 188)) + self.assertLess(max(abs(a - b) for a, b in zip(observed, reference)), 12) + + def test_missing_configured_back_cover_stops_build(self): + cfg = self.manifest() + cfg["back_cover_svg"] = "art/missing.svg" + with self.assertRaises(FileNotFoundError): + self.build(cfg) + + +if __name__ == "__main__": + unittest.main() diff --git a/docs/book/theme/book.css b/docs/book/theme/book.css index 5c0cc276..ceb7a79b 100644 --- a/docs/book/theme/book.css +++ b/docs/book/theme/book.css @@ -1,10 +1,12 @@ /* book.css — shared structural + typographic styling for both the PDF and the - EPUB editions of "Firefly for Rust by Example". + EPUB editions of "rsfly by example". tokens.css + pygments.css are loaded alongside this file by the build (the EPUB links all three stylesheets; the PDF passes them to WeasyPrint), so no @import is needed here — and @import breaks strict EPUB validation. */ /* ============ base ============ */ +.cover-page{ margin:0; padding:0; line-height:0; } +.cover-page > img{ display:block; width:100%; height:auto; max-width:100%; } html{ -webkit-text-size-adjust:100%; } body{ color:var(--ink); background:var(--page); @@ -54,13 +56,13 @@ blockquote{ margin:1.1rem 0; padding:.2rem 0 .2rem 1rem; border-left:3px solid v color:var(--ink-soft); font-style:italic; } /* inline code */ -code{ background:#f3ecdf; color:var(--rust-deep); padding:.05em .34em; border-radius:4px; +code{ background:#f3f1eb; color:var(--rust-deep); padding:.05em .34em; border-radius:4px; font-family:"SF Mono","JetBrains Mono",Menlo,Consolas,monospace; font-size:.84em; word-break:break-word; } /* ============ code listings ============ */ .listing{ margin:1.15rem 0; } -.filetab{ display:inline-block; background:#f3ead9; color:var(--rust-deep); +.filetab{ display:inline-block; background:#f3f1eb; color:var(--rust-deep); font:700 .7rem/1 "SF Mono","JetBrains Mono",Menlo,monospace; padding:.42rem .7rem; border:1px solid var(--code-border); border-bottom:none; border-radius:8px 8px 0 0; letter-spacing:.02em; } @@ -85,14 +87,14 @@ figcaption b,figcaption strong{ color:var(--rust-deep); } /* ============ tables ============ */ table{ width:100%; border-collapse:collapse; margin:1.1rem 0; font-size:.88rem; line-height:1.45; } th,td{ border:1px solid var(--code-border); padding:.48rem .7rem; text-align:left; vertical-align:top; } -thead th{ background:#fbf0da; color:var(--rust-deep); font-weight:700; +thead th{ background:#f3f1eb; color:var(--rust-deep); font-weight:700; font-family:"Avenir Next",Avenir,Helvetica,Arial,sans-serif; } -tbody tr:nth-child(even){ background:#fdfaf2; } -td code,th code{ background:#f3ecdf; } +tbody tr:nth-child(even){ background:#f9f8f4; } +td code,th code{ background:#f3f1eb; } /* ============ callouts ============ */ .admonition{ border-radius:10px; padding:.78rem 1rem; margin:1.15rem 0; font-size:.9rem; - line-height:1.55; border:1px solid var(--rule); border-left-width:4px; background:#faf6ee; } + line-height:1.55; border:1px solid var(--rule); border-left-width:4px; background:#f3f1eb; } .admonition>:last-child{ margin-bottom:0; } .admonition>:first-child{ margin-top:0; } .admonition-title{ font-weight:800; font-size:.69rem; letter-spacing:.08em; text-transform:uppercase; @@ -101,15 +103,15 @@ td code,th code{ background:#f3ecdf; } .adm-ico{ flex:none; width:15px; height:15px; } /* Status icons in capability/coverage matrices (see md.py _STATUS_ICON). */ .status-ico{ width:16px; height:16px; vertical-align:-0.18em; } -.admonition.note{ background:var(--note-bg); border-color:#cfe0fb; border-left-color:var(--note); } +.admonition.note{ background:var(--note-bg); border-color:#b4bdb8; border-left-color:var(--note); } .admonition.note>.admonition-title{ color:var(--note); } -.admonition.tip{ background:var(--tip-bg); border-color:#cdeccd; border-left-color:var(--tip); } +.admonition.tip{ background:var(--tip-bg); border-color:#b4bdb8; border-left-color:var(--tip); } .admonition.tip>.admonition-title{ color:var(--tip); } -.admonition.warning{ background:var(--warn-bg); border-color:#fcd9c2; border-left-color:var(--warn); } +.admonition.warning{ background:var(--warn-bg); border-color:#ffb34a; border-left-color:var(--warn); } .admonition.warning>.admonition-title{ color:var(--warn); } -.admonition.spring{ background:var(--spring-bg); border-color:#d4e9bd; border-left-color:var(--spring); } +.admonition.spring{ background:var(--spring-bg); border-color:#b4bdb8; border-left-color:var(--spring); } .admonition.spring>.admonition-title{ color:var(--spring-deep); } -.admonition.reactor{ background:var(--reactor-bg); border-color:#ddd0fb; border-left-color:var(--reactor); } +.admonition.reactor{ background:var(--reactor-bg); border-color:#b4bdb8; border-left-color:var(--reactor); } .admonition.reactor>.admonition-title{ color:var(--reactor-deep); } /* ============ recap / exercises (applied to the section H2) ============ */ @@ -160,8 +162,9 @@ nav#toc li.toc-part > ol a{ font-weight:400; color:var(--ink); } } .part-title::after{ content:""; display:block; width:4rem; height:5px; border-radius:3px; margin:1.1rem auto 0; background:linear-gradient(90deg,var(--amber),var(--rust)); } -.part-divider svg.part-glyph{ width:120px; height:auto; margin:0 auto 1.6rem; display:block; } +.part-divider svg.part-glyph{ width:180px; height:auto; margin:0 auto 1.6rem; display:block; } /* ============ Front-matter title page ============ */ .frontmatter h1.chtitle{ font-size:2.6rem; } .frontmatter h3{ color:var(--rust-deep); font-weight:600; } +.frontmatter.title-page h1.chtitle::after{ margin-right:auto; margin-left:auto; } diff --git a/docs/book/theme/firefly.css b/docs/book/theme/firefly.css index 256b829e..7a4780c3 100644 --- a/docs/book/theme/firefly.css +++ b/docs/book/theme/firefly.css @@ -3,15 +3,15 @@ only adds a warm Firefly accent, tighter typography, and styled callouts. */ :root { - --firefly-amber: #f5a623; - --firefly-amber-soft: rgba(245, 166, 35, 0.12); + --firefly-amber: #ffb34a; + --firefly-amber-soft: rgba(255, 179, 74, 0.12); --firefly-red: #d64545; } /* Warm the link + sidebar-active accent toward the Firefly amber. */ .content a:link, .content a:visited { - color: #e0922f; + color: #855414; text-decoration: none; } .content a:hover { @@ -53,7 +53,7 @@ blockquote { border-radius: 0 6px 6px 0; } blockquote p:first-child strong:first-child { - color: #e0922f; + color: #855414; } /* The mapping tables (Reactor / Spring → firefly) deserve to feel canonical. */ diff --git a/docs/book/theme/print.css b/docs/book/theme/print.css index eb7495cc..aeb7e854 100644 --- a/docs/book/theme/print.css +++ b/docs/book/theme/print.css @@ -5,31 +5,34 @@ margin: 0.82in 0.78in 0.86in; @bottom-center{ content: counter(page); - font:9pt "Avenir Next","Helvetica Neue",sans-serif; color:#b3a690; + font:9pt "Avenir Next","Helvetica Neue",sans-serif; color:#62645b; } @top-center{ content: string(runhead); font:8pt "Avenir Next","Helvetica Neue",sans-serif; - letter-spacing:.12em; text-transform:uppercase; color:#bcaf99; + letter-spacing:.12em; text-transform:uppercase; color:#62645b; } } /* opening page of each chapter/section carries no header (cleaner opener) */ @page :first{ @top-center{ content:none; } @bottom-center{ content:none; } } +@page title{ @top-center{ content:none; } @bottom-center{ content:none; } } html{ font-size: 11pt; } /* map rem-based sizes to print */ body{ background:#fff; } /* ============ cover ============ */ -/* The cover is a full-bleed SVG on its own first page (no header/number). */ +/* Front and back covers occupy full trim pages without running furniture. */ @page cover{ margin:0; @top-center{ content:none; } @bottom-center{ content:none; } } .cover-page{ page:cover; break-after:page; margin:0; padding:0; line-height:0; } .cover-page svg, .cover-page img{ display:block; width:7.5in; height:9.25in; } +.back-cover{ break-before:page; break-after:auto; } /* ============ front matter ============ */ .frontmatter{ break-before:page; } .cover-page + .frontmatter{ break-before:auto; } /* the very first title page is vertically centred, no running head */ .frontmatter.title-page{ + page:title; display:flex; flex-direction:column; justify-content:center; min-height:7.3in; text-align:center; string-set:runhead ""; } @@ -49,6 +52,7 @@ body{ background:#fff; } .chapter{ break-before:page; padding-bottom:0; } /* running head from the chapter title */ h1.chtitle{ string-set: runhead content(text); } +.frontmatter.title-page h1.chtitle{ string-set:runhead ""; } /* keep blocks intact across page boundaries */ table,figure.fig,.admonition,.listing{ break-inside:avoid; } diff --git a/docs/book/theme/tokens.css b/docs/book/theme/tokens.css index 648b03b5..53658824 100644 --- a/docs/book/theme/tokens.css +++ b/docs/book/theme/tokens.css @@ -1,23 +1,23 @@ -/* Design tokens for "Firefly for Rust by Example". - Firefly brand: warm amber/gold on dark; Rust-orange accents for the - language-specific touches. Loaded first; book.css + print.css build on it. */ +/* Design tokens for "rsfly by example". + Firefly brand palette, shared with the approved rsfly artwork. + Loaded first; book.css + print.css build on it. */ :root{ /* amber / gold — the firefly glow */ - --amber:#f6a821; --amber-bright:#ffbf3f; --amber-soft:#ffd87a; --amber-deep:#c97e10; - /* rust-orange — the language accent */ - --rust:#d4793a; --rust-deep:#b5531f; --rust-soft:#e8a06a; + --amber:#ffb34a; --amber-bright:#ffb34a; --amber-soft:#fff1dd; --amber-deep:#855414; + /* Preserve existing token names for the shared brand accent. */ + --rust:#ffb34a; --rust-deep:#855414; --rust-soft:#fff1dd; /* ink / paper */ - --ink:#1b1714; --ink-soft:#3a322c; --muted:#8a7f74; --page:#fffdf8; - --ink-deep:#120f0c; + --ink:#272820; --ink-soft:#474a42; --muted:#62645b; --page:#ffffff; + --ink-deep:#10110f; /* headings sit in a warm near-black */ - --head:#2a211a; --head-soft:#43352a; + --head:#10110f; --head-soft:#474a42; /* code surfaces */ - --code-bg:#fbf7ef; --code-border:#ece1cf; --code-ink:#2b2620; - --rule:#ece3d6; + --code-bg:#f3f1eb; --code-border:#d8d4ca; --code-ink:#272820; + --rule:#d8d4ca; /* callouts */ - --note:#2563c9; --note-bg:#eef4ff; - --tip:#1f8a4c; --tip-bg:#ecf9f0; - --warn:#c2410c; --warn-bg:#fff3ec; - --spring:#43b02a; --spring-bg:#f2faea; --spring-deep:#2c8a1c; - --reactor:#7c3aed; --reactor-bg:#f5f0ff; --reactor-deep:#5b21b6; + --note:#456578; --note-bg:#edf1f3; + --tip:#52613d; --tip-bg:#f0f2eb; + --warn:#855414; --warn-bg:#fff1dd; + --spring:#7a8460; --spring-bg:#f0f2eb; --spring-deep:#52613d; + --reactor:#6b8795; --reactor-bg:#edf1f3; --reactor-deep:#456578; }