diff --git a/Cargo.lock b/Cargo.lock index dff99dcd..7b480b30 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,15 @@ # It is not intended for manual editing. version = 4 +[[package]] +name = "addr2line" +version = "0.25.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b5d307320b3181d6d7954e663bd7c774a838b8220fe0593c86d9fb09f498b4b" +dependencies = [ + "gimli", +] + [[package]] name = "adler2" version = "2.0.1" @@ -17,6 +26,15 @@ dependencies = [ "memchr", ] +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + [[package]] name = "anstream" version = "1.0.0" @@ -192,6 +210,21 @@ dependencies = [ "url", ] +[[package]] +name = "backtrace" +version = "0.3.76" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb531853791a215d7c62a30daf0dde835f381ab5de4589cfe7c649d2cbe92bd6" +dependencies = [ + "addr2line", + "cfg-if", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", + "windows-link", +] + [[package]] name = "base64" version = "0.22.1" @@ -285,6 +318,20 @@ dependencies = [ "rand_core 0.10.1", ] +[[package]] +name = "chrono" +version = "0.4.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1aa79e62e7697b8e29b513a68abacf485adcd1fe8284a4316c5ae868e6633327" +dependencies = [ + "iana-time-zone", + "js-sys", + "num-traits", + "serde", + "wasm-bindgen", + "windows-link", +] + [[package]] name = "clap" version = "4.6.1" @@ -533,6 +580,16 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + [[package]] name = "event-listener" version = "5.4.1" @@ -554,6 +611,21 @@ dependencies = [ "pin-project-lite", ] +[[package]] +name = "exceptionless" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e27ceb062263fdafc1b4495948c847c49fc8a0ee4cc9800e1d29f38781bbd575" +dependencies = [ + "async-trait", + "backtrace", + "chrono", + "reqwest 0.12.28", + "serde", + "serde_json", + "thiserror", +] + [[package]] name = "find-msvc-tools" version = "0.1.9" @@ -720,6 +792,12 @@ dependencies = [ "wasip3", ] +[[package]] +name = "gimli" +version = "0.32.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e629b9b98ef3dd8afe6ca2bd0f89306cec16d43d907889945bc5d6687f2f13c7" + [[package]] name = "hashbrown" version = "0.15.5" @@ -799,9 +877,11 @@ dependencies = [ "azure_identity", "clap", "console", + "exceptionless", "httpgenerator-core", "pollster", "serde_json", + "tokio", "unicode-width", ] @@ -890,6 +970,30 @@ dependencies = [ "tracing", ] +[[package]] +name = "iana-time-zone" +version = "0.1.65" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + [[package]] name = "icu_collections" version = "2.2.0" @@ -1150,6 +1254,15 @@ version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0" +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + [[package]] name = "log" version = "0.4.29" @@ -1204,6 +1317,15 @@ dependencies = [ "autocfg", ] +[[package]] +name = "object" +version = "0.37.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff76201f031d8863c38aa7f905eca4f53abbfa15f609db4277d44cd8938f33fe" +dependencies = [ + "memchr", +] + [[package]] name = "once_cell" version = "1.21.4" @@ -1266,6 +1388,29 @@ version = "2.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" +[[package]] +name = "parking_lot" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-link", +] + [[package]] name = "percent-encoding" version = "2.3.2" @@ -1489,6 +1634,15 @@ version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69" +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags", +] + [[package]] name = "regex" version = "1.12.3" @@ -1610,6 +1764,12 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "rustc-demangle" +version = "0.1.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b50b8869d9fc858ce7266cce0194bd74df58b9d0e3f6df3a9fc8eb470d95c09d" + [[package]] name = "rustc-hash" version = "2.1.2" @@ -1731,6 +1891,12 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + [[package]] name = "security-framework" version = "3.7.0" @@ -1833,6 +1999,16 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" +[[package]] +name = "signal-hook-registry" +version = "1.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" +dependencies = [ + "errno", + "libc", +] + [[package]] name = "simd-adler32" version = "0.3.9" @@ -2022,7 +2198,9 @@ dependencies = [ "bytes", "libc", "mio", + "parking_lot", "pin-project-lite", + "signal-hook-registry", "socket2", "tokio-macros", "windows-sys 0.61.2", @@ -2457,12 +2635,65 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "windows-core" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-implement" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "windows-interface" +version = "0.59.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + [[package]] name = "windows-link" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" +[[package]] +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +dependencies = [ + "windows-link", +] + [[package]] name = "windows-sys" version = "0.52.0" diff --git a/Cargo.toml b/Cargo.toml index ac09f27d..ff8320a7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -19,6 +19,7 @@ azure_core = "1.0.0" azure_identity = "1.0.0" clap = { version = "4.6.1", features = ["derive"] } console = "0.16.3" +exceptionless = "0.1.0" httpgenerator-core = { version = "0.1.0", path = "src/rust/core" } openapiv3 = "2.2.0" openapiv3_1 = "0.1.5" @@ -31,5 +32,6 @@ reqwest = { version = "0.12.28", default-features = false, features = [ ] } serde = { version = "1.0.228", features = ["derive"] } serde_json = { version = "1.0.145", features = ["preserve_order"] } +tokio = { version = "1", features = ["full"] } url = "2.5.7" yaml_serde = "0.10.4" diff --git a/src/dotnet/HttpGenerator.Tests/ExampleTests.cs b/src/dotnet/HttpGenerator.Tests/ExampleTests.cs deleted file mode 100644 index 59f229ef..00000000 --- a/src/dotnet/HttpGenerator.Tests/ExampleTests.cs +++ /dev/null @@ -1,28 +0,0 @@ -using FluentAssertions; -using FluentAssertions.Execution; -using HttpGenerator.Core; - -namespace HttpGenerator.Tests; - -public class ExampleTests -{ - [Theory] - [Trait("Category", "Integration")] - [InlineData("https://demo.netbox.dev/api/schema", OutputType.OneFile)] - [InlineData("https://demo.netbox.dev/api/schema", OutputType.OneFilePerTag)] - [InlineData("https://demo.netbox.dev/api/schema", OutputType.OneRequestPerFile)] - public async Task Should_Return_Valid_Code(string url, OutputType outputType) - { - var generateCode = await HttpFileGenerator.Generate( - new() - { - OpenApiPath = url, - OutputType = outputType, - GenerateIntelliJTests = true, - }); - - using var scope = new AssertionScope(); - generateCode.Should().NotBeNull(); - generateCode.Files.Should().NotBeNullOrEmpty(); - } -} diff --git a/src/dotnet/HttpGenerator.Tests/OpenApiDocumentFactoryTests.cs b/src/dotnet/HttpGenerator.Tests/OpenApiDocumentFactoryTests.cs index bf6c7ae4..ce4380e8 100644 --- a/src/dotnet/HttpGenerator.Tests/OpenApiDocumentFactoryTests.cs +++ b/src/dotnet/HttpGenerator.Tests/OpenApiDocumentFactoryTests.cs @@ -9,7 +9,6 @@ public class OpenApiDocumentFactoryTests [Theory] [InlineData("https://developers.intellihr.io/docs/v1/swagger.json")] // GZIP encoded [InlineData("http://raw.githubusercontent.com/christianhelle/httpgenerator/main/test/OpenAPI/v3.0/petstore.json")] - [InlineData("https://demo.netbox.dev/api/schema")] public async Task Create_From_Uri_Returns_NotNull(string url) { (await OpenApiDocumentFactory.CreateAsync(url)) diff --git a/src/rust/cli/Cargo.toml b/src/rust/cli/Cargo.toml index 66c69d02..3e8762f0 100644 --- a/src/rust/cli/Cargo.toml +++ b/src/rust/cli/Cargo.toml @@ -24,7 +24,9 @@ azure_core.workspace = true azure_identity.workspace = true clap.workspace = true console.workspace = true +exceptionless.workspace = true httpgenerator-core = { workspace = true, features = ["openapi"] } pollster.workspace = true serde_json.workspace = true +tokio.workspace = true unicode-width = "0.2" diff --git a/src/rust/cli/src/lib.rs b/src/rust/cli/src/lib.rs index 355f03de..57e6e875 100644 --- a/src/rust/cli/src/lib.rs +++ b/src/rust/cli/src/lib.rs @@ -9,4 +9,4 @@ mod writer; pub use error::CliError; pub use execution::{execute, execute_with_observer, should_attempt_azure_auth}; pub use observer::{AzureAuthStatus, ExecutionObserver, ExecutionSummary}; -pub use telemetry::{NoopTelemetrySink, TelemetryRecorder}; +pub use telemetry::{ExceptionlessTelemetrySink, NoopTelemetrySink, TelemetryRecorder, TelemetrySinkCollection}; diff --git a/src/rust/cli/src/main.rs b/src/rust/cli/src/main.rs index a4ffd28f..c0bb6a96 100644 --- a/src/rust/cli/src/main.rs +++ b/src/rust/cli/src/main.rs @@ -2,22 +2,27 @@ mod ui; use clap::FromArgMatches; use httpgenerator_cli::{ - NoopTelemetrySink, TelemetryRecorder, + ExceptionlessTelemetrySink, NoopTelemetrySink, TelemetryRecorder, TelemetrySinkCollection, args::{CliArgs, build_command}, execute_with_observer, }; use std::{ffi::OsString, time::Instant}; use ui::CliPresenter; -fn main() { +#[tokio::main] +async fn main() { let raw_args = raw_args_with_help(); let args = parse_args(&raw_args); - let mut telemetry = TelemetryRecorder::from_cli_args(&raw_args, &args, NoopTelemetrySink); let started_at = Instant::now(); let mut presenter = CliPresenter::detect(); presenter.print_header(args.no_logging); - match execute_with_observer(args.clone(), &mut presenter) { + let sink = create_telemetry_sink(&args); + let mut telemetry = TelemetryRecorder::from_cli_args(&raw_args, &args, sink); + + let result = execute_with_observer(args.clone(), &mut presenter); + + match result { Ok(_summary) => { telemetry.record_feature_usage(&args); presenter.print_success(started_at.elapsed()); @@ -28,6 +33,21 @@ fn main() { std::process::exit(1); } } + + let recorder = telemetry.into_sink(); + flush_telemetry(recorder).await; +} + +fn create_telemetry_sink(args: &CliArgs) -> TelemetrySinkCollection { + if args.no_logging { + NoopTelemetrySink.into() + } else { + ExceptionlessTelemetrySink::new().into() + } +} + +async fn flush_telemetry(sink: TelemetrySinkCollection) { + sink.flush().await; } fn raw_args_with_help() -> Vec { diff --git a/src/rust/cli/src/telemetry/exceptionless.rs b/src/rust/cli/src/telemetry/exceptionless.rs new file mode 100644 index 00000000..714641a2 --- /dev/null +++ b/src/rust/cli/src/telemetry/exceptionless.rs @@ -0,0 +1,104 @@ +use std::sync::Mutex; + +use exceptionless::ExceptionlessClient; + +use super::{TelemetryEvent, TelemetrySink}; + +#[derive(Debug)] +pub struct ExceptionlessTelemetrySink { + client: ExceptionlessClient, + events: Mutex>, +} + +const EXCEPTIONLESS_API_KEY: &str = "7VSRHLYiJdF7Xp0WaVwmEbJxVmrjqHnTIZNKkrkI"; + +impl Default for ExceptionlessTelemetrySink { + fn default() -> Self { + Self::new() + } +} + +impl ExceptionlessTelemetrySink { + pub fn new() -> Self { + Self { + client: ExceptionlessClient::with_api_key(EXCEPTIONLESS_API_KEY), + events: Mutex::new(Vec::new()), + } + } + + pub async fn flush(&self) { + let events = { + let mut guard = self.events.lock().unwrap(); + std::mem::take(&mut *guard) + }; + + if events.is_empty() { + return; + } + + let client = self.client.clone(); + let mut failed = 0; + + for event in events { + match event { + TelemetryEvent::FeatureUsage(event) => { + let builder = client + .feature(&event.feature_name) + .user_identity(&event.anonymous_identity) + .data("supportKey", event.support_key.as_str()); + + if builder.send().await.is_err() { + failed += 1; + } + } + TelemetryEvent::Error(event) => { + let error = ExceptionlessErrorOwned( + event.error_type.clone(), + event.message.clone(), + ); + + let builder = client + .error(&error) + .tag("error") + .source("httpgenerator") + .user_identity(&event.anonymous_identity) + .data("supportKey", event.support_key.as_str()) + .data("commandLine", event.command_line.as_str()) + .data("settings", serde_json::Value::String(event.settings_json)); + + if builder.send().await.is_err() { + failed += 1; + } + } + } + } + + if failed > 0 { + eprintln!("Warning: {failed} telemetry events failed to submit"); + } + } + + pub fn take_events(&self) -> Vec { + let mut guard = self.events.lock().unwrap(); + std::mem::take(&mut *guard) + } +} + +impl TelemetrySink for ExceptionlessTelemetrySink { + fn emit(&mut self, event: TelemetryEvent) { + if let Ok(mut guard) = self.events.lock() { + guard.push(event); + } + } +} + +#[derive(Debug)] +struct ExceptionlessErrorOwned(String, String); + +impl std::fmt::Display for ExceptionlessErrorOwned { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "[{}] {}", self.0, self.1) + } +} + +impl std::error::Error for ExceptionlessErrorOwned {} diff --git a/src/rust/cli/src/telemetry/mod.rs b/src/rust/cli/src/telemetry/mod.rs index 8be5b21e..4c4be5aa 100644 --- a/src/rust/cli/src/telemetry/mod.rs +++ b/src/rust/cli/src/telemetry/mod.rs @@ -1,11 +1,49 @@ mod events; +mod exceptionless; mod recorder; mod redaction; mod sink; pub use events::{ErrorEvent, FeatureUsageEvent, TelemetryContext, TelemetryEvent}; +pub use exceptionless::ExceptionlessTelemetrySink; pub use recorder::TelemetryRecorder; pub use sink::{MemoryTelemetrySink, NoopTelemetrySink, TelemetrySink}; +#[derive(Debug)] +pub enum TelemetrySinkCollection { + Exceptionless(ExceptionlessTelemetrySink), + Memory(MemoryTelemetrySink), + Noop, +} + +impl From for TelemetrySinkCollection { + fn from(sink: ExceptionlessTelemetrySink) -> Self { + TelemetrySinkCollection::Exceptionless(sink) + } +} + +impl TelemetrySinkCollection { + pub fn emit(&mut self, event: TelemetryEvent) { + match self { + TelemetrySinkCollection::Exceptionless(sink) => sink.emit(event), + TelemetrySinkCollection::Memory(sink) => sink.emit(event), + TelemetrySinkCollection::Noop => {} + } + } + + pub async fn flush(&self) { + if let TelemetrySinkCollection::Exceptionless(sink) = self { + sink.flush().await; + } + } + + pub fn events(&self) -> &[TelemetryEvent] { + match self { + TelemetrySinkCollection::Memory(sink) => sink.events(), + _ => &[], + } + } +} + #[cfg(test)] mod tests; diff --git a/src/rust/cli/src/telemetry/recorder.rs b/src/rust/cli/src/telemetry/recorder.rs index ab32242a..e707652c 100644 --- a/src/rust/cli/src/telemetry/recorder.rs +++ b/src/rust/cli/src/telemetry/recorder.rs @@ -6,19 +6,16 @@ use crate::args::CliArgs; use super::{ ErrorEvent, FeatureUsageEvent, TelemetryContext, TelemetryEvent, redaction::{feature_usage_names, redacted_command_line, redacted_settings}, - sink::TelemetrySink, + TelemetrySinkCollection, }; -pub struct TelemetryRecorder { +pub struct TelemetryRecorder { context: Option, - sink: S, + sink: TelemetrySinkCollection, } -impl TelemetryRecorder -where - S: TelemetrySink, -{ - pub fn from_cli_args(raw_args: &[OsString], args: &CliArgs, sink: S) -> Self { +impl TelemetryRecorder { + pub fn from_cli_args(raw_args: &[OsString], args: &CliArgs, sink: TelemetrySinkCollection) -> Self { let context = (!args.no_logging).then(|| { let anonymous_identity = anonymous_identity(); let support_key = support_key_from_anonymous_identity(&anonymous_identity); @@ -39,12 +36,11 @@ where }; for feature_name in feature_usage_names(args) { - self.sink - .emit(TelemetryEvent::FeatureUsage(FeatureUsageEvent { - feature_name, - support_key: context.support_key.clone(), - anonymous_identity: context.anonymous_identity.clone(), - })); + self.sink.emit(TelemetryEvent::FeatureUsage(FeatureUsageEvent { + feature_name, + support_key: context.support_key.clone(), + anonymous_identity: context.anonymous_identity.clone(), + })); } } @@ -67,7 +63,11 @@ where })); } - pub fn into_sink(self) -> S { + pub fn into_sink(self) -> TelemetrySinkCollection { self.sink } + + pub async fn flush(self) { + self.sink.flush().await; + } } diff --git a/src/rust/cli/src/telemetry/sink.rs b/src/rust/cli/src/telemetry/sink.rs index d61b759c..2283e885 100644 --- a/src/rust/cli/src/telemetry/sink.rs +++ b/src/rust/cli/src/telemetry/sink.rs @@ -1,4 +1,4 @@ -use super::TelemetryEvent; +use super::{TelemetryEvent, TelemetrySinkCollection}; pub trait TelemetrySink { fn emit(&mut self, event: TelemetryEvent); @@ -11,6 +11,12 @@ impl TelemetrySink for NoopTelemetrySink { fn emit(&mut self, _event: TelemetryEvent) {} } +impl From for TelemetrySinkCollection { + fn from(_: NoopTelemetrySink) -> Self { + TelemetrySinkCollection::Noop + } +} + #[derive(Debug, Default)] pub struct MemoryTelemetrySink { events: Vec, @@ -27,3 +33,9 @@ impl TelemetrySink for MemoryTelemetrySink { self.events.push(event); } } + +impl From for TelemetrySinkCollection { + fn from(sink: MemoryTelemetrySink) -> Self { + TelemetrySinkCollection::Memory(sink) + } +} diff --git a/src/rust/cli/src/telemetry/tests.rs b/src/rust/cli/src/telemetry/tests.rs index 05609f4c..9d29a0bc 100644 --- a/src/rust/cli/src/telemetry/tests.rs +++ b/src/rust/cli/src/telemetry/tests.rs @@ -5,6 +5,7 @@ use crate::args::{CliArgs, OutputTypeArg}; use super::{ MemoryTelemetrySink, TelemetryEvent, TelemetryRecorder, redaction::{feature_usage_names, redacted_command_line}, + sink::TelemetrySink, }; #[test] @@ -97,8 +98,9 @@ fn record_error_captures_redacted_settings_and_support_context() { OsString::from("--authorization-header"), OsString::from("Bearer secret-token"), ]; + let mut memory_sink = MemoryTelemetrySink::default(); let mut recorder = - TelemetryRecorder::from_cli_args(&raw_args, &args, MemoryTelemetrySink::default()); + TelemetryRecorder::from_cli_args(&raw_args, &args, memory_sink.into()); recorder.record_error(&args, "CliError", "boom"); @@ -141,7 +143,7 @@ fn no_logging_disables_feature_and_error_events() { let mut recorder = TelemetryRecorder::from_cli_args( &[OsString::from("httpgenerator")], &args, - MemoryTelemetrySink::default(), + MemoryTelemetrySink::default().into(), ); recorder.record_feature_usage(&args); @@ -160,7 +162,7 @@ fn record_feature_usage_emits_ordered_feature_events() { let mut recorder = TelemetryRecorder::from_cli_args( &[OsString::from("httpgenerator")], &args, - MemoryTelemetrySink::default(), + MemoryTelemetrySink::default().into(), ); recorder.record_feature_usage(&args); diff --git a/src/rust/cli/tests/facade_contract.rs b/src/rust/cli/tests/facade_contract.rs index c6edd884..012fb9ea 100644 --- a/src/rust/cli/tests/facade_contract.rs +++ b/src/rust/cli/tests/facade_contract.rs @@ -2,7 +2,7 @@ use std::{ffi::OsString, path::PathBuf}; use httpgenerator_cli::{ AzureAuthStatus, CliError, ExecutionObserver, ExecutionSummary, NoopTelemetrySink, - TelemetryRecorder, args, execute, execute_with_observer, should_attempt_azure_auth, telemetry, + TelemetryRecorder, TelemetrySinkCollection, args, execute, execute_with_observer, should_attempt_azure_auth, telemetry, }; struct ContractObserver; @@ -34,19 +34,12 @@ fn lib_facade_exposes_the_intentional_public_cli_surface() { assert!(!should_attempt_azure_auth(&cli_args)); let raw_args = [OsString::from("httpgenerator")]; - let mut root_recorder: TelemetryRecorder = - TelemetryRecorder::from_cli_args(&raw_args, &cli_args, NoopTelemetrySink); + let mut root_recorder = + TelemetryRecorder::from_cli_args(&raw_args, &cli_args, NoopTelemetrySink.into()); root_recorder.record_feature_usage(&cli_args); - let _: NoopTelemetrySink = root_recorder.into_sink(); - - struct ModuleSink; - - impl telemetry::TelemetrySink for ModuleSink { - fn emit(&mut self, _event: telemetry::TelemetryEvent) {} - } - - let _module_recorder = - telemetry::TelemetryRecorder::from_cli_args(&raw_args, &cli_args, ModuleSink); + let TelemetrySinkCollection::Noop = root_recorder.into_sink() else { + panic!("expected Noop sink"); + }; let feature_event = telemetry::TelemetryEvent::FeatureUsage(telemetry::FeatureUsageEvent { feature_name: "facade-contract".to_string(), diff --git a/src/rust/core/src/base_url.rs b/src/rust/core/src/base_url.rs index 393bcb8f..21927e0f 100644 --- a/src/rust/core/src/base_url.rs +++ b/src/rust/core/src/base_url.rs @@ -50,10 +50,10 @@ pub fn resolve_base_url( base_url.push_str(server_url); } - if let Some(configured_base_url) = configured_base_url { - if configured_base_url.starts_with("{{") && configured_base_url.ends_with("}}") { - return base_url; - } + if let Some(configured_base_url) = configured_base_url + && configured_base_url.starts_with("{{") && configured_base_url.ends_with("}}") + { + return base_url; } if !is_absolute_uri(&base_url) diff --git a/src/rust/core/src/openapi/error.rs b/src/rust/core/src/openapi/error.rs index 2c935a4d..c6f05d4f 100644 --- a/src/rust/core/src/openapi/error.rs +++ b/src/rust/core/src/openapi/error.rs @@ -205,7 +205,7 @@ pub enum TypedOpenApiParseError { /// Version detection failed before typed parsing could start. VersionDetection { source: OpenApiSource, - error: SpecificationVersionDetectionError, + error: Box, }, /// The crate does not expose a typed parser for the detected version. UnsupportedVersion { @@ -257,7 +257,7 @@ pub enum OpenApiDocumentLoadError { /// Loading the raw document failed. RawLoad(RawOpenApiLoadError), /// Parsing the typed document failed. - TypedParse(TypedOpenApiParseError), + TypedParse(Box), } impl fmt::Display for OpenApiDocumentLoadError { @@ -343,9 +343,9 @@ impl Error for OpenApiNormalizationError {} #[derive(Debug, Clone, PartialEq, Eq)] pub enum OpenApiDocumentNormalizationError { /// Loading or typed parsing failed. - Load(OpenApiDocumentLoadError), + Load(Box), /// Normalization of the loaded document failed. - Normalize(OpenApiNormalizationError), + Normalize(Box), } impl fmt::Display for OpenApiDocumentNormalizationError { diff --git a/src/rust/core/src/openapi/loader.rs b/src/rust/core/src/openapi/loader.rs index 60b74d6e..ffa5d135 100644 --- a/src/rust/core/src/openapi/loader.rs +++ b/src/rust/core/src/openapi/loader.rs @@ -60,14 +60,14 @@ pub enum LoadedOpenApiDocument { /// The decoded source document and its metadata. raw: RawOpenApiDocument, /// The parsed OpenAPI 3.0 model. - document: openapiv3::OpenAPI, + document: Box, }, /// An OpenAPI 3.1 document with both raw and typed representations. OpenApi31 { /// The decoded source document and its metadata. raw: RawOpenApiDocument, /// The parsed OpenAPI 3.1 model. - document: openapiv3_1::OpenApi, + document: Box, }, /// An OpenAPI 3.1 document kept as raw input because typed parsing was intentionally skipped. OpenApi31Raw { @@ -210,7 +210,7 @@ pub fn load_document_from_raw( }) if should_fallback_to_raw_openapi31(&raw, options.tolerate_invalid_openapi31) => { Ok(LoadedOpenApiDocument::OpenApi31Raw { raw }) } - Err(error) => Err(OpenApiDocumentLoadError::TypedParse(error)), + Err(error) => Err(OpenApiDocumentLoadError::TypedParse(Box::new(error))), } } diff --git a/src/rust/core/src/openapi/normalize/mod.rs b/src/rust/core/src/openapi/normalize/mod.rs index d36a9ee7..7f462525 100644 --- a/src/rust/core/src/openapi/normalize/mod.rs +++ b/src/rust/core/src/openapi/normalize/mod.rs @@ -39,8 +39,8 @@ pub fn load_and_normalize_document( options: LoadOptions, ) -> Result { let document = - load_document(input, options).map_err(OpenApiDocumentNormalizationError::Load)?; - normalize_loaded_document(&document).map_err(OpenApiDocumentNormalizationError::Normalize) + load_document(input, options).map_err(|e| OpenApiDocumentNormalizationError::Load(Box::new(e)))?; + normalize_loaded_document(&document).map_err(|e| OpenApiDocumentNormalizationError::Normalize(Box::new(e))) } /// Normalizes a previously loaded document into the generator's stable handoff model. diff --git a/src/rust/core/src/openapi/normalize/parameters.rs b/src/rust/core/src/openapi/normalize/parameters.rs index c1aa3701..cbc1be4b 100644 --- a/src/rust/core/src/openapi/normalize/parameters.rs +++ b/src/rust/core/src/openapi/normalize/parameters.rs @@ -28,13 +28,13 @@ pub(super) fn normalize_parameters( }; let parameter_key = normalized.inline_key(); - if let Some(parameter_key) = parameter_key { - if let Some(index) = merged.iter().position(|existing: &NormalizedParameter| { + if let Some(parameter_key) = parameter_key + && let Some(index) = merged.iter().position(|existing: &NormalizedParameter| { existing.inline_key() == Some(parameter_key) - }) { - merged[index] = normalized; - continue; - } + }) + { + merged[index] = normalized; + continue; } merged.push(normalized); diff --git a/src/rust/core/src/openapi/typed.rs b/src/rust/core/src/openapi/typed.rs index 471ecb93..1379f922 100644 --- a/src/rust/core/src/openapi/typed.rs +++ b/src/rust/core/src/openapi/typed.rs @@ -9,9 +9,9 @@ use super::{OpenApiSpecificationVersion, RawOpenApiDocument, TypedOpenApiParseEr /// Version-specific typed OpenAPI models exposed by this crate. pub enum TypedOpenApiDocument { /// A parsed [`openapiv3::OpenAPI`] document. - OpenApi30(openapiv3::OpenAPI), + OpenApi30(Box), /// A parsed [`openapiv3_1::OpenApi`] document. - OpenApi31(openapiv3_1::OpenApi), + OpenApi31(Box), } impl TypedOpenApiDocument { @@ -57,7 +57,7 @@ pub fn parse_typed_document( match document.specification_version().map_err(|error| { TypedOpenApiParseError::VersionDetection { source: document.source().clone(), - error, + error: Box::new(error), } })? { OpenApiSpecificationVersion::Swagger2 => Err(TypedOpenApiParseError::UnsupportedVersion { @@ -65,10 +65,12 @@ pub fn parse_typed_document( version: OpenApiSpecificationVersion::Swagger2, }), OpenApiSpecificationVersion::OpenApi30 => { - parse_openapi30_document(document).map(TypedOpenApiDocument::OpenApi30) + let document = parse_openapi30_document(document)?; + Ok(TypedOpenApiDocument::OpenApi30(Box::new(document))) } OpenApiSpecificationVersion::OpenApi31 => { - parse_openapi31_document(document).map(TypedOpenApiDocument::OpenApi31) + let document = parse_openapi31_document(document)?; + Ok(TypedOpenApiDocument::OpenApi31(Box::new(document))) } } } @@ -103,7 +105,7 @@ where let detected_version = document.specification_version().map_err(|error| { TypedOpenApiParseError::VersionDetection { source: document.source().clone(), - error, + error: Box::new(error), } })?;