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