delete the old api macro stuff

to be replaced by a new set of macros for the current api
schema in proxmox-backup

Signed-off-by: Wolfgang Bumiller <w.bumiller@proxmox.com>
This commit is contained in:
Wolfgang Bumiller 2019-11-21 09:18:34 +01:00
parent 7f4d223a4a
commit 068d56ed3d
38 changed files with 1 additions and 5752 deletions

View File

@ -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",
]

View File

@ -1,23 +0,0 @@
[package]
name = "api-test"
edition = "2018"
version = "0.1.0"
authors = [
"Dietmar Maurer <dietmar@proxmox.com>",
"Wolfgang Bumiller <w.bumiller@proxmox.com>",
]
[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" }

View File

@ -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

View File

@ -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<String> = 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<bool>,
}
//
// API methods
//
router! {
pub static ROUTER: Router<Body> = {
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<Response<Body>, 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<Response<Body>, 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<String>,
) -> Result<String, Error> {
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<String, Error> {
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<Value, Error> {
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<Body>) -> Result<http::Response<Body>, 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<Body>) -> Result<http::Response<Body>, 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));
}

View File

@ -1,4 +0,0 @@
//! PVE base library
pub mod lxc;
pub mod schema;

View File

@ -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/<ostype>.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::<crate::schema::memory::Mb>
},
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<schema::ConfigLock>,
pub onboot: Option<bool>,
pub startup: Option<crate::schema::StartupOrder>,
pub template: Option<bool>,
pub arch: Option<Architecture>,
pub ostype: Option<schema::OsType>,
pub console: Option<bool>,
pub tty: Option<usize>,
pub cores: Option<usize>,
pub cpulimit: Option<usize>,
pub cpuunits: Option<usize>,
pub memory: Option<Memory>,
pub swap: Option<Memory>,
pub hostname: Option<String>,
pub description: Option<String>,
pub searchdomain: Option<Vec<String>>,
pub nameserver: Option<Vec<String>>,
pub rootfs: Option<Rootfs>,
// pub parent: Option<String>,
// pub snaptime: Option<usize>,
pub cmode: Option<schema::ConsoleMode>,
pub protection: Option<bool>,
pub unprivileged: Option<bool>,
// pub features: Option<schema::Features>,
pub hookscript: Option<VolumeId>,
}
#[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<Memory>,
pub acl: Option<bool>,
pub ro: Option<bool>,
pub mountoptions: Option<HashSet<String>>,
pub quota: Option<bool>,
pub replicate: Option<bool>,
pub shared: Option<bool>,
}

View File

@ -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<T: crate::schema::tools::StringContainer>(value: &T) -> bool {
value.all(|s| VALID_MOUNT_OPTIONS.contains(&s))
}
}

View File

@ -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<U: Unit>(PhantomData<U>);
impl<'de, U: Unit> serde::de::Visitor<'de> for MemoryVisitor<U> {
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<E: serde::de::Error>(self, v: u8) -> Result<Self::Value, E> {
Ok(Memory::from_bytes(u64::from(v) * U::FACTOR))
}
fn visit_u16<E: serde::de::Error>(self, v: u16) -> Result<Self::Value, E> {
Ok(Memory::from_bytes(u64::from(v) * U::FACTOR))
}
fn visit_u32<E: serde::de::Error>(self, v: u32) -> Result<Self::Value, E> {
Ok(Memory::from_bytes(u64::from(v) * U::FACTOR))
}
fn visit_u64<E: serde::de::Error>(self, v: u64) -> Result<Self::Value, E> {
Ok(Memory::from_bytes(v * U::FACTOR))
}
fn visit_str<E: serde::de::Error>(self, v: &str) -> Result<Self::Value, E> {
match v.parse::<u64>() {
Ok(v) => Ok(Memory::from_bytes(v * U::FACTOR)),
Err(_) => v.parse().map_err(serde::de::Error::custom),
}
}
}
pub struct Parser<U: Unit>(PhantomData<U>);
impl<U: Unit> Parser<U> {
pub fn serialize<S>(value: Memory, ser: S) -> Result<S::Ok, S::Error>
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<Memory, D::Error>
where
D: serde::de::Deserializer<'de>,
{
de.deserialize_any(MemoryVisitor::<U>(PhantomData))
}
}
pub mod optional {
use std::marker::PhantomData;
use super::Unit;
use crate::schema::types::Memory;
pub struct Parser<U: Unit>(PhantomData<U>);
impl<U: Unit> Parser<U> {
pub fn serialize<S>(value: &Option<Memory>, ser: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
super::Parser::<U>::serialize::<S>(value.unwrap(), ser)
}
pub fn deserialize<'de, D>(de: D) -> Result<Option<Memory>, D::Error>
where
D: serde::de::Deserializer<'de>,
{
super::Parser::<U>::deserialize::<'de, D>(de).map(Some)
}
}
}

