Skip to main content

aion_package/
manifest.rs

1//! Typed `manifest.json` model and `.aion` format-version checks.
2
3use std::time::Duration;
4
5use serde::{Deserialize, Serialize};
6
7use crate::PackageError;
8
9/// Current `.aion` manifest and archive-layout schema version supported by this crate.
10pub const CURRENT_FORMAT_VERSION: u32 = 1;
11
12/// Textual content-hash version stored in `manifest.json`.
13///
14/// The content hash is computed and stamped by later package-building code; this
15/// type keeps the manifest field distinct from unrelated strings while this
16/// module remains side-effect free.
17#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
18pub struct ManifestVersion(pub String);
19
20impl ManifestVersion {
21    /// Creates a manifest version value from the hash's stable textual form.
22    #[must_use]
23    pub fn new(version: impl Into<String>) -> Self {
24        Self(version.into())
25    }
26
27    /// Returns the stored textual content-hash version.
28    #[must_use]
29    pub fn as_str(&self) -> &str {
30        &self.0
31    }
32}
33
34/// Activity declaration recorded in the package manifest.
35#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
36pub struct DeclaredActivity {
37    /// Stable `activity_type` key naming an activity type invoked by workflow code.
38    ///
39    /// This follows the current `aion-core` event convention, where scheduled
40    /// activity types are represented as strings.
41    #[serde(rename = "activity_type")]
42    pub activity_type: String,
43}
44
45/// Typed on-disk `manifest.json` descriptor for a `.aion` package.
46///
47/// The public field names are the stable JSON keys written into `manifest.json`:
48/// `entry_module`, `entry_function`, `input_schema`, `output_schema`, `timeout`,
49/// `activities`, `version`, and `format_version`.
50#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
51pub struct Manifest {
52    /// Stable `entry_module` key naming the logical workflow entry module.
53    #[serde(rename = "entry_module")]
54    pub entry_module: String,
55    /// Stable `entry_function` key naming the exported workflow entry function.
56    #[serde(rename = "entry_function")]
57    pub entry_function: String,
58    /// Stable `input_schema` key containing a JSON-Schema document for input payloads.
59    #[serde(rename = "input_schema")]
60    pub input_schema: serde_json::Value,
61    /// Stable `output_schema` key containing a JSON-Schema document for result payloads.
62    #[serde(rename = "output_schema")]
63    pub output_schema: serde_json::Value,
64    /// Stable `timeout` key containing the workflow timeout as a serde-encoded duration.
65    #[serde(rename = "timeout")]
66    pub timeout: Duration,
67    /// Stable `activities` key listing activity types declared by the workflow.
68    #[serde(rename = "activities")]
69    pub activities: Vec<DeclaredActivity>,
70    /// Stable `version` key containing the package content hash textual value.
71    #[serde(rename = "version")]
72    pub version: ManifestVersion,
73    /// Stable `format_version` key identifying the `.aion` format schema version.
74    ///
75    /// This lets future layout changes be detected rather than silently misread.
76    #[serde(rename = "format_version")]
77    pub format_version: u32,
78}
79
80impl Manifest {
81    /// Checks whether this manifest declares a supported `.aion` format version.
82    ///
83    /// # Errors
84    ///
85    /// Returns [`PackageError::UnknownFormatVersion`] when `format_version` is
86    /// not [`CURRENT_FORMAT_VERSION`].
87    pub fn check_format_version(&self) -> Result<(), PackageError> {
88        if self.format_version == CURRENT_FORMAT_VERSION {
89            Ok(())
90        } else {
91            Err(PackageError::UnknownFormatVersion {
92                found: self.format_version,
93            })
94        }
95    }
96}
97
98#[cfg(test)]
99mod tests {
100    use std::time::Duration;
101
102    use serde_json::json;
103
104    use super::{CURRENT_FORMAT_VERSION, DeclaredActivity, Manifest, ManifestVersion};
105    use crate::PackageError;
106
107    fn sample_manifest() -> Manifest {
108        Manifest {
109            entry_module: "workflow/order".to_owned(),
110            entry_function: "run".to_owned(),
111            input_schema: json!({
112                "$schema": "https://json-schema.org/draft/2020-12/schema",
113                "type": "object",
114                "required": ["order_id"],
115                "properties": {
116                    "order_id": { "type": "string" },
117                    "retry": { "type": "boolean" }
118                }
119            }),
120            output_schema: json!({
121                "$schema": "https://json-schema.org/draft/2020-12/schema",
122                "type": "object",
123                "required": ["status"],
124                "properties": {
125                    "status": { "enum": ["accepted", "rejected"] },
126                    "total": { "type": "number" }
127                }
128            }),
129            timeout: Duration::new(30, 250_000_000),
130            activities: vec![
131                DeclaredActivity {
132                    activity_type: "charge_card".to_owned(),
133                },
134                DeclaredActivity {
135                    activity_type: "send_receipt".to_owned(),
136                },
137            ],
138            version: ManifestVersion::new(
139                "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef",
140            ),
141            format_version: CURRENT_FORMAT_VERSION,
142        }
143    }
144
145    #[test]
146    fn manifest_round_trips_losslessly_through_json() -> Result<(), serde_json::Error> {
147        let manifest = sample_manifest();
148
149        let json = serde_json::to_string(&manifest)?;
150        let decoded: Manifest = serde_json::from_str(&json)?;
151
152        assert_eq!(decoded, manifest);
153        Ok(())
154    }
155
156    #[test]
157    fn manifest_with_schemas_and_declared_activities_round_trips() -> Result<(), serde_json::Error>
158    {
159        let manifest = sample_manifest();
160
161        let json = serde_json::to_string(&manifest)?;
162        let decoded: Manifest = serde_json::from_str(&json)?;
163
164        assert_eq!(
165            decoded.input_schema["properties"]["order_id"]["type"],
166            "string"
167        );
168        assert_eq!(
169            decoded.output_schema["properties"]["status"]["enum"][0],
170            "accepted"
171        );
172        assert_eq!(decoded.activities.len(), 2);
173        assert_eq!(decoded, manifest);
174        Ok(())
175    }
176
177    #[test]
178    fn supported_format_version_passes() -> Result<(), PackageError> {
179        sample_manifest().check_format_version()
180    }
181
182    #[test]
183    fn unsupported_format_version_returns_typed_error() {
184        let mut manifest = sample_manifest();
185        manifest.format_version = CURRENT_FORMAT_VERSION + 1;
186
187        let result = manifest.check_format_version();
188
189        assert!(matches!(
190            result,
191            Err(PackageError::UnknownFormatVersion { found }) if found == CURRENT_FORMAT_VERSION + 1
192        ));
193    }
194
195    #[test]
196    fn manifest_json_keys_are_stable() -> Result<(), serde_json::Error> {
197        let manifest = sample_manifest();
198
199        let json = serde_json::to_value(&manifest)?;
200
201        assert!(json.get("entry_module").is_some());
202        assert!(json.get("entry_function").is_some());
203        assert!(json.get("input_schema").is_some());
204        assert!(json.get("output_schema").is_some());
205        assert!(json.get("timeout").is_some());
206        assert!(json.get("activities").is_some());
207        assert!(json.get("version").is_some());
208        assert!(json.get("format_version").is_some());
209        assert_eq!(json["activities"][0]["activity_type"], "charge_card");
210        Ok(())
211    }
212}