feat(scaffold): idempotency: spec block + scaffolder integration (#174)#179
Merged
Merged
Conversation
Adds the spec-driven entry point for the idempotency primitive delivered by #172 (storage) + #173 (middleware). Spec block - idempotency.ttl (required; ^[0-9]+(ms|s|m|h|d)$) - idempotency.scope (optional; default 'tenant') - idempotency.mode (optional; 'optional' | 'required'; default 'optional') When absent, no metadata is emitted on the Action — pre-existing scaffolds stay byte-for-byte identical. Pieces - IdempotencySpec AST node + Spec.idempotency + Spec::hasIdempotency() - Parser reads the block; throws SpecParseException when ttl is missing. - Validator enforces ttl pattern, non-empty scope, and mode enum. - ActionEmitter adds a static idempotency() accessor on the generated Action when the spec carries the block, exposing the ttl/scope/mode policy for the host application's IdempotencyKeyMiddleware to consume via IdempotencyConfiguration. Configuration - New Altair\Idempotency\Configuration\IdempotencyConfiguration (univeros/idempotency) binds IdempotencyStoreInterface to InMemoryStore by default. Host applications swap to ApcuStore or RedisStore by re-binding after this Configuration applies. - univeros/idempotency picks up univeros/configuration + univeros/container deps to support the binding. Out of scope (per #171) - The x-altair-idempotency round-trip activation — #175. - Auto-wiring the middleware into the route pipeline — host's job in v1; will be tightened once a clear pattern emerges across consumers. - docs/packages/idempotency.md + benchmark variant — #176. Tests (+12) - IdempotencyParserTest covers happy path, defaults, missing-ttl raise, absent-block null, validator's ttl-pattern / mode / good cases, and the aggregated SpecValidationException. - ActionEmitterIdempotencyTest verifies the accessor is absent without the block, present and structurally correct with it, and that the generated PHP tokenises cleanly. - SpecFixture::createUserWithIdempotency() added so future tests can reuse a canonical fixture. Part of #171. Closes #174.
This was referenced May 31, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #174. Part of #171. Depends on #172 + #173.
Summary
Spec-driven entry point for the idempotency primitive. A spec carrying:
now scaffolds an Action that exposes the policy via a static
idempotency()accessor:The host application's
IdempotencyKeyMiddleware(#173) consumes that metadata viaIdempotencyConfigurationto build the per-endpoint policy from spec — no hand-wiring per route.Pieces
Altair\Scaffold\Spec\Ast\IdempotencySpec— readonly value object withttl+scope(defaulttenant) +mode(defaultoptional).Spec.idempotency+Spec::hasIdempotency()— backwards-compatible defaultnull.Parser::parseIdempotency()— reads the block; raisesSpecParseExceptionon missing ttl.Validator::validateIdempotency()— enforces ttl pattern^[0-9]+(ms|s|m|h|d)$, non-empty scope, and mode enum.ActionEmitter— renders the staticidempotency()accessor on the generated Action when the spec carries the block. When absent, output is byte-for-byte identical to today's scaffold.Altair\Idempotency\Configuration\IdempotencyConfiguration— minimal DI binding:IdempotencyStoreInterface\xe2\x86\x92InMemoryStoreby default; hosts swap toApcuStore/RedisStoreby re-binding.Why a static accessor and not a base-class slot
The current
Altair\Http\Base\Actiondoesn't carry a middleware-pipeline slot. Modifying it would expand #174 well beyond the issue scope. A static accessor on the generated Action is the smallest invasive change — host middleware reads it from the resolved Action instance viaIdempotencyConfigurationand configures the runtimeIdempotencyKeyMiddlewareaccordingly.The actual route-pipeline auto-wiring is intentionally left as a follow-up because the pattern depends on the host's router and middleware stack (which varies). #176 will document the wiring recipe; once a clear convention emerges across consumers a dedicated issue can tighten the auto-wiring.
Tests (+12)
IdempotencyParserTest(8) \xe2\x80\x94 happy path, defaults, missing-ttl raise, absent-block null, validator's ttl-pattern / mode / all-units / valid-block / aggregated-exception cases.ActionEmitterIdempotencyTest(3) \xe2\x80\x94 accessor absent without block, accessor present + structurally correct with block, generated PHP tokenises cleanly.SpecFixture::createUserWithIdempotency()added for downstream reuse.Test plan
composer cs\xe2\x80\x94 greencomposer stan\xe2\x80\x94 greencomposer rector(full tree, no cache) \xe2\x80\x94 greencomposer test\xe2\x80\x94 6287 tests (+12 new), 0 new failuresbin/altair manifest:generate\xe2\x80\x94 cleanOut of scope
x-altair-idempotencyround-trip activation \xe2\x80\x94 x-altair-idempotency round-trip activation: forward emit + reverse import + drift gate #175 (lands next).