Skip to content

Commit c167fba

Browse files
authored
Added support for extra metadata reflected in the OpenApiSpecification (#1811)
1 parent 0d64c44 commit c167fba

5 files changed

Lines changed: 958 additions & 31 deletions

File tree

documentation/components/bridges/openapi-specification-bridge.md

Lines changed: 187 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -24,19 +24,19 @@ This bridge allows you to convert Flow PHP schemas to OpenAPI specification form
2424

2525
use function Flow\ETL\DSL\{schema, int_schema, str_schema, bool_schema};
2626
use function Flow\Bridge\OpenAPI\Specification\DSL\schema_to_openapi_specification;
27-
use Flow\ETL\Schema\Metadata;
27+
use Flow\Bridge\OpenAPI\Specification\OpenAPIMetadata;
2828

2929
$userSchema = schema(
30-
int_schema('id', false, Metadata::empty()
31-
->add('description', 'Unique user identifier')
32-
->add('example', 123)
30+
int_schema('id', false,
31+
OpenAPIMetadata::description('Unique user identifier')
32+
->merge(OpenAPIMetadata::example(123))
3333
),
34-
str_schema('name', true, Metadata::empty()
35-
->add('description', 'User full name')
36-
->add('example', 'John Doe')
34+
str_schema('name', true,
35+
OpenAPIMetadata::description('User full name')
36+
->merge(OpenAPIMetadata::example('John Doe'))
3737
),
38-
bool_schema('active', false, Metadata::empty()
39-
->add('description', 'Account status')
38+
bool_schema('active', false,
39+
OpenAPIMetadata::description('Account status')
4040
)
4141
);
4242

@@ -106,6 +106,155 @@ $flowSchema = schema_from_openapi_specification($openApiSpec);
106106
```
107107

108108

109+
## OpenAPI Metadata
110+
111+
The OpenAPI Specification Bridge provides specialized metadata handling through the `OpenAPIMetadata` enum, allowing you to add OpenAPI-specific properties to your Flow schema definitions. This metadata is used during schema conversion to generate rich OpenAPI specifications.
112+
113+
### Available OpenAPI Metadata
114+
115+
The `OpenAPIMetadata` enum provides the following metadata types:
116+
117+
- `DESCRIPTION` - Human-readable description of the property
118+
- `FORMAT` - OpenAPI format specification (e.g., 'email', 'date-time', 'uuid')
119+
- `EXAMPLE` - Example value for the property
120+
- `EXAMPLES` - Multiple named examples
121+
- `DEPRECATED` - Mark property as deprecated
122+
- `TITLE` - Short title for the property
123+
- `DEFAULT` - Default value for the property
124+
- `READ_ONLY` - Mark property as read-only
125+
- `WRITE_ONLY` - Mark property as write-only
126+
- `NULLABLE` - Override schema nullability
127+
128+
### Using OpenAPI Metadata
129+
130+
```php
131+
<?php
132+
133+
use function Flow\ETL\DSL\{schema, int_schema, str_schema};
134+
use Flow\Bridge\OpenAPI\Specification\{OpenAPIConverter, OpenAPIMetadata};
135+
136+
$userSchema = schema(
137+
int_schema('id', false,
138+
OpenAPIMetadata::description('Unique user identifier')
139+
->merge(OpenAPIMetadata::format('int64'))
140+
->merge(OpenAPIMetadata::example(12345))
141+
->merge(OpenAPIMetadata::readOnly())
142+
),
143+
str_schema('email', false,
144+
OpenAPIMetadata::description('User email address')
145+
->merge(OpenAPIMetadata::format('email'))
146+
->merge(OpenAPIMetadata::example('user@example.com'))
147+
->merge(OpenAPIMetadata::title('Email Address'))
148+
),
149+
str_schema('password', false,
150+
OpenAPIMetadata::description('User password')
151+
->merge(OpenAPIMetadata::format('password'))
152+
->merge(OpenAPIMetadata::writeOnly())
153+
),
154+
str_schema('status', false,
155+
OpenAPIMetadata::description('Account status')
156+
->merge(OpenAPIMetadata::default('active'))
157+
->merge(OpenAPIMetadata::examples([
158+
'active' => 'active',
159+
'inactive' => 'inactive',
160+
'suspended' => 'suspended'
161+
]))
162+
)
163+
);
164+
165+
$converter = new OpenAPIConverter();
166+
$openApiSpec = $converter->toOpenAPI($userSchema);
167+
168+
// Results in:
169+
// [
170+
// 'type' => 'object',
171+
// 'properties' => [
172+
// 'id' => [
173+
// 'type' => 'integer',
174+
// 'nullable' => false,
175+
// 'description' => 'Unique user identifier',
176+
// 'format' => 'int64',
177+
// 'example' => 12345,
178+
// 'readOnly' => true
179+
// ],
180+
// 'email' => [
181+
// 'type' => 'string',
182+
// 'nullable' => false,
183+
// 'description' => 'User email address',
184+
// 'format' => 'email',
185+
// 'example' => 'user@example.com',
186+
// 'title' => 'Email Address'
187+
// ],
188+
// 'password' => [
189+
// 'type' => 'string',
190+
// 'nullable' => false,
191+
// 'description' => 'User password',
192+
// 'format' => 'password',
193+
// 'writeOnly' => true
194+
// ],
195+
// 'status' => [
196+
// 'type' => 'string',
197+
// 'nullable' => false,
198+
// 'description' => 'Account status',
199+
// 'default' => 'active',
200+
// 'examples' => [
201+
// 'active' => 'active',
202+
// 'inactive' => 'inactive',
203+
// 'suspended' => 'suspended'
204+
// ]
205+
// ]
206+
// ]
207+
// ]
208+
```
209+
210+
### Metadata Priority System
211+
212+
The OpenAPI bridge uses a priority system when handling metadata:
213+
214+
1. **OpenAPI-specific metadata** (from `OpenAPIMetadata`) takes highest priority
215+
2. **Legacy metadata keys** are used as fallback when OpenAPI metadata is not present
216+
217+
```php
218+
<?php
219+
220+
use Flow\ETL\Schema\Metadata;
221+
use Flow\Bridge\OpenAPI\Specification\OpenAPIMetadata;
222+
223+
// OpenAPI metadata takes priority over legacy metadata
224+
$metadata = Metadata::with('description', 'Legacy description')
225+
->merge(OpenAPIMetadata::description('OpenAPI description')) // This wins
226+
->merge(Metadata::with('example', 'Legacy example'))
227+
->merge(OpenAPIMetadata::example('OpenAPI example')); // This wins
228+
229+
$schema = schema(
230+
str_schema('field', false, $metadata)
231+
);
232+
233+
// Result will use OpenAPI values: "OpenAPI description" and "OpenAPI example"
234+
```
235+
236+
### Common OpenAPI Formats
237+
238+
Here are some commonly used OpenAPI formats you can specify with `OpenAPIMetadata::format()`:
239+
240+
**String formats:**
241+
- `email` - Email address
242+
- `uri` - URI/URL
243+
- `uuid` - UUID string
244+
- `date` - Date (YYYY-MM-DD)
245+
- `date-time` - Date and time (RFC 3339)
246+
- `password` - Password field
247+
- `byte` - Base64 encoded string
248+
- `binary` - Binary data
249+
250+
**Integer formats:**
251+
- `int32` - 32-bit integer
252+
- `int64` - 64-bit integer
253+
254+
**Number formats:**
255+
- `float` - Floating point number
256+
- `double` - Double precision number
257+
109258
### Usage Example
110259

111260
```php
@@ -114,17 +263,39 @@ $flowSchema = schema_from_openapi_specification($openApiSpec);
114263
use function Flow\ETL\DSL\{schema, int_schema, str_schema, list_schema, structure_schema};
115264
use function Flow\Types\DSL\{type_list, type_string, type_structure};
116265
use function Flow\Bridge\OpenAPI\Specification\DSL\schema_to_openapi_specification;
266+
use Flow\Bridge\OpenAPI\Specification\OpenAPIMetadata;
117267

118-
// Define your data schema
268+
// Define your data schema with rich OpenAPI metadata
119269
$productSchema = schema(
120-
int_schema('id', false),
121-
str_schema('name', false),
122-
str_schema('description', true),
270+
int_schema('id', false,
271+
OpenAPIMetadata::description('Product identifier')
272+
->merge(OpenAPIMetadata::format('int64'))
273+
->merge(OpenAPIMetadata::example(123))
274+
->merge(OpenAPIMetadata::readOnly())
275+
),
276+
str_schema('name', false,
277+
OpenAPIMetadata::description('Product name')
278+
->merge(OpenAPIMetadata::title('Name'))
279+
->merge(OpenAPIMetadata::example('iPhone 15'))
280+
),
281+
str_schema('description', true,
282+
OpenAPIMetadata::description('Detailed product description')
283+
->merge(OpenAPIMetadata::example('Latest smartphone with advanced features'))
284+
),
123285
structure_schema('category', type_structure([
124286
'id' => type_string(),
125287
'name' => type_string()
126-
]), false),
127-
list_schema('tags', type_list(type_string()), true)
288+
]), false,
289+
OpenAPIMetadata::description('Product category information')
290+
),
291+
list_schema('tags', type_list(type_string()), true,
292+
OpenAPIMetadata::description('Product tags for categorization')
293+
->merge(OpenAPIMetadata::examples([
294+
'electronics' => 'electronics',
295+
'smartphone' => 'smartphone',
296+
'apple' => 'apple'
297+
]))
298+
)
128299
);
129300

130301
$apiSpec = schema_to_openapi_specification($productSchema);
@@ -157,15 +328,4 @@ $fullApiSpec = [
157328
]
158329
]
159330
];
160-
```
161-
162-
## Supported OpenAPI Features
163-
164-
### ✅ Supported
165-
- Basic types (boolean, integer, number, string)
166-
- String formats (date, date-time, time, uuid, json, xml)
167-
- Arrays with typed items
168-
- Objects with properties and additionalProperties
169-
- Nullable types
170-
- Descriptions and examples
171-
- Nested structures
331+
```

