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