Skip to content

Commit c97c2bf

Browse files
committed
templates refactor, pure file-based, reduce redundancy naming <-> templates
1 parent b5b7503 commit c97c2bf

9 files changed

Lines changed: 209 additions & 170 deletions

File tree

json_explorer/codegen/core/__init__.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
)
1515
from .naming import NameSanitizer, NamingCase
1616
from .config import GeneratorConfig, ConfigManager, ConfigError, load_config
17-
from .templates import TemplateEngine, TemplateError, get_default_template_engine
17+
from .templates import TemplateEngine, TemplateError, create_template_engine
1818

1919
__all__ = [
2020
# Base generator interface
@@ -36,8 +36,8 @@
3636
"ConfigManager",
3737
"ConfigError",
3838
"load_config",
39-
# Template system
39+
# Template system - language-agnostic
4040
"TemplateEngine",
4141
"TemplateError",
42-
"get_default_template_engine",
42+
"create_template_engine",
4343
]

json_explorer/codegen/core/generator.py

Lines changed: 51 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,13 @@
22
Base generator interface for all code generation targets.
33
44
Defines the contract that all language generators must implement.
5-
Focuses on schema-level concerns while leaving type mapping to language generators.
65
"""
76

87
from abc import ABC, abstractmethod
98
from typing import Dict, List, Any, Optional
9+
from pathlib import Path
1010
from .schema import Schema, Field, FieldType
11+
from .templates import TemplateEngine, create_template_engine
1112

1213

1314
class GeneratorError(Exception):
@@ -22,6 +23,17 @@ class CodeGenerator(ABC):
2223
def __init__(self, config: Optional[Dict[str, Any]] = None):
2324
"""Initialize generator with optional configuration."""
2425
self.config = config or {}
26+
self._template_engine = None
27+
self._setup_templates()
28+
29+
def _setup_templates(self):
30+
"""Setup template engine for this generator."""
31+
template_dir = self.get_template_directory()
32+
if template_dir:
33+
self._template_engine = create_template_engine(template_dir)
34+
else:
35+
# Fallback to in-memory templates
36+
self._template_engine = create_template_engine()
2537

2638
@property
2739
@abstractmethod
@@ -35,6 +47,25 @@ def file_extension(self) -> str:
3547
"""Return the file extension for generated files (e.g., '.go', '.py')."""
3648
pass
3749

50+
def get_template_directory(self) -> Optional[Path]:
51+
"""
52+
Return the directory containing templates for this generator.
53+
54+
Subclasses should override this to provide their template directory.
55+
Return None to use in-memory templates only.
56+
57+
Returns:
58+
Path to template directory or None
59+
"""
60+
return None
61+
62+
@property
63+
def template_engine(self) -> TemplateEngine:
64+
"""Get the template engine for this generator."""
65+
if self._template_engine is None:
66+
self._setup_templates()
67+
return self._template_engine
68+
3869
@abstractmethod
3970
def generate(self, schemas: Dict[str, Schema], root_schema_name: str) -> str:
4071
"""
@@ -158,6 +189,25 @@ def format_code(self, code: str) -> str:
158189

159190
return "\n".join(formatted_lines)
160191

192+
# Template helper methods
193+
194+
def render_template(self, template_name: str, context: Dict[str, Any]) -> str:
195+
"""
196+
Render a template with context.
197+
198+
Args:
199+
template_name: Template file name
200+
context: Template variables
201+
202+
Returns:
203+
Rendered content
204+
"""
205+
return self.template_engine.render_template(template_name, context)
206+
207+
def template_exists(self, template_name: str) -> bool:
208+
"""Check if a template exists."""
209+
return self.template_engine.template_exists(template_name)
210+
161211

162212
class GenerationResult:
163213
"""Container for generation results and metadata."""
@@ -187,14 +237,6 @@ def error(cls, message: str, exception: Exception = None) -> "GenerationResult":
187237
result.exception = exception
188238
return result
189239

190-
def add_warning(self, warning: str):
191-
"""Add a warning to the result."""
192-
self.warnings.append(warning)
193-
194-
def add_metadata(self, key: str, value: Any):
195-
"""Add metadata to the result."""
196-
self.metadata[key] = value
197-
198240

