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.
- 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/utilities.py +3 -3
- {string_schema-0.1.2.dist-info → string_schema-0.1.4.dist-info}/METADATA +1 -1
- string_schema-0.1.4.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.4.dist-info}/WHEEL +0 -0
- {string_schema-0.1.2.dist-info → string_schema-0.1.4.dist-info}/licenses/LICENSE +0 -0
- {string_schema-0.1.2.dist-info → string_schema-0.1.4.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,703 @@
|
|
|
1
|
+
"""
|
|
2
|
+
String-based schema parsing for String Schema
|
|
3
|
+
|
|
4
|
+
Contains functionality for parsing human-readable schema strings into JSON Schema.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import re
|
|
8
|
+
from typing import Dict, Any, List, Union, Optional
|
|
9
|
+
import logging
|
|
10
|
+
|
|
11
|
+
from ..core.fields import SimpleField
|
|
12
|
+
from ..core.builders import simple_schema, list_of_objects_schema
|
|
13
|
+
|
|
14
|
+
logger = logging.getLogger(__name__)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def parse_string_schema(schema_str: str, is_list: bool = False) -> Dict[str, Any]:
|
|
18
|
+
"""Parse enhanced string schema with support for arrays, enums, unions, and special types"""
|
|
19
|
+
schema_str = schema_str.strip()
|
|
20
|
+
parsed_structure = _parse_schema_structure(schema_str)
|
|
21
|
+
return _structure_to_json_schema(parsed_structure)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _parse_schema_structure(schema_str: str) -> Dict[str, Any]:
|
|
25
|
+
"""Parse the overall structure of the schema with enhanced syntax support"""
|
|
26
|
+
schema_str = schema_str.strip()
|
|
27
|
+
|
|
28
|
+
# Handle curly brace syntax: {field1, field2}
|
|
29
|
+
if schema_str.startswith('{') and schema_str.endswith('}'):
|
|
30
|
+
inner_content = schema_str[1:-1].strip()
|
|
31
|
+
fields = _parse_object_fields(inner_content)
|
|
32
|
+
return {
|
|
33
|
+
"type": "object",
|
|
34
|
+
"fields": fields
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
# Handle array syntax: [type] or [{field1, field2}] with optional constraints
|
|
38
|
+
elif schema_str.startswith('['):
|
|
39
|
+
# Check for array constraints like [string](max=5)
|
|
40
|
+
array_constraints = {}
|
|
41
|
+
|
|
42
|
+
# Extract constraints if present: [type](constraints)
|
|
43
|
+
constraint_match = re.match(r'^\[([^\]]+)\]\(([^)]+)\)$', schema_str)
|
|
44
|
+
if constraint_match:
|
|
45
|
+
inner_content = constraint_match.group(1).strip()
|
|
46
|
+
constraint_str = constraint_match.group(2).strip()
|
|
47
|
+
array_constraints = _parse_array_constraints(constraint_str)
|
|
48
|
+
elif schema_str.endswith(']'):
|
|
49
|
+
# Simple array without constraints
|
|
50
|
+
inner_content = schema_str[1:-1].strip()
|
|
51
|
+
else:
|
|
52
|
+
# Malformed array syntax
|
|
53
|
+
return {"type": "object", "fields": {}} # Fallback
|
|
54
|
+
|
|
55
|
+
# Array of objects: [{field1, field2}]
|
|
56
|
+
if inner_content.startswith('{') and inner_content.endswith('}'):
|
|
57
|
+
object_fields = _parse_object_fields(inner_content[1:-1].strip())
|
|
58
|
+
return {
|
|
59
|
+
"type": "array",
|
|
60
|
+
"items": {
|
|
61
|
+
"type": "object",
|
|
62
|
+
"fields": object_fields
|
|
63
|
+
},
|
|
64
|
+
"constraints": array_constraints
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
# Array of simple types: [string], [int], [email], etc.
|
|
68
|
+
elif _is_simple_type(inner_content):
|
|
69
|
+
original_type = inner_content.strip().lower()
|
|
70
|
+
normalized_type = _normalize_type_name(original_type)
|
|
71
|
+
return {
|
|
72
|
+
"type": "array",
|
|
73
|
+
"items": {
|
|
74
|
+
"type": "simple",
|
|
75
|
+
"simple_type": normalized_type,
|
|
76
|
+
"original_type": original_type # Keep original for format hints
|
|
77
|
+
},
|
|
78
|
+
"constraints": array_constraints
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
# Array of complex fields: [name:string, age:int]
|
|
82
|
+
else:
|
|
83
|
+
fields = _parse_object_fields(inner_content)
|
|
84
|
+
return {
|
|
85
|
+
"type": "array",
|
|
86
|
+
"items": {
|
|
87
|
+
"type": "object",
|
|
88
|
+
"fields": fields
|
|
89
|
+
},
|
|
90
|
+
"constraints": array_constraints
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
# Simple object fields: name, age:int, email
|
|
94
|
+
else:
|
|
95
|
+
fields = _parse_object_fields(schema_str)
|
|
96
|
+
return {
|
|
97
|
+
"type": "object",
|
|
98
|
+
"fields": fields
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def _parse_array_constraints(constraint_str: str) -> Dict[str, Any]:
|
|
103
|
+
"""Parse array constraints like 'min=1,max=5'"""
|
|
104
|
+
constraints = {}
|
|
105
|
+
parts = [part.strip() for part in constraint_str.split(',')]
|
|
106
|
+
|
|
107
|
+
for part in parts:
|
|
108
|
+
if '=' in part:
|
|
109
|
+
key, value = part.split('=', 1)
|
|
110
|
+
key = key.strip()
|
|
111
|
+
value = value.strip()
|
|
112
|
+
|
|
113
|
+
try:
|
|
114
|
+
if key in ['min', 'max']:
|
|
115
|
+
constraints[key] = int(value)
|
|
116
|
+
else:
|
|
117
|
+
constraints[key] = value
|
|
118
|
+
except ValueError:
|
|
119
|
+
logger.warning(f"Invalid array constraint: {part}")
|
|
120
|
+
|
|
121
|
+
return constraints
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def _parse_object_fields(fields_str: str) -> Dict[str, Any]:
|
|
125
|
+
"""Parse object fields with enhanced syntax"""
|
|
126
|
+
fields_str = _normalize_string_schema(fields_str)
|
|
127
|
+
field_parts = _split_field_definitions_with_nesting(fields_str)
|
|
128
|
+
|
|
129
|
+
fields = {}
|
|
130
|
+
for field_part in field_parts:
|
|
131
|
+
field_name, field_def = _parse_single_field_with_nesting(field_part.strip())
|
|
132
|
+
if field_name:
|
|
133
|
+
fields[field_name] = field_def
|
|
134
|
+
|
|
135
|
+
return fields
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def _parse_single_field_with_nesting(field_str: str) -> tuple:
|
|
139
|
+
"""Parse a single field with enhanced syntax support"""
|
|
140
|
+
if not field_str:
|
|
141
|
+
return None, None
|
|
142
|
+
|
|
143
|
+
# Check for optional marker
|
|
144
|
+
required = True
|
|
145
|
+
if field_str.endswith('?'):
|
|
146
|
+
required = False
|
|
147
|
+
field_str = field_str[:-1].strip()
|
|
148
|
+
|
|
149
|
+
# Split field name and definition
|
|
150
|
+
if ':' in field_str:
|
|
151
|
+
field_name, field_def = field_str.split(':', 1)
|
|
152
|
+
field_name = field_name.strip()
|
|
153
|
+
field_def = field_def.strip()
|
|
154
|
+
|
|
155
|
+
# Handle nested structures
|
|
156
|
+
if field_def.startswith('[') or field_def.startswith('{'):
|
|
157
|
+
nested_structure = _parse_schema_structure(field_def)
|
|
158
|
+
nested_structure['required'] = required
|
|
159
|
+
return field_name, nested_structure
|
|
160
|
+
|
|
161
|
+
# Handle union types: string|int|null
|
|
162
|
+
elif '|' in field_def:
|
|
163
|
+
union_types = [t.strip() for t in field_def.split('|')]
|
|
164
|
+
field_type = _normalize_type_name(union_types[0]) # Use first type as primary
|
|
165
|
+
|
|
166
|
+
# Create field with union support
|
|
167
|
+
field_obj = SimpleField(
|
|
168
|
+
field_type=field_type,
|
|
169
|
+
required=required
|
|
170
|
+
)
|
|
171
|
+
# Store union info for JSON schema generation
|
|
172
|
+
field_obj.union_types = [_normalize_type_name(t) for t in union_types]
|
|
173
|
+
return field_name, field_obj
|
|
174
|
+
|
|
175
|
+
# Handle enum types: enum(value1,value2,value3) or choice(...)
|
|
176
|
+
elif field_def.startswith(('enum(', 'choice(', 'select(')):
|
|
177
|
+
enum_values = _parse_enum_values(field_def)
|
|
178
|
+
field_obj = SimpleField(
|
|
179
|
+
field_type="string", # Enums are string-based
|
|
180
|
+
required=required,
|
|
181
|
+
choices=enum_values
|
|
182
|
+
)
|
|
183
|
+
return field_name, field_obj
|
|
184
|
+
|
|
185
|
+
# Handle array types: array(string,max=5) or list(int,min=1)
|
|
186
|
+
elif field_def.startswith(('array(', 'list(')):
|
|
187
|
+
array_type, constraints = _parse_array_type_definition(field_def)
|
|
188
|
+
# Return as nested array structure
|
|
189
|
+
return field_name, {
|
|
190
|
+
"type": "array",
|
|
191
|
+
"items": {
|
|
192
|
+
"type": "simple",
|
|
193
|
+
"simple_type": array_type
|
|
194
|
+
},
|
|
195
|
+
"constraints": constraints,
|
|
196
|
+
"required": required
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
# Handle regular type with constraints
|
|
200
|
+
else:
|
|
201
|
+
original_type = field_def.split('(')[0].strip() # Get type before any constraints
|
|
202
|
+
field_type, constraints = _parse_type_definition(field_def)
|
|
203
|
+
|
|
204
|
+
# Add format hint for special types
|
|
205
|
+
if original_type in ['email', 'url', 'uri', 'datetime', 'date', 'uuid', 'phone']:
|
|
206
|
+
constraints['format_hint'] = original_type
|
|
207
|
+
field_type = 'string' # All special types are strings with format hints
|
|
208
|
+
|
|
209
|
+
field_obj = SimpleField(
|
|
210
|
+
field_type=field_type,
|
|
211
|
+
required=required,
|
|
212
|
+
**constraints
|
|
213
|
+
)
|
|
214
|
+
return field_name, field_obj
|
|
215
|
+
|
|
216
|
+
# Field name only (default to string)
|
|
217
|
+
else:
|
|
218
|
+
field_name = field_str.strip()
|
|
219
|
+
field_obj = SimpleField(
|
|
220
|
+
field_type="string",
|
|
221
|
+
required=required
|
|
222
|
+
)
|
|
223
|
+
return field_name, field_obj
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
def _parse_enum_values(enum_def: str) -> List[str]:
|
|
227
|
+
"""Parse enum values from enum(value1,value2,value3)"""
|
|
228
|
+
# Extract content between parentheses
|
|
229
|
+
match = re.match(r'^(?:enum|choice|select)\(([^)]+)\)$', enum_def)
|
|
230
|
+
if not match:
|
|
231
|
+
return []
|
|
232
|
+
|
|
233
|
+
values_str = match.group(1)
|
|
234
|
+
values = [v.strip() for v in values_str.split(',')]
|
|
235
|
+
return values
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
def _parse_array_type_definition(array_def: str) -> tuple:
|
|
239
|
+
"""Parse array(type,constraints) or list(type,constraints)"""
|
|
240
|
+
# Extract content between parentheses
|
|
241
|
+
match = re.match(r'^(?:array|list)\(([^)]+)\)$', array_def)
|
|
242
|
+
if not match:
|
|
243
|
+
return "string", {}
|
|
244
|
+
|
|
245
|
+
content = match.group(1)
|
|
246
|
+
parts = [p.strip() for p in content.split(',')]
|
|
247
|
+
|
|
248
|
+
# First part is the type
|
|
249
|
+
array_type = _normalize_type_name(parts[0]) if parts else "string"
|
|
250
|
+
|
|
251
|
+
# Remaining parts are constraints
|
|
252
|
+
constraints = {}
|
|
253
|
+
for part in parts[1:]:
|
|
254
|
+
if '=' in part:
|
|
255
|
+
key, value = part.split('=', 1)
|
|
256
|
+
key = key.strip()
|
|
257
|
+
value = value.strip()
|
|
258
|
+
|
|
259
|
+
try:
|
|
260
|
+
if key in ['min', 'max']:
|
|
261
|
+
constraints[key] = int(value)
|
|
262
|
+
else:
|
|
263
|
+
constraints[key] = value
|
|
264
|
+
except ValueError:
|
|
265
|
+
logger.warning(f"Invalid array constraint: {part}")
|
|
266
|
+
|
|
267
|
+
return array_type, constraints
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
def _is_simple_type(type_str: str) -> bool:
|
|
271
|
+
"""Check if string represents a simple type"""
|
|
272
|
+
simple_types = [
|
|
273
|
+
'string', 'str', 'text', 'int', 'integer', 'number', 'float', 'decimal',
|
|
274
|
+
'bool', 'boolean', 'email', 'url', 'uri', 'datetime', 'date', 'uuid', 'phone'
|
|
275
|
+
]
|
|
276
|
+
return type_str.strip().lower() in simple_types
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
def _structure_to_json_schema(structure: Dict[str, Any]) -> Dict[str, Any]:
|
|
280
|
+
"""Convert parsed structure to JSON Schema"""
|
|
281
|
+
if structure["type"] == "object":
|
|
282
|
+
return _object_structure_to_schema(structure)
|
|
283
|
+
elif structure["type"] == "array":
|
|
284
|
+
return _array_structure_to_schema(structure)
|
|
285
|
+
else:
|
|
286
|
+
raise ValueError(f"Unknown structure type: {structure['type']}")
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
def _object_structure_to_schema(structure: Dict[str, Any]) -> Dict[str, Any]:
|
|
290
|
+
"""Convert object structure to JSON Schema"""
|
|
291
|
+
properties = {}
|
|
292
|
+
required = []
|
|
293
|
+
|
|
294
|
+
for field_name, field_def in structure["fields"].items():
|
|
295
|
+
if isinstance(field_def, SimpleField):
|
|
296
|
+
prop_schema = _simple_field_to_json_schema(field_def)
|
|
297
|
+
properties[field_name] = prop_schema
|
|
298
|
+
if field_def.required:
|
|
299
|
+
required.append(field_name)
|
|
300
|
+
else:
|
|
301
|
+
# Nested structure
|
|
302
|
+
prop_schema = _structure_to_json_schema(field_def)
|
|
303
|
+
properties[field_name] = prop_schema
|
|
304
|
+
if field_def.get('required', True):
|
|
305
|
+
required.append(field_name)
|
|
306
|
+
|
|
307
|
+
schema = {
|
|
308
|
+
"type": "object",
|
|
309
|
+
"properties": properties
|
|
310
|
+
}
|
|
311
|
+
if required:
|
|
312
|
+
schema["required"] = required
|
|
313
|
+
|
|
314
|
+
return schema
|
|
315
|
+
|
|
316
|
+
|
|
317
|
+
def _array_structure_to_schema(structure: Dict[str, Any]) -> Dict[str, Any]:
|
|
318
|
+
"""Convert array structure to JSON Schema"""
|
|
319
|
+
items_structure = structure["items"]
|
|
320
|
+
constraints = structure.get("constraints", {})
|
|
321
|
+
|
|
322
|
+
# Simple array: [string], [int], etc.
|
|
323
|
+
if items_structure["type"] == "simple":
|
|
324
|
+
simple_type = items_structure["simple_type"]
|
|
325
|
+
original_type = items_structure.get("original_type", simple_type)
|
|
326
|
+
items_schema = {"type": simple_type}
|
|
327
|
+
|
|
328
|
+
# Add format for special types
|
|
329
|
+
if simple_type == "string" and original_type in ['email', 'url', 'uri', 'datetime', 'date', 'uuid']:
|
|
330
|
+
if original_type == "email":
|
|
331
|
+
items_schema["format"] = "email"
|
|
332
|
+
elif original_type in ["url", "uri"]:
|
|
333
|
+
items_schema["format"] = "uri"
|
|
334
|
+
elif original_type == "datetime":
|
|
335
|
+
items_schema["format"] = "date-time"
|
|
336
|
+
elif original_type == "date":
|
|
337
|
+
items_schema["format"] = "date"
|
|
338
|
+
elif original_type == "uuid":
|
|
339
|
+
items_schema["format"] = "uuid"
|
|
340
|
+
|
|
341
|
+
else:
|
|
342
|
+
# Complex array: [{field1, field2}]
|
|
343
|
+
items_schema = _structure_to_json_schema(items_structure)
|
|
344
|
+
|
|
345
|
+
array_schema = {
|
|
346
|
+
"type": "array",
|
|
347
|
+
"items": items_schema
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
# Add array constraints
|
|
351
|
+
if "min" in constraints:
|
|
352
|
+
array_schema["minItems"] = constraints["min"]
|
|
353
|
+
if "max" in constraints:
|
|
354
|
+
array_schema["maxItems"] = constraints["max"]
|
|
355
|
+
|
|
356
|
+
return array_schema
|
|
357
|
+
|
|
358
|
+
|
|
359
|
+
def _simple_field_to_json_schema(field: SimpleField) -> Dict[str, Any]:
|
|
360
|
+
"""Convert SimpleField to JSON Schema property with enhanced features"""
|
|
361
|
+
prop = {"type": field.field_type}
|
|
362
|
+
|
|
363
|
+
# Basic metadata
|
|
364
|
+
if field.description:
|
|
365
|
+
prop["description"] = field.description
|
|
366
|
+
if field.default is not None:
|
|
367
|
+
prop["default"] = field.default
|
|
368
|
+
|
|
369
|
+
# Handle union types
|
|
370
|
+
if hasattr(field, 'union_types') and field.union_types and len(field.union_types) > 1:
|
|
371
|
+
# Create anyOf for union types
|
|
372
|
+
union_schemas = []
|
|
373
|
+
for union_type in field.union_types:
|
|
374
|
+
if union_type == "null":
|
|
375
|
+
union_schemas.append({"type": "null"})
|
|
376
|
+
else:
|
|
377
|
+
union_schemas.append({"type": union_type})
|
|
378
|
+
prop = {"anyOf": union_schemas}
|
|
379
|
+
|
|
380
|
+
# Handle enum/choices
|
|
381
|
+
if field.choices:
|
|
382
|
+
prop["enum"] = field.choices
|
|
383
|
+
|
|
384
|
+
# Add format hints for special types
|
|
385
|
+
if hasattr(field, 'format_hint') and field.format_hint:
|
|
386
|
+
if field.format_hint == "email":
|
|
387
|
+
prop["format"] = "email"
|
|
388
|
+
elif field.format_hint in ["url", "uri"]:
|
|
389
|
+
prop["format"] = "uri"
|
|
390
|
+
elif field.format_hint == "datetime":
|
|
391
|
+
prop["format"] = "date-time"
|
|
392
|
+
elif field.format_hint == "date":
|
|
393
|
+
prop["format"] = "date"
|
|
394
|
+
elif field.format_hint == "uuid":
|
|
395
|
+
prop["format"] = "uuid"
|
|
396
|
+
# Note: phone doesn't have a standard JSON Schema format
|
|
397
|
+
|
|
398
|
+
# Numeric constraints
|
|
399
|
+
if field.field_type in ["integer", "number"]:
|
|
400
|
+
if field.min_val is not None:
|
|
401
|
+
prop["minimum"] = field.min_val
|
|
402
|
+
if field.max_val is not None:
|
|
403
|
+
prop["maximum"] = field.max_val
|
|
404
|
+
|
|
405
|
+
# String constraints
|
|
406
|
+
if field.field_type == "string":
|
|
407
|
+
if field.min_length is not None:
|
|
408
|
+
prop["minLength"] = field.min_length
|
|
409
|
+
if field.max_length is not None:
|
|
410
|
+
prop["maxLength"] = field.max_length
|
|
411
|
+
|
|
412
|
+
return prop
|
|
413
|
+
|
|
414
|
+
|
|
415
|
+
def _split_field_definitions_with_nesting(schema_str: str) -> List[str]:
|
|
416
|
+
"""Split field definitions while respecting nesting"""
|
|
417
|
+
parts = []
|
|
418
|
+
current_part = ""
|
|
419
|
+
bracket_depth = 0
|
|
420
|
+
brace_depth = 0
|
|
421
|
+
paren_depth = 0
|
|
422
|
+
|
|
423
|
+
for char in schema_str:
|
|
424
|
+
if char == '[':
|
|
425
|
+
bracket_depth += 1
|
|
426
|
+
elif char == ']':
|
|
427
|
+
bracket_depth -= 1
|
|
428
|
+
elif char == '{':
|
|
429
|
+
brace_depth += 1
|
|
430
|
+
elif char == '}':
|
|
431
|
+
brace_depth -= 1
|
|
432
|
+
elif char == '(':
|
|
433
|
+
paren_depth += 1
|
|
434
|
+
elif char == ')':
|
|
435
|
+
paren_depth -= 1
|
|
436
|
+
elif char == ',' and bracket_depth == 0 and brace_depth == 0 and paren_depth == 0:
|
|
437
|
+
if current_part.strip():
|
|
438
|
+
parts.append(current_part.strip())
|
|
439
|
+
current_part = ""
|
|
440
|
+
continue
|
|
441
|
+
|
|
442
|
+
current_part += char
|
|
443
|
+
|
|
444
|
+
if current_part.strip():
|
|
445
|
+
parts.append(current_part.strip())
|
|
446
|
+
|
|
447
|
+
return parts
|
|
448
|
+
|
|
449
|
+
|
|
450
|
+
def _normalize_string_schema(schema_str: str) -> str:
|
|
451
|
+
"""Normalize string schema by removing comments and extra whitespace"""
|
|
452
|
+
# Remove triple quotes
|
|
453
|
+
schema_str = re.sub(r'^[\'\"]{3}|[\'\"]{3}$', '', schema_str.strip())
|
|
454
|
+
|
|
455
|
+
# Process line by line
|
|
456
|
+
lines = schema_str.split('\n')
|
|
457
|
+
normalized_parts = []
|
|
458
|
+
|
|
459
|
+
for line in lines:
|
|
460
|
+
line = line.strip()
|
|
461
|
+
if not line or line.startswith('#'):
|
|
462
|
+
continue
|
|
463
|
+
|
|
464
|
+
# Remove inline comments
|
|
465
|
+
if ' #' in line:
|
|
466
|
+
line = line.split(' #')[0].strip()
|
|
467
|
+
elif '#' in line and not line.startswith('#'):
|
|
468
|
+
line = line.split('#')[0].strip()
|
|
469
|
+
|
|
470
|
+
normalized_parts.append(line)
|
|
471
|
+
|
|
472
|
+
# Join with commas and clean up
|
|
473
|
+
result = ', '.join(part.rstrip(',') for part in normalized_parts)
|
|
474
|
+
return result
|
|
475
|
+
|
|
476
|
+
|
|
477
|
+
def _parse_type_definition(type_def: str) -> tuple:
|
|
478
|
+
"""Parse enhanced type definitions with constraints"""
|
|
479
|
+
constraints = {}
|
|
480
|
+
|
|
481
|
+
# Handle constraints in parentheses: type(min=1,max=10)
|
|
482
|
+
constraint_match = re.match(r'^(\w+)\(([^)]+)\)$', type_def)
|
|
483
|
+
if constraint_match:
|
|
484
|
+
base_type = constraint_match.group(1)
|
|
485
|
+
constraint_str = constraint_match.group(2)
|
|
486
|
+
|
|
487
|
+
# Parse constraints
|
|
488
|
+
constraint_parts = [part.strip() for part in constraint_str.split(',')]
|
|
489
|
+
|
|
490
|
+
# Handle positional constraints like int(0,120) -> min=0, max=120
|
|
491
|
+
if len(constraint_parts) == 2 and all('=' not in part for part in constraint_parts):
|
|
492
|
+
try:
|
|
493
|
+
min_val = float(constraint_parts[0]) if '.' in constraint_parts[0] else int(constraint_parts[0])
|
|
494
|
+
max_val = float(constraint_parts[1]) if '.' in constraint_parts[1] else int(constraint_parts[1])
|
|
495
|
+
|
|
496
|
+
if base_type in ['string', 'str', 'text']:
|
|
497
|
+
constraints['min_length'] = min_val
|
|
498
|
+
constraints['max_length'] = max_val
|
|
499
|
+
else:
|
|
500
|
+
constraints['min_val'] = min_val
|
|
501
|
+
constraints['max_val'] = max_val
|
|
502
|
+
except ValueError as e:
|
|
503
|
+
logger.warning(f"Invalid positional constraints '{constraint_str}': {e}")
|
|
504
|
+
else:
|
|
505
|
+
# Handle named constraints like string(min=1,max=100)
|
|
506
|
+
for part in constraint_parts:
|
|
507
|
+
if '=' in part:
|
|
508
|
+
key, value = part.split('=', 1)
|
|
509
|
+
key = key.strip()
|
|
510
|
+
value = value.strip()
|
|
511
|
+
|
|
512
|
+
try:
|
|
513
|
+
if key in ['min', 'max']:
|
|
514
|
+
if base_type in ['string', 'str', 'text']:
|
|
515
|
+
constraints[f'{key}_length'] = int(value)
|
|
516
|
+
else:
|
|
517
|
+
constraints[f'{key}_val'] = float(value) if '.' in value else int(value)
|
|
518
|
+
else:
|
|
519
|
+
constraints[key] = value
|
|
520
|
+
except ValueError as e:
|
|
521
|
+
logger.warning(f"Invalid constraint '{part}': {e}")
|
|
522
|
+
else:
|
|
523
|
+
# Single value constraint (treat as max)
|
|
524
|
+
try:
|
|
525
|
+
if base_type in ['string', 'str', 'text']:
|
|
526
|
+
constraints['max_length'] = int(part)
|
|
527
|
+
else:
|
|
528
|
+
constraints['max_val'] = float(part) if '.' in part else int(part)
|
|
529
|
+
except ValueError as e:
|
|
530
|
+
logger.warning(f"Invalid constraint value '{part}': {e}")
|
|
531
|
+
else:
|
|
532
|
+
base_type = type_def
|
|
533
|
+
|
|
534
|
+
# Normalize type name
|
|
535
|
+
normalized_type = _normalize_type_name(base_type)
|
|
536
|
+
|
|
537
|
+
return normalized_type, constraints
|
|
538
|
+
|
|
539
|
+
|
|
540
|
+
def _normalize_type_name(type_name: str) -> str:
|
|
541
|
+
"""Normalize type names with enhanced support"""
|
|
542
|
+
type_mapping = {
|
|
543
|
+
# Basic types
|
|
544
|
+
'str': 'string',
|
|
545
|
+
'string': 'string',
|
|
546
|
+
'text': 'string',
|
|
547
|
+
'int': 'integer',
|
|
548
|
+
'integer': 'integer',
|
|
549
|
+
'num': 'number',
|
|
550
|
+
'number': 'number',
|
|
551
|
+
'float': 'number',
|
|
552
|
+
'double': 'number',
|
|
553
|
+
'decimal': 'number',
|
|
554
|
+
'bool': 'boolean',
|
|
555
|
+
'boolean': 'boolean',
|
|
556
|
+
|
|
557
|
+
# Special types (normalized to string, format handled separately)
|
|
558
|
+
'email': 'string',
|
|
559
|
+
'url': 'string',
|
|
560
|
+
'uri': 'string',
|
|
561
|
+
'datetime': 'string',
|
|
562
|
+
'date': 'string',
|
|
563
|
+
'uuid': 'string',
|
|
564
|
+
'phone': 'string',
|
|
565
|
+
'tel': 'string',
|
|
566
|
+
'null': 'null',
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
return type_mapping.get(type_name.lower(), 'string')
|
|
570
|
+
|
|
571
|
+
|
|
572
|
+
# Clear function name aliases
|
|
573
|
+
def string_to_json_schema(schema_str: str) -> Dict[str, Any]:
|
|
574
|
+
"""
|
|
575
|
+
Convert string syntax to JSON Schema dictionary.
|
|
576
|
+
|
|
577
|
+
Args:
|
|
578
|
+
schema_str: String schema definition (e.g., "name:string, email:email")
|
|
579
|
+
|
|
580
|
+
Returns:
|
|
581
|
+
JSON Schema dictionary
|
|
582
|
+
|
|
583
|
+
Example:
|
|
584
|
+
schema = string_to_json_schema("name:string, email:email")
|
|
585
|
+
"""
|
|
586
|
+
return parse_string_schema(schema_str)
|
|
587
|
+
|
|
588
|
+
|
|
589
|
+
def validate_string_syntax(schema_str: str) -> Dict[str, Any]:
|
|
590
|
+
"""
|
|
591
|
+
Validate string schema syntax and return detailed feedback.
|
|
592
|
+
|
|
593
|
+
Args:
|
|
594
|
+
schema_str: String schema definition to validate
|
|
595
|
+
|
|
596
|
+
Returns:
|
|
597
|
+
Dictionary with validation results: valid, errors, warnings, features_used, etc.
|
|
598
|
+
|
|
599
|
+
Example:
|
|
600
|
+
result = validate_string_syntax("name:string, email:email")
|
|
601
|
+
print(f"Valid: {result['valid']}")
|
|
602
|
+
"""
|
|
603
|
+
return validate_string_schema(schema_str)
|
|
604
|
+
|
|
605
|
+
|
|
606
|
+
def validate_string_schema(schema_str: str) -> Dict[str, Any]:
|
|
607
|
+
"""Validate enhanced string schema with detailed feedback"""
|
|
608
|
+
result = {
|
|
609
|
+
'valid': False,
|
|
610
|
+
'errors': [],
|
|
611
|
+
'warnings': [],
|
|
612
|
+
'parsed_fields': {},
|
|
613
|
+
'generated_schema': None,
|
|
614
|
+
'features_used': []
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
try:
|
|
618
|
+
# Parse the schema
|
|
619
|
+
schema = parse_string_schema(schema_str)
|
|
620
|
+
result['generated_schema'] = schema
|
|
621
|
+
result['valid'] = True
|
|
622
|
+
|
|
623
|
+
# Analyze features used
|
|
624
|
+
features = set()
|
|
625
|
+
if '[' in schema_str and ']' in schema_str:
|
|
626
|
+
features.add('arrays')
|
|
627
|
+
if '{' in schema_str and '}' in schema_str:
|
|
628
|
+
features.add('objects')
|
|
629
|
+
if '?' in schema_str:
|
|
630
|
+
features.add('optional_fields')
|
|
631
|
+
if 'enum(' in schema_str or 'choice(' in schema_str or 'select(' in schema_str:
|
|
632
|
+
features.add('enums')
|
|
633
|
+
if '|' in schema_str:
|
|
634
|
+
features.add('union_types')
|
|
635
|
+
if any(t in schema_str for t in ['email', 'url', 'datetime', 'date', 'uuid', 'phone']):
|
|
636
|
+
features.add('special_types')
|
|
637
|
+
# Check for constraints more carefully
|
|
638
|
+
constraint_patterns = [
|
|
639
|
+
r'string\([^)]+\)', # string(min=1,max=100)
|
|
640
|
+
r'int\([^)]+\)', # int(0,120)
|
|
641
|
+
r'number\([^)]+\)', # number(min=0)
|
|
642
|
+
r'text\([^)]+\)', # text(max=500)
|
|
643
|
+
r'\]\([^)]+\)' # [string](max=5)
|
|
644
|
+
]
|
|
645
|
+
if any(re.search(pattern, schema_str) for pattern in constraint_patterns):
|
|
646
|
+
features.add('constraints')
|
|
647
|
+
|
|
648
|
+
result['features_used'] = list(features)
|
|
649
|
+
|
|
650
|
+
# Extract field information
|
|
651
|
+
_extract_field_info(schema, result['parsed_fields'], "")
|
|
652
|
+
|
|
653
|
+
# Add warnings for potential issues
|
|
654
|
+
if len(result['parsed_fields']) > 20:
|
|
655
|
+
result['warnings'].append("Schema has many fields (>20), consider simplifying")
|
|
656
|
+
|
|
657
|
+
if 'arrays' in features and 'constraints' not in features:
|
|
658
|
+
result['warnings'].append("Consider adding array size constraints for better LLM guidance")
|
|
659
|
+
|
|
660
|
+
except Exception as e:
|
|
661
|
+
result['errors'].append(str(e))
|
|
662
|
+
|
|
663
|
+
return result
|
|
664
|
+
|
|
665
|
+
|
|
666
|
+
def _extract_field_info(schema: Dict[str, Any], fields_dict: Dict[str, Any], prefix: str = ""):
|
|
667
|
+
"""Extract field information from generated schema"""
|
|
668
|
+
if schema.get('type') == 'object' and 'properties' in schema:
|
|
669
|
+
required_fields = set(schema.get('required', []))
|
|
670
|
+
|
|
671
|
+
for field_name, field_schema in schema['properties'].items():
|
|
672
|
+
full_name = f"{prefix}.{field_name}" if prefix else field_name
|
|
673
|
+
|
|
674
|
+
field_info = {
|
|
675
|
+
'type': field_schema.get('type', 'unknown'),
|
|
676
|
+
'required': field_name in required_fields,
|
|
677
|
+
'constraints': {}
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
# Extract constraints
|
|
681
|
+
for key in ['minimum', 'maximum', 'minLength', 'maxLength', 'minItems', 'maxItems', 'format', 'enum']:
|
|
682
|
+
if key in field_schema:
|
|
683
|
+
field_info['constraints'][key] = field_schema[key]
|
|
684
|
+
|
|
685
|
+
# Handle union types
|
|
686
|
+
if 'anyOf' in field_schema:
|
|
687
|
+
field_info['type'] = 'union'
|
|
688
|
+
field_info['union_types'] = [item.get('type', 'unknown') for item in field_schema['anyOf']]
|
|
689
|
+
|
|
690
|
+
fields_dict[full_name] = field_info
|
|
691
|
+
|
|
692
|
+
# Recurse into nested objects
|
|
693
|
+
if field_schema.get('type') == 'object':
|
|
694
|
+
_extract_field_info(field_schema, fields_dict, full_name)
|
|
695
|
+
elif field_schema.get('type') == 'array' and 'items' in field_schema:
|
|
696
|
+
items_schema = field_schema['items']
|
|
697
|
+
if items_schema.get('type') == 'object':
|
|
698
|
+
_extract_field_info(items_schema, fields_dict, f"{full_name}[]")
|
|
699
|
+
|
|
700
|
+
elif schema.get('type') == 'array' and 'items' in schema:
|
|
701
|
+
items_schema = schema['items']
|
|
702
|
+
if items_schema.get('type') == 'object':
|
|
703
|
+
_extract_field_info(items_schema, fields_dict, f"{prefix}[]" if prefix else "[]")
|