Skip to main content

typed_openapi/
values.rs

1//! The arguments for one operation, keyed on the names the document uses.
2//!
3//! This is the seam that lets one request builder serve two callers. A CLI
4//! translates flags into a [`Values`]; a generated Rust wrapper builds one
5//! directly from typed arguments. Neither knows about the other, and neither
6//! re-spells a path template or a query string.
7
8/// What goes in the request body. Which variant an operation wants is a
9/// document fact, so the media type is not carried here — [`Body`] holds it.
10///
11/// [`Body`]: crate::Body
12#[derive(Debug, Clone, PartialEq, Eq)]
13pub enum Payload {
14    /// JSON, either assembled from per-field flags or taken whole from a file.
15    Json(serde_json::Value),
16    /// Bytes the CLI does not interpret, sent under the document's media type.
17    Raw(Vec<u8>),
18    /// `multipart/form-data`, assembled part by part.
19    Multipart(Vec<Part>),
20}
21
22/// One part of a multipart body.
23#[derive(Debug, Clone, PartialEq, Eq)]
24pub struct Part {
25    name: String,
26    filename: Option<String>,
27    bytes: Vec<u8>,
28}
29
30impl Part {
31    /// A text part: `--field name=value`.
32    #[must_use]
33    pub fn text(name: impl Into<String>, value: impl Into<String>) -> Self {
34        Self {
35            name: name.into(),
36            filename: None,
37            bytes: value.into().into_bytes(),
38        }
39    }
40
41    /// A file part: `--file name=@path`. `filename` is what the server sees.
42    #[must_use]
43    pub fn file(name: impl Into<String>, filename: impl Into<String>, bytes: Vec<u8>) -> Self {
44        Self {
45            name: name.into(),
46            filename: Some(filename.into()),
47            bytes,
48        }
49    }
50
51    #[must_use]
52    pub fn name(&self) -> &str {
53        &self.name
54    }
55
56    #[must_use]
57    pub fn filename(&self) -> Option<&str> {
58        self.filename.as_deref()
59    }
60
61    #[must_use]
62    pub fn bytes(&self) -> &[u8] {
63        &self.bytes
64    }
65}
66
67/// Parameters and a body for one operation, keyed on wire names.
68///
69/// Nothing here is checked against a document — that happens once, in
70/// [`Invocation::new`], so there is exactly one place that decides what
71/// satisfies an operation.
72///
73/// [`Invocation::new`]: crate::Invocation::new
74#[derive(Debug, Clone, Default, PartialEq, Eq)]
75pub struct Values {
76    params: Vec<(String, String)>,
77    body: Option<Payload>,
78}
79
80impl Values {
81    #[must_use]
82    pub fn new() -> Self {
83        Self::default()
84    }
85
86    /// One parameter, under the name the document gives it.
87    #[must_use]
88    #[expect(
89        clippy::needless_pass_by_value,
90        reason = "taking a reference would put `&` in front of every argument \
91                  at every generated call site, for values that cost nothing to move"
92    )]
93    pub fn param(mut self, name: impl Into<String>, value: impl ToString) -> Self {
94        self.params.push((name.into(), value.to_string()));
95        self
96    }
97
98    /// The same, for an optional parameter: `None` adds nothing.
99    #[must_use]
100    pub fn maybe(self, name: impl Into<String>, value: Option<impl ToString>) -> Self {
101        match value {
102            Some(value) => self.param(name, value),
103            None => self,
104        }
105    }
106
107    /// Every value of a list parameter, under the name the document gives it.
108    ///
109    /// A list reaches the request builder as the name repeated, which is exactly
110    /// what a repeated flag reaches it with — so the two consumers hand over the
111    /// same thing, and how the repeats are laid out in the request is the
112    /// document's to say rather than either caller's. An empty list adds
113    /// nothing, and is the same as not naming the parameter at all.
114    #[must_use]
115    pub fn each(
116        mut self,
117        name: impl Into<String>,
118        values: impl IntoIterator<Item = impl ToString>,
119    ) -> Self {
120        let name = name.into();
121        for value in values {
122            self.params.push((name.clone(), value.to_string()));
123        }
124        self
125    }
126
127    #[must_use]
128    pub fn json(mut self, value: serde_json::Value) -> Self {
129        self.body = Some(Payload::Json(value));
130        self
131    }
132
133    #[must_use]
134    pub fn raw(mut self, bytes: Vec<u8>) -> Self {
135        self.body = Some(Payload::Raw(bytes));
136        self
137    }
138
139    #[must_use]
140    pub fn multipart(mut self, parts: Vec<Part>) -> Self {
141        self.body = Some(Payload::Multipart(parts));
142        self
143    }
144
145    /// Attach `body` when there is one; leave the body alone when there is not.
146    #[must_use]
147    pub fn body(mut self, body: Option<Payload>) -> Self {
148        if let Some(body) = body {
149            self.body = Some(body);
150        }
151        self
152    }
153
154    #[must_use]
155    pub fn params(&self) -> &[(String, String)] {
156        &self.params
157    }
158
159    #[must_use]
160    pub fn payload(&self) -> Option<&Payload> {
161        self.body.as_ref()
162    }
163}