View File

@ -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<usize>,
pub up: Option<usize>,
pub down: Option<usize>,
}
#[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<T: crate::schema::tools::StringContainer>(value: &T) -> bool {
value.all(|s| REGEX.is_match(s))
}
}
pub mod ip_address {
pub const NAME: &str = "IP Address";
pub fn verify<T: crate::schema::tools::StringContainer>(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<T: crate::schema::tools::StringContainer>(value: &T) -> bool {
value.all(|s| {
s != ".." && !s.starts_with("../") && !s.ends_with("/..") && !s.contains("/../")
})
}
}

View File

@ -1,97 +0,0 @@
//! Comma separated string list.
//!
//! Used as a proxy type for when a struct should contain a `Vec<String>` which should be
//! serialized as a single comma separated list.
use failure::{bail, Error};
pub trait ForEachStr {
fn for_each_str<F>(&self, func: F) -> Result<(), Error>
where
F: FnMut(&str) -> Result<(), Error>;
}
impl ForEachStr for Vec<String> {
fn for_each_str<F>(&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<S, T: ForEachStr>(value: &T, ser: S) -> Result<S::Ok, S::Error>
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<String>;
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<E: serde::de::Error>(self, v: &str) -> Result<Self::Value, E> {
Ok(v.split(',').map(|i| i.trim().to_string()).collect())
}
fn visit_seq<A: serde::de::SeqAccess<'de>>(self, mut seq: A) -> Result<Self::Value, A::Error> {
let mut out = seq.size_hint().map_or_else(Vec::new, Vec::with_capacity);
while let Some(el) = seq.next_element::<String>()? {
out.push(el);
}
Ok(out)
}
}
pub fn deserialize<'de, D>(de: D) -> Result<Vec<String>, D::Error>
where
D: serde::de::Deserializer<'de>,
{
de.deserialize_any(StringListVisitor)
}
pub mod optional {
pub fn serialize<S, T: super::ForEachStr>(value: &Option<T>, ser: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
match value {
Some(value) => super::serialize(value, ser),
None => ser.serialize_none(),
}
}
pub fn deserialize<'de, D>(de: D) -> Result<Option<Vec<String>>, D::Error>
where
D: serde::de::Deserializer<'de>,
{
super::deserialize(de).map(Some)
}
}

View File

@ -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<String>` 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<F>(&self, func: F) -> Result<(), Error>
where
F: FnMut(&str) -> Result<(), Error>;
}
impl<S: std::hash::BuildHasher> ForEachStr for HashSet<String, S> {
fn for_each_str<F>(&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<S, T: ForEachStr>(value: &T, ser: S) -> Result<S::Ok, S::Error>
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<String>;
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<E: serde::de::Error>(self, v: &str) -> Result<Self::Value, E> {
Ok(v.split(';').map(|i| i.trim().to_string()).collect())
}
fn visit_seq<A: serde::de::SeqAccess<'de>>(self, mut seq: A) -> Result<Self::Value, A::Error> {
let mut out = seq
.size_hint()
.map_or_else(HashSet::new, HashSet::with_capacity);
while let Some(el) = seq.next_element::<String>()? {
out.insert(el);
}
Ok(out)
}
}
pub fn deserialize<'de, D>(de: D) -> Result<HashSet<String>, D::Error>
where
D: serde::de::Deserializer<'de>,
{
de.deserialize_any(StringSetVisitor)
}
pub mod optional {
use std::collections::HashSet;
pub fn serialize<S, T: super::ForEachStr>(value: &Option<T>, ser: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
match value {
Some(value) => super::serialize(value, ser),
None => ser.serialize_none(),
}
}
pub fn deserialize<'de, D>(de: D) -> Result<Option<HashSet<String>>, D::Error>
where
D: serde::de::Deserializer<'de>,
{
super::deserialize(de).map(Some)
}
}

View File

@ -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<F: Fn(&str) -> bool>(&self, pred: F) -> bool;
}
impl StringContainer for String {
fn all<F: Fn(&str) -> bool>(&self, pred: F) -> bool {
pred(&self)
}
}
impl StringContainer for Option<String> {
fn all<F: Fn(&str) -> bool>(&self, pred: F) -> bool {
match self {
Some(ref v) => pred(v),
None => true,
}
}
}
impl StringContainer for Vec<String> {
fn all<F: Fn(&str) -> bool>(&self, pred: F) -> bool {
self.iter().all(|s| pred(&s))
}
}
impl StringContainer for Option<Vec<String>> {
fn all<F: Fn(&str) -> bool>(&self, pred: F) -> bool {
self.as_ref()
.map(|c| StringContainer::all(c, pred))
.unwrap_or(true)
}
}
impl<S: std::hash::BuildHasher> StringContainer for HashSet<String, S> {
fn all<F: Fn(&str) -> bool>(&self, pred: F) -> bool {
self.iter().all(|s| pred(s))
}
}
impl<S: std::hash::BuildHasher> StringContainer for Option<HashSet<String, S>> {
fn all<F: Fn(&str) -> bool>(&self, pred: F) -> bool {
self.as_ref()
.map(|c| StringContainer::all(c, pred))
.unwrap_or(true)
}
}

View File

@ -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<Self, Self::Err> {
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<Memory> for Memory {
type Output = Memory;
fn add(self, rhs: Memory) -> Memory {
Self(self.0 + rhs.0)
}
}
impl std::ops::AddAssign<Memory> for Memory {
fn add_assign(&mut self, rhs: Memory) {
self.0 += rhs.0;
}
}
impl std::ops::Sub<Memory> for Memory {
type Output = Memory;
fn sub(self, rhs: Memory) -> Memory {
Self(self.0 - rhs.0)
}
}
impl std::ops::SubAssign<Memory> for Memory {
fn sub_assign(&mut self, rhs: Memory) {
self.0 -= rhs.0;
}
}
impl std::ops::Mul<u64> for Memory {
type Output = Memory;
fn mul(self, rhs: u64) -> Memory {
Self(self.0 * rhs)
}
}
impl std::ops::MulAssign<u64> 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::<Memory>().unwrap(), Memory::from_mebibytes(1));
assert_eq!("1MiB".parse::<Memory>().unwrap(), Memory::from_mebibytes(1));
}

View File

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

View File

@ -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<Self, Self::Err> {
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)
}
}

View File

@ -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<T>(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::<lxc::Config>(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(())
}

View File

@ -1,6 +0,0 @@
<!DOCTYPE html>
<html>
<body>
Hello
</body>
</html>

View File

@ -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<Expression> for CliMode {
type Error = Error;
fn try_from(expr: Expression) -> Result<Self, Error> {
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<Self, Error> {
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<syn::Expr>,
#[builder(default)]
pub description: Option<syn::LitStr>,
#[builder(default)]
pub maximum: Option<syn::Expr>,
#[builder(default)]
pub minimum: Option<syn::Expr>,
#[builder(default)]
pub maximum_length: Option<syn::Expr>,
#[builder(default)]
pub minimum_length: Option<syn::Expr>,
#[builder(default)]
pub validate: Option<syn::Expr>,
/// 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<syn::Path>,
/// 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<syn::Expr>,
#[builder(default)]
pub serialize_with: Option<syn::Path>,
#[builder(default)]
pub deserialize_with: Option<syn::Path>,
}
impl ParameterDefinition {
pub fn builder() -> ParameterDefinitionBuilder {
Default::default()
}
pub fn from_object(obj: Object) -> Result<Self, Error> {
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<Self, Error> {
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"),
}
}
}

View File

@ -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<TokenStream, Error> {
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"),
}
}

View File

@ -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<F>(attrs: &mut Vec<syn::Attribute>, 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<TokenStream, Error> {
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<Self, Self::Err> {
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
}
})
}

View File

@ -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<TokenStream, Error> {
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::<Ident, Token![,]>::new();
let mut passed_args = syn::punctuated::Punctuated::<Ident, Token![,]>::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<Box<Future>>.
// 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<Box<
dyn ::std::future::Future<Output = #return_type> + Send
>>;
fn deref(&self) -> &Self::Target {
const FUNC: fn(#inputs) -> ::std::pin::Pin<Box<dyn ::std::future::Future<
Output = #return_type,
> + 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<Box<Future>>.
// 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(())
}

View File

@ -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<TokenStream, Error> {
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<TokenStream, Error> {
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<String> = 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(())
}
})
}

View File

@ -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<TokenStream, Error> {
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<Foo>, but
// really Option<Foo> 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<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
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<D>(deserializer: D) -> std::result::Result<Self, D::Error>
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<TokenStream, Error> {
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<S>(&self, serializer: S) -> ::std::result::Result<S::Ok, S::Error>
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<TokenStream, Error> {
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<D>(deserializer: D) -> ::std::result::Result<Self, D::Error>
where
D: ::serde::de::Deserializer<'de>,
{
#[repr(transparent)]
struct Field(isize);
impl<'de> ::serde::de::Deserialize<'de> for Field {
fn deserialize<D>(deserializer: D) -> ::std::result::Result<Self, D::Error>
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<E>(self, value: &str) -> ::std::result::Result<Field, E>
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<V>(
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<TokenStream, Error> {
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<TokenStream, Error> {
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
}
}
}
})
}

View File

@ -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<syn::Attribute>,
) -> Result<TokenStream, Error> {
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<S>(&self, serializer: S) -> ::std::result::Result<S::Ok, S::Error>
where
S: ::serde::ser::Serializer,
{
::serde::ser::Serialize::serialize::<S>(&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<D>(deserializer: D) -> ::std::result::Result<Self, D::Error>
where
D: ::serde::de::Deserializer<'de>,
{
Ok(Self(::serde::de::Deserialize::<'de>::deserialize::<D>(deserializer)?))
}
}
}
}
fn newtype_filter_derive_attrs(
type_ident: &Ident,
inner_type: &syn::Type,
attrs: &mut Vec<syn::Attribute>,
) -> Result<TokenStream, Error> {
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<Self, Self::Err> {
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)
}
}
}
}

View File

@ -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<TokenStream, Error> {
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
}
}
})
}

View File

@ -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::<syn::Error>() {
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),
}
}

View File

@ -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<TokenStream, Error> {
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<LitStr, Router>),
Parameter(LitStr, Box<Router>),
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<Ident>,
pub put: Option<Ident>,
pub post: Option<Ident>,
pub delete: Option<Ident>,
pub subroute: Option<SubRoute>,
}
/// 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<Component>;
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<Option<::proxmox::api::Router<#body_type>>>,
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<Router, Error> {
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<Option<Entry>, 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<Path, Error> {
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<Span>) {
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)
}

View File

@ -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 == "<bad>" {
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<String, Error> {
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<Bytes> {
Box::pin(async { proxmox::api::IntoApiOutput::into_api_output(param) })
}
proxmox_api_macro::router! {
static TEST_ROUTER: Router<Bytes> = {
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<Bytes>, 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"
);
}

View File

@ -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<bool, Error> {
Ok(number < reference)
}
proxmox_api_macro::router! {
static TEST_ROUTER: Router<Bytes> = {
GET: less_than,
};
}
fn check_parameter(
router: &Router<Bytes>,
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"
);
}

View File

@ -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<Body, T> {
fn into_api_output(self) -> ApiOutput<Body>;
}
impl<Body, T> IntoApiOutput<Body, ()> for T
where
Body: 'static,
T: ApiType + serde::Serialize,
Body: From<String>,
{
/// 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<Body> {
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<http::Result<Bytes>, Error>`) don't need
/// anything to happen to the value anymore, return the result as is:
impl<Body> IntoApiOutput<Body, ApiOutput<Body>> for ApiOutput<Body> {
fn into_api_output(self) -> ApiOutput<Body> {
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<Body> IntoApiOutput<Body, ApiOutput<Body>> for http::Response<Body> {
fn into_api_output(self) -> ApiOutput<Body> {
Ok(self)
}
}

View File

@ -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<Self::Body>;
fn method_info(&self) -> &dyn ApiMethodInfo;
}
impl<Body: 'static> dyn ApiHandler<Body = Body> {
pub fn call_as<ToBody>(&self, params: Value) -> super::ApiFuture<ToBody>
where
Body: Into<ToBody>,
{
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<String>;
/// 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<Value, Error> {
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<Value, Error>;
/// 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<CompleteFn>,
pub parse_cli: Option<ParseCliFn>,
}
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<Body> {
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<Body>,
}
impl<Body> ApiMethodInfo for ApiMethod<Body> {
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<Body> ApiHandler for ApiMethod<Body> {
type Body = Body;
fn call(&self, params: Value) -> super::ApiFuture<Body> {
(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<T>() {
// fn bar<U>(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<F, E>(this: Option<Self>, missing_error: F) -> Result<Self, E>
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<T: ApiType> ApiType for Option<T> {
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...
<T as ApiType>::type_info()
/* DOES NOT WORK:
struct Data {
info: Cell<Option<TypeInfo>>,
once: Once,
name: Cell<Option<String>>,
description: Cell<Option<String>>,
}
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<F, E>(this: Option<Self>, _missing_error: F) -> Result<Self, E>
where
F: FnOnce() -> E,
{
Ok(this.unwrap_or(None))
}
}
/// Any `Result<T, Error>` of course gets the same info as `T`, since this only means that it can
/// fail...
impl<T: ApiType> ApiType for Result<T, Error> {
fn verify(&self) -> Result<(), Error> {
if let Ok(inner) = self {
inner.verify()?
}
Ok(())
}
fn type_info() -> &'static TypeInfo {
<T as ApiType>::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<String>}
unconstrained_api_type! {HashSet<String>}
// Raw return types are also okay:
impl<Body> ApiType for Response<Body> {
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<T: ApiType>() -> &'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<Bytes>`.
pub trait UnifiedApiMethod<Body>: Send + Sync {
fn parameters(&self) -> &'static [Parameter];
fn call(&self, params: Value) -> super::ApiFuture<Body>;
}
impl<T: Send + Sync + 'static, Body> UnifiedApiMethod<Body> for T
where
T: ApiHandler,
T::Body: 'static + Into<Body>,
{
fn parameters(&self) -> &'static [Parameter] {
ApiMethodInfo::parameters(self)
}
fn call(&self, params: Value) -> super::ApiFuture<Body> {
(self as &dyn ApiHandler<Body = T::Body>).call_as(params)
}
}

View File

@ -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<Bytes>;
/// A CLI root node.
pub struct App {
name: &'static str,
command: Option<Command>,
}
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<Bytes> {
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<T: Send + Sync>(
method: &'static T,
positional_args: &'static [&'static str],
) -> Self
where
T: ApiHandler,
T::Body: 'static + Into<Bytes>,
{
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<String, Value>,
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<Arg<'a>> {
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<T>(name: &str, value: Option<&str>) -> Result<Value, Error>
where
T: FromStr + Serialize,
<T as FromStr>::Err: Into<Error>,
{
let this: T = value
.ok_or_else(|| format_err!("missing parameter value for '{}'", name))?
.parse()
.map_err(|e: <T as FromStr>::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<Value, Error>;
}
/// This is a version of ParseCli with a default implementation falling to FromStr.
pub trait ParseCliFromStr
where
Self: FromStr + Serialize,
<Self as FromStr>::Err: Into<Error>,
{
fn parse_cli(name: &str, value: Option<&str>) -> Result<Value, Error> {
parse_cli_from_str::<Self>(name, value)
}
}
impl<T> ParseCliFromStr for T
where
T: FromStr + Serialize,
<T as FromStr>::Err: Into<Error>,
{
}
#[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<Value, Error> {
bail!(
"invalid type for command line interface found for parameter '{}'",
name
);
}
}
$crate::derive_parse_cli_from_str!{$($more),*}
};
() => {};
}
no_cli_type! {Vec<String>}
#[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<Value, Error> {
panic!("() type must not be used in command line interface!");
}
}
impl ParseCli for bool {
fn parse_cli(name: &str, value: Option<&str>) -> Result<Value, Error> {
// 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<Value, Error> {
// 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<Value, Error> {
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<Value, Error> {
Ok(Value::String(
value
.ok_or_else(|| format_err!("missing value for parameter '{}'", name))?
.to_string(),
))
}
}
impl<S: std::hash::BuildHasher> ParseCli for HashSet<String, S> {
fn parse_cli(name: &str, value: Option<&str>) -> Result<Value, Error> {
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
}),
))
}
}

View File

@ -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<Body> = Result<Response<Body>, Error>;

View File

@ -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<T> OrDefault for Option<T>
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<String> {
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
}
}

View File

@ -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<Body: 'static> {
/// 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<Body>>),
/// Match subdirectories as the given parameter name to the underlying router.
Parameter(&'static str, Box<Router<Body>>),
}
/// 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<Body: 'static> {
/// The `GET` http method.
pub get: Option<&'static (dyn ApiHandler<Body = Body> + Send + Sync)>,
/// The `PUT` http method.
pub put: Option<&'static (dyn ApiHandler<Body = Body> + Send + Sync)>,
/// The `POST` http method.
pub post: Option<&'static (dyn ApiHandler<Body = Body> + Send + Sync)>,
/// The `DELETE` http method.
pub delete: Option<&'static (dyn ApiHandler<Body = Body> + Send + Sync)>,
/// Specifies the behavior of sub directories. See [`SubRoute`].
pub subroute: Option<SubRoute<Body>>,
}
impl<Body> Router<Body>
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<T: AsRef<str>>(&self, path: T) -> Option<(&Self, Option<Map<String, Value>>)> {
self.lookup_do(path.as_ref())
}
// The actual implementation taking the parameter as &str
fn lookup_do(&self, path: &str) -> Option<(&Self, Option<Map<String, Value>>)> {
let mut matched_params = None;
let mut matched_wildcard: Option<String> = 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::<String, Value>::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<Body> Router<Body>
where
Self: Default,
{
/// Builder method to provide a `GET` method info.
pub fn get<I>(mut self, method: &'static I) -> Self
where
I: ApiHandler<Body = Body> + Send + Sync,
{
self.get = Some(method);
self
}
/// Builder method to provide a `PUT` method info.
pub fn put<I>(mut self, method: &'static I) -> Self
where
I: ApiHandler<Body = Body> + Send + Sync,
{
self.put = Some(method);
self
}
/// Builder method to provide a `POST` method info.
pub fn post<I>(mut self, method: &'static I) -> Self
where
I: ApiHandler<Body = Body> + Send + Sync,
{
self.post = Some(method);
self
}
/// Builder method to provide a `DELETE` method info.
pub fn delete<I>(mut self, method: &'static I) -> Self
where
I: ApiHandler<Body = Body> + 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<Body>) -> 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<Body>) -> 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
}
}

View File

@ -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<T> = Option<T>;
//!
//! #[api({
//! fields: {
//! foo: {
//! description: "Test",
//! default: 2,
//! minimum: 1,
//! maximum: 5,
//! },
//! bar: {
//! description: "Test",
//! default: 2,
//! minimum: 1,
//! maximum: 5,
//! },
//! },
//! })]
//! struct Foo {
//! foo: Option<usize>,
//! bar: Annoying<usize>,
//! }
//! ```
//!
//! 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<Other> {
fn test_minimum(&self, minimum: &Other) -> bool;
fn test_maximum(&self, maximum: &Other) -> bool;
}
impl<Other> TestMinMax<Other> 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<Other> TestMinMax<Other> for Option<Other>
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<Mark> {
fn test_minimum_length(&self, minimum: usize) -> bool;
fn test_maximum_length(&self, maximum: usize) -> bool;
}
impl<T: AsRef<str>> TestMinMaxLen<mark::Default> 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<T: AsRef<str>> TestMinMaxLen<mark::Special> for Option<T> {
#[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)
}
}

View File

@ -1,183 +0,0 @@
use bytes::Bytes;
use proxmox_api::cli;
#[test]
fn simple() {
let simple_method: &proxmox_api::ApiMethod<Bytes> = &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<Bytes> {
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<Parameter> = {
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::<String>::type_info,
},
Parameter {
name: "maybe",
description: "optional boolean test parameter",
type_info: Option::<bool>::type_info,
},
]
};
pub static ref SIMPLE_METHOD: ApiMethod<Bytes> = {
ApiMethod {
description: "get some parameters back",
parameters: &SIMPLE_PARAMS,
return_type: get_type_info::<String>(),
protected: false,
reload_timezone: false,
handler: |value: Value| -> ApiFuture<Bytes> { Box::pin(simple_method(value)) },
}
};
}
}

View File

@ -1,179 +0,0 @@
use bytes::Bytes;
use serde_json::Value;
use proxmox_api::Router;
#[test]
fn basic() {
let info: &proxmox_api::ApiMethod<Bytes> = &methods::GET_PEOPLE;
let get_subpath: &proxmox_api::ApiMethod<Bytes> = &methods::GET_SUBPATH;
let router: Router<Bytes> = 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<Bytes>,
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<Bytes> {
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<Bytes> {
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<Parameter> = {
vec![Parameter {
name: "person",
description: "the person to get",
type_info: String::type_info,
}]
};
pub static ref GET_PEOPLE: ApiMethod<Bytes> = {
ApiMethod {
description: "get some people",
parameters: &GET_PEOPLE_PARAMS,
return_type: get_type_info::<String>(),
protected: false,
reload_timezone: false,
handler: |value: Value| -> ApiFuture<Bytes> { Box::pin(get_people(value)) },
}
};
static ref GET_SUBPATH_PARAMS: Vec<Parameter> = {
vec![Parameter {
name: "subpath",
description: "the matched relative subdir path",
type_info: String::type_info,
}]
};
pub static ref GET_SUBPATH: ApiMethod<Bytes> = {
ApiMethod {
description: "get the 'subpath' parameter returned back",
parameters: &GET_SUBPATH_PARAMS,
return_type: get_type_info::<String>(),
protected: false,
reload_timezone: false,
handler: |value: Value| -> ApiFuture<Bytes> { 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(())
}
}
}