1mod answers;
57mod front_matter;
58mod scan;
59
60use std::collections::{BTreeMap, HashMap, HashSet, VecDeque};
61use std::fmt;
62use std::path::{Component, Path, PathBuf};
63use std::sync::{Arc, Mutex, PoisonError};
64
65use schemars::JsonSchema;
66use serde::Serialize;
67use serde_json::Value;
68use sha2::{Digest as _, Sha256};
69
70pub use answers::Answers;
71use answers::Given;
72pub use front_matter::{DECLARATION_KEYS, FRONT_MATTER_KEYS, is_variable_name};
73
74#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, JsonSchema)]
76#[serde(rename_all = "kebab-case")]
77pub enum VariableType {
78 String,
80 Text,
82 Integer,
84 Boolean,
86 List,
88 Object,
90}
91
92impl VariableType {
93 pub const ALL: [Self; 6] = [
95 Self::String,
96 Self::Text,
97 Self::Integer,
98 Self::Boolean,
99 Self::List,
100 Self::Object,
101 ];
102
103 fn parse(text: &str) -> Option<Self> {
105 Self::ALL.into_iter().find(|kind| kind.as_str() == text)
106 }
107
108 #[must_use]
110 pub fn as_str(self) -> &'static str {
111 match self {
112 Self::String => "string",
113 Self::Text => "text",
114 Self::Integer => "integer",
115 Self::Boolean => "boolean",
116 Self::List => "list",
117 Self::Object => "object",
118 }
119 }
120}
121
122impl fmt::Display for VariableType {
123 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
124 formatter.write_str(self.as_str())
125 }
126}
127
128#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, JsonSchema)]
130#[serde(rename_all = "kebab-case")]
131pub enum ItemType {
132 String,
134 Object,
136}
137
138impl ItemType {
139 pub const ALL: [Self; 2] = [Self::String, Self::Object];
141
142 fn parse(text: &str) -> Option<Self> {
144 Self::ALL.into_iter().find(|items| items.as_str() == text)
145 }
146
147 #[must_use]
149 pub fn as_str(self) -> &'static str {
150 match self {
151 Self::String => "string",
152 Self::Object => "object",
153 }
154 }
155}
156
157#[derive(Debug, Clone, PartialEq, Serialize, JsonSchema)]
163pub struct TemplateVariable {
164 name: String,
166 description: String,
168 #[serde(rename = "type")]
170 kind: VariableType,
171 #[serde(skip_serializing_if = "Option::is_none")]
173 items: Option<ItemType>,
174 required: bool,
177 #[serde(skip_serializing_if = "Option::is_none")]
179 default: Option<Value>,
180 declared_in: String,
182}
183impl TemplateVariable {
186 #[must_use]
188 pub fn name(&self) -> &str {
189 &self.name
190 }
191
192 #[must_use]
194 pub fn description(&self) -> &str {
195 &self.description
196 }
197
198 #[must_use]
200 pub fn kind(&self) -> VariableType {
201 self.kind
202 }
203
204 #[must_use]
206 pub fn items(&self) -> Option<ItemType> {
207 self.items
208 }
209
210 #[must_use]
212 pub fn required(&self) -> bool {
213 self.required
214 }
215
216 #[must_use]
218 pub fn default(&self) -> Option<&Value> {
219 self.default.as_ref()
220 }
221
222 #[must_use]
224 pub fn declared_in(&self) -> &str {
225 &self.declared_in
226 }
227
228 #[must_use]
231 pub fn multi_line(&self) -> bool {
232 matches!(
233 self.kind,
234 VariableType::Text | VariableType::List | VariableType::Object
235 )
236 }
237
238 pub fn parse(&self, text: &str) -> Result<Value, String> {
245 let value = match self.kind {
246 VariableType::String | VariableType::Text => Value::String(text.to_owned()),
247 _ => {
248 serde_norway::from_str(text).map_err(|error| format!("it is not YAML: {error}"))?
249 }
250 };
251 self.check(&value)?;
252 Ok(value)
253 }
254
255 pub fn check(&self, value: &Value) -> Result<(), String> {
261 check_value(self.kind, self.items, value)
262 }
263}
264
265fn check_value(kind: VariableType, items: Option<ItemType>, value: &Value) -> Result<(), String> {
267 let fits = match kind {
268 VariableType::String => {
269 if let Value::String(text) = value
270 && text.contains(['\n', '\r'])
271 {
272 return Err("a `string` is one line; declare the variable `text` for more".into());
273 }
274 value.is_string()
275 }
276 VariableType::Text => value.is_string(),
277 VariableType::Integer => value.is_i64() || value.is_u64(),
278 VariableType::Boolean => value.is_boolean(),
279 VariableType::Object => value.is_object(),
280 VariableType::List => {
281 let Value::Array(entries) = value else {
282 return Err(format!("expected a list, got {}", describe(value)));
283 };
284 let items = items.unwrap_or(ItemType::String);
285 if let Some((index, entry)) =
286 entries.iter().enumerate().find(|(_, entry)| match items {
287 ItemType::String => !entry.is_string(),
288 ItemType::Object => !entry.is_object(),
289 })
290 {
291 return Err(format!(
292 "entry {index} of the list is {}, and its items are {}s",
293 describe(entry),
294 items.as_str()
295 ));
296 }
297 true
298 }
299 };
300 if fits {
301 Ok(())
302 } else {
303 Err(format!(
304 "expected {}, got {}",
305 article(kind),
306 describe(value)
307 ))
308 }
309}
310
311fn article(kind: VariableType) -> String {
313 match kind {
314 VariableType::Integer | VariableType::Object => format!("an {kind}"),
315 _ => format!("a {kind}"),
316 }
317}
318
319fn describe(value: &Value) -> String {
321 match value {
322 Value::Null => "null".to_owned(),
323 Value::Bool(value) => format!("the boolean {value}"),
324 Value::Number(number) => format!("the number {number}"),
325 Value::String(text) => format!("the string {text:?}"),
326 Value::Array(_) => "a list".to_owned(),
327 Value::Object(_) => "a mapping".to_owned(),
328 }
329}
330
331#[derive(Debug, Clone, Copy, PartialEq, Eq)]
333pub enum ChainField {
334 Type,
336 Items,
338}
339
340impl fmt::Display for ChainField {
341 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
342 formatter.write_str(match self {
343 Self::Type => "type",
344 Self::Items => "items",
345 })
346 }
347}
348
349#[derive(Debug, Clone, PartialEq, thiserror::Error)]
351#[non_exhaustive]
352pub enum TemplateError {
353 #[error(
355 "template {name:?} was not found{}\nnext: {}",
356 referenced_from.as_ref().map(|from| format!(" (named by {from})")).unwrap_or_default(),
357 if searched.is_empty() {
358 "give the directory holding it as a search path, or register it by name.".to_owned()
359 } else {
360 format!("it was looked for in {}; add the directory holding it as a search path.", searched.join(", "))
361 }
362 )]
363 NotFound {
364 name: String,
366 referenced_from: Option<String>,
368 searched: Vec<String>,
370 },
371 #[error(
374 "template {name} could not be read: {message}\n\
375 next: name a readable file, or give it the permissions this process reads with."
376 )]
377 Unreadable {
378 name: String,
380 message: String,
382 },
383 #[error(
386 "search path {directory} cannot be searched: {message}\n\
387 next: give a directory that exists, or leave it out of the search path."
388 )]
389 SearchPath {
390 directory: String,
392 message: String,
394 },
395 #[error(
397 "template {file}: {}{message}\nnext: {}",
398 key.as_ref().map(|key| format!("front matter key `{key}` ")).unwrap_or_default(),
399 "correct that file; a template's front matter is described in the README under \"Task templates\"."
400 )]
401 Malformed {
402 file: String,
404 key: Option<String>,
406 message: String,
408 },
409 #[error(
411 "template variable {variable:?} is declared with {field} {nearer_value} in {nearer} and \
412 with {field} {farther_value} in {farther}; a redeclaration may change `description`, \
413 `default` and `required`, never `type` or `items`\n\
414 next: declare it with the same {field} in both files, or rename one of them."
415 )]
416 ChainConflict {
417 variable: String,
419 field: ChainField,
421 nearer: String,
423 nearer_value: &'static str,
425 farther: String,
427 farther_value: &'static str,
429 },
430 #[error(
432 "the answers document is refused: {message}\n\
433 next: write the answers as a YAML mapping from variable name to value."
434 )]
435 MalformedAnswers {
436 message: String,
438 },
439 #[error(
441 "{} no variable the template declares: {}\n\
442 next: remove {}, or declare {} in the template's front matter — `onetaskgraph template \
443 variables` lists what it declares.",
444 if names.len() == 1 { "an answer names" } else { "answers name" },
445 names.join(", "),
446 if names.len() == 1 { "that answer" } else { "those answers" },
447 if names.len() == 1 { "it" } else { "them" }
448 )]
449 UnknownAnswer {
450 names: Vec<String>,
452 },
453 #[error(
455 "the answer to {name:?} is not {expected}: {problem}\n\
456 next: give {name} a value of type {kind}."
457 )]
458 MistypedAnswer {
459 name: String,
461 kind: VariableType,
463 expected: String,
465 problem: String,
467 },
468 #[error(
470 "{} unanswered: {}\n\
471 next: answer {} with --var NAME=VALUE or in an answers file (--answers FILE), or run \
472 interactively to be asked.",
473 if names.len() == 1 { "a required variable is" } else { "required variables are" },
474 names.join(", "),
475 if names.len() == 1 { "it" } else { "each" }
476 )]
477 MissingRequired {
478 names: Vec<String>,
480 },
481 #[error(
483 "template {}{}: {message}\n\
484 next: {}",
485 file.as_deref().unwrap_or("?"),
486 line.map(|line| format!(" line {line}")).unwrap_or_default(),
487 match name {
488 Some(name) => format!(
489 "declare {name} in the front matter of a file in the chain, or set it in the \
490 template before it is used."
491 ),
492 None => "correct the template at that line.".to_owned(),
493 }
494 )]
495 Render {
496 file: Option<String>,
498 line: Option<usize>,
500 name: Option<String>,
502 message: String,
504 },
505}
506
507impl TemplateError {
508 #[must_use]
510 pub fn kind(&self) -> &'static str {
511 match self {
512 Self::NotFound { .. } => "template-not-found",
513 Self::Unreadable { .. } => "template-unreadable",
514 Self::SearchPath { .. } => "template-search-path",
515 Self::Malformed { .. } => "template-malformed",
516 Self::ChainConflict { .. } => "template-chain-conflict",
517 Self::MalformedAnswers { .. } => "template-answers-malformed",
518 Self::UnknownAnswer { .. } => "template-unknown-answer",
519 Self::MistypedAnswer { .. } => "template-mistyped-answer",
520 Self::MissingRequired { .. } => "template-missing-required",
521 Self::Render { .. } => "template-render",
522 }
523 }
524
525 #[must_use]
529 pub fn refuses_answers(&self) -> bool {
530 matches!(
531 self,
532 Self::MalformedAnswers { .. }
533 | Self::UnknownAnswer { .. }
534 | Self::MistypedAnswer { .. }
535 | Self::MissingRequired { .. }
536 )
537 }
538
539 fn malformed(file: &str, key: Option<&str>, message: impl Into<String>) -> Self {
540 Self::Malformed {
541 file: file.to_owned(),
542 key: key.map(str::to_owned),
543 message: message.into(),
544 }
545 }
546}
547
548#[derive(Debug, Clone, Default)]
554pub struct TemplateLoader {
555 directories: Vec<PathBuf>,
556 registered: Vec<(String, String)>,
557}
558
559impl TemplateLoader {
560 #[must_use]
562 pub fn new() -> Self {
563 Self::default()
564 }
565
566 #[must_use]
568 pub fn with_directory(mut self, directory: impl Into<PathBuf>) -> Self {
569 self.directories.push(directory.into());
570 self
571 }
572
573 #[must_use]
575 pub fn with_template(mut self, name: impl Into<String>, source: impl Into<String>) -> Self {
576 self.registered.push((name.into(), source.into()));
577 self
578 }
579
580 pub fn load_path(&self, path: &Path) -> Result<Template, TemplateError> {
592 self.searchable()?;
593 let shown = path.display().to_string();
594 let bytes = std::fs::read(path).map_err(|error| {
595 if error.kind() == std::io::ErrorKind::NotFound {
596 TemplateError::NotFound {
597 name: shown.clone(),
598 referenced_from: None,
599 searched: Vec::new(),
600 }
601 } else {
602 TemplateError::Unreadable {
603 name: shown.clone(),
604 message: error.to_string(),
605 }
606 }
607 })?;
608 let source = String::from_utf8(bytes)
609 .map_err(|_| TemplateError::malformed(&shown, None, "it is not UTF-8 text"))?;
610 let name = path
611 .file_name()
612 .map_or_else(|| shown.clone(), |name| name.to_string_lossy().into_owned());
613 self.load_chain(name, source)
614 }
615
616 pub fn load_name(&self, name: &str) -> Result<Template, TemplateError> {
624 self.searchable()?;
625 let source = self.find(name)?.ok_or_else(|| self.not_found(name, None))?;
626 self.load_chain(name.to_owned(), source)
627 }
628
629 fn find(&self, name: &str) -> Result<Option<String>, TemplateError> {
637 let path = Path::new(name);
638 let spelled_within = !name.is_empty()
639 && path
640 .components()
641 .all(|component| matches!(component, Component::Normal(_)));
642 if spelled_within {
643 for directory in &self.directories {
644 let candidate = directory.join(path);
645 if candidate.is_file() {
646 let bytes =
647 std::fs::read(&candidate).map_err(|error| TemplateError::Unreadable {
648 name: candidate.display().to_string(),
649 message: error.to_string(),
650 })?;
651 return String::from_utf8(bytes)
652 .map(Some)
653 .map_err(|_| TemplateError::malformed(name, None, "it is not UTF-8 text"));
654 }
655 }
656 }
657 Ok(self
658 .registered
659 .iter()
660 .find(|(registered, _)| registered == name)
661 .map(|(_, source)| source.clone()))
662 }
663
664 fn not_found(&self, name: &str, referenced_from: Option<&str>) -> TemplateError {
665 TemplateError::NotFound {
666 name: name.to_owned(),
667 referenced_from: referenced_from.map(str::to_owned),
668 searched: self
669 .directories
670 .iter()
671 .map(|directory| directory.display().to_string())
672 .collect(),
673 }
674 }
675
676 fn load_chain(&self, root: String, source: String) -> Result<Template, TemplateError> {
683 let resolutions = Resolutions::new();
684 let files = self.build(root.clone(), source, &resolutions)?;
685 let variables = merge(&files)?;
686 let Discovered { read, .. } = discover(&root, &files, &variables, self, &Answers::new());
687 let files = in_read_order(files, &read);
688 let variables = merge(&files)?;
689 Ok(Template::assemble(
690 root,
691 files,
692 variables,
693 resolutions,
694 self.clone(),
695 ))
696 }
697
698 fn searchable(&self) -> Result<(), TemplateError> {
700 for directory in &self.directories {
701 let refused = |message: String| TemplateError::SearchPath {
702 directory: directory.display().to_string(),
703 message,
704 };
705 match std::fs::metadata(directory) {
706 Ok(metadata) if metadata.is_dir() => {}
707 Ok(_) => return Err(refused("it is not a directory".to_owned())),
708 Err(error) => return Err(refused(error.to_string())),
709 }
710 }
711 Ok(())
712 }
713
714 fn build(
721 &self,
722 name: String,
723 source: String,
724 resolutions: &Resolutions,
725 ) -> Result<Vec<ChainFile>, TemplateError> {
726 let mut files = Vec::new();
727 let mut seen: HashSet<String> = HashSet::new();
728 let mut pending = vec![(name, source)];
729 while let Some((name, source)) = pending.pop() {
731 if !seen.insert(name.clone()) {
732 continue;
733 }
734 let split = front_matter::split(&name, &source)?;
735 compile_check(&name, &split.body, split.offset_lines)?;
736 let mut names = Vec::new();
737 let mut children = Vec::new();
738 for named in scan::named(&split.body) {
739 let references = match named {
740 scan::Named::Literal(reference) => vec![reference],
741 scan::Named::Expression { ordinal, .. } => resolutions
743 .get(&NamingTag {
744 file: name.clone(),
745 ordinal,
746 })
747 .into_iter()
748 .flatten()
749 .map(|candidates| scan::Reference {
750 candidates: candidates.names().to_vec(),
751 optional: true,
752 })
753 .collect(),
754 };
755 for reference in references {
756 let mut resolved = false;
759 for candidate in &reference.candidates {
760 if seen.contains(candidate) || *candidate == name {
761 names.push(candidate.clone());
762 resolved = true;
763 break;
764 }
765 if let Some(source) = self.find(candidate)? {
766 names.push(candidate.clone());
767 children.push((candidate.clone(), source));
768 resolved = true;
769 break;
770 }
771 }
772 if !resolved && !reference.optional {
773 return Err(self.not_found(&reference.candidates.join(" or "), Some(&name)));
774 }
775 }
776 }
777 pending.extend(children.into_iter().rev());
778 files.push(ChainFile {
779 name,
780 source,
781 body: split.body,
782 offset_lines: split.offset_lines,
783 declarations: split.declarations,
784 names,
785 });
786 }
787 Ok(files)
788 }
789}
790
791#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
794struct NamingTag {
795 file: String,
796 ordinal: usize,
797}
798
799#[derive(Debug, Clone, PartialEq, Eq)]
802struct Candidates(Vec<String>);
803
804impl Candidates {
805 fn of(value: &minijinja::Value) -> Option<Self> {
808 let names: Vec<String> = match value.as_str() {
809 Some(name) => vec![name.to_owned()],
810 None if value.kind() == minijinja::value::ValueKind::Seq => value
811 .try_iter()
812 .into_iter()
813 .flatten()
814 .filter_map(|item| item.as_str().map(str::to_owned))
815 .collect(),
816 None => Vec::new(),
817 };
818 (!names.is_empty()).then_some(Self(names))
819 }
820
821 fn names(&self) -> &[String] {
822 &self.0
823 }
824}
825
826type Resolutions = BTreeMap<NamingTag, Vec<Candidates>>;
829
830fn hooked_bodies(files: &[ChainFile]) -> HashMap<String, String> {
833 files
834 .iter()
835 .enumerate()
836 .map(|(index, file)| (file.name.clone(), scan::hooked(&file.body, index)))
837 .collect()
838}
839
840fn record_names(
843 environment: &mut minijinja::Environment<'static>,
844 files: &[ChainFile],
845) -> Arc<Mutex<Resolutions>> {
846 let names: Vec<String> = files.iter().map(|file| file.name.clone()).collect();
847 let recorded: Arc<Mutex<Resolutions>> = Arc::default();
848 let sink = Arc::clone(&recorded);
849 environment.add_function(
850 scan::HOOK,
851 move |file: usize, ordinal: usize, value: minijinja::Value| {
852 if let Some(name) = names.get(file)
853 && let Some(candidates) = Candidates::of(&value)
854 {
855 let mut recorded = sink.lock().unwrap_or_else(PoisonError::into_inner);
856 let found = recorded
857 .entry(NamingTag {
858 file: name.clone(),
859 ordinal,
860 })
861 .or_default();
862 if !found.contains(&candidates) {
863 found.push(candidates);
864 }
865 }
866 value
867 },
868 );
869 recorded
870}
871
872fn compile_check(name: &str, body: &str, offset_lines: usize) -> Result<(), TemplateError> {
874 let environment = environment();
875 environment
876 .template_from_named_str(name, body)
877 .map(|_| ())
878 .map_err(|error| TemplateError::Malformed {
879 file: name.to_owned(),
880 key: None,
881 message: format!(
882 "its body does not parse{}: {}",
883 error
884 .line()
885 .map(|line| format!(" at line {}", line + offset_lines))
886 .unwrap_or_default(),
887 error.detail().unwrap_or("a syntax error")
888 ),
889 })
890}
891
892#[derive(Debug, Clone)]
894struct ChainFile {
895 name: String,
896 source: String,
897 body: String,
898 offset_lines: usize,
899 declarations: Vec<TemplateVariable>,
900 names: Vec<String>,
903}
904
905fn merge(files: &[ChainFile]) -> Result<Vec<TemplateVariable>, TemplateError> {
910 let index_of: HashMap<&str, usize> = files
911 .iter()
912 .enumerate()
913 .map(|(index, file)| (file.name.as_str(), index))
914 .collect();
915 let mut depth = vec![usize::MAX; files.len()];
916 if !files.is_empty() {
917 depth[0] = 0;
918 }
919 let mut queue = VecDeque::from([0usize]);
920 while let Some(at) = queue.pop_front() {
921 for child in &files[at].names {
922 if let Some(&child) = index_of.get(child.as_str())
923 && depth[child] == usize::MAX
924 {
925 depth[child] = depth[at] + 1;
926 queue.push_back(child);
927 }
928 }
929 }
930 let mut nearness: Vec<usize> = (0..files.len()).collect();
931 nearness.sort_by_key(|&index| (depth[index], index));
932
933 let mut merged: BTreeMap<&str, TemplateVariable> = BTreeMap::new();
934 for &index in &nearness {
935 let file = &files[index];
936 for declaration in &file.declarations {
937 match merged.get(declaration.name.as_str()) {
938 None => {
939 merged.insert(&declaration.name, declaration.clone());
940 }
941 Some(nearer) => {
942 if nearer.kind != declaration.kind {
943 return Err(conflict(
944 nearer,
945 ChainField::Type,
946 nearer.kind.as_str(),
947 &file.name,
948 declaration.kind.as_str(),
949 &declaration.name,
950 ));
951 }
952 if nearer.items != declaration.items {
953 let spell =
954 |items: Option<ItemType>| items.map_or("none", ItemType::as_str);
955 return Err(conflict(
956 nearer,
957 ChainField::Items,
958 spell(nearer.items),
959 &file.name,
960 spell(declaration.items),
961 &declaration.name,
962 ));
963 }
964 }
965 }
966 }
967 }
968
969 let mut ordered = Vec::with_capacity(merged.len());
970 for file in files {
971 for declaration in &file.declarations {
972 if let Some(variable) = merged.remove(declaration.name.as_str()) {
973 ordered.push(variable);
974 }
975 }
976 }
977 Ok(ordered)
978}
979
980fn conflict(
981 nearer: &TemplateVariable,
982 field: ChainField,
983 nearer_value: &'static str,
984 farther: &str,
985 farther_value: &'static str,
986 variable: &str,
987) -> TemplateError {
988 TemplateError::ChainConflict {
989 variable: variable.to_owned(),
990 field,
991 nearer: nearer.declared_in.clone(),
992 nearer_value,
993 farther: farther.to_owned(),
994 farther_value,
995 }
996}
997
998fn digest<'a>(files: impl IntoIterator<Item = (&'a str, &'a str)>) -> String {
1000 let mut hasher = Sha256::new();
1001 for (name, source) in files {
1002 hasher.update(name.as_bytes());
1003 hasher.update([0]);
1004 hasher.update(source.as_bytes());
1005 hasher.update([0]);
1006 }
1007 let hash = hasher.finalize();
1008 let mut rendered = String::with_capacity(7 + hash.len() * 2);
1009 rendered.push_str("sha256:");
1010 for byte in hash {
1011 rendered.push_str(&format!("{byte:02x}"));
1012 }
1013 rendered
1014}
1015
1016fn environment() -> minijinja::Environment<'static> {
1018 let mut environment = minijinja::Environment::new();
1019 environment.set_undefined_behavior(minijinja::UndefinedBehavior::Strict);
1020 environment.set_auto_escape_callback(|_| minijinja::AutoEscape::None);
1021 environment.set_keep_trailing_newline(true);
1022 environment.set_trim_blocks(true);
1023 environment.set_lstrip_blocks(true);
1024 environment
1025}
1026
1027#[derive(Debug, Clone)]
1029pub struct Template {
1030 name: String,
1031 files: Vec<ChainFile>,
1032 variables: Vec<TemplateVariable>,
1033 resolutions: Resolutions,
1035 digest: String,
1036 loader: TemplateLoader,
1037}
1038
1039#[derive(Debug, Clone, PartialEq, Serialize, JsonSchema)]
1041pub struct TemplateVariables {
1042 pub template: String,
1044 pub digest: String,
1048 pub variables: Vec<TemplateVariable>,
1050}
1051
1052#[derive(Debug, Clone, PartialEq, Serialize, JsonSchema)]
1054pub struct RenderedTemplate {
1055 pub body: String,
1057 pub digest: String,
1062 pub answers: BTreeMap<String, Value>,
1065}
1066
1067impl Template {
1068 fn assemble(
1069 name: String,
1070 files: Vec<ChainFile>,
1071 variables: Vec<TemplateVariable>,
1072 resolutions: Resolutions,
1073 loader: TemplateLoader,
1074 ) -> Self {
1075 let digest = digest(
1076 files
1077 .iter()
1078 .map(|file| (file.name.as_str(), file.source.as_str())),
1079 );
1080 Self {
1081 name,
1082 files,
1083 variables,
1084 resolutions,
1085 digest,
1086 loader,
1087 }
1088 }
1089
1090 pub fn expand(&self, answers: &Answers) -> Result<Self, TemplateError> {
1117 let mut expanded = self.clone();
1118 let mut named = vec![expanded.resolutions.clone()];
1119 loop {
1120 let Discovered {
1121 resolutions,
1122 read,
1123 failure,
1124 } = discover(
1125 &expanded.name,
1126 &expanded.files,
1127 &expanded.variables,
1128 &expanded.loader,
1129 answers,
1130 );
1131 if let Some(error) = failure {
1132 return Err(error);
1133 }
1134 if resolutions == expanded.resolutions {
1135 let files = in_read_order(expanded.files, &read);
1136 let variables = merge(&files)?;
1137 return Ok(Self::assemble(
1138 self.name.clone(),
1139 files,
1140 variables,
1141 resolutions,
1142 self.loader.clone(),
1143 ));
1144 }
1145 if named.contains(&resolutions) {
1146 return Err(TemplateError::malformed(
1147 &self.name,
1148 None,
1149 "what its expressions name never settles: each template they name gives a \
1150 default naming another, round a cycle; answer the variable that decides it",
1151 ));
1152 }
1153 named.push(resolutions.clone());
1154 let files = expanded.rebuild(&resolutions)?;
1155 let variables = merge(&files)?;
1156 expanded = Self::assemble(
1157 self.name.clone(),
1158 files,
1159 variables,
1160 resolutions,
1161 self.loader.clone(),
1162 );
1163 }
1164 }
1165
1166 fn rebuild(&self, resolutions: &Resolutions) -> Result<Vec<ChainFile>, TemplateError> {
1167 let root = self
1168 .files
1169 .first()
1170 .map_or_else(String::new, |file| file.source.clone());
1171 self.loader.build(self.name.clone(), root, resolutions)
1172 }
1173
1174 #[must_use]
1176 pub fn name(&self) -> &str {
1177 &self.name
1178 }
1179
1180 #[must_use]
1182 pub fn digest(&self) -> &str {
1183 &self.digest
1184 }
1185
1186 #[must_use]
1188 pub fn variables(&self) -> &[TemplateVariable] {
1189 &self.variables
1190 }
1191
1192 pub fn chain(&self) -> impl Iterator<Item = &str> {
1197 self.files.iter().map(|file| file.name.as_str())
1198 }
1199
1200 #[must_use]
1202 pub fn describe(&self) -> TemplateVariables {
1203 TemplateVariables {
1204 template: self.name.clone(),
1205 digest: self.digest.clone(),
1206 variables: self.variables.clone(),
1207 }
1208 }
1209
1210 pub fn unanswered(&self, answers: &Answers) -> Result<Vec<TemplateVariable>, TemplateError> {
1220 let expanded = self.expand(answers)?;
1221 let typed = expanded.typed(answers)?;
1222 Ok(expanded
1223 .variables
1224 .into_iter()
1225 .filter(|variable| !typed.contains_key(&variable.name))
1226 .collect())
1227 }
1228
1229 pub fn resolve(&self, answers: &Answers) -> Result<BTreeMap<String, Value>, TemplateError> {
1238 self.expand(answers)?.resolve_here(answers)
1239 }
1240
1241 fn resolve_here(&self, answers: &Answers) -> Result<BTreeMap<String, Value>, TemplateError> {
1243 let mut typed = self.typed(answers)?;
1244 let missing: Vec<String> = self
1245 .variables
1246 .iter()
1247 .filter(|variable| {
1248 variable.required
1249 && variable.default.is_none()
1250 && !typed.contains_key(&variable.name)
1251 })
1252 .map(|variable| variable.name.clone())
1253 .collect();
1254 if !missing.is_empty() {
1255 return Err(TemplateError::MissingRequired { names: missing });
1256 }
1257 for variable in &self.variables {
1258 typed
1259 .entry(variable.name.clone())
1260 .or_insert_with(|| variable.default.clone().unwrap_or(Value::Null));
1261 }
1262 Ok(typed)
1263 }
1264
1265 pub fn render(&self, answers: &Answers) -> Result<RenderedTemplate, TemplateError> {
1274 self.expand(answers)?.render_here(answers)
1275 }
1276
1277 fn render_here(&self, answers: &Answers) -> Result<RenderedTemplate, TemplateError> {
1283 let resolved = self.resolve_here(answers)?;
1284
1285 let offsets: HashMap<String, usize> = self
1286 .files
1287 .iter()
1288 .map(|file| (file.name.clone(), file.offset_lines))
1289 .collect();
1290 let bodies: HashMap<String, String> = self
1291 .files
1292 .iter()
1293 .map(|file| (file.name.clone(), file.body.clone()))
1294 .collect();
1295 let mut environment = environment();
1296 environment.set_loader(move |name| Ok(bodies.get(name).cloned()));
1297 let rendered = environment
1298 .get_template(&self.name)
1299 .and_then(|template| template.render(&resolved))
1300 .map_err(|error| render_error(&error, &offsets))?;
1301 Ok(RenderedTemplate {
1302 body: rendered,
1303 digest: self.digest.clone(),
1304 answers: resolved,
1305 })
1306 }
1307
1308 fn typed(&self, answers: &Answers) -> Result<BTreeMap<String, Value>, TemplateError> {
1310 let declared: HashMap<&str, &TemplateVariable> = self
1311 .variables
1312 .iter()
1313 .map(|variable| (variable.name.as_str(), variable))
1314 .collect();
1315 let unknown: Vec<String> = answers
1316 .names()
1317 .filter(|name| !declared.contains_key(name))
1318 .map(str::to_owned)
1319 .collect();
1320 if !unknown.is_empty() {
1321 return Err(TemplateError::UnknownAnswer { names: unknown });
1322 }
1323
1324 let mut typed = BTreeMap::new();
1325 for variable in &self.variables {
1326 let value = match answers.given(&variable.name) {
1327 None => continue,
1328 Some(Given::Value(value)) => variable.check(value).map(|()| value.clone()),
1329 Some(Given::Text(text)) => variable.parse(text),
1330 };
1331 let value = value.map_err(|problem| TemplateError::MistypedAnswer {
1332 name: variable.name.clone(),
1333 kind: variable.kind,
1334 expected: article(variable.kind),
1335 problem,
1336 })?;
1337 typed.insert(variable.name.clone(), value);
1338 }
1339 Ok(typed)
1340 }
1341}
1342
1343fn discover(
1355 root: &str,
1356 files: &[ChainFile],
1357 variables: &[TemplateVariable],
1358 loader: &TemplateLoader,
1359 answers: &Answers,
1360) -> Discovered {
1361 let mut context = BTreeMap::new();
1362 for variable in variables {
1363 let answered = match answers.given(&variable.name) {
1364 Some(Given::Value(value)) => variable.check(value).ok().map(|()| value.clone()),
1365 Some(Given::Text(text)) => variable.parse(text).ok(),
1366 None => None,
1367 };
1368 if let Some(value) = answered.or_else(|| variable.default.clone()) {
1369 context.insert(variable.name.clone(), value);
1370 } else if !variable.required {
1371 context.insert(variable.name.clone(), Value::Null);
1372 }
1373 }
1374
1375 let bodies = hooked_bodies(files);
1376 let loader = loader.clone();
1377 let failed: Arc<Mutex<Option<TemplateError>>> = Arc::default();
1378 let failure = Arc::clone(&failed);
1379 let read: Arc<Mutex<Vec<String>>> = Arc::default();
1380 let reading = Arc::clone(&read);
1381 let mut environment = environment();
1382 environment.set_undefined_behavior(minijinja::UndefinedBehavior::Lenient);
1383 let recorded = record_names(&mut environment, files);
1384 environment.set_loader(move |name| {
1385 let mut reading = reading.lock().unwrap_or_else(PoisonError::into_inner);
1386 if let Some(body) = bodies.get(name) {
1387 if !reading.iter().any(|read| read == name) {
1388 reading.push(name.to_owned());
1389 }
1390 return Ok(Some(body.clone()));
1391 }
1392 let source = match loader.find(name) {
1393 Ok(Some(source)) => source,
1394 Ok(None) => return Ok(None),
1395 Err(error) => {
1396 failure
1397 .lock()
1398 .unwrap_or_else(PoisonError::into_inner)
1399 .get_or_insert(error);
1400 return Ok(None);
1401 }
1402 };
1403 Ok(Some(
1404 front_matter::split(name, &source)
1405 .map(|split| split.body)
1406 .unwrap_or_default(),
1407 ))
1408 });
1409 let _ = environment
1410 .get_template(root)
1411 .and_then(|template| template.render(&context));
1412 let failure = failed.lock().unwrap_or_else(PoisonError::into_inner).take();
1413 let resolutions = recorded
1414 .lock()
1415 .unwrap_or_else(PoisonError::into_inner)
1416 .clone();
1417 let read = read.lock().unwrap_or_else(PoisonError::into_inner).clone();
1418 Discovered {
1419 resolutions,
1420 read,
1421 failure,
1422 }
1423}
1424
1425struct Discovered {
1429 resolutions: Resolutions,
1430 read: Vec<String>,
1431 failure: Option<TemplateError>,
1432}
1433
1434fn in_read_order(files: Vec<ChainFile>, read: &[String]) -> Vec<ChainFile> {
1438 let mut files: Vec<(usize, ChainFile)> = files
1439 .into_iter()
1440 .enumerate()
1441 .map(|(index, file)| {
1442 let at = if index == 0 {
1443 0
1444 } else {
1445 read.iter()
1446 .position(|name| *name == file.name)
1447 .map_or(read.len() + index, |at| at + 1)
1448 };
1449 (at, file)
1450 })
1451 .collect();
1452 files.sort_by_key(|(at, _)| *at);
1453 files.into_iter().map(|(_, file)| file).collect()
1454}
1455
1456fn render_error(error: &minijinja::Error, offsets: &HashMap<String, usize>) -> TemplateError {
1461 let mut error = error;
1462 while let Some(inner) = std::error::Error::source(error)
1463 .and_then(|source| source.downcast_ref::<minijinja::Error>())
1464 {
1465 error = inner;
1466 }
1467 let file = error.name().map(str::to_owned);
1468 let offset = file
1469 .as_deref()
1470 .and_then(|file| offsets.get(file))
1471 .copied()
1472 .unwrap_or(0);
1473 let line = error.line().map(|line| line + offset);
1474 if error.kind() == minijinja::ErrorKind::UndefinedError {
1475 let name = error
1476 .template_source()
1477 .zip(error.range())
1478 .and_then(|(source, range)| source.get(range))
1479 .map(str::trim)
1480 .filter(|name| !name.is_empty())
1481 .map(str::to_owned);
1482 return TemplateError::Render {
1483 message: match &name {
1484 Some(name) => format!(
1485 "{name} is undefined: it is neither a declared variable nor set by the template"
1486 ),
1487 None => "a value there is undefined".to_owned(),
1488 },
1489 file,
1490 line,
1491 name,
1492 };
1493 }
1494 TemplateError::Render {
1495 file,
1496 line,
1497 name: None,
1498 message: error
1499 .detail()
1500 .map_or_else(|| error.kind().to_string(), str::to_owned),
1501 }
1502}