use std::collections::BTreeMap;
use std::collections::HashMap;
use serde::Deserialize;
use serde::Serialize;
use quilt_uri::S3Uri;
use quilt_uri::UriError;
#[derive(Debug, Clone, PartialEq)]
pub struct WorkflowId {
pub id: String,
pub schemas: BTreeMap<String, S3Uri>,
}
#[derive(Debug, Clone, PartialEq)]
pub struct Workflow {
pub config: S3Uri,
pub id: Option<WorkflowId>,
}
impl<'de> Deserialize<'de> for Workflow {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
#[derive(Deserialize)]
struct WorkflowHelper {
config: String,
id: Option<String>,
schemas: Option<HashMap<String, String>>,
}
let helper = WorkflowHelper::deserialize(deserializer)?;
let id = match helper.id {
None => None,
Some(id) => {
let mut schemas = BTreeMap::new();
for (schema_id, schema_url) in helper.schemas.unwrap_or_default() {
let url = schema_url
.parse()
.map_err(|_| serde::de::Error::custom(UriError::S3(schema_url.clone())))?;
schemas.insert(schema_id, url);
}
Some(WorkflowId { id, schemas })
}
};
Ok(Workflow {
config: helper.config.parse().map_err(serde::de::Error::custom)?,
id,
})
}
}
impl Serialize for Workflow {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
use serde::ser::SerializeStruct;
if let Some(workflow_id) = &self.id {
let emit_schemas = !workflow_id.schemas.is_empty();
let mut state =
serializer.serialize_struct("Workflow", 2 + usize::from(emit_schemas))?;
state.serialize_field("config", &self.config.to_string())?;
state.serialize_field("id", &workflow_id.id)?;
if emit_schemas {
let schemas: BTreeMap<&str, String> = workflow_id
.schemas
.iter()
.map(|(id, url)| (id.as_str(), url.to_string()))
.collect();
state.serialize_field("schemas", &schemas)?;
}
state.end()
} else {
let mut state = serializer.serialize_struct("Workflow", 2)?;
state.serialize_field("config", &self.config.to_string())?;
state.serialize_field("id", &None::<String>)?;
state.end()
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use test_log::test;
type Res = Result<(), Box<dyn std::error::Error>>;
#[test]
fn test_workflow_deserialization() -> Res {
let json = r#"{
"config": "s3://workflow/config",
"id": "test-workflow",
"schemas": {
"test-schema": "s3://bucket/workflows/test.json"
}
}"#;
let workflow: Workflow = serde_json::from_str(json)?;
assert_eq!(workflow.config, "s3://workflow/config".parse()?);
assert_eq!(
workflow.id,
Some(WorkflowId {
id: "test-workflow".to_string(),
schemas: BTreeMap::from([(
"test-schema".to_string(),
"s3://bucket/workflows/test.json".parse()?
)]),
})
);
Ok(())
}
#[test]
fn test_workflow_deserialization_two_schemas() -> Res {
let json = r#"{
"config": "s3://workflow/config",
"id": "dual",
"schemas": {
"meta-schema": "s3://bucket/workflows/meta.json",
"entries-schema": "s3://bucket/workflows/entries.json"
}
}"#;
let workflow: Workflow = serde_json::from_str(json)?;
assert_eq!(
workflow.id,
Some(WorkflowId {
id: "dual".to_string(),
schemas: BTreeMap::from([
(
"meta-schema".to_string(),
"s3://bucket/workflows/meta.json".parse()?
),
(
"entries-schema".to_string(),
"s3://bucket/workflows/entries.json".parse()?
),
]),
})
);
Ok(())
}
#[test]
fn test_workflow_deserialization_none() -> Res {
let json = r#"{
"config": "s3://workflow/config",
"id": null
}"#;
let workflow: Workflow = serde_json::from_str(json)?;
assert_eq!(workflow.config, "s3://workflow/config".parse()?);
assert_eq!(workflow.id, None);
Ok(())
}
#[test]
fn test_workflow_serialization() -> Res {
let workflow = Workflow {
config: "s3://workflow/config".parse()?,
id: Some(WorkflowId {
id: "test-workflow".to_string(),
schemas: BTreeMap::from([(
"test-schema".to_string(),
"s3://bucket/workflows/test.json".parse()?,
)]),
}),
};
let json = serde_json::to_value(&workflow).unwrap();
assert_eq!(
json,
serde_json::json!({
"config": "s3://workflow/config",
"id": "test-workflow",
"schemas": {
"test-schema": "s3://bucket/workflows/test.json"
}
})
);
Ok(())
}
#[test]
fn test_workflow_serialization_two_schemas() -> Res {
let workflow = Workflow {
config: "s3://workflow/config".parse()?,
id: Some(WorkflowId {
id: "dual".to_string(),
schemas: BTreeMap::from([
(
"meta-schema".to_string(),
"s3://bucket/workflows/meta.json".parse()?,
),
(
"entries-schema".to_string(),
"s3://bucket/workflows/entries.json".parse()?,
),
]),
}),
};
let json = serde_json::to_value(&workflow).unwrap();
assert_eq!(
json,
serde_json::json!({
"config": "s3://workflow/config",
"id": "dual",
"schemas": {
"meta-schema": "s3://bucket/workflows/meta.json",
"entries-schema": "s3://bucket/workflows/entries.json"
}
})
);
Ok(())
}
#[test]
fn test_workflow_serialization_no_schemas_omits_field() -> Res {
let workflow = Workflow {
config: "s3://workflow/config".parse()?,
id: Some(WorkflowId {
id: "schema-less".to_string(),
schemas: BTreeMap::new(),
}),
};
let json = serde_json::to_value(&workflow).unwrap();
assert_eq!(
json,
serde_json::json!({
"config": "s3://workflow/config",
"id": "schema-less"
})
);
Ok(())
}
#[test]
fn test_workflow_serialization_none() -> Res {
let workflow = Workflow {
config: "s3://workflow/config".parse()?,
id: None,
};
let json = serde_json::to_value(&workflow).unwrap();
assert_eq!(
json,
serde_json::json!({
"config": "s3://workflow/config",
"id": null
})
);
Ok(())
}
}