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.
- struct_frame/__init__.py +11 -0
- struct_frame/__main__.py +8 -0
- struct_frame/base.py +85 -0
- struct_frame/c_gen.py +252 -0
- struct_frame/generate.py +607 -0
- struct_frame/gql_gen.py +207 -0
- struct_frame/py_gen.py +213 -0
- struct_frame/ts_gen.py +260 -0
- struct_frame-0.0.29.dist-info/METADATA +311 -0
- struct_frame-0.0.29.dist-info/RECORD +12 -0
- struct_frame-0.0.29.dist-info/WHEEL +4 -0
- struct_frame-0.0.29.dist-info/licenses/LICENSE +21 -0
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'
|
struct_frame/py_gen.py
ADDED
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# kate: replace-tabs on; indent-width 4;
|
|
3
|
+
|
|
4
|
+
from struct_frame import version, NamingStyleC, CamelToSnakeCase, pascalCase
|
|
5
|
+
import time
|
|
6
|
+
|
|
7
|
+
StyleC = NamingStyleC()
|
|
8
|
+
|
|
9
|
+
py_types = {"uint8": "uint8",
|
|
10
|
+
"int8": "int8",
|
|
11
|
+
"uint16": "uint16",
|
|
12
|
+
"int16": "int16",
|
|
13
|
+
"uint32": "uint32",
|
|
14
|
+
"int32": "int32",
|
|
15
|
+
"bool": "bool8",
|
|
16
|
+
"float": "float32",
|
|
17
|
+
"double": "float64",
|
|
18
|
+
"uint64": 'uint64',
|
|
19
|
+
"int64": 'int64',
|
|
20
|
+
"string": "str", # Add string type support
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class EnumPyGen():
|
|
25
|
+
@staticmethod
|
|
26
|
+
def generate(field):
|
|
27
|
+
leading_comment = field.comments
|
|
28
|
+
|
|
29
|
+
result = ''
|
|
30
|
+
if leading_comment:
|
|
31
|
+
for c in leading_comment:
|
|
32
|
+
result = '#%s\n' % c
|
|
33
|
+
|
|
34
|
+
enumName = '%s%s' % (pascalCase(field.package), field.name)
|
|
35
|
+
result += 'class %s(Enum):\n' % (enumName)
|
|
36
|
+
|
|
37
|
+
enum_length = len(field.data)
|
|
38
|
+
enum_values = []
|
|
39
|
+
for index, (d) in enumerate(field.data):
|
|
40
|
+
leading_comment = field.data[d][1]
|
|
41
|
+
|
|
42
|
+
if leading_comment:
|
|
43
|
+
for c in leading_comment:
|
|
44
|
+
enum_values.append("#" + c)
|
|
45
|
+
|
|
46
|
+
enum_value = " %s_%s = %d" % (CamelToSnakeCase(
|
|
47
|
+
field.name).upper(), StyleC.enum_entry(d), field.data[d][0])
|
|
48
|
+
|
|
49
|
+
enum_values.append(enum_value)
|
|
50
|
+
|
|
51
|
+
result += '\n'.join(enum_values)
|
|
52
|
+
return result
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class FieldPyGen():
|
|
56
|
+
@staticmethod
|
|
57
|
+
def generate(field):
|
|
58
|
+
result = ''
|
|
59
|
+
|
|
60
|
+
var_name = field.name
|
|
61
|
+
type_name = field.fieldType
|
|
62
|
+
|
|
63
|
+
# Handle basic type resolution
|
|
64
|
+
if type_name in py_types:
|
|
65
|
+
base_type = py_types[type_name]
|
|
66
|
+
else:
|
|
67
|
+
if field.isEnum:
|
|
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
|
|
105
|
+
|
|
106
|
+
result += ' %s: %s' % (var_name, type_annotation)
|
|
107
|
+
|
|
108
|
+
leading_comment = field.comments
|
|
109
|
+
if leading_comment:
|
|
110
|
+
for c in leading_comment:
|
|
111
|
+
result = "#" + c + "\n" + result
|
|
112
|
+
|
|
113
|
+
return result
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
class MessagePyGen():
|
|
117
|
+
@staticmethod
|
|
118
|
+
def generate(msg):
|
|
119
|
+
leading_comment = msg.comments
|
|
120
|
+
|
|
121
|
+
result = ''
|
|
122
|
+
if leading_comment:
|
|
123
|
+
for c in msg.comments:
|
|
124
|
+
result = '#%s\n' % c
|
|
125
|
+
|
|
126
|
+
structName = '%s%s' % (pascalCase(msg.package), msg.name)
|
|
127
|
+
result += 'class %s(Structured, byte_order=ByteOrder.LE, byte_order_mode=ByteOrderMode.OVERRIDE):\n' % structName
|
|
128
|
+
result += ' msg_size = %s\n' % msg.size
|
|
129
|
+
if msg.id != None:
|
|
130
|
+
result += ' msg_id = %s\n' % msg.id
|
|
131
|
+
|
|
132
|
+
result += '\n'.join([FieldPyGen.generate(f)
|
|
133
|
+
for key, f in msg.fields.items()])
|
|
134
|
+
|
|
135
|
+
result += '\n\n def __str__(self):\n'
|
|
136
|
+
result += f' out = "{msg.name} Msg, ID {msg.id}, Size {msg.size} \\n"\n'
|
|
137
|
+
for key, f in msg.fields.items():
|
|
138
|
+
result += f' out += f"{key} = '
|
|
139
|
+
result += '{self.' + key + '}\\n"\n'
|
|
140
|
+
result += f' out += "\\n"\n'
|
|
141
|
+
result += f' return out'
|
|
142
|
+
|
|
143
|
+
result += '\n\n def to_dict(self, include_name = True, include_id = True):\n'
|
|
144
|
+
result += ' out = {}\n'
|
|
145
|
+
# Handle all field types including arrays
|
|
146
|
+
for key, f in msg.fields.items():
|
|
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'
|
|
165
|
+
result += f' out["name"] = "{msg.name}"\n'
|
|
166
|
+
result += ' if include_id:\n'
|
|
167
|
+
result += f' out["msg_id"] = "{msg.id}"\n'
|
|
168
|
+
result += ' return out\n'
|
|
169
|
+
|
|
170
|
+
return result
|
|
171
|
+
|
|
172
|
+
@staticmethod
|
|
173
|
+
def get_initializer(msg, null_init):
|
|
174
|
+
if not msg.fields:
|
|
175
|
+
return '{0}'
|
|
176
|
+
|
|
177
|
+
parts = []
|
|
178
|
+
for field in msg.fields:
|
|
179
|
+
parts.append(field.get_initializer(null_init))
|
|
180
|
+
return '{' + ', '.join(parts) + '}'
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
class FilePyGen():
|
|
184
|
+
@staticmethod
|
|
185
|
+
def generate(package):
|
|
186
|
+
yield '# Automatically generated struct frame header \n'
|
|
187
|
+
yield '# Generated by %s at %s. \n\n' % (version, time.asctime())
|
|
188
|
+
|
|
189
|
+
yield 'from structured import *\n'
|
|
190
|
+
yield 'from enum import Enum\n\n'
|
|
191
|
+
|
|
192
|
+
if package.enums:
|
|
193
|
+
yield '# Enum definitions\n'
|
|
194
|
+
for key, enum in package.enums.items():
|
|
195
|
+
yield EnumPyGen.generate(enum) + '\n\n'
|
|
196
|
+
|
|
197
|
+
if package.messages:
|
|
198
|
+
yield '# Struct definitions \n'
|
|
199
|
+
# Need to sort messages to make sure dependecies are properly met
|
|
200
|
+
|
|
201
|
+
for key, msg in package.sortedMessages().items():
|
|
202
|
+
yield MessagePyGen.generate(msg) + '\n'
|
|
203
|
+
yield '\n'
|
|
204
|
+
|
|
205
|
+
if package.messages:
|
|
206
|
+
|
|
207
|
+
yield '%s_definitions = {\n' % package.name
|
|
208
|
+
for key, msg in package.sortedMessages().items():
|
|
209
|
+
if msg.id != None:
|
|
210
|
+
structName = '%s%s' % (pascalCase(msg.package), msg.name)
|
|
211
|
+
yield ' %s: %s,\n' % (msg.id, structName)
|
|
212
|
+
|
|
213
|
+
yield '}\n'
|
struct_frame/ts_gen.py
ADDED
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# kate: replace-tabs on; indent-width 4;
|
|
3
|
+
|
|
4
|
+
from struct_frame import version, NamingStyleC
|
|
5
|
+
import time
|
|
6
|
+
|
|
7
|
+
StyleC = NamingStyleC()
|
|
8
|
+
|
|
9
|
+
ts_types = {
|
|
10
|
+
"int8": 'Int8',
|
|
11
|
+
"uint8": 'UInt8',
|
|
12
|
+
"int16": 'Int16LE',
|
|
13
|
+
"uint16": 'UInt16LE',
|
|
14
|
+
"bool": 'Boolean8',
|
|
15
|
+
"double": 'Float64LE',
|
|
16
|
+
"float": 'Float32LE',
|
|
17
|
+
"int32": 'Int32LE',
|
|
18
|
+
"uint32": 'UInt32LE',
|
|
19
|
+
"uint64": 'BigInt64LE',
|
|
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',
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class EnumTsGen():
|
|
42
|
+
@staticmethod
|
|
43
|
+
def generate(field, packageName):
|
|
44
|
+
leading_comment = field.comments
|
|
45
|
+
result = ''
|
|
46
|
+
if leading_comment:
|
|
47
|
+
for c in leading_comment:
|
|
48
|
+
result = '%s\n' % c
|
|
49
|
+
|
|
50
|
+
result += 'export enum %s%s' % (packageName,
|
|
51
|
+
StyleC.enum_name(field.name))
|
|
52
|
+
|
|
53
|
+
result += ' {\n'
|
|
54
|
+
|
|
55
|
+
enum_length = len(field.data)
|
|
56
|
+
enum_values = []
|
|
57
|
+
for index, (d) in enumerate(field.data):
|
|
58
|
+
leading_comment = field.data[d][1]
|
|
59
|
+
|
|
60
|
+
if leading_comment:
|
|
61
|
+
for c in leading_comment:
|
|
62
|
+
enum_values.append(c)
|
|
63
|
+
|
|
64
|
+
comma = ","
|
|
65
|
+
if index == enum_length - 1:
|
|
66
|
+
# last enum member should not end with a comma
|
|
67
|
+
comma = ""
|
|
68
|
+
|
|
69
|
+
enum_value = " %s = %d%s" % (
|
|
70
|
+
StyleC.enum_entry(d), field.data[d][0], comma)
|
|
71
|
+
|
|
72
|
+
enum_values.append(enum_value)
|
|
73
|
+
|
|
74
|
+
result += '\n'.join(enum_values)
|
|
75
|
+
result += '\n}'
|
|
76
|
+
|
|
77
|
+
return result
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
class FieldTsGen():
|
|
81
|
+
@staticmethod
|
|
82
|
+
def generate(field, packageName):
|
|
83
|
+
result = ''
|
|
84
|
+
isEnum = False
|
|
85
|
+
var_name = StyleC.var_name(field.name)
|
|
86
|
+
type_name = field.fieldType
|
|
87
|
+
|
|
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})'
|
|
119
|
+
else:
|
|
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}\')'
|
|
145
|
+
|
|
146
|
+
leading_comment = field.comments
|
|
147
|
+
if leading_comment:
|
|
148
|
+
for c in leading_comment:
|
|
149
|
+
result = c + "\n" + result
|
|
150
|
+
|
|
151
|
+
return result
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
# ---------------------------------------------------------------------------
|
|
155
|
+
# Generation of messages (structures)
|
|
156
|
+
# ---------------------------------------------------------------------------
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
class MessageTsGen():
|
|
160
|
+
@staticmethod
|
|
161
|
+
def generate(msg, packageName):
|
|
162
|
+
leading_comment = msg.comments
|
|
163
|
+
|
|
164
|
+
result = ''
|
|
165
|
+
if leading_comment:
|
|
166
|
+
for c in msg.comments:
|
|
167
|
+
result = '%s\n' % c
|
|
168
|
+
|
|
169
|
+
package_msg_name = '%s_%s' % (packageName, msg.name)
|
|
170
|
+
|
|
171
|
+
result += 'export const %s = new typed_struct.Struct(\'%s\') ' % (
|
|
172
|
+
package_msg_name, package_msg_name)
|
|
173
|
+
|
|
174
|
+
result += '\n'
|
|
175
|
+
|
|
176
|
+
size = 1
|
|
177
|
+
if not msg.fields:
|
|
178
|
+
# Empty structs are not allowed in C standard.
|
|
179
|
+
# Therefore add a dummy field if an empty message occurs.
|
|
180
|
+
result += ' .UInt8(\'dummy_field\');'
|
|
181
|
+
else:
|
|
182
|
+
size = msg.size
|
|
183
|
+
|
|
184
|
+
result += '\n'.join([FieldTsGen.generate(f, packageName)
|
|
185
|
+
for key, f in msg.fields.items()])
|
|
186
|
+
result += '\n .compile();\n\n'
|
|
187
|
+
|
|
188
|
+
result += 'export const %s_max_size = %d;\n' % (package_msg_name, size)
|
|
189
|
+
|
|
190
|
+
if msg.id:
|
|
191
|
+
result += 'export const %s_msgid = %d\n' % (
|
|
192
|
+
package_msg_name, msg.id)
|
|
193
|
+
|
|
194
|
+
result += 'export function %s_encode(buffer: struct_frame_buffer, msg: any) {\n' % (
|
|
195
|
+
package_msg_name)
|
|
196
|
+
result += ' msg_encode(buffer, msg, %s_msgid)\n}\n' % (package_msg_name)
|
|
197
|
+
|
|
198
|
+
result += 'export function %s_reserve(buffer: struct_frame_buffer) {\n' % (
|
|
199
|
+
package_msg_name)
|
|
200
|
+
result += ' const msg_buffer = msg_reserve(buffer, %s_msgid, %s_max_size);\n' % (
|
|
201
|
+
package_msg_name, package_msg_name)
|
|
202
|
+
result += ' if (msg_buffer){\n'
|
|
203
|
+
result += ' return new %s(msg_buffer)\n }\n return;\n}\n' % (
|
|
204
|
+
package_msg_name)
|
|
205
|
+
|
|
206
|
+
result += 'export function %s_finish(buffer: struct_frame_buffer) {\n' % (
|
|
207
|
+
package_msg_name)
|
|
208
|
+
result += ' msg_finish(buffer);\n}\n'
|
|
209
|
+
return result + '\n'
|
|
210
|
+
|
|
211
|
+
@staticmethod
|
|
212
|
+
def get_initializer(msg, null_init):
|
|
213
|
+
if not msg.fields:
|
|
214
|
+
return '{0}'
|
|
215
|
+
|
|
216
|
+
parts = []
|
|
217
|
+
for field in msg.fields:
|
|
218
|
+
parts.append(field.get_initializer(null_init))
|
|
219
|
+
return '{' + ', '.join(parts) + '}'
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
class FileTsGen():
|
|
223
|
+
@staticmethod
|
|
224
|
+
def generate(package):
|
|
225
|
+
yield '/* Automatically generated struct frame header */\n'
|
|
226
|
+
yield '/* Generated by %s at %s. */\n\n' % (version, time.asctime())
|
|
227
|
+
|
|
228
|
+
yield 'const typed_struct = require(\'typed-struct\')\n'
|
|
229
|
+
yield 'const ExtractType = typeof typed_struct.ExtractType;\n'
|
|
230
|
+
yield 'const type = typeof typed_struct.ExtractType;\n\n'
|
|
231
|
+
|
|
232
|
+
yield "import { struct_frame_buffer } from './struct_frame_types';\n"
|
|
233
|
+
|
|
234
|
+
yield "import { msg_encode, msg_reserve, msg_finish } from './struct_frame';\n\n"
|
|
235
|
+
|
|
236
|
+
# include additional header files here if available in the future
|
|
237
|
+
|
|
238
|
+
if package.enums:
|
|
239
|
+
yield '/* Enum definitions */\n'
|
|
240
|
+
for key, enum in package.enums.items():
|
|
241
|
+
yield EnumTsGen.generate(enum, package.name) + '\n\n'
|
|
242
|
+
|
|
243
|
+
if package.messages:
|
|
244
|
+
yield '/* Struct definitions */\n'
|
|
245
|
+
for key, msg in package.sortedMessages().items():
|
|
246
|
+
yield MessageTsGen.generate(msg, package.name) + '\n'
|
|
247
|
+
yield '\n'
|
|
248
|
+
|
|
249
|
+
if package.messages:
|
|
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}'
|
|
260
|
+
yield '\n'
|