Skip to main content

awsim_core/protocol/
mod.rs

1pub mod json;
2pub mod query;
3pub mod rest;
4
5use axum::http::{HeaderMap, Method, Uri};
6use bytes::Bytes;
7use serde_json::Value;
8
9use crate::error::AwsError;
10
11/// AWS API protocols.
12#[derive(Debug, Clone, Copy, PartialEq, Eq)]
13pub enum Protocol {
14    AwsJson1_0,
15    AwsJson1_1,
16    RestJson1,
17    RestXml,
18    AwsQuery,
19    Ec2Query,
20}
21
22impl Protocol {
23    pub fn response_content_type(&self) -> &'static str {
24        match self {
25            Self::AwsJson1_0 | Self::AwsJson1_1 | Self::RestJson1 => "application/x-amz-json-1.0",
26            Self::RestXml | Self::AwsQuery | Self::Ec2Query => "application/xml",
27        }
28    }
29
30    pub fn is_json(&self) -> bool {
31        matches!(self, Self::AwsJson1_0 | Self::AwsJson1_1 | Self::RestJson1)
32    }
33
34    pub fn is_xml(&self) -> bool {
35        matches!(self, Self::RestXml | Self::AwsQuery | Self::Ec2Query)
36    }
37}
38
39/// Parsed AWS request ready for dispatch to a service handler.
40#[derive(Debug)]
41pub struct ParsedRequest {
42    pub operation: String,
43    pub input: Value,
44}
45
46/// Route definition for REST-style services.
47#[derive(Debug, Clone)]
48pub struct RouteDefinition {
49    pub method: &'static str,
50    pub path_pattern: &'static str,
51    pub operation: &'static str,
52    /// For S3-style query parameter disambiguation.
53    /// e.g., PUT /{Bucket}?versioning → PutBucketVersioning
54    pub required_query_param: Option<&'static str>,
55}
56
57/// Detect which protocol an incoming request uses.
58pub fn detect_protocol(headers: &HeaderMap, body: &Bytes) -> Option<Protocol> {
59    // Check X-Amz-Target header → awsJson
60    if let Some(target) = headers.get("x-amz-target") {
61        let content_type = headers
62            .get("content-type")
63            .and_then(|v| v.to_str().ok())
64            .unwrap_or("");
65        if content_type.contains("x-amz-json-1.0") {
66            return Some(Protocol::AwsJson1_0);
67        }
68        // Default to 1.1 if X-Amz-Target present but content-type doesn't specify 1.0
69        let _ = target;
70        return Some(Protocol::AwsJson1_1);
71    }
72
73    let content_type = headers
74        .get("content-type")
75        .and_then(|v| v.to_str().ok())
76        .unwrap_or("");
77
78    // Check form-encoded → awsQuery or ec2Query
79    if content_type.contains("x-www-form-urlencoded") {
80        let body_str = std::str::from_utf8(body).unwrap_or("");
81        if body_str.contains("Action=") {
82            return Some(Protocol::AwsQuery);
83        }
84    }
85
86    // Check JSON content type → restJson1
87    if content_type.contains("json") {
88        return Some(Protocol::RestJson1);
89    }
90
91    // Check XML content type → restXml
92    if content_type.contains("xml") {
93        return Some(Protocol::RestXml);
94    }
95
96    // For REST protocols without explicit content-type (GET/HEAD/DELETE with no body),
97    // we determine protocol from the service's declared protocol
98    None
99}
100
101/// Parse a request based on the detected protocol.
102pub fn parse_request(
103    protocol: Protocol,
104    method: &Method,
105    uri: &Uri,
106    headers: &HeaderMap,
107    body: &Bytes,
108    routes: &[RouteDefinition],
109) -> Result<ParsedRequest, AwsError> {
110    match protocol {
111        Protocol::AwsJson1_0 | Protocol::AwsJson1_1 => json::parse_request(headers, body),
112        Protocol::AwsQuery | Protocol::Ec2Query => query::parse_request(body),
113        Protocol::RestJson1 => rest::parse_json_request(method, uri, body, routes),
114        Protocol::RestXml => rest::parse_xml_request(method, uri, headers, body, routes),
115    }
116}
117
118/// Serialize a successful response based on protocol.
119pub fn serialize_response(
120    protocol: Protocol,
121    operation: &str,
122    output: &Value,
123    request_id: &str,
124) -> (axum::http::StatusCode, HeaderMap, Bytes) {
125    match protocol {
126        Protocol::AwsJson1_0 | Protocol::AwsJson1_1 | Protocol::RestJson1 => {
127            json::serialize_response(output, request_id)
128        }
129        Protocol::AwsQuery | Protocol::Ec2Query => {
130            query::serialize_response(operation, output, request_id)
131        }
132        Protocol::RestXml => rest::serialize_xml_response(output, request_id),
133    }
134}
135
136/// Serialize an error response based on protocol.
137pub fn serialize_error(
138    protocol: Protocol,
139    error: &AwsError,
140    request_id: &str,
141) -> (axum::http::StatusCode, HeaderMap, Bytes) {
142    match protocol {
143        Protocol::AwsJson1_0 | Protocol::AwsJson1_1 | Protocol::RestJson1 => {
144            json::serialize_error(error, request_id)
145        }
146        Protocol::AwsQuery | Protocol::Ec2Query | Protocol::RestXml => {
147            query::serialize_error(error, request_id)
148        }
149    }
150}