rhales 0.3.0
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 +7 -0
- data/CLAUDE.locale.txt +7 -0
- data/CLAUDE.md +90 -0
- data/LICENSE.txt +21 -0
- data/README.md +881 -0
- data/lib/rhales/adapters/base_auth.rb +106 -0
- data/lib/rhales/adapters/base_request.rb +97 -0
- data/lib/rhales/adapters/base_session.rb +93 -0
- data/lib/rhales/configuration.rb +156 -0
- data/lib/rhales/context.rb +240 -0
- data/lib/rhales/csp.rb +94 -0
- data/lib/rhales/errors/hydration_collision_error.rb +85 -0
- data/lib/rhales/errors.rb +36 -0
- data/lib/rhales/hydration_data_aggregator.rb +220 -0
- data/lib/rhales/hydration_registry.rb +58 -0
- data/lib/rhales/hydrator.rb +141 -0
- data/lib/rhales/parsers/handlebars-grammar-review.txt +39 -0
- data/lib/rhales/parsers/handlebars_parser.rb +727 -0
- data/lib/rhales/parsers/rue_format_parser.rb +385 -0
- data/lib/rhales/refinements/require_refinements.rb +236 -0
- data/lib/rhales/rue_document.rb +304 -0
- data/lib/rhales/template_engine.rb +353 -0
- data/lib/rhales/tilt.rb +214 -0
- data/lib/rhales/version.rb +6 -0
- data/lib/rhales/view.rb +412 -0
- data/lib/rhales/view_composition.rb +165 -0
- data/lib/rhales.rb +57 -0
- data/rhales.gemspec +46 -0
- metadata +78 -0
@@ -0,0 +1,165 @@
|
|
1
|
+
# lib/rhales/view_composition.rb
|
2
|
+
|
3
|
+
require_relative 'rue_document'
|
4
|
+
require_relative 'refinements/require_refinements'
|
5
|
+
|
6
|
+
using Rhales::Ruequire
|
7
|
+
|
8
|
+
module Rhales
|
9
|
+
# ViewComposition builds and represents the complete template dependency graph
|
10
|
+
# for a view render. It provides a data-agnostic, immutable representation
|
11
|
+
# of all templates (layout, view, partials) required for rendering.
|
12
|
+
#
|
13
|
+
# This class is a key component in the two-pass rendering architecture,
|
14
|
+
# enabling server-side data aggregation before HTML generation.
|
15
|
+
#
|
16
|
+
# Responsibilities:
|
17
|
+
# - Dependency Resolution: Recursively discovers and loads all partials
|
18
|
+
# - Structural Representation: Organizes templates into a traversable tree
|
19
|
+
# - Traversal Interface: Provides methods to iterate templates in render order
|
20
|
+
#
|
21
|
+
# Key Characteristics:
|
22
|
+
# - Data-Agnostic: Knows nothing about runtime context or request data
|
23
|
+
# - Immutable: Once created, the composition is read-only
|
24
|
+
# - Cacheable: Can be cached in production for performance
|
25
|
+
class ViewComposition
|
26
|
+
class TemplateNotFoundError < StandardError; end
|
27
|
+
class CircularDependencyError < StandardError; end
|
28
|
+
|
29
|
+
attr_reader :root_template_name, :templates, :dependencies
|
30
|
+
|
31
|
+
def initialize(root_template_name, loader:)
|
32
|
+
@root_template_name = root_template_name
|
33
|
+
@loader = loader
|
34
|
+
@templates = {}
|
35
|
+
@dependencies = {}
|
36
|
+
@loading = Set.new
|
37
|
+
end
|
38
|
+
|
39
|
+
# Resolve all template dependencies
|
40
|
+
def resolve!
|
41
|
+
load_template_recursive(@root_template_name)
|
42
|
+
freeze_composition
|
43
|
+
self
|
44
|
+
end
|
45
|
+
|
46
|
+
# Iterate through all documents in render order
|
47
|
+
# Layout -> View -> Partials (depth-first)
|
48
|
+
def each_document_in_render_order(&)
|
49
|
+
return enum_for(:each_document_in_render_order) unless block_given?
|
50
|
+
|
51
|
+
visited = Set.new
|
52
|
+
|
53
|
+
# Process layout first if specified
|
54
|
+
root_doc = @templates[@root_template_name]
|
55
|
+
if root_doc && root_doc.layout
|
56
|
+
layout_name = root_doc.layout
|
57
|
+
if @templates[layout_name]
|
58
|
+
yield_template_recursive(layout_name, visited, &)
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
# Then process the root template and its dependencies
|
63
|
+
yield_template_recursive(@root_template_name, visited, &)
|
64
|
+
end
|
65
|
+
|
66
|
+
# Get a specific template by name
|
67
|
+
def template(name)
|
68
|
+
@templates[name]
|
69
|
+
end
|
70
|
+
|
71
|
+
# Check if a template exists in the composition
|
72
|
+
def has_template?(name)
|
73
|
+
@templates.key?(name)
|
74
|
+
end
|
75
|
+
|
76
|
+
# Get all template names
|
77
|
+
def template_names
|
78
|
+
@templates.keys
|
79
|
+
end
|
80
|
+
|
81
|
+
# Get direct dependencies of a template
|
82
|
+
def dependencies_of(template_name)
|
83
|
+
@dependencies[template_name] || []
|
84
|
+
end
|
85
|
+
|
86
|
+
private
|
87
|
+
|
88
|
+
def load_template_recursive(template_name, parent_path = nil)
|
89
|
+
# Check for circular dependencies
|
90
|
+
if @loading.include?(template_name)
|
91
|
+
raise CircularDependencyError, "Circular dependency detected: #{template_name} -> #{@loading.to_a.join(' -> ')}"
|
92
|
+
end
|
93
|
+
|
94
|
+
# Skip if already loaded
|
95
|
+
return if @templates.key?(template_name)
|
96
|
+
|
97
|
+
@loading.add(template_name)
|
98
|
+
|
99
|
+
begin
|
100
|
+
# Load template using the provided loader
|
101
|
+
parser = @loader.call(template_name)
|
102
|
+
|
103
|
+
unless parser
|
104
|
+
raise TemplateNotFoundError, "Template not found: #{template_name}"
|
105
|
+
end
|
106
|
+
|
107
|
+
# Store the template
|
108
|
+
@templates[template_name] = parser
|
109
|
+
@dependencies[template_name] = []
|
110
|
+
|
111
|
+
# Extract and load partials
|
112
|
+
extract_partials(parser).each do |partial_name|
|
113
|
+
@dependencies[template_name] << partial_name
|
114
|
+
load_template_recursive(partial_name, template_name)
|
115
|
+
end
|
116
|
+
|
117
|
+
# Load layout if specified and not already loaded
|
118
|
+
if parser.layout && !@templates.key?(parser.layout)
|
119
|
+
load_template_recursive(parser.layout, template_name)
|
120
|
+
end
|
121
|
+
ensure
|
122
|
+
@loading.delete(template_name)
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
def extract_partials(parser)
|
127
|
+
partials = Set.new
|
128
|
+
template_content = parser.section('template')
|
129
|
+
|
130
|
+
return partials unless template_content
|
131
|
+
|
132
|
+
# Extract partial references from template
|
133
|
+
# Looking for {{> partial_name}} patterns
|
134
|
+
template_content.scan(/\{\{>\s*([^\s}]+)\s*\}\}/) do |match|
|
135
|
+
partials.add(match[0])
|
136
|
+
end
|
137
|
+
|
138
|
+
partials
|
139
|
+
end
|
140
|
+
|
141
|
+
def yield_template_recursive(template_name, visited, &)
|
142
|
+
return if visited.include?(template_name)
|
143
|
+
|
144
|
+
visited.add(template_name)
|
145
|
+
|
146
|
+
# First yield dependencies (partials)
|
147
|
+
(@dependencies[template_name] || []).each do |dep_name|
|
148
|
+
yield_template_recursive(dep_name, visited, &)
|
149
|
+
end
|
150
|
+
|
151
|
+
# Then yield the template itself
|
152
|
+
if @templates[template_name]
|
153
|
+
yield template_name, @templates[template_name]
|
154
|
+
end
|
155
|
+
end
|
156
|
+
|
157
|
+
def freeze_composition
|
158
|
+
@templates.freeze
|
159
|
+
@dependencies.freeze
|
160
|
+
@templates.each_value(&:freeze)
|
161
|
+
@dependencies.each_value(&:freeze)
|
162
|
+
freeze
|
163
|
+
end
|
164
|
+
end
|
165
|
+
end
|
data/lib/rhales.rb
ADDED
@@ -0,0 +1,57 @@
|
|
1
|
+
# lib/rhales.rb
|
2
|
+
|
3
|
+
require_relative 'rhales/version'
|
4
|
+
require_relative 'rhales/errors'
|
5
|
+
require_relative 'rhales/configuration'
|
6
|
+
require_relative 'rhales/adapters/base_auth'
|
7
|
+
require_relative 'rhales/adapters/base_session'
|
8
|
+
require_relative 'rhales/context'
|
9
|
+
require_relative 'rhales/rue_document'
|
10
|
+
require_relative 'rhales/parsers/handlebars_parser'
|
11
|
+
require_relative 'rhales/parsers/rue_format_parser'
|
12
|
+
require_relative 'rhales/template_engine'
|
13
|
+
require_relative 'rhales/hydrator'
|
14
|
+
require_relative 'rhales/view_composition'
|
15
|
+
require_relative 'rhales/hydration_data_aggregator'
|
16
|
+
require_relative 'rhales/refinements/require_refinements'
|
17
|
+
require_relative 'rhales/view'
|
18
|
+
|
19
|
+
# Ruby Single File Components (RSFC)
|
20
|
+
#
|
21
|
+
# A framework for building server-rendered components with client-side hydration
|
22
|
+
# using .rue files (Ruby Single File Components). Similar to .vue files but for Ruby.
|
23
|
+
#
|
24
|
+
# Features:
|
25
|
+
# - Server-side template rendering with Handlebars-style syntax
|
26
|
+
# - Client-side data hydration with JSON injection
|
27
|
+
# - Partial support for component composition
|
28
|
+
# - Pluggable authentication and session adapters
|
29
|
+
# - Security-first design with XSS protection and CSP support
|
30
|
+
#
|
31
|
+
# Usage:
|
32
|
+
# Rhales.configure do |config|
|
33
|
+
# config.default_localhas_role?e = 'en'
|
34
|
+
# config.template_paths = ['app/templates']
|
35
|
+
# config.features = { dark_mode: true }
|
36
|
+
# end
|
37
|
+
#
|
38
|
+
# view = Rhales::View.new(request, session, user)
|
39
|
+
# html = view.render('my_component')
|
40
|
+
module Rhales
|
41
|
+
# Convenience method to create a view with props
|
42
|
+
def self.render(template_name, request: nil, session: nil, user: nil, locale: nil, **props)
|
43
|
+
view = View.new(request, session, user, locale, props: props)
|
44
|
+
view.render(template_name)
|
45
|
+
end
|
46
|
+
|
47
|
+
# Quick template rendering for testing/simple use cases
|
48
|
+
def self.render_template(template_content, context_data = {})
|
49
|
+
context = Context.minimal(props: context_data)
|
50
|
+
TemplateEngine.render(template_content, context)
|
51
|
+
end
|
52
|
+
|
53
|
+
# Create context with props (for advanced usage)
|
54
|
+
def self.create_context(request: nil, session: nil, user: nil, locale: nil, **props)
|
55
|
+
Context.for_view(request, session, user, locale, **props)
|
56
|
+
end
|
57
|
+
end
|
data/rhales.gemspec
ADDED
@@ -0,0 +1,46 @@
|
|
1
|
+
# rhales.gemspec
|
2
|
+
|
3
|
+
require_relative 'lib/rhales/version'
|
4
|
+
|
5
|
+
Gem::Specification.new do |spec|
|
6
|
+
spec.name = 'rhales'
|
7
|
+
spec.version = Rhales::VERSION
|
8
|
+
spec.authors = ['delano']
|
9
|
+
spec.email = ['gems@onetimesecret.com']
|
10
|
+
|
11
|
+
spec.summary = 'Rhales - Server-rendered components with client-side hydration (RSFCs)'
|
12
|
+
spec.description = <<~DESC
|
13
|
+
Rhales is a framework for building server-rendered components with
|
14
|
+
client-side data hydration using .rue files called RSFCs (Ruby
|
15
|
+
Single File Components). Similar to Vue.js single file components
|
16
|
+
but for server-side Ruby applications.
|
17
|
+
|
18
|
+
Features include Handlebars-style templating, JSON data injection, partial support,
|
19
|
+
pluggable authentication adapters, and security-first design.
|
20
|
+
DESC
|
21
|
+
|
22
|
+
spec.homepage = 'https://github.com/onetimesecret/rhales'
|
23
|
+
spec.license = 'MIT'
|
24
|
+
spec.required_ruby_version = '>= 3.4.0'
|
25
|
+
|
26
|
+
|
27
|
+
spec.metadata['source_code_uri'] = 'https://github.com/onetimesecret/rhales'
|
28
|
+
spec.metadata['changelog_uri'] = 'https://github.com/onetimesecret/rhales/blob/main/CHANGELOG.md'
|
29
|
+
spec.metadata['documentation_uri'] = 'https://github.com/onetimesecret/rhales/blob/main/README.md'
|
30
|
+
spec.metadata['rubygems_mfa_required'] = 'true'
|
31
|
+
|
32
|
+
# Specify which files should be added to the gem
|
33
|
+
spec.files = Dir.chdir(__dir__) do
|
34
|
+
Dir['{lib}/**/*', '*.md', '*.txt', '*.gemspec'].select { |f| File.file?(f) }
|
35
|
+
end
|
36
|
+
|
37
|
+
spec.bindir = 'exe'
|
38
|
+
spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
|
39
|
+
spec.require_paths = ['lib']
|
40
|
+
|
41
|
+
# Runtime dependencies
|
42
|
+
# (none currently - all parsing is done with manual recursive descent parsers)
|
43
|
+
|
44
|
+
# Development dependencies should be specified in Gemfile instead of gemspec
|
45
|
+
# See: https://bundler.io/guides/creating_gem.html#testing-our-gem
|
46
|
+
end
|
metadata
ADDED
@@ -0,0 +1,78 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: rhales
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.3.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- delano
|
8
|
+
bindir: exe
|
9
|
+
cert_chain: []
|
10
|
+
date: 2025-07-09 00:00:00.000000000 Z
|
11
|
+
dependencies: []
|
12
|
+
description: |
|
13
|
+
Rhales is a framework for building server-rendered components with
|
14
|
+
client-side data hydration using .rue files called RSFCs (Ruby
|
15
|
+
Single File Components). Similar to Vue.js single file components
|
16
|
+
but for server-side Ruby applications.
|
17
|
+
|
18
|
+
Features include Handlebars-style templating, JSON data injection, partial support,
|
19
|
+
pluggable authentication adapters, and security-first design.
|
20
|
+
email:
|
21
|
+
- gems@onetimesecret.com
|
22
|
+
executables: []
|
23
|
+
extensions: []
|
24
|
+
extra_rdoc_files: []
|
25
|
+
files:
|
26
|
+
- CLAUDE.locale.txt
|
27
|
+
- CLAUDE.md
|
28
|
+
- LICENSE.txt
|
29
|
+
- README.md
|
30
|
+
- lib/rhales.rb
|
31
|
+
- lib/rhales/adapters/base_auth.rb
|
32
|
+
- lib/rhales/adapters/base_request.rb
|
33
|
+
- lib/rhales/adapters/base_session.rb
|
34
|
+
- lib/rhales/configuration.rb
|
35
|
+
- lib/rhales/context.rb
|
36
|
+
- lib/rhales/csp.rb
|
37
|
+
- lib/rhales/errors.rb
|
38
|
+
- lib/rhales/errors/hydration_collision_error.rb
|
39
|
+
- lib/rhales/hydration_data_aggregator.rb
|
40
|
+
- lib/rhales/hydration_registry.rb
|
41
|
+
- lib/rhales/hydrator.rb
|
42
|
+
- lib/rhales/parsers/handlebars-grammar-review.txt
|
43
|
+
- lib/rhales/parsers/handlebars_parser.rb
|
44
|
+
- lib/rhales/parsers/rue_format_parser.rb
|
45
|
+
- lib/rhales/refinements/require_refinements.rb
|
46
|
+
- lib/rhales/rue_document.rb
|
47
|
+
- lib/rhales/template_engine.rb
|
48
|
+
- lib/rhales/tilt.rb
|
49
|
+
- lib/rhales/version.rb
|
50
|
+
- lib/rhales/view.rb
|
51
|
+
- lib/rhales/view_composition.rb
|
52
|
+
- rhales.gemspec
|
53
|
+
homepage: https://github.com/onetimesecret/rhales
|
54
|
+
licenses:
|
55
|
+
- MIT
|
56
|
+
metadata:
|
57
|
+
source_code_uri: https://github.com/onetimesecret/rhales
|
58
|
+
changelog_uri: https://github.com/onetimesecret/rhales/blob/main/CHANGELOG.md
|
59
|
+
documentation_uri: https://github.com/onetimesecret/rhales/blob/main/README.md
|
60
|
+
rubygems_mfa_required: 'true'
|
61
|
+
rdoc_options: []
|
62
|
+
require_paths:
|
63
|
+
- lib
|
64
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - ">="
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: 3.4.0
|
69
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
70
|
+
requirements:
|
71
|
+
- - ">="
|
72
|
+
- !ruby/object:Gem::Version
|
73
|
+
version: '0'
|
74
|
+
requirements: []
|
75
|
+
rubygems_version: 3.6.2
|
76
|
+
specification_version: 4
|
77
|
+
summary: Rhales - Server-rendered components with client-side hydration (RSFCs)
|
78
|
+
test_files: []
|