rhales 0.3.0 → 0.5.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.github/renovate.json5 +52 -0
- data/.github/workflows/ci.yml +123 -0
- data/.github/workflows/claude-code-review.yml +69 -0
- data/.github/workflows/claude.yml +49 -0
- data/.github/workflows/code-smells.yml +146 -0
- data/.github/workflows/ruby-lint.yml +78 -0
- data/.github/workflows/yardoc.yml +126 -0
- data/.gitignore +55 -0
- data/.pr_agent.toml +63 -0
- data/.pre-commit-config.yaml +89 -0
- data/.prettierignore +8 -0
- data/.prettierrc +38 -0
- data/.reek.yml +98 -0
- data/.rubocop.yml +428 -0
- data/.serena/.gitignore +3 -0
- data/.yardopts +56 -0
- data/CHANGELOG.md +44 -0
- data/CLAUDE.md +1 -2
- data/Gemfile +29 -0
- data/Gemfile.lock +189 -0
- data/README.md +706 -589
- data/Rakefile +46 -0
- data/debug_context.rb +25 -0
- data/demo/rhales-roda-demo/.gitignore +7 -0
- data/demo/rhales-roda-demo/Gemfile +32 -0
- data/demo/rhales-roda-demo/Gemfile.lock +151 -0
- data/demo/rhales-roda-demo/MAIL.md +405 -0
- data/demo/rhales-roda-demo/README.md +376 -0
- data/demo/rhales-roda-demo/RODA-TEMPLATE-ENGINES.md +192 -0
- data/demo/rhales-roda-demo/Rakefile +49 -0
- data/demo/rhales-roda-demo/app.rb +325 -0
- data/demo/rhales-roda-demo/bin/rackup +26 -0
- data/demo/rhales-roda-demo/config.ru +13 -0
- data/demo/rhales-roda-demo/db/migrate/001_create_rodauth_tables.rb +266 -0
- data/demo/rhales-roda-demo/db/migrate/002_create_rodauth_password_tables.rb +79 -0
- data/demo/rhales-roda-demo/db/migrate/003_add_admin_account.rb +68 -0
- data/demo/rhales-roda-demo/templates/change_login.rue +31 -0
- data/demo/rhales-roda-demo/templates/change_password.rue +36 -0
- data/demo/rhales-roda-demo/templates/close_account.rue +31 -0
- data/demo/rhales-roda-demo/templates/create_account.rue +40 -0
- data/demo/rhales-roda-demo/templates/dashboard.rue +150 -0
- data/demo/rhales-roda-demo/templates/home.rue +78 -0
- data/demo/rhales-roda-demo/templates/layouts/main.rue +168 -0
- data/demo/rhales-roda-demo/templates/login.rue +65 -0
- data/demo/rhales-roda-demo/templates/logout.rue +25 -0
- data/demo/rhales-roda-demo/templates/reset_password.rue +26 -0
- data/demo/rhales-roda-demo/templates/verify_account.rue +27 -0
- data/demo/rhales-roda-demo/test_full_output.rb +27 -0
- data/demo/rhales-roda-demo/test_simple.rb +24 -0
- data/docs/.gitignore +9 -0
- data/docs/architecture/data-flow.md +499 -0
- data/examples/dashboard-with-charts.rue +271 -0
- data/examples/form-with-validation.rue +180 -0
- data/examples/simple-page.rue +61 -0
- data/examples/vue.rue +136 -0
- data/generate-json-schemas.ts +158 -0
- data/json_schemer_migration_summary.md +172 -0
- data/lib/rhales/adapters/base_auth.rb +2 -0
- data/lib/rhales/adapters/base_request.rb +2 -0
- data/lib/rhales/adapters/base_session.rb +2 -0
- data/lib/rhales/adapters.rb +7 -0
- data/lib/rhales/configuration.rb +161 -1
- data/lib/rhales/core/context.rb +354 -0
- data/lib/rhales/{rue_document.rb → core/rue_document.rb} +59 -43
- data/lib/rhales/{template_engine.rb → core/template_engine.rb} +80 -33
- data/lib/rhales/core/view.rb +529 -0
- data/lib/rhales/{view_composition.rb → core/view_composition.rb} +81 -9
- data/lib/rhales/core.rb +9 -0
- data/lib/rhales/errors/hydration_collision_error.rb +2 -0
- data/lib/rhales/errors.rb +2 -0
- data/lib/rhales/hydration/earliest_injection_detector.rb +153 -0
- data/lib/rhales/hydration/hydration_data_aggregator.rb +487 -0
- data/lib/rhales/hydration/hydration_endpoint.rb +215 -0
- data/lib/rhales/hydration/hydration_injector.rb +175 -0
- data/lib/rhales/{hydration_registry.rb → hydration/hydration_registry.rb} +2 -0
- data/lib/rhales/hydration/hydrator.rb +102 -0
- data/lib/rhales/hydration/link_based_injection_detector.rb +195 -0
- data/lib/rhales/hydration/mount_point_detector.rb +109 -0
- data/lib/rhales/hydration/safe_injection_validator.rb +103 -0
- data/lib/rhales/hydration.rb +13 -0
- data/lib/rhales/{refinements → integrations/refinements}/require_refinements.rb +7 -13
- data/lib/rhales/{tilt.rb → integrations/tilt.rb} +26 -18
- data/lib/rhales/integrations.rb +6 -0
- data/lib/rhales/middleware/json_responder.rb +191 -0
- data/lib/rhales/middleware/schema_validator.rb +300 -0
- data/lib/rhales/middleware.rb +6 -0
- data/lib/rhales/parsers/handlebars_parser.rb +2 -0
- data/lib/rhales/parsers/rue_format_parser.rb +55 -36
- data/lib/rhales/parsers.rb +9 -0
- data/lib/rhales/{csp.rb → security/csp.rb} +27 -3
- data/lib/rhales/utils/json_serializer.rb +114 -0
- data/lib/rhales/utils/logging_helpers.rb +75 -0
- data/lib/rhales/utils/schema_extractor.rb +132 -0
- data/lib/rhales/utils/schema_generator.rb +194 -0
- data/lib/rhales/utils.rb +40 -0
- data/lib/rhales/version.rb +5 -1
- data/lib/rhales.rb +47 -19
- data/lib/tasks/rhales_schema.rake +197 -0
- data/package.json +10 -0
- data/pnpm-lock.yaml +345 -0
- data/pnpm-workspace.yaml +2 -0
- data/proofs/error_handling.rb +79 -0
- data/proofs/expanded_object_inheritance.rb +82 -0
- data/proofs/partial_context_scoping_fix.rb +168 -0
- data/proofs/ui_context_partial_inheritance.rb +236 -0
- data/rhales.gemspec +14 -6
- data/schema_vs_data_comparison.md +254 -0
- data/test_direct_access.rb +36 -0
- metadata +142 -18
- data/CLAUDE.locale.txt +0 -7
- data/lib/rhales/context.rb +0 -240
- data/lib/rhales/hydration_data_aggregator.rb +0 -220
- data/lib/rhales/hydrator.rb +0 -141
- data/lib/rhales/parsers/handlebars-grammar-review.txt +0 -39
- data/lib/rhales/view.rb +0 -412
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# lib/rhales/utils/logging_helpers.rb
|
|
2
|
+
#
|
|
3
|
+
# frozen_string_literal: true
|
|
4
|
+
|
|
5
|
+
module Rhales
|
|
6
|
+
module Utils
|
|
7
|
+
# Helper methods for consistent logging and timing instrumentation across Rhales components
|
|
8
|
+
module LoggingHelpers
|
|
9
|
+
include Rhales::Utils
|
|
10
|
+
|
|
11
|
+
# Log with timing for an operation
|
|
12
|
+
#
|
|
13
|
+
# @param logger [Logger] The logger instance to use
|
|
14
|
+
# @param level [Symbol] The log level (:debug, :info, :warn, :error)
|
|
15
|
+
# @param message [String] The log message
|
|
16
|
+
# @param metadata [Hash] Additional metadata to include in the log
|
|
17
|
+
# @yield The block to execute and time
|
|
18
|
+
# @return The result of the block
|
|
19
|
+
#
|
|
20
|
+
# Logs the operation with timing information in microseconds.
|
|
21
|
+
def log_timed_operation(logger, level, message, **metadata)
|
|
22
|
+
start_time = now_in_μs
|
|
23
|
+
result = yield
|
|
24
|
+
duration = now_in_μs - start_time
|
|
25
|
+
|
|
26
|
+
log_with_metadata(logger, level, message, metadata.merge(duration: duration))
|
|
27
|
+
|
|
28
|
+
result
|
|
29
|
+
rescue StandardError => ex
|
|
30
|
+
duration = now_in_μs - start_time
|
|
31
|
+
log_with_metadata(logger, :error, "#{message} failed",
|
|
32
|
+
metadata.merge(
|
|
33
|
+
duration: duration,
|
|
34
|
+
error: ex.message,
|
|
35
|
+
error_class: ex.class.name,
|
|
36
|
+
)
|
|
37
|
+
)
|
|
38
|
+
raise
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Log a message with structured metadata
|
|
42
|
+
def log_with_metadata(logger, level, message, metadata = {})
|
|
43
|
+
return logger.public_send(level, message) if metadata.empty?
|
|
44
|
+
|
|
45
|
+
metadata_str = metadata.map { |k, v| "#{k}=#{format_value(v)}" }.join(' ')
|
|
46
|
+
logger.public_send(level, "#{message}: #{metadata_str}")
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
private
|
|
50
|
+
|
|
51
|
+
# Format individual log values
|
|
52
|
+
def format_value(value)
|
|
53
|
+
case value
|
|
54
|
+
when String
|
|
55
|
+
value.include?(' ') ? "\"#{value}\"" : value
|
|
56
|
+
when Symbol, Numeric, true, false, nil
|
|
57
|
+
value.to_s
|
|
58
|
+
when Array
|
|
59
|
+
# For arrays longer than 5 items, show count + first/last items
|
|
60
|
+
if value.size > 5
|
|
61
|
+
first_three = value.first(3).join(', ')
|
|
62
|
+
last_two = value.last(2).join(', ')
|
|
63
|
+
"[#{value.size} items: #{first_three} ... #{last_two}]"
|
|
64
|
+
elsif value.empty?
|
|
65
|
+
'[]'
|
|
66
|
+
else
|
|
67
|
+
"[#{value.join(', ')}]"
|
|
68
|
+
end
|
|
69
|
+
else
|
|
70
|
+
value.to_s
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
# lib/rhales/schema_extractor.rb
|
|
2
|
+
#
|
|
3
|
+
# frozen_string_literal: true
|
|
4
|
+
|
|
5
|
+
require 'pathname'
|
|
6
|
+
require_relative '../core/rue_document'
|
|
7
|
+
|
|
8
|
+
module Rhales
|
|
9
|
+
# Extracts schema definitions from .rue files
|
|
10
|
+
#
|
|
11
|
+
# This class scans template directories for .rue files containing <schema>
|
|
12
|
+
# sections and extracts the schema code along with metadata (attributes).
|
|
13
|
+
#
|
|
14
|
+
# Usage:
|
|
15
|
+
# extractor = SchemaExtractor.new('./templates')
|
|
16
|
+
# schemas = extractor.extract_all
|
|
17
|
+
# schemas.each do |schema_info|
|
|
18
|
+
# puts "#{schema_info[:template_name]}: #{schema_info[:lang]}"
|
|
19
|
+
# end
|
|
20
|
+
class SchemaExtractor
|
|
21
|
+
class ExtractionError < StandardError; end
|
|
22
|
+
|
|
23
|
+
attr_reader :templates_dir
|
|
24
|
+
|
|
25
|
+
def initialize(templates_dir)
|
|
26
|
+
@templates_dir = File.expand_path(templates_dir)
|
|
27
|
+
validate_directory!
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Extract all schemas from .rue files in the templates directory
|
|
31
|
+
#
|
|
32
|
+
# @return [Array<Hash>] Array of schema information hashes
|
|
33
|
+
# @example
|
|
34
|
+
# [
|
|
35
|
+
# {
|
|
36
|
+
# template_name: 'dashboard',
|
|
37
|
+
# template_path: '/path/to/dashboard.rue',
|
|
38
|
+
# schema_code: 'const schema = z.object({...});',
|
|
39
|
+
# lang: 'ts-zod',
|
|
40
|
+
# version: '2',
|
|
41
|
+
# envelope: 'SuccessEnvelope',
|
|
42
|
+
# window: 'appData',
|
|
43
|
+
# merge: 'deep',
|
|
44
|
+
# layout: 'layouts/main',
|
|
45
|
+
# extends: nil
|
|
46
|
+
# }
|
|
47
|
+
# ]
|
|
48
|
+
def extract_all
|
|
49
|
+
rue_files = find_rue_files
|
|
50
|
+
schemas = []
|
|
51
|
+
|
|
52
|
+
rue_files.each do |file_path|
|
|
53
|
+
begin
|
|
54
|
+
schema_info = extract_from_file(file_path)
|
|
55
|
+
schemas << schema_info if schema_info
|
|
56
|
+
rescue => e
|
|
57
|
+
warn "Warning: Failed to extract schema from #{file_path}: #{e.message}"
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
schemas
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# Extract schema from a single .rue file
|
|
65
|
+
#
|
|
66
|
+
# @param file_path [String] Path to the .rue file
|
|
67
|
+
# @return [Hash, nil] Schema information hash or nil if no schema section
|
|
68
|
+
def extract_from_file(file_path)
|
|
69
|
+
doc = RueDocument.parse_file(file_path)
|
|
70
|
+
|
|
71
|
+
return nil unless doc.section?('schema')
|
|
72
|
+
|
|
73
|
+
template_name = derive_template_name(file_path)
|
|
74
|
+
schema_code = doc.section('schema')
|
|
75
|
+
|
|
76
|
+
{
|
|
77
|
+
template_name: template_name,
|
|
78
|
+
template_path: file_path,
|
|
79
|
+
schema_code: schema_code.strip,
|
|
80
|
+
lang: doc.schema_lang,
|
|
81
|
+
version: doc.schema_version,
|
|
82
|
+
envelope: doc.schema_envelope,
|
|
83
|
+
window: doc.schema_window,
|
|
84
|
+
merge: doc.schema_merge_strategy,
|
|
85
|
+
layout: doc.schema_layout,
|
|
86
|
+
extends: doc.schema_extends
|
|
87
|
+
}
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# Find all .rue files in the templates directory (recursive)
|
|
91
|
+
#
|
|
92
|
+
# @return [Array<String>] Array of absolute file paths
|
|
93
|
+
def find_rue_files
|
|
94
|
+
pattern = File.join(@templates_dir, '**', '*.rue')
|
|
95
|
+
Dir.glob(pattern).sort
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# Count how many .rue files have schema sections
|
|
99
|
+
#
|
|
100
|
+
# @return [Hash] Count information
|
|
101
|
+
def schema_stats
|
|
102
|
+
all_files = find_rue_files
|
|
103
|
+
schemas = extract_all
|
|
104
|
+
|
|
105
|
+
{
|
|
106
|
+
total_files: all_files.count,
|
|
107
|
+
files_with_schemas: schemas.count,
|
|
108
|
+
files_without_schemas: all_files.count - schemas.count,
|
|
109
|
+
schemas_by_lang: schemas.group_by { |s| s[:lang] }.transform_values(&:count)
|
|
110
|
+
}
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
private
|
|
114
|
+
|
|
115
|
+
def validate_directory!
|
|
116
|
+
unless File.directory?(@templates_dir)
|
|
117
|
+
raise ExtractionError, "Templates directory does not exist: #{@templates_dir}"
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
# Derive template name from file path
|
|
122
|
+
# Examples:
|
|
123
|
+
# /path/to/templates/dashboard.rue => 'dashboard'
|
|
124
|
+
# /path/to/templates/pages/user/profile.rue => 'pages/user/profile'
|
|
125
|
+
def derive_template_name(file_path)
|
|
126
|
+
templates_pathname = Pathname.new(@templates_dir)
|
|
127
|
+
file_pathname = Pathname.new(file_path)
|
|
128
|
+
relative_path = file_pathname.relative_path_from(templates_pathname)
|
|
129
|
+
relative_path.to_s.sub(/\.rue$/, '')
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
end
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
# lib/rhales/schema_generator.rb
|
|
2
|
+
#
|
|
3
|
+
# frozen_string_literal: true
|
|
4
|
+
|
|
5
|
+
require 'open3'
|
|
6
|
+
require 'tempfile'
|
|
7
|
+
require 'fileutils'
|
|
8
|
+
require_relative 'schema_extractor'
|
|
9
|
+
require_relative 'json_serializer'
|
|
10
|
+
|
|
11
|
+
module Rhales
|
|
12
|
+
# Generates JSON Schemas from Zod schemas using TypeScript execution
|
|
13
|
+
#
|
|
14
|
+
# This class uses pnpm exec tsx to execute Zod schema code and convert it
|
|
15
|
+
# to JSON Schema format. The generated schemas are saved to disk for use
|
|
16
|
+
# by the validation middleware.
|
|
17
|
+
#
|
|
18
|
+
# Usage:
|
|
19
|
+
# generator = SchemaGenerator.new(
|
|
20
|
+
# templates_dir: './templates',
|
|
21
|
+
# output_dir: './public/schemas'
|
|
22
|
+
# )
|
|
23
|
+
# results = generator.generate_all
|
|
24
|
+
class SchemaGenerator
|
|
25
|
+
class GenerationError < StandardError; end
|
|
26
|
+
|
|
27
|
+
attr_reader :templates_dir, :output_dir
|
|
28
|
+
|
|
29
|
+
# @param templates_dir [String] Directory containing .rue files
|
|
30
|
+
# @param output_dir [String] Directory to save generated JSON schemas
|
|
31
|
+
# Defaults to './public/schemas' (implementing project's public directory)
|
|
32
|
+
def initialize(templates_dir:, output_dir: nil)
|
|
33
|
+
@templates_dir = File.expand_path(templates_dir)
|
|
34
|
+
|
|
35
|
+
# Smart default: place schemas in public/schemas relative to current working directory
|
|
36
|
+
# This ensures schemas are generated in the implementing project, not the gem directory
|
|
37
|
+
@output_dir = if output_dir
|
|
38
|
+
File.expand_path(output_dir)
|
|
39
|
+
else
|
|
40
|
+
# Default to public/schemas in current working directory
|
|
41
|
+
File.expand_path('./public/schemas')
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
validate_setup!
|
|
45
|
+
ensure_output_directory!
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Generate JSON Schemas for all templates with <schema> sections
|
|
49
|
+
#
|
|
50
|
+
# @return [Hash] Generation results with stats
|
|
51
|
+
def generate_all
|
|
52
|
+
extractor = SchemaExtractor.new(@templates_dir)
|
|
53
|
+
schemas = extractor.extract_all
|
|
54
|
+
|
|
55
|
+
if schemas.empty?
|
|
56
|
+
return {
|
|
57
|
+
success: true,
|
|
58
|
+
generated: 0,
|
|
59
|
+
failed: 0,
|
|
60
|
+
message: 'No schemas found in templates'
|
|
61
|
+
}
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
results = {
|
|
65
|
+
success: true,
|
|
66
|
+
generated: 0,
|
|
67
|
+
failed: 0,
|
|
68
|
+
errors: []
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
schemas.each do |schema_info|
|
|
72
|
+
begin
|
|
73
|
+
generate_schema(schema_info)
|
|
74
|
+
results[:generated] += 1
|
|
75
|
+
puts "✓ Generated schema for: #{schema_info[:template_name]}"
|
|
76
|
+
rescue => e
|
|
77
|
+
results[:failed] += 1
|
|
78
|
+
results[:success] = false
|
|
79
|
+
error_msg = "Failed to generate schema for #{schema_info[:template_name]}: #{e.message}"
|
|
80
|
+
results[:errors] << error_msg
|
|
81
|
+
warn error_msg
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
results
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# Generate JSON Schema for a single template
|
|
89
|
+
#
|
|
90
|
+
# @param schema_info [Hash] Schema information from SchemaExtractor
|
|
91
|
+
# @return [Hash] Generated JSON Schema
|
|
92
|
+
def generate_schema(schema_info)
|
|
93
|
+
# Create temp file in project directory so Node.js can resolve modules
|
|
94
|
+
temp_dir = File.join(Dir.pwd, 'tmp')
|
|
95
|
+
FileUtils.mkdir_p(temp_dir) unless Dir.exist?(temp_dir)
|
|
96
|
+
|
|
97
|
+
temp_file = Tempfile.new(['schema', '.mts'], temp_dir)
|
|
98
|
+
|
|
99
|
+
begin
|
|
100
|
+
# Write TypeScript script
|
|
101
|
+
temp_file.write(build_typescript_script(schema_info))
|
|
102
|
+
temp_file.close
|
|
103
|
+
|
|
104
|
+
# Execute with tsx via pnpm
|
|
105
|
+
stdout, stderr, status = Open3.capture3('pnpm', 'exec', 'tsx', temp_file.path)
|
|
106
|
+
|
|
107
|
+
unless status.success?
|
|
108
|
+
raise GenerationError, "TypeScript execution failed: #{stderr}"
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
# Parse JSON Schema from stdout
|
|
112
|
+
json_schema = JSONSerializer.parse(stdout)
|
|
113
|
+
|
|
114
|
+
# Save to disk
|
|
115
|
+
save_schema(schema_info[:template_name], json_schema)
|
|
116
|
+
|
|
117
|
+
json_schema
|
|
118
|
+
ensure
|
|
119
|
+
temp_file.unlink if temp_file
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
private
|
|
124
|
+
|
|
125
|
+
def build_typescript_script(schema_info)
|
|
126
|
+
# Escape single quotes in template name for TypeScript string
|
|
127
|
+
safe_name = schema_info[:template_name].gsub("'", "\\'")
|
|
128
|
+
|
|
129
|
+
<<~TYPESCRIPT
|
|
130
|
+
// Auto-generated schema generator for #{safe_name}
|
|
131
|
+
import { z } from 'zod/v4';
|
|
132
|
+
|
|
133
|
+
// Schema code from .rue template
|
|
134
|
+
#{schema_info[:schema_code].strip}
|
|
135
|
+
|
|
136
|
+
// Generate JSON Schema
|
|
137
|
+
try {
|
|
138
|
+
const jsonSchema = z.toJSONSchema(schema, {
|
|
139
|
+
target: 'draft-2020-12',
|
|
140
|
+
unrepresentable: 'any',
|
|
141
|
+
cycles: 'ref',
|
|
142
|
+
reused: 'inline',
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
// Add metadata
|
|
146
|
+
const schemaWithMeta = {
|
|
147
|
+
$schema: 'https://json-schema.org/draft/2020-12/schema',
|
|
148
|
+
$id: `https://rhales.dev/schemas/#{safe_name}.json`,
|
|
149
|
+
title: '#{safe_name}',
|
|
150
|
+
description: 'Schema for #{safe_name} template',
|
|
151
|
+
...jsonSchema,
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
// Output JSON to stdout
|
|
155
|
+
console.log(JSON.stringify(schemaWithMeta, null, 2));
|
|
156
|
+
} catch (error) {
|
|
157
|
+
console.error('Schema generation error:', error.message);
|
|
158
|
+
process.exit(1);
|
|
159
|
+
}
|
|
160
|
+
TYPESCRIPT
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
def save_schema(template_name, json_schema)
|
|
164
|
+
# Create subdirectories if template name contains paths
|
|
165
|
+
schema_file = File.join(@output_dir, "#{template_name}.json")
|
|
166
|
+
schema_dir = File.dirname(schema_file)
|
|
167
|
+
FileUtils.mkdir_p(schema_dir) unless File.directory?(schema_dir)
|
|
168
|
+
|
|
169
|
+
File.write(schema_file, JSONSerializer.dump(json_schema))
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
def validate_setup!
|
|
173
|
+
unless File.directory?(@templates_dir)
|
|
174
|
+
raise GenerationError, "Templates directory does not exist: #{@templates_dir}"
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
# Check pnpm is available
|
|
178
|
+
stdout, stderr, status = Open3.capture3('pnpm', '--version')
|
|
179
|
+
unless status.success?
|
|
180
|
+
raise GenerationError, "pnpm not found. Install pnpm to generate schemas: npm install -g pnpm"
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
# Check tsx is available (will be installed by pnpm if needed)
|
|
184
|
+
stdout, stderr, status = Open3.capture3('pnpm', 'exec', 'tsx', '--version')
|
|
185
|
+
unless status.success?
|
|
186
|
+
raise GenerationError, "tsx not found. Run: pnpm install tsx --save-dev"
|
|
187
|
+
end
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
def ensure_output_directory!
|
|
191
|
+
FileUtils.mkdir_p(@output_dir) unless File.directory?(@output_dir)
|
|
192
|
+
end
|
|
193
|
+
end
|
|
194
|
+
end
|
data/lib/rhales/utils.rb
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# lib/rhales/utils.rb
|
|
2
|
+
#
|
|
3
|
+
# frozen_string_literal: true
|
|
4
|
+
|
|
5
|
+
require 'pathname'
|
|
6
|
+
|
|
7
|
+
module Rhales
|
|
8
|
+
module Utils
|
|
9
|
+
# Utility modules and classes
|
|
10
|
+
|
|
11
|
+
# @return [Time] Current time in UTC
|
|
12
|
+
def now
|
|
13
|
+
Time.now.utc
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# Returns the current time in microseconds.
|
|
17
|
+
# This is used to measure the duration of Database commands.
|
|
18
|
+
#
|
|
19
|
+
# Alias: now_in_microseconds
|
|
20
|
+
#
|
|
21
|
+
# @return [Integer] The current time in microseconds.
|
|
22
|
+
def now_in_μs
|
|
23
|
+
Process.clock_gettime(Process::CLOCK_MONOTONIC, :microsecond)
|
|
24
|
+
end
|
|
25
|
+
alias now_in_microseconds now_in_μs
|
|
26
|
+
|
|
27
|
+
# @param filepath [String, nil] The file path to prettify
|
|
28
|
+
# @return [String, nil] The expanded absolute path, or nil if input is
|
|
29
|
+
def pretty_path(filepath)
|
|
30
|
+
return nil if filepath.nil?
|
|
31
|
+
|
|
32
|
+
Pathname.new(filepath).expand_path.to_s
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
require_relative 'utils/json_serializer'
|
|
38
|
+
require_relative 'utils/schema_generator'
|
|
39
|
+
require_relative 'utils/schema_extractor'
|
|
40
|
+
require_relative 'utils/logging_helpers'
|
data/lib/rhales/version.rb
CHANGED
data/lib/rhales.rb
CHANGED
|
@@ -1,20 +1,25 @@
|
|
|
1
1
|
# lib/rhales.rb
|
|
2
|
+
#
|
|
3
|
+
# frozen_string_literal: true
|
|
4
|
+
|
|
5
|
+
require 'logger'
|
|
2
6
|
|
|
7
|
+
# Core framework files
|
|
3
8
|
require_relative 'rhales/version'
|
|
4
|
-
require_relative 'rhales/errors'
|
|
5
9
|
require_relative 'rhales/configuration'
|
|
6
|
-
require_relative 'rhales/
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
require_relative 'rhales/
|
|
10
|
-
require_relative 'rhales/parsers
|
|
11
|
-
require_relative 'rhales/
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
require_relative 'rhales/
|
|
15
|
-
require_relative 'rhales/
|
|
16
|
-
require_relative 'rhales/
|
|
17
|
-
require_relative 'rhales/
|
|
10
|
+
require_relative 'rhales/errors'
|
|
11
|
+
|
|
12
|
+
# Load components in dependency order
|
|
13
|
+
require_relative 'rhales/adapters'
|
|
14
|
+
require_relative 'rhales/parsers'
|
|
15
|
+
require_relative 'rhales/utils'
|
|
16
|
+
|
|
17
|
+
# Security (depends on utils)
|
|
18
|
+
require_relative 'rhales/security/csp'
|
|
19
|
+
require_relative 'rhales/core'
|
|
20
|
+
require_relative 'rhales/hydration'
|
|
21
|
+
require_relative 'rhales/integrations'
|
|
22
|
+
require_relative 'rhales/middleware'
|
|
18
23
|
|
|
19
24
|
# Ruby Single File Components (RSFC)
|
|
20
25
|
#
|
|
@@ -28,19 +33,42 @@ require_relative 'rhales/view'
|
|
|
28
33
|
# - Pluggable authentication and session adapters
|
|
29
34
|
# - Security-first design with XSS protection and CSP support
|
|
30
35
|
#
|
|
36
|
+
# Modular Loading:
|
|
37
|
+
# require 'rhales' # Loads everything (default)
|
|
38
|
+
# require 'rhales/core' # Core engine only
|
|
39
|
+
# require 'rhales/hydration' # Hydration system only
|
|
40
|
+
# require 'rhales/parsers' # Template parsers only
|
|
41
|
+
# require 'rhales/utils' # Utilities only
|
|
42
|
+
# require 'rhales/all' # Explicit full load
|
|
43
|
+
#
|
|
31
44
|
# Usage:
|
|
32
45
|
# Rhales.configure do |config|
|
|
33
|
-
# config.
|
|
46
|
+
# config.default_locale = 'en'
|
|
34
47
|
# config.template_paths = ['app/templates']
|
|
35
48
|
# config.features = { dark_mode: true }
|
|
49
|
+
#
|
|
50
|
+
# # Hydration configuration
|
|
51
|
+
# config.hydration.injection_strategy = :early # :early or :late (default)
|
|
52
|
+
# config.hydration.mount_point_selectors = ['#app', '#root', '[data-mount]']
|
|
53
|
+
# config.hydration.fallback_to_late = true
|
|
36
54
|
# end
|
|
37
55
|
#
|
|
38
|
-
# view = Rhales::View.new(request
|
|
56
|
+
# view = Rhales::View.new(request)
|
|
39
57
|
# html = view.render('my_component')
|
|
40
58
|
module Rhales
|
|
59
|
+
extend Utils
|
|
60
|
+
|
|
61
|
+
class << self
|
|
62
|
+
attr_writer :logger
|
|
63
|
+
|
|
64
|
+
def logger
|
|
65
|
+
@logger ||= Logger.new($stdout)
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
41
69
|
# Convenience method to create a view with props
|
|
42
|
-
def self.render(template_name, request: nil,
|
|
43
|
-
view = View.new(request,
|
|
70
|
+
def self.render(template_name, request: nil, locale: nil, **props)
|
|
71
|
+
view = View.new(request, locale, props: props)
|
|
44
72
|
view.render(template_name)
|
|
45
73
|
end
|
|
46
74
|
|
|
@@ -51,7 +79,7 @@ module Rhales
|
|
|
51
79
|
end
|
|
52
80
|
|
|
53
81
|
# Create context with props (for advanced usage)
|
|
54
|
-
def self.create_context(request: nil,
|
|
55
|
-
Context.for_view(request,
|
|
82
|
+
def self.create_context(request: nil, locale: nil, **props)
|
|
83
|
+
Context.for_view(request, locale, **props)
|
|
56
84
|
end
|
|
57
85
|
end
|