diff --git a/CHANGELOG.adoc b/CHANGELOG.adoc index e9be2e2b..eb00a647 100644 --- a/CHANGELOG.adoc +++ b/CHANGELOG.adoc @@ -20,6 +20,7 @@ https://github.com/oxidecomputer/progenitor/compare/v0.1.1\...HEAD[Full list of * Derive `Debug` for `Client` and builders for the various operations (#145) * Builders for `struct` types (#171) * Add a prelude that include the `Client` and any extension traits (#176) +* Added `Error::IoError` variant for channel connection failures (breaks `match` exhaustivity) == 0.1.1 (released 2022-05-13) diff --git a/Cargo.lock b/Cargo.lock index e835b576..1c9d3e33 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -985,6 +985,12 @@ version = "0.3.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "58893f751c9b0412871a09abd62ecd2a00298c6c83befa223ef98c52aef40cbe" +[[package]] +name = "ppv-lite86" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb9f9e6e233e5c4a35559a617bf40a4ec447db2e84c20b55a6f83167b7e57872" + [[package]] name = "proc-macro-error" version = "1.0.4" @@ -1026,6 +1032,7 @@ dependencies = [ "chrono", "clap", "futures", + "http", "openapiv3", "percent-encoding", "progenitor-client", @@ -1036,6 +1043,7 @@ dependencies = [ "schemars", "serde", "serde_json", + "tokio", "uuid", ] @@ -1043,13 +1051,17 @@ dependencies = [ name = "progenitor-client" version = "0.1.2-dev" dependencies = [ + "base64", "bytes", "futures-core", + "http", "percent-encoding", + "rand", "reqwest", "serde", "serde_json", "serde_urlencoded", + "tokio", ] [[package]] @@ -1100,6 +1112,36 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d34f1408f55294453790c48b2f1ebbb1c5b4b7563eb1f418bcfcfdbb06ebb4e7" +dependencies = [ + "getrandom", +] + [[package]] name = "redox_syscall" version = "0.2.10" diff --git a/progenitor-client/Cargo.toml b/progenitor-client/Cargo.toml index ed770a75..1c3403ff 100644 --- a/progenitor-client/Cargo.toml +++ b/progenitor-client/Cargo.toml @@ -9,8 +9,16 @@ description = "An OpenAPI client generator - client support" [dependencies] bytes = "1.2.1" futures-core = "0.3.23" +http = "0.2" percent-encoding = "2.1" reqwest = { version = "0.11", default-features = false, features = ["json", "stream"] } serde = "1.0" serde_json = "1.0" serde_urlencoded = "0.7.1" + +# deps for websocket support +base64 = "0.13" +rand = "0.8" +# reqwest wraps hyper, but does not expose a read/write socket for us to work with, +# so we must make direct socket connections. +tokio = { version = "1.0", features = ["net"] } diff --git a/progenitor-client/src/progenitor_client.rs b/progenitor-client/src/progenitor_client.rs index 7096294c..8be5c9a5 100644 --- a/progenitor-client/src/progenitor_client.rs +++ b/progenitor-client/src/progenitor_client.rs @@ -5,6 +5,7 @@ //! Support code for generated clients. use std::ops::{Deref, DerefMut}; +use std::str::FromStr; use bytes::Bytes; use futures_core::Stream; @@ -14,6 +15,9 @@ use serde::{de::DeserializeOwned, Serialize}; type InnerByteStream = std::pin::Pin> + Send + Sync>>; +// used for Websockets requests +type HttpBareRequest = http::Request<()>; + /// Untyped byte stream used for both success and error responses. pub struct ByteStream(InnerByteStream); @@ -195,6 +199,65 @@ impl std::fmt::Debug for ResponseValue { } } +/// Value returned by generated client methods for Websocket channels. +/// +/// Currently, the only interface available for Dropshot websocket channels is +/// providing their raw, unmodeled form (that is, Dropshot does not yet define +/// higher-level constructs for modeling the structure of Websocket messages +/// themselves). +/// +/// The user is responsible for passing it to a websocket implementation, i.e.: +/// ```ignore +/// let (request, tcp_stream) = my_progenitor_client +/// .my_websocket_channel() +/// .answer(42) +/// .send() +/// .await? +/// .into_request_and_tcp_stream(); +/// let ws_client = tokio_tungstenite::client_async(request, tcp_stream).await?; +/// ``` +/// +/// (As no request has been *made*, returning a [ResponseValue] would be inappropriate.) +pub struct WebsocketReactants { + request: HttpBareRequest, + tcp_stream: tokio::net::TcpStream, +} + +impl WebsocketReactants { + pub fn new( + rqw: reqwest::Request, + tcp_stream: tokio::net::TcpStream, + ) -> Result> { + // rebuild as http::Request, which tungstenite re-exports as its + // "IntoClientRequest" type. + // FIXME: this is obviously a hack, the better thing to do would be to + // implement using http::Request::builder() in the proc macro + let mut rb = http::Request::builder() + .method(rqw.method()) + .version(rqw.version()) + .uri( + http::Uri::from_str(rqw.url().as_str()) + .map_err(|e| Error::InvalidRequest(format!("{:?}", e)))?, + ); + for (k, v) in rqw.headers().iter() { + rb = rb.header(k, v); + } + let request = rb + .body(()) + .map_err(|e| Error::InvalidRequest(e.to_string()))?; + Ok(Self { + request, + tcp_stream, + }) + } + + pub fn into_request_and_tcp_stream( + self, + ) -> (HttpBareRequest, tokio::net::TcpStream) { + (self.request, self.tcp_stream) + } +} + /// Error produced by generated client methods. /// /// The type parameter may be a struct if there's a single expected error type @@ -207,6 +270,9 @@ pub enum Error { /// A server error either due to the data, or with the connection. CommunicationError(reqwest::Error), + /// A fundamental input/output error has occurred (e.g. unable to make a socket connection) + IoError(std::io::Error), + /// A documented, expected error response. ErrorResponse(ResponseValue), @@ -225,6 +291,7 @@ impl Error { match self { Error::InvalidRequest(_) => None, Error::CommunicationError(e) => e.status(), + Error::IoError(_) => None, Error::ErrorResponse(rv) => Some(rv.status()), Error::InvalidResponsePayload(e) => e.status(), Error::UnexpectedResponse(r) => Some(r.status()), @@ -239,6 +306,7 @@ impl Error { match self { Error::InvalidRequest(s) => Error::InvalidRequest(s), Error::CommunicationError(e) => Error::CommunicationError(e), + Error::IoError(e) => Error::IoError(e), Error::ErrorResponse(ResponseValue { inner: _, status, @@ -262,6 +330,12 @@ impl From for Error { } } +impl From for Error { + fn from(e: std::io::Error) -> Self { + Self::IoError(e) + } +} + impl std::fmt::Display for Error where ResponseValue: ErrorFormat, @@ -274,6 +348,9 @@ where Error::CommunicationError(e) => { write!(f, "Communication Error: {}", e) } + Error::IoError(e) => { + write!(f, "Input/Output Error: {}", e) + } Error::ErrorResponse(rve) => { write!(f, "Error Response: ")?; rve.fmt_info(f) @@ -377,3 +454,8 @@ impl RequestBuilderExt for RequestBuilder { })?)) } } + +#[doc(hidden)] +pub fn generate_websocket_key() -> String { + base64::encode(rand::random::<[u8; 16]>()) +} diff --git a/progenitor-impl/Cargo.toml b/progenitor-impl/Cargo.toml index 33a1368c..1a592b8e 100644 --- a/progenitor-impl/Cargo.toml +++ b/progenitor-impl/Cargo.toml @@ -23,7 +23,7 @@ thiserror = "1.0" # To publish, use a numbered version #typify = "0.0.9" typify = { git = "https://github.com/oxidecomputer/typify" } -unicode-ident = "1.0.3" +unicode-ident = "1.0.2" [dev-dependencies] dropshot = { git = "https://github.com/oxidecomputer/dropshot", default-features = false } diff --git a/progenitor-impl/src/lib.rs b/progenitor-impl/src/lib.rs index 49882108..5a23821e 100644 --- a/progenitor-impl/src/lib.rs +++ b/progenitor-impl/src/lib.rs @@ -26,6 +26,8 @@ pub enum Error { UnexpectedFormat(String), #[error("invalid operation path {0}")] InvalidPath(String), + #[error("invalid dropshot extension use: {0}")] + InvalidExtension(String), #[error("internal error {0}")] InternalError(String), } @@ -213,7 +215,13 @@ impl Generator { let file = quote! { // Re-export ResponseValue and Error since those are used by the // public interface of Client. - pub use progenitor_client::{ByteStream, Error, ResponseValue}; + pub use progenitor_client::{ + ByteStream, + Error, + ResponseValue, + WebsocketReactants, + generate_websocket_key, + }; #[allow(unused_imports)] use progenitor_client::{encode_path, RequestBuilderExt}; @@ -319,10 +327,12 @@ impl Generator { #[allow(unused_imports)] use super::{ encode_path, + generate_websocket_key, ByteStream, Error, RequestBuilderExt, ResponseValue, + WebsocketReactants, }; #[allow(unused_imports)] use std::convert::TryInto; @@ -358,11 +368,15 @@ impl Generator { #[allow(unused_imports)] use super::{ encode_path, + generate_websocket_key, ByteStream, Error, RequestBuilderExt, ResponseValue, + WebsocketReactants, }; + #[allow(unused_imports)] + use std::convert::TryInto; #(#builder_struct)* diff --git a/progenitor-impl/src/method.rs b/progenitor-impl/src/method.rs index a8c1b628..201176f4 100644 --- a/progenitor-impl/src/method.rs +++ b/progenitor-impl/src/method.rs @@ -29,6 +29,7 @@ pub(crate) struct OperationMethod { params: Vec, responses: Vec, dropshot_paginated: Option, + dropshot_websocket: bool, } enum HttpMethod { @@ -449,9 +450,20 @@ impl Generator { }); } + // TODO: should dropshot::{WEBSOCKET_EXTENSION, PAGINATION_EXTENSION} be pub for these? let dropshot_paginated = self.dropshot_pagination_data(operation, ¶ms, &responses); + let dropshot_websocket = + operation.extensions.get("x-dropshot-websocket").is_some(); + + if dropshot_websocket && dropshot_paginated.is_some() { + return Err(Error::InvalidExtension(format!( + "conflicting extensions in {:?}", + operation_id + ))); + } + Ok(OperationMethod { operation_id: sanitize(operation_id, Case::Snake), tags: operation.tags.clone(), @@ -465,6 +477,7 @@ impl Generator { params, responses, dropshot_paginated, + dropshot_websocket, }) } @@ -515,15 +528,19 @@ impl Generator { body, } = self.method_sig_body(method, quote! { self })?; + let ret_type = match method.dropshot_websocket { + false => { + quote!(Result, Error<#error_type>>) + } + true => quote!(Result<#success_type, Error<#error_type>>), + }; + let method_impl = quote! { #[doc = #doc_comment] pub async fn #operation_id #bounds ( &'a self, #(#params),* - ) -> Result< - ResponseValue<#success_type>, - Error<#error_type>, - > { + ) -> #ret_type { #body } }; @@ -705,6 +722,20 @@ impl Generator { (query_build, query_use) }; + let websock_hdrs = if method.dropshot_websocket { + quote! { + .header(reqwest::header::CONNECTION, "Upgrade") + .header(reqwest::header::UPGRADE, "websocket") + .header(reqwest::header::SEC_WEBSOCKET_VERSION, "13") + .header( + reqwest::header::SEC_WEBSOCKET_KEY, + generate_websocket_key(), + ) + } + } else { + quote! {} + }; + // Generate the path rename map; then use it to generate code for // assigning the path parameters to the `url` variable. let url_renames = method @@ -765,6 +796,26 @@ impl Generator { OperationResponseStatus::is_success_or_default, ); + let execute_request = if method.dropshot_websocket { + quote! { + let baseurl = #client.baseurl(); + let uri: reqwest::Url = baseurl.parse() + .map_err(|e| Error::InvalidRequest(format!("Invalid baseurl {:?}: {:?}", baseurl, e)))?; + let host = uri.host_str() + .ok_or_else(|| Error::InvalidRequest(format!("No host in baseurl {:?}", baseurl)))?; + let port = uri.port_or_known_default() + .ok_or_else(|| Error::InvalidRequest(format!("Cannot determine port from baseurl {:?}", baseurl)))?; + let addr = (host, port); + let sock_result = tokio::net::TcpStream::connect(addr).await; + } + } else { + quote! { + let result = #client.client + .execute(request) + .await; + } + }; + let success_response_matches = success_response_items.iter().map(|response| { let pat = match &response.status_code { @@ -871,6 +922,51 @@ impl Generator { let method_func = format_ident!("{}", method.method.as_str()); + let wrap_response = if method.dropshot_websocket { + quote! { + let tcp_stream = sock_result?; + Ok(WebsocketReactants::new(request, tcp_stream)?) + } + } else { + quote! { + let response = result?; + match response.status().as_u16() { + // These will be of the form... + // 201 => ResponseValue::from_response(response).await, + // 200..299 => ResponseValue::empty(response), + // TODO this kind of enumerated response isn't implemented + // ... or in the case of an operation with multiple + // successful response types... + // 200 => { + // ResponseValue::from_response() + // .await? + // .map(OperationXResponse::ResponseTypeA) + // } + // 201 => { + // ResponseValue::from_response() + // .await? + // .map(OperationXResponse::ResponseTypeB) + // } + #(#success_response_matches)* + + // This is almost identical to the success types except + // they are wrapped in Error::ErrorResponse... + // 400 => { + // Err(Error::ErrorResponse( + // ResponseValue::from_response(response.await?) + // )) + // } + #(#error_response_matches)* + + // The default response is either an Error with a known + // type if the operation defines a default (as above) or + // an Error::UnexpectedResponse... + // _ => Err(Error::UnexpectedResponse(response)), + #default_response + } + } + }; + let body_impl = quote! { #url_path #query_build @@ -879,49 +975,12 @@ impl Generator { . #method_func (url) #(#body_func)* #query_use + #websock_hdrs .build()?; #pre_hook - let result = #client.client - .execute(request) - .await; + #execute_request #post_hook - - let response = result?; - - match response.status().as_u16() { - // These will be of the form... - // 201 => ResponseValue::from_response(response).await, - // 200..299 => ResponseValue::empty(response), - // TODO this kind of enumerated response isn't implemented - // ... or in the case of an operation with multiple - // successful response types... - // 200 => { - // ResponseValue::from_response() - // .await? - // .map(OperationXResponse::ResponseTypeA) - // } - // 201 => { - // ResponseValue::from_response() - // .await? - // .map(OperationXResponse::ResponseTypeB) - // } - #(#success_response_matches)* - - // This is almost identical to the success types except - // they are wrapped in Error::ErrorResponse... - // 400 => { - // Err(Error::ErrorResponse( - // ResponseValue::from_response(response.await?) - // )) - // } - #(#error_response_matches)* - - // The default response is either an Error with a known - // type if the operation defines a default (as above) or - // an Error::UnexpectedResponse... - // _ => Err(Error::UnexpectedResponse(response)), - #default_response - } + #wrap_response }; Ok(MethodSigBody { @@ -936,6 +995,18 @@ impl Generator { method: &'a OperationMethod, filter: fn(&OperationResponseStatus) -> bool, ) -> (Vec<&'a OperationResponse>, TokenStream) { + if method.dropshot_websocket { + return ( + vec![], + // hack: we want to unconditionally return a generic error + // presently, as we don't model anything more sophisticated + match filter(&OperationResponseStatus::Code(200)) { + true => quote!(WebsocketReactants), + false => quote!(types::Error), + }, + ); + } + let mut response_items = method .responses .iter() @@ -1485,6 +1556,11 @@ impl Generator { } }; + let ret_type = match method.dropshot_websocket { + false => quote!(Result, Error<#error>>), + true => quote!(Result<#success, Error<#error>>), + }; + Ok(quote! { #[doc = #struct_doc] #derive @@ -1504,10 +1580,7 @@ impl Generator { #( #param_impls )* #[doc = #send_doc] - pub async fn send(self) -> Result< - ResponseValue<#success>, - Error<#error>, - > { + pub async fn send(self) -> #ret_type { // Destructure the builder for convenience. let Self { client, diff --git a/progenitor-impl/tests/output/buildomat-builder-tagged.out b/progenitor-impl/tests/output/buildomat-builder-tagged.out index fe9815cf..c8804fad 100644 --- a/progenitor-impl/tests/output/buildomat-builder-tagged.out +++ b/progenitor-impl/tests/output/buildomat-builder-tagged.out @@ -1,6 +1,8 @@ #[allow(unused_imports)] use progenitor_client::{encode_path, RequestBuilderExt}; -pub use progenitor_client::{ByteStream, Error, ResponseValue}; +pub use progenitor_client::{ + generate_websocket_key, ByteStream, Error, ResponseValue, WebsocketReactants, +}; pub mod types { use serde::{Deserialize, Serialize}; #[allow(unused_imports)] @@ -1500,7 +1502,12 @@ impl Client { pub mod builder { use super::types; #[allow(unused_imports)] - use super::{encode_path, ByteStream, Error, RequestBuilderExt, ResponseValue}; + use super::{ + encode_path, generate_websocket_key, ByteStream, Error, RequestBuilderExt, ResponseValue, + WebsocketReactants, + }; + #[allow(unused_imports)] + use std::convert::TryInto; ///Builder for [`Client::control_hold`] /// ///[`Client::control_hold`]: super::Client::control_hold diff --git a/progenitor-impl/tests/output/buildomat-builder.out b/progenitor-impl/tests/output/buildomat-builder.out index e1d4c58a..a926f1d3 100644 --- a/progenitor-impl/tests/output/buildomat-builder.out +++ b/progenitor-impl/tests/output/buildomat-builder.out @@ -1,6 +1,8 @@ #[allow(unused_imports)] use progenitor_client::{encode_path, RequestBuilderExt}; -pub use progenitor_client::{ByteStream, Error, ResponseValue}; +pub use progenitor_client::{ + generate_websocket_key, ByteStream, Error, ResponseValue, WebsocketReactants, +}; pub mod types { use serde::{Deserialize, Serialize}; #[allow(unused_imports)] @@ -1500,7 +1502,10 @@ impl Client { pub mod builder { use super::types; #[allow(unused_imports)] - use super::{encode_path, ByteStream, Error, RequestBuilderExt, ResponseValue}; + use super::{ + encode_path, generate_websocket_key, ByteStream, Error, RequestBuilderExt, ResponseValue, + WebsocketReactants, + }; #[allow(unused_imports)] use std::convert::TryInto; ///Builder for [`Client::control_hold`] diff --git a/progenitor-impl/tests/output/buildomat-positional.out b/progenitor-impl/tests/output/buildomat-positional.out index 6a039bb2..87567a85 100644 --- a/progenitor-impl/tests/output/buildomat-positional.out +++ b/progenitor-impl/tests/output/buildomat-positional.out @@ -1,6 +1,8 @@ #[allow(unused_imports)] use progenitor_client::{encode_path, RequestBuilderExt}; -pub use progenitor_client::{ByteStream, Error, ResponseValue}; +pub use progenitor_client::{ + generate_websocket_key, ByteStream, Error, ResponseValue, WebsocketReactants, +}; pub mod types { use serde::{Deserialize, Serialize}; #[allow(unused_imports)] diff --git a/progenitor-impl/tests/output/keeper-builder-tagged.out b/progenitor-impl/tests/output/keeper-builder-tagged.out index 00f1772b..73139c7b 100644 --- a/progenitor-impl/tests/output/keeper-builder-tagged.out +++ b/progenitor-impl/tests/output/keeper-builder-tagged.out @@ -1,6 +1,8 @@ #[allow(unused_imports)] use progenitor_client::{encode_path, RequestBuilderExt}; -pub use progenitor_client::{ByteStream, Error, ResponseValue}; +pub use progenitor_client::{ + generate_websocket_key, ByteStream, Error, ResponseValue, WebsocketReactants, +}; pub mod types { use serde::{Deserialize, Serialize}; #[allow(unused_imports)] @@ -829,7 +831,12 @@ impl Client { pub mod builder { use super::types; #[allow(unused_imports)] - use super::{encode_path, ByteStream, Error, RequestBuilderExt, ResponseValue}; + use super::{ + encode_path, generate_websocket_key, ByteStream, Error, RequestBuilderExt, ResponseValue, + WebsocketReactants, + }; + #[allow(unused_imports)] + use std::convert::TryInto; ///Builder for [`Client::enrol`] /// ///[`Client::enrol`]: super::Client::enrol diff --git a/progenitor-impl/tests/output/keeper-builder.out b/progenitor-impl/tests/output/keeper-builder.out index bb334b2c..9e5e682b 100644 --- a/progenitor-impl/tests/output/keeper-builder.out +++ b/progenitor-impl/tests/output/keeper-builder.out @@ -1,6 +1,8 @@ #[allow(unused_imports)] use progenitor_client::{encode_path, RequestBuilderExt}; -pub use progenitor_client::{ByteStream, Error, ResponseValue}; +pub use progenitor_client::{ + generate_websocket_key, ByteStream, Error, ResponseValue, WebsocketReactants, +}; pub mod types { use serde::{Deserialize, Serialize}; #[allow(unused_imports)] @@ -829,7 +831,10 @@ impl Client { pub mod builder { use super::types; #[allow(unused_imports)] - use super::{encode_path, ByteStream, Error, RequestBuilderExt, ResponseValue}; + use super::{ + encode_path, generate_websocket_key, ByteStream, Error, RequestBuilderExt, ResponseValue, + WebsocketReactants, + }; #[allow(unused_imports)] use std::convert::TryInto; ///Builder for [`Client::enrol`] diff --git a/progenitor-impl/tests/output/keeper-positional.out b/progenitor-impl/tests/output/keeper-positional.out index 6a3bf9c8..4b4d6611 100644 --- a/progenitor-impl/tests/output/keeper-positional.out +++ b/progenitor-impl/tests/output/keeper-positional.out @@ -1,6 +1,8 @@ #[allow(unused_imports)] use progenitor_client::{encode_path, RequestBuilderExt}; -pub use progenitor_client::{ByteStream, Error, ResponseValue}; +pub use progenitor_client::{ + generate_websocket_key, ByteStream, Error, ResponseValue, WebsocketReactants, +}; pub mod types { use serde::{Deserialize, Serialize}; #[allow(unused_imports)] diff --git a/progenitor-impl/tests/output/nexus-builder-tagged.out b/progenitor-impl/tests/output/nexus-builder-tagged.out index 4c1f9f1b..ee928fe7 100644 --- a/progenitor-impl/tests/output/nexus-builder-tagged.out +++ b/progenitor-impl/tests/output/nexus-builder-tagged.out @@ -1,6 +1,8 @@ #[allow(unused_imports)] use progenitor_client::{encode_path, RequestBuilderExt}; -pub use progenitor_client::{ByteStream, Error, ResponseValue}; +pub use progenitor_client::{ + generate_websocket_key, ByteStream, Error, ResponseValue, WebsocketReactants, +}; pub mod types { use serde::{Deserialize, Serialize}; #[allow(unused_imports)] @@ -13617,7 +13619,12 @@ impl ClientVpcsExt for Client { pub mod builder { use super::types; #[allow(unused_imports)] - use super::{encode_path, ByteStream, Error, RequestBuilderExt, ResponseValue}; + use super::{ + encode_path, generate_websocket_key, ByteStream, Error, RequestBuilderExt, ResponseValue, + WebsocketReactants, + }; + #[allow(unused_imports)] + use std::convert::TryInto; ///Builder for [`ClientDisksExt::disk_view_by_id`] /// ///[`ClientDisksExt::disk_view_by_id`]: super::ClientDisksExt::disk_view_by_id diff --git a/progenitor-impl/tests/output/nexus-builder.out b/progenitor-impl/tests/output/nexus-builder.out index 46857e82..6ec722f3 100644 --- a/progenitor-impl/tests/output/nexus-builder.out +++ b/progenitor-impl/tests/output/nexus-builder.out @@ -1,6 +1,8 @@ #[allow(unused_imports)] use progenitor_client::{encode_path, RequestBuilderExt}; -pub use progenitor_client::{ByteStream, Error, ResponseValue}; +pub use progenitor_client::{ + generate_websocket_key, ByteStream, Error, ResponseValue, WebsocketReactants, +}; pub mod types { use serde::{Deserialize, Serialize}; #[allow(unused_imports)] @@ -13437,7 +13439,10 @@ impl Client { pub mod builder { use super::types; #[allow(unused_imports)] - use super::{encode_path, ByteStream, Error, RequestBuilderExt, ResponseValue}; + use super::{ + encode_path, generate_websocket_key, ByteStream, Error, RequestBuilderExt, ResponseValue, + WebsocketReactants, + }; #[allow(unused_imports)] use std::convert::TryInto; ///Builder for [`Client::disk_view_by_id`] diff --git a/progenitor-impl/tests/output/nexus-positional.out b/progenitor-impl/tests/output/nexus-positional.out index 916cfb75..d8e14e97 100644 --- a/progenitor-impl/tests/output/nexus-positional.out +++ b/progenitor-impl/tests/output/nexus-positional.out @@ -1,6 +1,8 @@ #[allow(unused_imports)] use progenitor_client::{encode_path, RequestBuilderExt}; -pub use progenitor_client::{ByteStream, Error, ResponseValue}; +pub use progenitor_client::{ + generate_websocket_key, ByteStream, Error, ResponseValue, WebsocketReactants, +}; pub mod types { use serde::{Deserialize, Serialize}; #[allow(unused_imports)] diff --git a/progenitor-impl/tests/output/test_default_params_builder.out b/progenitor-impl/tests/output/test_default_params_builder.out index cc576a97..4329e0a2 100644 --- a/progenitor-impl/tests/output/test_default_params_builder.out +++ b/progenitor-impl/tests/output/test_default_params_builder.out @@ -1,6 +1,8 @@ #[allow(unused_imports)] use progenitor_client::{encode_path, RequestBuilderExt}; -pub use progenitor_client::{ByteStream, Error, ResponseValue}; +pub use progenitor_client::{ + generate_websocket_key, ByteStream, Error, ResponseValue, WebsocketReactants, +}; pub mod types { use serde::{Deserialize, Serialize}; #[allow(unused_imports)] @@ -236,7 +238,10 @@ impl Client { pub mod builder { use super::types; #[allow(unused_imports)] - use super::{encode_path, ByteStream, Error, RequestBuilderExt, ResponseValue}; + use super::{ + encode_path, generate_websocket_key, ByteStream, Error, RequestBuilderExt, ResponseValue, + WebsocketReactants, + }; #[allow(unused_imports)] use std::convert::TryInto; ///Builder for [`Client::default_params`] diff --git a/progenitor-impl/tests/output/test_default_params_positional.out b/progenitor-impl/tests/output/test_default_params_positional.out index 9eadbcab..8b7365e5 100644 --- a/progenitor-impl/tests/output/test_default_params_positional.out +++ b/progenitor-impl/tests/output/test_default_params_positional.out @@ -1,6 +1,8 @@ #[allow(unused_imports)] use progenitor_client::{encode_path, RequestBuilderExt}; -pub use progenitor_client::{ByteStream, Error, ResponseValue}; +pub use progenitor_client::{ + generate_websocket_key, ByteStream, Error, ResponseValue, WebsocketReactants, +}; pub mod types { use serde::{Deserialize, Serialize}; #[allow(unused_imports)] diff --git a/progenitor-impl/tests/output/test_freeform_response.out b/progenitor-impl/tests/output/test_freeform_response.out index 97216ca1..d9b8a395 100644 --- a/progenitor-impl/tests/output/test_freeform_response.out +++ b/progenitor-impl/tests/output/test_freeform_response.out @@ -1,6 +1,8 @@ #[allow(unused_imports)] use progenitor_client::{encode_path, RequestBuilderExt}; -pub use progenitor_client::{ByteStream, Error, ResponseValue}; +pub use progenitor_client::{ + generate_websocket_key, ByteStream, Error, ResponseValue, WebsocketReactants, +}; pub mod types { use serde::{Deserialize, Serialize}; #[allow(unused_imports)] diff --git a/progenitor-impl/tests/output/test_renamed_parameters.out b/progenitor-impl/tests/output/test_renamed_parameters.out index 8f13e283..6b95b98d 100644 --- a/progenitor-impl/tests/output/test_renamed_parameters.out +++ b/progenitor-impl/tests/output/test_renamed_parameters.out @@ -1,6 +1,8 @@ #[allow(unused_imports)] use progenitor_client::{encode_path, RequestBuilderExt}; -pub use progenitor_client::{ByteStream, Error, ResponseValue}; +pub use progenitor_client::{ + generate_websocket_key, ByteStream, Error, ResponseValue, WebsocketReactants, +}; pub mod types { use serde::{Deserialize, Serialize}; #[allow(unused_imports)] diff --git a/progenitor/Cargo.toml b/progenitor/Cargo.toml index 8d50909d..9c42f436 100644 --- a/progenitor/Cargo.toml +++ b/progenitor/Cargo.toml @@ -24,3 +24,5 @@ regress = "0.4.1" reqwest = { version = "0.11", features = ["json", "stream"] } schemars = { version = "0.8.10", features = ["uuid1"] } uuid = { version = "1.0", features = ["serde", "v4"] } +tokio = { version = "1", features = ["net"] } +http = "0.2.8" diff --git a/progenitor/tests/build_propolis.rs b/progenitor/tests/build_propolis.rs new file mode 100644 index 00000000..3eba2d31 --- /dev/null +++ b/progenitor/tests/build_propolis.rs @@ -0,0 +1,24 @@ +// Copyright 2022 Oxide Computer Company + +// ensure that the websocket channel used for serial console compiles. +mod propolis_client { + progenitor::generate_api!( + spec = "../sample_openapi/propolis-server.json", + interface = Builder, + tags = Merged, + ); +} + +use propolis_client::Client; + +pub fn _ignore() { + let _ = async { + let (_req, _tcp): (http::Request<()>, tokio::net::TcpStream) = + Client::new("") + .instance_serial() + .send() + .await + .unwrap() + .into_request_and_tcp_stream(); + }; +} diff --git a/sample_openapi/propolis-server.json b/sample_openapi/propolis-server.json new file mode 100644 index 00000000..ee6ba4e4 --- /dev/null +++ b/sample_openapi/propolis-server.json @@ -0,0 +1,850 @@ +{ + "openapi": "3.0.3", + "info": { + "title": "Oxide Propolis Server API", + "description": "API for interacting with the Propolis hypervisor frontend.", + "contact": { + "url": "https://oxide.computer", + "email": "api@oxide.computer" + }, + "version": "0.0.1" + }, + "paths": { + "/instance": { + "get": { + "operationId": "instance_get", + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InstanceGetResponse" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + }, + "put": { + "operationId": "instance_ensure", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InstanceEnsureRequest" + } + } + }, + "required": true + }, + "responses": { + "201": { + "description": "successful creation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InstanceEnsureResponse" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/instance/disk/{id}/snapshot/{snapshot_id}": { + "post": { + "summary": "Issue a snapshot request to a crucible backend", + "operationId": "instance_issue_crucible_snapshot_request", + "parameters": [ + { + "in": "path", + "name": "id", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + }, + "style": "simple" + }, + { + "in": "path", + "name": "snapshot_id", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + }, + "style": "simple" + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "title": "Null", + "type": "string", + "enum": [ + null + ] + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/instance/migrate/status": { + "get": { + "operationId": "instance_migrate_status", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InstanceMigrateStatusRequest" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InstanceMigrateStatusResponse" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/instance/serial": { + "get": { + "operationId": "instance_serial", + "responses": { + "default": { + "description": "", + "content": { + "*/*": { + "schema": {} + } + } + } + }, + "x-dropshot-websocket": {} + } + }, + "/instance/state": { + "put": { + "operationId": "instance_state_put", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InstanceStateRequested" + } + } + }, + "required": true + }, + "responses": { + "204": { + "description": "resource updated" + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/instance/state-monitor": { + "get": { + "operationId": "instance_state_monitor", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InstanceStateMonitorRequest" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InstanceStateMonitorResponse" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + } + }, + "components": { + "responses": { + "Error": { + "description": "Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + } + }, + "schemas": { + "CrucibleOpts": { + "type": "object", + "properties": { + "cert_pem": { + "nullable": true, + "type": "string" + }, + "control": { + "nullable": true, + "type": "string" + }, + "flush_timeout": { + "nullable": true, + "type": "integer", + "format": "uint32", + "minimum": 0 + }, + "id": { + "type": "string", + "format": "uuid" + }, + "key": { + "nullable": true, + "type": "string" + }, + "key_pem": { + "nullable": true, + "type": "string" + }, + "lossy": { + "type": "boolean" + }, + "read_only": { + "type": "boolean" + }, + "root_cert_pem": { + "nullable": true, + "type": "string" + }, + "target": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "required": [ + "id", + "lossy", + "read_only", + "target" + ] + }, + "DiskAttachment": { + "type": "object", + "properties": { + "disk_id": { + "type": "string", + "format": "uuid" + }, + "generation_id": { + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "state": { + "$ref": "#/components/schemas/DiskAttachmentState" + } + }, + "required": [ + "disk_id", + "generation_id", + "state" + ] + }, + "DiskAttachmentState": { + "oneOf": [ + { + "type": "string", + "enum": [ + "Detached", + "Destroyed", + "Faulted" + ] + }, + { + "type": "object", + "properties": { + "Attached": { + "type": "string", + "format": "uuid" + } + }, + "required": [ + "Attached" + ], + "additionalProperties": false + } + ] + }, + "DiskRequest": { + "type": "object", + "properties": { + "device": { + "type": "string" + }, + "gen": { + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "name": { + "type": "string" + }, + "read_only": { + "type": "boolean" + }, + "slot": { + "$ref": "#/components/schemas/Slot" + }, + "volume_construction_request": { + "$ref": "#/components/schemas/VolumeConstructionRequest" + } + }, + "required": [ + "device", + "gen", + "name", + "read_only", + "slot", + "volume_construction_request" + ] + }, + "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" + ] + }, + "Instance": { + "type": "object", + "properties": { + "disks": { + "type": "array", + "items": { + "$ref": "#/components/schemas/DiskAttachment" + } + }, + "nics": { + "type": "array", + "items": { + "$ref": "#/components/schemas/NetworkInterface" + } + }, + "properties": { + "$ref": "#/components/schemas/InstanceProperties" + }, + "state": { + "$ref": "#/components/schemas/InstanceState" + } + }, + "required": [ + "disks", + "nics", + "properties", + "state" + ] + }, + "InstanceEnsureRequest": { + "type": "object", + "properties": { + "cloud_init_bytes": { + "nullable": true, + "type": "string" + }, + "disks": { + "default": [], + "type": "array", + "items": { + "$ref": "#/components/schemas/DiskRequest" + } + }, + "migrate": { + "nullable": true, + "allOf": [ + { + "$ref": "#/components/schemas/InstanceMigrateInitiateRequest" + } + ] + }, + "nics": { + "default": [], + "type": "array", + "items": { + "$ref": "#/components/schemas/NetworkInterfaceRequest" + } + }, + "properties": { + "$ref": "#/components/schemas/InstanceProperties" + } + }, + "required": [ + "properties" + ] + }, + "InstanceEnsureResponse": { + "type": "object", + "properties": { + "migrate": { + "nullable": true, + "allOf": [ + { + "$ref": "#/components/schemas/InstanceMigrateInitiateResponse" + } + ] + } + } + }, + "InstanceGetResponse": { + "type": "object", + "properties": { + "instance": { + "$ref": "#/components/schemas/Instance" + } + }, + "required": [ + "instance" + ] + }, + "InstanceMigrateInitiateRequest": { + "type": "object", + "properties": { + "migration_id": { + "type": "string", + "format": "uuid" + }, + "src_addr": { + "type": "string" + }, + "src_uuid": { + "type": "string", + "format": "uuid" + } + }, + "required": [ + "migration_id", + "src_addr", + "src_uuid" + ] + }, + "InstanceMigrateInitiateResponse": { + "type": "object", + "properties": { + "migration_id": { + "type": "string", + "format": "uuid" + } + }, + "required": [ + "migration_id" + ] + }, + "InstanceMigrateStatusRequest": { + "type": "object", + "properties": { + "migration_id": { + "type": "string", + "format": "uuid" + } + }, + "required": [ + "migration_id" + ] + }, + "InstanceMigrateStatusResponse": { + "type": "object", + "properties": { + "state": { + "$ref": "#/components/schemas/MigrationState" + } + }, + "required": [ + "state" + ] + }, + "InstanceProperties": { + "type": "object", + "properties": { + "bootrom_id": { + "description": "ID of the bootrom used to initialize this Instance.", + "type": "string", + "format": "uuid" + }, + "description": { + "description": "Free-form text description of an Instance.", + "type": "string" + }, + "id": { + "description": "Unique identifier for this Instance.", + "type": "string", + "format": "uuid" + }, + "image_id": { + "description": "ID of the image used to initialize this Instance.", + "type": "string", + "format": "uuid" + }, + "memory": { + "description": "Size of memory allocated to the Instance, in MiB.", + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "name": { + "description": "Human-readable name of the Instance.", + "type": "string" + }, + "vcpus": { + "description": "Number of vCPUs to be allocated to the Instance.", + "type": "integer", + "format": "uint8", + "minimum": 0 + } + }, + "required": [ + "bootrom_id", + "description", + "id", + "image_id", + "memory", + "name", + "vcpus" + ] + }, + "InstanceState": { + "description": "Current state of an Instance.", + "type": "string", + "enum": [ + "Creating", + "Starting", + "Running", + "Stopping", + "Stopped", + "Rebooting", + "Migrating", + "Repairing", + "Failed", + "Destroyed" + ] + }, + "InstanceStateMonitorRequest": { + "type": "object", + "properties": { + "gen": { + "type": "integer", + "format": "uint64", + "minimum": 0 + } + }, + "required": [ + "gen" + ] + }, + "InstanceStateMonitorResponse": { + "type": "object", + "properties": { + "gen": { + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "state": { + "$ref": "#/components/schemas/InstanceState" + } + }, + "required": [ + "gen", + "state" + ] + }, + "InstanceStateRequested": { + "type": "string", + "enum": [ + "Run", + "Stop", + "Reboot", + "MigrateStart" + ] + }, + "MigrationState": { + "type": "string", + "enum": [ + "Sync", + "RamPush", + "Pause", + "RamPushDirty", + "Device", + "Arch", + "Resume", + "RamPull", + "Finish", + "Error" + ] + }, + "NetworkInterface": { + "type": "object", + "properties": { + "attachment": { + "$ref": "#/components/schemas/NetworkInterfaceAttachmentState" + }, + "name": { + "type": "string" + } + }, + "required": [ + "attachment", + "name" + ] + }, + "NetworkInterfaceAttachmentState": { + "oneOf": [ + { + "type": "string", + "enum": [ + "Detached", + "Faulted" + ] + }, + { + "type": "object", + "properties": { + "Attached": { + "$ref": "#/components/schemas/Slot" + } + }, + "required": [ + "Attached" + ], + "additionalProperties": false + } + ] + }, + "NetworkInterfaceRequest": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "slot": { + "$ref": "#/components/schemas/Slot" + } + }, + "required": [ + "name", + "slot" + ] + }, + "Slot": { + "description": "A stable index which is translated by Propolis into a PCI BDF, visible to the guest.", + "type": "integer", + "format": "uint8", + "minimum": 0 + }, + "VolumeConstructionRequest": { + "oneOf": [ + { + "type": "object", + "properties": { + "block_size": { + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "id": { + "type": "string", + "format": "uuid" + }, + "read_only_parent": { + "nullable": true, + "allOf": [ + { + "$ref": "#/components/schemas/VolumeConstructionRequest" + } + ] + }, + "sub_volumes": { + "type": "array", + "items": { + "$ref": "#/components/schemas/VolumeConstructionRequest" + } + }, + "type": { + "type": "string", + "enum": [ + "volume" + ] + } + }, + "required": [ + "block_size", + "id", + "sub_volumes", + "type" + ] + }, + { + "type": "object", + "properties": { + "block_size": { + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "id": { + "type": "string", + "format": "uuid" + }, + "type": { + "type": "string", + "enum": [ + "url" + ] + }, + "url": { + "type": "string" + } + }, + "required": [ + "block_size", + "id", + "type", + "url" + ] + }, + { + "type": "object", + "properties": { + "block_size": { + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "gen": { + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "opts": { + "$ref": "#/components/schemas/CrucibleOpts" + }, + "type": { + "type": "string", + "enum": [ + "region" + ] + } + }, + "required": [ + "block_size", + "gen", + "opts", + "type" + ] + }, + { + "type": "object", + "properties": { + "block_size": { + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "id": { + "type": "string", + "format": "uuid" + }, + "path": { + "type": "string" + }, + "type": { + "type": "string", + "enum": [ + "file" + ] + } + }, + "required": [ + "block_size", + "id", + "path", + "type" + ] + } + ] + } + } + } +} \ No newline at end of file