struct-frame 0.0.25__py3-none-any.whl → 0.0.28__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.
Potentially problematic release.
This version of struct-frame might be problematic. Click here for more details.
- struct_frame/__init__.py +2 -1
- struct_frame/c_gen.py +72 -9
- struct_frame/generate.py +215 -9
- struct_frame/gql_gen.py +207 -0
- struct_frame/py_gen.py +79 -6
- struct_frame/ts_gen.py +83 -15
- struct_frame-0.0.28.dist-info/METADATA +311 -0
- struct_frame-0.0.28.dist-info/RECORD +12 -0
- struct_frame/boilerplate/c/struct_frame.h +0 -103
- struct_frame/boilerplate/c/struct_frame_cpp.h +0 -41
- struct_frame/boilerplate/c/struct_frame_gen.h +0 -1
- struct_frame/boilerplate/c/struct_frame_parser.h +0 -101
- struct_frame/boilerplate/c/struct_frame_types.h +0 -67
- struct_frame/boilerplate/py/__init__.py +0 -0
- struct_frame/boilerplate/py/struct_frame_parser.py +0 -118
- struct_frame/boilerplate/ts/struct_frame.ts +0 -65
- struct_frame/boilerplate/ts/struct_frame_gen.ts +0 -7
- struct_frame/boilerplate/ts/struct_frame_parser.ts +0 -98
- struct_frame/boilerplate/ts/struct_frame_types.ts +0 -80
- struct_frame-0.0.25.dist-info/METADATA +0 -29
- struct_frame-0.0.25.dist-info/RECORD +0 -22
- {struct_frame-0.0.25.dist-info → struct_frame-0.0.28.dist-info}/WHEEL +0 -0
- {struct_frame-0.0.25.dist-info → struct_frame-0.0.28.dist-info}/licenses/LICENSE +0 -0
struct_frame/__init__.py
CHANGED
|
@@ -3,8 +3,9 @@ from .base import version, NamingStyleC, CamelToSnakeCase, pascalCase
|
|
|
3
3
|
from .c_gen import FileCGen
|
|
4
4
|
from .ts_gen import FileTsGen
|
|
5
5
|
from .py_gen import FilePyGen
|
|
6
|
+
from .gql_gen import FileGqlGen
|
|
6
7
|
|
|
7
8
|
from .generate import main
|
|
8
9
|
|
|
9
|
-
__all__ = ["main", "FileCGen", "FileTsGen", "FilePyGen", "version",
|
|
10
|
+
__all__ = ["main", "FileCGen", "FileTsGen", "FilePyGen", "FileGqlGen", "version",
|
|
10
11
|
"NamingStyleC", "CamelToSnakeCase", "pascalCase"]
|
struct_frame/c_gen.py
CHANGED
|
@@ -17,6 +17,7 @@ c_types = {"uint8": "uint8_t",
|
|
|
17
17
|
"double": "double",
|
|
18
18
|
"uint64": 'uint64_t',
|
|
19
19
|
"int64": 'int64_t',
|
|
20
|
+
"string": "char", # Add string type support
|
|
20
21
|
}
|
|
21
22
|
|
|
22
23
|
|
|
@@ -60,6 +61,16 @@ class EnumCGen():
|
|
|
60
61
|
result += ' %s;\n' % (enumName)
|
|
61
62
|
|
|
62
63
|
result += 'typedef uint8_t %s_t;' % (enumName)
|
|
64
|
+
|
|
65
|
+
# Add module-prefixed enum constants for compatibility
|
|
66
|
+
result += '\n\n/* Enum constants with module prefix */\n'
|
|
67
|
+
module_prefix = CamelToSnakeCase(field.package).upper()
|
|
68
|
+
for d in field.data:
|
|
69
|
+
# Use the already correct enum constant name
|
|
70
|
+
enum_constant = f"{CamelToSnakeCase(field.name).upper()}_{StyleC.enum_entry(d)}"
|
|
71
|
+
module_constant = f"{module_prefix}_{enum_constant}"
|
|
72
|
+
result += f'#define {module_constant:<35} {enum_constant}\n'
|
|
73
|
+
|
|
63
74
|
return result
|
|
64
75
|
|
|
65
76
|
|
|
@@ -67,18 +78,70 @@ class FieldCGen():
|
|
|
67
78
|
@staticmethod
|
|
68
79
|
def generate(field):
|
|
69
80
|
result = ''
|
|
70
|
-
|
|
71
81
|
var_name = field.name
|
|
72
82
|
type_name = field.fieldType
|
|
83
|
+
|
|
84
|
+
# Handle basic type resolution
|
|
73
85
|
if type_name in c_types:
|
|
74
|
-
|
|
86
|
+
base_type = c_types[type_name]
|
|
75
87
|
else:
|
|
76
|
-
type_name = '%s%s' % (pascalCase(field.package), type_name)
|
|
77
88
|
if field.isEnum:
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
89
|
+
base_type = '%s%s_t' % (pascalCase(field.package), type_name)
|
|
90
|
+
else:
|
|
91
|
+
base_type = '%s%s' % (pascalCase(field.package), type_name)
|
|
92
|
+
|
|
93
|
+
# Handle arrays
|
|
94
|
+
if field.is_array:
|
|
95
|
+
if field.fieldType == "string":
|
|
96
|
+
# String arrays need both array size and individual string size
|
|
97
|
+
if field.size_option is not None:
|
|
98
|
+
# Fixed string array: size_option strings, each element_size chars
|
|
99
|
+
declaration = f"char {var_name}[{field.size_option}][{field.element_size}];"
|
|
100
|
+
comment = f" // Fixed string array: {field.size_option} strings, each max {field.element_size} chars"
|
|
101
|
+
elif field.max_size is not None:
|
|
102
|
+
# Variable string array: count byte + max_size strings of element_size chars each
|
|
103
|
+
declaration = f"struct {{ uint8_t count; char data[{field.max_size}][{field.element_size}]; }} {var_name};"
|
|
104
|
+
comment = f" // Variable string array: up to {field.max_size} strings, each max {field.element_size} chars"
|
|
105
|
+
else:
|
|
106
|
+
declaration = f"char {var_name}[1][1];" # Fallback
|
|
107
|
+
comment = " // String array (error in size specification)"
|
|
108
|
+
else:
|
|
109
|
+
# Non-string arrays
|
|
110
|
+
if field.size_option is not None:
|
|
111
|
+
# Fixed array: always exact size
|
|
112
|
+
declaration = f"{base_type} {var_name}[{field.size_option}];"
|
|
113
|
+
comment = f" // Fixed array: always {field.size_option} elements"
|
|
114
|
+
elif field.max_size is not None:
|
|
115
|
+
# Variable array: count byte + max elements
|
|
116
|
+
declaration = f"struct {{ uint8_t count; {base_type} data[{field.max_size}]; }} {var_name};"
|
|
117
|
+
comment = f" // Variable array: up to {field.max_size} elements"
|
|
118
|
+
else:
|
|
119
|
+
declaration = f"{base_type} {var_name}[1];" # Fallback
|
|
120
|
+
comment = " // Array (error in size specification)"
|
|
121
|
+
|
|
122
|
+
result += f" {declaration}{comment}"
|
|
123
|
+
|
|
124
|
+
# Handle regular strings
|
|
125
|
+
elif field.fieldType == "string":
|
|
126
|
+
if field.size_option is not None:
|
|
127
|
+
# Fixed string: exactly size_option characters
|
|
128
|
+
declaration = f"char {var_name}[{field.size_option}];"
|
|
129
|
+
comment = f" // Fixed string: exactly {field.size_option} chars"
|
|
130
|
+
elif field.max_size is not None:
|
|
131
|
+
# Variable string: length byte + max characters
|
|
132
|
+
declaration = f"struct {{ uint8_t length; char data[{field.max_size}]; }} {var_name};"
|
|
133
|
+
comment = f" // Variable string: up to {field.max_size} chars"
|
|
134
|
+
else:
|
|
135
|
+
declaration = f"char {var_name}[1];" # Fallback
|
|
136
|
+
comment = " // String (error in size specification)"
|
|
137
|
+
|
|
138
|
+
result += f" {declaration}{comment}"
|
|
139
|
+
|
|
140
|
+
# Handle regular fields
|
|
141
|
+
else:
|
|
142
|
+
result += f" {base_type} {var_name};"
|
|
81
143
|
|
|
144
|
+
# Add leading comments
|
|
82
145
|
leading_comment = field.comments
|
|
83
146
|
if leading_comment:
|
|
84
147
|
for c in leading_comment:
|
|
@@ -178,12 +241,12 @@ class FileCGen():
|
|
|
178
241
|
# yield '\n'
|
|
179
242
|
|
|
180
243
|
if package.messages:
|
|
181
|
-
yield '
|
|
244
|
+
yield 'bool get_message_length(size_t msg_id, size_t* size){\n switch (msg_id)\n {\n'
|
|
182
245
|
for key, msg in package.sortedMessages().items():
|
|
183
246
|
name = '%s_%s' % (CamelToSnakeCase(
|
|
184
247
|
msg.package).upper(), CamelToSnakeCase(msg.name).upper())
|
|
185
248
|
if msg.id:
|
|
186
|
-
yield ' case %s_MSG_ID:
|
|
249
|
+
yield ' case %s_MSG_ID: *size = %s_MAX_SIZE; return true;\n' % (name, name)
|
|
187
250
|
|
|
188
|
-
yield ' default: break;\n } return
|
|
251
|
+
yield ' default: break;\n } return false;\n}'
|
|
189
252
|
yield '\n'
|
struct_frame/generate.py
CHANGED
|
@@ -7,8 +7,10 @@ import shutil
|
|
|
7
7
|
from struct_frame import FileCGen
|
|
8
8
|
from struct_frame import FileTsGen
|
|
9
9
|
from struct_frame import FilePyGen
|
|
10
|
+
from struct_frame import FileGqlGen
|
|
10
11
|
from proto_schema_parser.parser import Parser
|
|
11
12
|
from proto_schema_parser import ast
|
|
13
|
+
from proto_schema_parser.ast import FieldCardinality
|
|
12
14
|
|
|
13
15
|
import argparse
|
|
14
16
|
|
|
@@ -26,7 +28,8 @@ default_types = {
|
|
|
26
28
|
"float": {"size": 4},
|
|
27
29
|
"double": {"size": 8},
|
|
28
30
|
"int64": {"size": 8},
|
|
29
|
-
"uint64": {"size": 8}
|
|
31
|
+
"uint64": {"size": 8},
|
|
32
|
+
"string": {"size": 4} # Variable length, estimated size for length prefix
|
|
30
33
|
}
|
|
31
34
|
|
|
32
35
|
|
|
@@ -79,14 +82,78 @@ class Field:
|
|
|
79
82
|
self.comments = comments
|
|
80
83
|
self.package = package
|
|
81
84
|
self.isEnum = False
|
|
85
|
+
self.flatten = False
|
|
86
|
+
self.is_array = False
|
|
87
|
+
self.size_option = None # Fixed size using [size=X]
|
|
88
|
+
self.max_size = None # Variable size using [max_size=X]
|
|
89
|
+
# Element size for repeated string arrays [element_size=X]
|
|
90
|
+
self.element_size = None
|
|
82
91
|
|
|
83
92
|
def parse(self, field):
|
|
84
93
|
self.name = field.name
|
|
85
94
|
self.fieldType = field.type
|
|
95
|
+
|
|
96
|
+
# Check if this is a repeated field (array)
|
|
97
|
+
if hasattr(field, 'cardinality') and field.cardinality == FieldCardinality.REPEATED:
|
|
98
|
+
self.is_array = True
|
|
99
|
+
|
|
86
100
|
if self.fieldType in default_types:
|
|
87
101
|
self.isDefaultType = True
|
|
88
102
|
self.size = default_types[self.fieldType]["size"]
|
|
89
103
|
self.validated = True
|
|
104
|
+
|
|
105
|
+
try:
|
|
106
|
+
if hasattr(field, 'options') and field.options:
|
|
107
|
+
# options is typically a list of ast.Option
|
|
108
|
+
for opt in field.options:
|
|
109
|
+
oname = getattr(opt, 'name', None)
|
|
110
|
+
ovalue = getattr(opt, 'value', None)
|
|
111
|
+
if not oname:
|
|
112
|
+
continue
|
|
113
|
+
lname = str(oname).strip()
|
|
114
|
+
# Support unqualified and a couple of qualified names
|
|
115
|
+
if lname in ('flatten', '(sf.flatten)', '(struct_frame.flatten)'):
|
|
116
|
+
sval = str(ovalue).strip().lower()
|
|
117
|
+
if sval in ('true', '1', 'yes', 'on') or ovalue is True:
|
|
118
|
+
self.flatten = True
|
|
119
|
+
elif lname in ('size', '(sf.size)', '(struct_frame.size)'):
|
|
120
|
+
# Fixed size for arrays or strings
|
|
121
|
+
try:
|
|
122
|
+
self.size_option = int(ovalue)
|
|
123
|
+
if self.size_option <= 0 or self.size_option > 255:
|
|
124
|
+
print(
|
|
125
|
+
f"Invalid size {self.size_option} for field {self.name}, must be 1-255")
|
|
126
|
+
return False
|
|
127
|
+
except (ValueError, TypeError):
|
|
128
|
+
print(
|
|
129
|
+
f"Invalid size value {ovalue} for field {self.name}, must be an integer")
|
|
130
|
+
return False
|
|
131
|
+
elif lname in ('max_size', '(sf.max_size)', '(struct_frame.max_size)'):
|
|
132
|
+
# Variable size for arrays or strings
|
|
133
|
+
try:
|
|
134
|
+
self.max_size = int(ovalue)
|
|
135
|
+
if self.max_size <= 0 or self.max_size > 255:
|
|
136
|
+
print(
|
|
137
|
+
f"Invalid max_size {self.max_size} for field {self.name}, must be 1-255")
|
|
138
|
+
return False
|
|
139
|
+
except (ValueError, TypeError):
|
|
140
|
+
print(
|
|
141
|
+
f"Invalid max_size value {ovalue} for field {self.name}, must be an integer")
|
|
142
|
+
return False
|
|
143
|
+
elif lname in ('element_size', '(sf.element_size)', '(struct_frame.element_size)'):
|
|
144
|
+
# Individual element size for repeated string arrays
|
|
145
|
+
try:
|
|
146
|
+
self.element_size = int(ovalue)
|
|
147
|
+
if self.element_size <= 0 or self.element_size > 255:
|
|
148
|
+
print(
|
|
149
|
+
f"Invalid element_size {self.element_size} for field {self.name}, must be 1-255")
|
|
150
|
+
return False
|
|
151
|
+
except (ValueError, TypeError):
|
|
152
|
+
print(
|
|
153
|
+
f"Invalid element_size value {ovalue} for field {self.name}, must be an integer")
|
|
154
|
+
return False
|
|
155
|
+
except Exception:
|
|
156
|
+
pass
|
|
90
157
|
return True
|
|
91
158
|
|
|
92
159
|
def validate(self, currentPackage, packages):
|
|
@@ -99,8 +166,8 @@ class Field:
|
|
|
99
166
|
if ret:
|
|
100
167
|
if ret.validate(currentPackage, packages):
|
|
101
168
|
self.isEnum = ret.isEnum
|
|
102
|
-
self.
|
|
103
|
-
|
|
169
|
+
self.validated = True
|
|
170
|
+
base_size = ret.size
|
|
104
171
|
else:
|
|
105
172
|
print(
|
|
106
173
|
f"Failed to validate Field: {self.name} of Type: {self.fieldType} in Package: {currentPackage.name}")
|
|
@@ -109,6 +176,77 @@ class Field:
|
|
|
109
176
|
print(
|
|
110
177
|
f"Failed to find Field: {self.name} of Type: {self.fieldType} in Package: {currentPackage.name}")
|
|
111
178
|
return False
|
|
179
|
+
else:
|
|
180
|
+
base_size = self.size
|
|
181
|
+
|
|
182
|
+
# Calculate size for arrays and strings
|
|
183
|
+
if self.is_array:
|
|
184
|
+
if self.fieldType == "string":
|
|
185
|
+
# String arrays need both array size AND individual element size
|
|
186
|
+
if self.element_size is None:
|
|
187
|
+
print(
|
|
188
|
+
f"String array field {self.name} missing required element_size option")
|
|
189
|
+
return False
|
|
190
|
+
|
|
191
|
+
if self.size_option is not None:
|
|
192
|
+
# Fixed string array: size_option strings, each element_size bytes
|
|
193
|
+
self.size = self.size_option * self.element_size
|
|
194
|
+
elif self.max_size is not None:
|
|
195
|
+
# Variable string array: 1 byte count + max_size strings of element_size bytes each
|
|
196
|
+
self.size = 1 + (self.max_size * self.element_size)
|
|
197
|
+
else:
|
|
198
|
+
print(
|
|
199
|
+
f"String array field {self.name} missing required size or max_size option")
|
|
200
|
+
return False
|
|
201
|
+
else:
|
|
202
|
+
# Non-string arrays
|
|
203
|
+
if self.size_option is not None:
|
|
204
|
+
# Fixed array: always full, no count byte needed
|
|
205
|
+
self.size = base_size * self.size_option
|
|
206
|
+
elif self.max_size is not None:
|
|
207
|
+
# Variable array: 1 byte for count + max space
|
|
208
|
+
self.size = 1 + (base_size * self.max_size)
|
|
209
|
+
else:
|
|
210
|
+
print(
|
|
211
|
+
f"Array field {self.name} missing required size or max_size option")
|
|
212
|
+
return False
|
|
213
|
+
elif self.fieldType == "string":
|
|
214
|
+
if self.size_option is not None:
|
|
215
|
+
# Fixed string: exactly size_option characters
|
|
216
|
+
self.size = self.size_option
|
|
217
|
+
elif self.max_size is not None:
|
|
218
|
+
# Variable string: 1 byte length + max characters
|
|
219
|
+
self.size = 1 + self.max_size
|
|
220
|
+
else:
|
|
221
|
+
print(
|
|
222
|
+
f"String field {self.name} missing required size or max_size option")
|
|
223
|
+
return False
|
|
224
|
+
else:
|
|
225
|
+
self.size = base_size
|
|
226
|
+
|
|
227
|
+
# Debug output
|
|
228
|
+
array_info = ""
|
|
229
|
+
if self.is_array:
|
|
230
|
+
if self.fieldType == "string":
|
|
231
|
+
# String arrays show both array size and individual element size
|
|
232
|
+
if self.size_option is not None:
|
|
233
|
+
array_info = f", fixed_string_array size={self.size_option}, element_size={self.element_size}"
|
|
234
|
+
elif self.max_size is not None:
|
|
235
|
+
array_info = f", bounded_string_array max_size={self.max_size}, element_size={self.element_size}"
|
|
236
|
+
else:
|
|
237
|
+
# Regular arrays
|
|
238
|
+
if self.size_option is not None:
|
|
239
|
+
array_info = f", fixed_array size={self.size_option}"
|
|
240
|
+
elif self.max_size is not None:
|
|
241
|
+
array_info = f", bounded_array max_size={self.max_size}"
|
|
242
|
+
elif self.fieldType == "string":
|
|
243
|
+
# Regular strings
|
|
244
|
+
if self.size_option is not None:
|
|
245
|
+
array_info = f", fixed_string size={self.size_option}"
|
|
246
|
+
elif self.max_size is not None:
|
|
247
|
+
array_info = f", variable_string max_size={self.max_size}"
|
|
248
|
+
print(
|
|
249
|
+
f" Field {self.name}: type={self.fieldType}, is_array={self.is_array}{array_info}, calculated_size={self.size}")
|
|
112
250
|
|
|
113
251
|
return True
|
|
114
252
|
|
|
@@ -116,8 +254,21 @@ class Field:
|
|
|
116
254
|
output = ""
|
|
117
255
|
for c in self.comments:
|
|
118
256
|
output = output + c + "\n"
|
|
257
|
+
array_info = ""
|
|
258
|
+
if self.is_array:
|
|
259
|
+
if self.size_option is not None:
|
|
260
|
+
array_info = f", Array[size={self.size_option}]"
|
|
261
|
+
elif self.max_size is not None:
|
|
262
|
+
array_info = f", Array[max_size={self.max_size}]"
|
|
263
|
+
else:
|
|
264
|
+
array_info = ", Array[no size specified]"
|
|
265
|
+
elif self.fieldType == "string":
|
|
266
|
+
if self.size_option is not None:
|
|
267
|
+
array_info = f", String[size={self.size_option}]"
|
|
268
|
+
elif self.max_size is not None:
|
|
269
|
+
array_info = f", String[max_size={self.max_size}]"
|
|
119
270
|
output = output + \
|
|
120
|
-
f"Field: {self.name}, Type:{self.fieldType}, Size:{self.size}"
|
|
271
|
+
f"Field: {self.name}, Type:{self.fieldType}, Size:{self.size}{array_info}"
|
|
121
272
|
return output
|
|
122
273
|
|
|
123
274
|
|
|
@@ -166,6 +317,44 @@ class Message:
|
|
|
166
317
|
return False
|
|
167
318
|
self.size = self.size + value.size
|
|
168
319
|
|
|
320
|
+
# Flatten collision detection: if a field is marked as flatten and is a message,
|
|
321
|
+
# ensure none of the child field names collide with fields in this message.
|
|
322
|
+
parent_field_names = set(self.fields.keys())
|
|
323
|
+
for key, value in self.fields.items():
|
|
324
|
+
if getattr(value, 'flatten', False):
|
|
325
|
+
# Only meaningful for non-default, non-enum message types
|
|
326
|
+
if value.isDefaultType or value.isEnum:
|
|
327
|
+
# Flatten has no effect on primitives/enums; skip
|
|
328
|
+
continue
|
|
329
|
+
child = currentPackage.findFieldType(value.fieldType)
|
|
330
|
+
if not child or getattr(child, 'isEnum', False) or not hasattr(child, 'fields'):
|
|
331
|
+
# Unknown or non-message type; skip
|
|
332
|
+
continue
|
|
333
|
+
for ck in child.fields.keys():
|
|
334
|
+
if ck in parent_field_names:
|
|
335
|
+
print(
|
|
336
|
+
f"Flatten collision in Message {self.name}: field '{key}.{ck}' collides with existing field '{ck}'.")
|
|
337
|
+
return False
|
|
338
|
+
|
|
339
|
+
# Array validation
|
|
340
|
+
for key, value in self.fields.items():
|
|
341
|
+
if value.is_array:
|
|
342
|
+
# All arrays must have size or max_size specified
|
|
343
|
+
if value.size_option is None and value.max_size is None:
|
|
344
|
+
print(
|
|
345
|
+
f"Array field {key} in Message {self.name}: must specify size or max_size option")
|
|
346
|
+
return False
|
|
347
|
+
elif value.fieldType == "string":
|
|
348
|
+
# Strings must have size or max_size specified
|
|
349
|
+
if value.size_option is None and value.max_size is None:
|
|
350
|
+
print(
|
|
351
|
+
f"String field {key} in Message {self.name}: must specify size or max_size option")
|
|
352
|
+
return False
|
|
353
|
+
elif value.max_size is not None or value.size_option is not None or value.element_size is not None:
|
|
354
|
+
print(
|
|
355
|
+
f"Field {key} in Message {self.name}: size/max_size/element_size options can only be used with repeated fields or strings")
|
|
356
|
+
return False
|
|
357
|
+
|
|
169
358
|
self.validated = True
|
|
170
359
|
return True
|
|
171
360
|
|
|
@@ -260,9 +449,12 @@ parser.add_argument('--debug', action='store_true')
|
|
|
260
449
|
parser.add_argument('--build_c', action='store_true')
|
|
261
450
|
parser.add_argument('--build_ts', action='store_true')
|
|
262
451
|
parser.add_argument('--build_py', action='store_true')
|
|
263
|
-
parser.add_argument('--c_path', nargs=1, type=str, default=['c/'])
|
|
264
|
-
parser.add_argument('--ts_path', nargs=1, type=str, default=['ts/'])
|
|
265
|
-
parser.add_argument('--py_path', nargs=1, type=str, default=['py/'])
|
|
452
|
+
parser.add_argument('--c_path', nargs=1, type=str, default=['generated/c/'])
|
|
453
|
+
parser.add_argument('--ts_path', nargs=1, type=str, default=['generated/ts/'])
|
|
454
|
+
parser.add_argument('--py_path', nargs=1, type=str, default=['generated/py/'])
|
|
455
|
+
parser.add_argument('--build_gql', action='store_true')
|
|
456
|
+
parser.add_argument('--gql_path', nargs=1, type=str,
|
|
457
|
+
default=['generated/gql/'])
|
|
266
458
|
|
|
267
459
|
|
|
268
460
|
def parseFile(filename):
|
|
@@ -350,15 +542,21 @@ def main():
|
|
|
350
542
|
args = parser.parse_args()
|
|
351
543
|
parseFile(args.filename)
|
|
352
544
|
|
|
353
|
-
if (not args.build_c and not args.build_ts and not args.build_py):
|
|
545
|
+
if (not args.build_c and not args.build_ts and not args.build_py and not args.build_gql):
|
|
354
546
|
print("Select at least one build argument")
|
|
355
547
|
return
|
|
356
548
|
|
|
549
|
+
valid = False
|
|
357
550
|
try:
|
|
358
|
-
validatePackages()
|
|
551
|
+
valid = validatePackages()
|
|
359
552
|
except RecursionError as err:
|
|
360
553
|
print(
|
|
361
554
|
f'Recursion Error. Messages most likely have a cyclical dependancy. Check Message: {recErrCurrentMessage} and Field: {recErrCurrentField}')
|
|
555
|
+
return
|
|
556
|
+
|
|
557
|
+
if not valid:
|
|
558
|
+
print("Validation failed; aborting code generation.")
|
|
559
|
+
return
|
|
362
560
|
|
|
363
561
|
files = {}
|
|
364
562
|
if (args.build_c):
|
|
@@ -370,6 +568,12 @@ def main():
|
|
|
370
568
|
if (args.build_py):
|
|
371
569
|
files.update(generatePyFileStrings(args.py_path[0]))
|
|
372
570
|
|
|
571
|
+
if (args.build_gql):
|
|
572
|
+
for key, value in packages.items():
|
|
573
|
+
name = os.path.join(args.gql_path[0], value.name + '.graphql')
|
|
574
|
+
data = ''.join(FileGqlGen.generate(value))
|
|
575
|
+
files[name] = data
|
|
576
|
+
|
|
373
577
|
for filename, filedata in files.items():
|
|
374
578
|
dirname = os.path.dirname(filename)
|
|
375
579
|
if dirname and not os.path.exists(dirname):
|
|
@@ -392,6 +596,8 @@ def main():
|
|
|
392
596
|
shutil.copytree(os.path.join(dir_path, "boilerplate/py"),
|
|
393
597
|
args.py_path[0], dirs_exist_ok=True)
|
|
394
598
|
|
|
599
|
+
# No boilerplate for GraphQL currently
|
|
600
|
+
|
|
395
601
|
if args.debug:
|
|
396
602
|
printPackages()
|
|
397
603
|
print("Struct Frame successfully completed")
|
struct_frame/gql_gen.py
ADDED
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# Simple GraphQL schema generator for struct-frame
|
|
3
|
+
|
|
4
|
+
from struct_frame import version, pascalCase, CamelToSnakeCase
|
|
5
|
+
import time
|
|
6
|
+
|
|
7
|
+
# Mapping from proto primitive types to GraphQL scalar types
|
|
8
|
+
gql_types = {
|
|
9
|
+
"uint8": "Int",
|
|
10
|
+
"int8": "Int",
|
|
11
|
+
"uint16": "Int",
|
|
12
|
+
"int16": "Int",
|
|
13
|
+
"uint32": "Int",
|
|
14
|
+
"int32": "Int",
|
|
15
|
+
"uint64": "Int", # Could be custom scalar if needed
|
|
16
|
+
"int64": "Int", # Could be custom scalar if needed
|
|
17
|
+
"bool": "Boolean",
|
|
18
|
+
"float": "Float",
|
|
19
|
+
"double": "Float",
|
|
20
|
+
"string": "String",
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _gql_enum_value_name(name: str) -> str:
|
|
25
|
+
# If already in ALL_CAPS (possibly with underscores) keep as is
|
|
26
|
+
if name.replace('_', '').isupper():
|
|
27
|
+
return name
|
|
28
|
+
return CamelToSnakeCase(name).upper()
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _clean_comment_line(c: str) -> str:
|
|
32
|
+
c = c.strip()
|
|
33
|
+
if c.startswith('#'):
|
|
34
|
+
c = c[1:].strip()
|
|
35
|
+
# Remove leading // once or twice
|
|
36
|
+
if c.startswith('//'):
|
|
37
|
+
c = c[2:].strip()
|
|
38
|
+
# If parser already kept leading markers inside line, remove repeated
|
|
39
|
+
if c.startswith('//'):
|
|
40
|
+
c = c[2:].strip()
|
|
41
|
+
return c
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _triple_quote_block(lines):
|
|
45
|
+
cleaned = [_clean_comment_line(l) for l in lines if _clean_comment_line(l)]
|
|
46
|
+
if not cleaned:
|
|
47
|
+
return None
|
|
48
|
+
return '"""\n' + '\n'.join(cleaned) + '\n"""'
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _single_quote_line(lines):
|
|
52
|
+
cleaned = [_clean_comment_line(l) for l in lines if _clean_comment_line(l)]
|
|
53
|
+
if not cleaned:
|
|
54
|
+
return None
|
|
55
|
+
# Join multi-line into one sentence for single-line description
|
|
56
|
+
return '"' + ' '.join(cleaned) + '"'
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class EnumGqlGen:
|
|
60
|
+
@staticmethod
|
|
61
|
+
def generate(enum):
|
|
62
|
+
lines = []
|
|
63
|
+
if enum.comments:
|
|
64
|
+
desc = _triple_quote_block(enum.comments)
|
|
65
|
+
if desc:
|
|
66
|
+
lines.append(desc)
|
|
67
|
+
enum_name = f"{pascalCase(enum.package)}{enum.name}"
|
|
68
|
+
lines.append(f"enum {enum_name} {{")
|
|
69
|
+
for key, value in enum.data.items():
|
|
70
|
+
if value[1]:
|
|
71
|
+
desc = _single_quote_line(value[1])
|
|
72
|
+
if desc:
|
|
73
|
+
lines.append(f" {desc}")
|
|
74
|
+
lines.append(f" {_gql_enum_value_name(key)}")
|
|
75
|
+
lines.append("}\n")
|
|
76
|
+
return '\n'.join(lines)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
class FieldGqlGen:
|
|
80
|
+
@staticmethod
|
|
81
|
+
def type_name(field):
|
|
82
|
+
t = field.fieldType
|
|
83
|
+
base_type = gql_types.get(t, f"{pascalCase(field.package)}{t}")
|
|
84
|
+
|
|
85
|
+
# Handle arrays
|
|
86
|
+
if getattr(field, 'is_array', False):
|
|
87
|
+
# Arrays in GraphQL are represented as [Type!]! for non-null arrays of non-null elements
|
|
88
|
+
# or [Type] for nullable arrays, etc. We'll use [Type!]! as the standard
|
|
89
|
+
return f"[{base_type}!]!"
|
|
90
|
+
|
|
91
|
+
return base_type
|
|
92
|
+
|
|
93
|
+
@staticmethod
|
|
94
|
+
def generate(field, name_override=None):
|
|
95
|
+
lines = []
|
|
96
|
+
|
|
97
|
+
# Generate clean comments with size information, preferring our generated descriptions over proto comments
|
|
98
|
+
if getattr(field, 'is_array', False):
|
|
99
|
+
# Array field - use our size descriptions
|
|
100
|
+
if getattr(field, 'size_option', None) is not None:
|
|
101
|
+
# Fixed array
|
|
102
|
+
if field.fieldType == "string":
|
|
103
|
+
comment_lines = [
|
|
104
|
+
f"Fixed string array: {field.size_option} strings, each {getattr(field, 'element_size', 'N/A')} chars"]
|
|
105
|
+
else:
|
|
106
|
+
comment_lines = [
|
|
107
|
+
f"Fixed array: always {field.size_option} elements"]
|
|
108
|
+
else:
|
|
109
|
+
# Variable array
|
|
110
|
+
if field.fieldType == "string":
|
|
111
|
+
comment_lines = [
|
|
112
|
+
f"Variable string array: up to {getattr(field, 'max_size', 'N/A')} strings, each max {getattr(field, 'element_size', 'N/A')} chars"]
|
|
113
|
+
else:
|
|
114
|
+
comment_lines = [
|
|
115
|
+
f"Variable array: up to {getattr(field, 'max_size', 'N/A')} elements"]
|
|
116
|
+
elif field.fieldType == "string":
|
|
117
|
+
# Non-array string field
|
|
118
|
+
if getattr(field, 'size_option', None) is not None:
|
|
119
|
+
comment_lines = [
|
|
120
|
+
f"Fixed string: exactly {field.size_option} characters"]
|
|
121
|
+
elif getattr(field, 'max_size', None) is not None:
|
|
122
|
+
comment_lines = [
|
|
123
|
+
f"Variable string: up to {field.max_size} characters"]
|
|
124
|
+
else:
|
|
125
|
+
comment_lines = field.comments[:] if field.comments else []
|
|
126
|
+
else:
|
|
127
|
+
# Regular field - use original comments
|
|
128
|
+
comment_lines = field.comments[:] if field.comments else []
|
|
129
|
+
|
|
130
|
+
if comment_lines:
|
|
131
|
+
desc = _single_quote_line(comment_lines)
|
|
132
|
+
if desc:
|
|
133
|
+
lines.append(f" {desc}")
|
|
134
|
+
|
|
135
|
+
fname = name_override if name_override else field.name
|
|
136
|
+
lines.append(f" {fname}: {FieldGqlGen.type_name(field)}")
|
|
137
|
+
return '\n'.join(lines)
|
|
138
|
+
|
|
139
|
+
@staticmethod
|
|
140
|
+
def generate_flattened_children(field, package, parent_msg):
|
|
141
|
+
# Expand a message-typed field into its child fields.
|
|
142
|
+
# If a child field name collides, raise an error and fail generation.
|
|
143
|
+
t = field.fieldType
|
|
144
|
+
child_msg = package.messages.get(t)
|
|
145
|
+
if not child_msg:
|
|
146
|
+
# Fallback to normal generation if unknown
|
|
147
|
+
return [FieldGqlGen.generate(field)]
|
|
148
|
+
|
|
149
|
+
out_lines = []
|
|
150
|
+
for ck, cf in child_msg.fields.items():
|
|
151
|
+
out_lines.append(FieldGqlGen.generate(cf, name_override=ck))
|
|
152
|
+
return out_lines
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
class MessageGqlGen:
|
|
156
|
+
@staticmethod
|
|
157
|
+
def generate(package, msg):
|
|
158
|
+
lines = []
|
|
159
|
+
if msg.comments:
|
|
160
|
+
desc = _triple_quote_block(msg.comments)
|
|
161
|
+
if desc:
|
|
162
|
+
lines.append(desc)
|
|
163
|
+
type_name = f"{pascalCase(msg.package)}{msg.name}"
|
|
164
|
+
lines.append(f"type {type_name} {{")
|
|
165
|
+
if not msg.fields:
|
|
166
|
+
lines.append(" _empty: Boolean")
|
|
167
|
+
else:
|
|
168
|
+
for key, f in msg.fields.items():
|
|
169
|
+
if getattr(f, 'flatten', False) and f.fieldType not in gql_types:
|
|
170
|
+
lines.extend(
|
|
171
|
+
FieldGqlGen.generate_flattened_children(f, package, msg))
|
|
172
|
+
else:
|
|
173
|
+
lines.append(FieldGqlGen.generate(f))
|
|
174
|
+
lines.append("}\n")
|
|
175
|
+
return '\n'.join(lines)
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
class FileGqlGen:
|
|
179
|
+
@staticmethod
|
|
180
|
+
def generate(package):
|
|
181
|
+
# Multiline triple-quoted header block
|
|
182
|
+
yield f"# Automatically generated GraphQL schema\n# Generated by struct-frame {version} at {time.asctime()}\n\n"
|
|
183
|
+
|
|
184
|
+
first_block = True
|
|
185
|
+
# Enums
|
|
186
|
+
for _, enum in package.enums.items():
|
|
187
|
+
if not first_block:
|
|
188
|
+
yield '\n'
|
|
189
|
+
first_block = False
|
|
190
|
+
yield EnumGqlGen.generate(enum).rstrip() + '\n'
|
|
191
|
+
|
|
192
|
+
# Messages (object types)
|
|
193
|
+
for _, msg in package.sortedMessages().items():
|
|
194
|
+
if not first_block:
|
|
195
|
+
yield '\n'
|
|
196
|
+
first_block = False
|
|
197
|
+
yield MessageGqlGen.generate(package, msg).rstrip() + '\n'
|
|
198
|
+
|
|
199
|
+
# Root Query type
|
|
200
|
+
if package.messages:
|
|
201
|
+
if not first_block:
|
|
202
|
+
yield '\n'
|
|
203
|
+
yield 'type Query {\n'
|
|
204
|
+
for _, msg in package.sortedMessages().items():
|
|
205
|
+
type_name = f"{pascalCase(msg.package)}{msg.name}"
|
|
206
|
+
yield f" {msg.name}: {type_name}\n"
|
|
207
|
+
yield '}\n'
|