Skip to main content

endpoint_gen/
docs.rs

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    // Ensure the parent directories exist
26    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    // Only write FE facing endpoints to the services.json file
34    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
75/// Wraps ` ` around the given string
76fn 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        // Type::DataTable { name, fields } => {
134        //     format!(
135        //         "{}: Vec<{}{:#}>",
136        //         field_name.to_case(Case::Camel),
137        //         name.to_case(Case::Pascal),
138        //         format!(
139        //             "{{ {} }}",
140        //             fields
141        //                 .iter()
142        //                 .map(|x| format!(
143        //                     "{}: {}",
144        //                     x.name.to_case(Case::Camel),
145        //                     x.ty.to_rust_ref(false)
146        //                 ))
147        //                 .join(", ")
148        //         )
149        //     )
150        // }
151        _ => 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    // Ensure the parent directories exist, and create the file
245    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    // Read the file contents
258    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}