199241
def generate_code(
200242
generator: CodeGenerator, schemas: Dict[str, Schema], root_schema_name: str
Lines changed: 33 additions & 79 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,18 @@
11
"""
22
Template engine wrapper for code generation.
33
4-
Provides a simple interface for Jinja2 template rendering
4+
Provides a clean, language-agnostic interface for Jinja2 template rendering
55
with common utilities for code generation.
66
"""
77

8-
import os
98
from typing import Dict, Any, Optional
109
from pathlib import Path
10+
from .naming import NameSanitizer
1111

1212
try:
1313
from jinja2 import (
1414
Environment,
1515
FileSystemLoader,
16-
BaseLoader,
1716
DictLoader,
1817
select_autoescape,
1918
)
@@ -33,7 +32,7 @@ class TemplateError(Exception):
3332

3433

3534
class TemplateEngine:
36-
"""Wrapper for Jinja2 template engine with code generation utilities."""
35+
"""Language-agnostic wrapper for Jinja2 template engine."""
3736

3837
def __init__(self, template_dir: Optional[Path] = None):
3938
"""
@@ -42,6 +41,8 @@ def __init__(self, template_dir: Optional[Path] = None):
4241
Args:
4342
template_dir: Directory containing template files
4443
"""
44+
self._name_sanitizer = NameSanitizer()
45+
4546
if Environment is None:
4647
raise TemplateError(
4748
"Jinja2 is required for template functionality. "
@@ -57,17 +58,17 @@ def _setup_environment(self):
5758
if self.template_dir and self.template_dir.exists():
5859
loader = FileSystemLoader(str(self.template_dir))
5960
else:
60-
# Use in-memory templates
61+
# Use in-memory templates as fallback
6162
loader = DictLoader({})
6263

6364
self._env = Environment(
6465
loader=loader,
6566
autoescape=select_autoescape(["html", "xml"]),
66-
# trim_blocks=True,
6767
lstrip_blocks=True,
68+
# trim_blocks=True,
6869
)
6970

70-
# Add custom filters for code generation
71+
# Add language-agnostic filters for code generation
7172
self._env.filters["snake_case"] = self._snake_case_filter
7273
self._env.filters["camel_case"] = self._camel_case_filter
7374
self._env.filters["pascal_case"] = self._pascal_case_filter
@@ -79,7 +80,7 @@ def render_template(self, template_name: str, context: Dict[str, Any]) -> str:
7980
Render a template with the given context.
8081
8182
Args:
82-
template_name: Name of template file
83+
template_name: Name of template file (e.g., 'struct.go.j2')
8384
context: Variables to pass to template
8485
8586
Returns:
@@ -122,31 +123,31 @@ def add_template(self, name: str, content: str):
122123

123124
self._env.loader.mapping[name] = content
124125

125-
# Template filters for code generation
126+
def template_exists(self, template_name: str) -> bool:
127+
"""Check if a template exists."""
128+
try:
129+
self._env.get_template(template_name)
130+
return True
131+
except:
132+
return False
126133

127-
def _snake_case_filter(self, value: str) -> str:
128-
"""Convert string to snake_case."""
129-
import re
134+
def list_templates(self) -> list[str]:
135+
"""List all available templates."""
136+
try:
137+
return self._env.list_templates()
138+
except:
139+
return []
140+
141+
# Language-agnostic template filters
130142

131-
# Insert underscore before uppercase letters
132-
s1 = re.sub("([a-z0-9])([A-Z])", r"\1_\2", str(value))
133-
# Replace spaces and hyphens with underscores
134-
s2 = re.sub(r"[-\s]+", "_", s1)
135-
return s2.lower()
143+
def _snake_case_filter(self, value: str) -> str:
144+
return self._name_sanitizer._to_snake_case(value)
136145

137146
def _camel_case_filter(self, value: str) -> str:
138-
"""Convert string to camelCase."""
139-
snake = self._snake_case_filter(value)
140-
parts = snake.split("_")
141-
if not parts:
142-
return str(value)
143-
return parts[0].lower() + "".join(p.capitalize() for p in parts[1:])
147+
return self._name_sanitizer._to_camel_case(value)
144148

145149
def _pascal_case_filter(self, value: str) -> str:
146-
"""Convert string to PascalCase."""
147-
snake = self._snake_case_filter(value)
148-
parts = snake.split("_")
149-
return "".join(p.capitalize() for p in parts if p)
150+
return self._name_sanitizer._to_pascal_case(value)
150151

151152
def _indent_filter(self, value: str, spaces: int = 4) -> str:
152153
"""Indent all lines in a string."""
@@ -160,61 +161,14 @@ def _comment_filter(self, value: str, style: str = "//") -> str:
160161
return "\n".join(f"{style} {line}" if line.strip() else line for line in lines)
161162

162163

163-
# Built-in templates for common patterns
164-
GO_STRUCT_TEMPLATE = """
165-
{%- if description %}
166-
// {{ description }}
167-
{%- endif %}
168-
type {{ struct_name }} struct {
169-
{%- for field in fields %}
170-
{%- if field.comment %}
171-
// {{ field.comment }}
172-
{%- endif %}
173-
{{ field.name }} {{ field.type }} {% if field.json_tag %} {{ field.json_tag }} {% endif %}
174-
{%- endfor %}
175-
}
176-
"""
177-
178-
PYTHON_DATACLASS_TEMPLATE = """
179-
@dataclass
180-
class {{ class_name }}:
181-
{%- for field in fields %}
182-
{{ field.name }}: {{ field.type }}{% if field.optional %} = None{% endif %}
183-
{%- endfor %}
184-
"""
185-
186-
# Default template engine instance
187-
_default_engine = None
188-
189-
190-
def get_default_template_engine() -> TemplateEngine:
191-
"""Get the default template engine instance."""
192-
global _default_engine
193-
if _default_engine is None:
194-
_default_engine = TemplateEngine()
195-
196-
# Add built-in templates
197-
_default_engine.add_template("go_struct", GO_STRUCT_TEMPLATE)
198-
_default_engine.add_template("python_dataclass", PYTHON_DATACLASS_TEMPLATE)
199-
200-
return _default_engine
201-
202-
203-
def render_go_struct(
204-
struct_name: str, fields: list, context: Dict[str, Any] = None
205-
) -> str:
164+
def create_template_engine(template_dir: Optional[Path] = None) -> TemplateEngine:
206165
"""
207-
Convenience function to render a Go struct.
166+
Create a template engine instance.
208167
209168
Args:
210-
struct_name: Name of the struct
211-
fields: List of field dictionaries
212-
context: Additional context variables
169+
template_dir: Directory containing template files
213170
214171
Returns:
215-
Rendered Go struct code
172+
Configured TemplateEngine instance
216173
"""
217-
engine = get_default_template_engine()
218-
template_context = {"struct_name": struct_name, "fields": fields, **(context or {})}
219-
220-
return engine.render_template("go_struct", template_context)
174+
return TemplateEngine(template_dir)

0 commit comments

Comments
 (0)