1use indexmap::IndexMap;
4
5use crate::schema::{EndpointSchema, SchemaRegistry};
6
7use super::convert::{
8 is_null_schema, operation_id, parameters_from_schema, path_param_names, schema_ref,
9 to_openapi_path, SchemaCatalog,
10};
11use super::document::{
12 OpenApiComponents, OpenApiDocument, OpenApiInfo, OpenApiMediaType, OpenApiOperation,
13 OpenApiRequestBody, OpenApiResponse, OpenApiResponses, OpenApiServer,
14};
15
16const JSON_CONTENT: &str = "application/json";
17
18#[derive(Debug, Default)]
20pub struct OpenApiBuilder {
21 title: String,
22 version: String,
23 description: Option<String>,
24 servers: Vec<OpenApiServer>,
25}
26
27impl OpenApiBuilder {
28 pub fn new() -> Self {
30 Self {
31 title: "API".into(),
32 version: "0.1.0".into(),
33 description: None,
34 servers: Vec::new(),
35 }
36 }
37
38 pub fn title(mut self, title: impl Into<String>) -> Self {
40 self.title = title.into();
41 self
42 }
43
44 pub fn version(mut self, version: impl Into<String>) -> Self {
46 self.version = version.into();
47 self
48 }
49
50 pub fn description(mut self, description: impl Into<String>) -> Self {
52 self.description = Some(description.into());
53 self
54 }
55
56 pub fn server(mut self, url: impl Into<String>) -> Self {
58 self.servers.push(OpenApiServer {
59 url: url.into(),
60 description: None,
61 });
62 self
63 }
64
65 pub fn server_with_description(
67 mut self,
68 url: impl Into<String>,
69 description: impl Into<String>,
70 ) -> Self {
71 self.servers.push(OpenApiServer {
72 url: url.into(),
73 description: Some(description.into()),
74 });
75 self
76 }
77
78 pub fn from_registry(&self, registry: &SchemaRegistry) -> OpenApiDocument {
82 let mut catalog = SchemaCatalog::default();
83 let mut paths: IndexMap<String, IndexMap<String, OpenApiOperation>> = IndexMap::new();
84
85 for entry in registry.entries() {
86 let operation = build_operation(entry, &mut catalog);
87 let openapi_path = to_openapi_path(&entry.path);
88 let method = entry.method.as_str().to_ascii_lowercase();
89 paths
90 .entry(openapi_path)
91 .or_default()
92 .insert(method, operation);
93 }
94
95 let components = if catalog.schemas.is_empty() {
96 None
97 } else {
98 Some(OpenApiComponents {
99 schemas: catalog.schemas,
100 })
101 };
102
103 OpenApiDocument {
104 openapi: "3.0.3".into(),
105 info: OpenApiInfo {
106 title: self.title.clone(),
107 version: self.version.clone(),
108 description: self.description.clone(),
109 },
110 servers: if self.servers.is_empty() {
111 None
112 } else {
113 Some(self.servers.clone())
114 },
115 paths,
116 components,
117 }
118 }
119}
120
121fn build_operation(entry: &EndpointSchema, catalog: &mut SchemaCatalog) -> OpenApiOperation {
122 let path_names = path_param_names(&entry.path);
123 let prefix = operation_id(&entry.method, &entry.path);
124
125 let mut parameters = Vec::new();
126 if let Some(params_schema) = &entry.params_schema {
127 parameters.extend(parameters_from_schema(
128 params_schema,
129 "path",
130 &path_names,
131 catalog,
132 &format!("{prefix}Path"),
133 ));
134 }
135 if let Some(query_schema) = &entry.query_schema {
136 parameters.extend(parameters_from_schema(
137 query_schema,
138 "query",
139 &path_names,
140 catalog,
141 &format!("{prefix}Query"),
142 ));
143 }
144
145 let request_body = entry.request_schema.as_ref().and_then(|schema| {
146 if is_null_schema(schema) {
147 return None;
148 }
149 let ref_path = catalog.register(&format!("{prefix}Request"), schema)?;
150 Some(OpenApiRequestBody {
151 description: Some("Request body".into()),
152 required: true,
153 content: json_content(schema_ref(ref_path)),
154 })
155 });
156
157 let mut response_map: IndexMap<String, OpenApiResponse> = IndexMap::new();
158 if let Some(response_schema) = &entry.response_schema {
159 if !is_null_schema(response_schema) {
160 if let Some(ref_path) = catalog.register(&format!("{prefix}Response"), response_schema)
161 {
162 response_map.insert(
163 "200".into(),
164 OpenApiResponse {
165 description: "Success".into(),
166 content: Some(json_content(schema_ref(ref_path))),
167 },
168 );
169 }
170 }
171 }
172 if response_map.is_empty() {
173 response_map.insert(
174 "200".into(),
175 OpenApiResponse {
176 description: "Success".into(),
177 content: None,
178 },
179 );
180 }
181
182 let summary = format!("{} {}", entry.method, entry.path);
183
184 OpenApiOperation {
185 summary: Some(summary),
186 description: None,
187 operation_id: Some(prefix),
188 parameters,
189 request_body,
190 responses: OpenApiResponses {
191 statuses: response_map,
192 },
193 }
194}
195
196fn json_content(schema: super::document::OpenApiSchemaRef) -> IndexMap<String, OpenApiMediaType> {
197 let mut map = IndexMap::new();
198 map.insert(JSON_CONTENT.into(), OpenApiMediaType { schema });
199 map
200}