9
9
from jinja2 import Environment , FileSystemLoader , select_autoescape
10
10
11
11
from ts_oas_generator import constants
12
- from ts_oas_generator .generator .filters import FILTERS , ts_camel_case , ts_pascal_case , ts_type
12
+ from ts_oas_generator .generator .filters import FILTERS , ts_camel_case , ts_kebab_case , ts_pascal_case , ts_type
13
13
from ts_oas_generator .parser .oas_parser import OASParser
14
14
15
15
# Type aliases for clarity
@@ -120,7 +120,6 @@ def __init__(self, template_dir: Path | None = None) -> None:
120
120
self .env = self ._create_environment ()
121
121
122
122
def _create_environment (self ) -> Environment :
123
- """Create and configure Jinja2 environment."""
124
123
env = Environment (
125
124
loader = FileSystemLoader (str (self .template_dir )),
126
125
autoescape = select_autoescape (["html" , "xml" ]),
@@ -131,12 +130,10 @@ def _create_environment(self) -> Environment:
131
130
return env
132
131
133
132
def render (self , template_name : str , context : TemplateContext ) -> str :
134
- """Render a single template."""
135
133
template = self .env .get_template (template_name )
136
134
return template .render (** context )
137
135
138
136
def render_batch (self , template_map : dict [Path , tuple [str , TemplateContext ]]) -> FileMap :
139
- """Render multiple templates."""
140
137
return {path : self .render (template , context ) for path , (template , context ) in template_map .items ()}
141
138
142
139
@@ -145,25 +142,25 @@ class SchemaProcessor:
145
142
146
143
def __init__ (self , renderer : TemplateRenderer ) -> None :
147
144
self .renderer = renderer
145
+ self ._wire_to_canonical : dict [str , str ] = {}
146
+ self ._camel_to_wire : dict [str , str ] = {}
148
147
149
148
def generate_models (self , output_dir : Path , schemas : Schema ) -> FileMap :
150
- """Generate TypeScript model files from schemas."""
151
149
models_dir = output_dir / constants .DirectoryName .SRC / constants .DirectoryName .MODELS
152
150
files : FileMap = {}
153
151
154
152
# Generate individual model files
155
153
for name , schema in schemas .items ():
156
154
context = self ._create_model_context (name , schema , schemas )
157
155
content = self .renderer .render (constants .MODEL_TEMPLATE , context )
158
- files [models_dir / f"{ name .lower ()} { constants .MODEL_FILE_EXTENSION } " ] = content
156
+ file_name = f"{ ts_kebab_case (name )} { constants .MODEL_FILE_EXTENSION } "
157
+ files [models_dir / file_name ] = content
159
158
160
- # Generate comprehensive AlgokitSignedTransaction model
161
159
# TODO(utils-ts): Delete this temporary model once utils-ts is part of the monorepo
162
160
files [models_dir / f"algokitsignedtransaction{ constants .MODEL_FILE_EXTENSION } " ] = self .renderer .render (
163
161
"models/algokitsignedtransaction.ts.j2" , {}
164
162
)
165
163
166
- # Generate barrel export
167
164
files [models_dir / constants .INDEX_FILE ] = self .renderer .render (
168
165
constants .MODELS_INDEX_TEMPLATE ,
169
166
{"schemas" : schemas , "include_temp_signed_txn" : True },
@@ -172,7 +169,6 @@ def generate_models(self, output_dir: Path, schemas: Schema) -> FileMap:
172
169
return files
173
170
174
171
def _create_model_context (self , name : str , schema : Schema , all_schemas : Schema ) -> TemplateContext :
175
- """Create context for model template rendering."""
176
172
is_object = self ._is_object_schema (schema )
177
173
properties = self ._extract_properties (schema ) if is_object else []
178
174
@@ -188,21 +184,19 @@ def _create_model_context(self, name: str, schema: Schema, all_schemas: Schema)
188
184
189
185
@staticmethod
190
186
def _is_object_schema (schema : Schema ) -> bool :
191
- """Check if schema represents an object type."""
192
187
is_type_object = schema .get (constants .SchemaKey .TYPE ) == constants .TypeScriptType .OBJECT
193
188
has_properties = constants .SchemaKey .PROPERTIES in schema
194
189
has_composition = any (
195
190
k in schema for k in [constants .SchemaKey .ALL_OF , constants .SchemaKey .ONE_OF , constants .SchemaKey .ANY_OF ]
196
191
)
197
192
return (is_type_object or has_properties ) and not has_composition
198
193
199
- @staticmethod
200
- def _extract_properties (schema : Schema ) -> list [dict [str , Any ]]:
201
- """Extract properties from an object schema."""
194
+ def _extract_properties (self , schema : Schema ) -> list [dict [str , Any ]]:
202
195
properties = []
203
196
required_fields = set (schema .get (constants .SchemaKey .REQUIRED , []))
204
197
205
198
for prop_name , prop_schema in (schema .get (constants .SchemaKey .PROPERTIES ) or {}).items ():
199
+ self ._register_rename (prop_name , prop_schema )
206
200
properties .append (
207
201
{
208
202
"name" : prop_name ,
@@ -213,6 +207,19 @@ def _extract_properties(schema: Schema) -> list[dict[str, Any]]:
213
207
214
208
return properties
215
209
210
+ def _register_rename (self , wire_name : str , schema : dict [str , Any ]) -> None :
211
+ rename_value = schema .get (constants .X_ALGOKIT_FIELD_RENAME )
212
+ if not isinstance (rename_value , str ) or not rename_value :
213
+ return
214
+
215
+ # Preserve first occurrence to avoid accidental overrides from conflicting specs
216
+ self ._wire_to_canonical .setdefault (wire_name , rename_value )
217
+ self ._camel_to_wire .setdefault (ts_camel_case (rename_value ), wire_name )
218
+
219
+ @property
220
+ def rename_mappings (self ) -> tuple [dict [str , str ], dict [str , str ]]:
221
+ return self ._wire_to_canonical , self ._camel_to_wire
222
+
216
223
217
224
class OperationProcessor :
218
225
"""Processes OpenAPI operations and generates API services."""
@@ -609,6 +616,7 @@ def generate(
609
616
files .update (self .schema_processor .generate_models (output_dir , all_schemas ))
610
617
files .update (self .operation_processor .generate_service (output_dir , ops_by_tag , tags , service_class ))
611
618
files .update (self ._generate_client_files (output_dir , client_class , service_class ))
619
+ files .update (self ._generate_rename_map (output_dir ))
612
620
613
621
return files
614
622
@@ -642,6 +650,7 @@ def _generate_runtime(
642
650
core_dir / "json.ts" : ("base/src/core/json.ts.j2" , context ),
643
651
core_dir / "msgpack.ts" : ("base/src/core/msgpack.ts.j2" , context ),
644
652
core_dir / "casing.ts" : ("base/src/core/casing.ts.j2" , context ),
653
+ core_dir / "codecs.ts" : ("base/src/core/codecs.ts.j2" , context ),
645
654
# Project files
646
655
src_dir / "index.ts" : ("base/src/index.ts.j2" , context ),
647
656
output_dir / "package.json" : ("base/package.json.j2" , context ),
@@ -669,6 +678,23 @@ def _generate_client_files(self, output_dir: Path, client_class: str, service_cl
669
678
670
679
return self .renderer .render_batch (template_map )
671
680
681
+ def _generate_rename_map (self , output_dir : Path ) -> FileMap :
682
+ """Render rename map supporting vendor rename extensions."""
683
+ wire_to_canonical , camel_to_wire = self .schema_processor .rename_mappings
684
+
685
+ core_dir = output_dir / constants .DirectoryName .SRC / constants .DirectoryName .CORE
686
+ return {
687
+ core_dir / "rename-map.ts" : (
688
+ self .renderer .render (
689
+ "base/src/core/rename-map.ts.j2" ,
690
+ {
691
+ "wire_to_canonical" : wire_to_canonical ,
692
+ "camel_to_wire" : camel_to_wire ,
693
+ },
694
+ )
695
+ )
696
+ }
697
+
672
698
@staticmethod
673
699
def _extract_class_names (package_name : str ) -> tuple [str , str ]:
674
700
"""Extract client and service class names from package name."""
0 commit comments