Class: Para::ComponentsConfiguration

Inherits:
Object
  • Object
show all
Defined in:
lib/para/components_configuration.rb

Defined Under Namespace

Classes: Component, ComponentTooDeepError, Section, UndefinedComponentTypeError

Constant Summary collapse

RELOADABLE_ERRORS =

Errors that signal the cached component state went stale, usually after a schema migration, a database reset or when a component class was unloaded by the code reloader. When one of these is raised while resolving a component, we reset the caches (and optionally reload the tree) and retry instead of letting it bubble up and break rails commands or the server.

[
  ActiveRecord::RecordNotFound,
  ActiveRecord::StatementInvalid,
  ActiveRecord::SubclassNotFound,
  ActiveModel::MissingAttributeError,
  NameError
].freeze

Instance Attribute Summary collapse

Instance Method Summary collapse

Dynamic Method Handling

This class handles dynamic methods through the method_missing method

#method_missing(method, *args, &block) ⇒ Object



51
52
53
54
55
56
57
# File 'lib/para/components_configuration.rb', line 51

def method_missing(method, *args, &block)
  if (component = resolve_component(method))
    component.tap(&ActiveDecorator::Decorator.instance.method(:decorate))
  else
    super
  end
end

Instance Attribute Details

#components_config_pathObject

Path to the app’s ‘config/components.rb`, set by the engine. Used to rebuild the components tree when auto-reloading after a stale lookup.



23
24
25
# File 'lib/para/components_configuration.rb', line 23

def components_config_path
  @components_config_path
end

Instance Method Details

#component_configuration_for(identifier) ⇒ Object



143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
# File 'lib/para/components_configuration.rb', line 143

def component_configuration_for(identifier)
  sections.each do |section|
    section.components.each do |component|
      # If one of the section component has the searched identifier return it
      return component if component.identifier.to_s == identifier.to_s

      component.child_components.each do |child_component|
        # If one of the component children has the searched identifier return it
        return child_component if child_component.identifier.to_s == identifier.to_s
      end
    end
  end

  # Return nil if the identifier was not found
  nil
end

#component_for(identifier) ⇒ Object



131
132
133
134
135
136
137
138
139
140
141
# File 'lib/para/components_configuration.rb', line 131

def component_for(identifier)
  if (component = components_cache[identifier])
    component
  else
    components_cache[identifier] = if (component_id = components_ids_hash[identifier])
                                     Para::Component::Base.find(component_id)
                                   else
                                     Para::Component::Base.find_by(identifier: identifier)
                                   end
  end
end

#components_ids_hashObject



164
165
166
# File 'lib/para/components_configuration.rb', line 164

def components_ids_hash
  @components_ids_hash ||= {}.with_indifferent_access
end

#draw(&block) ⇒ Object



25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
# File 'lib/para/components_configuration.rb', line 25

def draw(&block)
  return unless components_installed?

  Para::LogConfig.with_log_level(:fatal) do
    log_level = Rails.logger.level
    Rails.logger.level = :fatal

    # Reset any previously drawn definitions so re-drawing (e.g. on reload)
    # rebuilds a clean tree instead of appending duplicates.
    @sections = []
    reset!

    eager_load_components!
    instance_eval(&block)
    build
  end
end

#reload!Object

Re-runs the app’s ‘config/components.rb` to rebuild the whole components tree from scratch. Returns true on success, false if the file is missing or the reload itself fails (e.g. schema still mid-migration).



81
82
83
84
85
86
87
88
89
# File 'lib/para/components_configuration.rb', line 81

def reload!
  path = components_config_path
  return false unless path && File.exist?(path.to_s)

  load(path.to_s)
  true
rescue *RELOADABLE_ERRORS
  false
end

#reset!Object

Clears the memoized identifier -> database id maps and the per-request resolved instance caches, so the next lookup rebuilds against fresh data. Keeps the in-memory section/component definitions (pure Ruby, schema independent) untouched.



70
71
72
73
74
75
# File 'lib/para/components_configuration.rb', line 70

def reset!
  @sections_ids_hash = nil
  @components_ids_hash = nil
  RequestStore.store[:components_cache] = nil
  RequestStore.store[:sections_cache] = nil
end

#resolve_component(identifier, reload: true, retried: false) ⇒ Object

Resolves a component by identifier while healing stale caches. It covers both failure modes seen after a schema migration or a code reload:

* `component_for` raises a RELOADABLE_ERROR (stale id, missing column,
  unloaded STI subclass) ;
* `component_for` returns nil because the ids hash is empty/stale, which
  would otherwise surface as `undefined method 'xxx'` from method_missing.

On the first failure it resets the caches and – in auto-reload mode – rebuilds the tree from config/components.rb, then retries once. If the component still can’t be found it returns nil, letting method_missing raise the usual NoMethodError for genuinely unknown components.



116
117
118
119
120
121
122
123
124
125
126
127
128
129
# File 'lib/para/components_configuration.rb', line 116

def resolve_component(identifier, reload: true, retried: false)
  component = component_for(identifier)
  return component if component
  return nil if retried || !reload

  heal_components_cache!
  resolve_component(identifier, reload: reload, retried: true)
rescue *RELOADABLE_ERRORS => error
  return nil if retried || !reload

  log_components_failure(error)
  heal_components_cache!
  resolve_component(identifier, reload: reload, retried: true)
end

#respond_to_missing?(method, include_private = false) ⇒ Boolean

Returns:

  • (Boolean)


59
60
61
62
63
# File 'lib/para/components_configuration.rb', line 59

def respond_to_missing?(method, include_private = false)
  # Answer respond_to? from the current state only: never trigger a reload
  # just to probe whether a component exists.
  !resolve_component(method, reload: false).nil? || super
end

#section(*args, &block) ⇒ Object



43
44
45
# File 'lib/para/components_configuration.rb', line 43

def section(*args, &block)
  sections << Section.new(*args, &block)
end

#section_for(identifier) ⇒ Object



91
92
93
94
95
96
97
98
99
100
101
# File 'lib/para/components_configuration.rb', line 91

def section_for(identifier)
  if (section = sections_cache[identifier])
    section
  else
    sections_cache[identifier] = if (section_id = sections_ids_hash[identifier])
                                   Para::ComponentSection.find(section_id)
                                 else
                                   Para::ComponentSection.find_by(identifier: identifier)
                                 end
  end
end

#sectionsObject



47
48
49
# File 'lib/para/components_configuration.rb', line 47

def sections
  @sections ||= []
end

#sections_ids_hashObject



160
161
162
# File 'lib/para/components_configuration.rb', line 160

def sections_ids_hash
  @sections_ids_hash ||= {}.with_indifferent_access
end