Class: LcpRuby::Engine

Inherits:
Rails::Engine
  • Object
show all
Defined in:
lib/lcp_ruby/engine.rb

Constant Summary collapse

BIND_TO_APPLICATOR_MAP =
{
  "ransack"          => ModelFactory::RansackApplicator,
  "scopes"           => ModelFactory::ScopeApplicator,
  "validations"      => ModelFactory::ValidationApplicator,
  "virtual_columns"  => ModelFactory::AggregateApplicator,
  "transforms"       => ModelFactory::TransformApplicator,
  "computed_fields"  => ModelFactory::ComputedApplicator,
  "custom_fields"    => CustomFields::Applicator,
  "workflow"         => ModelFactory::WorkflowApplicator,
  "userstamps"       => ModelFactory::UserstampsApplicator,
  "soft_delete"      => ModelFactory::SoftDeleteApplicator,
  "auditing"         => ModelFactory::AuditingApplicator,
  "tree"             => ModelFactory::TreeApplicator,
  "sequences"        => ModelFactory::SequenceApplicator,
  "positioning"      => ModelFactory::PositioningApplicator,
  "attachments"      => ModelFactory::AttachmentApplicator,
  "enums"            => ModelFactory::EnumApplicator,
  "service_accessors" => ModelFactory::ServiceAccessorApplicator,
  "defaults"         => ModelFactory::DefaultApplicator,
  "inherited_parent_validator" => ModelFactory::InheritedParentValidatorApplicator
}.freeze
RELOAD_MUTEX =

Serializes ‘reload!` against itself in dev/test so a YAML edit that triggers autoreload while another thread is mid-`reset!` does not crash the in-flight request with `RegistryError`. Production deploys are expected to drain traffic before reload (rolling restart, blue-green) and never hit this; the mutex is a no-op there but costs nothing on the read path which never enters `reload!`.

Mutex.new

Class Method Summary collapse

Class Method Details

.apply_bind_to_managed!(model_def) ⇒ Object

Re-apply ‘lcp_managed:` schema (additive columns) and AR macros for a bind_to: host class. Idempotent. Used by `lcp_ruby:ensure_tables` to recover after `db:schema:load` has recreated the host table without the managed columns (those columns are LCP-runtime-added and never present in `schema.rb`).

Safe to call repeatedly: ‘SchemaManager.apply_managed!` skips existing columns; `AssociationApplicator` short-circuits on `already_managed?` for previously-installed macros.



387
388
389
390
391
392
393
394
395
# File 'lib/lcp_ruby/engine.rb', line 387

def apply_bind_to_managed!(model_def)
  return unless model_def.bind_to?

  host_class = LcpRuby.registry.model_for(model_def.name)
  return unless host_class

  apply_bind_to_managed_schema(host_class, model_def)
  apply_bind_to_managed_associations(host_class, model_def)
end

.ensure_active_record_loaded!Object

Drains the ‘on_load(:active_record)` queue at the caller’s expense rather than during ‘load_metadata!`. The engine’s ‘lcp_ruby.load_metadata` initializer registers a callback there; in non-eager-load contexts (rake, `bin/rails runner`) AR::Base is loaded lazily, so the callback fires synchronously the first time `build_model` references it — re-entering `load_metadata!`. Rake tasks that call `load_metadata!` directly should call this first to make the on_load fire deterministically. The `@metadata_loading` guard inside `load_metadata!` is the in-engine backstop.



186
187
188
# File 'lib/lcp_ruby/engine.rb', line 186

def self.ensure_active_record_loaded!
  ActiveRecord::Base
end

.load_metadata!Object



267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
# File 'lib/lcp_ruby/engine.rb', line 267

def load_metadata!
  return if @metadata_loaded
  # Re-entrance guard: model building below references ActiveRecord::Base,
  # which fires the `on_load(:active_record)` hook registered by the
  # `lcp_ruby.load_metadata` initializer. In dev/test rake contexts the
  # boot-time AR load is deferred, so that hook fires for the first time
  # *during* this method — re-entering it. Without this flag the second
  # entry would run `loader.load_all` on the already-populated, memoized
  # Loader and raise "Duplicate presenter/model ...". @metadata_loaded
  # is set true only at the end, so it cannot serve as the re-entry
  # guard on its own.
  return if @metadata_loading
  @metadata_loading = true

  # Re-register engine-internal action keys and (re-)evaluate the
  # OIDC bearer gate on every load. Both registries are cleared by
  # `LcpRuby.reset!` (called from `Engine.reload!`), so the bare
  # initializer-once registrations are gone by the time we get here
  # on a reload. Idempotent — safe at first boot too.
  register_engine_action_keys!
  register_oidc_bearer_resolver_if_enabled!

  Types::BuiltInTypes.register_all!
  Services::BuiltInTransforms.register_all!
  Services::BuiltInDefaults.register_all!
  Services::BuiltInAccessors.register_all!
  Services::Registry.discover!(Rails.root.join("app").to_s)
  Display::RendererRegistry.register_built_ins!
  Display::RendererRegistry.discover!(Rails.root.join("app").to_s)
  ViewSlots::Registry.register_built_ins!
  ConditionServiceRegistry.register_built_ins!

  loader = LcpRuby.loader
  loader.load_all

  # Build models in topological order (parents before children) for STI
  sorted_model_defs = Metadata::ModelInheritanceResolver.sort_definitions(loader.model_definitions)
  sorted_model_defs.each do |model_def|
    build_model(model_def, loader)
  end

  CustomFields::Setup.apply!(loader)
  RecordAliases::Setup.apply!(loader)
  Roles::Setup.apply!(loader)
  Permissions::Setup.apply!(loader)
  Groups::Setup.apply!(loader)
  Auditing::Setup.apply!(loader)
  Workflow::Setup.apply!(loader)
  BackgroundJobs::Setup.apply!(loader)
  SavedFilters::Setup.apply!(loader)
  Export::Setup.apply!(loader)
  Import::Setup.apply!(loader)
  Pages::Setup.apply!(loader)
  loader.merge_db_pages!
  DataSource::Setup.apply!(loader)

  # Late-bound host extensions (e.g. `LcpRuby.on_models_loaded { ... }`
  # registering custom AR scopes referenced by permission YAML) fire
  # *before* the runtime invariant validator, so AUTH-001's
  # `klass.respond_to?(method)` check sees them.
  LcpRuby._drain_models_loaded_callbacks!(loader)
  Authorization::RuntimeInvariantValidator.new(loader).run!

  Metrics::Setup.apply!(loader)

  LcpRuby.check_services!
  LcpRuby.check_action_text_compat!

  @metadata_loaded = true
  LcpRuby.instance_variable_set(:@booted, true)

  ActiveSupport::Notifications.instrument("boot.lcp_ruby", instrumentation_payload(loader))
