diff --git a/Cargo.lock b/Cargo.lock index 7840fd1..db041e3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -731,7 +731,7 @@ checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" [[package]] name = "openapi-aggregator" -version = "0.2.2" +version = "0.2.3" dependencies = [ "clap", "mockito", diff --git a/config.example.yaml b/config.example.yaml index fd5ae54..86c0990 100644 --- a/config.example.yaml +++ b/config.example.yaml @@ -47,3 +47,17 @@ merge: title: "My Aggregated API" version: "1.0.0" description: "Combined specification from multiple services" + + # Explicit servers for the merged spec (replaces any servers from sources) + # servers: + # - url: https://api.example.com + # description: Production + # - url: https://staging.example.com + # description: Staging + + # Explicit tags for the merged spec (replaces any tags from sources) + # tags: + # - name: pets + # description: Pet operations + # - name: users + # description: User operations diff --git a/src/config.rs b/src/config.rs index 0c26d1a..dc128e6 100644 --- a/src/config.rs +++ b/src/config.rs @@ -103,6 +103,13 @@ pub struct MergeConfig { pub tag_separator: String, /// Override the `info` block in the merged output. pub info: Option, + /// Explicit list of servers for the merged spec. + /// If set, replaces any servers collected from sources. + pub servers: Option>, + /// Explicit list of tags for the merged spec. + /// If set, replaces any tags collected from sources. + /// (Operation-level tag references are NOT rewritten; these should match.) + pub tags: Option>, } impl Default for MergeConfig { @@ -113,6 +120,8 @@ impl Default for MergeConfig { tag_prefix: TagPrefixStrategy::default(), tag_separator: default_tag_separator(), info: None, + servers: None, + tags: None, } } } @@ -152,3 +161,17 @@ pub struct InfoOverride { pub version: Option, pub description: Option, } + +/// A server entry for the merged spec. +#[derive(Debug, Clone, Deserialize, Serialize)] +pub struct ServerEntry { + pub url: String, + pub description: Option, +} + +/// A tag entry for the merged spec. +#[derive(Debug, Clone, Deserialize, Serialize)] +pub struct TagEntry { + pub name: String, + pub description: Option, +} diff --git a/src/lib.rs b/src/lib.rs index 73a82f5..e627eeb 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -4,7 +4,8 @@ pub mod merge; pub mod source; pub use config::{ - Config, ConflictStrategy, InfoOverride, MergeConfig, OutputFormat, Source, TagPrefixStrategy, + Config, ConflictStrategy, InfoOverride, MergeConfig, OutputFormat, ServerEntry, Source, + TagEntry, TagPrefixStrategy, }; pub use error::Error; pub use merge::merge_specs; diff --git a/src/merge.rs b/src/merge.rs index f1f1216..6027b2f 100644 --- a/src/merge.rs +++ b/src/merge.rs @@ -129,10 +129,39 @@ pub fn merge_specs( merged.insert("components".into(), Value::Object(comp_obj)); } - if !merged_tags.is_empty() { + // --- tags: config override takes priority, then merged from sources --- + if let Some(config_tags) = &config.tags { + let tags_value: Vec = config_tags + .iter() + .map(|t| { + let mut obj = Map::new(); + obj.insert("name".into(), Value::String(t.name.clone())); + if let Some(desc) = &t.description { + obj.insert("description".into(), Value::String(desc.clone())); + } + Value::Object(obj) + }) + .collect(); + merged.insert("tags".into(), Value::Array(tags_value)); + } else if !merged_tags.is_empty() { merged.insert("tags".into(), Value::Array(merged_tags)); } - if !merged_servers.is_empty() { + + // --- servers: config override takes priority, then merged from sources --- + if let Some(config_servers) = &config.servers { + let servers_value: Vec = config_servers + .iter() + .map(|s| { + let mut obj = Map::new(); + obj.insert("url".into(), Value::String(s.url.clone())); + if let Some(desc) = &s.description { + obj.insert("description".into(), Value::String(desc.clone())); + } + Value::Object(obj) + }) + .collect(); + merged.insert("servers".into(), Value::Array(servers_value)); + } else if !merged_servers.is_empty() { merged.insert("servers".into(), Value::Array(merged_servers)); } @@ -624,4 +653,82 @@ mod tests { let op_tags = merged["paths"]["/pets"]["get"]["tags"].as_array().unwrap(); assert_eq!(op_tags[0], "MyPets/pets"); } + + #[test] + fn merge_with_servers_override() { + use crate::config::ServerEntry; + + let specs = vec![("a".into(), "a".into(), petstore_spec())]; + let config = MergeConfig { + servers: Some(vec![ + ServerEntry { + url: "https://api.example.com".into(), + description: Some("Production".into()), + }, + ServerEntry { + url: "https://staging.example.com".into(), + description: None, + }, + ]), + ..Default::default() + }; + let merged = merge_specs(specs, &config).unwrap(); + + let servers = merged["servers"].as_array().unwrap(); + assert_eq!(servers.len(), 2); + assert_eq!(servers[0]["url"], "https://api.example.com"); + assert_eq!(servers[0]["description"], "Production"); + assert_eq!(servers[1]["url"], "https://staging.example.com"); + assert!(servers[1].get("description").is_none()); + } + + #[test] + fn merge_with_tags_override() { + use crate::config::TagEntry; + + let mut a = petstore_spec(); + a["tags"] = json!([{"name": "pets", "description": "From source"}]); + + let specs = vec![("a".into(), "a".into(), a)]; + let config = MergeConfig { + tags: Some(vec![ + TagEntry { + name: "animals".into(), + description: Some("Animal operations".into()), + }, + TagEntry { + name: "admin".into(), + description: None, + }, + ]), + ..Default::default() + }; + let merged = merge_specs(specs, &config).unwrap(); + + // Config tags override source tags entirely + let tags = merged["tags"].as_array().unwrap(); + assert_eq!(tags.len(), 2); + assert_eq!(tags[0]["name"], "animals"); + assert_eq!(tags[0]["description"], "Animal operations"); + assert_eq!(tags[1]["name"], "admin"); + } + + #[test] + fn merge_servers_from_sources_when_no_override() { + let mut a = petstore_spec(); + a["servers"] = json!([{"url": "https://a.example.com"}]); + let mut b = users_spec(); + b["servers"] = json!([ + {"url": "https://a.example.com"}, + {"url": "https://b.example.com"} + ]); + + let specs = vec![("a".into(), "a".into(), a), ("b".into(), "b".into(), b)]; + let config = MergeConfig::default(); + let merged = merge_specs(specs, &config).unwrap(); + + let servers = merged["servers"].as_array().unwrap(); + // Deduplicated by url + assert_eq!(servers.len(), 2); + } }