string-schema 0.1.2__py3-none-any.whl → 0.1.4__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.
@@ -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)