Class: Sxn::Templates::TemplateEngine

Inherits:
Object
  • Object
show all
Defined in:
lib/sxn/templates/template_engine.rb

Overview

TemplateEngine is the main interface for template processing in sxn. It combines template processing, variable collection, and security validation to provide a safe and convenient API for generating files from templates.

Features:

  • Built-in template discovery

  • Automatic variable collection

  • Security validation

  • Template caching

  • Multiple template formats support

Example:

engine = TemplateEngine.new(session: session, project: project)
engine.process_template("rails/CLAUDE.md", "/path/to/output.md")

Constant Summary collapse

TEMPLATES_DIR =

Built-in template directory

File.expand_path("../templates", __dir__)

Instance Method Summary collapse

Constructor Details

#initialize(session: nil, project: nil, config: nil) ⇒ TemplateEngine

Returns a new instance of TemplateEngine.



44
45
46
47
48
49
50
51
52
53
# File 'lib/sxn/templates/template_engine.rb', line 44

def initialize(session: nil, project: nil, config: nil)
  @session = session
  @project = project
  @config = config

  @processor = TemplateProcessor.new
  @variables_collector = TemplateVariables.new(session, project, config)
  @security = TemplateSecurity.new
  @template_cache = {}
end

Instance Method Details

#apply_template_set(template_set, destination_dir, custom_variables = {}, options = {}) ⇒ Array<String>

Apply a set of templates to a directory

Parameters:

  • template_set (String)

    Name of template set (rails, javascript, common)

  • destination_dir (String)

    Directory to apply templates to

  • custom_variables (Hash) (defaults to: {})

    Additional variables

  • options (Hash) (defaults to: {})

    Processing options

Returns:

  • (Array<String>)

    List of created files



271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
# File 'lib/sxn/templates/template_engine.rb', line 271

def apply_template_set(template_set, destination_dir, custom_variables = {}, options = {})
  templates = list_templates(template_set)
  created_files = []

  templates.each do |template_name|
    # Determine output filename (remove .liquid extension)
    output_name = template_name.sub(/\.liquid$/, "")
    output_path = File.join(destination_dir, File.basename(output_name))

    begin
      process_template(template_name, output_path, custom_variables, options)
      created_files << output_path
    rescue StandardError => e
      # Log error but continue with other templates
      warn "Failed to process template #{template_name}: #{e.message}"
    end
  end

  created_files
end

#available_variables(custom_variables = {}) ⇒ Hash

Get available variables for templates

Parameters:

  • custom_variables (Hash) (defaults to: {})

    Additional variables to include

Returns:

  • (Hash)

    All available variables



204
205
206
# File 'lib/sxn/templates/template_engine.rb', line 204

def available_variables(custom_variables = {})
  collect_variables(custom_variables)
end

#clear_cache!Object

Clear template cache



214
215
216
217
# File 'lib/sxn/templates/template_engine.rb', line 214

def clear_cache!
  @template_cache.clear
  @security.clear_cache!
end

#list_templates(category = nil) ⇒ Array<String>

List available built-in templates

Parameters:

  • category (String) (defaults to: nil)

    Optional category filter (rails, javascript, common)

Returns:

  • (Array<String>)

    List of available template names



116
117
118
119
120
121
122
123
# File 'lib/sxn/templates/template_engine.rb', line 116

def list_templates(category = nil)
  templates_dir = category ? File.join(TEMPLATES_DIR, category) : TEMPLATES_DIR
  return [] unless Dir.exist?(templates_dir)

  Dir.glob("**/*.liquid", base: templates_dir).map do |path|
    category ? File.join(category, path) : path
  end.sort
end

#process_string(template_content, custom_variables = {}, options = {}) ⇒ String

Process a template string directly (not from file)

Parameters:

  • template_content (String)

    The template content

  • custom_variables (Hash) (defaults to: {})

    Variables to use

  • options (Hash) (defaults to: {})

    Processing options

Returns:

  • (String)

    Processed template



225
226
227
228
229
230
231
232
233
# File 'lib/sxn/templates/template_engine.rb', line 225

def process_string(template_content, custom_variables = {}, options = {})
  options = { validate: true }.merge(options)

  variables = collect_variables(custom_variables)

  @security.validate_template(template_content, variables) if options[:validate]

  @processor.process(template_content, variables, options)
end

#process_template(template_name, destination_path, custom_variables = {}, options = {}) ⇒ String

Process a template and write it to the specified destination

Parameters:

  • template_name (String)

    Name/path of the template (e.g., “rails/CLAUDE.md”)

  • destination_path (String)

    Where to write the processed template

  • custom_variables (Hash) (defaults to: {})

    Additional variables to merge

  • options (Hash) (defaults to: {})

    Processing options

Options Hash (options):

  • :force (Boolean) — default: false

    Overwrite existing files

  • :validate (Boolean) — default: true

    Validate template security

  • :template_dir (String)

    Custom template directory

Returns:

  • (String)

    Path to the created file



65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
# File 'lib/sxn/templates/template_engine.rb', line 65

