Skip to content

Commit f568bd3

Browse files
committed
Typescript union handling
Signed-off-by: Clemens Vasters <clemens@vasters.com>
1 parent 0a47bf4 commit f568bd3

File tree

2 files changed

+63
-39
lines changed

2 files changed

+63
-39
lines changed

avrotize/avrotots.py

+58-38
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,7 @@ def generate_class(self, avro_schema: Dict, parent_namespace: str, write_file: b
158158
'is_primitive': field['definition']['is_primitive'],
159159
'is_enum': field['definition']['is_enum'],
160160
'is_array': field['definition']['is_array'],
161+
'is_union': field['definition']['is_union'],
161162
'docstring': field['docstring'],
162163
} for field in fields]
163164

@@ -231,12 +232,14 @@ def generate_field(self, field: Dict, parent_namespace: str, import_types: Set[s
231232
field['type'], parent_namespace, import_types_this, class_name, field['name'])
232233
import_types.update(import_types_this)
233234
field_name = field['name']
235+
import_name = import_types_this.pop() if len(import_types_this) > 0 else ''
234236
return {
235237
'name': field_name,
236238
'type': field_type,
237239
'is_primitive': self.is_typescript_primitive(field_type.replace('[]', '')),
238240
'is_array': field_type.endswith('[]'),
239-
'is_enum': len(import_types_this) > 0 and self.is_enum_type(import_types_this.pop(),'')
241+
'is_union': self.generated_types.get(import_name, '') == 'union',
242+
'is_enum': self.generated_types.get(import_name, '') == 'enum',
240243
}
241244

242245
def get_is_json_match_clause(self, field_name: str, field_type: str, field_is_enum: bool) -> str:
@@ -281,16 +284,9 @@ def get_is_json_match_clause(self, field_name: str, field_type: str, field_is_en
281284
def generate_embedded_union(self, class_name: str, field_name: str, avro_type: List, parent_namespace: str, parent_import_types: Set[str], write_file: bool = True) -> str:
282285
"""Generate embedded Union class for a field with namespace support."""
283286
union_class_name = pascal(field_name) + 'Union' if field_name else pascal(class_name) + 'Union'
284-
namespace = parent_namespace
285-
union_types = [self.convert_avro_type_to_typescript( t, parent_namespace, parent_import_types) for t in avro_type if t != 'null']
286-
import_types = []
287-
for t in avro_type:
288-
if isinstance(t, str) and t in self.generated_types:
289-
import_types.append(t)
290-
elif isinstance(t, dict) and 'type' in t and t['type'] == "array" and isinstance(t['items'], str) and t['items'] in self.generated_types:
291-
import_types.append(t['items'])
292-
elif isinstance(t, dict) and 'type' in t and t['type'] == "map" and isinstance(t['values'], str) and t['values'] in self.generated_types:
293-
import_types.append(t['values'])
287+
namespace = self.concat_namespace(self.base_package, parent_namespace)
288+
import_types:Set[str] = set()
289+
union_types = [self.convert_avro_type_to_typescript( t, parent_namespace, import_types) for t in avro_type if t != 'null']
294290
if not import_types:
295291
return '|'.join(union_types)
296292
class_definition = ''
@@ -299,67 +295,91 @@ def generate_embedded_union(self, class_name: str, field_name: str, avro_type: L
299295
continue # Avoid importing itself
300296
import_type_parts = import_type.split('.')
301297
import_type_name = pascal(import_type_parts[-1])
302-
import_namespace = '.'.join(import_type_parts[:-1])
303298
import_path = '/'.join(import_type_parts)
304299
current_path = '/'.join(namespace.split('.'))
305300
relative_import_path = os.path.relpath(import_path, current_path).replace(os.sep, '/')
306301
if not relative_import_path.startswith('.'):
307302
relative_import_path = f'./{relative_import_path}'
308303
class_definition += f"import {{ {import_type_name} }} from '{relative_import_path}.js';\n"
304+
305+
if self.typed_json_annotation:
306+
class_definition += "import 'reflect-metadata';\n"
307+
class_definition += "import { CustomDeserializerParams, CustomSerializerParams } from 'typedjson/lib/types/metadata.js';\n"
308+
309309

310-
class_definition += f"\nexport namespace {namespace} {{\n"
311-
class_definition += f"{self.INDENT}export class {union_class_name} {{\n"
310+
class_definition += f"\nexport class {union_class_name} {{\n"
312311

313-
class_definition += f"{self.INDENT*2}private value: any;\n\n"
312+
class_definition += f"{self.INDENT}private value: any;\n\n"
314313

315314
# Constructor
316-
class_definition += f"{self.INDENT*2}constructor(value: { ' | '.join(union_types) }) {{\n"
317-
class_definition += f"{self.INDENT*3}this.value = value;\n"
318-
class_definition += f"{self.INDENT*2}}}\n\n"
315+
class_definition += f"{self.INDENT}constructor(value: { ' | '.join(union_types) }) {{\n"
316+
class_definition += f"{self.INDENT*2}this.value = value;\n"
317+
class_definition += f"{self.INDENT}}}\n\n"
319318

320319
# Method to check which type is set
321320
for union_type in union_types:
322-
type_check_method = f"{self.INDENT*2}public is{pascal(union_type)}(): boolean {{\n"
321+
type_check_method = f"{self.INDENT}public is{pascal(union_type)}(): boolean {{\n"
323322
if union_type.strip() in ['string', 'number', 'boolean']:
324-
type_check_method += f"{self.INDENT*3}return typeof this.value === '{union_type.strip()}';\n"
323+
type_check_method += f"{self.INDENT*2}return typeof this.value === '{union_type.strip()}';\n"
325324
elif union_type.strip() == 'Date':
326-
type_check_method += f"{self.INDENT*3}return this.value instanceof Date;\n"
325+
type_check_method += f"{self.INDENT*2}return this.value instanceof Date;\n"
327326
else:
328-
type_check_method += f"{self.INDENT*3}return this.value instanceof {union_type.strip()};\n"
329-
type_check_method += f"{self.INDENT*2}}}\n\n"
327+
type_check_method += f"{self.INDENT*2}return this.value instanceof {union_type.strip()};\n"
328+
type_check_method += f"{self.INDENT}}}\n\n"
330329
class_definition += type_check_method
331330

332331
# Method to return the current value
333-
class_definition += f"{self.INDENT*2}public toObject(): any {{\n"
334-
class_definition += f"{self.INDENT*3}return this.value;\n"
335-
class_definition += f"{self.INDENT*2}}}\n\n"
332+
class_definition += f"{self.INDENT}public toJSON(): string {{\n"
333+
class_definition += f"{self.INDENT*2}let rawJson : Uint8Array = this.value.toByteArray('application/json');\n"
334+
class_definition += f"{self.INDENT*2}return new TextDecoder().decode(rawJson);\n"
335+
class_definition += f"{self.INDENT}}}\n\n"
336336

337337
# Method to check if JSON matches any of the union types
338-
class_definition += f"{self.INDENT*2}public static isJsonMatch(element: any): boolean {{\n"
338+
class_definition += f"{self.INDENT}public static isJsonMatch(element: any): boolean {{\n"
339339
match_clauses = []
340340
for union_type in union_types:
341341
match_clauses.append(f"({self.get_is_json_match_clause('value', union_type, False)})")
342-
class_definition += f"{self.INDENT*3}return {' || '.join(match_clauses)};\n"
343-
class_definition += f"{self.INDENT*2}}}\n\n"
342+
class_definition += f"{self.INDENT*2}return {' || '.join(match_clauses)};\n"
343+
class_definition += f"{self.INDENT}}}\n\n"
344344

345345
# Method to deserialize from JSON
346-
class_definition += f"{self.INDENT*2}public static fromData(element: any, contentTypeString: string): {union_class_name} {{\n"
347-
class_definition += f"{self.INDENT*3}const unionTypes = [{', '.join([t.strip() for t in union_types if not self.is_typescript_primitive(t.strip())])}];\n"
348-
class_definition += f"{self.INDENT*3}for (const type of unionTypes) {{\n"
349-
class_definition += f"{self.INDENT*4}if (type.isJsonMatch(element)) {{\n"
350-
class_definition += f"{self.INDENT*5}return new {union_class_name}(type.fromData(element, contentTypeString));\n"
351-
class_definition += f"{self.INDENT*4}}}\n"
346+
class_definition += f"{self.INDENT}public static fromData(element: any, contentTypeString: string): {union_class_name} {{\n"
347+
class_definition += f"{self.INDENT*2}const unionTypes = [{', '.join([t.strip() for t in union_types if not self.is_typescript_primitive(t.strip())])}];\n"
348+
class_definition += f"{self.INDENT*2}for (const type of unionTypes) {{\n"
349+
class_definition += f"{self.INDENT*3}if (type.isJsonMatch(element)) {{\n"
350+
class_definition += f"{self.INDENT*4}return new {union_class_name}(type.fromData(element, contentTypeString));\n"
352351
class_definition += f"{self.INDENT*3}}}\n"
353-
class_definition += f"{self.INDENT*3}throw new Error('No matching type for union');\n"
354352
class_definition += f"{self.INDENT*2}}}\n"
355-
353+
class_definition += f"{self.INDENT*2}throw new Error('No matching type for union');\n"
356354
class_definition += f"{self.INDENT}}}\n"
355+
356+
# Method to deserialize from JSON with custom deserializer params
357+
class_definition += f"{self.INDENT}public static fromJSON(json: any, params: CustomDeserializerParams): {union_class_name} {{\n"
358+
class_definition += f"{self.INDENT*2}try {{\n"
359+
class_definition += f"{self.INDENT*3}return {union_class_name}.fromData(json, 'application/json');\n"
360+
class_definition += f"{self.INDENT*2}}} catch (error) {{\n"
361+
class_definition += f"{self.INDENT*3}return params.fallback(json, {union_class_name});\n"
362+
class_definition += f"{self.INDENT*2}}}\n"
363+
class_definition += f"{self.INDENT}}}\n\n"
364+
365+
# Method to serialize to JSON with custom serializer params
366+
class_definition += f"{self.INDENT}public static toJSON(obj: any, params: CustomSerializerParams): any {{\n"
367+
class_definition += f"{self.INDENT*2}try {{\n"
368+
class_definition += f"{self.INDENT*3}const val = new {union_class_name}(obj);\n"
369+
class_definition += f"{self.INDENT*3}return val.toJSON();\n"
370+
class_definition += f"{self.INDENT*2}}} catch (error) {{\n"
371+
class_definition += f"{self.INDENT*3}return params.fallback(this, {union_class_name});\n"
372+
class_definition += f"{self.INDENT*2}}}\n"
373+
class_definition += f"{self.INDENT}}}\n\n"
374+
357375
class_definition += "}\n"
358376

359377
if write_file:
360378
self.write_to_file(namespace, union_class_name, class_definition)
361379

362-
return f"{namespace}.{union_class_name}"
380+
parent_import_types.add(f"{namespace}.{union_class_name}")
381+
self.generated_types[f"{namespace}.{union_class_name}"] = 'union'
382+
return f"{union_class_name}"
363383

364384
def write_to_file(self, namespace: str, name: str, content: str):
365385
"""Write TypeScript class to file in the correct namespace directory."""

avrotize/avrotots/class_core.ts.jinja

+5-1
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,11 @@ export class {{ class_name }} {
2727
/** {{ field.docstring }} */
2828
{%- if typed_json_annotation %}
2929
{%- set field_type = field.type_no_null if not field.is_primitive else (field.type_no_null | pascal ) %}
30-
{% if field.is_array -%}@jsonArrayMember({{ field_type | replace('[]', '') }}) {%- else -%}@jsonMember({%-if not field.is_enum-%}{{ field_type }}{%-else-%}String{%-endif-%}){%- endif %}
30+
{% if field.is_union -%}
31+
@jsonMember({serializer: {{ field_type }}.toJSON, deserializer: {{ field_type }}.fromJSON})
32+
{%- elif field.is_array -%}@jsonArrayMember({{ field_type | replace('[]', '') }})
33+
{%- else -%}@jsonMember({%-if not field.is_enum-%}{{ field_type }}{%-else-%}String{%-endif-%})
34+
{%- endif %}
3135
{%- endif %}
3236
public {{ field.name }}{%- if field.type.endswith('?')-%}?{%-endif-%} : {{ field.type_no_null }};
3337
{%- endfor %}

0 commit comments

Comments
 (0)