diff --git a/Cargo.toml b/Cargo.toml index 7a957f1b..742bcd23 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,9 +6,5 @@ members = [ "proxmox-sortable-macro", "proxmox-sys", "proxmox-tools", - -# This is an api server test and may be temporarily broken by changes to -# proxmox-api or proxmox-api-macro, but should ultimately be updated to work -# again as it's supposed to serve as an example code! - "api-test", + "proxmox", ] diff --git a/api-test/Cargo.toml b/api-test/Cargo.toml deleted file mode 100644 index 555b1625..00000000 --- a/api-test/Cargo.toml +++ /dev/null @@ -1,23 +0,0 @@ -[package] -name = "api-test" -edition = "2018" -version = "0.1.0" -authors = [ - "Dietmar Maurer ", - "Wolfgang Bumiller ", -] - -[dependencies] -bytes = "0.4" -endian_trait = { version = "0.6", features = [ "arrays" ] } -failure = "0.1" -futures-preview = "0.3.0-alpha" -http = "0.1" -hyper = { version = "0.13.0-alpha.2" } -lazy_static = "1.3" -proxmox = { path = "../proxmox", features = [ "api-macro" ] } -regex = "1.1" -serde = "1.0" -serde_json = "1.0" -serde_plain = "0.3" -tokio = { version = "0.2.0-alpha.4" } diff --git a/api-test/example.sh b/api-test/example.sh deleted file mode 100644 index 55a70a39..00000000 --- a/api-test/example.sh +++ /dev/null @@ -1,30 +0,0 @@ -#!/bin/sh - -# Example api-test client commands: -echo "Calling /api/1/greet:" -curl -XGET -H 'Content-type: application/json' \ - -d '{"person":"foo","message":"a message"}' \ - 'http://127.0.0.1:3000/api/1/greet' -echo - -echo "Calling /api/1/mount/rootfs" -# without the optional 'ro' field -curl -XPOST -H 'Content-type: application/json' \ - -d '{"entry":{"mount_type":"volume","source":"/source","destination":"/destination"}}' \ - 'http://127.0.0.1:3000/api/1/mount/rootfs' -echo - -echo "Calling /api/1/mount/rootfs again" -# with the optional 'ro' field -curl -XPOST -H 'Content-type: application/json' \ - -d '{"entry":{"mount_type":"volume","source":"/source","destination":"/destination","ro":true}}' \ - 'http://127.0.0.1:3000/api/1/mount/rootfs' -echo - -echo "Calling /api/1/mount/rootfs again, but with a destination which does NOT match the regex" -echo "Expect an error:" -# with the optional 'ro' field -curl -XPOST -H 'Content-type: application/json' \ - -d '{"entry":{"mount_type":"volume","source":"/source","destination":"./foo","ro":true}}' \ - 'http://127.0.0.1:3000/api/1/mount/rootfs' -echo diff --git a/api-test/src/bin/api-test.rs b/api-test/src/bin/api-test.rs deleted file mode 100644 index 96ec219f..00000000 --- a/api-test/src/bin/api-test.rs +++ /dev/null @@ -1,280 +0,0 @@ -use std::io; -use std::path::Path; - -use failure::{bail, format_err, Error}; -use http::Request; -use http::Response; -use hyper::service::{make_service_fn, service_fn}; -use hyper::{Body, Server}; -use serde_json::Value; -use tokio::io::AsyncReadExt; - -use proxmox::api::{api, router}; - -// -// Configuration: -// - -static mut WWW_DIR: Option = None; - -pub fn www_dir() -> &'static str { - unsafe { - WWW_DIR - .as_ref() - .expect("expected WWW_DIR to be initialized") - .as_str() - } -} - -pub fn set_www_dir(dir: String) { - unsafe { - assert!(WWW_DIR.is_none(), "WWW_DIR must only be initialized once!"); - - WWW_DIR = Some(dir); - } -} - -// -// Complex types allowed in the API -// - -#[api({ - description: "A test enum", -})] -#[derive(Clone, Copy, Debug, Eq, PartialEq)] -pub enum MountType { - Volume, - BindMount, - #[api(rename = "pass-through-device")] - PassThrough, -} - -#[api({ - description: "A test struct", - cli: false, // no CLI interface for now... - fields: { - mount_type: "The type of mount point", - source: "The path to mount", - destination: { - description: "Target path to mount at", - pattern: r#"^[^.]"#, // must not start with a dot - }, - ro: { - description: "Whether to mount read-only", - default: false, - }, - }, -})] -#[derive(Debug)] -pub struct MountEntry { - mount_type: MountType, - source: String, - destination: String, - ro: Option, -} - -// -// API methods -// - -router! { - pub static ROUTER: Router = { - GET: hello, - /www/{path}*: { GET: get_www }, - /api/1: { - /greet: { GET: greet_person_with }, - /mount/{id}: { POST: update_mount_point }, - } - }; -} - -#[api({ - description: "Hello API call", -})] -async fn hello() -> Result, Error> { - Ok(http::Response::builder() - .status(200) - .header("content-type", "text/html") - .body(Body::from("Hello"))?) -} - -#[api({ - description: "Get a file from the www/ subdirectory.", - parameters: { - path: "Path to the file to fetch", - }, -})] -async fn get_www(path: String) -> Result, Error> { - if path.contains("..") { - bail!("illegal path"); - } - - // FIXME: Add support for an ApiError type for 404s etc. to reduce error handling code size: - // Compiler bug: cannot use format!() in await expressions... - let file_path = format!("{}/{}", www_dir(), path); - let mut file = match tokio::fs::File::open(&file_path).await { - Ok(file) => file, - Err(ref err) if err.kind() == io::ErrorKind::NotFound => { - return Ok(http::Response::builder() - .status(404) - .body(Body::from(format!("No such file or directory: {}", path)))?); - } - Err(e) => return Err(e.into()), - }; - - let mut data = Vec::new(); - file.read_to_end(&mut data).await?; - - let mut response = http::Response::builder(); - response.status(200); - - let content_type = match Path::new(&path).extension().and_then(|e| e.to_str()) { - Some("html") => Some("text/html"), - Some("css") => Some("text/css"), - Some("js") => Some("application/javascript"), - Some("txt") => Some("text/plain"), - // ... - _ => None, - }; - if let Some(content_type) = content_type { - response.header("content-type", content_type); - } - - Ok(response.body(Body::from(data))?) -} - -#[api({ - description: "Create a greeting message with various parameters...", - parameters: { - person: "The person to greet", - message: "The message to give", - ps: "An optional PS message", - }, -})] -async fn greet_person_with( - person: String, - message: String, - ps: Option, -) -> Result { - Ok(match ps { - Some(ps) => format!("{}, {}.\n{}", person, message, ps), - None => format!("{}, {}.", person, message), - }) -} - -#[api({ - description: "Update or create the configuration for a mount point", - parameters: { - id: "Which mount point entry to configure", - entry: "The mount point configuration to replace the entry with", - }, -})] -async fn update_mount_point(id: String, entry: MountEntry) -> Result { - eprintln!("Got request to update mount point '{}'", id); - eprintln!("New configuration: {:?}", entry); - Ok(format!("Updating '{}' with: {:?}", id, entry)) -} - -// -// Hyper glue -// - -async fn json_body(mut body: Body) -> Result { - let mut data = Vec::new(); - while let Some(chunk) = body.next().await { - data.extend(chunk?); - } - Ok(serde_json::from_str(std::str::from_utf8(&data)?)?) -} - -async fn route_request(request: Request) -> Result, Error> { - let (parts, body) = request.into_parts(); - let path = parts.uri.path(); - - let (target, mut params) = ROUTER - .lookup(path) - .ok_or_else(|| format_err!("missing path: {}", path))?; - - use hyper::Method; - let method = match parts.method { - Method::GET => target.get.as_ref(), - Method::PUT => target.put.as_ref(), - Method::POST => target.post.as_ref(), - Method::DELETE => target.delete.as_ref(), - _ => bail!("unexpected method type"), - }; - - if let Some(ty) = parts.headers.get(http::header::CONTENT_TYPE) { - if ty.to_str()? == "application/json" { - let json = json_body(body).await?; - match json { - Value::Object(map) => { - for (k, v) in map { - let existed = params - .get_or_insert_with(serde_json::Map::new) - .insert(k, v) - .is_some(); - if existed { - bail!("tried to override path-based parameter!"); - } - } - } - _ => bail!("expected a json object"), - } - } - } - - method - .ok_or_else(|| format_err!("no {:?} method found for: {}", parts.method, path))? - .call(params.map(Value::Object).unwrap_or(Value::Null)) - .await -} - -async fn service_func(request: Request) -> Result, hyper::Error> { - match route_request(request).await { - Ok(r) => Ok(r), - Err(err) => Ok(Response::builder() - .status(400) - .body(Body::from(format!("ERROR: {}", err))) - .expect("building an error response...")), - } -} - -// -// Main entry point -// -async fn main_do(www_dir: String) { - // Construct our SocketAddr to listen on... - let addr = ([0, 0, 0, 0], 3000).into(); - - // And a MakeService to handle each connection... - let service = make_service_fn(|_| async { Ok::<_, hyper::Error>(service_fn(service_func)) }); - - // Then bind and serve... - let server = Server::bind(&addr).serve(service); - - println!("Serving {} under http://localhost:3000/www/", www_dir); - - if let Err(e) = server.await { - eprintln!("server error: {}", e); - } -} - -fn main() { - // We expect a path, where to find our files we expose via the www/ dir: - let mut args = std::env::args(); - - // real code should have better error handling - let _program_name = args.next(); - let www_dir = args.next().expect("expected a www/ subdirectory"); - set_www_dir(www_dir.to_string()); - - // show our api info: - println!( - "{}", - serde_json::to_string_pretty(&ROUTER.api_dump()).unwrap() - ); - - let rt = tokio::runtime::Runtime::new().unwrap(); - rt.block_on(main_do(www_dir)); -} diff --git a/api-test/src/lib.rs b/api-test/src/lib.rs deleted file mode 100644 index 35d66fbd..00000000 --- a/api-test/src/lib.rs +++ /dev/null @@ -1,4 +0,0 @@ -//! PVE base library - -pub mod lxc; -pub mod schema; diff --git a/api-test/src/lxc/mod.rs b/api-test/src/lxc/mod.rs deleted file mode 100644 index 728719da..00000000 --- a/api-test/src/lxc/mod.rs +++ /dev/null @@ -1,226 +0,0 @@ -//! PVE LXC module - -use std::collections::HashSet; - -use proxmox::api::api; - -use crate::schema::{ - types::{Memory, VolumeId}, - Architecture, -}; - -pub mod schema; - -#[api({ - description: "The PVE side of an lxc container configuration.", - cli: false, - fields: { - lock: "The current long-term lock held on this container by another operation.", - onboot: { - description: "Specifies whether a VM will be started during system bootup.", - default: false, - }, - startup: "The container's startup order.", - template: { - description: "Whether this is a template.", - default: false, - }, - arch: { - description: "The container's architecture type.", - default: Architecture::Amd64, - }, - ostype: { - description: - "OS type. This is used to setup configuration inside the container, \ - and corresponds to lxc setup scripts in \ - /usr/share/lxc/config/.common.conf. \ - Value 'unmanaged' can be used to skip and OS specific setup.", - }, - console: { - description: "Attach a console device (/dev/console) to the container.", - default: true, - }, - tty: { - description: "Number of ttys available to the container", - minimum: 0, - maximum: 6, - default: 2, - }, - cores: { - description: - "The number of cores assigned to the container. \ - A container can use all available cores by default.", - minimum: 1, - maximum: 128, - }, - cpulimit: { - description: - "Limit of CPU usage.\ - \n\n\ - NOTE: If the computer has 2 CPUs, it has a total of '2' CPU time. \ - Value '0' indicates no CPU limit.", - minimum: 0, - maximum: 128, - default: 0, - }, - cpuunits: { - description: - "CPU weight for a VM. Argument is used in the kernel fair scheduler. \ - The larger the number is, the more CPU time this VM gets. \ - Number is relative to the weights of all the other running VMs.\ - \n\n\ - NOTE: You can disable fair-scheduler configuration by setting this to 0.", - minimum: 0, - maximum: 500_000, - default: 1024, - }, - memory: { - description: "Amount of RAM for the VM.", - minimum: Memory::from_mebibytes(16), - default: Memory::from_mebibytes(512), - serialization: crate::schema::memory::optional::Parser:: - }, - swap: { - description: "Amount of SWAP for the VM.", - minimum: Memory::from_bytes(0), - default: Memory::from_mebibytes(512), - }, - hostname: { - description: "Set a host name for the container.", - maximum_length: 255, - minimum_length: 3, - format: crate::schema::dns_name, - }, - description: "Container description. Only used on the configuration web interface.", - searchdomain: { - description: - "Sets DNS search domains for a container. Create will automatically use the \ - setting from the host if you neither set searchdomain nor nameserver.", - format: crate::schema::dns_name, - serialization: crate::schema::string_list::optional, - }, - nameserver: { - description: - "Sets DNS server IP address for a container. Create will automatically use the \ - setting from the host if you neither set searchdomain nor nameserver.", - format: crate::schema::ip_address, - serialization: crate::schema::string_list::optional, - }, - rootfs: "Container root volume", - cmode: { - description: - "Console mode. By default, the console command tries to open a connection to one \ - of the available tty devices. By setting cmode to 'console' it tries to attach \ - to /dev/console instead. \ - If you set cmode to 'shell', it simply invokes a shell inside the container \ - (no login).", - default: schema::ConsoleMode::Tty, - }, - protection: { - description: - "Sets the protection flag of the container. \ - This will prevent the CT or CT's disk remove/update operation.", - default: false, - }, - unprivileged: { - description: - "Makes the container run as unprivileged user. (Should not be modified manually.)", - default: false, - }, - hookscript: { - description: - "Script that will be exectued during various steps in the containers lifetime.", - }, - }, -})] -#[derive(Default)] -pub struct Config { - // FIXME: short form? Since all the type info is literally factored out into the ConfigLock - // type already... - //#[api("The current long-term lock held on this container by another operation.")] - pub lock: Option, - pub onboot: Option, - pub startup: Option, - pub template: Option, - pub arch: Option, - pub ostype: Option, - pub console: Option, - pub tty: Option, - pub cores: Option, - pub cpulimit: Option, - pub cpuunits: Option, - pub memory: Option, - pub swap: Option, - pub hostname: Option, - pub description: Option, - pub searchdomain: Option>, - pub nameserver: Option>, - pub rootfs: Option, - // pub parent: Option, - // pub snaptime: Option, - pub cmode: Option, - pub protection: Option, - pub unprivileged: Option, - // pub features: Option, - pub hookscript: Option, -} - -#[api({ - description: "Container's rootfs definition", - cli: false, - fields: { - volume: { - description: "Volume, device or directory to mount into the container.", - format: crate::schema::safe_path, - // format_description: 'volume', - // default_key: 1, - }, - size: { - description: "Volume size (read only value).", - // format_description: 'DiskSize', - }, - acl: { - description: "Explicitly enable or disable ACL support.", - default: false, - }, - ro: { - description: "Read-only mount point.", - default: false, - }, - mountoptions: { - description: "Extra mount options for rootfs/mps.", - //format_description: "opt[;opt...]", - format: schema::mount_options, - serialization: crate::schema::string_set::optional, - }, - quota: { - description: - "Enable user quotas inside the container (not supported with zfs subvolumes)", - default: false, - }, - replicate: { - description: "Will include this volume to a storage replica job.", - default: true, - }, - shared: { - description: - "Mark this non-volume mount point as available on multiple nodes (see 'nodes')", - //verbose_description: - // "Mark this non-volume mount point as available on all nodes.\n\ - // \n\ - // WARNING: This option does not share the mount point automatically, it assumes \ - // it is shared already!", - default: false, - }, - }, -})] -pub struct Rootfs { - pub volume: String, - pub size: Option, - pub acl: Option, - pub ro: Option, - pub mountoptions: Option>, - pub quota: Option, - pub replicate: Option, - pub shared: Option, -} diff --git a/api-test/src/lxc/schema/mod.rs b/api-test/src/lxc/schema/mod.rs deleted file mode 100644 index dd3fe8db..00000000 --- a/api-test/src/lxc/schema/mod.rs +++ /dev/null @@ -1,50 +0,0 @@ -//! PVE LXC related schema module. - -use proxmox::api::api; - -#[api({ - description: "A long-term lock on a container", -})] -#[derive(Clone, Debug, Eq, PartialEq)] -pub enum ConfigLock { - Backup, - Create, - Disk, - Fstrim, - Migrate, - Mounted, - Rollback, - Snapshot, - #[api(rename = "snapshot-delete")] - SnapshotDelete, -} - -#[api({ - description: "Operating System Type.", -})] -#[derive(Clone, Copy, Debug, Eq, PartialEq)] -pub enum OsType { - Unmanaged, - Debian, - //... -} - -#[api({ - description: "Console mode.", -})] -#[derive(Clone, Copy, Debug, Eq, PartialEq)] -pub enum ConsoleMode { - Tty, - Console, - Shell, -} - -pub mod mount_options { - pub const NAME: &str = "mount options"; - - const VALID_MOUNT_OPTIONS: &[&str] = &["noatime", "nodev", "noexec", "nosuid"]; - - pub fn verify(value: &T) -> bool { - value.all(|s| VALID_MOUNT_OPTIONS.contains(&s)) - } -} diff --git a/api-test/src/schema/memory.rs b/api-test/src/schema/memory.rs deleted file mode 100644 index 4c65220e..00000000 --- a/api-test/src/schema/memory.rs +++ /dev/null @@ -1,110 +0,0 @@ -//! Serialization/deserialization for memory values with specific units. - -use std::marker::PhantomData; - -use super::types::Memory; - -pub trait Unit { - const FACTOR: u64; - const NAME: &'static str; -} - -pub struct B; -impl Unit for B { - const FACTOR: u64 = 1; - const NAME: &'static str = "bytes"; -} - -pub struct Kb; -impl Unit for Kb { - const FACTOR: u64 = 1024; - const NAME: &'static str = "kilobytes"; -} - -pub struct Mb; -impl Unit for Mb { - const FACTOR: u64 = 1024 * 1024; - const NAME: &'static str = "megabytes"; -} - -pub struct Gb; -impl Unit for Gb { - const FACTOR: u64 = 1024 * 1024 * 1024; - const NAME: &'static str = "gigabytes"; -} - -struct MemoryVisitor(PhantomData); -impl<'de, U: Unit> serde::de::Visitor<'de> for MemoryVisitor { - type Value = Memory; - - fn expecting(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { - write!(f, "amount of memory in {}", U::NAME) - } - - fn visit_u8(self, v: u8) -> Result { - Ok(Memory::from_bytes(u64::from(v) * U::FACTOR)) - } - fn visit_u16(self, v: u16) -> Result { - Ok(Memory::from_bytes(u64::from(v) * U::FACTOR)) - } - fn visit_u32(self, v: u32) -> Result { - Ok(Memory::from_bytes(u64::from(v) * U::FACTOR)) - } - fn visit_u64(self, v: u64) -> Result { - Ok(Memory::from_bytes(v * U::FACTOR)) - } - - fn visit_str(self, v: &str) -> Result { - match v.parse::() { - Ok(v) => Ok(Memory::from_bytes(v * U::FACTOR)), - Err(_) => v.parse().map_err(serde::de::Error::custom), - } - } -} - -pub struct Parser(PhantomData); - -impl Parser { - pub fn serialize(value: Memory, ser: S) -> Result - where - S: serde::Serializer, - { - if (value.as_bytes() % U::FACTOR) == 0 { - ser.serialize_u64(value.as_bytes() / U::FACTOR) - } else { - ser.serialize_str(&value.to_string()) - } - } - - pub fn deserialize<'de, D>(de: D) -> Result - where - D: serde::de::Deserializer<'de>, - { - de.deserialize_any(MemoryVisitor::(PhantomData)) - } -} - -pub mod optional { - use std::marker::PhantomData; - - use super::Unit; - use crate::schema::types::Memory; - - pub struct Parser(PhantomData); - - impl Parser { - pub fn serialize(value: &Option, ser: S) -> Result - where - S: serde::Serializer, - { - super::Parser::::serialize::(value.unwrap(), ser) - } - - pub fn deserialize<'de, D>(de: D) -> Result, D::Error> - where - D: serde::de::Deserializer<'de>, - { - super::Parser::::deserialize::<'de, D>(de).map(Some) - } - } -} diff --git a/api-test/src/schema/mod.rs b/api-test/src/schema/mod.rs deleted file mode 100644 index 8c720f24..00000000 --- a/api-test/src/schema/mod.rs +++ /dev/null @@ -1,79 +0,0 @@ -//! Common schema definitions. - -use proxmox::api::api; - -pub mod memory; -pub mod string_list; -pub mod string_set; -pub mod tools; -pub mod types; - -#[api({ - cli: false, - description: - r"Startup and shutdown behavior. \ - Order is a non-negative number defining the general startup order. \ - Shutdown in done with reverse ordering. \ - Additionally you can set the 'up' or 'down' delay in seconds, which specifies a delay \ - to wait before the next VM is started or stopped.", - fields: { - order: "Absolute ordering", - up: "Delay to wait before moving on to the next VM during startup.", - down: "Delay to wait before moving on to the next VM during shutdown.", - }, -})] -#[derive(Default)] -pub struct StartupOrder { - pub order: Option, - pub up: Option, - pub down: Option, -} - -#[api({description: "Architecture."})] -#[derive(Clone, Copy, Debug, Eq, PartialEq)] -pub enum Architecture { - // FIXME: suppport: #[api(alternatives = ["x86_64"])] - Amd64, - I386, - Arm64, - Armhf, -} - -pub mod dns_name { - use lazy_static::lazy_static; - use regex::Regex; - - pub const NAME: &str = "DNS name"; - - lazy_static! { - //static ref DNS_BASE_RE: Regex = - // Regex::new(r#"(?:[a-zA-Z0-9]([a-zA-Z0-9\-]*[a-zA-Z0-9])?)"#).unwrap(); - static ref REGEX: Regex = - Regex::new(r#"^(?x) - (?:[a-zA-Z0-9]([a-zA-Z0-9\-]*[a-zA-Z0-9])?) - (?:\.(?:[a-zA-Z0-9]([a-zA-Z0-9\-]*[a-zA-Z0-9])?))* - $"#).unwrap(); - } - - pub fn verify(value: &T) -> bool { - value.all(|s| REGEX.is_match(s)) - } -} - -pub mod ip_address { - pub const NAME: &str = "IP Address"; - - pub fn verify(value: &T) -> bool { - value.all(|s| proxmox::tools::common_regex::IP_REGEX.is_match(s)) - } -} - -pub mod safe_path { - pub const NAME: &str = "A canonical, absolute file system path"; - - pub fn verify(value: &T) -> bool { - value.all(|s| { - s != ".." && !s.starts_with("../") && !s.ends_with("/..") && !s.contains("/../") - }) - } -} diff --git a/api-test/src/schema/string_list.rs b/api-test/src/schema/string_list.rs deleted file mode 100644 index 04ae178a..00000000 --- a/api-test/src/schema/string_list.rs +++ /dev/null @@ -1,97 +0,0 @@ -//! Comma separated string list. -//! -//! Used as a proxy type for when a struct should contain a `Vec` which should be -//! serialized as a single comma separated list. - -use failure::{bail, Error}; - -pub trait ForEachStr { - fn for_each_str(&self, func: F) -> Result<(), Error> - where - F: FnMut(&str) -> Result<(), Error>; -} - -impl ForEachStr for Vec { - fn for_each_str(&self, mut func: F) -> Result<(), Error> - where - F: FnMut(&str) -> Result<(), Error>, - { - for i in self.iter() { - func(i.as_str())?; - } - Ok(()) - } -} - -pub fn serialize(value: &T, ser: S) -> Result -where - S: serde::Serializer, -{ - let mut data = String::new(); - value - .for_each_str(|s| { - if s.contains(',') { - bail!("cannot include value \"{}\" in a comma separated list", s); - } - - if !data.is_empty() { - data.push_str(", "); - } - data.push_str(s); - Ok(()) - }) - .map_err(serde::ser::Error::custom)?; - ser.serialize_str(&data) -} - -// maybe a custom visitor can also decode arrays by implementing visit_seq? -struct StringListVisitor; - -impl<'de> serde::de::Visitor<'de> for StringListVisitor { - type Value = Vec; - - fn expecting(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { - write!( - f, - "a comma separated list as a string, or an array of strings" - ) - } - - fn visit_str(self, v: &str) -> Result { - Ok(v.split(',').map(|i| i.trim().to_string()).collect()) - } - - fn visit_seq>(self, mut seq: A) -> Result { - let mut out = seq.size_hint().map_or_else(Vec::new, Vec::with_capacity); - while let Some(el) = seq.next_element::()? { - out.push(el); - } - Ok(out) - } -} - -pub fn deserialize<'de, D>(de: D) -> Result, D::Error> -where - D: serde::de::Deserializer<'de>, -{ - de.deserialize_any(StringListVisitor) -} - -pub mod optional { - pub fn serialize(value: &Option, ser: S) -> Result - where - S: serde::Serializer, - { - match value { - Some(value) => super::serialize(value, ser), - None => ser.serialize_none(), - } - } - - pub fn deserialize<'de, D>(de: D) -> Result>, D::Error> - where - D: serde::de::Deserializer<'de>, - { - super::deserialize(de).map(Some) - } -} diff --git a/api-test/src/schema/string_set.rs b/api-test/src/schema/string_set.rs deleted file mode 100644 index d041bbf1..00000000 --- a/api-test/src/schema/string_set.rs +++ /dev/null @@ -1,106 +0,0 @@ -//! A "set" of strings, semicolon separated, loaded into a `HashSet`. -//! -//! Used as a proxy type for when a struct should contain a `HashSet` which should be -//! serialized as a single comma separated list. - -use std::collections::HashSet; - -use failure::{bail, Error}; - -pub trait ForEachStr { - fn for_each_str(&self, func: F) -> Result<(), Error> - where - F: FnMut(&str) -> Result<(), Error>; -} - -impl ForEachStr for HashSet { - fn for_each_str(&self, mut func: F) -> Result<(), Error> - where - F: FnMut(&str) -> Result<(), Error>, - { - for i in self.iter() { - func(i.as_str())?; - } - Ok(()) - } -} - -pub fn serialize(value: &T, ser: S) -> Result -where - S: serde::Serializer, -{ - let mut data = String::new(); - value - .for_each_str(|s| { - if s.contains(';') { - bail!( - "cannot include value \"{}\" in a semicolon separated list", - s - ); - } - - if !data.is_empty() { - data.push_str(";"); - } - data.push_str(s); - Ok(()) - }) - .map_err(serde::ser::Error::custom)?; - ser.serialize_str(&data) -} - -// maybe a custom visitor can also decode arrays by implementing visit_seq? -struct StringSetVisitor; - -impl<'de> serde::de::Visitor<'de> for StringSetVisitor { - type Value = HashSet; - - fn expecting(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { - write!( - f, - "a string containing semicolon separated elements, or an array of strings" - ) - } - - fn visit_str(self, v: &str) -> Result { - Ok(v.split(';').map(|i| i.trim().to_string()).collect()) - } - - fn visit_seq>(self, mut seq: A) -> Result { - let mut out = seq - .size_hint() - .map_or_else(HashSet::new, HashSet::with_capacity); - while let Some(el) = seq.next_element::()? { - out.insert(el); - } - Ok(out) - } -} - -pub fn deserialize<'de, D>(de: D) -> Result, D::Error> -where - D: serde::de::Deserializer<'de>, -{ - de.deserialize_any(StringSetVisitor) -} - -pub mod optional { - use std::collections::HashSet; - - pub fn serialize(value: &Option, ser: S) -> Result - where - S: serde::Serializer, - { - match value { - Some(value) => super::serialize(value, ser), - None => ser.serialize_none(), - } - } - - pub fn deserialize<'de, D>(de: D) -> Result>, D::Error> - where - D: serde::de::Deserializer<'de>, - { - super::deserialize(de).map(Some) - } -} diff --git a/api-test/src/schema/tools.rs b/api-test/src/schema/tools.rs deleted file mode 100644 index 83376698..00000000 --- a/api-test/src/schema/tools.rs +++ /dev/null @@ -1,54 +0,0 @@ -//! Helper module to perform the same format checks on various string types. -//! -//! This is used for formats which should be checked on strings, arrays of strings, and optional -//! variants of both. - -use std::collections::HashSet; - -/// Allows testing predicates on all the contained strings of a type. -pub trait StringContainer { - fn all bool>(&self, pred: F) -> bool; -} - -impl StringContainer for String { - fn all bool>(&self, pred: F) -> bool { - pred(&self) - } -} - -impl StringContainer for Option { - fn all bool>(&self, pred: F) -> bool { - match self { - Some(ref v) => pred(v), - None => true, - } - } -} - -impl StringContainer for Vec { - fn all bool>(&self, pred: F) -> bool { - self.iter().all(|s| pred(&s)) - } -} - -impl StringContainer for Option> { - fn all bool>(&self, pred: F) -> bool { - self.as_ref() - .map(|c| StringContainer::all(c, pred)) - .unwrap_or(true) - } -} - -impl StringContainer for HashSet { - fn all bool>(&self, pred: F) -> bool { - self.iter().all(|s| pred(s)) - } -} - -impl StringContainer for Option> { - fn all bool>(&self, pred: F) -> bool { - self.as_ref() - .map(|c| StringContainer::all(c, pred)) - .unwrap_or(true) - } -} diff --git a/api-test/src/schema/types/memory.rs b/api-test/src/schema/types/memory.rs deleted file mode 100644 index ed64c8e1..00000000 --- a/api-test/src/schema/types/memory.rs +++ /dev/null @@ -1,203 +0,0 @@ -//! 'Memory' type, represents an amount of memory. - -use failure::Error; - -use proxmox::api::api; - -// TODO: manually implement Serialize/Deserialize to support both numeric and string -// representations. Numeric always being bytes, string having suffixes. -#[api({ - description: "Represents an amount of memory and can be expressed with suffixes such as MiB.", - serialize_as_string: true, -})] -#[derive(Clone, Copy, PartialEq, PartialOrd, Eq, Ord, Debug)] -#[repr(transparent)] -pub struct Memory(pub u64); - -impl std::str::FromStr for Memory { - type Err = Error; - - fn from_str(s: &str) -> Result { - if s.ends_with("KiB") { - Ok(Self::from_kibibytes(s[..s.len() - 3].parse()?)) - } else if s.ends_with("MiB") { - Ok(Self::from_mebibytes(s[..s.len() - 3].parse()?)) - } else if s.ends_with("GiB") { - Ok(Self::from_gibibytes(s[..s.len() - 3].parse()?)) - } else if s.ends_with("TiB") { - Ok(Self::from_tebibytes(s[..s.len() - 3].parse()?)) - } else if s.ends_with('K') { - Ok(Self::from_kibibytes(s[..s.len() - 1].parse()?)) - } else if s.ends_with('M') { - Ok(Self::from_mebibytes(s[..s.len() - 1].parse()?)) - } else if s.ends_with('G') { - Ok(Self::from_gibibytes(s[..s.len() - 1].parse()?)) - } else if s.ends_with('T') { - Ok(Self::from_tebibytes(s[..s.len() - 1].parse()?)) - } else if s.ends_with('b') || s.ends_with('B') { - Ok(Self::from_bytes(s[..s.len() - 1].parse()?)) - } else { - Ok(Self::from_bytes(s.parse()?)) - } - } -} -proxmox::api::derive_parse_cli_from_str!(Memory); - -#[test] -fn test_suffixes() { - use std::str::FromStr; - assert_eq!( - Memory::from_str("1234b").unwrap(), - Memory::from_str("1234").unwrap() - ); - assert_eq!( - Memory::from_str("4096K").unwrap(), - Memory::from_str("4M").unwrap() - ); -} - -impl std::fmt::Display for Memory { - fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { - const SUFFIXES: &[&str] = &["b", "KiB", "MiB", "GiB", "TiB"]; - let mut n = self.0; - let mut i = 0; - while i < SUFFIXES.len() && n.trailing_zeros() >= 10 { - n >>= 10; - i += 1; - } - write!(f, "{}{}", n, SUFFIXES[i]) - } -} - -impl Memory { - pub const fn from_bytes(v: u64) -> Self { - Self(v) - } - - pub const fn as_bytes(self) -> u64 { - self.0 - } - - pub const fn from_kibibytes(v: u64) -> Self { - Self(v * 1024) - } - - pub const fn as_kibibytes(self) -> u64 { - self.0 / 1024 - } - - pub const fn from_si_kilobytes(v: u64) -> Self { - Self(v * 1_000) - } - - pub const fn as_si_kilobytes(self) -> u64 { - self.0 / 1_000 - } - - pub const fn from_mebibytes(v: u64) -> Self { - Self(v * 1024 * 1024) - } - - pub const fn as_mebibytes(self) -> u64 { - self.0 / 1024 / 1024 - } - - pub const fn from_si_megabytes(v: u64) -> Self { - Self(v * 1_000_000) - } - - pub const fn as_si_megabytes(self) -> u64 { - self.0 / 1_000_000 - } - - pub const fn from_gibibytes(v: u64) -> Self { - Self(v * 1024 * 1024 * 1024) - } - - pub const fn as_gibibytes(self) -> u64 { - self.0 / 1024 / 1024 / 1024 - } - - pub const fn from_si_gigabytes(v: u64) -> Self { - Self(v * 1_000_000_000) - } - - pub const fn as_si_gigabytes(self) -> u64 { - self.0 / 1_000_000_000 - } - - pub const fn from_tebibytes(v: u64) -> Self { - Self(v * 1024 * 1024 * 1024 * 1024) - } - - pub const fn as_tebibytes(self) -> u64 { - self.0 / 1024 / 1024 / 1024 / 1024 - } - - pub const fn from_si_terabytes(v: u64) -> Self { - Self(v * 1_000_000_000_000) - } - - pub const fn as_si_terabytes(self) -> u64 { - self.0 / 1_000_000_000_000 - } -} - -impl std::ops::Add for Memory { - type Output = Memory; - - fn add(self, rhs: Memory) -> Memory { - Self(self.0 + rhs.0) - } -} - -impl std::ops::AddAssign for Memory { - fn add_assign(&mut self, rhs: Memory) { - self.0 += rhs.0; - } -} - -impl std::ops::Sub for Memory { - type Output = Memory; - - fn sub(self, rhs: Memory) -> Memory { - Self(self.0 - rhs.0) - } -} - -impl std::ops::SubAssign for Memory { - fn sub_assign(&mut self, rhs: Memory) { - self.0 -= rhs.0; - } -} - -impl std::ops::Mul for Memory { - type Output = Memory; - - fn mul(self, rhs: u64) -> Memory { - Self(self.0 * rhs) - } -} - -impl std::ops::MulAssign for Memory { - fn mul_assign(&mut self, rhs: u64) { - self.0 *= rhs; - } -} - -#[test] -fn memory() { - assert_eq!(Memory::from_mebibytes(1).as_kibibytes(), 1024); - assert_eq!(Memory::from_mebibytes(1).as_bytes(), 1024 * 1024); - assert_eq!(Memory::from_si_megabytes(1).as_bytes(), 1_000_000); - assert_eq!(Memory::from_tebibytes(1), Memory::from_gibibytes(1024)); - assert_eq!(Memory::from_gibibytes(1), Memory::from_mebibytes(1024)); - assert_eq!(Memory::from_mebibytes(1), Memory::from_kibibytes(1024)); - assert_eq!(Memory::from_kibibytes(1), Memory::from_bytes(1024)); - assert_eq!( - Memory::from_kibibytes(1) + Memory::from_bytes(6), - Memory::from_bytes(1030) - ); - assert_eq!("1M".parse::().unwrap(), Memory::from_mebibytes(1)); - assert_eq!("1MiB".parse::().unwrap(), Memory::from_mebibytes(1)); -} diff --git a/api-test/src/schema/types/mod.rs b/api-test/src/schema/types/mod.rs deleted file mode 100644 index 8e97b3f0..00000000 --- a/api-test/src/schema/types/mod.rs +++ /dev/null @@ -1,7 +0,0 @@ -//! Commonly used basic types, such as a type safe `Memory` type. - -mod memory; -pub use memory::Memory; - -mod volume_id; -pub use volume_id::VolumeId; diff --git a/api-test/src/schema/types/volume_id.rs b/api-test/src/schema/types/volume_id.rs deleted file mode 100644 index d14fadad..00000000 --- a/api-test/src/schema/types/volume_id.rs +++ /dev/null @@ -1,53 +0,0 @@ -//! A 'VolumeId' is a storage + volume combination. - -use failure::{format_err, Error}; - -use proxmox::api::api; - -#[api({ - serialize_as_string: true, - cli: FromStr, - description: "A volume ID consisting of a storage name and a volume name", - fields: { - storage: { - description: "A storage name", - pattern: r#"^[a-z][a-z0-9\-_.]*[a-z0-9]$"#, - }, - volume: "A volume name", - }, -})] -#[derive(Clone)] -pub struct VolumeId { - storage: String, - volume: String, -} - -impl std::fmt::Display for VolumeId { - fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { - write!(f, "{}:{}", self.storage, self.volume) - } -} - -impl std::str::FromStr for VolumeId { - type Err = Error; - - fn from_str(s: &str) -> Result { - let mut parts = s.splitn(2, ':'); - - let this = Self { - storage: parts - .next() - .ok_or_else(|| format_err!("not a volume id: {}", s))? - .to_string(), - volume: parts - .next() - .ok_or_else(|| format_err!("not a volume id: {}", s))? - .to_string(), - }; - assert!(parts.next().is_none()); - - proxmox::api::ApiType::verify(&this)?; - - Ok(this) - } -} diff --git a/api-test/tests/lxc-config.rs b/api-test/tests/lxc-config.rs deleted file mode 100644 index c65220d8..00000000 --- a/api-test/tests/lxc-config.rs +++ /dev/null @@ -1,190 +0,0 @@ -use failure::Error; - -use proxmox::api::ApiType; - -use api_test::lxc; - -/// This just checks the string in order to avoid `T: Eq` as requirement. -/// in other words: -/// assert that serialize(value) == serialize(deserialize(serialize(value))) -/// We assume that serialize(value) has already been checked before entering this function. -fn check_ser_de(value: &T) -> Result<(), Error> -where - T: ApiType + serde::Serialize + serde::de::DeserializeOwned, -{ - assert!(value.verify().is_ok()); - let s1 = serde_json::to_string(value)?; - let v2: T = serde_json::from_str(&s1)?; - assert!(v2.verify().is_ok()); - let s2 = serde_json::to_string(&v2)?; - assert_eq!(s1, s2); - Ok(()) -} - -#[test] -fn lxc_config() -> Result<(), Error> { - let mut config = lxc::Config::default(); - assert!(config.verify().is_ok()); - assert_eq!(serde_json::to_string(&config)?, "{}"); - check_ser_de(&config)?; - assert_eq!(*config.onboot(), false); - assert_eq!(*config.template(), false); - assert_eq!(*config.arch(), api_test::schema::Architecture::Amd64); - assert_eq!(*config.console(), true); - assert_eq!(*config.tty(), 2); - assert_eq!(*config.cmode(), api_test::lxc::schema::ConsoleMode::Tty); - assert_eq!(config.memory().as_bytes(), 512 << 20); - - config.lock = Some(lxc::schema::ConfigLock::Backup); - check_ser_de(&config)?; - assert_eq!(serde_json::to_string(&config)?, r#"{"lock":"backup"}"#); - - // test the renamed one: - config.lock = Some(lxc::schema::ConfigLock::SnapshotDelete); - check_ser_de(&config)?; - assert_eq!( - serde_json::to_string(&config)?, - r#"{"lock":"snapshot-delete"}"# - ); - - config.onboot = Some(true); - check_ser_de(&config)?; - assert_eq!( - serde_json::to_string(&config)?, - r#"{"lock":"snapshot-delete","onboot":true}"# - ); - assert_eq!(*config.onboot(), true); - - config.lock = None; - config.onboot = Some(false); - check_ser_de(&config)?; - assert_eq!(serde_json::to_string(&config)?, r#"{"onboot":false}"#); - assert_eq!(*config.onboot(), false); - - config.onboot = None; - check_ser_de(&config)?; - assert_eq!(*config.onboot(), false); - - config.set_onboot(true); - check_ser_de(&config)?; - assert_eq!(*config.onboot(), true); - assert_eq!(serde_json::to_string(&config)?, r#"{"onboot":true}"#); - - config.set_onboot(false); - check_ser_de(&config)?; - assert_eq!(*config.onboot(), false); - assert_eq!(serde_json::to_string(&config)?, r#"{"onboot":false}"#); - - config.set_template(true); - check_ser_de(&config)?; - assert_eq!(*config.template(), true); - assert_eq!( - serde_json::to_string(&config)?, - r#"{"onboot":false,"template":true}"# - ); - - config.onboot = None; - config.template = None; - - config.startup = Some(api_test::schema::StartupOrder { - order: Some(5), - ..Default::default() - }); - check_ser_de(&config)?; - assert_eq!( - serde_json::to_string(&config)?, - r#"{"startup":{"order":5}}"# - ); - - config = serde_json::from_str(r#"{"memory":"123MiB"}"#)?; - assert!(config.verify().is_ok()); - assert_eq!(serde_json::to_string(&config)?, r#"{"memory":123}"#); - - config = serde_json::from_str(r#"{"memory":"1024MiB"}"#)?; - assert!(config.verify().is_ok()); - assert_eq!(serde_json::to_string(&config)?, r#"{"memory":1024}"#); - - config = serde_json::from_str(r#"{"memory":"1300001KiB"}"#)?; - assert!(config.verify().is_ok()); - assert_eq!( - serde_json::to_string(&config)?, - r#"{"memory":"1300001KiB"}"# - ); - - // test numeric values - config = serde_json::from_str(r#"{"tty":3}"#)?; - assert!(config.verify().is_ok()); - assert_eq!(serde_json::to_string(&config)?, r#"{"tty":3}"#); - assert!(serde_json::from_str::(r#"{"tty":"3"}"#).is_err()); // string as int - - config = serde_json::from_str(r#"{"tty":9}"#)?; - assert_eq!( - config.verify().map_err(|e| e.to_string()), - Err("field tty out of range, must be <= 6".to_string()) - ); - - config = serde_json::from_str(r#"{"hostname":"xx"}"#)?; - assert_eq!( - config.verify().map_err(|e| e.to_string()), - Err("field hostname too short, must be >= 3 characters".to_string()) - ); - - config = serde_json::from_str(r#"{"hostname":"foo.bar.com"}"#)?; - assert_eq!( - serde_json::to_string(&config)?, - r#"{"hostname":"foo.bar.com"}"# - ); - assert!(config.verify().is_ok()); - - config = serde_json::from_str(r#"{"hostname":"foo"}"#)?; - assert!(config.verify().is_ok()); - - config = serde_json::from_str(r#"{"hostname":"..."}"#)?; - assert_eq!( - config.verify().map_err(|e| e.to_string()), - Err("field hostname does not match format DNS name".to_string()), - ); - - config = serde_json::from_str(r#"{"searchdomain":"foo.bar"}"#)?; - assert_eq!( - serde_json::to_string(&config)?, - r#"{"searchdomain":"foo.bar"}"# - ); - - config = serde_json::from_str(r#"{"searchdomain":"foo.."}"#)?; - assert_eq!( - config.verify().map_err(|e| e.to_string()), - Err("field searchdomain does not match format DNS name".to_string()), - ); - - config = serde_json::from_str(r#"{"searchdomain":"foo.com, bar.com"}"#)?; - assert!(config.verify().is_ok()); - assert_eq!( - serde_json::to_string(&config)?, - r#"{"searchdomain":"foo.com, bar.com"}"# - ); - config = serde_json::from_str(r#"{"searchdomain":["foo.com", "bar.com"]}"#)?; - assert!(config.verify().is_ok()); - assert_eq!( - serde_json::to_string(&config)?, - r#"{"searchdomain":"foo.com, bar.com"}"# - ); - - config = serde_json::from_str(r#"{"nameserver":["127.0.0.1", "::1"]}"#)?; - check_ser_de(&config)?; - - config = serde_json::from_str(r#"{"nameserver":"127.0.0.1, foo"}"#)?; - assert_eq!( - config.verify().map_err(|e| e.to_string()), - Err("field nameserver does not match format IP Address".to_string()), - ); - - config = serde_json::from_str(r#"{"cmode":"tty"}"#)?; - check_ser_de(&config)?; - config = serde_json::from_str(r#"{"cmode":"shell"}"#)?; - check_ser_de(&config)?; - config = serde_json::from_str(r#"{"hookscript":"local:snippets/foo.sh"}"#)?; - check_ser_de(&config)?; - - Ok(()) -} diff --git a/api-test/www/index.html b/api-test/www/index.html deleted file mode 100644 index 5b7ca3c4..00000000 --- a/api-test/www/index.html +++ /dev/null @@ -1,6 +0,0 @@ - - - -Hello - - diff --git a/proxmox-api-macro/src/api_def.rs b/proxmox-api-macro/src/api_def.rs deleted file mode 100644 index 3bcdc313..00000000 --- a/proxmox-api-macro/src/api_def.rs +++ /dev/null @@ -1,205 +0,0 @@ -use std::convert::TryFrom; - -use proc_macro2::{Ident, TokenStream}; - -use derive_builder::Builder; -use failure::{bail, Error}; -use quote::quote_spanned; - -use super::parsing::{Expression, Object}; - -#[derive(Clone)] -pub enum CliMode { - Disabled, - ParseCli, // By default we try proxmox::cli::ParseCli - FromStr, - Function(syn::Expr), -} - -impl Default for CliMode { - fn default() -> Self { - CliMode::ParseCli - } -} - -impl TryFrom for CliMode { - type Error = Error; - fn try_from(expr: Expression) -> Result { - if expr.is_ident("FromStr") { - return Ok(CliMode::FromStr); - } - - if let Ok(value) = expr.is_lit_bool() { - return Ok(if value.value { - CliMode::ParseCli - } else { - CliMode::Disabled - }); - } - - Ok(CliMode::Function(expr.expect_expr()?)) - } -} - -impl CliMode { - pub fn quote(&self, name: &Ident) -> TokenStream { - match self { - CliMode::Disabled => quote_spanned! { name.span() => None }, - CliMode::ParseCli => quote_spanned! { name.span() => - Some(<#name as ::proxmox::api::cli::ParseCli>::parse_cli) - }, - CliMode::FromStr => quote_spanned! { name.span() => - Some(<#name as ::proxmox::api::cli::ParseCliFromStr>::parse_cli) - }, - CliMode::Function(func) => quote_spanned! { name.span() => Some(#func) }, - } - } -} - -#[derive(Builder)] -pub struct CommonTypeDefinition { - pub description: syn::LitStr, - #[builder(default)] - pub cli: CliMode, -} - -impl CommonTypeDefinition { - fn builder() -> CommonTypeDefinitionBuilder { - CommonTypeDefinitionBuilder::default() - } - - pub fn from_object(obj: &mut Object) -> Result { - let mut def = Self::builder(); - - if let Some(value) = obj.remove("description") { - def.description(value.expect_lit_str()?); - } - if let Some(value) = obj.remove("cli") { - def.cli(CliMode::try_from(value)?); - } - - match def.build() { - Ok(r) => Ok(r), - Err(err) => bail!("{}", err), - } - } -} - -#[derive(Builder)] -pub struct ParameterDefinition { - #[builder(default)] - pub default: Option, - #[builder(default)] - pub description: Option, - #[builder(default)] - pub maximum: Option, - #[builder(default)] - pub minimum: Option, - #[builder(default)] - pub maximum_length: Option, - #[builder(default)] - pub minimum_length: Option, - #[builder(default)] - pub validate: Option, - - /// Formats are module paths. The module must contain a verify function: - /// `fn verify(Option<&str>) -> bool`, and a `NAME` constant used in error messages to refer to - /// the format name. - #[builder(default)] - pub format: Option, - - /// Patterns are regular expressions. When a literal string is provided, a `lazy_static` regex - /// is created for the verifier. Otherwise it is taken as an expression (i.e. a path) to an - /// existing regex variable/method. - #[builder(default)] - pub pattern: Option, - - #[builder(default)] - pub serialize_with: Option, - #[builder(default)] - pub deserialize_with: Option, -} - -impl ParameterDefinition { - pub fn builder() -> ParameterDefinitionBuilder { - Default::default() - } - - pub fn from_object(obj: Object) -> Result { - let mut def = ParameterDefinition::builder(); - - let obj_span = obj.span(); - for (key, value) in obj { - match key.as_str() { - "default" => { - def.default(Some(value.expect_expr()?)); - } - "description" => { - def.description(Some(value.expect_lit_str()?)); - } - "maximum" => { - def.maximum(Some(value.expect_expr()?)); - } - "minimum" => { - def.minimum(Some(value.expect_expr()?)); - } - "maximum_length" => { - def.maximum_length(Some(value.expect_expr()?)); - } - "minimum_length" => { - def.minimum_length(Some(value.expect_expr()?)); - } - "validate" => { - def.validate(Some(value.expect_expr()?)); - } - "format" => { - def.format(Some(value.expect_path()?)); - } - "pattern" => { - def.pattern(Some(value.expect_expr()?)); - } - "serialize_with" => { - def.serialize_with(Some(value.expect_path()?)); - } - "deserialize_with" => { - def.deserialize_with(Some(value.expect_path()?)); - } - "serialization" => { - let mut de = value.expect_path()?; - let mut ser = de.clone(); - ser.segments.push(syn::PathSegment { - ident: Ident::new("serialize", obj_span), - arguments: syn::PathArguments::None, - }); - de.segments.push(syn::PathSegment { - ident: Ident::new("deserialize", obj_span), - arguments: syn::PathArguments::None, - }); - def.deserialize_with(Some(de)); - def.serialize_with(Some(ser)); - } - other => c_bail!(key.span(), "invalid key in type definition: {}", other), - } - } - - match def.build() { - Ok(r) => Ok(r), - Err(err) => c_bail!(obj_span, "{}", err), - } - } - - pub fn from_expression(expr: Expression) -> Result { - let span = expr.span(); - match expr { - Expression::Expr(syn::Expr::Lit(lit)) => match lit.lit { - syn::Lit::Str(description) => Ok(ParameterDefinition::builder() - .description(Some(description)) - .build() - .map_err(|e| c_format_err!(span, "{}", e))?), - _ => c_bail!(span, "expected description or field definition"), - }, - Expression::Object(obj) => ParameterDefinition::from_object(obj), - _ => c_bail!(span, "expected description or field definition"), - } - } -} diff --git a/proxmox-api-macro/src/api_macro.rs b/proxmox-api-macro/src/api_macro.rs deleted file mode 100644 index 5e3f71ad..00000000 --- a/proxmox-api-macro/src/api_macro.rs +++ /dev/null @@ -1,47 +0,0 @@ -use proc_macro2::{Delimiter, TokenStream, TokenTree}; - -use failure::Error; -use quote::ToTokens; -use syn::spanned::Spanned; - -use crate::parsing::parse_object; - -mod enum_types; -mod function; -mod struct_types; - -pub fn api_macro(attr: TokenStream, item: TokenStream) -> Result { - let definition = attr - .into_iter() - .next() - .expect("expected api definition in braces"); - - let definition = match definition { - TokenTree::Group(ref group) if group.delimiter() == Delimiter::Brace => group.stream(), - _ => c_bail!(definition => "expected api definition in braces"), - }; - - let def_span = definition.span(); - let definition = parse_object(definition)?; - - // Now parse the item, based on which we decide whether this is an API method which needs a - // wrapper, or an API type which needs an ApiType implementation! - let mut item: syn::Item = syn::parse2(item).unwrap(); - - match item { - syn::Item::Struct(mut itemstruct) => { - let extra = struct_types::handle_struct(definition, &mut itemstruct)?; - let mut output = itemstruct.into_token_stream(); - output.extend(extra); - Ok(output) - } - syn::Item::Fn(func) => function::handle_function(def_span, definition, func), - syn::Item::Enum(ref mut itemenum) => { - let extra = enum_types::handle_enum(definition, itemenum)?; - let mut output = item.into_token_stream(); - output.extend(extra); - Ok(output) - } - _ => c_bail!(item => "api macro currently only applies to structs and functions"), - } -} diff --git a/proxmox-api-macro/src/api_macro/enum_types.rs b/proxmox-api-macro/src/api_macro/enum_types.rs deleted file mode 100644 index 1fe2ba64..00000000 --- a/proxmox-api-macro/src/api_macro/enum_types.rs +++ /dev/null @@ -1,187 +0,0 @@ -//! `#[api]` handler for enums. -//! -//! Simple enums without data are string types. Note that we usually use lower case enum values, -//! but rust wants CamelCase, so unless otherwise requested by the user, we convert `CamelCase` to -//! `underscore_case` automatically. -//! -//! For "string" enums we automatically implement `ToString`, `FromStr`, and derive `Serialize` and -//! `Deserialize` via `serde_plain`. - -use std::mem; - -use proc_macro2::TokenStream; - -use failure::Error; -use quote::quote_spanned; -use syn::spanned::Spanned; - -use crate::api_def::{CommonTypeDefinition, ParameterDefinition}; -use crate::parsing::Object; - -use crate::util; - -fn filter_api_items(attrs: &mut Vec, mut func: F) -> Result<(), Error> -where - F: FnMut(util::ApiItem) -> Result<(), Error>, -{ - let cap = attrs.len(); - for attr in mem::replace(attrs, Vec::with_capacity(cap)) { - if attr.path.get_ident().map(|i| i == "api").unwrap_or(false) { - let attrs: util::ApiAttr = syn::parse2(attr.tokens)?; - - for attr in attrs.items { - func(attr)?; - } - } else { - attrs.push(attr); - } - } - - Ok(()) -} - -pub fn handle_enum(mut definition: Object, item: &mut syn::ItemEnum) -> Result { - if item.generics.lt_token.is_some() { - c_bail!( - item.generics.span(), - "generic types are currently not supported" - ); - } - - let enum_ident = &item.ident; - let enum_name = enum_ident.to_string(); - let expected = format!("valid {}", enum_ident); - - let mut has_fields = false; - let mut has_verifier_unit_case = false; - let mut display_entries = TokenStream::new(); - let mut from_str_entries = TokenStream::new(); - let mut verify_entries = TokenStream::new(); - - for variant in item.variants.iter_mut() { - let variant_ident = &variant.ident; - let span = variant_ident.span(); - let underscore_name = util::to_underscore_case(&variant_ident.to_string()); - let mut underscore_name = syn::LitStr::new(&underscore_name, variant_ident.span()); - - filter_api_items(&mut variant.attrs, |attr| { - use util::ApiItem; - match attr { - ApiItem::Rename(to) => underscore_name = to, - //other => c_bail!(other.span(), "unsupported attribute on enum variant"), - } - Ok(()) - })?; - - match &variant.fields { - syn::Fields::Unit => { - if !has_fields { - display_entries.extend(quote_spanned! { - span => #enum_ident::#variant_ident => write!(f, #underscore_name), - }); - - from_str_entries.extend(quote_spanned! { - span => #underscore_name => Ok(#enum_ident::#variant_ident), - }); - } - - if !has_verifier_unit_case { - has_verifier_unit_case = true; - verify_entries.extend(quote_spanned! { span => _ => Ok(()), }); - } - } - syn::Fields::Named(_) => { - c_bail!(variant.span(), "#[api] enums cannot have struct fields"); - } - syn::Fields::Unnamed(unnamedfields) => { - has_fields = true; - let unnamed = &unnamedfields.unnamed; - - if unnamed.len() != 1 { - c_bail!( - unnamed.span(), - "#[api] enums variants may have at most 1 element" - ); - } - - verify_entries.extend(quote_spanned! { unnamed.span() => - #enum_ident::#variant_ident(ref inner) => { - ::proxmox::api::ApiType::verify(inner) - } - }); - } - } - } - - let common = CommonTypeDefinition::from_object(&mut definition)?; - let apidef = ParameterDefinition::from_object(definition)?; - - if let Some(validate) = apidef.validate { - c_bail!(validate => "validators are not allowed on enum types"); - } - - let display_fromstr_impls = if has_fields { - None - } else { - Some(quote_spanned! { item.span() => - impl ::std::fmt::Display for #enum_ident { - fn fmt(&self, f: &mut ::std::fmt::Formatter) -> ::std::fmt::Result { - match self { - #display_entries - } - } - } - - impl ::std::str::FromStr for #enum_ident { - type Err = ::failure::Error; - - fn from_str(s: &str) -> ::std::result::Result { - match s { - #from_str_entries - _ => ::failure::bail!("expected {}", #expected), - } - } - } - }) - }; - - let verify_impl = if has_fields { - quote_spanned! { item.span() => - fn verify(&self) -> ::std::result::Result<(), ::failure::Error> { - match self { - #verify_entries - } - } - } - } else { - quote_spanned! { item.span() => - fn verify(&self) -> ::std::result::Result<(), ::failure::Error> { - Ok(()) - } - } - }; - - let description = common.description; - let parse_cli = common.cli.quote(&enum_ident); - Ok(quote_spanned! { item.span() => - #display_fromstr_impls - - ::serde_plain::derive_deserialize_from_str!(#enum_ident, #expected); - ::serde_plain::derive_serialize_from_display!(#enum_ident); - ::proxmox::api::derive_parse_cli_from_str!(#enum_ident); - - impl ::proxmox::api::ApiType for #enum_ident { - fn type_info() -> &'static ::proxmox::api::TypeInfo { - const INFO: ::proxmox::api::TypeInfo = ::proxmox::api::TypeInfo { - name: #enum_name, - description: #description, - complete_fn: None, // FIXME! - parse_cli: #parse_cli, - }; - &INFO - } - - #verify_impl - } - }) -} diff --git a/proxmox-api-macro/src/api_macro/function.rs b/proxmox-api-macro/src/api_macro/function.rs deleted file mode 100644 index a356a9f5..00000000 --- a/proxmox-api-macro/src/api_macro/function.rs +++ /dev/null @@ -1,391 +0,0 @@ -//! Module for function handling. - -use proc_macro2::{Ident, Span, TokenStream}; - -use failure::{bail, format_err, Error}; -use quote::{quote, quote_spanned, ToTokens}; -use syn::{spanned::Spanned, Expr, Token}; - -use crate::parsing::{Expression, Object}; -use crate::util; - -pub fn handle_function( - def_span: Span, - mut definition: Object, - mut item: syn::ItemFn, -) -> Result { - if item.sig.generics.lt_token.is_some() { - c_bail!( - item.sig.generics.span(), - "cannot use generic functions for api macros currently", - ); - // Not until we stabilize our generated representation! - } - - // We cannot use #{foo.bar} in quote!, we can only use #foo, so these must all be local - // variables. (I'd prefer a struct and using `#{func.description}`, `#{func.protected}` etc. - // but that's not supported. - - let fn_api_description = definition - .remove("description") - .ok_or_else(|| c_format_err!(def_span, "missing 'description' in method definition"))? - .expect_lit_str()?; - - let fn_api_protected = definition - .remove("protected") - .map(|v| v.expect_lit_bool()) - .transpose()? - .unwrap_or_else(|| syn::LitBool { - span: Span::call_site(), - value: false, - }); - - let fn_api_reload_timezone = definition - .remove("reload-timezone") - .map(|v| v.expect_lit_bool()) - .transpose()? - .unwrap_or_else(|| syn::LitBool { - span: Span::call_site(), - value: false, - }); - - let body_type = definition - .remove("body") - .map(|v| v.expect_type()) - .transpose()? - .map_or_else(|| quote! { ::hyper::Body }, |v| v.into_token_stream()); - - let mut parameters = definition - .remove("parameters") - .map(|v| v.expect_object()) - .transpose()? - .unwrap_or_else(|| Object::new(Span::call_site())); - let mut parameter_entries = TokenStream::new(); - let mut parameter_verifiers = TokenStream::new(); - - let vis = std::mem::replace(&mut item.vis, syn::Visibility::Inherited); - let span = item.sig.ident.span(); - let name_str = item.sig.ident.to_string(); - //let impl_str = format!("{}_impl", name_str); - //let impl_ident = Ident::new(&impl_str, span); - let impl_checked_str = format!("{}_checked_impl", name_str); - let impl_checked_ident = Ident::new(&impl_checked_str, span); - let impl_unchecked_str = format!("{}_unchecked_impl", name_str); - let impl_unchecked_ident = Ident::new(&impl_unchecked_str, span); - let name = std::mem::replace(&mut item.sig.ident, impl_unchecked_ident.clone()); - let mut return_type = match item.sig.output { - syn::ReturnType::Default => syn::Type::Tuple(syn::TypeTuple { - paren_token: syn::token::Paren { - span: Span::call_site(), - }, - elems: syn::punctuated::Punctuated::new(), - }), - syn::ReturnType::Type(_, ref ty) => ty.as_ref().clone(), - }; - - let mut extracted_args = syn::punctuated::Punctuated::::new(); - let mut passed_args = syn::punctuated::Punctuated::::new(); - let mut arg_extraction = Vec::new(); - - let inputs = item.sig.inputs.clone(); - for arg in item.sig.inputs.iter() { - let arg = match arg { - syn::FnArg::Typed(ref arg) => arg, - other => bail!("unhandled type of method parameter ({:?})", other), - }; - - let arg_type = &arg.ty; - let name = match &*arg.pat { - syn::Pat::Ident(name) => &name.ident, - other => bail!("invalid kind of parameter pattern: {:?}", other), - }; - passed_args.push(name.clone()); - let name_str = name.to_string(); - - let arg_name = Ident::new(&format!("arg_{}", name_str), name.span()); - extracted_args.push(arg_name.clone()); - - arg_extraction.push(quote! { - let #arg_name = ::serde_json::from_value( - args - .remove(#name_str) - .unwrap_or(::serde_json::Value::Null) - )?; - }); - - let info = parameters - .remove(&name_str) - .ok_or_else(|| format_err!("missing parameter '{}' in api defintion", name_str))?; - - parameter_verifiers.extend(quote_spanned! { name.span() => - ::proxmox::api::ApiType::verify(&#name)?; - }); - match info { - Expression::Expr(Expr::Lit(lit)) => { - parameter_entries.extend(quote! { - ::proxmox::api::Parameter { - name: #name_str, - description: #lit, - type_info: <#arg_type as ::proxmox::api::ApiType>::type_info, - }, - }); - } - Expression::Expr(_) => bail!("description must be a string literal!"), - Expression::Object(mut param_info) => { - let description = param_info - .remove("description") - .ok_or_else(|| format_err!("missing 'description' in parameter definition"))? - .expect_lit_str()?; - - parameter_entries.extend(quote! { - ::proxmox::api::Parameter { - name: #name_str, - description: #description, - type_info: <#arg_type as ::proxmox::api::ApiType>::type_info, - }, - }); - - make_parameter_verifier( - &name, - &name_str, - &mut param_info, - &mut parameter_verifiers, - )?; - } - } - } - - if !parameters.is_empty() { - let mut list = String::new(); - for param in parameters.keys() { - if !list.is_empty() { - list.push_str(", "); - } - list.push_str(param.as_str()); - } - bail!( - "api definition contains parameters not found in function declaration: {}", - list - ); - } - - use std::iter::FromIterator; - let arg_extraction = TokenStream::from_iter(arg_extraction.into_iter()); - - // The router expects an ApiMethod, or more accurately, an object implementing ApiHandler. - // This is because we need access to a bunch of additional attributes of the functions both at - // runtime and when doing command line parsing/completion/help output. - // - // When manually implementing methods, we usually just write them out as an `ApiMethod` which - // is a type requiring all the info made available by the ApiHandler trait as members. - // - // While we could just generate a `const ApiMethod` for our functions, we would like them to - // also be usable as functions simply because the syntax we use to create them makes them - // *look* like functions, so it would be nice if they also *behaved* like real functions. - // - // Therefore all the fields of an ApiMethod are accessed via methods from the ApiHandler trait - // and we perform the same trick lazy_static does: Create a new type implementing ApiHandler, - // and make its instance Deref to an actual function. - // This way the function can still be used normally. Validators for parameters will be - // executed, serialization happens only when coming from the method's `handler`. - - let name_str = name.to_string(); - let struct_name = Ident::new(&util::to_camel_case(&name_str), name.span()); - let mut body = Vec::new(); - body.push(quote! { - // This is our helper struct which Derefs to a wrapper of our original function, which - // applies the added validators. - #vis struct #struct_name; - - #[allow(non_upper_case_globals)] - const #name: &#struct_name = &#struct_name; - - // Namespace some of our code into the helper type: - impl #struct_name { - // This is the original function, renamed to `#impl_unchecked_ident` - #item - - // This is the handler used by our router, which extracts the parameters out of a - // serde_json::Value, running the actual method, then serializing the output into an - // API response. - fn wrapped_api_handler( - args: ::serde_json::Value, - ) -> ::proxmox::api::ApiFuture<#body_type> { - async fn handler( - mut args: ::serde_json::Value, - ) -> ::proxmox::api::ApiOutput<#body_type> { - let mut empty_args = ::serde_json::map::Map::new(); - let args = args.as_object_mut() - .unwrap_or(&mut empty_args); - - #arg_extraction - - if !args.is_empty() { - let mut extra = String::new(); - for arg in args.keys() { - if !extra.is_empty() { - extra.push_str(", "); - } - extra.push_str(arg); - } - ::failure::bail!("unexpected extra parameters: {}", extra); - } - - let output = #struct_name::#impl_checked_ident(#extracted_args).await?; - ::proxmox::api::IntoApiOutput::into_api_output(output) - } - Box::pin(handler(args)) - } - } - }); - - if item.sig.asyncness.is_some() { - // An async function is expected to return its value, so we wrap it a bit: - body.push(quote! { - impl #struct_name { - async fn #impl_checked_ident(#inputs) -> #return_type { - #parameter_verifiers - Self::#impl_unchecked_ident(#passed_args).await - } - } - - // Our helper type derefs to a wrapper performing input validation and returning a - // Pin>. - // Unfortunately we cannot return the actual function since that won't work for - // `async fn`, since an `async fn` cannot appear as a return type :( - impl ::std::ops::Deref for #struct_name { - type Target = fn(#inputs) -> ::std::pin::Pin + Send - >>; - - fn deref(&self) -> &Self::Target { - const FUNC: fn(#inputs) -> ::std::pin::Pin + Send>> = |#inputs| { - Box::pin(#struct_name::#impl_checked_ident(#passed_args)) - }; - &FUNC - } - } - }); - } else { - // Non async fn must return an ApiFuture already! - return_type = syn::Type::Verbatim( - definition - .remove("returns") - .ok_or_else(|| { - format_err!( - "non async-fn must return a Response \ - and specify its return type via the `returns` property", - ) - })? - .expect_type()? - .into_token_stream(), - ); - - body.push(quote! { - impl #struct_name { - fn #impl_checked_ident(#inputs) -> ::proxmox::api::ApiFuture<#body_type> { - let check = (|| -> Result<(), Error> { - #parameter_verifiers - Ok(()) - })(); - if let Err(err) = check { - return Box::pin(async move { Err(err) }); - } - Self::#impl_unchecked_ident(#passed_args) - } - } - - // Our helper type derefs to a wrapper performing input validation and returning a - // Pin>. - // Unfortunately we cannot return the actual function since that won't work for - // `async fn`, since an `async fn` cannot appear as a return type :( - impl ::std::ops::Deref for #struct_name { - type Target = fn(#inputs) -> ::proxmox::api::ApiFuture<#body_type>; - - fn deref(&self) -> &Self::Target { - &(Self::#impl_checked_ident as Self::Target) - } - } - }); - } - - body.push(quote! { - // We now need to provide all the info required for routing, command line completion, API - // documentation, etc. - // - // Note that technically we don't need the `description` member in this trait, as this is - // mostly used at compile time for documentation! - impl ::proxmox::api::ApiMethodInfo for #struct_name { - fn description(&self) -> &'static str { - #fn_api_description - } - - fn parameters(&self) -> &'static [::proxmox::api::Parameter] { - // FIXME! - &[ #parameter_entries ] - } - - fn return_type(&self) -> &'static ::proxmox::api::TypeInfo { - <#return_type as ::proxmox::api::ApiType>::type_info() - } - - fn protected(&self) -> bool { - #fn_api_protected - } - - fn reload_timezone(&self) -> bool { - #fn_api_reload_timezone - } - } - - impl ::proxmox::api::ApiHandler for #struct_name { - type Body = #body_type; - - fn call(&self, params: ::serde_json::Value) -> ::proxmox::api::ApiFuture<#body_type> { - #struct_name::wrapped_api_handler(params) - } - - fn method_info(&self) -> &dyn ::proxmox::api::ApiMethodInfo { - self as _ - } - } - }); - - let body = TokenStream::from_iter(body); - //dbg!("{}", &body); - Ok(body) -} - -// FIXME: Unify with the struct version of this to avoid duplicate code! -fn make_parameter_verifier( - var: &Ident, - var_str: &str, - info: &mut Object, - out: &mut TokenStream, -) -> Result<(), Error> { - match info.remove("minimum") { - None => (), - Some(Expression::Expr(expr)) => out.extend(quote! { - let cmp = #expr; - if #var < cmp { - bail!("parameter '{}' is out of range (must be >= {})", #var_str, cmp); - } - }), - Some(_) => bail!("invalid value for 'minimum'"), - } - - match info.remove("maximum") { - None => (), - Some(Expression::Expr(expr)) => out.extend(quote! { - let cmp = #expr; - if #var > cmp { - bail!("parameter '{}' is out of range (must be <= {})", #var_str, cmp); - } - }), - Some(_) => bail!("invalid value for 'maximum'"), - } - - Ok(()) -} diff --git a/proxmox-api-macro/src/api_macro/struct_types.rs b/proxmox-api-macro/src/api_macro/struct_types.rs deleted file mode 100644 index b56a0cec..00000000 --- a/proxmox-api-macro/src/api_macro/struct_types.rs +++ /dev/null @@ -1,184 +0,0 @@ -//! Module for struct handling. -//! -//! This will forward to specialized variants for named structs, tuple structs and newtypes. - -use proc_macro2::{Ident, Span, TokenStream}; - -use failure::Error; -use quote::quote_spanned; -use syn::spanned::Spanned; - -use crate::api_def::ParameterDefinition; -use crate::parsing::Object; - -mod named; -mod newtype; -mod unnamed; - -/// Commonly used items of a struct field. -pub struct StructField<'i, 't> { - def: ParameterDefinition, - ident: Option<&'i Ident>, - access: syn::Member, - mem_id: isize, - string: String, - strlit: syn::LitStr, - ty: &'t syn::Type, -} - -pub fn handle_struct(definition: Object, item: &mut syn::ItemStruct) -> Result { - if item.generics.lt_token.is_some() { - c_bail!( - item.generics.span(), - "generic types are currently not supported" - ); - } - - let name = &item.ident; - - match item.fields { - syn::Fields::Unit => c_bail!(item.span(), "unit types are not allowed"), - syn::Fields::Unnamed(ref fields) if fields.unnamed.len() == 1 => { - newtype::handle_newtype(definition, name, fields, &mut item.attrs) - } - syn::Fields::Unnamed(ref fields) => { - unnamed::handle_struct_unnamed(definition, name, fields) - } - syn::Fields::Named(ref fields) => named::handle_struct_named(definition, name, fields), - } -} - -fn struct_fields_impl_verify(span: Span, fields: &[StructField]) -> Result { - let mut body = TokenStream::new(); - for field in fields { - let field_access = &field.access; - let field_str = &field.strlit; - - // first of all, recurse into the contained types: - body.extend(quote_spanned! { field_access.span() => - ::proxmox::api::ApiType::verify(&self.#field_access)?; - }); - - // then go through all the additional verifiers: - - if let Some(ref value) = field.def.minimum { - body.extend(quote_spanned! { value.span() => - let value = #value; - if !::proxmox::api::verify::TestMinMax::test_minimum(&self.#field_access, &value) { - error_list.push( - format!("field {} out of range, must be >= {}", #field_str, value) - ); - } - }); - } - - if let Some(ref value) = field.def.maximum { - body.extend(quote_spanned! { value.span() => - let value = #value; - if !::proxmox::api::verify::TestMinMax::test_maximum(&self.#field_access, &value) { - error_list.push( - format!("field {} out of range, must be <= {}", #field_str, value) - ); - } - }); - } - - if let Some(ref value) = field.def.minimum_length { - body.extend(quote_spanned! { value.span() => - let value = #value; - if !::proxmox::api::verify::TestMinMaxLen::test_minimum_length( - &self.#field_access, - value, - ) { - error_list.push( - format!("field {} too short, must be >= {} characters", #field_str, value) - ); - } - }); - } - - if let Some(ref value) = field.def.maximum_length { - body.extend(quote_spanned! { value.span() => - let value = #value; - if !::proxmox::api::verify::TestMinMaxLen::test_maximum_length( - &self.#field_access, - value, - ) { - error_list.push( - format!("field {} too long, must be <= {} characters", #field_str, value) - ); - } - }); - } - - if let Some(ref value) = field.def.format { - body.extend(quote_spanned! { value.span() => - if !#value::verify(&self.#field_access) { - error_list.push( - format!("field {} does not match format {}", #field_str, #value::NAME) - ); - } - }); - } - - if let Some(ref value) = field.def.pattern { - match value { - syn::Expr::Lit(regex) => body.extend(quote_spanned! { value.span() => - { - ::lazy_static::lazy_static! { - static ref RE: ::regex::Regex = ::regex::Regex::new(#regex).unwrap(); - } - if !RE.is_match(&self.#field_access) { - error_list.push(format!( - "field {} does not match the allowed pattern: {}", - #field_str, - #regex, - )); - } - } - }), - regex => body.extend(quote_spanned! { value.span() => - if !#regex.is_match(&self.#field_access) { - error_list.push( - format!("field {} does not match the allowed pattern", #field_str) - ); - } - }), - } - } - - if let Some(ref value) = field.def.validate { - body.extend(quote_spanned! { value.span() => - if let Err(err) = #value(&self.#field_access) { - error_list.push(err.to_string()); - } - }); - } - } - - if !body.is_empty() { - body = quote_spanned! { span => - #[allow(unused_mut)] - let mut error_list: Vec = Vec::new(); - #body - if !error_list.is_empty() { - let mut error_string = String::new(); - for e in error_list.iter() { - if !error_string.is_empty() { - error_string.push_str("\n"); - } - error_string.push_str(&e); - } - return Err(::failure::format_err!("{}", error_string)); - } - }; - } - - Ok(quote_spanned! { span => - fn verify(&self) -> ::std::result::Result<(), ::failure::Error> { - #body - - Ok(()) - } - }) -} diff --git a/proxmox-api-macro/src/api_macro/struct_types/named.rs b/proxmox-api-macro/src/api_macro/struct_types/named.rs deleted file mode 100644 index 41a23afd..00000000 --- a/proxmox-api-macro/src/api_macro/struct_types/named.rs +++ /dev/null @@ -1,474 +0,0 @@ -//! Handler for named struct types `struct Foo { name: T, ... }`. - -use proc_macro2::{Ident, Span, TokenStream}; - -use failure::{bail, Error}; -use quote::quote_spanned; -use syn::spanned::Spanned; - -use crate::api_def::{CommonTypeDefinition, ParameterDefinition}; -use crate::parsing::Object; - -use super::StructField; - -pub fn handle_struct_named( - mut definition: Object, - type_ident: &Ident, - item: &syn::FieldsNamed, -) -> Result { - let common = CommonTypeDefinition::from_object(&mut definition)?; - let mut field_def = definition - .remove("fields") - .ok_or_else(|| c_format_err!(definition.span(), "missing 'fields' entry"))? - .expect_object()?; - - let derive_default = definition - .remove("derive_default") - .map(|e| e.expect_lit_bool_direct()) - .transpose()? - .unwrap_or(false); - - if derive_default { - // We currently fill the actual `default` values from the schema into Option, but - // really Option should default to None even when there's a Default as our accessors - // will fill in the default at use-time... - bail!("derive_default is not finished"); - } - - let serialize_as_string = definition - .remove("serialize_as_string") - .map(|e| e.expect_lit_bool_direct()) - .transpose()? - .unwrap_or(false); - - let type_s = type_ident.to_string(); - let type_span = type_ident.span(); - let type_str = syn::LitStr::new(&type_s, type_span); - - let mut mem_id: isize = 0; - let mut fields = Vec::new(); - for field in item.named.iter() { - mem_id += 1; - let field_ident = field - .ident - .as_ref() - .ok_or_else(|| c_format_err!(field => "missing field name"))?; - let field_string = field_ident.to_string(); - - let field_strlit = syn::LitStr::new(&field_string, field_ident.span()); - - let def = field_def.remove(&field_string).ok_or_else( - || c_format_err!(field => "missing api description entry for field {}", field_string), - )?; - let def = ParameterDefinition::from_expression(def)?; - fields.push(StructField { - def, - ident: Some(field_ident), - access: syn::Member::Named(field_ident.clone()), - mem_id, - string: field_string, - strlit: field_strlit, - ty: &field.ty, - }); - } - - let impl_verify = super::struct_fields_impl_verify(item.span(), &fields)?; - let (impl_serialize, impl_deserialize) = if serialize_as_string { - let expected = format!("valid {}", type_ident); - ( - quote_spanned! { item.span() => - ::serde_plain::derive_serialize_from_display!(#type_ident); - }, - quote_spanned! { item.span() => - ::serde_plain::derive_deserialize_from_str!(#type_ident, #expected); - }, - ) - } else { - ( - named_struct_derive_serialize(item.span(), type_ident, &type_str, &fields)?, - named_struct_derive_deserialize(item.span(), type_ident, &type_str, &fields)?, - ) - }; - - let accessors = named_struct_impl_accessors(item.span(), type_ident, &fields)?; - - let impl_default = if derive_default { - named_struct_impl_default(item.span(), type_ident, &fields)? - } else { - TokenStream::new() - }; - - let description = common.description; - let parse_cli = common.cli.quote(&type_ident); - Ok(quote_spanned! { item.span() => - #impl_serialize - - #impl_deserialize - - #impl_default - - #accessors - - impl ::proxmox::api::ApiType for #type_ident { - fn type_info() -> &'static ::proxmox::api::TypeInfo { - const INFO: ::proxmox::api::TypeInfo = ::proxmox::api::TypeInfo { - name: #type_str, - description: #description, - complete_fn: None, // FIXME! - parse_cli: #parse_cli, - }; - &INFO - } - - #impl_verify - } - }) -} - -fn wrap_serialize_with( - span: Span, - name: &Ident, - ty: &syn::Type, - with: &syn::Path, -) -> (TokenStream, Ident) { - let helper_name = Ident::new( - &format!( - "SerializeWith{}", - crate::util::to_camel_case(&name.to_string()) - ), - name.span(), - ); - - ( - quote_spanned! { span => - struct #helper_name<'a>(&'a #ty); - - impl<'a> ::serde::ser::Serialize for #helper_name<'a> { - fn serialize(&self, serializer: S) -> std::result::Result - where - S: ::serde::ser::Serializer, - { - #with(self.0, serializer) - } - } - }, - helper_name, - ) -} - -fn wrap_deserialize_with( - span: Span, - name: &Ident, - ty: &syn::Type, - with: &syn::Path, -) -> (TokenStream, Ident) { - let helper_name = Ident::new( - &format!( - "DeserializeWith{}", - crate::util::to_camel_case(&name.to_string()) - ), - name.span(), - ); - - ( - quote_spanned! { span => - struct #helper_name<'de> { - value: #ty, - _lifetime: ::std::marker::PhantomData<&'de ()>, - } - - impl<'de> ::serde::de::Deserialize<'de> for #helper_name<'de> { - fn deserialize(deserializer: D) -> std::result::Result - where - D: ::serde::de::Deserializer<'de>, - { - Ok(Self { - value: #with(deserializer)?, - _lifetime: ::std::marker::PhantomData, - }) - } - } - }, - helper_name, - ) -} - -fn named_struct_derive_serialize( - span: Span, - type_ident: &Ident, - type_str: &syn::LitStr, - fields: &[StructField], -) -> Result { - let field_count = fields.len(); - - let mut entries = TokenStream::new(); - for field in fields { - let field_ident = field.ident.unwrap(); - let field_span = field_ident.span(); - let field_str = &field.strlit; - match field.def.serialize_with.as_ref() { - Some(path) => { - let (serializer, serializer_name) = - wrap_serialize_with(field_span, field_ident, &field.ty, path); - - entries.extend(quote_spanned! { field_span => - if !::proxmox::api::ApiType::should_skip_serialization(&self.#field_ident) { - #serializer - - state.serialize_field(#field_str, &#serializer_name(&self.#field_ident))?; - } - }); - } - None => { - entries.extend(quote_spanned! { field_span => - if !::proxmox::api::ApiType::should_skip_serialization(&self.#field_ident) { - state.serialize_field(#field_str, &self.#field_ident)?; - } - }); - } - } - } - - Ok(quote_spanned! { span => - impl ::serde::ser::Serialize for #type_ident { - fn serialize(&self, serializer: S) -> ::std::result::Result - where - S: ::serde::ser::Serializer, - { - use ::serde::ser::SerializeStruct; - let mut state = serializer.serialize_struct(#type_str, #field_count)?; - #entries - state.end() - } - } - }) -} - -fn named_struct_derive_deserialize( - span: Span, - type_ident: &Ident, - type_str: &syn::LitStr, - fields: &[StructField], -) -> Result { - let type_s = type_ident.to_string(); - let struct_type_str = syn::LitStr::new(&format!("struct {}", type_s), type_ident.span()); - let struct_type_field_str = - syn::LitStr::new(&format!("struct {} field name", type_s), type_ident.span()); - let visitor_ident = Ident::new(&format!("{}Visitor", type_s), type_ident.span()); - - let mut field_ident_list = TokenStream::new(); // ` member1, member2, ` - let mut field_name_matches = TokenStream::new(); // ` "member0" => 0, "member1" => 1, ` - let mut field_name_str_list = TokenStream::new(); // ` "member1", "member2", ` - let mut field_option_check_or_default_list = TokenStream::new(); - let mut field_option_init_list = TokenStream::new(); - let mut field_value_matches = TokenStream::new(); - for field in fields { - let field_ident = field.ident.unwrap(); - let field_span = field_ident.span(); - let field_str = &field.strlit; - let mem_id = field.mem_id; - - field_ident_list.extend(quote_spanned! { field_span => #field_ident, }); - - field_name_matches.extend(quote_spanned! { field_span => - #field_str => Field(#mem_id), - }); - - field_name_str_list.extend(quote_spanned! { field_span => #field_str, }); - - field_option_check_or_default_list.extend(quote_spanned! { field_span => - let #field_ident = ::proxmox::api::ApiType::deserialization_check( - #field_ident, - || ::serde::de::Error::missing_field(#field_str), - )?; - }); - - match field.def.deserialize_with.as_ref() { - Some(path) => { - let (deserializer, deserializer_name) = - wrap_deserialize_with(field_span, field_ident, &field.ty, path); - - field_option_init_list.extend(quote_spanned! { field_span => - #deserializer - - let mut #field_ident = None; - }); - - field_value_matches.extend(quote_spanned! { field_span => - Field(#mem_id) => { - if #field_ident.is_some() { - return Err(::serde::de::Error::duplicate_field(#field_str)); - } - let tmp: #deserializer_name = _api_macro_map_.next_value()?; - #field_ident = Some(tmp.value); - } - }); - } - None => { - field_option_init_list.extend(quote_spanned! { field_span => - let mut #field_ident = None; - }); - - field_value_matches.extend(quote_spanned! { field_span => - Field(#mem_id) => { - if #field_ident.is_some() { - return Err(::serde::de::Error::duplicate_field(#field_str)); - } - #field_ident = Some(_api_macro_map_.next_value()?); - } - }); - } - } - } - - Ok(quote_spanned! { span => - impl<'de> ::serde::de::Deserialize<'de> for #type_ident { - fn deserialize(deserializer: D) -> ::std::result::Result - where - D: ::serde::de::Deserializer<'de>, - { - #[repr(transparent)] - struct Field(isize); - - impl<'de> ::serde::de::Deserialize<'de> for Field { - fn deserialize(deserializer: D) -> ::std::result::Result - where - D: ::serde::de::Deserializer<'de>, - { - struct FieldVisitor; - - impl<'de> ::serde::de::Visitor<'de> for FieldVisitor { - type Value = Field; - - fn expecting( - &self, - formatter: &mut ::std::fmt::Formatter, - ) -> ::std::fmt::Result { - formatter.write_str(#struct_type_field_str) - } - - fn visit_str(self, value: &str) -> ::std::result::Result - where - E: ::serde::de::Error, - { - Ok(match value { - #field_name_matches - _ => { - return Err( - ::serde::de::Error::unknown_field(value, FIELDS) - ); - } - }) - } - } - - deserializer.deserialize_identifier(FieldVisitor) - } - } - - struct #visitor_ident; - - impl<'de> ::serde::de::Visitor<'de> for #visitor_ident { - type Value = #type_ident; - - fn expecting( - &self, - formatter: &mut ::std::fmt::Formatter, - ) -> ::std::fmt::Result { - formatter.write_str(#struct_type_str) - } - - fn visit_map( - self, - mut _api_macro_map_: V, - ) -> ::std::result::Result<#type_ident, V::Error> - where - V: ::serde::de::MapAccess<'de>, - { - #field_option_init_list - while let Some(_api_macro_key_) = _api_macro_map_.next_key()? { - match _api_macro_key_ { - #field_value_matches - _ => unreachable!(), - } - } - #field_option_check_or_default_list - Ok(#type_ident { - #field_ident_list - }) - } - } - - const FIELDS: &[&str] = &[ #field_name_str_list ]; - deserializer.deserialize_struct(#type_str, FIELDS, #visitor_ident) - } - } - }) -} - -fn named_struct_impl_accessors( - span: Span, - type_ident: &Ident, - fields: &[StructField], -) -> Result { - let mut accessor_methods = TokenStream::new(); - - for field in fields { - if let Some(ref default) = field.def.default { - let field_ident = field.ident; - let field_ty = &field.ty; - let set_field_ident = Ident::new(&format!("set_{}", field.string), field_ident.span()); - - accessor_methods.extend(quote_spanned! { default.span() => - pub fn #field_ident( - &self, - ) -> &<#field_ty as ::proxmox::api::meta::OrDefault>::Output { - static DEF: <#field_ty as ::proxmox::api::meta::OrDefault>::Output = #default; - ::proxmox::api::meta::OrDefault::or_default(&self.#field_ident, &DEF) - } - - pub fn #set_field_ident( - &mut self, - value: <#field_ty as ::proxmox::api::meta::OrDefault>::Output, - ) { - ::proxmox::api::meta::OrDefault::set(&mut self.#field_ident, value) - } - }); - } - } - - Ok(quote_spanned! { span => - impl #type_ident { - #accessor_methods - } - }) -} - -fn named_struct_impl_default( - span: Span, - type_ident: &Ident, - fields: &[StructField], -) -> Result { - let mut entries = TokenStream::new(); - for field in fields { - let field_ident = field.ident; - if let Some(ref default) = field.def.default { - entries.extend(quote_spanned! { field_ident.span() => - #field_ident: #default.into(), - }); - } else { - entries.extend(quote_spanned! { field_ident.span() => - #field_ident: Default::default(), - }); - } - } - Ok(quote_spanned! { span => - impl ::std::default::Default for #type_ident { - fn default() -> Self { - Self { - #entries - } - } - } - }) -} diff --git a/proxmox-api-macro/src/api_macro/struct_types/newtype.rs b/proxmox-api-macro/src/api_macro/struct_types/newtype.rs deleted file mode 100644 index bd84df21..00000000 --- a/proxmox-api-macro/src/api_macro/struct_types/newtype.rs +++ /dev/null @@ -1,195 +0,0 @@ -//! Handler for newtype structs `struct Foo(T)`. - -use std::mem; - -use proc_macro2::{Ident, Span, TokenStream}; - -use failure::Error; -use quote::{quote, quote_spanned}; -use syn::spanned::Spanned; - -use crate::api_def::{CommonTypeDefinition, ParameterDefinition}; -use crate::parsing::Object; - -use super::StructField; - -pub fn handle_newtype( - mut definition: Object, - type_ident: &Ident, - item: &syn::FieldsUnnamed, - attrs: &mut Vec, -) -> Result { - let type_s = type_ident.to_string(); - let type_span = type_ident.span(); - let type_str = syn::LitStr::new(&type_s, type_span); - - let fields = &item.unnamed; - let field = fields.first().unwrap(); - - let common = CommonTypeDefinition::from_object(&mut definition)?; - - let serialize_as_string = definition - .remove("serialize_as_string") - .map(|e| e.expect_lit_bool_direct()) - .transpose()? - .unwrap_or(false); - - let apidef = ParameterDefinition::from_object(definition)?; - - let impl_verify = super::struct_fields_impl_verify( - item.span(), - &[StructField { - def: apidef, - ident: None, - access: syn::Member::Unnamed(syn::Index { - index: 0, - span: type_ident.span(), - }), - mem_id: 0, - string: "0".to_string(), - strlit: syn::LitStr::new("0", type_ident.span()), - ty: &field.ty, - }], - )?; - - let (impl_serialize, impl_deserialize) = if serialize_as_string { - let expected = format!("valid {}", type_ident); - ( - quote_spanned! { item.span() => - ::serde_plain::derive_serialize_from_display!(#type_ident); - }, - quote_spanned! { item.span() => - ::serde_plain::derive_deserialize_from_str!(#type_ident, #expected); - }, - ) - } else { - ( - newtype_derive_serialize(item.span(), type_ident), - newtype_derive_deserialize(item.span(), type_ident), - ) - }; - - let derive_impls = newtype_filter_derive_attrs(type_ident, &field.ty, attrs)?; - - let description = common.description; - let parse_cli = common.cli.quote(&type_ident); - Ok(quote! { - #impl_serialize - - #impl_deserialize - - #derive_impls - - impl ::proxmox::api::ApiType for #type_ident { - fn type_info() -> &'static ::proxmox::api::TypeInfo { - use ::proxmox::api::cli::ParseCli; - use ::proxmox::api::cli::ParseCliFromStr; - const INFO: ::proxmox::api::TypeInfo = ::proxmox::api::TypeInfo { - name: #type_str, - description: #description, - complete_fn: None, // FIXME! - parse_cli: #parse_cli, - }; - &INFO - } - - #impl_verify - } - }) -} - -fn newtype_derive_serialize(span: Span, type_ident: &Ident) -> TokenStream { - quote_spanned! { span => - impl ::serde::ser::Serialize for #type_ident { - fn serialize(&self, serializer: S) -> ::std::result::Result - where - S: ::serde::ser::Serializer, - { - ::serde::ser::Serialize::serialize::(&self.0, serializer) - } - } - } -} - -fn newtype_derive_deserialize(span: Span, type_ident: &Ident) -> TokenStream { - quote_spanned! { span => - impl<'de> ::serde::de::Deserialize<'de> for #type_ident { - fn deserialize(deserializer: D) -> ::std::result::Result - where - D: ::serde::de::Deserializer<'de>, - { - Ok(Self(::serde::de::Deserialize::<'de>::deserialize::(deserializer)?)) - } - } - } -} - -fn newtype_filter_derive_attrs( - type_ident: &Ident, - inner_type: &syn::Type, - attrs: &mut Vec, -) -> Result { - let mut code = TokenStream::new(); - let mut had_from_str = false; - let mut had_display = false; - - let cap = attrs.len(); - for mut attr in mem::replace(attrs, Vec::with_capacity(cap)) { - if !attr.path.is_ident("derive") { - attrs.push(attr); - continue; - } - - let mut content: syn::Expr = syn::parse2(attr.tokens)?; - if let syn::Expr::Tuple(ref mut exprtuple) = content { - for ty in mem::replace(&mut exprtuple.elems, syn::punctuated::Punctuated::new()) { - if let syn::Expr::Path(ref exprpath) = ty { - if exprpath.path.is_ident("FromStr") { - if !had_from_str { - code.extend(newtype_derive_from_str( - exprpath.path.span(), - type_ident, - inner_type, - )); - } - had_from_str = true; - continue; - } else if exprpath.path.is_ident("Display") { - if !had_display { - code.extend(newtype_derive_display(exprpath.path.span(), type_ident)); - } - had_display = true; - continue; - } - } - exprtuple.elems.push(ty); - } - } - attr.tokens = quote! { #content }; - attrs.push(attr); - } - - Ok(code) -} - -fn newtype_derive_from_str(span: Span, type_ident: &Ident, inner_type: &syn::Type) -> TokenStream { - quote_spanned! { span => - impl ::std::str::FromStr for #type_ident { - type Err = <#inner_type as ::std::str::FromStr>::Err; - - fn from_str(s: &str) -> Result { - Ok(Self(::std::str::FromStr::from_str(s)?)) - } - } - } -} - -fn newtype_derive_display(span: Span, type_ident: &Ident) -> TokenStream { - quote_spanned! { span => - impl ::std::fmt::Display for #type_ident { - fn fmt(&self, f: &mut ::std::fmt::Formatter) -> ::std::fmt::Result { - ::std::fmt::Display::fmt(&self.0, f) - } - } - } -} diff --git a/proxmox-api-macro/src/api_macro/struct_types/unnamed.rs b/proxmox-api-macro/src/api_macro/struct_types/unnamed.rs deleted file mode 100644 index cae42a20..00000000 --- a/proxmox-api-macro/src/api_macro/struct_types/unnamed.rs +++ /dev/null @@ -1,56 +0,0 @@ -//! Handler for unnamed struct types `struct Foo(T1, T2, ...)`. -//! -//! Note that single-type structs are handled in the `newtype` module instead. - -use proc_macro2::{Ident, TokenStream}; - -use failure::{bail, Error}; -use quote::quote; - -use crate::api_def::{CommonTypeDefinition, ParameterDefinition}; -use crate::parsing::Object; - -//use super::StructField; - -pub fn handle_struct_unnamed( - mut definition: Object, - name: &Ident, - item: &syn::FieldsUnnamed, -) -> Result { - let fields = &item.unnamed; - if fields.len() != 1 { - bail!("only 1 unnamed field is currently allowed for api types"); - } - - //let field = fields.first().unwrap().value(); - - let common = CommonTypeDefinition::from_object(&mut definition)?; - let apidef = ParameterDefinition::from_object(definition)?; - - let validator = match apidef.validate { - Some(ident) => quote! { #ident(&self.0) }, - None => quote! { ::proxmox::api::ApiType::verify(&self.0) }, - }; - - let description = common.description; - let parse_cli = common.cli.quote(&name); - Ok(quote! { - impl ::proxmox::api::ApiType for #name { - fn type_info() -> &'static ::proxmox::api::TypeInfo { - use ::proxmox::api::cli::ParseCli; - use ::proxmox::api::cli::ParseCliFromStr; - const INFO: ::proxmox::api::TypeInfo = ::proxmox::api::TypeInfo { - name: stringify!(#name), - description: #description, - complete_fn: None, // FIXME! - parse_cli: #parse_cli, - }; - &INFO - } - - fn verify(&self) -> ::std::result::Result<(), ::failure::Error> { - #validator - } - } - }) -} diff --git a/proxmox-api-macro/src/lib.rs b/proxmox-api-macro/src/lib.rs index d42c2037..ff35caa4 100644 --- a/proxmox-api-macro/src/lib.rs +++ b/proxmox-api-macro/src/lib.rs @@ -1,160 +1 @@ #![recursion_limit = "256"] - -extern crate proc_macro; -extern crate proc_macro2; - -use proc_macro::TokenStream; - -#[macro_use] -mod util; - -mod api_def; -mod parsing; -mod types; - -mod api_macro; -mod router_macro; - -fn handle_error( - mut item: proc_macro2::TokenStream, - kind: &'static str, - err: failure::Error, -) -> TokenStream { - match err.downcast::() { - Ok(err) => { - let err: proc_macro2::TokenStream = err.to_compile_error(); - item.extend(err); - item.into() - } - Err(err) => panic!("error in {}: {}", kind, err), - } -} - -/// This is the `#[api(api definition)]` attribute for functions. An Api definition defines the -/// parameters and return type of an API call. The function will automatically be wrapped in a -/// function taking and returning a json `Value`, while performing validity checks on both input -/// and output. -/// -/// Example: -/// ```ignore -/// #[api({ -/// parameters: { -/// // Short form: [`optional`] TYPE ("description") -/// name: string ("A person's name"), -/// gender: optional string ("A person's gender"), -/// // Long form uses json-ish syntax: -/// coolness: { -/// type: integer, // we don't enclose type names in quotes though... -/// description: "the coolness of a person, using the coolness scale", -/// minimum: 0, -/// maximum: 10, -/// }, -/// // Hyphenated parameters are allowed, but need quotes (due to how proc_macro -/// // TokenStreams work) -/// "is-weird": optional float ("hyphenated names must be enclosed in quotes") -/// }, -/// // TODO: returns: {} -/// })] -/// fn test() { -/// } -/// ``` -#[proc_macro_attribute] -pub fn api(attr: TokenStream, item: TokenStream) -> TokenStream { - let item: proc_macro2::TokenStream = item.into(); - match api_macro::api_macro(attr.into(), item.clone()) { - Ok(output) => output.into(), - Err(err) => handle_error(item, "api definition", err), - } -} - -/// The router macro helps to avoid having to type out strangely nested `Router` expressions. -/// -/// Note that without `proc_macro_hack` we currently cannot use macros in expression position, so -/// this cannot be used inline within an expression. -/// -/// Example: -/// ```ignore -/// router!{ -/// let my_router = { -/// /people/{person}: { -/// POST: create_person, -/// GET: get_person, -/// PUT: update_person, -/// DELETE: delete_person, -/// }, -/// /people/{person}/kick: { POST: kick_person }, -/// /groups/{group}: { -/// /: { -/// POST: create_group, -/// PUT: update_group_info, -/// GET: get_group_info, -/// DELETE: delete_group, -/// }, -/// /people/{person}: { -/// POST: add_person_to_group, -/// DELETE: delete_person_from_group, -/// PUT: update_person_details_for_group, -/// GET: get_person_details_from_group, -/// }, -/// }, -/// /other: (an_external_router) -/// }; -/// } -/// ``` -/// -/// The above should produce the following output: -/// ```ignore -/// let my_router = Router::new() -/// .subdir( -/// "people", -/// Router::new() -/// .parameter_subdir( -/// "person", -/// Router::new() -/// .post(create_person) -/// .get(get_person) -/// .put(update_person) -/// .delete(delete_person) -/// .subdir( -/// "kick", -/// Router::new() -/// .post(kick_person) -/// ) -/// ) -/// ) -/// .subdir( -/// "groups", -/// Router::new() -/// .parameter_subdir( -/// "group", -/// Router::new() -/// .post(create_group) -/// .put(update_group_info) -/// .get(get_group_info) -/// .delete(delete_group_info) -/// .subdir( -/// "people", -/// Router::new() -/// .parameter_subdir( -/// "person", -/// Router::new() -/// .post(add_person_to_group) -/// .delete(delete_person_from_group) -/// .put(update_person_details_for_group) -/// .get(get_person_details_from_group) -/// ) -/// ) -/// ) -/// ) -/// .subdir("other", an_external_router) -/// ; -/// ``` -#[proc_macro] -pub fn router(input: TokenStream) -> TokenStream { - // TODO... - let input: proc_macro2::TokenStream = input.into(); - match router_macro::router_macro(input.clone()) { - Ok(output) => output.into(), - Err(err) => handle_error(input, "router", err), - } -} diff --git a/proxmox-api-macro/src/router_macro.rs b/proxmox-api-macro/src/router_macro.rs deleted file mode 100644 index 1d04c998..00000000 --- a/proxmox-api-macro/src/router_macro.rs +++ /dev/null @@ -1,433 +0,0 @@ -use std::collections::HashMap; - -use proc_macro2::{Delimiter, Ident, Span, TokenStream, TokenTree}; - -use failure::{bail, Error}; -use quote::{quote, quote_spanned}; -use syn::LitStr; - -use super::parsing::*; - -pub fn router_macro(input: TokenStream) -> Result { - let mut input = input.into_iter().peekable(); - - let mut out = TokenStream::new(); - - while let Some(ref peek_val) = input.peek() { - let mut at_span = peek_val.span(); - - let public = optional_visibility(&mut input)?; - - at_span = match_keyword(at_span, &mut input, "static")?; - let router_name = need_ident(at_span, &mut input)?; - - at_span = match_colon2(router_name.span(), &mut input)?; - at_span = match_keyword(at_span, &mut input, "Router")?; - at_span = match_punct(at_span, &mut input, '<')?; - let body_type = need_ident(at_span, &mut input)?; - at_span = match_punct(body_type.span(), &mut input, '>')?; - - at_span = match_punct(at_span, &mut input, '=')?; - let content = need_group(&mut input, Delimiter::Brace)?; - let _ = at_span; - at_span = content.span(); - - let router = parse_router(content.stream().into_iter().peekable())?; - let router = router.into_token_stream(&body_type, Some((router_name, public))); - - //eprintln!("{}", router.to_string()); - - out.extend(router); - - match_punct(at_span, &mut input, ';')?; - } - - Ok(out) -} - -/// A sub-route entry. This represents subdirectories in a route entry. -/// -/// This can either be a fixed set of directories, or a parameter name, in which case it matches -/// all directory names into the parameter of the specified name. -pub enum SubRoute { - Directories(HashMap), - Parameter(LitStr, Box), - Wildcard(LitStr), -} - -impl SubRoute { - /// Create an ampty directories entry. - fn directories() -> Self { - SubRoute::Directories(HashMap::new()) - } - - /// Create a parameter entry with an empty default router. - fn parameter(name: LitStr) -> Self { - SubRoute::Parameter(name, Box::new(Router::default())) - } -} - -/// A set of operations for a specific directory entry, and an optional sub router. -#[derive(Default)] -pub struct Router { - pub get: Option, - pub put: Option, - pub post: Option, - pub delete: Option, - pub subroute: Option, -} - -/// An entry for a router. -/// -/// While parsing a router we either get a `path: router` key/value entry, or a -/// `method: function_name` entry. -enum Entry { - /// This entry represents a path containing a sub router. - Path(Path), - /// This entry represents a method name. - Method(Ident), -} - -/// The components making up a path. -enum Component { - /// This component is a fixed sub directory name. Eg. `foo` or `baz` in `/foo/{bar}/baz`. - Name(LitStr), - - /// This component matches everything into a parameter. Eg. `bar` in `/foo/{bar}/baz`. - Match(LitStr), - - /// Matches the rest of the path into a parameters - Wildcard(LitStr), -} - -/// A path is just a list of components. -type Path = Vec; - -impl Router { - /// Insert a new router at a specific path. - /// - /// Note that this does not allow replacing an already existing router node. - fn insert(&mut self, path: Path, mut router: Router) -> Result<(), Error> { - let mut at = self; - let mut created = false; - for component in path { - created = false; - match component { - Component::Name(name) => { - let subroute = at.subroute.get_or_insert_with(SubRoute::directories); - match subroute { - SubRoute::Directories(hash) => { - at = hash.entry(name).or_insert_with(|| { - created = true; - Router::default() - }); - } - SubRoute::Parameter(_, _) => { - bail!("subdir '{}' clashes with matched parameter", name.value()); - } - SubRoute::Wildcard(_) => { - bail!("cannot add subdir '{}', it is already matched by a wildcard"); - } - } - } - Component::Match(name) => { - let subroute = at.subroute.get_or_insert_with(|| { - created = true; - SubRoute::parameter(name.clone()) - }); - match subroute { - SubRoute::Parameter(existing_name, router) => { - if name != *existing_name { - bail!( - "paramter matcher '{}' clashes with existing name '{}'", - name.value(), - existing_name.value(), - ); - } - at = router.as_mut(); - } - SubRoute::Directories(_) => { - bail!( - "parameter matcher '{}' clashes with existing directory", - name.value() - ); - } - SubRoute::Wildcard(_) => { - bail!("parameter matcher '{}' clashes with wildcard", name.value()); - } - } - } - Component::Wildcard(name) => { - if at.subroute.is_some() { - bail!("wildcard clashes with existing subdirectory"); - } - created = true; - if router.subroute.is_some() { - bail!("wildcard sub router cannot have subdirectories!"); - } - router.subroute = Some(SubRoute::Wildcard(name.clone())); - } - } - } - - if !created { - bail!("tried to replace existing path in router"); - } - std::mem::replace(at, router); - Ok(()) - } - - fn into_token_stream( - self, - body_type: &Ident, - name: Option<(Ident, syn::Visibility)>, - ) -> TokenStream { - use std::iter::FromIterator; - - let mut out = quote_spanned! { - body_type.span() => ::proxmox::api::Router::<#body_type>::new() - }; - - fn add_method(out: &mut TokenStream, name: &'static str, func_name: Ident) { - let name = Ident::new(name, func_name.span()); - out.extend(quote! { - .#name(#func_name) - }); - } - - if let Some(method) = self.get { - add_method(&mut out, "get", method); - } - if let Some(method) = self.put { - add_method(&mut out, "put", method); - } - if let Some(method) = self.post { - add_method(&mut out, "post", method); - } - if let Some(method) = self.delete { - add_method(&mut out, "delete", method); - } - - match self.subroute { - None => (), - Some(SubRoute::Parameter(name, router)) => { - let router = router.into_token_stream(body_type, None); - out.extend(quote! { - .parameter_subdir(#name, #router) - }); - } - Some(SubRoute::Directories(hash)) => { - for (name, router) in hash { - let router = router.into_token_stream(body_type, None); - out.extend(quote! { - .subdir(#name, #router) - }); - } - } - Some(SubRoute::Wildcard(name)) => { - out.extend(quote! { - .wildcard(#name) - }); - } - } - - if let Some((name, vis)) = name { - let type_name = Ident::new(&format!("{}_TYPE", name.to_string()), name.span()); - let var_name = name; - let router_expression = TokenStream::from_iter(out); - - quote! { - #[allow(non_camel_case_types)] - #vis struct #type_name( - std::cell::Cell>>, - std::sync::Once, - ); - unsafe impl Sync for #type_name {} - impl std::ops::Deref for #type_name { - type Target = ::proxmox::api::Router<#body_type>; - fn deref(&self) -> &Self::Target { - self.1.call_once(|| unsafe { - self.0.set(Some(#router_expression)); - }); - unsafe { - (*self.0.as_ptr()).as_ref().unwrap() - } - } - } - #vis static #var_name : #type_name = #type_name( - std::cell::Cell::new(None), - std::sync::Once::new(), - ); - } - } else { - TokenStream::from_iter(out) - } - } -} - -fn parse_router(mut input: TokenIter) -> Result { - let mut router = Router::default(); - loop { - match parse_entry_key(&mut input)? { - Some(Entry::Method(name)) => { - let function = need_ident(name.span(), &mut input)?; - - let method_ptr = match name.to_string().as_str() { - "GET" => &mut router.get, - "PUT" => &mut router.put, - "POST" => &mut router.post, - "DELETE" => &mut router.delete, - other => bail!("not a valid method name: {}", other.to_string()), - }; - - if method_ptr.is_some() { - bail!("duplicate method entry: {}", name.to_string()); - } - - *method_ptr = Some(function); - } - Some(Entry::Path(path)) => { - let sub_content = need_group(&mut input, Delimiter::Brace)?; - let sub_router = parse_router(sub_content.stream().into_iter().peekable())?; - router.insert(path, sub_router)?; - } - None => break, - } - comma_or_end(&mut input)?; - } - Ok(router) -} - -fn parse_entry_key(tokens: &mut TokenIter) -> Result, Error> { - match tokens.next() { - None => Ok(None), - Some(TokenTree::Punct(ref punct)) if punct.as_char() == '/' => { - Ok(Some(Entry::Path(parse_path_name(tokens)?))) - } - Some(TokenTree::Ident(ident)) => { - match_colon(tokens)?; - Ok(Some(Entry::Method(ident))) - } - Some(other) => bail!("invalid router entry: {:?}", other), - } -} - -fn parse_path_name(tokens: &mut TokenIter) -> Result { - let mut path = Path::new(); - let mut component = String::new(); - let mut span = None; - - fn push_component(path: &mut Path, component: &mut String, span: &mut Option) { - if !component.is_empty() { - path.push(Component::Name(LitStr::new( - &component, - span.take().unwrap(), - ))); - component.clear(); - } - }; - - loop { - match tokens.next() { - None => bail!("expected path component"), - Some(TokenTree::Group(group)) => { - if group.delimiter() != Delimiter::Brace { - bail!("invalid path component: {:?}", group); - } - let name = - need_hyphenated_name(group.span(), &mut group.stream().into_iter().peekable())?; - push_component(&mut path, &mut component, &mut span); - path.push(Component::Match(name)); - - // Now: - // `component` is empty - // Next tokens: - // `:` (and we're done) - // `/` (and we start the next component) - } - Some(TokenTree::Punct(ref punct)) if punct.as_char() == ':' => { - if !component.is_empty() { - // this only happens when we hit the '-' case - bail!("name must not end with a hyphen"); - } - break; - } - Some(TokenTree::Ident(ident)) => { - component.push_str(&ident.to_string()); - if span.is_none() { - span = Some(ident.span()); - } - - // Now: - // `component` is partially or fully filled - // Next tokens: - // `:` (and we're done) - // `/` (and we start the next component) - // `-` (the component name is not finished yet) - } - Some(TokenTree::Literal(literal)) => { - let text = literal.to_string(); - let litspan = literal.span(); - match syn::Lit::new(literal) { - syn::Lit::Int(_) => { - component.push_str(&text); - if span.is_none() { - span = Some(litspan); - } - } - other => { - bail!("invalid literal path component: {:?}", other); - } - } - // Same case as the Ident case above: - // Now: - // `component` is partially or fully filled - // Next tokens: - // `:` (and we're done) - // `/` (and we start the next component) - // `-` (the component name is not finished yet) - } - Some(other) => bail!("invalid path component: {:?}", other), - } - - // there may be hyphens here, but we don't allow space separated paths or other symbols - match tokens.next() { - None => break, - Some(TokenTree::Punct(punct)) => match punct.as_char() { - ':' => break, // okay in both cases - '-' => { - if component.is_empty() { - bail!("unexpected hyphen after parameter matcher"); - } - component.push('-'); - // `component` is partially filled, we need more - } - '/' => { - push_component(&mut path, &mut component, &mut span); - // `component` is cleared, we start the next one - } - '*' => { - // must be the last component, after a matcher - if !component.is_empty() { - bail!("wildcard must be the final matcher"); - } - if let Some(Component::Match(name)) = path.pop() { - path.push(Component::Wildcard(name)); - match_colon(&mut *tokens)?; - break; - } - bail!("asterisk only allowed at the end of a match pattern"); - } - other => bail!("invalid punctuation in path: {:?}", other), - }, - Some(other) => bail!( - "invalid path component, expected hyphen or slash: {:?}", - other - ), - } - } - - push_component(&mut path, &mut component, &mut span); - - Ok(path) -} diff --git a/proxmox-api-macro/tests/basic.rs b/proxmox-api-macro/tests/basic.rs deleted file mode 100644 index 4d82c0ae..00000000 --- a/proxmox-api-macro/tests/basic.rs +++ /dev/null @@ -1,147 +0,0 @@ -use bytes::Bytes; -use failure::{bail, Error}; -use serde_json::Value; - -use proxmox::api::{api, Router}; - -#[api({ - description: "A hostname or IP address", - validate: validate_hostname, -})] -#[repr(transparent)] -pub struct HostOrIp(String); - -// We don't bother with the CLI interface in this test: -proxmox::api::no_cli_type! {HostOrIp} - -// Simplified for example purposes -fn validate_hostname(name: &str) -> Result<(), Error> { - if name == "" { - bail!("found bad hostname"); - } - Ok(()) -} - -#[api({ - description: "A person definition containing name and ID", - fields: { - name: { - description: "The person's full name", - }, - id: { - description: "The person's ID number", - minimum: 1000, - maximum: 10000, - }, - }, - cli: false, -})] -pub struct Person { - name: String, - id: usize, -} - -#[api({ - body: Bytes, - description: "A test function returning a fixed text", - parameters: {}, -})] -async fn test_body() -> Result<&'static str, Error> { - Ok("test body") -} - -#[api({ - body: Bytes, - description: "Loopback the `input` parameter", - parameters: { - param: "the input", - }, -})] -async fn get_loopback(param: String) -> Result { - Ok(param) -} - -#[api({ - body: Bytes, - description: "Loopback the `input` parameter", - parameters: { - param: "the input", - }, - returns: String -})] -fn non_async_test(param: String) -> proxmox::api::ApiFuture { - Box::pin(async { proxmox::api::IntoApiOutput::into_api_output(param) }) -} - -proxmox_api_macro::router! { - static TEST_ROUTER: Router = { - GET: test_body, - - /subdir: { GET: test_body }, - /subdir/repeated: { GET: test_body }, - - /other: { GET: test_body }, - /other/subdir: { GET: test_body }, - - /more/{param}: { GET: get_loopback }, - /more/{param}/info: { GET: get_loopback }, - - /another/{param}: { - GET: get_loopback, - - /dir: { GET: non_async_test }, - }, - - /wild/{param}*: { GET: get_loopback }, - }; -} - -fn check_body(router: &Router, path: &str, expect: &'static str) { - let (router, parameters) = router - .lookup(path) - .expect("expected method to exist on test router"); - let method = router - .get - .as_ref() - .expect("expected GET method on router at path"); - let fut = method.call(parameters.map(Value::Object).unwrap_or(Value::Null)); - let resp = futures::executor::block_on(fut) - .expect("expected `GET` on test_body to return successfully"); - assert!(resp.status() == 200, "test response should have status 200"); - let body = resp.into_body(); - let body = std::str::from_utf8(&body).expect("expected test body to be valid utf8"); - assert!( - body == expect, - "expected test body output to be {:?}, found: {:?}", - expect, - body - ); -} - -#[test] -fn router() { - check_body(&TEST_ROUTER, "/subdir", r#"{"data":"test body"}"#); - check_body(&TEST_ROUTER, "/subdir/repeated", r#"{"data":"test body"}"#); - check_body(&TEST_ROUTER, "/more/argvalue", r#"{"data":"argvalue"}"#); - check_body( - &TEST_ROUTER, - "/more/argvalue/info", - r#"{"data":"argvalue"}"#, - ); - check_body(&TEST_ROUTER, "/another/foo", r#"{"data":"foo"}"#); - check_body(&TEST_ROUTER, "/another/foo/dir", r#"{"data":"foo"}"#); - - check_body(&TEST_ROUTER, "/wild", r#"{"data":""}"#); - check_body(&TEST_ROUTER, "/wild/", r#"{"data":""}"#); - check_body(&TEST_ROUTER, "/wild/asdf", r#"{"data":"asdf"}"#); - check_body(&TEST_ROUTER, "/wild//asdf", r#"{"data":"asdf"}"#); - check_body(&TEST_ROUTER, "/wild/asdf/poiu", r#"{"data":"asdf/poiu"}"#); - - // And can I... - let res = futures::executor::block_on(get_loopback("FOO".to_string())) - .expect("expected result from get_loopback"); - assert!( - res == "FOO", - "expected FOO from direct get_loopback('FOO') call" - ); -} diff --git a/proxmox-api-macro/tests/verification.rs b/proxmox-api-macro/tests/verification.rs deleted file mode 100644 index 1c2b0aec..00000000 --- a/proxmox-api-macro/tests/verification.rs +++ /dev/null @@ -1,135 +0,0 @@ -use bytes::Bytes; -use failure::{bail, Error}; -use serde_json::{json, Value}; - -use proxmox::api::{api, Router}; - -#[api({ - body: Bytes, - description: "A test function returning a fixed text", - parameters: { - number: { - description: "A number", - minimum: 3, - maximum: 10, - }, - reference: { - description: "A reference number", - minimum: 3, - maximum: 10, - }, - }, -})] -async fn less_than(number: usize, reference: usize) -> Result { - Ok(number < reference) -} - -proxmox_api_macro::router! { - static TEST_ROUTER: Router = { - GET: less_than, - }; -} - -fn check_parameter( - router: &Router, - path: &str, - parameters: Value, - expect: Result<&'static str, &'static str>, -) { - let (router, _) = router - .lookup(path) - .expect("expected method to exist on test router"); - let method = router - .get - .as_ref() - .expect("expected GET method on router at path"); - let fut = method.call(parameters); - match (futures::executor::block_on(fut), expect) { - (Ok(resp), Ok(exp)) => { - assert_eq!(resp.status(), 200, "test response should have status 200"); - let body = resp.into_body(); - let body = std::str::from_utf8(&body).expect("expected test body to be valid utf8"); - assert_eq!(body, exp, "expected successful output"); - } - (Err(resp), Err(exp)) => { - assert_eq!(resp.to_string(), exp.to_string(), "expected specific error"); - } - (Ok(resp), Err(exp)) => { - let body = resp.into_body(); - let body = std::str::from_utf8(&body).expect("expected test body to be valid utf8"); - panic!( - "expected function to fail with `{}`, but it succeeded with `{}`", - exp, body - ); - } - (Err(resp), Ok(exp)) => { - panic!( - "expected function to succeed with `{}`, but it failed with `{}`", - exp, resp - ); - } - } -} - -#[test] -fn router() { - // Expected successes: - check_parameter( - &TEST_ROUTER, - "/", - json!({ - "number": 3, - "reference": 5, - }), - Ok(r#"{"data":true}"#), - ); - - check_parameter( - &TEST_ROUTER, - "/", - json!({ - "number": 5, - "reference": 5, - }), - Ok(r#"{"data":false}"#), - ); - - // Expected failures: - check_parameter( - &TEST_ROUTER, - "/", - json!({ - "number": 1, - "reference": 5, - }), - Err("parameter 'number' is out of range (must be >= 3)"), - ); - - check_parameter( - &TEST_ROUTER, - "/", - json!({ - "number": 3, - "reference": 2, - }), - Err("parameter 'reference' is out of range (must be >= 3)"), - ); - - check_parameter( - &TEST_ROUTER, - "/", - json!({ - "number": 3, - "reference": 20, - }), - Err("parameter 'reference' is out of range (must be <= 10)"), - ); - - //// And can I... - let res = futures::executor::block_on(less_than(1, 5)).map_err(|x| x.to_string()); - assert_eq!( - res, - Err("parameter 'number' is out of range (must be >= 3)".to_string()), - "expected FOO from direct get_loopback('FOO') call" - ); -} diff --git a/proxmox-api/src/api_output.rs b/proxmox-api/src/api_output.rs deleted file mode 100644 index 6b1a03f8..00000000 --- a/proxmox-api/src/api_output.rs +++ /dev/null @@ -1,50 +0,0 @@ -//! Module to help converting various types into an ApiOutput, mostly required to support - -use serde_json::json; - -use super::{ApiOutput, ApiType}; - -/// Helper trait to convert a variable into an API output. -/// -/// If an API method returns a `String`, we want it to be jsonified into `{"data": result}` and -/// wrapped in a `http::Response` with a status code of `200`, but if an API method returns a -/// `http::Response`, we don't want that, our wrappers produced by the `#[api]` macro simply call -/// `output.into_api_output()`, and the trait implementation decides how to proceed. -pub trait IntoApiOutput { - fn into_api_output(self) -> ApiOutput; -} - -impl IntoApiOutput for T -where - Body: 'static, - T: ApiType + serde::Serialize, - Body: From, -{ - /// By default, any serializable type is serialized into a `{"data": output}` json structure, - /// and returned as http status 200. - fn into_api_output(self) -> ApiOutput { - let output = serde_json::to_value(self)?; - let res = json!({ "data": output }); - let output = serde_json::to_string(&res)?; - Ok(http::Response::builder() - .status(200) - .header("content-type", "application/json") - .body(Body::from(output))?) - } -} - -/// Methods returning `ApiOutput` (which is a `Result, Error>`) don't need -/// anything to happen to the value anymore, return the result as is: -impl IntoApiOutput> for ApiOutput { - fn into_api_output(self) -> ApiOutput { - self - } -} - -/// Methods returning a `http::Response` (without the `Result<_, Error>` around it) need to be -/// wrapped in a `Result`, as we do apply a `?` operator on our methods. -impl IntoApiOutput> for http::Response { - fn into_api_output(self) -> ApiOutput { - Ok(self) - } -} diff --git a/proxmox-api/src/api_type.rs b/proxmox-api/src/api_type.rs deleted file mode 100644 index 48f5b6d4..00000000 --- a/proxmox-api/src/api_type.rs +++ /dev/null @@ -1,366 +0,0 @@ -//! This contains traits used to implement methods to be added to the `Router`. - -use std::collections::HashSet; - -use failure::{bail, Error}; -use http::Response; -use serde_json::{json, Value}; - -/// Method entries in a `Router` are actually just `&dyn ApiMethodInfo` trait objects. -/// This contains all the info required to call, document, or command-line-complete parameters for -/// a method. -pub trait ApiMethodInfo: Send + Sync { - fn description(&self) -> &'static str; - fn parameters(&self) -> &'static [Parameter]; - fn return_type(&self) -> &'static TypeInfo; - fn protected(&self) -> bool; - fn reload_timezone(&self) -> bool; -} - -pub trait ApiHandler: ApiMethodInfo { - type Body; - - fn call(&self, params: Value) -> super::ApiFuture; - fn method_info(&self) -> &dyn ApiMethodInfo; -} - -impl dyn ApiHandler { - pub fn call_as(&self, params: Value) -> super::ApiFuture - where - Body: Into, - { - use futures::future::TryFutureExt; - Box::pin(self.call(params).map_ok(|res| res.map(|res| res.into()))) - } -} - -/// Shortcut to not having to type it out. This function signature is just a dummy and not yet -/// stabalized! -pub type CompleteFn = fn(&str) -> Vec; - -/// Provides information about a method's parameter. Every parameter has a name and must be -/// documented with a description, type information, and optional constraints. -pub struct Parameter { - pub name: &'static str, - pub description: &'static str, - pub type_info: fn() -> &'static TypeInfo, -} - -impl Parameter { - pub fn api_dump(&self) -> (&'static str, Value) { - ( - self.name, - json!({ - "description": self.description, - "type": (self.type_info)().name, - }), - ) - } - - /// Parse a commnd line option: if it is None, we only saw an `--option` without value, this is - /// fine for booleans. If we saw a value, we should try to parse it out into a json value. For - /// string parameters this means passing them as is, for others it means using FromStr... - pub fn parse_cli(&self, name: &str, value: Option<&str>) -> Result { - let info = (self.type_info)(); - match info.parse_cli { - Some(func) => func(name, value), - None => bail!( - "cannot parse parameter '{}' as command line parameter", - name - ), - } - } -} - -pub type ParseCliFn = fn(name: &str, value: Option<&str>) -> Result; - -/// Bare type info. Types themselves should also have a description, even if a method's parameter -/// usually overrides it. Ideally we can hyperlink the parameter to the type information in the -/// generated documentation. -pub struct TypeInfo { - pub name: &'static str, - pub description: &'static str, - pub complete_fn: Option, - pub parse_cli: Option, -} - -impl TypeInfo { - pub fn api_dump(&self) -> Value { - Value::String(self.name.to_string()) - } -} - -/// Until we can slap `#[api]` onto all the functions we can start translating our existing -/// `ApiMethod` structs to this new layout. -/// Otherwise this is mostly there so we can run the tests in the tests subdirectory without -/// depending on the api-macro crate. Tests using the macros belong into the api-macro crate itself -/// after all! -pub struct ApiMethod { - pub description: &'static str, - pub parameters: &'static [Parameter], - pub return_type: &'static TypeInfo, - pub protected: bool, - pub reload_timezone: bool, - pub handler: fn(Value) -> super::ApiFuture, -} - -impl ApiMethodInfo for ApiMethod { - fn description(&self) -> &'static str { - self.description - } - - fn parameters(&self) -> &'static [Parameter] { - self.parameters - } - - fn return_type(&self) -> &'static TypeInfo { - self.return_type - } - - fn protected(&self) -> bool { - self.protected - } - - fn reload_timezone(&self) -> bool { - self.reload_timezone - } -} - -impl ApiHandler for ApiMethod { - type Body = Body; - - fn call(&self, params: Value) -> super::ApiFuture { - (self.handler)(params) - } - - fn method_info(&self) -> &dyn ApiMethodInfo { - self as _ - } -} - -impl dyn ApiMethodInfo { - pub fn api_dump(&self) -> Value { - let parameters = Value::Object(std::iter::FromIterator::from_iter( - self.parameters() - .iter() - .map(|p| p.api_dump()) - .map(|(name, value)| (name.to_string(), value)), - )); - - json!({ - "description": self.description(), - "protected": self.protected(), - "reload-timezone": self.reload_timezone(), - "parameters": parameters, - "returns": self.return_type().api_dump(), - }) - } -} - -/// We're supposed to only use types in the API which implement `ApiType`, which forces types ot -/// have a `verify` method. The idea is that all parameters used in the API are documented -/// somewhere with their formats and limits, which are checked when entering and leaving API entry -/// points. -/// -/// Any API type is also required to implement `Serialize` and `DeserializeOwned`, since they're -/// read out of json `Value` types. -/// -/// While this is very useful for structural types, we sometimes to want to be able to pass a -/// simple unconstrainted type like a `String` with no restrictions, so most basic types implement -/// `ApiType` as well. -pub trait ApiType: Sized { - /// API types need to provide a `TypeInfo`, providing details about the underlying type. - fn type_info() -> &'static TypeInfo; - - /// Additionally, ApiTypes must provide a way to verify their constraints! - fn verify(&self) -> Result<(), Error>; - - /// This is a workaround for when we cannot name the type but have an object available we can - /// call a method on. (We cannot call associated methods on objects without being able to write - /// out the type, and rust has some restrictions as to what types are available.) - // eg. nested generics: - // fn foo() { - // fn bar(x: &T) { - // cannot use T::method() here, but can use x.method() - // (compile error "can't use generic parameter of outer function", - // and yes, that's a stupid restriction as it is still completely static...) - // } - // } - fn get_type_info(&self) -> &'static TypeInfo { - Self::type_info() - } - - #[inline] - fn should_skip_serialization(&self) -> bool { - false - } - - #[inline] - fn deserialization_check(this: Option, missing_error: F) -> Result - where - F: FnOnce() -> E, - { - this.ok_or_else(missing_error) - } -} - -/// Option types are supposed to wrap their underlying types with an `optional:` text in their -/// description. -// BUT it requires some anti-static magic. And while this looks like the result of lazy_static!, -// it's not exactly the same, lazy_static! here does not actually work as it'll curiously produce -// the same error as we pointed out above in the `get_type_info` method (as it does a lot more -// extra stuff we don't need)... -impl ApiType for Option { - fn verify(&self) -> Result<(), Error> { - if let Some(inner) = self { - inner.verify()? - } - Ok(()) - } - - fn type_info() -> &'static TypeInfo { - // FIXME: rust does not parameterize statics by the outer functions' generic parameters, so - // we cannot build special TypeInfo objects for options... - ::type_info() - /* DOES NOT WORK: - struct Data { - info: Cell>, - once: Once, - name: Cell>, - description: Cell>, - } - unsafe impl Sync for Data {} - static DATA: Data = Data { - info: Cell::new(None), - once: Once::new(), - name: Cell::new(None), - description: Cell::new(None), - }; - DATA.once.call_once(|| { - let info = T::type_info(); - DATA.name.set(Some(format!("optional: {}", info.name))); - DATA.description - .set(Some(format!("optional: {}", info.description))); - DATA.info.set(Some(TypeInfo { - name: unsafe { (*DATA.name.as_ptr()).as_ref().unwrap().as_str() }, - description: unsafe { (*DATA.description.as_ptr()).as_ref().unwrap().as_str() }, - complete_fn: info.complete_fn, - parse_cli: info.parse_cli, - })); - }); - unsafe { (*DATA.info.as_ptr()).as_ref().unwrap() } - */ - } - - #[inline] - fn should_skip_serialization(&self) -> bool { - self.is_none() - } - - #[inline] - fn deserialization_check(this: Option, _missing_error: F) -> Result - where - F: FnOnce() -> E, - { - Ok(this.unwrap_or(None)) - } -} - -/// Any `Result` of course gets the same info as `T`, since this only means that it can -/// fail... -impl ApiType for Result { - fn verify(&self) -> Result<(), Error> { - if let Ok(inner) = self { - inner.verify()? - } - Ok(()) - } - - fn type_info() -> &'static TypeInfo { - ::type_info() - } -} - -/// This is not supposed to be used, but can be if needed. This will provide an empty `ApiType` -/// declaration with no description and no verifier. -/// -/// This requires that the type already implements the `ParseCli` trait (or has a `parse_cli` type -/// of the same signature in view from any other trait). -/// -/// This rarely makes sense, but sometimes a `string` is just a `string`. -#[macro_export] -macro_rules! unconstrained_api_type { - ($type:ty $(, $more:ty)*) => { - impl $crate::ApiType for $type { - fn verify(&self) -> Result<(), ::failure::Error> { - Ok(()) - } - - fn type_info() -> &'static $crate::TypeInfo { - const INFO: $crate::TypeInfo = $crate::TypeInfo { - name: stringify!($type), - description: stringify!($type), - complete_fn: None, - parse_cli: Some(<$type as $crate::cli::ParseCli>::parse_cli), - }; - &INFO - } - } - - $crate::unconstrained_api_type!{$($more),*} - }; - () => {}; -} - -unconstrained_api_type! {Value} // basically our API's "any" type -unconstrained_api_type! {String, &str} -unconstrained_api_type! {()} -unconstrained_api_type! {bool} -unconstrained_api_type! {isize, usize, i64, u64, i32, u32, i16, u16, i8, u8, f64, f32} -unconstrained_api_type! {Vec} -unconstrained_api_type! {HashSet} - -// Raw return types are also okay: -impl ApiType for Response { - fn verify(&self) -> Result<(), Error> { - Ok(()) - } - - fn type_info() -> &'static TypeInfo { - const INFO: TypeInfo = TypeInfo { - name: "http::Response<>", - description: "A raw http response", - complete_fn: None, - parse_cli: None, - }; - &INFO - } -} - -// FIXME: make const once feature(const_fn) is stable! -pub fn get_type_info() -> &'static TypeInfo { - T::type_info() -} - -/// API methods can have different body types. For the CLI we don't care whether it is a -/// hyper::Body or a bytes::Bytes (also because we don't care for partia bodies etc.), so the -/// output needs to be wrapped to a common format. So basically the CLI will only ever see -/// `ApiOutput`. -pub trait UnifiedApiMethod: Send + Sync { - fn parameters(&self) -> &'static [Parameter]; - fn call(&self, params: Value) -> super::ApiFuture; -} - -impl UnifiedApiMethod for T -where - T: ApiHandler, - T::Body: 'static + Into, -{ - fn parameters(&self) -> &'static [Parameter] { - ApiMethodInfo::parameters(self) - } - - fn call(&self, params: Value) -> super::ApiFuture { - (self as &dyn ApiHandler).call_as(params) - } -} diff --git a/proxmox-api/src/cli.rs b/proxmox-api/src/cli.rs deleted file mode 100644 index 1b321ac8..00000000 --- a/proxmox-api/src/cli.rs +++ /dev/null @@ -1,406 +0,0 @@ -//! Provides Command Line Interface to API methods - -use std::collections::{HashMap, HashSet}; -use std::str::FromStr; - -use bytes::Bytes; -use failure::{bail, format_err, Error}; -use serde::Serialize; -use serde_json::Value; - -use super::{ApiHandler, ApiOutput, Parameter, UnifiedApiMethod}; - -type MethodInfoRef = &'static dyn UnifiedApiMethod; - -/// A CLI root node. -pub struct App { - name: &'static str, - command: Option, -} - -impl App { - /// Create a new empty App instance. - pub fn new(name: &'static str) -> Self { - Self { - name, - command: None, - } - } - - /// Directly connect this instance to a single API method. - /// - /// This is a builder method and will panic if there's already a method registered! - pub fn method(mut self, method: Method) -> Self { - assert!( - self.command.is_none(), - "app {} already has a comman!", - self.name - ); - - self.command = Some(Command::Method(method)); - self - } - - /// Add a subcommand to this instance. - /// - /// This is a builder method and will panic if the subcommand already exists or no subcommands - /// may be added. - pub fn subcommand(mut self, name: &'static str, subcommand: Command) -> Self { - match self - .command - .get_or_insert_with(|| Command::SubCommands(SubCommands::new())) - { - Command::SubCommands(ref mut commands) => { - commands.add_subcommand(name, subcommand); - self - } - _ => panic!("app {} cannot have subcommands!", self.name), - } - } - - /// Resolve a list of parameters to a method and a parameter json value. - pub fn resolve(&self, args: &[&str]) -> Result<(MethodInfoRef, Value), Error> { - self.command - .as_ref() - .ok_or_else(|| format_err!("no commands available"))? - .resolve(args.iter()) - } - - /// Run a command through this command line interface. - pub fn run(&self, args: &[&str]) -> ApiOutput { - let (method, params) = self.resolve(args)?; - let future = method.call(params); - futures::executor::block_on(future) - } -} - -/// A node in the CLI command router. This is either -pub enum Command { - Method(Method), - SubCommands(SubCommands), -} - -impl Command { - /// Create a Command entry pointing to an API method - pub fn method( - method: &'static T, - positional_args: &'static [&'static str], - ) -> Self - where - T: ApiHandler, - T::Body: 'static + Into, - { - Command::Method(Method::new(method, positional_args)) - } - - /// Create a new empty subcommand entry. - pub fn new() -> Self { - Command::SubCommands(SubCommands::new()) - } - - fn resolve(&self, args: std::slice::Iter<&str>) -> Result<(MethodInfoRef, Value), Error> { - match self { - Command::Method(method) => method.resolve(args), - Command::SubCommands(subcmd) => subcmd.resolve(args), - } - } -} - -pub struct SubCommands { - commands: HashMap<&'static str, Command>, -} - -#[allow(clippy::new_without_default)] -impl SubCommands { - /// Create a new empty SubCommands hash. - pub fn new() -> Self { - Self { - commands: HashMap::new(), - } - } - - /// Add a subcommand. - /// - /// Note that it is illegal for the subcommand to already exist, which will cause a panic. - pub fn add_subcommand(&mut self, name: &'static str, command: Command) -> &mut Self { - let old = self.commands.insert(name, command); - assert!(old.is_none(), "subcommand '{}' already exists", name); - self - } - - /// Builder method to add a subcommand. - /// - /// Note that it is illegal for the subcommand to already exist, which will cause a panic. - pub fn subcommand(mut self, name: &'static str, command: Command) -> Self { - self.add_subcommand(name, command); - self - } - - fn resolve(&self, mut args: std::slice::Iter<&str>) -> Result<(MethodInfoRef, Value), Error> { - match args.next() { - None => bail!("missing subcommand"), - Some(arg) => match self.commands.get(arg) { - None => bail!("no such subcommand: {}", arg), - Some(cmd) => cmd.resolve(args), - }, - } - } -} - -/// A reference to an API method. Note that when coming from the command line, it is possible to -/// match some parameters as positional parameters rather than argument switches, therefor this -/// contains an ordered list of positional parameters. -/// -/// Note that we currently do not support optional positional parameters. -// XXX: If we want optional positional parameters - should we make an enum or just say the -// parameter name should have brackets around it? -pub struct Method { - pub method: MethodInfoRef, - pub positional_args: &'static [&'static str], - //pub formatter: Option<()>, // TODO: output formatter -} - -impl Method { - /// Create a new reference to an API method. - pub fn new(method: MethodInfoRef, positional_args: &'static [&'static str]) -> Self { - Self { - method, - positional_args, - } - } - - fn resolve(&self, mut args: std::slice::Iter<&str>) -> Result<(MethodInfoRef, Value), Error> { - let mut params = serde_json::Map::new(); - let mut positionals = self.positional_args.iter(); - - let mut current_option = None; - loop { - match next_arg(&mut args) { - Some(Arg::Opt(arg)) => { - if let Some(arg) = current_option.take() { - self.add_parameter(&mut params, arg, None)?; - } - - current_option = Some(arg); - } - Some(Arg::OptArg(arg, value)) => { - if let Some(arg) = current_option.take() { - self.add_parameter(&mut params, arg, None)?; - } - - self.add_parameter(&mut params, arg, Some(value))?; - } - Some(Arg::Positional(value)) => match current_option.take() { - Some(arg) => self.add_parameter(&mut params, arg, Some(value))?, - None => match positionals.next() { - Some(arg) => self.add_parameter(&mut params, arg, Some(value))?, - None => bail!("unexpected positional parameter: '{}'", value), - }, - }, - None => { - if let Some(arg) = current_option.take() { - self.add_parameter(&mut params, arg, None)?; - } - break; - } - } - } - assert!( - current_option.is_none(), - "current_option must have been dealt with" - ); - - let missing = positionals.fold(String::new(), |mut acc, more| { - if acc.is_empty() { - more.to_string() - } else { - acc.push_str(", "); - acc.push_str(more); - acc - } - }); - if !missing.is_empty() { - bail!("missing positional parameters: {}", missing); - } - - Ok((self.method, Value::Object(params))) - } - - /// This should insert the parameter 'arg' with value 'value' into 'params'. - /// This means we need to verify `arg` exists in self.method, `value` deserializes to its type, - /// and then serialize it into the Value. - fn add_parameter( - &self, - params: &mut serde_json::Map, - arg: &str, - value: Option<&str>, - ) -> Result<(), Error> { - let param_def = self - .find_parameter(arg) - .ok_or_else(|| format_err!("no such parameter: '{}'", arg))?; - params.insert(arg.to_string(), param_def.parse_cli(arg, value)?); - Ok(()) - } - - fn find_parameter(&self, name: &str) -> Option<&Parameter> { - self.method.parameters().iter().find(|p| p.name == name) - } -} - -#[allow(clippy::enum_variant_names)] -enum Arg<'a> { - Positional(&'a str), - Opt(&'a str), - OptArg(&'a str, &'a str), -} - -fn next_arg<'a>(args: &mut std::slice::Iter<&'a str>) -> Option> { - args.next().map(|arg| { - if arg.starts_with("--") { - let arg = &arg[2..]; - - match arg.find('=') { - Some(idx) => Arg::OptArg(&arg[0..idx], &arg[idx + 1..]), - None => Arg::Opt(arg), - } - } else { - Arg::Positional(arg) - } - }) -} - -pub fn parse_cli_from_str(name: &str, value: Option<&str>) -> Result -where - T: FromStr + Serialize, - ::Err: Into, -{ - let this: T = value - .ok_or_else(|| format_err!("missing parameter value for '{}'", name))? - .parse() - .map_err(|e: ::Err| e.into())?; - Ok(serde_json::to_value(this)?) -} - -/// We use this trait so we can keep the "mass implementation macro" for the ApiType trait simple -/// and specialize the CLI parameter parsing via this trait separately. -pub trait ParseCli { - fn parse_cli(name: &str, value: Option<&str>) -> Result; -} - -/// This is a version of ParseCli with a default implementation falling to FromStr. -pub trait ParseCliFromStr -where - Self: FromStr + Serialize, - ::Err: Into, -{ - fn parse_cli(name: &str, value: Option<&str>) -> Result { - parse_cli_from_str::(name, value) - } -} - -impl ParseCliFromStr for T -where - T: FromStr + Serialize, - ::Err: Into, -{ -} - -#[macro_export] -macro_rules! no_cli_type { - ($type:ty $(, $more:ty)*) => { - impl $crate::cli::ParseCli for $type { - fn parse_cli(name: &str, _value: Option<&str>) -> Result { - bail!( - "invalid type for command line interface found for parameter '{}'", - name - ); - } - } - - $crate::derive_parse_cli_from_str!{$($more),*} - }; - () => {}; -} - -no_cli_type! {Vec} - -#[macro_export] -macro_rules! derive_parse_cli_from_str { - ($type:ty $(, $more:ty)*) => { - impl $crate::cli::ParseCli for $type { - fn parse_cli( - name: &str, - value: Option<&str>, - ) -> Result<::serde_json::Value, ::failure::Error> { - $crate::cli::parse_cli_from_str::<$type>(name, value) - } - } - - $crate::derive_parse_cli_from_str!{$($more),*} - }; - () => {}; -} - -derive_parse_cli_from_str! {isize, usize, i64, u64, i32, u32, i16, u16, i8, u8, f64, f32} - -impl ParseCli for () { - fn parse_cli(_name: &str, _value: Option<&str>) -> Result { - panic!("() type must not be used in command line interface!"); - } -} - -impl ParseCli for bool { - fn parse_cli(name: &str, value: Option<&str>) -> Result { - // for booleans, using `--arg` without an option counts as `true`: - match value { - None => Ok(Value::Bool(true)), - Some("true") | Some("yes") | Some("on") | Some("1") => Ok(Value::Bool(true)), - Some("false") | Some("no") | Some("off") | Some("0") => Ok(Value::Bool(false)), - Some(other) => bail!("parameter '{}' must be a boolean, found: '{}'", name, other), - } - } -} - -impl ParseCli for Value { - fn parse_cli(name: &str, _value: Option<&str>) -> Result { - // FIXME: we could of course allow generic json parameters...? - bail!( - "found generic json parameter ('{}') in command line...", - name - ); - } -} - -impl ParseCli for &str { - fn parse_cli(name: &str, value: Option<&str>) -> Result { - Ok(Value::String( - value - .ok_or_else(|| format_err!("missing value for parameter '{}'", name))? - .to_string(), - )) - } -} - -impl ParseCli for String { - fn parse_cli(name: &str, value: Option<&str>) -> Result { - Ok(Value::String( - value - .ok_or_else(|| format_err!("missing value for parameter '{}'", name))? - .to_string(), - )) - } -} - -impl ParseCli for HashSet { - fn parse_cli(name: &str, value: Option<&str>) -> Result { - Ok(serde_json::Value::Array( - value - .ok_or_else(|| format_err!("missing value for parameter '{}'", name))? - .split(';') - .fold(Vec::new(), |mut list, entry| { - list.push(serde_json::Value::String(entry.trim().to_string())); - list - }), - )) - } -} diff --git a/proxmox-api/src/lib.rs b/proxmox-api/src/lib.rs index 0d0aa839..d2796a36 100644 --- a/proxmox-api/src/lib.rs +++ b/proxmox-api/src/lib.rs @@ -1,10 +1,4 @@ //! Proxmox API module. This provides utilities for HTTP and command line APIs. -//! -//! The main component here is the [`Router`] which is filled with entries pointing to -//! [`ApiMethodInfos`](crate::ApiMethodInfo). -//! -//! Note that you'll rarely need the [`Router`] type itself, as you'll most likely be creating them -//! with the `router` macro provided by the `proxmox-api-macro` crate. use std::future::Future; use std::pin::Pin; @@ -12,20 +6,6 @@ use std::pin::Pin; use failure::Error; use http::Response; -mod api_output; -pub use api_output::*; - -mod api_type; -pub use api_type::*; - -mod router; -pub use router::*; - -pub mod cli; - -pub mod meta; -pub mod verify; - /// Return type of an API method. pub type ApiOutput = Result, Error>; diff --git a/proxmox-api/src/meta.rs b/proxmox-api/src/meta.rs deleted file mode 100644 index 331345ed..00000000 --- a/proxmox-api/src/meta.rs +++ /dev/null @@ -1,56 +0,0 @@ -//! Type related meta information, mostly used by the macro code. - -use crate::ApiType; - -/// Helper trait for entries with a `default` value in their api type definition. -pub trait OrDefault { - type Output; - - fn or_default(&self, def: &'static Self::Output) -> &Self::Output; - fn set(&mut self, value: Self::Output); -} - -impl OrDefault for Option -where - T: ApiType, -{ - type Output = T; - - #[inline] - fn or_default(&self, def: &'static Self::Output) -> &Self::Output { - self.as_ref().unwrap_or(def) - } - - #[inline] - fn set(&mut self, value: Self::Output) { - *self = Some(value); - } -} - -pub trait AsOptionStr { - fn as_option_str(&self) -> Option<&str>; -} - -impl AsOptionStr for String { - fn as_option_str(&self) -> Option<&str> { - Some(self.as_str()) - } -} - -impl AsOptionStr for str { - fn as_option_str(&self) -> Option<&str> { - Some(self) - } -} - -impl AsOptionStr for Option { - fn as_option_str(&self) -> Option<&str> { - self.as_ref().map(String::as_str) - } -} - -impl AsOptionStr for Option<&str> { - fn as_option_str(&self) -> Option<&str> { - *self - } -} diff --git a/proxmox-api/src/router.rs b/proxmox-api/src/router.rs deleted file mode 100644 index b011a96e..00000000 --- a/proxmox-api/src/router.rs +++ /dev/null @@ -1,247 +0,0 @@ -//! This module provides a router used for http servers. - -use std::collections::HashMap; - -use serde_json::{json, map::Map, Value}; - -use super::ApiHandler; - -/// This enum specifies what to do when a subdirectory is requested from the current router. -/// -/// For plain subdirectories a `Directories` entry is used. -/// -/// When subdirectories are supposed to be passed as a `String` parameter to methods beneath the -/// current directory, a `Parameter` entry is used. Note that the parameter name is fixed at this -/// point, so all method calls beneath will receive a parameter ot that particular name. -pub enum SubRoute { - /// Call this router for any further subdirectory paths, and provide the relative path via the - /// given parameter. - Wildcard(&'static str), - - /// This is used for plain subdirectories. - Directories(HashMap<&'static str, Router>), - - /// Match subdirectories as the given parameter name to the underlying router. - Parameter(&'static str, Box>), -} - -/// A router is a nested structure. On the one hand it contains HTTP method entries (`GET`, `PUT`, -/// ...), and on the other hand it contains sub directories. In some cases we want to match those -/// sub directories as parameters, so the nesting uses a `SubRoute` `enum` representing which of -/// the two is the case. -#[derive(Default)] -pub struct Router { - /// The `GET` http method. - pub get: Option<&'static (dyn ApiHandler + Send + Sync)>, - - /// The `PUT` http method. - pub put: Option<&'static (dyn ApiHandler + Send + Sync)>, - - /// The `POST` http method. - pub post: Option<&'static (dyn ApiHandler + Send + Sync)>, - - /// The `DELETE` http method. - pub delete: Option<&'static (dyn ApiHandler + Send + Sync)>, - - /// Specifies the behavior of sub directories. See [`SubRoute`]. - pub subroute: Option>, -} - -impl Router -where - Self: Default, -{ - /// Create a new empty router. - pub fn new() -> Self { - Self::default() - } - - /// Lookup a path in the router. Note that this returns a tuple: the router we ended up on - /// (providing methods and subdirectories available for the given path), and optionally a json - /// value containing all the matched parameters ([`SubRoute::Parameter`] subdirectories). - pub fn lookup>(&self, path: T) -> Option<(&Self, Option>)> { - self.lookup_do(path.as_ref()) - } - - // The actual implementation taking the parameter as &str - fn lookup_do(&self, path: &str) -> Option<(&Self, Option>)> { - let mut matched_params = None; - let mut matched_wildcard: Option = None; - - let mut this = self; - for component in path.split('/') { - if let Some(ref mut relative_path) = matched_wildcard { - relative_path.push('/'); - relative_path.push_str(component); - continue; - } - if component.is_empty() { - // `foo//bar` or the first `/` in `/foo` - continue; - } - this = match &this.subroute { - Some(SubRoute::Wildcard(_)) => { - matched_wildcard = Some(component.to_string()); - continue; - } - Some(SubRoute::Directories(subdirs)) => subdirs.get(component)?, - Some(SubRoute::Parameter(param_name, router)) => { - let previous = matched_params - .get_or_insert_with(serde_json::Map::new) - .insert(param_name.to_string(), Value::String(component.to_string())); - if previous.is_some() { - panic!("API contains the same parameter twice in route"); - } - &*router - } - None => return None, - }; - } - - if let Some(SubRoute::Wildcard(param_name)) = &this.subroute { - matched_params - .get_or_insert_with(serde_json::Map::new) - .insert( - param_name.to_string(), - Value::String(matched_wildcard.unwrap_or_else(String::new)), - ); - } - - Some((this, matched_params)) - } - - pub fn api_dump(&self) -> Value { - let mut this = serde_json::Map::::new(); - - if let Some(get) = self.get { - this.insert("GET".to_string(), get.method_info().api_dump()); - } - - if let Some(put) = self.put { - this.insert("PUT".to_string(), put.method_info().api_dump()); - } - - if let Some(post) = self.post { - this.insert("POST".to_string(), post.method_info().api_dump()); - } - - if let Some(delete) = self.delete { - this.insert("DELETE".to_string(), delete.method_info().api_dump()); - } - - match &self.subroute { - None => (), - Some(SubRoute::Wildcard(name)) => { - this.insert("wildcard".to_string(), Value::String(name.to_string())); - } - Some(SubRoute::Directories(subdirs)) => { - for (dir, router) in subdirs.iter() { - this.insert(dir.to_string(), router.api_dump()); - } - } - Some(SubRoute::Parameter(name, other)) => { - this.insert( - "sub-router".to_string(), - json!({ - "parameter": name, - "router": other.api_dump(), - }), - ); - } - } - - Value::Object(this) - } -} - -// -// Router as a builder methods: -// - -impl Router -where - Self: Default, -{ - /// Builder method to provide a `GET` method info. - pub fn get(mut self, method: &'static I) -> Self - where - I: ApiHandler + Send + Sync, - { - self.get = Some(method); - self - } - - /// Builder method to provide a `PUT` method info. - pub fn put(mut self, method: &'static I) -> Self - where - I: ApiHandler + Send + Sync, - { - self.put = Some(method); - self - } - - /// Builder method to provide a `POST` method info. - pub fn post(mut self, method: &'static I) -> Self - where - I: ApiHandler + Send + Sync, - { - self.post = Some(method); - self - } - - /// Builder method to provide a `DELETE` method info. - pub fn delete(mut self, method: &'static I) -> Self - where - I: ApiHandler + Send + Sync, - { - self.delete = Some(method); - self - } - - /// Builder method to make this router match the next subdirectory into a parameter. - /// - /// This is supposed to be used statically (via `lazy_static!), therefore we panic if we - /// already have a subdir entry! - pub fn parameter_subdir(mut self, parameter_name: &'static str, router: Router) -> Self { - if self.subroute.is_some() { - panic!("match_parameter can only be used once and without sub directories"); - } - self.subroute = Some(SubRoute::Parameter(parameter_name, Box::new(router))); - self - } - - /// Builder method to add a regular directory entry to this router. - /// - /// This is supposed to be used statically (via `lazy_static!), therefore we panic if we - /// already have a subdir entry! - pub fn subdir(mut self, dir_name: &'static str, router: Router) -> Self { - let previous = match self.subroute { - Some(SubRoute::Directories(ref mut map)) => map.insert(dir_name, router), - None => { - let mut map = HashMap::new(); - map.insert(dir_name, router); - self.subroute = Some(SubRoute::Directories(map)); - None - } - _ => panic!("subdir and match_parameter are mutually exclusive"), - }; - if previous.is_some() { - panic!("duplicate subdirectory: {}", dir_name); - } - self - } - - /// Builder method to match the rest of the path into a parameter. - /// - /// This is supposed to be used statically (via `lazy_static!), therefore we panic if we - /// already have a subdir entry! - pub fn wildcard(mut self, path_parameter_name: &'static str) -> Self { - if self.subroute.is_some() { - panic!("'wildcard' and other sub routers are mutually exclusive"); - } - - self.subroute = Some(SubRoute::Wildcard(path_parameter_name)); - - self - } -} diff --git a/proxmox-api/src/verify.rs b/proxmox-api/src/verify.rs deleted file mode 100644 index a37beaec..00000000 --- a/proxmox-api/src/verify.rs +++ /dev/null @@ -1,109 +0,0 @@ -//! Helper module for verifiers implemented via the api macro crate. -//! -//! We need this to seamlessly support verifying optional types. Consider this: -//! -//! ```ignore -//! type Annoying = Option; -//! -//! #[api({ -//! fields: { -//! foo: { -//! description: "Test", -//! default: 2, -//! minimum: 1, -//! maximum: 5, -//! }, -//! bar: { -//! description: "Test", -//! default: 2, -//! minimum: 1, -//! maximum: 5, -//! }, -//! }, -//! })] -//! struct Foo { -//! foo: Option, -//! bar: Annoying, -//! } -//! ``` -//! -//! The macro does not know that `foo` and `bar` have in fact the same type, and wouldn't know that -//! in order to check `bar` it needs to first check the `Option`. -//! -//! With OIBITs or specialization, we could implement a trait that always gives us "the value we -//! actually want to check", but those aren't stable and guarded by a feature gate. -//! -//! So instead, we implement checks another way. - -pub mod mark { - pub struct Default; - pub struct Special; -} - -pub trait TestMinMax { - fn test_minimum(&self, minimum: &Other) -> bool; - fn test_maximum(&self, maximum: &Other) -> bool; -} - -impl TestMinMax for Other -where - Other: Ord, -{ - #[inline] - fn test_minimum(&self, minimum: &Other) -> bool { - *self >= *minimum - } - - #[inline] - fn test_maximum(&self, maximum: &Other) -> bool { - *self <= *maximum - } -} - -impl TestMinMax for Option -where - Other: Ord, -{ - #[inline] - fn test_minimum(&self, minimum: &Other) -> bool { - self.as_ref().map(|x| *x >= *minimum).unwrap_or(true) - } - - #[inline] - fn test_maximum(&self, maximum: &Other) -> bool { - self.as_ref().map(|x| *x <= *maximum).unwrap_or(true) - } -} - -pub trait TestMinMaxLen { - fn test_minimum_length(&self, minimum: usize) -> bool; - fn test_maximum_length(&self, maximum: usize) -> bool; -} - -impl> TestMinMaxLen for T { - #[inline] - fn test_minimum_length(&self, minimum: usize) -> bool { - self.as_ref().len() >= minimum - } - - #[inline] - fn test_maximum_length(&self, maximum: usize) -> bool { - self.as_ref().len() <= maximum - } -} - -impl> TestMinMaxLen for Option { - #[inline] - fn test_minimum_length(&self, minimum: usize) -> bool { - self.as_ref() - .map(|x| x.as_ref().len() >= minimum) - .unwrap_or(true) - } - - #[inline] - fn test_maximum_length(&self, maximum: usize) -> bool { - self.as_ref() - .map(|x| x.as_ref().len() <= maximum) - .unwrap_or(true) - } -} diff --git a/proxmox-api/tests/cli.rs b/proxmox-api/tests/cli.rs deleted file mode 100644 index 8ce06505..00000000 --- a/proxmox-api/tests/cli.rs +++ /dev/null @@ -1,183 +0,0 @@ -use bytes::Bytes; - -use proxmox_api::cli; - -#[test] -fn simple() { - let simple_method: &proxmox_api::ApiMethod = &methods::SIMPLE_METHOD; - - let cli = cli::App::new("simple") - .subcommand("new", cli::Command::method(simple_method, &[])) - .subcommand("newfoo", cli::Command::method(simple_method, &["foo"])) - .subcommand("newbar", cli::Command::method(simple_method, &["bar"])) - .subcommand( - "newboth", - cli::Command::method(simple_method, &["foo", "bar"]), - ); - - check_cli(&cli, &["new", "--foo=FOO", "--bar=BAR"], Ok("FOO:BAR")); - check_cli(&cli, &["new", "--foo", "FOO", "--bar=BAR"], Ok("FOO:BAR")); - check_cli( - &cli, - &["new", "--foo", "FOO", "--bar", "BAR"], - Ok("FOO:BAR"), - ); - check_cli(&cli, &["new", "--foo=FOO"], Err("missing parameter: 'bar'")); - check_cli(&cli, &["new", "--bar=BAR"], Err("missing parameter: 'foo'")); - check_cli(&cli, &["new"], Err("missing parameter: 'foo'")); - - check_cli(&cli, &["newfoo", "POSFOO"], Err("missing parameter: 'bar'")); - check_cli(&cli, &["newfoo", "POSFOO", "--bar=BAR"], Ok("POSFOO:BAR")); - check_cli(&cli, &["newfoo", "--bar=BAR", "POSFOO"], Ok("POSFOO:BAR")); - check_cli( - &cli, - &["newfoo", "--bar=BAR"], - Err("missing positional parameters: foo"), - ); - - check_cli(&cli, &["newbar", "POSBAR"], Err("missing parameter: 'foo'")); - check_cli(&cli, &["newbar", "POSBAR", "--foo=ABC"], Ok("ABC:POSBAR")); - check_cli(&cli, &["newbar", "--foo=ABC", "POSBAR"], Ok("ABC:POSBAR")); - check_cli( - &cli, - &["newbar", "--foo=ABC"], - Err("missing positional parameters: bar"), - ); - - check_cli( - &cli, - &["newfoo", "FOO1", "--foo=FOO2", "--bar=BAR", "--baz=OMG"], - Ok("FOO2:BAR:OMG"), - ); - - check_cli(&cli, &["newboth", "a", "b", "--maybe"], Ok("a:b:[true]")); - check_cli( - &cli, - &["newboth", "a", "b", "--maybe=false"], - Ok("a:b:[false]"), - ); - check_cli( - &cli, - &["newboth", "a", "b", "--maybe", "false"], - Ok("a:b:[false]"), - ); -} - -fn check_cli(cli: &cli::App, args: &[&str], expect: Result<&str, &str>) { - match (cli.run(args), expect) { - (Ok(result), Ok(expect)) => { - let body = std::str::from_utf8(result.body().as_ref()) - .expect("expected a valid utf8 repsonse body"); - assert_eq!(body, expect, "expected successful CLI invocation"); - } - (Err(result), Err(expected)) => { - let result = result.to_string(); - assert_eq!(result, expected, "expected specific error message"); - } - (Ok(result), Err(err)) => match std::str::from_utf8(result.body().as_ref()) { - Ok(value) => panic!( - "expected error '{}', got success with value '{}'", - err, value - ), - Err(_) => panic!("expected error '{}', got success with non-utf8 string", err), - }, - (Err(err), Ok(expected)) => { - let err = err.to_string(); - panic!( - "expected success with value '{}', got error '{}'", - expected, err - ); - } - } -} - -mod methods { - use bytes::Bytes; - use failure::{format_err, Error}; - use http::Response; - use lazy_static::lazy_static; - use serde_json::Value; - - use proxmox_api::{get_type_info, ApiFuture, ApiMethod, ApiOutput, ApiType, Parameter}; - - fn required_str<'a>(value: &'a Value, name: &'static str) -> Result<&'a str, Error> { - value[name] - .as_str() - .ok_or_else(|| format_err!("missing parameter: '{}'", name)) - } - - pub async fn simple_method(value: Value) -> ApiOutput { - let foo = required_str(&value, "foo")?; - let bar = required_str(&value, "bar")?; - - let baz = value - .get("baz") - .map(|value| { - value - .as_str() - .ok_or_else(|| format_err!("'baz' must be a string")) - }) - .transpose()?; - - let maybe = value - .get("maybe") - .map(|value| { - value - .as_bool() - .ok_or_else(|| format_err!("'maybe' must be a boolean, found: {:?}", value)) - }) - .transpose()?; - - let output = match baz { - Some(baz) => format!("{}:{}:{}", foo, bar, baz), - None => format!("{}:{}", foo, bar), - }; - - let output = match maybe { - Some(maybe) => format!("{}:[{}]", output, maybe), - None => output, - }; - - Ok(Response::builder() - .status(200) - .header("content-type", "application/json") - .body(output.into())?) - } - - lazy_static! { - static ref SIMPLE_PARAMS: Vec = { - vec![ - Parameter { - name: "foo", - description: "a test parameter", - type_info: String::type_info, - }, - Parameter { - name: "bar", - description: "another test parameter", - type_info: String::type_info, - }, - Parameter { - name: "baz", - description: "another test parameter", - type_info: Option::::type_info, - }, - Parameter { - name: "maybe", - description: "optional boolean test parameter", - type_info: Option::::type_info, - }, - ] - }; - pub static ref SIMPLE_METHOD: ApiMethod = { - ApiMethod { - description: "get some parameters back", - parameters: &SIMPLE_PARAMS, - return_type: get_type_info::(), - protected: false, - reload_timezone: false, - handler: |value: Value| -> ApiFuture { Box::pin(simple_method(value)) }, - } - }; - } -} diff --git a/proxmox-api/tests/router.rs b/proxmox-api/tests/router.rs deleted file mode 100644 index eff544fc..00000000 --- a/proxmox-api/tests/router.rs +++ /dev/null @@ -1,179 +0,0 @@ -use bytes::Bytes; -use serde_json::Value; - -use proxmox_api::Router; - -#[test] -fn basic() { - let info: &proxmox_api::ApiMethod = &methods::GET_PEOPLE; - let get_subpath: &proxmox_api::ApiMethod = &methods::GET_SUBPATH; - let router: Router = Router::new() - .subdir( - "people", - Router::new().parameter_subdir("person", Router::new().get(info)), - ) - .subdir( - "wildcard", - Router::new().wildcard("subpath").get(get_subpath), - ); - - check_with_matched_params(&router, "people/foo", "person", "foo", "foo"); - check_with_matched_params(&router, "people//foo", "person", "foo", "foo"); - check_with_matched_params(&router, "wildcard", "subpath", "", ""); - check_with_matched_params(&router, "wildcard/", "subpath", "", ""); - check_with_matched_params(&router, "wildcard//", "subpath", "", ""); - check_with_matched_params(&router, "wildcard/dir1", "subpath", "dir1", "dir1"); - check_with_matched_params( - &router, - "wildcard/dir1/dir2", - "subpath", - "dir1/dir2", - "dir1/dir2", - ); - check_with_matched_params(&router, "wildcard/dir1//2", "subpath", "dir1//2", "dir1//2"); -} - -fn check_with_matched_params( - router: &Router, - path: &str, - param_name: &str, - param_value: &str, - expected_body: &str, -) { - let (target, params) = router - .lookup(path) - .expect(&format!("must be able to lookup '{}'", path)); - - let params = params.expect(&format!( - "expected parameters to be matched into '{}'", - param_name, - )); - - let arg = params[param_name].as_str().expect(&format!( - "expected lookup() to fill the '{}' parameter", - param_name - )); - - assert_eq!( - arg, param_value, - "lookup of '{}' should set '{}' to '{}'", - path, param_name, param_value, - ); - - let apifut = target - .get - .as_ref() - .expect(&format!("expected GET method on {}", path)) - .call(Value::Object(params)); - - let response = futures::executor::block_on(apifut) - .expect("expected the simple test api function to be ready immediately"); - - assert_eq!(response.status(), 200, "response status must be 200"); - - let body = - std::str::from_utf8(response.body().as_ref()).expect("expected a valid utf8 repsonse body"); - - assert_eq!( - body, expected_body, - "response of {} should be '{}', got '{}'", - path, expected_body, body, - ); -} - -#[cfg(test)] -mod methods { - use bytes::Bytes; - use failure::{bail, Error}; - use http::Response; - use lazy_static::lazy_static; - use serde_derive::{Deserialize, Serialize}; - use serde_json::Value; - - use proxmox_api::{ - get_type_info, ApiFuture, ApiMethod, ApiOutput, ApiType, Parameter, TypeInfo, - }; - - pub async fn get_people(value: Value) -> ApiOutput { - Ok(Response::builder() - .status(200) - .header("content-type", "application/json") - .body(value["person"].as_str().unwrap().into())?) - } - - pub async fn get_subpath(value: Value) -> ApiOutput { - Ok(Response::builder() - .status(200) - .header("content-type", "application/json") - .body(value["subpath"].as_str().unwrap().into())?) - } - - lazy_static! { - static ref GET_PEOPLE_PARAMS: Vec = { - vec![Parameter { - name: "person", - description: "the person to get", - type_info: String::type_info, - }] - }; - pub static ref GET_PEOPLE: ApiMethod = { - ApiMethod { - description: "get some people", - parameters: &GET_PEOPLE_PARAMS, - return_type: get_type_info::(), - protected: false, - reload_timezone: false, - handler: |value: Value| -> ApiFuture { Box::pin(get_people(value)) }, - } - }; - static ref GET_SUBPATH_PARAMS: Vec = { - vec![Parameter { - name: "subpath", - description: "the matched relative subdir path", - type_info: String::type_info, - }] - }; - pub static ref GET_SUBPATH: ApiMethod = { - ApiMethod { - description: "get the 'subpath' parameter returned back", - parameters: &GET_SUBPATH_PARAMS, - return_type: get_type_info::(), - protected: false, - reload_timezone: false, - handler: |value: Value| -> ApiFuture { Box::pin(get_subpath(value)) }, - } - }; - } - - #[derive(Deserialize, Serialize)] - pub struct CubicMeters(f64); - - // We don't bother with the CLI interface in this test: - proxmox_api::no_cli_type! {CubicMeters} - proxmox_api::unconstrained_api_type! {CubicMeters} - - #[derive(Deserialize, Serialize)] - pub struct Thing { - shape: String, - size: CubicMeters, - } - - impl ApiType for Thing { - fn type_info() -> &'static TypeInfo { - const INFO: TypeInfo = TypeInfo { - name: "Thing", - description: "A thing", - complete_fn: None, - parse_cli: None, - }; - &INFO - } - - fn verify(&self) -> Result<(), Error> { - if self.shape == "flat" { - bail!("flat shapes not allowed..."); - } - Ok(()) - } - } -}