use serde::{Deserialize, Serialize};
#[derive(Debug, Deserialize, Serialize)]
pub(crate) struct MetadataArtifact {
pub(crate) modules: Vec<ModuleEntry>,
pub(crate) routes: Vec<RouteEntry>,
pub(crate) services: Vec<ServiceEntry>,
}
#[derive(Debug, Deserialize, Serialize)]
pub(crate) struct ModuleEntry {
pub(crate) name: String,
#[serde(default)]
pub(crate) imports: Vec<String>,
#[serde(default)]
pub(crate) exports: Vec<String>,
#[serde(default)]
pub(crate) controllers: Vec<String>,
#[serde(default)]
pub(crate) services: Vec<String>,
#[serde(default)]
pub(crate) policies: Vec<String>,
#[serde(default)]
pub(crate) routes: Vec<RouteEntry>,
#[serde(default)]
pub(crate) listeners: Vec<ListenerEntry>,
#[serde(default)]
pub(crate) jobs: Vec<JobEntry>,
#[serde(default)]
pub(crate) commands: Vec<CommandEntry>,
#[serde(default)]
pub(crate) schedules: Vec<ScheduleEntry>,
}
#[derive(Debug, Deserialize, Serialize)]
pub(crate) struct RouteEntry {
pub(crate) method: String,
pub(crate) path: String,
#[serde(default)]
pub(crate) name: String,
pub(crate) handler: String,
}
#[derive(Debug, Deserialize, Serialize)]
pub(crate) struct ServiceEntry {
pub(crate) name: String,
#[serde(default)]
pub(crate) deps: Vec<String>,
}
#[derive(Debug, Deserialize, Serialize)]
pub(crate) struct ListenerEntry {
pub(crate) event: String,
pub(crate) listener: String,
}
#[derive(Debug, Deserialize, Serialize)]
pub(crate) struct JobEntry {
pub(crate) kind: String,
pub(crate) version: i16,
pub(crate) handler: String,
}
#[derive(Debug, Deserialize, Serialize)]
pub(crate) struct CommandEntry {
pub(crate) name: String,
pub(crate) function: String,
}
#[derive(Debug, Deserialize, Serialize)]
pub(crate) struct ScheduleEntry {
pub(crate) job: String,
pub(crate) version: i16,
pub(crate) cadence: CadenceEntry,
}
#[derive(Debug, Deserialize, Serialize, PartialEq)]
#[serde(tag = "kind", rename_all = "snake_case")]
pub(crate) enum CadenceEntry {
Every { seconds: u64 },
Daily { hour: u8, minute: u8 },
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn deserializes_empty_artifact() {
let json = r#"{"modules":[],"routes":[],"services":[]}"#;
let artifact: MetadataArtifact =
serde_json::from_str(json).expect("empty artifact should parse");
assert!(artifact.modules.is_empty());
assert!(artifact.routes.is_empty());
assert!(artifact.services.is_empty());
}
#[test]
fn deserializes_full_artifact() {
let json = r#"{
"modules": [{
"name": "Links",
"imports": ["Accounts"],
"exports": ["LinkService"],
"controllers": ["LinksController"],
"services": ["LinkService"],
"policies": ["LinkPolicy"],
"routes": [{
"method": "get",
"path": "/links",
"name": "links.index",
"handler": "LinksController::index"
}],
"listeners": [{"event": "LinkCreated", "listener": "log_creation"}],
"jobs": [{"kind": "check_links", "version": 1, "handler": "handle_check_links"}],
"commands": [{"name": "links:prune", "function": "prune_links"}],
"schedules": [{
"job": "cleanup_sessions",
"version": 1,
"cadence": {"kind": "every", "seconds": 300}
}]
}],
"routes": [{
"method": "get",
"path": "/links/{link}",
"name": "links.show",
"handler": "LinksController::show"
}],
"services": [{
"name": "LinkService",
"deps": ["Db", "Cache"]
}]
}"#;
let artifact: MetadataArtifact =
serde_json::from_str(json).expect("full artifact should parse");
assert_eq!(artifact.modules.len(), 1);
assert_eq!(artifact.modules[0].name, "Links");
assert_eq!(artifact.modules[0].imports, ["Accounts"]);
assert_eq!(artifact.modules[0].controllers, ["LinksController"]);
assert_eq!(artifact.modules[0].routes.len(), 1);
assert_eq!(artifact.modules[0].routes[0].method, "get");
assert_eq!(artifact.modules[0].routes[0].path, "/links");
assert_eq!(artifact.modules[0].listeners.len(), 1);
assert_eq!(artifact.modules[0].jobs[0].kind, "check_links");
assert_eq!(artifact.modules[0].commands[0].name, "links:prune");
assert_eq!(artifact.modules[0].schedules.len(), 1);
assert_eq!(
artifact.modules[0].schedules[0].cadence,
CadenceEntry::Every { seconds: 300 }
);
assert_eq!(artifact.routes.len(), 1);
assert_eq!(artifact.routes[0].handler, "LinksController::show");
assert_eq!(artifact.services.len(), 1);
assert_eq!(artifact.services[0].deps, ["Db", "Cache"]);
}
#[test]
fn deserializes_daily_cadence() {
let json = r#"{
"job": "nightly_report",
"version": 2,
"cadence": {"kind": "daily", "hour": 3, "minute": 0}
}"#;
let entry: ScheduleEntry = serde_json::from_str(json).expect("daily cadence should parse");
assert_eq!(entry.job, "nightly_report");
assert_eq!(entry.version, 2);
assert_eq!(entry.cadence, CadenceEntry::Daily { hour: 3, minute: 0 });
}
#[test]
fn deserializes_with_defaults_for_empty_sections() {
let json = r#"{
"modules": [{"name": "Empty"}],
"routes": [],
"services": []
}"#;
let artifact: MetadataArtifact = serde_json::from_str(json).expect("defaults should parse");
assert_eq!(artifact.modules.len(), 1);
assert!(artifact.modules[0].imports.is_empty());
assert!(artifact.modules[0].controllers.is_empty());
assert!(artifact.modules[0].routes.is_empty());
assert!(artifact.modules[0].schedules.is_empty());
}
#[test]
fn rejects_invalid_cadence_kind() {
let json = r#"{
"job": "bad",
"version": 1,
"cadence": {"kind": "weekly", "day": 1}
}"#;
let err = serde_json::from_str::<ScheduleEntry>(json);
assert!(err.is_err(), "unknown cadence kind should fail");
}
#[test]
fn round_trips_through_serialize_deserialize() {
let original = MetadataArtifact {
modules: vec![ModuleEntry {
name: "Test".to_owned(),
imports: vec!["A".to_owned()],
exports: vec![],
controllers: vec!["C".to_owned()],
services: vec![],
policies: vec![],
routes: vec![],
listeners: vec![],
jobs: vec![],
commands: vec![],
schedules: vec![ScheduleEntry {
job: "j".to_owned(),
version: 1,
cadence: CadenceEntry::Every { seconds: 60 },
}],
}],
routes: vec![RouteEntry {
method: "post".to_owned(),
path: "/x".to_owned(),
name: "x".to_owned(),
handler: "X::create".to_owned(),
}],
services: vec![ServiceEntry {
name: "S".to_owned(),
deps: vec!["Db".to_owned()],
}],
};
let json = serde_json::to_string(&original).expect("serialize should succeed");
let round_tripped: MetadataArtifact =
serde_json::from_str(&json).expect("deserialize should succeed");
assert_eq!(round_tripped.modules.len(), 1);
assert_eq!(round_tripped.routes.len(), 1);
assert_eq!(round_tripped.services.len(), 1);
assert_eq!(
round_tripped.modules[0].schedules[0].cadence,
CadenceEntry::Every { seconds: 60 }
);
}
}