use std::path::{Path, PathBuf};
use crol::project::{FILE_NAME, Project};
use crol::schema::SchemaFile;
use crol::{CheckOptions, Mode};
use crol_eval::{Evaluation, Sources};
use crate::diagnostics::Reporter;
use crate::layers::InputArgs;
pub(crate) struct Document {
pub(crate) sources: Sources,
pub(crate) evaluation: Evaluation,
pub(crate) schema: Option<SchemaFile>,
pub(crate) root: Option<String>,
}
#[derive(Default)]
struct Expected {
schema: Option<SchemaFile>,
root: Option<String>,
options: CheckOptions,
}
pub(crate) fn argument(arguments: &[String]) -> Option<&Path> {
match arguments {
[one] if Mode::of(one) == Some(Mode::Value) && !is_named(one) => Some(Path::new(one)),
_ => None,
}
}
fn is_named(argument: &str) -> bool {
argument
.split_once('=')
.is_some_and(|(name, _)| crol_eval::is_identifier(name))
}
pub(crate) fn no_contexts(path: &Path, flag: &str, reporter: &mut Reporter) -> bool {
reporter.error(&format!(
"{}: a value document has no variants, so `{flag}` does not apply",
path.display()
));
false
}
pub(crate) fn read(path: &Path, args: &InputArgs, reporter: &mut Reporter) -> Option<Document> {
if args.declares_variants() {
no_contexts(path, "--discrete, --group, --parent", reporter);
return None;
}
let mut fail = |message: String| {
reporter.error(&message);
None
};
let text = match std::fs::read(path) {
Ok(bytes) => bytes,
Err(error) => return fail(format!("cannot read {}: {error}", path.display())),
};
let Ok(text) = String::from_utf8(text) else {
return fail(format!("{} is not valid UTF-8", path.display()));
};
let mut sources = Sources::new();
let file = sources.add(path.display().to_string(), text.as_str());
let mut evaluation = crol_eval::evaluate_value(&sources, file);
let expected = match expected(path, &text, args) {
Ok(expected) => expected,
Err(message) => return fail(message),
};
let root = checked(&mut evaluation, &expected);
Some(Document {
sources,
evaluation,
schema: expected.schema,
root,
})
}
fn checked(evaluation: &mut Evaluation, expected: &Expected) -> Option<String> {
let schema = expected.schema.as_ref()?;
let value = evaluation.value.as_ref();
let root = (expected.root.clone()).or_else(|| crol::value_type(value?, schema));
if let Some(value) = value {
let found = crol::check_value(value, schema, root.as_deref(), &expected.options);
evaluation.diagnostics.extend(found);
}
root
}
fn expected(path: &Path, text: &str, args: &InputArgs) -> Result<Expected, String> {
let schema_only = |schema| Expected {
schema: Some(schema),
..Default::default()
};
if let Some(schema) = args.schema() {
return SchemaFile::read(schema)
.map(schema_only)
.map_err(|e| e.to_string());
}
let is_project_file = path.file_name().is_some_and(|name| name == FILE_NAME)
|| args
.project()
.is_some_and(|project| same_file(project, path));
if is_project_file {
return Ok(schema_only(crol::schema::build::<Project>()));
}
if crol::schema::is_schema_file(text) {
return Ok(schema_only(crol::schema::build::<SchemaFile>()));
}
let project_path = project_path(args);
let working = std::env::current_dir().unwrap_or_default();
let root = project_path
.as_deref()
.map(crol::directory_of)
.map_or(working, Path::to_path_buf);
match crol::schema::attribute_path(text, path, &root, !args.offline()) {
Some((_, Ok(schema))) => SchemaFile::read(schema)
.map(schema_only)
.map_err(|e| format!("{}: {e}", path.display())),
Some((_, Err(error))) => Err(format!("{}: {error}", path.display())),
None => match project_path {
Some(project) => project_files(&project, path, args),
None => Ok(Expected::default()),
},
}
}
fn project_files(project_path: &Path, path: &Path, args: &InputArgs) -> Result<Expected, String> {
let project = Project::read(project_path).map_err(|e| e.to_string())?;
let dir = crol::directory_of(project_path);
let Some(type_name) = project.file_type(dir, path) else {
return Ok(Expected::default());
};
let named = || {
format!(
"{}: `files` gives {} the type `{type_name}`",
project_path.display(),
path.display()
)
};
let schema = project
.schema_path(dir, !args.offline())
.map_err(|e| e.to_string())?
.ok_or_else(|| format!("{}, and the project has no schema", named()))?;
let schema = SchemaFile::read(schema).map_err(|e| e.to_string())?;
let root = schema
.type_path(type_name)
.ok_or_else(|| format!("{}, which the schema does not have", named()))?
.to_owned();
let mut options = CheckOptions::default();
options.implicit_some = project.options.implicit_some;
options.allow_unknown_fields = project.options.allow_unknown_fields;
options.require_complete = project.options.require_complete;
Ok(Expected {
schema: Some(schema),
root: Some(root),
options,
})
}
fn project_path(args: &InputArgs) -> Option<PathBuf> {
match args.project() {
Some(project) => Some(project.to_path_buf()),
None => Project::find(&std::env::current_dir().unwrap_or_default()),
}
}
fn same_file(a: &Path, b: &Path) -> bool {
let canonical = |p: &Path| p.canonicalize().unwrap_or_else(|_| p.to_path_buf());
canonical(a) == canonical(b)
}