Skip to main content

sofie_model/
manifest.rs

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    /// The compiler only checks a manifest it builds.
23    pub declared: bool,
24}
25
26/// Ordered lowest first, so a module may depend on any tier `<=` its own.
27#[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}