Skip to content

Commit f736f5d

Browse files
[APIView] Generate API Report in JSON (#2019)
* temp-project and rust parser init * generates structs, enums and functions without errors * block for function too * all serializable items * start from rustdoc output * temp-project to test * Azure Rust API Exporter * structured generate_api_report tool * --package flag * derive example * .rust.json * more filtering impls * lock file * lock lib.rs and add comments * package_name/review/package_name.rust.json * expand template * export configuration mod * remove azure_template_macros * cargo files update * add to the exclude array * add review folder to gitignore and delete checkedin files * retain existign sample * sample * sample in azure_template * remove JSON prettification * make it less verbose * fix format * revert the exclude array change for generate_api_report * fix cspell error * Update sdk/template/azure_template/src/lib.rs * add more examples, type alias, assoc_const and assoc_type * add examples * update template - categorize * Update .gitignore Co-authored-by: Heath Stewart <[email protected]> * format * revert template example change * remove models.rs, use rustdoc-types package instead to have identical effect in terms of functionality * troubleshooting * formatting --------- Co-authored-by: Heath Stewart <[email protected]>
1 parent 618e5c6 commit f736f5d

File tree

18 files changed

+957
-2
lines changed

18 files changed

+957
-2
lines changed

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,3 +26,5 @@ tests/data/
2626

2727
# Temporary folder to refresh SDK with TypeSpec.
2828
TempTypeSpecFiles/
29+
30+
review/

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
[package]
2+
name = "generate_api_report"
3+
authors = ["Microsoft"]
4+
edition = "2021"
5+
license = "MIT"
6+
repository = "https://github.com/azure/azure-sdk-for-rust"
7+
rust-version = "1.80"
8+
9+
[dependencies]
10+
rustdoc-types = "0.33.0"
11+
serde_json = "1.0"
12+
toml = "0.8.20"
13+
14+
[workspace]
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
# Azure Rust API Exporter
2+
3+
## Generate API Report
4+
5+
This tool generates a JSON report of the API documentation for a specified Rust package.
6+
It uses the following command `cargo +nightly rustdoc -Z unstable-options --output-format json --package {package_name} --all-features` to generate the documentation in JSON format, processes the JSON to remove unnecessary attributes, and outputs a cleaned-up version of the JSON.
7+
8+
## Version Compatibility
9+
10+
When updating the `rustdoc-types` in Cargo.toml, ensure that the `rust-api-parser` in the tools repository is also updated along with this project.
11+
12+
1. Check the `FORMAT_VERSION` value in the new version.
13+
2. Verify that it is compatible with your rustdoc JSON output version (generated by `cargo +nightly rustdoc ...`).
14+
3. If they do not match, you may need to use a different version of `rustdoc-types`.
15+
16+
Current `FORMAT_VERSION`: 37 (as of rustdoc-types 0.33.0)
17+
18+
## Usage
19+
20+
To run the tool, navigate to the root of the `azure-sdk-for-rust` repository and use the following command:
21+
22+
```sh
23+
cargo run --manifest-path eng/tools/generate_api_report/Cargo.toml -- --package package_name
24+
```
25+
26+
Generates `package_name.rust.json` in the `sdk/service_folder/package_name/review` directory.
27+
28+
For example, to generate the report for a package named `azure_core`, run:
29+
30+
```bash
31+
cargo run --manifest-path eng/tools/generate_api_report/Cargo.toml -- --package azure_core
32+
```
33+
34+
## Functionality
35+
36+
1. **Generate JSON Documentation**: The tool runs `cargo +nightly rustdoc ...` to generate the JSON documentation.
37+
2. **Process JSON**: The tool reads the JSON file, removes the `span` attribute from each item, and retains important attributes like `crate_version`, `inner`, and `format_version`.
38+
3. **Output Cleaned JSON**: The tool writes the cleaned-up JSON to a new file `package_name/review/package_name.rust.json`.
39+
40+
## Contributing
41+
42+
Contributions are welcome! Please open an issue or submit a pull request with your changes.
43+
44+
## Troubleshooting
45+
46+
If you encounter any issues, please check the [Version Compatibility](#version-compatibility) section.
47+
48+
## License
49+
50+
This project is licensed under the MIT License.
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
use rustdoc_types::Crate;
2+
use std::env;
3+
use std::error::Error;
4+
use std::fs::File;
5+
use std::io::prelude::*;
6+
use std::path::Path;
7+
use std::process::Command;
8+
9+
fn main() -> Result<(), Box<dyn Error>> {
10+
// Get the package name from command-line arguments
11+
let args: Vec<String> = env::args().collect();
12+
if args.len() != 3 || args[1] != "--package" {
13+
eprintln!("Usage: {} --package <package_name>", args[0]);
14+
std::process::exit(1);
15+
}
16+
let package_name = &args[2];
17+
let path_str = format!("./target/doc/{}.json", package_name);
18+
let path = Path::new(&path_str);
19+
20+
// Call cargo +nightly rustdoc to generate the JSON file
21+
let output = Command::new("cargo")
22+
.arg("+nightly")
23+
.arg("rustdoc")
24+
.arg("-Z")
25+
.arg("unstable-options")
26+
.arg("--output-format")
27+
.arg("json")
28+
.arg("--package")
29+
.arg(package_name)
30+
.arg("--all-features")
31+
.output()?;
32+
33+
if !output.status.success() {
34+
eprintln!(
35+
"Failed to generate JSON file: {}",
36+
String::from_utf8_lossy(&output.stderr)
37+
);
38+
std::process::exit(1);
39+
}
40+
41+
let mut file = File::open(path)?;
42+
let mut contents = String::new();
43+
file.read_to_string(&mut contents)?;
44+
45+
let mut root: Crate = serde_json::from_str(&contents)?;
46+
47+
// Remove items
48+
// 1. with item.inner.impl.is_synthetic set to true - [auto traits]
49+
// 2. with item.inner.impl.blanket_impl is not null - [blanket impls]
50+
root.index.retain(|_id, item| {
51+
if let rustdoc_types::ItemEnum::Impl(impl_item) = &item.inner {
52+
// Filter out auto traits
53+
if impl_item.is_synthetic {
54+
return false;
55+
}
56+
// Filter out blanket implementations
57+
if impl_item.blanket_impl.is_some() {
58+
return false;
59+
}
60+
}
61+
true
62+
});
63+
64+
// Clear unnecessary fields in the Crate structure
65+
// 1. paths
66+
// 2. external_crates
67+
// 3. span in all items
68+
root.paths.clear();
69+
root.external_crates.clear();
70+
for (_id, item) in root.index.iter_mut() {
71+
// Reset span to default empty value
72+
item.span = Default::default();
73+
}
74+
75+
// Navigate to Cargo.toml and get the path for the package
76+
let cargo_toml_path = Path::new("Cargo.toml");
77+
let cargo_toml_content = std::fs::read_to_string(cargo_toml_path)?;
78+
let cargo_toml: toml::Value = toml::from_str(&cargo_toml_content)?;
79+
80+
let package_path = cargo_toml
81+
.get("workspace")
82+
.and_then(|ws| ws.get("members"))
83+
.and_then(|members| members.as_array())
84+
.and_then(|members| {
85+
members.iter().find_map(|member| {
86+
if member.as_str()?.ends_with(package_name) {
87+
Some(member.as_str()?.to_string())
88+
} else {
89+
None
90+
}
91+
})
92+
})
93+
.ok_or("Package path not found in Cargo.toml")?;
94+
95+
// Create the review/ folder under the obtained path if it doesn't exist
96+
let review_folder_path = Path::new(&package_path).join("review");
97+
if !review_folder_path.exists() {
98+
std::fs::create_dir_all(&review_folder_path)?;
99+
}
100+
101+
// Create the package_name.rust.json in the review/ folder
102+
let output_path_str = review_folder_path.join(format!("{}.rust.json", package_name));
103+
let output_path = Path::new(&output_path_str);
104+
let mut output_file = File::create(output_path)?;
105+
serde_json::to_writer(&mut output_file, &root)?;
106+
107+
println!("File has been generated at: {}", output_path_str.display());
108+
109+
Ok(())
110+
}

sdk/template/azure_template/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ rust-version.workspace = true
1010

1111
[dependencies]
1212
azure_template_core = { path = "../azure_template_core" }
13+
serde = { version = "1", features = ["derive"] }
1314

1415
[lints]
1516
workspace = true
Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,3 @@
1-
Template crate for Azure SDK pipeline testing
1+
# Azure Template client library for Rust
2+
3+
> Template crate for Azure SDK pipeline testing and to showcase various rust themes/concepts to use as a testing ground for the rust-api-parser.
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
4+
//! Examples of constants in Rust
5+
6+
/// A basic constant declaration with a type annotation
7+
pub const MAX_CONNECTIONS: usize = 100;
8+
9+
/// A string constant
10+
pub const API_VERSION: &str = "v1.0.0";
11+
12+
/// Constants can be expressions too
13+
pub const MILLISECONDS_PER_DAY: u64 = 24 * 60 * 60 * 1000;
14+
15+
/// Constants in a specific type
16+
pub const DEFAULT_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(30);
17+
18+
/// Constants can be defined in modules to limit scope
19+
pub mod config {
20+
/// Database connection timeout
21+
pub const DB_TIMEOUT: u64 = 10;
22+
}
23+
24+
/// Constants can be defined in traits as associated constants
25+
pub trait Limits {
26+
/// The maximum allowed value
27+
const MAX: Self;
28+
/// The minimum allowed value
29+
const MIN: Self;
30+
}
31+
32+
impl Limits for i32 {
33+
const MAX: i32 = i32::MAX;
34+
const MIN: i32 = i32::MIN;
35+
}
36+
37+
/// Constants inside impl blocks (associated constants)
38+
pub struct Temperature {
39+
pub value: f64,
40+
pub unit: TemperatureUnit,
41+
}
42+
43+
#[derive(Debug, Clone, Copy, PartialEq)]
44+
pub enum TemperatureUnit {
45+
Celsius,
46+
Fahrenheit,
47+
Kelvin,
48+
}
49+
50+
impl Temperature {
51+
/// Absolute zero in Celsius
52+
pub const ABSOLUTE_ZERO_C: f64 = -273.15;
53+
54+
pub fn new(value: f64, unit: TemperatureUnit) -> Self {
55+
Self { value, unit }
56+
}
57+
58+
pub fn is_below_freezing(&self) -> bool {
59+
match self.unit {
60+
TemperatureUnit::Celsius => self.value < 0.0,
61+
TemperatureUnit::Fahrenheit => self.value < 32.0,
62+
TemperatureUnit::Kelvin => self.value < 273.15,
63+
}
64+
}
65+
}
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
4+
//! Examples of enums in Rust
5+
6+
/// A basic enum with simple variants
7+
#[derive(Debug, Clone, PartialEq)]
8+
pub enum Status {
9+
Active,
10+
Inactive,
11+
Pending,
12+
Canceled,
13+
}
14+
15+
/// An enum with variants containing data
16+
#[derive(Debug, Clone, PartialEq)]
17+
pub enum Message {
18+
Text(String),
19+
Number(i32),
20+
Empty,
21+
Complex {
22+
subject: String,
23+
content: String,
24+
urgent: bool,
25+
},
26+
}
27+
28+
impl Message {
29+
/// Create a new text message
30+
pub fn text(content: &str) -> Self {
31+
Self::Text(content.to_string())
32+
}
33+
34+
/// Get the message as a string
35+
pub fn as_string(&self) -> String {
36+
match self {
37+
Self::Text(text) => text.clone(),
38+
Self::Number(num) => num.to_string(),
39+
Self::Empty => String::new(),
40+
Self::Complex {
41+
subject,
42+
content,
43+
urgent,
44+
} => {
45+
let priority = if *urgent { "URGENT" } else { "Normal" };
46+
format!("[{}] {}: {}", priority, subject, content)
47+
}
48+
}
49+
}
50+
}
51+
52+
/// An enum with recursive variants
53+
#[derive(Debug, Clone)]
54+
pub enum JsonValue {
55+
Null,
56+
Boolean(bool),
57+
Number(f64),
58+
String(String),
59+
Array(Vec<JsonValue>),
60+
Object(std::collections::HashMap<String, JsonValue>),
61+
}
62+
63+
/// An enum representing results with detailed error information
64+
#[derive(Debug)]
65+
pub enum ApiResult<T> {
66+
Success(T),
67+
NotFound { resource: String },
68+
Unauthorized { reason: String },
69+
ServerError { code: i32, message: String },
70+
}

0 commit comments

Comments
 (0)