struct-frame 0.0.29__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.
@@ -0,0 +1,607 @@
1
+ #!/usr/bin/env python3
2
+ # kate: replace-tabs on; indent-width 4;
3
+
4
+
5
+ import os
6
+ import shutil
7
+ from struct_frame import FileCGen
8
+ from struct_frame import FileTsGen
9
+ from struct_frame import FilePyGen
10
+ from struct_frame import FileGqlGen
11
+ from proto_schema_parser.parser import Parser
12
+ from proto_schema_parser import ast
13
+ from proto_schema_parser.ast import FieldCardinality
14
+
15
+ import argparse
16
+
17
+ recErrCurrentField = ""
18
+ recErrCurrentMessage = ""
19
+
20
+ default_types = {
21
+ "uint8": {"size": 1},
22
+ "int8": {"size": 1},
23
+ "uint16": {"size": 2},
24
+ "int16": {"size": 2},
25
+ "uint32": {"size": 4},
26
+ "int32": {"size": 4},
27
+ "bool": {"size": 1},
28
+ "float": {"size": 4},
29
+ "double": {"size": 8},
30
+ "int64": {"size": 8},
31
+ "uint64": {"size": 8},
32
+ "string": {"size": 4} # Variable length, estimated size for length prefix
33
+ }
34
+
35
+
36
+ class Enum:
37
+ def __init__(self, package, comments):
38
+ self.name = None
39
+ self.data = {}
40
+ self.size = 1
41
+ self.comments = comments
42
+ self.package = package
43
+ self.isEnum = True
44
+
45
+ def parse(self, enum):
46
+ self.name = enum.name
47
+ comments = []
48
+ for e in enum.elements:
49
+ if type(e) == ast.Comment:
50
+ comments.append(e.text)
51
+ else:
52
+ if e.name in self.data:
53
+ print(f"Enum Field Redclaration")
54
+ return False
55
+ self.data[e.name] = (e.number, comments)
56
+ comments = []
57
+
58
+ return True
59
+
60
+ def validate(self, currentPackage, packages):
61
+ return True
62
+
63
+ def __str__(self):
64
+ output = ""
65
+ for c in self.comments:
66
+ output = output + c + "\n"
67
+
68
+ output = output + f"Enum: {self.name}\n"
69
+
70
+ for key, value in self.data.items():
71
+ output = output + f"Key: {key}, Value: {value}" + "\n"
72
+ return output
73
+
74
+
75
+ class Field:
76
+ def __init__(self, package, comments):
77
+ self.name = None
78
+ self.fieldType = None
79
+ self.isDefaultType = False
80
+ self.size = 0
81
+ self.validated = False
82
+ self.comments = comments
83
+ self.package = package
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
91
+
92
+ def parse(self, field):
93
+ self.name = field.name
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
+
100
+ if self.fieldType in default_types:
101
+ self.isDefaultType = True
102
+ self.size = default_types[self.fieldType]["size"]
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
157
+ return True
158
+
159
+ def validate(self, currentPackage, packages):
160
+
161
+ global recErrCurrentField
162
+ recErrCurrentField = self.name
163
+ if not self.validated:
164
+ ret = currentPackage.findFieldType(self.fieldType)
165
+
166
+ if ret:
167
+ if ret.validate(currentPackage, packages):
168
+ self.isEnum = ret.isEnum
169
+ self.validated = True
170
+ base_size = ret.size
171
+ else:
172
+ print(
173
+ f"Failed to validate Field: {self.name} of Type: {self.fieldType} in Package: {currentPackage.name}")
174
+ return False
175
+ else:
176
+ print(
177
+ f"Failed to find Field: {self.name} of Type: {self.fieldType} in Package: {currentPackage.name}")
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}")
250
+
251
+ return True
252
+
253
+ def __str__(self):
254
+ output = ""
255
+ for c in self.comments:
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}]"
270
+ output = output + \
271
+ f"Field: {self.name}, Type:{self.fieldType}, Size:{self.size}{array_info}"
272
+ return output
273
+
274
+
275
+ class Message:
276
+ def __init__(self, package, comments):
277
+ self.id = None
278
+ self.size = 0
279
+ self.name = None
280
+ self.fields = {}
281
+ self.validated = False
282
+ self.comments = comments
283
+ self.package = package
284
+ self.isEnum = False
285
+
286
+ def parse(self, msg):
287
+ self.name = msg.name
288
+ comments = []
289
+ for e in msg.elements:
290
+ if type(e) == ast.Option:
291
+ if e.name == "msgid":
292
+ if self.id:
293
+ raise Exception(f"Redefinition of msg_id for {e.name}")
294
+ self.id = e.value
295
+ elif type(e) == ast.Comment:
296
+ comments.append(e.text)
297
+ elif type(e) == ast.Field:
298
+ if e.name in self.fields:
299
+ print(f"Field Redclaration")
300
+ return False
301
+ self.fields[e.name] = Field(self.package, comments)
302
+ comments = []
303
+ if not self.fields[e.name].parse(e):
304
+ return False
305
+ return True
306
+
307
+ def validate(self, currentPackage, packages):
308
+ if self.validated:
309
+ return True
310
+
311
+ global recErrCurrentMessage
312
+ recErrCurrentMessage = self.name
313
+ for key, value in self.fields.items():
314
+ if not value.validate(currentPackage, packages):
315
+ print(
316
+ f"Failed To validate Field: {key}, in Message {self.name}\n")
317
+ return False
318
+ self.size = self.size + value.size
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
+
358
+ self.validated = True
359
+ return True
360
+
361
+ def __str__(self):
362
+ output = ""
363
+ for c in self.comments:
364
+ output = output + c + "\n"
365
+ output = output + \
366
+ f"Message: {self.name}, Size: {self.size}, ID: {self.id}\n"
367
+
368
+ for key, value in self.fields.items():
369
+ output = output + value.__str__() + "\n"
370
+ return output
371
+
372
+
373
+ class Package:
374
+ def __init__(self, name):
375
+ self.name = name
376
+ self.enums = {}
377
+ self.messages = {}
378
+
379
+ def addEnum(self, enum, comments):
380
+ self.comments = comments
381
+ if enum.name in self.enums:
382
+ print(f"Enum Redclaration")
383
+ return False
384
+ self.enums[enum.name] = Enum(self.name, comments)
385
+ return self.enums[enum.name].parse(enum)
386
+
387
+ def addMessage(self, message, comments):
388
+ if message.name in self.messages:
389
+ print(f"Message Redclaration")
390
+ return False
391
+ self.messages[message.name] = Message(self.name, comments)
392
+ return self.messages[message.name].parse(message)
393
+
394
+ def validatePackage(self, allPackages):
395
+ names = []
396
+ for key, value in self.enums.items():
397
+ if value.name in names:
398
+ print(
399
+ f"Name collision with Enum and Message: {value.name} in Packaage {self.name}")
400
+ return False
401
+ names.append(value.name)
402
+ for key, value in self.messages.items():
403
+ if value.name in names:
404
+ print(
405
+ f"Name collision with Enum and Message: {value.name} in Packaage {self.name}")
406
+ return False
407
+ names.append(value.name)
408
+
409
+ for key, value in self.messages.items():
410
+ if not value.validate(self, allPackages):
411
+ print(
412
+ f"Failed To validate Message: {key}, in Package {self.name}\n")
413
+ return False
414
+
415
+ return True
416
+
417
+ def findFieldType(self, name):
418
+ for key, value in self.enums.items():
419
+ if value.name == name:
420
+ return value
421
+
422
+ for key, value in self.messages.items():
423
+ if value.name == name:
424
+ return value
425
+
426
+ def sortedMessages(self):
427
+ # Need to sort messages to ensure no out of order dependencies.
428
+ return self.messages
429
+
430
+ def __str__(self):
431
+ output = "Package: " + self.name + "\n"
432
+ for key, value in self.enums.items():
433
+ output = output + value.__str__() + "\n"
434
+ for key, value in self.messages.items():
435
+ output = output + value.__str__() + "\n"
436
+ return output
437
+
438
+
439
+ packages = {}
440
+ processed_file = []
441
+ required_file = []
442
+
443
+ parser = argparse.ArgumentParser(
444
+ prog='struct_frame',
445
+ description='Message serialization and header generation program')
446
+
447
+ parser.add_argument('filename')
448
+ parser.add_argument('--debug', action='store_true')
449
+ parser.add_argument('--build_c', action='store_true')
450
+ parser.add_argument('--build_ts', action='store_true')
451
+ parser.add_argument('--build_py', action='store_true')
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/'])
458
+
459
+
460
+ def parseFile(filename):
461
+ processed_file.append(filename)
462
+ with open(filename, "r") as f:
463
+ result = Parser().parse(f.read())
464
+
465
+ foundPackage = False
466
+ package_name = ""
467
+ comments = []
468
+
469
+ for e in result.file_elements:
470
+ if (type(e) == ast.Package):
471
+ if foundPackage:
472
+ print(
473
+ f"Multiple Package declaration found in file {filename} - {package_name}")
474
+ return False
475
+ foundPackage = True
476
+ package_name = e.name
477
+ if package_name not in packages:
478
+ packages[package_name] = Package(package_name)
479
+ packages
480
+
481
+ elif (type(e) == ast.Enum):
482
+ if not packages[package_name].addEnum(e, comments):
483
+ print(
484
+ f"Enum Error in Package: {package_name} FileName: {filename} EnumName: {e.name}")
485
+ return False
486
+ comments = []
487
+
488
+ elif (type(e) == ast.Message):
489
+ if not packages[package_name].addMessage(e, comments):
490
+ print(
491
+ f"Message Error in Package: {package_name} FileName: {filename} MessageName: {e.name}")
492
+ return False
493
+ comments = []
494
+
495
+ elif (type(e) == ast.Comment):
496
+ comments.append(e.text)
497
+
498
+
499
+ def validatePackages():
500
+ for key, value in packages.items():
501
+ if not value.validatePackage(packages):
502
+ print(f"Failed To Validate Package: {key}")
503
+ return False
504
+
505
+ return True
506
+
507
+
508
+ def printPackages():
509
+ for key, value in packages.items():
510
+ print(value)
511
+
512
+
513
+ def generateCFileStrings(path):
514
+ out = {}
515
+ for key, value in packages.items():
516
+ name = os.path.join(path, value.name + ".sf.h")
517
+ data = ''.join(FileCGen.generate(value))
518
+ out[name] = data
519
+
520
+ return out
521
+
522
+
523
+ def generateTsFileStrings(path):
524
+ out = {}
525
+ for key, value in packages.items():
526
+ name = os.path.join(path, value.name + ".sf.ts")
527
+ data = ''.join(FileTsGen.generate(value))
528
+ out[name] = data
529
+ return out
530
+
531
+
532
+ def generatePyFileStrings(path):
533
+ out = {}
534
+ for key, value in packages.items():
535
+ name = os.path.join(path, value.name + "_sf.py")
536
+ data = ''.join(FilePyGen.generate(value))
537
+ out[name] = data
538
+ return out
539
+
540
+
541
+ def main():
542
+ args = parser.parse_args()
543
+ parseFile(args.filename)
544
+
545
+ if (not args.build_c and not args.build_ts and not args.build_py and not args.build_gql):
546
+ print("Select at least one build argument")
547
+ return
548
+
549
+ valid = False
550
+ try:
551
+ valid = validatePackages()
552
+ except RecursionError as err:
553
+ print(
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
560
+
561
+ files = {}
562
+ if (args.build_c):
563
+ files.update(generateCFileStrings(args.c_path[0]))
564
+
565
+ if (args.build_ts):
566
+ files.update(generateTsFileStrings(args.ts_path[0]))
567
+
568
+ if (args.build_py):
569
+ files.update(generatePyFileStrings(args.py_path[0]))
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
+
577
+ for filename, filedata in files.items():
578
+ dirname = os.path.dirname(filename)
579
+ if dirname and not os.path.exists(dirname):
580
+ os.makedirs(dirname)
581
+
582
+ with open(filename, 'w', encoding='utf-8') as f:
583
+ f.write(filedata)
584
+
585
+ dir_path = os.path.dirname(os.path.realpath(__file__))
586
+
587
+ if (args.build_c):
588
+ shutil.copytree(os.path.join(dir_path, "boilerplate/c"),
589
+ args.c_path[0], dirs_exist_ok=True)
590
+
591
+ if (args.build_ts):
592
+ shutil.copytree(os.path.join(dir_path, "boilerplate/ts"),
593
+ args.ts_path[0], dirs_exist_ok=True)
594
+
595
+ if (args.build_py):
596
+ shutil.copytree(os.path.join(dir_path, "boilerplate/py"),
597
+ args.py_path[0], dirs_exist_ok=True)
598
+
599
+ # No boilerplate for GraphQL currently
600
+
601
+ if args.debug:
602
+ printPackages()
603
+ print("Struct Frame successfully completed")
604
+
605
+
606
+ if __name__ == '__main__':
607
+ main()