added the ability to order server addons (like ipv4 address) + updated server ordering example script to showcase new functionality

This commit is contained in:
Maxime Van Hees 2025-07-24 13:52:01 +02:00
parent 1e95a6a9f1
commit 038229bb92
7 changed files with 509 additions and 7 deletions

View File

@ -17,6 +17,14 @@ let TRANSACTION_ID = "YOUR_TRANSACTION_ID_HERE";
// Example: 2739642 // Example: 2739642
let AUCTION_PRODUCT_ID = 0; // Replace with an actual auction product ID let AUCTION_PRODUCT_ID = 0; // Replace with an actual auction product ID
// Server Number for fetching server addon products or ordering addons.
// Example: 1234567
let SERVER_NUMBER = 0; // Replace with an actual server number
// Addon Transaction ID for fetching specific addon transaction details.
// Example: "B20220210-1843193-S33055"
let ADDON_TRANSACTION_ID = "YOUR_ADDON_TRANSACTION_ID_HERE";
// --- Server Ordering Operations --- // --- Server Ordering Operations ---
// 1. Get all available server products // 1. Get all available server products
@ -176,4 +184,66 @@ try {
} catch (err) { } catch (err) {
print("Error ordering auction server: " + err); print("Error ordering auction server: " + err);
} }
*/
// 11. Get all available server addon products
// This section retrieves all available server addon products for a specific server
// and prints them in a human-readable table format.
// Uncomment the following lines and set SERVER_NUMBER to an actual server number to use.
/*
print("\nFetching all available server addon products for server: " + SERVER_NUMBER + "...");
try {
let available_server_addons = hetzner.get_server_addon_products(SERVER_NUMBER);
available_server_addons.pretty_print();
print("All available server addon products fetched and displayed.");
} catch (err) {
print("Error fetching all available server addon products: " + err);
}
*/
// 12. List all addon transactions from the past 30 days
// This section retrieves all server addon order transactions from the past 30 days.
// Uncomment the following lines to list addon transactions.
/*
print("\nFetching all server addon transactions from the past 30 days...");
try {
let addon_transactions_last_30 = hetzner.get_server_addon_transactions();
addon_transactions_last_30.pretty_print();
print("All addon transactions fetched and displayed.");
} catch (err) {
print("Error fetching addon transactions: " + err);
}
*/
// 13. Order a server addon
// This section demonstrates how to order a new server addon.
// Ensure SERVER_NUMBER, PRODUCT_ID, and REASON are set correctly.
// Uncomment the following lines to order a server addon.
/*
print("\nAttempting to order a server addon for server: " + SERVER_NUMBER + " with product ID: " + SERVER_PRODUCT_ID);
try {
let order_addon_builder = new_server_addon_builder(SERVER_NUMBER, SERVER_PRODUCT_ID)
.with_reason("Test order") // Mandatory for some addon types, e.g., "ip_ipv4"
.with_test(true); // Set to 'false' for a real order
let ordered_addon_transaction = hetzner.order_server_addon(order_addon_builder);
print("Server addon ordered successfully. Transaction details:");
print(ordered_addon_transaction);
} catch (err) {
print("Error ordering server addon: " + err);
}
*/
// 14. Query a specific server addon transaction by ID
// This section demonstrates how to fetch details for a specific server addon transaction.
// Uncomment the following lines and set ADDON_TRANSACTION_ID to an actual transaction ID to use.
/*
print("\nAttempting to fetch specific server addon transaction with ID: " + ADDON_TRANSACTION_ID);
try {
let queried_addon_transaction = hetzner.get_server_addon_transaction_by_id(ADDON_TRANSACTION_ID);
print("Specific server addon transaction details:");
print(queried_addon_transaction);
} catch (err) {
print("Error fetching specific server addon transaction: " + err);
}
*/ */

View File