ensure
  @metadata_loading = false
end

.register_engine_action_keys!Object

Re-applies engine-internal ‘Actions::ActionRegistry.register` calls. Called from the `lcp_ruby.api_tokens` initializer at boot AND from the start of every `Engine.load_metadata!` — `LcpRuby.reset!` clears `Actions::ActionRegistry`, so the bare initializer-once registration would lose these keys on `Engine.reload!`. `register` overwrites by name, so re-firing at every load is idempotent. Engine-internal `on_model_ready` registrations live in the initializer body directly — they survive reload via `@configuration` preservation in `LcpRuby.reset!` and would accumulate if re-fired here.



170
171
172
173
174
175
# File 'lib/lcp_ruby/engine.rb', line 170

def self.register_engine_action_keys!
  LcpRuby::Actions::ActionRegistry.register(
    "api_token/revoke",
    LcpRuby::Actions::ApiTokens::Revoke
  )
end

.register_oidc_bearer_resolver_if_enabled!Object

Boot-time gate for the OIDC bearer resolver. Extracted as a class method so it can be unit-tested without booting Rails — see spec/lib/lcp_ruby/engine_oidc_bearer_spec.rb. Idempotent: re-registering by name overwrites in the registry.



194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
# File 'lib/lcp_ruby/engine.rb', line 194

def self.register_oidc_bearer_resolver_if_enabled!
  return false unless LcpRuby::Authentication::ProviderRegistry.configured?
  return false unless LcpRuby::Authentication::ProviderRegistry.oidc_enabled?
  return false if LcpRuby::Authentication::ProviderRegistry.oidc_providers.none? { |p| p.audience.to_s.length.positive? }

  LcpRuby::Authentication::OidcBearerResolver.register!
  true
rescue LcpRuby::Authentication::ConfigurationError => e
  # Symmetric to OmniAuthBuilder.install_middleware!: in production, fail
  # loud; in dev/test, log + skip so `rake lcp_ruby:validate` can run
  # and surface the problem.
  raise if defined?(Rails) && Rails.respond_to?(:env) && Rails.env.production?

  Rails.logger.warn(
    "[lcp_ruby] OIDC bearer resolver NOT registered — auth.yml is invalid: #{e.message}"
  ) if defined?(Rails) && Rails.respond_to?(:logger) && Rails.logger
  false
end

.reload!Object



343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
# File 'lib/lcp_ruby/engine.rb', line 343

def reload!
  RELOAD_MUTEX.synchronize do
    @metadata_loaded = false
    # `reset_for_reload!` (NOT `reset!`) — preserves
    # `@configuration` and `@models_loaded_callbacks` so host
    # settings and host-registered callbacks from
    # `config/initializers/lcp_ruby.rb` survive the reload.
    # See docs/design/boot_reload_lifecycle.md § Q7.
    LcpRuby.reset_for_reload!
    begin
      load_metadata!
    rescue Authorization::InvariantError, MetadataError
      # Rollback half-loaded state so the next request re-boots
      # cleanly rather than serving from a partially-populated
      # registry. Rescue scope is intentionally narrow — host-side
      # StandardError from a custom `on_models_loaded` callback or
      # unexpected platform bugs propagate without rollback so the
      # host's restart path handles them. See
      # docs/design/authorization_hardening.md § "Reload-failure
      # rollback".
      @metadata_loaded = false
      LcpRuby.reset_for_reload!
      raise
    end

    # Reload-only signal so host apps can subscribe to "metadata
    # was reloaded" without also being notified on initial boot.
    # `boot.lcp_ruby` fires from inside `load_metadata!` and so
    # also fires here — that is the general "metadata is now
    # loaded" signal. `reload.lcp_ruby` is the more specific "and
    # it was a reload, not an initial boot" signal.
    ActiveSupport::Notifications.instrument("reload.lcp_ruby", instrumentation_payload(LcpRuby.loader))
  end
end