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#[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#[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 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 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#[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 _ => "",
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!(
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 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}