From ef4a16777fdd69c815eeebde9af4eef8b235b43f Mon Sep 17 00:00:00 2001 From: Rain Date: Thu, 18 Dec 2025 23:24:11 +0000 Subject: [PATCH 1/4] [spr] initial version Created using spr 1.3.6-beta.1 --- crates/dropshot-api-manager/src/output.rs | 29 ++++++ crates/dropshot-api-manager/src/resolved.rs | 29 ++++++ crates/integration-tests/src/fixtures.rs | 98 ++++++++++++++++++- .../tests/integration/versioned.rs | 50 +++++++++- e2e-example/apis/src/lib.rs | 11 ++- .../versioned/versioned-3.0.0-b48dc9.json | 85 ++++++++++++++++ .../documents/versioned/versioned-latest.json | 2 +- 7 files changed, 290 insertions(+), 14 deletions(-) create mode 100644 e2e-example/documents/versioned/versioned-3.0.0-b48dc9.json diff --git a/crates/dropshot-api-manager/src/output.rs b/crates/dropshot-api-manager/src/output.rs index aa240b1..9ab04e8 100644 --- a/crates/dropshot-api-manager/src/output.rs +++ b/crates/dropshot-api-manager/src/output.rs @@ -529,6 +529,35 @@ pub fn display_resolution_problems<'a, T>( } } + // For BlessedLatestVersionBytewiseMismatch, show a diff between blessed + // and generated versions even though there's no fix. + if let Problem::BlessedLatestVersionBytewiseMismatch { + blessed, + generated, + } = p + { + let diff = + TextDiff::from_lines(blessed.contents(), generated.contents()); + let path1 = + env.openapi_abs_dir().join(blessed.spec_file_name().path()); + let path2 = + env.openapi_abs_dir().join(generated.spec_file_name().path()); + let indent = " ".repeat(HEADER_WIDTH + 1); + let _ = write_diff( + &diff, + &path1, + &path2, + styles, + // context_radius: show enough context to understand the changes. + 3, + /* missing_newline_hint */ true, + &mut indent_write::io::IndentWriter::new( + &indent, + std::io::stderr(), + ), + ); + } + let Some(fix) = p.fix() else { continue; }; diff --git a/crates/dropshot-api-manager/src/resolved.rs b/crates/dropshot-api-manager/src/resolved.rs index f438238..a18acbc 100644 --- a/crates/dropshot-api-manager/src/resolved.rs +++ b/crates/dropshot-api-manager/src/resolved.rs @@ -176,6 +176,20 @@ pub enum Problem<'a> { )] BlessedVersionBroken { compatibility_issues: Vec }, + #[error( + "For the latest blessed version, the OpenAPI document generated from \ + the current code is wire-compatible but not bytewise \ + identical to the blessed document. This implies one or more \ + trivial changes such as type renames or documentation updates. \ + To proceed, bump the API version in the `api_versions!` macro; \ + unless you're introducing other changes, there's no need to make \ + changes to the API itself." + )] + BlessedLatestVersionBytewiseMismatch { + blessed: &'a BlessedApiSpecFile, + generated: &'a GeneratedApiSpecFile, + }, + #[error( "No local OpenAPI document was found for this lockstep API. This is \ only expected if you're adding a new lockstep API. This tool can \ @@ -281,6 +295,7 @@ impl<'a> Problem<'a> { } Problem::BlessedVersionCompareError { .. } => None, Problem::BlessedVersionBroken { .. } => None, + Problem::BlessedLatestVersionBytewiseMismatch { .. } => None, Problem::LockstepMissingLocal { generated } | Problem::LockstepStale { generated, .. } => { Some(Fix::UpdateLockstepFile { generated }) @@ -945,6 +960,7 @@ fn resolve_api_version_blessed<'a>( local: &'a [LocalApiSpecFile], ) -> Resolution<'a> { let mut problems = Vec::new(); + let is_latest = version.is_latest; // Validate the generated API document. // @@ -973,6 +989,19 @@ fn resolve_api_version_blessed<'a>( } }; + // For the latest version, also require bytewise equality. This ensures that + // trivial changes don't accumulate invisibly. If the generated spec is + // semantically equivalent but bytewise different, require a version bump. + if is_latest + && problems.is_empty() + && generated.contents() != blessed.contents() + { + problems.push(Problem::BlessedLatestVersionBytewiseMismatch { + blessed, + generated, + }); + } + // Now, there should be at least one local spec that exactly matches the // blessed one. let (matching, non_matching): (Vec<_>, Vec<_>) = diff --git a/crates/integration-tests/src/fixtures.rs b/crates/integration-tests/src/fixtures.rs index 3250871..18a3100 100644 --- a/crates/integration-tests/src/fixtures.rs +++ b/crates/integration-tests/src/fixtures.rs @@ -521,8 +521,7 @@ pub mod versioned_health_skip_middle { rqctx: RequestContext, ) -> Result, HttpError>; - /// Get detailed health status (v2+, but only available in v3 since we - /// skip v2). + /// Get detailed health status (v2+). #[endpoint { method = GET, path = "/health/detailed", @@ -626,6 +625,64 @@ pub mod versioned_health_incompat { }; } +/// Versioned health API with 4 versions (adds v4 to the base API). Used to test +/// that older versions (v1-v3) pass with semantic equality when v4 is the +/// latest. +pub mod versioned_health_with_v4 { + use super::*; + use dropshot_api_manager_types::api_versions; + + api_versions!([ + (4, WITH_ENHANCED_METRICS), + (3, WITH_METRICS), + (2, WITH_DETAILED_STATUS), + (1, INITIAL), + ]); + + #[dropshot::api_description { module = "api_mod" }] + pub trait VersionedHealthApi { + type Context; + + /// Check if the service is healthy (all versions). + #[endpoint { + method = GET, + path = "/health", + operation_id = "health_check", + versions = "1.0.0".. + }] + async fn health_check( + rqctx: RequestContext, + ) -> Result, HttpError>; + + /// Get detailed health status (v2+). + #[endpoint { + method = GET, + path = "/health/detailed", + operation_id = "detailed_health_check", + versions = "2.0.0".. + }] + async fn detailed_health_check( + rqctx: RequestContext, + ) -> Result, HttpError>; + + /// Get service metrics (v3+). + #[endpoint { + method = GET, + path = "/metrics", + operation_id = "get_metrics", + versions = "3.0.0".. + }] + async fn get_metrics( + rqctx: RequestContext, + ) -> Result, HttpError>; + } + + // Reuse response types from the main versioned_health module. + pub use super::versioned_health::{ + DependencyStatus, DetailedHealthStatus, HealthStatusV1, ServiceMetrics, + }; +} + pub fn versioned_health_api() -> ManagedApiConfig { ManagedApiConfig { ident: "versioned-health", @@ -778,6 +835,33 @@ pub fn versioned_health_trivial_change_apis() -> Result { .context("failed to create trivial change versioned health ManagedApis") } +/// Create versioned health API with trivial changes AND a new version (v4). +/// This allows testing that older versions (v1-v3) pass with semantic equality +/// even when they have trivial changes, since v4 is now the latest. +pub fn versioned_health_trivial_change_with_new_latest_apis() +-> Result { + let config = ManagedApiConfig { + ident: "versioned-health", + versions: Versions::Versioned { + supported_versions: versioned_health_with_v4::supported_versions(), + }, + // Trivial change: different title. + title: "Modified Versioned Health API", + metadata: ManagedApiMetadata { + // Trivial change: different description. + description: Some("A versioned health API with trivial changes"), + ..Default::default() + }, + api_description: + versioned_health_with_v4::api_mod::stub_api_description, + extra_validation: None, + }; + + ManagedApis::new(vec![config]).context( + "failed to create trivial change with new latest versioned health ManagedApis", + ) +} + /// Create versioned health API with reduced versions (simulating version /// removal). pub fn versioned_health_reduced_apis() -> Result { @@ -791,7 +875,11 @@ pub fn versioned_health_reduced_apis() -> Result { }, title: "Versioned Health API", metadata: ManagedApiMetadata { - description: Some("A versioned health API with reduced versions"), + // Use the same description as the original to ensure bytewise + // equality for unchanged versions. + description: Some( + "A versioned health API for testing version evolution", + ), ..Default::default() }, api_description: @@ -816,8 +904,10 @@ pub fn versioned_health_skip_middle_apis() -> Result { }, title: "Versioned Health API", metadata: ManagedApiMetadata { + // Use the same description as the original to ensure bytewise + // equality for unchanged versions. description: Some( - "A versioned health API that skips middle version", + "A versioned health API for testing version evolution", ), ..Default::default() }, diff --git a/crates/integration-tests/tests/integration/versioned.rs b/crates/integration-tests/tests/integration/versioned.rs index c7482ea..16d6904 100644 --- a/crates/integration-tests/tests/integration/versioned.rs +++ b/crates/integration-tests/tests/integration/versioned.rs @@ -299,13 +299,14 @@ fn test_blessed_document_lifecycle() -> Result<()> { Ok(()) } -/// Test that API changes require regeneration and recommitting. +/// Test that trivial changes to the latest blessed version require a version +/// bump. #[test] -fn test_blessed_api_changes_should_not_do_trivial_updates() -> Result<()> { +fn test_blessed_api_trivial_changes_fail_for_latest() -> Result<()> { let env = TestEnvironment::new()?; let apis = versioned_health_apis()?; - // Generate and commit initial documents. + // Generate and commit initial documents (v1, v2, v3). env.generate_documents(&apis)?; env.commit_documents()?; @@ -314,9 +315,50 @@ fn test_blessed_api_changes_should_not_do_trivial_updates() -> Result<()> { assert_eq!(result, CheckResult::Success); // Create a modified API with trivial changes (different title/description). + // This affects all versions, including the latest (v3.0.0). let modified_apis = versioned_health_trivial_change_apis()?; - // The check should indicate that *no updates are needed* to the blessed version. + // The check should FAIL because the latest version (v3.0.0) has trivial + // changes that are semantically equivalent but bytewise different. This + // requires a version bump to make the changes visible in PR review. + let result = check_apis_up_to_date(env.environment(), &modified_apis)?; + assert_eq!(result, CheckResult::Failures); + + Ok(()) +} + +/// Test that trivial changes to older (non-latest) blessed versions pass with +/// semantic equality only. +#[test] +fn test_blessed_api_trivial_changes_pass_for_older_versions() -> Result<()> { + let env = TestEnvironment::new()?; + let apis = versioned_health_apis()?; + + // Generate and commit initial documents (v1, v2, v3). + env.generate_documents(&apis)?; + env.commit_documents()?; + + // Verify initial state is up-to-date. + let result = check_apis_up_to_date(env.environment(), &apis)?; + assert_eq!(result, CheckResult::Success); + + // Create a modified API with trivial changes AND a new version (v4). + // The trivial changes affect v1, v2, v3, but v4 is now the latest. + let modified_apis = versioned_health_trivial_change_with_new_latest_apis()?; + + // The check should indicate NeedsUpdate because v4 is a new locally-added + // version that needs to be generated. Importantly, v1-v3 should pass + // despite having trivial changes because they're now older versions + // (semantic equality only). + let result = check_apis_up_to_date(env.environment(), &modified_apis)?; + assert_eq!(result, CheckResult::NeedsUpdate); + + // Generate the new v4 document. + env.generate_documents(&modified_apis)?; + + // Now everything should pass: v1-v3 use semantic equality (pass despite + // trivial changes), and v4 uses bytewise equality (passes because it was + // just generated). let result = check_apis_up_to_date(env.environment(), &modified_apis)?; assert_eq!(result, CheckResult::Success); diff --git a/e2e-example/apis/src/lib.rs b/e2e-example/apis/src/lib.rs index 24c353d..eb7d763 100644 --- a/e2e-example/apis/src/lib.rs +++ b/e2e-example/apis/src/lib.rs @@ -25,9 +25,10 @@ pub mod versioned { use serde::Serialize; api_versions!([ - // EXERCISE: Try uncommenting the following line, then running - // `cargo example-openapi generate`. - // (3, THREE_DOT_OH), + // Version 3.0.0 was added to capture bytewise changes to the schema + // serialization (e.g., the Number wrapper type being serialized as a + // separate schema instead of inlined). + (3, THREE_DOT_OH), (2, TWO_DOT_OH), (1, INITIAL), ]); @@ -77,8 +78,8 @@ pub mod versioned { #[derive(Serialize, JsonSchema)] struct ThingV2 { // Note: this was originally `thing_number: u32`, but a wrapper type was - // added afterwards to test out wire-compatible changes to the schema - // (which don't require changing the version). + // added afterwards to test out wire-compatible changes to the schema. + // This trivial change caused THREE_DOT_OH to be generated. thing_number: Number, } diff --git a/e2e-example/documents/versioned/versioned-3.0.0-b48dc9.json b/e2e-example/documents/versioned/versioned-3.0.0-b48dc9.json new file mode 100644 index 0000000..5de5b38 --- /dev/null +++ b/e2e-example/documents/versioned/versioned-3.0.0-b48dc9.json @@ -0,0 +1,85 @@ +{ + "openapi": "3.0.3", + "info": { + "title": "Versioned API", + "description": "A versioned API", + "version": "3.0.0" + }, + "paths": { + "/thing": { + "get": { + "summary": "Fetch `thing`", + "operationId": "get_thing", + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ThingV2" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + } + }, + "components": { + "schemas": { + "Error": { + "description": "Error information from a response.", + "type": "object", + "properties": { + "error_code": { + "type": "string" + }, + "message": { + "type": "string" + }, + "request_id": { + "type": "string" + } + }, + "required": [ + "message", + "request_id" + ] + }, + "Number": { + "type": "integer", + "format": "uint32", + "minimum": 0 + }, + "ThingV2": { + "type": "object", + "properties": { + "thing_number": { + "$ref": "#/components/schemas/Number" + } + }, + "required": [ + "thing_number" + ] + } + }, + "responses": { + "Error": { + "description": "Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + } + } + } +} diff --git a/e2e-example/documents/versioned/versioned-latest.json b/e2e-example/documents/versioned/versioned-latest.json index 0b4ac72..679e087 120000 --- a/e2e-example/documents/versioned/versioned-latest.json +++ b/e2e-example/documents/versioned/versioned-latest.json @@ -1 +1 @@ -versioned-2.0.0-88d282.json \ No newline at end of file +versioned-3.0.0-b48dc9.json \ No newline at end of file From cc3b97fee3b1dfce96d07727db181ad488670aab Mon Sep 17 00:00:00 2001 From: Rain Date: Thu, 18 Dec 2025 23:32:44 +0000 Subject: [PATCH 2/4] make error message clearer Created using spr 1.3.6-beta.1 --- crates/dropshot-api-manager/src/resolved.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/dropshot-api-manager/src/resolved.rs b/crates/dropshot-api-manager/src/resolved.rs index a18acbc..1b9370f 100644 --- a/crates/dropshot-api-manager/src/resolved.rs +++ b/crates/dropshot-api-manager/src/resolved.rs @@ -183,7 +183,7 @@ pub enum Problem<'a> { trivial changes such as type renames or documentation updates. \ To proceed, bump the API version in the `api_versions!` macro; \ unless you're introducing other changes, there's no need to make \ - changes to the API itself." + changes to any endpoints." )] BlessedLatestVersionBytewiseMismatch { blessed: &'a BlessedApiSpecFile, From a8d53c3ef1514d7e95e49bfd5cf16406ed46d02b Mon Sep 17 00:00:00 2001 From: Rain Date: Fri, 19 Dec 2025 00:05:02 +0000 Subject: [PATCH 3/4] allow the new behavior to be disabled Created using spr 1.3.6-beta.1 --- crates/dropshot-api-manager/src/apis.rs | 41 +++++++++++++++++-- crates/dropshot-api-manager/src/resolved.rs | 3 ++ crates/integration-tests/src/fixtures.rs | 29 ++++++++++++- .../tests/integration/lockstep.rs | 4 +- .../tests/integration/versioned.rs | 25 +++++++++++ 5 files changed, 95 insertions(+), 7 deletions(-) diff --git a/crates/dropshot-api-manager/src/apis.rs b/crates/dropshot-api-manager/src/apis.rs index 2a0906d..10755d0 100644 --- a/crates/dropshot-api-manager/src/apis.rs +++ b/crates/dropshot-api-manager/src/apis.rs @@ -46,9 +46,13 @@ pub struct ManagedApiConfig { pub extra_validation: Option)>, } -/// Used internally to describe an API managed by this tool. +/// Describes an API managed by the Dropshot API manager. +/// +/// This type is typically created from a [`ManagedApiConfig`] and can be +/// further configured using builder methods before being passed to +/// [`ManagedApis::new`]. #[derive(Debug)] -pub(crate) struct ManagedApi { +pub struct ManagedApi { /// The API-specific part of the filename that's used for API descriptions /// /// This string is sometimes used as an identifier for developers. @@ -73,6 +77,12 @@ pub(crate) struct ManagedApi { /// Extra validation to perform on the OpenAPI document, if any. extra_validation: Option)>, + + /// If true, allow trivial changes (doc updates, type renames) for the + /// latest blessed version without requiring version bumps. + /// + /// Default: false (bytewise check is performed for latest version). + allow_trivial_changes_for_latest: bool, } impl From for ManagedApi { @@ -84,6 +94,7 @@ impl From for ManagedApi { metadata: value.metadata, api_description: value.api_description, extra_validation: value.extra_validation, + allow_trivial_changes_for_latest: false, } } } @@ -174,6 +185,22 @@ impl ManagedApi { extra_validation(openapi, validation_context); } } + + /// Allow trivial changes (doc updates, type renames) for the latest + /// blessed version without requiring a version bump. + /// + /// By default, the latest blessed version requires bytewise equality + /// between blessed and generated documents. This prevents trivial changes + /// from accumulating invisibly. Calling this method allows semantic-only + /// checking for all versions, including the latest. + pub fn allow_trivial_changes_for_latest(mut self) -> Self { + self.allow_trivial_changes_for_latest = true; + self + } + + pub(crate) fn allows_trivial_changes_for_latest(&self) -> bool { + self.allow_trivial_changes_for_latest + } } /// Describes the Rust-defined configuration for all of the APIs managed by this @@ -192,10 +219,16 @@ impl ManagedApis { /// configurations. /// /// This is the main entry point for creating a new `ManagedApis` instance. - pub fn new(api_list: Vec) -> anyhow::Result { + /// Accepts any iterable of items that can be converted into [`ManagedApi`], + /// including `Vec` and `Vec`. + pub fn new(api_list: I) -> anyhow::Result + where + I: IntoIterator, + I::Item: Into, + { let mut apis = BTreeMap::new(); for api in api_list { - let api = ManagedApi::from(api); + let api = api.into(); if let Some(old) = apis.insert(api.ident.clone(), api) { bail!("API is defined twice: {:?}", &old.ident); } diff --git a/crates/dropshot-api-manager/src/resolved.rs b/crates/dropshot-api-manager/src/resolved.rs index 1b9370f..73177d3 100644 --- a/crates/dropshot-api-manager/src/resolved.rs +++ b/crates/dropshot-api-manager/src/resolved.rs @@ -992,7 +992,10 @@ fn resolve_api_version_blessed<'a>( // For the latest version, also require bytewise equality. This ensures that // trivial changes don't accumulate invisibly. If the generated spec is // semantically equivalent but bytewise different, require a version bump. + // + // This check can be disabled via `allow_trivial_changes_for_latest()`. if is_latest + && !api.allows_trivial_changes_for_latest() && problems.is_empty() && generated.contents() != blessed.contents() { diff --git a/crates/integration-tests/src/fixtures.rs b/crates/integration-tests/src/fixtures.rs index 18a3100..310a4e9 100644 --- a/crates/integration-tests/src/fixtures.rs +++ b/crates/integration-tests/src/fixtures.rs @@ -7,7 +7,7 @@ use chrono::{DateTime, Utc}; use dropshot::{ HttpError, HttpResponseOk, Path, Query, RequestContext, TypedBody, }; -use dropshot_api_manager::{ManagedApiConfig, ManagedApis}; +use dropshot_api_manager::{ManagedApi, ManagedApiConfig, ManagedApis}; use dropshot_api_manager_types::{ ManagedApiMetadata, ValidationContext, Versions, }; @@ -835,6 +835,33 @@ pub fn versioned_health_trivial_change_apis() -> Result { .context("failed to create trivial change versioned health ManagedApis") } +/// Create versioned health API with trivial changes, but with the +/// `allow_trivial_changes_for_latest` option set. This should pass the check +/// because the option allows semantic-only checking for the latest version. +pub fn versioned_health_trivial_change_allowed_apis() -> Result { + let config = ManagedApiConfig { + ident: "versioned-health", + versions: Versions::Versioned { + supported_versions: versioned_health::supported_versions(), + }, + // Trivial change: different title. + title: "Modified Versioned Health API", + metadata: ManagedApiMetadata { + // Trivial change: different description. + description: Some("A versioned health API with trivial changes"), + ..Default::default() + }, + api_description: + versioned_health::versioned_health_api_mod::stub_api_description, + extra_validation: None, + }; + + ManagedApis::new(vec![ + ManagedApi::from(config).allow_trivial_changes_for_latest(), + ]) + .context("failed to create trivial change allowed APIs") +} + /// Create versioned health API with trivial changes AND a new version (v4). /// This allows testing that older versions (v1-v3) pass with semantic equality /// even when they have trivial changes, since v4 is now the latest. diff --git a/crates/integration-tests/tests/integration/lockstep.rs b/crates/integration-tests/tests/integration/lockstep.rs index 04f6dd8..0cb2933 100644 --- a/crates/integration-tests/tests/integration/lockstep.rs +++ b/crates/integration-tests/tests/integration/lockstep.rs @@ -8,7 +8,7 @@ use anyhow::Result; use dropshot_api_manager::{ - ManagedApis, + ManagedApiConfig, ManagedApis, test_util::{CheckResult, check_apis_up_to_date}, }; use integration_tests::*; @@ -95,7 +95,7 @@ fn test_lockstep_multiple_apis() -> Result<()> { #[test] fn test_empty_api_set() -> Result<()> { let env = TestEnvironment::new()?; - let apis = ManagedApis::new(vec![])?; + let apis = ManagedApis::new(Vec::::new())?; env.generate_documents(&apis)?; diff --git a/crates/integration-tests/tests/integration/versioned.rs b/crates/integration-tests/tests/integration/versioned.rs index 16d6904..869d795 100644 --- a/crates/integration-tests/tests/integration/versioned.rs +++ b/crates/integration-tests/tests/integration/versioned.rs @@ -327,6 +327,31 @@ fn test_blessed_api_trivial_changes_fail_for_latest() -> Result<()> { Ok(()) } +/// Test that trivial changes to the latest blessed version pass when the +/// `allow_trivial_changes_for_latest` option is set. +#[test] +fn test_blessed_api_trivial_changes_pass_when_allowed() -> Result<()> { + let env = TestEnvironment::new()?; + let apis = versioned_health_apis()?; + + // Generate and commit initial documents (v1, v2, v3). + env.generate_documents(&apis)?; + env.commit_documents()?; + + // Verify initial state is up-to-date. + let result = check_apis_up_to_date(env.environment(), &apis)?; + assert_eq!(result, CheckResult::Success); + + // Create a modified API with trivial changes AND the option set. + let modified_apis = versioned_health_trivial_change_allowed_apis()?; + + // Should pass because the option allows trivial changes for latest. + let result = check_apis_up_to_date(env.environment(), &modified_apis)?; + assert_eq!(result, CheckResult::Success); + + Ok(()) +} + /// Test that trivial changes to older (non-latest) blessed versions pass with /// semantic equality only. #[test] From b2d8f070396900bdfe846f53cedbb1b963ec9984 Mon Sep 17 00:00:00 2001 From: Rain Date: Fri, 19 Dec 2025 00:08:03 +0000 Subject: [PATCH 4/4] add doc comments Created using spr 1.3.6-beta.1 --- crates/dropshot-api-manager/src/apis.rs | 49 ++++++++++++++----------- 1 file changed, 28 insertions(+), 21 deletions(-) diff --git a/crates/dropshot-api-manager/src/apis.rs b/crates/dropshot-api-manager/src/apis.rs index 10755d0..57b2634 100644 --- a/crates/dropshot-api-manager/src/apis.rs +++ b/crates/dropshot-api-manager/src/apis.rs @@ -100,41 +100,64 @@ impl From for ManagedApi { } impl ManagedApi { + /// Returns the API identifier. pub fn ident(&self) -> &ApiIdent { &self.ident } + /// Returns the API versions. pub fn versions(&self) -> &Versions { &self.versions } + /// Returns the API title. pub fn title(&self) -> &'static str { self.title } + /// Returns the API metadata. pub fn metadata(&self) -> &ManagedApiMetadata { &self.metadata } + /// Returns true if the API is lockstep. pub fn is_lockstep(&self) -> bool { self.versions.is_lockstep() } + /// Returns true if the API is versioned. pub fn is_versioned(&self) -> bool { self.versions.is_versioned() } - pub fn iter_versioned_versions( + /// Allows trivial changes (doc updates, type renames) for the latest + /// blessed version without requiring a version bump. + /// + /// By default, the latest blessed version requires bytewise equality + /// between blessed and generated documents. This prevents trivial changes + /// from accumulating invisibly. Calling this method allows semantic-only + /// checking for all versions, including the latest. + pub fn allow_trivial_changes_for_latest(mut self) -> Self { + self.allow_trivial_changes_for_latest = true; + self + } + + /// Returns true if trivial changes are allowed for the latest version. + pub fn allows_trivial_changes_for_latest(&self) -> bool { + self.allow_trivial_changes_for_latest + } + + pub(crate) fn iter_versioned_versions( &self, ) -> Option + '_> { self.versions.iter_versioned_versions() } - pub fn iter_versions_semver(&self) -> IterVersionsSemvers<'_> { + pub(crate) fn iter_versions_semver(&self) -> IterVersionsSemvers<'_> { self.versions.iter_versions_semvers() } - pub fn generate_openapi_doc( + pub(crate) fn generate_openapi_doc( &self, version: &semver::Version, ) -> anyhow::Result { @@ -147,7 +170,7 @@ impl ManagedApi { .context("generated document is not valid OpenAPI") } - pub fn generate_spec_bytes( + pub(crate) fn generate_spec_bytes( &self, version: &semver::Version, ) -> anyhow::Result> { @@ -176,7 +199,7 @@ impl ManagedApi { Ok(contents) } - pub fn extra_validation( + pub(crate) fn extra_validation( &self, openapi: &OpenAPI, validation_context: ValidationContext<'_>, @@ -185,22 +208,6 @@ impl ManagedApi { extra_validation(openapi, validation_context); } } - - /// Allow trivial changes (doc updates, type renames) for the latest - /// blessed version without requiring a version bump. - /// - /// By default, the latest blessed version requires bytewise equality - /// between blessed and generated documents. This prevents trivial changes - /// from accumulating invisibly. Calling this method allows semantic-only - /// checking for all versions, including the latest. - pub fn allow_trivial_changes_for_latest(mut self) -> Self { - self.allow_trivial_changes_for_latest = true; - self - } - - pub(crate) fn allows_trivial_changes_for_latest(&self) -> bool { - self.allow_trivial_changes_for_latest - } } /// Describes the Rust-defined configuration for all of the APIs managed by this