use std::collections::BTreeMap;
use std::fmt::Write as _;
use super::activity_model::ResolvedActivity;
use super::schema::{GleamType, SchemaArtifact};
pub(crate) const GENERATED_ACTIVITY_HEADER: &str =
"//// Generated by aion generate — do not edit; regenerate from the activity declarations.";
pub(crate) fn emit_codecs_module(package_name: &str, schemas: &[SchemaArtifact]) -> String {
let mut types: BTreeMap<&str, &str> = BTreeMap::new();
for artifact in schemas {
if let GleamType::Named {
type_name,
fn_prefix,
} = &artifact.root
{
types.insert(type_name.as_str(), fn_prefix.as_str());
}
}
let mut out = String::new();
let _ = writeln!(out, "{GENERATED_ACTIVITY_HEADER}");
out.push_str("////\n");
let _ = writeln!(
out,
"//// Typed codecs for the `{package_name}` activity value types, wrapping the"
);
out.push_str("//// encoders and decoders generated from `schemas/*.json`.\n");
out.push('\n');
out.push_str("import aion/codec\n");
let _ = writeln!(out, "import {package_name}_io as io");
for (gleam_type, fn_prefix) in &types {
let _ = write!(
out,
"\npub fn {fn_prefix}_codec() -> codec.Codec(io.{gleam_type}) {{\n \
codec.json_codec(io.{fn_prefix}_to_json, io.{fn_prefix}_decoder())\n}}\n"
);
}
out
}
pub(crate) fn emit_wrappers_module(package_name: &str, activities: &[ResolvedActivity]) -> String {
let mut out = String::new();
let _ = writeln!(out, "{GENERATED_ACTIVITY_HEADER}");
out.push_str("////\n");
let _ = writeln!(
out,
"//// Typed activity wrappers for the `{package_name}` workflow project, derived"
);
out.push_str("//// from the declarations its `manifest()` returns.\n");
out.push('\n');
out.push_str("import aion/activity\n");
let _ = writeln!(out, "import {package_name}_activities as activities");
let _ = writeln!(out, "import {package_name}_codecs as codecs");
let _ = writeln!(out, "import {package_name}_io as io");
for activity in activities {
let name = &activity.declaration.name;
let input_type = &activity.input.gleam_type;
let output_type = &activity.output.gleam_type;
let input_prefix = &activity.input.fn_prefix;
let output_prefix = &activity.output.fn_prefix;
let _ = write!(
out,
"\npub fn {name}_activity(\n input: io.{input_type},\n\
) -> activity.Activity(io.{input_type}, io.{output_type}) {{\n \
activity.new(\n \"{name}\",\n input,\n \
codecs.{input_prefix}_codec(),\n codecs.{output_prefix}_codec(),\n \
activities.{name},\n )\n}}\n"
);
}
out
}
#[cfg(test)]
mod tests {
use std::path::PathBuf;
use super::{emit_codecs_module, emit_wrappers_module};
use crate::codegen::activity_model::{ResolvedActivity, ResolvedType};
use crate::codegen::declaration::{ActivityDeclaration, Tier};
use crate::codegen::schema::{GleamType, SchemaArtifact};
fn artifact() -> SchemaArtifact {
SchemaArtifact {
file: PathBuf::from("schemas/placeholder.json"),
stem: "placeholder".to_owned(),
root: GleamType::String,
defs: Vec::new(),
}
}
fn declaration(name: &str, tier: Tier, input: &str, output: &str) -> ActivityDeclaration {
ActivityDeclaration {
name: name.to_owned(),
tier,
input_type: input.to_owned(),
output_type: output.to_owned(),
}
}
fn resolved<'a>(
declaration: &'a ActivityDeclaration,
art: &'a SchemaArtifact,
) -> ResolvedActivity<'a> {
let prefix = |ty: &str| ty.to_lowercase();
ResolvedActivity {
declaration,
input: ResolvedType {
gleam_type: declaration.input_type.clone(),
fn_prefix: prefix(&declaration.input_type),
artifact: art,
},
output: ResolvedType {
gleam_type: declaration.output_type.clone(),
fn_prefix: prefix(&declaration.output_type),
artifact: art,
},
}
}
fn named_artifact(type_name: &str) -> SchemaArtifact {
SchemaArtifact {
file: PathBuf::from(format!("schemas/{}.json", type_name.to_lowercase())),
stem: type_name.to_lowercase(),
root: GleamType::Named {
type_name: type_name.to_owned(),
fn_prefix: type_name.to_lowercase(),
},
defs: Vec::new(),
}
}
#[test]
fn codecs_module_emits_one_codec_per_named_root_in_order() {
let scalar = artifact();
let schemas = [
named_artifact("Order"),
named_artifact("Receipt"),
named_artifact("Shipment"),
scalar,
];
let module = emit_codecs_module("demo", &schemas);
assert!(module.starts_with(super::GENERATED_ACTIVITY_HEADER));
assert!(module.contains("import aion/codec\n"));
assert!(module.contains("import demo_io as io\n"));
let order = module.find("order_codec");
let receipt = module.find("receipt_codec");
let shipment = module.find("shipment_codec");
assert!(order.is_some() && receipt.is_some() && shipment.is_some());
assert!(order < receipt && receipt < shipment, "type-name order");
assert_eq!(module.matches("order_codec").count(), 1, "one per type");
assert!(!module.contains("placeholder_codec"));
assert!(module.contains(
"pub fn order_codec() -> codec.Codec(io.Order) {\n \
codec.json_codec(io.order_to_json, io.order_decoder())\n}\n"
));
}
#[test]
fn wrappers_module_emits_one_constructor_per_declaration_in_order() {
let art = artifact();
let d1 = declaration("reserve_inventory", Tier::RemotePython, "Order", "Receipt");
let d2 = declaration("ship_order", Tier::InVm, "Order", "Shipment");
let activities = [resolved(&d1, &art), resolved(&d2, &art)];
let module = emit_wrappers_module("demo", &activities);
assert!(module.starts_with(super::GENERATED_ACTIVITY_HEADER));
assert!(module.contains("import demo_activities as activities\n"));
assert!(module.contains("import demo_codecs as codecs\n"));
assert!(module.contains("import demo_io as io\n"));
let first = module.find("reserve_inventory_activity");
let second = module.find("ship_order_activity");
assert!(first.is_some() && second.is_some());
assert!(first < second, "declaration order preserved");
assert!(module.contains(
"pub fn reserve_inventory_activity(\n input: io.Order,\n\
) -> activity.Activity(io.Order, io.Receipt) {\n activity.new(\n \
\"reserve_inventory\",\n input,\n codecs.order_codec(),\n \
codecs.receipt_codec(),\n activities.reserve_inventory,\n )\n}\n"
));
}
}