Skip to main content

pray_core/
manifest.rs

1use crate::hashing::sha256_prefixed;
2use crate::{PrayError, PrayResult};
3use serde::{Deserialize, Serialize};
4use std::collections::BTreeMap;
5use std::fs;
6use std::io;
7use std::path::Path;
8
9pub use crate::manifest_format::{format_package_declaration, replace_package_declaration};
10pub use crate::manifest_parse::parse_manifest;
11
12#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
13pub struct Manifest {
14    pub prayfile_version: String,
15    pub sources: Vec<ManifestSource>,
16    pub targets: Vec<ManifestTarget>,
17    pub packages: Vec<ManifestPackage>,
18    pub local: Vec<ManifestLocal>,
19    #[serde(default, skip_serializing_if = "Vec::is_empty")]
20    pub publish_remotes: Vec<crate::publish_remote::ManifestPublishRemote>,
21    #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
22    pub symbols: BTreeMap<String, String>,
23    pub render: RenderPolicy,
24    /// Deprecated Prayfile keywords encountered while parsing (`target`, `output`, `agent`, `skills`).
25    #[serde(default, skip)]
26    pub deprecated_keywords: Vec<String>,
27}
28
29impl Manifest {
30    pub fn note_deprecated_keyword(&mut self, keyword: &str) {
31        if !crate::deprecation::is_deprecated_prayfile_keyword(keyword) {
32            return;
33        }
34        if self
35            .deprecated_keywords
36            .iter()
37            .any(|existing| existing == keyword)
38        {
39            return;
40        }
41        self.deprecated_keywords.push(keyword.to_string());
42    }
43
44    pub fn deprecation_warnings(&self) -> Vec<String> {
45        crate::deprecation::deprecation_warnings_for(&self.deprecated_keywords)
46    }
47}
48
49#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
50pub struct ManifestSource {
51    pub name: String,
52    pub kind: String,
53    pub url: String,
54    #[serde(default, skip_serializing_if = "Option::is_none")]
55    pub subdir: Option<String>,
56    #[serde(default, skip_serializing_if = "Option::is_none")]
57    pub rev: Option<String>,
58    #[serde(default, skip_serializing_if = "Option::is_none")]
59    pub tag: Option<String>,
60}
61
62#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, PartialEq, Eq)]
63#[serde(rename_all = "snake_case")]
64pub enum DestinationMode {
65    #[default]
66    Legacy,
67    Compose,
68    Tree,
69}
70
71#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
72#[serde(tag = "kind", rename_all = "snake_case")]
73pub enum DestinationEntry {
74    Package { name: String },
75    Local { path: String },
76}
77
78#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
79#[serde(rename_all = "snake_case")]
80pub enum ExportRole {
81    Fragment,
82    Folder,
83    File,
84}
85
86#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
87pub struct ManifestTarget {
88    pub name: String,
89    #[serde(default)]
90    pub outputs: Vec<String>,
91    #[serde(default)]
92    pub skills: Vec<String>,
93    #[serde(default)]
94    pub commands: Vec<String>,
95    #[serde(default)]
96    pub rules: Vec<String>,
97    pub max_bytes: Option<u64>,
98    #[serde(default)]
99    pub mode: DestinationMode,
100    #[serde(default)]
101    pub scoped: bool,
102    #[serde(default)]
103    pub entries: Vec<DestinationEntry>,
104    #[serde(default, skip_serializing_if = "Option::is_none")]
105    pub header: Option<bool>,
106}
107
108#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
109pub struct ManifestPackage {
110    pub name: String,
111    #[serde(default = "default_constraint")]
112    pub constraint: String,
113    pub source: Option<String>,
114    #[serde(default)]
115    pub exports: Vec<String>,
116    #[serde(default)]
117    pub targets: Vec<String>,
118    #[serde(default)]
119    pub features: Vec<String>,
120    #[serde(default)]
121    pub groups: Vec<String>,
122    #[serde(default)]
123    pub optional: bool,
124    pub path: Option<String>,
125    pub git: Option<String>,
126    pub tag: Option<String>,
127    pub rev: Option<String>,
128    pub tarball: Option<String>,
129    pub oci: Option<String>,
130    #[serde(default, skip_serializing_if = "Option::is_none")]
131    pub file: Option<String>,
132    #[serde(default, skip_serializing_if = "Vec::is_empty")]
133    pub roles: Vec<ExportRole>,
134    #[serde(default, skip_serializing_if = "std::ops::Not::not")]
135    pub bound: bool,
136}
137
138fn default_constraint() -> String {
139    "*".to_string()
140}
141
142#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
143pub struct ManifestLocal {
144    pub path: String,
145    #[serde(default = "default_local_position")]
146    pub position: String,
147    #[serde(default)]
148    pub optional: bool,
149    #[serde(default, skip_serializing_if = "std::ops::Not::not")]
150    pub bound: bool,
151    #[serde(default, skip_serializing_if = "Option::is_none")]
152    pub file: Option<String>,
153}
154
155fn default_local_position() -> String {
156    "after".to_string()
157}
158
159#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
160pub struct RenderPolicy {
161    pub mode: String,
162    pub conflict: String,
163    pub churn: String,
164    pub header: bool,
165}
166
167impl Default for RenderPolicy {
168    fn default() -> Self {
169        Self {
170            mode: "managed".to_string(),
171            conflict: "fail".to_string(),
172            churn: "minimal".to_string(),
173            header: true,
174        }
175    }
176}
177
178impl Manifest {
179    pub fn canonicalized(&self) -> Self {
180        let mut manifest = self.clone();
181        manifest
182            .sources
183            .sort_by(|left, right| left.name.cmp(&right.name));
184        manifest
185            .targets
186            .sort_by(|left, right| left.name.cmp(&right.name));
187        manifest.packages.sort_by(|left, right| {
188            left.name
189                .cmp(&right.name)
190                .then(left.source.cmp(&right.source))
191                .then(left.constraint.cmp(&right.constraint))
192        });
193        manifest
194            .local
195            .sort_by(|left, right| left.path.cmp(&right.path));
196        manifest
197            .publish_remotes
198            .sort_by(|left, right| left.name.cmp(&right.name));
199        manifest
200    }
201
202    pub fn manifest_hash(&self) -> PrayResult<String> {
203        let canonical = self.canonicalized();
204        let bytes = serde_json::to_vec(&canonical)
205            .map_err(|error| PrayError::Manifest(error.to_string()))?;
206        Ok(sha256_prefixed(&bytes))
207    }
208}
209
210pub fn read_manifest_text(manifest_path: &Path) -> PrayResult<String> {
211    fs::read_to_string(manifest_path).map_err(|error| {
212        if error.kind() == io::ErrorKind::NotFound {
213            let manifest_label = manifest_path
214                .file_name()
215                .and_then(|name| name.to_str())
216                .unwrap_or("Prayfile");
217            PrayError::Manifest(format!(
218                "missing {manifest_label}; run pray init to create one"
219            ))
220        } else {
221            PrayError::Io(error)
222        }
223    })
224}