Class: LcpRuby::Metadata::Loader

Inherits:
Object
  • Object
show all
Defined in:
lib/lcp_ruby/metadata/loader.rb

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(base_path) ⇒ Loader

Returns a new instance of Loader.



12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
# File 'lib/lcp_ruby/metadata/loader.rb', line 12

def initialize(base_path)
  @base_path = Pathname.new(base_path)
  @model_definitions = {}
  @presenter_definitions = {}
  @permission_definitions = {}
  @view_group_definitions = {}
  @page_definitions = {}
  @workflow_definitions = {}
  @job_definitions = {}
  @menu_definition = nil
  @sti_builder_hashes = {}
  @abstract_model_names = []
  # Memoizes merged permission definitions per model_name. Keyed by
  # input identity so DB-backed permission_source paths invalidate
  # naturally: Permissions::Registry.reload! returns a fresh
  # PermissionDefinition instance after a DB change → object_id
  # differs → cache miss → recompute. YAML inputs are stable across
  # the loader's lifetime, so they cache forever (until LcpRuby.reset!
  # rebuilds the loader).
  @merged_permission_cache = {}
end

Instance Attribute Details

#abstract_model_namesObject (readonly)

Returns the value of attribute abstract_model_names.



7
8
9
# File 'lib/lcp_ruby/metadata/loader.rb', line 7

def abstract_model_names
  @abstract_model_names
end

#base_pathObject (readonly)

Returns the value of attribute base_path.



7
8
9
# File 'lib/lcp_ruby/metadata/loader.rb', line 7

def base_path
  @base_path
end

#job_definitionsObject (readonly)

Returns the value of attribute job_definitions.



7
8
9
# File 'lib/lcp_ruby/metadata/loader.rb', line 7

def job_definitions
  @job_definitions
end

Returns the value of attribute menu_definition.



7
8
9
# File 'lib/lcp_ruby/metadata/loader.rb', line 7

def menu_definition
  @menu_definition
end

#model_definitionsObject (readonly)

Returns the value of attribute model_definitions.



7
8
9
# File 'lib/lcp_ruby/metadata/loader.rb', line 7

def model_definitions
  @model_definitions
end

#page_definitionsObject (readonly)

Returns the value of attribute page_definitions.



7
8
9
# File 'lib/lcp_ruby/metadata/loader.rb', line 7

def page_definitions
  @page_definitions
end

#permission_definitionsObject (readonly)

Returns the value of attribute permission_definitions.



7
8
9
# File 'lib/lcp_ruby/metadata/loader.rb', line 7

def permission_definitions
  @permission_definitions
end

#presenter_definitionsObject (readonly)

Returns the value of attribute presenter_definitions.



7
8
9
# File 'lib/lcp_ruby/metadata/loader.rb', line 7

def presenter_definitions
  @presenter_definitions
end

#view_group_definitionsObject (readonly)

Returns the value of attribute view_group_definitions.



7
8
9
# File 'lib/lcp_ruby/metadata/loader.rb', line 7

def view_group_definitions
  @view_group_definitions
end

#workflow_definitionsObject (readonly)

Returns the value of attribute workflow_definitions.



7
8
9
# File 'lib/lcp_ruby/metadata/loader.rb', line 7

def workflow_definitions
  @workflow_definitions
end

Instance Method Details

#has_permission_definition?(model_name) ⇒ Boolean

Returns true iff a non-fallback permission definition exists for this model. Distinguishes “the configurator wrote a permission file” from “the lookup chain returned _default.”

Used by the inherits_from validator to confirm parent references point to real permission files, not phantom names.

Returns:

  • (Boolean)


194
195
196
197
198
199
200
201
202
203
# File 'lib/lcp_ruby/metadata/loader.rb', line 194

def has_permission_definition?(model_name)
  key = model_name.to_s
  return false if key == "_default"
  return true if @permission_definitions.key?(key)

  # STI parent fallback (mirror of yaml_permission_definition's logic)
  model_def = @model_definitions[key]
  parent_name = model_def&.sti_parent_name
  !!(parent_name && @permission_definitions.key?(parent_name))
end

#load_allObject



34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
# File 'lib/lcp_ruby/metadata/loader.rb', line 34

def load_all
  load_types
  load_models
  load_presenters
  load_pages
  auto_create_pages
  warn_unreachable_presenters
  warn_depends_on_without_action_cable
  load_permissions
  load_view_groups
  validate_references
  auto_create_view_groups
  load_menu
  load_workflows
  load_jobs
  load_theme
end

#load_modelsObject



71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
# File 'lib/lcp_ruby/metadata/loader.rb', line 71

