Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,35 @@ and `php -l` gates, before anything ships.
400 instead of 422), and the error-bag-to-schema mapping is application semantics the spec does
not encode. The generator contributes the typed half (error component schemas already generate
Data classes); the docs guide `guides/validation-errors` holds the bootstrap renderer recipe.
**Extended, not re-litigated, by the inlined `ApiError` throwable (issue #168):** a `final`
exception, inlined into the consumer's own `\Support` namespace exactly like `RespondsWithStatus`,
that carries any generated Data class (or other Responsable/Arrayable/JsonSerializable value) plus
an HTTP status and self-renders through Laravel's `render(Request): Response` exception-handler
hook (no `bootstrap/app.php` registration needed). It is a schema-agnostic CARRIER, not a
renderer: it never inspects or maps a spec's error shape itself (that mapping is still the
documented recipe's job), so this decision's core stance is unchanged. It solves a narrower,
different problem than #79: the generated abstract controller method's return type is always the
operation's success DTO (by design, error responses are never inspected for typing), so a concrete
controller that must answer a spec-declared error status previously had to hand-roll a helper that
RETURNS a JsonResponse, which does not satisfy the success return type. Throwing (never returning)
satisfies any return type, so `ApiError::notFound($errorData)` and its sibling named-status
factories give that throw an ergonomic, typed home without inventing a new generated renderer.
**Further extended by the generated `<Operation>Errors` factory layer (issue #168):** the ApiError
carrier is now complemented by a GENERATED per-operation factory, `<Operation>Errors` (one static
method per concrete 4xx/5xx error response whose JSON schema resolves to a named component object; v1
scope, inline-object, non-object, unresolvable, and default/wildcard error slots are documented
residuals). An operation that DOES get a factory warns once per declared error slot it could not
turn into a method; an operation with NO qualifying error slot generates no factory and stays
silent (so specs whose error bodies are entirely non-objects or `default` catch-alls are not
flooded with warnings).
`throw GetPetByIdErrors::notFound(message: '...');` is now the PRIMARY, RECOMMENDED pattern for a
spec-declared error whose operation has a generated factory; `ApiError`'s own named factories and
general constructor remain a documented escape hatch for anything a generated factory does not
cover (a cross-cutting error the spec does not declare per-operation, an operation whose error
responses do not qualify for flattening, or a status the spec's per-operation responses map omits).
Neither layer maps Laravel's error bag into a spec shape or inspects a spec's error schema on the
developer's behalf beyond flattening an ALREADY-NAMED schema's own constructor; decision #11's core
stance (no generated renderer) remains unchanged by either layer.
12. **Config diet: the generator is opinionated about style.** New options must be environment
facts the generator cannot know (paths, middleware names, FQCNs) or correctness escape
hatches, never style preferences. Style is the generator's job. The #93 (`--group-by-tag` /
Expand Down
3 changes: 3 additions & 0 deletions composer-require-checker.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,14 @@
"config",
"config_path",
"base_path",
"response",
"Illuminate\\Console\\Command",
"Illuminate\\Support\\ServiceProvider",
"Illuminate\\Support\\Arr",
"Illuminate\\Contracts\\Validation\\ValidationRule",
"Illuminate\\Contracts\\Validation\\DataAwareRule",
"Illuminate\\Contracts\\Support\\Arrayable",
"Illuminate\\Contracts\\Support\\Responsable",
"Illuminate\\Http\\Request",
"Symfony\\Component\\HttpFoundation\\Response"
]
Expand Down
15 changes: 8 additions & 7 deletions docs/src/content/docs/guides/runtime-coupling.mdx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
---
title: Runtime coupling of generated code
description: Generated code used to import eight support classes from the generator package, making it a runtime dependency. The 1.0.0 question, keep that dependency or make generation self-contained, is decided and shipped, Option B (inline into the consumer's output), with the analysis and tradeoffs that led there.
description: Generated code used to import nine support classes from the generator package, making it a runtime dependency. The 1.0.0 question, keep that dependency or make generation self-contained, is decided and shipped, Option B (inline into the consumer's output), with the analysis and tradeoffs that led there.
---

import { Aside } from '@astrojs/starlight/components'
Expand All @@ -21,6 +21,7 @@ The generated Data classes stand alone in your repo. When a class needs a rule o
- `App\Data\Support\MapObjectTransformer` (every `additionalProperties` map property, via `#[WithTransformer(...)]`)
- `App\Data\Support\NoUnknownPropertiesRule` (the default `additionalProperties: false` enforcement)
- `App\Data\Support\RespondsWithStatus` (the route middleware that enforces non-200 declared success status codes)
- `App\Data\Support\ApiError` (the self-rendering throwable for a spec-declared error response)

(The `App\Data` prefix mirrors whatever Data namespace you configured: the support namespace is always the Data namespace plus a `\Support` suffix.)

Expand All @@ -32,14 +33,14 @@ The result is the headline this decision delivers: **`codewithagents/openapi-lar

The project's stated philosophy is [you own the output](/philosophy): readable PHP in your repo that keeps working even if you stop using the generator. The runtime coupling (now removed) dented that in two ways:

- **It was not fully owned.** Eight classes that your generated `rules()` depended on lived in `vendor/`, outside the code you committed and review in diffs.
- **It was not fully owned.** Nine classes that your generated code depended on lived in `vendor/`, outside the code you committed and review in diffs.
- **A `composer update` could change runtime behavior under committed code.** Because the support classes were versioned with the generator, upgrading the generator could change how your already-generated, already-committed classes validate and serialize, without you regenerating anything. That was the exact silent-change surface the [versioning policy](/guides/versioning-policy) pins down. Inlining closes it: a rule changes only when you regenerate and review the diff.

`spatie/laravel-data` itself is a genuine runtime dependency under **every** option below, that is unavoidable and expected, the generated classes *are* laravel-data classes. The decision below concerned only the eight `openapi-laravel`-owned support classes, which now live in your own `Support` namespace.
`spatie/laravel-data` itself is a genuine runtime dependency under **every** option below, that is unavoidable and expected, the generated classes *are* laravel-data classes. The decision below concerned only the nine `openapi-laravel`-owned support classes, which now live in your own `Support` namespace.

## The 1.0.0 question

This had to settle **before** 1.0.0, because the import lines are part of the frozen output format. Moving the namespace later (for example from `CodeWithAgents\OpenApiLaravel\Support\...` to `App\Data\Support\...`) is a breaking output change, exactly what the 1.0.0 freeze is meant to prevent. The discriminator-aware cast ([#38](https://github.com/codewithagents/openapi-laravel/issues/38)) has since **shipped using spatie's native `PropertyMorphableData`** (an abstract morphable base plus `morph()`), so it added **no** new runtime support class. The inlined set under Option B is therefore the eight classes above (including `NoUnknownPropertiesRule`, emitted whenever a closed object is present, which is now the default), not a growing list driven by #38.
This had to settle **before** 1.0.0, because the import lines are part of the frozen output format. Moving the namespace later (for example from `CodeWithAgents\OpenApiLaravel\Support\...` to `App\Data\Support\...`) is a breaking output change, exactly what the 1.0.0 freeze is meant to prevent. The discriminator-aware cast ([#38](https://github.com/codewithagents/openapi-laravel/issues/38)) has since **shipped using spatie's native `PropertyMorphableData`** (an abstract morphable base plus `morph()`), so it added **no** new runtime support class. The inlined set under Option B is therefore the nine classes above (including `NoUnknownPropertiesRule`, emitted whenever a closed object is present, which is now the default), not a growing list driven by #38.

## Option A: keep the runtime dependency (status quo)

Expand All @@ -56,7 +57,7 @@ Generated code keeps importing the support classes from the generator package. `

- **Generated code is not self-contained.** "Stop using the generator and your code still works" is false: remove the package and the classes that import `Support\...` break.
- **Silent runtime-behavior changes on upgrade**, as above. This is the strongest argument against A.
- **The generator is a heavier dependency than it needs to be.** Consumers pull the whole generator (parser, emitter, symfony/yaml) into production just to get eight small runtime classes.
- **The generator is a heavier dependency than it needs to be.** Consumers pull the whole generator (parser, emitter, symfony/yaml) into production just to get nine small runtime classes.

## Option B: inline the support classes into the consumer's output (adopted and shipped)

Expand All @@ -76,7 +77,7 @@ The generator emits the referenced support classes into the consumer's own names

## Option C: a tiny frozen runtime package

Split the eight support classes into a separate, minimal, semver-frozen package (for example `codewithagents/openapi-laravel-runtime`) that changes essentially never. The generator depends on it; generated code imports from it; consumers `require` only the tiny runtime, not the whole generator.
Split the nine support classes into a separate, minimal, semver-frozen package (for example `codewithagents/openapi-laravel-runtime`) that changes essentially never. The generator depends on it; generated code imports from it; consumers `require` only the tiny runtime, not the whole generator.

**Pros**

Expand Down Expand Up @@ -114,6 +115,6 @@ Option C was the reasonable fallback if duplication had been judged unacceptable
**Option B is implemented.** The generator inlines the referenced support classes into the consumer's own namespace (the Data namespace plus a `\Support` suffix, for example `App\Data\Support\...`), making generated output fully self-contained with no runtime dependency on the generator.

- **Status:** done. The support classes are emitted into `<output>/Support/`, imported by the generated Data classes from there, and drift-checked byte-for-byte by `openapi:check`. Only the classes a spec references are emitted.
- **No growing set from [#38](https://github.com/codewithagents/openapi-laravel/issues/38):** the discriminator-aware cast shipped on spatie's native `PropertyMorphableData`, adding zero support classes. The inlined set is the eight listed above (including `NoUnknownPropertiesRule`, emitted whenever a closed object is present, which is now the default).
- **No growing set from [#38](https://github.com/codewithagents/openapi-laravel/issues/38):** the discriminator-aware cast shipped on spatie's native `PropertyMorphableData`, adding zero support classes. The inlined set is the nine listed above (including `NoUnknownPropertiesRule`, emitted whenever a closed object is present, which is now the default).
- **It resolves [#41 (versioning policy)](/guides/versioning-policy):** now that they are inlined, the support classes fall under the **output** surface and are governed by the same major-bump rule as the rest of the generated code.
</Aside>
22 changes: 22 additions & 0 deletions docs/src/content/docs/guides/server-scaffold.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -357,6 +357,28 @@ final class PetController extends AbstractPetController
}
```

Every abstract method is typed to the operation's success DTO, so a concrete method that needs to
answer a spec-declared **error** status throws instead of returning (a `throw` never reaches the
`return`, so the success type stays satisfied). For an operation whose spec declares a
named-component object error response, the generator emits a `<Operation>Errors` factory with one
status-named method per qualifying error status, so `show()` can answer the spec's own 404 shape
rather than let a generic lookup decide it:

```php
public function show(int $petId): PetData
{
return $this->petService->find($petId)
?? throw GetPetByIdErrors::notFound(message: "Pet {$petId} not found.");
}
```

The status lives in the method name, the error DTO is built and flattened for you, and the thrown
value self-renders with no `bootstrap/app.php` wiring. For a status a generated factory does not
cover, the `App\Data\Support\ApiError` carrier is directly available
(`throw ApiError::forbidden($body)` or `new ApiError($body, $status)`). See
[Throwing other error statuses](/guides/validation-errors#throwing-other-error-statuses) for the
full treatment.

### Generated routes file

```php
Expand Down
4 changes: 2 additions & 2 deletions docs/src/content/docs/guides/stability.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,9 @@ release produces byte-identical PHP: the same class and property names, the same
same type hints and docblocks, the same file set, the same `#[MapName]` attributes. The
[drift check](/guides/drift-check) (`openapi:check`) enforces this byte-for-byte in CI.

**2. The support-class namespace and import lines.** The eight support classes the generator inlines
**2. The support-class namespace and import lines.** The nine support classes the generator inlines
(`MultipleOfRule`, `Rfc3339DateTimeRule`, `Rfc3339TimeRule`, `Iso8601DurationRule`, `HostnameRule`,
`MapObjectTransformer`, `NoUnknownPropertiesRule`, `RespondsWithStatus`) land at a fixed location
`MapObjectTransformer`, `NoUnknownPropertiesRule`, `RespondsWithStatus`, `ApiError`) land at a fixed location
relative to your configured Data namespace, for example `App\Data\Support\MultipleOfRule`. Their
import lines are part of the committed output, so moving them would be an output-shape change covered
by the same rule. The namespace is not changing after `1.0.0`.
Expand Down
Loading
Loading