def process_template(template_name, destination_path, custom_variables = {}, options = {})
  options = { force: false, validate: true, template_dir: nil }.merge(options)

  # Find the template file
  template_path = find_template(template_name, options[:template_dir])

  # Check if destination exists and handle accordingly
  destination = Pathname.new(destination_path)
  if destination.exist? && !options[:force]
    raise Errors::TemplateError,
          "Destination file already exists: #{destination_path}. Use force: true to overwrite."
  end

  # steep:ignore:start - Template processing uses dynamic variable resolution
  # Liquid template processing and variable collection use dynamic features
  # that cannot be statically typed. Runtime validation provides safety.

  # Collect variables
  variables = collect_variables(custom_variables)

  # Runtime validation of template variables
  variables = RuntimeValidations.validate_template_variables(variables)

  # Validate template security if requested
  if options[:validate]
    template_content = File.read(template_path)
    @security.validate_template(template_content, variables)
  end

  # Process the template with runtime validation
  result = @processor.process_file(template_path, variables, options)

  # Create destination directory if it doesn't exist
  destination.dirname.mkpath

  # Write the result
  destination.write(result)

  destination_path
rescue Errors::TemplateSecurityError
  # Re-raise security errors without wrapping
  raise
rescue StandardError => e
  raise Errors::TemplateProcessingError,
        "Failed to process template '#{template_name}': #{e.message}"
end

#refresh_variables!Object

Refresh variable cache (useful for long-running processes)



209
210
211
# File 'lib/sxn/templates/template_engine.rb', line 209

def refresh_variables!
  @variables_collector.refresh!
end

#render_template(template_name, variables = {}, options = {}) ⇒ String

Render a template with variables

Parameters:

  • template_name (String)

    Name/path of the template

  • variables (Hash) (defaults to: {})

    Variables to use for rendering

  • options (Hash) (defaults to: {})

    Processing options

Returns:

  • (String)

    Rendered template content



241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
# File 'lib/sxn/templates/template_engine.rb', line 241

def render_template(template_name, variables = {}, options = {})
  # Find the template file
  template_path = find_template(template_name, options[:template_dir])

  # Read template content
  template_content = File.read(template_path)

  # Merge with available variables
  all_variables = collect_variables(variables)

  # Validate template security if requested
  @security.validate_template(template_content, all_variables) if options.fetch(:validate, true)

  # Process and return the result
  @processor.process(template_content, all_variables, options)
rescue Errors::TemplateSecurityError
  # Re-raise security errors without wrapping
  raise
rescue StandardError => e
  raise Errors::TemplateProcessingError,
        "Failed to render template '#{template_name}': #{e.message}"
end

#template_categoriesArray<String>

Get template categories

Returns:

  • (Array<String>)

    List of template categories



128
129
130
131
132
133
134
135
# File 'lib/sxn/templates/template_engine.rb', line 128

def template_categories
  return [] unless Dir.exist?(TEMPLATES_DIR)

  Dir.entries(TEMPLATES_DIR)
     .select { |entry| File.directory?(File.join(TEMPLATES_DIR, entry)) }
     .reject { |entry| entry.start_with?(".") }
     .sort
end

#template_exists?(template_name, template_dir = nil) ⇒ Boolean

Check if a template exists

Parameters:

  • template_name (String)

    Name of the template to check

  • template_dir (String) (defaults to: nil)

    Optional custom template directory

Returns:

  • (Boolean)

    true if template exists



142
143
144
145
146
147
# File 'lib/sxn/templates/template_engine.rb', line 142

def template_exists?(template_name, template_dir = nil)
  find_template(template_name, template_dir)
  true
rescue Errors::TemplateNotFoundError
  false
end

#template_info(template_name, template_dir = nil) ⇒ Hash

Get template information

Parameters:

  • template_name (String)

    Name of the template

  • template_dir (String) (defaults to: nil)

    Optional custom template directory

Returns:

  • (Hash)

    Template metadata



154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
# File 'lib/sxn/templates/template_engine.rb', line 154

def template_info(template_name, template_dir = nil)
  template_path = find_template(template_name, template_dir)
  template_content = File.read(template_path)

  {
    name: template_name,
    path: template_path,
    size: template_content.bytesize,
    variables: @processor.extract_variables(template_content),
    syntax_valid: validate_template_syntax(template_content)
  }
rescue StandardError => e
  {
    name: template_name,
    error: e.message,
    syntax_valid: false
  }
end

#validate_template_syntax(template_name, template_dir = nil) ⇒ Boolean

Validate template syntax without processing

Parameters:

  • template_name (String)

    Name of the template

  • template_dir (String) (defaults to: nil)

    Optional custom template directory

Returns:

  • (Boolean)

    true if template syntax is valid



178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
# File 'lib/sxn/templates/template_engine.rb', line 178

def validate_template_syntax(template_name, template_dir = nil)
  # Better detection: if it contains Liquid syntax and no path separators, treat as content
  if template_name.is_a?(String) &&
     (template_name.include?("{{") || template_name.include?("{%")) &&
     !template_name.include?("/") && !template_name.end_with?(".liquid")
    # It's template content with Liquid syntax
    template_content = template_name
  elsif template_name.is_a?(String) && !template_name.include?("\n") &&
        (template_name.include?("/") || template_name.match?(/\.\w+$/) || !template_name.include?("{{"))
    # It's a template name/path
    template_path = find_template(template_name, template_dir)
    template_content = File.read(template_path)
  else
    # Default: treat as content for backward compatibility
    template_content = template_name
  end

  @processor.validate_syntax(template_content)
rescue Errors::TemplateSyntaxError, Errors::TemplateNotFoundError
  false
end