1use crate::definitions::{EnumElement, 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::{Deserialize, Serialize};
9use serde_json::json;
10use std::fs::{File, OpenOptions, create_dir_all};
11use std::io::Write;
12use std::path::{Path, PathBuf};
13
14pub struct Data {
15 pub project_root: PathBuf,
16 pub output_dir: PathBuf,
17 pub services: Vec<GenService>,
18 pub enums: Vec<EnumElement>,
19 pub structs: Vec<StructElement>,
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: Vec<Type> = docs
51 .enums
52 .clone()
53 .into_iter()
54 .map(|enum_element| enum_element.inner)
55 .collect();
56
57 let structs: Vec<Type> = docs
58 .structs
59 .clone()
60 .into_iter()
61 .map(|struct_element| struct_element.inner)
62 .collect();
63
64 serde_json::to_writer_pretty(
65 &mut docs_file,
66 &json!({
67 "services": services,
68 "enums": enums,
69 "structs": structs,
70 }),
71 )?;
72 Ok(())
73}
74
75fn wrap_code_md(value: String) -> String {
77 format!(r#"`{value}`"#)
78}
79
80fn format_type(field_name: &str, ty: &Type, datamodels: bool) -> String {
81 match ty {
82 Type::Struct { name, fields } => {
83 if !datamodels {
84 format!(
85 r#"{}: {}{:#}"#,
86 field_name.to_case(Case::Camel),
87 name.to_case(Case::Pascal),
88 format!(
89 "{{ {} }}",
90 fields
91 .iter()
92 .map(|x| format!("{}: {}", x.name, x.ty.to_rust_ref(false)))
93 .join(", ")
94 )
95 )
96 } else {
97 format!(
98 r#"{}{:#}"#,
99 name.to_case(Case::Pascal),
100 format!(
101 "{{ {} }}",
102 fields
103 .iter()
104 .map(|x| format!("{}: {}", x.name, x.ty.to_rust_ref(false)))
105 .join(", ")
106 )
107 )
108 }
109 }
110 Type::StructTable { struct_ref } => {
111 format!(
112 "{}: Vec<{}>",
113 field_name.to_case(Case::Camel),
114 struct_ref.to_case(Case::Pascal),
115 )
116 }
117 Type::Enum { name, variants } => {
118 format!(
119 "{} {{ {} }}",
120 name.to_case(Case::Pascal),
121 variants.iter().map(|v| &v.name).join(", ")
122 )
123 }
124 Type::EnumRef { name, prefixed_name } => {
125 format!(
126 "{}: {}",
127 field_name.to_case(Case::Camel),
128 prefixed_name
129 .then(|| format!("Enum{}", name.to_case(Case::Pascal)))
130 .unwrap_or(name.to_case(Case::Pascal))
131 )
132 }
133 _ => format!("{}: {}", field_name.to_case(Case::Camel), ty.to_rust_ref(false)),
152 }
153}
154
155pub fn gen_md_docs(data: &Data) -> eyre::Result<()> {
156 let docs_filename = data.project_root.join("docs").join("README.md");
157 let mut docs_file = File::create(docs_filename)?;
158 writeln!(
159 &mut docs_file,
160 r#"
161# API Reference
162
163## Structs/Datamodels
164
165```rust
166{}
167```
168---
169
170## Enums
171
172```rust
173{}
174```
175---
176
177 "#,
178 data.structs
179 .iter()
180 .map(|s| format!(
181 "struct {:#}\n",
182 format_type(&s.inner.to_rust_ref(false), &s.inner, true)
183 ))
184 .join("\n\n"),
185 data.enums
186 .iter()
187 .map(|e| format!("enum {:#}\n", format_type(&e.inner.to_rust_ref(false), &e.inner, true)))
188 .join("\n\n")
189 )?;
190 for s in &data.services {
191 writeln!(
192 &mut docs_file,
193 r#"
194## {} Server
195ID: {}
196### Endpoints
197|Code|Name|Parameters|Response|Description|FE Facing|
198|-----------|-----------|----------|--------|-----------|-----------|"#,
199 s.name, s.id
200 )?;
201 for e in &s.endpoints {
202 writeln!(
203 &mut docs_file,
204 "|{}|{}|{}|{}|{}|{}|",
205 e.schema.code,
206 e.schema.name,
207 e.schema
208 .parameters
209 .iter()
210 .map(|x| wrap_code_md(format_type(&x.name, &x.ty, false)))
211 .join(", "),
212 e.schema
213 .returns
214 .iter()
215 .map(|x| wrap_code_md(format_type(&x.name, &x.ty, false)))
216 .join(", "),
217 e.schema.description,
218 e.frontend_facing,
219 )?;
220 }
221 }
222 Ok(())
223}
224
225pub fn gen_systemd_services(data: &Data, app_name: &str, user: &str) -> eyre::Result<()> {
226 create_dir_all(data.project_root.join("etc").join("systemd"))?;
227
228 for srv in &data.services {
229 let service_filename = data
230 .project_root
231 .join("etc")
232 .join("systemd")
233 .join(format!("{}_{}.service", app_name, srv.name));
234 let mut service_file = File::create(&service_filename)?;
235 let v = get_systemd_service(app_name, &srv.name, user);
236 write!(&mut service_file, "{v}")?;
237 }
238 Ok(())
239}
240
241pub fn get_error_messages(root: &Path) -> eyre::Result<ErrorMessages> {
242 let def_filename = root.join("docs").join("error_codes").join("error_codes.json");
243
244 if let Some(parent) = def_filename.parent() {
246 std::fs::create_dir_all(parent)?;
247 }
248
249 if !def_filename.exists() {
250 let _file = OpenOptions::new()
251 .create(true)
252 .write(true)
253 .truncate(true)
254 .open(&def_filename)?;
255 }
256
257 let def_file = std::fs::read(&def_filename)?;
259
260 if def_file.is_empty() {
261 Ok(ErrorMessages {
262 language: String::from("TODO"),
263 codes: vec![ErrorMessage {
264 code: 0,
265 symbol: String::from("XXX"),
266 message: String::from("Please populate error_codes.json"),
267 source: String::from("None"),
268 }],
269 })
270 } else {
271 let definitions: ErrorMessages = serde_json::from_slice(&def_file)?;
272 Ok(definitions)
273 }
274}
275
276pub fn gen_error_message_md(root: &Path) -> eyre::Result<()> {
277 let definitions = get_error_messages(root)?;
278 let doc_filename = root.join("docs").join("error_codes").join("error_codes.md");
279 let mut doc_file = File::create(doc_filename)?;
280 writeln!(
281 &mut doc_file,
282 r#"
283# Error Messages
284|Error Code|Error Symbol|Error Message|Error Source|
285|----------|------------|-------------|------------|"#,
286 )?;
287 for item in definitions.codes {
288 writeln!(
289 &mut doc_file,
290 "|{}|{}|{}|{}|",
291 item.code, item.symbol, item.message, item.source
292 )?;
293 }
294 Ok(())
295}
296
297#[derive(Debug, Serialize, Deserialize)]
298pub struct ErrorMessages {
299 pub language: String,
300 pub codes: Vec<ErrorMessage>,
301}
302
303#[derive(Debug, Serialize, Deserialize)]
304pub struct ErrorMessage {
305 pub code: i64,
306 #[serde(default)]
307 pub symbol: String,
308 pub message: String,
309 #[serde(default)]
310 pub source: String,
311}