inertia_hanami 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.
Files changed (36) hide show
  1. checksums.yaml +7 -0
  2. data/.rspec +1 -0
  3. data/CHANGELOG.md +18 -0
  4. data/CODE_OF_CONDUCT.md +10 -0
  5. data/LICENSE.txt +21 -0
  6. data/PLAN.md +193 -0
  7. data/README.md +417 -0
  8. data/Rakefile +16 -0
  9. data/lib/generators/inertia_hanami/install_generator.rb +216 -0
  10. data/lib/generators/inertia_hanami/templates/helpers.rb.erb +12 -0
  11. data/lib/generators/inertia_hanami/templates/layout.html.erb.erb +15 -0
  12. data/lib/generators/inertia_hanami/templates/provider.rb.erb +5 -0
  13. data/lib/generators/inertia_hanami/templates/sample_action.rb.erb +15 -0
  14. data/lib/generators/inertia_hanami/templates/sample_template.html.erb.erb +1 -0
  15. data/lib/generators/inertia_hanami/templates/sample_view.rb.erb +13 -0
  16. data/lib/inertia_hanami/action.rb +214 -0
  17. data/lib/inertia_hanami/asset_version.rb +23 -0
  18. data/lib/inertia_hanami/cli/commands/install.rb +29 -0
  19. data/lib/inertia_hanami/cli.rb +16 -0
  20. data/lib/inertia_hanami/configuration.rb +22 -0
  21. data/lib/inertia_hanami/helper.rb +50 -0
  22. data/lib/inertia_hanami/middleware/csrf.rb +63 -0
  23. data/lib/inertia_hanami/middleware/redirects.rb +67 -0
  24. data/lib/inertia_hanami/middleware/version.rb +37 -0
  25. data/lib/inertia_hanami/prop_evaluator.rb +32 -0
  26. data/lib/inertia_hanami/props.rb +68 -0
  27. data/lib/inertia_hanami/protocol_builder.rb +194 -0
  28. data/lib/inertia_hanami/provider.rb +33 -0
  29. data/lib/inertia_hanami/renderer.rb +92 -0
  30. data/lib/inertia_hanami/request_context.rb +75 -0
  31. data/lib/inertia_hanami/ssr_renderer.rb +66 -0
  32. data/lib/inertia_hanami/testing/rspec.rb +265 -0
  33. data/lib/inertia_hanami/version.rb +5 -0
  34. data/lib/inertia_hanami.rb +24 -0
  35. data/sig/inertia_hanami.rbs +4 -0
  36. metadata +190 -0
