1use http::{Request, Uri, header};
14use thiserror::Error;
15
16use crate::model::{Body, Join, Location, Operation, Param, Shape, Unsupported};
17use crate::multipart;
18use crate::scalar::ScalarError;
19use crate::values::{Payload, Values};
20
21#[derive(Debug, Clone, Error, PartialEq, Eq)]
23pub enum ValueError {
24 #[error("{op}: `{name}` is required")]
25 MissingParam { op: String, name: String },
26 #[error("{op}: there is no `{name}` parameter")]
27 UnknownParam { op: String, name: String },
28 #[error("{op}: `{name}` is {why}, so there is nowhere in the request to put a value for it")]
31 UnsupportedParam {
32 op: String,
33 name: String,
34 why: Unsupported,
35 },
36 #[error("{op}: `{name}` takes one value, and was given {given}")]
39 RepeatedParam {
40 op: String,
41 name: String,
42 given: usize,
43 },
44 #[error("{op}: `{name}`: {source}")]
45 BadValue {
46 op: String,
47 name: String,
48 #[source]
49 source: ScalarError,
50 },
51 #[error("{op}: a request body is required")]
52 MissingBody { op: String },
53 #[error("{op}: takes no request body")]
54 UnexpectedBody { op: String },
55 #[error("{op}: expects a {expected} body")]
56 WrongBodyKind { op: String, expected: String },
57}
58
59#[derive(Debug, Clone)]
61pub struct Invocation<'a> {
62 op: &'a Operation,
63 values: Values,
64}
65
66impl<'a> Invocation<'a> {
67 pub fn new(op: &'a Operation, values: Values) -> Result<Self, ValueError> {
69 let name = || op.id().to_owned();
70 for (wire, raw) in values.params() {
71 let param = op.param(wire).ok_or_else(|| ValueError::UnknownParam {
72 op: name(),
73 name: wire.clone(),
74 })?;
75 match param.shape() {
76 Shape::Flag { scalar, .. } => {
77 scalar.parse(raw).map_err(|source| ValueError::BadValue {
78 op: name(),
79 name: wire.clone(),
80 source,
81 })?;
82 }
83 Shape::Unreachable(why) => {
84 return Err(ValueError::UnsupportedParam {
85 op: name(),
86 name: wire.clone(),
87 why: why.clone(),
88 });
89 }
90 }
91 }
92 for param in op.params() {
93 let given = values
94 .params()
95 .iter()
96 .filter(|(wire, _)| wire == param.name())
97 .count();
98 if param.required() && given == 0 {
99 return Err(ValueError::MissingParam {
100 op: name(),
101 name: param.name().to_owned(),
102 });
103 }
104 if given > 1 && !param.shape().repeatable() {
105 return Err(ValueError::RepeatedParam {
106 op: name(),
107 name: param.name().to_owned(),
108 given,
109 });
110 }
111 }
112 check_body(op, values.payload())?;
113 Ok(Self { op, values })
114 }
115
116 #[must_use]
117 pub fn operation(&self) -> &'a Operation {
118 self.op
119 }
120
121 pub fn request(&self, base: &Uri) -> Result<Request<Vec<u8>>, http::Error> {
123 let mut builder = Request::builder()
124 .method(self.op.method().clone())
125 .uri(self.url(base));
126 for sent in self.placed(Location::Header) {
127 builder = builder.header(sent.name, sent.values.join(","));
131 }
132 match self.body() {
133 None => builder.body(Vec::new()),
134 Some((content_type, bytes)) => builder
135 .header(header::CONTENT_TYPE, content_type)
136 .body(bytes),
137 }
138 }
139
140 fn body(&self) -> Option<(String, Vec<u8>)> {
142 match self.values.payload()? {
143 Payload::Json(value) => Some((
144 "application/json".to_owned(),
145 value.to_string().into_bytes(),
146 )),
147 Payload::Raw(bytes) => {
148 let media_type = match self.op.body() {
150 Body::Opaque { media_type, .. } => media_type.clone(),
151 Body::None
152 | Body::JsonFields(_)
153 | Body::JsonWhole { .. }
154 | Body::Multipart { .. } => "application/octet-stream".to_owned(),
155 };
156 Some((media_type, bytes.clone()))
157 }
158 Payload::Multipart(parts) => {
159 let encoded = multipart::encode(parts);
160 Some((encoded.content_type, encoded.bytes))
161 }
162 }
163 }
164
165 fn url(&self, base: &Uri) -> String {
166 let mut url = String::new();
167 if let Some(scheme) = base.scheme_str() {
168 url.push_str(scheme);
169 url.push_str("://");
170 }
171 if let Some(authority) = base.authority() {
172 url.push_str(authority.as_str());
173 }
174 url.push_str(base.path().trim_end_matches('/'));
175
176 let mut path = self.op.path().to_owned();
177 for sent in self.placed(Location::Path) {
178 let placeholder = format!("{{{}}}", sent.name);
181 path = path.replace(&placeholder, &commas(&sent.values));
182 }
183 url.push_str(&path);
184
185 let query = self.query();
186 if !query.is_empty() {
187 url.push('?');
188 url.push_str(&query.join("&"));
189 }
190 url
191 }
192
193 fn query(&self) -> Vec<String> {
201 let mut fields: Vec<String> = Vec::new();
202 for sent in self.placed(Location::Query) {
203 let name = encode(sent.name);
204 match sent.join {
205 Some(Join::Pairs) => fields.extend(
206 sent.values
207 .iter()
208 .map(|value| format!("{name}={}", encode(value))),
209 ),
210 Some(Join::Commas) | None => {
211 fields.push(format!("{name}={}", commas(&sent.values)));
212 }
213 }
214 }
215 fields
216 }
217
218 fn placed(&self, location: Location) -> Vec<Sent<'_>> {
226 let mut out: Vec<Sent<'_>> = Vec::new();
227 for (name, value) in self.values.params() {
228 let Some(Shape::Flag {
229 location: at, join, ..
230 }) = self.op.param(name).map(Param::shape)
231 else {
232 continue;
233 };
234 if *at != location {
235 continue;
236 }
237 match out.iter_mut().find(|sent| sent.name == name.as_str()) {
238 Some(sent) => sent.values.push(value),
239 None => out.push(Sent {
240 name,
241 join: *join,
242 values: vec![value],
243 }),
244 }
245 }
246 out
247 }
248}
249
250struct Sent<'v> {
253 name: &'v str,
254 join: Option<Join>,
255 values: Vec<&'v str>,
256}
257
258fn check_body(op: &Operation, body: Option<&Payload>) -> Result<(), ValueError> {
260 let name = || op.id().to_owned();
261 let wrong = |expected: &str| {
262 Err(ValueError::WrongBodyKind {
263 op: name(),
264 expected: expected.to_owned(),
265 })
266 };
267 match (op.body(), body) {
268 (
269 Body::None
270 | Body::JsonFields(_)
271 | Body::JsonWhole { required: false }
272 | Body::Multipart {
273 required: false, ..
274 }
275 | Body::Opaque {
276 required: false, ..
277 },
278 None,
279 )
280 | (Body::JsonFields(_) | Body::JsonWhole { .. }, Some(Payload::Json(_)))
281 | (Body::Multipart { .. }, Some(Payload::Multipart(_)))
282 | (Body::Opaque { .. }, Some(Payload::Raw(_))) => Ok(()),
283 (Body::None, Some(_)) => Err(ValueError::UnexpectedBody { op: name() }),
284 (
285 Body::JsonWhole { required: true }
286 | Body::Multipart { required: true, .. }
287 | Body::Opaque { required: true, .. },
288 None,
289 ) => Err(ValueError::MissingBody { op: name() }),
290 (Body::JsonFields(_) | Body::JsonWhole { .. }, Some(_)) => wrong("JSON"),
291 (Body::Multipart { .. }, Some(_)) => wrong("multipart/form-data"),
292 (Body::Opaque { media_type, .. }, Some(_)) => wrong(media_type),
293 }
294}
295
296fn commas(values: &[&str]) -> String {
304 values
305 .iter()
306 .copied()
307 .map(encode)
308 .collect::<Vec<_>>()
309 .join(",")
310}
311
312fn encode(raw: &str) -> String {
316 use std::fmt::Write as _;
317
318 let mut out = String::with_capacity(raw.len());
319 for byte in raw.bytes() {
320 if byte.is_ascii_alphanumeric() || matches!(byte, b'-' | b'.' | b'_' | b'~') {
321 out.push(char::from(byte));
322 } else {
323 let _ = write!(out, "%{byte:02X}");
324 }
325 }
326 out
327}
328
329#[must_use]
334pub fn render(request: &Request<Vec<u8>>) -> String {
335 use std::fmt::Write as _;
336
337 let mut out = String::new();
338 let _ = writeln!(
339 out,
340 "{} {} HTTP/1.1",
341 request.method(),
342 request
343 .uri()
344 .path_and_query()
345 .map_or("/", http::uri::PathAndQuery::as_str)
346 );
347 if let Some(authority) = request.uri().authority() {
348 let _ = writeln!(out, "host: {authority}");
349 }
350 for (name, value) in request.headers() {
351 let _ = writeln!(out, "{name}: {}", value.to_str().unwrap_or("<non-utf8>"));
352 }
353 if !request.body().is_empty() {
354 out.push('\n');
355 match std::str::from_utf8(request.body()) {
356 Ok(text) => {
357 out.push_str(text);
358 if !text.ends_with('\n') {
359 out.push('\n');
360 }
361 }
362 Err(_) => {
363 let _ = writeln!(out, "<{} bytes>", request.body().len());
364 }
365 }
366 }
367 out
368}
369
370#[cfg(test)]
371#[expect(
372 clippy::expect_used,
373 reason = "a test that cannot build its fixture should fail loudly and name it"
374)]
375mod tests {
376 use super::*;
377
378 #[test]
379 fn encoding_leaves_the_unreserved_set_alone_and_escapes_the_rest() {
380 assert_eq!(encode("abc-123_x.y~z"), "abc-123_x.y~z");
381 assert_eq!(encode("a b/c?d&e=f"), "a%20b%2Fc%3Fd%26e%3Df");
382 assert_eq!(encode("Grüß"), "Gr%C3%BC%C3%9F");
383 }
384
385 #[test]
386 fn a_binary_body_is_summarised_rather_than_printed() {
387 let request = Request::builder()
388 .uri("http://x/y")
389 .body(vec![0xFF, 0xFE])
390 .expect("a request with a two-byte body");
391 assert!(
392 render(&request).ends_with("<2 bytes>\n"),
393 "{}",
394 render(&request)
395 );
396 }
397}