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
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ A modern, fluent builder for [OpenAPI 3.1](https://spec.openapis.org/oas/v3.1.0)
- Schemas via `cortexphp/json-schema` — no parallel schema DSL to learn
- Vendor extensions (`x-*`) and `$ref` on every object
- JSON and YAML output (YAML via optional `symfony/yaml`)
- Meta-schema validation proxied through the existing `cortex/json-schema` pipeline
- Meta-schema validation against the official OpenAPI 3.1 schemas (bundled, offline)

## Requirements

Expand Down Expand Up @@ -44,7 +44,7 @@ $userSchema = Schema::object('User')->properties(
);

$openApi = OpenApi::create()
->info(Info::create()->title('Example API')->version('1.0.0'))
->info(Info::create('Example API', '1.0.0'))
->tags(Tag::create('Users')->description('User endpoints'))
->components(Components::create()->schema('User', $userSchema))
->paths(
Expand Down
3 changes: 2 additions & 1 deletion docs/docs.json
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,8 @@
"openapi/responses",
"openapi/components",
"openapi/security",
"openapi/webhooks-and-callbacks"
"openapi/webhooks-and-callbacks",
"openapi/validation-and-output"
]
}
]
Expand Down
18 changes: 9 additions & 9 deletions docs/openapi/components.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ Components supports ten registries, each addressable by its own `$ref` path:

## Schemas

Register any `cortexphp/json-schema` schema or raw array by name:
Register any `cortexphp/json-schema` schema or raw array by name. The component key (`'User'`) is what `$ref` pointers use, so there is no need to name the schema itself — see [how schemas are embedded](/openapi/introduction).

```php
use Cortex\JsonSchema\Schema;
Expand Down Expand Up @@ -72,9 +72,9 @@ MediaType::json(Reference::schema('User'))
// In a Parameter schema slot
Parameter::query('filter', Reference::schema('Filter'))

// In a composed schema (allOf/oneOf/anyOf)
// Composing inside a schema — use the JSON Schema builder's own ref()
Schema::object()->allOf(
Reference::schema('BaseEntity'),
Schema::typeless()->ref('#/components/schemas/BaseEntity'),
Schema::object()->properties(Schema::string('title')),
)
```
Expand Down Expand Up @@ -153,14 +153,14 @@ $components = Components::create()
);
```

Reference them from operations using `Response::ref()`:
Reference them from operations with `->ref()`, which keeps the status code from the named constructor:

```php
Operation::get()
->responses(
Response::ok()->content(MediaType::json(Reference::schema('User'))),
Response::ref('NotFound'),
Response::ref('Unauthorized'),
Response::ok()->json(Reference::schema('User')),
Response::notFound()->ref('NotFound'),
Response::unauthorized()->ref('Unauthorized'),
);
```

Expand All @@ -184,8 +184,8 @@ $components = Components::create()
);

// Reference
Operation::post()->requestBody(RequestBody::ref('CreateUser'));
Operation::patch()->requestBody(RequestBody::ref('UpdateUser'));
Operation::post()->requestBody(Reference::requestBody('CreateUser'));
Operation::patch()->requestBody(Reference::requestBody('UpdateUser'));
```

## Security Schemes
Expand Down
10 changes: 7 additions & 3 deletions docs/openapi/info-and-metadata.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,7 @@ PathItem::create('/uploads')

## Tags

Tags group operations together in documentation renderers. Declare them at the document level with `Tag`, then reference them by name string inside each operation.
Tags group operations together in documentation renderers. Declare them at the document level with `Tag`, then attach them on each operation with `Operation::tags()`.

```php
use Cortex\OpenApi\Objects\Tag;
Expand All @@ -156,10 +156,14 @@ OpenApi::create()
);
```

Reference a tag in an operation by its name string:
Pass the `Tag` objects you already declared, plain name strings, or a mix — the name is taken from the tag for you:

```php
Operation::get()->tags('users', 'payments')
$users = Tag::create('users')->description('User account management');

Operation::get()->tags($users);
Operation::get()->tags('users', 'payments');
Operation::get()->tags($users, 'payments');
```

<Tip>
Expand Down
10 changes: 10 additions & 0 deletions docs/openapi/installation.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,16 @@ icon: 'terminal'
composer require cortexphp/openapi
```

## YAML Output

JSON serialization (`toJson()`, `toArray()`) is built-in. YAML output requires [symfony/yaml](https://symfony.com/doc/current/components/yaml.html):

```bash
composer require symfony/yaml
```

`toYaml()` throws a `RuntimeException` if the package is not installed.

## Development Setup

### For Package Development
Expand Down
87 changes: 80 additions & 7 deletions docs/openapi/introduction.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -154,17 +154,90 @@ MediaType::json($addressSchema);
Parameter::query('filter', Schema::string()->enum(['active', 'archived']));
```

When serializing, `$schema` URI and `title` are automatically stripped from inline schemas per the OpenAPI spec — the sibling schema object is never mutated.
Embedding copies the schema into the slot and never mutates the object you passed, so the same schema can be reused across a document. Two things are cleaned up on the way in, at every depth:

