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 return Ok(spec.canonicalized());
102 }
103 self.apply_statement(&mut spec, statement)?;
104 }
105 Err(PrayError::Parse {
106 kind: "prayspec",
107 message: "missing 'end'".to_string(),
108 })
109 }
110
111 fn expect_start(&mut self) -> PrayResult<()> {
112 let statement = self.next_statement()?.ok_or_else(|| PrayError::Parse {
113 kind: "prayspec",
114 message: "empty package spec".to_string(),
115 })?;
116 if !statement.starts_with("Package::Specification.new") {
117 return Err(PrayError::Parse {
118 kind: "prayspec",
119 message: "expected Package::Specification.new".to_string(),
120 });
121 }
122 Ok(())
123 }
124
125 fn apply_statement(&mut self, spec: &mut PackageSpec, statement: String) -> PrayResult<()> {
126 if let Some(rest) = statement.strip_prefix("spec.add_dependency ") {
127 spec.dependencies.push(parse_dependency(rest, false)?);
128 return Ok(());
129 }
130 if let Some(rest) = statement.strip_prefix("spec.add_optional_dependency ") {
131 spec.dependencies.push(parse_dependency(rest, true)?);
132 return Ok(());
133 }
134 if let Some(rest) = statement.strip_prefix("spec.upstream ") {
135 if spec.upstream.is_some() {
136 return Err(PrayError::Parse {
137 kind: "prayspec",
138 message: "upstream may only be declared once".to_string(),
139 });
140 }
141 spec.upstream = Some(parse_upstream(rest)?);
142 return Ok(());
143 }
144 if let Some(rest) = statement.strip_prefix("spec.") {
145 if let Some((name, value)) = rest.split_once(" = ") {
146 return self.apply_assignment(spec, name.trim(), value.trim());
147 }
148 }
149 Err(PrayError::Parse {
150 kind: "prayspec",
151 message: format!("unrecognized statement: {statement}"),
152 })
153 }
154
155 fn apply_assignment(&self, spec: &mut PackageSpec, field: &str, value: &str) -> PrayResult<()> {
156 match field {
157 "name" => spec.name = string_from_literal(value)?,
158 "version" => spec.version = string_from_literal(value)?,
159 "summary" => spec.summary = Some(string_from_literal(value)?),
160 "description" => spec.description = Some(string_from_literal(value)?),
161 "authors" => spec.authors = array_of_strings(value)?,
162 "maintainers" => spec.maintainers = array_of_strings(value)?,
163 "license" => spec.license = Some(string_from_literal(value)?),
164 "homepage" => spec.homepage = Some(string_from_literal(value)?),
165 "source_code_uri" => spec.source_code_uri = Some(string_from_literal(value)?),
166 "changelog_uri" => spec.changelog_uri = Some(string_from_literal(value)?),
167 "prayfile_version" | "pray_version" => {
168 spec.prayfile_version = Some(string_from_literal(value)?)
169 }
170 "files" => spec.files = array_of_strings(value)?,
171 "targets" => spec.targets = array_of_strings(value)?,
172 "exports" => spec.exports = parse_exports(value)?,
173 "skills" => spec.skills = parse_skills(value)?,
174 "templates" => spec.templates = parse_templates(value)?,
175 "adapters" => spec.adapters = parse_string_map(value)?,
176 "metadata" => spec.metadata = parse_metadata(value)?,
177 _ => {
178 return Err(PrayError::Parse {
179 kind: "prayspec",
180 message: format!("unsupported assignment: {field}"),
181 })
182 }
183 }
184 Ok(())
185 }
186
187 fn next_statement(&mut self) -> PrayResult<Option<String>> {
188 while self.cursor < self.lines.len() {
189 let mut statement = self.lines[self.cursor].trim().to_string();
190 self.cursor += 1;
191 if statement.is_empty() {
192 continue;
193 }
194 while !statement.ends_with(" do")
195 && statement != "end"
196 && self.cursor < self.lines.len()
197 && (statement.trim_end().ends_with(',') || !is_balanced(&statement))
198 {
199 let next = self.lines[self.cursor].trim();
200 self.cursor += 1;
201 if next.is_empty() {
202 continue;
203 }
204 statement.push(' ');
205 statement.push_str(next);
206 }
207 return Ok(Some(statement));
208 }
209 Ok(None)
210 }
211}
212
213fn parse_dependency(rest: &str, optional: bool) -> PrayResult<PackageDependency> {
214 let (values, keywords) = parse_call(rest)?;
215 let name = string_from_value(values.first().ok_or_else(|| PrayError::Parse {
216 kind: "prayspec",
217 message: "missing dependency name".to_string(),
218 })?)?;
219 let constraint = values
220 .get(1)
221 .map(string_from_value)
222 .transpose()?
223 .unwrap_or("*".to_string());
224 Ok(PackageDependency {
225 name,
226 constraint,
227 optional: keywords
228 .get("optional")
229 .and_then(|value| value.as_bool())
230 .unwrap_or(optional),
231 })
232}
233
234fn parse_call(rest: &str) -> PrayResult<(Vec<LiteralValue>, BTreeMap<String, LiteralValue>)> {
235 let mut positional = Vec::new();
236 let mut keywords = BTreeMap::new();
237 for segment in split_top_level(rest.trim().trim_end_matches(','), ',') {
238 if let Some((key, value)) = parse_keyword_segment(&segment)? {
239 keywords.insert(key, value);
240 } else if !segment.is_empty() {
241 positional.push(parse_literal(&segment)?);
242 }
243 }
244 Ok((positional, keywords))
245}
246
247fn parse_keyword_segment(segment: &str) -> PrayResult<Option<(String, LiteralValue)>> {
248 if let Some(index) = find_top_level(segment, "=>") {
249 let key = string_from_literal(segment[..index].trim())?;
250 return Ok(Some((key, parse_literal(segment[index + 2..].trim())?)));
251 }
252 if let Some(index) = find_top_level(segment, ":") {
253 let left = segment[..index].trim();
254 let right = segment[index + 1..].trim();
255 if left.is_empty() {
256 return Ok(None);
257 }
258 return Ok(Some((left.to_string(), parse_literal(right)?)));
259 }
260 Ok(None)
261}