mirror of
https://github.com/linabutler/ploidy
synced 2026-07-14 18:45:32 +00:00
feat(cli): Add --stats to generate; log to stderr.
This commit is contained in:
Generated
+2
@@ -1128,6 +1128,8 @@ dependencies = [
|
||||
"ploidy-codegen-rust",
|
||||
"ploidy-core",
|
||||
"semver",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"tempfile",
|
||||
]
|
||||
|
||||
|
||||
@@ -27,6 +27,8 @@ ploidy-pointer-derive = { path = "ploidy-pointer-derive", version = "0.14.1" }
|
||||
pretty_assertions = "1"
|
||||
ref-cast = "1"
|
||||
rustc-hash = "2"
|
||||
serde = "1"
|
||||
serde_json = "1"
|
||||
toml_edit = { version = "0.25", features = ["serde"] }
|
||||
|
||||
[workspace.lints.rust]
|
||||
|
||||
@@ -11,7 +11,7 @@ rust-version.workspace = true
|
||||
|
||||
[dependencies]
|
||||
itertools = "0.14"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde = { workspace = true, features = ["derive"] }
|
||||
miette = "7"
|
||||
ploidy-core = { workspace = true, features = ["proc-macro2"] }
|
||||
prettyplease = "0.2"
|
||||
|
||||
@@ -4,7 +4,7 @@ use itertools::Itertools;
|
||||
use proc_macro2::TokenStream;
|
||||
use quote::quote;
|
||||
|
||||
use ploidy_core::codegen::{IntoCode, write_to_disk};
|
||||
use ploidy_core::codegen::{IntoCode, WrittenFile, write_to_disk};
|
||||
|
||||
mod cargo;
|
||||
mod cfg;
|
||||
@@ -45,18 +45,26 @@ pub use schema::*;
|
||||
pub use statics::*;
|
||||
pub use types::*;
|
||||
|
||||
pub fn write_types_to_disk(output: &Path, graph: &CodegenGraph<'_>) -> miette::Result<()> {
|
||||
pub fn write_types_to_disk(
|
||||
output: &Path,
|
||||
graph: &CodegenGraph<'_>,
|
||||
) -> miette::Result<Vec<WrittenFile>> {
|
||||
let mut written = Vec::new();
|
||||
|
||||
for schema in graph.schemas() {
|
||||
let code = CodegenSchemaType::new(graph, &schema).into_code();
|
||||
write_to_disk(output, code)?;
|
||||
written.push(write_to_disk(output, code)?);
|
||||
}
|
||||
|
||||
write_to_disk(output, CodegenTypesModule::new(graph))?;
|
||||
written.push(write_to_disk(output, CodegenTypesModule::new(graph))?);
|
||||
|
||||
Ok(())
|
||||
Ok(written)
|
||||
}
|
||||
|
||||
pub fn write_client_to_disk(output: &Path, graph: &CodegenGraph<'_>) -> miette::Result<()> {
|
||||
pub fn write_client_to_disk(
|
||||
output: &Path,
|
||||
graph: &CodegenGraph<'_>,
|
||||
) -> miette::Result<Vec<WrittenFile>> {
|
||||
// Group operations by resource name.
|
||||
let ops_by_resource: BTreeMap<_, Vec<_>> =
|
||||
graph.operations().fold(BTreeMap::default(), |mut map, op| {
|
||||
@@ -65,16 +73,24 @@ pub fn write_client_to_disk(output: &Path, graph: &CodegenGraph<'_>) -> miette::
|
||||
map
|
||||
});
|
||||
|
||||
let mut written = Vec::new();
|
||||
|
||||
// Write a module per resource.
|
||||
for (ident, ops) in &ops_by_resource {
|
||||
write_to_disk(output, CodegenResource::new(graph, *ident, ops))?;
|
||||
written.push(write_to_disk(
|
||||
output,
|
||||
CodegenResource::new(graph, *ident, ops),
|
||||
)?);
|
||||
}
|
||||
|
||||
// Write the top-level client module.
|
||||
let idents = ops_by_resource.keys().copied().collect_vec();
|
||||
write_to_disk(output, CodegenClientModule::new(graph, &idents))?;
|
||||
written.push(write_to_disk(
|
||||
output,
|
||||
CodegenClientModule::new(graph, &idents),
|
||||
)?);
|
||||
|
||||
Ok(())
|
||||
Ok(written)
|
||||
}
|
||||
|
||||
/// Generates one or more `#[doc]` attributes for a schema description,
|
||||
|
||||
@@ -18,8 +18,8 @@ form_urlencoded = "1"
|
||||
indexmap = { version = "2", features = ["serde"] }
|
||||
itertools = "0.14"
|
||||
miette = "7"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = { version = "1", features = ["preserve_order"] }
|
||||
serde = { workspace = true, features = ["derive"] }
|
||||
serde_json = { workspace = true, features = ["preserve_order"] }
|
||||
serde-saphyr = "0.0.27"
|
||||
percent-encoding = "2.3"
|
||||
petgraph = "0.8"
|
||||
|
||||
@@ -27,19 +27,33 @@ pub mod unique;
|
||||
|
||||
pub use unique::{AsKebabCase, AsPascalCase, AsSnakeCase, NamePart, UniqueName, UniqueNames};
|
||||
|
||||
pub fn write_to_disk(output: &Path, code: impl IntoCode) -> miette::Result<()> {
|
||||
/// A record of a file that [`write_to_disk`] wrote.
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub struct WrittenFile {
|
||||
/// The path to the file, relative to the output directory.
|
||||
pub path: String,
|
||||
/// The size of the written contents in bytes.
|
||||
pub size: usize,
|
||||
}
|
||||
|
||||
pub fn write_to_disk(output: &Path, code: impl IntoCode) -> miette::Result<WrittenFile> {
|
||||
let code = code.into_code();
|
||||
let path = output.join(code.path());
|
||||
let relative = code.path().to_owned();
|
||||
let absolute = output.join(&relative);
|
||||
let string = code.into_string()?;
|
||||
if let Some(parent) = path.parent() {
|
||||
if let Some(parent) = absolute.parent() {
|
||||
std::fs::create_dir_all(parent)
|
||||
.into_diagnostic()
|
||||
.with_context(|| format!("Failed to create directory `{}`", parent.display()))?;
|
||||
}
|
||||
std::fs::write(&path, string)
|
||||
let size = string.len();
|
||||
std::fs::write(&absolute, string)
|
||||
.into_diagnostic()
|
||||
.with_context(|| format!("Failed to write `{}`", path.display()))?;
|
||||
Ok(())
|
||||
.with_context(|| format!("Failed to write `{}`", absolute.display()))?;
|
||||
Ok(WrittenFile {
|
||||
path: relative,
|
||||
size,
|
||||
})
|
||||
}
|
||||
|
||||
pub trait Code {
|
||||
|
||||
@@ -4,7 +4,7 @@ use indexmap::IndexMap;
|
||||
use itertools::Itertools;
|
||||
use ploidy_pointer::{JsonPointee, JsonPointer, JsonPointerBuf, JsonPointerTarget};
|
||||
use serde::{
|
||||
Deserialize, Deserializer,
|
||||
Deserialize, Deserializer, Serialize,
|
||||
de::{DeserializeOwned, Error as DeserializeError},
|
||||
};
|
||||
use serde_json::Value as JsonValue;
|
||||
@@ -61,7 +61,7 @@ impl Info {
|
||||
}
|
||||
|
||||
/// The title and optional version from an [`Info`] section.
|
||||
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
|
||||
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq, Serialize)]
|
||||
pub struct Label<'a> {
|
||||
pub title: &'a str,
|
||||
pub version: Option<&'a str>,
|
||||
|
||||
@@ -13,9 +13,9 @@ repository.workspace = true
|
||||
indexmap = { version = "2", optional = true }
|
||||
ploidy-pointer-derive = { workspace = true, optional = true }
|
||||
ref-cast = { workspace = true }
|
||||
serde = { version = "1", optional = true }
|
||||
serde = { workspace = true, optional = true }
|
||||
serde_bytes = { version = "0.11", optional = true }
|
||||
serde_json = { version = "1", optional = true }
|
||||
serde_json = { workspace = true, optional = true }
|
||||
strsim = { version = "0.11", optional = true }
|
||||
thiserror = "2"
|
||||
|
||||
|
||||
@@ -32,9 +32,9 @@ reqwest = { version = "0.13", default-features = false, features = [
|
||||
"query",
|
||||
"rustls",
|
||||
] }
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde = { workspace = true, features = ["derive"] }
|
||||
serde_bytes = "0.11"
|
||||
serde_json = "1"
|
||||
serde_json = { workspace = true }
|
||||
serde_path_to_error = "0.1"
|
||||
thiserror = "2"
|
||||
url = { version = "2", features = ["serde"] }
|
||||
|
||||
@@ -17,6 +17,8 @@ mimalloc = { version = "0.1", optional = true }
|
||||
ploidy-codegen-rust = { workspace = true }
|
||||
ploidy-core = { workspace = true }
|
||||
semver = "1"
|
||||
serde = { workspace = true, features = ["derive"] }
|
||||
serde_json = { workspace = true }
|
||||
|
||||
[features]
|
||||
mimalloc = ["dep:mimalloc"]
|
||||
|
||||
@@ -24,6 +24,10 @@ pub struct RawGenerateArgs<T: clap::Args> {
|
||||
#[arg(short, long)]
|
||||
pub output: Option<PathBuf>,
|
||||
|
||||
/// Print statistics to standard output.
|
||||
#[arg(long)]
|
||||
pub stats: bool,
|
||||
|
||||
#[command(flatten)]
|
||||
pub language: T,
|
||||
}
|
||||
|
||||
@@ -66,6 +66,7 @@ impl Generate {
|
||||
Ok(Self::Rust(GenerateArgs {
|
||||
input,
|
||||
output,
|
||||
stats: args.stats,
|
||||
language,
|
||||
}))
|
||||
}
|
||||
@@ -77,6 +78,7 @@ impl Generate {
|
||||
pub struct GenerateArgs<T> {
|
||||
pub input: PathBuf,
|
||||
pub output: PathBuf,
|
||||
pub stats: bool,
|
||||
pub language: T,
|
||||
}
|
||||
|
||||
@@ -186,6 +188,7 @@ mod tests {
|
||||
let args = RawGenerate::Rust(RawGenerateArgs {
|
||||
input: PathBuf::from("specs/petstore.yaml"),
|
||||
output: None,
|
||||
stats: false,
|
||||
language: RawGenerateRustArgs::default(),
|
||||
});
|
||||
let Generate::Rust(result) = Generate::try_new(args).unwrap();
|
||||
@@ -197,6 +200,7 @@ mod tests {
|
||||
let args = RawGenerate::Rust(RawGenerateArgs {
|
||||
input: PathBuf::from("specs/petstore.yaml"),
|
||||
output: Some(PathBuf::from("my-output")),
|
||||
stats: false,
|
||||
language: RawGenerateRustArgs::default(),
|
||||
});
|
||||
let Generate::Rust(result) = Generate::try_new(args).unwrap();
|
||||
@@ -208,6 +212,7 @@ mod tests {
|
||||
let args = RawGenerate::Rust(RawGenerateArgs {
|
||||
input: PathBuf::from("/"),
|
||||
output: None,
|
||||
stats: false,
|
||||
language: RawGenerateRustArgs::default(),
|
||||
});
|
||||
let err = Generate::try_new(args).unwrap_err();
|
||||
|
||||
+112
-42
@@ -1,6 +1,9 @@
|
||||
use itertools::Itertools;
|
||||
use miette::{Context, IntoDiagnostic, Result};
|
||||
use ploidy_codegen_rust::{CodegenCargoManifest, CodegenErrorModule, CodegenGraph, CodegenLibrary};
|
||||
use ploidy_codegen_rust::{
|
||||
CodegenCargoManifest, CodegenErrorModule, CodegenGraph, CodegenIdentUsage, CodegenLibrary,
|
||||
ResourceGroup,
|
||||
};
|
||||
use ploidy_core::{
|
||||
arena::Arena,
|
||||
codegen::write_to_disk,
|
||||
@@ -10,8 +13,12 @@ use ploidy_core::{
|
||||
|
||||
mod args;
|
||||
mod cmd;
|
||||
mod stats;
|
||||
|
||||
use self::cmd::{Generate, GenerateArgs, Main};
|
||||
use self::{
|
||||
cmd::{Generate, GenerateArgs, Main},
|
||||
stats::{GenerateStats, OutputStats, Timings, timed},
|
||||
};
|
||||
|
||||
#[cfg(feature = "mimalloc")]
|
||||
#[global_allocator]
|
||||
@@ -23,29 +30,51 @@ fn main() -> Result<()> {
|
||||
Main::Generate(Generate::Rust(GenerateArgs {
|
||||
input,
|
||||
output,
|
||||
stats,
|
||||
language,
|
||||
})) => {
|
||||
let mut timings = Timings::default();
|
||||
|
||||
let source = std::fs::read_to_string(&input)
|
||||
.into_diagnostic()
|
||||
.with_context(|| format!("Failed to read `{}`", input.display()))?;
|
||||
|
||||
let doc = Document::from_yaml(&source)
|
||||
.into_diagnostic()
|
||||
.context("Failed to parse OpenAPI document")?;
|
||||
let doc = {
|
||||
let timing = timed(|| {
|
||||
Document::from_yaml(&source)
|
||||
.into_diagnostic()
|
||||
.context("Failed to parse OpenAPI document")
|
||||
});
|
||||
timings.parse = timing.as_secs_f64();
|
||||
timing.into_inner()
|
||||
}?;
|
||||
|
||||
if let Some(label) = doc.info.label() {
|
||||
let label = doc.info.label();
|
||||
if let Some(label) = label {
|
||||
match label.version {
|
||||
Some(version) => println!("OpenAPI: {} (version {version})", label.title),
|
||||
None => println!("OpenAPI: {}", label.title),
|
||||
Some(version) => eprintln!("OpenAPI: {} (version {version})", label.title),
|
||||
None => eprintln!("OpenAPI: {}", label.title),
|
||||
}
|
||||
}
|
||||
|
||||
let arena = Arena::new();
|
||||
let spec = Spec::from_doc(&arena, &doc).into_diagnostic()?;
|
||||
let mut raw = RawGraph::new(&arena, &spec);
|
||||
raw.collapse_trivial_inlines();
|
||||
raw.inline_tagged_variants();
|
||||
raw.inline_untagged_variants();
|
||||
let spec = {
|
||||
let timing = timed(|| Spec::from_doc(&arena, &doc).into_diagnostic());
|
||||
timings.ir = timing.as_secs_f64();
|
||||
timing.into_inner()
|
||||
}?;
|
||||
|
||||
let raw = {
|
||||
let timing = timed(|| {
|
||||
let mut raw = RawGraph::new(&arena, &spec);
|
||||
raw.collapse_trivial_inlines();
|
||||
raw.inline_tagged_variants();
|
||||
raw.inline_untagged_variants();
|
||||
raw
|
||||
});
|
||||
timings.ir += timing.as_secs_f64();
|
||||
timing.into_inner()
|
||||
};
|
||||
|
||||
let config = language
|
||||
.manifest
|
||||
@@ -54,45 +83,86 @@ fn main() -> Result<()> {
|
||||
.transpose()?
|
||||
.flatten();
|
||||
let graph = {
|
||||
let graph = raw.cook();
|
||||
match config.as_ref() {
|
||||
Some(config) => CodegenGraph::with_config(graph, config),
|
||||
None => CodegenGraph::new(graph),
|
||||
}
|
||||
let timing = timed(|| {
|
||||
let graph = raw.cook();
|
||||
match config.as_ref() {
|
||||
Some(config) => CodegenGraph::with_config(graph, config),
|
||||
None => CodegenGraph::new(graph),
|
||||
}
|
||||
});
|
||||
timings.cook = timing.as_secs_f64();
|
||||
timing.into_inner()
|
||||
};
|
||||
|
||||
println!("Writing generated code to `{}`...", output.display());
|
||||
|
||||
println!("Generating `Cargo.toml`...");
|
||||
write_to_disk(
|
||||
&output,
|
||||
CodegenCargoManifest::new(&graph, &language.manifest),
|
||||
)?;
|
||||
|
||||
println!("Generating `lib.rs`...");
|
||||
write_to_disk(&output, CodegenLibrary)?;
|
||||
|
||||
println!("Generating `error.rs`...");
|
||||
write_to_disk(&output, CodegenErrorModule)?;
|
||||
|
||||
println!("Generating {} types...", graph.schemas().count());
|
||||
ploidy_codegen_rust::write_types_to_disk(&output, &graph)?;
|
||||
eprintln!("Writing generated code to `{}`...", output.display());
|
||||
|
||||
let schemas = graph.schemas().count();
|
||||
let counts = graph
|
||||
.operations()
|
||||
.into_grouping_map_by(|op| graph.resource_for(op))
|
||||
.fold(0, |count, _, _| count + 1);
|
||||
println!(
|
||||
"Generating {} client methods across {} resources...",
|
||||
counts.values().copied().sum::<usize>(),
|
||||
counts.len(),
|
||||
);
|
||||
ploidy_codegen_rust::write_client_to_disk(&output, &graph)?;
|
||||
|
||||
println!("Generation complete");
|
||||
let written = {
|
||||
let timing = timed(|| -> Result<_> {
|
||||
let mut written = Vec::new();
|
||||
|
||||
eprintln!("Generating `Cargo.toml`...");
|
||||
written.push(write_to_disk(
|
||||
&output,
|
||||
CodegenCargoManifest::new(&graph, &language.manifest),
|
||||
)?);
|
||||
|
||||
eprintln!("Generating `lib.rs`...");
|
||||
written.push(write_to_disk(&output, CodegenLibrary)?);
|
||||
|
||||
eprintln!("Generating `error.rs`...");
|
||||
written.push(write_to_disk(&output, CodegenErrorModule)?);
|
||||
|
||||
eprintln!("Generating {schemas} types...");
|
||||
written.extend(ploidy_codegen_rust::write_types_to_disk(&output, &graph)?);
|
||||
|
||||
eprintln!(
|
||||
"Generating {} client methods across {} resources...",
|
||||
counts.values().copied().sum::<usize>(),
|
||||
counts.len(),
|
||||
);
|
||||
written.extend(ploidy_codegen_rust::write_client_to_disk(&output, &graph)?);
|
||||
|
||||
Ok(written)
|
||||
});
|
||||
timings.codegen = timing.as_secs_f64();
|
||||
timing.into_inner()
|
||||
}?;
|
||||
|
||||
eprintln!("Generation complete");
|
||||
|
||||
if stats {
|
||||
let stats = GenerateStats {
|
||||
spec: label,
|
||||
schemas,
|
||||
operations: counts
|
||||
.iter()
|
||||
.map(|(&resource, &count)| {
|
||||
let key = match resource {
|
||||
ResourceGroup::Named(name) => {
|
||||
CodegenIdentUsage::Module(name).display().to_string()
|
||||
}
|
||||
ResourceGroup::Default => "default".to_owned(),
|
||||
};
|
||||
(key, count)
|
||||
})
|
||||
.collect(),
|
||||
timings,
|
||||
output: OutputStats {
|
||||
files: written.len(),
|
||||
size: written.iter().map(|file| file.size).sum(),
|
||||
},
|
||||
};
|
||||
println!("{}", serde_json::to_string(&stats).into_diagnostic()?);
|
||||
}
|
||||
|
||||
if language.check {
|
||||
println!("Running `cargo check`...");
|
||||
eprintln!("Running `cargo check`...");
|
||||
let status = std::process::Command::new("cargo")
|
||||
.arg("check")
|
||||
.arg("--all-targets")
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
use std::{
|
||||
collections::BTreeMap,
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
|
||||
use ploidy_core::parse::Label;
|
||||
use serde::Serialize;
|
||||
|
||||
/// Statistics for a single generation run.
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct GenerateStats<'a> {
|
||||
pub spec: Option<Label<'a>>,
|
||||
pub schemas: usize,
|
||||
pub operations: BTreeMap<String, usize>,
|
||||
pub timings: Timings,
|
||||
pub output: OutputStats,
|
||||
}
|
||||
|
||||
/// Wall-clock durations, in seconds, for each generation phase.
|
||||
#[derive(Debug, Default, Serialize)]
|
||||
pub struct Timings {
|
||||
pub parse: f64,
|
||||
pub ir: f64,
|
||||
pub cook: f64,
|
||||
pub codegen: f64,
|
||||
}
|
||||
|
||||
/// The number of files written, and their combined size in bytes.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Serialize)]
|
||||
pub struct OutputStats {
|
||||
pub files: usize,
|
||||
pub size: usize,
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn timed<T>(f: impl FnOnce() -> T) -> TimedResult<T> {
|
||||
let start = Instant::now();
|
||||
let result = f();
|
||||
TimedResult(result, start.elapsed())
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct TimedResult<T>(T, Duration);
|
||||
|
||||
impl<T> TimedResult<T> {
|
||||
#[inline]
|
||||
pub fn as_secs_f64(&self) -> f64 {
|
||||
self.1.as_secs_f64()
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn into_inner(self) -> T {
|
||||
self.0
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user