inertia_builder 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: 671869c4438679ee4343c013de503e09f283cbdc46c4335426b14a1902e006b3
4
+ data.tar.gz: bdb6b02a5cf77582a76d9b924775afa58a33aff02bdc677aab23b0c21ba124da
5
+ SHA512:
6
+ metadata.gz: 6729a9608eeaf81447b9d38350d25080b4856de2150f9e3a6b1a556af4dd73012c5ddf4a34d3e29e0d6ae220a93f156c0b616b4abcdebd2246858df5718645c6
7
+ data.tar.gz: 55b3c90f175f655aefb21912ab84e34e99e481b61580af7baa87f2da23d40ff24ac945eb0bdee4a3e0d72fe276262f12b1c6ba7193db6c75c632643729a83fd0
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module InertiaBuilder
4
+ module Controller
5
+ extend ActiveSupport::Concern
6
+
7
+ included do
8
+ layout -> { inertia_json_request? ? false : 'application' }
9
+
10
+ before_action :force_json_response_with_html_template, if: -> { inertia_json_request? }
11
+ end
12
+
13
+ private
14
+
15
+ def inertia_json_request?
16
+ request.headers['X-Inertia'].present?
17
+ end
18
+
19
+ def force_json_response_with_html_template
20
+ response.content_type = Mime[:json]
21
+ response.headers['Vary'] = if response.headers['Vary'].blank?
22
+ 'X-Inertia'
23
+ else
24
+ "#{response.headers['Vary']}, X-Inertia"
25
+ end
26
+ response.set_header('X-Inertia', 'true')
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module InertiaBuilder
4
+ class Handler
5
+ def self.call(template, source = nil)
6
+ source ||= template.source
7
+
8
+ # Keep line numbers right in the error.
9
+ %{__already_defined = defined?(prop); prop||=InertiaBuilder::PropBuilder.new(self);component=true;#{source};
10
+ InertiaBuilder::Renderer.new(self, prop.props, component).render unless __already_defined}
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rails/railtie'
4
+
5
+ module InertiaBuilder
6
+ class Railtie < ::Rails::Railtie
7
+ initializer :inertia_builder do
8
+ ActiveSupport.on_load(:action_controller) do
9
+ include InertiaBuilder::Controller
10
+ end
11
+
12
+ ActiveSupport.on_load(:action_view) do
13
+ ActionView::Template.register_template_handler :inertia, InertiaBuilder::Handler
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module InertiaBuilder
4
+ class Renderer
5
+ def initialize(view_context, props, component)
6
+ @view_context = view_context
7
+ @inertia_renderer = ::InertiaRails::Renderer.new(
8
+ component,
9
+ view_context.controller,
10
+ view_context.request,
11
+ view_context.response,
12
+ view_context.controller.method(:render),
13
+ props: props
14
+ )
15
+ end
16
+
17
+ def render
18
+ page = @inertia_renderer.send(:page)
19
+
20
+ if @view_context.request.headers['X-Inertia']
21
+ page.to_json
22
+ else
23
+ @view_context.controller.render_to_string(
24
+ template: 'inertia',
25
+ layout: false,
26
+ locals: @inertia_renderer.send(:view_data).merge(page: page)
27
+ )
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module InertiaBuilder
4
+ VERSION = '0.1.0'
5
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'jbuilder/jbuilder_template'
4
+ require 'inertia_builder/handler'
5
+ require 'inertia_builder/controller'
6
+ require 'inertia_builder/renderer'
7
+ require 'inertia_builder/railtie' if defined?(Rails)
8
+
9
+ module InertiaBuilder
10
+ class PropBuilder < JbuilderTemplate
11
+ alias props attributes!
12
+
13
+ private
14
+
15
+ def _render_partial(options)
16
+ options[:handlers] = [:inertia]
17
+ options[:locals][:prop] = self
18
+ @context.render options
19
+ end
20
+
21
+ def _render_active_model_partial(object)
22
+ @context.render object, prop: self, handlers: [:inertia]
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,82 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'test_helper'
4
+ require 'action_view/testing/resolvers'
5
+ require 'rails/controller/testing'
6
+
7
+ class TestController < ActionController::Base
8
+ include InertiaBuilder::Controller
9
+
10
+ def index; end
11
+ end
12
+
13
+ class ControllerTest < ActionController::TestCase
14
+ include Rails::Controller::Testing::TemplateAssertions
15
+
16
+ tests TestController
17
+
18
+ def setup
19
+ super
20
+
21
+ @routes = ActionDispatch::Routing::RouteSet.new
22
+ @routes.draw { get 'index' => 'test#index' }
23
+
24
+ resolver = ActionView::FixtureResolver.new(
25
+ 'layouts/application.html.erb' => '<html><body><%= yield %></body></html>',
26
+ 'test/index.html.inertia' => 'prop.content "content"'
27
+ )
28
+
29
+ @controller.prepend_view_path(resolver)
30
+ end
31
+
32
+ def teardown
33
+ super
34
+ @routes.clear!
35
+ end
36
+
37
+ def test_renders_index_html_inertia_for_a_standard_non_inertia_request
38
+ get :index
39
+
40
+ assert_response :success
41
+ assert_template 'test/index'
42
+
43
+ assert_equal 'text/html; charset=utf-8', response.content_type
44
+ assert_nil response.headers['X-Inertia']
45
+
46
+ assert_equal response.body, inertia_html_with_props(content: 'content')
47
+ end
48
+
49
+ def test_renders_index_html_inertia_for_an_inertia_request_with_json_headers
50
+ @request.headers['X-Inertia'] = 'true'
51
+ get :index
52
+
53
+ assert_response :success
54
+ assert_template 'test/index'
55
+
56
+ assert_equal 'application/json; charset=utf-8', response.content_type
57
+ assert_equal 'X-Inertia', response.headers['Vary']
58
+ assert_equal 'true', response.headers['X-Inertia']
59
+
60
+ assert_equal response.body, inertia_json_with_props(content: 'content')
61
+ end
62
+
63
+ private
64
+
65
+ def inertia_html_with_props(props)
66
+ <<~HTML.chomp
67
+ <html><body><div id="app" data-page="#{ERB::Util.html_escape(inertia_json_with_props(props))}"></div>
68
+ </body></html>
69
+ HTML
70
+ end
71
+
72
+ def inertia_json_with_props(props)
73
+ {
74
+ component: 'test/index',
75
+ props:,
76
+ url: '/index',
77
+ version: nil,
78
+ encryptHistory: false,
79
+ clearHistory: false,
80
+ }.to_json
81
+ end
82
+ end
@@ -0,0 +1,161 @@
1
+ require 'test_helper'
2
+ require 'action_view'
3
+ require "active_model"
4
+ require 'action_view/testing/resolvers'
5
+
6
+ class User < Struct.new(:id, :first_name, :last_name, :email)
7
+ include ActiveModel::Conversion
8
+ end
9
+
10
+ class InertiaBuilderTest < Minitest::Test
11
+ USER_PARTIAL = <<~INERTIA
12
+ prop.id user.id
13
+ prop.first_name user.first_name
14
+ prop.last_name user.last_name
15
+ prop.email user.email
16
+ INERTIA
17
+
18
+ PARTIALS = {
19
+ "users/_user.html.inertia" => USER_PARTIAL,
20
+ }
21
+
22
+ def test_basic_html_rendering
23
+ result = render_view('prop.hello "World"')
24
+ assert_equal inertia_html_with_props({ hello: 'World' }), result
25
+ end
26
+
27
+ def test_basic_json_rendering
28
+ result = render_view('prop.hello "World"', json: true)
29
+ assert_equal inertia_json_with_props({ hello: 'World' }), result
30
+ end
31
+
32
+ def test_nested_prop
33
+ template = <<~INERTIA
34
+ prop.product do
35
+ prop.id 10
36
+ prop.name 'Keyboard'
37
+ prop.price 29.99
38
+ end
39
+ INERTIA
40
+
41
+ expected_props = {
42
+ product: {
43
+ id: 10,
44
+ name: 'Keyboard',
45
+ price: 29.99
46
+ }
47
+ }
48
+
49
+ assert_equal inertia_html_with_props(expected_props), render_view(template)
50
+ assert_equal inertia_json_with_props(expected_props), render_view(template, json: true)
51
+ end
52
+
53
+ def test_collection_prop
54
+ products = [
55
+ {
56
+ id: 10,
57
+ name: 'Keyboard',
58
+ price: 29.99
59
+ },
60
+ {
61
+ id: 11,
62
+ name: 'Monitor',
63
+ price: 599.00
64
+ }
65
+ ]
66
+
67
+ template = <<~INERTIA
68
+ prop.products @products do |product|
69
+ prop.id product.fetch(:id)
70
+ prop.name product.fetch(:name)
71
+ prop.price product.fetch(:price)
72
+ end
73
+ INERTIA
74
+
75
+ expected_props = { products: }
76
+
77
+ assert_equal inertia_html_with_props(expected_props), render_view(template, assigns: { products: })
78
+ assert_equal inertia_json_with_props(expected_props), render_view(template, assigns: { products: }, json: true)
79
+ end
80
+
81
+ def test_basic_partial_prop
82
+ user = User.new({ id: 42, first_name: 'John', last_name: 'Doe', email: 'john@email.com' })
83
+
84
+ template = <<~INERTIA
85
+ prop.user do
86
+ prop.partial! 'users/user', user: @user
87
+ end
88
+ INERTIA
89
+
90
+ expected_props = { user: }
91
+
92
+ assert_equal inertia_html_with_props(expected_props), render_view(template, assigns: { user: })
93
+ assert_equal inertia_json_with_props(expected_props), render_view(template, assigns: { user: }, json: true)
94
+ end
95
+
96
+ def test_collection_partial_prop
97
+ users = [
98
+ User.new({ id: 42, first_name: 'John', last_name: 'Doe', email: 'john@email.com' }),
99
+ User.new({ id: 43, first_name: 'Jane', last_name: 'Smith', email: 'jane@email.com' }),
100
+ ]
101
+
102
+ template = <<~INERTIA
103
+ prop.users @users do |user|
104
+ prop.partial! user
105
+ end
106
+ INERTIA
107
+
108
+ expected_props = { users: }
109
+
110
+ assert_equal inertia_html_with_props(expected_props), render_view(template, assigns: { users: })
111
+ assert_equal inertia_json_with_props(expected_props), render_view(template, assigns: { users: }, json: true)
112
+ end
113
+
114
+ def test_nil_prop_block
115
+ template = <<~INERTIA
116
+ prop.current_user do
117
+ prop.nil!
118
+ end
119
+ INERTIA
120
+
121
+
122
+ expected_props = { current_user: nil }
123
+
124
+ assert_equal inertia_html_with_props(expected_props), render_view(template)
125
+ assert_equal inertia_json_with_props(expected_props), render_view(template, json: true)
126
+ end
127
+
128
+ private
129
+
130
+ def render_view(source, assigns: {}, json: false)
131
+ req = ActionDispatch::TestRequest.create
132
+ req.headers['X-Inertia'] = 'true' if json
133
+
134
+ controller = ActionView::TestCase::TestController.new
135
+ controller.request = req
136
+
137
+ resolver = ActionView::FixtureResolver.new(PARTIALS.merge('source.html.inertia' => source))
138
+ lookup = ActionView::LookupContext.new([resolver], {}, [''])
139
+
140
+ view = ActionView::Base.with_empty_template_cache.new(lookup, assigns, controller)
141
+
142
+ view.render(template: 'source')
143
+ end
144
+
145
+ def inertia_html_with_props(props)
146
+ <<~HTML
147
+ <div id="app" data-page="#{ERB::Util.html_escape(inertia_json_with_props(props))}"></div>
148
+ HTML
149
+ end
150
+
151
+ def inertia_json_with_props(props)
152
+ {
153
+ component: '/',
154
+ props:,
155
+ url: '/',
156
+ version: nil,
157
+ encryptHistory: false,
158
+ clearHistory: false,
159
+ }.to_json
160
+ end
161
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rails'
4
+ require 'action_controller/railtie'
5
+
6
+ require 'jbuilder'
7
+ require 'inertia_rails'
8
+ require 'inertia_builder'
9
+ require 'active_support/testing/autorun'
10
+
11
+ ActiveSupport.test_order = :random
12
+
13
+ # Instantiate an Application in order to trigger the initializers
14
+ Class.new(Rails::Application) do
15
+ config.secret_key_base = 'secret'
16
+ config.eager_load = false
17
+ end.initialize!
18
+
19
+ # Touch AV::Base in order to trigger :action_view on_load hook before running the tests
20
+ ActionView::Base.inspect
metadata ADDED
@@ -0,0 +1,124 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: inertia_builder
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Rodrigo Lima
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: inertia_rails
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - "~>"
17
+ - !ruby/object:Gem::Version
18
+ version: '3.5'
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - "~>"
24
+ - !ruby/object:Gem::Version
25
+ version: '3.5'
26
+ - !ruby/object:Gem::Dependency
27
+ name: jbuilder
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - "~>"
31
+ - !ruby/object:Gem::Version
32
+ version: '2.0'
33
+ type: :runtime
34
+ prerelease: false
35
+ version_requirements: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - "~>"
38
+ - !ruby/object:Gem::Version
39
+ version: '2.0'
40
+ - !ruby/object:Gem::Dependency
41
+ name: rails
42
+ requirement: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - "~>"
45
+ - !ruby/object:Gem::Version
46
+ version: '7.0'
47
+ type: :development
48
+ prerelease: false
49
+ version_requirements: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - "~>"
52
+ - !ruby/object:Gem::Version
53
+ version: '7.0'
54
+ - !ruby/object:Gem::Dependency
55
+ name: rails-controller-testing
56
+ requirement: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - "~>"
59
+ - !ruby/object:Gem::Version
60
+ version: '1.0'
61
+ type: :development
62
+ prerelease: false
63
+ version_requirements: !ruby/object:Gem::Requirement
64
+ requirements:
65
+ - - "~>"
66
+ - !ruby/object:Gem::Version
67
+ version: '1.0'
68
+ - !ruby/object:Gem::Dependency
69
+ name: rake
70
+ requirement: !ruby/object:Gem::Requirement
71
+ requirements:
72
+ - - "~>"
73
+ - !ruby/object:Gem::Version
74
+ version: '13.3'
75
+ type: :development
76
+ prerelease: false
77
+ version_requirements: !ruby/object:Gem::Requirement
78
+ requirements:
79
+ - - "~>"
80
+ - !ruby/object:Gem::Version
81
+ version: '13.3'
82
+ email: rodrigotavio91@gmail.com
83
+ executables: []
84
+ extensions: []
85
+ extra_rdoc_files: []
86
+ files:
87
+ - lib/inertia_builder.rb
88
+ - lib/inertia_builder/controller.rb
89
+ - lib/inertia_builder/handler.rb
90
+ - lib/inertia_builder/railtie.rb
91
+ - lib/inertia_builder/renderer.rb
92
+ - lib/inertia_builder/version.rb
93
+ - test/controller_test.rb
94
+ - test/inertia_builder_test.rb
95
+ - test/test_helper.rb
96
+ homepage: https://github.com/rodrigotavio91/inertia-builder
97
+ licenses:
98
+ - MIT
99
+ metadata:
100
+ bug_tracker_uri: https://github.com/rodrigotavio91/inertia-builder/issues
101
+ changelog_uri: https://github.com/rodrigotavio91/inertia-builder/releases/tag/v0.1.0
102
+ source_code_uri: https://github.com/rodrigotavio91/inertia-builder/tree/v0.1.0
103
+ rubygems_mfa_required: 'true'
104
+ rdoc_options: []
105
+ require_paths:
106
+ - lib
107
+ required_ruby_version: !ruby/object:Gem::Requirement
108
+ requirements:
109
+ - - ">="
110
+ - !ruby/object:Gem::Version
111
+ version: 3.0.0
112
+ required_rubygems_version: !ruby/object:Gem::Requirement
113
+ requirements:
114
+ - - ">="
115
+ - !ruby/object:Gem::Version
116
+ version: '0'
117
+ requirements: []
118
+ rubygems_version: 3.6.9
119
+ specification_version: 4
120
+ summary: Create Inertia.js props via a Jbuilder-style DSL
121
+ test_files:
122
+ - test/controller_test.rb
123
+ - test/inertia_builder_test.rb
124
+ - test/test_helper.rb