Module: Upkeep::Rails::Replay

Defined in:
lib/upkeep/rails/replay.rb

Defined Under Namespace

Classes: RackSession

Class Method Summary collapse

Class Method Details

.application_env_configObject



133
134
135
136
137
138
# File 'lib/upkeep/rails/replay.rb', line 133

def application_env_config
  return {} unless defined?(::Rails) && ::Rails.respond_to?(:application)

  application = ::Rails.application
  application.respond_to?(:env_config) ? application.env_config : {}
end

.collect_response_body(body) ⇒ Object



236
237
238
239
240
# File 'lib/upkeep/rails/replay.rb', line 236

def collect_response_body(body)
  body.each.to_a.join
ensure
  body.close if body.respond_to?(:close)
end

.constantize(name) ⇒ Object



232
233
234
# File 'lib/upkeep/rails/replay.rb', line 232

def constantize(name)
  name.to_s.split("::").reduce(Object) { |namespace, constant_name| namespace.const_get(constant_name) }
end

.inferred_local_name(partial) ⇒ Object



228
229
230
# File 'lib/upkeep/rails/replay.rb', line 228

def inferred_local_name(partial)
  File.basename(partial.to_s).sub(/\A_/, "").to_sym
end

.partial_path(template) ⇒ Object



224
225
226
# File 'lib/upkeep/rails/replay.rb', line 224

def partial_path(template)
  template.to_s.sub(%r{(^|/)_([^/]+)\z}, "\\1\\2")
end

.rack_env(env) ⇒ Object



124
125
126
127
128
129
130
131
# File 'lib/upkeep/rails/replay.rb', line 124

def rack_env(env)
  env = application_env_config.merge(
    env.each_with_object({}) { |(key, value), copy| copy[key.to_s] = revive_env_value(value) }
  )
  env["rack.input"] = StringIO.new
  env["rack.errors"] ||= StringIO.new
  env
end

.render(recipe) ⇒ Object



22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
# File 'lib/upkeep/rails/replay.rb', line 22

def render(recipe)
  replay = recipe.replay

  case replay
  when ::Upkeep::Replay::ControllerPage
    render_controller_page(replay)
  when ::Upkeep::Replay::ControllerTurboFrame
    render_controller_turbo_frame(replay)
  when ::Upkeep::Replay::Template
    render_template(replay)
  when ::Upkeep::Replay::Fragment
    render_fragment(replay)
  when ::Upkeep::Replay::Collection
    render_collection(replay)
  when ::Upkeep::Replay::CollectionMember
    render_collection_member(replay)
  else
    raise "unknown Rails replay recipe type: #{replay.class.name}"
  end
end

.render_collection(replay) ⇒ Object



88
89
90
91
92
93
94
95
96
97
98
99
100
101
# File 'lib/upkeep/rails/replay.rb', line 88

def render_collection(replay)
  template_context = replay_template_context
  options = revive_hash(replay.options, template: template_context)
  collection = revive_value(replay.collection, template: template_context)

  if replay.derived_partial?
    renderer_for(replay).render(collection)
  else
    renderer_for(replay).render(options.merge(
      partial: replay.partial,
      collection: collection
    ))
  end
end

.render_collection_member(replay) ⇒ Object



103
104
105
106
107
108
109
110
111
112
113
114
# File 'lib/upkeep/rails/replay.rb', line 103

def render_collection_member(replay)
  template_context = replay_template_context
  options = revive_hash(replay.options, template: template_context)
  record = revive_value(replay.record, template: template_context)
  locals = options.fetch(:locals, {})
  local_name = (options[:as] || inferred_local_name(replay.partial)).to_sym

  renderer_for(replay).render(
    partial: replay.partial,
    locals: locals.merge(local_name => record)
  )
end

.render_controller_page(replay, env: replay.env) ⇒ Object



43
44
45
46
47
48
49
# File 'lib/upkeep/rails/replay.rb', line 43

def render_controller_page(replay, env: replay.env)
  controller = constantize(replay.controller_class)
  _status, _headers, body = ControllerRuntime.suppress do
    controller.action(replay.action).call(rack_env(env))
  end
  collect_response_body(body)
end

.render_controller_turbo_frame(replay) ⇒ Object



51
52
53
54
55
56
57
58
59
60
61
# File 'lib/upkeep/rails/replay.rb', line 51

def render_controller_turbo_frame(replay)
  html = render_controller_page(
    replay,
    env: replay.env.merge("HTTP_TURBO_FRAME" => replay.target_id)
  )
  target = Targeting::Target.new("turbo_frame", replay.target_id, "Turbo Frame replay")
  frame_html = Targeting::Extraction.extract_target_html(html, target)
  frame = Nokogiri::HTML5.fragment(frame_html).at_css("turbo-frame")
  frame["src"] = turbo_frame_src(replay.env)
  frame.to_html
end

.render_fragment(replay) ⇒ Object



79
80
81
82
83
84
85
86
# File 'lib/upkeep/rails/replay.rb', line 79

def render_fragment(replay)
  template_context = replay_template_context
  renderer_for(replay).render(
    partial: partial_path(replay.template),
    locals: revive_hash(replay.locals, template: template_context),
    assigns: revive_hash(replay.assigns, template: template_context)
  )
