Skip to main content

pray_core/
package_spec.rs

1use crate::literal::{
2    find_top_level, is_balanced, parse_literal, prepare_parser_lines, split_top_level, LiteralValue,
3};
4use crate::package_upstream::{parse_upstream, PackageUpstream};
5use crate::{PrayError, PrayResult};
6use serde::{Deserialize, Serialize};
7use std::borrow::Cow;
8use std::collections::BTreeMap;
9
10#[path = "package_spec_maps.rs"]
11mod maps;
12#[path = "package_spec_hash.rs"]
13mod package_hash;
14use maps::{
15    array_of_strings, parse_exports, parse_metadata, parse_skills, parse_string_map,
16    parse_templates, string_from_literal, string_from_value,
17};
18
19#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
20pub struct PackageSpec {
21    pub name: String,
22    pub version: String,
23    pub summary: Option<String>,
24    pub description: Option<String>,
25    pub authors: Vec<String>,
26    pub license: Option<String>,
27    pub homepage: Option<String>,
28    pub source_code_uri: Option<String>,
29    pub changelog_uri: Option<String>,
30    pub prayfile_version: Option<String>,
31    pub files: Vec<String>,
32    pub exports: BTreeMap<String, PackageExport>,
33    pub skills: BTreeMap<String, PackageSkill>,
34    pub templates: BTreeMap<String, PackageTemplate>,
35    pub adapters: BTreeMap<String, String>,
36    pub targets: Vec<String>,
37    pub dependencies: Vec<PackageDependency>,
38    pub metadata: BTreeMap<String, LiteralValue>,
39    #[serde(default, skip_serializing_if = "Option::is_none")]
40    pub upstream: Option<PackageUpstream>,
41}
42
43#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
44pub struct PackageExport {
45    pub kind: String,
46    pub path: String,
47    pub summary: Option<String>,
48    #[serde(default, skip_serializing_if = "Vec::is_empty")]
49    pub only: Vec<String>,
50    #[serde(default, skip_serializing_if = "Vec::is_empty")]
51    pub except: Vec<String>,
52    #[serde(default, skip_serializing_if = "Option::is_none")]
53    pub default_path: Option<String>,
54}
55
56#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
57pub struct PackageSkill {
58    pub path: String,
59    pub summary: Option<String>,
60}
61
62#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
63pub struct PackageTemplate {
64    pub path: String,
65    pub summary: Option<String>,
66}
67
68#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
69pub struct PackageDependency {
70    pub name: String,
71    pub constraint: String,
72    pub optional: bool,
73}
74
75pub fn parse_package_spec(text: &str) -> PrayResult<PackageSpec> {
76    let lines = prepare_parser_lines(text);
77    let mut parser = BlockParser::new(&lines);
78    parser.parse_root()
79}
80
81struct BlockParser<'a> {
82    lines: &'a [Cow<'a, str>],
83    cursor: usize,
84}
85
86impl<'a> BlockParser<'a> {
87    fn new(lines: &'a [Cow<'a, str>]) -> Self {
88        Self { lines, cursor: 0 }
89    }
90
91    fn parse_root(&mut self) -> PrayResult<PackageSpec> {
92        self.expect_start()?;
93        let mut spec = PackageSpec::default();
94        while let Some(statement) = self.next_statement()? {
95            if statement == "end" {
96                return Ok(spec.canonicalized());
97            }
98            self.apply_statement(&mut spec, statement)?;
99        }
100        Err(PrayError::Parse {
101            kind: "prayspec",
102            message: "missing 'end'".to_string(),
103        })
104    }
105
106    fn expect_start(&mut self) -> PrayResult<()> {
107        let statement = self.next_statement()?.ok_or_else(|| PrayError::Parse {
108            kind: "prayspec",
109            message: "empty package spec".to_string(),
110        })?;
111        if !statement.starts_with("Package::Specification.new") {
112            return Err(PrayError::Parse {
113                kind: "prayspec",
114                message: "expected Package::Specification.new".to_string(),
115            });
116        }
117        Ok(())
118    }
119
120    fn apply_statement(&mut self, spec: &mut PackageSpec, statement: String) -> PrayResult<()> {
121        if let Some(rest) = statement.strip_prefix("spec.add_dependency ") {
122            spec.dependencies.push(parse_dependency(rest, false)?);
123            return Ok(());
124        }
125        if let Some(rest) = statement.strip_prefix("spec.add_optional_dependency ") {
126            spec.dependencies.push(parse_dependency(rest, true)?);
127            return Ok(());
128        }
129        if let Some(rest) = statement.strip_prefix("spec.upstream ") {
130            if spec.upstream.is_some() {
131                return Err(PrayError::Parse {
132                    kind: "prayspec",
133                    message: "upstream may only be declared once".to_string(),
134                });
135            }
136            spec.upstream = Some(parse_upstream(rest)?);
137            return Ok(());
138        }
139        if let Some(rest) = statement.strip_prefix("spec.") {
140            if let Some((name, value)) = rest.split_once(" = ") {
141                return self.apply_assignment(spec, name.trim(), value.trim());
142            }
143        }
144        Err(PrayError::Parse {
145            kind: "prayspec",
146            message: format!("unrecognized statement: {statement}"),
147        })
148    }
149
150    fn apply_assignment(&self, spec: &mut PackageSpec, field: &str, value: &str) -> PrayResult<()> {
151        match field {
152            "name" => spec.name = string_from_literal(value)?,
153            "version" => spec.version = string_from_literal(value)?,
154            "summary" => spec.summary = Some(string_from_literal(value)?),
155            "description" => spec.description = Some(string_from_literal(value)?),
156            "authors" => spec.authors = array_of_strings(value)?,
157            "license" => spec.license = Some(string_from_literal(value)?),
158            "homepage" => spec.homepage = Some(string_from_literal(value)?),
159            "source_code_uri" => spec.source_code_uri = Some(string_from_literal(value)?),
160            "changelog_uri" => spec.changelog_uri = Some(string_from_literal(value)?),
161            "prayfile_version" => spec.prayfile_version = Some(string_from_literal(value)?),
162            "files" => spec.files = array_of_strings(value)?,
163            "targets" => spec.targets = array_of_strings(value)?,
164            "exports" => spec.exports = parse_exports(value)?,
165            "skills" => spec.skills = parse_skills(value)?,
166            "templates" => spec.templates = parse_templates(value)?,
167            "adapters" => spec.adapters = parse_string_map(value)?,
168            "metadata" => spec.metadata = parse_metadata(value)?,
169            _ => {
170                return Err(PrayError::Parse {
171                    kind: "prayspec",
172                    message: format!("unsupported assignment: {field}"),
173                })
174            }
175        }
176        Ok(())
177    }
178
179    fn next_statement(&mut self) -> PrayResult<Option<String>> {
180        while self.cursor < self.lines.len() {
181            let mut statement = self.lines[self.cursor].trim().to_string();
182            self.cursor += 1;
183            if statement.is_empty() {
184                continue;
185            }
186            while !statement.ends_with(" do")
187                && statement != "end"
188                && self.cursor < self.lines.len()
189                && (statement.trim_end().ends_with(',') || !is_balanced(&statement))
190            {
191                let next = self.lines[self.cursor].trim();
192                self.cursor += 1;
193                if next.is_empty() {
194                    continue;
195                }
196                statement.push(' ');
197                statement.push_str(next);
198            }
199            return Ok(Some(statement));
200        }
201        Ok(None)
202    }
203}
204
205fn parse_dependency(rest: &str, optional: bool) -> PrayResult<PackageDependency> {
206    let (values, keywords) = parse_call(rest)?;
207    let name = string_from_value(values.first().ok_or_else(|| PrayError::Parse {
208        kind: "prayspec",
209        message: "missing dependency name".to_string(),
210    })?)?;
211    let constraint = values
212        .get(1)
213        .map(string_from_value)
214        .transpose()?
215        .unwrap_or("*".to_string());
216    Ok(PackageDependency {
217        name,
218        constraint,
219        optional: keywords
220            .get("optional")
221            .and_then(|value| value.as_bool())
222            .unwrap_or(optional),
223    })
224}
225
226fn parse_call(rest: &str) -> PrayResult<(Vec<LiteralValue>, BTreeMap<String, LiteralValue>)> {
227    let mut positional = Vec::new();
228    let mut keywords = BTreeMap::new();
229    for segment in split_top_level(rest.trim().trim_end_matches(','), ',') {
230        if let Some((key, value)) = parse_keyword_segment(&segment)? {
231            keywords.insert(key, value);
232        } else if !segment.is_empty() {
233            positional.push(parse_literal(&segment)?);
234        }
235    }
236    Ok((positional, keywords))
237}
238
239fn parse_keyword_segment(segment: &str) -> PrayResult<Option<(String, LiteralValue)>> {
240    if let Some(index) = find_top_level(segment, "=>") {
241        let key = string_from_literal(segment[..index].trim())?;
242        return Ok(Some((key, parse_literal(segment[index + 2..].trim())?)));
243    }
244    if let Some(index) = find_top_level(segment, ":") {
245        let left = segment[..index].trim();
246        let right = segment[index + 1..].trim();
247        if left.is_empty() {
248            return Ok(None);
249        }
250        return Ok(Some((left.to_string(), parse_literal(right)?)));
251    }
252    Ok(None)
253}