1use std::{
2 collections::BTreeMap,
3 ops::{Deref, DerefMut},
4};
5
6use schemars::{schema::Schema, JsonSchema, SchemaGenerator};
7use serde::{Deserialize, Serialize};
8use serde_with::{serde_as, KeyValueMap};
9
10use sdf_parser_core::config::{
11 dev::DevConfig,
12 import::{PackageImport, PackageMetadata},
13 transform::TypedState,
14 types::MetadataTypesMapWrapper,
15};
16
17use super::functions::Function;
18
19pub fn parse_package(pkg: &str) -> anyhow::Result<PackageConfig> {
20 let yd = serde_yaml::Deserializer::from_str(pkg);
21 let config = serde_path_to_error::deserialize(yd)?;
22
23 Ok(config)
24}
25
26pub type CurrentPkgConfig = PackageWrapperV0_5_0;
27pub type DevPkgConfig = PackageWrapperV0_5_0;
28
29#[derive(Serialize, Deserialize, Debug, JsonSchema)]
30#[serde(tag = "apiVersion")]
31pub enum PackageConfig {
32 #[serde(rename = "0.4.0")]
33 V0_4_0(CurrentPkgConfig),
34 #[serde(rename = "0.5.0")]
35 V0_5_0(DevPkgConfig),
36 #[serde(rename = "0.6.0")]
37 V0_6_0(DevPkgConfig),
38}
39
40impl PackageConfig {
41 pub fn is_v5(&self) -> bool {
42 matches!(self, Self::V0_5_0(_))
43 }
44
45 pub fn is_v4(&self) -> bool {
46 matches!(self, Self::V0_4_0(_))
47 }
48
49 pub fn imports(&self) -> &Vec<PackageImport> {
50 match self {
51 Self::V0_4_0(wrapper) => &wrapper.imports,
52 Self::V0_5_0(wrapper) => &wrapper.imports,
53 Self::V0_6_0(wrapper) => &wrapper.imports,
54 }
55 }
56
57 pub fn types(&self) -> &MetadataTypesMapWrapper {
58 match self {
59 Self::V0_4_0(wrapper) => &wrapper.types,
60 Self::V0_5_0(wrapper) => &wrapper.types,
61 Self::V0_6_0(wrapper) => &wrapper.types,
62 }
63 }
64
65 pub fn states(&self) -> &BTreeMap<String, TypedState> {
66 match self {
67 Self::V0_4_0(wrapper) => &wrapper.states,
68 Self::V0_5_0(wrapper) => &wrapper.states,
69 Self::V0_6_0(wrapper) => &wrapper.states,
70 }
71 }
72
73 pub fn functions(&self) -> &Vec<Function> {
74 match self {
75 Self::V0_4_0(wrapper) => &wrapper.functions,
76 Self::V0_5_0(wrapper) => &wrapper.functions,
77 Self::V0_6_0(wrapper) => &wrapper.functions,
78 }
79 }
80
81 pub fn dev(&self) -> Option<&DevConfig> {
82 match self {
83 Self::V0_4_0(_) => None,
84 Self::V0_5_0(wrapper) => wrapper.dev.as_ref(),
85 Self::V0_6_0(wrapper) => wrapper.dev.as_ref(),
86 }
87 }
88}
89
90impl Deref for PackageConfig {
91 type Target = CurrentPkgConfig;
92
93 fn deref(&self) -> &Self::Target {
94 match self {
95 Self::V0_4_0(wrapper) => wrapper,
96 Self::V0_5_0(wrapper) => wrapper,
97 Self::V0_6_0(wrapper) => wrapper,
98 }
99 }
100}
101
102impl DerefMut for PackageConfig {
103 fn deref_mut(&mut self) -> &mut Self::Target {
104 match self {
105 Self::V0_4_0(wrapper) => wrapper,
106 Self::V0_5_0(wrapper) => wrapper,
107 Self::V0_6_0(wrapper) => wrapper,
108 }
109 }
110}
111
112#[serde_as]
113#[derive(Serialize, Deserialize, Debug, Clone, Default, JsonSchema)]
114pub struct PackageWrapperV0_5_0 {
115 pub meta: PackageMetadata,
116 #[serde(skip_serializing_if = "Vec::is_empty", default)]
117 pub imports: Vec<PackageImport>,
118 #[serde(default)]
119 pub types: MetadataTypesMapWrapper,
120 #[serde(skip_serializing_if = "BTreeMap::is_empty", default)]
121 pub states: BTreeMap<String, TypedState>,
122 #[serde(skip_serializing_if = "Vec::is_empty", default)]
123 #[serde_as(as = "KeyValueMap<_>")]
124 #[schemars(schema_with = "function_schema")]
125 pub functions: Vec<Function>,
126 pub dev: Option<DevConfig>,
127}
128
129fn function_schema(generator: &mut SchemaGenerator) -> Schema {
131 String::json_schema(generator)
132}
133
134#[derive(Serialize, Deserialize, Debug, Clone, Default)]
135pub struct PackageUnsupportedVersion {
136 pub meta: PackageMetadata,
137}
138#[cfg(test)]
139mod tests {
140
141 use schemars::schema_for;
142 use sdf_parser_core::{
143 config::{
144 transform::{Lang, StepInvocationDefinition},
145 types::{MetadataTypeInner, MetadataTypeTagged, NamedType},
146 SerdeConverter,
147 },
148 MaybeValid,
149 };
150
151 use super::*;
152
153 #[test]
154 fn test_parse_package() {
155 let yaml = "
156apiVersion: 0.5.0
157meta:
158 name: my-package
159 version: 0.1.0
160 namespace: example
161
162types:
163 sentence:
164 type: string
165
166states:
167 count-per-model:
168 type: keyed-state
169 properties:
170 key:
171 type: string
172 value:
173 type: u32
174
175functions:
176 my-hello-fn:
177 operator: filter-map
178 language: rust
179 inputs:
180 - name: input
181 type: sentence
182 output:
183 type: string
184
185dev:
186 converter: raw # options: raw, json
187"
188 .to_string();
189
190 let config = parse_package(&yaml).expect("should validate");
191
192 assert_eq!(config.meta.name, "my-package");
193 assert_eq!(config.meta.version, "0.1.0");
194 assert_eq!(config.meta.namespace, "example");
195
196 let types = &config.types;
197 let sentence_ty = types.map.get("sentence").expect("type to be found");
198 assert_eq!(
199 sentence_ty,
200 &MaybeValid::Valid(
201 MetadataTypeInner::MetadataTypeTagged(MetadataTypeTagged::String).into()
202 )
203 );
204
205 let states = &config.states;
206 assert_eq!(states.len(), 1);
207
208 config
209 .states
210 .iter()
211 .find(|(state_name, _)| *state_name == "count-per-model")
212 .expect("State to have parsed");
213
214 let function = config.functions.first().expect("should have a function");
215
216 match &function.inner().definition {
217 StepInvocationDefinition::Function(function) => {
218 assert_eq!(Lang::Rust, function.lang);
219 assert_eq!(function.uses, "my-hello-fn");
220 assert_eq!(function.inputs.len(), 1);
221 assert_eq!(function.inputs[0].name, "input");
222 assert_eq!(
223 function.inputs[0].ty,
224 MetadataTypeInner::NamedType(NamedType {
225 ty: "sentence".to_string()
226 })
227 .into()
228 );
229 assert_eq!(
230 function.output.as_ref().unwrap().ty,
231 MetadataTypeInner::MetadataTypeTagged(MetadataTypeTagged::String).into()
232 );
233 }
234 _ => panic!("incorrect function type parsed"),
235 }
236
237 assert_eq!(
238 config.dev.as_ref().unwrap().converter,
239 Some(SerdeConverter::Raw)
240 );
241 }
242
243 #[test]
244 fn test_valid_imports_validate() {
245 let yaml = "
246apiVersion: 0.5.0
247meta:
248 name: my-package
249 version: 0.1.0
250 namespace: example
251
252imports:
253 - pkg: example/bank-types@0.1.0
254 types:
255 - name: bank-event
256 states:
257 - name: account-balance
258
259functions:
260 update-bank-account:
261 operator: update-state
262 language: rust
263 states:
264 - name: account-balance
265 inputs:
266 - name: input
267 type: string
268
269dev:
270 converter: json
271 imports:
272 - pkg: example/bank-types@0.1.0
273 path: ../bank-types
274"
275 .to_string();
276
277 let config: PackageConfig = serde_yaml::from_str(&yaml).expect("function to parse");
278
279 let import = config.imports.first().expect("Should have an import");
280
281 assert_eq!(import.package.namespace, "example");
282 assert_eq!(import.package.name, "bank-types");
283 assert_eq!(import.package.version, "0.1.0");
284 assert_eq!(import.types[0].name, "bank-event");
285 assert_eq!(import.states[0].name, "account-balance");
286
287 let function = config.functions.first().expect("Should have a function");
288
289 match &function.inner().definition {
290 StepInvocationDefinition::Function(function) => {
291 assert_eq!(Lang::Rust, function.lang);
292 assert_eq!(function.uses, "update-bank-account");
293 assert_eq!(function.state_imports[0].name, "account-balance");
294 assert_eq!(function.inputs[0].name, "input");
295 assert_eq!(
296 function.inputs[0].ty,
297 MetadataTypeInner::MetadataTypeTagged(MetadataTypeTagged::String).into()
298 );
299 }
300 _ => panic!("incorrect function type parsed"),
301 }
302
303 let dev_config = config.dev.as_ref().expect("Should have dev config");
304
305 assert_eq!(dev_config.imports[0].package.namespace, "example");
306 assert_eq!(dev_config.imports[0].package.name, "bank-types");
307 assert_eq!(dev_config.imports[0].package.version, "0.1.0");
308 assert_eq!(
309 dev_config.imports[0].path,
310 Some(String::from("../bank-types"))
311 );
312 }
313
314 #[test]
315 fn test_import_names_are_validated() {
316 let yaml = "
317apiVersion: 0.5.0
318meta:
319 name: my-package
320 version: 0.1.0
321 namespace: example
322
323imports:
324 - pkg: bank-types@0.1.0
325 types:
326 - name: bank-event
327 states:
328 - name: account-balance
329
330functions:
331 update-bank-account:
332 operator: update-state
333 language: rust
334 states:
335 - name: account-balance
336 inputs:
337 - name: input
338 type: string
339
340dev:
341 converter: json
342 imports:
343 - pkg: example/bank-types@0.1.0
344 path: ../bank-types
345"
346 .to_string();
347
348 let error = serde_yaml::from_str::<PackageConfig>(&yaml).unwrap_err();
349
350 assert_eq!(
351 error.to_string(),
352 "invalid value: string \"bank-types@0.1.0\", expected a string of the form `<namespace>/<name>@<version>`"
353 );
354 }
355
356 #[test]
357 fn test_api_version() {
358 let v5_yaml = "
359apiVersion: 0.5.0
360meta:
361 name: my-package
362 version: 0.1.0
363 namespace: example
364"
365 .to_string();
366
367 let config: PackageConfig = serde_yaml::from_str(&v5_yaml).expect("function to parse");
368 assert!(config.is_v5());
369 assert!(!config.is_v4());
370 drop(config);
371
372 let v4_yaml = "
373 apiVersion: 0.4.0
374 meta:
375 name: my-package
376 version: 0.1.0
377 namespace: example
378
379 "
380 .to_string();
381
382 let config: PackageConfig = serde_yaml::from_str(&v4_yaml).expect("function to parse");
383 assert!(!config.is_v5());
384 assert!(config.is_v4());
385 }
386
387 #[test]
388 fn test_json_schema_def() {
389 let schema = schema_for!(PackageConfig);
390 let output = serde_json::to_string_pretty(&schema).expect("Failed to serialize JSON");
391 assert!(output.contains("$schema"));
392 }
393}