- The JSON Schema `$schema` URI is dropped. An OpenAPI document declares its dialect once, at the top level.
- The name you gave a builder — `Schema::object('Pet')`, `Schema::string('id')` — is dropped. It labels the variable, not the API.

A `title` you set explicitly is kept, because that is documentation you meant to publish:

```php
// The constructor name is a builder label
MediaType::json(Schema::object('Consult'));
// schema: { "type": "object" }

// ->title() is published
MediaType::json(Schema::object('Consult')->title('Consultation'));
// schema: { "type": "object", "title": "Consultation" }
```

<Tip>
`properties()` takes its key from each child's constructor name (`Schema::string('email')`), so property schemas do need one — and it is never duplicated into the output as a `title`.
</Tip>

### OpenAPI-only Schema Fields

`discriminator` and `xml` come from OpenAPI rather than JSON Schema, so they have no builder method. Every schema slot also accepts a plain array, which lets you spread a built schema and add them:

```php
use Cortex\JsonSchema\Schema;
use Cortex\OpenApi\Objects\Discriminator;
use Cortex\OpenApi\Objects\MediaType;

$pet = Schema::object()->properties(
Schema::string('petType')->required(),
);

MediaType::json([
...$pet->toArray(includeSchemaRef: false),
'discriminator' => Discriminator::create('petType')
->mapping(['dog' => '#/components/schemas/Dog'])
->toArray(),
]);
```

### References Inside Schemas

`Reference` is an OpenAPI object for schema *slots* — `MediaType`, `Parameter`, `Header`, `Components::schema()`. To point at a component from inside a schema you are composing, use the JSON Schema builder's own `ref()`:

```php
use Cortex\JsonSchema\Schema;
use Cortex\OpenApi\Objects\MediaType;
use Cortex\OpenApi\Objects\Reference;

// A slot takes a Reference
MediaType::json(Reference::schema('User'));

// Composition takes a schema — typeless() emits a bare $ref with no stray "type"
Schema::object()->allOf(
Schema::typeless()->ref('#/components/schemas/BaseEntity'),
Schema::object()->properties(Schema::string('title')),
);
```

## OpenAPI Version Support

<Info>
The default version is **3.1.0**. Use `OpenApi::v311()` to target the 3.1.1 revision, which includes clarifications and minor fixes.
The default version is **3.1.0**. Pass `OpenApiVersion::V3_1_1` to target the 3.1.1 revision, which includes clarifications and minor fixes.
</Info>

| Version | Status | Named Constructor |
|---------|--------|-------------------|
| 3.1.0 | Supported | `OpenApi::create()` or `OpenApi::v310()` |
| 3.1.1 | Supported | `OpenApi::v311()` |
| Version | Status | Constructor |
|---------|--------|-------------|
| 3.1.0 | Supported | `OpenApi::create()` or `OpenApi::create(OpenApiVersion::V3_1_0)` |
| 3.1.1 | Supported | `OpenApi::create(OpenApiVersion::V3_1_1)` |

Both versions ship with their official meta-schema bundled — validation works fully offline.
```php
use Cortex\OpenApi\OpenApi;
use Cortex\OpenApi\Enums\OpenApiVersion;

OpenApi::create(); // 3.1.0
OpenApi::create(OpenApiVersion::V3_1_1); // 3.1.1
```

Both versions ship with their official meta-schema bundled, so [validation](/openapi/validation-and-output) works fully offline.

Schema Objects are read as JSON Schema 2020-12 unless you say otherwise. If your schemas use a different dialect, announce it once on the document:

```php
OpenApi::create()->jsonSchemaDialect('https://json-schema.org/draft/2020-12/schema');
```
41 changes: 40 additions & 1 deletion docs/openapi/paths-and-operations.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -94,13 +94,27 @@ Operation::trace()
```php Common fields
Operation::get()
->operationId('articles.show') // unique ID used by tooling and links
->tags('articles', 'public') // groups the operation in docs
->tags('articles', 'public') // Tag objects or name strings
->summary('Get an article') // short label
->description('Returns a single article by its slug.')
->deprecated() // marks operation as deprecated
```
</CodeGroup>

### Tags

`Operation::tags()` accepts name strings, `Tag` objects, or both. Document-level `Tag` objects are serialized as `{ name, description, ... }`; on an operation they become the name string only:

```php
use Cortex\OpenApi\Objects\Tag;

$articles = Tag::create('articles')->description('Publishing');

Operation::get()->tags('articles', 'public');
Operation::get()->tags($articles);
Operation::get()->tags($articles, 'public');
```

### operationId

`operationId` must be unique across the entire document. Tools use it for client SDK method naming and inter-operation links.
Expand All @@ -122,6 +136,31 @@ Operation::get()
->description('Deprecated. Use /v2/items instead.')
```

### Attaching Responses

`responses()` takes `Response` objects and keys them from their own status code, so you rarely repeat yourself. Point a status code at a reusable response with `->ref()`:

```php
use Cortex\OpenApi\Objects\Response;
use Cortex\OpenApi\Objects\Reference;

