use http::{Request, Uri, header};
use thiserror::Error;
use crate::model::{Body, Join, Location, Operation, Param, Shape, Unsupported};
use crate::multipart;
use crate::scalar::ScalarError;
use crate::values::{Payload, Values};
#[derive(Debug, Clone, Error, PartialEq, Eq)]
pub enum ValueError {
#[error("{op}: `{name}` is required")]
MissingParam { op: String, name: String },
#[error("{op}: there is no `{name}` parameter")]
UnknownParam { op: String, name: String },
#[error("{op}: `{name}` is {why}, so there is nowhere in the request to put a value for it")]
UnsupportedParam {
op: String,
name: String,
why: Unsupported,
},
#[error("{op}: `{name}` takes one value, and was given {given}")]
RepeatedParam {
op: String,
name: String,
given: usize,
},
#[error("{op}: `{name}`: {source}")]
BadValue {
op: String,
name: String,
#[source]
source: ScalarError,
},
#[error("{op}: a request body is required")]
MissingBody { op: String },
#[error("{op}: takes no request body")]
UnexpectedBody { op: String },
#[error("{op}: expects a {expected} body")]
WrongBodyKind { op: String, expected: String },
}
#[derive(Debug, Clone)]
pub struct Invocation<'a> {
op: &'a Operation,
values: Values,
}
impl<'a> Invocation<'a> {
pub fn new(op: &'a Operation, values: Values) -> Result<Self, ValueError> {
let name = || op.id().to_owned();
for (wire, raw) in values.params() {
let param = op.param(wire).ok_or_else(|| ValueError::UnknownParam {
op: name(),
name: wire.clone(),
})?;
match param.shape() {
Shape::Flag { scalar, .. } => {
scalar.parse(raw).map_err(|source| ValueError::BadValue {
op: name(),
name: wire.clone(),
source,
})?;
}
Shape::Unreachable(why) => {
return Err(ValueError::UnsupportedParam {
op: name(),
name: wire.clone(),
why: why.clone(),
});
}
}
}
for param in op.params() {
let given = values
.params()
.iter()
.filter(|(wire, _)| wire == param.name())
.count();
if param.required() && given == 0 {
return Err(ValueError::MissingParam {
op: name(),
name: param.name().to_owned(),
});
}
if given > 1 && !param.shape().repeatable() {
return Err(ValueError::RepeatedParam {
op: name(),
name: param.name().to_owned(),
given,
});
}
}
check_body(op, values.payload())?;
Ok(Self { op, values })
}
#[must_use]
pub fn operation(&self) -> &'a Operation {
self.op
}
pub fn request(&self, base: &Uri) -> Result<Request<Vec<u8>>, http::Error> {
let mut builder = Request::builder()
.method(self.op.method().clone())
.uri(self.url(base));
for sent in self.placed(Location::Header) {
builder = builder.header(sent.name, sent.values.join(","));
}
match self.body() {
None => builder.body(Vec::new()),
Some((content_type, bytes)) => builder
.header(header::CONTENT_TYPE, content_type)
.body(bytes),
}
}
fn body(&self) -> Option<(String, Vec<u8>)> {
match self.values.payload()? {
Payload::Json(value) => Some((
"application/json".to_owned(),
value.to_string().into_bytes(),
)),
Payload::Raw(bytes) => {
let media_type = match self.op.body() {
Body::Opaque { media_type, .. } => media_type.clone(),
Body::None
| Body::JsonFields(_)
| Body::JsonWhole { .. }
| Body::Multipart { .. } => "application/octet-stream".to_owned(),
};
Some((media_type, bytes.clone()))
}
Payload::Multipart(parts) => {
let encoded = multipart::encode(parts);
Some((encoded.content_type, encoded.bytes))
}
}
}
fn url(&self, base: &Uri) -> String {
let mut url = String::new();
if let Some(scheme) = base.scheme_str() {
url.push_str(scheme);
url.push_str("://");
}
if let Some(authority) = base.authority() {
url.push_str(authority.as_str());
}
url.push_str(base.path().trim_end_matches('/'));
let mut path = self.op.path().to_owned();
for sent in self.placed(Location::Path) {
let placeholder = format!("{{{}}}", sent.name);
path = path.replace(&placeholder, &commas(&sent.values));
}
url.push_str(&path);
let query = self.query();
if !query.is_empty() {
url.push('?');
url.push_str(&query.join("&"));
}
url
}
fn query(&self) -> Vec<String> {
let mut fields: Vec<String> = Vec::new();
for sent in self.placed(Location::Query) {
let name = encode(sent.name);
match sent.join {
Some(Join::Pairs) => fields.extend(
sent.values
.iter()
.map(|value| format!("{name}={}", encode(value))),
),
Some(Join::Commas) | None => {
fields.push(format!("{name}={}", commas(&sent.values)));
}
}
}
fields
}
fn placed(&self, location: Location) -> Vec<Sent<'_>> {
let mut out: Vec<Sent<'_>> = Vec::new();
for (name, value) in self.values.params() {
let Some(Shape::Flag {
location: at, join, ..
}) = self.op.param(name).map(Param::shape)
else {
continue;
};
if *at != location {
continue;
}
match out.iter_mut().find(|sent| sent.name == name.as_str()) {
Some(sent) => sent.values.push(value),
None => out.push(Sent {
name,
join: *join,
values: vec![value],
}),
}
}
out
}
}
struct Sent<'v> {
name: &'v str,
join: Option<Join>,
values: Vec<&'v str>,
}
fn check_body(op: &Operation, body: Option<&Payload>) -> Result<(), ValueError> {
let name = || op.id().to_owned();
let wrong = |expected: &str| {
Err(ValueError::WrongBodyKind {
op: name(),
expected: expected.to_owned(),
})
};
match (op.body(), body) {
(
Body::None
| Body::JsonFields(_)
| Body::JsonWhole { required: false }
| Body::Multipart {
required: false, ..
}
| Body::Opaque {
required: false, ..
},
None,
)
| (Body::JsonFields(_) | Body::JsonWhole { .. }, Some(Payload::Json(_)))
| (Body::Multipart { .. }, Some(Payload::Multipart(_)))
| (Body::Opaque { .. }, Some(Payload::Raw(_))) => Ok(()),
(Body::None, Some(_)) => Err(ValueError::UnexpectedBody { op: name() }),
(
Body::JsonWhole { required: true }
| Body::Multipart { required: true, .. }
| Body::Opaque { required: true, .. },
None,
) => Err(ValueError::MissingBody { op: name() }),
(Body::JsonFields(_) | Body::JsonWhole { .. }, Some(_)) => wrong("JSON"),
(Body::Multipart { .. }, Some(_)) => wrong("multipart/form-data"),
(Body::Opaque { media_type, .. }, Some(_)) => wrong(media_type),
}
}
fn commas(values: &[&str]) -> String {
values
.iter()
.copied()
.map(encode)
.collect::<Vec<_>>()
.join(",")
}
fn encode(raw: &str) -> String {
use std::fmt::Write as _;
let mut out = String::with_capacity(raw.len());
for byte in raw.bytes() {
if byte.is_ascii_alphanumeric() || matches!(byte, b'-' | b'.' | b'_' | b'~') {
out.push(char::from(byte));
} else {
let _ = write!(out, "%{byte:02X}");
}
}
out
}
#[must_use]
pub fn render(request: &Request<Vec<u8>>) -> String {
use std::fmt::Write as _;
let mut out = String::new();
let _ = writeln!(
out,
"{} {} HTTP/1.1",
request.method(),
request
.uri()
.path_and_query()
.map_or("/", http::uri::PathAndQuery::as_str)
);
if let Some(authority) = request.uri().authority() {
let _ = writeln!(out, "host: {authority}");
}
for (name, value) in request.headers() {
let _ = writeln!(out, "{name}: {}", value.to_str().unwrap_or("<non-utf8>"));
}
if !request.body().is_empty() {
out.push('\n');
match std::str::from_utf8(request.body()) {
Ok(text) => {
out.push_str(text);
if !text.ends_with('\n') {
out.push('\n');
}
}
Err(_) => {
let _ = writeln!(out, "<{} bytes>", request.body().len());
}
}
}
out
}
#[cfg(test)]
#[expect(
clippy::expect_used,
reason = "a test that cannot build its fixture should fail loudly and name it"
)]
mod tests {
use super::*;
#[test]
fn encoding_leaves_the_unreserved_set_alone_and_escapes_the_rest() {
assert_eq!(encode("abc-123_x.y~z"), "abc-123_x.y~z");
assert_eq!(encode("a b/c?d&e=f"), "a%20b%2Fc%3Fd%26e%3Df");
assert_eq!(encode("Grüß"), "Gr%C3%BC%C3%9F");
}
#[test]
fn a_binary_body_is_summarised_rather_than_printed() {
let request = Request::builder()
.uri("http://x/y")
.body(vec![0xFF, 0xFE])
.expect("a request with a two-byte body");
assert!(
render(&request).ends_with("<2 bytes>\n"),
"{}",
render(&request)
);
}
}