struct-frame 0.0.27__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/c_gen.py +69 -6
- struct_frame/generate.py +199 -5
- struct_frame/gql_gen.py +72 -10
- struct_frame/py_gen.py +65 -11
- 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-0.0.27.dist-info/METADATA +0 -62
- struct_frame-0.0.27.dist-info/RECORD +0 -12
- {struct_frame-0.0.27.dist-info → struct_frame-0.0.28.dist-info}/WHEEL +0 -0
- {struct_frame-0.0.27.dist-info → struct_frame-0.0.28.dist-info}/licenses/LICENSE +0 -0
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:
|
struct_frame/generate.py
CHANGED
|
@@ -10,6 +10,7 @@ from struct_frame import FilePyGen
|
|
|
10
10
|
from struct_frame import FileGqlGen
|
|
11
11
|
from proto_schema_parser.parser import Parser
|
|
12
12
|
from proto_schema_parser import ast
|
|
13
|
+
from proto_schema_parser.ast import FieldCardinality
|
|
13
14
|
|
|
14
15
|
import argparse
|
|
15
16
|
|
|
@@ -27,7 +28,8 @@ default_types = {
|
|
|
27
28
|
"float": {"size": 4},
|
|
28
29
|
"double": {"size": 8},
|
|
29
30
|
"int64": {"size": 8},
|
|
30
|
-
"uint64": {"size": 8}
|
|
31
|
+
"uint64": {"size": 8},
|
|
32
|
+
"string": {"size": 4} # Variable length, estimated size for length prefix
|
|
31
33
|
}
|
|
32
34
|
|
|
33
35
|
|
|
@@ -80,14 +82,78 @@ class Field:
|
|
|
80
82
|
self.comments = comments
|
|
81
83
|
self.package = package
|
|
82
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
|
|
83
91
|
|
|
84
92
|
def parse(self, field):
|
|
85
93
|
self.name = field.name
|
|
86
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
|
+
|
|
87
100
|
if self.fieldType in default_types:
|
|
88
101
|
self.isDefaultType = True
|
|
89
102
|
self.size = default_types[self.fieldType]["size"]
|
|
90
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
|
|
91
157
|
return True
|
|
92
158
|
|
|
93
159
|
def validate(self, currentPackage, packages):
|
|
@@ -100,8 +166,8 @@ class Field:
|
|
|
100
166
|
if ret:
|
|
101
167
|
if ret.validate(currentPackage, packages):
|
|
102
168
|
self.isEnum = ret.isEnum
|
|
103
|
-
self.
|
|
104
|
-
|
|
169
|
+
self.validated = True
|
|
170
|
+
base_size = ret.size
|
|
105
171
|
else:
|
|
106
172
|
print(
|
|
107
173
|
f"Failed to validate Field: {self.name} of Type: {self.fieldType} in Package: {currentPackage.name}")
|
|
@@ -110,6 +176,77 @@ class Field:
|
|
|
110
176
|
print(
|
|
111
177
|
f"Failed to find Field: {self.name} of Type: {self.fieldType} in Package: {currentPackage.name}")
|
|
112
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}")
|
|
113
250
|
|
|
114
251
|
return True
|
|
115
252
|
|
|
@@ -117,8 +254,21 @@ class Field:
|
|
|
117
254
|
output = ""
|
|
118
255
|
for c in self.comments:
|
|
119
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}]"
|
|
120
270
|
output = output + \
|
|
121
|
-
f"Field: {self.name}, Type:{self.fieldType}, Size:{self.size}"
|
|
271
|
+
f"Field: {self.name}, Type:{self.fieldType}, Size:{self.size}{array_info}"
|
|
122
272
|
return output
|
|
123
273
|
|
|
124
274
|
|
|
@@ -167,6 +317,44 @@ class Message:
|
|
|
167
317
|
return False
|
|
168
318
|
self.size = self.size + value.size
|
|
169
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
|
+
|
|
170
358
|
self.validated = True
|
|
171
359
|
return True
|
|
172
360
|
|
|
@@ -358,11 +546,17 @@ def main():
|
|
|
358
546
|
print("Select at least one build argument")
|
|
359
547
|
return
|
|
360
548
|
|
|
549
|
+
valid = False
|
|
361
550
|
try:
|
|
362
|
-
validatePackages()
|
|
551
|
+
valid = validatePackages()
|
|
363
552
|
except RecursionError as err:
|
|
364
553
|
print(
|
|
365
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
|
|
366
560
|
|
|
367
561
|
files = {}
|
|
368
562
|
if (args.build_c):
|
struct_frame/gql_gen.py
CHANGED
|
@@ -17,6 +17,7 @@ gql_types = {
|
|
|
17
17
|
"bool": "Boolean",
|
|
18
18
|
"float": "Float",
|
|
19
19
|
"double": "Float",
|
|
20
|
+
"string": "String",
|
|
20
21
|
}
|
|
21
22
|
|
|
22
23
|
|
|
@@ -79,24 +80,81 @@ class FieldGqlGen:
|
|
|
79
80
|
@staticmethod
|
|
80
81
|
def type_name(field):
|
|
81
82
|
t = field.fieldType
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
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
|
|
85
92
|
|
|
86
93
|
@staticmethod
|
|
87
|
-
def generate(field):
|
|
94
|
+
def generate(field, name_override=None):
|
|
88
95
|
lines = []
|
|
89
|
-
|
|
90
|
-
|
|
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)
|
|
91
132
|
if desc:
|
|
92
133
|
lines.append(f" {desc}")
|
|
93
|
-
|
|
134
|
+
|
|
135
|
+
fname = name_override if name_override else field.name
|
|
136
|
+
lines.append(f" {fname}: {FieldGqlGen.type_name(field)}")
|
|
94
137
|
return '\n'.join(lines)
|
|
95
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
|
+
|
|
96
154
|
|
|
97
155
|
class MessageGqlGen:
|
|
98
156
|
@staticmethod
|
|
99
|
-
def generate(msg):
|
|
157
|
+
def generate(package, msg):
|
|
100
158
|
lines = []
|
|
101
159
|
if msg.comments:
|
|
102
160
|
desc = _triple_quote_block(msg.comments)
|
|
@@ -108,7 +166,11 @@ class MessageGqlGen:
|
|
|
108
166
|
lines.append(" _empty: Boolean")
|
|
109
167
|
else:
|
|
110
168
|
for key, f in msg.fields.items():
|
|
111
|
-
|
|
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))
|
|
112
174
|
lines.append("}\n")
|
|
113
175
|
return '\n'.join(lines)
|
|
114
176
|
|
|
@@ -132,7 +194,7 @@ class FileGqlGen:
|
|
|
132
194
|
if not first_block:
|
|
133
195
|
yield '\n'
|
|
134
196
|
first_block = False
|
|
135
|
-
yield MessageGqlGen.generate(msg).rstrip() + '\n'
|
|
197
|
+
yield MessageGqlGen.generate(package, msg).rstrip() + '\n'
|
|
136
198
|
|
|
137
199
|
# Root Query type
|
|
138
200
|
if package.messages:
|
struct_frame/py_gen.py
CHANGED
|
@@ -17,6 +17,7 @@ py_types = {"uint8": "uint8",
|
|
|
17
17
|
"double": "float64",
|
|
18
18
|
"uint64": 'uint64',
|
|
19
19
|
"int64": 'int64',
|
|
20
|
+
"string": "str", # Add string type support
|
|
20
21
|
}
|
|
21
22
|
|
|
22
23
|
|
|
@@ -58,14 +59,51 @@ class FieldPyGen():
|
|
|
58
59
|
|
|
59
60
|
var_name = field.name
|
|
60
61
|
type_name = field.fieldType
|
|
62
|
+
|
|
63
|
+
# Handle basic type resolution
|
|
61
64
|
if type_name in py_types:
|
|
62
|
-
|
|
65
|
+
base_type = py_types[type_name]
|
|
63
66
|
else:
|
|
64
|
-
type_name = '%s%s' % (pascalCase(field.package), type_name)
|
|
65
67
|
if field.isEnum:
|
|
66
|
-
|
|
68
|
+
# For enums, use the full enum class name for better type safety
|
|
69
|
+
base_type = '%s%s' % (pascalCase(field.package), type_name)
|
|
70
|
+
else:
|
|
71
|
+
base_type = '%s%s' % (pascalCase(field.package), type_name)
|
|
72
|
+
|
|
73
|
+
# Handle arrays
|
|
74
|
+
if field.is_array:
|
|
75
|
+
if field.fieldType == "string":
|
|
76
|
+
# String arrays require both array size and individual element size
|
|
77
|
+
if field.size_option is not None:
|
|
78
|
+
type_annotation = f"list[{base_type}] # Fixed string array size={field.size_option}, each max {field.element_size} chars"
|
|
79
|
+
elif field.max_size is not None:
|
|
80
|
+
type_annotation = f"list[{base_type}] # Bounded string array max_size={field.max_size}, each max {field.element_size} chars"
|
|
81
|
+
else:
|
|
82
|
+
type_annotation = f"list[{base_type}] # String array"
|
|
83
|
+
else:
|
|
84
|
+
# Non-string arrays
|
|
85
|
+
if field.size_option is not None:
|
|
86
|
+
type_annotation = f"list[{base_type}] # Fixed array size={field.size_option}"
|
|
87
|
+
elif field.max_size is not None:
|
|
88
|
+
type_annotation = f"list[{base_type}] # Bounded array max_size={field.max_size}"
|
|
89
|
+
else:
|
|
90
|
+
type_annotation = f"list[{base_type}] # Array"
|
|
91
|
+
# Handle strings with size info
|
|
92
|
+
elif field.fieldType == "string":
|
|
93
|
+
if field.size_option is not None:
|
|
94
|
+
# Fixed string - exact length
|
|
95
|
+
type_annotation = f"str # Fixed string size={field.size_option}"
|
|
96
|
+
elif field.max_size is not None:
|
|
97
|
+
# Variable string - up to max length
|
|
98
|
+
type_annotation = f"str # Variable string max_size={field.max_size}"
|
|
99
|
+
else:
|
|
100
|
+
# Fallback (shouldn't happen with validation)
|
|
101
|
+
type_annotation = "str # String"
|
|
102
|
+
else:
|
|
103
|
+
# Regular field
|
|
104
|
+
type_annotation = base_type
|
|
67
105
|
|
|
68
|
-
result += ' %s: %s' % (var_name,
|
|
106
|
+
result += ' %s: %s' % (var_name, type_annotation)
|
|
69
107
|
|
|
70
108
|
leading_comment = field.comments
|
|
71
109
|
if leading_comment:
|
|
@@ -86,7 +124,7 @@ class MessagePyGen():
|
|
|
86
124
|
result = '#%s\n' % c
|
|
87
125
|
|
|
88
126
|
structName = '%s%s' % (pascalCase(msg.package), msg.name)
|
|
89
|
-
result += 'class %s(Structured):\n' % structName
|
|
127
|
+
result += 'class %s(Structured, byte_order=ByteOrder.LE, byte_order_mode=ByteOrderMode.OVERRIDE):\n' % structName
|
|
90
128
|
result += ' msg_size = %s\n' % msg.size
|
|
91
129
|
if msg.id != None:
|
|
92
130
|
result += ' msg_id = %s\n' % msg.id
|
|
@@ -103,15 +141,31 @@ class MessagePyGen():
|
|
|
103
141
|
result += f' return out'
|
|
104
142
|
|
|
105
143
|
result += '\n\n def to_dict(self, include_name = True, include_id = True):\n'
|
|
106
|
-
result +=
|
|
144
|
+
result += ' out = {}\n'
|
|
145
|
+
# Handle all field types including arrays
|
|
107
146
|
for key, f in msg.fields.items():
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
147
|
+
if f.is_array:
|
|
148
|
+
if f.isDefaultType or f.isEnum or f.fieldType == "string":
|
|
149
|
+
# Array of primitives, enums, or strings
|
|
150
|
+
result += f' out["{key}"] = self.{key}\n'
|
|
151
|
+
else:
|
|
152
|
+
# Array of nested messages - convert each element
|
|
153
|
+
result += f' out["{key}"] = [item.to_dict(False, False) for item in self.{key}]\n'
|
|
154
|
+
elif f.isDefaultType or f.isEnum or f.fieldType == "string":
|
|
155
|
+
# Regular primitive, enum, or string field
|
|
156
|
+
result += f' out["{key}"] = self.{key}\n'
|
|
157
|
+
else:
|
|
158
|
+
# Nested message field
|
|
159
|
+
if getattr(f, 'flatten', False):
|
|
160
|
+
# Merge nested dict into parent
|
|
161
|
+
result += f' out.update(self.{key}.to_dict(False, False))\n'
|
|
162
|
+
else:
|
|
163
|
+
result += f' out["{key}"] = self.{key}.to_dict(False, False)\n'
|
|
164
|
+
result += ' if include_name:\n'
|
|
111
165
|
result += f' out["name"] = "{msg.name}"\n'
|
|
112
|
-
result +=
|
|
166
|
+
result += ' if include_id:\n'
|
|
113
167
|
result += f' out["msg_id"] = "{msg.id}"\n'
|
|
114
|
-
result +=
|
|
168
|
+
result += ' return out\n'
|
|
115
169
|
|
|
116
170
|
return result
|
|
117
171
|
|
struct_frame/ts_gen.py
CHANGED
|
@@ -18,6 +18,23 @@ ts_types = {
|
|
|
18
18
|
"uint32": 'UInt32LE',
|
|
19
19
|
"uint64": 'BigInt64LE',
|
|
20
20
|
"int64": 'BigUInt64LE',
|
|
21
|
+
"string": 'String',
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
# TypeScript type mappings for array declarations
|
|
25
|
+
ts_array_types = {
|
|
26
|
+
"int8": 'number',
|
|
27
|
+
"uint8": 'number',
|
|
28
|
+
"int16": 'number',
|
|
29
|
+
"uint16": 'number',
|
|
30
|
+
"bool": 'boolean',
|
|
31
|
+
"double": 'number',
|
|
32
|
+
"float": 'number',
|
|
33
|
+
"int32": 'number',
|
|
34
|
+
"uint32": 'number',
|
|
35
|
+
"uint64": 'bigint',
|
|
36
|
+
"int64": 'bigint',
|
|
37
|
+
"string": 'string',
|
|
21
38
|
}
|
|
22
39
|
|
|
23
40
|
|
|
@@ -65,18 +82,66 @@ class FieldTsGen():
|
|
|
65
82
|
def generate(field, packageName):
|
|
66
83
|
result = ''
|
|
67
84
|
isEnum = False
|
|
68
|
-
# isEnum = field.pbtype in ('ENUM', 'UENUM')
|
|
69
85
|
var_name = StyleC.var_name(field.name)
|
|
70
86
|
type_name = field.fieldType
|
|
71
|
-
if type_name in ts_types:
|
|
72
|
-
type_name = ts_types[type_name]
|
|
73
|
-
else:
|
|
74
|
-
type_name = '%s_%s' % (packageName, StyleC.struct_name(type_name))
|
|
75
87
|
|
|
76
|
-
|
|
77
|
-
|
|
88
|
+
# Handle arrays
|
|
89
|
+
if field.is_array:
|
|
90
|
+
if field.fieldType == "string":
|
|
91
|
+
if field.size_option is not None: # Fixed size array [size=X]
|
|
92
|
+
# Fixed string array: string[size] -> Array<string> with fixed length
|
|
93
|
+
result += f' // Fixed string array: {field.size_option} strings, each exactly {field.element_size} chars\n'
|
|
94
|
+
result += f' .Array(\'{var_name}\', \'String\', {field.size_option})'
|
|
95
|
+
else: # Variable size array [max_size=X]
|
|
96
|
+
# Variable string array: string[max_size=X, element_size=Y] -> Array<string> with count
|
|
97
|
+
result += f' // Variable string array: up to {field.max_size} strings, each max {field.element_size} chars\n'
|
|
98
|
+
result += f' .UInt8(\'{var_name}_count\')\n'
|
|
99
|
+
result += f' .Array(\'{var_name}_data\', \'String\', {field.max_size})'
|
|
100
|
+
else:
|
|
101
|
+
# Regular type arrays
|
|
102
|
+
if type_name in ts_types:
|
|
103
|
+
base_type = ts_types[type_name]
|
|
104
|
+
else:
|
|
105
|
+
base_type = f'{packageName.lower()}_{StyleC.struct_name(type_name).lower()}'
|
|
106
|
+
|
|
107
|
+
if field.size_option is not None: # Fixed size array [size=X]
|
|
108
|
+
# Fixed array: type[size] -> Array<type> with fixed length
|
|
109
|
+
# For fixed arrays, size_option contains the exact size
|
|
110
|
+
array_size = field.size_option
|
|
111
|
+
result += f' // Fixed array: always {array_size} elements\n'
|
|
112
|
+
result += f' .Array(\'{var_name}\', \'{base_type}\', {array_size})'
|
|
113
|
+
else: # Variable size array [max_size=X]
|
|
114
|
+
# Variable array: type[max_size=X] -> count + Array<type>
|
|
115
|
+
max_count = field.max_size # For variable arrays, max_size is the maximum count
|
|
116
|
+
result += f' // Variable array: up to {max_count} elements\n'
|
|
117
|
+
result += f' .UInt8(\'{var_name}_count\')\n'
|
|
118
|
+
result += f' .Array(\'{var_name}_data\', \'{base_type}\', {max_count})'
|
|
78
119
|
else:
|
|
79
|
-
|
|
120
|
+
# Non-array fields (existing logic)
|
|
121
|
+
if field.fieldType == "string":
|
|
122
|
+
if hasattr(field, 'size_option') and field.size_option is not None:
|
|
123
|
+
# Fixed string: string[size] -> fixed length string
|
|
124
|
+
result += f' // Fixed string: exactly {field.size_option} chars\n'
|
|
125
|
+
result += f' .String(\'{var_name}\', {field.size_option})'
|
|
126
|
+
elif hasattr(field, 'max_size') and field.max_size is not None:
|
|
127
|
+
# Variable string: string[max_size=X] -> length + data
|
|
128
|
+
result += f' // Variable string: up to {field.max_size} chars\n'
|
|
129
|
+
result += f' .UInt8(\'{var_name}_length\')\n'
|
|
130
|
+
result += f' .String(\'{var_name}_data\', {field.max_size})'
|
|
131
|
+
else:
|
|
132
|
+
# Default string handling (should not occur with new parser)
|
|
133
|
+
result += f' .String(\'{var_name}\')'
|
|
134
|
+
else:
|
|
135
|
+
# Regular types
|
|
136
|
+
if type_name in ts_types:
|
|
137
|
+
type_name = ts_types[type_name]
|
|
138
|
+
else:
|
|
139
|
+
type_name = f'{packageName}_{StyleC.struct_name(type_name)}'
|
|
140
|
+
|
|
141
|
+
if isEnum:
|
|
142
|
+
result += f' .UInt8(\'{var_name}\', typed<{type_name}>())'
|
|
143
|
+
else:
|
|
144
|
+
result += f' .{type_name}(\'{var_name}\')'
|
|
80
145
|
|
|
81
146
|
leading_comment = field.comments
|
|
82
147
|
if leading_comment:
|
|
@@ -182,11 +247,14 @@ class FileTsGen():
|
|
|
182
247
|
yield '\n'
|
|
183
248
|
|
|
184
249
|
if package.messages:
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
yield '
|
|
190
|
-
|
|
191
|
-
|
|
250
|
+
# Only generate get_message_length if there are messages with IDs
|
|
251
|
+
messages_with_id = [
|
|
252
|
+
msg for key, msg in package.sortedMessages().items() if msg.id]
|
|
253
|
+
if messages_with_id:
|
|
254
|
+
yield 'export function get_message_length(msg_id : number){\n switch (msg_id)\n {\n'
|
|
255
|
+
for msg in messages_with_id:
|
|
256
|
+
package_msg_name = '%s_%s' % (package.name, msg.name)
|
|
257
|
+
yield ' case %s_msgid: return %s_max_size;\n' % (package_msg_name, package_msg_name)
|
|
258
|
+
|
|
259
|
+
yield ' default: break;\n } return 0;\n}'
|
|
192
260
|
yield '\n'
|
|
@@ -0,0 +1,311 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: struct-frame
|
|
3
|
+
Version: 0.0.28
|
|
4
|
+
Summary: A framework for serializing data with headers
|
|
5
|
+
Project-URL: Homepage, https://github.com/mylonics/struct-frame
|
|
6
|
+
Project-URL: Issues, https://github.com/mylonics/struct-frame/issues
|
|
7
|
+
Author-email: Rijesh Augustine <rijesh@mylonics.com>
|
|
8
|
+
License-Expression: MIT
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Classifier: Operating System :: OS Independent
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Requires-Python: >=3.8
|
|
13
|
+
Requires-Dist: proto-schema-parser>=1.4.5
|
|
14
|
+
Requires-Dist: structured-classes>=3.1.0
|
|
15
|
+
Description-Content-Type: text/markdown
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
# Struct Frame
|
|
19
|
+
|
|
20
|
+
A multi-language code generation framework that converts Protocol Buffer (.proto) files into serialization/deserialization code for C, TypeScript, Python, and GraphQL. It provides framing and parsing utilities for structured message communication.
|
|
21
|
+
|
|
22
|
+
## Quick Start
|
|
23
|
+
|
|
24
|
+
### Installation
|
|
25
|
+
```bash
|
|
26
|
+
# Install Python dependencies
|
|
27
|
+
pip install proto-schema-parser structured-classes
|
|
28
|
+
|
|
29
|
+
# Install Node.js dependencies (for TypeScript)
|
|
30
|
+
npm install
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
### Basic Usage
|
|
34
|
+
```bash
|
|
35
|
+
# Generate code for all languages
|
|
36
|
+
PYTHONPATH=src python3 src/main.py examples/myl_vehicle.proto --build_c --build_ts --build_py --build_gql
|
|
37
|
+
|
|
38
|
+
# Run comprehensive test suite
|
|
39
|
+
python test_all.py
|
|
40
|
+
|
|
41
|
+
# Generated files will be in the generated/ directory
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
### Test Suite
|
|
45
|
+
|
|
46
|
+
The project includes a comprehensive test suite that validates code generation, compilation, and serialization across all supported languages:
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
# Run all tests
|
|
50
|
+
python test_all.py
|
|
51
|
+
|
|
52
|
+
# Run with verbose output
|
|
53
|
+
python tests/run_tests.py --verbose
|
|
54
|
+
|
|
55
|
+
# Skip specific languages
|
|
56
|
+
python tests/run_tests.py --skip-ts --skip-c
|
|
57
|
+
|
|
58
|
+
# Generate code only (no compilation/execution)
|
|
59
|
+
python tests/run_tests.py --generate-only
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
See `tests/README.md` for detailed test documentation.
|
|
63
|
+
|
|
64
|
+
### Language-Specific Examples
|
|
65
|
+
|
|
66
|
+
#### Python
|
|
67
|
+
```bash
|
|
68
|
+
python src/main.py examples/myl_vehicle.proto --build_py
|
|
69
|
+
# Use generated Python classes directly
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
#### TypeScript
|
|
73
|
+
```bash
|
|
74
|
+
python src/main.py examples/myl_vehicle.proto --build_ts
|
|
75
|
+
npx tsc examples/index.ts --outDir generated/
|
|
76
|
+
node generated/examples/index.js
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
#### C
|
|
80
|
+
```bash
|
|
81
|
+
python src/main.py examples/myl_vehicle.proto --build_c
|
|
82
|
+
gcc examples/main.c -I generated/c -o main
|
|
83
|
+
./main
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
#### GraphQL
|
|
87
|
+
```bash
|
|
88
|
+
python src/main.py examples/myl_vehicle.proto --build_gql
|
|
89
|
+
# Use generated .graphql schema files
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
## Feature Compatibility Matrix
|
|
93
|
+
|
|
94
|
+
| Feature | C | TypeScript | Python | GraphQL | Status |
|
|
95
|
+
|---------|---|------------|--------|---------|--------|
|
|
96
|
+
| **Core Types** | ✓ | ✓ | ✓ | ✓ | Stable |
|
|
97
|
+
| **String** | ✓ | ✓ | ✓ | ✓ | Stable |
|
|
98
|
+
| **Enums** | ✓ | ✓ | ✓ | ✓ | Stable |
|
|
99
|
+
| **Nested Messages** | ✓ | ✓ | ✓ | ✓ | Stable |
|
|
100
|
+
| **Message IDs** | ✓ | ✓ | ✓ | N/A | Stable |
|
|
101
|
+
| **Message Serialization** | ✓ | ✓ | ✓ | N/A | Stable |
|
|
102
|
+
| **Flatten** | N/A | N/A | ✓ | ✓ | Partial |
|
|
103
|
+
| **Arrays** | ✓ | ✓ | ✓ | ✓ | Stable |
|
|
104
|
+
|
|
105
|
+
**Legend:**
|
|
106
|
+
- **✓** - Feature works as documented
|
|
107
|
+
- **Partial** - Basic functionality works, some limitations
|
|
108
|
+
- **✗** - Feature not yet available
|
|
109
|
+
- **N/A** - Not applicable for this language
|
|
110
|
+
|
|
111
|
+
## Project Structure
|
|
112
|
+
|
|
113
|
+
- `src/struct_frame/` - Core code generation framework
|
|
114
|
+
- `generate.py` - Main parser and validation logic
|
|
115
|
+
- `*_gen.py` - Language-specific code generators
|
|
116
|
+
- `boilerplate/` - Runtime libraries for each language
|
|
117
|
+
- `examples/` - Example .proto files and usage demos
|
|
118
|
+
- `main.c` - C API demonstration (encoding/decoding, parsing)
|
|
119
|
+
- `index.ts` - TypeScript API demonstration (similar functionality)
|
|
120
|
+
- `*.proto` - Protocol Buffer definitions for examples
|
|
121
|
+
- `generated/` - Output directory for generated code (git-ignored)
|
|
122
|
+
|
|
123
|
+
## Protocol Buffer Schema Reference
|
|
124
|
+
|
|
125
|
+
### Supported Data Types
|
|
126
|
+
|
|
127
|
+
| Type | Size (bytes) | Description | Range/Notes |
|
|
128
|
+
|------|--------------|-------------|-------------|
|
|
129
|
+
| **Integers** |
|
|
130
|
+
| `int8` | 1 | Signed 8-bit integer | -128 to 127 |
|
|
131
|
+
| `uint8` | 1 | Unsigned 8-bit integer | 0 to 255 |
|
|
132
|
+
| `int16` | 2 | Signed 16-bit integer | -32,768 to 32,767 |
|
|
133
|
+
| `uint16` | 2 | Unsigned 16-bit integer | 0 to 65,535 |
|
|
134
|
+
| `int32` | 4 | Signed 32-bit integer | -2.1B to 2.1B |
|
|
135
|
+
| `uint32` | 4 | Unsigned 32-bit integer | 0 to 4.3B |
|
|
136
|
+
| `int64` | 8 | Signed 64-bit integer | Large integers |
|
|
137
|
+
| `uint64` | 8 | Unsigned 64-bit integer | Large positive integers |
|
|
138
|
+
| **Floating Point** |
|
|
139
|
+
| `float` | 4 | Single precision (IEEE 754) | 7 decimal digits |
|
|
140
|
+
| `double` | 8 | Double precision (IEEE 754) | 15-17 decimal digits |
|
|
141
|
+
| **Other** |
|
|
142
|
+
| `bool` | 1 | Boolean value | `true` or `false` |
|
|
143
|
+
| `string` | Variable | UTF-8 encoded string | Length-prefixed |
|
|
144
|
+
| `EnumType` | 1 | Custom enumeration | Defined in .proto |
|
|
145
|
+
| `MessageType` | Variable | Nested message | User-defined structure |
|
|
146
|
+
|
|
147
|
+
> **Note:** All types use little-endian byte order for cross-platform compatibility.
|
|
148
|
+
|
|
149
|
+
### Array Support
|
|
150
|
+
|
|
151
|
+
Arrays (repeated fields) support all data types - primitives, enums, and messages across all target languages.
|
|
152
|
+
|
|
153
|
+
| Array Type | Syntax | Memory Usage | Use Case |
|
|
154
|
+
|------------|--------|--------------|----------|
|
|
155
|
+
| **Fixed** | `repeated type field = N [size=X];` | `sizeof(type) * X` | Matrices, buffers (always full) |
|
|
156
|
+
| **Bounded** | `repeated type field = N [max_size=X];` | 1 byte (count) + `sizeof(type) * X` | Dynamic lists with limits |
|
|
157
|
+
| **String Arrays** | `repeated string field = N [max_size=X, element_size=Y];` | 1 byte (count) + `X * Y` bytes | Text collections with size limits |
|
|
158
|
+
|
|
159
|
+
```proto
|
|
160
|
+
message ArrayExample {
|
|
161
|
+
repeated float matrix = 1 [size=9]; // 3x3 matrix (always 9 elements)
|
|
162
|
+
repeated string names = 2 [max_size=10, element_size=32]; // Up to 10 strings, each max 32 chars
|
|
163
|
+
repeated int32 values = 3 [max_size=100]; // Up to 100 integers (variable count)
|
|
164
|
+
}
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
**Generated Output** (all languages now supported):
|
|
168
|
+
- **Python**: `matrix: list[float]`, `names: list[str]`, `values: list[int]`
|
|
169
|
+
- **C**: `float matrix[9]`, `struct { uint8_t count; char data[10][32]; } names`
|
|
170
|
+
- **TypeScript**: `Array('matrix', 'Float32LE', 9)`, `Array('names_data', 'String', 10)`
|
|
171
|
+
- **GraphQL**: `matrix: [Float!]!`, `names: [String!]!`, `values: [Int!]!`
|
|
172
|
+
|
|
173
|
+
> **Important**: String arrays require both `max_size` (or `size`) AND `element_size` parameters because they are "arrays of arrays" - you need to specify both how many strings AND the maximum size of each individual string. This ensures predictable memory layout and prevents buffer overflows.
|
|
174
|
+
|
|
175
|
+
### String Type
|
|
176
|
+
|
|
177
|
+
Strings are a special case of bounded character arrays with built-in UTF-8 encoding and null-termination handling across all target languages.
|
|
178
|
+
|
|
179
|
+
| String Type | Syntax | Memory Usage | Use Case |
|
|
180
|
+
|-------------|--------|--------------|----------|
|
|
181
|
+
| **Fixed String** | `string field = N [size=X];` | `X` bytes | Fixed-width text fields |
|
|
182
|
+
| **Variable String** | `string field = N [max_size=X];` | 1 byte (length) + `X` bytes | Text with known maximum length |
|
|
183
|
+
|
|
184
|
+
```proto
|
|
185
|
+
message StringExample {
|
|
186
|
+
string device_name = 1 [size=16]; // Exactly 16 characters (pad with nulls)
|
|
187
|
+
string description = 2 [max_size=256]; // Up to 256 characters (length-prefixed)
|
|
188
|
+
string error_msg = 3 [max_size=128]; // Up to 128 characters for error messages
|
|
189
|
+
}
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
**String Benefits:**
|
|
193
|
+
- **Simplified Schema**: No need to specify `repeated uint8` for text data
|
|
194
|
+
- **Automatic Encoding**: UTF-8 encoding/decoding handled by generators
|
|
195
|
+
- **Null Handling**: Proper null-termination and padding for fixed strings
|
|
196
|
+
- **Type Safety**: Clear distinction between binary data and text
|
|
197
|
+
- **Cross-Language**: Consistent string handling across C, TypeScript, and Python
|
|
198
|
+
|
|
199
|
+
### Message Options
|
|
200
|
+
|
|
201
|
+
**Message ID (`msgid`)** - Required for serializable messages:
|
|
202
|
+
```proto
|
|
203
|
+
message MyMessage {
|
|
204
|
+
option msgid = 42; // Must be unique within package (0-65535)
|
|
205
|
+
string content = 1;
|
|
206
|
+
}
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
### Field Options
|
|
210
|
+
|
|
211
|
+
**Flatten (`flatten=true`)** - Merge nested message fields into parent:
|
|
212
|
+
```proto
|
|
213
|
+
message Position {
|
|
214
|
+
double lat = 1;
|
|
215
|
+
double lon = 2;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
message Status {
|
|
219
|
+
Position pos = 1 [flatten=true]; // lat, lon become direct fields
|
|
220
|
+
float battery = 2;
|
|
221
|
+
}
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
**Array Options** - Control array behavior:
|
|
225
|
+
```proto
|
|
226
|
+
message Data {
|
|
227
|
+
repeated int32 fixed_buffer = 1 [size=256]; // Always 256 integers
|
|
228
|
+
repeated int32 var_buffer = 2 [max_size=256]; // Up to 256 integers
|
|
229
|
+
repeated string messages = 3 [max_size=10, element_size=64]; // Up to 10 strings, each max 64 chars
|
|
230
|
+
string device_name = 4 [size=32]; // Always 32 characters
|
|
231
|
+
string description = 5 [max_size=256]; // Up to 256 characters
|
|
232
|
+
}
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
## Complete Example
|
|
236
|
+
|
|
237
|
+
```proto
|
|
238
|
+
package sensor_system;
|
|
239
|
+
|
|
240
|
+
enum SensorType {
|
|
241
|
+
TEMPERATURE = 0;
|
|
242
|
+
HUMIDITY = 1;
|
|
243
|
+
PRESSURE = 2;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
message Position {
|
|
247
|
+
double lat = 1;
|
|
248
|
+
double lon = 2;
|
|
249
|
+
float alt = 3;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
message SensorReading {
|
|
253
|
+
option msgid = 1;
|
|
254
|
+
|
|
255
|
+
uint32 device_id = 1;
|
|
256
|
+
int64 timestamp = 2;
|
|
257
|
+
SensorType type = 3;
|
|
258
|
+
|
|
259
|
+
// Device name (fixed 16-character string)
|
|
260
|
+
string device_name = 4 [size=16];
|
|
261
|
+
|
|
262
|
+
// Sensor location (flattened)
|
|
263
|
+
Position location = 5 [flatten=true];
|
|
264
|
+
|
|
265
|
+
// Measurement values (up to 8 readings)
|
|
266
|
+
repeated float values = 6 [max_size=8];
|
|
267
|
+
|
|
268
|
+
// Calibration matrix (always 3x3 = 9 elements)
|
|
269
|
+
repeated float calibration = 7 [size=9];
|
|
270
|
+
|
|
271
|
+
// Error message (up to 128 characters)
|
|
272
|
+
string error_msg = 8 [max_size=128];
|
|
273
|
+
|
|
274
|
+
bool valid = 9;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
message DeviceStatus {
|
|
278
|
+
option msgid = 2;
|
|
279
|
+
|
|
280
|
+
uint32 device_id = 1;
|
|
281
|
+
repeated SensorReading recent_readings = 2 [max_size=10];
|
|
282
|
+
float battery_level = 3;
|
|
283
|
+
}
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
## Schema Validation Rules
|
|
287
|
+
|
|
288
|
+
- **Message IDs**: Must be unique within package (0-65535)
|
|
289
|
+
- **Field numbers**: Must be unique within message
|
|
290
|
+
- **Array requirements**: All `repeated` fields must specify `[size=X]` (fixed) or `[max_size=X]` (bounded)
|
|
291
|
+
- **String requirements**: All `string` fields must specify `[size=X]` (fixed) or `[max_size=X]` (variable)
|
|
292
|
+
- **String array requirements**: `repeated string` fields must specify both array size AND `[element_size=Y]`
|
|
293
|
+
- **Flatten constraints**: No field name collisions after flattening
|
|
294
|
+
- **Size limits**: Arrays limited to 255 elements maximum
|
|
295
|
+
|
|
296
|
+
## Code Generation
|
|
297
|
+
|
|
298
|
+
```bash
|
|
299
|
+
# Generate all languages
|
|
300
|
+
python src/main.py schema.proto --build_c --build_ts --build_py --build_gql
|
|
301
|
+
|
|
302
|
+
# Language-specific paths
|
|
303
|
+
python src/main.py schema.proto --build_py --py_path output/python/
|
|
304
|
+
python src/main.py schema.proto --build_c --c_path output/c/
|
|
305
|
+
python src/main.py schema.proto --build_ts --ts_path output/typescript/
|
|
306
|
+
python src/main.py schema.proto --build_gql --gql_path output/graphql/
|
|
307
|
+
```
|
|
308
|
+
|
|
309
|
+
## Additional Documentation
|
|
310
|
+
|
|
311
|
+
- **[Array Implementation Guide](ARRAY_IMPLEMENTATION.md)** - Comprehensive documentation of array features, syntax, and generated code examples across all languages
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
struct_frame/__init__.py,sha256=m9hfIbcgCYvt-fIeYrJRQyk6I7rq35jSdZIhrMmZntg,374
|
|
2
|
+
struct_frame/__main__.py,sha256=tIybnBeFHvwiwVhodVOSnxhne5AX_80mtXBx4rneSB4,143
|
|
3
|
+
struct_frame/base.py,sha256=1Z_0vMkwz0X8r2hIVLv5yuhwwD929LwNMzVKBqFxxac,2012
|
|
4
|
+
struct_frame/c_gen.py,sha256=I-VFeVmOBlvjZuRehljEjRxJComWGIwOsNe1ex77RLI,9555
|
|
5
|
+
struct_frame/generate.py,sha256=8YL4MJ--vvOsv5MFqGjKSATSulanEqX_Qf0Jew6x7UU,23416
|
|
6
|
+
struct_frame/gql_gen.py,sha256=jDLsyZJmfpVBpdXE06mixZ3D01NAmRioGJqk_FoQt90,7543
|
|
7
|
+
struct_frame/py_gen.py,sha256=EQqw8u2o-FszROviQyQCpF45bbx0KqjdqVBefpyDqH4,7932
|
|
8
|
+
struct_frame/ts_gen.py,sha256=glT9eA--j_8vaR8tHQ0ekDg7udIVgE9FGA4MGAwiCsA,10103
|
|
9
|
+
struct_frame-0.0.28.dist-info/METADATA,sha256=30k3JtXITayQdbY9UmOxcHTriwp3A9k7rX_EirMMXGU,10831
|
|
10
|
+
struct_frame-0.0.28.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
11
|
+
struct_frame-0.0.28.dist-info/licenses/LICENSE,sha256=UjbLtGfcHCIqJg9UzEVGoNW8fyX4Ah9ZbsuAmJ_vhmk,1094
|
|
12
|
+
struct_frame-0.0.28.dist-info/RECORD,,
|
|
@@ -1,62 +0,0 @@
|
|
|
1
|
-
Metadata-Version: 2.4
|
|
2
|
-
Name: struct-frame
|
|
3
|
-
Version: 0.0.27
|
|
4
|
-
Summary: A framework for serializing data with headers
|
|
5
|
-
Project-URL: Homepage, https://github.com/mylonics/struct-frame
|
|
6
|
-
Project-URL: Issues, https://github.com/mylonics/struct-frame/issues
|
|
7
|
-
Author-email: Rijesh Augustine <rijesh@mylonics.com>
|
|
8
|
-
License-Expression: MIT
|
|
9
|
-
License-File: LICENSE
|
|
10
|
-
Classifier: Operating System :: OS Independent
|
|
11
|
-
Classifier: Programming Language :: Python :: 3
|
|
12
|
-
Requires-Python: >=3.8
|
|
13
|
-
Requires-Dist: proto-schema-parser>=1.4.5
|
|
14
|
-
Requires-Dist: structured-classes>=3.1.0
|
|
15
|
-
Description-Content-Type: text/markdown
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
# Struct Frame
|
|
19
|
-
|
|
20
|
-
A framework for serializing data with headers
|
|
21
|
-
|
|
22
|
-
## Quick Start
|
|
23
|
-
|
|
24
|
-
### Python Usage
|
|
25
|
-
```bash
|
|
26
|
-
# Install dependencies
|
|
27
|
-
pip install -e .
|
|
28
|
-
|
|
29
|
-
# Generate code from proto file
|
|
30
|
-
python src/main.py examples/myl_vehicle.proto --build_c --build_ts --build_py
|
|
31
|
-
|
|
32
|
-
# Generated files will be in the generated/ directory
|
|
33
|
-
```
|
|
34
|
-
|
|
35
|
-
### TypeScript Example
|
|
36
|
-
```bash
|
|
37
|
-
# Install TypeScript dependencies
|
|
38
|
-
npm i -D typescript typed-struct @types/node
|
|
39
|
-
|
|
40
|
-
# Generate TypeScript code first
|
|
41
|
-
python src/main.py examples/myl_vehicle.proto --build_ts
|
|
42
|
-
|
|
43
|
-
# Compile and run the example
|
|
44
|
-
npx tsc examples/index.ts --outDir generated/
|
|
45
|
-
node generated/examples/index.js
|
|
46
|
-
```
|
|
47
|
-
|
|
48
|
-
### C Example
|
|
49
|
-
```bash
|
|
50
|
-
# Generate C code first
|
|
51
|
-
python src/main.py examples/myl_vehicle.proto --build_c
|
|
52
|
-
|
|
53
|
-
# Compile the C example
|
|
54
|
-
gcc examples/main.c -I generated/c -o main
|
|
55
|
-
./main
|
|
56
|
-
```
|
|
57
|
-
|
|
58
|
-
## Project Structure
|
|
59
|
-
|
|
60
|
-
- `src/` - Source code for the struct-frame library
|
|
61
|
-
- `examples/` - Example usage and demo files
|
|
62
|
-
- `generated/` - Generated output files (ignored by git)
|
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
struct_frame/__init__.py,sha256=m9hfIbcgCYvt-fIeYrJRQyk6I7rq35jSdZIhrMmZntg,374
|
|
2
|
-
struct_frame/__main__.py,sha256=tIybnBeFHvwiwVhodVOSnxhne5AX_80mtXBx4rneSB4,143
|
|
3
|
-
struct_frame/base.py,sha256=1Z_0vMkwz0X8r2hIVLv5yuhwwD929LwNMzVKBqFxxac,2012
|
|
4
|
-
struct_frame/c_gen.py,sha256=RFzEx0EuMoxBwdRi3O1NZGJ4iw3c3h7g8rRU3zD2ubU,5997
|
|
5
|
-
struct_frame/generate.py,sha256=rfz7d0w37UWs0qlylOhYgD3k0GVmBoaZAGWQp_HUmHc,13131
|
|
6
|
-
struct_frame/gql_gen.py,sha256=6-zaocD3qgPQyovjAihxl3DJZ9OHXM225VvO5KQmtkI,4488
|
|
7
|
-
struct_frame/py_gen.py,sha256=7kvMsWPxAyLzTQg_BRj3g-yICH-6ilIGxMABEQdLayQ,4980
|
|
8
|
-
struct_frame/ts_gen.py,sha256=qdJgAMxL2lV_WmGZErjHpgukYHNNpKpECarXdfmngEI,6266
|
|
9
|
-
struct_frame-0.0.27.dist-info/METADATA,sha256=LwhsBrTZ0SQOMrMbHgWguP3d796SfASF8QwTe_y9cjU,1578
|
|
10
|
-
struct_frame-0.0.27.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
11
|
-
struct_frame-0.0.27.dist-info/licenses/LICENSE,sha256=UjbLtGfcHCIqJg9UzEVGoNW8fyX4Ah9ZbsuAmJ_vhmk,1094
|
|
12
|
-
struct_frame-0.0.27.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|