def load_models
  # Phase 1: Collect raw hashes from YAML and DSL
  raw_entries = collect_model_hashes

  # Phase 2: Resolve inheritance (merges abstract parents, excludes abstract models)
  @abstract_model_names = raw_entries.select { |_, e| e[:hash]["abstract"] }.keys
  resolved = ModelInheritanceResolver.resolve(raw_entries)

  # Phase 3: Create ModelDefinitions from resolved hashes
  resolved.each do |name, entry|
    definition = ModelDefinition.from_hash(entry[:hash])
    definition.source_path = entry[:source_path]
    definition.source_type = entry[:source_type]
    @model_definitions[name] = definition

    # Store original child hash for STI children (used by Builder to avoid double-applying)
    @sti_builder_hashes[name] = entry[:sti_builder_hash] if entry[:sti_builder_hash]
  end
end

#load_permissionsObject



102
103
104
105
106
107
108
109
# File 'lib/lcp_ruby/metadata/loader.rb', line 102

def load_permissions
  load_yamls("permissions") do |data, file_path|
    permission_data = data["permissions"] || raise(MetadataError, "Missing 'permissions' key in #{file_path}")
    definition = PermissionDefinition.from_hash(permission_data)
    set_source!(definition, PathUtils.relative_path(file_path), "yaml")
    @permission_definitions[definition.model] = definition
  end
end

#load_presentersObject



91
92
93
94
95
96
97
98
99
100
# File 'lib/lcp_ruby/metadata/loader.rb', line 91

def load_presenters
  load_yamls("presenters") do |data, file_path|
    presenter_data = data["presenter"] || raise(MetadataError, "Missing 'presenter' key in #{file_path}")
    definition = PresenterDefinition.from_hash(presenter_data)
    set_source!(definition, PathUtils.relative_path(file_path), "yaml")
    @presenter_definitions[definition.name] = definition
  end

  load_dsl_presenters("presenters")
end

#load_typesObject



61
62
63
64
65
66
67
68
69
# File 'lib/lcp_ruby/metadata/loader.rb', line 61

def load_types
  load_yamls("types") do |data, file_path|
    type_data = data["type"] || raise(MetadataError, "Missing 'type' key in #{file_path}")
    type_def = Types::TypeDefinition.from_hash(type_data)
    Types::TypeRegistry.register(type_def.name, type_def)
  end

  load_dsl_types("types")
end

#load_view_groupsObject



111
112
113
114
115
116
117
118
119
120
121
122
123
124
# File 'lib/lcp_ruby/metadata/loader.rb', line 111

def load_view_groups
  @view_group_definitions = {}

  load_yamls("views") do |data, file_path|
    vg_data = data["view_group"] || raise(MetadataError, "Missing 'view_group' key in #{file_path}")
    # Inject file-based name if not present
    vg_data["name"] ||= File.basename(file_path, ".*")
    definition = ViewGroupDefinition.from_hash({ "view_group" => vg_data })
    set_source!(definition, PathUtils.relative_path(file_path), "yaml")
    @view_group_definitions[definition.name] = definition
  end

  load_dsl_view_groups("views")
end

Returns:

  • (Boolean)


52
53
54
# File 'lib/lcp_ruby/metadata/loader.rb', line 52

def menu_defined?
  !@menu_definition.nil?
end

#merge_db_pages!Object

Merges DB-stored page definitions into the loaded page definitions. DB pages override YAML pages with the same name. Auto-pages are recreated after merge to account for newly claimed presenters. Called from engine boot after Pages::Setup marks the registry available.



228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
# File 'lib/lcp_ruby/metadata/loader.rb', line 228

def merge_db_pages!
  return unless Pages::Registry.available?

  db_pages = begin
    Pages::Registry.all_definitions
  rescue LcpRuby::Error, ActiveRecord::StatementInvalid => e
    safe_log_warn("[LcpRuby] Failed to load DB page definitions: #{e.message}")
    raise unless Rails.env.production? if defined?(Rails)
    return
  end

  db_pages.each do |db_page|
    db_page.instance_variable_set(:@source_type, "database")
    @page_definitions[db_page.name] = db_page
  end

  # Re-create auto-pages since DB pages may claim presenters that were auto-paged
  @page_definitions.reject! { |_, p| p.auto_generated? }
  auto_create_pages

  # Clear resolver cache since pages changed
  Pages::Resolver.clear!
end

#model_definition(name) ⇒ Object



134
135
136
# File 'lib/lcp_ruby/metadata/loader.rb', line 134

def model_definition(name)
  @model_definitions[name.to_s] || raise(MetadataError, "Model '#{name}' not found")
end

Returns navigable view groups (those not opted out with navigation: false)



