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

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

Raises:

  • (ArgumentError)


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

Raises:

  • (ArgumentError)


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_variantObject



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_modulesObject



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

Returns:

  • (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

Returns:

  • (Boolean)

Raises:

  • (ArgumentError)


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

Returns:

  • (Boolean)

Raises:

  • (ArgumentError)


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

Returns:

  • (Boolean)

Raises:

  • (ArgumentError)


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

Returns:

  • (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_idObject



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.

Returns:

  • (Boolean)


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_formatObject

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_streamObject

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_vocabularyObject

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

#renderObject

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(options = {})
  if respond_to?(:_render_to_body_with_renderer, true)
    rendered = _render_to_body_with_renderer(options)
    return rendered if rendered
  end

  super
end

#render_to_stringObject



109
110
111
# File 'lib/expo_turbo/rails/controller.rb', line 109

def render_to_string(*)
  expo_turbo_scoped_demand { super }
end

#turbo_streamObject

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