diff --git a/examples/server_ordering.rhai b/examples/server_ordering.rhai new file mode 100644 index 0000000..b741f36 --- /dev/null +++ b/examples/server_ordering.rhai @@ -0,0 +1,3 @@ +// Get all available products (servers) that we can order and print them in a table +let available_server_products = hetzner.get_server_ordering_product_overview(); +available_server_products.pretty_print(); \ No newline at end of file diff --git a/src/api/mod.rs b/src/api/mod.rs index 1762845..c64ce8d 100644 --- a/src/api/mod.rs +++ b/src/api/mod.rs @@ -4,7 +4,7 @@ pub mod models; use self::models::{Boot, Rescue, Server, SshKey}; use crate::api::error::ApiError; use crate::api::models::{ - BootWrapper, Cancellation, CancellationWrapper, RescueWrapped, ServerWrapper, SshKeyWrapper, + BootWrapper, Cancellation, CancellationWrapper, OrderServerProduct, OrderServerProductWrapper, RescueWrapped, ServerWrapper, SshKeyWrapper }; use crate::config::Config; use error::AppError; @@ -244,4 +244,16 @@ impl Client { let wrapped: RescueWrapped = self.handle_response(response)?; Ok(wrapped.rescue) } + + pub fn get_server_ordering_product_overview(&self) -> Result, AppError> { + let response = self + .http_client + .get(format!("{}/order/server/product", &self.config.api_url)) + .basic_auth(&self.config.username, Some(&self.config.password)) + .send()?; + + let wrapped: Vec = self.handle_response(response)?; + let products = wrapped.into_iter().map(|sop| sop.product).collect(); + Ok(products) + } } diff --git a/src/api/models.rs b/src/api/models.rs index 8a7bb5d..d430001 100644 --- a/src/api/models.rs +++ b/src/api/models.rs @@ -648,4 +648,190 @@ impl From for ApiError { message: value.text().unwrap_or("The API call returned an error.".to_string()), } } +} + +#[derive(Debug, Deserialize, Clone, CustomType)] +#[rhai_type(extra = Self::build_rhai_type)] +pub struct Price { + pub net: String, + pub gross: String, + pub hourly_net: String, + pub hourly_gross: String, +} + +impl Price { + fn build_rhai_type(builder: &mut TypeBuilder) { + builder + .with_name("Price") + .with_get("net", |p: &mut Price| p.net.clone()) + .with_get("gross", |p: &mut Price| p.gross.clone()) + .with_get("hourly_net", |p: &mut Price| p.hourly_net.clone()) + .with_get("hourly_gross", |p: &mut Price| p.hourly_gross.clone()) + .on_print(|p: &mut Price| p.to_string()) + .with_fn("pretty_print", |p: &mut Price| p.to_string()); + } +} + +impl fmt::Display for Price { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!( + f, + "Net: {}, Gross: {}, Hourly Net: {}, Hourly Gross: {}", + self.net, self.gross, self.hourly_net, self.hourly_gross + ) + } +} + +#[derive(Debug, Deserialize, Clone, CustomType)] +#[rhai_type(extra = Self::build_rhai_type)] +pub struct PriceSetup { + pub net: String, + pub gross: String, +} + +impl PriceSetup { + fn build_rhai_type(builder: &mut TypeBuilder) { + builder + .with_name("PriceSetup") + .with_get("net", |p: &mut PriceSetup| p.net.clone()) + .with_get("gross", |p: &mut PriceSetup| p.gross.clone()) + .on_print(|p: &mut PriceSetup| p.to_string()) + .with_fn("pretty_print", |p: &mut PriceSetup| p.to_string()); + } +} + +impl fmt::Display for PriceSetup { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "Net: {}, Gross: {}", self.net, self.gross) + } +} + +#[derive(Debug, Deserialize, Clone, CustomType)] +#[rhai_type(extra = Self::build_rhai_type)] +pub struct ProductPrice { + pub location: String, + pub price: Price, + pub price_setup: PriceSetup, +} + +impl ProductPrice { + fn build_rhai_type(builder: &mut TypeBuilder) { + builder + .with_name("ProductPrice") + .with_get("location", |p: &mut ProductPrice| p.location.clone()) + .with_get("price", |p: &mut ProductPrice| p.price.clone()) + .with_get("price_setup", |p: &mut ProductPrice| p.price_setup.clone()) + .on_print(|p: &mut ProductPrice| p.to_string()) + .with_fn("pretty_print", |p: &mut ProductPrice| p.to_string()); + } +} + +impl fmt::Display for ProductPrice { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!( + f, + "Location: {}, Price: ({}), Price Setup: ({})", + self.location, self.price, self.price_setup + ) + } +} + +#[derive(Debug, Deserialize, Clone, CustomType)] +#[rhai_type(extra = Self::build_rhai_type)] +pub struct OrderableAddon { + pub id: String, + pub name: String, + pub min: i32, + pub max: i32, + pub prices: Vec, +} + +impl OrderableAddon { + fn build_rhai_type(builder: &mut TypeBuilder) { + builder + .with_name("OrderableAddon") + .with_get("id", |o: &mut OrderableAddon| o.id.clone()) + .with_get("name", |o: &mut OrderableAddon| o.name.clone()) + .with_get("min", |o: &mut OrderableAddon| o.min) + .with_get("max", |o: &mut OrderableAddon| o.max) + .with_get("prices", |o: &mut OrderableAddon| o.prices.clone()) + .on_print(|o: &mut OrderableAddon| o.to_string()) + .with_fn("pretty_print", |o: &mut OrderableAddon| o.to_string()); + } +} + +impl fmt::Display for OrderableAddon { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let mut table = Table::new(); + table.add_row(row!["Property", "Value"]); + table.add_row(row!["ID", self.id.clone()]); + table.add_row(row!["Name", self.name.clone()]); + table.add_row(row!["Min", self.min.to_string()]); + table.add_row(row!["Max", self.max.to_string()]); + table.add_row(row!["Prices", format!("{:?}", self.prices)]); + write!(f, "{}", table) + } +} + +#[derive(Debug, Deserialize, Clone)] +pub struct OrderServerProductWrapper { + pub product: OrderServerProduct, +} + +#[derive(Debug, Deserialize, Clone, CustomType)] +#[rhai_type(extra = Self::build_rhai_type)] +pub struct OrderServerProduct { + pub id: String, + pub name: String, + #[serde(deserialize_with = "string_or_seq_string")] + pub description: Vec, + pub traffic: String, + #[serde(deserialize_with = "string_or_seq_string")] + pub dist: Vec, + #[serde(rename = "@deprecated arch", default, deserialize_with = "option_string_or_seq_string")] + #[deprecated(note = "use `dist` instead")] + pub arch: Option>, + #[serde(deserialize_with = "string_or_seq_string")] + pub lang: Vec, + #[serde(deserialize_with = "string_or_seq_string")] + pub location: Vec, + pub prices: Vec, + pub orderable_addons: Vec, +} + +impl OrderServerProduct { + fn build_rhai_type(builder: &mut TypeBuilder) { + builder + .with_name("OrderServerProduct") + .with_get("id", |o: &mut OrderServerProduct| o.id.clone()) + .with_get("name", |o: &mut OrderServerProduct| o.name.clone()) + .with_get("description", |o: &mut OrderServerProduct| o.description.clone()) + .with_get("traffic", |o: &mut OrderServerProduct| o.traffic.clone()) + .with_get("dist", |o: &mut OrderServerProduct| o.dist.clone()) + .with_get("arch", |o: &mut OrderServerProduct| o.arch.clone()) + .with_get("lang", |o: &mut OrderServerProduct| o.lang.clone()) + .with_get("location", |o: &mut OrderServerProduct| o.location.clone()) + .with_get("prices", |o: &mut OrderServerProduct| o.prices.clone()) + .with_get("orderable_addons", |o: &mut OrderServerProduct| o.orderable_addons.clone()) + .on_print(|o: &mut OrderServerProduct| o.to_string()) + .with_fn("pretty_print", |o: &mut OrderServerProduct| o.to_string()); + } +} + +impl fmt::Display for OrderServerProduct { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let mut table = Table::new(); + table.add_row(row!["Property", "Value"]); + table.add_row(row!["ID", self.id.clone()]); + table.add_row(row!["Name", self.name.clone()]); + table.add_row(row!["Description", self.description.join(", ")]); + table.add_row(row!["Traffic", self.traffic.clone()]); + table.add_row(row!["Distributions", self.dist.join(", ")]); + table.add_row(row!["Architectures", self.arch.as_deref().unwrap_or_default().join(", ")]); + table.add_row(row!["Languages", self.lang.join(", ")]); + table.add_row(row!["Locations", self.location.join(", ")]); + table.add_row(row!["Prices", format!("{:?}", self.prices)]); + table.add_row(row!["Orderable Addons", format!("{:?}", self.orderable_addons)]); + write!(f, "{}", table) + } } \ No newline at end of file diff --git a/src/scripting/mod.rs b/src/scripting/mod.rs index c2ef05b..25a8d12 100644 --- a/src/scripting/mod.rs +++ b/src/scripting/mod.rs @@ -1,11 +1,12 @@ use crate::api::Client; -use crate::api::models::{Rescue, Linux, Vnc, Windows, Plesk, Cpanel, Boot, Server, SshKey, Cancellation}; +use crate::api::models::{Rescue, Linux, Vnc, Windows, Plesk, Cpanel, Boot, Server, SshKey, Cancellation, OrderServerProduct}; use rhai::{Engine, Scope}; pub mod server; pub mod ssh_keys; pub mod boot; pub mod printing; +pub mod server_ordering; pub fn setup_engine(client: Client) -> (Engine, Scope<'static>) { let mut engine = Engine::new(); @@ -21,11 +22,13 @@ pub fn setup_engine(client: Client) -> (Engine, Scope<'static>) { engine.build_type::(); engine.build_type::(); engine.build_type::(); + engine.build_type::(); server::register(&mut engine); ssh_keys::register(&mut engine); boot::register(&mut engine); printing::register(&mut engine); + server_ordering::register(&mut engine); scope.push("hetzner", client); diff --git a/src/scripting/printing/mod.rs b/src/scripting/printing/mod.rs index 4f4a0fb..a68d4f7 100644 --- a/src/scripting/printing/mod.rs +++ b/src/scripting/printing/mod.rs @@ -1,9 +1,11 @@ use rhai::{Array, Engine}; -use crate::scripting::{Server, SshKey}; +use crate::{api::models::OrderServerProduct, scripting::{Server, SshKey}}; mod servers_table; mod ssh_keys_table; +mod server_ordering_table; +// This will be called when we print(...) or pretty_print() an Array (with Dynamic values) pub fn pretty_print_dispatch(array: Array) { println!("pretty print dispatch"); if array.is_empty() { @@ -17,6 +19,9 @@ pub fn pretty_print_dispatch(array: Array) { servers_table::pretty_print_servers(array); } else if first.is::() { ssh_keys_table::pretty_print_ssh_keys(array); + } + else if first.is::() { + server_ordering_table::pretty_print_server_products(array); } else { // Generic fallback for other types for item in array { diff --git a/src/scripting/printing/server_ordering_table.rs b/src/scripting/printing/server_ordering_table.rs new file mode 100644 index 0000000..c3cfda0 --- /dev/null +++ b/src/scripting/printing/server_ordering_table.rs @@ -0,0 +1,38 @@ +use prettytable::{row, Table}; +use crate::api::models::OrderServerProduct; + +pub fn pretty_print_server_products(products: rhai::Array) { + let mut table = Table::new(); + table.add_row(row![b => + "ID", + "Name", + "Description", + "Traffic", + "Location", + "Price (Net)", + "Price (Gross)", + ]); + + for product_dyn in products { + if let Some(product) = product_dyn.try_cast::() { + let mut price_net = "N/A".to_string(); + let mut price_gross = "N/A".to_string(); + + if let Some(first_price) = product.prices.first() { + price_net = first_price.price.net.clone(); + price_gross = first_price.price.gross.clone(); + } + + table.add_row(row![ + product.id, + product.name, + product.description.join(", "), + product.traffic, + product.location.join(", "), + price_net, + price_gross, + ]); + } + } + table.printstd(); +} \ No newline at end of file diff --git a/src/scripting/server_ordering.rs b/src/scripting/server_ordering.rs new file mode 100644 index 0000000..967a739 --- /dev/null +++ b/src/scripting/server_ordering.rs @@ -0,0 +1,23 @@ +use crate::api::{Client, models::OrderServerProduct}; +use rhai::{Array, Dynamic, plugin::*}; + +pub fn register(engine: &mut Engine) { + let server_order_module = exported_module!(server_order_api); + engine.register_global_module(server_order_module.into()); +} + +#[export_module] +pub mod server_order_api { + // use super::*; + // use rhai::EvalAltResult; + + #[rhai_fn(name = "get_server_ordering_product_overview", return_raw)] + pub fn get_server_ordering_product_overview( + client: &mut Client, + ) -> Result> { + let overview_servers = client + .get_server_ordering_product_overview() + .map_err(|e| Into::>::into(e.to_string()))?; + Ok(overview_servers.into_iter().map(Dynamic::from).collect()) + } +}