Implementation improved with macros

master
Tait Hoyem 2 years ago
parent 4dbcd30e23
commit dad9d5f8c7

@ -7,10 +7,13 @@ edition = "2021"
[dependencies]
clap = { version = "3.2.22", features = ["env", "derive"] }
chrono = { version = "0.4.22", features = ["serde"] }
envy = "0.4.2"
getset = "0.1.2"
hex = "0.4.3"
hmac = "0.12.1"
lunanode_macros = { path = "./lunanode_macros" }
parse-display-derive = "0.6.0"
sha2 = "0.10.5"
serde = { version = "1.0.0", features = ["derive"] }
serde_json = { version = "1.0.85", features = ["preserve_order"] }

@ -0,0 +1,13 @@
[package]
name = "lunanode_macros"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[lib]
proc_macro = true
[dependencies]
syn = "1.0"
quote = "1.0"

@ -0,0 +1,88 @@
/// This module is designed to automatically add the essential types to a LunaNodeRequest struct, without developer intervention.
/// You should apply this to every LunaNodeRequest struct *BEFORE* any other macros, and especially before any derive macros.
/// Usages:
/// ```rust
/// #[lunanode_request(response="ImageListResponse", endpoint="image/list/")]
/// #[derive(Serialize, Deserialize, Debug, ...)]
/// struct MyStruct {
/// ...
/// ```
///
/// TODO: Improve error messages.
use std::collections::HashMap;
use proc_macro::TokenStream;
use quote::quote;
use syn::parse::{Parse, ParseStream, Parser, Result};
use syn::{parse, parse_macro_input, punctuated::Punctuated, Attribute, AttributeArgs, Expr, Ident, ItemStruct, Local, Lit, LitStr, NestedMeta, Meta, MetaNameValue, Pat, Stmt, Type, Token, DeriveInput};
#[derive(Debug, Hash, Eq, PartialEq)]
enum LunanodeRequestParam {
Invalid,
Response(Type),
EndPoint(String),
}
impl LunanodeRequestParam {
fn from(key: String, val: String) -> Self {
match (key.as_str(), val.as_str()) {
("response", tp) => Self::Response(syn::parse_str(tp).expect("The value given to the 'response' parameter must be a valid type.")),
("endpoint", ep) => Self::EndPoint(ep.to_string()),
_ => Self::Invalid
}
}
}
#[proc_macro_attribute]
pub fn lunanode_request(attr: TokenStream, input: TokenStream) -> TokenStream {
let mut item_struct = parse_macro_input!(input as ItemStruct);
let args_parsed: Vec<LunanodeRequestParam> = parse_macro_input!(attr as AttributeArgs)
.into_iter()
.filter_map(|nm| match nm {
NestedMeta::Meta(Meta::NameValue(MetaNameValue { path, eq_token, lit: Lit::Str(lstr) })) => Some(
(path.segments
.into_iter()
.map(|seg| seg.ident.to_string())
.collect::<Vec<String>>()
.swap_remove(0),
lstr.value())
),
_ => None
})
.map(|(k,v)| LunanodeRequestParam::from(k, v))
.collect();
let response_type = match args_parsed.get(0).expect("There must be two argument for the macro.") {
LunanodeRequestParam::Response(res) => res,
_ => panic!("The response parameter must be a type; it must also be first."),
};
let url_endpoint = match args_parsed.get(1).expect("There must be two arguments for the macro.") {
LunanodeRequestParam::EndPoint(ep) => ep,
_ => panic!("The endpoint parameter must be a string; it must also be last.")
};
let name = item_struct.ident.clone();
if let syn::Fields::Named(ref mut fields) = item_struct.fields {
fields.named.push(
syn::Field::parse_named
.parse2(quote! {
#[serde(flatten)]
#[clap(flatten)]
pub keys: ApiKeys
})
.unwrap(),
);
}
return quote! {
#item_struct
impl LunaNodeRequest for #name {
type response = #response_type; // TODO: Last section that needs to be dynamic
fn get_keys(&self) -> ApiKeys {
self.keys.clone()
}
fn url_endpoint(&self) -> String {
#url_endpoint.to_string()
}
}
}
.into();
}

@ -65,8 +65,7 @@ impl ToString for LunaNodeAPIRequest {
}
fn main() -> Result<(), Box<dyn std::error::Error>> {
println!("Hello, world!");
let args = Args::parse();
args.make_request();
let _ = args.make_request();
Ok(())
}

