Class: Karst::Access::PrincipalSource

Inherits:
Object
  • Object
show all
Defined in:
lib/karst/access/principal_source.rb

Overview

One allowed principal population Karst may sample from or resolve into -- "which records may Karst consider at all," a different question from Karst::Access::CandidatePopulation ("which application-authored relation, within this source, is worth trying first").

records is a callable Karst evaluates lazily, exactly like a bare config.principals -- never enumerated, sampled, or materialized just by building a PrincipalSource. A single config.principals (plus any config.principal_populations) is normalized into one implicit :default PrincipalSource internally (see Karst::Configuration#principal_sources), so every downstream consumer (PrincipalSelection, Identity.resolve, the panel) only ever has to handle "one or more sources," never a separate single-source case.

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(name:, records:, populations: {}) ⇒ PrincipalSource

Returns a new instance of PrincipalSource.

Raises:

  • (ArgumentError)


22
23
24
25
26
27
28
# File 'lib/karst/access/principal_source.rb', line 22

def initialize(name:, records:, populations: {})
  raise ArgumentError, "principal source #{name.inspect} must be callable" unless records.respond_to?(:call)

  @name = name.to_sym
  @records = records
  @populations = self.class.normalize_populations(@name, populations)
end

Instance Attribute Details

#nameObject (readonly)

Returns the value of attribute name.



20
21
22
# File 'lib/karst/access/principal_source.rb', line 20

def name
  @name
end

#populationsObject (readonly)

Returns the value of attribute populations.



20
21
22
# File 'lib/karst/access/principal_source.rb', line 20

def populations
  @populations
end

#recordsObject (readonly)

Returns the value of attribute records.



20
21
22
# File 'lib/karst/access/principal_source.rb', line 20

def records
  @records
end

Class Method Details

.from_spec(name, spec) ⇒ Object



88
89
90
91
92
93
94
95
96
97
# File 'lib/karst/access/principal_source.rb', line 88

def self.from_spec(name, spec)
  return new(name: name, records: spec) if spec.respond_to?(:call)

  unless spec.is_a?(Hash)
    raise ArgumentError, "principal source #{name.inspect} must be callable or a Hash with :records"
  end

  reject_unknown_keys!(name, spec)
  new(name: name, records: fetch_any(spec, :records), populations: fetch_any(spec, :populations) || {})
end

.normalize(sources) ⇒ Object

Accepts a raw Hash of name => (callable, or populations:) -- the shape config.principal_sources= receives.

Raises:

  • (ArgumentError)


74
75
76
77
78
79
80
81
82
83
# File 'lib/karst/access/principal_source.rb', line 74

def self.normalize(sources)
  return nil if sources.nil?
  raise ArgumentError, "principal_sources must be a Hash of name => records/{records:, populations:}" unless
    sources.is_a?(Hash)

  sources.each_with_object({}) do |(name, spec), normalized|
    source = spec.is_a?(PrincipalSource) ? spec : from_spec(name, spec)
    normalized[source.name] = source
  end
end

.normalize_populations(source_name, populations) ⇒ Object

A configured population is a Hash of name => zero-argument callable expected to return an ActiveRecord::Relation scoped to this same source -- see Karst::Access::CandidatePopulation. Deliberately kept as raw callables here, not wrapped into CandidatePopulation instances: a CandidatePopulation represents one already-resolved (queried) population, which only happens once PrincipalSampler actually runs, never at configuration time.



125
126
127
128
129
130
131
132
133
134
135
# File 'lib/karst/access/principal_source.rb', line 125

def self.normalize_populations(source_name, populations)
  return {} if populations.nil?

  valid = populations.is_a?(Hash) && populations.all? { |n, c| n.is_a?(Symbol) && c.respond_to?(:call) }
  unless valid
    raise ArgumentError,
          "principal source #{source_name.inspect} populations must be a Hash of Symbol => callable"
  end

  populations
end

Instance Method Details

#evaluateObject

Evaluates the configured records callable. Never enumerates or queries on its own -- for an Active Record source this only builds a Relation, exactly like Karst::Identity.principals already did for the single-source case.



34
35
36
# File 'lib/karst/access/principal_source.rb', line 34

def evaluate
  records.call
end

#record_klassObject

The Active Record class this source's records ultimately belong to, or nil for a source whose evaluated records are not an ActiveRecord::Relation/Class (a plain Array/Enumerable source, or a callable that raises). Only ever builds a Relation to read its #klass -- never queries a row. Used to match a Karst::Access::PopulationDiscovery-discovered model against an already-configured principal source, and by the panel's guided population retry.



46
47
48
49
50
51
52
53
54
# File 'lib/karst/access/principal_source.rb', line 46

def record_klass
  evaluated = evaluate
  return evaluated if defined?(ActiveRecord::Base) && evaluated.is_a?(Class) && evaluated < ActiveRecord::Base
  return evaluated.klass if defined?(ActiveRecord::Relation) && evaluated.is_a?(ActiveRecord::Relation)

  nil
rescue StandardError
  nil
end

#with_populations(extra) ⇒ Object

A copy of this source with extra populations appended after its own configured ones. Used by Karst::Access::ApprovedPopulations to fold locally approved discovered scopes into the effective configuration, so nothing downstream has to know an approval workflow exists. Explicit configuration wins outright: a name this source already configures keeps its configured callable, and configured populations keep their position ahead of appended ones.



63
64
65
66
67
68
69
70
# File 'lib/karst/access/principal_source.rb', line 63

def with_populations(extra)
  return self if extra.nil? || extra.empty?

  merged = self.class.normalize_populations(@name, extra).reject { |name, _| @populations.key?(name) }
  return self if merged.empty?

  self.class.new(name: @name, records: @records, populations: @populations.merge(merged))
end