use super::*;
use crate::codegen::bridge_api::{UserHttpBridgeApi, UserMqttBridge};
fn render(tokens: Ts) -> String {
let file: syn::File =
syn::parse2(tokens).expect("generated tokens should be a valid Rust file");
prettyplease::unparse(&file)
}
fn root() -> Ts {
quote! { ::myrmic_sdk }
}
fn assert_self_contained(out: &str) {
assert!(
out.contains("::myrmic_sdk::codegen::exports::serde::Serialize"),
"serde derive not routed through exports:\n{out}"
);
assert!(
out.contains("#[serde(crate = \""),
"missing #[serde(crate = ...)] attr:\n{out}"
);
let stripped = out
.replace("codegen::exports::std", "")
.replace("codegen::exports::serde", "")
.replace("codegen::exports::chrono", "")
.replace("codegen::exports::uuid", "");
assert!(!stripped.contains("::std::"), "unrouted ::std path:\n{out}");
assert!(
!stripped.contains("::serde"),
"unrouted ::serde path:\n{out}"
);
assert!(
!stripped.contains("::chrono"),
"unrouted ::chrono path:\n{out}"
);
assert!(!stripped.contains("::uuid"), "unrouted ::uuid path:\n{out}");
assert!(
!out.contains("From<String>"),
"bare String (trait arg):\n{out}"
);
assert!(
!out.contains("value: String"),
"bare String (fn param):\n{out}"
);
}
const HTTP_SPEC: &str = r#"
name: http
base_url: http://localhost:10000
types:
definitions:
SendMessageRequest:
type: object
required: [message]
properties:
message:
type: string
TestResponse:
type: object
required: [body]
properties:
body:
type: string
endpoints:
- id: send_message
request:
method: POST
path: /messages
body: "${json:SendMessageRequest}"
response:
200: "${json:TestResponse}"
- id: test
request:
method: GET
path: /test
response:
200: "${json:TestResponse}"
"#;
const MQTT_SPEC: &str = r#"
name: mqtt
broker_url: mqtt://localhost:11000
ingress:
- id: receive_request
topic: e2e/test/ingress
payload: "${json:data}"
egress:
- id: publish_response
topic: "${db:e2e/test/data@topic}"
payload: "${json:data}"
"#;
#[test]
fn http_bridge_generates_typify_types_and_a_method_per_endpoint() {
let api: UserHttpBridgeApi = serde_yaml::from_str(HTTP_SPEC).expect("parse http spec");
let out = render(http_bridge(&root(), api).expect("generate http bridge"));
assert!(out.contains("pub struct SendMessageRequest"), "{out}");
assert!(out.contains("pub struct TestResponse"), "{out}");
assert!(
out.contains("pub message: ::myrmic_sdk::codegen::exports::std::string::String"),
"{out}"
);
assert!(out.contains("pub struct HttpClient"), "{out}");
assert!(out.contains("pub fn send_message("), "{out}");
assert!(out.contains("body: &SendMessageRequest"), "{out}");
assert!(
out.contains("cb: ::myrmic_sdk::Callback<SendMessageReply>"),
"{out}"
);
assert!(out.contains("pub fn test("), "{out}");
assert!(
out.contains("cb: ::myrmic_sdk::Callback<TestReply>"),
"{out}"
);
assert!(out.contains("pub enum SendMessageReply"), "{out}");
assert!(out.contains("pub enum TestReply"), "{out}");
assert!(out.contains("Ok(TestResponse)"), "{out}");
assert!(out.contains("Unknown(u16)"), "{out}");
assert!(
out.contains("send(sri, \"send_message\", &__payload)"),
"{out}"
);
assert_self_contained(&out);
}
#[test]
fn http_request_payload_is_json_keyed_by_name_not_postcard() {
let api: UserHttpBridgeApi = serde_yaml::from_str(HTTP_SPEC).expect("parse http spec");
let out = render(http_bridge(&root(), api).expect("generate http bridge"));
assert!(
!out.contains("Postcard"),
"request payload must not use the postcard codec:\n{out}"
);
assert!(
out.contains("to_value"),
"json body should be stored via to_value:\n{out}"
);
assert!(
!out.contains("to_string(body)"),
"json body must not be pre-stringified:\n{out}"
);
assert!(
out.contains("body: ::myrmic_sdk::JsonValue"),
"payload body field should be keyed `body`:\n{out}"
);
assert!(out.contains("__callback"), "{out}");
}
#[test]
fn a_request_placeholder_named_callback_is_rejected() {
let spec = r#"
name: http
base_url: http://localhost
endpoints:
- id: go
request:
method: GET
path: "/go/${string:__callback}"
response: {}
"#;
let api: UserHttpBridgeApi = serde_yaml::from_str(spec).expect("parse spec");
let err =
http_bridge(&root(), api).expect_err("a placeholder named __callback must be rejected");
assert!(err.contains("__callback"), "got: {err}");
}
#[test]
fn mqtt_bridge_generates_client_events_and_command_methods() {
let api: UserMqttBridge = serde_yaml::from_str(MQTT_SPEC).expect("parse mqtt spec");
let out = render(mqtt_bridge(&root(), api).expect("generate mqtt bridge"));
assert!(out.contains("pub struct MqttClient"), "{out}");
assert!(out.contains("pub struct ReceiveRequest"), "{out}");
assert!(
out.contains("impl ::myrmic_sdk::CellEvent for ReceiveRequest"),
"{out}"
);
assert!(out.contains("pub struct PublishResponse"), "{out}");
assert!(
out.contains("pub fn publish_response(&self, value: PublishResponse)"),
"{out}"
);
assert_self_contained(&out);
}
const TYPED_HTTP_SPEC: &str = r##"
name: weather
base_url: "https://api.example.com"
types:
definitions:
Coordinates:
type: object
required: [lat, lon]
properties:
lat: { type: number, format: double }
lon: { type: number, format: double }
ForecastRequest:
type: object
required: [where, units]
properties:
where:
$ref: "#/definitions/Coordinates"
units:
type: string
enum: [metric, imperial]
ForecastResponse:
type: object
required: [station_id, observed_at, summary, temp_c]
properties:
station_id: { type: string, format: uuid }
observed_at: { type: string, format: date-time }
summary: { type: string }
temp_c: { type: number, format: double }
humidity: { type: integer, format: uint32 }
endpoints:
- id: get_forecast
request:
method: POST
path: "/v1/forecast"
body: "${json:ForecastRequest}"
timeout_ms: 5000
response:
200: "${json:ForecastResponse}"
"##;
#[test]
fn richer_schema_bridge_spec_generates() {
let api: UserHttpBridgeApi = serde_yaml::from_str(TYPED_HTTP_SPEC).expect("parse example spec");
let out = render(http_bridge(&root(), api).expect("generate example bridge"));
assert!(out.contains("pub struct Coordinates"), "{out}");
assert!(out.contains("pub struct ForecastRequest"), "{out}");
assert!(out.contains("pub struct ForecastResponse"), "{out}");
assert!(out.contains("pub struct WeatherClient"), "{out}");
assert!(out.contains("pub fn get_forecast("), "{out}");
assert!(out.contains("body: &ForecastRequest"), "{out}");
assert!(
out.contains("cb: ::myrmic_sdk::Callback<GetForecastReply>"),
"{out}"
);
assert!(out.contains("pub enum GetForecastReply"), "{out}");
assert!(out.contains("Ok(ForecastResponse)"), "{out}");
assert!(out.contains("Unknown(u16)"), "{out}");
assert!(
out.contains("pub whereField: Coordinates") || out.contains(": Coordinates"),
"{out}"
);
assert!(out.contains("pub enum") && out.contains("Metric"), "{out}");
assert!(
out.contains("::myrmic_sdk::codegen::exports::uuid::Uuid"),
"uuid format not routed:\n{out}"
);
assert!(
out.contains("::myrmic_sdk::codegen::exports::chrono::"),
"date-time format not routed:\n{out}"
);
assert_self_contained(&out);
}
#[test]
fn json_body_without_a_matching_definition_falls_back_to_jsonvalue() {
let spec = r"
name: http
base_url: http://localhost
types:
definitions:
Known:
type: object
properties:
x: { type: string }
endpoints:
- id: go
request:
method: POST
path: /go
body: '${json:not_a_definition}'
response: {}
";
let api: UserHttpBridgeApi = serde_yaml::from_str(spec).expect("parse spec");
let out = render(http_bridge(&root(), api).expect("generate"));
assert!(out.contains("pub fn go("), "{out}");
assert!(out.contains("body: &::myrmic_sdk::JsonValue"), "{out}");
assert!(out.contains("cb: ::myrmic_sdk::Callback<GoReply>"), "{out}");
}
#[test]
fn response_statuses_become_reply_enum_variants() {
let spec = r#"
name: http
base_url: http://localhost
types:
definitions:
Ship:
type: object
properties:
id: { type: string }
endpoints:
- id: list_ships
request:
method: GET
path: /ships
response:
200:
headers:
x-api-time: "${string:time}"
body: "${json:Ship}"
404: "${string:message}"
204: {}
"#;
let api: UserHttpBridgeApi = serde_yaml::from_str(spec).expect("parse spec");
let out = render(http_bridge(&root(), api).expect("generate"));
assert!(out.contains("pub enum ListShipsReply"), "{out}");
assert!(out.contains("Ok {"), "{out}");
assert!(out.contains("time: ::myrmic_sdk::String"), "{out}");
assert!(out.contains("body: Ship"), "{out}");
assert!(out.contains("NotFound(::myrmic_sdk::String)"), "{out}");
assert!(out.contains("NoContent"), "{out}");
assert!(out.contains("Unknown(u16)"), "{out}");
}
const HTTP_PARAMS_SPEC: &str = r#"
name: spacetraders
base_url: https://api.spacetraders.io
endpoints:
- id: negotiate_contract
request:
method: POST
path: "/my/ships/${string:ship}/negotiate/contract"
headers:
authorization: "Bearer ${db:keys/secrets/spacetraders@token}"
response:
200: "${json:body}"
"#;
#[test]
fn http_endpoint_with_path_param_generates_positional_arg_and_typed_callback() {
let api: UserHttpBridgeApi = serde_yaml::from_str(HTTP_PARAMS_SPEC).expect("parse spec");
let out = render(http_bridge(&root(), api).expect("generate http bridge"));
assert!(out.contains("pub enum NegotiateContractReply"), "{out}");
assert!(out.contains("Ok(::myrmic_sdk::JsonValue)"), "{out}");
assert!(out.contains("Unknown(u16)"), "{out}");
assert!(out.contains("pub fn negotiate_contract("), "{out}");
assert!(out.contains("ship: &str"), "{out}");
assert!(
out.contains("cb: ::myrmic_sdk::Callback<NegotiateContractReply>"),
"{out}"
);
assert_self_contained(&out);
}