openapi_codegen/client/
mod.rs

1use crate::client::api::Api;
2use crate::client::api::Method;
3use crate::client::model::DataType;
4use failure::Error;
5use handlebars::Handlebars;
6use openapiv3::OpenAPI;
7use openapiv3::Operation;
8use openapiv3::Parameter;
9use openapiv3::PathItem;
10use openapiv3::ReferenceOr;
11use serde_derive::Serialize;
12use serde_yaml;
13use std::collections::HashSet;
14use std::fs::{DirBuilder, File};
15use std::io::Write;
16use std::path::Path;
17
18mod api;
19mod model;
20
21pub fn client(api_path: &str, output_dir: &str, tests: bool) -> Result<(), Error> {
22    let mut reg = Handlebars::new();
23    reg.register_escape_fn(handlebars::no_escape);
24    reg.register_template_string("api_mod", include_str!("resources/api_mod.mustache"))?;
25    reg.register_template_string("api", include_str!("resources/api.mustache"))?;
26    reg.register_template_string("model_enum", include_str!("resources/model_enum.mustache"))?;
27    reg.register_template_string("model_mod", include_str!("resources/model_mod.mustache"))?;
28    reg.register_template_string(
29        "model_struct",
30        include_str!("resources/model_struct.mustache"),
31    )?;
32    reg.register_template_string(
33        "model_newtype",
34        include_str!("resources/model_newtype.mustache"),
35    )?;
36    reg.register_template_string("mod", include_str!("resources/mod.mustache"))?;
37
38    let dest_path = Path::new(&output_dir);
39    DirBuilder::new().recursive(true).create(&dest_path)?;
40
41    let spec: OpenAPI = serde_yaml::from_reader(File::open(api_path)?)?;
42
43    DirBuilder::new()
44        .recursive(true)
45        .create(&dest_path.join("apis"))?;
46
47    let mut configuration = File::create(&dest_path.join("apis/configuration.rs"))?;
48    configuration.write_all(include_bytes!("resources/configuration.rs"))?;
49
50    let mut request = File::create(&dest_path.join("apis/request.rs"))?;
51    request.write_all(include_bytes!("resources/request.rs"))?;
52
53    let apis = spec_apis(&spec, tests);
54
55    let api_mod = File::create(&dest_path.join("apis/mod.rs"))?;
56    reg.render_to_write("api_mod", &apis, api_mod)?;
57
58    for api in &apis {
59        let api_file = File::create(&dest_path.join(format!("apis/{}_api.rs", api.snake_id)))?;
60        reg.render_to_write("api", &api, api_file)?;
61    }
62
63    let models = match spec.components {
64        Some(components) => components
65            .schemas
66            .into_iter()
67            .map(|entry| entry.into())
68            .collect::<Vec<DataType>>(),
69        None => vec![],
70    };
71
72    let models_path = dest_path.join("models");
73    DirBuilder::new().recursive(true).create(&models_path)?;
74
75    let models_mod = File::create(models_path.join("mod.rs"))?;
76    reg.render_to_write("model_mod", &models, models_mod)?;
77
78    for model in &models {
79        match model {
80            DataType::Struct(_struct) => {
81                let model = File::create(models_path.join(format!("{}.rs", _struct.snake_id)))?;
82                reg.render_to_write("model_struct", &_struct, model)?;
83            }
84            DataType::NewType(newtype) => {
85                let model = File::create(models_path.join(format!("{}.rs", newtype.snake_id)))?;
86                reg.render_to_write("model_newtype", &newtype, model)?;
87            }
88            DataType::Enum(_enum) => {
89                let model = File::create(models_path.join(format!("{}.rs", _enum.snake_id)))?;
90                reg.render_to_write("model_enum", &_enum, model)?;
91            }
92        }
93    }
94
95    let mod_file = if output_dir == "src" {
96        File::create(dest_path.join("lib.rs"))?
97    } else {
98        File::create(dest_path.join("mod.rs"))?
99    };
100
101    let r#mod = Mod {
102        root: output_dir == "src",
103    };
104
105    reg.render_to_write("mod", &r#mod, mod_file)?;
106
107    Ok(())
108}
109
110#[derive(Debug, Serialize)]
111struct Mod {
112    root: bool,
113}
114
115fn spec_apis(spec: &OpenAPI, tests: bool) -> Vec<Api> {
116    paths_tags(spec)
117        .into_iter()
118        .map(|tag| Api {
119            snake_id: tag.clone().unwrap_or("untagged".to_string()).into(),
120            pascal_id: tag.clone().unwrap_or("untagged".to_string()).into(),
121            methods: spec
122                .paths
123                .iter()
124                .filter(|(_path, reference_or_operations)| {
125                    operations_tags(reference_or_operations).contains(&tag)
126                })
127                .flat_map(|(path, reference_or_operations)| {
128                    operations_methods(path, reference_or_operations)
129                })
130                .collect(),
131            tests,
132        })
133        .collect()
134}
135
136fn operations_methods(path: &str, reference_or_operations: &ReferenceOr<PathItem>) -> Vec<Method> {
137    match reference_or_operations {
138        ReferenceOr::Reference { .. } => unimplemented!(),
139        ReferenceOr::Item(operations) => {
140            let options =
141                vec![
142                    operations.get.as_ref().map(|operation| {
143                        operation_method("GET".into(), path.to_owned(), &operation)
144                    }),
145                    operations.post.as_ref().map(|operation| {
146                        operation_method("POST".into(), path.to_owned(), &operation)
147                    }),
148                    operations.put.as_ref().map(|operation| {
149                        operation_method("PUT".into(), path.to_owned(), &operation)
150                    }),
151                    operations.patch.as_ref().map(|operation| {
152                        operation_method("PATCH".into(), path.to_owned(), &operation)
153                    }),
154                    operations.delete.as_ref().map(|operation| {
155                        operation_method("DELETE".into(), path.to_owned(), &operation)
156                    }),
157                ];
158
159            options.into_iter().filter_map(|o| o).collect()
160        }
161    }
162}
163
164fn operation_method(method: String, path: String, operation: &Operation) -> Method {
165    Method {
166        snake_id: match operation.operation_id.as_ref() {
167            Some(operation_id) => operation_id.to_owned(),
168            None => format!("{}/{}", method, path),
169        }
170        .into(),
171        path: path,
172        http_method: method,
173        path_parameters: operation
174            .parameters
175            .iter()
176            .filter_map(|reference_or_parameter| {
177                if let ReferenceOr::Item(parameter) = reference_or_parameter {
178                    Some(parameter)
179                } else {
180                    None
181                }
182            })
183            .filter_map(|parameter| {
184                if let Parameter::Path { parameter_data, .. } = parameter {
185                    Some(parameter_data)
186                } else {
187                    None
188                }
189            })
190            .map(|parameter_data| parameter_data.into())
191            .collect(),
192        query_parameters: operation
193            .parameters
194            .iter()
195            .filter_map(|reference_or_parameter| {
196                if let ReferenceOr::Item(parameter) = reference_or_parameter {
197                    Some(parameter)
198                } else {
199                    None
200                }
201            })
202            .filter_map(|parameter| {
203                if let Parameter::Query { parameter_data, .. } = parameter {
204                    Some(parameter_data)
205                } else {
206                    None
207                }
208            })
209            .map(|parameter_data| parameter_data.into())
210            .collect(),
211        body: operation
212            .request_body
213            .as_ref()
214            .map(|parameter| parameter.into()),
215        returns: operation
216            .responses
217            .responses
218            .get("200")
219            .and_then(|reference_or_response| match reference_or_response {
220                ReferenceOr::Item(response) => response.content.get("application/json"),
221                _ => unimplemented!(),
222            })
223            .and_then(|reference_or_mediatype| match reference_or_mediatype {
224                ReferenceOr::Item(mediatype) => mediatype.schema.as_ref(),
225                _ => unimplemented!(),
226            })
227            .map(|reference_or_schema| reference_or_schema.into()),
228    }
229}
230
231fn paths_tags(spec: &OpenAPI) -> HashSet<Option<String>> {
232    spec.paths
233        .iter()
234        .flat_map(|(_, operations)| operations_tags(operations))
235        .collect()
236}
237
238fn operations_tags(reference_or_operations: &ReferenceOr<PathItem>) -> Vec<Option<String>> {
239    match reference_or_operations {
240        ReferenceOr::Reference { .. } => unimplemented!(),
241        ReferenceOr::Item(operations) => {
242            let mut tags = operation_tags(operations.get.as_ref());
243            tags.append(&mut operation_tags(operations.post.as_ref()));
244            tags.append(&mut operation_tags(operations.put.as_ref()));
245            tags.append(&mut operation_tags(operations.patch.as_ref()));
246            tags.append(&mut operation_tags(operations.delete.as_ref()));
247
248            tags
249        }
250    }
251}
252
253fn operation_tags(operation: Option<&Operation>) -> Vec<Option<String>> {
254    match operation {
255        Some(operation) => {
256            if operation.tags.is_empty() {
257                vec![None]
258            } else {
259                operation.tags.iter().map(|s| Some(s.to_string())).collect()
260            }
261        }
262        None => Vec::new(),
263    }
264}