Releases: fireflyframework/fireflyframework-rust
v26.6.28
A Spring Boot parity increment: the declarative HTTP-interface client — the
highest single value lever from the parity-gap analysis (it lifts the REST/HTTP
clients area off the floor).
Added
#[http_client]— a declarative HTTP-interface client, the analog of
Spring 6's@HttpExchange(the modern OpenFeign replacement). Annotate a
trait of methods with the same verb attributes a#[rest_controller]
uses and the macro generates a<Trait>Implthat issues the requests over a
WebClient— the mirror image of a controller.- Verbs:
#[get("/path")]/#[post]/#[put]/#[delete]/
#[patch]+ generic#[request(method = "…")]. Path variables use the
framework's:idsyntax (same as the server macro);{id}is a compile
error pointing at:id. - Argument binding needs no attributes in the common case: a name-matched
:vararg is the path variable, the lone non-scalar arg on a body verb is
the JSON body, the rest are query params (Optionomits whenNone,
Vec/&[_]repeat). Override with#[path]/#[query("k")]/
#[header("X")]/#[body]. Every:varmust bind exactly once or it is a
compile error; anOption/Vec/slice path variable is rejected. - Return shapes:
async fn -> Result<T, ClientError>(the ergonomic
default),Result<T, E: From<ClientError>>, non-asyncMono<T>/Flux<T>
(returned directly; aFluxdefaultsAccept: application/x-ndjson), and
WebClientResponse(the.exchange()escape hatch). - Construction:
<Trait>Impl::new(base_url)or::with_client(WebClient);
the type isClone. With#[http_client(... bean)]it is registered as a
@Serviceand bound todyn Trait, so#[autowired] Arc<dyn Trait>
resolves (pulling a sharedWebClientbean, named viaclient = "…"). - Error fidelity (documented): an awaited
Result<T, ClientError>
surfaces every failure asClientError::Problem(carrying aFireflyError
with the original status/code, so the classifiers still work); the
structuredTransport/Decode/Encode/InvalidUrlvariants survive only
on theMono/Fluxreturn forms.
- Verbs:
firefly_client::encode_path_segment— RFC 3986 path-segment
percent-encoding (used by generated clients; also public).
The macro reuses the server #[rest_controller]'s verb-attribute grammar
(MappingAttr/VERBS/join_path), so client and server can't drift. Designed
via a scored 3-proposal panel and adversarially reviewed (the review caught a
runtime footgun — an Option path variable producing …/Some(x) URLs — now a
compile error). The firefly::prelude now also re-exports WebClient /
ClientError / new_web_client.
v26.6.27
A Spring Boot parity increment: declarative rollback rules on
#[transactional]. Chosen from a 16-area parity-gap analysis as the best
value-to-effort gap (the transaction runtime already supported it).
Added
-
#[transactional(no_rollback_for = "<pat>", rollback_only_for = "<pat>")]
— declarative transaction rollback rules. Spring names exception types;
because Rust'sResultalready separates failure from success, the Firefly
analog names an error pattern. By default everyErrrolls back; then:no_rollback_for = "P"— Spring's@Transactional(noRollbackFor = …):
anErrmatching patternPcommits instead of rolling back;rollback_only_for = "P": roll back only when theErrmatchesP,
committing the rest;- with both,
no_rollback_forwins on overlap.
The pattern is any Rust match pattern valid for the fn's error type (no
if
guard), alternatives included ("Error::A | Error::B"). The macro lowers to
the already-presenttransactional_with/transactional_with_onruntime
entry points (which take ashould_rollback(&E) -> boolpredicate), composes
withmanager = "…", and the generated predicate ismatches!-based, so a
pattern that does not fit the error type is a compile error.rollback_only_foris not namedrollback_for: Spring'srollbackForis
additive (it widens the set of exceptions that roll back), but Rust has no
checked/unchecked split — everyErralready rolls back — so the faithful
rule here is restrictive. Writingrollback_foris a friendly compile error
pointing at the two rules above, so a Spring port can't be silently inverted.
No runtime or API changes elsewhere.
v26.6.26
A correctness release. Every one of the 74 per-crate README.md files was
audited against that crate's actual shipped public API (43 confirmed fixes
across 28 crates), and the audit surfaced a real framework bug: the per-crate
VERSION constant was a hardcoded literal frozen at "26.6.24" instead of
tracking the crate version.
Fixed
-
VERSIONno longer drifts from the crate version. Every crate's
pub const VERSIONwas a hardcoded"26.6.24"string that nothing kept in
sync with the workspace version — sofirefly_kernel::VERSION, the actuator
/actuator/versionpayload, and the startup banner all reported a stale
release number, and theversion_matches_crate_versionguard tests only
passed while the workspace happened to sit at26.6.24. All 52 hardcoded
constants now derive fromenv!("CARGO_PKG_VERSION")(the re-exporting
crates already chained tofirefly_kernel::VERSION), soVERSIONis now
always exactly the crate version and can never drift again. Thecli
FRAMEWORK_VERSIONconstant got the same treatment, and the handful of
unit/integration tests that assertedVERSION == "26.6.24"against a frozen
literal now assert againstenv!("CARGO_PKG_VERSION"), so the guard holds for
every release instead of only when the workspace happened to sit at that
number. (The CLI'srender_for/ SBOM-parser fixtures keep their literal
sample versions — that string is arbitrary test data, not the build version.) -
Phantom / incomplete public-surface docs. Documented APIs now match the
source:admin'sAdminDepsgained itsenvironmentfield;openapi's
RouteDef(4 missing fields:request_schema/response_schema/
query_schema/pageable),Parameter::{query,header},Builder::{add_schema, add_schema_descriptors, from_inventory, docs_router}, and theDocsConfig
struct are now listed;orchestration'sCompensationPolicy(now all six
variants incl.GroupedParallel) andSagaError(all four variants);
starter-web'sWebStack::{set_security, set_exception_advice};testkit's
BuiltSlice::web_client;idp'sErrorenum +change_passwordsignature;
security,webhooks,kernel,transactional,pluginssurface fixes. -
Wrong signatures / variant names. The
notifications-*READMEs used the
wire spellingsEmailStatus::SENT/FAILED; the Rust variants areSent/
Failed(#[serde(rename = "SENT")]only affects the JSON).pluginsshowed
aVec<Arc<dyn Any>>annotation that does not type-check against the real
Extension = Arc<dyn Any + Send + Sync>.notifications-twilio,
session-redisparameter names corrected. -
Wrong facts.
admin: the bean graph does ship dependencyedges
(one per autowired dependency), not "nodes-only".backoffice: the middleware
order includesTraceContext(Problem → TraceContext → Correlation → Idempotency → BackOffice).resilience,starter-core,eda-kafka,
session-postgres,session-mongodb,container(awarm→formtypo) fixes. -
Stale version pins. Crate-README dependency examples that still pinned the
long-stale26.6.7now use the self-maintaining minor pinversion = "26.6"
(the conventionfirefly/testkit/webhooksalready used); example
VERSIONoutputs updated to the release version. -
firefly-cachedoc comment. Removed the stale "once the Redis adapter
ships in the next minor" note —firefly-cache-redis(RedisAdapter) has
shipped and is a published workspace member.
v26.6.25
A correctness-and-hygiene release: the workspace is rustfmt-clean again and
every book example was re-audited against the shipped API. No behavioural or
API changes.
Fixed
- Workspace formatting.
cargo fmt --all --check(and therefore
make ci) was silently failing onmain: prior changes to
firefly-openapi, an observability test, and thelumen-ledgersample
controller/main.rswere never formatted. Reformatted;make cinow passes
fmt-check→clippy -D warnings→build→testend to end. - Book example accuracy (full-chapter audit, every finding verified against
source). Corrected: aFireflyApplication::new(..).run()missing.await;
aWalletViewread model missing itsSchemaderive; the CQRS middleware
order (ValidationMiddlewareis installed first byCore, so with the bus's
reverse-iteration wrapping it is outermost — prose and the SVG diagram
now readValidation → Correlation → QueryCache, not Correlation-first);
threeWebClientsnippets that built aMono/Fluxwithout
.block().await; the experience-tier signal endpoint path
(POST /journeys/:id/data, not/confirm); a CLI--url :8081lacking a
scheme; a streaming handler/test missing aResponseimport and the
open_with_deposit(&app)argument; a stale79 members/ “four samples”
module-index count (now 86 / five reference samples); and twosamples/lumen
GitHub links that pointed at the org root instead of the file.
v26.6.24
Spec-based filtering is now first-class on derived repositories, with a
filtering endpoint in the sample.
Added
#[derive(SqlxRepository)]also implementsReactiveSpecificationRepository
(find_by_spec/find_by_spec_paged) by delegation — so any derived
repository runs composable, dialect-awareSpecificationqueries out of the
box (the Spring DataJpaSpecificationExecutoranalog), not just CRUD +
derived queries.- lumen-ledger
GET /api/v1/wallets/search— aWalletFilterquery DTO
(owner / currency / status / minBalance / maxBalance, each an OpenAPI query
parameter) that the@Serviceturns into afirefly::data::Specificationthe
repository compiles to aWHERE. At least one criterion is required — a
no-filter search is a 422, not an unscoped list-everything (and, like the
rest of this auth-free demo, a real service would authorization-scope the
results). NewWalletFilterDTO;WalletService::search.
v26.6.23
OpenAPI operation parameters & bodies now render in full (Swagger UI / ReDoc
showed bare operations with no inputs before).
Added
- Query parameters are generated from a
Query<T>/ValidQuery<T>
extractor: the generator expandsT's#[derive(Schema)]fields into one
in: queryparameter each (required iff non-optional). APageRequest
argument adds the standardpage/size/sortparameters. - Header parameters are declared on a verb attribute:
#[post("/x", header("Idempotency-Key", required, description = "…"))](and
query("…")for an extra query parameter) → anin: headerparameter the
handler reads like any axum header. RouteDescriptorgainsquery_schema/pageable/parameters
(ParamDescriptor); the openapiBuilderexpands them;Parameter::{query, header}constructors.
Fixed
- Request bodies are inferred from
Valid<T>, not onlyJson<T>— so the
validating extractor's body (the common case) is documented as a
requestBody. POST/PATCH operations previously showed no body in Swagger UI.
lumen-ledger
- Query DTOs (
OwnerQuery,StatusQuery) andStatusBodyare#[derive(Schema)]
so their fields render;POST /walletsdeclares anIdempotency-Keyheader.
v26.6.22
Security: remove an unauthenticated all-wallets listing from the sample
(flagged by automated security review of 26.6.21).
Fixed
- lumen-ledger:
GET /api/v1/walletsis owner-scoped again —
?owner=is required. 26.6.21 had made it list every wallet whenowner
was omitted (WalletService::list_all), an unauthenticated enumeration of all
account holders + balances (IDOR / broken access control). The unfiltered
listing andlist_allare removed; a missingownernow returns a clear
RFC 9457400(the \owner` query parameter is required …) instead of the confusing rawmissing field owner` deserialization error — the actual DX issue
behind the original report. The controller documents why an unfiltered listing
must be authorization-scoped (admin authority + caller-scoping) in a real
service, which this auth-free feature sample intentionally does not wire.
v26.6.21
API docs move to the management port, and the surfaces are hardened.
Changed
- OpenAPI docs (Swagger UI / ReDoc /
/v3/api-docs) are served on the
management port, beside actuator + admin — not the public API port. They
expose the whole API surface and every schema, a control-plane concern, so the
public data-plane port no longer serves them. - The OpenAPI document now declares the API base URL as its
server
(Builder::add_server), so Swagger UI's Try it out / ReDoc target the API
port rather than the management origin the docs load from. Derived from the API
bind address (wildcard host →localhost); overridable with
FIREFLY_OPENAPI_SERVER_URL.
Fixed
- The management listener now answers an RFC 9457
application/problem+json
404 for unknown paths (it previously returned axum's bare empty body),
matching the public API. - lumen-ledger
GET /api/v1/walletslists every wallet when?owner=is
omitted (an optional filter) — a bare collection request is a 200, not a
"missing query parameter" 400. AddsWalletService::list_all.
v26.6.20
The lumen-ledger sample gains the transactional transfer use case, on a new
#[transactional] option that binds the boundary to an explicit manager.
Added
#[transactional(manager = "<expr>")](firefly-macros) — Spring's
@Transactional("txManager"). Drives an explicitTransactionManager(the
expressionmyields a value with&m: &Arc<dyn TransactionManager>, e.g.
self.tx_manager()) viatransactional_on, instead of the process-global
registry. For a multi-datasource service, or to keep per-instance / per-test
isolation.- lumen-ledger
transfer—POST /api/v1/wallets/:id/transfer+
WalletService::transfermove funds between wallets atomically under
#[transactional(manager = "self.tx_manager()")]: the debit and credit commit
together or not at all, and a rejected transfer (insufficient funds, inactive
party, self-transfer, bad destination) moves no money and renders RFC 9457
422. NewTransferRequestDTO; the service autowires theDbto build its
own manager.
Docs
- Book: the layered-microservices web-surface table gains an atomic transfer
row; the declarative-macros#[transactional]section documents themanager
option; the persistence-config note explains the per-instance-manager choice.
v26.6.19
Spring Boot parity push, PR 9/N — Tier B: Actuator DI/route introspection.
Added
/actuator/beans,/actuator/mappings,/actuator/conditions
(firefly-actuator) — Spring Boot Actuator's introspection endpoints, rendered
from the framework's compile-time inventory (firefly_container::{discovered, routes}), so they need no live container:beans— every DI bean (type, module, scope, stereotype, primary, lazy),
grouped undercontexts.application.beans.mappings— every#[rest_controller]route (method, path, controller,
handler, summary), theRequestMappingHandlerMappinganalog.conditions— the@Profile/@ConditionalOn…guards each
conditionally-registered bean declares.mount()auto-registers all three (override-respecting); each is served only
when theExposureConfigincludes it, exactly as Spring gates them behind
exposure.include. Also exposed viaregister_introspection/
BeansEndpoint/MappingsEndpoint/ConditionsEndpoint.
This completes the prioritized Tier A + Tier B Spring-Boot-parity gap list.