Class: Rhales::SchemaExtractor

Inherits:
Object
  • Object
show all
Defined in:
lib/rhales/utils/schema_extractor.rb

Overview

Extracts schema definitions from .rue files

This class scans template directories for .rue files containing sections and extracts the schema code along with metadata (attributes).

Usage: extractor = SchemaExtractor.new(‘./templates’) schemas = extractor.extract_all schemas.each do |schema_info| puts “#schema_info[:template_name]: #schema_info[:lang]” end

Defined Under Namespace

Classes: ExtractionError

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(templates_dir) ⇒ SchemaExtractor

Returns a new instance of SchemaExtractor.



25
26
27
28
# File 'lib/rhales/utils/schema_extractor.rb', line 25

def initialize(templates_dir)
  @templates_dir = File.expand_path(templates_dir)
  validate_directory!
end

Instance Attribute Details

#templates_dirObject (readonly)

Returns the value of attribute templates_dir.



23
24
25
# File 'lib/rhales/utils/schema_extractor.rb', line 23

def templates_dir
  @templates_dir
end

Instance Method Details

#derive_template_name(file_path) ⇒ Object (private)

Derive template name from file path Examples: /path/to/templates/dashboard.rue => ‘dashboard’ /path/to/templates/pages/user/profile.rue => ‘pages/user/profile’



143
144
145
146
147
148
# File 'lib/rhales/utils/schema_extractor.rb', line 143

def derive_template_name(file_path)
  templates_pathname = Pathname.new(@templates_dir)
  file_pathname = Pathname.new(file_path)
  relative_path = file_pathname.relative_path_from(templates_pathname)
  relative_path.to_s.sub(/\.rue$/, '')
end

#extract_allArray<Hash>

Extract all schemas from .rue files in the templates directory

Examples:

[
  {
    template_name: 'dashboard',
    template_path: '/path/to/dashboard.rue',
    schema_code: 'const schema = z.object({...});',
    lang: 'ts-zod',
    version: '2',
    envelope: 'SuccessEnvelope',
    window: 'appData',
    merge: 'deep',
    layout: 'layouts/main',
    extends: nil
  }
]

Returns:

  • (Array<Hash>)

    Array of schema information hashes



48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
# File 'lib/rhales/utils/schema_extractor.rb', line 48

def extract_all
  rue_files = find_rue_files
  schemas = []

  rue_files.each do |file_path|
    begin
      schema_info = extract_from_file(file_path)
      schemas << schema_info if schema_info
    rescue => e
      warn "Warning: Failed to extract schema from #{file_path}: #{e.message}"
    end
  end

  schemas
end

#extract_from_file(file_path) ⇒ Hash?

Extract schema from a single .rue file

Parameters:

  • file_path (String)

    Path to the .rue file

Returns:

  • (Hash, nil)

    Schema information hash or nil if no schema section



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
# File 'lib/rhales/utils/schema_extractor.rb', line 68

def extract_from_file(file_path)
  doc = RueDocument.parse_file(file_path)

  return nil unless doc.section?('schema')

  template_name = derive_template_name(file_path)
  src = doc.schema_src
  resolved_path = nil
  schema_code = nil

  if src
    # External schema: resolve path and read content
    resolved_path = resolve_schema_src_path(file_path, src)
    schema_code = read_schema_from_src(resolved_path, src, template_name)
  else
    # Inline schema: use content from the schema section
    schema_code = doc.section('schema')
  end

  {
    template_name: template_name,
    template_path: file_path,
    schema_code: schema_code.strip,
    lang: doc.schema_lang,
    version: doc.schema_version,
    envelope: doc.schema_envelope,
    window: doc.schema_window,
    merge: doc.schema_merge_strategy,
    layout: doc.schema_layout,
    extends: doc.schema_extends,
    src: src,
    resolved_path: resolved_path
  }
end

#find_rue_filesArray<String>

Find all .rue files in the templates directory (recursive)

Returns:

  • (Array<String>)

    Array of absolute file paths



106
107
108
109
# File 'lib/rhales/utils/schema_extractor.rb', line 106

def find_rue_files
  pattern = File.join(@templates_dir, '**', '*.rue')
  Dir.glob(pattern).sort
end

#path_within_allowed_directories?(path) ⇒ Boolean (private)

Check if a path is within any allowed directory

Allowed directories include: - The templates directory - Any configured schema_search_paths

Parameters:

  • path (String)

    Path to check

Returns:

  • (Boolean)

    True if path is within an allowed directory



202
203
204
205
206
207
208
209
210
# File 'lib/rhales/utils/schema_extractor.rb', line 202

