unc_rpc_error_core/
lib.rs

1use std::collections::BTreeMap;
2use syn::{Data, DataEnum, DataStruct, DeriveInput, Fields, FieldsNamed, FieldsUnnamed};
3
4#[derive(Default, Debug, serde::Deserialize, serde::Serialize)]
5pub struct ErrorType {
6    /// A type name of the error
7    pub name: String,
8    /// Names of subtypes of the error
9    pub subtypes: Vec<String>,
10    /// An error input name and a type
11    pub props: BTreeMap<String, String>,
12}
13
14fn parse_rpc_error_variant(input: &DeriveInput) -> String {
15    let type_name = input.ident.to_string();
16    let type_kind: Vec<&str> = type_name.split("Kind").collect();
17    type_kind[0].to_string()
18}
19
20fn error_type_name(schema: &mut BTreeMap<String, ErrorType>, name: String) -> &mut ErrorType {
21    let error_type = ErrorType { name: name.clone(), ..Default::default() };
22    schema.entry(name).or_insert(error_type)
23}
24
25pub fn parse_error_type(schema: &mut BTreeMap<String, ErrorType>, input: &DeriveInput) {
26    let name = parse_rpc_error_variant(input);
27    match &input.data {
28        Data::Enum(DataEnum { ref variants, .. }) => {
29            // TODO: check for uniqueness
30            let error_type = error_type_name(schema, name);
31            let mut direct_error_types = vec![];
32            for variant in variants {
33                error_type.subtypes.push(variant.ident.to_string());
34                match &variant.fields {
35                    Fields::Unnamed(FieldsUnnamed { ref unnamed, .. }) => {
36                        // Subtype
37                        if unnamed.iter().count() > 1 {
38                            panic!(
39                                "Error types doesn't support tuple variants with multiple fields"
40                            );
41                        }
42                    }
43                    Fields::Named(FieldsNamed { ref named, .. }) => {
44                        // If variant is Enum with a named fields - create a new type for each variant with named props
45                        let mut error_type =
46                            ErrorType { name: variant.ident.to_string(), ..Default::default() };
47                        for field in named {
48                            error_type.props.insert(
49                                field
50                                    .ident
51                                    .as_ref()
52                                    .expect("named fields must have ident")
53                                    .to_string(),
54                                "".to_owned(),
55                            );
56                        }
57                        direct_error_types.push(error_type);
58                    }
59                    Fields::Unit => {
60                        direct_error_types.push(ErrorType {
61                            name: variant.ident.to_string(),
62                            ..Default::default()
63                        });
64                    }
65                }
66            }
67            for e in direct_error_types {
68                let error_type = error_type_name(schema, e.name.clone());
69                error_type.name = e.name;
70                error_type.props = e.props;
71            }
72        }
73        Data::Struct(DataStruct { ref fields, .. }) => {
74            let error_type = error_type_name(schema, name);
75            match fields {
76                Fields::Named(FieldsNamed { ref named, .. }) => {
77                    for field in named {
78                        let field_name =
79                            field.ident.as_ref().expect("named fields must have ident").to_string();
80                        if field_name == "kind" {
81                            continue;
82                        }
83                        error_type.props.insert(field_name, "".to_owned()); // TODO: add prop type
84                    }
85                }
86                _ => {
87                    panic!("RpcError supports structs with the named fields only");
88                }
89            }
90        }
91        Data::Union(_) => {
92            panic!("Unions are not supported");
93        }
94    }
95}
96
97#[cfg(test)]
98mod tests {
99    use super::*;
100    use quote::quote;
101    #[test]
102    fn should_merge_kind() {
103        let mut schema = BTreeMap::default();
104        let error_type = syn::parse2(quote! {
105            pub struct ActionError {
106                pub index: Option<u64>,
107                pub kind: ActionErrorKind,
108            }
109        })
110        .unwrap();
111        parse_error_type(&mut schema, &error_type);
112        let expected: BTreeMap<String, ErrorType> = serde_json::from_str(
113            r#"
114        {
115            "ActionError": {
116                "name": "ActionError",
117                "subtypes": [],
118                "props": {
119                "index": ""
120                }
121            }
122          }
123        "#,
124        )
125        .unwrap();
126        assert_eq!(
127            serde_json::to_string(&expected).unwrap(),
128            serde_json::to_string(&schema).unwrap()
129        );
130        let error_type_kind: DeriveInput = syn::parse2(quote! {
131            pub enum ActionErrorKind {
132                AccountAlreadyExists { account_id: String },
133            }
134        })
135        .unwrap();
136        let expected: BTreeMap<String, ErrorType> = serde_json::from_str(
137            r#"
138        {
139            "ActionError": {
140                "name": "ActionError",
141                "subtypes": ["AccountAlreadyExists"],
142                "props": {
143                    "index": ""
144                }
145            },
146            "AccountAlreadyExists": {
147                "name": "AccountAlreadyExists",
148                "subtypes": [],
149                "props": {
150                    "account_id": ""
151                }
152            }
153          }
154        "#,
155        )
156        .unwrap();
157        parse_error_type(&mut schema, &error_type_kind);
158        assert_eq!(
159            serde_json::to_string(&expected).unwrap(),
160            serde_json::to_string(&schema).unwrap()
161        );
162    }
163
164    #[test]
165    fn complex() {
166        let mut schema = BTreeMap::default();
167        parse_error_type(
168            &mut schema,
169            &syn::parse2(quote! {
170                pub enum TxExecutionError {
171                    ActionError(ActionError),
172                    InvalidTxError(InvalidTxError),
173                }
174            })
175            .unwrap(),
176        );
177        parse_error_type(
178            &mut schema,
179            &syn::parse2(quote! {
180                pub enum InvalidTxError {
181                    InvalidAccessKeyError(InvalidAccessKeyError),
182                    InvalidSignerId { signer_id: AccountId },
183                }
184            })
185            .unwrap(),
186        );
187        parse_error_type(
188            &mut schema,
189            &syn::parse2(quote! {
190                pub enum InvalidAccessKeyError {
191                    /// The access key identified by the `public_key` doesn't exist for the account
192                    AccessKeyNotFound { account_id: AccountId, public_key: PublicKey },
193                }
194            })
195            .unwrap(),
196        );
197        parse_error_type(
198            &mut schema,
199            &syn::parse2(quote! {
200                pub struct ActionError {
201                    pub index: Option<u64>,
202                    pub kind: ActionErrorKind,
203                }
204            })
205            .unwrap(),
206        );
207        parse_error_type(
208            &mut schema,
209            &syn::parse2(quote! {
210                pub enum ActionErrorKind {
211                    AccountAlreadyExists { account_id: String },
212                }
213            })
214            .unwrap(),
215        );
216        let expected: BTreeMap<String, ErrorType> = serde_json::from_str(
217            r#"
218            {
219                "AccessKeyNotFound": {
220                  "name": "AccessKeyNotFound",
221                  "subtypes": [],
222                  "props": {
223                    "account_id": "",
224                    "public_key": ""
225                  }
226                },
227                "AccountAlreadyExists": {
228                  "name": "AccountAlreadyExists",
229                  "subtypes": [],
230                  "props": {
231                    "account_id": ""
232                  }
233                },
234                "ActionError": {
235                  "name": "ActionError",
236                  "subtypes": [
237                    "AccountAlreadyExists"
238                  ],
239                  "props": {
240                    "index": ""
241                  }
242                },
243                "InvalidAccessKeyError": {
244                  "name": "InvalidAccessKeyError",
245                  "subtypes": [
246                    "AccessKeyNotFound"
247                  ],
248                  "props": {}
249                },
250                "InvalidSignerId": {
251                  "name": "InvalidSignerId",
252                  "subtypes": [],
253                  "props": {
254                    "signer_id": ""
255                  }
256                },
257                "InvalidTxError": {
258                  "name": "InvalidTxError",
259                  "subtypes": [
260                    "InvalidAccessKeyError",
261                    "InvalidSignerId"
262                  ],
263                  "props": {}
264                },
265                "TxExecutionError": {
266                  "name": "TxExecutionError",
267                  "subtypes": [
268                    "ActionError",
269                    "InvalidTxError"
270                  ],
271                  "props": {}
272                }
273              }"#,
274        )
275        .unwrap();
276        assert_eq!(
277            serde_json::to_string(&expected).unwrap(),
278            serde_json::to_string(&schema).unwrap()
279        );
280    }
281    #[test]
282    #[should_panic]
283    fn should_not_accept_tuples() {
284        let mut schema = BTreeMap::default();
285        parse_error_type(
286            &mut schema,
287            &syn::parse2(quote! {
288                pub enum ErrorWithATupleVariant {
289                    Var(One, Two)
290                }
291            })
292            .unwrap(),
293        );
294    }
295}