@ -1,14 +1,16 @@
pub mod error; pub mod error;
pub mod models; pub mod models;
use self::models::{Boot, Rescue, Server, SshKey}; use self::models::{
use crate::api::error::ApiError; Boot, Rescue, Server, SshKey, ServerAddonProduct, ServerAddonProductWrapper,
use crate::api::models::{
AuctionServerProduct, AuctionServerProductWrapper, AuctionTransaction, AuctionServerProduct, AuctionServerProductWrapper, AuctionTransaction,
AuctionTransactionWrapper, BootWrapper, Cancellation, CancellationWrapper, AuctionTransactionWrapper, BootWrapper, Cancellation, CancellationWrapper,
OrderServerBuilder, OrderServerProduct, OrderServerProductWrapper, RescueWrapped, OrderServerBuilder, OrderServerProduct, OrderServerProductWrapper, RescueWrapped,
ServerWrapper, SshKeyWrapper, Transaction, TransactionWrapper, ServerWrapper, SshKeyWrapper, Transaction, TransactionWrapper,
ServerAddonTransaction, ServerAddonTransactionWrapper,
OrderServerAddonBuilder,
}; };
use crate::api::error::ApiError;
use crate::config::Config; use crate::config::Config;
use error::AppError; use error::AppError;
use reqwest::blocking::Client as HttpClient; use reqwest::blocking::Client as HttpClient;
@ -378,6 +380,25 @@ impl Client {
let wrapped: AuctionTransactionWrapper = self.handle_response(response)?; let wrapped: AuctionTransactionWrapper = self.handle_response(response)?;
Ok(wrapped.transaction) Ok(wrapped.transaction)
} }
pub fn get_server_addon_products(
&self,
server_number: i64,
) -> Result<Vec<ServerAddonProduct>, AppError> {
let response = self
.http_client
.get(format!(
"{}/order/server_addon/{}/product",
&self.config.api_url, server_number
))
.basic_auth(&self.config.username, Some(&self.config.password))
.send()?;
let wrapped: Vec<ServerAddonProductWrapper> = self.handle_response(response)?;
let products = wrapped.into_iter().map(|sap| sap.product).collect();
Ok(products)
}
pub fn order_auction_server( pub fn order_auction_server(
&self, &self,
product_id: i64, product_id: i64,
@ -428,4 +449,65 @@ impl Client {
let wrapped: AuctionTransactionWrapper = self.handle_response(response)?; let wrapped: AuctionTransactionWrapper = self.handle_response(response)?;
Ok(wrapped.transaction) Ok(wrapped.transaction)
} }
pub fn get_server_addon_transactions(&self) -> Result<Vec<ServerAddonTransaction>, AppError> {
let response = self
.http_client
.get(format!("{}/order/server_addon/transaction", &self.config.api_url))
.basic_auth(&self.config.username, Some(&self.config.password))
.send()?;
let wrapped: Vec<ServerAddonTransactionWrapper> = self.handle_response(response)?;
let transactions = wrapped.into_iter().map(|satw| satw.transaction).collect();
Ok(transactions)
}
pub fn get_server_addon_transaction_by_id(
&self,
transaction_id: &str,
) -> Result<ServerAddonTransaction, AppError> {
let response = self
.http_client
.get(format!(
"{}/order/server_addon/transaction/{}",
&self.config.api_url, transaction_id
))
.basic_auth(&self.config.username, Some(&self.config.password))
.send()?;
let wrapped: ServerAddonTransactionWrapper = self.handle_response(response)?;
Ok(wrapped.transaction)
}
pub fn order_server_addon(
&self,
order: OrderServerAddonBuilder,
) -> Result<ServerAddonTransaction, AppError> {
let mut params = json!({
"server_number": order.server_number,
"product_id": order.product_id,
});
if let Some(reason) = order.reason {
params["reason"] = json!(reason);
}
if let Some(gateway) = order.gateway {
params["gateway"] = json!(gateway);
}
if let Some(test) = order.test {
if test {
params["test"] = json!(test);
}
}
let response = self
.http_client
.post(format!("{}/order/server_addon/transaction", &self.config.api_url))
.basic_auth(&self.config.username, Some(&self.config.password))
.json(&params)
.send()?;
let wrapped: ServerAddonTransactionWrapper = self.handle_response(response)?;
Ok(wrapped.transaction)
}
} }

View File

