Coverage for src/lib.mys : 89%

Hot-keys on this page
r m x p toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1from string import StringReader
3class GraphqlError(Error):
4 message: string
6trait _Token:
7 pass
9class _Punctuator(_Token):
10 value: char
12class _Dots(_Token):
13 pass
15class _Name(_Token):
16 value: string
18class _IntValue(_Token):
19 value: string
21class _FloatValue(_Token):
22 value: string
24class _StringValue(_Token):
25 value: string
27class _End(_Token):
28 pass
30class _Tokens:
31 _tokens: [_Token]
32 _pos: i64
34 func __init__(self, tokens: [_Token]):
35 self._tokens = tokens
36 self._pos = 0
38 func available(self) -> bool:
39 return self._pos < self._tokens.length()
41 func get(self) -> _Token:
42 if not self.available():
43 raise GraphqlError("Out of tokens.")
45 self._pos += 1
47 return self._tokens[self._pos - 1]
49 func peek(self) -> _Token:
50 if not self.available():
51 raise GraphqlError("Out of tokens.")
53 return self._tokens[self._pos]
55 func unget(self):
56 self._pos -= 1
58class Document:
59 definitions: [Definition]
61class Definition:
62 executable_definition: ExecutableDefinition
64class ExecutableDefinition:
65 operation_definition: OperationDefinition
67class OperationDefinition:
68 operation_type: string?
69 name: string?
70 variable_definitions: [VariableDefinition]?
71 selections: [Selection]
73class VariableDefinition:
74 name: string
75 value: Type
77class Type:
78 name: string
79 types: [Type]?
80 non_null: bool
82class Selection:
83 field: Field
85class Field:
86 name: string
87 arguments: [Argument]?
88 selections: [Selection]?
90class Argument:
91 name: string
92 value: string?
93 variable: string?
95func _tokenize_name(reader: StringReader) -> _Token:
96 name = ""
98 while True:
99 ch = reader.get()
101 if ch.is_alpha() or ch.is_digit() or ch == '_':
102 name += ch
103 else:
104 if ch != '':
105 reader.unget()
107 break
109 return _Name(name)
111func _tokenize_number(reader: StringReader) -> _Token:
112 value = ""
114 while True:
115 ch = reader.get()
117 if ch.is_digit():
118 value += ch
119 else:
120 if ch != '':
121 reader.unget()
123 break
125 return _IntValue(value)
127func _tokenize_dots(reader: StringReader) -> _Token:
128 for _ in range(3):
129 ch = reader.get()
131 if ch != '.':
132 raise GraphqlError("No '.'.")
134 return _Dots()
136func _tokenize_string(reader: StringReader) -> _Token:
137 value = ""
139 while True:
140 ch = reader.get()
142 if ch == '\"':
143 break
144 elif ch == '':
145 raise GraphqlError("No end of string.")
146 else:
147 value += ch
149 return _StringValue(value)
151func _tokenize(document: string) -> _Tokens:
152 reader = StringReader(document)
153 token: _Token? = None
154 tokens: [_Token] = []
156 while True:
157 ch = reader.get()
159 if ch in ",\t\r\n ":
160 continue
161 elif ch in "!$&():=@[]{|}":
162 token = _Punctuator(ch)
163 elif ch == '.':
164 reader.unget()
165 token = _tokenize_dots(reader)
166 elif ch == '-' or ch.is_digit():
167 reader.unget()
168 token = _tokenize_number(reader)
169 elif ch == '\"':
170 token = _tokenize_string(reader)
171 elif ch.is_alpha() or ch == '_':
172 reader.unget()
173 token = _tokenize_name(reader)
174 elif ch == '':
175 break
176 else:
177 raise GraphqlError(f"Invalid character '{ch}'")
179 tokens.append(token)
181 # tokens.append(_End())
183 return _Tokens(tokens)
185func _parse_arguments(tokens: _Tokens) -> [Argument]?:
186 match tokens.peek():
187 case _Punctuator(value='('):
188 pass
189 case _:
190 return None
192 tokens.get()
193 arguments: [Argument]= []
195 while True:
196 match tokens.get():
197 case _Name() as name_token:
198 _parse_colon(tokens)
200 value: string? = None
201 variable: string? = None
203 match tokens.get():
204 case _StringValue() as string_value_token:
205 value = string_value_token.value
206 case _IntValue() as int_value_token:
207 pass
208 case _Punctuator(value='$'):
209 variable = _parse_variable(tokens)
210 case _:
211 raise GraphqlError("No value.")
213 arguments.append(Argument(name_token.value, value, variable))
214 case _Punctuator(value=')'):
215 break
216 case _:
217 raise GraphqlError("No ).")
219 return arguments
221func _parse_name(tokens: _Tokens) -> string?:
222 match tokens.get():
223 case _Name() as name_token:
224 return name_token.value
225 case _:
226 tokens.unget()
228 return None
230func _parse_field(tokens: _Tokens) -> Field?:
231 name = _parse_name(tokens)
233 if name is None:
234 return None
236 arguments = _parse_arguments(tokens)
237 selections = _parse_selection_set(tokens)
239 return Field(name, arguments, selections)
241func _parse_selection(tokens: _Tokens) -> Selection?:
242 field = _parse_field(tokens)
244 if field is None:
245 return None
247 return Selection(field)
249func _parse_selection_set(tokens: _Tokens) -> [Selection]?:
250 selections: [Selection] = []
252 match tokens.peek():
253 case _Punctuator(value='{'):
254 pass
255 case _:
256 return None
258 tokens.get()
260 while True:
261 match tokens.peek():
262 case _Punctuator(value='}'):
263 break
265 selection = _parse_selection(tokens)
267 if selection is None:
268 return None
270 selections.append(selection)
272 tokens.get()
274 return selections
276func _parse_operation_type(tokens: _Tokens) -> string?:
277 operation_type = _parse_name(tokens)
279 if operation_type is None:
280 return None
282 if operation_type not in ["query", "mutation", "subscription"]:
283 raise GraphqlError(f"Bad operation '{operation_type}'.")
285 return operation_type
287func _parse_non_null(tokens: _Tokens) -> bool:
288 match tokens.get():
289 case _Punctuator(value='!'):
290 return True
291 case _:
292 tokens.unget()
294 return False
297func _parse_type(tokens: _Tokens) -> Type:
298 match tokens.get():
299 case _Name() as name_token:
300 non_null = _parse_non_null(tokens)
302 return Type(name_token.value, None, non_null)
303 case _:
304 raise GraphqlError("No type name.")
306func _parse_variable(tokens: _Tokens) -> string:
307 match tokens.get():
308 case _Name() as name_token:
309 return name_token.value
310 case _:
311 raise GraphqlError("No variable name.")
313func _parse_colon(tokens: _Tokens):
314 match tokens.get():
315 case _Punctuator(value=':'):
316 pass
317 case _:
318 raise GraphqlError("No :.")
320func _parse_variable_definitions(tokens: _Tokens) -> [VariableDefinition]?:
321 match tokens.peek():
322 case _Punctuator(value='('):
323 pass
324 case _:
325 return None
327 tokens.get()
328 variable_definitions: [VariableDefinition] = []
330 while True:
331 match tokens.get():
332 case _Punctuator(value='$'):
333 name = _parse_variable(tokens)
334 _parse_colon(tokens)
335 type = _parse_type(tokens)
336 variable_definitions.append(VariableDefinition(name, type))
337 case _Punctuator(value=')'):
338 break
339 case _:
340 raise GraphqlError("No ).")
342 return variable_definitions
344func _parse_operation_definition(tokens: _Tokens) -> OperationDefinition?:
345 name: string? = None
346 variable_definitions: [VariableDefinition]? = None
347 operation_type = _parse_operation_type(tokens)
349 if operation_type is not None:
350 name = _parse_name(tokens)
352 if name is not None:
353 variable_definitions = _parse_variable_definitions(tokens)
355 selections = _parse_selection_set(tokens)
357 if selections is None:
358 return None
360 return OperationDefinition(operation_type,
361 name,
362 variable_definitions,
363 selections)
365func _parse_executable_definition(tokens: _Tokens) -> ExecutableDefinition?:
366 operation_definition = _parse_operation_definition(tokens)
368 if operation_definition is None:
369 return None
371 return ExecutableDefinition(operation_definition)
373func _parse_definition(tokens: _Tokens) -> Definition?:
374 executable_definition = _parse_executable_definition(tokens)
376 if executable_definition is None:
377 return None
379 return Definition(executable_definition)
381func _parse_document(tokens: _Tokens) -> Document:
382 document = Document([])
384 while tokens.available():
385 definition = _parse_definition(tokens)
387 if definition is None:
388 raise GraphqlError("Bad definition.")
390 document.definitions.append(definition)
392 return document
394func parse(document: string) -> Document:
395 """Parse given document.
397 """
399 tokens = _tokenize(document)
401 return _parse_document(tokens)
403test parse_query():
404 document = parse("\t\r"
405 "{\n"
406 " foo(id: 10, name: \"kalle\") {\n"
407 " id\n"
408 " name\n"
409 " value\n"
410 " }\n"
411 "}")
412 assert document.definitions.length() == 1
413 selections = (document
414 .definitions[0]
415 .executable_definition
416 .operation_definition
417 .selections)
418 assert selections.length() == 1
419 selection = selections[0]
420 assert selection.field.name == "foo"
421 arguments = selection.field.arguments
422 assert arguments.length() == 2
423 assert arguments[0].name == "id"
424 assert arguments[1].name == "name"
425 selections = selection.field.selections
426 assert selections.length() == 3
427 assert selections[0].field.name == "id"
428 assert selections[0].field.arguments is None
429 assert selections[0].field.selections is None
430 assert selections[1].field.name == "name"
431 assert selections[1].field.arguments is None
432 assert selections[1].field.selections is None
433 assert selections[2].field.name == "value"
434 assert selections[2].field.arguments is None
435 assert selections[2].field.selections is None
437test spacex():
438 document = parse("{"
439 " launchesPast(limit: 10) {"
440 " mission_name"
441 " launch_date_local"
442 " launch_site {"
443 " site_name_long"
444 " }"
445 " ships {"
446 " name"
447 " home_port"
448 " image"
449 " }"
450 " }"
451 "}")
452 assert document.definitions.length() == 1
453 selections = (document
454 .definitions[0]
455 .executable_definition
456 .operation_definition
457 .selections)
458 assert selections.length() == 1
459 assert selections[0].field.name == "launchesPast"
460 selections = selections[0].field.selections
461 assert selections[0].field.name == "mission_name"
462 assert selections[1].field.name == "launch_date_local"
463 assert selections[2].field.name == "launch_site"
464 assert selections[2].field.selections[0].field.name == "site_name_long"
465 assert selections[3].field.name == "ships"
466 assert selections[3].field.selections[0].field.name == "name"
467 assert selections[3].field.selections[1].field.name == "home_port"
468 assert selections[3].field.selections[2].field.name == "image"
470test bad_definition():
471 try:
472 parse("{{name}}")
473 assert False
474 except GraphqlError as error:
475 assert error.message == "Bad definition."
477 try:
478 parse("{")
479 assert False
480 except GraphqlError as error:
481 assert error.message == "Out of tokens."
483test name():
484 document = parse("{foo_2}")
485 assert document.definitions.length() == 1
486 selections = (document
487 .definitions[0]
488 .executable_definition
489 .operation_definition
490 .selections)
491 assert selections.length() == 1
492 assert selections[0].field.name == "foo_2"
494test operation_type():
495 document = parse("query {\n"
496 " packages {\n"
497 " name\n"
498 " }\n"
499 "}")
500 assert document.definitions.length() == 1
501 operation_definition = (document
502 .definitions[0]
503 .executable_definition
504 .operation_definition)
505 assert operation_definition.operation_type == "query"
506 assert operation_definition.name is None
508test operation_type_with_name():
509 document = parse("query MyQuery {\n"
510 " packages {\n"
511 " name\n"
512 " }\n"
513 "}")
514 assert document.definitions.length() == 1
515 operation_definition = (document
516 .definitions[0]
517 .executable_definition
518 .operation_definition)
519 assert operation_definition.operation_type == "query"
520 assert operation_definition.name == "MyQuery"
522test variable():
523 document = parse("query ExampleQuery($name: String!) {\n"
524 " standard_library {\n"
525 " package(name: $name) {\n"
526 " name\n"
527 " }\n"
528 " }\n"
529 "}")
530 assert document.definitions.length() == 1
531 operation_definition = (document
532 .definitions[0]
533 .executable_definition
534 .operation_definition)
535 assert operation_definition.variable_definitions.length() == 1
536 print(operation_definition.variable_definitions[0])
537 assert operation_definition.variable_definitions[0].name == "name"
538 assert operation_definition.variable_definitions[0].value.name == "String"
539 assert operation_definition.variable_definitions[0].value.non_null
541 document = parse("query ExampleQuery($count: Int) {\n"
542 " standard_library {\n"
543 " package(count: $count) {\n"
544 " name\n"
545 " }\n"
546 " }\n"
547 "}")
548 assert document.definitions.length() == 1
549 operation_definition = (document
550 .definitions[0]
551 .executable_definition
552 .operation_definition)
553 assert operation_definition.variable_definitions.length() == 1
554 print(operation_definition.variable_definitions[0])
555 assert operation_definition.variable_definitions[0].name == "count"
556 assert operation_definition.variable_definitions[0].value.name == "Int"
557 assert not operation_definition.variable_definitions[0].value.non_null
559test parse_schema():
560 return
561 parse("type Query {"
562 " standard_library: StandardLibrary!"
563 " statistics: Statistics!"
564 "}"
565 "type StandardLibrary {"
566 " package(name: String!): Package!"
567 " packages: [String!]"
568 "}"
569 "type Package {"
570 " name: String!"
571 " latest_release: Release!"
572 " number_of_downloads: Int!"
573 " builds: Boolean"
574 "}"
575 "type Release {"
576 " version: String!"
577 " description: String!"
578 "}"
579 "type Statistics {"
580 " start_date_time: String!"
581 " total_number_of_requests: Int!"
582 " number_of_unique_visitors: Int!"
583 " number_of_graphql_requests: Int!"
584 " no_idle_client_handlers: Int!"
585 "}")