sdf_parser_package/pkg/
config.rs

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
129// for now treat as string but we will need to change this to a proper schema
130fn 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}