57 line = [f
"{nonterminal} ::= ",
"object_begin "]
60 for field, _field_info
in current.fields().items():
61 field_name = f
"{nonterminal}_{field}"
62 fields.append(f
"'\"{field}\"' colon {field_name}")
63 result.append((_field_info, field_name))
64 line.append(
" comma ".join(fields))
65 line.append(
" object_end;\n")
66 return "".join(line), result
69 def field_info(current: typing.Type, nonterminal: str):
71 annotation = current.annotation
73 return "", [(annotation, nonterminal)]
74 new_nonterminal = f
"{nonterminal}_required"
75 return f
"{nonterminal} ::= {new_nonterminal}?;\n", [(annotation, new_nonterminal)]
78 def string_metadata(current: typing.Type, nonterminal: str):
79 min_length = current.metadata.get(
"min_length")
80 max_length = current.metadata.get(
"max_length")
81 pattern = current.metadata.get(
"pattern")
82 substring_of = current.metadata.get(
"substring_of")
84 assert not (min_length
or max_length
or substring_of),
"pattern is mutually exclusive with min_length, max_length and substring_of"
86 assert not (min_length
or max_length
or pattern),
"substring_of is mutually exclusive with min_length, max_length and pattern"
88 (
True,
False): f
"{{{min_length},}}",
89 (
False,
True): f
"{{0,{max_length}}}",
90 (
True,
True): f
"{{{min_length},{max_length}}}"
92 repetition = repetition_map.get((min_length
is not None, max_length
is not None))
93 if repetition
is not None:
94 return fr
"""{nonterminal} ::= #'"([^\\\\"\u0000-\u001f]|\\\\["\\\\bfnrt/]|\\\\u[0-9A-Fa-f]{{4}}){repetition}"';
96 if pattern
is not None:
97 pattern = pattern.replace(
"'",
"\\'")
98 return f
"""{nonterminal} ::= #'"{pattern}"';\n""", []
99 if substring_of
is not None:
100 return f
"""{nonterminal} ::= '"' #substrs{repr(substring_of)} '"';\n""", []
102 def number_metadata(current: typing.Type, nonterminal: str):
103 gt = current.metadata.get(
"gt")
104 ge = current.metadata.get(
"ge")
105 lt = current.metadata.get(
"lt")
106 le = current.metadata.get(
"le")
115 for (condition, value), prefix
in prefix_map.items():
116 if condition
is not None and condition == value:
117 if issubclass(current.type, int):
118 return f
"""{nonterminal} ::= #'{prefix}[1-9][0-9]*';\n""", []
119 elif issubclass(current.type, float):
120 return f
"""{nonterminal} ::= #'{prefix}[1-9][0-9]*(\\.[0-9]+)?([eE][+-]?[0-9]+)?';\n""", []
122 raise ValueError(f
"{current.type.__name__} metadata {current.metadata} is not supported in json_generators!")
124 def sequence_metadata(current: typing.Type, nonterminal: str):
125 min_items = current.metadata.get(
"min_length")
126 max_items = current.metadata.get(
"max_length")
127 prefix_items = current.metadata.get(
"prefix_items")
128 additional_items = current.metadata.get(
"additional_items")
129 if max_items
is not None and prefix_items
is not None and max_items <= len(prefix_items):
130 prefix_items = prefix_items[:max_items+1]
134 if not additional_items:
135 if min_items > len(prefix_items):
136 raise ValueError(f
"min_items {min_items} is greater than the number of prefix_items {len(prefix_items)} and additional_items is not allowed")
137 max_items = len(prefix_items)
138 if min_items
is not None or max_items
is not None:
139 new_nonterminal = f
"{nonterminal}_item"
141 if min_items
is None:
143 if min_items == 0
and max_items
is None and prefix_items
is None:
144 return "", [(current.type, new_nonterminal)]
145 prefix_items_nonterminals = [f
"{new_nonterminal}_{i}" for i
in range(len(prefix_items))]
if prefix_items
else []
146 prefix_items_parts = []
147 if prefix_items
is not None:
148 for i
in range(max(min_items,1), len(prefix_items)+1):
149 prefix_items_parts.append(prefix_items_nonterminals[:i])
151 ebnf_rules.append(f
"{nonterminal} ::= array_begin array_end;")
152 if max_items
is None:
154 min_items_part =
' comma '.join([new_nonterminal] * (min_items - 1))
155 ebnf_rules.append(f
"{nonterminal} ::= array_begin {min_items_part} comma {new_nonterminal}+ array_end;")
156 elif len(prefix_items_parts) >= min_items:
157 for prefix_items_part
in prefix_items_parts:
158 prefix_items_part =
' comma '.join(prefix_items_part)
159 ebnf_rules.append(f
"{nonterminal} ::= array_begin {prefix_items_part} (comma {new_nonterminal})* array_end;")
161 min_items_part =
' comma '.join([new_nonterminal] * (min_items - len(prefix_items_nonterminals)-1))
163 min_items_part =
"comma " + min_items_part
164 prefix_items_part =
' comma '.join(prefix_items_nonterminals)
165 ebnf_rules.append(f
"{nonterminal} ::= array_begin {prefix_items_part} {min_items_part} comma {new_nonterminal}+ array_end;")
166 elif min_items == 0
and not prefix_items:
167 for i
in range(min_items, max_items + 1):
168 items =
' comma '.join([new_nonterminal] * i)
169 ebnf_rules.append(f
"{nonterminal} ::= array_begin {items} array_end;")
171 prefix_items_num = len(prefix_items_nonterminals)
173 for prefix_items_part
in prefix_items_parts:
174 prefix_items_part =
' comma '.join(prefix_items_part)
175 ebnf_rules.append(f
"{nonterminal} ::= array_begin {prefix_items_part} array_end;")
176 min_items_part =
' comma '.join([new_nonterminal] * (min_items - prefix_items_num))
177 prefix_items_part =
' comma '.join(prefix_items_nonterminals)
178 if min_items_part
and prefix_items_part:
179 ebnf_rules.append(f
"{nonterminal}_min ::= {prefix_items_part} comma {min_items_part};")
181 ebnf_rules.append(f
"{nonterminal}_min ::= {min_items_part};")
182 elif prefix_items_part:
183 ebnf_rules.append(f
"{nonterminal}_min ::= {prefix_items_part};")
185 common = max(min_items, prefix_items_num)
186 for i
in range(1, max_items + 1 - common):
187 items =
' comma '.join([new_nonterminal] * i)
188 ebnf_rules.append(f
"{nonterminal} ::= array_begin {nonterminal}_min comma {items} array_end;")
190 args = typing.get_args(current.type)
195 item_type = typing.Any
197 return "\n".join(ebnf_rules) +
"\n", list(zip(prefix_items, prefix_items_nonterminals)) + [(item_type, new_nonterminal)]
198 return "\n".join(ebnf_rules) +
"\n", [(item_type, new_nonterminal)]
201 def is_sequence_like(current: typing.Type) -> bool:
203 Check if the given type is sequence-like.
205 This function returns True for:
209 - Any subclass of collections.abc.Sequence
214 current: The type to check.
217 bool: True if the type is sequence-like, False otherwise.
219 original = typing.get_origin(current)
223 original
is typing.Sequence
or
224 original
is typing.List
or
225 original
is typing.Tuple
or
226 (isinstance(original, type)
and (issubclass(original, collections.abc.Sequence)
or
227 issubclass(original, list)
or
228 issubclass(original, tuple)))
231 def metadata(current: typing.Type, nonterminal: str):
233 original = typing.get_origin(current.type)
235 original = current.type
236 if not current.metadata:
237 return "", [(current.type, nonterminal)]
238 if isinstance(current.type, type)
and issubclass(current.type, str):
239 return string_metadata(current, nonterminal)
240 elif isinstance(current.type, type)
and issubclass(current.type, (int, float)):
241 return number_metadata(current, nonterminal)
242 elif is_sequence_like(original):
243 return sequence_metadata(current, nonterminal)
246 def builtin_sequence(current: typing.Type, nonterminal: str):
247 original = typing.get_origin(current)
250 if is_sequence_like(original):
251 new_nonterminal = f
"{nonterminal}_value"
252 annotation = typing.get_args(current)
254 annotation = typing.Any
256 annotation = annotation[0]
257 return f
"{nonterminal} ::= array_begin ({new_nonterminal} (comma {new_nonterminal})*)? array_end;\n", \
258 [(annotation, new_nonterminal)]
261 def builtin_dict(current: typing.Type, nonterminal: str):
262 original = typing.get_origin(current)
265 if original
is typing.Mapping
or isinstance(original, type)
and issubclass(original,
266 collections.abc.Mapping):
267 new_nonterminal = f
"{nonterminal}_value"
268 args = typing.get_args(current)
273 args[0], str), f
"{args[0]} is not string!"
275 return f
"{nonterminal} ::=" \
276 f
" object_begin (string colon {new_nonterminal} (comma string colon {new_nonterminal})*)?" \
278 [(value, new_nonterminal)]
281 def builtin_tuple(current: typing.Type, nonterminal: str):
282 if typing.get_origin(current)
is tuple
or isinstance(current, type)
and issubclass(current, tuple):
283 args = typing.get_args(current)
284 new_nonterminals = []
286 for i, arg
in enumerate(args):
288 new_nonterminals.append(f
"{nonterminal}_{i}")
289 return f
"{nonterminal} ::=array_begin {' comma '.join(new_nonterminals)} array_end;\n", \
290 zip(result, new_nonterminals)
292 def builtin_union(current: typing.Type, nonterminal: str):
293 if typing.get_origin(current)
is typing.Union:
294 args = typing.get_args(current)
295 assert args, f
"{current} from {nonterminal} cannot be an empty union!"
296 new_nonterminals = []
298 for i, arg
in enumerate(args):
300 new_nonterminals.append(f
"{nonterminal}_{i}")
301 return f
"{nonterminal} ::= {' | '.join(new_nonterminals)};\n", zip(result, new_nonterminals)
303 def builtin_literal(current: typing.Type, nonterminal: str):
304 if typing.get_origin(current)
is typing.Literal:
305 args = typing.get_args(current)
306 assert args, f
"{current} from {nonterminal} cannot be an empty literal!"
309 for i, arg
in enumerate(args):
310 if isinstance(arg, str):
311 new_items.append(f
'"\\"{repr(arg)[1:-1]}\\""')
312 elif isinstance(arg, bool):
313 new_items.append(f
'"{str(arg).lower()}"')
314 elif isinstance(arg, int):
315 new_items.append(f
'"{str(arg)}"')
316 elif isinstance(arg, float):
317 new_items.append(f
'"{str(arg)}"')
319 new_items.append(
"null")
320 elif isinstance(arg, tuple):
321 for j,item
in enumerate(arg):
322 new_nonterminal = f
"{nonterminal}_{i}_{j}"
323 result.append((typing.Literal[item], new_nonterminal))
324 new_item = f
"(array_begin {' comma '.join(map(lambda x:x[1], result))} array_end)"
325 new_items.append(new_item)
326 elif isinstance(arg, frozendict):
327 for key, value
in arg.items():
328 new_nonterminal = f
"{nonterminal}_{i}_{key}"
329 result.append((typing.Literal[value], new_nonterminal))
330 new_item = f
"object_begin {' comma '.join(map(lambda x:x[1], result))} object_end"
331 new_items.append(new_item)
333 new_nonterminal = f
"{nonterminal}_{i}"
334 result.append((arg, new_nonterminal))
335 new_items.append(new_nonterminal)
336 return f
"{nonterminal} ::= {' | '.join(new_items)};\n", result
338 def builtin_simple_types(current: typing.Type, nonterminal: str):
339 if isinstance(current, type)
and issubclass(current, bool):
340 return f
"{nonterminal} ::= boolean;\n", []
341 elif isinstance(current, type)
and issubclass(current, int):
342 return f
"{nonterminal} ::= integer;\n", []
343 elif isinstance(current, type)
and issubclass(current, float):
344 return f
"{nonterminal} ::= number;\n", []
345 elif isinstance(current, type)
and issubclass(current, decimal.Decimal):
346 return f
"{nonterminal} ::= number;\n", []
347 elif isinstance(current, type)
and issubclass(current, str):
348 return f
"{nonterminal} ::= string;\n", []
349 elif isinstance(current, type)
and issubclass(current, type(
None)):
350 return f
"{nonterminal} ::= null;\n", []
351 elif current
is typing.Any:
352 return f
"{nonterminal} ::= json_value;\n", []
353 elif isinstance(current, typing.NewType):
354 current: typing.NewType
355 return "", [(current.__supertype__, nonterminal)]
367def _generate_kbnf_grammar(schema: schemas.schema.Schema|collections.abc.Sequence, start_nonterminal: str) -> str:
369 Generate a KBNF grammar string from a schema for JSON format.
372 schema: The schema to generate a grammar for.
373 start_nonterminal: The start nonterminal of the grammar. Default is "start".