Skip to main content

apollo_errors/
metadata.rs

1//! Metadata structures for error catalog
2
3use serde::Serialize;
4
5/// Field name case for extension field names in error output.
6///
7/// Applied to every `#[extension]` field name at render time. When a field uses
8/// `#[extension(rename = "...")]`, the rename value is the input; otherwise the
9/// Rust field name is used.
10///
11/// See [`FormatConfig`] for how to set this per renderer.
12#[derive(Debug, Clone, Copy, PartialEq, Eq)]
13pub enum FieldCase {
14    /// `"config_file"` → `"config_file"` (default, no transformation)
15    SnakeCase,
16    /// `"config_file"` → `"configFile"`
17    CamelCase,
18    /// `"config_file"` → `"ConfigFile"`
19    PascalCase,
20    /// `"config_file"` → `"CONFIG_FILE"`
21    ScreamingSnakeCase,
22    /// `"config_file"` → `"config-file"`
23    KebabCase,
24}
25
26/// Error code case for error codes in error output.
27///
28/// Applied to every error code at render time. `::` separators are treated as
29/// word boundaries for all non-[`Default`](CodeCase::Default) variants.
30///
31/// See [`FormatConfig`] for how to set this per renderer.
32#[derive(Debug, Clone, Copy, PartialEq, Eq)]
33pub enum CodeCase {
34    /// `"config::invalid_port"` → `"config::invalid_port"` (no transformation)
35    Default,
36    /// `"config::invalid_port"` → `"CONFIG_INVALID_PORT"`
37    ScreamingSnakeCase,
38    /// `"config::invalid_port"` → `"configInvalidPort"`
39    CamelCase,
40    /// `"config::invalid_port"` → `"ConfigInvalidPort"`
41    PascalCase,
42    /// `"config::invalid_port"` → `"config-invalid-port"`
43    KebabCase,
44}
45
46/// Configuration for error output formatting
47#[derive(Debug, Clone, Copy, PartialEq, Eq)]
48pub struct FormatConfig {
49    pub field_case: FieldCase,
50    pub code_case: CodeCase,
51}
52
53impl Default for FormatConfig {
54    fn default() -> Self {
55        Self {
56            field_case: FieldCase::SnakeCase,
57            code_case: CodeCase::Default,
58        }
59    }
60}
61
62/// Metadata for a field within an error variant
63#[derive(Debug, Clone, Serialize)]
64#[serde(rename_all = "camelCase")]
65pub struct FieldMetadata {
66    /// The field name in Rust (snake_case)
67    pub rust_name: &'static str,
68
69    /// The output name for serialization — explicit rename if set, else equals `rust_name`
70    pub output_name: &'static str,
71
72    /// Pre-computed snake_case variant, e.g. `"config_file"`
73    pub snake_case: &'static str,
74
75    /// Pre-computed camelCase variant, e.g. `"configFile"`
76    pub camel_case: &'static str,
77
78    /// Pre-computed PascalCase variant, e.g. `"ConfigFile"`
79    pub pascal_case: &'static str,
80
81    /// Pre-computed SCREAMING_SNAKE_CASE variant, e.g. `"CONFIG_FILE"`
82    pub screaming_snake_case: &'static str,
83
84    /// Pre-computed kebab-case variant, e.g. `"config-file"`
85    pub kebab_case: &'static str,
86
87    /// The Rust type as a string
88    pub ty: &'static str,
89
90    /// Whether this field is an extension field
91    pub is_extension: bool,
92
93    /// HTTP header name if this field should be returned as a header
94    #[serde(skip_serializing_if = "Option::is_none")]
95    pub http_header: Option<&'static str>,
96}
97
98impl FieldMetadata {
99    /// Return the field name in the requested case.
100    pub fn name_for(&self, case: FieldCase) -> &'static str {
101        match case {
102            FieldCase::SnakeCase => self.snake_case,
103            FieldCase::CamelCase => self.camel_case,
104            FieldCase::PascalCase => self.pascal_case,
105            FieldCase::ScreamingSnakeCase => self.screaming_snake_case,
106            FieldCase::KebabCase => self.kebab_case,
107        }
108    }
109}
110
111/// All pre-computed case variants for an error code.
112#[derive(Debug, Clone, Serialize)]
113#[serde(rename_all = "camelCase")]
114pub struct CodeMetadata {
115    /// Original form, e.g. `"config::invalid_port"`
116    pub default: &'static str,
117    /// `"CONFIG_INVALID_PORT"`
118    pub screaming_snake: &'static str,
119    /// `"configInvalidPort"`
120    pub camel: &'static str,
121    /// `"ConfigInvalidPort"`
122    pub pascal: &'static str,
123    /// `"config-invalid-port"`
124    pub kebab: &'static str,
125}
126
127/// Metadata for a regular error variant with its own message, code, and status
128#[derive(Debug, Clone, Serialize)]
129#[serde(rename_all = "camelCase")]
130pub struct RegularVariantMetadata {
131    /// The variant name
132    pub name: &'static str,
133
134    /// Error message template
135    pub message: &'static str,
136
137    /// Error code in its original form, e.g. `"config::invalid_port"`
138    pub code: &'static str,
139
140    /// Pre-computed `CONFIG_INVALID_PORT` form
141    pub code_screaming_snake: &'static str,
142
143    /// Pre-computed `configInvalidPort` form
144    pub code_camel: &'static str,
145
146    /// Pre-computed `ConfigInvalidPort` form
147    pub code_pascal: &'static str,
148
149    /// Pre-computed `config-invalid-port` form
150    pub code_kebab: &'static str,
151
152    /// HTTP status code
153    pub http_status: u16,
154
155    /// JSON-RPC error code
156    pub jsonrpc_code: i32,
157
158    /// Optional help text
159    #[serde(skip_serializing_if = "Option::is_none")]
160    pub help: Option<&'static str>,
161
162    /// Optional documentation URL
163    #[serde(skip_serializing_if = "Option::is_none")]
164    pub url: Option<&'static str>,
165
166    /// Optional severity
167    #[serde(skip_serializing_if = "Option::is_none")]
168    pub severity: Option<&'static str>,
169
170    /// Fields in this variant
171    pub fields: &'static [FieldMetadata],
172}
173
174impl RegularVariantMetadata {
175    /// Return the error code in the requested case.
176    pub fn code_for(&self, case: CodeCase) -> &'static str {
177        match case {
178            CodeCase::Default => self.code,
179            CodeCase::ScreamingSnakeCase => self.code_screaming_snake,
180            CodeCase::CamelCase => self.code_camel,
181            CodeCase::PascalCase => self.code_pascal,
182            CodeCase::KebabCase => self.code_kebab,
183        }
184    }
185}
186
187/// Metadata for a transparent variant that forwards to another error type
188#[derive(Debug, Clone, Serialize)]
189#[serde(rename_all = "camelCase")]
190pub struct TransparentVariantMetadata {
191    /// The variant name
192    pub name: &'static str,
193
194    /// The type name this variant forwards to
195    pub forward_to: &'static str,
196}
197
198/// Metadata for a single error variant
199#[derive(Debug, Clone, Serialize)]
200#[serde(untagged)]
201pub enum VariantMetadata {
202    /// A regular error variant
203    Regular(RegularVariantMetadata),
204
205    /// A transparent variant that forwards to another error type
206    Transparent(TransparentVariantMetadata),
207}
208
209/// Metadata for an error enum type
210#[derive(Debug, Clone, Serialize)]
211#[serde(rename_all = "camelCase")]
212pub struct ErrorMetadata {
213    /// The Rust type name
214    pub type_name: &'static str,
215
216    /// All variants of this error type
217    pub variants: &'static [VariantMetadata],
218}
219
220#[cfg(test)]
221mod tests {
222    use super::{
223        CodeCase, ErrorMetadata, FieldCase, FieldMetadata, RegularVariantMetadata, VariantMetadata,
224    };
225
226    #[test]
227    fn test_serialize_error_metadata() {
228        static FIELDS: &[FieldMetadata] = &[];
229        static VARIANTS: &[VariantMetadata] = &[VariantMetadata::Regular(RegularVariantMetadata {
230            name: "TestVariant",
231            message: "test message",
232            code: "test::code",
233            code_screaming_snake: "TEST_CODE",
234            code_camel: "testCode",
235            code_pascal: "TestCode",
236            code_kebab: "test-code",
237            http_status: 200,
238            jsonrpc_code: -32000,
239            help: None,
240            url: None,
241            severity: None,
242            fields: FIELDS,
243        })];
244
245        let metadata = ErrorMetadata {
246            type_name: "TestError",
247            variants: VARIANTS,
248        };
249
250        let json = serde_json::to_value(&metadata).unwrap();
251        assert_eq!(json["typeName"], "TestError");
252        let variants_json = json["variants"].as_array().unwrap();
253        assert_eq!(variants_json.len(), 1);
254        assert_eq!(variants_json[0]["name"], "TestVariant");
255        assert_eq!(variants_json[0]["code"], "test::code");
256    }
257
258    #[test]
259    fn test_field_metadata_name_for() {
260        let field_metadata = FieldMetadata {
261            rust_name: "my_field",
262            output_name: "my_field",
263            snake_case: "my_field",
264            camel_case: "myField",
265            pascal_case: "MyField",
266            screaming_snake_case: "MY_FIELD",
267            kebab_case: "my-field",
268            ty: "String",
269            is_extension: false,
270            http_header: None,
271        };
272        assert_eq!(field_metadata.name_for(FieldCase::SnakeCase), "my_field");
273        assert_eq!(field_metadata.name_for(FieldCase::CamelCase), "myField");
274        assert_eq!(field_metadata.name_for(FieldCase::PascalCase), "MyField");
275        assert_eq!(
276            field_metadata.name_for(FieldCase::ScreamingSnakeCase),
277            "MY_FIELD"
278        );
279        assert_eq!(field_metadata.name_for(FieldCase::KebabCase), "my-field");
280    }
281
282    #[test]
283    fn test_field_metadata_name_for_with_rename() {
284        let field = FieldMetadata {
285            rust_name: "my_field",
286            output_name: "myRenamedField",
287            snake_case: "my_renamed_field",
288            camel_case: "myRenamedField",
289            pascal_case: "MyRenamedField",
290            screaming_snake_case: "MY_RENAMED_FIELD",
291            kebab_case: "my-renamed-field",
292            ty: "String",
293            is_extension: true,
294            http_header: None,
295        };
296        assert_eq!(field.name_for(FieldCase::SnakeCase), "my_renamed_field");
297        assert_eq!(field.name_for(FieldCase::CamelCase), "myRenamedField");
298        assert_eq!(field.name_for(FieldCase::PascalCase), "MyRenamedField");
299        assert_eq!(
300            field.name_for(FieldCase::ScreamingSnakeCase),
301            "MY_RENAMED_FIELD"
302        );
303        assert_eq!(field.name_for(FieldCase::KebabCase), "my-renamed-field");
304    }
305
306    #[test]
307    fn test_regular_variant_metadata_code_for() {
308        let regular_variant_metadata = RegularVariantMetadata {
309            name: "MyError",
310            message: "My error message",
311            code: "my_error",
312            code_screaming_snake: "MY_ERROR",
313            code_camel: "myError",
314            code_pascal: "MyError",
315            code_kebab: "my-error",
316            http_status: 500,
317            jsonrpc_code: 1000,
318            help: None,
319            url: None,
320            severity: None,
321            fields: &[],
322        };
323        assert_eq!(
324            regular_variant_metadata.code_for(CodeCase::Default),
325            "my_error"
326        );
327        assert_eq!(
328            regular_variant_metadata.code_for(CodeCase::ScreamingSnakeCase),
329            "MY_ERROR"
330        );
331        assert_eq!(
332            regular_variant_metadata.code_for(CodeCase::CamelCase),
333            "myError"
334        );
335        assert_eq!(
336            regular_variant_metadata.code_for(CodeCase::PascalCase),
337            "MyError"
338        );
339        assert_eq!(
340            regular_variant_metadata.code_for(CodeCase::KebabCase),
341            "my-error"
342        );
343    }
344}