Skip to main content

endpoint_gen/
definitions.rs

1use crate::rust::ToRust;
2use convert_case::{Case, Casing};
3use endpoint_gen_macros::DefinitionVariant;
4use endpoint_libs::model::{EndpointSchema, Type};
5use itertools::Itertools;
6use serde::{Deserialize, Serialize};
7use smart_default::SmartDefault;
8use smart_serde_default::smart_serde_default;
9
10/// Marker trait for types that can be used as Definition variants
11/// All types used in Definition must implement this to ensure they are properly validatable
12// pub trait DefinitionVariant: GenElement<Self> {}
13
14#[derive(Serialize, Deserialize)]
15pub enum Definition {
16    EndpointSchema(Box<EndpointSchemaDefinition>),
17    EndpointSchemaList(EndpointSchemaListDefinition),
18    Enum(EnumElement),
19    EnumList(EnumListDefinition),
20    ErrorCodeList(ErrorCodeListDefinition),
21    Struct(StructElement),
22    StructList(StructListDefinition),
23}
24
25impl Definition {
26    pub fn validate_self(&self) -> eyre::Result<()> {
27        match self {
28            Definition::Enum(e) => e.validate_element(),
29            Definition::EnumList(list) => list.validate_element(),
30            Definition::ErrorCodeList(list) => list.validate_element(),
31            Definition::Struct(s) => s.validate_element(),
32            Definition::StructList(list) => list.validate_element(),
33            Definition::EndpointSchema(schema) => schema.validate_element(),
34            Definition::EndpointSchemaList(schemas) => schemas.validate_element(),
35        }
36    }
37}
38
39pub trait GenElement<T: ?Sized>
40where
41    T: GenElement<T>,
42{
43    fn validate_element(&self) -> eyre::Result<()>;
44}
45
46#[derive(Clone, Debug, Serialize, Deserialize, Hash, PartialEq, PartialOrd, Eq, Ord)]
47pub struct ErrorCodeSchema {
48    pub name: String,
49    pub code: i64,
50    #[serde(default)]
51    pub description: String,
52}
53
54impl ErrorCodeSchema {
55    pub fn new(name: impl Into<String>, code: i64, description: impl Into<String>) -> Self {
56        Self {
57            name: name.into(),
58            code,
59            description: description.into(),
60        }
61    }
62}
63
64#[derive(Clone, Debug, Serialize, Deserialize, Hash, PartialEq, PartialOrd, Eq, Ord, DefinitionVariant)]
65pub struct ErrorCodeListDefinition {
66    pub codes: Vec<ErrorCodeSchema>,
67}
68
69impl GenElement<ErrorCodeListDefinition> for ErrorCodeListDefinition {
70    fn validate_element(&self) -> eyre::Result<()> {
71        Ok(())
72    }
73}
74
75/// Wraps the [Type::Enum] variant with extra config
76#[derive(Clone, Debug, Serialize, Deserialize, Hash, PartialEq, PartialOrd, Eq, Ord, DefinitionVariant)]
77pub struct EnumElement {
78    #[serde(default)]
79    pub config: RustGenConfig,
80    pub inner: Type,
81}
82
83impl GenElement<EnumElement> for EnumElement {
84    fn validate_element(&self) -> eyre::Result<()> {
85        match &self.inner {
86            Type::Enum { .. } => Ok(()),
87            _ => eyre::bail!("Expected enum type"),
88        }
89    }
90}
91
92#[derive(Clone, Debug, Serialize, Deserialize, Hash, PartialEq, PartialOrd, Eq, Ord, DefinitionVariant)]
93pub struct EnumListDefinition {
94    #[serde(default)]
95    pub config: RustGenConfig,
96    pub enum_elements: Vec<EnumElement>,
97}
98
99impl GenElement<EnumListDefinition> for EnumListDefinition {
100    fn validate_element(&self) -> eyre::Result<()> {
101        if self.enum_elements.iter().all(|e| matches!(e.inner, Type::Enum { .. })) {
102            Ok(())
103        } else {
104            eyre::bail!("Not all elements of the EnumListDefinition are Enum types")
105        }
106    }
107}
108
109impl ToRust for EnumElement {
110    fn to_rust_ref(&self, _serde_with: bool) -> String {
111        self.validate_element()
112            .unwrap_or_else(|_| panic!("EnumElement is invalid: {self:?}"));
113
114        let name = match &self.inner {
115            Type::Enum { name, .. } => name.to_case(Case::Pascal),
116            _ => unreachable!("The previous validation ensured that this type is a valid Enum"),
117        };
118
119        if self.config.prefix_enum {
120            format!("Enum{name}")
121        } else {
122            name
123        }
124    }
125
126    fn to_rust_decl(&self, serde_with: bool, add_derives: bool) -> String {
127        self.validate_element()
128            .unwrap_or_else(|_| panic!("EnumElement is invalid: {self:?}"));
129
130        let code_regex = regex::Regex::new(r"=\s*(\d+)").expect("Error building regex to extract endpoint code");
131
132        match &self.inner {
133            Type::Enum {
134                name: _,
135                variants: fields,
136            } => {
137                let mut fields = fields
138                    .iter()
139                    .map(|x| {
140                        let variant_name = if x.name.chars().last().unwrap().is_lowercase() {
141                            x.name.to_case(Case::Pascal)
142                        } else {
143                            x.name.clone()
144                        };
145                        // A blank description must omit the `///` line entirely: an
146                        // empty doc comment trips clippy::empty_docs downstream.
147                        if x.description.trim().is_empty() {
148                            format!(
149                                r#"
150    {} = {}
151"#,
152                                variant_name, x.value
153                            )
154                        } else {
155                            format!(
156                                r#"
157    /// {}
158    {} = {}
159"#,
160                                x.description, variant_name, x.value
161                            )
162                        }
163                    })
164                    .sorted_by(|a, b| {
165                        // Sort by the endpoint code
166                        let code_a = {
167                            match code_regex.captures(a) {
168                                Some(code) => code[1].parse::<u64>().unwrap_or_else(|err| {
169                                    eprintln!("Sorting error: {err}: Rust output may not be sorted correctly");
170                                    0
171                                }),
172                                None => {
173                                    eprintln!("Sorting error: Rust output may not be sorted correctly");
174                                    0
175                                }
176                            }
177                        };
178
179                        let code_b = {
180                            match code_regex.captures(b) {
181                                Some(code) => code[1].parse::<u64>().unwrap_or_else(|err| {
182                                    eprintln!("Sorting error: {err}: Rust output may not be sorted correctly");
183                                    0
184                                }),
185                                None => {
186                                    eprintln!("Sorting error: Rust output may not be sorted correctly");
187                                    0
188                                }
189                            }
190                        };
191
192                        code_a.cmp(&code_b)
193                    });
194                let enum_content = format!(r#"pub enum {} {{{}}}"#, self.to_rust_ref(serde_with), fields.join(","));
195
196                if add_derives {
197                    self.add_derives(enum_content)
198                } else {
199                    enum_content
200                }
201            }
202            _ => unreachable!(),
203        }
204    }
205
206    fn add_derives(&self, input: String) -> String {
207        if self.config.worktable_support {
208            format!(
209                r#"#[derive(
210                    MemStat,
211                    Archive,
212                    Clone,
213                    Copy,
214                    Debug,
215                    Display,
216                    PartialEq,
217                    PartialOrd,
218                    Eq,
219                    Hash,
220                    Ord,
221                    EnumString,
222                    rkyv::Deserialize,
223                    rkyv::Serialize,
224                    serde::Serialize,
225                    serde::Deserialize,
226                    {}
227                )]
228                #[rkyv(compare(PartialEq), derive(Debug))]
229                #[repr(u8)]
230                {input}
231            "#,
232                if self.config.json_schema_gen {
233                    "JsonSchema,"
234                } else {
235                    Default::default()
236                }
237            )
238        } else {
239            Type::add_default_enum_derives(input)
240        }
241    }
242}
243#[derive(Clone, Debug, Serialize, Deserialize, Hash, PartialEq, PartialOrd, Eq, Ord, DefinitionVariant)]
244pub struct StructListDefinition {
245    #[serde(default)]
246    pub config: RustGenConfig,
247    pub struct_elements: Vec<StructElement>,
248}
249
250impl GenElement<StructListDefinition> for StructListDefinition {
251    fn validate_element(&self) -> eyre::Result<()> {
252        if self
253            .struct_elements
254            .iter()
255            .all(|s| matches!(s.inner, Type::Struct { .. }))
256        {
257            Ok(())
258        } else {
259            eyre::bail!("Not all elements of the StructListDefinition are Struct types")
260        }
261    }
262}
263
264/// Wraps the [Type::Struct] variant with extra config
265#[derive(Clone, Debug, Serialize, Deserialize, Hash, PartialEq, PartialOrd, Eq, Ord, DefinitionVariant)]
266pub struct StructElement {
267    #[serde(default)]
268    pub config: RustGenConfig,
269    pub inner: Type,
270}
271
272impl GenElement<StructElement> for StructElement {
273    fn validate_element(&self) -> eyre::Result<()> {
274        match &self.inner {
275            Type::Struct { .. } => Ok(()),
276            _ => eyre::bail!("Expected struct type"),
277        }
278    }
279}
280
281impl ToRust for StructElement {
282    fn to_rust_ref(&self, _serde_with: bool) -> String {
283        self.validate_element()
284            .unwrap_or_else(|_| panic!("StructElement is invalid: {self:?}"));
285
286        match &self.inner {
287            Type::Struct { name, .. } => name.clone(),
288            _ => unreachable!("The previous validation ensured that this type is a valid Struct"),
289        }
290    }
291
292    fn to_rust_decl(&self, serde_with: bool, add_derives: bool) -> String {
293        self.validate_element()
294            .unwrap_or_else(|_| panic!("StructElement is invalid: {self:?}"));
295
296        let (name, fields) = match &self.inner {
297            Type::Struct { name, fields } => (name.to_case(Case::Pascal), fields),
298            _ => unreachable!("The previous validation ensured that this type is a valid Struct"),
299        };
300
301        let mut fields = fields.iter().map(|x| {
302            let opt = matches!(&x.ty, Type::Optional(_));
303            let serde_with_opt = match &x.ty {
304                Type::BlockchainDecimal => "rust_decimal::serde::str",
305                Type::BlockchainAddress if serde_with => "WithBlockchainAddress",
306                Type::BlockchainTransactionHash if serde_with => "WithBlockchainTransactionHash",
307                // TODO: handle optional decimals
308                // Type::Optional(t) if matches!(**t, Type::BlockchainDecimal) => {
309                //     "WithBlockchainDecimal"
310                // }
311                // Type::Optional(t) if matches!(**t, Type::BlockchainAddress) => {
312                //     "WithBlockchainAddress"
313                // }
314                // Type::Optional(t) if matches!(**t, Type::BlockchainTransactionHash) => {
315                //     "WithBlockchainTransactionHash"
316                // }
317                _ => "",
318            };
319            format!(
320                "{} {} pub {}: {}",
321                if opt { "#[serde(default)]" } else { "" },
322                if serde_with_opt.is_empty() {
323                    "".to_string()
324                } else {
325                    format!("#[serde(with = \"{serde_with_opt}\")]")
326                },
327                x.name,
328                x.ty.to_rust_ref(serde_with)
329            )
330        });
331        let input = format!("pub struct {} {{{}}}", name, fields.join(","));
332
333        if add_derives { self.add_derives(input) } else { input }
334    }
335
336    fn add_derives(&self, input: String) -> String {
337        if self.config.worktable_support {
338            // format!(
339            //     r#"#[derive(
340            //             Clone,
341            //             Copy,
342            //             Debug,
343            //             Default,
344            //             Eq,
345            //             Hash,
346            //             Ord,
347            //             PartialEq,
348            //             PartialOrd,
349            //             derive_more::Display,
350            //             derive_more::From,
351            //             derive_more::FromStr,
352            //             derive_more::Into,
353            //             MemStat,
354            //             rkyv::Archive,
355            //             SizeMeasure,
356            //             rkyv::Deserialize,
357            //             rkyv::Serialize,
358            //             serde::Serialize,
359            //             serde::Deserialize,
360            //             derive_more::Display,
361            //         )]
362            //         #[rkyv(compare(PartialEq), derive(Debug, PartialOrd, PartialEq, Eq, Ord))]
363            //         {input}
364            //     "#
365
366            // TODO: Fix worktable support for structs
367            format!(
368                r#" #[derive(Serialize, Deserialize, Debug, Clone, {})]
369                #[serde(rename_all = "camelCase")]
370                {input}
371            "#,
372                if self.config.json_schema_gen {
373                    "JsonSchema,"
374                } else {
375                    Default::default()
376                }
377            )
378        } else {
379            Type::add_default_struct_derives(input)
380        }
381    }
382}
383
384#[derive(Clone, Debug, Serialize, Deserialize, Hash, PartialEq, PartialOrd, Eq, Ord, Default)]
385pub struct RustGenConfig {
386    #[serde(default)]
387    pub prefix_enum: bool,
388    #[serde(default)]
389    pub worktable_support: bool,
390    #[serde(default)]
391    pub json_schema_gen: bool,
392    #[serde(default)]
393    pub snake_case_fields: bool,
394    #[serde(default)]
395    pub override_parent: bool,
396}
397
398#[derive(Serialize, Deserialize, Clone)]
399pub struct GenService {
400    pub name: String,
401    pub id: u16,
402    pub endpoints: Vec<EndpointSchemaElement>,
403}
404
405impl GenService {
406    pub fn new(name: String, id: u16, endpoints: Vec<EndpointSchemaElement>) -> Self {
407        Self { name, id, endpoints }
408    }
409}
410
411#[derive(Serialize, Deserialize)]
412pub struct EndpointSchemaDefinition {
413    pub service_name: String,
414    pub service_id: u16,
415    pub schema: EndpointSchemaElement,
416}
417
418#[smart_serde_default]
419#[derive(Serialize, Deserialize, SmartDefault, Clone)]
420pub struct EndpointSchemaElement {
421    #[smart_default(true)]
422    pub frontend_facing: bool,
423    #[serde(default)]
424    pub config: RustGenConfig,
425    pub schema: EndpointSchema,
426}
427
428impl From<EndpointSchemaElement> for EndpointSchema {
429    fn from(val: EndpointSchemaElement) -> Self {
430        // Was a field-by-field copy, which `#[non_exhaustive]` (endpoint-libs 2.0)
431        // now forbids from outside that crate — and which was always a no-op, since
432        // the source field *is* an EndpointSchema. Moving it also means new fields
433        // (e.g. `meta`) are carried through automatically instead of being dropped.
434        val.schema
435    }
436}
437
438impl FromIterator<EndpointSchemaElement> for Vec<EndpointSchema> {
439    fn from_iter<T: IntoIterator<Item = EndpointSchemaElement>>(iter: T) -> Self {
440        iter.into_iter().map(|element| element.schema).collect()
441    }
442}
443
444impl GenElement<EndpointSchemaDefinition> for EndpointSchemaDefinition {
445    fn validate_element(&self) -> eyre::Result<()> {
446        Ok(())
447    }
448}
449
450#[derive(Serialize, Deserialize, DefinitionVariant)]
451pub struct EndpointSchemaListDefinition {
452    pub service_name: String,
453    pub service_id: u16,
454    #[serde(default)]
455    pub config: RustGenConfig,
456    pub endpoints: Vec<EndpointSchemaElement>,
457}
458
459impl GenElement<EndpointSchemaListDefinition> for EndpointSchemaListDefinition {
460    fn validate_element(&self) -> eyre::Result<()> {
461        Ok(())
462    }
463}
464
465#[cfg(test)]
466mod tests {
467    use super::*;
468    use endpoint_libs::model::EnumVariant;
469
470    #[test]
471    fn enum_element_decl_omits_doc_comment_for_blank_descriptions() {
472        let element = EnumElement {
473            config: RustGenConfig::default(),
474            inner: Type::enum_(
475                "sample",
476                vec![
477                    EnumVariant::new_with_description("Documented", "Has docs.", 0),
478                    EnumVariant::new("Bare", 1),
479                    EnumVariant::new_with_description("Blank", "   ", 2),
480                ],
481            ),
482        };
483        let decl = element.to_rust_decl(false, false);
484        assert!(decl.contains("/// Has docs."));
485        assert!(
486            !decl.lines().any(|l| l.trim() == "///"),
487            "empty doc comment emitted (clippy::empty_docs):\n{decl}"
488        );
489        assert!(decl.contains("Bare = 1"));
490        assert!(decl.contains("Blank = 2"));
491    }
492}