mirror of
https://git.proxmox.com/git/proxmox
synced 2025-05-04 13:29:38 +00:00
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:
parent
7f4d223a4a
commit
068d56ed3d
@ -6,9 +6,5 @@ members = [
|
|||||||
"proxmox-sortable-macro",
|
"proxmox-sortable-macro",
|
||||||
"proxmox-sys",
|
"proxmox-sys",
|
||||||
"proxmox-tools",
|
"proxmox-tools",
|
||||||
|
"proxmox",
|
||||||
# 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",
|
|
||||||
]
|
]
|
||||||
|
@ -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" }
|
|
@ -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
|
|
@ -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));
|
|
||||||
}
|
|
@ -1,4 +0,0 @@
|
|||||||
//! PVE base library
|
|
||||||
|
|
||||||
pub mod lxc;
|
|
||||||
pub mod schema;
|
|
@ -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>,
|
|
||||||
}
|
|
@ -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))
|
|
||||||
}
|
|
||||||
}
|
|
@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -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("/../")
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
@ -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));
|
|
||||||
}
|
|
@ -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;
|
|
@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
@ -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(())
|
|
||||||
}
|
|
@ -1,6 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html>
|
|
||||||
<body>
|
|
||||||
Hello
|
|
||||||
</body>
|
|
||||||
</html>
|
|
@ -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"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -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"),
|
|
||||||
}
|
|
||||||
}
|
|
@ -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
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
@ -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(())
|
|
||||||
}
|
|
@ -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(())
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
@ -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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
@ -1,160 +1 @@
|
|||||||
#![recursion_limit = "256"]
|
#![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),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
@ -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)
|
|
||||||
}
|
|
@ -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"
|
|
||||||
);
|
|
||||||
}
|
|
@ -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"
|
|
||||||
);
|
|
||||||
}
|
|
@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
@ -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
|
|
||||||
}),
|
|
||||||
))
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,10 +1,4 @@
|
|||||||
//! Proxmox API module. This provides utilities for HTTP and command line APIs.
|
//! 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::future::Future;
|
||||||
use std::pin::Pin;
|
use std::pin::Pin;
|
||||||
@ -12,20 +6,6 @@ use std::pin::Pin;
|
|||||||
use failure::Error;
|
use failure::Error;
|
||||||
use http::Response;
|
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.
|
/// Return type of an API method.
|
||||||
pub type ApiOutput<Body> = Result<Response<Body>, Error>;
|
pub type ApiOutput<Body> = Result<Response<Body>, Error>;
|
||||||
|
|
||||||
|
@ -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
|
|
||||||
}
|
|
||||||
}
|
|
@ -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
|
|
||||||
}
|
|
||||||
}
|
|
@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
@ -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)) },
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
@ -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(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
Loading…
Reference in New Issue
Block a user