use crate::diagnostic::Location;
use crate::parse::definitions::{declaration, type_pointer};
use crate::parse::yaml::{self, Mark};
use crate::tree::Declaration;
use serde_json::{Map, Value};
use std::collections::HashMap;
#[derive(Clone, Debug, PartialEq)]
pub(crate) struct Located<T> {
pub(crate) value: T,
pub(crate) at: Location,
}
#[derive(Clone, Debug, PartialEq)]
pub(crate) enum Names {
All(Location),
Listed(Vec<NameEntry>),
}
#[derive(Clone, Debug, PartialEq)]
pub(crate) struct NameEntry {
pub(crate) name: String,
pub(crate) renamed: String,
pub(crate) at: Location,
}
#[derive(Clone, Debug, PartialEq)]
pub(crate) struct Import {
pub(crate) from: Located<String>,
pub(crate) integrity: Option<Located<String>>,
pub(crate) names: Names,
}
#[derive(Clone, Debug, PartialEq)]
pub(crate) struct Export {
pub(crate) pack: Option<String>,
pub(crate) version: Option<String>,
pub(crate) at: Location,
pub(crate) names: Names,
}
#[derive(Clone, Debug, PartialEq)]
pub(crate) struct DataImport {
pub(crate) from: Located<String>,
pub(crate) text_at: Location,
pub(crate) name: String,
pub(crate) schema: Option<Vec<Field>>,
pub(crate) fallback: Option<String>,
pub(crate) delimiter: Option<u8>,
}
#[derive(Clone, Debug, PartialEq)]
pub(crate) struct Field {
pub(crate) declaration: Declaration,
pub(crate) key: String,
}
#[derive(Clone, Debug, PartialEq)]
pub(crate) struct Input {
pub(crate) declaration: Declaration,
pub(crate) at: Location,
}
#[derive(Clone, Debug, Default, PartialEq)]
pub(crate) struct Settings {
pub(crate) doc: Map<String, Value>,
pub(crate) imports: Vec<Import>,
pub(crate) export: Option<Export>,
pub(crate) data: Vec<DataImport>,
pub(crate) inputs: Vec<Input>,
pub(crate) version: Option<String>,
pub(crate) lang: Option<String>,
pub(crate) tz: Option<String>,
}
impl Settings {
pub(crate) fn read(document: yaml::Document, text: &str) -> Self {
let reader = Reader {
lines: text.split('\n').collect(),
keys: document.keys,
values: document.values,
};
let Value::Object(mut doc) = document.value else {
return Self::default();
};
let md = doc.remove("md").unwrap_or(Value::Null);
let text = |key: &str| match md.get(key) {
Some(Value::String(text)) => Some(text.clone()),
Some(Value::Number(number)) => Some(number.to_string()),
_ => None,
};
Self {
imports: reader.list(&md, "import", Reader::import),
export: md.get("export").and_then(|export| reader.export(export)),
data: reader.list(&md, "data", Reader::data),
inputs: reader.inputs(&md),
version: text("version"),
lang: text("lang"),
tz: text("tz"),
doc,
}
}
}
struct Reader<'a> {
lines: Vec<&'a str>,
keys: HashMap<String, Mark>,
values: HashMap<String, Mark>,
}
impl Reader<'_> {
fn location(mark: Mark) -> Location {
Location::new(1 + mark.line, mark.column + 1)
}
fn key_at(&self, pointer: &str) -> Location {
self.keys
.get(pointer)
.copied()
.map_or(Location::new(1, 1), Self::location)
}
fn value_at(&self, pointer: &str) -> Location {
self.values
.get(pointer)
.copied()
.map_or(Location::new(1, 1), Self::location)
}
fn content_at(&self, pointer: &str) -> Location {
let at = self.value_at(pointer);
let quoted = self
.lines
.get(at.line.saturating_sub(2))
.and_then(|line| line.chars().nth(at.column - 1))
.is_some_and(|c| c == '"' || c == '\'');
Location::new(at.line, at.column + usize::from(quoted))
}
fn text(&self, entry: &Value, pointer: &str, key: &str) -> Option<Located<String>> {
Some(Located {
value: entry.get(key)?.as_str()?.to_string(),
at: self.value_at(&format!("{pointer}/{key}")),
})
}
fn list<T>(&self, md: &Value, key: &str, read: fn(&Self, &Value, &str) -> Option<T>) -> Vec<T> {
md.get(key)
.and_then(Value::as_array)
.into_iter()
.flatten()
.enumerate()
.filter_map(|(i, entry)| read(self, entry, &format!("/md/{key}/{i}")))
.collect()
}
fn import(&self, entry: &Value, pointer: &str) -> Option<Import> {
Some(Import {
from: self.text(entry, pointer, "from")?,
integrity: self.text(entry, pointer, "integrity"),
names: self.names(entry.get("names")?, &format!("{pointer}/names"))?,
})
}
fn export(&self, export: &Value) -> Option<Export> {
let text = |key: &str| export.get(key).and_then(Value::as_str).map(str::to_string);
Some(Export {
pack: text("pack"),
version: text("version"),
at: self.key_at("/md/export"),
names: self.names(export.get("names")?, "/md/export/names")?,
})
}
fn names(&self, names: &Value, pointer: &str) -> Option<Names> {
match names {
Value::String(all) if all == "*" => Some(Names::All(self.value_at(pointer))),
Value::Array(entries) => Some(Names::Listed(
entries
.iter()
.enumerate()
.filter_map(|(i, entry)| self.name_entry(entry, &format!("{pointer}/{i}")))
.collect(),
)),
_ => None,
}
}
fn name_entry(&self, entry: &Value, pointer: &str) -> Option<NameEntry> {
if let Some(name) = entry.as_str() {
return Some(NameEntry {
name: name.to_string(),
renamed: name.to_string(),
at: self.value_at(pointer),
});
}
let name = entry.get("name")?.as_str()?;
let renamed = entry.get("as").and_then(Value::as_str).unwrap_or(name);
Some(NameEntry {
name: name.to_string(),
renamed: renamed.to_string(),
at: self.value_at(&format!("{pointer}/name")),
})
}
fn data(&self, entry: &Value, pointer: &str) -> Option<DataImport> {
let schema = entry
.get("schema")
.and_then(Value::as_object)
.map(|fields| {
fields
.iter()
.map(|(name, declared)| Field {
declaration: self.declaration(
&format!("{pointer}/schema/{name}"),
name,
declared,
),
key: declared
.get("from")
.and_then(Value::as_str)
.unwrap_or(name)
.to_string(),
})
.collect()
});
let text = |key: &str| entry.get(key).and_then(Value::as_str);
Some(DataImport {
from: self.text(entry, pointer, "from")?,
text_at: self.content_at(&format!("{pointer}/from")),
name: text("as")?.to_string(),
schema,
fallback: text("fallback").map(str::to_string),
delimiter: text("delimiter")
.and_then(|delimiter| <[u8; 1]>::try_from(delimiter.as_bytes()).ok())
.map(|[byte]| byte),
})
}
fn inputs(&self, md: &Value) -> Vec<Input> {
md.get("inputs")
.and_then(Value::as_object)
.into_iter()
.flatten()
.map(|(name, declared)| Input {
declaration: self.declaration(&format!("/md/inputs/{name}"), name, declared),
at: self.key_at(&format!("/md/inputs/{name}")),
})
.collect()
}
fn declaration(&self, pointer: &str, name: &str, declared: &Value) -> Declaration {
let at = self.content_at(&type_pointer(pointer, declared));
declaration(name, declared, Some(at))
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::parse::{ParseOptions, parse};
fn settings(text: &str) -> Settings {
parse(text, &ParseOptions::default()).settings
}
#[test]
fn reads_imports_with_their_places() {
let read = settings(
"---\nmd:\n import:\n - from: \"@acme/kit@1\"\n integrity: sha512-A\n names: [chip, {name: tag, as: label}]\n - from: ./a.comp.md\n names: \"*\"\n - names: [x]\n - from: ./b.comp.md\n names: 3\n---\n",
);
assert_eq!(read.imports.len(), 2);
let kit = &read.imports[0];
assert_eq!(kit.from.value, "@acme/kit@1");
assert_eq!(kit.from.at, Location::new(4, 13));
assert_eq!(
kit.integrity.as_ref().map(|i| i.at),
Some(Location::new(5, 18))
);
let entry = |name: &str, renamed: &str, at| NameEntry {
name: name.into(),
renamed: renamed.into(),
at,
};
assert_eq!(
kit.names,
Names::Listed(vec![
entry("chip", "chip", Location::new(6, 15)),
entry("tag", "label", Location::new(6, 28)),
])
);
assert_eq!(read.imports[1].names, Names::All(Location::new(8, 14)));
}
#[test]
fn reads_exports_data_inputs_and_the_authors_keys() {
let read = settings(
"---\ntitle: T\nmd:\n export:\n pack: \"@a/b\"\n names: \"*\"\n data:\n - from: \"./x-{{md.input.y}}.csv\"\n as: x\n schema: {n: number}\n - from: ./y.csv\n inputs:\n y: string = \"\"\n---\n",
);
assert_eq!(read.doc.keys().collect::<Vec<_>>(), ["title"]);
let export = read.export.expect("an export");
assert_eq!(
(export.pack.as_deref(), export.at),
(Some("@a/b"), Location::new(4, 3))
);
assert_eq!(read.data.len(), 1);
assert_eq!(read.data[0].from.value, "./x-{{md.input.y}}.csv");
assert_eq!(
(read.data[0].from.at, read.data[0].text_at),
(Location::new(8, 13), Location::new(8, 14))
);
assert_eq!(read.data[0].schema.as_ref().map(Vec::len), Some(1));
assert_eq!(read.inputs[0].at, Location::new(13, 5));
}
#[test]
fn a_document_without_settings_has_none() {
assert_eq!(settings("text"), Settings::default());
assert_eq!(
settings("---\nmd:\n version: 2\n---\n"),
Settings::default()
);
assert_eq!(settings("---\n- a\n---\n"), Settings::default());
}
}