Module: CrudComponents::RouteResolver

Defined in:
lib/crud_components/route_resolver.rb

Overview

Resolves derived actions and record links to routes: the most specific conventional route first (association-scoped when the collection came from an association), then the top-level route, then nil — a nil means the button/link is omitted, never broken.

Class Method Summary collapse

Class Method Details

.action_path(view, action, record: nil, model: nil, owner: nil) ⇒ Object



9
10
11
12
13
14
15
16
17
# File 'lib/crud_components/route_resolver.rb', line 9

def action_path(view, action, record: nil, model: nil, owner: nil)
  return safe_block(view, record || model, action.path_block) if action.path_block

  if action.collection?
    collection_path(view, action, model, owner)
  else
    member_path(view, action, record, owner)
  end
end

.app_path(view, record) ⇒ Object

The host application's own page for a record — the same candidate logic, resolved against main_app instead of the current route set. A declared app_path block wins. nil when nothing resolves.



22
23
24
25
26
27
28
29
30
31
# File 'lib/crud_components/route_resolver.rb', line 22

def app_path(view, record)
  structure = Structure.for(record.class)
  if (block = structure.app_path_block)
    return safe_block(view, record, block)
  end

  return nil unless view.respond_to?(:main_app)

  try_helpers(view.main_app, member_candidates(nil, record, nil))
end

.collection_index_path(view, target, owner, assoc_name) ⇒ Object

The index a has_many "+n more" link points at:

1. the nested index under the owner (publisher_books_path(publisher)),
2. else the target's index filtered by the owner — but ONLY when the
 target actually has a filterable belongs_to back to the owner
 (publisher→books works; a habtm like author↔books does not, so we
 do not emit a link that would silently show everything),
3. else nil (the renderer shows "+n more" as plain text).

assoc_name is the owner's reflection name (e.g. :books).



57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
# File 'lib/crud_components/route_resolver.rb', line 57

def collection_index_path(view, target, owner, assoc_name)
  return nil unless owner

  key = target.model_name.route_key
  owner_key = owner.model_name.singular_route_key
  nested = "#{owner_key}_#{key}_path"
  return safe_url(view, nested, owner) if view.respond_to?(nested)

  flat = "#{key}_path"
  return nil unless view.respond_to?(flat)

  filter = inverse_filter(target, owner, assoc_name)
  return nil unless filter

  safe_url(view, flat, **{ filter[:param] => filter[:value] })
end

.collection_path(view, action, model, owner) ⇒ Object



122
123
124
125
126
127
128
129
# File 'lib/crud_components/route_resolver.rb', line 122

def collection_path(view, action, model, owner)
  prefix = action.name == :new ? 'new_' : "#{action.name}_"
  key = action.name == :new ? model.model_name.singular_route_key : model.model_name.route_key
  candidates = []
  candidates << ["#{prefix}#{owner.model_name.singular_route_key}_#{key}_path", [owner]] if owner
  candidates << ["#{prefix}#{key}_path", []]
  try_helpers(view, candidates)
end

.inverse_filter(target, owner, assoc_name) ⇒ Object

The target's belongs_to that mirrors the owner's collection (matched by foreign key), if it is filterable — with the owner's identify_by value.



76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
# File 'lib/crud_components/route_resolver.rb', line 76

def inverse_filter(target, owner, assoc_name)
  owner_reflection = owner.class.reflect_on_association(assoc_name)
  return nil unless owner_reflection&.foreign_key

  fk = owner_reflection.foreign_key.to_s
  inverse = target.reflect_on_all_associations(:belongs_to).find { |r| r.foreign_key.to_s == fk }
  return nil unless inverse

  field = Structure.for(target).field(inverse.name)
  return nil unless field.filterable?

  identify = Structure.for(owner.class).identify_by
  { param: inverse.name, value: owner.public_send(identify) }
rescue CrudComponents::DefinitionError
  nil
end

.member_candidates(prefix, record, owner) ⇒ Object



131
132
133
134
135
136
137
# File 'lib/crud_components/route_resolver.rb', line 131

def member_candidates(prefix, record, owner)
  key = record.model_name.singular_route_key
  candidates = []
  candidates << ["#{prefix}#{owner.model_name.singular_route_key}_#{key}_path", [owner, record]] if owner
  candidates << ["#{prefix}#{key}_path", [record]]
  candidates
end

.member_path(view, action, record, owner) ⇒ Object



117
118
119
120
# File 'lib/crud_components/route_resolver.rb', line 117

def member_path(view, action, record, owner)
  prefix = { show: nil, destroy: nil, edit: 'edit_' }.fetch(action.name, "#{action.name}_")
  try_helpers(view, member_candidates(prefix, record, owner))
end

.missing_route_helper?(error) ⇒ Boolean

Returns:

  • (Boolean)


107
108
109
# File 'lib/crud_components/route_resolver.rb', line 107

def missing_route_helper?(error)
  error.name.to_s.end_with?('_path', '_url')
end

.record_path(view, record, owner: nil) ⇒ Object

The plain link to a record (label cells, association cells): show route, then edit. Returns [path, kind] or nil.



35
36
37
38
39
40
41
# File 'lib/crud_components/route_resolver.rb', line 35

def record_path(view, record, owner: nil)
  path = try_helpers(view, member_candidates(nil, record, owner))
  return [path, :show] if path

  path = try_helpers(view, member_candidates('edit_', record, owner))
  path ? [path, :edit] : nil
end

.safe_block(view, subject, block) ⇒ Object

A path block naming a route helper that does not exist here — the same page rendered under a mount that lacks it — yields nil, so the button is omitted. Anything else the block gets wrong is the app's own error and stays loud.



97
98
99
100
101
102
103
104
105
# File 'lib/crud_components/route_resolver.rb', line 97

def safe_block(view, subject, block)
  view.instance_exec(subject, &block)
rescue ActionController::UrlGenerationError
  nil
rescue NameError => e
  raise unless missing_route_helper?(e)

  nil
end

.safe_url(view, helper, *args, **kwargs) ⇒ Object



111
112
113
114
115
# File 'lib/crud_components/route_resolver.rb', line 111

def safe_url(view, helper, *args, **kwargs)
  kwargs.empty? ? view.public_send(helper, *args) : view.public_send(helper, *args, **kwargs)
rescue ActionController::UrlGenerationError, NoMethodError
  nil
end

.show_path(view, record, owner: nil) ⇒ Object

Whether the record has a plain (show) route — feeds the ":show button only without a label link" rule.



45
46
47
# File 'lib/crud_components/route_resolver.rb', line 45

def show_path(view, record, owner: nil)
  try_helpers(view, member_candidates(nil, record, owner))
end

.try_helpers(helpers, candidates) ⇒ Object

helpers is anything exposing route helpers — the view, or main_app.



140
141
142
143
144
145
146
147
148
149
150
151
# File 'lib/crud_components/route_resolver.rb', line 140

def try_helpers(helpers, candidates)
  candidates.each do |helper, args|
    next unless helpers.respond_to?(helper)

    begin
      return helpers.public_send(helper, *args)
    rescue ActionController::UrlGenerationError, NoMethodError
      next
    end
  end
  nil
end