@ -3,16 +3,19 @@ use crate::{
VMListResponse,
BillingCreditResponse,
LNImageListResponse,
VolumeListResponse,
FloatingIpListResponse,
},
types::{
LNError,
LNErrorResponse,
LunaNodeRequest,
LunaNodeResponse,
Requestable,
ApiKeys,
},
};
use lunanode_macros::lunanode_request;
use parse_display_derive::Display;
use ureq::post;
use serde::{
Serialize,
@ -22,19 +25,34 @@ use serde::{
use hmac::{Hmac, Mac};
use sha2::Sha512;
#[derive(Serialize, Deserialize, Debug, clap::Subcommand)]
#[serde(untagged)]
pub enum FloatingSubArgs {
/// List all images on my account.
List(FloatingIpListRequest),
}
#[derive(Serialize, Deserialize, Debug, clap::Subcommand)]
#[serde(untagged)]
pub enum ImageSubArgs {
/// List all images on my account.
List(ImageListRequest),
}
#[derive(Serialize, Deserialize, Debug, clap::Subcommand)]
#[serde(untagged)]
pub enum VmSubArgs {
/// List all VMs on my account.
List(VMListRequest),
}
#[derive(Serialize, Deserialize, Debug, clap::Subcommand)]
#[serde(untagged)]
pub enum VolumeSubArgs {
/// List all volumes I'm paying for.
List(VolumeListRequest),
}
#[derive(Serialize, Deserialize, Debug, clap::Subcommand)]
#[serde(untagged)]
pub enum BillingSubArgs {
/// How much money is left in my account.
Credit(BillingCreditRequest),
}
@ -42,24 +60,36 @@ pub enum BillingSubArgs {
#[serde(untagged)]
pub enum Args {
#[clap(subcommand)]
/// See `lunanode image help`
Image(ImageSubArgs),
#[clap(subcommand)]
/// See `lunanode ip help`
Floating(FloatingSubArgs),
#[clap(subcommand)]
/// See `lunanode vm help`
Vm(VmSubArgs),
#[clap(subcommand)]
/// See `lunanode billing help`
Billing(BillingSubArgs),
}
impl ToString for Args {
fn to_string(&self) -> String {
"Not implemented TODO!".to_string()
}
#[clap(subcommand)]
/// See `lunanode volume help`
Volume(VolumeSubArgs),
}
impl Args {
pub fn make_request(&self) -> Result<(), LNError> {
match self {
Self::Floating(FloatingSubArgs::List(ip_list)) => {
let list = ip_list.make_request()?;
println!("{:#?}", list);
},
Self::Vm(VmSubArgs::List(vm_list)) => {
let list = vm_list.make_request()?;
println!("{:#?}", list);
},
Self::Volume(VolumeSubArgs::List(vol_list)) => {
let list = vol_list.make_request()?;
println!("{:#?}", list);
},
Self::Image(ImageSubArgs::List(image_list)) => {
let list = image_list.make_request()?;
println!("{:#?}", list);
@ -71,84 +101,29 @@ impl Args {
}
Ok(())
}
fn get_info(&self) -> (&str, ApiKeys) {
match self {
Self::Vm(VmSubArgs::List(vm_list)) => ("vm/list/", vm_list.keys.clone()),
Self::Image(ImageSubArgs::List(image_list)) => ("image/list/", image_list.keys.clone()),
Self::Billing(BillingSubArgs::Credit(credit)) => ("billing/credit/", credit.keys.clone()),
}
}
fn get_keys(&self) -> ApiKeys {
self.get_info().1
}
fn url_endpoint(&self) -> &str {
self.get_info().0
}
}
#[lunanode_request(response="LNImageListResponse", endpoint="image/list/")]
#[derive(Serialize, Deserialize, Debug, Hash, PartialEq, Eq, clap::Args)]
/// ImageListRequest is used to create a new request for the /vm/list endpoint.
pub struct ImageListRequest {
#[serde(flatten)]
#[clap(flatten)]
pub keys: ApiKeys,
}
impl ToString for ImageListRequest {
fn to_string(&self) -> String {
"N/A".to_string()
}
}
impl Requestable for ImageListRequest {}
impl LunaNodeRequest for ImageListRequest {
type response = LNImageListResponse;
fn url_endpoint(&self) -> String {
"image/list/".to_string()
}
fn get_keys(&self) -> ApiKeys {
self.keys.clone()
}
}
#[derive(Serialize, Deserialize, Debug, Hash, PartialEq, Eq, clap::Args)]
/// ImageListRequest is used to create a new request for the /image/list endpoint.
pub struct ImageListRequest {}
#[lunanode_request(response="BillingCreditResponse", endpoint="billing/credit/")]
#[derive(Serialize, Deserialize, Debug, Hash, PartialEq, Eq, clap::Args, Display)]
/// BillingCreditRequest handles the /billing/credits/ endpoint. It will produce a BillingCreditResponse.
pub struct BillingCreditRequest {
#[serde(flatten)]
#[clap(flatten)]
pub keys: ApiKeys,
}
impl ToString for BillingCreditRequest {
fn to_string(&self) -> String {
"N/A".to_string()
}
}
impl Requestable for BillingCreditRequest {}
impl LunaNodeRequest for BillingCreditRequest {
type response = BillingCreditResponse;
fn get_keys(&self) -> ApiKeys {
self.keys.clone()
}
fn url_endpoint(&self) -> String {
"vm/list/".to_string()
}
}
#[derive(Serialize, Deserialize, Debug, Hash, PartialEq, Eq, clap::Args)]
pub struct BillingCreditRequest {}
#[lunanode_request(response="VMListResponse", endpoint="vm/list/")]
#[derive(Serialize, Deserialize, Debug, Hash, PartialEq, Eq, clap::Args, Display)]
/// VMListRequest is used to create a new request for the /vm/list endpoint.
pub struct VMListRequest {
#[serde(flatten)]
#[clap(flatten)]
pub keys: ApiKeys,
}
impl ToString for VMListRequest {
fn to_string(&self) -> String {
"N/A".to_string()
}
}
impl Requestable for VMListRequest {}
impl LunaNodeRequest for VMListRequest {
type response = VMListResponse;
fn get_keys(&self) -> ApiKeys {
self.keys.clone()
}
fn url_endpoint(&self) -> String {
"vm/list/".to_string()
}
}
pub struct VMListRequest {}
#[lunanode_request(response="VolumeListResponse", endpoint="volume/list/")]
#[derive(Serialize, Deserialize, Debug, Hash, PartialEq, Eq, clap::Args, Display)]
/// VolumeListRequest is used to create a new request for the /volume/list endpoint.
pub struct VolumeListRequest {}
#[lunanode_request(response="FloatingIpListResponse", endpoint="floating/list/")]
#[derive(Serialize, Deserialize, Debug, Hash, PartialEq, Eq, clap::Args, Display)]
/// IpListRequest is used to create a new request for the /ip/list endpoint.
pub struct FloatingIpListRequest {}

@ -4,7 +4,6 @@ use crate::external;
use crate::types::{
LunaNodeResponse,
LunaNodeRequest,
Requestable,
};
use serde_json;
@ -66,6 +65,27 @@ impl ToString for VMListResponse {
}
impl LunaNodeResponse for VMListResponse {}
#[derive(Serialize, Deserialize, Debug)]
#[serde(rename_all="lowercase")]
enum AttachmentType {
Unattached,
Vm,
}
#[serde_as]
#[derive(Serialize, Deserialize, Debug)]
struct FloatingIp {
attached_id: Option<uuid::Uuid>,
attached_name: Option<String>,
attached_type: AttachmentType,
hostname: String,
ip: std::net::Ipv4Addr,
region: LNRegion,
reverse: Option<String>,
#[serde_as(as="DisplayFromStr")]
time_updated: chrono::DateTime<chrono::Utc>,
}
#[serde_as]
#[derive(Serialize, Deserialize, Debug)]
pub struct VMInfoExtra {
@ -252,6 +272,33 @@ pub struct LNImage {
status: String, // should be stricter, at least "active", "inactive"(?)
}
#[serde_as]
#[derive(Serialize, Deserialize, Debug)]
pub struct VolumeResponse {
#[serde_as(as="DisplayFromStr")]
/// The personal ID used for the volume.
id: i32,
#[serde_as(as="DisplayFromStr")]
/// the UUID for the volume
identification: uuid::Uuid,
/// the name set by the user
name: String,
/// The region where the volume is located.
region: LNRegion,
/// The size of the volume, in GB
#[serde_as(as="DisplayFromStr")]
size: i32,
/// The status of the volume; this should be more string, but for now at least "active".
status: String,
#[serde_as(as="DisplayFromStr")]
/// a datetime (explicitly set to UTC) of when the volume was created
time_created: chrono::DateTime<chrono::Utc>,
#[serde_as(as="DisplayFromStr")]
/// ID of the user who created the volume
user_id: i32,
}
#[serde_as]
#[derive(Serialize, Deserialize, Debug)]
pub struct LNImageDetails {
cache_mode: String, // should be stricter, at least "writeback",
@ -268,7 +315,9 @@ pub struct LNImageDetails {
region: LNRegion, // sufficiently typed
size: i32, // in MB, maybe?
status: String, // should be stricter, at least: "active",
time_created: String, // should be a datetime
#[serde_as(as="DisplayFromStr")]
/// An (explicitly UTC) datetime of when the VM was created
time_created: chrono::DateTime<chrono::Utc>, // should be a datetime
}
#[derive(Serialize, Deserialize, Debug)]
pub struct LNImageDetailResponse {
@ -295,3 +344,21 @@ pub struct BillingCreditResponse {
success: bool, // this should be stricter
}
impl LunaNodeResponse for BillingCreditResponse {}
#[serde_as]
#[derive(Serialize, Deserialize, Debug)]
pub struct VolumeListResponse {
volumes: Vec<VolumeResponse>,
#[serde(with="success")]
success: bool,
}
impl LunaNodeResponse for VolumeListResponse {}
#[serde_as]
#[derive(Serialize, Deserialize, Debug)]
pub struct FloatingIpListResponse {
ips: Vec<FloatingIp>,
#[serde(with="success")]
success: bool,
}
impl LunaNodeResponse for FloatingIpListResponse {}

@ -1,5 +1,6 @@
use crate::success;
use clap;
use parse_display_derive::Display;
use hmac::{Hmac, Mac};
use serde::{
Serialize,
@ -10,7 +11,8 @@ use sha2::Sha512;
use ureq::post;
type HmacSHA512 = Hmac<Sha512>;
#[derive(Serialize, Deserialize, Debug, Eq, Hash, PartialEq, clap::Args, Clone)]
#[derive(Serialize, Deserialize, Debug, Eq, Hash, PartialEq, clap::Args, Clone, Display)]
#[display("{api_id}")]
/// Used to specify API credentials. These are not required as long as LUNANODE_KEY_ID and LUNANODE_API_KEY are specified in the environment.
pub struct ApiKeys {
#[clap(long, takes_value=true, env="LUNANODE_KEY_ID", help="The API key ID from the Lunanode Dashboard.", long_help="The API key ID from the Lunanode dashboard. You may also specify this option via the LUNANODE_KEY_ID environment variable.")]
@ -26,19 +28,9 @@ pub struct ApiKeys {
/// Since this should always be a 64 byte array, this will be enforced at some point in the future. TODO
pub api_partialkey: String,
}
impl ApiKeys {
pub fn new(api_id: String, api_key: String) -> Self {
Self {
api_id,
api_key: api_key.clone(),
api_partialkey: api_key[..64].to_string(),
}
}
}
pub trait Requestable: Serialize + ToString {}
pub trait LunaNodeResponse: DeserializeOwned {}
pub trait LunaNodeRequest: Requestable {
pub trait LunaNodeRequest: Serialize + std::fmt::Debug {
/// The resposne type you expect after making this request.
/// Setting this will allow acces to a generic function, make_request, that will expect this type to be recieved.
/// See: make_request
@ -95,9 +87,8 @@ fn make_request<S, D>(req: &S) -> Result<D, LNError>
{
let json_req_data = match serde_json::to_string(req) {
Ok(jrd) => jrd,
Err(e) => return Err(LNError::SerdeError(e, req.to_string())),
Err(e) => return Err(LNError::SerdeError(e, format!("{:?}", req))),
};
println!("RAW: {}", json_req_data);
let epoch = match std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH) {
Err(e) => return Err(LNError::TimeError(e)),
Ok(ue) => ue,
@ -108,7 +99,6 @@ fn make_request<S, D>(req: &S) -> Result<D, LNError>
hmac.update(hmac_input.as_bytes());
let signature = hex::encode(hmac.finalize().into_bytes());
let full_url = format!("https://dynamic.lunanode.com/api/{}", req.url_endpoint());
println!("FULL URL: {}", full_url);
match post(&full_url)
.send_form(&[
("req", &json_req_data),

Loading…
Cancel
Save