1use std::time::Duration;
4
5use serde::{Deserialize, Serialize};
6
7use crate::PackageError;
8
9pub const CURRENT_FORMAT_VERSION: u32 = 1;
11
12#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
18pub struct ManifestVersion(pub String);
19
20impl ManifestVersion {
21 #[must_use]
23 pub fn new(version: impl Into<String>) -> Self {
24 Self(version.into())
25 }
26
27 #[must_use]
29 pub fn as_str(&self) -> &str {
30 &self.0
31 }
32}
33
34#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
36pub struct DeclaredActivity {
37 #[serde(rename = "activity_type")]
42 pub activity_type: String,
43}
44
45#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
51pub struct Manifest {
52 #[serde(rename = "entry_module")]
54 pub entry_module: String,
55 #[serde(rename = "entry_function")]
57 pub entry_function: String,
58 #[serde(rename = "input_schema")]
60 pub input_schema: serde_json::Value,
61 #[serde(rename = "output_schema")]
63 pub output_schema: serde_json::Value,
64 #[serde(rename = "timeout")]
66 pub timeout: Duration,
67 #[serde(rename = "activities")]
69 pub activities: Vec<DeclaredActivity>,
70 #[serde(rename = "version")]
72 pub version: ManifestVersion,
73 #[serde(rename = "format_version")]
77 pub format_version: u32,
78}
79
80impl Manifest {
81 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}