///|
/// The discovery inputs that a host gives from its arguments. Every field
/// defaults to "not given". Discovery never reads the arguments itself.
pub(all) struct DiscoveryOptions {
/// The environment name, for example from `--environment`.
environment : String?
/// The project primary, for example `Explicit(path)` from `--config`.
config : @config.SourceSelection[String]
/// Skip the user and machine files, for example from `--isolated`.
isolated : Bool
/// The project selector, for example from `--project`.
project : String?
/// Explicit profile selection; profiles remain opt-in.
profiles : @config.ProfileSelection
} derive(Eq, Debug)
///|
pub extend DiscoveryOptions with Eq::{equal, not_equal}
///|
pub extend DiscoveryOptions with @debug.Debug::{to_repr}
///|
/// Options with every field not given.
pub fn DiscoveryOptions::new(
environment? : String,
config? : @config.SourceSelection[String] = Discover,
isolated? : Bool = false,
project? : String,
profiles? : @config.ProfileSelection = Unspecified,
) -> DiscoveryOptions {
{ environment, config, isolated, project, profiles }
}
///|
/// An immutable convention set: the application name, the variable prefix,
/// the display name, the standard parts to use, the walk-up rule, the stop
/// boundary, the legacy fallback file, the environment opt-in, the workspace
/// keys, and the key filters.
pub struct Conventions {
priv name : String
priv prefix : String
priv display : String
priv parts : Array[Part]
priv walk_up : Bool
priv boundary : String?
priv legacy : Legacy?
priv environments : Environments?
priv profiles : @profiles.Policy?
priv workspace : WorkspaceKeys?
priv member_scope : Array[String]
priv inherited : Array[String]
priv root_policy : RootPolicy
priv member_loading : MemberLoading
priv layers : FormatLayers?
priv selected_file : SelectedFilePolicy
}
///|
/// A standard part of a convention set, which `without` can remove.
pub(all) enum Part {
/// The system directory: `/etc/`, or `%PROGRAMDATA%/` on Windows.
SystemDir
/// `/.` in the global user configuration directory.
UserConfigDir
/// `.` in the application home, `$_HOME` or `~/.`.
AppHome
/// The three primary layouts at each discovery location.
Layouts
/// `*.local.*` files next to each primary.
LocalOverrides
/// `*.user.*` files next to each primary.
UserOverrides
} derive(Eq, Debug)
///|
pub extend Part with Eq::{equal, not_equal}
///|
pub extend Part with @debug.Debug::{to_repr}
///|
/// Where a primary file sits in a directory.
priv enum Layout {
/// `.` in the directory.
Root
/// `./.`.
Hidden
/// `.config//config.`.
DotConfig
}
///|
/// A legacy fallback file: its name, its own format, and a decoder from the
/// parsed value to a configuration layer.
priv struct Legacy {
name : String
format : @config.Format
decode : (@config.ConfigValue) -> @config.ConfigValue raise @config.ConfigError
}
///|
/// The environment opt-in and its default name.
priv struct Environments {
default : String?
}
///|
/// The key paths of the workspace values, in the text form of a key path.
priv struct WorkspaceKeys {
members : String
exclude : String
default_member : String
project_name : String
}
///|
/// An empty convention set: no parts, no walk-up, no environments and no
/// workspace.
/// `prefix` defaults to the name in upper case with `-` changed to `_`, and
/// `display` to the name.
pub fn base(
name : String,
prefix? : String,
display? : String,
) -> Conventions {
{
name,
prefix: prefix.unwrap_or(name.to_upper().replace_all(old="-", new="_")),
display: display.unwrap_or(name),
parts: [],
walk_up: false,
boundary: None,
legacy: None,
environments: None,
profiles: None,
workspace: None,
member_scope: [],
inherited: [],
root_policy: NearestFile,
member_loading: Automatic,
layers: None,
selected_file: Primary,
}
}
///|
/// The standard convention set: `base` with every standard part (the system
/// directory, the global user directory, the application home, the three
/// primary layouts, local overrides, and user overrides) and walk-up from the
/// working directory.
pub fn app_dirs(
name : String,
prefix? : String,
display? : String,
) -> Conventions {
{
..base(name, prefix?, display?),
parts: [
SystemDir, UserConfigDir, AppHome, Layouts, LocalOverrides, UserOverrides,
],
walk_up: true,
}
}
///|
/// Add the system directory: `/etc/` on Linux and macOS, and
/// `%PROGRAMDATA%/` on Windows, or `C:/ProgramData/` when
/// `PROGRAMDATA` is not set. The candidates are `.`.
pub fn system_dir(conventions : Conventions) -> Conventions {
conventions.with_part(SystemDir)
}
///|
/// Add the global user configuration directory: `$XDG_CONFIG_HOME` when it is
/// absolute, else `$HOME/.config` on Linux and
/// `$HOME/Library/Application Support` on macOS, and `%APPDATA%` on Windows.
/// The candidates are `/.`.
pub fn user_config_dir(conventions : Conventions) -> Conventions {
conventions.with_part(UserConfigDir)
}
///|
/// Add the application home: `$_HOME`, else `.` in the user home
/// (`$HOME`, or `%USERPROFILE%` on Windows). The candidates are `.`.
/// The application home and the user configuration directory are one global
/// source, so two global files are `Ambiguous`.
pub fn app_home(conventions : Conventions) -> Conventions {
conventions.with_part(AppHome)
}
///|
/// Add the three primary layouts: `.`, `./.`, and
/// `.config//config.`.
pub fn layouts(conventions : Conventions) -> Conventions {
conventions.with_part(Layouts)
}
///|
/// Add local overrides: `.local.` next to a primary in the
/// `.` or `./.` layout, and `config.local.` next
/// to `.config//config.`, with the role `LocalOverride` and the
/// priority 390.
pub fn local_overrides(conventions : Conventions) -> Conventions {
conventions.with_part(LocalOverrides)
}
///|
/// Add user overrides: `.user.` next to a primary in the
/// `.` or `./.` layout, and `config.user.` next
/// to `.config//config.`, with the role `UserOverride` and the
/// priority 400.
pub fn user_overrides(conventions : Conventions) -> Conventions {
conventions.with_part(UserOverrides)
}
///|
/// Remove one standard part, for example `without(SystemDir)`.
pub fn without(conventions : Conventions, part : Part) -> Conventions {
{ ..conventions, parts: conventions.parts.filter(p => p != part) }
}
///|
fn Conventions::with_part(self : Conventions, part : Part) -> Conventions {
if self.parts.contains(part) {
self
} else {
{ ..self, parts: [..self.parts, part] }
}
}
///|
fn Conventions::has(self : Conventions, part : Part) -> Bool {
self.parts.contains(part)
}
///|
/// Stop the walk-up at `dir`: discovery looks in `dir` and never above it. A
/// relative `dir` joins the working directory. A working directory outside
/// `dir` raises `OutsideBoundary`.
pub fn boundary(conventions : Conventions, dir : String) -> Conventions {
{ ..conventions, boundary: Some(dir) }
}
///|
/// Add a legacy fallback file, for example `acme.json`. At a location with no
/// primary file, the legacy file is the primary and marks the project root.
/// `format` parses it and is not registered, so its extensions never join the
/// searched extensions. `decode` turns the parsed value into a layer.
pub fn legacy(
conventions : Conventions,
name : String,
format~ : @config.Format,
decode~ : (@config.ConfigValue) -> @config.ConfigValue raise @config.ConfigError,
) -> Conventions {
{ ..conventions, legacy: Some({ name, format, decode }) }
}
///|
/// Turn on workspaces. A project file whose value sets `members` or
/// `default_member` is a workspace. Its members are the directories that the
/// member entries select, without the entries of `exclude`. Each argument is a
/// key path in text form. Discovery reads the members, the exclude entries
/// and the default member from the merged value of the system file, the
/// global user file and the workspace primary, and the project name from the
/// same value or from the primary of a member.
///
/// In an entry, `*` matches any run of characters in one segment, `**`
/// matches zero or more whole segments, and every other character matches
/// itself. An absolute entry, an entry with a drive, a `\` or a `..`
/// segment gives a `Note` warning, and discovery skips it. The workspace root
/// is never its own member.
pub fn workspace(
conventions : Conventions,
members? : String = "workspace.members",
exclude? : String = "workspace.exclude",
default_member? : String = "workspace.default_member",
project_name? : String = "project.name",
) -> Conventions {
{
..conventions,
workspace: Some({ members, exclude, default_member, project_name }),
}
}
///|
/// Remove the key paths `paths` from each member layer: the member primary
/// and its environment and profile files, local override and user override. Only the
/// workspace can set them. A removed key gives a `KeyRemoved` warning in the
/// report of that source, and it has no origin. Each path is the text form
/// of a key path, for example `workspace.out_dir`.
pub fn member_scope_keys(
conventions : Conventions,
paths : Array[String],
) -> Conventions {
{ ..conventions, member_scope: [..conventions.member_scope, ..paths] }
}
///|
/// Remove the keys `keys` from the layers that a selected member inherits:
/// the system file, the global user file, the workspace primary and its
/// environment and profile files, local override and user override. The removal gives no
/// warning. With no selected member, every layer stays complete. Each key is
/// the text form of a key path, usually a top-level key such as `project`.
pub fn inherited_filter(
conventions : Conventions,
keys : Array[String],
) -> Conventions {
{ ..conventions, inherited: [..conventions.inherited, ..keys] }
}
///|
/// The application name.
pub fn Conventions::name(self : Conventions) -> String {
self.name
}
///|
/// The prefix of the application's environment variables.
pub fn Conventions::prefix(self : Conventions) -> String {
self.prefix
}
///|
/// The display name, which the ambiguity message shows.
pub fn Conventions::display(self : Conventions) -> String {
self.display
}
///|
/// Add the convention set to a core builder as a source provider. During
/// `@config.load`, discovery gives its sources at its position in merge order:
/// the system source (priority 100), the global user source (200), the project
/// or workspace primary with the role `Workspace`, `Legacy`, or `Explicit`
/// (300), the selected member primary with the role `Member` (350), the
/// environment files (375), profile files (380), local overrides (390), and user overrides
/// (400). Each kind of adjacent file loads for the workspace, then for the
/// member, for each active name in the profile stage.
///
/// Discovery reads `isolated`, `config`, `environment`, `profiles` and `project` from
/// `options`. In isolated mode the system, global user, and override sources
/// report `Skipped`. `Explicit(path)` is the project primary with no walk-up,
/// and `Skip` skips it. `project` selects a workspace member by its path or
/// by its project name. With environments off, a given `environment` adds a
/// `Note` warning, and with no workspace part a given `project` does too.
pub fn add(
builder : @config.ConfigBuilder,
conventions : Conventions,
options? : DiscoveryOptions = DiscoveryOptions::new(),
) -> @config.ConfigBuilder {
@config.provider(builder, {
name: conventions.display,
expand: context => discover(conventions, options, context),
})
}
///|
/// Enable independent configuration profiles with an immutable policy.
pub fn profiles(conventions : Conventions, policy : @profiles.Policy) -> Conventions {
{ ..conventions, profiles: Some(policy) }
}
///|
/// Set the root selection policy. NearestFile retains the legacy discovery
/// behavior. WorkspaceFirst inspects documents up to the first declared section.
pub fn root_policy(conventions : Conventions, policy : RootPolicy) -> Conventions {
{ ..conventions, root_policy: policy }
}
///|
/// Select automatic single-member loading or leave the member walk to the caller.
pub fn member_loading(conventions : Conventions, loading : MemberLoading) -> Conventions {
{ ..conventions, member_loading: loading }
}
///|
/// Whether this convention set reserves profile controls and its selector.
pub fn Conventions::profiles_enabled(self : Conventions) -> Bool { self.profiles is Some(_) }
///|
/// How an explicit config selection participates in discovery.
pub(all) enum SelectedFilePolicy {
/// Use it as the project primary without walk-up (the default).
Primary
/// Retain discovery and move the selected file to one Explicit layer at 325.
Overlay
} derive(Eq, Debug)
///|
pub extend SelectedFilePolicy with Eq::{equal, not_equal}
///|
pub extend SelectedFilePolicy with @debug.Debug::{to_repr}
///|
/// Select explicit-primary compatibility or an overlay above base primaries
/// and below members. Overlay paths resolve against the supplied working directory.
pub fn selected_file_policy(conventions : Conventions, policy : SelectedFilePolicy) -> Conventions {
{ ..conventions, selected_file: policy }
}