liquid_response 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: bf11fe2f34626e9a1de07771d207a6fd41b305358f40251711ca1c0f8c97b7c2
4
+ data.tar.gz: e31a1a2426d569019b198f7b948d513cda3789fec8befe89f663cea2be0d33fb
5
+ SHA512:
6
+ metadata.gz: a94e27ac67684b698d91b535e3c4945ac29c729afb772a18aa854f76d2da97fe63d63170df834231cb28defa02426a8f2f5fd23bf1cdea9faf1883c54f64cd85
7
+ data.tar.gz: c19d90983a3f2b216e0fa73a4cd759bce8c67a6baf92ce04ee6617643d1cce34c1660b85fac39f026b42131d9379e04c8b5f54414eeeaeed4d5d44288d7960f0
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2026 Elia Schito
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,266 @@
1
+ # LiquidResponse
2
+
3
+ Rails controller and view helpers for rendering [Liquid](https://shopify.github.io/liquid/) templates — designed for [Shopify App Proxy](https://shopify.dev/docs/apps/online-store/app-proxies) and any Rails app that serves Liquid markup to a storefront.
4
+
5
+ ## How it works
6
+
7
+ Shopify's App Proxy lets your Rails app respond to storefront requests with Liquid templates, which Shopify then renders in the context of the live theme. `LiquidResponse` handles:
8
+
9
+ - Registering the `liquid` MIME type with Rails
10
+ - Telling Rails to prefer `liquid` over `html` for app proxy requests
11
+ - Stripping `turbo-stream` from the Accept header (Turbo injects it everywhere; the proxy can't use it)
12
+ - View helpers for building valid Liquid `{% render %}`, `{% assign %}`, and `{% capture %}` tags from ERB
13
+
14
+ ## Installation
15
+
16
+ Add to your Gemfile:
17
+
18
+ ```ruby
19
+ gem "liquid_response"
20
+ ```
21
+
22
+ ## Setup
23
+
24
+ ### 1. Register the MIME type
25
+
26
+ ```ruby
27
+ # config/initializers/liquid_response.rb
28
+ require "liquid_response"
29
+
30
+ LiquidResponse.register_liquid_mime_type!
31
+ ```
32
+
33
+ ### 2. Include the controller helper
34
+
35
+ ```ruby
36
+ class AppProxyController < ApplicationController
37
+ include LiquidResponse::ControllerHelper
38
+
39
+ allow_liquid_rendering_when_html_is_requested
40
+ end
41
+ ```
42
+
43
+ `allow_liquid_rendering_when_html_is_requested` installs a `before_action` that:
44
+ - Strips `turbo-stream` from `request.formats`
45
+ - Prepends `Mime[:liquid]` so Rails picks the `.liquid.erb` template
46
+
47
+ ### 3. Create `.liquid.erb` views
48
+
49
+ Rails will look for `app/views/posts/show.liquid.erb` when the request format resolves to `liquid`. Write Liquid tags using the view helpers:
50
+
51
+ ```erb
52
+ <%# app/views/posts/show.liquid.erb %>
53
+ <%= render_liquid "my-app--post--card", title: @post.title, published: @post.published? %>
54
+ ```
55
+
56
+ This outputs:
57
+
58
+ ```liquid
59
+ {% render "my-app--post--card", title: "Hello World", published: true %}
60
+ ```
61
+
62
+ ## View helpers
63
+
64
+ All helpers are available in views once `LiquidResponse::ControllerHelper` is included (it auto-includes `LiquidResponse::ViewHelper` as a helper module).
65
+
66
+ ### `render_liquid(snippet_name, **assigns)`
67
+
68
+ Emits a Liquid `{% render %}` tag with the given assigns.
69
+
70
+ ```erb
71
+ <%= render_liquid "my-app--product--card",
72
+ title: @product.title,
73
+ price: @product.price,
74
+ available: @product.available? %>
75
+ ```
76
+
77
+ ```liquid
78
+ {% render "my-app--product--card", title: "Widget", price: 9.99, available: true %}
79
+ ```
80
+
81
+ ### `assign_liquid(variable_name, value)`
82
+
83
+ Emits a `{% assign %}` tag. Useful for hoisting a value to Liquid scope before passing it to a snippet.
84
+
85
+ ```erb
86
+ <%= assign_liquid :greeting, "Welcome" %>
87
+ ```
88
+
89
+ ```liquid
90
+ {% assign greeting = "Welcome" %}
91
+ ```
92
+
93
+ ### `liquid_variable(name)`
94
+
95
+ Returns a `LiquidVariable` object — a placeholder that renders as a bare Liquid variable name (no quotes). Pass it as an assign value when the variable already exists in Liquid scope.
96
+
97
+ ```erb
98
+ {% assign products = collections.all.products %}
99
+ <%= render_liquid "my-app--product--list", items: liquid_variable(:products) %>
100
+ ```
101
+
102
+ ```liquid
103
+ {% render "my-app--product--list", items: products %}
104
+ ```
105
+
106
+ ### `liquid_capture(name, &block)`
107
+
108
+ Wraps the block output in `{% capture %}` / `{% endcapture %}`.
109
+
110
+ ```erb
111
+ <%= liquid_capture(:sidebar) do %>
112
+ <p>Some sidebar content</p>
113
+ <% end %>
114
+ ```
115
+
116
+ ```liquid
117
+ {% capture sidebar %}<p>Some sidebar content</p>{% endcapture %}
118
+ ```
119
+
120
+ ## Passing complex values
121
+
122
+ ### Strings, numbers, booleans, nil, dates, times, symbols
123
+
124
+ Converted to their JSON equivalents automatically.
125
+
126
+ ```erb
127
+ <%= render_liquid "my-app--order",
128
+ id: 42,
129
+ status: :pending,
130
+ placed_on: Date.today,
131
+ gift: false,
132
+ note: nil %>
133
+ ```
134
+
135
+ ```liquid
136
+ {% render "my-app--order", id: 42, status: "pending", placed_on: "2026-05-26", gift: false, note: null %}
137
+ ```
138
+
139
+ ### Hashes (nested objects)
140
+
141
+ Each Hash assign is promoted to an intermediate `{% assign %}` variable so Liquid receives it as an object.
142
+
143
+ ```erb
144
+ <%= render_liquid "my-app--address--card",
145
+ address: {
146
+ street: @address.line1,
147
+ city: @address.city,
148
+ zip: @address.zip
149
+ } %>
150
+ ```
151
+
152
+ ```liquid
153
+ {% assign h_address_a1b2c = null | default: street: "Via Roma 1", city: "Roma", zip: "00100" %}
154
+ {% render "my-app--address--card", address: h_address_a1b2c %}
155
+ ```
156
+
157
+ Nesting works recursively:
158
+
159
+ ```erb
160
+ <%= render_liquid "my-app--subscription",
161
+ plan: {
162
+ name: "Monthly",
163
+ interval: { unit: "week", count: 4 }
164
+ } %>
165
+ ```
166
+
167
+ ```liquid
168
+ {% assign h_interval_x = null | default: unit: "week", count: 4 %}
169
+ {% assign h_plan_y = null | default: name: "Monthly", interval: h_interval_x %}
170
+ {% render "my-app--subscription", plan: h_plan_y %}
171
+ ```
172
+
173
+ ### Arrays
174
+
175
+ Arrays are converted to indexed hashes (Liquid has no native array literal):
176
+
177
+ ```erb
178
+ <%= render_liquid "my-app--tag--list", tags: ["sale", "new"] %>
179
+ ```
180
+
181
+ ```liquid
182
+ {% assign h_tags_z = null | default: 0: "sale", 1: "new" %}
183
+ {% render "my-app--tag--list", tags: h_tags_z %}
184
+ ```
185
+
186
+ ### Custom objects — `as_liquid` protocol
187
+
188
+ Any object that implements `as_liquid` is serialized through it. The method must return a `Hash`, a scalar, or another `as_liquid`-capable object.
189
+
190
+ ```ruby
191
+ class Product
192
+ def as_liquid
193
+ { "title" => title, "price" => price.to_f, "available" => available? }
194
+ end
195
+ end
196
+ ```
197
+
198
+ ```erb
199
+ <%= render_liquid "my-app--product--card", product: @product %>
200
+ ```
201
+
202
+ `LiquidResponse` ships with `as_liquid` on `String`, `Numeric`, `TrueClass`, `FalseClass`, `NilClass`, `Date`, `Time`, `Symbol`, `Array`, `Hash`, and `Data` — all the types you use day-to-day.
203
+
204
+ ### ActiveModel / Serializable objects
205
+
206
+ Objects that respond to `serializable_hash` are converted automatically:
207
+
208
+ ```erb
209
+ <%= render_liquid "my-app--user--card", user: current_user %>
210
+ ```
211
+
212
+ ### Mixing types
213
+
214
+ Everything composes:
215
+
216
+ ```erb
217
+ <%= render_liquid "my-app--portal",
218
+ customer_name: @customer.name,
219
+ logged_in: true,
220
+ items: liquid_variable(:cart_items),
221
+ address: {
222
+ city: @shipping_address.city,
223
+ zip: @shipping_address.zip
224
+ } %>
225
+ ```
226
+
227
+ ## `liquid_routes` (app-level, not in the gem)
228
+
229
+ The helper `liquid_routes` — which exposes Rails route helpers as a Liquid-usable hash — is intentionally **not** part of this gem. It depends on your app's routing conventions. Define it yourself in your base controller and expose it via `helper_method`:
230
+
231
+ ```ruby
232
+ class AppProxyController < ApplicationController
233
+ include LiquidResponse::ControllerHelper
234
+ allow_liquid_rendering_when_html_is_requested
235
+
236
+ helper_method :liquid_routes
237
+
238
+ private
239
+
240
+ def liquid_routes
241
+ Rails.application.routes.routes
242
+ .filter_map do |route|
243
+ next unless route.name&.start_with?("my_app_proxy_")
244
+ [route.name.sub("my_app_proxy_", "") + "_url", route.path.spec.to_s.sub(/\([^)]*\)/, "")]
245
+ end
246
+ .sort.to_h
247
+ end
248
+ end
249
+ ```
250
+
251
+ Then in views:
252
+
253
+ ```erb
254
+ <%= render_liquid "my-app--nav", routes: liquid_routes %>
255
+ ```
256
+
257
+ ## Development
258
+
259
+ ```bash
260
+ bundle install
261
+ bundle exec rake test
262
+ ```
263
+
264
+ ## License
265
+
266
+ MIT
data/Rakefile ADDED
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ require "minitest/test_task"
5
+
6
+ Minitest::TestTask.create
7
+
8
+ task default: :test
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_support/concern"
4
+ require "liquid_response/view_helper"
5
+
6
+ module LiquidResponse
7
+ module ControllerHelper
8
+ extend ActiveSupport::Concern
9
+
10
+ class_methods do
11
+ def allow_liquid_rendering_when_html_is_requested
12
+ before_action do
13
+ # Strip turbo-stream from all positions — Turbo's Accept header lists it first,
14
+ # which causes Rails to select that format and return the wrong content type.
15
+ request.formats.reject! { |f| f.turbo_stream? }
16
+ html_like = request.formats.any? { |f| f.html? || f.all? }
17
+ request.formats.unshift Mime[:liquid] if html_like
18
+ end
19
+ end
20
+ end
21
+
22
+ included do
23
+ helper LiquidResponse::ViewHelper
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "date"
4
+
5
+ [String, Numeric, TrueClass, FalseClass, NilClass, Date, Time, Symbol].each do |klass|
6
+ klass.class_eval do
7
+ def as_liquid
8
+ self
9
+ end
10
+ end
11
+ end
12
+
13
+ Array.class_eval do
14
+ def as_liquid
15
+ map.with_index { |v, i| [i, v] }.to_h.as_liquid
16
+ end
17
+ end
18
+
19
+ Hash.class_eval do
20
+ def as_liquid
21
+ transform_values { it.respond_to?(:as_liquid) ? it.as_liquid : it }
22
+ end
23
+ end
24
+
25
+ Data.class_eval do
26
+ def as_liquid
27
+ to_h.as_liquid
28
+ end
29
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module LiquidResponse
4
+ class UnsupportedValueError < ArgumentError
5
+ attr_accessor :key, :value
6
+
7
+ def initialize(value, key = nil)
8
+ @value = value
9
+ @key = key
10
+ super("Can't convert value #{value.inspect} of type #{value.class} to a liquid-compatible format")
11
+ end
12
+
13
+ def message
14
+ key ? "Error converting value for key '#{key}': #{super}" : super
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module LiquidResponse
4
+ VERSION = "0.1.0"
5
+ end
@@ -0,0 +1,112 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+ require "active_support/core_ext/object/blank"
5
+ require "active_support/core_ext/object/json"
6
+ require "liquid_response/errors"
7
+ require "liquid_response/core_ext"
8
+
9
+ module LiquidResponse
10
+ module ViewHelper
11
+ class LiquidBuffer
12
+ class LiquidVariable
13
+ def initialize(name)
14
+ @name = name.to_s
15
+ self.class.validate_name!(@name)
16
+ end
17
+
18
+ def self.validate_name!(name)
19
+ unless name.to_s.match?(/\A[a-zA-Z_][a-zA-Z0-9_]*\z/)
20
+ raise ArgumentError, "Invalid Liquid variable name: #{name}"
21
+ end
22
+ end
23
+
24
+ attr_reader :name
25
+
26
+ def self.random(prefix = "var")
27
+ prefix = prefix.to_s.gsub(/[^a-zA-Z0-9]+/, "_")
28
+ prefix = "var" if prefix.blank? || prefix.match?(/\A\d/)
29
+ new("#{prefix}_#{SecureRandom.hex(5)}")
30
+ end
31
+
32
+ def inspect
33
+ "LiquidVariable(#{name})"
34
+ end
35
+
36
+ def as_liquid
37
+ self
38
+ end
39
+ end
40
+
41
+ def initialize
42
+ @lines = []
43
+ end
44
+
45
+ def line(liquid_line)
46
+ @lines << liquid_line
47
+ nil
48
+ end
49
+
50
+ def to_s
51
+ @lines.join("\n").html_safe
52
+ end
53
+
54
+ def render_liquid(name, **assigns)
55
+ assigns_liquid = ", #{liquid_arguments(assigns)}" unless assigns.blank?
56
+ line "{% render #{name.to_s.to_json}#{assigns_liquid} %}"
57
+ end
58
+
59
+ def assign_liquid(variable, value)
60
+ LiquidVariable.validate_name!(variable)
61
+
62
+ if value.is_a?(Hash)
63
+ value = "null | default: #{liquid_arguments(value, variable)}"
64
+ else
65
+ value = liquid_value(value, variable)
66
+ end
67
+
68
+ line "{% assign #{variable} = #{value} %}"
69
+ end
70
+
71
+ def liquid_arguments(assigns, _base_name = nil)
72
+ assigns.map { |k, v| "#{k}: #{liquid_value(v, k.to_s)}" }.join(", ")
73
+ end
74
+
75
+ def liquid_value(value, variable_name = nil)
76
+ if value.respond_to?(:as_liquid)
77
+ value = value.as_liquid
78
+ elsif value.respond_to?(:serializable_hash)
79
+ value = value.serializable_hash
80
+ end
81
+
82
+ value = case value
83
+ when String, Numeric, TrueClass, FalseClass, NilClass, Date, Time, Symbol
84
+ value.to_json
85
+ when LiquidVariable
86
+ value.name
87
+ when Hash
88
+ assign_name = LiquidVariable.random("h_#{variable_name}").name
89
+ assign_liquid(assign_name, value)
90
+ assign_name
91
+ else
92
+ raise UnsupportedValueError.new(value, variable_name)
93
+ end
94
+ value.html_safe
95
+ end
96
+ end
97
+
98
+ %i[render_liquid assign_liquid].each do |method_name|
99
+ define_method method_name do |*args, **kwargs|
100
+ LiquidBuffer.new.tap { _1.send(method_name, *args, **kwargs) }.to_s
101
+ end
102
+ end
103
+
104
+ def liquid_capture(name, &block)
105
+ safe_join(["{% capture #{name} %}".html_safe, capture(&block).html_safe, "{% endcapture %}".html_safe])
106
+ end
107
+
108
+ def liquid_variable(name)
109
+ LiquidBuffer::LiquidVariable.new(name)
110
+ end
111
+ end
112
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "liquid_response/version"
4
+ require "liquid_response/errors"
5
+ require "liquid_response/core_ext"
6
+ require "liquid_response/view_helper"
7
+ require "liquid_response/controller_helper"
8
+
9
+ module LiquidResponse
10
+ def self.register_liquid_mime_type!
11
+ return if Mime::Type.lookup_by_extension(:liquid)
12
+
13
+ Mime::Type.register "application/liquid", :liquid
14
+ end
15
+ end
@@ -0,0 +1,4 @@
1
+ module LiquidResponse
2
+ VERSION: String
3
+ # See the writing guide of rbs: https://github.com/ruby/rbs#guides
4
+ end
metadata ADDED
@@ -0,0 +1,84 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: liquid_response
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Elia Schito
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: railties
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - ">="
17
+ - !ruby/object:Gem::Version
18
+ version: '7.0'
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - ">="
24
+ - !ruby/object:Gem::Version
25
+ version: '7.0'
26
+ - !ruby/object:Gem::Dependency
27
+ name: activesupport
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - ">="
31
+ - !ruby/object:Gem::Version
32
+ version: '7.0'
33
+ type: :runtime
34
+ prerelease: false
35
+ version_requirements: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - ">="
38
+ - !ruby/object:Gem::Version
39
+ version: '7.0'
40
+ description: LiquidResponse provides Rails controller and view helpers for rendering
41
+ Liquid templates, designed for use with Shopify App Proxy and similar setups where
42
+ Rails serves Liquid markup to a storefront.
43
+ email:
44
+ - elia@schito.me
45
+ executables: []
46
+ extensions: []
47
+ extra_rdoc_files: []
48
+ files:
49
+ - LICENSE.txt
50
+ - README.md
51
+ - Rakefile
52
+ - lib/liquid_response.rb
53
+ - lib/liquid_response/controller_helper.rb
54
+ - lib/liquid_response/core_ext.rb
55
+ - lib/liquid_response/errors.rb
56
+ - lib/liquid_response/version.rb
57
+ - lib/liquid_response/view_helper.rb
58
+ - sig/liquid_response.rbs
59
+ homepage: https://github.com/elia/liquid_response
60
+ licenses:
61
+ - MIT
62
+ metadata:
63
+ allowed_push_host: https://rubygems.org
64
+ homepage_uri: https://github.com/elia/liquid_response
65
+ source_code_uri: https://github.com/elia/liquid_response
66
+ changelog_uri: https://github.com/nebulab/liquid_response/releases
67
+ rdoc_options: []
68
+ require_paths:
69
+ - lib
70
+ required_ruby_version: !ruby/object:Gem::Requirement
71
+ requirements:
72
+ - - ">="
73
+ - !ruby/object:Gem::Version
74
+ version: 3.2.0
75
+ required_rubygems_version: !ruby/object:Gem::Requirement
76
+ requirements:
77
+ - - ">="
78
+ - !ruby/object:Gem::Version
79
+ version: '0'
80
+ requirements: []
81
+ rubygems_version: 4.0.10
82
+ specification_version: 4
83
+ summary: Render Liquid templates from Rails controllers and views
84
+ test_files: []