string-schema 0.1.2__py3-none-any.whl → 0.1.3__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- string_schema/core/__init__.py +23 -0
- string_schema/core/builders.py +244 -0
- string_schema/core/fields.py +138 -0
- string_schema/core/validators.py +242 -0
- string_schema/examples/__init__.py +36 -0
- string_schema/examples/presets.py +345 -0
- string_schema/examples/recipes.py +380 -0
- string_schema/integrations/__init__.py +15 -0
- string_schema/integrations/json_schema.py +385 -0
- string_schema/integrations/openapi.py +484 -0
- string_schema/integrations/pydantic.py +662 -0
- string_schema/integrations/reverse.py +275 -0
- string_schema/parsing/__init__.py +16 -0
- string_schema/parsing/optimizer.py +246 -0
- string_schema/parsing/string_parser.py +703 -0
- string_schema/parsing/syntax.py +250 -0
- {string_schema-0.1.2.dist-info → string_schema-0.1.3.dist-info}/METADATA +1 -1
- string_schema-0.1.3.dist-info/RECORD +24 -0
- string_schema-0.1.2.dist-info/RECORD +0 -8
- {string_schema-0.1.2.dist-info → string_schema-0.1.3.dist-info}/WHEEL +0 -0
- {string_schema-0.1.2.dist-info → string_schema-0.1.3.dist-info}/licenses/LICENSE +0 -0
- {string_schema-0.1.2.dist-info → string_schema-0.1.3.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,484 @@
|
|
|
1
|
+
"""
|
|
2
|
+
OpenAPI integration for Simple Schema
|
|
3
|
+
|
|
4
|
+
Contains functions for creating OpenAPI schema definitions from Simple Schema.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from typing import Any, Dict, List, Union, Optional
|
|
8
|
+
import logging
|
|
9
|
+
|
|
10
|
+
from ..core.fields import SimpleField
|
|
11
|
+
from .json_schema import to_json_schema, convert_to_openapi_schema
|
|
12
|
+
|
|
13
|
+
logger = logging.getLogger(__name__)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def to_openapi_schema(fields: Dict[str, Union[str, SimpleField]],
|
|
17
|
+
title: str = "Generated Schema",
|
|
18
|
+
description: str = "",
|
|
19
|
+
version: str = "1.0.0") -> Dict[str, Any]:
|
|
20
|
+
"""
|
|
21
|
+
Convert Simple Schema fields to OpenAPI 3.0 schema format.
|
|
22
|
+
|
|
23
|
+
Args:
|
|
24
|
+
fields: Dictionary of field definitions
|
|
25
|
+
title: Schema title
|
|
26
|
+
description: Schema description
|
|
27
|
+
version: Schema version
|
|
28
|
+
|
|
29
|
+
Returns:
|
|
30
|
+
OpenAPI 3.0 compatible schema
|
|
31
|
+
"""
|
|
32
|
+
# First convert to JSON Schema
|
|
33
|
+
json_schema = to_json_schema(fields, title, description)
|
|
34
|
+
|
|
35
|
+
# Then convert to OpenAPI format
|
|
36
|
+
openapi_schema = convert_to_openapi_schema(json_schema)
|
|
37
|
+
|
|
38
|
+
# Add OpenAPI specific metadata
|
|
39
|
+
if version:
|
|
40
|
+
openapi_schema["version"] = version
|
|
41
|
+
|
|
42
|
+
return openapi_schema
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def create_openapi_component(name: str,
|
|
46
|
+
fields: Dict[str, Union[str, SimpleField]],
|
|
47
|
+
description: str = "") -> Dict[str, Any]:
|
|
48
|
+
"""
|
|
49
|
+
Create an OpenAPI component schema.
|
|
50
|
+
|
|
51
|
+
Args:
|
|
52
|
+
name: Component name
|
|
53
|
+
fields: Dictionary of field definitions
|
|
54
|
+
description: Component description
|
|
55
|
+
|
|
56
|
+
Returns:
|
|
57
|
+
OpenAPI component definition
|
|
58
|
+
"""
|
|
59
|
+
schema = to_openapi_schema(fields, name, description)
|
|
60
|
+
|
|
61
|
+
return {
|
|
62
|
+
name: schema
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def create_openapi_request_body(fields: Dict[str, Union[str, SimpleField]],
|
|
67
|
+
description: str = "Request body",
|
|
68
|
+
content_type: str = "application/json",
|
|
69
|
+
required: bool = True) -> Dict[str, Any]:
|
|
70
|
+
"""
|
|
71
|
+
Create OpenAPI request body definition.
|
|
72
|
+
|
|
73
|
+
Args:
|
|
74
|
+
fields: Dictionary of field definitions
|
|
75
|
+
description: Request body description
|
|
76
|
+
content_type: Content type (default: application/json)
|
|
77
|
+
required: Whether request body is required
|
|
78
|
+
|
|
79
|
+
Returns:
|
|
80
|
+
OpenAPI request body definition
|
|
81
|
+
"""
|
|
82
|
+
schema = to_openapi_schema(fields)
|
|
83
|
+
|
|
84
|
+
return {
|
|
85
|
+
"description": description,
|
|
86
|
+
"required": required,
|
|
87
|
+
"content": {
|
|
88
|
+
content_type: {
|
|
89
|
+
"schema": schema
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def string_to_openapi(schema_str: str, title: str = "Generated Schema",
|
|
96
|
+
description: str = "", version: str = "1.0.0") -> Dict[str, Any]:
|
|
97
|
+
"""
|
|
98
|
+
Create OpenAPI schema directly from string syntax.
|
|
99
|
+
|
|
100
|
+
Args:
|
|
101
|
+
schema_str: String schema definition (e.g., "name:string, email:email")
|
|
102
|
+
title: Schema title
|
|
103
|
+
description: Schema description
|
|
104
|
+
version: Schema version
|
|
105
|
+
|
|
106
|
+
Returns:
|
|
107
|
+
OpenAPI 3.0 compatible schema dictionary
|
|
108
|
+
|
|
109
|
+
Example:
|
|
110
|
+
openapi_schema = string_to_openapi("name:string, email:email", title="User Schema")
|
|
111
|
+
"""
|
|
112
|
+
# Import here to avoid circular imports
|
|
113
|
+
from ..parsing.string_parser import parse_string_schema
|
|
114
|
+
from .json_schema import convert_to_openapi_schema
|
|
115
|
+
|
|
116
|
+
# Convert string to JSON Schema, then to OpenAPI
|
|
117
|
+
json_schema = parse_string_schema(schema_str)
|
|
118
|
+
return convert_to_openapi_schema(json_schema)
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
# Reverse conversion functions
|
|
122
|
+
def openapi_to_string(openapi_schema: Dict[str, Any]) -> str:
|
|
123
|
+
"""
|
|
124
|
+
Convert OpenAPI schema to Simple Schema string syntax.
|
|
125
|
+
|
|
126
|
+
Args:
|
|
127
|
+
openapi_schema: OpenAPI schema dictionary
|
|
128
|
+
|
|
129
|
+
Returns:
|
|
130
|
+
String representation in Simple Schema syntax
|
|
131
|
+
|
|
132
|
+
Example:
|
|
133
|
+
openapi_schema = {"type": "object", "properties": {"name": {"type": "string"}}}
|
|
134
|
+
schema_str = openapi_to_string(openapi_schema)
|
|
135
|
+
# Returns: "name:string"
|
|
136
|
+
"""
|
|
137
|
+
from .reverse import openapi_to_string as _openapi_to_string
|
|
138
|
+
return _openapi_to_string(openapi_schema)
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def openapi_to_json_schema(openapi_schema: Dict[str, Any]) -> Dict[str, Any]:
|
|
142
|
+
"""
|
|
143
|
+
Convert OpenAPI schema to JSON Schema.
|
|
144
|
+
|
|
145
|
+
Args:
|
|
146
|
+
openapi_schema: OpenAPI schema dictionary
|
|
147
|
+
|
|
148
|
+
Returns:
|
|
149
|
+
JSON Schema dictionary
|
|
150
|
+
|
|
151
|
+
Example:
|
|
152
|
+
openapi_schema = {"type": "string", "format": "email"}
|
|
153
|
+
json_schema = openapi_to_json_schema(openapi_schema)
|
|
154
|
+
"""
|
|
155
|
+
from .reverse import openapi_to_json_schema as _openapi_to_json_schema
|
|
156
|
+
return _openapi_to_json_schema(openapi_schema)
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def create_openapi_response(fields: Dict[str, Union[str, SimpleField]],
|
|
160
|
+
description: str = "Successful response",
|
|
161
|
+
status_code: str = "200",
|
|
162
|
+
content_type: str = "application/json") -> Dict[str, Any]:
|
|
163
|
+
"""
|
|
164
|
+
Create OpenAPI response definition.
|
|
165
|
+
|
|
166
|
+
Args:
|
|
167
|
+
fields: Dictionary of field definitions
|
|
168
|
+
description: Response description
|
|
169
|
+
status_code: HTTP status code
|
|
170
|
+
content_type: Content type (default: application/json)
|
|
171
|
+
|
|
172
|
+
Returns:
|
|
173
|
+
OpenAPI response definition
|
|
174
|
+
"""
|
|
175
|
+
schema = to_openapi_schema(fields)
|
|
176
|
+
|
|
177
|
+
return {
|
|
178
|
+
status_code: {
|
|
179
|
+
"description": description,
|
|
180
|
+
"content": {
|
|
181
|
+
content_type: {
|
|
182
|
+
"schema": schema
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def create_openapi_parameter(field_name: str,
|
|
190
|
+
field: Union[str, SimpleField],
|
|
191
|
+
location: str = "query",
|
|
192
|
+
description: str = "",
|
|
193
|
+
required: bool = False) -> Dict[str, Any]:
|
|
194
|
+
"""
|
|
195
|
+
Create OpenAPI parameter definition.
|
|
196
|
+
|
|
197
|
+
Args:
|
|
198
|
+
field_name: Parameter name
|
|
199
|
+
field: Field definition
|
|
200
|
+
location: Parameter location (query, path, header, cookie)
|
|
201
|
+
description: Parameter description
|
|
202
|
+
required: Whether parameter is required
|
|
203
|
+
|
|
204
|
+
Returns:
|
|
205
|
+
OpenAPI parameter definition
|
|
206
|
+
"""
|
|
207
|
+
if isinstance(field, str):
|
|
208
|
+
field = SimpleField(field)
|
|
209
|
+
|
|
210
|
+
# Convert field to basic schema
|
|
211
|
+
schema = {
|
|
212
|
+
"type": field.field_type
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
if field.format_hint:
|
|
216
|
+
if field.format_hint == "email":
|
|
217
|
+
schema["format"] = "email"
|
|
218
|
+
elif field.format_hint in ["url", "uri"]:
|
|
219
|
+
schema["format"] = "uri"
|
|
220
|
+
elif field.format_hint == "datetime":
|
|
221
|
+
schema["format"] = "date-time"
|
|
222
|
+
elif field.format_hint == "date":
|
|
223
|
+
schema["format"] = "date"
|
|
224
|
+
elif field.format_hint == "uuid":
|
|
225
|
+
schema["format"] = "uuid"
|
|
226
|
+
|
|
227
|
+
if field.choices:
|
|
228
|
+
schema["enum"] = field.choices
|
|
229
|
+
|
|
230
|
+
# Add constraints
|
|
231
|
+
if field.min_val is not None:
|
|
232
|
+
schema["minimum"] = field.min_val
|
|
233
|
+
if field.max_val is not None:
|
|
234
|
+
schema["maximum"] = field.max_val
|
|
235
|
+
if field.min_length is not None:
|
|
236
|
+
schema["minLength"] = field.min_length
|
|
237
|
+
if field.max_length is not None:
|
|
238
|
+
schema["maxLength"] = field.max_length
|
|
239
|
+
|
|
240
|
+
parameter = {
|
|
241
|
+
"name": field_name,
|
|
242
|
+
"in": location,
|
|
243
|
+
"required": required or field.required,
|
|
244
|
+
"schema": schema
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
if description or field.description:
|
|
248
|
+
parameter["description"] = description or field.description
|
|
249
|
+
|
|
250
|
+
return parameter
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
def create_openapi_path_item(method: str,
|
|
254
|
+
summary: str = "",
|
|
255
|
+
description: str = "",
|
|
256
|
+
request_fields: Optional[Dict[str, Union[str, SimpleField]]] = None,
|
|
257
|
+
response_fields: Optional[Dict[str, Union[str, SimpleField]]] = None,
|
|
258
|
+
parameters: Optional[List[Dict[str, Any]]] = None,
|
|
259
|
+
tags: Optional[List[str]] = None) -> Dict[str, Any]:
|
|
260
|
+
"""
|
|
261
|
+
Create OpenAPI path item definition.
|
|
262
|
+
|
|
263
|
+
Args:
|
|
264
|
+
method: HTTP method (get, post, put, delete, etc.)
|
|
265
|
+
summary: Operation summary
|
|
266
|
+
description: Operation description
|
|
267
|
+
request_fields: Request body field definitions
|
|
268
|
+
response_fields: Response field definitions
|
|
269
|
+
parameters: List of parameter definitions
|
|
270
|
+
tags: List of operation tags
|
|
271
|
+
|
|
272
|
+
Returns:
|
|
273
|
+
OpenAPI path item definition
|
|
274
|
+
"""
|
|
275
|
+
operation = {}
|
|
276
|
+
|
|
277
|
+
if summary:
|
|
278
|
+
operation["summary"] = summary
|
|
279
|
+
if description:
|
|
280
|
+
operation["description"] = description
|
|
281
|
+
if tags:
|
|
282
|
+
operation["tags"] = tags
|
|
283
|
+
|
|
284
|
+
# Add parameters
|
|
285
|
+
if parameters:
|
|
286
|
+
operation["parameters"] = parameters
|
|
287
|
+
|
|
288
|
+
# Add request body
|
|
289
|
+
if request_fields and method.lower() in ["post", "put", "patch"]:
|
|
290
|
+
operation["requestBody"] = create_openapi_request_body(request_fields)
|
|
291
|
+
|
|
292
|
+
# Add responses
|
|
293
|
+
responses = {}
|
|
294
|
+
if response_fields:
|
|
295
|
+
responses.update(create_openapi_response(response_fields))
|
|
296
|
+
else:
|
|
297
|
+
# Default response
|
|
298
|
+
responses["200"] = {
|
|
299
|
+
"description": "Successful operation"
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
operation["responses"] = responses
|
|
303
|
+
|
|
304
|
+
return {method.lower(): operation}
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
def generate_openapi_spec(title: str,
|
|
308
|
+
version: str = "1.0.0",
|
|
309
|
+
description: str = "",
|
|
310
|
+
paths: Optional[Dict[str, Any]] = None,
|
|
311
|
+
components: Optional[Dict[str, Any]] = None,
|
|
312
|
+
servers: Optional[List[Dict[str, str]]] = None) -> Dict[str, Any]:
|
|
313
|
+
"""
|
|
314
|
+
Generate complete OpenAPI specification.
|
|
315
|
+
|
|
316
|
+
Args:
|
|
317
|
+
title: API title
|
|
318
|
+
version: API version
|
|
319
|
+
description: API description
|
|
320
|
+
paths: Path definitions
|
|
321
|
+
components: Component definitions
|
|
322
|
+
servers: Server definitions
|
|
323
|
+
|
|
324
|
+
Returns:
|
|
325
|
+
Complete OpenAPI specification
|
|
326
|
+
"""
|
|
327
|
+
spec = {
|
|
328
|
+
"openapi": "3.0.3",
|
|
329
|
+
"info": {
|
|
330
|
+
"title": title,
|
|
331
|
+
"version": version
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
if description:
|
|
336
|
+
spec["info"]["description"] = description
|
|
337
|
+
|
|
338
|
+
if servers:
|
|
339
|
+
spec["servers"] = servers
|
|
340
|
+
else:
|
|
341
|
+
spec["servers"] = [{"url": "https://api.example.com"}]
|
|
342
|
+
|
|
343
|
+
if paths:
|
|
344
|
+
spec["paths"] = paths
|
|
345
|
+
else:
|
|
346
|
+
spec["paths"] = {}
|
|
347
|
+
|
|
348
|
+
if components:
|
|
349
|
+
spec["components"] = components
|
|
350
|
+
|
|
351
|
+
return spec
|
|
352
|
+
|
|
353
|
+
|
|
354
|
+
def validate_openapi_compatibility(fields: Dict[str, SimpleField]) -> Dict[str, Any]:
|
|
355
|
+
"""
|
|
356
|
+
Validate that Simple Schema fields are compatible with OpenAPI.
|
|
357
|
+
|
|
358
|
+
Args:
|
|
359
|
+
fields: Dictionary of SimpleField objects
|
|
360
|
+
|
|
361
|
+
Returns:
|
|
362
|
+
Validation result dictionary
|
|
363
|
+
"""
|
|
364
|
+
result = {
|
|
365
|
+
'compatible': True,
|
|
366
|
+
'warnings': [],
|
|
367
|
+
'errors': [],
|
|
368
|
+
'unsupported_features': []
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
for field_name, field in fields.items():
|
|
372
|
+
# Check for unsupported union types
|
|
373
|
+
if field.union_types and len(field.union_types) > 1:
|
|
374
|
+
# OpenAPI 3.0 supports oneOf/anyOf but it's more complex
|
|
375
|
+
result['warnings'].append(f"Field '{field_name}' uses union types - consider using oneOf/anyOf")
|
|
376
|
+
|
|
377
|
+
# Check for unsupported format hints
|
|
378
|
+
unsupported_formats = ['phone'] # OpenAPI doesn't have standard phone format
|
|
379
|
+
if field.format_hint in unsupported_formats:
|
|
380
|
+
result['warnings'].append(f"Field '{field_name}' format '{field.format_hint}' not standard in OpenAPI")
|
|
381
|
+
|
|
382
|
+
# Check for complex constraints
|
|
383
|
+
if field.min_items is not None or field.max_items is not None:
|
|
384
|
+
if field.field_type != 'array':
|
|
385
|
+
result['warnings'].append(f"Field '{field_name}' has array constraints but is not array type")
|
|
386
|
+
|
|
387
|
+
return result
|
|
388
|
+
|
|
389
|
+
|
|
390
|
+
def generate_openapi_documentation(spec: Dict[str, Any]) -> str:
|
|
391
|
+
"""
|
|
392
|
+
Generate human-readable documentation from OpenAPI spec.
|
|
393
|
+
|
|
394
|
+
Args:
|
|
395
|
+
spec: OpenAPI specification
|
|
396
|
+
|
|
397
|
+
Returns:
|
|
398
|
+
Markdown documentation string
|
|
399
|
+
"""
|
|
400
|
+
lines = []
|
|
401
|
+
|
|
402
|
+
# API info
|
|
403
|
+
info = spec.get('info', {})
|
|
404
|
+
title = info.get('title', 'API Documentation')
|
|
405
|
+
lines.append(f"# {title}")
|
|
406
|
+
|
|
407
|
+
version = info.get('version', 'Unknown')
|
|
408
|
+
lines.append(f"**Version:** {version}")
|
|
409
|
+
|
|
410
|
+
if 'description' in info:
|
|
411
|
+
lines.append(f"\n{info['description']}")
|
|
412
|
+
|
|
413
|
+
# Servers
|
|
414
|
+
servers = spec.get('servers', [])
|
|
415
|
+
if servers:
|
|
416
|
+
lines.append("\n## Servers")
|
|
417
|
+
for server in servers:
|
|
418
|
+
url = server.get('url', '')
|
|
419
|
+
description = server.get('description', '')
|
|
420
|
+
if description:
|
|
421
|
+
lines.append(f"- {url} - {description}")
|
|
422
|
+
else:
|
|
423
|
+
lines.append(f"- {url}")
|
|
424
|
+
|
|
425
|
+
# Paths
|
|
426
|
+
paths = spec.get('paths', {})
|
|
427
|
+
if paths:
|
|
428
|
+
lines.append("\n## Endpoints")
|
|
429
|
+
|
|
430
|
+
for path, path_item in paths.items():
|
|
431
|
+
lines.append(f"\n### {path}")
|
|
432
|
+
|
|
433
|
+
for method, operation in path_item.items():
|
|
434
|
+
if method.lower() in ['get', 'post', 'put', 'delete', 'patch', 'head', 'options']:
|
|
435
|
+
lines.append(f"\n#### {method.upper()}")
|
|
436
|
+
|
|
437
|
+
if 'summary' in operation:
|
|
438
|
+
lines.append(f"**Summary:** {operation['summary']}")
|
|
439
|
+
|
|
440
|
+
if 'description' in operation:
|
|
441
|
+
lines.append(f"**Description:** {operation['description']}")
|
|
442
|
+
|
|
443
|
+
# Parameters
|
|
444
|
+
if 'parameters' in operation:
|
|
445
|
+
lines.append("**Parameters:**")
|
|
446
|
+
for param in operation['parameters']:
|
|
447
|
+
param_name = param.get('name', '')
|
|
448
|
+
param_type = param.get('schema', {}).get('type', 'unknown')
|
|
449
|
+
param_required = param.get('required', False)
|
|
450
|
+
param_desc = param.get('description', '')
|
|
451
|
+
|
|
452
|
+
required_text = " (required)" if param_required else " (optional)"
|
|
453
|
+
lines.append(f"- `{param_name}` ({param_type}){required_text}: {param_desc}")
|
|
454
|
+
|
|
455
|
+
# Responses
|
|
456
|
+
if 'responses' in operation:
|
|
457
|
+
lines.append("**Responses:**")
|
|
458
|
+
for status_code, response in operation['responses'].items():
|
|
459
|
+
response_desc = response.get('description', '')
|
|
460
|
+
lines.append(f"- `{status_code}`: {response_desc}")
|
|
461
|
+
|
|
462
|
+
# Components
|
|
463
|
+
components = spec.get('components', {})
|
|
464
|
+
if 'schemas' in components:
|
|
465
|
+
lines.append("\n## Schemas")
|
|
466
|
+
|
|
467
|
+
for schema_name, schema_def in components['schemas'].items():
|
|
468
|
+
lines.append(f"\n### {schema_name}")
|
|
469
|
+
|
|
470
|
+
if 'description' in schema_def:
|
|
471
|
+
lines.append(schema_def['description'])
|
|
472
|
+
|
|
473
|
+
if 'properties' in schema_def:
|
|
474
|
+
lines.append("**Properties:**")
|
|
475
|
+
required_fields = set(schema_def.get('required', []))
|
|
476
|
+
|
|
477
|
+
for prop_name, prop_schema in schema_def['properties'].items():
|
|
478
|
+
prop_type = prop_schema.get('type', 'unknown')
|
|
479
|
+
required_text = " (required)" if prop_name in required_fields else " (optional)"
|
|
480
|
+
prop_desc = prop_schema.get('description', '')
|
|
481
|
+
|
|
482
|
+
lines.append(f"- `{prop_name}` ({prop_type}){required_text}: {prop_desc}")
|
|
483
|
+
|
|
484
|
+
return '\n'.join(lines)
|