1use std::collections::BTreeMap;
2use std::path::{Path, PathBuf};
3
4use syn::{Expr, ExprStruct, Item, ItemConst, Member};
5
6use crate::ModelError;
7use crate::syntax::{last_name, line_of, parsed_or_read, string_literal};
8
9pub const MANIFEST_CONSTANT: &str = "MODULE";
10const EXAMPLE: &str = "declare `pub const MODULE: sofie::Module = sofie::Module { name: \"order\", \
11 tier: sofie::Tier::Core, context: None, settings: &[], \
12 ..crate::wiring::GENERATED };`";
13
14#[derive(Clone, Debug, PartialEq, Eq)]
15pub struct Manifest {
16 pub name: String,
17 pub tier: Tier,
18 pub context: Option<String>,
19 pub settings: Vec<String>,
20 pub file: PathBuf,
21 pub line: usize,
22 pub declared: bool,
24}
25
26#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
28pub enum Tier {
29 Platform,
30 Core,
31 Experience,
32}
33
34impl Tier {
35 const ALL: [Self; 3] = [Self::Platform, Self::Core, Self::Experience];
36
37 #[must_use]
38 pub fn name(self) -> &'static str {
39 match self {
40 Self::Platform => "Platform",
41 Self::Core => "Core",
42 Self::Experience => "Experience",
43 }
44 }
45
46 #[must_use]
47 pub fn may_depend_on(self, other: Self) -> bool {
48 other <= self
49 }
50
51 fn from_name(name: &str) -> Option<Self> {
52 Self::ALL.into_iter().find(|tier| tier.name() == name)
53 }
54}
55
56#[must_use]
57pub fn manifest_file(directory: &Path) -> PathBuf {
58 directory.join("src").join("module.rs")
59}
60
61pub(crate) fn is_manifest_constant(file: &Path, name: &str) -> bool {
62 name == MANIFEST_CONSTANT && file.ends_with("src/module.rs")
63}
64
65pub(crate) fn read_manifest(
66 directory: &Path,
67 syntax: &BTreeMap<PathBuf, syn::File>,
68) -> Result<Option<Manifest>, ModelError> {
69 let file = manifest_file(directory);
70 if !file.is_file() {
71 return Ok(None);
72 }
73 let declared = syntax.contains_key(&file);
74 let parsed = parsed_or_read(syntax, &file)?;
75 let constant = parsed
76 .items
77 .iter()
78 .find_map(|item| match item {
79 Item::Const(constant) if constant.ident == MANIFEST_CONSTANT => Some(constant),
80 _ => None,
81 })
82 .ok_or_else(|| ModelError::in_file(&file, EXAMPLE))?;
83 let mut manifest = manifest_of(constant).map_err(|message| {
84 ModelError::new(&file, line_of(constant), format!("{message}; {EXAMPLE}"))
85 })?;
86 manifest.file = file;
87 manifest.declared = declared;
88 Ok(Some(manifest))
89}
90
91fn manifest_of(constant: &ItemConst) -> Result<Manifest, String> {
92 let Expr::Struct(fields) = constant.expr.as_ref() else {
93 return Err("the manifest must be a `Module { … }` literal".to_owned());
94 };
95 let name = field(fields, "name")
96 .and_then(string_literal)
97 .ok_or("`name` must be a string literal")?;
98 let tier = field(fields, "tier")
99 .and_then(last_path_segment)
100 .as_deref()
101 .and_then(Tier::from_name)
102 .ok_or("`tier` must be `Tier::Experience`, `Tier::Core` or `Tier::Platform`")?;
103 let context = field(fields, "context").map_or(Ok(None), context_of)?;
104 let settings = field(fields, "settings").and_then(setting_names).ok_or(
105 "`settings` must be `&[]` or `&[Setting { name: \"…\", … }, …]` with literal names",
106 )?;
107 Ok(Manifest {
108 name,
109 tier,
110 context,
111 settings,
112 file: PathBuf::new(),
113 line: line_of(constant),
114 declared: false,
115 })
116}
117
118fn field<'a>(fields: &'a ExprStruct, name: &str) -> Option<&'a Expr> {
119 fields.fields.iter().find_map(|field| match &field.member {
120 Member::Named(ident) if ident == name => Some(&field.expr),
121 _ => None,
122 })
123}
124
125fn last_path_segment(expression: &Expr) -> Option<String> {
126 let Expr::Path(path) = expression else {
127 return None;
128 };
129 last_name(&path.path)
130}
131
132fn context_of(expression: &Expr) -> Result<Option<String>, &'static str> {
133 const REFUSAL: &str = "`context` must be `None` or `Some(\"name\")` with a literal name";
134 if last_path_segment(expression).as_deref() == Some("None") {
135 return Ok(None);
136 }
137 let Expr::Call(call) = expression else {
138 return Err(REFUSAL);
139 };
140 if last_path_segment(&call.func).as_deref() != Some("Some") || call.args.len() != 1 {
141 return Err(REFUSAL);
142 }
143 call.args
144 .first()
145 .and_then(string_literal)
146 .map(Some)
147 .ok_or(REFUSAL)
148}
149
150fn setting_names(expression: &Expr) -> Option<Vec<String>> {
151 let Expr::Reference(reference) = expression else {
152 return None;
153 };
154 let Expr::Array(settings) = reference.expr.as_ref() else {
155 return None;
156 };
157 settings
158 .elems
159 .iter()
160 .map(|setting| match setting {
161 Expr::Struct(setting) => field(setting, "name").and_then(string_literal),
162 _ => None,
163 })
164 .collect()
165}