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
563 564 565 |
# File 'lib/expo_turbo/rails/controller.rb', line 563 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
559 560 561 |
# File 'lib/expo_turbo/rails/controller.rb', line 559 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
551 552 553 554 555 556 557 |
# File 'lib/expo_turbo/rails/controller.rb', line 551 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
543 544 545 546 547 548 549 |
# File 'lib/expo_turbo/rails/controller.rb', line 543 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
239 240 241 242 |
# File 'lib/expo_turbo/rails/controller.rb', line 239 def expo_turbo_cache_key(*keys) expo_turbo_vary! [*keys, *expo_turbo_cache_variant] end |
#expo_turbo_cache_variant ⇒ Object
213 214 215 216 217 218 219 |
# File 'lib/expo_turbo/rails/controller.rb', line 213 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
150 151 152 |
# File 'lib/expo_turbo/rails/controller.rb', line 150 def expo_turbo_client_modules expo_turbo_module_negotiation.fetch(:modules) end |
#expo_turbo_client_revision_satisfies?(requirement) ⇒ Boolean
181 182 183 184 185 186 187 188 189 190 |
# File 'lib/expo_turbo/rails/controller.rb', line 181 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
160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 |
# File 'lib/expo_turbo/rails/controller.rb', line 160 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
201 202 203 204 205 206 207 208 209 210 211 |
# File 'lib/expo_turbo/rails/controller.rb', line 201 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
192 193 194 195 196 197 198 199 |
# File 'lib/expo_turbo/rails/controller.rb', line 192 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
125 126 127 |
# File 'lib/expo_turbo/rails/controller.rb', line 125 def expo_turbo_frame_request? expo_turbo_frame_request_id.present? end |
#expo_turbo_frame_request_id ⇒ Object
129 130 131 132 |
# File 'lib/expo_turbo/rails/controller.rb', line 129 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.
137 138 139 140 141 142 |
# File 'lib/expo_turbo/rails/controller.rb', line 137 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.
146 147 148 |
# File 'lib/expo_turbo/rails/controller.rb', line 146 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.
85 86 87 |
# File 'lib/expo_turbo/rails/controller.rb', line 85 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.
251 252 253 |
# File 'lib/expo_turbo/rails/controller.rb', line 251 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.
229 230 231 232 233 234 235 236 237 |
# File 'lib/expo_turbo/rails/controller.rb', line 229 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.
156 157 158 |
# File 'lib/expo_turbo/rails/controller.rb', line 156 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.
94 95 96 97 98 |
# File 'lib/expo_turbo/rails/controller.rb', line 94 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.
104 105 106 |
# File 'lib/expo_turbo/rails/controller.rb', line 104 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.
116 117 118 119 120 121 122 123 |
# File 'lib/expo_turbo/rails/controller.rb', line 116 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
108 109 110 |
# File 'lib/expo_turbo/rails/controller.rb', line 108 def render_to_string(*) expo_turbo_scoped_demand { super } end |
#turbo_stream ⇒ Object
Format-aware in a request, exactly as in the view.
245 246 247 |
# File 'lib/expo_turbo/rails/controller.rb', line 245 def turbo_stream view_context.turbo_stream end |