///|
/// A parsed lowercase ASCII profile name, from 1 to 64 characters. The first
/// character is a letter; later characters may also be digits, `_`, or `-`.
pub struct ProfileName {
priv text : String
} derive(Eq, Hash, Debug)
///|
pub extend ProfileName with Eq::{equal, not_equal}
///|
pub extend ProfileName with Hash::{hash, hash_combine}
///|
pub extend ProfileName with @debug.Debug::{to_repr}
///|
/// Parse one profile name at its input boundary.
pub fn ProfileName::parse(text : String, origin? : ProfileOrigin = Policy) -> ProfileName raise ConfigError {
if text.length() < 1 || text.length() > 64 {
raise Profile(failure=ProfileFailure::new(text, NameLength(text.length()), origin~))
}
let chars = text.to_array()
if !(chars[0] >= 'a' && chars[0] <= 'z') {
raise Profile(failure=ProfileFailure::new(text, NameStart, origin~))
}
for char in chars {
if !((char >= 'a' && char <= 'z') || (char >= '0' && char <= '9') || char == '_' || char == '-') {
raise Profile(failure=ProfileFailure::new(text, NameCharacter, origin~))
}
}
{ text, }
}
///|
/// The exact profile name as text.
pub impl Show for ProfileName with output(self, logger) {
logger.write_string(self.text)
}
///|
pub extend ProfileName with Show::{output, to_string}
///|
/// An immutable ordered collection of parsed names. Repeats are retained.
pub struct ProfileNames {
priv items : Array[ProfileName]
} derive(Eq, Debug)
///|
pub extend ProfileNames with Eq::{equal, not_equal}
///|
pub extend ProfileNames with @debug.Debug::{to_repr}
///|
/// Copy the supplied names into an immutable collection.
pub fn ProfileNames::new(names : Array[ProfileName]) -> ProfileNames {
{ items: names.copy() }
}
///|
/// A copy of the names in their original order.
pub fn ProfileNames::names(self : ProfileNames) -> Array[ProfileName] {
self.items.copy()
}
///|
/// Whether the collection contains the parsed name.
pub fn ProfileNames::contains(self : ProfileNames, name : ProfileName) -> Bool {
self.items.contains(name)
}
///|
/// An activation input. An explicit empty selection keeps includes; Disabled
/// suppresses all profile processing.
pub(all) enum ProfileSelection {
Unspecified
Selected(ProfileNames)
Disabled
} derive(Eq, Debug)
///|
pub extend ProfileSelection with Eq::{equal, not_equal}
///|
pub extend ProfileSelection with @debug.Debug::{to_repr}
///|
/// The input boundary or literal control that supplied profile information.
pub(all) enum ProfileOrigin {
Policy
Discovery
Variable(String)
Control(Origin, KeyPath)
} derive(Eq, Debug)
///|
pub extend ProfileOrigin with Eq::{equal, not_equal}
///|
pub extend ProfileOrigin with @debug.Debug::{to_repr}
///|
/// The winning activation selector. File selectors retain their file origin.
pub(all) enum ProfileSelector {
Discovery
Variable(String)
BaseActive(Origin)
BaseDefault(Origin)
Fallback
} derive(Eq, Debug)
///|
pub extend ProfileSelector with Eq::{equal, not_equal}
///|
pub extend ProfileSelector with @debug.Debug::{to_repr}
///|
/// Why a name appears in the final active order.
pub(all) enum ProfileReason {
Root(ProfileName, ProfileOrigin)
Include(ProfileName, ProfileOrigin)
GroupMember(ProfileName, ProfileName, ProfileOrigin)
} derive(Eq, Debug)
///|
pub extend ProfileReason with Eq::{equal, not_equal}
///|
pub extend ProfileReason with @debug.Debug::{to_repr}
///|
/// A typed profile rule. Count failures retain the observed count.
pub(all) enum ProfileRule {
NameLength(Int)
NameStart
NameCharacter
EmptyElement
ExpectedList
ExpectedString
ExpectedTable
UnknownField
NonLiteral
ForbiddenLocation
Cycle
GroupLimit(Int)
MemberLimit(Int)
DepthLimit(Int)
ActiveLimit(Int)
CompetingProviders(String, String)
} derive(Eq, Debug)
///|
pub extend ProfileRule with Eq::{equal, not_equal}
///|
pub extend ProfileRule with @debug.Debug::{to_repr}
///|
/// A profile failure with its raw input, typed rule and input origin.
pub struct ProfileFailure {
priv input : String
priv rule : ProfileRule
priv origin : ProfileOrigin
priv cycle : ProfileNames
} derive(Eq, Debug)
///|
pub extend ProfileFailure with Eq::{equal, not_equal}
///|
pub extend ProfileFailure with @debug.Debug::{to_repr}
///|
/// Keep the evidence for a profile failure. A cycle holds its closed path.
pub fn ProfileFailure::new(input : String, rule : ProfileRule, origin? : ProfileOrigin = Policy, cycle? : ProfileNames = ProfileNames::new([])) -> ProfileFailure {
{ input, rule, origin, cycle }
}
///|
/// The raw name or control input that failed.
pub fn ProfileFailure::input(self : ProfileFailure) -> String { self.input }
///|
/// The rule that the input breaks.
pub fn ProfileFailure::rule(self : ProfileFailure) -> ProfileRule { self.rule }
///|
/// The boundary or control that supplied the input.
pub fn ProfileFailure::origin(self : ProfileFailure) -> ProfileOrigin { self.origin }
///|
/// The closed cycle path, or an empty collection for another failure.
pub fn ProfileFailure::cycle(self : ProfileFailure) -> ProfileNames { self.cycle }
///|
/// A human-readable failure. Programs can inspect the typed rule instead.
pub fn ProfileFailure::message(self : ProfileFailure) -> String {
"Invalid profiles input \"\{self.input}\" at \{self.origin.to_repr()}: \{self.rule.to_repr()}"
}
///|
/// Immutable activation metadata retained only by a successful complete load.
pub struct ProfileResolution {
priv selector : ProfileSelector
priv roots : ProfileNames
priv active : ProfileNames
priv disabled : Bool
priv reasons : Array[ProfileReason]
priv controls : Array[ProfileOrigin]
priv authorities : Array[Origin]
} derive(Eq, Debug)
///|
pub extend ProfileResolution with Eq::{equal, not_equal}
///|
pub extend ProfileResolution with @debug.Debug::{to_repr}
///|
/// Record a resolved ordered set. The active set has at most 128 distinct names.
/// Repeated active names keep their last occurrence. `failure_origin` identifies
/// the expansion input that first exceeds the active-name limit.
/// `authorities` names every eligible base source, including a base whose
/// controls a later base fully replaces. Existing callers derive that list
/// from `controls`. Winning control origins stay separate from eligibility.
pub fn ProfileResolution::new(selector : ProfileSelector, roots : ProfileNames, active : ProfileNames, disabled? : Bool = false, reasons? : Array[ProfileReason] = [], controls? : Array[ProfileOrigin] = [], authorities? : Array[Origin] = [], failure_origin? : ProfileOrigin) -> ProfileResolution raise ConfigError {
let unique : Array[ProfileName] = []
for name in active.items.rev() {
if !unique.contains(name) { unique.push(name) }
}
if !disabled && unique.length() > 128 {
let origin = failure_origin.unwrap_or_else(() => match selector {
Discovery => ProfileOrigin::Discovery
Variable(name) => ProfileOrigin::Variable(name)
BaseActive(source) => ProfileOrigin::Control(source, KeyPath::{ names: ["profiles", "active"] })
BaseDefault(source) => ProfileOrigin::Control(source, KeyPath::{ names: ["profiles", "default"] })
Fallback => ProfileOrigin::Policy
})
raise Profile(failure=ProfileFailure::new("active profiles", ActiveLimit(unique.length()), origin~))
}
let eligible = authorities.copy()
for control in controls {
if control is Control(origin, _) && !eligible.contains(origin) { eligible.push(origin) }
}
{ selector, roots: if disabled { ProfileNames::new([]) } else { roots }, active: ProfileNames::new(if disabled { [] } else { unique.rev() }), disabled, reasons: if disabled { [] } else { reasons.copy() }, controls: if disabled { [] } else { controls.copy() }, authorities: if disabled { [] } else { eligible } }
}
///|
/// The winning selection source.
pub fn ProfileResolution::selector(self : ProfileResolution) -> ProfileSelector { self.selector }
///|
/// The selected active or default roots, before group expansion.
pub fn ProfileResolution::roots(self : ProfileResolution) -> ProfileNames { self.roots }
///|
/// The final active names in merge order.
pub fn ProfileResolution::active(self : ProfileResolution) -> ProfileNames { self.active }
///|
/// Whether an explicit input disabled processing.
pub fn ProfileResolution::disabled(self : ProfileResolution) -> Bool { self.disabled }
///|
/// A defensive copy of the activation reasons.
pub fn ProfileResolution::reasons(self : ProfileResolution) -> Array[ProfileReason] { self.reasons.copy() }
///|
/// A defensive copy of the interpreted control origins.
pub fn ProfileResolution::controls(self : ProfileResolution) -> Array[ProfileOrigin] { self.controls.copy() }
///|
/// Whether this source is an eligible base control source. Authority is
/// separate from winning control origins, so fully overridden bases remain
/// eligible. Legacy constructors derive authority from their control origins.
pub fn ProfileResolution::permits_controls(self : ProfileResolution, origin : Origin) -> Bool { self.authorities.contains(origin) }
///|
/// Whether the parsed name is active.
pub fn ProfileResolution::contains(self : ProfileResolution, name : ProfileName) -> Bool { self.active.contains(name) }
///|
/// Off is different from an enabled policy whose active result is empty.
pub(all) enum ProfileState {
Off
Resolved(ProfileResolution)
} derive(Eq, Debug)
///|
pub extend ProfileState with Eq::{equal, not_equal}
///|
pub extend ProfileState with @debug.Debug::{to_repr}
///|
/// Whether the parsed name is active in this loaded state.
pub fn ProfileState::contains(self : ProfileState, name : ProfileName) -> Bool {
match self { Off => false; Resolved(resolution) => resolution.contains(name) }
}
///|
/// Parse a comma-separated selector, trimming ASCII whitespace around names.
/// Empty names, including an empty whole selector, are an error.
pub fn ProfileNames::parse_csv(text : String, origin? : ProfileOrigin = Discovery) -> ProfileNames raise ConfigError {
let names : Array[ProfileName] = []
for part in text.split(",") {
let name = profile_ascii_trim(part.to_owned())
if name.is_empty() {
raise Profile(failure=ProfileFailure::new(text, EmptyElement, origin~))
}
names.push(ProfileName::parse(name, origin~))
}
ProfileNames::new(names)
}
///|
/// Interpret a present selector variable. An empty or ASCII-whitespace-only
/// value disables profile processing; absence is handled as Unspecified.
pub fn ProfileSelection::from_variable(text : String, variable~ : String) -> ProfileSelection raise ConfigError {
if profile_ascii_trim(text).is_empty() {
Disabled
} else {
Selected(ProfileNames::parse_csv(text, origin=Variable(variable)))
}
}
///|
fn profile_ascii_trim(text : String) -> String {
text.trim(chars=" \t\n\r\u{000b}\u{000c}").to_owned()
}
///|
/// Authorize an additional ordinary source identity for an already resolved
/// base. Providers use this when moving a base's values without changing its
/// control origins. This returns a new resolution and preserves disabled mode.
pub fn ProfileResolution::with_control_authority(self : ProfileResolution, origin : Origin) -> ProfileResolution {
if self.disabled || self.authorities.contains(origin) { self } else {
{ ..self, authorities: [..self.authorities, origin] }
}
}