schematools/codegen/openapi/
responses.rs1use 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 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 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 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}