use std::collections::HashSet;
use serde::Deserialize;
use super::error::CodegenError;
use super::names::{is_reserved_word, is_snake_identifier};
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Tier {
InVm,
RemotePython,
RemoteRust,
}
impl Tier {
fn from_wire(value: &str) -> Option<Self> {
match value {
"in_vm" => Some(Self::InVm),
"remote_python" => Some(Self::RemotePython),
"remote_rust" => Some(Self::RemoteRust),
_ => None,
}
}
#[must_use]
pub fn is_remote(self) -> bool {
matches!(self, Self::RemotePython | Self::RemoteRust)
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct ActivityDeclaration {
pub name: String,
pub tier: Tier,
pub input_type: String,
pub output_type: String,
}
#[derive(Deserialize)]
#[serde(deny_unknown_fields)]
struct RawDeclaration {
name: String,
tier: String,
input: String,
output: String,
}
pub fn parse_declarations(json: &[u8]) -> Result<Vec<ActivityDeclaration>, CodegenError> {
let raw: Vec<RawDeclaration> =
serde_json::from_slice(json).map_err(|source| CodegenError::ManifestParse { source })?;
let mut declarations = Vec::with_capacity(raw.len());
let mut seen: HashSet<&str> = HashSet::with_capacity(raw.len());
for entry in &raw {
if !is_snake_identifier(&entry.name) || is_reserved_word(&entry.name) {
return Err(CodegenError::InvalidActivityName {
name: entry.name.clone(),
reason: "must be a snake_case identifier (a lowercase letter followed by \
lowercase letters, digits, or underscores) and not a Gleam reserved \
word, so it can name the generated wrapper function and worker handler"
.to_owned(),
});
}
if !seen.insert(entry.name.as_str()) {
return Err(CodegenError::DuplicateActivity {
name: entry.name.clone(),
});
}
let tier = Tier::from_wire(&entry.tier).ok_or_else(|| CodegenError::UnknownTier {
value: entry.tier.clone(),
})?;
declarations.push(ActivityDeclaration {
name: entry.name.clone(),
tier,
input_type: entry.input.clone(),
output_type: entry.output.clone(),
});
}
Ok(declarations)
}
#[cfg(test)]
mod tests {
use super::{Tier, parse_declarations};
use crate::codegen::error::CodegenError;
const WIRE: &[u8] = br#"[
{"name":"reserve_inventory","tier":"remote_python","input":"OrderInput","output":"InventoryReservation"},
{"name":"ship_order","tier":"in_vm","input":"OrderInput","output":"Shipment"}
]"#;
#[test]
fn parses_in_declaration_order() -> Result<(), CodegenError> {
let declarations = parse_declarations(WIRE)?;
assert_eq!(declarations.len(), 2);
assert_eq!(declarations[0].name, "reserve_inventory");
assert_eq!(declarations[0].tier, Tier::RemotePython);
assert!(declarations[0].tier.is_remote());
assert_eq!(declarations[0].input_type, "OrderInput");
assert_eq!(declarations[0].output_type, "InventoryReservation");
assert_eq!(declarations[1].name, "ship_order");
assert_eq!(declarations[1].tier, Tier::InVm);
assert!(!declarations[1].tier.is_remote());
Ok(())
}
#[test]
fn empty_manifest_parses_to_no_declarations() -> Result<(), CodegenError> {
assert!(parse_declarations(b"[]")?.is_empty());
Ok(())
}
#[test]
fn unknown_tier_is_rejected() {
let json = br#"[{"name":"x","tier":"remote_go","input":"A","output":"B"}]"#;
assert!(matches!(
parse_declarations(json),
Err(CodegenError::UnknownTier { value }) if value == "remote_go"
));
}
#[test]
fn duplicate_activity_is_rejected() {
let json = br#"[
{"name":"x","tier":"in_vm","input":"A","output":"B"},
{"name":"x","tier":"in_vm","input":"A","output":"B"}
]"#;
assert!(matches!(
parse_declarations(json),
Err(CodegenError::DuplicateActivity { name }) if name == "x"
));
}
#[test]
fn names_that_cannot_derive_an_identifier_are_rejected() {
for bad in [
"a/b",
"ReserveInventory",
"reserve-inventory",
"1st",
"type",
"",
] {
let json = format!(r#"[{{"name":"{bad}","tier":"in_vm","input":"A","output":"B"}}]"#);
assert!(
matches!(
parse_declarations(json.as_bytes()),
Err(CodegenError::InvalidActivityName { name, .. }) if name == bad
),
"expected InvalidActivityName for `{bad}`"
);
}
}
#[test]
fn well_formed_snake_case_names_are_accepted() -> Result<(), CodegenError> {
let json = br#"[{"name":"reserve_inventory_v2","tier":"in_vm","input":"A","output":"B"}]"#;
assert_eq!(parse_declarations(json)?[0].name, "reserve_inventory_v2");
Ok(())
}
#[test]
fn malformed_json_is_rejected() {
assert!(matches!(
parse_declarations(b"not json"),
Err(CodegenError::ManifestParse { .. })
));
}
}