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



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

Raises:

  • (ArgumentError)


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

Raises:

  • (ArgumentError)


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_variantObject



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_modulesObject



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

Returns:

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

Returns:

  • (Boolean)

Raises:

  • (ArgumentError)


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

Returns:

  • (Boolean)

Raises:

  • (ArgumentError)


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

Returns:

  • (Boolean)

Raises:

  • (ArgumentError)


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

Returns:

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



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.

Returns:

  • (Boolean)


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_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.



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_streamObject

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_vocabularyObject

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

#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.



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



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

def render_to_string(*)
  expo_turbo_scoped_demand { super }
end

#turbo_streamObject

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