///|
/// The root selection rule. The section path is parsed once at the boundary.
pub(all) enum RootPolicy {
/// Retain nearest-file discovery and legacy enclosing-member selection.
NearestFile
/// Stop at the nearest document that contains this workspace section.
WorkspaceFirst(@config.KeyPath)
}
///|
/// Whether discovery selects and loads one workspace member.
pub(all) enum MemberLoading {
/// Select a member from the working directory, default or sole member.
Automatic
/// Load a member only when DiscoveryOptions.project explicitly selects it.
ExplicitOnly
}
///|
/// Root evidence from a pure discovery pass. A nearest project is the first
/// primary encountered, including a primary that also declares a workspace.
pub struct RootEvidence {
priv project : Enclosing?
priv workspace : Enclosing?
priv sources : Array[@config.ConfigSource]
}
///|
/// The nearest primary's project directory, when one exists.
pub fn RootEvidence::nearest_project(self : RootEvidence) -> @config.FileTreePath? {
self.project.map(found => found.file.root)
}
///|
/// The selected declared workspace directory, when one exists.
pub fn RootEvidence::workspace(self : RootEvidence) -> @config.FileTreePath? {
self.workspace.map(found => found.file.root)
}
///|
/// The selected root's primary sources, already parsed, in merge order. This
/// does not choose a member or read any member's contents.
pub fn RootEvidence::sources(self : RootEvidence) -> Array[@config.ConfigSource] {
self.sources.copy()
}
///|
fn root_evidence(
project : Enclosing?,
workspace : Enclosing?,
) -> RootEvidence {
let selected = if workspace is Some(_) { workspace } else { project }
let sources = selected.map(found => found.primaries.copy()).unwrap_or([])
{ project, workspace, sources }
}
///|
/// Resolve the explicit boundary once against the normalized working directory.
fn Conventions::root_bounds(
self : Conventions,
context : @config.LoadContext,
start : @config.FileTreePath?,
) -> (@config.FileTreePath?, @config.FileTreePath?) raise @config.ConfigError {
let stop = match self.boundary {
None => None
Some(text) => {
let dir = input_path(text, context.platform)
if dir.is_absolute() {
Some(normalize(dir))
} else {
start.map(start => normalize(start.resolve(dir)))
}
}
}
(start, stop)
}
///|
/// Inspect roots through the same partial-tree protocol as configuration load.
/// No member is selected. Pass the returned primary sources to public merge
/// APIs and retain an application-owned all-member walk when needed.
pub fn Conventions::roots(
self : Conventions,
context : @config.LoadContext,
options? : DiscoveryOptions = DiscoveryOptions::new(),
) -> @config.LoadStep[RootEvidence] raise @config.ConfigError {
self.check_formats(context.formats)
let scan = Scan::new(self, context, searched_extensions(context.formats), [], options~)
scan.roots(options)
}
///|
/// Internal discovery already checked the final policy. Share its scan and
/// canonical start rather than re-entering a public boundary and reparsing.
fn Scan::roots(self : Scan, options : DiscoveryOptions) -> @config.LoadStep[RootEvidence] raise @config.ConfigError {
let options = self.conventions.primary_options(options)
if options.config is Skip { return Done(root_evidence(None, None)) }
let (start, stop) = self.conventions.root_bounds(self.context, self.start)
if options.config is Explicit(path) {
let file = self.conventions.explicit_file(path, self.context, self.extensions)
return self.nearest_roots(file, stop)
}
guard start is Some(start) else { return Done(root_evidence(None, None)) }
if stop is Some(stop) && !start.is_within(stop) {
raise OutsideBoundary(start=start.to_string(), boundary=stop.to_string())
}
match self.conventions.root_policy {
WorkspaceFirst(section) => self.workspace_roots(start, stop, section)
NearestFile => match self.walk_up(start, stop) {
None => Need(self.needs)
Some(NoRoot) => Done(root_evidence(None, None))
Some(Crowded(dir)) => self.ambiguous_root(dir)
Some(Root(file)) => self.nearest_roots(file, stop)
}
}
}
///|
fn[T] Scan::ambiguous_root(self : Scan, dir : @config.FileTreePath) -> T raise @config.ConfigError {
let candidates = self.conventions.candidates(dir, self.extensions).filter(path => self.context.tree.is_file(path) is Found(true))
raise @config.Ambiguous(display=self.conventions.display, location=dir.to_string(), candidates=candidates.map(path => path.to_string()))
}
///|
fn Scan::nearest_roots(
self : Scan,
file : ProjectFile,
stop : @config.FileTreePath?,
) -> @config.LoadStep[RootEvidence] raise @config.ConfigError {
let project = match self.value_of(file) {
Found(value) => self.found(file, value)
Missing => raise @config.FileNotFound(path=file.path.to_string())
Unknown => return Need(self.needs)
}
let workspace = if self.conventions.workspace is Some(_) {
match self.enclosing(file.root, stop) {
None => return Need(self.needs)
Some(Some(found)) => Some(found)
Some(None) => if self.conventions.declaration(project.value) is Some(_) { Some(project) } else { None }
}
} else { None }
Done(root_evidence(Some(project), workspace))
}
///|
fn Scan::workspace_roots(
self : Scan,
start : @config.FileTreePath,
stop : @config.FileTreePath?,
section : @config.KeyPath,
) -> @config.LoadStep[RootEvidence] raise @config.ConfigError {
let mut nearest : Enclosing? = None
let mut dir = start
while true {
let file = match self.config_at(dir) {
NotKnown => return Need(self.needs)
Several => self.ambiguous_root(dir)
NoFile => None
OneFile(path) => Some(ProjectFile::{ path, root: dir, kind: Discovered })
LegacyAt(path) => Some(ProjectFile::{ path, root: dir, kind: LegacyFile })
}
if file is Some(file) {
let value = match self.value_of(file) {
Found(value) => value
Missing => raise @config.FileNotFound(path=file.path.to_string())
Unknown => return Need(self.needs)
}
let found = self.found(file, value)
if nearest is None { nearest = Some(found) }
let document = self.conventions.discovery_value(value)
if document.get(section) is Some(_) {
// A section must be an object, including an empty one. Bind at this
// document boundary so a scalar gives the ordinary typed Decode error.
let _ : Map[String, Json] = @config.bind(document, section.to_string())
return Done(root_evidence(nearest, Some(found)))
}
}
guard self.conventions.walk_up && stop != Some(dir) && dir.parent() is Some(parent) else { break }
dir = parent
}
Done(root_evidence(nearest, None))
}
///|
fn Conventions::workspace_first_sources(
self : Conventions,
project : ProjectInputs,
scan : Scan,
machine : Array[MachineSource],
) -> @config.LoadStep[Tier] raise @config.ConfigError {
let evidence = match scan.roots(project.options) {
Need(requests) => return Need(requests)
Done(evidence) => evidence
}
let selected = if evidence.workspace is Some(_) { evidence.workspace } else { evidence.project }
guard selected is Some(base) else {
if project.options.project is Some(selector) && self.workspace is Some(_) && scan.start is Some(start) {
raise @config.UnknownProject(selector~, workspace=start.to_string())
}
if self.layers is Some(_) && scan.start is Some(start) {
guard scan.ordered_layer(start, project, as_member=false) is Some(layer) else { return Need(scan.needs) }
return Done(self.tier(layer, None))
}
let sources = match scan.start {
Some(start) => [self.primary(self.candidates(start, project.extensions))]
_ => [self.skipped_project()]
}
return Done({ sources, has_member: false, locations: [], controls: [] })
}
let layer = self.layer(base.file, project, scan, as_member=false, parsed=base.value)
guard layer is Some(layer) else { return Need(scan.needs) }
let selection = if (self.member_loading is ExplicitOnly && project.options.project is None) || self.workspace is None {
Some(None)
} else {
guard scan.root_value(machine, base.value) is Some(value) else { return Need(scan.needs) }
let declaration = self.declaration(value)
match project.options.project {
Some(selector) => scan.selected_project(base.file.root, declaration, value, selector)
None => match declaration {
Some(declaration) if evidence.workspace is Some(_) => scan.automatic_member(base.file.root, declaration, scan.start)
_ => Some(None)
}
}
}
guard selection is Some(member_dir) else { return Need(scan.needs) }
guard member_dir is Some(dir) else {
return Done(self.tier(layer, None))
}
let chosen = match evidence.project {
Some(found) if found.file.root == dir => self.layer(found.file, project, scan, as_member=true, parsed=found.value)
_ => self.member_layer(dir, project, scan)
}
guard chosen is Some(chosen) else { return Need(scan.needs) }
Done(self.tier(layer, Some(chosen)))
}