@ -726,6 +726,175 @@ impl fmt::Display for OrderableAddon {
} }
} }
#[derive(Debug, Deserialize, Clone)]
pub struct ServerAddonProductWrapper {
pub product: ServerAddonProduct,
}
#[derive(Debug, Deserialize, Clone, CustomType)]
#[rhai_type(extra = Self::build_rhai_type)]
pub struct ServerAddonProduct {
pub id: String,
pub name: String,
#[serde(rename = "type")]
pub product_type: String,
pub price: ProductPrice,
}
impl ServerAddonProduct {
fn build_rhai_type(builder: &mut TypeBuilder<Self>) {
builder
.with_name("ServerAddonProduct")
.with_get("id", |p: &mut ServerAddonProduct| p.id.clone())
.with_get("name", |p: &mut ServerAddonProduct| p.name.clone())
.with_get("product_type", |p: &mut ServerAddonProduct| {
p.product_type.clone()
})
.with_get("price", |p: &mut ServerAddonProduct| p.price.clone())
.on_print(|p: &mut ServerAddonProduct| p.to_string())
.with_fn("pretty_print", |p: &mut ServerAddonProduct| p.to_string());
}
}
impl fmt::Display for ServerAddonProduct {
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!["Type", self.product_type.clone()]);
table.add_row(row!["Price", self.price.to_string()]);
write!(f, "{}", table)
}
}
#[derive(Debug, Deserialize, Clone)]
pub struct ServerAddonTransactionWrapper {
pub transaction: ServerAddonTransaction,
}
#[derive(Debug, Deserialize, Clone, CustomType)]
#[rhai_type(extra = Self::build_rhai_type)]
pub struct ServerAddonTransaction {
pub id: String,
pub date: String,
pub status: String,
pub server_number: i32,
pub product: ServerAddonTransactionProduct,
pub resources: Vec<ServerAddonResource>,
}
impl ServerAddonTransaction {
fn build_rhai_type(builder: &mut TypeBuilder<Self>) {
builder
.with_name("ServerAddonTransaction")
.with_get("id", |t: &mut ServerAddonTransaction| t.id.clone())
.with_get("date", |t: &mut ServerAddonTransaction| t.date.clone())
.with_get("status", |t: &mut ServerAddonTransaction| t.status.clone())
.with_get("server_number", |t: &mut ServerAddonTransaction| {
t.server_number
})
.with_get("product", |t: &mut ServerAddonTransaction| {
t.product.clone()
})
.with_get("resources", |t: &mut ServerAddonTransaction| {
t.resources.clone()
})
.on_print(|t: &mut ServerAddonTransaction| t.to_string())
.with_fn("pretty_print", |t: &mut ServerAddonTransaction| {
t.to_string()
});
}
}
impl fmt::Display for ServerAddonTransaction {
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!["Date", self.date.clone()]);
table.add_row(row!["Status", self.status.clone()]);
table.add_row(row!["Server Number", self.server_number.to_string()]);
table.add_row(row!["Product ID", self.product.id.clone()]);
table.add_row(row!["Product Name", self.product.name.clone()]);
table.add_row(row!["Product Price", self.product.price.to_string()]);
let mut resources_table = Table::new();
resources_table.add_row(row![b => "Type", "ID"]);
for resource in &self.resources {
resources_table.add_row(row![resource.resource_type, resource.id]);
}
table.add_row(row!["Resources", resources_table]);
write!(f, "{}", table)
}
}
#[derive(Debug, Deserialize, Clone, CustomType)]
#[rhai_type(extra = Self::build_rhai_type)]
pub struct ServerAddonTransactionProduct {
pub id: String,
pub name: String,
pub price: ProductPrice,
}
impl ServerAddonTransactionProduct {
fn build_rhai_type(builder: &mut TypeBuilder<Self>) {
builder
.with_name("ServerAddonTransactionProduct")
.with_get("id", |p: &mut ServerAddonTransactionProduct| p.id.clone())
.with_get("name", |p: &mut ServerAddonTransactionProduct| {
p.name.clone()
})
.with_get("price", |p: &mut ServerAddonTransactionProduct| {
p.price.clone()
})
.on_print(|p: &mut ServerAddonTransactionProduct| p.to_string())
.with_fn("pretty_print", |p: &mut ServerAddonTransactionProduct| {
p.to_string()
});
}
}
impl fmt::Display for ServerAddonTransactionProduct {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"ID: {}, Name: {}, Price: ({})",
self.id, self.name, self.price
)
}
}
#[derive(Debug, Deserialize, Clone, CustomType)]
#[rhai_type(extra = Self::build_rhai_type)]
pub struct ServerAddonResource {
#[serde(rename = "type")]
pub resource_type: String,
pub id: String,
}
impl ServerAddonResource {
fn build_rhai_type(builder: &mut TypeBuilder<Self>) {
builder
.with_name("ServerAddonResource")
.with_get("resource_type", |r: &mut ServerAddonResource| {
r.resource_type.clone()
})
.with_get("id", |r: &mut ServerAddonResource| r.id.clone())
.on_print(|r: &mut ServerAddonResource| r.to_string())
.with_fn("pretty_print", |r: &mut ServerAddonResource| {
r.to_string()
});
}
}
impl fmt::Display for ServerAddonResource {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "Type: {}, ID: {}", self.resource_type, self.id)
}
}
#[derive(Debug, Deserialize, Clone)] #[derive(Debug, Deserialize, Clone)]
pub struct OrderServerProductWrapper { pub struct OrderServerProductWrapper {
pub product: OrderServerProduct, pub product: OrderServerProduct,
@ -1544,6 +1713,63 @@ impl OrderAuctionServerBuilder {
} }
} }
#[derive(Debug, Deserialize, Clone, CustomType)]
#[rhai_type(extra = Self::build_rhai_type)]
pub struct OrderServerAddonBuilder {
pub server_number: i64,
pub product_id: String,
pub reason: Option<String>,
pub gateway: Option<String>,
pub test: Option<bool>,
}
impl OrderServerAddonBuilder {
pub fn new(server_number: i64, product_id: &str) -> Self {
Self {
server_number,
product_id: product_id.to_string(),
reason: None,
gateway: None,
test: Some(true), // by default test is enabled
}
}
pub fn with_reason(mut self, reason: &str) -> Self {
self.reason = Some(reason.to_string());
self
}
pub fn with_gateway(mut self, gateway: &str) -> Self {
self.gateway = Some(gateway.to_string());
self
}
pub fn with_test(mut self, test: bool) -> Self {
self.test = Some(test);
self
}
fn build_rhai_type(builder: &mut TypeBuilder<Self>) {
builder
.with_name("OrderServerAddonBuilder")
.with_fn("new_server_addon_builder", Self::new)
.with_fn("with_reason", Self::with_reason)
.with_fn("with_gateway", Self::with_gateway)
.with_fn("with_test", Self::with_test)
.with_get("server_number", |b: &mut OrderServerAddonBuilder| {
b.server_number
})
.with_get("product_id", |b: &mut OrderServerAddonBuilder| {
b.product_id.clone()
})
.with_get("reason", |b: &mut OrderServerAddonBuilder| b.reason.clone())
.with_get("gateway", |b: &mut OrderServerAddonBuilder| {
b.gateway.clone()
})
.with_get("test", |b: &mut OrderServerAddonBuilder| b.test.clone());
}
}
fn string_or_seq_string<'de, D>(deserializer: D) -> Result<Vec<String>, D::Error> fn string_or_seq_string<'de, D>(deserializer: D) -> Result<Vec<String>, D::Error>
where where