end

.render_template(replay) ⇒ Object



70
71
72
73
74
75
76
77
# File 'lib/upkeep/rails/replay.rb', line 70

def render_template(replay)
  template_context = replay_template_context
  renderer_for(replay).render(
    template: replay.template,
    locals: revive_hash(replay.locals, template: template_context),
    assigns: revive_hash(replay.assigns, template: template_context)
  )
end

.renderer_for(replay) ⇒ Object



116
117
118
119
120
121
122
# File 'lib/upkeep/rails/replay.rb', line 116

def renderer_for(replay)
  if replay.controller_class
    constantize(replay.controller_class).renderer
  else
    ::ActionController::Base.renderer
  end
end

.replay_session_snapshot?(value) ⇒ Boolean

Returns:

  • (Boolean)


196
197
198
199
200
201
# File 'lib/upkeep/rails/replay.rb', line 196

def replay_session_snapshot?(value)
  return false unless value.is_a?(Hash)

  type = value["__upkeep_replay_type"] || value[:__upkeep_replay_type]
  type == "rack_session"
end

.replay_template_contextObject



178
179
180
# File 'lib/upkeep/rails/replay.rb', line 178

def replay_template_context
  ::ActionView::Base.empty
end

.replay_warden_snapshot?(value) ⇒ Boolean

Returns:

  • (Boolean)


208
209
210
211
212
213
# File 'lib/upkeep/rails/replay.rb', line 208

def replay_warden_snapshot?(value)
  return false unless value.is_a?(Hash)

  type = value["__upkeep_replay_type"] || value[:__upkeep_replay_type]
  type == "warden"
end

.revive_env_value(value) ⇒ Object



182
183
184
185
186
187
188
189
190
191
192
193
194
# File 'lib/upkeep/rails/replay.rb', line 182

def revive_env_value(value)
  return revive_replay_session(value) if replay_session_snapshot?(value)
  return revive_replay_warden(value) if replay_warden_snapshot?(value)

  case value
  when Hash
    value.transform_values { |nested_value| revive_env_value(nested_value) }
  when Array
    value.map { |nested_value| revive_env_value(nested_value) }
  else
    value
  end
end

.revive_form_builder(snapshot, template:) ⇒ Object



169
170
171
172
173
174
175
176
# File 'lib/upkeep/rails/replay.rb', line 169

def revive_form_builder(snapshot, template:)
  constantize(snapshot.builder_class).new(
    snapshot.object_name,
    revive_value(snapshot.object, template: template),
    template,
    revive_hash(snapshot.options, template: template)
  )
end

.revive_hash(values, template: replay_template_context) ⇒ Object



140
141
142
143
144
145
146
# File 'lib/upkeep/rails/replay.rb', line 140

def revive_hash(values, template: replay_template_context)
  values = values.entries if values.is_a?(::Upkeep::Replay::HashValue)

  values.each_with_object({}) do |(key, value), revived|
    revived[key.to_sym] = revive_value(value, template: template)
  end
end

.revive_replay_session(snapshot) ⇒ Object



203
204
205
206
# File 'lib/upkeep/rails/replay.rb', line 203

def revive_replay_session(snapshot)
  values = snapshot["values"] || snapshot[:values] || {}
  RackSession.new(revive_env_value(values))
end

.revive_replay_warden(snapshot) ⇒ Object



215
216
217
218
219
220
221
222
# File 'lib/upkeep/rails/replay.rb', line 215

def revive_replay_warden(snapshot)
  users = snapshot["users"] || snapshot[:users] || {}
  revived_users = users.to_h do |scope, user_snapshot|
    value = ::Upkeep::Replay::Value.from_h(user_snapshot)
    [scope, revive_value(value)]
  end
  Runtime::ObservedWarden.new(revived_users)
end

.revive_value(snapshot, template: replay_template_context) ⇒ Object



148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
# File 'lib/upkeep/rails/replay.rb', line 148

def revive_value(snapshot, template: replay_template_context)
  snapshot = ::Upkeep::Replay.value(snapshot)

  case snapshot
  when ::Upkeep::Replay::ActiveRecordValue
    constantize(snapshot.model).find(snapshot.id)
  when ::Upkeep::Replay::ActiveRecordRelationValue
    constantize(snapshot.model).find_by_sql(snapshot.sql)
  when ::Upkeep::Replay::ArrayValue
    snapshot.items.map { |item| revive_value(item, template: template) }
  when ::Upkeep::Replay::HashValue
    revive_hash(snapshot.entries, template: template)
  when ::Upkeep::Replay::LiteralValue
    snapshot.value
  when ::Upkeep::Replay::RailsFormBuilderValue
    revive_form_builder(snapshot, template: template)
  else
    raise "unsupported Rails replay value type: #{snapshot.class.name}"
  end
end

.turbo_frame_src(env) ⇒ Object



63
64
65
66
67
68
# File 'lib/upkeep/rails/replay.rb', line 63

def turbo_frame_src(env)
  env = env.transform_keys(&:to_s)
  path = "#{env["SCRIPT_NAME"]}#{env["PATH_INFO"]}"
  query = env["QUERY_STRING"].to_s
  query.empty? ? path : "#{path}?#{query}"
end