Module: LcpRuby::Generators::FeatureRegistry

Extended by:
FeatureRegistry
Included in:
FeatureRegistry
Defined in:
lib/lcp_ruby/generators/feature_registry.rb

Overview

Single source of truth for feature generator metadata.

Three consumers read this:

  1. CLI (‘lcp new`) — preset expansion + wizard feature picker. Pure-Ruby, no Rails boot — must be require-able from `bin/lcp` before `rails new` runs.

  2. Generator runtime (via ‘Prerequisites` concern) — `requires:` is the contract checked in `check_lcp_prerequisites!`.

  3. ‘lcp_ruby:doctor` rake task — feature state walk.

Per-entry shape:

label:           — wizard-visible string
group:           — wizard grouping (Authentication, Data Management, …)
requires:        — feature names that must already be installed
provides_models: — model file names dropped under
                   `config/lcp_ruby/models/<name>.{rb,yml}`. Used by
                   `installed?` as a file-presence marker. Empty
                   list means "no canonical model marker" (rare).

Drift between this hash and actual generator output is caught by contract specs that run each generator against a tmpdir and inspect the resulting ‘config/lcp_ruby/models/` directory.

Defined Under Namespace

Classes: CyclicDependency, UnknownFeature

Constant Summary collapse

MODELS_CONFIG_DIR =

Path constants used by ‘installed?`, the `lcp_ruby:doctor` rake task, and `lcp new` manifest plumbing. `app_template.rb` cannot reuse these — it runs in the `rails new` scaffold context before Bundler loads the gem — so those literals are mirrored inline there and pinned by the registry-vs-template drift test.

"config/lcp_ruby/models"
INSTALL_MANIFEST_PATH =
"tmp/lcp_ruby/install_manifest.json"
FEATURES =
{
  "install_auth" => {
    label:           "Built-in authentication (Devise)",
    group:           "Authentication",
    requires:        [],
    provides_models: %w[user]
  },
  "custom_fields" => {
    label:           "Custom fields",
    group:           "Data Management",
    requires:        [],
    provides_models: %w[custom_field_definition]
  },
  "export" => {
    label:           "Export",
    group:           "Data Management",
    requires:        [],
    provides_models: %w[export_log export_profile]
  },
  "import" => {
    label:           "Import",
    group:           "Data Management",
    requires:        %w[background_jobs],
    provides_models: %w[import_row import_profile]
  },
  "batch_operations" => {
    label:           "Batch operations",
    group:           "Data Management",
    requires:        [],
    provides_models: %w[batch_operation batch_operation_item]
  },
  "role_model" => {
    label:           "Database-backed roles",
    group:           "Access Control",
    requires:        [],
    provides_models: %w[role]
  },
  "permission_source" => {
    label:           "Dynamic permissions",
    group:           "Access Control",
    requires:        [],
    provides_models: %w[permission_config]
  },
  "groups" => {
    label:           "Groups",
    group:           "Access Control",
    requires:        [],
    provides_models: %w[group group_membership group_role_mapping]
  },
  "auditing" => {
    label:           "Auditing",
    group:           "Audit & Monitoring",
    requires:        [],
    provides_models: %w[audit_log]
  },
  "monitoring" => {
    label:           "Monitoring dashboard",
    group:           "Audit & Monitoring",
    requires:        [],
    provides_models: %w[lcp_error_log]
  },
  "saved_filters" => {
    label:           "Saved filters",
    group:           "Audit & Monitoring",
    requires:        [],
    provides_models: %w[saved_filter]
  },
  "pages" => {
    label:           "Database-backed pages",
    group:           "Pages & Jobs",
    requires:        [],
    provides_models: %w[page_config]
  },
  "background_jobs" => {
    label:           "Background job tracking",
    group:           "Pages & Jobs",
    requires:        [],
    provides_models: %w[job_execution]
  },
  "workflow_definition" => {
    label:           "Workflow definitions",
    group:           "Workflows",
    requires:        [],
    provides_models: %w[workflow_definition]
  },
  "workflow_approvals" => {
    label:           "Workflow approvals",
    group:           "Workflows",
    requires:        [],
    provides_models: %w[workflow_approval_request workflow_approval_step workflow_approval_task]
  },
  "workflow_audit_log" => {
    label:           "Workflow audit log",
    group:           "Workflows",
    requires:        [],
    provides_models: %w[workflow_audit_log]
  },
  "gapfree_sequences" => {
    label:           "Gap-free sequences",
    group:           "Other",
    requires:        [],
    provides_models: %w[gapfree_sequence]
  },
  "api_tokens" => {
    label:           "API tokens",
    group:           "Authentication",
    requires:        %w[install_auth],
    provides_models: %w[api_token]
  },
  "oidc_role_mappings" => {
    label:           "OIDC role mappings",
    group:           "Authentication",
    requires:        [],
    provides_models: %w[oidc_role_mapping]
  }
}.freeze

Instance Method Summary collapse

Instance Method Details

#expand_with_dependencies(names) ⇒ Object

Topologically sort feature names so dependencies appear before their dependents. Stable: order within the input list — and within each ‘requires:` list — is preserved.

Raises ‘UnknownFeature` if any name is not in `FEATURES`, `CyclicDependency` if a cycle is detected.



184
185
186
187
188
189
190
# File 'lib/lcp_ruby/generators/feature_registry.rb', line 184

def expand_with_dependencies(names)
  ordered  = []
  visited  = {}
  visiting = {}
  Array(names).each { |n| visit(n.to_s, ordered, visited, visiting) }
  ordered
end

#installed?(feature, destination_root) ⇒ Boolean

File-presence marker. Returns true when every model in ‘provides_models` exists at `config/lcp_ruby/models/<name>.rb,yml` under destination_root. Empty `provides_models` collapses to `true` — features without a canonical marker are reported as “installed” if their entry exists at all.

Returns:

  • (Boolean)


167
168
169
170
171
172
173
174
175
176
# File 'lib/lcp_ruby/generators/feature_registry.rb', line 167

def installed?(feature, destination_root)
  meta = FEATURES[feature.to_s]
  return false unless meta
  models = meta[:provides_models]
  return true if models.empty?
  models_dir = File.join(destination_root, MODELS_CONFIG_DIR)
  models.all? do |name|
    %w[rb yml].any? { |ext| File.exist?(File.join(models_dir, "#{name}.#{ext}")) }
  end
end