def path_within_allowed_directories?(path)
  return true if path_within_directory?(path, @templates_dir)

  search_paths = Rhales.configuration.schema_search_paths || []
  search_paths.any? do |search_path|
    expanded_search_path = File.expand_path(search_path)
    path_within_directory?(path, expanded_search_path)
  end
end

#path_within_directory?(path, directory) ⇒ Boolean (private)

Check if a path is within a given directory (security check)

Parameters:

  • path (String)

    Path to check

  • directory (String)

    Directory that should contain the path

Returns:

  • (Boolean)

    True if path is within directory



240
241
242
243
244
245
246
247
248
# File 'lib/rhales/utils/schema_extractor.rb', line 240

def path_within_directory?(path, directory)
  expanded_path = File.expand_path(path)
  expanded_dir = File.expand_path(directory)

  # Ensure directory ends with separator for accurate prefix matching
  expanded_dir_with_sep = expanded_dir.end_with?(File::SEPARATOR) ? expanded_dir : "#{expanded_dir}#{File::SEPARATOR}"

  expanded_path.start_with?(expanded_dir_with_sep) || expanded_path == expanded_dir
end

#read_schema_from_src(resolved_path, src, template_name) ⇒ String (private)

Read schema content from external file

Parameters:

  • resolved_path (String)

    Absolute path to the schema file

  • src (String)

    Original src attribute value (for error messages)

  • template_name (String)

    Template name (for error messages)

Returns:

  • (String)

    Schema file content

Raises:



219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
# File 'lib/rhales/utils/schema_extractor.rb', line 219

def read_schema_from_src(resolved_path, src, template_name)
  unless File.exist?(resolved_path)
    raise ExtractionError,
          "External schema file not found: '#{src}' (resolved to: #{resolved_path}) " \
          "referenced by template '#{template_name}'"
  end

  File.read(resolved_path)
rescue Errno::EACCES => e
  raise ExtractionError,
        "Permission denied reading external schema '#{src}': #{e.message}"
rescue Errno::EISDIR
  raise ExtractionError,
        "External schema path '#{src}' is a directory, not a file"
end

#resolve_schema_src_path(template_path, src) ⇒ String (private)

Resolve external schema src path

Resolution order: 1. Relative to template file directory 2. Search through configured schema_search_paths

Parameters:

  • template_path (String)

    Absolute path to the .rue template

  • src (String)

    The src attribute value from the schema tag

Returns:

  • (String)

    Absolute path to the external schema file

Raises:



160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
# File 'lib/rhales/utils/schema_extractor.rb', line 160

def resolve_schema_src_path(template_path, src)
  template_dir = File.dirname(template_path)
  resolved = File.expand_path(src, template_dir)
  searched_paths = [resolved]

  # First, check if the path exists relative to template
  if File.exist?(resolved) && path_within_allowed_directories?(resolved)
    return resolved
  end

  # If the relative path does not exist or is not allowed,
  # search through configured schema_search_paths
  search_paths = Rhales.configuration.schema_search_paths || []
  search_paths.each do |search_path|
    expanded_search_path = File.expand_path(search_path)
    candidate = File.join(expanded_search_path, src)
    searched_paths << candidate

    if File.exist?(candidate) && path_within_allowed_directories?(candidate)
      return candidate
    end
  end

  # Security check on the template-relative path
  unless path_within_allowed_directories?(resolved)
    raise ExtractionError,
          "Schema src path traversal not allowed: '#{src}' resolves outside allowed directories"
  end

  # File not found in any location - raise helpful error listing all searched paths
  raise ExtractionError,
        "Schema file not found: '#{src}'. Searched:\n  - #{searched_paths.join("\n  - ")}"
end

#schema_statsHash

Count how many .rue files have schema sections

Returns:

  • (Hash)

    Count information including external vs inline breakdown



114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
# File 'lib/rhales/utils/schema_extractor.rb', line 114

def schema_stats
  all_files = find_rue_files
  schemas = extract_all

  external_count = schemas.count { |s| s[:src] }
  inline_count = schemas.count { |s| s[:src].nil? }

  {
    total_files: all_files.count,
    files_with_schemas: schemas.count,
    files_without_schemas: all_files.count - schemas.count,
    external_schemas: external_count,
    inline_schemas: inline_count,
    schemas_by_lang: schemas.group_by { |s| s[:lang] }.transform_values(&:count)
  }
end

#validate_directory!Object (private)



133
134
135
136
137
# File 'lib/rhales/utils/schema_extractor.rb', line 133

def validate_directory!
  unless File.directory?(@templates_dir)
    raise ExtractionError, "Templates directory does not exist: #{@templates_dir}"
  end
end