@@ -0,0 +1,265 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+ require "cgi"
5
+
6
+ module InertiaHanami
7
+ # RSpec support for testing Inertia responses: matchers (`have_props`,
8
+ # `have_exact_props`, `have_no_prop`, `be_inertia_response`,
9
+ # `render_component`) plus partial-reload request helpers
10
+ # (`inertia_reload_only`, `inertia_reload_except`,
11
+ # `inertia_load_deferred_props`). Requires the host example group to
12
+ # `include Rack::Test::Methods` (or otherwise provide `last_response`,
13
+ # `last_request`, and `get`) - not auto-included, since a plain Rack env
14
+ # is all this needs.
15
+ #
16
+ # Ported from inertia-rails' `inertia_rails/{testing,rspec}.rb`, adapted
17
+ # to this gem's lack of a swappable renderer factory (there is nothing to
18
+ # monkeypatch to intercept the page hash, so `TestResponse` parses
19
+ # `last_response` directly instead) and its lack of ActiveSupport.
20
+ module Testing
21
+ def self.deep_symbolize_keys(value)
22
+ case value
23
+ when Hash
24
+ value.each_with_object({}) { |(k, v), acc| acc[k.to_sym] = deep_symbolize_keys(v) }
25
+ when Array
26
+ value.map { |v| deep_symbolize_keys(v) }
27
+ else
28
+ value
29
+ end
30
+ end
31
+
32
+ # Wraps a Rack::Test response, extracting the Inertia page envelope
33
+ # either from the JSON body (X-Inertia XHR response) or from the
34
+ # `<script data-page="...">` tag rendered into the initial HTML load.
35
+ class TestResponse
36
+ DATA_PAGE_REGEXP = %r{<script data-page="[^"]*" type="application/json">(.*?)</script>}m
37
+
38
+ def initialize(response)
39
+ @response = response
40
+ @page = extract_page
41
+ end
42
+
43
+ def inertia_response?
44
+ !@page.nil?
45
+ end
46
+
47
+ def component
48
+ @page && @page["component"]
49
+ end
50
+
51
+ def url
52
+ @page && @page["url"]
53
+ end
54
+
55
+ def version
56
+ @page && @page["version"]
57
+ end
58
+
59
+ def encrypt_history
60
+ @page && @page["encryptHistory"]
61
+ end
62
+
63
+ def clear_history
64
+ @page && @page["clearHistory"]
65
+ end
66
+
67
+ def props
68
+ @props ||= Testing.deep_symbolize_keys((@page && @page["props"]) || {})
69
+ end
70
+
71
+ def once_props
72
+ @once_props ||= Testing.deep_symbolize_keys((@page && @page["onceProps"]) || {})
73
+ end
74
+
75
+ def merge_props
76
+ (@page && @page["mergeProps"]) || []
77
+ end
78
+
79
+ def deep_merge_props
80
+ (@page && @page["deepMergeProps"]) || []
81
+ end
82
+
83
+ def match_props_on
84
+ (@page && @page["matchPropsOn"]) || []
85
+ end
86
+
87
+ def deferred_props
88
+ @deferred_props ||= ((@page && @page["deferredProps"]) || {}).each_with_object({}) do |(group, paths), acc|
89
+ acc[group.to_sym] = paths
90
+ end
91
+ end
92
+
93
+ private
94
+
95
+ def extract_page
96
+ return JSON.parse(@response.body) if @response.headers["X-Inertia"] == "true"
97
+
98
+ match = @response.body.match(DATA_PAGE_REGEXP)
99
+ return nil unless match
100
+
101
+ JSON.parse(CGI.unescapeHTML(match[1]))
102
+ rescue JSON::ParserError
103
+ nil
104
+ end
105
+ end
106
+
107
+ # Pure, framework-agnostic validators backing the RSpec matchers. Each
108
+ # returns a `{passed:, message:, negated_message:}` hash.
109
+ module Assertions
110
+ module_function
111
+
112
+ def validate_with_block(inertia, field, &block)
113
+ actual = inertia.public_send(field)
114
+ negated_message = "expected #{field} block to return false"
115
+
116
+ if block.call(actual)
117
+ { passed: true, negated_message: negated_message }
118
+ else
119
+ { passed: false, message: "#{field} block validation failed", negated_message: negated_message }
120
+ end
121
+ end
122
+
123
+ def validate_partial_match(inertia, field, expected)
124
+ actual = inertia.public_send(field)
125
+ expected = Testing.deep_symbolize_keys(expected)
126
+ negated = "expected #{field} not to include #{expected.inspect}"
127
+ return { passed: true, negated_message: negated } if expected.all? { |key, value| actual[key] == value }
128
+
129
+ message = "expected #{field} to include #{expected.inspect}\ngot: #{actual.inspect}"
130
+ { passed: false, message: message, negated_message: negated }
131
+ end
132
+
133
+ def validate_exact_match(inertia, field, expected)
134
+ actual = inertia.public_send(field)
135
+ expected = Testing.deep_symbolize_keys(expected)
136
+ negated = "expected #{field} not to equal #{expected.inspect}"
137
+ return { passed: true, negated_message: negated } if actual == expected
138
+
139
+ message = "expected #{field} to equal #{expected.inspect}, got #{actual.inspect}"
140
+ { passed: false, message: message, negated_message: negated }
141
+ end
142
+
143
+ def validate_key_absent(inertia, field, key)
144
+ actual = inertia.public_send(field)
145
+ key = key.to_sym
146
+ negated = "expected #{field} to have key #{key.inspect}"
147
+ return { passed: true, negated_message: negated } unless actual.key?(key)
148
+
149
+ { passed: false, message: "expected #{field} not to have key #{key.inspect}", negated_message: negated }
150
+ end
151
+
152
+ def validate_component(inertia, expected)
153
+ actual = inertia.component
154
+ negated = "expected component not to be #{expected.inspect}"
155
+ return { passed: true, negated_message: negated } if actual == expected
156
+
157
+ got = actual.nil? ? "nothing" : actual.inspect
158
+ { passed: false, message: "expected component to be #{expected.inspect}, got #{got}", negated_message: negated }
159
+ end
160
+
161
+ def validate_inertia_response(inertia)
162
+ negated = "expected response not to be an Inertia response"
163
+ unless inertia.is_a?(TestResponse)
164
+ message = "expected the `inertia` helper, got #{inertia.class}"
165
+ return { passed: false, message: message, negated_message: negated }
166
+ end
167
+ return { passed: true, negated_message: negated } if inertia.inertia_response?
168
+
169
+ { passed: false, message: "expected an Inertia response", negated_message: negated }
170
+ end
171
+ end
172
+
173
+ # Builds the RSpec matchers on top of Assertions' pure validators.
174
+ module MatcherFactory
175
+ def self.define_inertia_matcher(name, &validation_block)
176
+ ::RSpec::Matchers.define(name) do |*matcher_args|
177
+ match do |actual|
178
+ @result = instance_exec(actual, *matcher_args, &validation_block)
179
+ @result[:passed]
180
+ end
181
+
182
+ failure_message { @result[:message] }
183
+ failure_message_when_negated { @result[:negated_message] }
184
+ end
185
+ end
186
+
187
+ def self.partial_match_result(inertia, field, expected, block_arg, name)
188
+ return Assertions.validate_with_block(inertia, field, &block_arg) if block_arg
189
+ raise ArgumentError, "#{name} requires either an expected hash or a block" if expected.nil?
190
+
191
+ Assertions.validate_partial_match(inertia, field, expected)
192
+ end
193
+
194
+ def self.define_partial_matcher(name, field)
195
+ ::RSpec::Matchers.define(name) do |expected = nil|
196
+ match do |inertia|
197
+ @result = MatcherFactory.partial_match_result(inertia, field, expected, block_arg, name)
198
+ @result[:passed]
199
+ end
200
+
201
+ failure_message { @result[:message] }
202
+ failure_message_when_negated { @result[:negated_message] }
203
+ end
204
+ end
205
+
206
+ def self.define_exact_matcher(name, field)
207
+ define_inertia_matcher(name) { |inertia, expected| Assertions.validate_exact_match(inertia, field, expected) }
208
+ end
209
+
210
+ def self.define_key_absent_matcher(name, field)
211
+ define_inertia_matcher(name) { |inertia, key| Assertions.validate_key_absent(inertia, field, key) }
212
+ end
213
+ end
214
+
215
+ # Request-spec helpers for reading the current Inertia response and
216
+ # re-issuing partial-reload requests. Mixed into RSpec example groups
217
+ # below; assumes the host group already provides `last_response`,
218
+ # `last_request`, and `get` (e.g. via `include Rack::Test::Methods`).
219
+ module RequestHelpers
220
+ def inertia
221
+ TestResponse.new(last_response)
222
+ end
223
+
224
+ def inertia_reload_only(*props)
225
+ get last_request.fullpath, {}, partial_reload_headers.merge("HTTP_X_INERTIA_PARTIAL_DATA" => props.join(","))
226
+ end
227
+
228
+ def inertia_reload_except(*props)
229
+ get last_request.fullpath, {}, partial_reload_headers.merge("HTTP_X_INERTIA_PARTIAL_EXCEPT" => props.join(","))
230
+ end
231
+
232
+ def inertia_load_deferred_props(group = nil)
233
+ deferred = inertia.deferred_props
234
+ keys = group ? Array(deferred[group.to_sym]) : deferred.values.flatten
235
+ return if keys.empty?
236
+
237
+ inertia_reload_only(*keys)
238
+ end
239
+
240
+ private
241
+
242
+ def partial_reload_headers
243
+ headers = { "HTTP_X_INERTIA" => "true", "HTTP_X_INERTIA_PARTIAL_COMPONENT" => inertia.component }
244
+ headers["HTTP_X_INERTIA_VERSION"] = inertia.version if inertia.version
245
+ headers
246
+ end
247
+ end
248
+ end
249
+ end
250
+
251
+ RSpec.configure do |config|
252
+ config.include InertiaHanami::Testing::RequestHelpers
253
+ end
254
+
255
+ InertiaHanami::Testing::MatcherFactory.define_partial_matcher(:have_props, :props)
256
+ InertiaHanami::Testing::MatcherFactory.define_exact_matcher(:have_exact_props, :props)
257
+ InertiaHanami::Testing::MatcherFactory.define_key_absent_matcher(:have_no_prop, :props)
258
+
259
+ InertiaHanami::Testing::MatcherFactory.define_inertia_matcher(:be_inertia_response) do |inertia|
260
+ InertiaHanami::Testing::Assertions.validate_inertia_response(inertia)
261
+ end
262
+
263
+ InertiaHanami::Testing::MatcherFactory.define_inertia_matcher(:render_component) do |inertia, expected|
264
+ InertiaHanami::Testing::Assertions.validate_component(inertia, expected)
265
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module InertiaHanami
4
+ VERSION = "0.1.0"
5
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "inertia_hanami/version"
4
+ require_relative "inertia_hanami/configuration"
5
+ require_relative "inertia_hanami/provider"
6
+ require_relative "inertia_hanami/props"
7
+ require_relative "inertia_hanami/prop_evaluator"
8
+ require_relative "inertia_hanami/protocol_builder"
9
+ require_relative "inertia_hanami/request_context"
10
+ require_relative "inertia_hanami/ssr_renderer"
11
+ require_relative "inertia_hanami/renderer"
12
+ require_relative "inertia_hanami/action"
13
+ require_relative "inertia_hanami/helper"
14
+ require_relative "inertia_hanami/middleware/version"
15
+ require_relative "inertia_hanami/middleware/redirects"
16
+
17
+ module InertiaHanami
18
+ class Error < StandardError; end
19
+ end
20
+
21
+ # Only loaded when the `hanami` executable has already required hanami-cli
22
+ # (e.g. this gem is placed in Bundler's `:cli` group) - adds zero runtime
23
+ # dependency on hanami-cli for normal app boot.
24
+ require "inertia_hanami/cli" if defined?(Hanami::CLI)
@@ -0,0 +1,4 @@
1
+ module InertiaHanami
2
+ VERSION: String
3
+ # See the writing guide of rbs: https://github.com/ruby/rbs#guides
4
+ end
metadata ADDED
@@ -0,0 +1,190 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: inertia_hanami
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Paul Oguda
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: dry-configurable
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - "~>"
17
+ - !ruby/object:Gem::Version
18
+ version: '1.0'
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - "~>"
24
+ - !ruby/object:Gem::Version
25
+ version: '1.0'
26
+ - !ruby/object:Gem::Dependency
27
+ name: hanami-view
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - ">="
31
+ - !ruby/object:Gem::Version
32
+ version: '2.1'
33
+ type: :runtime
34
+ prerelease: false
35
+ version_requirements: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - ">="
38
+ - !ruby/object:Gem::Version
39
+ version: '2.1'
40
+ - !ruby/object:Gem::Dependency
41
+ name: hanami
42
+ requirement: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - ">="
45
+ - !ruby/object:Gem::Version
46
+ version: '0'
47
+ type: :development
48
+ prerelease: false
49
+ version_requirements: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - ">="
52
+ - !ruby/object:Gem::Version
53
+ version: '0'
54
+ - !ruby/object:Gem::Dependency
55
+ name: hanami-action
56
+ requirement: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - ">="
59
+ - !ruby/object:Gem::Version
60
+ version: '0'
61
+ type: :development
62
+ prerelease: false
63
+ version_requirements: !ruby/object:Gem::Requirement
64
+ requirements:
65
+ - - ">="
66
+ - !ruby/object:Gem::Version
67
+ version: '0'
68
+ - !ruby/object:Gem::Dependency
69
+ name: hanami-assets
70
+ requirement: !ruby/object:Gem::Requirement
71
+ requirements:
72
+ - - ">="
73
+ - !ruby/object:Gem::Version
74
+ version: '0'
75
+ type: :development
76
+ prerelease: false
77
+ version_requirements: !ruby/object:Gem::Requirement
78
+ requirements:
79
+ - - ">="
80
+ - !ruby/object:Gem::Version
81
+ version: '0'
82
+ - !ruby/object:Gem::Dependency
83
+ name: hanami-router
84
+ requirement: !ruby/object:Gem::Requirement
85
+ requirements:
86
+ - - ">="
87
+ - !ruby/object:Gem::Version
88
+ version: '0'
89
+ type: :development
90
+ prerelease: false
91
+ version_requirements: !ruby/object:Gem::Requirement
92
+ requirements:
93
+ - - ">="
94
+ - !ruby/object:Gem::Version
95
+ version: '0'
96
+ - !ruby/object:Gem::Dependency
97
+ name: rack-test
98
+ requirement: !ruby/object:Gem::Requirement
99
+ requirements:
100
+ - - ">="
101
+ - !ruby/object:Gem::Version
102
+ version: '0'
103
+ type: :development
104
+ prerelease: false
105
+ version_requirements: !ruby/object:Gem::Requirement
106
+ requirements:
107
+ - - ">="
108
+ - !ruby/object:Gem::Version
109
+ version: '0'
110
+ - !ruby/object:Gem::Dependency
111
+ name: rspec
112
+ requirement: !ruby/object:Gem::Requirement
113
+ requirements:
114
+ - - ">="
115
+ - !ruby/object:Gem::Version
116
+ version: '0'
117
+ type: :development
118
+ prerelease: false
119
+ version_requirements: !ruby/object:Gem::Requirement
120
+ requirements:
121
+ - - ">="
122
+ - !ruby/object:Gem::Version
123
+ version: '0'
124
+ description: Server-side adapter implementing the Inertia.js protocol for the Hanami
125
+ web framework.
126
+ email:
127
+ - mcpaul2058@gmail.com
128
+ executables: []
129
+ extensions: []
130
+ extra_rdoc_files: []
131
+ files:
132
+ - ".rspec"
133
+ - CHANGELOG.md
134
+ - CODE_OF_CONDUCT.md
135
+ - LICENSE.txt
136
+ - PLAN.md
137
+ - README.md
138
+ - Rakefile
139
+ - lib/generators/inertia_hanami/install_generator.rb
140
+ - lib/generators/inertia_hanami/templates/helpers.rb.erb
141
+ - lib/generators/inertia_hanami/templates/layout.html.erb.erb
142
+ - lib/generators/inertia_hanami/templates/provider.rb.erb
143
+ - lib/generators/inertia_hanami/templates/sample_action.rb.erb
144
+ - lib/generators/inertia_hanami/templates/sample_template.html.erb.erb
145
+ - lib/generators/inertia_hanami/templates/sample_view.rb.erb
146
+ - lib/inertia_hanami.rb
147
+ - lib/inertia_hanami/action.rb
148
+ - lib/inertia_hanami/asset_version.rb
149
+ - lib/inertia_hanami/cli.rb
150
+ - lib/inertia_hanami/cli/commands/install.rb
151
+ - lib/inertia_hanami/configuration.rb
152
+ - lib/inertia_hanami/helper.rb
153
+ - lib/inertia_hanami/middleware/csrf.rb
154
+ - lib/inertia_hanami/middleware/redirects.rb
155
+ - lib/inertia_hanami/middleware/version.rb
156
+ - lib/inertia_hanami/prop_evaluator.rb
157
+ - lib/inertia_hanami/props.rb
158
+ - lib/inertia_hanami/protocol_builder.rb
159
+ - lib/inertia_hanami/provider.rb
160
+ - lib/inertia_hanami/renderer.rb
161
+ - lib/inertia_hanami/request_context.rb
162
+ - lib/inertia_hanami/ssr_renderer.rb
163
+ - lib/inertia_hanami/testing/rspec.rb
164
+ - lib/inertia_hanami/version.rb
165
+ - sig/inertia_hanami.rbs
166
+ homepage: https://github.com/kamalogudah/inertia_hanami
167
+ licenses:
168
+ - MIT
169
+ metadata:
170
+ homepage_uri: https://github.com/kamalogudah/inertia_hanami
171
+ source_code_uri: https://github.com/kamalogudah/inertia_hanami/tree/main
172
+ changelog_uri: https://github.com/kamalogudah/inertia_hanami/blob/main/CHANGELOG.md
173
+ rdoc_options: []
174
+ require_paths:
175
+ - lib
176
+ required_ruby_version: !ruby/object:Gem::Requirement
177
+ requirements:
178
+ - - ">="
179
+ - !ruby/object:Gem::Version
180
+ version: 3.2.0
181
+ required_rubygems_version: !ruby/object:Gem::Requirement
182
+ requirements:
183
+ - - ">="
184
+ - !ruby/object:Gem::Version
185
+ version: '0'
186
+ requirements: []
187
+ rubygems_version: 4.0.3
188
+ specification_version: 4
189
+ summary: Inertia.js protocol adapter for Hanami
190
+ test_files: []