diff --git a/progenitor-impl/src/lib.rs b/progenitor-impl/src/lib.rs index e7e24f5..b3af7f4 100644 --- a/progenitor-impl/src/lib.rs +++ b/progenitor-impl/src/lib.rs @@ -179,18 +179,17 @@ impl Generator { .flat_map(|(path, ref_or_item)| { // Exclude externally defined path items. let item = ref_or_item.as_item().unwrap(); - // TODO punt on parameters that apply to all path items for now. - assert!(item.parameters.is_empty()); item.iter().map(move |(method, operation)| { - (path.as_str(), method, operation) + (path.as_str(), method, operation, &item.parameters) }) }) - .map(|(path, method, operation)| { + .map(|(path, method, operation, path_parameters)| { self.process_operation( operation, &spec.components, path, method, + path_parameters, ) }) .collect::>>()?; diff --git a/progenitor-impl/src/method.rs b/progenitor-impl/src/method.rs index 63e8db1..1f8b8b3 100644 --- a/progenitor-impl/src/method.rs +++ b/progenitor-impl/src/method.rs @@ -6,14 +6,14 @@ use std::{ str::FromStr, }; -use openapiv3::{Components, Response, StatusCode}; +use openapiv3::{Components, Parameter, ReferenceOr, Response, StatusCode}; use proc_macro2::TokenStream; use quote::{format_ident, quote, ToTokens}; use typify::TypeId; use crate::{ template::PathTemplate, - util::{sanitize, Case}, + util::{items, parameter_map, sanitize, Case}, Error, Generator, Result, TagStyle, }; use crate::{to_schema::ToSchema, util::ReferenceOrExt}; @@ -237,15 +237,25 @@ impl Generator { components: &Option, path: &str, method: &str, + path_parameters: &[ReferenceOr], ) -> Result { let operation_id = operation.operation_id.as_ref().unwrap(); let mut query: Vec<(String, bool)> = Vec::new(); - let mut params = operation - .parameters - .iter() + + let mut combined_path_parameters = + parameter_map(&path_parameters, &components)?; + for operation_param in items(&operation.parameters, &components) { + let parameter = operation_param?; + combined_path_parameters + .insert(¶meter.parameter_data_ref().name, parameter); + } + + // Filter out any path parameters that have been overridden by an operation parameter + let mut params = combined_path_parameters + .values() .map(|parameter| { - match parameter.item(components)? { + match parameter { openapiv3::Parameter::Path { parameter_data, style: openapiv3::PathStyle::Simple, @@ -760,6 +770,7 @@ impl Generator { _ => None, }) .collect(); + let url_path = method.path.compile(url_renames, client.clone()); // Generate code to handle the body param. diff --git a/progenitor-impl/src/util.rs b/progenitor-impl/src/util.rs index 6227c6c..e7c018f 100644 --- a/progenitor-impl/src/util.rs +++ b/progenitor-impl/src/util.rs @@ -1,5 +1,7 @@ // Copyright 2022 Oxide Computer Company +use std::collections::BTreeMap; + use indexmap::IndexMap; use openapiv3::{ Components, Parameter, ReferenceOr, RequestBody, Response, Schema, @@ -32,6 +34,25 @@ impl ReferenceOrExt for openapiv3::ReferenceOr { } } +pub(crate) fn items<'a, T>( + refs: &'a [ReferenceOr], + components: &'a Option, +) -> impl Iterator> +where + T: ComponentLookup, +{ + refs.iter().map(|r| r.item(components)) +} + +pub(crate) fn parameter_map<'a>( + refs: &'a [ReferenceOr], + components: &'a Option, +) -> Result> { + items(refs, components) + .map(|res| res.map(|param| (¶m.parameter_data_ref().name, param))) + .collect() +} + impl ComponentLookup for Parameter { fn get_components( components: &Components, diff --git a/progenitor-impl/tests/output/param-overrides-builder-tagged.out b/progenitor-impl/tests/output/param-overrides-builder-tagged.out new file mode 100644 index 0000000..e971fdb --- /dev/null +++ b/progenitor-impl/tests/output/param-overrides-builder-tagged.out @@ -0,0 +1,141 @@ +#[allow(unused_imports)] +use progenitor_client::{encode_path, RequestBuilderExt}; +pub use progenitor_client::{ByteStream, Error, ResponseValue}; +pub mod types { + use serde::{Deserialize, Serialize}; + #[allow(unused_imports)] + use std::convert::TryFrom; +} + +#[derive(Clone, Debug)] +pub struct Client { + pub(crate) baseurl: String, + pub(crate) client: reqwest::Client, +} + +impl Client { + pub fn new(baseurl: &str) -> Self { + let dur = std::time::Duration::from_secs(15); + let client = reqwest::ClientBuilder::new() + .connect_timeout(dur) + .timeout(dur) + .build() + .unwrap(); + Self::new_with_client(baseurl, client) + } + + pub fn new_with_client(baseurl: &str, client: reqwest::Client) -> Self { + Self { + baseurl: baseurl.to_string(), + client, + } + } + + pub fn baseurl(&self) -> &String { + &self.baseurl + } + + pub fn client(&self) -> &reqwest::Client { + &self.client + } +} + +impl Client { + ///Gets a key + /// + ///Sends a `GET` request to `/key` + /// + ///Arguments: + /// - `key`: The same key parameter that overlaps with the path level + /// parameter + /// - `unique_key`: A key parameter that will not be overridden by the path + /// spec + /// + ///```ignore + /// let response = client.key_get() + /// .key(key) + /// .unique_key(unique_key) + /// .send() + /// .await; + /// ``` + pub fn key_get(&self) -> builder::KeyGet { + builder::KeyGet::new(self) + } +} + +pub mod builder { + use super::types; + #[allow(unused_imports)] + use super::{encode_path, ByteStream, Error, RequestBuilderExt, ResponseValue}; + ///Builder for [`Client::key_get`] + /// + ///[`Client::key_get`]: super::Client::key_get + #[derive(Debug, Clone)] + pub struct KeyGet<'a> { + client: &'a super::Client, + key: Result, String>, + unique_key: Result, String>, + } + + impl<'a> KeyGet<'a> { + pub fn new(client: &'a super::Client) -> Self { + Self { + client, + key: Ok(None), + unique_key: Ok(None), + } + } + + pub fn key(mut self, value: V) -> Self + where + V: std::convert::TryInto, + { + self.key = value + .try_into() + .map(Some) + .map_err(|_| "conversion to `Option < bool >` for key failed".to_string()); + self + } + + pub fn unique_key(mut self, value: V) -> Self + where + V: std::convert::TryInto, + { + self.unique_key = value + .try_into() + .map(Some) + .map_err(|_| "conversion to `Option < String >` for unique_key failed".to_string()); + self + } + + ///Sends a `GET` request to `/key` + pub async fn send(self) -> Result, Error<()>> { + let Self { + client, + key, + unique_key, + } = self; + let key = key.map_err(Error::InvalidRequest)?; + let unique_key = unique_key.map_err(Error::InvalidRequest)?; + let url = format!("{}/key", client.baseurl,); + let mut query = Vec::new(); + if let Some(v) = &key { + query.push(("key", v.to_string())); + } + if let Some(v) = &unique_key { + query.push(("uniqueKey", v.to_string())); + } + let request = client.client.get(url).query(&query).build()?; + let result = client.client.execute(request).await; + let response = result?; + match response.status().as_u16() { + 200u16 => Ok(ResponseValue::empty(response)), + _ => Err(Error::UnexpectedResponse(response)), + } + } + } +} + +pub mod prelude { + pub use super::Client; +} diff --git a/progenitor-impl/tests/output/param-overrides-builder.out b/progenitor-impl/tests/output/param-overrides-builder.out new file mode 100644 index 0000000..e3dcf9a --- /dev/null +++ b/progenitor-impl/tests/output/param-overrides-builder.out @@ -0,0 +1,141 @@ +#[allow(unused_imports)] +use progenitor_client::{encode_path, RequestBuilderExt}; +pub use progenitor_client::{ByteStream, Error, ResponseValue}; +pub mod types { + use serde::{Deserialize, Serialize}; + #[allow(unused_imports)] + use std::convert::TryFrom; +} + +#[derive(Clone, Debug)] +pub struct Client { + pub(crate) baseurl: String, + pub(crate) client: reqwest::Client, +} + +impl Client { + pub fn new(baseurl: &str) -> Self { + let dur = std::time::Duration::from_secs(15); + let client = reqwest::ClientBuilder::new() + .connect_timeout(dur) + .timeout(dur) + .build() + .unwrap(); + Self::new_with_client(baseurl, client) + } + + pub fn new_with_client(baseurl: &str, client: reqwest::Client) -> Self { + Self { + baseurl: baseurl.to_string(), + client, + } + } + + pub fn baseurl(&self) -> &String { + &self.baseurl + } + + pub fn client(&self) -> &reqwest::Client { + &self.client + } +} + +impl Client { + ///Gets a key + /// + ///Sends a `GET` request to `/key` + /// + ///Arguments: + /// - `key`: The same key parameter that overlaps with the path level + /// parameter + /// - `unique_key`: A key parameter that will not be overridden by the path + /// spec + /// + ///```ignore + /// let response = client.key_get() + /// .key(key) + /// .unique_key(unique_key) + /// .send() + /// .await; + /// ``` + pub fn key_get(&self) -> builder::KeyGet { + builder::KeyGet::new(self) + } +} + +pub mod builder { + use super::types; + #[allow(unused_imports)] + use super::{encode_path, ByteStream, Error, RequestBuilderExt, ResponseValue}; + ///Builder for [`Client::key_get`] + /// + ///[`Client::key_get`]: super::Client::key_get + #[derive(Debug, Clone)] + pub struct KeyGet<'a> { + client: &'a super::Client, + key: Result, String>, + unique_key: Result, String>, + } + + impl<'a> KeyGet<'a> { + pub fn new(client: &'a super::Client) -> Self { + Self { + client, + key: Ok(None), + unique_key: Ok(None), + } + } + + pub fn key(mut self, value: V) -> Self + where + V: std::convert::TryInto, + { + self.key = value + .try_into() + .map(Some) + .map_err(|_| "conversion to `Option < bool >` for key failed".to_string()); + self + } + + pub fn unique_key(mut self, value: V) -> Self + where + V: std::convert::TryInto, + { + self.unique_key = value + .try_into() + .map(Some) + .map_err(|_| "conversion to `Option < String >` for unique_key failed".to_string()); + self + } + + ///Sends a `GET` request to `/key` + pub async fn send(self) -> Result, Error<()>> { + let Self { + client, + key, + unique_key, + } = self; + let key = key.map_err(Error::InvalidRequest)?; + let unique_key = unique_key.map_err(Error::InvalidRequest)?; + let url = format!("{}/key", client.baseurl,); + let mut query = Vec::new(); + if let Some(v) = &key { + query.push(("key", v.to_string())); + } + if let Some(v) = &unique_key { + query.push(("uniqueKey", v.to_string())); + } + let request = client.client.get(url).query(&query).build()?; + let result = client.client.execute(request).await; + let response = result?; + match response.status().as_u16() { + 200u16 => Ok(ResponseValue::empty(response)), + _ => Err(Error::UnexpectedResponse(response)), + } + } + } +} + +pub mod prelude { + pub use self::super::Client; +} diff --git a/progenitor-impl/tests/output/param-overrides-positional.out b/progenitor-impl/tests/output/param-overrides-positional.out new file mode 100644 index 0000000..7b565e0 --- /dev/null +++ b/progenitor-impl/tests/output/param-overrides-positional.out @@ -0,0 +1,80 @@ +#[allow(unused_imports)] +use progenitor_client::{encode_path, RequestBuilderExt}; +pub use progenitor_client::{ByteStream, Error, ResponseValue}; +pub mod types { + use serde::{Deserialize, Serialize}; + #[allow(unused_imports)] + use std::convert::TryFrom; +} + +#[derive(Clone, Debug)] +pub struct Client { + pub(crate) baseurl: String, + pub(crate) client: reqwest::Client, +} + +impl Client { + pub fn new(baseurl: &str) -> Self { + let dur = std::time::Duration::from_secs(15); + let client = reqwest::ClientBuilder::new() + .connect_timeout(dur) + .timeout(dur) + .build() + .unwrap(); + Self::new_with_client(baseurl, client) + } + + pub fn new_with_client(baseurl: &str, client: reqwest::Client) -> Self { + Self { + baseurl: baseurl.to_string(), + client, + } + } + + pub fn baseurl(&self) -> &String { + &self.baseurl + } + + pub fn client(&self) -> &reqwest::Client { + &self.client + } +} + +impl Client { + ///Gets a key + /// + ///Sends a `GET` request to `/key` + /// + ///Arguments: + /// - `key`: The same key parameter that overlaps with the path level + /// parameter + /// - `unique_key`: A key parameter that will not be overridden by the path + /// spec + pub async fn key_get<'a>( + &'a self, + key: Option, + unique_key: Option<&'a str>, + ) -> Result, Error<()>> { + let url = format!("{}/key", self.baseurl,); + let mut query = Vec::new(); + if let Some(v) = &key { + query.push(("key", v.to_string())); + } + + if let Some(v) = &unique_key { + query.push(("uniqueKey", v.to_string())); + } + + let request = self.client.get(url).query(&query).build()?; + let result = self.client.execute(request).await; + let response = result?; + match response.status().as_u16() { + 200u16 => Ok(ResponseValue::empty(response)), + _ => Err(Error::UnexpectedResponse(response)), + } + } +} + +pub mod prelude { + pub use super::Client; +} diff --git a/progenitor-impl/tests/test_output.rs b/progenitor-impl/tests/test_output.rs index 8bd129a..7abb38f 100644 --- a/progenitor-impl/tests/test_output.rs +++ b/progenitor-impl/tests/test_output.rs @@ -66,6 +66,11 @@ fn test_propolis_server() { verify_apis("propolis-server"); } +#[test] +fn test_param_override() { + verify_apis("param-overrides"); +} + // TODO this file is full of inconsistencies and incorrectly specified types. // It's an interesting test to consider whether we try to do our best to // interpret the intent or just fail. diff --git a/sample_openapi/param-overrides.json b/sample_openapi/param-overrides.json new file mode 100644 index 0000000..a5de3c8 --- /dev/null +++ b/sample_openapi/param-overrides.json @@ -0,0 +1,60 @@ +{ + "openapi": "3.0.0", + "info": { + "description": "Minimal API for testing parameter overrides", + "title": "Parameter override test", + "version": "v1" + }, + "paths": { + "/key": { + "get": { + "description": "Gets a key", + "operationId": "key.get", + "parameters": [ + { + "description": "The same key parameter that overlaps with the path level parameter", + "in": "query", + "name": "key", + "schema": { + "type": "boolean" + } + } + ], + "responses": { + "200": { + "type": "string", + "description": "Successful response" + } + } + }, + "parameters": [ + { + "$ref": "#/components/parameters/key" + }, + { + "$ref": "#/components/parameters/unique-key" + } + ] + } + }, + "components": { + "parameters": { + "key": { + "description": "A key parameter that will be overridden by the path spec", + "in": "query", + "name": "key", + "schema": { + "type": "string" + } + }, + "unique-key": { + "description": "A key parameter that will not be overridden by the path spec", + "in": "query", + "name": "uniqueKey", + "schema": { + "type": "string" + } + } + } + } +} \ No newline at end of file