alba-inertia 0.1.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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 249d46c95a7a834638b3a75adfa694fc073dbd8df7dec61c810b7c94f5c667c0
4
+ data.tar.gz: a20aee6a7c1ecda7a3b16bf020fec41f77c55d5d085ec71141431bbb7e1eace5
5
+ SHA512:
6
+ metadata.gz: f2f3d7b2c1a04456bb9421dda758f70e8e06de0f38fa7f0db8a6f0ed52861d63deeb20c611351e38964133d22b3292b0d9a3f555ae543768244e5d250a43487e
7
+ data.tar.gz: 692f121c110342f99fa059f3ea4aeb2d2b4b18bffe8e12a3519ad28078ec25ee2a840b79eb37adde59e29b82d8026a0bb4707615181b56e034bdae2af432dd1f
data/CHANGELOG.md ADDED
@@ -0,0 +1,20 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ The format is based on [Keep a Changelog],
6
+ and this project adheres to [Semantic Versioning].
7
+
8
+ ## [Unreleased]
9
+
10
+ ## [0.1.0] - 2025-11-04
11
+
12
+ - Initial release ([@skryukov])
13
+
14
+ [@skryukov]: https://github.com/skryukov
15
+
16
+ [Unreleased]: https://github.com/skryukov/typelizer/compare/v0.1.0...HEAD
17
+ [0.1.0]: https://github.com/skryukov/typelizer/commits/v0.1.0
18
+
19
+ [Keep a Changelog]: https://keepachangelog.com/en/1.0.0/
20
+ [Semantic Versioning]: https://semver.org/spec/v2.0.0.html
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2025 Svyatoslav Kryukov
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,206 @@
1
+ # Alba::Inertia
2
+
3
+ Seamless integration between [Alba](https://github.com/okuramasafumi/alba) serializers and [Inertia Rails](https://inertia-rails.dev/).
4
+
5
+ ## Features
6
+
7
+ - Support for all Inertia prop types: optional, deferred, and merge props
8
+ - Lazy evaluation for efficient data loading on partial reloads
9
+ - Auto-detection of resource classes based on controller/action naming
10
+
11
+ ## Installation
12
+
13
+ Add to your Gemfile:
14
+
15
+ ```ruby
16
+ gem "alba"
17
+ gem "inertia_rails"
18
+ gem "alba-inertia"
19
+ ```
20
+
21
+ ## Usage
22
+
23
+ ### Basic Setup
24
+
25
+ Include `Alba::Inertia::Resource` in your resource classes:
26
+
27
+ ```ruby
28
+ class ApplicationResource
29
+ include Alba::Resource
30
+ include Alba::Inertia::Resource
31
+ end
32
+ ```
33
+
34
+ Include `Alba::Inertia::Controller` in your controllers:
35
+
36
+ ```ruby
37
+ class InertiaController < ApplicationController
38
+ include Alba::Inertia::Controller
39
+ end
40
+ ```
41
+
42
+ ### Defining Inertia Props
43
+
44
+ #### Inline `inertia:` option (recommended)
45
+
46
+ ```ruby
47
+ class CoursesIndexResource < ApplicationResource
48
+ # Simple attributes
49
+ attributes :id, :title
50
+
51
+ # Optional prop (loaded only when requested)
52
+ has_many :courses, serializer: CourseResource, inertia: :optional
53
+
54
+ # Deferred prop (loaded in separate request)
55
+ has_many :students, serializer: StudentResource, inertia: :defer
56
+
57
+ # Deferred with options
58
+ attribute :stats, inertia: { defer: { group: 'analytics', merge: true } } do |object|
59
+ expensive_calculation(object)
60
+ end
61
+
62
+ # Merge prop (for partial reloads)
63
+ has_many :comments, serializer: CommentResource, inertia: { merge: { match_on: :id } }
64
+ end
65
+ ```
66
+
67
+ #### Separate `inertia_prop` declaration
68
+
69
+ ```ruby
70
+ class CoursesIndexResource < ApplicationResource
71
+ has_many :courses, serializer: CourseResource
72
+ inertia_prop :courses, optional: true
73
+
74
+ attribute :stats
75
+ inertia_prop :stats, defer: { merge: true, group: 'analytics' }
76
+ end
77
+ ```
78
+
79
+ ### Controller Integration
80
+
81
+ ```ruby
82
+ class CoursesController < InertiaController
83
+ def index
84
+ @courses = Course.all
85
+ @current_category_id = params[:category_id]
86
+ # Auto-detects CoursesIndexResource and passes instance variables
87
+ end
88
+
89
+ def show
90
+ @course = Course.find(params[:id])
91
+
92
+ # With a custom component
93
+ render_inertia "Courses/Show"
94
+ end
95
+
96
+ def create
97
+ @course = Course.new(course_params)
98
+
99
+ if @course.save
100
+ redirect_to courses_path
101
+ else
102
+ # With errors
103
+ render_inertia inertia: { errors: user.errors }
104
+ end
105
+ end
106
+ end
107
+ ```
108
+
109
+ ### Serialization Modes
110
+
111
+ #### `.to_inertia` - For Inertia.js rendering
112
+
113
+ Returns lazy procs and Inertia prop objects:
114
+
115
+ ```ruby
116
+ resource = CoursesIndexResource.new(courses: @courses)
117
+ resource.to_inertia
118
+ # => { "courses" => <InertiaRails::OptionalProp>, "stats" => <Proc> }
119
+ ```
120
+
121
+ #### `.as_json` - For standard JSON
122
+
123
+ Returns normal data (Typelizer, API endpoints):
124
+
125
+ ```ruby
126
+ resource = CoursesIndexResource.new(courses: @courses)
127
+ resource.as_json
128
+ # => { "courses" => [...], "stats" => 42 }
129
+ ```
130
+
131
+ ### Inheritance
132
+
133
+ Metadata is inherited from parent resources:
134
+
135
+ ```ruby
136
+ class BaseResource < ApplicationResource
137
+ attribute :created_at, inertia: :optional
138
+ end
139
+
140
+ class CourseResource < BaseResource
141
+ attributes :id, :title
142
+ # Inherits created_at with optional: true
143
+ end
144
+ ```
145
+
146
+ Child can override parent metadata:
147
+
148
+ ```ruby
149
+ class ExtendedCourseResource < CourseResource
150
+ inertia_prop :created_at, defer: true # Override parent's optional: true
151
+ end
152
+ ```
153
+
154
+ ## Configuration
155
+
156
+ ```ruby
157
+ Alba::Inertia.configure do |config|
158
+ # Render with Alba resource class by default
159
+ config.default_render = true
160
+
161
+ # Wrap all props in lambdas by default
162
+ config.lazy_by_default = true
163
+ end
164
+ ```
165
+
166
+ ## Advanced Usage
167
+
168
+ ### Custom Serializer Selection
169
+
170
+ ```ruby
171
+ render_inertia(serializer: CustomResource)
172
+ ```
173
+
174
+ ### Custom Props
175
+
176
+ ```ruby
177
+ render_inertia(locals: { custom: 'props'})
178
+ ```
179
+
180
+ ## Naming Convention
181
+
182
+ The controller integration follows Rails conventions:
183
+
184
+ ```ruby
185
+ # Controller: CoursesController
186
+ # Action: index
187
+ # Expected Resource: CoursesIndexResource or CoursesIndexSerializer
188
+
189
+ # Controller: Admin::UsersController
190
+ # Action: show
191
+ # Expected Resource: Admin::UsersShowResource or Admin::UsersShowSerializer
192
+ ```
193
+
194
+ ## Development
195
+
196
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
197
+
198
+ To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).
199
+
200
+ ## Contributing
201
+
202
+ Bug reports and pull requests are welcome on GitHub at https://github.com/skryukov/alba-inertia.
203
+
204
+ ## License
205
+
206
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Alba
4
+ module Inertia
5
+ class Configuration
6
+ # Render with Alba resource class by default
7
+ attr_accessor :default_render
8
+
9
+ # Wrap all props in lambdas by default
10
+ attr_accessor :lazy_by_default
11
+
12
+ def initialize
13
+ @default_render = true
14
+ @lazy_by_default = true
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Alba
4
+ module Inertia
5
+ module Controller
6
+ private
7
+
8
+ def default_render
9
+ Alba::Inertia.config.default_render ? render_inertia : super
10
+ end
11
+
12
+ def render_inertia(component = nil, serializer: inertia_serializer_class, locals: view_assigns, **props)
13
+ resource = serializer&.new(locals.symbolize_keys!)
14
+ data = resource.respond_to?(:to_inertia) ? resource.to_inertia : resource.as_json
15
+
16
+ render inertia: component || true, props: data || {}, **props
17
+ end
18
+
19
+ def inertia_serializer_class
20
+ class_name = "#{controller_name}_#{action_name}_resource".classify
21
+ class_name.safe_constantize || class_name.gsub(/Resource$/, "Serializer").safe_constantize
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,57 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Alba
4
+ module Inertia
5
+ class PropBuilder
6
+ class << self
7
+ def build(evaluation_block, options)
8
+ if options[:optional]
9
+ wrap_optional(evaluation_block, options[:optional])
10
+ elsif options[:defer]
11
+ wrap_defer(evaluation_block, options[:defer])
12
+ elsif options[:merge]
13
+ wrap_merge(evaluation_block, options[:merge])
14
+ elsif options[:always]
15
+ wrap_always(evaluation_block, options[:always])
16
+ else
17
+ Alba::Inertia.config.lazy_by_default ? evaluation_block : evaluation_block.call
18
+ end
19
+ end
20
+
21
+ private
22
+
23
+ def wrap_always(value_block, _opts)
24
+ ::InertiaRails.always(&value_block)
25
+ end
26
+
27
+ def wrap_optional(value_block, _opts)
28
+ ::InertiaRails.optional(&value_block)
29
+ end
30
+
31
+ def wrap_defer(value_block, opts)
32
+ if opts.is_a?(Hash)
33
+ options = {
34
+ group: opts[:group],
35
+ merge: opts[:merge],
36
+ deep_merge: opts[:deep_merge],
37
+ match_on: opts[:match_on]
38
+ }.compact
39
+
40
+ ::InertiaRails.defer(**options, &value_block)
41
+ else
42
+ ::InertiaRails.defer(&value_block)
43
+ end
44
+ end
45
+
46
+ def wrap_merge(value_block, opts)
47
+ if opts.is_a?(Hash)
48
+ options = {match_on: opts[:match_on]}.compact
49
+ ::InertiaRails.merge(**options, &value_block)
50
+ else
51
+ ::InertiaRails.merge(&value_block)
52
+ end
53
+ end
54
+ end
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,149 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Alba
4
+ module Inertia
5
+ module Resource
6
+ def self.included(base)
7
+ base.extend(ClassMethods)
8
+ end
9
+
10
+ def self.extended(base)
11
+ base.include(self)
12
+ end
13
+
14
+ module ClassMethods
15
+ # Override Alba's attribute method to support inertia: option
16
+ #
17
+ # @example
18
+ # attribute :stats, inertia: :optional do |object|
19
+ # expensive_calc(object)
20
+ # end
21
+ #
22
+ # attribute :data, inertia: { defer: true } do |object|
23
+ # object.data
24
+ # end
25
+ def attribute(name, **options, &block)
26
+ extract_inertia_metadata(name, options)
27
+ super
28
+ end
29
+
30
+ # Override Alba's association method to support inertia: option
31
+ #
32
+ # @example
33
+ # has_many :courses, serializer: CourseResource, inertia: { defer: true }
34
+ # has_one :instructor, serializer: AuthorResource, inertia: :optional
35
+ # has_many :items, serializer: ItemResource, key: :products, inertia: :optional
36
+ def association(name, condition = nil, **options, &block)
37
+ key = options[:key] || name
38
+ extract_inertia_metadata(key, options)
39
+ super
40
+ end
41
+ alias_method :one, :association
42
+ alias_method :many, :association
43
+ alias_method :has_one, :association
44
+ alias_method :has_many, :association
45
+
46
+ # Mark an attribute or association to be wrapped with Inertia props.
47
+ # This does NOT change the attribute definition itself - it only stores metadata
48
+ # that will be used when .to_inertia is called.
49
+ #
50
+ # @example Mark existing association as optional
51
+ # has_many :courses, serializer: CourseResource
52
+ # inertia_prop :courses, optional: true
53
+ #
54
+ # @example Defer with merge option
55
+ # inertia_prop :stats, defer: { merge: true, group: 'analytics' }
56
+ #
57
+ # @example Merge with custom options
58
+ # inertia_prop :metadata, merge: { match_on: :id, prepend: 'meta_' }
59
+ def inertia_prop(name, **kwargs)
60
+ options = {
61
+ optional: kwargs.delete(:optional) || false,
62
+ defer: kwargs.delete(:defer) || false,
63
+ merge: kwargs.delete(:merge) || false,
64
+ always: kwargs.delete(:always) || false
65
+ }.select { |_k, v| v.present? }
66
+
67
+ inertia_metadata[name] = options.freeze
68
+ auto_typelize_from_inertia(name, options)
69
+ end
70
+
71
+ def inertia_metadata
72
+ @inertia_metadata ||= begin
73
+ metadata = {}
74
+ if superclass.respond_to?(:inertia_metadata) && superclass != Alba::Resource
75
+ parent_metadata = superclass.inertia_metadata
76
+ metadata.merge!(parent_metadata) unless parent_metadata.empty?
77
+ end
78
+ metadata
79
+ end
80
+ end
81
+
82
+ private
83
+
84
+ def extract_inertia_metadata(name, options)
85
+ return unless options.key?(:inertia)
86
+
87
+ inertia_opts = parse_inertia_option(options.delete(:inertia))
88
+ inertia_prop(name, **inertia_opts) if inertia_opts.present?
89
+ end
90
+
91
+ def parse_inertia_option(value)
92
+ case value
93
+ when Symbol
94
+ # inertia: :optional => { optional: true }
95
+ {value => true}
96
+ when Array
97
+ # inertia: [:optional, :defer] => { optional: true, defer: true }
98
+ value.each_with_object({}) { |key, hash| hash[key] = true }
99
+ when Hash
100
+ # inertia: { defer: true } or { defer: { merge: true } } => unchanged
101
+ value
102
+ else
103
+ {}
104
+ end
105
+ end
106
+
107
+ def auto_typelize_from_inertia(name, inertia_opts)
108
+ return unless respond_to?(:typelize)
109
+
110
+ should_be_optional = inertia_opts[:optional].present? ||
111
+ inertia_opts[:defer].present? ||
112
+ inertia_opts[:merge].present?
113
+
114
+ typelize(name.to_sym => [optional: true]) if should_be_optional
115
+ end
116
+ end
117
+
118
+ def to_inertia
119
+ return {} if object.nil?
120
+
121
+ metadata = self.class.inertia_metadata
122
+ result = {}
123
+
124
+ self.class._attributes.each do |attr_name, attr_body|
125
+ attr_name_str = attr_name.to_s
126
+
127
+ evaluation_block = build_evaluation_block(attr_name, attr_body)
128
+
129
+ result[attr_name_str] = if metadata.key?(attr_name)
130
+ PropBuilder.build(evaluation_block, metadata[attr_name])
131
+ else
132
+ Alba::Inertia.config.lazy_by_default ? evaluation_block : evaluation_block.call
133
+ end
134
+ end
135
+
136
+ result
137
+ end
138
+
139
+ private
140
+
141
+ def build_evaluation_block(attr_name, attr_body)
142
+ serializer = self
143
+ obj = object
144
+
145
+ -> { serializer.send(:fetch_attribute, obj, attr_name, attr_body) }
146
+ end
147
+ end
148
+ end
149
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Alba
4
+ module Inertia
5
+ VERSION = "0.1.0"
6
+ end
7
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "inertia/version"
4
+ require_relative "inertia/configuration"
5
+ require_relative "inertia/controller"
6
+ require_relative "inertia/prop_builder"
7
+ require_relative "inertia/resource"
8
+
9
+ module Alba
10
+ module Inertia
11
+ class << self
12
+ attr_accessor :configuration
13
+ end
14
+
15
+ def self.configure
16
+ self.configuration ||= Configuration.new
17
+ yield(configuration)
18
+ end
19
+
20
+ def self.config
21
+ self.configuration ||= Configuration.new
22
+ end
23
+
24
+ def self.reset_configuration!
25
+ self.configuration = Configuration.new
26
+ end
27
+ end
28
+ end
metadata ADDED
@@ -0,0 +1,97 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: alba-inertia
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Svyatoslav Kryukov
8
+ bindir: bin
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: alba
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - ">="
17
+ - !ruby/object:Gem::Version
18
+ version: '0'
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - ">="
24
+ - !ruby/object:Gem::Version
25
+ version: '0'
26
+ - !ruby/object:Gem::Dependency
27
+ name: inertia_rails
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - ">="
31
+ - !ruby/object:Gem::Version
32
+ version: '0'
33
+ type: :runtime
34
+ prerelease: false
35
+ version_requirements: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - ">="
38
+ - !ruby/object:Gem::Version
39
+ version: '0'
40
+ - !ruby/object:Gem::Dependency
41
+ name: zeitwerk
42
+ requirement: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - ">="
45
+ - !ruby/object:Gem::Version
46
+ version: '0'
47
+ type: :runtime
48
+ prerelease: false
49
+ version_requirements: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - ">="
52
+ - !ruby/object:Gem::Version
53
+ version: '0'
54
+ description: Seamless integration between Alba and Inertia Rails.
55
+ email:
56
+ - me@skryukov.dev
57
+ executables: []
58
+ extensions: []
59
+ extra_rdoc_files: []
60
+ files:
61
+ - CHANGELOG.md
62
+ - LICENSE.txt
63
+ - README.md
64
+ - lib/alba/inertia.rb
65
+ - lib/alba/inertia/configuration.rb
66
+ - lib/alba/inertia/controller.rb
67
+ - lib/alba/inertia/prop_builder.rb
68
+ - lib/alba/inertia/resource.rb
69
+ - lib/alba/inertia/version.rb
70
+ homepage: https://github.com/skryukov/typelizer
71
+ licenses:
72
+ - MIT
73
+ metadata:
74
+ bug_tracker_uri: https://github.com/skryukov/typelizer/issues
75
+ changelog_uri: https://github.com/skryukov/typelizer/blob/main/CHANGELOG.md
76
+ documentation_uri: https://github.com/skryukov/typelizer/blob/main/README.md
77
+ homepage_uri: https://github.com/skryukov/typelizer
78
+ source_code_uri: https://github.com/skryukov/typelizer
79
+ rubygems_mfa_required: 'true'
80
+ rdoc_options: []
81
+ require_paths:
82
+ - lib
83
+ required_ruby_version: !ruby/object:Gem::Requirement
84
+ requirements:
85
+ - - ">="
86
+ - !ruby/object:Gem::Version
87
+ version: 3.2.0
88
+ required_rubygems_version: !ruby/object:Gem::Requirement
89
+ requirements:
90
+ - - ">="
91
+ - !ruby/object:Gem::Version
92
+ version: '0'
93
+ requirements: []
94
+ rubygems_version: 3.6.9
95
+ specification_version: 4
96
+ summary: Seamless integration between Alba and Inertia Rails.
97
+ test_files: []