src/bridge/openapi/specification/src/Flow/Bridge/OpenAPI/Specification/OpenAPIConverter.php

Lines changed: 42 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -120,11 +120,51 @@ private function convertDefinitionToOpenAPI(Definition $definition) : array
120120
$property = $this->convertTypeToOpenAPI($definition->type());
121121
$property['nullable'] = $definition->isNullable();
122122

123-
if ($definition->metadata()->has('description')) {
123+
if ($definition->metadata()->has(OpenAPIMetadata::DESCRIPTION->value)) {
124+
$property['description'] = $definition->metadata()->get(OpenAPIMetadata::DESCRIPTION->value);
125+
}
126+
127+
if ($definition->metadata()->has(OpenAPIMetadata::FORMAT->value)) {
128+
$property['format'] = $definition->metadata()->get(OpenAPIMetadata::FORMAT->value);
129+
}
130+
131+
if ($definition->metadata()->has(OpenAPIMetadata::EXAMPLE->value)) {
132+
$property['example'] = $definition->metadata()->get(OpenAPIMetadata::EXAMPLE->value);
133+
}
134+
135+
if ($definition->metadata()->has(OpenAPIMetadata::EXAMPLES->value)) {
136+
$property['examples'] = $definition->metadata()->get(OpenAPIMetadata::EXAMPLES->value);
137+
}
138+
139+
if ($definition->metadata()->has(OpenAPIMetadata::DEPRECATED->value)) {
140+
$property['deprecated'] = $definition->metadata()->get(OpenAPIMetadata::DEPRECATED->value);
141+
}
142+
143+
if ($definition->metadata()->has(OpenAPIMetadata::TITLE->value)) {
144+
$property['title'] = $definition->metadata()->get(OpenAPIMetadata::TITLE->value);
145+
}
146+
147+
if ($definition->metadata()->has(OpenAPIMetadata::DEFAULT->value)) {
148+
$property['default'] = $definition->metadata()->get(OpenAPIMetadata::DEFAULT->value);
149+
}
150+
151+
if ($definition->metadata()->has(OpenAPIMetadata::READ_ONLY->value)) {
152+
$property['readOnly'] = $definition->metadata()->get(OpenAPIMetadata::READ_ONLY->value);
153+
}
154+
155+
if ($definition->metadata()->has(OpenAPIMetadata::WRITE_ONLY->value)) {
156+
$property['writeOnly'] = $definition->metadata()->get(OpenAPIMetadata::WRITE_ONLY->value);
157+
}
158+
159+
if ($definition->metadata()->has(OpenAPIMetadata::NULLABLE->value)) {
160+
$property['nullable'] = $definition->metadata()->get(OpenAPIMetadata::NULLABLE->value);
161+
}
162+
163+
if (!isset($property['description']) && $definition->metadata()->has('description')) {
124164
$property['description'] = $definition->metadata()->get('description');
125165
}
126166

127-
if ($definition->metadata()->has('example')) {
167+
if (!isset($property['example']) && $definition->metadata()->has('example')) {
128168
$property['example'] = $definition->metadata()->get('example');
129169
}
130170

@@ -238,7 +278,6 @@ private function convertOpenAPIObjectToFlowType(array $typeSpec) : Type
238278
return type_map(type_string(), $valueType);
239279
}
240280

241-
// If it has properties, it's a structure
242281
if (isset($typeSpec['properties']) && \is_array($typeSpec['properties'])) {
243282
$elements = [];
244283
$optionalElements = [];
@@ -262,7 +301,6 @@ private function convertOpenAPIObjectToFlowType(array $typeSpec) : Type
262301
return type_structure($elements, $optionalElements);
263302
}
264303

265-
// Default to empty map
266304
return type_map(type_string(), type_string());
267305
}
268306

0 commit comments

Comments
 (0)