mod format;
mod from;
mod inputs;
mod schema;
use crate::cel::{Key, Value};
use crate::context::RenderContext;
use crate::diagnostic::{Code, Diagnostic, Location, Stage};
use crate::expanded::Inputs;
use crate::host::{Data, Fetch, Needed};
use crate::resolve::{DocumentId, Origin, Resolved, Store};
use crate::settings::DataImport;
use crate::validate::Validated;
use format::{Format, Parsed};
use from::Target;
use indexmap::IndexMap;
use std::collections::HashMap;
use std::rc::Rc;
#[derive(Clone, Debug, Default)]
pub(crate) struct Loaded {
pub(crate) inputs: Inputs,
pub(crate) diagnostics: Vec<Diagnostic>,
data: HashMap<DocumentId, Value>,
}
impl Loaded {
pub(crate) fn data(&self, id: DocumentId) -> Value {
self.data
.get(&id)
.cloned()
.unwrap_or_else(|| Value::Map(Rc::default()))
}
}
pub(crate) fn sources(
resolved: &Resolved,
validated: &Validated,
context: &RenderContext,
) -> Needed {
let (inputs, _) = inputs::of(resolved, context);
let input = inputs.value();
let mut needed = Needed::default();
for (id, _, import) in imports(resolved, validated) {
let (list, source) = match from::target(resolved, id, import, &input) {
Ok(Target::Remote(url)) => (&mut needed.remote, url),
Ok(Target::Host(source)) => (&mut needed.host, source),
Ok(Target::File(_)) | Err(_) => continue,
};
if !list.contains(&source) {
list.push(source);
}
}
needed
}
pub(crate) struct Host<'a> {
pub(crate) store: Store<'a>,
pub(crate) fetch: &'a dyn Fetch,
pub(crate) data: &'a dyn Data,
}
pub(crate) fn load(
resolved: &Resolved,
validated: &Validated,
context: &RenderContext,
host: &Host,
) -> Loaded {
let (inputs, diagnostics) = inputs::of(resolved, context);
let mut loader = Loader {
resolved,
host,
loaded: Loaded {
inputs,
diagnostics,
data: HashMap::new(),
},
};
let input = loader.loaded.inputs.value();
let mut data: HashMap<DocumentId, IndexMap<Key, Value>> = HashMap::new();
for (id, index, import) in all_imports(resolved) {
let value = if validated.refuses_data(id, index) {
loader.fallback(id, import).unwrap_or(Value::Null)
} else {
let target = from::target(resolved, id, import, &input);
loader.import(id, import, target)
};
data.entry(id)
.or_default()
.insert(Key::String(Rc::from(import.name.as_str())), value);
}
loader.loaded.data = data
.into_iter()
.map(|(id, fields)| (id, Value::Map(Rc::new(fields))))
.collect();
loader.loaded
}
fn imports<'r>(
resolved: &'r Resolved,
validated: &'r Validated,
) -> impl Iterator<Item = (DocumentId, usize, &'r DataImport)> {
all_imports(resolved).filter(|(id, index, _)| !validated.refuses_data(*id, *index))
}
fn all_imports(resolved: &Resolved) -> impl Iterator<Item = (DocumentId, usize, &DataImport)> {
resolved.ids().flat_map(move |id| {
resolved
.document(id)
.settings
.data
.iter()
.enumerate()
.map(move |(index, import)| (id, index, import))
})
}
#[derive(Clone, Debug, PartialEq, Eq)]
struct Violation {
message: String,
at: Option<(String, Location)>,
}
#[derive(Debug)]
struct Failure {
message: String,
inside: Option<(String, Location)>,
}
impl Failure {
fn new(message: impl Into<String>) -> Self {
Self {
message: message.into(),
inside: None,
}
}
}
struct Loader<'r, 'h> {
resolved: &'r Resolved,
host: &'h Host<'h>,
loaded: Loaded,
}
impl Loader<'_, '_> {
fn import(
&mut self,
id: DocumentId,
import: &DataImport,
target: Result<Target, String>,
) -> Value {
let loaded = target
.map_err(Failure::new)
.and_then(|target| self.target(import, &target));
match loaded {
Ok(parsed) => self.typed(id, import, parsed),
Err(failure) => self.fallback(id, import).unwrap_or_else(|| {
self.failed(id, import, failure);
Value::Null
}),
}
}
fn fallback(&mut self, id: DocumentId, import: &DataImport) -> Option<Value> {
let parsed = self.fallback_parsed(id, import)?;
Some(self.typed(id, import, parsed))
}
fn fallback_parsed(&self, id: DocumentId, import: &DataImport) -> Option<Parsed> {
let fallback = import.fallback.as_deref()?;
let origin = self.resolved.document(id).origin.join(fallback)?;
self.file(&origin, import).ok()
}
fn target(&self, import: &DataImport, target: &Target) -> Result<Parsed, Failure> {
match target {
Target::File(origin) => self.file(origin, import),
Target::Remote(url) => {
let fetched = self
.host
.fetch
.fetch(url)
.ok_or_else(|| Failure::new(format!("`{url}` couldn't be fetched")))?;
let format = fetched
.content_type
.as_deref()
.and_then(Format::of_content_type)
.or_else(|| Format::of_path(from::path_of(url), import.delimiter))
.ok_or_else(|| Failure::new(format!("the format of `{url}` is unknown")))?;
format.parse(url, &fetched.text)
}
Target::Host(source) => {
let value = self.host.data.data(source).ok_or_else(|| {
Failure::new(format!("this processor doesn't provide `host:{source}`"))
})?;
Ok(Parsed::value(value))
}
}
}
fn file(&self, origin: &Origin, import: &DataImport) -> Result<Parsed, Failure> {
let name = origin.to_string();
let text = self
.host
.store
.read(origin)
.ok_or_else(|| Failure::new(format!("`{name}` doesn't exist")))?;
let format = Format::of_path(origin.path(), import.delimiter)
.ok_or_else(|| Failure::new(format!("the format of `{name}` is unknown")))?;
format.parse(&name, &text)
}
fn typed(&mut self, id: DocumentId, import: &DataImport, parsed: Parsed) -> Value {
let (value, violations) = match &import.schema {
Some(fields) => schema::apply(parsed, fields),
None => (parsed.value, parsed.dropped),
};
for violation in violations {
let diagnostic = match violation.at {
Some((file, at)) => {
Diagnostic::new(Code::F027, at, violation.message).in_file(Some(file))
}
None => Diagnostic::new(Code::F027, import.from.at, violation.message)
.in_file(self.resolved.file(id)),
};
self.loaded
.diagnostics
.push(diagnostic.in_stage(Stage::Load));
}
Value::from_json(&value)
}
fn failed(&mut self, id: DocumentId, import: &DataImport, failure: Failure) {
let message = format!("{}; `md.data.{}` is null", failure.message, import.name);
let diagnostic = match failure.inside {
Some((file, at)) => Diagnostic::new(Code::F026, at, message).in_file(Some(file)),
None => {
Diagnostic::new(Code::F026, import.from.at, message).in_file(self.resolved.file(id))
}
};
self.loaded
.diagnostics
.push(diagnostic.in_stage(Stage::Load));
}
}