implement requirements for ordering server

This commit is contained in:
Maxime Van Hees 2025-07-22 12:48:52 +02:00
parent 84aa8cc11d
commit 6cdc468b0a
7 changed files with 273 additions and 3 deletions

View File

@ -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();

View File

@ -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<Vec<OrderServerProduct>, 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<OrderServerProductWrapper> = self.handle_response(response)?;
let products = wrapped.into_iter().map(|sop| sop.product).collect();
Ok(products)
}
}

View File

@ -649,3 +649,189 @@ impl From<reqwest::blocking::Response> for ApiError {
}
}
}
#[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<Self>) {
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<Self>) {
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<Self>) {
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<ProductPrice>,
}
impl OrderableAddon {
fn build_rhai_type(builder: &mut TypeBuilder<Self>) {
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<String>,
pub traffic: String,
#[serde(deserialize_with = "string_or_seq_string")]
pub dist: Vec<String>,
#[serde(rename = "@deprecated arch", default, deserialize_with = "option_string_or_seq_string")]
#[deprecated(note = "use `dist` instead")]
pub arch: Option<Vec<String>>,
#[serde(deserialize_with = "string_or_seq_string")]
pub lang: Vec<String>,
#[serde(deserialize_with = "string_or_seq_string")]
pub location: Vec<String>,
pub prices: Vec<ProductPrice>,
pub orderable_addons: Vec<OrderableAddon>,
}
impl OrderServerProduct {
fn build_rhai_type(builder: &mut TypeBuilder<Self>) {
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)
}
}

View File

@ -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::<Plesk>();
engine.build_type::<Cpanel>();
engine.build_type::<Cancellation>();
engine.build_type::<OrderServerProduct>();
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);

View File

@ -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::<SshKey>() {
ssh_keys_table::pretty_print_ssh_keys(array);
}
else if first.is::<OrderServerProduct>() {
server_ordering_table::pretty_print_server_products(array);
} else {
// Generic fallback for other types
for item in array {

View File

@ -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::<OrderServerProduct>() {
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();
}

View File

@ -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<Array, Box<EvalAltResult>> {
let overview_servers = client
.get_server_ordering_product_overview()
.map_err(|e| Into::<Box<EvalAltResult>>::into(e.to_string()))?;
Ok(overview_servers.into_iter().map(Dynamic::from).collect())
}
}