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