1use bon::Builder;
2use serde::{Deserialize, Serialize};
3
4use crate::types::Record;
5
6#[derive(Builder, Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
7#[serde(rename_all = "camelCase")]
8pub struct FieldDefinition {
9 #[serde(rename = "type", skip_serializing_if = "Option::is_none")]
10 #[builder(into)]
11 pub field_type: Option<String>,
12
13 #[serde(skip_serializing_if = "Option::is_none")]
14 #[builder(into)]
15 pub required: Option<FieldRequired>,
16
17 #[serde(skip_serializing_if = "Option::is_none")]
18 #[builder(into)]
19 pub description: Option<String>,
20
21 #[serde(rename = "enum", skip_serializing_if = "Option::is_none")]
22 #[builder(with = |iter: impl for<'a> IntoIterator<Item = &'static str>| iter.into_iter().map(|s| s.to_string()).collect())]
23 pub field_enum: Option<Vec<String>>,
24
25 #[serde(skip_serializing_if = "Option::is_none")]
26 #[builder(with = |iter: impl IntoIterator<Item = (&'static str, FieldDefinition)>| {
27 iter.into_iter()
28 .map(|(k, v)| (k.to_string(), v))
29 .collect()
30 })]
31 pub properties: Option<Record<FieldDefinition>>,
32}
33
34impl TryFrom<serde_json::Value> for FieldDefinition {
35 type Error = serde_json::Error;
36
37 fn try_from(value: serde_json::Value) -> Result<Self, Self::Error> {
38 serde_json::from_value(value)
39 }
40}
41
42#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
43#[serde(untagged)]
44pub enum FieldRequired {
45 Boolean(bool),
46 VecString(Vec<String>),
47}
48
49pub struct Required;
54
55impl From<Required> for FieldRequired {
56 fn from(_: Required) -> Self {
57 FieldRequired::Boolean(true)
58 }
59}
60
61impl<I: IntoIterator<Item = &'static str>> From<I> for FieldRequired {
62 fn from(value: I) -> Self {
63 FieldRequired::VecString(value.into_iter().map(|s| s.to_string()).collect())
64 }
65}
66
67#[derive(Builder, Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
68#[serde(rename_all = "camelCase")]
69pub struct HttpInput {
70 pub discoverable: bool,
71
72 pub method: Method,
73
74 #[serde(skip_serializing_if = "Option::is_none")]
75 pub body_type: Option<InputBodyType>,
76
77 #[serde(skip_serializing_if = "Option::is_none")]
78 #[builder(with = |iter: impl IntoIterator<Item = (&'static str, FieldDefinition)>| {
79 iter.into_iter()
80 .map(|(k, v)| (k.to_string(), v))
81 .collect()
82 })]
83 pub query_params: Option<Record<FieldDefinition>>,
84
85 #[serde(skip_serializing_if = "Option::is_none")]
86 #[builder(with = |iter: impl IntoIterator<Item = (&'static str, FieldDefinition)>| {
87 iter.into_iter()
88 .map(|(k, v)| (k.to_string(), v))
89 .collect()
90 })]
91 pub body_fields: Option<Record<FieldDefinition>>,
92
93 #[serde(skip_serializing_if = "Option::is_none")]
94 #[builder(with = |iter: impl IntoIterator<Item = (&'static str, FieldDefinition)>| {
95 iter.into_iter()
96 .map(|(k, v)| (k.to_string(), v))
97 .collect()
98 })]
99 pub header_fields: Option<Record<FieldDefinition>>,
100}
101
102#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
103#[serde(tag = "type")]
104pub enum Input {
105 #[serde(rename = "http")]
106 Http(HttpInput),
107}
108
109impl Input {
110 pub fn as_http(&self) -> Option<&HttpInput> {
111 match self {
112 Input::Http(http_input) => Some(http_input),
113 }
114 }
115}
116
117#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
118pub enum Method {
119 #[serde(rename = "GET")]
120 Get,
121 #[serde(rename = "POST")]
122 Post,
123}
124
125#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
126pub enum InputBodyType {
127 #[serde(rename = "json")]
128 Json,
129 #[serde(rename = "form-data")]
130 FormData,
131 #[serde(rename = "multipart-form-data")]
132 MultipartFormData,
133 #[serde(rename = "text")]
134 Text,
135 #[serde(rename = "binary")]
136 Binary,
137 #[serde(rename = "event-stream")]
138 EventStream,
139}
140
141#[derive(Builder, Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
142#[serde(rename_all = "camelCase")]
143pub struct OutputSchema {
144 pub input: Input,
145
146 #[serde(skip_serializing_if = "Option::is_none")]
147 #[builder(with = |iter: impl IntoIterator<Item = (&'static str, FieldDefinition)>| {
148 iter.into_iter().map(|(k, v)| (k.to_string(), v)).collect()
149 })]
150 pub output: Option<Record<FieldDefinition>>,
151}
152
153impl OutputSchema {
154 pub fn http_get_discoverable() -> Self {
155 Self::builder()
156 .input(Input::Http(
157 HttpInput::builder()
158 .method(Method::Get)
159 .discoverable(true)
160 .build(),
161 ))
162 .build()
163 }
164
165 pub fn http_post_discoverable() -> Self {
166 Self::builder()
167 .input(Input::Http(
168 HttpInput::builder()
169 .method(Method::Post)
170 .discoverable(true)
171 .build(),
172 ))
173 .build()
174 }
175}
176
177#[cfg(test)]
178mod tests {
179 use serde_json::json;
180
181 use super::*;
182
183 fn setup_complex_input() -> Input {
184 Input::Http(
185 HttpInput::builder()
186 .method(Method::Post)
187 .discoverable(true)
188 .body_type(InputBodyType::Json)
189 .header_fields([(
190 "example_header",
191 FieldDefinition::builder()
192 .description("An example header")
193 .field_type("string")
194 .required(Required)
195 .build(),
196 )])
197 .query_params([(
198 "exmple_query",
199 FieldDefinition::builder()
200 .description("An example query parameter")
201 .field_type("string")
202 .build(),
203 )])
204 .body_fields([(
205 "example",
206 FieldDefinition::builder()
207 .description("An example field")
208 .field_type("string")
209 .required(["nested_field", "nested_field2"])
210 .properties([
211 (
212 "nested_field",
213 FieldDefinition::builder()
214 .field_type("number")
215 .description("A nested field")
216 .required(Required)
217 .build(),
218 ),
219 (
220 "nested_field2",
221 FieldDefinition::builder()
222 .field_type("string")
223 .description("Optional nested field")
224 .field_enum(["a", "b", "c"])
225 .build(),
226 ),
227 ])
228 .build(),
229 )])
230 .build(),
231 )
232 }
233
234 #[test]
235 fn build_input() {
236 let input = setup_complex_input();
237
238 let input_json = json!({
239 "discoverable": true,
240 "type": "http",
241 "method": "post",
242 "bodyType": "json",
243 "headerFields": {
244 "example_header": {
245 "type": "string",
246 "required": true,
247 "description": "An example header"
248 }
249 },
250 "queryParams": {
251 "exmple_query": {
252 "type": "string",
253 "description": "An example query parameter"
254 }
255 },
256 "bodyFields": {
257 "example": {
258 "type": "string",
259 "required": ["nested_field", "nested_field2"],
260 "description": "An example field",
261 "properties": {
262 "nested_field": {
263 "type": "number",
264 "required": true,
265 "description": "A nested field"
266 },
267 "nested_field2": {
268 "type": "string",
269 "description": "Optional nested field",
270 "enum": ["a", "b", "c"]
271 }
272 }
273 }
274 }
275 });
276
277 assert_eq!(serde_json::to_value(&input).unwrap(), input_json);
278 }
279
280 #[test]
281 fn build_output_schema() {
282 let input = setup_complex_input();
283
284 let output_schema = OutputSchema::builder()
285 .input(input.clone())
286 .output([(
287 "response_field",
288 FieldDefinition::builder()
289 .field_type("string")
290 .description("A response field")
291 .required(Required)
292 .build(),
293 )])
294 .build();
295
296 let output_schema_json = json!({
297 "input": {
298 "discoverable": true,
299 "type": "http",
300 "method": "post",
301 "bodyType": "json",
302 "headerFields": {
303 "example_header": {
304 "type": "string",
305 "required": true,
306 "description": "An example header"
307 }
308 },
309 "queryParams": {
310 "exmple_query": {
311 "type": "string",
312 "description": "An example query parameter"
313 }
314 },
315 "bodyFields": {
316 "example": {
317 "type": "string",
318 "required": ["nested_field", "nested_field2"],
319 "description": "An example field",
320 "properties": {
321 "nested_field": {
322 "type": "number",
323 "required": true,
324 "description": "A nested field"
325 },
326 "nested_field2": {
327 "type": "string",
328 "description": "Optional nested field",
329 "enum": ["a", "b", "c"]
330 }
331 }
332 }
333 }
334 },
335 "output": {
336 "response_field": {
337 "type": "string",
338 "required": true,
339 "description": "A response field"
340 }
341 }
342 });
343
344 assert_eq!(
345 serde_json::to_value(&output_schema).unwrap(),
346 output_schema_json
347 );
348 }
349
350 #[test]
351 fn discoverable_helpers() {
352 let get_schema = OutputSchema::http_get_discoverable();
353 assert!(get_schema.input.as_http().unwrap().discoverable);
354 assert_eq!(get_schema.input.as_http().unwrap().method, Method::Get);
355
356 let get_schema_json = json!({
357 "input": {
358 "discoverable": true,
359 "type": "http",
360 "method": "get"
361 }
362 });
363
364 assert_eq!(serde_json::to_value(&get_schema).unwrap(), get_schema_json);
365
366 let post_schema = OutputSchema::http_post_discoverable();
367 assert_eq!(post_schema.input.as_http().unwrap().method, Method::Post);
368 assert!(post_schema.input.as_http().unwrap().discoverable);
369
370 let post_schema_json = json!({
371 "input": {
372 "discoverable": true,
373 "type": "http",
374 "method": "post"
375 }
376 });
377
378 assert_eq!(
379 serde_json::to_value(&post_schema).unwrap(),
380 post_schema_json
381 );
382 }
383}