Skip to content

Commit c2212a2

Browse files
committed
simplify json parsing logic
1 parent 31beefe commit c2212a2

File tree

1 file changed

+177
-110
lines changed

1 file changed

+177
-110
lines changed

contrib/tools/config-docs-generator/src/extract_docs.rs

Lines changed: 177 additions & 110 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,46 @@ struct ConfigDocs {
5151
referenced_constants: HashMap<String, Option<String>>, // Name -> Resolved Value (or None)
5252
}
5353

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+
5494
fn main() -> Result<()> {
5595
let matches = ClapCommand::new("extract-docs")
5696
.about("Extract documentation from Rust source code using rustdoc JSON")
@@ -200,33 +240,28 @@ fn extract_config_docs_from_rustdoc(
200240
let mut all_referenced_constants = std::collections::HashSet::new();
201241

202242
// 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"])
206244
.context("Missing 'index' field in rustdoc JSON")?;
207245

208246
for (_item_id, item) in index {
209247
// 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;
220255
}
256+
}
221257

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)?;
224260

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);
229263
}
264+
all_referenced_constants.extend(referenced_constants);
230265
}
231266
}
232267
}
@@ -252,10 +287,7 @@ fn extract_struct_from_rustdoc_index(
252287
let mut all_referenced_constants = std::collections::HashSet::new();
253288

254289
// 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());
259291

260292
// Collect constant references from struct description
261293
if let Some(desc) = &description {
@@ -289,56 +321,43 @@ fn extract_struct_fields(
289321

290322
// Navigate through rustdoc JSON structure to access struct fields
291323
// 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+
};
335338

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);
341357
}
358+
359+
// Extend referenced constants
360+
all_referenced_constants.extend(referenced_constants);
342361
}
343362
}
344363
}
@@ -808,7 +827,7 @@ fn resolve_constant_reference(
808827
let json_file_path = format!("target/rustdoc-json/doc/{}.json", lib_name);
809828
if let Ok(json_content) = std::fs::read_to_string(&json_file_path) {
810829
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"]) {
812831
if let Some(value) = resolve_constant_in_index(name, index) {
813832
return Some(value);
814833
}
@@ -827,61 +846,60 @@ fn resolve_constant_in_index(
827846
// Look for a constant with the given name in the rustdoc index
828847
for (_item_id, item) in rustdoc_index {
829848
// 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"]) {
831850
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"])
841864
{
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()) {
861865
if expr != "_" {
862866
return Some(expr.to_string());
863867
}
864868
}
865869
}
866870

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+
{
869875
return Some(strip_type_suffix(value));
870876
}
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+
{
872882
if expr != "_" {
873883
return Some(expr.to_string());
874884
}
875885
}
886+
}
876887

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());
883895
}
884896
}
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+
}
885903
}
886904
}
887905
}
@@ -2492,4 +2510,53 @@ and includes various formatting.
24922510
let expected_folded = "Next line content Another line";
24932511
assert_eq!(folded_result.trim(), expected_folded);
24942512
}
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+
}
24952562
}

0 commit comments

Comments
 (0)