Coverage for src/prisma/generator/_dsl_parser/parser.py: 100%

32 statements  

« prev     ^ index     » next       coverage.py v7.2.7, created at 2024-08-27 18:25 +0000

1"""Lark parser for our custom DSL inside Prisma Schemas, e.g. 

2 

3```prisma 

4/// @Python(name: foo) 

5model bar { 

6 // ... 

7} 

8``` 

9""" 

10 

11from __future__ import annotations 

12 

13from typing import Union 

14from typing_extensions import Literal, TypedDict 

15 

16from .transformer import TransformResult, DefinitionTransformer 

17from ..._vendor.lark_schema_parser import Lark_StandAlone as LarkParser, UnexpectedInput 

18from ..._vendor.lark_schema_scan_parser import Lark_StandAlone as LarkScanner 

19 

20scanner = LarkScanner() # type: ignore[no-untyped-call] 

21schema_extension_parser = LarkParser() # type: ignore[no-untyped-call] 

22 

23transformer = DefinitionTransformer() 

24 

25 

26def parse_schema_dsl(text: str) -> ParseResult: 

27 """Given a string like `@Python(foo: bar)` 

28 returns `{ 'type': 'ok', 'value': { 'arguments': { 'foo': 'bar' } } }`. 

29 

30 If the string is not valid syntax, then `{'type': 'invalid', 'error': 'msg'}` 

31 is returned. 

32 

33 If the string is not actually even attempting to represent the `@Python` 

34 DSL, then `{'type': 'not_applicable'}` is returned. 

35 

36 Note, currently `not_applicable` will be returned if there are no arguments given 

37 to `@Python`, e.g. `@Python()`. This currently doesn't matter, but it may be changed 

38 in the future. 

39 """ 

40 parts = scan_for_declarations(text) 

41 if not parts: 

42 return {'type': 'not_applicable'} 

43 

44 if len(parts) > 1: 

45 # TODO: include context in error message 

46 return {'type': 'invalid', 'error': f'Encountered multiple `@Python` declarations'} 

47 

48 start, end = parts[0] 

49 

50 snippet = text[start:end] 

51 

52 try: 

53 parsed = schema_extension_parser.parse(snippet) 

54 except UnexpectedInput as exc: 

55 return {'type': 'invalid', 'error': str(exc) + exc.get_context(snippet)} 

56 

57 transformed = transformer.transform(parsed) 

58 return {'type': 'ok', 'value': transformed} 

59 

60 

61def scan_for_declarations(text: str) -> list[tuple[int, int]]: 

62 """Returns a list of (start, end) of parts of the text that 

63 look like `@Python(...)`. 

64 

65 Note: this is just needed until Lark provides a more complete 

66 way to scan for a grammar and also provide syntax errors. 

67 

68 https://github.com/lark-parser/lark/discussions/1390#discussioncomment-8354420 

69 """ 

70 return [indices for indices, _ in scanner.scan(text)] 

71 

72 

73class ParseResultOk(TypedDict): 

74 type: Literal['ok'] 

75 value: TransformResult 

76 

77 

78class ParseResultInvalid(TypedDict): 

79 type: Literal['invalid'] 

80 error: str 

81 

82 

83class ParseResultNotApplicable(TypedDict): 

84 type: Literal['not_applicable'] 

85 

86 

87ParseResult = Union[ParseResultOk, ParseResultInvalid, ParseResultNotApplicable]