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::{CacheKey, Data, Fetch, Needed, Pending};
use crate::resolve::{DocumentId, Origin, Resolved, Store};
use crate::settings::DataImport;
use crate::validate::Validated;
use crate::validate::data::origin;
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>,
pub(crate) pending: Pending,
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,
allowed_origins: &[String],
) -> Needed {
let (inputs, _) = inputs::of(resolved, context);
let input = inputs.value();
let mut needed = Needed {
packs: resolved.missing.clone(),
..Needed::default()
};
for (id, index, import) in all_imports(resolved) {
let refused = validated.refuses_data(id, index);
let (list, source) = match from::target(resolved, id, import, &input) {
Ok(Target::Remote(url)) if !refused => (&mut needed.remote, url),
Ok(Target::Remote(url))
if origin(&url).is_some_and(|origin| {
!allowed_origins.iter().any(|allowed| allowed == origin)
}) =>
{
(&mut needed.disallowed, url)
}
Ok(Target::Host(source)) if !refused => (&mut needed.host, source),
Ok(_) | Err(_) => continue,
};
if !list.contains(&source) {
list.push(source);
}
}
needed
}
pub(crate) fn cache_keys(
resolved: &Resolved,
validated: &Validated,
inputs: &Inputs,
) -> Vec<CacheKey> {
let input = inputs.value();
imports(resolved, validated)
.filter(|(id, _, _)| *id == resolved.main)
.filter_map(|(id, _, import)| {
let key = match from::target(resolved, id, import, &input).ok()? {
Target::File(origin) => origin.to_string(),
Target::Remote(url) => url,
Target::Host(source) => format!("host:{source}"),
};
Some(CacheKey {
name: import.name.clone(),
key,
})
})
.collect()
}
pub(crate) struct Host<'a> {
pub(crate) store: Store<'a>,
pub(crate) fetch: &'a dyn Fetch,
pub(crate) data: &'a dyn Data,
pub(crate) report: bool,
}
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,
pending: Pending::default(),
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);
if let Ok(Target::Host(source)) = &target
&& host.report
{
loader.not_portable(id, import, source);
}
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)>,
pending: bool,
}
impl Failure {
fn new(message: impl Into<String>) -> Self {
Self {
message: message.into(),
inside: None,
pending: false,
}
}
fn inside(message: String, name: &str, at: Location) -> Self {
Self {
inside: Some((name.to_string(), at)),
..Self::new(message)
}
}
fn missing(message: String, pending: bool) -> Self {
Self {
pending,
..Self::new(message)
}
}
}
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 local = matches!(target, Ok(Target::File(_)));
let loaded = match target {
Ok(target) => self.target(import, target),
Err(why) => Err(Failure::new(why.clone())),
};
let failure = match loaded {
Ok(parsed) => return self.typed(id, import, parsed),
Err(failure) => failure,
};
let fallback = self.fallback(id, import);
match &fallback {
_ if failure.pending => self.pending(target),
None => self.failed(id, import, failure, "is null"),
Some(_) if local => self.failed(id, import, failure, "is its fallback"),
Some(_) => {}
}
fallback.unwrap_or(Value::Null)
}
fn pending(&mut self, target: &Result<Target, String>) {
let pending = &mut self.loaded.pending;
let (list, source) = match target {
Ok(Target::Remote(url)) => (&mut pending.remote, url),
Ok(Target::Host(source)) => (&mut pending.host, source),
Ok(Target::File(_)) | Err(_) => return,
};
if !list.contains(source) {
list.push(source.clone());
}
}
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(|| {
let pending = self.host.fetch.pending(url);
Failure::missing(format!("`{url}` couldn't be fetched"), pending)
})?;
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(|| {
let pending = self.host.data.pending(source);
Failure::missing(
format!("this processor doesn't provide `host:{source}`"),
pending,
)
})?;
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 not_portable(&mut self, id: DocumentId, import: &DataImport, source: &str) {
let diagnostic = Diagnostic::new(
Code::W113,
import.from.at,
format!(
"`host:{source}` is this processor's own data, so `md.data.{}` is empty where another processor renders the document",
import.name
),
);
self.loaded.diagnostics.push(
diagnostic
.in_file(self.resolved.file(id))
.in_stage(Stage::Load),
);
}
fn failed(&mut self, id: DocumentId, import: &DataImport, failure: Failure, recovered: &str) {
let message = format!("{}; `md.data.{}` {recovered}", 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));
}
}