@@ -158,29 +158,57 @@ fn derive_query_response_impl(input: DeriveInput) -> Result<TokenStream2, syn::E
158158
159159/// A segment in a field path.
160160///
161- /// Paths like `"data.nodes[].name"` are parsed into segments:
162- /// - `PathSegment { field: "data", is_array: false }`
163- /// - `PathSegment { field: "nodes", is_array: true }`
164- /// - `PathSegment { field: "name", is_array: false }`
161+ /// Segments can include an alias using `@` syntax for GraphQL aliases:
162+ /// - The field name (before `@`) is used for schema validation
163+ /// - The alias (after `@`) is used for JSON extraction
164+ ///
165+ /// Examples:
166+ /// - `"data.nodes[].name"` parses to:
167+ /// - `{ field: "data", alias: None, is_array: false }`
168+ /// - `{ field: "nodes", alias: None, is_array: true }`
169+ /// - `{ field: "name", alias: None, is_array: false }`
170+ ///
171+ /// - `"[email protected] [].sequenceNumber"` parses to: 172+ /// - `{ field: "epoch", alias: None, is_array: false }`
173+ /// - `{ field: "checkpoints", alias: Some("firstCheckpoints"), is_array: false }`
174+ /// - `{ field: "nodes", alias: None, is_array: true }`
175+ /// - `{ field: "sequenceNumber", alias: None, is_array: false }`
165176struct PathSegment < ' a > {
166- /// The field name to access
177+ /// The field name (used for schema validation)
167178 field : & ' a str ,
179+ /// Optional alias (used for JSON extraction instead of field name)
180+ alias : Option < & ' a str > ,
168181 /// Whether this is an array field (ends with `[]`)
169182 is_array : bool ,
170183}
171184
172185/// Parse a path string into segments.
173186///
174- /// Each dot-separated part becomes a `PathSegment`. If it ends with `[]`, it's an array field.
187+ /// Each dot-separated part is parsed for:
188+ /// - Array suffix `[]` (e.g., `nodes[]`)
189+ /// - Alias syntax `@` (e.g., `checkpoints@firstCheckpoints`)
175190fn parse_path ( path : & str ) -> Vec < PathSegment < ' _ > > {
176191 path. split ( '.' )
177192 . map ( |segment| {
178- let ( field, is_array) = if let Some ( stripped) = segment. strip_suffix ( "[]" ) {
193+ // Check for array suffix first
194+ let ( segment, is_array) = if let Some ( stripped) = segment. strip_suffix ( "[]" ) {
179195 ( stripped, true )
180196 } else {
181197 ( segment, false )
182198 } ;
183- PathSegment { field, is_array }
199+
200+ // Check for alias syntax: field@alias
201+ let ( field, alias) = if let Some ( at_pos) = segment. find ( '@' ) {
202+ ( & segment[ ..at_pos] , Some ( & segment[ at_pos + 1 ..] ) )
203+ } else {
204+ ( segment, None )
205+ } ;
206+
207+ PathSegment {
208+ field,
209+ alias,
210+ is_array,
211+ }
184212 } )
185213 . collect ( )
186214}
@@ -205,6 +233,7 @@ fn generate_field_extraction(path: &str, field_ident: &syn::Ident) -> TokenStrea
205233
206234/// Recursively generate extraction code by traversing path segments.
207235///
236+ /// For JSON extraction, uses the alias if present, otherwise uses the field name.
208237/// Returns code that evaluates to `Result<T, String>` (caller adds `?` to unwrap).
209238///
210239/// ## Example: Simple path `"object.address"`
@@ -242,20 +271,23 @@ fn generate_from_segments(full_path: &str, segments: &[PathSegment<'_>]) -> Toke
242271 } ;
243272 }
244273
245- let segment = & segments[ 0 ] ;
246- let name = segment. field ;
247274 let rest = generate_from_segments ( full_path, & segments[ 1 ..] ) ;
275+ let segment = & segments[ 0 ] ;
276+
277+ // Use alias for JSON extraction if present, otherwise use field name
278+ let json_key = segment. alias . unwrap_or ( segment. field ) ;
248279
249280 if segment. is_array {
281+ // Array field: check for null, then iterate and collect into Option<Vec>
250282 quote ! {
251283 {
252- let field_value = current. get( #name )
253- . ok_or_else( || format!( "missing field '{}' in path '{}'" , #name , #full_path) ) ?;
284+ let field_value = current. get( #json_key )
285+ . ok_or_else( || format!( "missing field '{}' in path '{}'" , #json_key , #full_path) ) ?;
254286 if field_value. is_null( ) {
255287 Ok ( None )
256288 } else {
257289 let array = field_value. as_array( )
258- . ok_or_else( || format!( "expected array at '{}' in path '{}'" , #name , #full_path) ) ?;
290+ . ok_or_else( || format!( "expected array at '{}' in path '{}'" , #json_key , #full_path) ) ?;
259291 array. iter( )
260292 . map( |current| { #rest } )
261293 . collect:: <Result <Vec <_>, String >>( )
@@ -265,17 +297,15 @@ fn generate_from_segments(full_path: &str, segments: &[PathSegment<'_>]) -> Toke
265297 }
266298 } else {
267299 quote ! {
268- {
269- let current = current. get( #name)
270- . ok_or_else( || format!( "missing field '{}' in path '{}'" , #name, #full_path) ) ?;
271- // If null, skip remaining navigation and let serde handle it
272- // (returns Ok(None) for Option<T>, error for non-Option)
273- if current. is_null( ) {
274- serde_json:: from_value( current. clone( ) )
275- . map_err( |e| format!( "failed to deserialize '{}': {}" , #full_path, e) )
276- } else {
277- #rest
278- }
300+ let current = current. get( #json_key)
301+ . ok_or_else( || format!( "missing field '{}' in path '{}'" , #json_key, #full_path) ) ?;
302+ // If null, skip remaining navigation and let serde handle it
303+ // (returns Ok(None) for Option<T>, error for non-Option)
304+ if current. is_null( ) {
305+ serde_json:: from_value( current. clone( ) )
306+ . map_err( |e| format!( "failed to deserialize '{}': {}" , #full_path, e) )
307+ } else {
308+ #rest
279309 }
280310 }
281311 }
0 commit comments