use std::path::{Path, PathBuf};
use crol::project::{FILE_NAME, Project};
use crol::schema::SchemaFile;
use crol::{CheckOptions, CrolComposer, CrolConfig, FileLoaders, LayerSource, Mode};
use crol_eval::{DataOptions, Diagnostic, Evaluation, Layer, Sources, Span, Variants};
use crate::diagnostics::Reporter;
#[derive(Debug, Clone, Default, clap::Args)]
pub(crate) struct InputArgs {
#[arg(long, value_delimiter = ',', value_name = "NAMES")]
discrete: Vec<String>,
#[arg(long, value_name = "NAMES")]
group: Vec<String>,
#[arg(long, value_name = "CHILD=PARENT")]
parent: Vec<String>,
#[arg(long, value_name = "FILE")]
project: Option<PathBuf>,
#[arg(long, value_name = "FILE")]
schema: Option<PathBuf>,
#[arg(long)]
offline: bool,
}
impl InputArgs {
pub(crate) fn schema(&self) -> Option<&Path> {
self.schema.as_deref()
}
pub(crate) fn project(&self) -> Option<&Path> {
self.project.as_deref()
}
pub(crate) fn offline(&self) -> bool {
self.offline
}
pub(crate) fn declares_variants(&self) -> bool {
!self.discrete.is_empty() || !self.group.is_empty() || !self.parent.is_empty()
}
fn variants(&self) -> Result<Variants, String> {
let mut variants = Variants::new();
for group in &self.group {
variants.add_grouped(group.split(','))?;
}
for name in &self.discrete {
variants.add_discrete(name)?;
}
for pair in &self.parent {
let (child, parent) = pair
.split_once('=')
.ok_or_else(|| format!("`--parent {pair}` is not `child=parent`"))?;
variants.add_parent(child, parent)?;
}
Ok(variants)
}
}
pub(crate) struct Inputs {
pub(crate) sources: Sources,
pub(crate) layers: Vec<Layer>,
pub(crate) options: DataOptions,
pub(crate) schema: Option<SchemaFile>,
pub(crate) root: Option<String>,
pub(crate) check: CheckOptions,
pub(crate) warnings: Vec<Diagnostic>,
}
fn is_module_path(text: &str) -> bool {
let rest = text.strip_prefix("::").unwrap_or(text);
!rest.is_empty() && rest.split("::").all(crol_eval::is_identifier)
}
pub(crate) fn is_key(argument: &str) -> bool {
crol_eval::parse_path(argument).is_ok() && Mode::of(argument).is_none()
}
pub(crate) fn no_layers_for(key: &str, args: &InputArgs) -> Option<String> {
let dir = std::env::current_dir().ok()?;
let none = args.project.is_none() && Project::find(&dir).is_none();
none.then(|| {
format!(
"no layers given, and no {FILE_NAME} in this directory or above; `{key}` is read as a key, so write `./{key}` to name a layer"
)
})
}
pub(crate) fn inputs(
arguments: &[String],
args: &InputArgs,
contexts: Option<Vec<Vec<String>>>,
reporter: &mut Reporter,
) -> Option<Inputs> {
let fail = |reporter: &mut Reporter, message: String| {
reporter.error(&message);
None
};
let project_path = match &args.project {
Some(path) => Some(path.clone()),
None => std::env::current_dir()
.ok()
.and_then(|dir| Project::find(&dir)),
};
let project = match &project_path {
Some(path) if arguments.is_empty() || args.project.is_some() => match Project::read(path) {
Ok(project) => Some(project),
Err(error) => return fail(reporter, error.to_string()),
},
_ => None,
};
let project_dir = project_path
.as_deref()
.map(|p| crol::directory_of(p).to_path_buf())
.unwrap_or_default();
let root_dir = if project.is_some() {
project_dir.clone()
} else {
std::env::current_dir().unwrap_or_default()
};
let file_loaders = match &project_path {
Some(file) => FileLoaders::new().with_project_file(file),
None => FileLoaders::new(),
};
let mut composer = CrolComposer::with_cfg(CrolConfig {
load_extends: true,
root_path: root_dir.clone(),
file_loaders: file_loaders.clone(),
..CrolConfig::<String>::default()
});
let config = CrolConfig::<String> {
file_loaders: file_loaders.clone(),
..CrolConfig::default()
};
if arguments.is_empty() {
let Some(project) = &project else {
return fail(
reporter,
format!("no layers given, and no {FILE_NAME} in this directory or above"),
);
};
let layers = match project.layer_list(&project_dir) {
Ok(layers) => layers,
Err(error) => return fail(reporter, error.to_string()),
};
for layer in layers {
let source = match file_loaders.resolve(&layer.dir, &project_dir) {
Ok(source) => source,
Err(error) => {
return fail(
reporter,
format!("project: layer `{}`: {error}", layer.name),
);
}
};
match source
.load(layer.name.clone())
.and_then(|loaded| composer.load(loaded))
{
Ok(loaded) => composer = loaded,
Err(error) => return fail(reporter, error.to_string()),
}
}
}
for argument in arguments {
let (name, source) = match argument.split_once('=') {
Some((name, source)) if crol_eval::is_identifier(name) => (Some(name), source),
_ => (None, argument.as_str()),
};
let module = is_module_path(source).then(|| source.load(&config));
let layer = match module {
Some(Ok(layer)) => Ok(layer),
Some(Err(error)) if !Path::new(source).exists() => Err(error),
_ => Path::new(source).load(&config),
};
let named = layer.map(|layer| match name {
Some(name) => layer.with_name(name),
None => layer,
});
match named.and_then(|layer| composer.load(layer)) {
Ok(loaded) => composer = loaded,
Err(error) => return fail(reporter, error.to_string()),
}
}
let variants = if args.declares_variants() || project.is_none() {
args.variants()
} else {
project.as_ref().map_or(Ok(Variants::new()), |p| {
p.variants().map_err(|e| e.to_string())
})
};
let variants = match variants {
Ok(variants) => variants,
Err(message) => return fail(reporter, message),
};
let mut options = DataOptions::default().with_variants(variants);
let mut check = CheckOptions::default();
let mut root = None;
let mut schema_path = args.schema.clone();
let mut used = "the one given with `--schema`";
if let Some(project) = &project {
let project_options = &project.options;
options = options
.with_warn_cross_file_patches(project_options.warn_cross_file_patches)
.with_init_defaults(project_options.init_defaults)
.with_each_variant(project_options.each_variant)
.with_max_contexts(project_options.max_contexts)
.with_contexts(project_options.contexts.clone());
match project_options.variant_keys() {
Ok(rule) => options = options.with_variant_keys(rule),
Err(error) => return fail(reporter, error.to_string()),
}
check.implicit_some = project_options.implicit_some;
check.allow_unknown_fields = project_options.allow_unknown_fields;
check.require_complete = project_options.require_complete;
root.clone_from(&project.root);
if schema_path.is_none() {
match project.schema_path(&project_dir, !args.offline) {
Ok(path) => {
schema_path = path;
used = "the project's";
}
Err(error) => return fail(reporter, error.to_string()),
}
}
}
if contexts.is_some() {
options = options.with_contexts(contexts);
}
let given = schema_path.clone();
if schema_path.is_none() {
let mut named: Vec<PathBuf> = Vec::new();
for layer in composer.layers() {
for (_, display, text) in layer.documents() {
match crol::schema::attribute_path(
text,
Path::new(display),
&root_dir,
!args.offline,
) {
Some((_, Ok(path))) => {
let path = path.canonicalize().unwrap_or(path);
if !named.contains(&path) {
named.push(path);
}
}
Some((_, Err(error))) => {
return fail(reporter, format!("{display}: {error}"));
}
None => {}
}
}
}
match named.len() {
0 => {}
1 => schema_path = named.pop(),
_ => {
return fail(
reporter,
"the documents name different schemas; choose one with --schema".to_owned(),
);
}
}
}
let schema = match &schema_path {
Some(path) => match SchemaFile::read(path) {
Ok(schema) => Some(schema),
Err(error) => return fail(reporter, error.to_string()),
},
None => None,
};
let mut sources = Sources::new();
let mut warnings = Vec::new();
let layers: Vec<Layer> = composer
.layers()
.iter()
.map(|layer| Layer {
name: layer.name().to_owned(),
documents: layer
.documents()
.map(|(relative, display, text)| {
let file = sources.add(display, text);
if let Some(given) = &given
&& let Some((range, message)) = crol::schema::attribute_disagrees(
text,
Path::new(display),
&root_dir,
given,
used,
!args.offline,
)
{
warnings.push(Diagnostic::warning(Span::new(file, range), message));
}
(relative.to_owned(), file)
})
.collect(),
})
.collect();
let schema = schema.map(|schema| {
let (schema, mut found) = schema.patched(&mut sources, &layers);
warnings.append(&mut found);
schema
});
options = options.with_defaults(schema.as_ref().map(|s| s.defaults_for(root.as_deref())));
Some(Inputs {
sources,
layers,
options,
schema,
root,
check,
warnings,
})
}
pub(crate) fn evaluate(inputs: &Inputs) -> Evaluation {
let mut evaluation = crol_eval::evaluate_data(&inputs.sources, &inputs.layers, &inputs.options);
evaluation
.diagnostics
.extend(inputs.warnings.iter().cloned());
if let (Some(schema), Some(base)) = (&inputs.schema, &evaluation.value) {
let view = crol::DataView {
base,
contexts: &evaluation.contexts,
variants: &evaluation.variants,
};
let found = crol::check_view(view, schema, inputs.root.as_deref(), &inputs.check);
evaluation.diagnostics.extend(found);
}
evaluation
}