Module: ExpoTurbo::Rails::Controller
- Extended by:
- ActiveSupport::Concern
- Includes:
- ActionController::Helpers, ActionController::ImplicitRender, ActionView::Rendering
- Defined in:
- lib/expo_turbo/rails/controller.rb
Constant Summary collapse
- VARY_DIMENSIONS =
Keep the legacy dimension while the 0.3 gem reads the 0.2 modules header. Removing it now would let two old clients share one response.
["Accept", "Turbo-Frame", "X-Expo-Turbo-Client", "X-Expo-Turbo-Modules"].freeze
Instance Method Summary collapse
- #broadcast_expo_turbo_refresh_later_to(*streamables, request_id: ::Turbo.current_request_id, **attributes) ⇒ Object
- #broadcast_expo_turbo_refresh_to(*streamables, request_id: ::Turbo.current_request_id, **attributes) ⇒ Object
- #broadcast_expo_turbo_stream_later_to(*streamables, content: nil) ⇒ Object
- #broadcast_expo_turbo_stream_to(*streamables, content: nil) ⇒ Object
- #expo_turbo_cache_key(*keys) ⇒ Object
- #expo_turbo_cache_variant ⇒ Object
- #expo_turbo_client_modules ⇒ Object
- #expo_turbo_client_revision_satisfies?(requirement) ⇒ Boolean
- #expo_turbo_client_supports?(module_name, requirement) ⇒ Boolean
- #expo_turbo_client_supports_attribute?(tag, attribute) ⇒ Boolean
- #expo_turbo_client_supports_component?(tag) ⇒ Boolean
- #expo_turbo_frame_request? ⇒ Boolean
- #expo_turbo_frame_request_id ⇒ Object
-
#expo_turbo_reject_invalid_frame_request! ⇒ Object
A malformed Frame id must not become a document request.
-
#expo_turbo_request? ⇒ Boolean
True only when the request names the Expo Turbo media type exactly.
-
#expo_turbo_selected_format ⇒ Object
The format this render answers in, and the one rule that decides both the media type and whether a helper takes its Expo Turbo branch.
-
#expo_turbo_stream ⇒ Object
Always Expo Turbo.
-
#expo_turbo_vary! ⇒ Object
Applied to every response, not only to a request that already carries a Frame header: a shared cache can receive a Frame request for the same URL later.
-
#expo_turbo_vocabulary ⇒ Object
Reports which vocabulary answered this request: :declared, :assumed_latest, or :assumed_none.
-
#formats=(values) ⇒ Object
Both framework assignments arrive here, because ActionView::ViewPaths delegates the writer to the lookup context and this concern sits above it.
-
#render ⇒ Object
A demand belongs to the render that carried it and to nothing after it.
-
#render_to_body(options = {}) ⇒ Object
ActionView::Rendering renders a template for every option, and this concern adds it to API controllers, where it would otherwise hide the registered renderers such as
render turbo_stream:. - #render_to_string ⇒ Object
-
#turbo_stream ⇒ Object
Format-aware in a request, exactly as in the view.
Instance Method Details
#broadcast_expo_turbo_refresh_later_to(*streamables, request_id: ::Turbo.current_request_id, **attributes) ⇒ Object
562 563 564 |
# File 'lib/expo_turbo/rails/controller.rb', line 562 def broadcast_expo_turbo_refresh_later_to(*streamables, request_id: ::Turbo.current_request_id, **attributes) ExpoTurbo::Rails::Streams.broadcast_refresh_later_to(*streamables, request_id:, **attributes) end |
#broadcast_expo_turbo_refresh_to(*streamables, request_id: ::Turbo.current_request_id, **attributes) ⇒ Object
558 559 560 |
# File 'lib/expo_turbo/rails/controller.rb', line 558 def broadcast_expo_turbo_refresh_to(*streamables, request_id: ::Turbo.current_request_id, **attributes) ExpoTurbo::Rails::Streams.broadcast_refresh_to(*streamables, request_id:, **attributes) end |
#broadcast_expo_turbo_stream_later_to(*streamables, content: nil) ⇒ Object
550 551 552 553 554 555 556 |
# File 'lib/expo_turbo/rails/controller.rb', line 550 def broadcast_expo_turbo_stream_later_to(*streamables, content: nil) raise ArgumentError, "provide content or a block, not both" if block_given? && !content.nil? content = yield(expo_turbo_stream) if block_given? expo_turbo_validate_broadcast_stream!(content) ExpoTurbo::Rails::Streams.broadcast_later_to(*streamables, content: content) end |
#broadcast_expo_turbo_stream_to(*streamables, content: nil) ⇒ Object
542 543 544 545 546 547 548 |
# File 'lib/expo_turbo/rails/controller.rb', line 542 def broadcast_expo_turbo_stream_to(*streamables, content: nil) raise ArgumentError, "provide content or a block, not both" if block_given? && !content.nil? content = yield(expo_turbo_stream) if block_given? expo_turbo_validate_broadcast_stream!(content) ExpoTurbo::Rails::Streams.broadcast_to(*streamables, content: content) end |
#expo_turbo_cache_key(*keys) ⇒ Object
240 241 242 243 |
# File 'lib/expo_turbo/rails/controller.rb', line 240 def expo_turbo_cache_key(*keys) expo_turbo_vary! [*keys, *expo_turbo_cache_variant] end |
#expo_turbo_cache_variant ⇒ Object
214 215 216 217 218 219 220 |
# File 'lib/expo_turbo/rails/controller.rb', line 214 def expo_turbo_cache_variant [ *Frames.cache_variant(expo_turbo_frame_request_id), :modules, expo_turbo_module_negotiation.fetch(:cache_variant) ] end |
#expo_turbo_client_modules ⇒ Object
151 152 153 |
# File 'lib/expo_turbo/rails/controller.rb', line 151 def expo_turbo_client_modules expo_turbo_module_negotiation.fetch(:modules) end |
#expo_turbo_client_revision_satisfies?(requirement) ⇒ Boolean
182 183 184 185 186 187 188 189 190 191 |
# File 'lib/expo_turbo/rails/controller.rb', line 182 def expo_turbo_client_revision_satisfies?(requirement) parsed_requirement = expo_turbo_parse_requirement(requirement) negotiation = expo_turbo_module_negotiation return true if negotiation.fetch(:latest) entry = negotiation[:entry] return false unless entry parsed_requirement.satisfied_by?(Gem::Version.new(entry.revision.to_s)) end |
#expo_turbo_client_supports?(module_name, requirement) ⇒ Boolean
161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 |
# File 'lib/expo_turbo/rails/controller.rb', line 161 def expo_turbo_client_supports?(module_name, requirement) raise ArgumentError, "module_name must be a String" unless module_name.is_a?(String) raise ArgumentError, "module_name must not be blank" if module_name.blank? raise ArgumentError, "requirement must be a String" unless requirement.is_a?(String) raise ArgumentError, "requirement must not be empty" if requirement.strip.empty? parsed_requirement = expo_turbo_parse_requirement(requirement) negotiation = expo_turbo_module_negotiation return true if negotiation.fetch(:latest) if negotiation[:entry] raise ArgumentError, "module-scoped requirements cannot test a vocabulary revision; use expo_turbo_client_revision_satisfies?" end version = negotiation.fetch(:modules)[module_name] return false unless version parsed_requirement.satisfied_by?(Gem::Version.new(version)) end |
#expo_turbo_client_supports_attribute?(tag, attribute) ⇒ Boolean
202 203 204 205 206 207 208 209 210 211 212 |
# File 'lib/expo_turbo/rails/controller.rb', line 202 def expo_turbo_client_supports_attribute?(tag, attribute) raise ArgumentError, "tag must be a nonblank String" unless tag.is_a?(String) && tag.present? unless attribute.is_a?(String) && attribute.present? raise ArgumentError, "attribute must be a nonblank String" end negotiation = expo_turbo_module_negotiation return true if negotiation.fetch(:latest) negotiation[:entry]&.supports_attribute?(tag, attribute) || false end |
#expo_turbo_client_supports_component?(tag) ⇒ Boolean
193 194 195 196 197 198 199 200 |
# File 'lib/expo_turbo/rails/controller.rb', line 193 def expo_turbo_client_supports_component?(tag) raise ArgumentError, "tag must be a nonblank String" unless tag.is_a?(String) && tag.present? negotiation = expo_turbo_module_negotiation return true if negotiation.fetch(:latest) negotiation[:entry]&.supports_component?(tag) || false end |
#expo_turbo_frame_request? ⇒ Boolean
126 127 128 |
# File 'lib/expo_turbo/rails/controller.rb', line 126 def expo_turbo_frame_request? expo_turbo_frame_request_id.present? end |
#expo_turbo_frame_request_id ⇒ Object
130 131 132 133 |
# File 'lib/expo_turbo/rails/controller.rb', line 130 def expo_turbo_frame_request_id frame_id = expo_turbo_frame_header Frames.valid_id?(frame_id) ? frame_id : nil end |
#expo_turbo_reject_invalid_frame_request! ⇒ Object
A malformed Frame id must not become a document request. That failure is silent: the client asked for one representation and receives another one.
138 139 140 141 142 143 |
# File 'lib/expo_turbo/rails/controller.rb', line 138 def expo_turbo_reject_invalid_frame_request! frame_id = expo_turbo_frame_header return if frame_id.nil? || Frames.valid_id?(frame_id) head :bad_request end |
#expo_turbo_request? ⇒ Boolean
True only when the request names the Expo Turbo media type exactly. A wildcard Accept value is not proof of a native client.
147 148 149 |
# File 'lib/expo_turbo/rails/controller.rb', line 147 def expo_turbo_request? MediaType.explicitly_accepted?(request.get_header("HTTP_ACCEPT")) end |
#expo_turbo_selected_format ⇒ Object
The format this render answers in, and the one rule that decides both the media type and whether a helper takes its Expo Turbo branch. Two sources can name it, and they do not rank equally:
- resolved: Rails worked it out, from the Accept header in ActionController::Rendering#process_action or from the respond_to branch that matched.
- demanded: the caller wrote it, as
render ..., formats: [...].
A demand wins. Naming a format is a decision, and answering render formats: [:html] with Expo Turbo XML because the client happened to
send a native Accept header overrules the one party that said what it
wanted. A demand lasts only for the render that carried it.
Neither source is lookup_context.formats.first during a render. ActionView prepends the format of the template that answered, so a shared .html template rewrites the lookup context to :html while still answering a native request. nil when nothing named a format, such as a broadcast rendered through ApplicationController.render.
86 87 88 |
# File 'lib/expo_turbo/rails/controller.rb', line 86 def expo_turbo_selected_format @expo_turbo_demanded_format || @expo_turbo_resolved_format end |
#expo_turbo_stream ⇒ Object
Always Expo Turbo. Broadcasts have no request and therefore no format, so they need a builder that does not depend on one.
252 253 254 |
# File 'lib/expo_turbo/rails/controller.rb', line 252 def expo_turbo_stream view_context.expo_turbo_stream end |
#expo_turbo_vary! ⇒ Object
Applied to every response, not only to a request that already carries a Frame header: a shared cache can receive a Frame request for the same URL later. Accept is included even when the route forced the format, because the vocabulary decision reads Accept.
230 231 232 233 234 235 236 237 238 |
# File 'lib/expo_turbo/rails/controller.rb', line 230 def expo_turbo_vary! values = response.headers["Vary"].to_s.split(",").map(&:strip).reject(&:blank?) return response.headers["Vary"] if values.include?("*") VARY_DIMENSIONS.each do |dimension| values << dimension if values.none? { |value| value.casecmp?(dimension) } end response.set_header "Vary", values.join(", ") end |
#expo_turbo_vocabulary ⇒ Object
Reports which vocabulary answered this request: :declared, :assumed_latest, or :assumed_none.
157 158 159 |
# File 'lib/expo_turbo/rails/controller.rb', line 157 def expo_turbo_vocabulary expo_turbo_module_negotiation.fetch(:vocabulary) end |
#formats=(values) ⇒ Object
Both framework assignments arrive here, because ActionView::ViewPaths delegates the writer to the lookup context and this concern sits above it. Appending :html lets one template serve both audiences; the Expo Turbo format stays first, so an .expo_turbo template always wins over the .html template beside it.
95 96 97 98 99 |
# File 'lib/expo_turbo/rails/controller.rb', line 95 def formats=(values) values = expo_turbo_lookup_formats(values) @expo_turbo_resolved_format = Array(values).first super end |
#render ⇒ Object
A demand belongs to the render that carried it and to nothing after it. Both entry points restore what was in force, so a helper called between two renders, or after a render_to_string, is already back on the resolved format rather than on the last format anyone named.
105 106 107 |
# File 'lib/expo_turbo/rails/controller.rb', line 105 def render(*) expo_turbo_scoped_demand { super } end |
#render_to_body(options = {}) ⇒ Object
ActionView::Rendering renders a template for every option, and this
concern adds it to API controllers, where it would otherwise hide the
registered renderers such as render turbo_stream:. Ask the renderers
first, exactly as ActionController::Base orders them.
117 118 119 120 121 122 123 124 |
# File 'lib/expo_turbo/rails/controller.rb', line 117 def render_to_body( = {}) if respond_to?(:_render_to_body_with_renderer, true) rendered = _render_to_body_with_renderer() return rendered if rendered end super end |
#render_to_string ⇒ Object
109 110 111 |
# File 'lib/expo_turbo/rails/controller.rb', line 109 def render_to_string(*) expo_turbo_scoped_demand { super } end |
#turbo_stream ⇒ Object
Format-aware in a request, exactly as in the view.
246 247 248 |
# File 'lib/expo_turbo/rails/controller.rb', line 246 def turbo_stream view_context.turbo_stream end |