1use http::{Request, Uri, header};
14use thiserror::Error;
15
16use crate::model::{Body, Effect, Location, Operation};
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}`: {source}")]
29 BadValue {
30 op: String,
31 name: String,
32 #[source]
33 source: ScalarError,
34 },
35 #[error("{op}: a request body is required")]
36 MissingBody { op: String },
37 #[error("{op}: takes no request body")]
38 UnexpectedBody { op: String },
39 #[error("{op}: expects a {expected} body")]
40 WrongBodyKind { op: String, expected: String },
41}
42
43#[derive(Debug, Clone)]
45pub struct Invocation<'a> {
46 op: &'a Operation,
47 values: Values,
48}
49
50impl<'a> Invocation<'a> {
51 pub fn new(op: &'a Operation, values: Values) -> Result<Self, ValueError> {
53 let name = || op.id().to_owned();
54 for (wire, raw) in values.params() {
55 let param = op.param(wire).ok_or_else(|| ValueError::UnknownParam {
56 op: name(),
57 name: wire.clone(),
58 })?;
59 param
60 .scalar()
61 .parse(raw)
62 .map_err(|source| ValueError::BadValue {
63 op: name(),
64 name: wire.clone(),
65 source,
66 })?;
67 }
68 for param in op.params() {
69 let given = values.params().iter().any(|(wire, _)| wire == param.name());
70 if param.required() && !given {
71 return Err(ValueError::MissingParam {
72 op: name(),
73 name: param.name().to_owned(),
74 });
75 }
76 }
77 check_body(op, values.payload())?;
78 Ok(Self { op, values })
79 }
80
81 #[must_use]
82 pub fn operation(&self) -> &'a Operation {
83 self.op
84 }
85
86 #[must_use]
88 pub fn effect(&self) -> Effect {
89 self.op.effect()
90 }
91
92 pub fn request(&self, base: &Uri) -> Result<Request<Vec<u8>>, http::Error> {
94 let mut builder = Request::builder()
95 .method(self.op.method().clone())
96 .uri(self.url(base));
97 for (name, value) in self.located(Location::Header) {
98 builder = builder.header(name, value);
99 }
100 match self.body() {
101 None => builder.body(Vec::new()),
102 Some((content_type, bytes)) => builder
103 .header(header::CONTENT_TYPE, content_type)
104 .body(bytes),
105 }
106 }
107
108 fn body(&self) -> Option<(String, Vec<u8>)> {
110 match self.values.payload()? {
111 Payload::Json(value) => Some((
112 "application/json".to_owned(),
113 value.to_string().into_bytes(),
114 )),
115 Payload::Raw(bytes) => {
116 let media_type = match self.op.body() {
118 Body::Opaque { media_type, .. } => media_type.clone(),
119 Body::None
120 | Body::JsonFields(_)
121 | Body::JsonWhole { .. }
122 | Body::Multipart { .. } => "application/octet-stream".to_owned(),
123 };
124 Some((media_type, bytes.clone()))
125 }
126 Payload::Multipart(parts) => {
127 let encoded = multipart::encode(parts);
128 Some((encoded.content_type, encoded.bytes))
129 }
130 }
131 }
132
133 fn url(&self, base: &Uri) -> String {
134 let mut url = String::new();
135 if let Some(scheme) = base.scheme_str() {
136 url.push_str(scheme);
137 url.push_str("://");
138 }
139 if let Some(authority) = base.authority() {
140 url.push_str(authority.as_str());
141 }
142 url.push_str(base.path().trim_end_matches('/'));
143
144 let mut path = self.op.path().to_owned();
145 for (name, value) in self.located(Location::Path) {
146 path = path.replace(&format!("{{{name}}}"), &encode(value));
147 }
148 url.push_str(&path);
149
150 let query: Vec<String> = self
151 .located(Location::Query)
152 .map(|(name, value)| format!("{}={}", encode(name), encode(value)))
153 .collect();
154 if !query.is_empty() {
155 url.push('?');
156 url.push_str(&query.join("&"));
157 }
158 url
159 }
160
161 fn located(&self, location: Location) -> impl Iterator<Item = (&str, &str)> {
162 self.values
163 .params()
164 .iter()
165 .filter_map(move |(name, value)| {
166 let param = self.op.param(name)?;
167 (param.location() == location).then_some((name.as_str(), value.as_str()))
168 })
169 }
170}
171
172fn check_body(op: &Operation, body: Option<&Payload>) -> Result<(), ValueError> {
174 let name = || op.id().to_owned();
175 let wrong = |expected: &str| {
176 Err(ValueError::WrongBodyKind {
177 op: name(),
178 expected: expected.to_owned(),
179 })
180 };
181 match (op.body(), body) {
182 (
183 Body::None
184 | Body::JsonFields(_)
185 | Body::JsonWhole { required: false }
186 | Body::Multipart {
187 required: false, ..
188 }
189 | Body::Opaque {
190 required: false, ..
191 },
192 None,
193 )
194 | (Body::JsonFields(_) | Body::JsonWhole { .. }, Some(Payload::Json(_)))
195 | (Body::Multipart { .. }, Some(Payload::Multipart(_)))
196 | (Body::Opaque { .. }, Some(Payload::Raw(_))) => Ok(()),
197 (Body::None, Some(_)) => Err(ValueError::UnexpectedBody { op: name() }),
198 (
199 Body::JsonWhole { required: true }
200 | Body::Multipart { required: true, .. }
201 | Body::Opaque { required: true, .. },
202 None,
203 ) => Err(ValueError::MissingBody { op: name() }),
204 (Body::JsonFields(_) | Body::JsonWhole { .. }, Some(_)) => wrong("JSON"),
205 (Body::Multipart { .. }, Some(_)) => wrong("multipart/form-data"),
206 (Body::Opaque { media_type, .. }, Some(_)) => wrong(media_type),
207 }
208}
209
210fn encode(raw: &str) -> String {
214 use std::fmt::Write as _;
215
216 let mut out = String::with_capacity(raw.len());
217 for byte in raw.bytes() {
218 if byte.is_ascii_alphanumeric() || matches!(byte, b'-' | b'.' | b'_' | b'~') {
219 out.push(char::from(byte));
220 } else {
221 let _ = write!(out, "%{byte:02X}");
222 }
223 }
224 out
225}
226
227#[must_use]
232pub fn render(request: &Request<Vec<u8>>) -> String {
233 use std::fmt::Write as _;
234
235 let mut out = String::new();
236 let _ = writeln!(
237 out,
238 "{} {} HTTP/1.1",
239 request.method(),
240 request
241 .uri()
242 .path_and_query()
243 .map_or("/", http::uri::PathAndQuery::as_str)
244 );
245 if let Some(authority) = request.uri().authority() {
246 let _ = writeln!(out, "host: {authority}");
247 }
248 for (name, value) in request.headers() {
249 let _ = writeln!(out, "{name}: {}", value.to_str().unwrap_or("<non-utf8>"));
250 }
251 if !request.body().is_empty() {
252 out.push('\n');
253 match std::str::from_utf8(request.body()) {
254 Ok(text) => {
255 out.push_str(text);
256 if !text.ends_with('\n') {
257 out.push('\n');
258 }
259 }
260 Err(_) => {
261 let _ = writeln!(out, "<{} bytes>", request.body().len());
262 }
263 }
264 }
265 out
266}
267
268#[cfg(test)]
269#[expect(
270 clippy::expect_used,
271 reason = "a test that cannot build its fixture should fail loudly and name it"
272)]
273mod tests {
274 use super::*;
275
276 #[test]
277 fn encoding_leaves_the_unreserved_set_alone_and_escapes_the_rest() {
278 assert_eq!(encode("abc-123_x.y~z"), "abc-123_x.y~z");
279 assert_eq!(encode("a b/c?d&e=f"), "a%20b%2Fc%3Fd%26e%3Df");
280 assert_eq!(encode("Grüß"), "Gr%C3%BC%C3%9F");
281 }
282
283 #[test]
284 fn a_binary_body_is_summarised_rather_than_printed() {
285 let request = Request::builder()
286 .uri("http://x/y")
287 .body(vec![0xFF, 0xFE])
288 .expect("a request with a two-byte body");
289 assert!(
290 render(&request).ends_with("<2 bytes>\n"),
291 "{}",
292 render(&request)
293 );
294 }
295}