Operation::get()
->responses(
Response::ok()->json(Reference::schema('Article')),
Response::notFound()->ref('NotFound'),
);
```

`response()` sets a single status key, which is useful for a code with no named constructor or when adding to an existing map — `responses()` replaces the map wholesale:

```php
Operation::get()
->responses(Response::ok()->json(Reference::schema('Article')))
->response(418, Reference::response('Teapot'));
```

See [Responses](/openapi/responses) for headers, links, and reusable response components.

## Parameters

Parameters live in four locations: `path`, `query`, `header`, and `cookie`. Named constructors set the location automatically.
Expand Down
14 changes: 8 additions & 6 deletions docs/openapi/quickstart.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -273,16 +273,18 @@ Reference::requestBody('CreateUser') // #/components/requestBodies/CreateUser
Reference::header('RateLimit') // #/components/headers/RateLimit
Reference::securityScheme('OAuth2') // #/components/securitySchemes/OAuth2
Reference::link('GetPet') // #/components/links/GetPet
Reference::example('AdminUser') // #/components/examples/AdminUser
Reference::callback('EventWebhook') // #/components/callbacks/EventWebhook
Reference::pathItem('LegacyPets') // #/components/pathItems/LegacyPets

// Generic reference — use when the bucket isn't covered above
Reference::to('#/components/schemas/Pet')
// Generic reference — for a pointer outside the component buckets
Reference::to('./common.yaml#/components/schemas/Pet')
```

Every reference is built here, so there is one place to look and one spelling to remember. The exception is a response inside `Operation::responses()`, which needs a status code to key on — see [Responses](/openapi/responses):

// Shortcut on the target class (equivalent, and communicates intent)
Response::ref('NotFound')
Parameter::ref('PetId')
RequestBody::ref('CreateUser')
```php
Response::notFound()->ref('NotFound')
```

## Vendor Extensions
Expand Down
4 changes: 2 additions & 2 deletions docs/openapi/request-bodies.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -162,7 +162,7 @@ MediaType::multipart($uploadSchema)->encoding([
|--------|-------------|
| `->contentType(string)` | Override MIME type for the part (default: field's schema type) |
| `->headers(array)` | Custom HTTP headers on this part |
| `->style(string)` | Serialization style for complex types |
| `->style(...)` | Serialization style for complex types (`Style` enum or a spec string such as `'form'`) |
| `->explode(bool)` | Explode arrays into separate parts |
| `->allowReserved(bool)` | Allow RFC3986 reserved characters unencoded |

Expand Down Expand Up @@ -223,7 +223,7 @@ $components = Components::create()
// Reference from an operation
Operation::post()
->operationId('articles.create')
->requestBody(RequestBody::ref('CreateArticle'));
->requestBody(Reference::requestBody('CreateArticle'));
```

## Complete Example
Expand Down
21 changes: 18 additions & 3 deletions docs/openapi/responses.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ icon: 'file-down'

## Response

`Response` describes a single HTTP status code outcome. Pass one or more responses to `Operation::responses()`.
`Response` describes a single HTTP status code outcome. Pass one or more of them to `Operation::responses()` and each is keyed from its own status code:

```php
use Cortex\OpenApi\Objects\Response;
Expand All @@ -20,6 +20,21 @@ Operation::get()
);
```

To answer a status code with a reusable response from `Components`, use `->ref()`. The status still comes from the named constructor, so it belongs in the same `responses()` call:

```php
Operation::get()
->responses(
Response::ok()->json(Reference::schema('User')),
Response::notFound()->ref('NotFound'),
Response::unauthorized()->ref('Unauthorized'),
);
```

<Tip>
Elsewhere, references are built with `Reference::response('NotFound')` — see [Working with References](/openapi/quickstart). `->ref()` exists on `Response` because `responses()` keys each entry by status code, and a bare reference has none. Slots that supply the status themselves, such as `Components::response()` and `Operation::response(404, ...)`, take `Reference::response()`.
</Tip>

### Named Constructors

Every common HTTP status code has a named constructor that pre-populates the standard description. Override it with `->description()`.
Expand Down Expand Up @@ -199,8 +214,8 @@ $components = Components::create()
Operation::get()
->responses(
Response::ok()->json($schema),
Response::ref('NotFound'),
Response::ref('Unauthorized'),
Response::notFound()->ref('NotFound'),
Response::unauthorized()->ref('Unauthorized'),
);
```

Expand Down
1 change: 1 addition & 0 deletions docs/openapi/security.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -278,6 +278,7 @@ use Cortex\OpenApi\Objects\SecurityRequirement;
use Cortex\OpenApi\Objects\OAuthFlows;
use Cortex\OpenApi\Objects\OAuthFlow;
use Cortex\OpenApi\Objects\Operation;
use Cortex\OpenApi\Objects\PathItem;
use Cortex\OpenApi\Enums\In;

$doc = OpenApi::create()
Expand Down
Loading