///|
/// Give the sources of the convention set in merge order: the system and
/// global user sources, then the project tier. When a member is selected, the
/// inherited filter applies to the system and global user sources.
fn discover(
conventions : Conventions,
options : DiscoveryOptions,
context : @config.LoadContext,
) -> @config.LoadStep[@config.Expansion] raise @config.ConfigError {
conventions.check_formats(context.formats)
let notes = unread_options(conventions, options)
let environment = environment_name(conventions, options~, context.env_vars)
let extensions = searched_extensions(context.formats)
let scan = Scan::new(conventions, context, extensions, notes, options~)
let machine = scan.machine_sources(options)
if !scan.needs.is_empty() { return Need(scan.needs) }
let inputs = { options: conventions.primary_options(options), environment, extensions }
match conventions.project_sources(inputs, scan, machine) {
Need(requests) => Need(requests)
Done(tier) => {
let inherited = if tier.has_member {
key_paths(conventions.inherited)
} else {
[]
}
let sources = machine.filter_map(machine_source => {
if scan.selected is Some(path) && machine_source.candidates.any(candidate => normalize(candidate) == path) { None } else {
Some(filtered(machine_source.source, inherited, warn=false))
}
})
let resolution = match conventions.profiles {
None => None
Some(policy) => {
let variable_name = "\{conventions.prefix}_PROFILES"
let variable = context.env_vars.get(variable_name).map(text => (variable_name, text))
let input = @profiles.SelectionInput::new(options.profiles, variable?)
if conventions.prepared_layers() {
let layers = @profiles.ControlLayers::new(if input.disabled() { [] } else { tier.controls })
Some(scan.overlay_authority(@profiles.resolve_layers(policy, input, layers)))
} else {
let mut controls : (@config.ConfigValue, @config.Origin)? = None
if !input.disabled() && tier.locations.get(0) is Some((path, false)) {
let value = match context.tree.file(path) {
Found(text) => @config.parse_file(context.formats, path.to_string(), text)
Missing => raise @config.FileNotFound(path=path.to_string())
Unknown => return Need([ReadFile(path)])
}
let kind = tier.sources[0].kind()
controls = Some((value, { kind, source: conventions.display, priority: 300, location: Some(path.to_string()) }))
}
Some(@profiles.resolve_input(policy, input, controls?))
}
}
}
let profile_sources = match resolution {
Some(resolution) => if conventions.prepared_layers() { scan.ordered_profile_sources(tier, resolution) } else { conventions.profile_sources(tier.locations, resolution, extensions, tier.has_member) }
None => []
}
if !scan.needs.is_empty() { return Need(scan.needs) }
guard scan.with_selected_overlay(tier) is Some(tier) else { return Need(scan.needs) }
let ordered = [..sources]
let mut inserted = false
for source in tier.sources {
if !inserted && source.kind() is File(LocalOverride | UserOverride) {
ordered.append(profile_sources)
inserted = true
}
ordered.push(source)
}
if !inserted { ordered.append(profile_sources) }
Done(@config.Expansion::new(ordered, warnings=notes, profiles?=resolution))
}
}
}
///|
/// The inputs of the project-tier sources: the discovery options, the
/// environment name of the run, and the searched extensions.
priv struct ProjectInputs {
options : DiscoveryOptions
environment : String?
extensions : Array[String]
}
///|
/// One discovery pass over the tree: the convention set, the load context,
/// the searched extensions, the requests for entries that the tree does not
/// hold yet, and the warnings.
priv struct Scan {
conventions : Conventions
context : @config.LoadContext
extensions : Array[String]
start : @config.FileTreePath?
selected : @config.FileTreePath?
member_files : @hashset.HashSet[@config.FileTreePath]
needs : Array[@config.FileRequest]
notes : Array[@config.ConfigWarning]
parsed : Map[@config.FileTreePath, @config.ConfigValue]
documents : Map[@config.FileTreePath, Array[PrimaryDocument]]
}
///|
fn Scan::new(conventions : Conventions, context : @config.LoadContext, extensions : Array[String], notes : Array[@config.ConfigWarning], options? : DiscoveryOptions = DiscoveryOptions::new()) -> Scan raise @config.ConfigError {
let start = context.working_dir.filter(dir => dir.is_absolute()).map(normalize)
let selected = match (conventions.selected_file, options.config) {
(Overlay, Explicit(path)) => Some(conventions.explicit_file(path, context, extensions).path)
_ => None
}
{ conventions, context, extensions, start, selected, member_files: @hashset.HashSet([]), needs: [], notes, parsed: Map([]), documents: Map([]) }
}
///|
/// What a directory holds for the primary layouts and the legacy file.
priv enum DirConfig {
/// The tree does not hold a listing yet.
NotKnown
/// No primary and no legacy file.
NoFile
/// One primary file.
OneFile(@config.FileTreePath)
/// No primary file, and the legacy file.
LegacyAt(@config.FileTreePath)
/// More than one primary file.
Several
}
///|
/// How a project-tier primary was found.
priv enum FileKind {
/// By walk-up, by the enclosing-workspace search, or as a member.
Discovered
/// From the `config` option.
ExplicitFile
/// The legacy fallback file.
LegacyFile
}
///|
/// A project-tier primary: its path, the project root that it belongs to,
/// and how it was found.
priv struct ProjectFile {
path : @config.FileTreePath
root : @config.FileTreePath
kind : FileKind
}
///|
/// The result of the walk-up.
priv enum Walk {
/// The nearest directory with a primary or a legacy file.
Root(ProjectFile)
/// The nearest directory with a primary holds more than one.
Crowded(@config.FileTreePath)
/// No directory up to the boundary holds a primary.
NoRoot
}
///|
/// A primary and the sources next to it: the environment file, the local
/// override and the user override, each when it applies.
priv struct Layer {
primaries : Array[@config.ConfigSource]
adjacent : Array[Array[@config.ConfigSource]]
paths : Array[@config.FileTreePath]
controls : Array[@profiles.ControlLayer]
}
///|
/// The project-tier sources in merge order, and whether a member is
/// selected.
priv struct Tier {
sources : Array[@config.ConfigSource]
has_member : Bool
locations : Array[(@config.FileTreePath, Bool)]
controls : Array[@profiles.ControlLayer]
}
///|
/// The project tier: the project or workspace primary, the selected member
/// primary, then their environment files, local overrides and user
/// overrides, workspace first.
///
/// `Explicit(path)` gives the primary with no walk-up, and `Skip` skips it.
/// Otherwise walk up from the working directory to the boundary, or to the
/// file-system root, and stop at the first directory that holds a primary
/// candidate or the legacy file. With no root, the candidate list at the
/// start directory reports `NotFound`. With the workspace part, the
/// enclosing-workspace search and member selection follow.
fn Conventions::project_sources(
self : Conventions,
project : ProjectInputs,
scan : Scan,
machine : Array[MachineSource],
) -> @config.LoadStep[Tier] raise @config.ConfigError {
if self.root_policy is WorkspaceFirst(_) && project.options.config is Discover {
return self.workspace_first_sources(project, scan, machine)
}
let context = scan.context
let extensions = project.extensions
let start = scan.start
let stop = match self.boundary {
Some(text) => {
let dir = input_path(text, context.platform)
match (dir.is_absolute(), start) {
(true, _) => Some(normalize(dir))
(false, Some(start)) => Some(normalize(start.resolve(dir)))
(false, None) => None
}
}
None => None
}
let alone = (sources : Array[@config.ConfigSource]) => {
@config.LoadStep::Done({ sources, has_member: false, locations: [], controls: [] })
}
let file = match project.options.config {
Explicit(path) => self.explicit_file(path, context, extensions)
Skip => return alone([self.skipped_project()])
Discover => {
guard context.working_dir is Some(working_dir) else {
scan.notes.push(
note(self, "discovery has no working directory to start from"),
)
return alone([self.skipped_project()])
}
// The core joins a relative candidate to the working directory again,
// and a relative directory has no parents to walk to.
guard start is Some(start) else {
scan.notes.push(
note(
self,
"discovery needs an absolute working directory, not \{working_dir}",
),
)
return alone([self.skipped_project()])
}
if stop is Some(stop) && !start.is_within(stop) {
raise OutsideBoundary(start=start.to_string(), boundary=stop.to_string())
}
match scan.walk_up(start, stop) {
None => return Need(scan.needs)
Some(Root(file)) => file
// The core load checks the candidates and raises `Ambiguous`.
Some(Crowded(dir)) =>
return alone([self.primary(self.candidates(dir, extensions))])
Some(NoRoot) => {
if project.options.project is Some(selector) &&
self.workspace is Some(_) {
raise UnknownProject(selector~, workspace=start.to_string())
}
if self.layers is Some(_) {
guard scan.ordered_layer(start, project, as_member=false) is Some(layer) else { return Need(scan.needs) }
return Done(self.tier(layer, None))
}
return alone([self.primary(self.candidates(start, extensions))])
}
}
}
}
guard self.workspace is Some(_) else {
guard self.layer(file, project, scan, as_member=false) is Some(layer) else {
return Need(scan.needs)
}
return Done(self.tier(layer, None))
}
guard scan.enclosing(file.root, stop) is Some(enclosing) else {
return Need(scan.needs)
}
let (workspace, value) = match enclosing {
Some(found) => (found.file, found.value)
None =>
match scan.value_of(file) {
Found(value) => (file, value)
Missing => raise FileNotFound(path=file.path.to_string())
Unknown => return Need(scan.needs)
}
}
let selected = if self.member_loading is ExplicitOnly && project.options.project is None { Some(None) } else { match (project.options.project, enclosing) {
// The project file is a member of the enclosing workspace.
(None, Some(_)) => Some(Some(file.root))
(selector, _) => {
guard scan.root_value(machine, value) is Some(root_value) else {
return Need(scan.needs)
}
let declaration = self.declaration(root_value)
match (selector, declaration) {
(Some(selector), _) =>
scan.selected_project(
workspace.root,
declaration,
root_value,
selector,
)
(None, Some(declaration)) =>
scan.automatic_member(workspace.root, declaration, start)
(None, None) => Some(None)
}
}
} }
guard selected is Some(member_dir) else { return Need(scan.needs) }
guard self.layer(workspace, project, scan, as_member=false) is Some(base) else {
return Need(scan.needs)
}
let chosen = match member_dir {
None => None
Some(dir) => {
let layer = if enclosing is Some(_) && dir == file.root {
self.layer(file, project, scan, as_member=true)
} else {
self.member_layer(dir, project, scan)
}
guard layer is Some(_) else { return Need(scan.needs) }
layer
}
}
Done(self.tier(base, chosen))
}
///|
/// The nearest directory from `start` up to the boundary `stop`, or to the
/// file-system root, that holds a primary or the legacy file. A legacy file
/// counts only when no primary exists there. `None` when the tree does not
/// hold a listing yet; the walk asks for the listings of one directory per
/// round, nearest first.
fn Scan::walk_up(
self : Scan,
start : @config.FileTreePath,
stop : @config.FileTreePath?,
) -> Walk? raise @config.ConfigError {
let mut dir = start
while true {
match self.config_at(dir) {
NotKnown => return None
Several => return Some(Crowded(dir))
OneFile(path) => return Some(Root({ path, root: dir, kind: Discovered }))
LegacyAt(path) => return Some(Root({ path, root: dir, kind: LegacyFile }))
NoFile => ()
}
guard self.conventions.walk_up &&
stop != Some(dir) &&
dir.parent() is Some(parent) else {
break
}
dir = parent
}
Some(NoRoot)
}
///|
/// What `dir` holds: the primary candidates of the three layouts, else the
/// legacy file. Asks for each listing that the tree does not hold, and for
/// each candidate that the listing holds as `Inaccessible`.
fn Scan::config_at(
self : Scan,
dir : @config.FileTreePath,
) -> DirConfig raise @config.ConfigError {
if self.conventions.layers is Some(_) {
match self.primary_paths(dir) {
None => return NotKnown
Some(paths) if paths.last() is Some(last) => return OneFile(last.path)
Some(_) => ()
}
// With no ordered primary, retain the existing legacy fallback below.
guard self.conventions.legacy is Some(legacy) else { return NoFile }
let path = dir.join(legacy.name)
return match self.context.tree.is_file(path) {
Found(true) => LegacyAt(path)
Found(false) | Missing => NoFile
Unknown => { add_need(self.needs, self.context.tree.file_request(path)); NotKnown }
}
}
let tree = self.context.tree
let existing = []
let mut known = true
for candidate in self.conventions.candidates(dir, self.extensions) {
match tree.is_file(candidate) {
Found(true) => existing.push(candidate)
Found(false) | Missing => ()
Unknown => {
add_need(self.needs, tree.file_request(candidate))
known = false
}
}
}
// The host answers for each candidate first, so the error of an
// inaccessible candidate passes out even when two others exist.
guard known else { return NotKnown }
if existing.length() > 1 {
return Several
}
if existing is [path] {
return OneFile(path)
}
guard self.conventions.legacy is Some(legacy) else { return NoFile }
let path = dir.join(legacy.name)
match tree.is_file(path) {
Found(true) => LegacyAt(path)
Found(false) | Missing => NoFile
Unknown => {
add_need(self.needs, tree.file_request(path))
NotKnown
}
}
}
///|
/// The parsed value of `file`, with the legacy format and decoder for a
/// legacy file and the registered formats for another file. Asks for the
/// file when the tree does not hold its text.
fn Scan::value_of(
self : Scan,
file : ProjectFile,
) -> @config.Lookup[@config.ConfigValue] raise @config.ConfigError {
if self.conventions.layers is Some(_) && file.kind is Discovered {
return match self.primary_documents(file.root) {
None => Unknown
Some(documents) => {
let sources = documents.map(document => {
let source = self.document_source(document, Workspace)
if self.conventions.profiles is Some(_) {
source.exclude([@config.KeyPath::parse("profiles")], warn=false)
} else { source }
})
Found(@config.build(sources).value())
}
}
}
if self.conventions.prepared_layers() && file.kind is (ExplicitFile | Discovered) {
return self.read_document(file.path)
}
match self.context.tree.file(file.path) {
Found(text) => {
if self.conventions.selected_file is Overlay && self.parsed.get(file.path) is Some(value) { return Found(value) }
let value = match (file.kind, self.conventions.legacy) {
(LegacyFile, Some(legacy)) =>
(legacy.decode)((legacy.format.parse)(text, file.path.to_string()))
_ =>
@config.parse_file(self.context.formats, file.path.to_string(), text)
}
if self.conventions.selected_file is Overlay { self.parsed[file.path] = value }
Found(value)
}
Missing => Missing
Unknown => {
add_need(self.needs, ReadFile(file.path))
Unknown
}
}
}
///|
/// Add a `Note` warning one time.
fn Scan::warn(self : Scan, message : String) -> Unit {
let warning = note(self.conventions, message)
if !self.notes.contains(warning) {
self.notes.push(warning)
}
}
///|
/// The primary source of `file` and the sources next to it. A member
/// primary has the role `Member`. Another primary has the role `Workspace`,
/// `Explicit` or `Legacy`. A legacy file has no adjacent source. `None` when
/// the tree does not hold the text of a legacy file yet.
fn Conventions::layer(
self : Conventions,
file : ProjectFile,
project : ProjectInputs,
scan : Scan,
as_member~ : Bool,
parsed? : @config.ConfigValue,
) -> Layer? raise @config.ConfigError {
if self.layers is Some(_) && file.kind is Discovered {
return scan.ordered_layer(file.root, project, as_member~)
}
let primary = match file.kind {
Discovered if parsed is Some(value) =>
@config.ConfigSource::parsed_file(file.path.to_string(), value,
role=if as_member { Member } else { Workspace }, name=self.display)
Discovered if self.selected_file is Overlay => match scan.read_document(file.path) {
Found(value) => @config.ConfigSource::parsed_file(file.path.to_string(), value, role=if as_member { Member } else { Workspace }, name=self.display)
Missing => raise @config.FileNotFound(path=file.path.to_string())
Unknown => return None
}
Discovered =>
@config.ConfigSource::candidates(
texts(self.candidates(file.root, project.extensions)),
display=self.display,
role=if as_member { Member } else { Workspace },
)
ExplicitFile if self.prepared_layers() => match scan.read_document(file.path) {
Found(value) => @config.ConfigSource::parsed_file(file.path.to_string(), value, role=if as_member { Member } else { Explicit }, name=self.display)
Missing => raise @config.FileNotFound(path=file.path.to_string())
Unknown => return None
}
ExplicitFile =>
@config.ConfigSource::file(
file.path.to_string(),
role=if as_member { Member } else { Explicit },
name=self.display,
)
LegacyFile =>
match scan.value_of(file) {
Found(value) =>
return Some({
primaries: scan.keep_source(file.path, @config.ConfigSource::parsed_file(
file.path.to_string(),
value,
role=if as_member { Member } else { Legacy },
name=self.display,
)),
adjacent: [],
paths: [],
controls: [],
})
Missing => raise FileNotFound(path=file.path.to_string())
Unknown => return None
}
}
if self.prepared_layers() {
let paths = if self.adjacent(file.path, project.extensions) is Some(_) { [file.path] } else { [] }
let adjacent = if paths.is_empty() { [] } else {
guard scan.ordered_overrides(self.anchors(paths, project.extensions), project, as_member~) is Some(stages) else { return None }
stages
}
let controls = if as_member || paths.is_empty() || self.profiles is None { [] } else {
match scan.read_document(file.path) {
Found(value) => [scan.control_layer(file.path, value, if file.kind is Discovered { Workspace } else { Explicit })]
Missing => raise @config.FileNotFound(path=file.path.to_string())
Unknown => return None
}
}
return Some({ primaries: scan.keep_source(file.path, primary), adjacent, paths, controls })
}
Some({ primaries: [primary], adjacent: self.overrides(file.path, project).map(source => source.map(value => [value]).unwrap_or([])), paths: if self.adjacent(file.path, project.extensions) is Some(_) { [file.path] } else { [] }, controls: [] })
}
///|
/// The layer of the selected member in `dir`. With no primary there, or more
/// than one, the member source is the candidate list, so the report shows
/// `NotFound` with every candidate, or the core load raises `Ambiguous`.
fn Conventions::member_layer(
self : Conventions,
dir : @config.FileTreePath,
project : ProjectInputs,
scan : Scan,
) -> Layer? raise @config.ConfigError {
match scan.config_at(dir) {
NotKnown => None
OneFile(path) =>
self.layer({ path, root: dir, kind: Discovered }, project, scan, as_member=true)
LegacyAt(path) =>
self.layer({ path, root: dir, kind: LegacyFile }, project, scan, as_member=true)
NoFile | Several => if self.layers is Some(_) {
scan.ordered_layer(dir, project, as_member=true)
} else {
Some({
primaries: [@config.ConfigSource::candidates(
texts(self.candidates(dir, project.extensions)),
display=self.display,
role=Member,
)],
adjacent: [],
paths: [],
controls: [],
})
}
}
}
///|
/// The explicit primary at `given`. The text is in the native form of the
/// platform, so on Windows `\` works. A relative path joins the working
/// directory. The project root is the directory of the file, or the directory
/// that holds `./` or `.config//` for those layouts.
fn Conventions::explicit_file(
self : Conventions,
given : String,
context : @config.LoadContext,
extensions : Array[String],
) -> ProjectFile raise @config.ConfigError {
let path = input_path(given, context.platform)
let resolved = if path.is_absolute() {
normalize(path)
} else {
match context.working_dir {
Some(dir) if dir.is_absolute() => normalize(dir.resolve(path))
_ => path
}
}
let root = match self.adjacent(resolved, extensions) {
Some((dir, "config")) if parent_of(dir).name() == Some(".config") =>
parent_of(parent_of(dir))
Some((dir, _)) if dir.name() == Some(".\{self.name}") => parent_of(dir)
_ => parent_of(resolved)
}
{ path: resolved, root, kind: ExplicitFile }
}
///|
/// The environment file, local override and user override sources next to
/// the primary at `path`, each when it applies, or none when the path has no
/// standard layout. In isolated mode the overrides report `Skipped`; the
/// environment file stays.
fn Conventions::overrides(
self : Conventions,
path : @config.FileTreePath,
project : ProjectInputs,
) -> Array[@config.ConfigSource?] raise @config.ConfigError {
let { options, environment, extensions } = project
guard self.adjacent(path, extensions) is Some((dir, stem)) else { return [] }
let sources = [
environment.map(name => self.environment_file(dir, stem, name, extensions)),
]
let kinds : Array[(Part, @config.FileRole, String)] = [
(LocalOverrides, LocalOverride, "local"),
(UserOverrides, UserOverride, "user"),
]
for kind in kinds {
let (part, role, word) = kind
guard self.has(part) else {
sources.push(None)
continue
}
sources.push(
Some(
if options.isolated {
@config.ConfigSource::skipped(File(role), name=self.display)
} else {
@config.ConfigSource::candidates(
texts(
extensions.map(extension => {
dir.child("\{stem}.\{word}.\{extension}")
}),
),
display=self.display,
role~,
name=self.display,
)
},
),
)
}
sources
}
///|
/// The directory and the file stem of the primary at `path` when the path has
/// a standard layout: `` for `.` (also `./.`),
/// and `config` for `.config//config.`.
fn Conventions::adjacent(
self : Conventions,
path : @config.FileTreePath,
extensions : Array[String],
) -> (@config.FileTreePath, String)? {
guard path.parent() is Some(dir) && path.name() is Some(file) else {
return None
}
for extension in extensions {
if file == "\{self.name}.\{extension}" {
return Some((dir, self.name))
}
if file == "config.\{extension}" &&
dir.name() == Some(self.name) &&
parent_of(dir).name() == Some(".config") {
return Some((dir, "config"))
}
}
None
}
///|
/// `source` without the key paths `paths`, through the core `exclude`.
fn filtered(
source : @config.ConfigSource,
paths : Array[@config.KeyPath],
warn~ : Bool,
) -> @config.ConfigSource {
if paths.is_empty() {
source
} else {
source.exclude(paths, warn~)
}
}
///|
/// The key paths of the text forms `texts`.
fn key_paths(
texts : Array[String],
) -> Array[@config.KeyPath] raise @config.ConfigError {
texts.map(text => @config.KeyPath::parse(text))
}
///|
/// A skipped project source.
fn Conventions::skipped_project(self : Conventions) -> @config.ConfigSource {
@config.ConfigSource::skipped(File(Workspace), name=self.display)
}
///|
fn note(conventions : Conventions, message : String) -> @config.ConfigWarning {
Note(source=conventions.display, message~)
}
///|
/// A note for each given option that discovery does not read: the environment
/// with environments off, and the project with no workspace part.
fn unread_options(
conventions : Conventions,
options : DiscoveryOptions,
) -> Array[@config.ConfigWarning] {
let notes = []
if options.profiles != Unspecified && conventions.profiles is None {
notes.push(note(conventions, "profiles are off, so discovery ignores the profiles option"))
}
if options.environment is Some(_) && conventions.environments is None {
notes.push(
note(
conventions, "environments are off, so discovery ignores the environment option",
),
)
}
if options.project is Some(_) && conventions.workspace is None {
notes.push(
note(
conventions, "the convention set has no workspace part, so discovery ignores the project option",
),
)
}
notes
}
///|
/// The primary candidates in `dir`, layout by layout, in extension order.
fn Conventions::candidates(
self : Conventions,
dir : @config.FileTreePath,
extensions : Array[String],
) -> Array[@config.FileTreePath] raise @config.ConfigError {
guard self.has(Layouts) else { return [] }
let layouts : Array[Layout] = [Root, Hidden, DotConfig]
let paths = []
for layout in layouts {
for extension in extensions {
let relative = match layout {
Root => "\{self.name}.\{extension}"
Hidden => ".\{self.name}/\{self.name}.\{extension}"
DotConfig => ".config/\{self.name}/config.\{extension}"
}
paths.push(dir.join(relative))
}
}
paths
}
///|
fn Conventions::primary(
self : Conventions,
candidates : Array[@config.FileTreePath],
) -> @config.ConfigSource {
@config.ConfigSource::candidates(
texts(candidates),
display=self.display,
role=Workspace,
)
}
///|
fn add_need(needs : Array[@config.FileRequest], request : @config.FileRequest) -> Unit {
if !needs.contains(request) {
needs.push(request)
}
}
///|
/// The extensions of the registered formats, in registration order, each one
/// time.
fn searched_extensions(formats : Array[@config.Format]) -> Array[String] {
let extensions = []
for format in formats {
for extension in format.extensions {
if !extensions.contains(extension) {
extensions.push(extension)
}
}
}
extensions
}
///|
/// The parsed path of the input text `text`. With a platform, the text is in
/// the native form of that platform, so `\` separators work on Windows.
fn input_path(
text : String,
platform : @config.Platform?,
) -> @config.FileTreePath raise @config.ConfigError {
match platform {
Some(platform) => @config.FileTreePath::from_native(text, platform)
None => @config.FileTreePath::parse(text)
}
}
///|
/// `path` with each `..` name applied to the name before it. `..` never goes
/// above the root. Discovery applies this rule to the paths that a run gives,
/// such as the explicit file `../configs/acme.toml` or a relative variable,
/// before it uses them. The file-tree path itself never applies `..`.
fn normalize(
path : @config.FileTreePath,
) -> @config.FileTreePath raise @config.ConfigError {
let mut result = path.root()
for name in path.names() {
result = if name == ".." {
result.parent().unwrap_or(result)
} else {
result.child(name)
}
}
result
}
///|
/// The directory of `path`, or `path` itself at a root.
fn parent_of(path : @config.FileTreePath) -> @config.FileTreePath {
path.parent().unwrap_or(path)
}
///|
/// The text form of each path, for a source or a report.
fn texts(paths : Array[@config.FileTreePath]) -> Array[String] {
paths.map(path => path.to_string())
}
///|
fn Conventions::profile_sources(
self : Conventions,
locations : Array[(@config.FileTreePath, Bool)],
resolution : @config.ProfileResolution,
extensions : Array[String],
has_member : Bool,
) -> Array[@config.ConfigSource] raise @config.ConfigError {
let sources = []
for name in resolution.active().names() {
for location in locations {
let (path, is_member) = location
guard self.adjacent(path, extensions) is Some((dir, stem)) else { continue }
let source = @config.ConfigSource::candidates(
texts(extensions.map(extension => dir.child("\{stem}.profile.\{name}.\{extension}"))),
display=self.display, role=ProfileFile(name), name=self.display,
)
let filters = if is_member { key_paths(self.member_scope) } else if has_member { key_paths(self.inherited) } else { [] }
sources.push(filtered(source, filters, warn=is_member))
}
}
sources
}