use std::collections::{BTreeMap, BTreeSet};
use serde::{Deserialize, Serialize};
use crate::inertia::contracts::PageSchema;
pub const SCHEMA_VERSION: u32 = 1;
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct UagArtifact {
format: String,
schema_version: u32,
modules: BTreeMap<String, UagModule>,
routes: Vec<UagRoute>,
pages: BTreeMap<String, PageSchema>,
}
impl UagArtifact {
pub const FORMAT: &'static str = "arcature.uag.v1";
#[must_use]
pub fn new(
modules: BTreeMap<String, UagModule>,
mut routes: Vec<UagRoute>,
pages: BTreeMap<String, PageSchema>,
) -> Self {
routes.sort_by(|a, b| {
(&a.path, &a.method, &a.name, &a.handler)
.cmp(&(&b.path, &b.method, &b.name, &b.handler))
});
Self {
format: Self::FORMAT.to_owned(),
schema_version: SCHEMA_VERSION,
modules,
routes,
pages,
}
}
#[must_use]
pub fn format(&self) -> &str {
&self.format
}
#[must_use]
pub fn schema_version(&self) -> u32 {
self.schema_version
}
#[must_use]
pub fn modules(&self) -> &BTreeMap<String, UagModule> {
&self.modules
}
#[must_use]
pub fn routes(&self) -> &[UagRoute] {
&self.routes
}
#[must_use]
pub fn pages(&self) -> &BTreeMap<String, PageSchema> {
&self.pages
}
pub fn to_json(&self) -> Result<Vec<u8>, serde_json::Error> {
serde_json::to_vec_pretty(self)
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct UagModule {
pub imports: BTreeSet<String>,
pub exports: BTreeSet<String>,
pub controllers: BTreeMap<String, Vec<UagControllerMethod>>,
pub services: BTreeSet<String>,
pub policies: BTreeSet<String>,
pub pages: BTreeSet<String>,
pub listeners: Vec<UagListener>,
pub jobs: Vec<UagJob>,
pub commands: Vec<UagCommand>,
pub schedules: Vec<UagSchedule>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct UagControllerMethod {
pub name: String,
pub params: Vec<String>,
pub page: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct UagListener {
pub event: String,
pub listener: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct UagJob {
pub kind: String,
pub version: i16,
pub handler: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct UagCommand {
pub name: String,
pub function: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct UagSchedule {
pub job: String,
pub version: i16,
pub cadence: UagCadence,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(tag = "kind", rename_all = "snake_case")]
pub enum UagCadence {
Every {
seconds: u64,
},
Daily {
hour: u8,
minute: u8,
},
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct UagRoute {
pub module: String,
pub method: String,
pub path: String,
pub name: String,
pub handler: String,
pub params: Vec<String>,
pub pages: BTreeSet<String>,
pub action: Option<UagPayload>,
pub query: Option<UagQuery>,
pub query_string: Option<UagPayload>,
pub policies: BTreeSet<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct UagPayload {
pub type_name: String,
pub fields: Vec<UagField>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct UagQuery {
pub type_name: String,
pub array: bool,
pub fields: Vec<UagField>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct UagField {
pub name: String,
pub ty: String,
pub validates: Vec<String>,
}
#[cfg(test)]
mod tests {
use super::*;
use crate::inertia::contracts::{ContractType, PropsSchema};
fn route(path: &str, method: &str) -> UagRoute {
UagRoute {
module: "Links".to_owned(),
method: method.to_owned(),
path: path.to_owned(),
name: String::new(),
handler: "LinksController::index".to_owned(),
params: Vec::new(),
pages: BTreeSet::new(),
action: None,
query: None,
query_string: None,
policies: BTreeSet::new(),
}
}
fn artifact() -> UagArtifact {
let mut pages = BTreeMap::new();
pages.insert(
"Home".to_owned(),
PageSchema::new(PropsSchema::new().required("name", ContractType::string())),
);
UagArtifact::new(
BTreeMap::from([("Links".to_owned(), UagModule::default())]),
vec![route("/links/{link}", "GET"), route("/links", "GET")],
pages,
)
}
#[test]
fn carries_the_stable_format_identifier_and_schema_version() {
assert_eq!(artifact().format(), UagArtifact::FORMAT);
assert_eq!(artifact().schema_version(), SCHEMA_VERSION);
}
#[test]
fn routes_are_ordered_by_path_regardless_of_insertion_order() {
let artifact = artifact();
let paths: Vec<&str> = artifact.routes().iter().map(|r| r.path.as_str()).collect();
assert_eq!(paths, vec!["/links", "/links/{link}"]);
}
#[test]
fn a_duplicate_path_and_method_survives_into_the_artifact() {
let both = UagArtifact::new(
BTreeMap::new(),
vec![route("/links", "GET"), route("/links", "GET")],
BTreeMap::new(),
);
assert_eq!(both.routes().len(), 2, "validate reports it, so keep both");
}
#[test]
fn json_is_deterministic() {
assert_eq!(artifact().to_json().unwrap(), artifact().to_json().unwrap());
}
#[test]
fn json_carries_no_timestamp_and_no_absolute_path() {
let json = String::from_utf8(artifact().to_json().unwrap()).unwrap();
assert!(!json.contains("C:\\"), "{json}");
assert!(!json.contains("generated_at"), "{json}");
}
#[test]
fn round_trips_through_json() {
let json = artifact().to_json().unwrap();
let parsed: UagArtifact = serde_json::from_slice(&json).unwrap();
assert_eq!(parsed, artifact());
}
#[test]
fn the_cadence_tag_matches_the_runtime_cadence() {
let uag = serde_json::to_string(&UagCadence::Every { seconds: 300 }).unwrap();
let runtime =
serde_json::to_string(&crate::jobs::ScheduleCadence::Every { seconds: 300 }).unwrap();
assert_eq!(uag, runtime);
}
}