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.
@@ -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: []