1use anyhow::Result;
11use serde::{Deserialize, Serialize};
12use std::collections::BTreeMap;
13use std::path::PathBuf;
14
15#[derive(Debug, Clone, Serialize, Deserialize)]
19pub struct Manifest {
20 pub name: String,
21 pub description: String,
22 pub version: String,
23 #[serde(default)]
24 pub types: Vec<String>,
25 #[serde(default)]
26 pub inputs: Inputs,
27 #[serde(default)]
28 pub outputs: Outputs,
29 #[serde(default)]
30 pub artifacts: Vec<Artifact>,
31}
32
33#[derive(Debug, Clone, Serialize, Deserialize, Default)]
37pub struct Inputs {
38 #[serde(default)]
40 pub dirs: Vec<PathBuf>,
41 #[serde(skip_serializing_if = "Option::is_none")]
43 pub file_regex: Option<String>,
44 #[serde(default)]
46 pub files: Vec<PathBuf>,
47}
48
49#[derive(Debug, Clone, Serialize, Deserialize, Default)]
51pub struct Outputs {
52 #[serde(default)]
53 pub source_dirs: Vec<PathBuf>,
54}
55
56#[derive(Debug, Clone, Serialize, Deserialize)]
58pub struct Artifact {
59 pub id: String,
61 pub group: String,
62 pub artifact: String,
63 pub version: String,
64 #[serde(skip_serializing_if = "Option::is_none")]
65 pub classifier: Option<String>,
66 pub extension: String,
67 #[serde(default)]
68 pub executable: bool,
69}
70
71#[derive(Debug, Deserialize)]
78pub struct Envelope<C> {
79 pub curie_version: String,
80 pub config: C,
81 #[serde(default)]
82 pub artifacts: BTreeMap<String, PathBuf>,
83}
84
85pub fn read_envelope<C: serde::de::DeserializeOwned>() -> Result<Envelope<C>> {
87 let mut s = String::new();
88 std::io::Read::read_to_string(&mut std::io::stdin(), &mut s)?;
89 Ok(serde_json::from_str(&s)?)
90}
91
92#[cfg(test)]
95mod tests {
96 use super::*;
97
98 fn minimal_manifest() -> Manifest {
99 Manifest {
100 name: "test".to_string(),
101 description: "A test plugin".to_string(),
102 version: "0.1.0".to_string(),
103 types: vec!["source-generator".to_string()],
104 inputs: Inputs {
105 dirs: vec![PathBuf::from("proto")],
106 file_regex: Some(r"\.proto$".to_string()),
107 files: vec![],
108 },
109 outputs: Outputs {
110 source_dirs: vec![PathBuf::from("target/generated-sources/test")],
111 },
112 artifacts: vec![Artifact {
113 id: "tool".to_string(),
114 group: "com.example".to_string(),
115 artifact: "tool".to_string(),
116 version: "1.0.0".to_string(),
117 classifier: Some("linux-x86_64".to_string()),
118 extension: "exe".to_string(),
119 executable: true,
120 }],
121 }
122 }
123
124 #[test]
125 fn manifest_roundtrips_through_json() {
126 let m = minimal_manifest();
127 let json = serde_json::to_string(&m).unwrap();
128 let m2: Manifest = serde_json::from_str(&json).unwrap();
129 assert_eq!(m2.name, "test");
130 assert_eq!(m2.inputs.dirs, vec![PathBuf::from("proto")]);
131 assert_eq!(m2.inputs.file_regex.as_deref(), Some(r"\.proto$"));
132 assert_eq!(m2.outputs.source_dirs, vec![PathBuf::from("target/generated-sources/test")]);
133 assert_eq!(m2.artifacts[0].classifier, Some("linux-x86_64".to_string()));
134 }
135
136 #[test]
137 fn missing_optional_fields_deserialize_as_defaults() {
138 let json = r#"{
139 "name": "mini",
140 "description": "desc",
141 "version": "0.1.0"
142 }"#;
143 let m: Manifest = serde_json::from_str(json).unwrap();
144 assert!(m.types.is_empty());
145 assert!(m.inputs.dirs.is_empty());
146 assert!(m.inputs.file_regex.is_none());
147 assert!(m.inputs.files.is_empty());
148 assert!(m.outputs.source_dirs.is_empty());
149 assert!(m.artifacts.is_empty());
150 }
151
152 #[test]
153 fn artifact_without_classifier_omits_field_in_json() {
154 let art = Artifact {
155 id: "cli".to_string(),
156 group: "org.example".to_string(),
157 artifact: "cli".to_string(),
158 version: "1.0".to_string(),
159 classifier: None,
160 extension: "jar".to_string(),
161 executable: false,
162 };
163 let json = serde_json::to_string(&art).unwrap();
164 assert!(!json.contains("classifier"), "classifier key must be absent: {json}");
165 }
166
167 #[test]
168 fn file_regex_none_omits_field_in_json() {
169 let inputs = Inputs {
170 dirs: vec![],
171 file_regex: None,
172 files: vec![PathBuf::from("spec.yaml")],
173 };
174 let json = serde_json::to_string(&inputs).unwrap();
175 assert!(!json.contains("file_regex"), "file_regex key must be absent: {json}");
176 }
177
178 #[test]
179 fn envelope_deserializes_config_and_artifacts() {
180 #[derive(Deserialize)]
181 struct MyConfig {
182 value: String,
183 }
184
185 let json = r#"{
186 "curie_version": "0.6.0",
187 "config": {"value": "hello"},
188 "artifacts": {"tool": "/path/to/tool"}
189 }"#;
190 let env: Envelope<MyConfig> = serde_json::from_str(json).unwrap();
191 assert_eq!(env.curie_version, "0.6.0");
192 assert_eq!(env.config.value, "hello");
193 assert_eq!(env.artifacts["tool"], PathBuf::from("/path/to/tool"));
194 }
195
196 #[test]
197 fn envelope_artifacts_defaults_to_empty() {
198 #[derive(Deserialize)]
199 struct MyConfig {
200 #[allow(dead_code)]
201 v: u32,
202 }
203
204 let json = r#"{"curie_version": "0.6.0", "config": {"v": 1}}"#;
205 let env: Envelope<MyConfig> = serde_json::from_str(json).unwrap();
206 assert!(env.artifacts.is_empty());
207 }
208}