golem_openapi_client_generator/
lib.rs

1// Copyright 2024 Golem Cloud
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15use std::collections::HashMap;
16use std::fs::File;
17use std::io::BufReader;
18use std::path::{Path, PathBuf};
19use std::result;
20
21use openapiv3::OpenAPI;
22
23pub use merger::merge_all_openapi_specs;
24
25use crate::rust::lib_gen::{Module, ModuleDef, ModuleName};
26use crate::rust::model_gen::RefCache;
27
28pub(crate) mod merger;
29pub(crate) mod printer;
30mod rust;
31mod toml;
32
33#[derive(Debug, Clone)]
34pub enum Error {
35    Unexpected { message: String },
36    Unimplemented { message: String },
37}
38
39impl Error {
40    pub fn unexpected<S: Into<String>>(message: S) -> Error {
41        Error::Unexpected {
42            message: message.into(),
43        }
44    }
45    pub fn unimplemented<S: Into<String>>(message: S) -> Error {
46        Error::Unimplemented {
47            message: message.into(),
48        }
49    }
50
51    pub fn extend<S: Into<String>>(&self, s: S) -> Error {
52        match self {
53            Error::Unexpected { message } => Error::Unexpected {
54                message: format!("{} {}", s.into(), message.clone()),
55            },
56            Error::Unimplemented { message } => Error::Unimplemented {
57                message: format!("{} {}", s.into(), message.clone()),
58            },
59        }
60    }
61}
62
63pub type Result<T> = result::Result<T, Error>;
64
65#[allow(clippy::too_many_arguments)]
66pub fn gen(
67    openapi_specs: Vec<OpenAPI>,
68    target: &Path,
69    name: &str,
70    version: &str,
71    overwrite_cargo: bool,
72    disable_clippy: bool,
73    mapping: &[(&str, &str)],
74    ignored_paths: &[&str],
75) -> Result<()> {
76    let mapping: HashMap<&str, &str> = HashMap::from_iter(mapping.iter().cloned());
77
78    let open_api = merge_all_openapi_specs(openapi_specs)?;
79
80    let src = target.join("src");
81    let api = src.join("api");
82    let model = src.join("model");
83
84    std::fs::create_dir_all(&api).unwrap();
85    std::fs::create_dir_all(&model).unwrap();
86
87    let context = rust::context_gen::context_gen(&open_api)?;
88    std::fs::write(src.join(context.def.name.file_name()), &context.code).unwrap();
89
90    let mut ref_cache = RefCache::new();
91
92    let modules: Result<Vec<Module>> = open_api
93        .tags
94        .iter()
95        .map(|tag| {
96            rust::client_gen::client_gen(
97                &open_api,
98                Some(tag.clone()),
99                &mut ref_cache,
100                ignored_paths,
101            )
102        })
103        .collect();
104
105    let mut api_module_defs = Vec::new();
106
107    for module in modules? {
108        std::fs::write(api.join(module.def.name.file_name()), module.code).unwrap();
109        api_module_defs.push(module.def.clone())
110    }
111
112    let mut known_refs = RefCache::new();
113    let mut models = Vec::new();
114
115    let multipart_field_file = rust::model_gen::multipart_field_module()?;
116    std::fs::write(
117        model.join(multipart_field_file.def.name.file_name()),
118        multipart_field_file.code,
119    )
120    .unwrap();
121    models.push(multipart_field_file.def);
122
123    while !ref_cache.is_empty() {
124        let mut next_ref_cache = RefCache::new();
125
126        for ref_str in ref_cache.refs {
127            if !known_refs.refs.contains(&ref_str) {
128                let model_file =
129                    rust::model_gen::model_gen(&ref_str, &open_api, &mapping, &mut next_ref_cache)?;
130                std::fs::write(model.join(model_file.def.name.file_name()), model_file.code)
131                    .unwrap();
132                models.push(model_file.def);
133                known_refs.add(ref_str);
134            }
135        }
136
137        let mut unknown_ref_cache = RefCache::new();
138
139        for ref_str in next_ref_cache.refs {
140            if !known_refs.refs.contains(&ref_str) {
141                unknown_ref_cache.add(ref_str)
142            }
143        }
144
145        ref_cache = unknown_ref_cache;
146    }
147
148    std::fs::write(
149        src.join("api.rs"),
150        rust::lib_gen::lib_gen("crate::api", &api_module_defs, disable_clippy),
151    )
152    .unwrap();
153
154    std::fs::write(
155        src.join("model.rs"),
156        rust::lib_gen::lib_gen("crate::model", &models, disable_clippy),
157    )
158    .unwrap();
159
160    let errors = rust::error_gen::error_gen();
161    std::fs::write(src.join(errors.def.name.file_name()), &errors.code).unwrap();
162
163    let module_defs = vec![
164        context.def,
165        ModuleDef::new(ModuleName::new_pub("api")),
166        ModuleDef::new(ModuleName::new_pub("model")),
167        errors.def,
168    ];
169
170    let lib = rust::lib_gen::lib_gen("crate", &module_defs, disable_clippy);
171    std::fs::write(src.join("lib.rs"), lib).unwrap();
172
173    if overwrite_cargo {
174        let cargo = toml::cargo::gen(name, version);
175        std::fs::write(target.join("Cargo.toml"), cargo).unwrap();
176    }
177
178    Ok(())
179}
180
181pub fn parse_openapi_specs(
182    spec: &[PathBuf],
183) -> std::result::Result<Vec<OpenAPI>, Box<dyn std::error::Error>> {
184    spec.iter()
185        .map(|spec_path| {
186            let file = File::open(spec_path)?;
187            let reader = BufReader::new(file);
188            let openapi: OpenAPI = serde_yaml::from_reader(reader)?;
189            Ok(openapi)
190        })
191        .collect()
192}