@@ -51,6 +51,46 @@ struct ConfigDocs {
51
51
referenced_constants : HashMap < String , Option < String > > , // Name -> Resolved Value (or None)
52
52
}
53
53
54
+ // JSON navigation helper functions
55
+ /// Navigate through nested JSON structure using an array of keys
56
+ /// Returns None if any part of the path doesn't exist
57
+ ///
58
+ /// Example: get_json_path(value, &["inner", "struct", "kind"])
59
+ /// is equivalent to value.get("inner")?.get("struct")?.get("kind")
60
+ fn get_json_path < ' a > ( value : & ' a serde_json:: Value , path : & [ & str ] ) -> Option < & ' a serde_json:: Value > {
61
+ let mut current = value;
62
+
63
+ for & key in path {
64
+ current = current. get ( key) ?;
65
+ }
66
+
67
+ Some ( current)
68
+ }
69
+
70
+ /// Navigate to an array at the given JSON path
71
+ /// Returns None if the path doesn't exist or the value is not an array
72
+ fn get_json_array < ' a > (
73
+ value : & ' a serde_json:: Value ,
74
+ path : & [ & str ] ,
75
+ ) -> Option < & ' a Vec < serde_json:: Value > > {
76
+ get_json_path ( value, path) ?. as_array ( )
77
+ }
78
+
79
+ /// Navigate to an object at the given JSON path
80
+ /// Returns None if the path doesn't exist or the value is not an object
81
+ fn get_json_object < ' a > (
82
+ value : & ' a serde_json:: Value ,
83
+ path : & [ & str ] ,
84
+ ) -> Option < & ' a serde_json:: Map < String , serde_json:: Value > > {
85
+ get_json_path ( value, path) ?. as_object ( )
86
+ }
87
+
88
+ /// Navigate to a string at the given JSON path
89
+ /// Returns None if the path doesn't exist or the value is not a string
90
+ fn get_json_string < ' a > ( value : & ' a serde_json:: Value , path : & [ & str ] ) -> Option < & ' a str > {
91
+ get_json_path ( value, path) ?. as_str ( )
92
+ }
93
+
54
94
fn main ( ) -> Result < ( ) > {
55
95
let matches = ClapCommand :: new ( "extract-docs" )
56
96
. about ( "Extract documentation from Rust source code using rustdoc JSON" )
@@ -200,33 +240,28 @@ fn extract_config_docs_from_rustdoc(
200
240
let mut all_referenced_constants = std:: collections:: HashSet :: new ( ) ;
201
241
202
242
// Access the main index containing all items from the rustdoc JSON output
203
- let index = rustdoc_json
204
- . get ( "index" )
205
- . and_then ( |v| v. as_object ( ) )
243
+ let index = get_json_object ( rustdoc_json, & [ "index" ] )
206
244
. context ( "Missing 'index' field in rustdoc JSON" ) ?;
207
245
208
246
for ( _item_id, item) in index {
209
247
// Extract the item's name from rustdoc JSON structure
210
- if let Some ( name) = item. get ( "name" ) . and_then ( |v| v. as_str ( ) ) {
211
- // Navigate to the item's type information
212
- if let Some ( inner) = item. get ( "inner" ) {
213
- // Check if this item is a struct by looking for the "struct" field
214
- if let Some ( _struct_data) = inner. get ( "struct" ) {
215
- // Check if this struct is in our target list (if specified)
216
- if let Some ( targets) = target_structs {
217
- if !targets. contains ( & name. to_string ( ) ) {
218
- continue ;
219
- }
248
+ if let Some ( name) = get_json_string ( item, & [ "name" ] ) {
249
+ // Check if this item is a struct by looking for the "struct" field
250
+ if get_json_object ( item, & [ "inner" , "struct" ] ) . is_some ( ) {
251
+ // Check if this struct is in our target list (if specified)
252
+ if let Some ( targets) = target_structs {
253
+ if !targets. contains ( & name. to_string ( ) ) {
254
+ continue ;
220
255
}
256
+ }
221
257
222
- let ( struct_doc_opt, referenced_constants) =
223
- extract_struct_from_rustdoc_index ( index, name, item) ?;
258
+ let ( struct_doc_opt, referenced_constants) =
259
+ extract_struct_from_rustdoc_index ( index, name, item) ?;
224
260
225
- if let Some ( struct_doc) = struct_doc_opt {
226
- structs. push ( struct_doc) ;
227
- }
228
- all_referenced_constants. extend ( referenced_constants) ;
261
+ if let Some ( struct_doc) = struct_doc_opt {
262
+ structs. push ( struct_doc) ;
229
263
}
264
+ all_referenced_constants. extend ( referenced_constants) ;
230
265
}
231
266
}
232
267
}
@@ -252,10 +287,7 @@ fn extract_struct_from_rustdoc_index(
252
287
let mut all_referenced_constants = std:: collections:: HashSet :: new ( ) ;
253
288
254
289
// Extract struct documentation
255
- let description = struct_item
256
- . get ( "docs" )
257
- . and_then ( |v| v. as_str ( ) )
258
- . map ( |s| s. to_string ( ) ) ;
290
+ let description = get_json_string ( struct_item, & [ "docs" ] ) . map ( |s| s. to_string ( ) ) ;
259
291
260
292
// Collect constant references from struct description
261
293
if let Some ( desc) = & description {
@@ -289,56 +321,43 @@ fn extract_struct_fields(
289
321
290
322
// Navigate through rustdoc JSON structure to access struct fields
291
323
// Path: item.inner.struct.kind.plain.fields[]
292
- if let Some ( inner) = struct_item. get ( "inner" ) {
293
- if let Some ( struct_data) = inner. get ( "struct" ) {
294
- if let Some ( kind) = struct_data. get ( "kind" ) {
295
- if let Some ( plain) = kind. get ( "plain" ) {
296
- // Access the array of field IDs that reference other items in the index
297
- if let Some ( field_ids) = plain. get ( "fields" ) . and_then ( |v| v. as_array ( ) ) {
298
- for field_id in field_ids {
299
- // Field IDs can be either integers or strings in rustdoc JSON, try both formats
300
- let field_item = if let Some ( field_id_num) = field_id. as_u64 ( ) {
301
- // Numeric field ID - convert to string for index lookup
302
- index. get ( & field_id_num. to_string ( ) )
303
- } else if let Some ( field_id_str) = field_id. as_str ( ) {
304
- // String field ID - use directly for index lookup
305
- index. get ( field_id_str)
306
- } else {
307
- None
308
- } ;
309
-
310
- if let Some ( field_item) = field_item {
311
- // Extract the field's name from the rustdoc item
312
- let field_name = field_item
313
- . get ( "name" )
314
- . and_then ( |v| v. as_str ( ) )
315
- . unwrap_or ( "unknown" )
316
- . to_string ( ) ;
317
-
318
- // Extract the field's documentation text from rustdoc
319
- let field_docs = field_item
320
- . get ( "docs" )
321
- . and_then ( |v| v. as_str ( ) )
322
- . unwrap_or ( "" )
323
- . to_string ( ) ;
324
-
325
- // Parse the structured documentation
326
- let ( field_doc, referenced_constants) =
327
- parse_field_documentation ( & field_docs, & field_name) ?;
328
-
329
- // Only include fields that have documentation
330
- if !field_doc. description . is_empty ( )
331
- || field_doc. default_value . is_some ( )
332
- {
333
- fields. push ( field_doc) ;
334
- }
324
+ if let Some ( field_ids) =
325
+ get_json_array ( struct_item, & [ "inner" , "struct" , "kind" , "plain" , "fields" ] )
326
+ {
327
+ for field_id in field_ids {
328
+ // Field IDs can be either integers or strings in rustdoc JSON, try both formats
329
+ let field_item = if let Some ( field_id_num) = field_id. as_u64 ( ) {
330
+ // Numeric field ID - convert to string for index lookup
331
+ index. get ( & field_id_num. to_string ( ) )
332
+ } else if let Some ( field_id_str) = field_id. as_str ( ) {
333
+ // String field ID - use directly for index lookup
334
+ index. get ( field_id_str)
335
+ } else {
336
+ None
337
+ } ;
335
338
336
- // Extend referenced constants
337
- all_referenced_constants. extend ( referenced_constants) ;
338
- }
339
- }
340
- }
339
+ if let Some ( field_item) = field_item {
340
+ // Extract the field's name from the rustdoc item
341
+ let field_name = get_json_string ( field_item, & [ "name" ] )
342
+ . unwrap_or ( "unknown" )
343
+ . to_string ( ) ;
344
+
345
+ // Extract the field's documentation text from rustdoc
346
+ let field_docs = get_json_string ( field_item, & [ "docs" ] )
347
+ . unwrap_or ( "" )
348
+ . to_string ( ) ;
349
+
350
+ // Parse the structured documentation
351
+ let ( field_doc, referenced_constants) =
352
+ parse_field_documentation ( & field_docs, & field_name) ?;
353
+
354
+ // Only include fields that have documentation
355
+ if !field_doc. description . is_empty ( ) || field_doc. default_value . is_some ( ) {
356
+ fields. push ( field_doc) ;
341
357
}
358
+
359
+ // Extend referenced constants
360
+ all_referenced_constants. extend ( referenced_constants) ;
342
361
}
343
362
}
344
363
}
@@ -808,7 +827,7 @@ fn resolve_constant_reference(
808
827
let json_file_path = format ! ( "target/rustdoc-json/doc/{}.json" , lib_name) ;
809
828
if let Ok ( json_content) = std:: fs:: read_to_string ( & json_file_path) {
810
829
if let Ok ( rustdoc_json) = serde_json:: from_str :: < serde_json:: Value > ( & json_content) {
811
- if let Some ( index) = rustdoc_json . get ( "index" ) . and_then ( |v| v . as_object ( ) ) {
830
+ if let Some ( index) = get_json_object ( & rustdoc_json , & [ "index" ] ) {
812
831
if let Some ( value) = resolve_constant_in_index ( name, index) {
813
832
return Some ( value) ;
814
833
}
@@ -827,61 +846,60 @@ fn resolve_constant_in_index(
827
846
// Look for a constant with the given name in the rustdoc index
828
847
for ( _item_id, item) in rustdoc_index {
829
848
// Check if this item's name matches the constant we're looking for
830
- if let Some ( item_name) = item . get ( "name" ) . and_then ( |v| v . as_str ( ) ) {
849
+ if let Some ( item_name) = get_json_string ( item , & [ "name" ] ) {
831
850
if item_name == name {
832
- // Navigate to the item's type information in rustdoc JSON
833
- if let Some ( inner) = item. get ( "inner" ) {
834
- // Check if this item is a constant by looking for the "constant" field
835
- if let Some ( constant_data) = inner. get ( "constant" ) {
836
- // Try newer rustdoc JSON structure first (with nested 'const' field)
837
- if let Some ( const_inner) = constant_data. get ( "const" ) {
838
- // For literal constants, prefer expr which doesn't have type suffix
839
- if let Some ( is_literal) =
840
- const_inner. get ( "is_literal" ) . and_then ( |v| v. as_bool ( ) )
851
+ // Check if this item is a constant by looking for the "constant" field
852
+ if let Some ( constant_data) = get_json_object ( item, & [ "inner" , "constant" ] ) {
853
+ // Try newer rustdoc JSON structure first (with nested 'const' field)
854
+ let constant_data_value = serde_json:: Value :: Object ( constant_data. clone ( ) ) ;
855
+ if get_json_object ( & constant_data_value, & [ "const" ] ) . is_some ( ) {
856
+ // For literal constants, prefer expr which doesn't have type suffix
857
+ if get_json_path ( & constant_data_value, & [ "const" , "is_literal" ] )
858
+ . and_then ( |v| v. as_bool ( ) )
859
+ == Some ( true )
860
+ {
861
+ // Access the expression field for literal constant values
862
+ if let Some ( expr) =
863
+ get_json_string ( & constant_data_value, & [ "const" , "expr" ] )
841
864
{
842
- if is_literal {
843
- // Access the expression field for literal constant values
844
- if let Some ( expr) =
845
- const_inner. get ( "expr" ) . and_then ( |v| v. as_str ( ) )
846
- {
847
- if expr != "_" {
848
- return Some ( expr. to_string ( ) ) ;
849
- }
850
- }
851
- }
852
- }
853
-
854
- // For computed constants or when expr is "_", use value but strip type suffix
855
- if let Some ( value) = const_inner. get ( "value" ) . and_then ( |v| v. as_str ( ) ) {
856
- return Some ( strip_type_suffix ( value) ) ;
857
- }
858
-
859
- // Fallback to expr if value is not available
860
- if let Some ( expr) = const_inner. get ( "expr" ) . and_then ( |v| v. as_str ( ) ) {
861
865
if expr != "_" {
862
866
return Some ( expr. to_string ( ) ) ;
863
867
}
864
868
}
865
869
}
866
870
867
- // Fall back to older rustdoc JSON structure for compatibility
868
- if let Some ( value) = constant_data. get ( "value" ) . and_then ( |v| v. as_str ( ) ) {
871
+ // For computed constants or when expr is "_", use value but strip type suffix
872
+ if let Some ( value) =
873
+ get_json_string ( & constant_data_value, & [ "const" , "value" ] )
874
+ {
869
875
return Some ( strip_type_suffix ( value) ) ;
870
876
}
871
- if let Some ( expr) = constant_data. get ( "expr" ) . and_then ( |v| v. as_str ( ) ) {
877
+
878
+ // Fallback to expr if value is not available
879
+ if let Some ( expr) =
880
+ get_json_string ( & constant_data_value, & [ "const" , "expr" ] )
881
+ {
872
882
if expr != "_" {
873
883
return Some ( expr. to_string ( ) ) ;
874
884
}
875
885
}
886
+ }
876
887
877
- // For some constants, the value might be in the type field if it's a simple literal
878
- if let Some ( type_info) = constant_data. get ( "type" ) {
879
- if let Some ( type_str) = type_info. as_str ( ) {
880
- // Handle simple numeric or string literals embedded in type
881
- return Some ( type_str. to_string ( ) ) ;
882
- }
888
+ // Fall back to older rustdoc JSON structure for compatibility
889
+ if let Some ( value) = get_json_string ( & constant_data_value, & [ "value" ] ) {
890
+ return Some ( strip_type_suffix ( value) ) ;
891
+ }
892
+ if let Some ( expr) = get_json_string ( & constant_data_value, & [ "expr" ] ) {
893
+ if expr != "_" {
894
+ return Some ( expr. to_string ( ) ) ;
883
895
}
884
896
}
897
+
898
+ // For some constants, the value might be in the type field if it's a simple literal
899
+ if let Some ( type_str) = get_json_string ( & constant_data_value, & [ "type" ] ) {
900
+ // Handle simple numeric or string literals embedded in type
901
+ return Some ( type_str. to_string ( ) ) ;
902
+ }
885
903
}
886
904
}
887
905
}
@@ -2492,4 +2510,53 @@ and includes various formatting.
2492
2510
let expected_folded = "Next line content Another line" ;
2493
2511
assert_eq ! ( folded_result. trim( ) , expected_folded) ;
2494
2512
}
2513
+
2514
+ #[ test]
2515
+ fn test_json_navigation_helpers ( ) {
2516
+ let test_json = json ! ( {
2517
+ "level1" : {
2518
+ "level2" : {
2519
+ "level3" : "value" ,
2520
+ "array" : [ "item1" , "item2" ] ,
2521
+ "object" : {
2522
+ "key" : "value"
2523
+ }
2524
+ } ,
2525
+ "string_field" : "test_string"
2526
+ }
2527
+ } ) ;
2528
+
2529
+ // Test get_json_path - valid paths
2530
+ assert ! ( get_json_path( & test_json, & [ "level1" ] ) . is_some( ) ) ;
2531
+ assert ! ( get_json_path( & test_json, & [ "level1" , "level2" ] ) . is_some( ) ) ;
2532
+ assert ! ( get_json_path( & test_json, & [ "level1" , "level2" , "level3" ] ) . is_some( ) ) ;
2533
+
2534
+ // Test get_json_path - invalid paths
2535
+ assert ! ( get_json_path( & test_json, & [ "nonexistent" ] ) . is_none( ) ) ;
2536
+ assert ! ( get_json_path( & test_json, & [ "level1" , "nonexistent" ] ) . is_none( ) ) ;
2537
+ assert ! ( get_json_path( & test_json, & [ "level1" , "level2" , "level3" , "too_deep" ] ) . is_none( ) ) ;
2538
+
2539
+ // Test get_json_string
2540
+ assert_eq ! (
2541
+ get_json_string( & test_json, & [ "level1" , "level2" , "level3" ] ) ,
2542
+ Some ( "value" )
2543
+ ) ;
2544
+ assert_eq ! (
2545
+ get_json_string( & test_json, & [ "level1" , "string_field" ] ) ,
2546
+ Some ( "test_string" )
2547
+ ) ;
2548
+ assert ! ( get_json_string( & test_json, & [ "level1" , "level2" , "array" ] ) . is_none( ) ) ; // not a string
2549
+
2550
+ // Test get_json_array
2551
+ let array_result = get_json_array ( & test_json, & [ "level1" , "level2" , "array" ] ) ;
2552
+ assert ! ( array_result. is_some( ) ) ;
2553
+ assert_eq ! ( array_result. unwrap( ) . len( ) , 2 ) ;
2554
+ assert ! ( get_json_array( & test_json, & [ "level1" , "string_field" ] ) . is_none( ) ) ; // not an array
2555
+
2556
+ // Test get_json_object
2557
+ assert ! ( get_json_object( & test_json, & [ "level1" ] ) . is_some( ) ) ;
2558
+ assert ! ( get_json_object( & test_json, & [ "level1" , "level2" ] ) . is_some( ) ) ;
2559
+ assert ! ( get_json_object( & test_json, & [ "level1" , "level2" , "object" ] ) . is_some( ) ) ;
2560
+ assert ! ( get_json_object( & test_json, & [ "level1" , "string_field" ] ) . is_none( ) ) ; // not an object
2561
+ }
2495
2562
}
0 commit comments