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