57
58
59
# File 'lib/lcp_ruby/metadata/loader.rb', line 57

def navigable_view_groups
  @view_group_definitions.values.select(&:navigable?)
end

#page_definition(name) ⇒ Object



138
139
140
# File 'lib/lcp_ruby/metadata/loader.rb', line 138

def page_definition(name)
  @page_definitions[name.to_s] || raise(MetadataError, "Page '#{name}' not found")
end

#permission_definition(model_name) ⇒ Object

Returns the merged permission definition for a model: the per-model definition (resolved through DB→YAML→STI fallback by SourceResolver) coverage-merged with the ‘_default` entry. Authors who want full shadow behavior declare `extends: “none”` on their per-model file.

See docs/design/permissions_default_role_coverage.md.



156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
# File 'lib/lcp_ruby/metadata/loader.rb', line 156

def permission_definition(model_name)
  key = model_name.to_s
  per_model = Permissions::SourceResolver.for(key, self)
  return per_model unless per_model
  return per_model if per_model.default?
  return per_model if per_model.extends == "none"

  default_def = Permissions::SourceResolver.for("_default", self)
  return per_model unless default_def

  cached = @merged_permission_cache[key]
  return cached[2] if cached && cached[0].equal?(per_model) && cached[1].equal?(default_def)

  merged = Metadata::PermissionMerger.merge(default: default_def, per_model: per_model)
  @merged_permission_cache[key] = [ per_model, default_def, merged ]
  merged
end

#presenter_definition(name) ⇒ Object



142
143
144
# File 'lib/lcp_ruby/metadata/loader.rb', line 142

def presenter_definition(name)
  @presenter_definitions[name.to_s] || raise(MetadataError, "Presenter '#{name}' not found")
end

#presenters_for_model(model_name) ⇒ Object



146
147
148
# File 'lib/lcp_ruby/metadata/loader.rb', line 146

def presenters_for_model(model_name)
  @presenter_definitions.values.select { |p| p.model == model_name.to_s }
end

#sti_builder_definition(name) ⇒ Object

Returns a ModelDefinition built from the original (un-merged) child hash, suitable for passing to Builder to avoid double-applying parent’s validations/callbacks/enums (which are already inherited via AR class hierarchy).



208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
# File 'lib/lcp_ruby/metadata/loader.rb', line 208

def sti_builder_definition(name)
  builder_hash = @sti_builder_hashes[name.to_s]
  return nil unless builder_hash

  full_def = @model_definitions[name.to_s]
  return nil unless full_def

  # Build a hash with child-only content but with STI metadata
  child_hash = builder_hash.dup
  child_hash["table_name"] = full_def.table_name
  child_hash[ModelInheritanceResolver::STI_CHILD_KEY] = true
  child_hash["inherits"] = full_def.inherits

  ModelDefinition.from_hash(child_hash)
end

#unreachable_presenter_namesObject



252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
# File 'lib/lcp_ruby/metadata/loader.rb', line 252

def unreachable_presenter_names
  used_in_zones = Set.new
  @page_definitions.each_value do |page|
    next if page.auto_generated?
    page.zones.each { |z| used_in_zones << z.presenter if z.presenter_zone? }
    used_in_zones << page.index_presenter if page.index_presenter
  end

  @presenter_definitions.each_value.filter_map do |presenter|
    auto_page = @page_definitions[presenter.name]
    next unless auto_page&.auto_generated?
    next if auto_page.routable?
    next if used_in_zones.include?(presenter.name)
    next if auto_page.dialog_only?

    presenter.name
  end
end

#view_group_for_page(page_name) ⇒ Object



130
131
132
# File 'lib/lcp_ruby/metadata/loader.rb', line 130

def view_group_for_page(page_name)
  @view_group_definitions.values.find { |vg| vg.page_names.include?(page_name.to_s) }
end

#view_groups_for_model(model_name) ⇒ Object



126
127
128
# File 'lib/lcp_ruby/metadata/loader.rb', line 126

def view_groups_for_model(model_name)
  @view_group_definitions.values.select { |vg| vg.model == model_name.to_s }
end

#yaml_permission_definition(model_name) ⇒ Object



174
175
176
177
178
179
180
181
182
183
184
185
186
# File 'lib/lcp_ruby/metadata/loader.rb', line 174

def yaml_permission_definition(model_name)
  perm = @permission_definitions[model_name.to_s]
  return perm if perm

  # STI child fallback: try parent model permissions
  model_def = @model_definitions[model_name.to_s]
  if (parent_name = model_def&.sti_parent_name)
    perm = @permission_definitions[parent_name]
    return perm if perm
  end

  @permission_definitions["_default"]
end