summaryrefslogtreecommitdiffhomepage
path: root/tools/unitctl/unit-openapi/openapi-templates/request.rs
diff options
context:
space:
mode:
authorAva Hahn <a.hahn@f5.com>2024-04-22 13:26:34 +0100
committeravahahn <110854134+avahahn@users.noreply.github.com>2024-04-30 09:14:55 -0700
commitdb3cf3e42d93112278f5e86cca2c886627ef48b2 (patch)
tree3c546922404965b3fc7773d319388b9241f9896b /tools/unitctl/unit-openapi/openapi-templates/request.rs
parentb26c119f4e535f617c9ffb10076f15c4ee54414d (diff)
downloadunit-db3cf3e42d93112278f5e86cca2c886627ef48b2.tar.gz
unit-db3cf3e42d93112278f5e86cca2c886627ef48b2.tar.bz2
tools: Add unitctl CLI
* Pull in entire unit-rust-sdk project * not included: CLA, COC, License * not included: duplicate openapi spec * not included: CI workflows * not included: changelog tooling * not included: commitsar tooling * not included: OpenAPI Web UI feature * update links in unitctl manpage * remove IDE configuration from .gitignore * rename Containerfile.debian to Dockerfile * simplify call to uname * keep Readmes and Makefiles to 80 character lines * outline specifically how to build unitctl for any desired target, and where to then find the binary for use * remove a section on the vision of the CLI which was superfluous given the state of completeness of the code and its use in unit * remove out of date feature proposals from readme * makefile: do not run when Rustup is not present * bump mio version to latest * generate openapi client library on demand * generate-openapi only runs when not present * generate-openapi now a dependency of binary build targets * deleted autogenerated code * reverted readme and Cargo document to autogenerated state * add additional build requirement to Readme Co-developed-by: Elijah Zupancic <e.zupancic@f5.com> Signed-off-by: Elijah Zupancic <e.zupancic@f5.com> Signed-off-by: Ava Hahn <a.hahn@f5.com> Reviewed-by: Andrew Clayton <a.clayton@nginx.com> # non rust stuff [ tools/cli => tools/unitctl and subject tweak - Andrew ] Signed-off-by: Andrew Clayton <a.clayton@nginx.com>
Diffstat (limited to 'tools/unitctl/unit-openapi/openapi-templates/request.rs')
-rw-r--r--tools/unitctl/unit-openapi/openapi-templates/request.rs248
1 files changed, 248 insertions, 0 deletions
diff --git a/tools/unitctl/unit-openapi/openapi-templates/request.rs b/tools/unitctl/unit-openapi/openapi-templates/request.rs
new file mode 100644
index 00000000..9cf480cc
--- /dev/null
+++ b/tools/unitctl/unit-openapi/openapi-templates/request.rs
@@ -0,0 +1,248 @@
+use std::collections::HashMap;
+use std::pin::Pin;
+
+use base64::{alphabet, Engine};
+use base64::engine::general_purpose::NO_PAD;
+use base64::engine::GeneralPurpose;
+
+use futures;
+use futures::Future;
+use futures::future::*;
+use hyper;
+use hyper::header::{AUTHORIZATION, CONTENT_LENGTH, CONTENT_TYPE, HeaderValue, USER_AGENT};
+use serde;
+use serde_json;
+
+use super::{configuration, Error};
+
+const MIME_ENCODER: GeneralPurpose = GeneralPurpose::new(&alphabet::STANDARD, NO_PAD);
+
+pub(crate) struct ApiKey {
+ pub in_header: bool,
+ pub in_query: bool,
+ pub param_name: String,
+}
+
+impl ApiKey {
+ fn key(&self, prefix: &Option<String>, key: &str) -> String {
+ match prefix {
+ None => key.to_owned(),
+ Some(ref prefix) => format!("{} {}", prefix, key),
+ }
+ }
+}
+
+#[allow(dead_code)]
+pub(crate) enum Auth {
+ None,
+ ApiKey(ApiKey),
+ Basic,
+ Oauth,
+}
+
+/// If the authorization type is unspecified then it will be automatically detected based
+/// on the configuration. This functionality is useful when the OpenAPI definition does not
+/// include an authorization scheme.
+pub(crate) struct Request {
+ auth: Option<Auth>,
+ method: hyper::Method,
+ path: String,
+ query_params: HashMap<String, String>,
+ no_return_type: bool,
+ path_params: HashMap<String, String>,
+ form_params: HashMap<String, String>,
+ header_params: HashMap<String, String>,
+ // TODO: multiple body params are possible technically, but not supported here.
+ serialized_body: Option<String>,
+}
+
+#[allow(dead_code)]
+impl Request {
+ pub fn new(method: hyper::Method, path: String) -> Self {
+ Request {
+ auth: None,
+ method,
+ path,
+ query_params: HashMap::new(),
+ path_params: HashMap::new(),
+ form_params: HashMap::new(),
+ header_params: HashMap::new(),
+ serialized_body: None,
+ no_return_type: false,
+ }
+ }
+
+ pub fn with_body_param<T: serde::Serialize>(mut self, param: T) -> Self {
+ self.serialized_body = Some(serde_json::to_string(&param).unwrap());
+ self
+ }
+
+ pub fn with_header_param(mut self, basename: String, param: String) -> Self {
+ self.header_params.insert(basename, param);
+ self
+ }
+
+ #[allow(unused)]
+ pub fn with_query_param(mut self, basename: String, param: String) -> Self {
+ self.query_params.insert(basename, param);
+ self
+ }
+
+ #[allow(unused)]
+ pub fn with_path_param(mut self, basename: String, param: String) -> Self {
+ self.path_params.insert(basename, param);
+ self
+ }
+
+ #[allow(unused)]
+ pub fn with_form_param(mut self, basename: String, param: String) -> Self {
+ self.form_params.insert(basename, param);
+ self
+ }
+
+ pub fn returns_nothing(mut self) -> Self {
+ self.no_return_type = true;
+ self
+ }
+
+ pub fn with_auth(mut self, auth: Auth) -> Self {
+ self.auth = Some(auth);
+ self
+ }
+
+ pub fn execute<'a, C, U>(
+ self,
+ conf: &configuration::Configuration<C>,
+ ) -> Pin<Box<dyn Future<Output=Result<U, Error>> + 'a>>
+ where
+ C: hyper::client::connect::Connect + Clone + std::marker::Send + Sync,
+ U: Sized + std::marker::Send + 'a,
+ for<'de> U: serde::Deserialize<'de>,
+ {
+ let mut query_string = ::url::form_urlencoded::Serializer::new("".to_owned());
+
+ let mut path = self.path;
+ for (k, v) in self.path_params {
+ // replace {id} with the value of the id path param
+ path = path.replace(&format!("{{{}}}", k), &v);
+ }
+
+ for (key, val) in self.query_params {
+ query_string.append_pair(&key, &val);
+ }
+
+ let mut uri_str = format!("{}{}", conf.base_path, path);
+
+ let query_string_str = query_string.finish();
+ if query_string_str != "" {
+ uri_str += "?";
+ uri_str += &query_string_str;
+ }
+ let uri: hyper::Uri = match uri_str.parse() {
+ Err(e) => return Box::pin(futures::future::err(Error::UriError(e))),
+ Ok(u) => u,
+ };
+
+ let mut req_builder = hyper::Request::builder()
+ .uri(uri)
+ .method(self.method);
+
+ // Detect the authorization type if it hasn't been set.
+ let auth = self.auth.unwrap_or_else(||
+ if conf.api_key.is_some() {
+ panic!("Cannot automatically set the API key from the configuration, it must be specified in the OpenAPI definition")
+ } else if conf.oauth_access_token.is_some() {
+ Auth::Oauth
+ } else if conf.basic_auth.is_some() {
+ Auth::Basic
+ } else {
+ Auth::None
+ }
+ );
+ match auth {
+ Auth::ApiKey(apikey) => {
+ if let Some(ref key) = conf.api_key {
+ let val = apikey.key(&key.prefix, &key.key);
+ if apikey.in_query {
+ query_string.append_pair(&apikey.param_name, &val);
+ }
+ if apikey.in_header {
+ req_builder = req_builder.header(&apikey.param_name, val);
+ }
+ }
+ }
+ Auth::Basic => {
+ if let Some(ref auth_conf) = conf.basic_auth {
+ let mut text = auth_conf.0.clone();
+ text.push(':');
+ if let Some(ref pass) = auth_conf.1 {
+ text.push_str(&pass[..]);
+ }
+ let encoded = MIME_ENCODER.encode(&text);
+ req_builder = req_builder.header(AUTHORIZATION, encoded);
+ }
+ }
+ Auth::Oauth => {
+ if let Some(ref token) = conf.oauth_access_token {
+ let text = "Bearer ".to_owned() + token;
+ req_builder = req_builder.header(AUTHORIZATION, text);
+ }
+ }
+ Auth::None => {}
+ }
+
+ if let Some(ref user_agent) = conf.user_agent {
+ req_builder = req_builder.header(USER_AGENT, match HeaderValue::from_str(user_agent) {
+ Ok(header_value) => header_value,
+ Err(e) => return Box::pin(futures::future::err(super::Error::Header(e)))
+ });
+ }
+
+ for (k, v) in self.header_params {
+ req_builder = req_builder.header(&k, v);
+ }
+
+ let req_headers = req_builder.headers_mut().unwrap();
+ let request_result = if self.form_params.len() > 0 {
+ req_headers.insert(CONTENT_TYPE, HeaderValue::from_static("application/x-www-form-urlencoded"));
+ let mut enc = ::url::form_urlencoded::Serializer::new("".to_owned());
+ for (k, v) in self.form_params {
+ enc.append_pair(&k, &v);
+ }
+ req_builder.body(hyper::Body::from(enc.finish()))
+ } else if let Some(body) = self.serialized_body {
+ req_headers.insert(CONTENT_TYPE, HeaderValue::from_static("application/json"));
+ req_headers.insert(CONTENT_LENGTH, body.len().into());
+ req_builder.body(hyper::Body::from(body))
+ } else {
+ req_builder.body(hyper::Body::default())
+ };
+ let request = match request_result {
+ Ok(request) => request,
+ Err(e) => return Box::pin(futures::future::err(Error::from(e)))
+ };
+
+ let no_return_type = self.no_return_type;
+ Box::pin(conf.client
+ .request(request)
+ .map_err(|e| Error::from(e))
+ .and_then(move |response| {
+ let status = response.status();
+ if !status.is_success() {
+ futures::future::err::<U, Error>(Error::from((status, response.into_body()))).boxed()
+ } else if no_return_type {
+ // This is a hack; if there's no_ret_type, U is (), but serde_json gives an
+ // error when deserializing "" into (), so deserialize 'null' into it
+ // instead.
+ // An alternate option would be to require U: Default, and then return
+ // U::default() here instead since () implements that, but then we'd
+ // need to impl default for all models.
+ futures::future::ok::<U, Error>(serde_json::from_str("null").expect("serde null value")).boxed()
+ } else {
+ hyper::body::to_bytes(response.into_body())
+ .map(|bytes| serde_json::from_slice(&bytes.unwrap()))
+ .map_err(|e| Error::from(e)).boxed()
+ }
+ }))
+ }
+}