use crate::cel::{self, Type};
use crate::resolve::{DocumentId, Resolved};
use crate::settings::{Input, Settings};
use crate::tree::{DeclaredType, Signature, TypeName};
use serde_json::Value;
use std::collections::BTreeMap;
const LOOP_FIELDS: [(&str, Type); 5] = [
("index", Type::Int),
("number", Type::Int),
("count", Type::Int),
("first", Type::Bool),
("last", Type::Bool),
];
#[derive(Clone, Debug)]
pub(crate) struct Scope<'r> {
settings: &'r Settings,
inputs: Option<&'r [Input]>,
definition: Option<&'r Signature>,
bindings: Vec<(String, Type)>,
in_loop: bool,
}
impl<'r> Scope<'r> {
pub(crate) fn body(resolved: &'r Resolved, id: DocumentId) -> Self {
let settings = &resolved.document(id).settings;
if id == resolved.main {
Self::document(settings)
} else {
Self::definition(settings, None)
}
}
fn document(settings: &'r Settings) -> Self {
Self {
settings,
inputs: Some(&settings.inputs),
definition: None,
bindings: Vec::new(),
in_loop: false,
}
}
pub(crate) fn definition(settings: &'r Settings, signature: Option<&'r Signature>) -> Self {
Self {
settings,
inputs: None,
definition: signature,
bindings: Vec::new(),
in_loop: false,
}
}
pub(crate) fn template(&self, name: &str, element: Type) -> Self {
let mut inner = self.clone();
inner.bindings.retain(|(known, _)| known != name);
inner.bindings.push((name.to_string(), element));
inner.in_loop = true;
inner
}
pub(crate) fn input(&self, name: &str) -> Option<&'r Input> {
self.inputs?
.iter()
.find(|input| input.declaration.name == name)
}
pub(crate) fn sees_inputs(&self) -> bool {
self.inputs.is_some()
}
fn params(&self) -> impl Iterator<Item = (String, &'r DeclaredType)> {
self.definition
.into_iter()
.flat_map(|signature| &signature.params)
.filter(|param| param.declared != DeclaredType::Name(TypeName::Name))
.map(|param| (param.name.replace('-', "_"), ¶m.declared))
}
pub(crate) fn knows(&self, path: &str) -> bool {
let segments: Vec<&str> = path.split('.').collect();
match segments[..] {
["md"] | ["md", "version" | "lang" | "tz" | "data"] => true,
["md", "doc", ref keys @ ..] => has_keys(&self.settings.doc, keys),
["md", "data", name, ..] => self.settings.data.iter().any(|data| data.name == name),
["md", "input"] => self.sees_inputs(),
["md", "input", name, ..] => self.input(name).is_some(),
["md", "loop"] => self.in_loop,
["md", "loop", field] => {
self.in_loop && LOOP_FIELDS.iter().any(|(known, _)| *known == field)
}
["md", "slot"] => self.has_slot("default"),
["md", "slot", slot] => self.has_slot(slot),
["md", ..] | [] => false,
[name, ..] => {
self.bindings.iter().any(|(bound, _)| bound == name)
|| self.params().any(|(param, _)| param == name)
}
}
}
pub(crate) fn missing_space(&self, path: &str) -> Option<String> {
let segments: Vec<&str> = path.split('.').collect();
let ["md", "doc", keys @ ..] = &segments[..] else {
return None;
};
let mut fields = &self.settings.doc;
for (i, key) in keys.iter().enumerate() {
let parent = i.checked_sub(1).map_or_else(
|| "the frontmatter".to_string(),
|before| format!("`{}`", keys[before]),
);
let joined = format!("{key}:");
if let Some(value) = fields.get(*key) {
match value {
Value::Object(inner) => {
fields = inner;
continue;
}
Value::String(text) => {
let next = keys.get(i + 1)?;
return text.starts_with(&format!("{next}:")).then(|| {
format!("`{key}` is the text `{text}`; a colon needs a space after it to make `{next}` a key with a value")
});
}
_ => return None,
}
}
let written = fields
.iter()
.find(|(name, value)| name.starts_with(&joined) && value.is_null())?
.0;
return Some(format!(
"{parent} has a key `{written}`; a colon needs a space after it to give `{key}` a value"
));
}
None
}
fn has_slot(&self, name: &str) -> bool {
self.definition
.and_then(|signature| signature.slots.as_ref())
.is_some_and(|slots| {
slots
.iter()
.any(|slot| slot.name().replace('-', "_") == name)
})
}
pub(crate) fn cel(&self) -> cel::Scope {
let mut md = BTreeMap::from([
(
"doc".to_string(),
Type::of_json(&Value::Object(self.settings.doc.clone())),
),
("data".to_string(), self.data()),
("version".to_string(), Type::String),
("lang".to_string(), Type::String),
("tz".to_string(), Type::String),
]);
if let Some(inputs) = self.inputs {
let fields = inputs
.iter()
.map(|input| {
(
input.declaration.name.clone(),
Type::declared(&input.declaration.declared),
)
})
.collect();
md.insert("input".into(), Type::record(fields));
}
if self.in_loop {
let fields = LOOP_FIELDS
.iter()
.map(|(name, ty)| ((*name).to_string(), ty.clone()))
.collect();
md.insert("loop".into(), Type::record(fields));
}
let mut scope = cel::Scope::default().with("md", Type::record(md));
for (name, declared) in self.params() {
scope = scope.with(name, Type::declared(declared));
}
for (name, ty) in &self.bindings {
scope = scope.with(name.clone(), ty.clone());
}
scope
}
fn data(&self) -> Type {
let fields = self
.settings
.data
.iter()
.map(|data| {
let ty = data.schema.as_ref().map_or(Type::Dyn, |schema| {
let record = schema
.iter()
.map(|field| {
let declaration = &field.declaration;
(
declaration.name.clone(),
Type::declared(&declaration.declared),
)
})
.collect();
Type::list(Type::record(record))
});
(data.name.clone(), ty)
})
.collect();
Type::record(fields)
}
}
fn has_keys(doc: &serde_json::Map<String, Value>, keys: &[&str]) -> bool {
let Some((first, rest)) = keys.split_first() else {
return true;
};
let mut value = doc.get(*first);
for key in rest {
value = value
.and_then(Value::as_object)
.and_then(|fields| fields.get(*key));
}
value.is_some()
}