use aion_core::DescribeWorkflowResponse;
use aion_proto::{ProtoDescribeWorkflowResponse, WireEnvelope, WireError};
use serde_json::Value;
use super::error::HttpWireError;
pub(crate) const JSON_CONTENT_TYPE: &str = "application/json";
pub(super) fn http_input_payload(
input: Value,
) -> Result<aion_proto::convert::ProtoPayload, WireError> {
if is_payload_envelope(&input) {
serde_json::from_value::<aion_core::Payload>(input)
.map(aion_proto::convert::ProtoPayload::from)
.map_err(|_error| invalid_start_input())
} else {
serde_json::to_vec(&input)
.map(|bytes| aion_proto::convert::ProtoPayload {
content_type: JSON_CONTENT_TYPE.to_owned(),
bytes,
})
.map_err(|_error| invalid_start_input())
}
}
fn is_payload_envelope(input: &Value) -> bool {
input
.as_object()
.is_some_and(|object| object.contains_key("content_type") && object.contains_key("bytes"))
}
fn invalid_start_input() -> WireError {
WireError::invalid_input(
"start workflow request must be JSON shaped like \
{\"namespace\":\"tenant-a\",\"workflow_type\":\"example\",\"input\":{\"name\":\"Ada\"}} \
or {\"namespace\":\"tenant-a\",\"workflow_type\":\"example\",\"input\":{\"content_type\":\"Json\",\"bytes\":[123,125]}} \
— the envelope form is the one this API returns for payloads, so a value read off a run can be resubmitted unchanged",
)
}
pub(crate) fn describe_response_to_ops_console(
response: &ProtoDescribeWorkflowResponse,
) -> Result<DescribeWorkflowResponse, HttpWireError> {
let summary = response
.summary
.as_ref()
.map(decode_summary_envelope)
.transpose()?;
let history = response
.history
.iter()
.map(decode_event_envelope)
.collect::<Result<Vec<_>, _>>()?;
Ok(DescribeWorkflowResponse {
summary,
history,
unserved: Vec::new(),
})
}
fn decode_summary_envelope(
envelope: &WireEnvelope,
) -> Result<aion_core::WorkflowSummary, HttpWireError> {
aion_proto::decode_core_value::<aion_core::WorkflowSummary>(envelope).map_err(HttpWireError)
}
fn decode_event_envelope(envelope: &WireEnvelope) -> Result<aion_core::Event, HttpWireError> {
aion_proto::decode_event(envelope).map_err(HttpWireError)
}
#[cfg(test)]
mod tests {
use aion_proto::WireErrorCode;
use serde_json::json;
use super::*;
#[test]
fn http_start_input_normalization_accepts_plain_json_and_an_envelope()
-> Result<(), Box<dyn std::error::Error>> {
let plain = http_input_payload(json!({ "name": "Ada" }))?;
assert_eq!(plain.content_type, JSON_CONTENT_TYPE);
assert_eq!(
serde_json::from_slice::<serde_json::Value>(&plain.bytes)?,
json!({ "name": "Ada" })
);
let malformed =
http_input_payload(json!({ "content_type": "Json", "bytes": "not-a-byte-array" }));
assert!(matches!(malformed, Err(error) if error.code == WireErrorCode::InvalidInput));
Ok(())
}
#[test]
fn a_payload_serialised_the_way_this_api_emits_it_is_accepted_as_input()
-> Result<(), Box<dyn std::error::Error>> {
let emitted = aion_core::Payload::from_json(&json!({ "name": "Ada" }))?;
let as_this_api_returns_it = serde_json::to_value(&emitted)?;
assert_eq!(
as_this_api_returns_it.get("content_type"),
Some(&json!("Json")),
"the emitted content type moved; this round-trip no longer tests the read form"
);
let resubmitted = http_input_payload(as_this_api_returns_it)?;
assert_eq!(resubmitted.content_type, JSON_CONTENT_TYPE);
assert_eq!(
serde_json::from_slice::<serde_json::Value>(&resubmitted.bytes)?,
json!({ "name": "Ada" })
);
Ok(())
}
#[test]
fn the_grpc_wire_spelling_is_refused_as_an_http_envelope() {
let refused = http_input_payload(json!({
"content_type": "application/json",
"bytes": [123, 125],
}));
assert!(matches!(refused, Err(error) if error.code == WireErrorCode::InvalidInput));
}
}