Skip to main content

schematools/codegen/openapi/
responses.rs

1use std::collections::HashMap;
2
3use crate::codegen::openapi::parameters::extract_parameter;
4use crate::codegen::openapi::MediaVendorType;
5use crate::{
6    codegen::jsonschema::{JsonSchemaExtractOptions, ModelContainer},
7    error::Error,
8    resolver::SchemaResolver,
9    scope::SchemaScope,
10};
11use serde::Serialize;
12use serde_json::Map;
13use serde_json::Value;
14
15use super::parameters::Parameter;
16
17#[derive(Debug, Serialize, Default, Clone)]
18#[serde(rename_all = "camelCase")]
19pub struct Responses {
20    pub success: Option<Response>,
21    pub all: Vec<Response>,
22}
23
24#[derive(Debug, Serialize, Clone)]
25#[serde(rename_all = "camelCase")]
26pub struct Response {
27    pub status_code: u32,
28
29    pub models: Option<super::MediaModelsContainer>,
30
31    pub description: Option<String>,
32
33    pub headers: Option<Vec<Parameter>>,
34}
35
36pub fn extract(
37    node: &Map<String, Value>,
38    scope: &mut SchemaScope,
39    mcontainer: &mut ModelContainer,
40    resolver: &SchemaResolver,
41    options: &JsonSchemaExtractOptions,
42) -> Result<Responses, Error> {
43    match node.get("responses") {
44        Some(body) => {
45            scope.property("responses");
46
47            let responses = extract_responses(body, scope, mcontainer, resolver, options)?;
48
49            scope.pop();
50
51            Ok(responses)
52        }
53        None => Ok(Responses::default()),
54    }
55}
56
57#[allow(clippy::needless_borrow)]
58pub fn extract_responses(
59    node: &Value,
60    scope: &mut SchemaScope,
61    mcontainer: &mut ModelContainer,
62    resolver: &SchemaResolver,
63    options: &JsonSchemaExtractOptions,
64) -> Result<Responses, Error> {
65    resolver.resolve(node, scope, |node, scope| match node {
66        Value::Object(ref data) => {
67            let mut responses = Responses::default();
68
69            // parse responses
70            let mut parsed = data
71                .iter()
72                .map(|(status_code, response_node)| {
73                    scope.property(status_code);
74
75                    let response = extract_response(
76                        status_code,
77                        response_node,
78                        scope,
79                        mcontainer,
80                        resolver,
81                        options,
82                    );
83
84                    scope.pop();
85
86                    response
87                })
88                .collect::<Result<Vec<Response>, _>>()?;
89
90            // find 2xx and unique models for entire endpoint
91            let mut occurrences: HashMap<String, u8> = HashMap::new();
92            for response in parsed.iter() {
93                if let Some(mcontainer) = &response.models {
94                    for mm in &mcontainer.list {
95                        occurrences
96                            .entry((&mm.model).into())
97                            .and_modify(|count| *count += 1)
98                            .or_insert(1);
99                    }
100                }
101            }
102
103            let re = regex::Regex::new(r"/vnd\.|\+").unwrap();
104            for response in parsed.iter_mut() {
105                if let Some(ref mut mcontainer) = response.models {
106                    // inidicates if an endpoint has multiple content types
107                    mcontainer.multiple_content_types = mcontainer.list.len() > 1;
108
109                    for mm in mcontainer.list.iter_mut() {
110                        let key: String = (&mm.model).into();
111
112                        mm.is_unique = *occurrences.get(&key).unwrap_or(&1) == 1;
113
114                        let mut base_content_type = mm.content_type.clone();
115                        if let [b, inner, e] = re.split(&mm.content_type).collect::<Vec<_>>()[..] {
116                            base_content_type = format!("{b}/{e}");
117                            mm.vnd = Some(MediaVendorType {
118                                base: base_content_type.clone(),
119                                vnd: inner.to_string(),
120                            });
121                        }
122
123                        if mcontainer.multiple_content_types
124                            && base_content_type != mcontainer.default_content_type
125                        {
126                            mm.alternative_content_type = true;
127                        }
128                    }
129                }
130            }
131
132            for response in parsed {
133                scope.property(&response.status_code.to_string());
134
135                if responses.success.is_none()
136                    && response.status_code >= 200
137                    && response.status_code < 300
138                {
139                    log::info!("{} -> success status code: {}", scope, response.status_code);
140                    responses.success = Some(response.clone());
141                }
142
143                responses.all.push(response);
144
145                scope.pop();
146            }
147
148            Ok(responses)
149        }
150        _ => Err(Error::CodegenInvalidEndpointProperty(
151            "responses".to_string(),
152            scope.to_string(),
153        )),
154    })
155}
156
157pub fn extract_response(
158    code: &str,
159    node: &Value,
160    scope: &mut SchemaScope,
161    mcontainer: &mut ModelContainer,
162    resolver: &SchemaResolver,
163    options: &JsonSchemaExtractOptions,
164) -> Result<Response, Error> {
165    resolver.resolve(node, scope, |node, scope| match node {
166        Value::Object(data) => {
167            log::trace!("{}", scope);
168
169            let description = data.get("description").map(|v| {
170                v.as_str()
171                    .map(|s| s.lines().collect::<Vec<_>>().join(" "))
172                    .unwrap()
173            });
174
175            let status_code = if code == "default" {
176                0
177            } else {
178                code.parse::<u32>().map_err(|_| {
179                    Error::CodegenInvalidEndpointProperty(
180                        format!("response:{code}"),
181                        scope.to_string(),
182                    )
183                })?
184            };
185
186            scope.glue(&status_code.to_string());
187
188            let model = super::get_content(data, scope, mcontainer, resolver, options)
189                .map_or(Ok(None), |v| v.map(Some));
190
191            scope.pop();
192
193            let headers = data
194                .get("headers")
195                .map(|s| match s {
196                    Value::Object(headers_map) => {
197                        let mut headers: Vec<Parameter> = vec![];
198
199                        for (name, param) in headers_map {
200                            headers.push(extract_parameter(
201                                &as_header_node(name, param, scope, resolver)?,
202                                scope,
203                                mcontainer,
204                                resolver,
205                                options,
206                            )?);
207                        }
208
209                        Ok(headers)
210                    }
211                    _ => Err(Error::CodegenInvalidEndpointProperty(
212                        format!("response:{code}:headers"),
213                        scope.to_string(),
214                    )),
215                })
216                .map_or(Ok(None), |v| v.map(Some))?;
217
218            Ok(Response {
219                models: model?,
220                headers,
221                description,
222                status_code,
223            })
224        }
225        _ => Err(Error::CodegenInvalidEndpointProperty(
226            format!("response:{code}"),
227            scope.to_string(),
228        )),
229    })
230}
231
232fn as_header_node(
233    name: &str,
234    node: &Value,
235    scope: &mut SchemaScope,
236    resolver: &SchemaResolver,
237) -> Result<Value, Error> {
238    resolver.resolve(node, scope, |node, _scope| {
239        let mut parameter = node.clone();
240
241        let obj = parameter.as_object_mut().ok_or_else(|| {
242            Error::CodegenInvalidEndpointProperty(format!("header:{name}"), "todo".to_string())
243        })?;
244
245        obj.insert("in".to_string(), Value::String("header".to_string()));
246        obj.insert("name".to_string(), Value::String(name.to_string()));
247
248        Ok(parameter)
249    })
250}
251
252#[cfg(test)]
253mod tests {
254    use super::*;
255    use serde_json::json;
256
257    #[test]
258    fn test_all_models_unique() {
259        let schema = json!({
260            "200": {
261                "description": "Success response",
262                "content": {
263                    "application/json": { "schema" : {"type": "string"} },
264                    "application/vnd.short+json": { "schema" : {"type": "object", "properties": { "test" : {"type": "string"}}} },
265                },
266            },
267            "400": {
268                "description": "Fail response",
269                "content": {
270                    "application/json": { "schema" : {"type": "object", "properties": { "errorCode" : {"type": "number"}}} },
271                },
272            }
273        });
274
275        let mut mcontainer = ModelContainer::default();
276        let mut scope = SchemaScope::default();
277        let resolver = SchemaResolver::empty();
278        let options = JsonSchemaExtractOptions::default();
279
280        let result = extract_responses(&schema, &mut scope, &mut mcontainer, &resolver, &options);
281
282        assert!(result.is_ok());
283
284        let responses = result.unwrap();
285        assert!(!responses.all.is_empty());
286
287        {
288            let mut it = responses.all.iter();
289            let response200 = it.next().unwrap();
290            let m1 = response200.models.as_ref().unwrap().list.get(0).unwrap();
291            assert_eq!(m1.alternative_content_type, false);
292
293            let m2 = response200.models.as_ref().unwrap().list.get(1).unwrap();
294            assert_eq!(m2.alternative_content_type, false);
295            assert_eq!(
296                m2.vnd,
297                Some(MediaVendorType {
298                    base: "application/json".to_string(),
299                    vnd: "short".to_string()
300                })
301            );
302
303            let response400 = it.next().unwrap();
304            let m1 = response400.models.as_ref().unwrap().list.get(0).unwrap();
305            assert_eq!(m1.alternative_content_type, false);
306        }
307
308        for response in responses.all {
309            let mcontainer = response.models.unwrap();
310            assert!(!mcontainer.list.is_empty());
311
312            for m in mcontainer.list {
313                assert!(m.is_unique, "{:?} should be unique", m.model);
314            }
315        }
316    }
317
318    #[test]
319    fn test_alternative() {
320        let schema = json!({
321            "200": {
322                "description": "Success response",
323                "content": {
324                    "application/json": { "schema" : {"type": "string"} },
325                    "text/html": { "schema" : {"type": "string"} },
326                },
327            }
328        });
329
330        let mut mcontainer = ModelContainer::default();
331        let mut scope = SchemaScope::default();
332        let resolver = SchemaResolver::empty();
333        let options = JsonSchemaExtractOptions::default();
334
335        let result = extract_responses(&schema, &mut scope, &mut mcontainer, &resolver, &options);
336
337        assert!(result.is_ok());
338
339        let responses = result.unwrap();
340        assert!(!responses.all.is_empty());
341
342        let response = responses.all.iter().next().unwrap();
343        let models = response.models.as_ref().unwrap();
344
345        let mut it = models.list.iter();
346        let first = it.next().unwrap();
347        assert_eq!(first.alternative_content_type, false);
348
349        let second = it.next().unwrap();
350        assert_eq!(second.alternative_content_type, true);
351    }
352
353    #[test]
354    fn test_no_unique_model() {
355        let schema = json!({
356            "200": {
357                "description": "Success response",
358                "content": {
359                    "application/json": { "schema" : {"type": "string"} },
360                    "application/vnd.short+json": { "schema" : {"type": "string"} },
361                },
362            },
363            "400": {
364                "description": "Fail response",
365                "content": {
366                    "application/json": { "schema" : {"type": "string"} },
367                },
368            }
369        });
370
371        let mut mcontainer = ModelContainer::default();
372        let mut scope = SchemaScope::default();
373        let resolver = SchemaResolver::empty();
374        let options = JsonSchemaExtractOptions::default();
375
376        let result = extract_responses(&schema, &mut scope, &mut mcontainer, &resolver, &options);
377
378        assert!(result.is_ok());
379
380        let responses = result.unwrap();
381        assert!(!responses.all.is_empty());
382
383        for response in responses.all {
384            let mcontainer = response.models.unwrap();
385
386            assert!(!mcontainer.list.is_empty());
387
388            for m in mcontainer.list {
389                assert!(!m.is_unique, "{:?} should not be unique", m.model);
390            }
391        }
392    }
393}