inertia-rage 0.0.1

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,85 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Inertia
4
+ ##
5
+ # Encapsulates request-specific context for Inertia partial reloads.
6
+ class RequestContext
7
+ EMPTY = [].freeze
8
+ private_constant :EMPTY
9
+
10
+ # Creates a new request context.
11
+ #
12
+ # @param request [Rage::Request] the HTTP request object
13
+ # @param component [String] the Inertia component name being rendered
14
+ def initialize(request, component:)
15
+ @request = request
16
+ @component = component
17
+ end
18
+
19
+ # Returns the full request path including query string.
20
+ # @return [String] the request URL path
21
+ def url
22
+ @request.fullpath
23
+ end
24
+
25
+ # Determines the inclusion status of a prop based on partial reload headers.
26
+ #
27
+ # @param prop_name [String] the full path of the prop (e.g., "user.profile")
28
+ # @return [Symbol] the prop status:
29
+ # - `:unspecified` - no partial reload filtering applies
30
+ # - `:requested` - prop is explicitly requested in partial reload
31
+ # - `:excluded` - prop is excluded from partial reload
32
+ def prop_status(prop_name)
33
+ return :unspecified unless partial_render?
34
+
35
+ is_excluded = partial_except.any? do |except_prop_name|
36
+ prop_name == except_prop_name ||
37
+ (prop_name.start_with?(except_prop_name) && prop_name.start_with?("#{except_prop_name}."))
38
+ end
39
+
40
+ return :excluded if is_excluded
41
+ return :requested if partial_only.empty? && partial_except.any?
42
+ return :unspecified if partial_only.empty?
43
+
44
+ is_included_in_partial_reload = partial_only.any? do |only_prop_name|
45
+ prop_name == only_prop_name ||
46
+ (only_prop_name.start_with?(prop_name) && only_prop_name.start_with?("#{prop_name}.")) ||
47
+ (prop_name.start_with?(only_prop_name) && prop_name.start_with?("#{only_prop_name}."))
48
+ end
49
+
50
+ is_included_in_partial_reload ? :requested : :excluded
51
+ end
52
+
53
+ # Checks if a once prop should be excluded based on the client's cached props.
54
+ #
55
+ # @param prop_name [String] the cache key of the once prop
56
+ # @return [Boolean] `true` if the client already has this prop cached
57
+ def once_prop_excluded?(prop_name)
58
+ except_once.include?(prop_name)
59
+ end
60
+
61
+ # Checks if this is a partial reload request for the current component.
62
+ # @return [Boolean] `true` if the client requested a partial reload for this component
63
+ def partial_render?
64
+ partial_component == @component
65
+ end
66
+
67
+ private
68
+
69
+ def partial_except
70
+ @partial_except ||= @request.env["HTTP_X_INERTIA_PARTIAL_EXCEPT"]&.split(",") || EMPTY
71
+ end
72
+
73
+ def partial_only
74
+ @partial_only ||= @request.env["HTTP_X_INERTIA_PARTIAL_DATA"]&.split(",") || EMPTY
75
+ end
76
+
77
+ def except_once
78
+ @except_once ||= @request.env["HTTP_X_INERTIA_EXCEPT_ONCE_PROPS"]&.split(",") || EMPTY
79
+ end
80
+
81
+ def partial_component
82
+ @partial_component ||= @request.env["HTTP_X_INERTIA_PARTIAL_COMPONENT"]
83
+ end
84
+ end
85
+ end
@@ -0,0 +1,171 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rage/rspec"
4
+
5
+ module Inertia
6
+ module RSpec
7
+ # Wraps an Inertia response for test assertions.
8
+ #
9
+ # @example Accessing response data
10
+ # require "inertia/rspec"
11
+ #
12
+ # RSpec.describe UsersController, type: :request do
13
+ # it "renders user posts" do
14
+ # get "posts"
15
+ #
16
+ # expect(inertia.component).to eq("Posts/Index")
17
+ # expect(inertia.props).to have_key(:posts)
18
+ # end
19
+ # end
20
+ class TestResponse
21
+ # @param data [Hash] the raw Inertia response data
22
+ def initialize(data)
23
+ @data = data
24
+ end
25
+
26
+ # Returns the props with symbolized keys.
27
+ # @note To comply with the Inertia protocol, props always include the `errors` object.
28
+ #
29
+ # @return [Hash{Symbol => Object}] the props hash
30
+ def props
31
+ sybmolize_keys(@data[:props])
32
+ end
33
+
34
+ # Returns the component name.
35
+ #
36
+ # @return [String] the Inertia component name
37
+ def component
38
+ @data[:component]
39
+ end
40
+
41
+ # Returns the deferred props configuration.
42
+ #
43
+ # @return [Hash] the deferred props, or an empty hash if none
44
+ def deferred_props
45
+ @data[:deferredProps] || {}
46
+ end
47
+
48
+ private
49
+
50
+ def sybmolize_keys(data)
51
+ data.each_with_object({}).each do |(k, v), memo|
52
+ transformed_value = if v.is_a?(Hash)
53
+ sybmolize_keys(v)
54
+ elsif v.is_a?(Array)
55
+ v.map { |el| el.is_a?(Hash) ? sybmolize_keys(el) : el }
56
+ else
57
+ v
58
+ end
59
+
60
+ memo[k.to_sym] = transformed_value
61
+ end
62
+ end
63
+ end
64
+
65
+ module TestHelpers
66
+ # Returns the Inertia response from the last request.
67
+ #
68
+ # @return [TestResponse, nil] the wrapped Inertia response, or nil if the last
69
+ # request was not an Inertia response
70
+ #
71
+ # @example
72
+ # get "/users/1"
73
+ # expect(inertia.component).to eq("Users/Show")
74
+ # expect(inertia.props[:user]).to include(name: "John")
75
+ def inertia
76
+ inertia_response = Fiber.current.instance_variable_get(:@__inertia_current_response)
77
+ return unless inertia_response
78
+
79
+ TestResponse.new(inertia_response)
80
+ end
81
+ end
82
+
83
+ module RequestHelpers
84
+ # Performs a GET request with optional Inertia partial reload headers.
85
+ #
86
+ # @param inertia [Hash] partial reload options
87
+ # @option inertia [String, Symbol, Array<String, Symbol>] :only request only these props
88
+ # @option inertia [String, Symbol, Array<String, Symbol>] :except request all props except these
89
+ # @param headers [Hash] additional request headers
90
+ # @raise [ArgumentError] if unknown options are passed in the inertia hash
91
+ #
92
+ # @example Request only specific props
93
+ # get "/users/1", inertia: { only: [:user, :permissions] }
94
+ #
95
+ # @example Request all props except some
96
+ # get "/users/1", inertia: { except: :audit_log }
97
+ def get(*, inertia: nil, headers: {}, **)
98
+ if inertia
99
+ inertia = {} unless inertia.is_a?(Hash)
100
+
101
+ if inertia.except(:only, :except).any?
102
+ raise ArgumentError, "Unknown :inertia option. Supported values are :only and :except"
103
+ end
104
+
105
+ headers = headers.merge({
106
+ "X-Inertia" => "true",
107
+ "X-Inertia-Partial-Component" => self.inertia&.component || double(:== => true)
108
+ })
109
+
110
+ if only = inertia[:only]
111
+ headers["X-Inertia-Partial-Data"] = Array(only).join(",")
112
+ elsif except = inertia[:except]
113
+ headers["X-Inertia-Partial-Except"] = Array(except).join(",")
114
+ end
115
+ end
116
+
117
+ super(*, headers:, **)
118
+ end
119
+ end
120
+ end
121
+ end
122
+
123
+ RSpec::Matchers.matcher :redirect_to do |expected, external: false|
124
+ failure_message do |response|
125
+ header = external ? "x-inertia-location" : "location"
126
+ actual = response.headers[header]
127
+
128
+ if expected.is_a?(Regexp)
129
+ "expected response to be a redirect to /#{expected.source}/ but was a redirect to \"#{actual}\""
130
+ else
131
+ "expected response to be a redirect to \"#{expected}\" but was a redirect to \"#{actual}\""
132
+ end
133
+ end
134
+
135
+ failure_message_when_negated do |response|
136
+ if expected.is_a?(Regexp)
137
+ "expected not to redirect to /#{expected.source}/, but did"
138
+ else
139
+ "expected not to redirect to \"#{expected}\", but did"
140
+ end
141
+ end
142
+
143
+ match do |response|
144
+ header = external ? "x-inertia-location" : "location"
145
+ actual = response.headers[header]
146
+
147
+ if expected.is_a?(Regexp)
148
+ actual&.match?(expected)
149
+ else
150
+ actual == expected
151
+ end
152
+ end
153
+ end
154
+
155
+ RSpec.configure do |config|
156
+ config.include(Inertia::RSpec::TestHelpers, type: :request)
157
+ config.prepend(Inertia::RSpec::RequestHelpers, type: :request)
158
+
159
+ config.before(:each) do
160
+ Fiber.current.instance_variable_set(:@__inertia_current_response, nil)
161
+ end
162
+
163
+ config.before(:each) do
164
+ test_fiber = Fiber.current
165
+
166
+ allow_any_instance_of(Inertia::ProtocolBuilder).to receive(:call).and_wrap_original do |m, *args, **kwargs|
167
+ data = m.call(*args, **kwargs)
168
+ test_fiber.instance_variable_set(:@__inertia_current_response, data)
169
+ end
170
+ end
171
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Inertia
4
+ VERSION = "0.0.1"
5
+ end
@@ -0,0 +1,27 @@
1
+ module Inertia
2
+ ##
3
+ # A daemon that runs the Vite development server alongside the Rage process.
4
+ #
5
+ class ViteDevServer < Rage::Daemon
6
+ def perform
7
+ puts "INFO: Starting Vite dev server"
8
+
9
+ config = Inertia.config.dev_server
10
+ host, port = config.host, config.port
11
+
12
+ command = "#{Frontend.package_runner} vite dev --clearScreen false --host #{host} --port #{port}"
13
+ @pid = Process.spawn(command, chdir: Frontend.root)
14
+ Process.wait(@pid)
15
+
16
+ # Vite child process sometimes receives a shutdown signal first. The supervisor sees that
17
+ # the Vite process has exited and schedules a restart before Rage processes its own shutdown.
18
+ # The delay allows Rage to handle the signal, stop supervision, and avoid a confusing log message.
19
+ sleep 0.1
20
+ end
21
+
22
+ def cleanup
23
+ Process.kill("TERM", @pid)
24
+ rescue Errno::ESRCH
25
+ end
26
+ end
27
+ end
@@ -0,0 +1 @@
1
+ require_relative "inertia/inertia"
metadata ADDED
@@ -0,0 +1,77 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: inertia-rage
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - Roman Samoilov
8
+ bindir: exe
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: rage-rb
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - "~>"
17
+ - !ruby/object:Gem::Version
18
+ version: '1.26'
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - "~>"
24
+ - !ruby/object:Gem::Version
25
+ version: '1.26'
26
+ email:
27
+ - developers@rage-rb.dev
28
+ executables: []
29
+ extensions: []
30
+ extra_rdoc_files: []
31
+ files:
32
+ - ".rspec"
33
+ - ".yardopts"
34
+ - CHANGELOG.md
35
+ - CODE_OF_CONDUCT.md
36
+ - LICENSE.txt
37
+ - README.md
38
+ - Rakefile
39
+ - lib/inertia-rage.rb
40
+ - lib/inertia/configuration.rb
41
+ - lib/inertia/controller_helpers.rb
42
+ - lib/inertia/frontend.rb
43
+ - lib/inertia/inertia.rb
44
+ - lib/inertia/middleware/assets.rb
45
+ - lib/inertia/middleware/version.rb
46
+ - lib/inertia/props.rb
47
+ - lib/inertia/protocol_builder.rb
48
+ - lib/inertia/rage_extension.rb
49
+ - lib/inertia/renderer.rb
50
+ - lib/inertia/request_context.rb
51
+ - lib/inertia/rspec.rb
52
+ - lib/inertia/version.rb
53
+ - lib/inertia/vite_dev_server.rb
54
+ homepage: https://rage-rb.dev
55
+ licenses:
56
+ - MIT
57
+ metadata:
58
+ homepage_uri: https://rage-rb.dev
59
+ source_code_uri: https://github.com/rage-rb/inertia-rage
60
+ rdoc_options: []
61
+ require_paths:
62
+ - lib
63
+ required_ruby_version: !ruby/object:Gem::Requirement
64
+ requirements:
65
+ - - ">="
66
+ - !ruby/object:Gem::Version
67
+ version: 3.3.0
68
+ required_rubygems_version: !ruby/object:Gem::Requirement
69
+ requirements:
70
+ - - ">="
71
+ - !ruby/object:Gem::Version
72
+ version: '0'
73
+ requirements: []
74
+ rubygems_version: 4.0.10
75
+ specification_version: 4
76
+ summary: Inertia.js adapter for the Rage framework
77
+ test_files: []