1use crate::definitions::{EnumElement, ErrorCodeSchema, GenService, StructElement};
2use crate::rust::ToRust;
3use crate::service::get_systemd_service;
4use convert_case::{Case, Casing};
5use endpoint_libs::model::{EndpointSchema, Service, Type};
6use eyre::Context;
7use itertools::Itertools;
8use serde_json::json;
9use std::fs::{File, create_dir_all};
10use std::io::Write;
11use std::path::{Path, PathBuf};
12
13pub struct Data {
14 pub project_root: PathBuf,
15 pub output_dir: PathBuf,
16 pub services: Vec<GenService>,
17 pub enums: Vec<EnumElement>,
18 pub structs: Vec<StructElement>,
19 pub error_codes: Vec<ErrorCodeSchema>,
20}
21
22pub fn gen_services_docs(docs: &Data) -> eyre::Result<()> {
23 let docs_filename = docs.project_root.join("docs").join("services.json");
24
25 if let Some(parent) = docs_filename.parent() {
27 std::fs::create_dir_all(parent)?;
28 }
29
30 let mut docs_file = File::create(&docs_filename)
31 .with_context(|| format!("Failed to create docs file: {}", docs_filename.display()))?;
32
33 let services = docs
35 .services
36 .clone()
37 .into_iter()
38 .map(|service| {
39 let fe_endpoints: Vec<EndpointSchema> = service
40 .endpoints
41 .into_iter()
42 .filter(|endpoint| endpoint.frontend_facing)
43 .collect();
44
45 Service::new(service.name, service.id, fe_endpoints)
46 })
47 .filter(|service| !service.endpoints.is_empty())
48 .collect::<Vec<Service>>();
49
50 let enums = doc_enums(docs);
51
52 let structs: Vec<Type> = docs
53 .structs
54 .clone()
55 .into_iter()
56 .map(|struct_element| struct_element.inner)
57 .collect();
58
59 serde_json::to_writer_pretty(
60 &mut docs_file,
61 &json!({
62 "services": services,
63 "enums": enums,
64 "structs": structs,
65 }),
66 )?;
67 Ok(())
68}
69
70fn error_code_enum(codes: &[ErrorCodeSchema]) -> Type {
71 Type::enum_(
72 "ErrorCode",
73 codes
74 .iter()
75 .map(|code| {
76 endpoint_libs::model::EnumVariant::new_with_description(
77 code.name.to_case(Case::Pascal),
78 code.description.clone(),
79 code.code,
80 )
81 })
82 .collect(),
83 )
84}
85
86fn doc_enums(data: &Data) -> Vec<Type> {
87 let mut enums: Vec<Type> = data
88 .enums
89 .clone()
90 .into_iter()
91 .map(|enum_element| enum_element.inner)
92 .collect();
93 enums.push(error_code_enum(&data.error_codes));
94 enums
95}
96
97fn wrap_code_md(value: String) -> String {
99 format!(r#"`{value}`"#)
100}
101
102fn format_type(field_name: &str, ty: &Type, datamodels: bool) -> String {
103 match ty {
104 Type::Struct { name, fields } => {
105 if !datamodels {
106 format!(
107 r#"{}: {}{:#}"#,
108 field_name.to_case(Case::Camel),
109 name.to_case(Case::Pascal),
110 format!(
111 "{{ {} }}",
112 fields
113 .iter()
114 .map(|x| format!("{}: {}", x.name, x.ty.to_rust_ref(false)))
115 .join(", ")
116 )
117 )
118 } else {
119 format!(
120 r#"{}{:#}"#,
121 name.to_case(Case::Pascal),
122 format!(
123 "{{ {} }}",
124 fields
125 .iter()
126 .map(|x| format!("{}: {}", x.name, x.ty.to_rust_ref(false)))
127 .join(", ")
128 )
129 )
130 }
131 }
132 Type::StructTable { struct_ref } => {
133 format!(
134 "{}: Vec<{}>",
135 field_name.to_case(Case::Camel),
136 struct_ref.to_case(Case::Pascal),
137 )
138 }
139 Type::Enum { name, variants } => {
140 format!(
141 "{} {{ {} }}",
142 name.to_case(Case::Pascal),
143 variants.iter().map(|v| &v.name).join(", ")
144 )
145 }
146 Type::EnumRef { name, prefixed_name } => {
147 format!(
148 "{}: {}",
149 field_name.to_case(Case::Camel),
150 prefixed_name
151 .then(|| format!("Enum{}", name.to_case(Case::Pascal)))
152 .unwrap_or(name.to_case(Case::Pascal))
153 )
154 }
155 _ => format!("{}: {}", field_name.to_case(Case::Camel), ty.to_rust_ref(false)),
174 }
175}
176
177fn format_errors(errors: &[endpoint_libs::model::EndpointErrorSchema]) -> String {
178 errors
179 .iter()
180 .map(|error| {
181 let fields = if error.fields.is_empty() {
182 String::new()
183 } else {
184 format!(
185 " {{{}}}",
186 error
187 .fields
188 .iter()
189 .map(|field| format!("{}: {}", field.name.to_case(Case::Camel), field.ty.to_rust_ref(false)))
190 .join(", ")
191 )
192 };
193 format!("{}({}){}", error.name, error.code, fields)
194 })
195 .join(", ")
196}
197
198pub fn gen_md_docs(data: &Data) -> eyre::Result<()> {
199 let docs_filename = data.project_root.join("docs").join("README.md");
200 let mut docs_file = File::create(docs_filename)?;
201 writeln!(
202 &mut docs_file,
203 r#"
204# API Reference
205
206## Structs/Datamodels
207
208```rust
209{}
210```
211---
212
213## Enums
214
215```rust
216{}
217```
218---
219
220 "#,
221 data.structs
222 .iter()
223 .map(|s| format!(
224 "struct {:#}\n",
225 format_type(&s.inner.to_rust_ref(false), &s.inner, true)
226 ))
227 .join("\n\n"),
228 data.enums
229 .iter()
230 .map(|e| e.inner.clone())
231 .chain(std::iter::once(error_code_enum(&data.error_codes)))
232 .map(|e| format!("enum {:#}\n", format_type(&e.to_rust_ref(false), &e, true)))
233 .join("\n\n")
234 )?;
235 for s in &data.services {
236 writeln!(
237 &mut docs_file,
238 r#"
239## {} Server
240ID: {}
241### Endpoints
242|Code|Name|Parameters|Response|Description|FE Facing|Errors|
243|-----------|-----------|----------|--------|-----------|-----------|-----------|"#,
244 s.name, s.id
245 )?;
246 for e in &s.endpoints {
247 writeln!(
248 &mut docs_file,
249 "|{}|{}|{}|{}|{}|{}|{}|",
250 e.schema.code,
251 e.schema.name,
252 e.schema
253 .parameters
254 .iter()
255 .map(|x| wrap_code_md(format_type(&x.name, &x.ty, false)))
256 .join(", "),
257 e.schema
258 .returns
259 .iter()
260 .map(|x| wrap_code_md(format_type(&x.name, &x.ty, false)))
261 .join(", "),
262 e.schema.description,
263 e.frontend_facing,
264 format_errors(&e.schema.errors),
265 )?;
266 }
267 }
268 Ok(())
269}
270
271pub fn gen_systemd_services(data: &Data, app_name: &str, user: &str) -> eyre::Result<()> {
272 create_dir_all(data.project_root.join("etc").join("systemd"))?;
273
274 for srv in &data.services {
275 let service_filename = data
276 .project_root
277 .join("etc")
278 .join("systemd")
279 .join(format!("{}_{}.service", app_name, srv.name));
280 let mut service_file = File::create(&service_filename)?;
281 let v = get_systemd_service(app_name, &srv.name, user);
282 write!(&mut service_file, "{v}")?;
283 }
284 Ok(())
285}
286
287pub fn gen_error_message_md(root: &Path, codes: &[ErrorCodeSchema]) -> eyre::Result<()> {
288 let doc_filename = root.join("docs").join("error_codes").join("error_codes.md");
289
290 if let Some(parent) = doc_filename.parent() {
291 std::fs::create_dir_all(parent)?;
292 }
293
294 let mut doc_file = File::create(doc_filename)?;
295 writeln!(
296 &mut doc_file,
297 r#"
298# Error Messages
299|Error Code|Error Name|Description|
300|----------|----------|-----------|"#,
301 )?;
302 for item in codes {
303 writeln!(&mut doc_file, "|{}|{}|{}|", item.code, item.name, item.description)?;
304 }
305 Ok(())
306}