1use std::collections::{BTreeMap, BTreeSet};
2
3use percent_encoding::{NON_ALPHANUMERIC, utf8_percent_encode};
4use serde_json::Value;
5
6use crate::{
7 client::OperationInput,
8 error::SdkError,
9 generated::{
10 GeneratedOperationDescriptor, GeneratedParameterDescriptor, GeneratedResponseDescriptor,
11 ParameterLocation,
12 },
13 transport::{SdkRequest, SdkResponse},
14};
15
16#[derive(Clone, Debug, Eq, PartialEq)]
18pub struct DecodedResponse {
19 pub headers: BTreeMap<String, String>,
21 pub json_body: Option<Value>,
23 pub raw_body: Vec<u8>,
25 pub schema_name: Option<&'static str>,
27 pub status: u16,
29}
30
31impl DecodedResponse {
32 #[must_use]
34 pub const fn json_body(&self) -> Option<&Value> {
35 self.json_body.as_ref()
36 }
37}
38
39pub(crate) fn encode_request(
40 descriptor: &'static GeneratedOperationDescriptor,
41 input: OperationInput,
42) -> Result<SdkRequest, SdkError> {
43 let mut headers = input.headers;
44 let body = encode_body(descriptor, input.body, &mut headers)?;
45 let path = encode_path(descriptor, &input.path_params)?;
46 let path = encode_query(descriptor, &path, &input.query_params)?;
47 validate_required_headers(descriptor, &headers)?;
48
49 Ok(SdkRequest {
50 body,
51 headers,
52 method: descriptor.method.to_string(),
53 path,
54 requires_auth: descriptor.requires_auth,
55 })
56}
57
58pub(crate) fn decode_response(
59 descriptor: &'static GeneratedOperationDescriptor,
60 response: SdkResponse,
61) -> Result<DecodedResponse, SdkError> {
62 let matched_response = match_response(descriptor, response.status)?;
63 let json_body = decode_json_body(matched_response, &response.body)?;
64 let decoded = DecodedResponse {
65 headers: response.headers,
66 json_body: json_body.clone(),
67 raw_body: response.body,
68 schema_name: matched_response.schema_name,
69 status: matched_response.status,
70 };
71
72 if matched_response.is_error {
73 return Err(SdkError::ApiResponse {
74 body: json_body,
75 operation_id: descriptor.operation_id.to_string(),
76 schema_name: matched_response.schema_name,
77 status: matched_response.status,
78 });
79 }
80
81 Ok(decoded)
82}
83
84fn encode_body(
85 descriptor: &'static GeneratedOperationDescriptor,
86 body: Option<Vec<u8>>,
87 headers: &mut BTreeMap<String, String>,
88) -> Result<Option<Vec<u8>>, SdkError> {
89 let Some(request_body) = descriptor.request_body else {
90 return Ok(None);
91 };
92
93 if body.is_none() && request_body.required && !request_body.nullable {
94 return Err(SdkError::MissingRequestBody {
95 operation_id: descriptor.operation_id.to_string(),
96 });
97 }
98
99 if body.is_some() &&
100 let Some(content_type) = request_body.content_type
101 {
102 headers.entry("content-type".to_string()).or_insert_with(|| content_type.to_string());
103 }
104
105 Ok(body)
106}
107
108fn encode_path(
109 descriptor: &'static GeneratedOperationDescriptor,
110 path_params: &BTreeMap<String, String>,
111) -> Result<String, SdkError> {
112 let mut encoded_path = descriptor.path.to_string();
113
114 for parameter in descriptor.parameters.iter().filter(is_path_parameter) {
115 let value = path_params
116 .get(parameter.name)
117 .ok_or_else(|| missing_parameter(descriptor, parameter))?;
118 let placeholder = format!("{{{}}}", parameter.name);
119 encoded_path = encoded_path.replace(&placeholder, &encode_component(value));
120 }
121
122 if encoded_path.contains('{') || encoded_path.contains('}') {
123 return Err(SdkError::InvalidPathTemplate {
124 operation_id: descriptor.operation_id.to_string(),
125 path_template: descriptor.path.to_string(),
126 });
127 }
128
129 Ok(encoded_path)
130}
131
132fn encode_query(
133 descriptor: &'static GeneratedOperationDescriptor,
134 path: &str,
135 query_params: &BTreeMap<String, Vec<String>>,
136) -> Result<String, SdkError> {
137 let known_query_names = descriptor
138 .parameters
139 .iter()
140 .filter(is_query_parameter)
141 .map(|parameter| parameter.name)
142 .collect::<BTreeSet<_>>();
143 let mut encoded_pairs = Vec::new();
144
145 for parameter in descriptor.parameters.iter().filter(is_query_parameter) {
146 if parameter.required && !query_params.contains_key(parameter.name) {
147 return Err(missing_parameter(descriptor, parameter));
148 }
149
150 if let Some(values) = query_params.get(parameter.name) {
151 encoded_pairs.extend(values.iter().map(|value| {
152 format!("{}={}", encode_component(parameter.name), encode_component(value))
153 }));
154 }
155 }
156
157 for (name, values) in query_params {
158 if known_query_names.contains(name.as_str()) {
159 continue;
160 }
161
162 encoded_pairs.extend(
163 values
164 .iter()
165 .map(|value| format!("{}={}", encode_component(name), encode_component(value))),
166 );
167 }
168
169 if encoded_pairs.is_empty() {
170 return Ok(path.to_string());
171 }
172
173 Ok(format!("{path}?{}", encoded_pairs.join("&")))
174}
175
176fn validate_required_headers(
177 descriptor: &'static GeneratedOperationDescriptor,
178 headers: &BTreeMap<String, String>,
179) -> Result<(), SdkError> {
180 for parameter in descriptor.parameters.iter().filter(is_header_parameter) {
181 if parameter.required && !headers.contains_key(parameter.name) {
182 return Err(missing_parameter(descriptor, parameter));
183 }
184 }
185
186 Ok(())
187}
188
189fn match_response(
190 descriptor: &'static GeneratedOperationDescriptor,
191 status: u16,
192) -> Result<&'static GeneratedResponseDescriptor, SdkError> {
193 descriptor.responses.iter().find(|response| response.status == status).ok_or_else(|| {
194 SdkError::UnexpectedStatus { actual: status, expected: descriptor.primary_success_status }
195 })
196}
197
198fn decode_json_body(
199 response: &'static GeneratedResponseDescriptor,
200 body: &[u8],
201) -> Result<Option<Value>, SdkError> {
202 if body.is_empty() {
203 return Ok(None);
204 }
205
206 let expects_json = response.content_type.map_or_else(
207 || response.schema_name.is_some(),
208 |content_type| content_type == "application/json" || content_type.ends_with("+json"),
209 );
210
211 if !expects_json {
212 return Ok(None);
213 }
214
215 serde_json::from_slice(body).map(Some).map_err(SdkError::Decode)
216}
217
218fn missing_parameter(
219 descriptor: &'static GeneratedOperationDescriptor,
220 parameter: &'static GeneratedParameterDescriptor,
221) -> SdkError {
222 SdkError::MissingParameter {
223 location: match parameter.location {
224 ParameterLocation::Header => "header",
225 ParameterLocation::Path => "path",
226 ParameterLocation::Query => "query",
227 },
228 name: parameter.name.to_string(),
229 operation_id: descriptor.operation_id.to_string(),
230 }
231}
232
233fn is_header_parameter(parameter: &&GeneratedParameterDescriptor) -> bool {
234 parameter.location == ParameterLocation::Header
235}
236
237fn is_path_parameter(parameter: &&GeneratedParameterDescriptor) -> bool {
238 parameter.location == ParameterLocation::Path
239}
240
241fn is_query_parameter(parameter: &&GeneratedParameterDescriptor) -> bool {
242 parameter.location == ParameterLocation::Query
243}
244
245fn encode_component(value: &str) -> String {
246 utf8_percent_encode(value, NON_ALPHANUMERIC).to_string()
247}