Skip to main content

pray_core/
package_spec.rs

1use crate::hashing::sha256_prefixed;
2use crate::literal::{
3    find_top_level, is_balanced, parse_literal, parse_literal_map, prepare_parser_lines,
4    split_top_level, LiteralValue,
5};
6use crate::{PrayError, PrayResult};
7use serde::{Deserialize, Serialize};
8use std::borrow::Cow;
9use std::collections::BTreeMap;
10
11#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
12pub struct PackageSpec {
13    pub name: String,
14    pub version: String,
15    pub summary: Option<String>,
16    pub description: Option<String>,
17    pub authors: Vec<String>,
18    pub license: Option<String>,
19    pub homepage: Option<String>,
20    pub source_code_uri: Option<String>,
21    pub changelog_uri: Option<String>,
22    pub prayfile_version: Option<String>,
23    pub files: Vec<String>,
24    pub exports: BTreeMap<String, PackageExport>,
25    pub skills: BTreeMap<String, PackageSkill>,
26    pub templates: BTreeMap<String, PackageTemplate>,
27    pub adapters: BTreeMap<String, String>,
28    pub targets: Vec<String>,
29    pub dependencies: Vec<PackageDependency>,
30    pub metadata: BTreeMap<String, LiteralValue>,
31}
32
33#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
34pub struct PackageExport {
35    pub kind: String,
36    pub path: String,
37    pub summary: Option<String>,
38    #[serde(default, skip_serializing_if = "Vec::is_empty")]
39    pub only: Vec<String>,
40    #[serde(default, skip_serializing_if = "Vec::is_empty")]
41    pub except: Vec<String>,
42    #[serde(default, skip_serializing_if = "Option::is_none")]
43    pub default_path: Option<String>,
44}
45
46#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
47pub struct PackageSkill {
48    pub path: String,
49    pub summary: Option<String>,
50}
51
52#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
53pub struct PackageTemplate {
54    pub path: String,
55    pub summary: Option<String>,
56}
57
58#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
59pub struct PackageDependency {
60    pub name: String,
61    pub constraint: String,
62    pub optional: bool,
63}
64
65impl PackageSpec {
66    pub fn canonicalized(&self) -> Self {
67        let mut package = self.clone();
68        package.files.sort();
69        package.authors.sort();
70        package.targets.sort();
71        package.dependencies.sort_by(|left, right| {
72            left.name
73                .cmp(&right.name)
74                .then(left.constraint.cmp(&right.constraint))
75                .then(left.optional.cmp(&right.optional))
76        });
77        package
78    }
79
80    pub fn tree_hash_for_root(&self, root: &std::path::Path) -> PrayResult<String> {
81        let mut file_bytes = BTreeMap::new();
82        for file in &self.files {
83            let path = root.join(file);
84            if !path.exists() {
85                return Err(PrayError::Integrity(format!(
86                    "package file missing: {}",
87                    file
88                )));
89            }
90            if path.is_dir() {
91                return Err(PrayError::Integrity(format!(
92                    "package file is a directory: {}",
93                    file
94                )));
95            }
96            file_bytes.insert(file.clone(), std::fs::read(&path)?);
97        }
98        Self::tree_hash_from_file_bytes(&file_bytes)
99    }
100
101    pub fn tree_hash_from_file_bytes(file_bytes: &BTreeMap<String, Vec<u8>>) -> PrayResult<String> {
102        let mut entries = Vec::new();
103        for (path, bytes) in file_bytes {
104            entries.push((path.clone(), sha256_prefixed(bytes)));
105        }
106        entries.sort_by(|left, right| left.0.cmp(&right.0));
107
108        let mut serialized = String::new();
109        for (path, hash) in entries {
110            serialized.push_str("file");
111            serialized.push('\0');
112            serialized.push_str("regular");
113            serialized.push('\0');
114            serialized.push_str(&path);
115            serialized.push('\0');
116            serialized.push_str(&hash);
117            serialized.push('\n');
118        }
119        Ok(sha256_prefixed(serialized.as_bytes()))
120    }
121}
122
123pub fn parse_package_spec(text: &str) -> PrayResult<PackageSpec> {
124    let lines = prepare_parser_lines(text);
125    let mut parser = BlockParser::new(&lines);
126    parser.parse_root()
127}
128
129struct BlockParser<'a> {
130    lines: &'a [Cow<'a, str>],
131    cursor: usize,
132}
133
134impl<'a> BlockParser<'a> {
135    fn new(lines: &'a [Cow<'a, str>]) -> Self {
136        Self { lines, cursor: 0 }
137    }
138
139    fn parse_root(&mut self) -> PrayResult<PackageSpec> {
140        self.expect_start()?;
141        let mut spec = PackageSpec::default();
142        while let Some(statement) = self.next_statement()? {
143            if statement == "end" {
144                return Ok(spec.canonicalized());
145            }
146            self.apply_statement(&mut spec, statement)?;
147        }
148        Err(PrayError::Parse {
149            kind: "prayspec",
150            message: "missing 'end'".to_string(),
151        })
152    }
153
154    fn expect_start(&mut self) -> PrayResult<()> {
155        let statement = self.next_statement()?.ok_or_else(|| PrayError::Parse {
156            kind: "prayspec",
157            message: "empty package spec".to_string(),
158        })?;
159        if !statement.starts_with("Package::Specification.new") {
160            return Err(PrayError::Parse {
161                kind: "prayspec",
162                message: "expected Package::Specification.new".to_string(),
163            });
164        }
165        Ok(())
166    }
167
168    fn apply_statement(&mut self, spec: &mut PackageSpec, statement: String) -> PrayResult<()> {
169        if let Some(rest) = statement.strip_prefix("spec.add_dependency ") {
170            spec.dependencies.push(parse_dependency(rest, false)?);
171            return Ok(());
172        }
173        if let Some(rest) = statement.strip_prefix("spec.add_optional_dependency ") {
174            spec.dependencies.push(parse_dependency(rest, true)?);
175            return Ok(());
176        }
177        if let Some(rest) = statement.strip_prefix("spec.") {
178            if let Some((name, value)) = rest.split_once(" = ") {
179                return self.apply_assignment(spec, name.trim(), value.trim());
180            }
181        }
182        Err(PrayError::Parse {
183            kind: "prayspec",
184            message: format!("unrecognized statement: {statement}"),
185        })
186    }
187
188    fn apply_assignment(&self, spec: &mut PackageSpec, field: &str, value: &str) -> PrayResult<()> {
189        match field {
190            "name" => spec.name = string_from_literal(value)?,
191            "version" => spec.version = string_from_literal(value)?,
192            "summary" => spec.summary = Some(string_from_literal(value)?),
193            "description" => spec.description = Some(string_from_literal(value)?),
194            "authors" => spec.authors = array_of_strings(value)?,
195            "license" => spec.license = Some(string_from_literal(value)?),
196            "homepage" => spec.homepage = Some(string_from_literal(value)?),
197            "source_code_uri" => spec.source_code_uri = Some(string_from_literal(value)?),
198            "changelog_uri" => spec.changelog_uri = Some(string_from_literal(value)?),
199            "prayfile_version" => spec.prayfile_version = Some(string_from_literal(value)?),
200            "files" => spec.files = array_of_strings(value)?,
201            "targets" => spec.targets = array_of_strings(value)?,
202            "exports" => spec.exports = parse_exports(value)?,
203            "skills" => spec.skills = parse_skills(value)?,
204            "templates" => spec.templates = parse_templates(value)?,
205            "adapters" => spec.adapters = parse_string_map(value)?,
206            "metadata" => spec.metadata = parse_metadata(value)?,
207            _ => {
208                return Err(PrayError::Parse {
209                    kind: "prayspec",
210                    message: format!("unsupported assignment: {field}"),
211                })
212            }
213        }
214        Ok(())
215    }
216
217    fn next_statement(&mut self) -> PrayResult<Option<String>> {
218        while self.cursor < self.lines.len() {
219            let mut statement = self.lines[self.cursor].trim().to_string();
220            self.cursor += 1;
221            if statement.is_empty() {
222                continue;
223            }
224            while !statement.ends_with(" do")
225                && statement != "end"
226                && self.cursor < self.lines.len()
227                && (statement.trim_end().ends_with(',') || !is_balanced(&statement))
228            {
229                let next = self.lines[self.cursor].trim();
230                self.cursor += 1;
231                if next.is_empty() {
232                    continue;
233                }
234                statement.push(' ');
235                statement.push_str(next);
236            }
237            return Ok(Some(statement));
238        }
239        Ok(None)
240    }
241}
242
243fn parse_dependency(rest: &str, optional: bool) -> PrayResult<PackageDependency> {
244    let (values, keywords) = parse_call(rest)?;
245    let name = string_from_value(values.first().ok_or_else(|| PrayError::Parse {
246        kind: "prayspec",
247        message: "missing dependency name".to_string(),
248    })?)?;
249    let constraint = values
250        .get(1)
251        .map(string_from_value)
252        .transpose()?
253        .unwrap_or("*".to_string());
254    Ok(PackageDependency {
255        name,
256        constraint,
257        optional: keywords
258            .get("optional")
259            .and_then(|value| value.as_bool())
260            .unwrap_or(optional),
261    })
262}
263
264fn parse_call(rest: &str) -> PrayResult<(Vec<LiteralValue>, BTreeMap<String, LiteralValue>)> {
265    let mut positional = Vec::new();
266    let mut keywords = BTreeMap::new();
267    for segment in split_top_level(rest.trim().trim_end_matches(','), ',') {
268        if let Some((key, value)) = parse_keyword_segment(&segment)? {
269            keywords.insert(key, value);
270        } else if !segment.is_empty() {
271            positional.push(parse_literal(&segment)?);
272        }
273    }
274    Ok((positional, keywords))
275}
276
277fn parse_keyword_segment(segment: &str) -> PrayResult<Option<(String, LiteralValue)>> {
278    if let Some(index) = find_top_level(segment, "=>") {
279        let key = string_from_literal(segment[..index].trim())?;
280        return Ok(Some((key, parse_literal(segment[index + 2..].trim())?)));
281    }
282    if let Some(index) = find_top_level(segment, ":") {
283        let left = segment[..index].trim();
284        let right = segment[index + 1..].trim();
285        if left.is_empty() {
286            return Ok(None);
287        }
288        return Ok(Some((left.to_string(), parse_literal(right)?)));
289    }
290    Ok(None)
291}
292
293fn parse_exports(value: &str) -> PrayResult<BTreeMap<String, PackageExport>> {
294    let map = parse_literal_map(value)?;
295    let mut exports = BTreeMap::new();
296    for (name, literal) in map {
297        let entry = literal.as_map().ok_or_else(|| PrayError::Parse {
298            kind: "prayspec",
299            message: format!("export {name} must be a map"),
300        })?;
301        let missing_path_name = name.clone();
302        exports.insert(
303            name,
304            PackageExport {
305                kind: map_string(entry, "type").unwrap_or_else(|| "fragment".to_string()),
306                path: map_string(entry, "path").ok_or_else(|| PrayError::Parse {
307                    kind: "prayspec",
308                    message: format!("export {missing_path_name} missing path"),
309                })?,
310                summary: map_string(entry, "summary"),
311                only: map_string_array(entry, "only"),
312                except: map_string_array(entry, "except"),
313                default_path: map_string(entry, "default_path"),
314            },
315        );
316    }
317    Ok(exports)
318}
319
320fn parse_skills(value: &str) -> PrayResult<BTreeMap<String, PackageSkill>> {
321    let map = parse_literal_map(value)?;
322    let mut output = BTreeMap::new();
323    for (name, literal) in map {
324        let entry = literal.as_map().ok_or_else(|| PrayError::Parse {
325            kind: "prayspec",
326            message: format!("skill {name} must be a map"),
327        })?;
328        output.insert(
329            name,
330            PackageSkill {
331                path: map_string(entry, "path").ok_or_else(|| PrayError::Parse {
332                    kind: "prayspec",
333                    message: "skill missing path".to_string(),
334                })?,
335                summary: map_string(entry, "summary"),
336            },
337        );
338    }
339    Ok(output)
340}
341
342fn parse_templates(value: &str) -> PrayResult<BTreeMap<String, PackageTemplate>> {
343    let map = parse_literal_map(value)?;
344    let mut output = BTreeMap::new();
345    for (name, literal) in map {
346        let entry = literal.as_map().ok_or_else(|| PrayError::Parse {
347            kind: "prayspec",
348            message: format!("template {name} must be a map"),
349        })?;
350        output.insert(
351            name,
352            PackageTemplate {
353                path: map_string(entry, "path").ok_or_else(|| PrayError::Parse {
354                    kind: "prayspec",
355                    message: "template missing path".to_string(),
356                })?,
357                summary: map_string(entry, "summary"),
358            },
359        );
360    }
361    Ok(output)
362}
363
364fn parse_string_map(value: &str) -> PrayResult<BTreeMap<String, String>> {
365    let map = parse_literal_map(value)?;
366    let mut output = BTreeMap::new();
367    for (key, literal) in map {
368        output.insert(key, string_from_value(&literal)?);
369    }
370    Ok(output)
371}
372
373fn parse_metadata(value: &str) -> PrayResult<BTreeMap<String, LiteralValue>> {
374    parse_literal_map(value)
375}
376
377fn map_string(map: &BTreeMap<String, LiteralValue>, key: &str) -> Option<String> {
378    map.get(key)
379        .and_then(|value| value.as_string().map(str::to_string))
380}
381
382fn map_string_array(map: &BTreeMap<String, LiteralValue>, key: &str) -> Vec<String> {
383    map.get(key)
384        .and_then(|value| value.as_array())
385        .map(|values| {
386            values
387                .iter()
388                .filter_map(|value| value.as_string().map(str::to_string))
389                .collect()
390        })
391        .unwrap_or_default()
392}
393
394fn array_of_strings(value: &str) -> PrayResult<Vec<String>> {
395    let array = parse_literal(value)?;
396    let values = array.as_array().ok_or_else(|| PrayError::Parse {
397        kind: "prayspec",
398        message: "expected array".to_string(),
399    })?;
400    values.iter().map(string_from_value).collect()
401}
402
403fn string_from_value(value: &LiteralValue) -> PrayResult<String> {
404    value
405        .as_string()
406        .map(str::to_string)
407        .ok_or_else(|| PrayError::Parse {
408            kind: "prayspec",
409            message: format!("expected string-like literal, found {:?}", value),
410        })
411}
412
413fn string_from_literal(value: &str) -> PrayResult<String> {
414    string_from_value(&parse_literal(value)?)
415}