Coverage for src/lexigram/web/cli/generators/graphql.py: 29%

41 statements  

« prev     ^ index     » next       coverage.py v7.15.4, created at 2026-08-25 04:37 +0800

1"""GraphQL generator for the web package.""" 

2 

3from __future__ import annotations 

4 

5from typing import Any 

6 

7from lexigram.codegen import GenerationResult, GeneratorBase, parse_fields 

8 

9GRAPHQL_TYPE_MAP = { 

10 "str": "str", 

11 "string": "str", 

12 "int": "int", 

13 "integer": "int", 

14 "float": "float", 

15 "bool": "bool", 

16 "boolean": "bool", 

17 "datetime": "datetime", 

18 "text": "str", 

19} 

20 

21SAMPLE_VALUES = { 

22 "str": '"sample_string"', 

23 "string": '"sample_string"', 

24 "int": "1", 

25 "integer": "1", 

26 "float": "1.5", 

27 "bool": "True", 

28 "boolean": "True", 

29 "datetime": "datetime.now()", 

30 "text": '"sample text"', 

31} 

32 

33 

34class GraphQLGenerator(GeneratorBase): 

35 """Generate a GraphQL schema scaffold.""" 

36 

37 template_name = "graphql.py.jinja2" 

38 

39 def __init__(self, output_dir: str = "src/graphql") -> None: 

40 super().__init__(output_dir=output_dir) 

41 

42 def generate( 

43 self, 

44 name: str, 

45 *, 

46 fields_str: str | None = None, 

47 doc: str | None = None, 

48 dry_run: bool = False, 

49 force: bool = False, 

50 **options: object, 

51 ) -> GenerationResult: 

52 type_name = self._to_snake_case(name) 

53 resource_name = self._pluralize(type_name) 

54 file_path = self.output_dir / f"{type_name}.py" 

55 fields = parse_fields(fields_str or "") 

56 

57 prepared_fields: list[dict[str, Any]] = [] 

58 required_fields: list[dict[str, Any]] = [] 

59 optional_fields: list[dict[str, Any]] = [] 

60 filterable_fields: list[dict[str, Any]] = [] 

61 

62 for field in fields: 

63 prepared_field = { 

64 "name": field.name, 

65 "type": field.type, 

66 "graphql_type": GRAPHQL_TYPE_MAP.get(field.type, "str"), 

67 "description": f"The {field.name} field", 

68 "sample_value": SAMPLE_VALUES.get(field.type, '"sample"'), 

69 } 

70 prepared_fields.append(prepared_field) 

71 if field.required: 

72 required_fields.append(prepared_field) 

73 else: 

74 optional_fields.append(prepared_field) 

75 if field.type in {"str", "int", "bool"}: 

76 filterable_fields.append(prepared_field) 

77 

78 if not prepared_fields: 

79 prepared_fields = [ 

80 { 

81 "name": "name", 

82 "type": "str", 

83 "graphql_type": "str", 

84 "description": "The name", 

85 "sample_value": '"Test"', 

86 }, 

87 { 

88 "name": "active", 

89 "type": "bool", 

90 "graphql_type": "bool", 

91 "description": "Is active", 

92 "sample_value": "True", 

93 }, 

94 ] 

95 required_fields = [prepared_fields[0]] 

96 optional_fields = [prepared_fields[1]] 

97 filterable_fields = list(prepared_fields) 

98 

99 content = self.render_template( 

100 self.template_name, 

101 { 

102 "name": name, 

103 "class_name": self._to_pascal_case(name), 

104 "resource_name": resource_name, 

105 "doc": doc, 

106 "fields": prepared_fields, 

107 "required_fields": required_fields, 

108 "optional_fields": optional_fields, 

109 "filterable_fields": filterable_fields, 

110 "has_timestamps": True, 

111 }, 

112 ) 

113 return self.write_file(file_path, content, dry_run=dry_run, force=force) 

114 

115 @staticmethod 

116 def _pluralize(value: str) -> str: 

117 if value.endswith("y") and value[-2:-1] not in {"a", "e", "i", "o", "u"}: 

118 return f"{value[:-1]}ies" 

119 if value.endswith("s"): 

120 return value 

121 return f"{value}s" 

122 

123 

124__all__ = ["GraphQLGenerator"]