rhales 0.4.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 -1
- data/Gemfile +29 -0
- data/Gemfile.lock +189 -0
- data/README.md +686 -868
- 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 +47 -0
- data/lib/rhales/core/context.rb +354 -0
- data/lib/rhales/{rue_document.rb → core/rue_document.rb} +56 -38
- data/lib/rhales/{template_engine.rb → core/template_engine.rb} +66 -59
- data/lib/rhales/{view.rb → core/view.rb} +112 -135
- data/lib/rhales/{view_composition.rb → core/view_composition.rb} +78 -8
- 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/{earliest_injection_detector.rb → hydration/earliest_injection_detector.rb} +4 -0
- data/lib/rhales/hydration/hydration_data_aggregator.rb +487 -0
- data/lib/rhales/{hydration_endpoint.rb → hydration/hydration_endpoint.rb} +16 -12
- data/lib/rhales/{hydration_injector.rb → hydration/hydration_injector.rb} +4 -0
- data/lib/rhales/{hydration_registry.rb → hydration/hydration_registry.rb} +2 -0
- data/lib/rhales/hydration/hydrator.rb +102 -0
- data/lib/rhales/{link_based_injection_detector.rb → hydration/link_based_injection_detector.rb} +4 -0
- data/lib/rhales/{mount_point_detector.rb → hydration/mount_point_detector.rb} +4 -0
- data/lib/rhales/{safe_injection_validator.rb → hydration/safe_injection_validator.rb} +4 -0
- data/lib/rhales/hydration.rb +13 -0
- data/lib/rhales/{refinements → integrations/refinements}/require_refinements.rb +3 -1
- data/lib/rhales/{tilt.rb → integrations/tilt.rb} +22 -15
- 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 +9 -7
- 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 +3 -1
- data/lib/rhales.rb +41 -24
- 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 +141 -23
- data/CLAUDE.locale.txt +0 -7
- data/lib/rhales/context.rb +0 -239
- data/lib/rhales/hydration_data_aggregator.rb +0 -221
- data/lib/rhales/hydrator.rb +0 -141
- data/lib/rhales/parsers/handlebars-grammar-review.txt +0 -39
|
@@ -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,26 +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/
|
|
18
|
-
require_relative 'rhales/
|
|
19
|
-
require_relative 'rhales/link_based_injection_detector'
|
|
20
|
-
require_relative 'rhales/hydration_injector'
|
|
21
|
-
require_relative 'rhales/hydration_endpoint'
|
|
22
|
-
require_relative 'rhales/refinements/require_refinements'
|
|
23
|
-
require_relative 'rhales/view'
|
|
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'
|
|
24
23
|
|
|
25
24
|
# Ruby Single File Components (RSFC)
|
|
26
25
|
#
|
|
@@ -34,6 +33,14 @@ require_relative 'rhales/view'
|
|
|
34
33
|
# - Pluggable authentication and session adapters
|
|
35
34
|
# - Security-first design with XSS protection and CSP support
|
|
36
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
|
+
#
|
|
37
44
|
# Usage:
|
|
38
45
|
# Rhales.configure do |config|
|
|
39
46
|
# config.default_locale = 'en'
|
|
@@ -46,12 +53,22 @@ require_relative 'rhales/view'
|
|
|
46
53
|
# config.hydration.fallback_to_late = true
|
|
47
54
|
# end
|
|
48
55
|
#
|
|
49
|
-
# view = Rhales::View.new(request
|
|
56
|
+
# view = Rhales::View.new(request)
|
|
50
57
|
# html = view.render('my_component')
|
|
51
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
|
+
|
|
52
69
|
# Convenience method to create a view with props
|
|
53
|
-
def self.render(template_name, request: nil,
|
|
54
|
-
view = View.new(request,
|
|
70
|
+
def self.render(template_name, request: nil, locale: nil, **props)
|
|
71
|
+
view = View.new(request, locale, props: props)
|
|
55
72
|
view.render(template_name)
|
|
56
73
|
end
|
|
57
74
|
|
|
@@ -62,7 +79,7 @@ module Rhales
|
|
|
62
79
|
end
|
|
63
80
|
|
|
64
81
|
# Create context with props (for advanced usage)
|
|
65
|
-
def self.create_context(request: nil,
|
|
66
|
-
Context.for_view(request,
|
|
82
|
+
def self.create_context(request: nil, locale: nil, **props)
|
|
83
|
+
Context.for_view(request, locale, **props)
|
|
67
84
|
end
|
|
68
85
|
end
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
# lib/tasks/rhales_schema.rake
|
|
2
|
+
|
|
3
|
+
namespace :rhales do
|
|
4
|
+
namespace :schema do
|
|
5
|
+
desc 'Generate JSON Schemas from .rue template files'
|
|
6
|
+
task :generate do
|
|
7
|
+
require 'rhales'
|
|
8
|
+
|
|
9
|
+
# Default to current working directory (implementing project)
|
|
10
|
+
templates_dir = ENV.fetch('TEMPLATES_DIR', './templates')
|
|
11
|
+
output_dir = ENV.fetch('OUTPUT_DIR', './public/schemas')
|
|
12
|
+
|
|
13
|
+
puts "Schema Generation"
|
|
14
|
+
puts "=" * 60
|
|
15
|
+
puts "Templates: #{templates_dir}"
|
|
16
|
+
puts "Output: #{output_dir}"
|
|
17
|
+
puts "Zod: (using pnpm exec tsx)"
|
|
18
|
+
puts
|
|
19
|
+
|
|
20
|
+
# Validate templates directory exists
|
|
21
|
+
unless Dir.exist?(templates_dir)
|
|
22
|
+
puts "⚠️ Templates directory not found: #{templates_dir}"
|
|
23
|
+
puts
|
|
24
|
+
puts "Usage:"
|
|
25
|
+
puts " rake rhales:schema:generate TEMPLATES_DIR=./templates OUTPUT_DIR=./public/schemas"
|
|
26
|
+
puts
|
|
27
|
+
puts "Or from project root where templates/ exists:"
|
|
28
|
+
puts " rake rhales:schema:generate"
|
|
29
|
+
exit 1
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Extract schemas
|
|
33
|
+
extractor = Rhales::SchemaExtractor.new(templates_dir)
|
|
34
|
+
schemas = extractor.extract_all
|
|
35
|
+
|
|
36
|
+
if schemas.empty?
|
|
37
|
+
puts "⚠️ No schema sections found in templates"
|
|
38
|
+
puts
|
|
39
|
+
puts "Make sure your .rue files have <schema> sections:"
|
|
40
|
+
puts " <schema lang=\"js-zod\" window=\"appData\">"
|
|
41
|
+
puts " const schema = z.object({ ... });"
|
|
42
|
+
puts " </schema>"
|
|
43
|
+
exit 0
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
puts "Found #{schemas.size} schema section(s):"
|
|
47
|
+
schemas.each do |schema|
|
|
48
|
+
puts " - #{schema[:template_name]} (#{schema[:lang]})"
|
|
49
|
+
end
|
|
50
|
+
puts
|
|
51
|
+
|
|
52
|
+
# Generate JSON Schemas
|
|
53
|
+
generator = Rhales::SchemaGenerator.new(
|
|
54
|
+
templates_dir: templates_dir,
|
|
55
|
+
output_dir: output_dir
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
puts "Generating JSON Schemas..."
|
|
59
|
+
results = generator.generate_all
|
|
60
|
+
|
|
61
|
+
# Report results
|
|
62
|
+
puts
|
|
63
|
+
puts "Results:"
|
|
64
|
+
puts "-" * 60
|
|
65
|
+
|
|
66
|
+
generated_count = results[:generated]
|
|
67
|
+
failed_count = results[:failed]
|
|
68
|
+
|
|
69
|
+
if results[:success]
|
|
70
|
+
puts "✓ Successfully generated #{generated_count} schema(s)"
|
|
71
|
+
puts "✓ Output directory: #{output_dir}"
|
|
72
|
+
else
|
|
73
|
+
puts "✗ Generation failed with #{failed_count} error(s)"
|
|
74
|
+
results[:errors].each do |error|
|
|
75
|
+
puts " - #{error}"
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
puts
|
|
80
|
+
|
|
81
|
+
exit(results[:success] ? 0 : 1)
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
desc 'Validate existing JSON Schemas'
|
|
85
|
+
task :validate do
|
|
86
|
+
require 'rhales'
|
|
87
|
+
require 'json'
|
|
88
|
+
require 'json_schemer'
|
|
89
|
+
|
|
90
|
+
# Default to current working directory
|
|
91
|
+
schemas_dir = ENV.fetch('OUTPUT_DIR', './public/schemas')
|
|
92
|
+
|
|
93
|
+
unless Dir.exist?(schemas_dir)
|
|
94
|
+
puts "⚠️ Schemas directory not found: #{schemas_dir}"
|
|
95
|
+
puts
|
|
96
|
+
puts "Generate schemas first:"
|
|
97
|
+
puts " rake rhales:schema:generate"
|
|
98
|
+
exit 1
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
schema_files = Dir.glob("#{schemas_dir}/**/*.json")
|
|
102
|
+
|
|
103
|
+
if schema_files.empty?
|
|
104
|
+
puts "⚠️ No schema files found in #{schemas_dir}"
|
|
105
|
+
puts
|
|
106
|
+
puts "Generate schemas first:"
|
|
107
|
+
puts " rake rhales:schema:generate"
|
|
108
|
+
exit 1
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
puts "Validating #{schema_files.size} schema file(s) in #{schemas_dir}..."
|
|
112
|
+
puts
|
|
113
|
+
|
|
114
|
+
errors = []
|
|
115
|
+
|
|
116
|
+
schema_files.each do |file|
|
|
117
|
+
relative_path = file.sub("#{schemas_dir}/", '')
|
|
118
|
+
|
|
119
|
+
begin
|
|
120
|
+
schema = JSON.parse(File.read(file))
|
|
121
|
+
|
|
122
|
+
# Validate against JSON Schema meta-schema
|
|
123
|
+
unless schema.is_a?(Hash)
|
|
124
|
+
errors << "#{relative_path}: Not a valid object"
|
|
125
|
+
next
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
# Use JSONSchemer to validate against the meta-schema
|
|
129
|
+
meta_schema = JSONSchemer.schema(
|
|
130
|
+
{
|
|
131
|
+
'$schema' => 'https://json-schema.org/draft/2020-12/schema'
|
|
132
|
+
}
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
validation_errors = meta_schema.validate(schema).to_a
|
|
136
|
+
|
|
137
|
+
if validation_errors.any?
|
|
138
|
+
validation_errors.each do |error|
|
|
139
|
+
errors << "#{relative_path}: #{error['type']} at #{error['data_pointer']}"
|
|
140
|
+
end
|
|
141
|
+
else
|
|
142
|
+
puts "✓ #{relative_path}"
|
|
143
|
+
end
|
|
144
|
+
rescue JSON::ParserError => e
|
|
145
|
+
errors << "#{relative_path}: Invalid JSON - #{e.message}"
|
|
146
|
+
rescue => e
|
|
147
|
+
errors << "#{relative_path}: #{e.message}"
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
puts
|
|
152
|
+
|
|
153
|
+
if errors.any?
|
|
154
|
+
puts "Errors:"
|
|
155
|
+
errors.each { |err| puts " ✗ #{err}" }
|
|
156
|
+
exit 1
|
|
157
|
+
else
|
|
158
|
+
puts "All schemas valid ✓"
|
|
159
|
+
end
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
desc 'Show statistics about schema sections'
|
|
163
|
+
task :stats do
|
|
164
|
+
require 'rhales'
|
|
165
|
+
|
|
166
|
+
# Default to current working directory
|
|
167
|
+
templates_dir = ENV.fetch('TEMPLATES_DIR', './templates')
|
|
168
|
+
|
|
169
|
+
unless Dir.exist?(templates_dir)
|
|
170
|
+
puts "⚠️ Templates directory not found: #{templates_dir}"
|
|
171
|
+
puts
|
|
172
|
+
puts "Usage:"
|
|
173
|
+
puts " rake rhales:schema:stats TEMPLATES_DIR=./templates"
|
|
174
|
+
exit 1
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
extractor = Rhales::SchemaExtractor.new(templates_dir)
|
|
178
|
+
stats = extractor.schema_stats
|
|
179
|
+
|
|
180
|
+
puts "Schema Statistics"
|
|
181
|
+
puts "=" * 60
|
|
182
|
+
puts "Templates directory: #{templates_dir}"
|
|
183
|
+
puts
|
|
184
|
+
puts "Total .rue files: #{stats[:total_files]}"
|
|
185
|
+
puts "Files with <schema>: #{stats[:files_with_schemas]}"
|
|
186
|
+
puts "Files without <schema>: #{stats[:files_without_schemas]}"
|
|
187
|
+
puts
|
|
188
|
+
|
|
189
|
+
if stats[:schemas_by_lang].any?
|
|
190
|
+
puts "By language:"
|
|
191
|
+
stats[:schemas_by_lang].each do |lang, count|
|
|
192
|
+
puts " #{lang}: #{count}"
|
|
193
|
+
end
|
|
194
|
+
end
|
|
195
|
+
end
|
|
196
|
+
end
|
|
197
|
+
end
|