View File

@ -1,6 +1,6 @@
use crate::api::Client; use crate::api::Client;
use crate::api::models::{ use crate::api::models::{
AuctionServerProduct, AuctionTransaction, AuctionTransactionProduct, AuthorizedKey, Boot, Cancellation, Cpanel, HostKey, Linux, OrderAuctionServerBuilder, OrderServerBuilder, OrderServerProduct, Plesk, Rescue, Server, SshKey, Transaction, TransactionProduct, Vnc, Windows AuctionServerProduct, AuctionTransaction, AuctionTransactionProduct, AuthorizedKey, Boot, Cancellation, Cpanel, HostKey, Linux, OrderAuctionServerBuilder, OrderServerBuilder, OrderServerProduct, Plesk, Rescue, Server, ServerAddonProduct, ServerAddonProductWrapper, ServerAddonTransaction, ServerAddonResource, SshKey, Transaction, TransactionProduct, Vnc, Windows, OrderServerAddonBuilder
}; };
use rhai::{Engine, Scope}; use rhai::{Engine, Scope};
@ -34,6 +34,10 @@ pub fn setup_engine(client: Client) -> (Engine, Scope<'static>) {
engine.build_type::<AuctionTransactionProduct>(); engine.build_type::<AuctionTransactionProduct>();
engine.build_type::<OrderAuctionServerBuilder>(); engine.build_type::<OrderAuctionServerBuilder>();
engine.build_type::<OrderServerBuilder>(); engine.build_type::<OrderServerBuilder>();
engine.build_type::<ServerAddonProduct>();
engine.build_type::<ServerAddonTransaction>();
engine.build_type::<ServerAddonResource>();
engine.build_type::<OrderServerAddonBuilder>();
server::register(&mut engine); server::register(&mut engine);
ssh_keys::register(&mut engine); ssh_keys::register(&mut engine);

View File

@ -1,5 +1,5 @@
use rhai::{Array, Engine}; use rhai::{Array, Engine};
use crate::{api::models::{OrderServerProduct, AuctionServerProduct, AuctionTransaction}, scripting::{Server, SshKey}}; use crate::{api::models::{OrderServerProduct, AuctionServerProduct, AuctionTransaction, ServerAddonProduct, ServerAddonTransaction}, scripting::{Server, SshKey}};
mod servers_table; mod servers_table;
mod ssh_keys_table; mod ssh_keys_table;
@ -25,6 +25,10 @@ pub fn pretty_print_dispatch(array: Array) {
server_ordering_table::pretty_print_auction_server_products(array); server_ordering_table::pretty_print_auction_server_products(array);
} else if first.is::<AuctionTransaction>() { } else if first.is::<AuctionTransaction>() {
server_ordering_table::pretty_print_auction_transactions(array); server_ordering_table::pretty_print_auction_transactions(array);
} else if first.is::<ServerAddonProduct>() {
server_ordering_table::pretty_print_server_addon_products(array);
} else if first.is::<ServerAddonTransaction>() {
server_ordering_table::pretty_print_server_addon_transactions(array);
} else { } else {
// Generic fallback for other types // Generic fallback for other types
for item in array { for item in array {

View File

@ -1,5 +1,5 @@
use prettytable::{row, Table}; use prettytable::{row, Table};
use crate::api::models::OrderServerProduct; use crate::api::models::{OrderServerProduct, ServerAddonProduct, ServerAddonTransaction, ServerAddonResource};
pub fn pretty_print_server_products(products: rhai::Array) { pub fn pretty_print_server_products(products: rhai::Array) {
let mut table = Table::new(); let mut table = Table::new();
@ -126,6 +126,40 @@ pub fn pretty_print_auction_server_products(products: rhai::Array) {
table.printstd(); table.printstd();
} }
pub fn pretty_print_server_addon_products(products: rhai::Array) {
let mut table = Table::new();
table.add_row(row![b =>
"ID",
"Name",
"Type",
"Location",
"Price (Net)",
"Price (Gross)",
"Hourly Net",
"Hourly Gross",
"Setup Net",
"Setup Gross",
]);
for product_dyn in products {
if let Some(product) = product_dyn.try_cast::<ServerAddonProduct>() {
table.add_row(row![
product.id,
product.name,
product.product_type,
product.price.location,
product.price.price.net,
product.price.price.gross,
product.price.price.hourly_net,
product.price.price.hourly_gross,
product.price.price_setup.net,
product.price.price_setup.gross,
]);
}
}
table.printstd();
}
pub fn pretty_print_auction_transactions(transactions: rhai::Array) { pub fn pretty_print_auction_transactions(transactions: rhai::Array) {
let mut table = Table::new(); let mut table = Table::new();
table.add_row(row![b => table.add_row(row![b =>
@ -220,4 +254,40 @@ pub fn pretty_print_auction_transactions(transactions: rhai::Array) {
} }
} }
table.printstd(); table.printstd();
}
pub fn pretty_print_server_addon_transactions(transactions: rhai::Array) {
let mut table = Table::new();
table.add_row(row![b =>
"ID",
"Date",
"Status",
"Server Number",
"Product ID",
"Product Name",
"Product Price",
"Resources",
]);
for transaction_dyn in transactions {
if let Some(transaction) = transaction_dyn.try_cast::<ServerAddonTransaction>() {
let mut resources_table = Table::new();
resources_table.add_row(row![b => "Type", "ID"]);
for resource in &transaction.resources {
resources_table.add_row(row![resource.resource_type, resource.id]);
}
table.add_row(row![
transaction.id,
transaction.date,
transaction.status,
transaction.server_number,
transaction.product.id,
transaction.product.name,
transaction.product.price.to_string(),
resources_table,
]);
}
}
table.printstd();
} }

View File

@ -2,7 +2,7 @@ use crate::api::{
Client, Client,
models::{ models::{
AuctionServerProduct, AuctionTransaction, OrderAuctionServerBuilder, OrderServerBuilder, AuctionServerProduct, AuctionTransaction, OrderAuctionServerBuilder, OrderServerBuilder,
OrderServerProduct, Transaction, OrderServerProduct, ServerAddonProduct, ServerAddonTransaction, Transaction,
}, },
}; };
use rhai::{Array, Dynamic, plugin::*}; use rhai::{Array, Dynamic, plugin::*};
@ -14,6 +14,8 @@ pub fn register(engine: &mut Engine) {
#[export_module] #[export_module]
pub mod server_order_api { pub mod server_order_api {
use crate::api::models::OrderServerAddonBuilder;
#[rhai_fn(name = "get_server_products", return_raw)] #[rhai_fn(name = "get_server_products", return_raw)]
pub fn get_server_ordering_product_overview( pub fn get_server_ordering_product_overview(
client: &mut Client, client: &mut Client,
@ -103,6 +105,38 @@ pub mod server_order_api {
Ok(transaction) Ok(transaction)
} }
#[rhai_fn(name = "get_server_addon_products", return_raw)]
pub fn get_server_addon_products(
client: &mut Client,
server_number: i64,
) -> Result<Array, Box<EvalAltResult>> {
let products = client
.get_server_addon_products(server_number)
.map_err(|e| Into::<Box<EvalAltResult>>::into(e.to_string()))?;
Ok(products.into_iter().map(Dynamic::from).collect())
}
#[rhai_fn(name = "get_server_addon_transactions", return_raw)]
pub fn get_server_addon_transactions(
client: &mut Client,
) -> Result<Array, Box<EvalAltResult>> {
let transactions = client
.get_server_addon_transactions()
.map_err(|e| Into::<Box<EvalAltResult>>::into(e.to_string()))?;
Ok(transactions.into_iter().map(Dynamic::from).collect())
}
#[rhai_fn(name = "get_server_addon_transaction_by_id", return_raw)]
pub fn get_server_addon_transaction_by_id(
client: &mut Client,
transaction_id: &str,
) -> Result<ServerAddonTransaction, Box<EvalAltResult>> {
let transaction = client
.get_server_addon_transaction_by_id(transaction_id)
.map_err(|e| Into::<Box<EvalAltResult>>::into(e.to_string()))?;
Ok(transaction)
}
#[rhai_fn(name = "order_auction_server", return_raw)] #[rhai_fn(name = "order_auction_server", return_raw)]
pub fn order_auction_server( pub fn order_auction_server(
client: &mut Client, client: &mut Client,
@ -121,4 +155,16 @@ pub mod server_order_api {
).map_err(|e| Into::<Box<EvalAltResult>>::into(e.to_string()))?; ).map_err(|e| Into::<Box<EvalAltResult>>::into(e.to_string()))?;
Ok(transaction) Ok(transaction)
} }
#[rhai_fn(name = "order_server_addon", return_raw)]
pub fn order_server_addon(
client: &mut Client,
order: OrderServerAddonBuilder,
) -> Result<ServerAddonTransaction, Box<EvalAltResult>> {
println!("Builder struct being used to order server addon: {:#?}", order);
let transaction = client
.order_server_addon(order)
.map_err(|e| Into::<Box<EvalAltResult>>::into(e.to_string()))?;
Ok(transaction)
}
} }