///|
pub(all) struct Tree {
files : Map[String, String]
directories : Array[String]
}
///|
pub(all) struct Project {
root : String
name : String
anchor : String
document : @config.Document
error : String?
}
///|
pub(all) struct Workspace {
root : String
anchor : String
document : @config.Document
projects : Array[Project]
primary : @config.Document
warnings : Array[String]
}
///|
pub fn primary_paths(root : String) -> Array[String] raise {
let paths = [join(root, "morphir-workspace.json")]
for stem in ["morphir", ".morphir/morphir", ".config/morphir/config"] {
for extension in ["json", "json5", "yaml", "yml", "toml"] {
paths.push(join(root, stem + "." + extension))
}
}
paths
}
///|
pub fn discover(
tree : Tree,
root~ : String = "",
system~ : @config.Document = @config.empty(),
global~ : @config.Document = @config.empty(),
environment~ : Map[String, String] = Map([]),
overlay~ : @config.Document = @config.empty(),
options~ : ConfigurationOptions = ConfigurationOptions::new(),
) -> Workspace raise {
let root = normalize(root)
let loaded = resolve_configuration(
tree,
root,
None,
options,
system,
global,
environment,
overlay,
) catch {
@lander.Ambiguous(candidates~, ..) =>
raise @config.Invalid("Ambiguous configuration: " + candidates.join(", "))
@lander.Profile(failure~) => raise @config.Invalid(failure.message())
error => raise @config.Invalid(error.message())
}
let anchor = loaded.anchor
let primary_doc = loaded.primary
let root_effective = loaded.document
root_effective.validate()
let warnings = loaded.warnings.copy()
let workspace_root = join(root, root_effective.text("workspace.root", ""))
let members = root_effective.strings("workspace.members").map(normalize)
let excludes = root_effective.strings("workspace.exclude").map(normalize)
for pattern in members + excludes {
ignore(matches(pattern, ""))
}
let roots : Map[String, Bool] = Map([])
if primary_doc.get("project") is Some(_) {
roots[workspace_root] = true
}
for directory in tree.directories {
let directory = normalize(directory)
if directory == workspace_root || !within(workspace_root, directory) {
continue
}
if primary_paths(directory).any(path => path == anchor) {
continue
}
let relative = suffix(workspace_root, directory)
if members.any(pattern => matches(pattern, relative)) &&
!excludes.any(pattern => matches(pattern, relative)) {
roots[directory] = true
}
}
for pattern in members {
if !pattern.contains("*") &&
!pattern.contains("?") &&
!pattern.contains("[") &&
!pattern.contains("{") &&
!excludes.any(excluded => matches(excluded, pattern)) {
if !tree.directories.any(directory => {
normalize(directory) == join(workspace_root, pattern)
}) {
warnings.push(
"Missing workspace member: " + join(workspace_root, pattern),
)
}
}
}
if roots.is_empty() && members.is_empty() && anchor == "" {
roots[workspace_root] = true
}
let paths = roots.keys().collect()
paths.sort()
let projects = []
for path in paths {
try {
let data = if path == workspace_root {
loaded
} else {
resolve_configuration(
tree,
root,
Some(path),
options,
system,
global,
environment,
overlay,
)
}
for warning in data.warnings {
if !warnings.contains(warning) {
warnings.push(warning)
}
}
if path != workspace_root && data.anchor == "" {
if members.contains(suffix(workspace_root, path)) {
raise @config.Invalid("Member has no project configuration: " + path)
}
continue
}
if path != workspace_root {
for
(key, message) in [
("workspace.out_dir", "Ignoring member workspace.out_dir: "),
("workspace.output_dir", "Removed workspace.output_dir alias: "),
] {
if data.primary.get(key) is Some(_) {
warnings.push(message + path)
}
}
}
let effective = data.document
effective.validate()
ignore(normalize(effective.text("project.source_directory", "src")))
let name = effective.text(
"project.name",
if path == workspace_root {
"user"
} else {
suffix(workspace_root, path)
},
)
if name == "" {
raise @config.Invalid("Project name must be nonempty: " + path)
}
projects.push({
root: path,
name,
anchor: data.anchor,
document: effective,
error: None,
})
} catch {
@lander.Ambiguous(candidates~, ..) =>
raise @config.Invalid(
"Ambiguous configuration: " + candidates.join(", "),
)
@lander.Profile(failure~) => raise @config.Invalid(failure.message())
error => {
let found = primary_paths(path).filter(candidate => {
tree.files.contains(candidate)
})
projects.push({
root: path,
name: path,
anchor: found.get(0).unwrap_or(""),
document: @config.empty(),
error: Some(error.to_string()),
})
}
}
}
let names : Map[String, Array[String]] = Map([])
for project in projects {
let entries = names.get(project.name).unwrap_or([])
entries.push(
if project.root == workspace_root {
"."
} else {
suffix(workspace_root, project.root)
},
)
names[project.name] = entries
}
let sorted = names.keys().collect()
sorted.sort()
for name in sorted {
if names[name].length() > 1 {
warnings.push(
"Duplicate project name '" + name + "': " + names[name].join(", "),
)
}
}
let default_member = root_effective.text("workspace.default_member", "")
if default_member != "" &&
!projects.any(project => {
project.root == join(workspace_root, default_member)
}) {
raise @config.Invalid("Invalid default member: " + default_member)
}
if root_effective.get("workspace.output_dir") is Some(_) {
warnings.push("Removed workspace.output_dir alias")
}
{
root: workspace_root,
anchor,
document: root_effective,
primary: primary_doc,
projects,
warnings,
}
}
///|
pub fn Workspace::select(
self : Workspace,
selector~ : String = "",
all~ : Bool = false,
invocation~ : String = "",
) -> Array[Project] raise {
if all && selector != "" {
raise @config.Invalid("--all and --project cannot be combined")
}
if all {
return self.projects.copy()
}
if selector != "" {
// Paths take precedence over display names, including `.` for the root.
let canonical = normalize(selector)
let by_path = self.projects.filter(p => {
suffix(self.root, p.root) == canonical
})
if by_path.length() == 1 {
return by_path
}
let by_name = self.projects.filter(p => p.name == selector)
if by_name.length() != 1 {
raise @config.Invalid("Unknown or ambiguous project: " + selector)
}
return by_name
}
let owners = self.projects.filter(p => {
within(p.root, invocation) && p.root != self.root
})
if !owners.is_empty() {
owners.sort_by(fn(a, b) { b.root.length().compare(a.root.length()) })
return [owners[0]]
}
let default_member = self.document.text("workspace.default_member", "")
if default_member != "" {
return self.projects.filter(p => p.root == join(self.root, default_member))
}
self.projects.copy()
}
///|
pub fn Workspace::to_json(self : Workspace) -> Json raise {
Json::object(
Map::from_array([
("protocolVersion", Json::string("0.1.0")),
("configAnchor", Json::string(self.anchor)),
(
"name",
match self.document.get("workspace.name") {
Some(_) => Json::string(self.document.text("workspace.name", ""))
None => Json::null()
},
),
(
"state",
Json::string(
if self.projects.any(p => p.error is Some(_)) {
"error"
} else {
"open"
},
),
),
(
"diagnostics",
Json::array(
self.warnings.map(message => {
Json::object(
Map::from_array([
("severity", Json::string("warning")),
("code", Json::string("workspace.warning")),
("message", Json::string(message)),
("path", Json::string(self.anchor)),
]),
)
}),
),
),
(
"projects",
Json::array(
self.projects.map(fn(p) raise {
Json::object(
Map::from_array([
("name", Json::string(p.name)),
(
"version",
match p.document.get("project.version") {
Some(_) =>
Json::string(p.document.text("project.version", ""))
None => Json::null()
},
),
(
"relativePath",
Json::string(
if p.root == self.root {
"."
} else {
suffix(self.root, p.root)
},
),
),
(
"configAnchor",
if p.anchor == "" {
Json::null()
} else {
Json::string(p.anchor)
},
),
(
"sourceDirectory",
Json::string(
p.document.text("project.source_directory", "src"),
),
),
(
"state",
Json::string(
if p.error is Some(_) {
"error"
} else {
"unloaded"
},
),
),
(
"diagnostics",
Json::array(
match p.error {
None => []
Some(message) =>
[
Json::object(
Map::from_array([
("severity", Json::string("error")),
("code", Json::string("workspace.member.invalid")),
("message", Json::string(message)),
("path", Json::string(p.anchor)),
(
"projectPath",
Json::string(
if p.root == self.root {
"."
} else {
suffix(self.root, p.root)
},
),
),
]),
),
]
},
),
),
]),
)
}),
),
),
]),
)
}