Skip to main content

endpoint_gen/
docs.rs

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    // 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 = 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
97/// Wraps ` ` around the given string
98fn 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        // Type::DataTable { name, fields } => {
156        //     format!(
157        //         "{}: Vec<{}{:#}>",
158        //         field_name.to_case(Case::Camel),
159        //         name.to_case(Case::Pascal),
160        //         format!(
161        //             "{{ {} }}",
162        //             fields
163        //                 .iter()
164        //                 .map(|x| format!(
165        //                     "{}: {}",
166        //                     x.name.to_case(Case::Camel),
167        //                     x.ty.to_rust_ref(false)
168        //                 ))
169        //                 .join(", ")
170        //         )
171        //     )
172        // }
173        _ => 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}