"proxmox-http",
"proxmox-io",
"proxmox-lang",
+ "proxmox-router",
+ "proxmox-schema",
"proxmox-sortable-macro",
"proxmox-tfa",
"proxmox-time",
proxmox-http \
proxmox-io \
proxmox-lang \
+ proxmox-router \
+ proxmox-schema \
proxmox-sortable-macro \
proxmox-tfa \
proxmox-time \
[package]
name = "proxmox-api-macro"
edition = "2018"
-version = "0.5.1"
+version = "1.0.0"
authors = [ "Wolfgang Bumiller <w.bumiller@proxmox.com>" ]
license = "AGPL-3"
description = "Proxmox API macro"
[dev-dependencies]
futures = "0.3"
-proxmox = { version = "0.13.0", path = "../proxmox", features = [ "test-harness", "api-macro" ] }
-serde = "1.0"
-serde_derive = "1.0"
+serde = { version = "1.0", features = [ "derive" ] }
serde_json = "1.0"
+[dev-dependencies.proxmox-schema]
+version = "1.0.0"
+path = "../proxmox-schema"
+features = [ "test-harness", "api-macro" ]
+
+[dev-dependencies.proxmox-router]
+version = "1.0.0"
+path = "../proxmox-router"
+features = [ "test-harness" ]
+
# [features]
# # Used to quickly filter out the serde derive noise when using `cargo expand` for debugging!
# # Add this in case you need it, but don't commit it (to avoid debcargo picking this up)!
+rust-proxmox-api-macro (1.0.0-1) stable; urgency=medium
+
+ * schema was split out of proxmox into a new proxmox-schema crate
+
+ -- Proxmox Support Team <support@proxmox.com> Thu, 07 Oct 2021 14:28:14 +0200
+
rust-proxmox-api-macro (0.5.1-1) stable; urgency=medium
* allow external `returns` specification on methods, refereincing a
librust-syn-1+visit-mut-dev
Provides:
librust-proxmox-api-macro+default-dev (= ${binary:Version}),
- librust-proxmox-api-macro-0-dev (= ${binary:Version}),
- librust-proxmox-api-macro-0+default-dev (= ${binary:Version}),
- librust-proxmox-api-macro-0.5-dev (= ${binary:Version}),
- librust-proxmox-api-macro-0.5+default-dev (= ${binary:Version}),
- librust-proxmox-api-macro-0.5.1-dev (= ${binary:Version}),
- librust-proxmox-api-macro-0.5.1+default-dev (= ${binary:Version})
+ librust-proxmox-api-macro-1-dev (= ${binary:Version}),
+ librust-proxmox-api-macro-1+default-dev (= ${binary:Version}),
+ librust-proxmox-api-macro-1.0-dev (= ${binary:Version}),
+ librust-proxmox-api-macro-1.0+default-dev (= ${binary:Version}),
+ librust-proxmox-api-macro-1.0.0-dev (= ${binary:Version}),
+ librust-proxmox-api-macro-1.0.0+default-dev (= ${binary:Version})
Description: Proxmox API macro - Rust source code
This package contains the source for the Rust proxmox-api-macro crate, packaged
by debcargo for use with cargo and dh-cargo.
};
variants.extend(quote_spanned! { variant.ident.span() =>
- ::proxmox::api::schema::EnumEntry {
+ ::proxmox_schema::EnumEntry {
value: #variant_string,
description: #comment,
},
#enum_ty
#[automatically_derived]
- impl ::proxmox::api::schema::ApiType for #name {
- const API_SCHEMA: ::proxmox::api::schema::Schema =
+ impl ::proxmox_schema::ApiType for #name {
+ const API_SCHEMA: ::proxmox_schema::Schema =
#schema
- .format(&::proxmox::api::schema::ApiStringFormat::Enum(&[#variants]))
+ .format(&::proxmox_schema::ApiStringFormat::Enum(&[#variants]))
.schema();
}
- impl ::proxmox::api::schema::UpdaterType for #name {
+ impl ::proxmox_schema::UpdaterType for #name {
type Updater = Option<Self>;
}
})
self.schema.to_schema(&mut out)?;
ts.extend(quote! {
- ::proxmox::api::router::ReturnType::new( #optional , &#out )
+ ::proxmox_schema::ReturnType::new( #optional , &#out )
});
Ok(())
}
}
let api_handler = if is_async {
- quote! { ::proxmox::api::ApiHandler::Async(&#api_func_name) }
+ quote! { ::proxmox_router::ApiHandler::Async(&#api_func_name) }
} else {
- quote! { ::proxmox::api::ApiHandler::Sync(&#api_func_name) }
+ quote! { ::proxmox_router::ApiHandler::Sync(&#api_func_name) }
};
Ok(quote_spanned! { func.sig.span() =>
#input_schema_code
- #vis const #api_method_name: ::proxmox::api::ApiMethod =
- ::proxmox::api::ApiMethod::new_full(
+ #vis const #api_method_name: ::proxmox_router::ApiMethod =
+ ::proxmox_router::ApiMethod::new_full(
&#api_handler,
#input_schema_parameter,
)
wrapper_ts.extend(quote! {
fn #api_func_name<'a>(
mut input_params: ::serde_json::Value,
- api_method_param: &'static ::proxmox::api::ApiMethod,
- rpc_env_param: &'a mut dyn ::proxmox::api::RpcEnvironment,
- ) -> ::proxmox::api::ApiFuture<'a> {
+ api_method_param: &'static ::proxmox_router::ApiMethod,
+ rpc_env_param: &'a mut dyn ::proxmox_router::RpcEnvironment,
+ ) -> ::proxmox_router::ApiFuture<'a> {
//async fn func<'a>(
// mut input_params: ::serde_json::Value,
- // api_method_param: &'static ::proxmox::api::ApiMethod,
- // rpc_env_param: &'a mut dyn ::proxmox::api::RpcEnvironment,
+ // api_method_param: &'static ::proxmox_router::ApiMethod,
+ // rpc_env_param: &'a mut dyn ::proxmox_router::RpcEnvironment,
//) -> ::std::result::Result<::serde_json::Value, ::anyhow::Error> {
// #body
//}
wrapper_ts.extend(quote! {
fn #api_func_name(
mut input_params: ::serde_json::Value,
- api_method_param: &::proxmox::api::ApiMethod,
- rpc_env_param: &mut dyn ::proxmox::api::RpcEnvironment,
+ api_method_param: &::proxmox_router::ApiMethod,
+ rpc_env_param: &mut dyn ::proxmox_router::RpcEnvironment,
) -> ::std::result::Result<::serde_json::Value, ::anyhow::Error> {
#body
}
let ty = param.ty;
body.extend(quote_spanned! { span =>
let #arg_name = <#ty as ::serde::Deserialize>::deserialize(
- ::proxmox::api::de::ExtractValueDeserializer::try_new(
+ ::proxmox_schema::de::ExtractValueDeserializer::try_new(
input_map,
#schema_ref,
)
input_schema.to_typed_schema(&mut ts)?;
return Ok((
quote_spanned! { func_sig_span =>
- pub const #input_schema_name: ::proxmox::api::schema::ObjectSchema = #ts;
+ pub const #input_schema_name: ::proxmox_schema::ObjectSchema = #ts;
},
quote_spanned! { func_sig_span =>
- ::proxmox::api::schema::ParameterSchema::Object(&#input_schema_name)
+ ::proxmox_schema::ParameterSchema::Object(&#input_schema_name)
},
));
}
(
quote_spanned!(func_sig_span =>
- const #inner_schema_name: ::proxmox::api::schema::Schema = #obj_schema;
+ const #inner_schema_name: ::proxmox_schema::Schema = #obj_schema;
),
quote_spanned!(func_sig_span => &#inner_schema_name,),
)
quote_spanned! { func_sig_span =>
#inner_schema
- pub const #input_schema_name: ::proxmox::api::schema::AllOfSchema =
- ::proxmox::api::schema::AllOfSchema::new(
+ pub const #input_schema_name: ::proxmox_schema::AllOfSchema =
+ ::proxmox_schema::AllOfSchema::new(
#description,
&[
#inner_schema_ref
);
},
quote_spanned! { func_sig_span =>
- ::proxmox::api::schema::ParameterSchema::AllOf(&#input_schema_name)
+ ::proxmox_schema::ParameterSchema::AllOf(&#input_schema_name)
},
))
}
fn to_schema_reference(&self) -> Option<TokenStream> {
match &self.item {
SchemaItem::ExternType(path) => Some(
- quote_spanned! { path.span() => &<#path as ::proxmox::api::schema::ApiType>::API_SCHEMA },
+ quote_spanned! { path.span() => &<#path as ::proxmox_schema::ApiType>::API_SCHEMA },
),
SchemaItem::ExternSchema(path) => Some(quote_spanned! { path.span() => &#path }),
_ => None,
SchemaItem::Null(span) => {
let description = check_description()?;
ts.extend(quote_spanned! { *span =>
- ::proxmox::api::schema::NullSchema::new(#description)
+ ::proxmox_schema::NullSchema::new(#description)
});
}
SchemaItem::Boolean(span) => {
let description = check_description()?;
ts.extend(quote_spanned! { *span =>
- ::proxmox::api::schema::BooleanSchema::new(#description)
+ ::proxmox_schema::BooleanSchema::new(#description)
});
}
SchemaItem::Integer(span) => {
let description = check_description()?;
ts.extend(quote_spanned! { *span =>
- ::proxmox::api::schema::IntegerSchema::new(#description)
+ ::proxmox_schema::IntegerSchema::new(#description)
});
}
SchemaItem::Number(span) => {
let description = check_description()?;
ts.extend(quote_spanned! { *span =>
- ::proxmox::api::schema::NumberSchema::new(#description)
+ ::proxmox_schema::NumberSchema::new(#description)
});
}
SchemaItem::String(span) => {
let description = check_description()?;
ts.extend(quote_spanned! { *span =>
- ::proxmox::api::schema::StringSchema::new(#description)
+ ::proxmox_schema::StringSchema::new(#description)
});
}
SchemaItem::Object(obj) => {
let mut elems = TokenStream::new();
obj.to_schema_inner(&mut elems)?;
ts.extend(quote_spanned! { obj.span =>
- ::proxmox::api::schema::ObjectSchema::new(#description, &[#elems])
+ ::proxmox_schema::ObjectSchema::new(#description, &[#elems])
});
}
SchemaItem::Array(array) => {
let mut items = TokenStream::new();
array.to_schema(&mut items)?;
ts.extend(quote_spanned! { array.span =>
- ::proxmox::api::schema::ArraySchema::new(#description, &#items)
+ ::proxmox_schema::ArraySchema::new(#description, &#items)
});
}
SchemaItem::ExternType(path) => {
error!(description => "description not allowed on external type");
}
- ts.extend(quote_spanned! { path.span() => <#path as ::proxmox::api::schema::ApiType>::API_SCHEMA });
+ ts.extend(quote_spanned! { path.span() => <#path as ::proxmox_schema::ApiType>::API_SCHEMA });
return Ok(true);
}
SchemaItem::ExternSchema(path) => {
let name = &stru.ident;
let mut schema = finish_schema(schema, &stru, name)?;
schema.extend(quote_spanned! { name.span() =>
- impl ::proxmox::api::schema::UpdaterType for #name {
+ impl ::proxmox_schema::UpdaterType for #name {
type Updater = Option<Self>;
}
});
#stru
#[automatically_derived]
- impl ::proxmox::api::schema::ApiType for #name {
- const API_SCHEMA: ::proxmox::api::schema::Schema = #schema;
+ impl ::proxmox_schema::ApiType for #name {
+ const API_SCHEMA: ::proxmox_schema::Schema = #schema;
}
})
}
(
quote_spanned!(name.span() =>
- const INNER_API_SCHEMA: ::proxmox::api::schema::Schema = #obj_schema;
+ const INNER_API_SCHEMA: ::proxmox_schema::Schema = #obj_schema;
),
quote_spanned!(name.span() => &Self::INNER_API_SCHEMA,),
)
}
#[automatically_derived]
- impl ::proxmox::api::schema::ApiType for #name {
- const API_SCHEMA: ::proxmox::api::schema::Schema =
- ::proxmox::api::schema::AllOfSchema::new(
+ impl ::proxmox_schema::ApiType for #name {
+ const API_SCHEMA: ::proxmox_schema::Schema =
+ ::proxmox_schema::AllOfSchema::new(
#description,
&[
#inner_schema_ref
if !is_empty_impl.is_empty() {
output.extend(quote::quote!(
#[automatically_derived]
- impl ::proxmox::api::schema::Updater for #updater_name {
+ impl ::proxmox_schema::Updater for #updater_name {
fn is_empty(&self) -> bool {
#is_empty_impl
}
}
output.extend(quote::quote!(
- impl ::proxmox::api::schema::UpdaterType for #original_name {
+ impl ::proxmox_schema::UpdaterType for #original_name {
type Updater = #updater_name;
}
));
qself: Some(syn::QSelf {
lt_token: syn::token::Lt { spans: [span] },
ty: Box::new(field.ty.clone()),
- position: 4, // 'Updater' is the 4th item in the 'segments' below
+ position: 2, // 'Updater' is item index 2 in the 'segments' below
as_token: Some(syn::token::As { span }),
gt_token: syn::token::Gt { spans: [span] },
}),
- path: util::make_path(
- span,
- true,
- &["proxmox", "api", "schema", "UpdaterType", "Updater"],
- ),
+ path: util::make_path(span, true, &["proxmox_schema", "UpdaterType", "Updater"]),
};
// we also need to update the schema to point to the updater's schema for `type: Foo` entries
if field_schema.flatten_in_struct {
let updater_ty = &field.ty;
all_of_schemas
- .extend(quote::quote! {&<#updater_ty as ::proxmox::api::schema::ApiType>::API_SCHEMA,});
+ .extend(quote::quote! {&<#updater_ty as ::proxmox_schema::ApiType>::API_SCHEMA,});
}
if !is_empty_impl.is_empty() {
```
# use proxmox_api_macro::api;
- # use proxmox::api::{ApiMethod, RpcEnvironment};
+ # use proxmox_router::{ApiMethod, RpcEnvironment};
use anyhow::Error;
use serde_json::Value;
```no_run
# struct RenamedStruct;
impl RenamedStruct {
- pub const API_SCHEMA: &'static ::proxmox::api::schema::Schema =
- &::proxmox::api::schema::ObjectSchema::new(
+ pub const API_SCHEMA: &'static ::proxmox_schema::Schema =
+ &::proxmox_schema::ObjectSchema::new(
"An example of a struct with renamed fields.",
&[
(
"test-string",
false,
- &::proxmox::api::schema::StringSchema::new("A test string.").schema(),
+ &::proxmox_schema::StringSchema::new("A test string.").schema(),
),
(
"SomeOther",
true,
- &::proxmox::api::schema::StringSchema::new(
+ &::proxmox_schema::StringSchema::new(
"An optional auto-derived value for testing:",
)
.schema(),
no_generics(generics);
quote_spanned! { full_span =>
- impl ::proxmox::api::schema::UpdaterType for #ident {
+ impl ::proxmox_schema::UpdaterType for #ident {
type Updater = Option<Self>;
}
}
use serde::{Deserialize, Serialize};
use serde_json::{json, Value};
-use proxmox::api::schema::{self, ApiType};
use proxmox_api_macro::api;
+use proxmox_schema as schema;
+use proxmox_schema::ApiType;
pub const NAME_SCHEMA: schema::Schema = schema::StringSchema::new("Name.").schema();
pub const VALUE_SCHEMA: schema::Schema = schema::IntegerSchema::new("Value.").schema();
#[test]
fn test_nvit() {
- const TEST_NAME_VALUE_SCHEMA: ::proxmox::api::schema::Schema =
- ::proxmox::api::schema::ObjectSchema::new(
- "Name and value.",
- &[
- ("name", false, &NAME_SCHEMA),
- ("value", false, &VALUE_SCHEMA),
- ],
- )
- .schema();
+ const TEST_NAME_VALUE_SCHEMA: ::proxmox_schema::Schema = ::proxmox_schema::ObjectSchema::new(
+ "Name and value.",
+ &[
+ ("name", false, &NAME_SCHEMA),
+ ("value", false, &VALUE_SCHEMA),
+ ],
+ )
+ .schema();
- const TEST_SCHEMA: ::proxmox::api::schema::Schema = ::proxmox::api::schema::AllOfSchema::new(
+ const TEST_SCHEMA: ::proxmox_schema::Schema = ::proxmox_schema::AllOfSchema::new(
"Name, value, index and text.",
&[&TEST_NAME_VALUE_SCHEMA, &IndexText::API_SCHEMA],
)
#[test]
fn test_extra() {
- const INNER_SCHEMA: ::proxmox::api::schema::Schema = ::proxmox::api::schema::ObjectSchema::new(
+ const INNER_SCHEMA: ::proxmox_schema::Schema = ::proxmox_schema::ObjectSchema::new(
"<INNER: Extra Schema>",
&[(
"extra",
false,
- &::proxmox::api::schema::StringSchema::new("Extra field.").schema(),
+ &::proxmox_schema::StringSchema::new("Extra field.").schema(),
)],
)
.schema();
- const TEST_SCHEMA: ::proxmox::api::schema::Schema = ::proxmox::api::schema::AllOfSchema::new(
+ const TEST_SCHEMA: ::proxmox_schema::Schema = ::proxmox_schema::AllOfSchema::new(
"Extra Schema",
&[
&INNER_SCHEMA,
#[test]
fn hello_schema_check() {
- const TEST_METHOD: ::proxmox::api::ApiMethod = ::proxmox::api::ApiMethod::new_full(
- &::proxmox::api::ApiHandler::Sync(&api_function_hello),
- ::proxmox::api::schema::ParameterSchema::AllOf(&::proxmox::api::schema::AllOfSchema::new(
+ const TEST_METHOD: ::proxmox_router::ApiMethod = ::proxmox_router::ApiMethod::new_full(
+ &::proxmox_router::ApiHandler::Sync(&api_function_hello),
+ ::proxmox_schema::ParameterSchema::AllOf(&::proxmox_schema::AllOfSchema::new(
"Hello method.",
&[&IndexText::API_SCHEMA, &NameValue::API_SCHEMA],
)),
#[test]
fn with_extra_schema_check() {
- const INNER_SCHEMA: ::proxmox::api::schema::Schema = ::proxmox::api::schema::ObjectSchema::new(
+ const INNER_SCHEMA: ::proxmox_schema::Schema = ::proxmox_schema::ObjectSchema::new(
"<INNER: Extra method.>",
&[(
"extra",
false,
- &::proxmox::api::schema::StringSchema::new("An extra field.").schema(),
+ &::proxmox_schema::StringSchema::new("An extra field.").schema(),
)],
)
.schema();
- const TEST_METHOD: ::proxmox::api::ApiMethod = ::proxmox::api::ApiMethod::new_full(
- &::proxmox::api::ApiHandler::Sync(&api_function_with_extra),
- ::proxmox::api::schema::ParameterSchema::AllOf(&::proxmox::api::schema::AllOfSchema::new(
+ const TEST_METHOD: ::proxmox_router::ApiMethod = ::proxmox_router::ApiMethod::new_full(
+ &::proxmox_router::ApiHandler::Sync(&api_function_with_extra),
+ ::proxmox_schema::ParameterSchema::AllOf(&::proxmox_schema::AllOfSchema::new(
"Extra method.",
&[
&INNER_SCHEMA,
}
struct RpcEnv;
-impl proxmox::api::RpcEnvironment for RpcEnv {
+impl proxmox_router::RpcEnvironment for RpcEnv {
fn result_attrib_mut(&mut self) -> &mut Value {
panic!("result_attrib_mut called");
}
}
/// The environment type
- fn env_type(&self) -> proxmox::api::RpcEnvironmentType {
+ fn env_type(&self) -> proxmox_router::RpcEnvironmentType {
panic!("env_type called");
}
use anyhow::Error;
use serde_json::{json, Value};
-use proxmox::api::Permission;
+use proxmox_router::Permission;
#[api(
input: {
#[test]
fn create_ticket_schema_check() {
- const TEST_METHOD: ::proxmox::api::ApiMethod = ::proxmox::api::ApiMethod::new(
- &::proxmox::api::ApiHandler::Sync(&api_function_create_ticket),
- &::proxmox::api::schema::ObjectSchema::new(
+ const TEST_METHOD: ::proxmox_router::ApiMethod = ::proxmox_router::ApiMethod::new(
+ &::proxmox_router::ApiHandler::Sync(&api_function_create_ticket),
+ &::proxmox_schema::ObjectSchema::new(
"Create or verify authentication ticket.",
&[
(
"password",
false,
- &::proxmox::api::schema::StringSchema::new(
+ &::proxmox_schema::StringSchema::new(
"The secret password or a valid ticket.",
)
.schema(),
(
"username",
false,
- &::proxmox::api::schema::StringSchema::new("User name")
+ &::proxmox_schema::StringSchema::new("User name")
.max_length(64)
.schema(),
),
],
),
)
- .returns(::proxmox::api::router::ReturnType::new(
+ .returns(::proxmox_schema::ReturnType::new(
false,
- &::proxmox::api::schema::ObjectSchema::new(
+ &::proxmox_schema::ObjectSchema::new(
"A ticket.",
&[
(
"CSRFPreventionToken",
false,
- &::proxmox::api::schema::StringSchema::new(
+ &::proxmox_schema::StringSchema::new(
"Cross Site Request Forgerty Prevention Token.",
)
.schema(),
(
"ticket",
false,
- &::proxmox::api::schema::StringSchema::new("Auth ticket.").schema(),
+ &::proxmox_schema::StringSchema::new("Auth ticket.").schema(),
),
(
"username",
false,
- &::proxmox::api::schema::StringSchema::new("User name.").schema(),
+ &::proxmox_schema::StringSchema::new("User name.").schema(),
),
],
)
#[test]
fn create_ticket_direct_schema_check() {
- const TEST_METHOD: ::proxmox::api::ApiMethod = ::proxmox::api::ApiMethod::new(
- &::proxmox::api::ApiHandler::Sync(&api_function_create_ticket_direct),
- &::proxmox::api::schema::ObjectSchema::new(
+ const TEST_METHOD: ::proxmox_router::ApiMethod = ::proxmox_router::ApiMethod::new(
+ &::proxmox_router::ApiHandler::Sync(&api_function_create_ticket_direct),
+ &::proxmox_schema::ObjectSchema::new(
"Create or verify authentication ticket.",
&[
(
"password",
false,
- &::proxmox::api::schema::StringSchema::new(
+ &::proxmox_schema::StringSchema::new(
"The secret password or a valid ticket.",
)
.schema(),
(
"username",
false,
- &::proxmox::api::schema::StringSchema::new("User name")
+ &::proxmox_schema::StringSchema::new("User name")
.max_length(64)
.schema(),
),
],
),
)
- .returns(::proxmox::api::router::ReturnType::new(
+ .returns(::proxmox_schema::ReturnType::new(
false,
- &::proxmox::api::schema::ObjectSchema::new(
+ &::proxmox_schema::ObjectSchema::new(
"A ticket.",
&[
(
"CSRFPreventionToken",
false,
- &::proxmox::api::schema::StringSchema::new(
+ &::proxmox_schema::StringSchema::new(
"Cross Site Request Forgerty Prevention Token.",
)
.schema(),
(
"ticket",
false,
- &::proxmox::api::schema::StringSchema::new("Auth ticket.").schema(),
+ &::proxmox_schema::StringSchema::new("Auth ticket.").schema(),
),
(
"username",
false,
- &::proxmox::api::schema::StringSchema::new("User name.").schema(),
+ &::proxmox_schema::StringSchema::new("User name.").schema(),
),
],
)
#[test]
fn func_with_option_schema_check() {
- const TEST_METHOD: ::proxmox::api::ApiMethod = ::proxmox::api::ApiMethod::new(
- &::proxmox::api::ApiHandler::Sync(&api_function_func_with_option),
- &::proxmox::api::schema::ObjectSchema::new(
+ const TEST_METHOD: ::proxmox_router::ApiMethod = ::proxmox_router::ApiMethod::new(
+ &::proxmox_router::ApiHandler::Sync(&api_function_func_with_option),
+ &::proxmox_schema::ObjectSchema::new(
"Optional parameter",
&[(
"verbose",
true,
- &::proxmox::api::schema::BooleanSchema::new("Verbose output.").schema(),
+ &::proxmox_schema::BooleanSchema::new("Verbose output.").schema(),
)],
),
)
}
struct RpcEnv;
-impl proxmox::api::RpcEnvironment for RpcEnv {
+impl proxmox_router::RpcEnvironment for RpcEnv {
fn result_attrib_mut(&mut self) -> &mut Value {
panic!("result_attrib_mut called");
}
}
/// The environment type
- fn env_type(&self) -> proxmox::api::RpcEnvironmentType {
+ fn env_type(&self) -> proxmox_router::RpcEnvironmentType {
panic!("env_type called");
}
#[test]
fn number_schema_check() {
- const TEST_METHOD: ::proxmox::api::ApiMethod = ::proxmox::api::ApiMethod::new(
- &::proxmox::api::ApiHandler::Async(&api_function_number),
- &::proxmox::api::schema::ObjectSchema::new(
+ const TEST_METHOD: ::proxmox_router::ApiMethod = ::proxmox_router::ApiMethod::new(
+ &::proxmox_router::ApiHandler::Async(&api_function_number),
+ &::proxmox_schema::ObjectSchema::new(
"Return the number...",
&[(
"num",
false,
- &::proxmox::api::schema::IntegerSchema::new("The version to upgrade to")
+ &::proxmox_schema::IntegerSchema::new("The version to upgrade to")
.minimum(0)
.maximum(0xffffffff)
.schema(),
#[test]
fn more_async_params_schema_check() {
- const TEST_METHOD: ::proxmox::api::ApiMethod = ::proxmox::api::ApiMethod::new(
- &::proxmox::api::ApiHandler::Async(&api_function_more_async_params),
- &::proxmox::api::schema::ObjectSchema::new(
+ const TEST_METHOD: ::proxmox_router::ApiMethod = ::proxmox_router::ApiMethod::new(
+ &::proxmox_router::ApiHandler::Async(&api_function_more_async_params),
+ &::proxmox_schema::ObjectSchema::new(
"Return the number...",
&[
(
"bar",
false,
- &::proxmox::api::schema::StringSchema::new("The great Bar").schema(),
+ &::proxmox_schema::StringSchema::new("The great Bar").schema(),
),
(
"foo",
false,
- &::proxmox::api::schema::StringSchema::new("The great Foo").schema(),
+ &::proxmox_schema::StringSchema::new("The great Foo").schema(),
),
],
),
#[test]
fn keyword_named_parameters_check() {
- const TEST_METHOD: ::proxmox::api::ApiMethod = ::proxmox::api::ApiMethod::new(
- &::proxmox::api::ApiHandler::Async(&api_function_keyword_named_parameters),
- &::proxmox::api::schema::ObjectSchema::new(
+ const TEST_METHOD: ::proxmox_router::ApiMethod = ::proxmox_router::ApiMethod::new(
+ &::proxmox_router::ApiHandler::Async(&api_function_keyword_named_parameters),
+ &::proxmox_schema::ObjectSchema::new(
"Returns nothing.",
&[(
"type",
false,
- &::proxmox::api::schema::StringSchema::new("The great Foo").schema(),
+ &::proxmox_schema::StringSchema::new("The great Foo").schema(),
)],
),
)
//! This should test the usage of "external" schemas. If a property is declared with a path instead
//! of an object, we expect the path to lead to a schema.
-use proxmox::api::{schema, RpcEnvironment};
use proxmox_api_macro::api;
+use proxmox_router::RpcEnvironment;
+use proxmox_schema as schema;
use anyhow::Error;
use serde_json::{json, Value};
#[test]
fn get_archive_schema_check() {
- const TEST_METHOD: ::proxmox::api::ApiMethod = ::proxmox::api::ApiMethod::new(
- &::proxmox::api::ApiHandler::Sync(&api_function_get_archive),
- &::proxmox::api::schema::ObjectSchema::new(
+ const TEST_METHOD: ::proxmox_router::ApiMethod = ::proxmox_router::ApiMethod::new(
+ &::proxmox_router::ApiHandler::Sync(&api_function_get_archive),
+ &::proxmox_schema::ObjectSchema::new(
"Get an archive.",
&[("archive-name", false, &NAME_SCHEMA)],
),
#[test]
fn get_archive_2_schema_check() {
- const TEST_METHOD: ::proxmox::api::ApiMethod = ::proxmox::api::ApiMethod::new(
- &::proxmox::api::ApiHandler::Sync(&api_function_get_archive_2),
- &::proxmox::api::schema::ObjectSchema::new(
+ const TEST_METHOD: ::proxmox_router::ApiMethod = ::proxmox_router::ApiMethod::new(
+ &::proxmox_router::ApiHandler::Sync(&api_function_get_archive_2),
+ &::proxmox_schema::ObjectSchema::new(
"Get an archive.",
&[("archive-name", false, &NAME_SCHEMA)],
),
#[test]
fn get_data_schema_test() {
- const TEST_METHOD: ::proxmox::api::ApiMethod = ::proxmox::api::ApiMethod::new(
- &::proxmox::api::ApiHandler::Sync(&api_function_get_data),
- &::proxmox::api::schema::ObjectSchema::new(
+ const TEST_METHOD: ::proxmox_router::ApiMethod = ::proxmox_router::ApiMethod::new(
+ &::proxmox_router::ApiHandler::Sync(&api_function_get_data),
+ &::proxmox_schema::ObjectSchema::new(
"Get data.",
&[(
"data",
false,
- &::proxmox::api::schema::ArraySchema::new("The data", &NAME_SCHEMA).schema(),
+ &::proxmox_schema::ArraySchema::new("The data", &NAME_SCHEMA).schema(),
)],
),
)
//! Test the automatic addition of integer limits.
-use proxmox::api::schema::ApiType;
+use proxmox_schema::ApiType;
use proxmox_api_macro::api;
/// An i16: -32768 to 32767.
#[test]
fn test_an_i16_schema() {
- const TEST_SCHEMA: ::proxmox::api::schema::Schema =
- ::proxmox::api::schema::IntegerSchema::new("An i16: -32768 to 32767.")
+ const TEST_SCHEMA: ::proxmox_schema::Schema =
+ ::proxmox_schema::IntegerSchema::new("An i16: -32768 to 32767.")
.minimum(-32768)
.maximum(32767)
.schema();
#[test]
fn test_i16g50_schema() {
- const TEST_SCHEMA: ::proxmox::api::schema::Schema =
- ::proxmox::api::schema::IntegerSchema::new("Already limited on one side.")
+ const TEST_SCHEMA: ::proxmox_schema::Schema =
+ ::proxmox_schema::IntegerSchema::new("Already limited on one side.")
.minimum(-50)
.maximum(32767)
.schema();
#[test]
fn test_an_i32_schema() {
- const TEST_SCHEMA: ::proxmox::api::schema::Schema =
- ::proxmox::api::schema::IntegerSchema::new("An i32: -0x8000_0000 to 0x7fff_ffff.")
+ const TEST_SCHEMA: ::proxmox_schema::Schema =
+ ::proxmox_schema::IntegerSchema::new("An i32: -0x8000_0000 to 0x7fff_ffff.")
.minimum(-0x8000_0000)
.maximum(0x7fff_ffff)
.schema();
#[test]
fn test_an_u32_schema() {
- const TEST_SCHEMA: ::proxmox::api::schema::Schema =
- ::proxmox::api::schema::IntegerSchema::new("Unsigned implies a minimum of zero.")
+ const TEST_SCHEMA: ::proxmox_schema::Schema =
+ ::proxmox_schema::IntegerSchema::new("Unsigned implies a minimum of zero.")
.minimum(0)
.maximum(0xffff_ffff)
.schema();
#[test]
fn test_an_i64_schema() {
- const TEST_SCHEMA: ::proxmox::api::schema::Schema =
- ::proxmox::api::schema::IntegerSchema::new("An i64: this is left unlimited.").schema();
+ const TEST_SCHEMA: ::proxmox_schema::Schema =
+ ::proxmox_schema::IntegerSchema::new("An i64: this is left unlimited.").schema();
assert_eq!(TEST_SCHEMA, AnI64::API_SCHEMA);
}
#[test]
fn test_an_u64_schema() {
- const TEST_SCHEMA: ::proxmox::api::schema::Schema =
- ::proxmox::api::schema::IntegerSchema::new("Unsigned implies a minimum of zero.")
+ const TEST_SCHEMA: ::proxmox_schema::Schema =
+ ::proxmox_schema::IntegerSchema::new("Unsigned implies a minimum of zero.")
.minimum(0)
.schema();
}
struct RpcEnv;
-impl proxmox::api::RpcEnvironment for RpcEnv {
+impl proxmox_router::RpcEnvironment for RpcEnv {
fn result_attrib_mut(&mut self) -> &mut Value {
panic!("result_attrib_mut called");
}
}
/// The environment type
- fn env_type(&self) -> proxmox::api::RpcEnvironmentType {
+ fn env_type(&self) -> proxmox_router::RpcEnvironmentType {
panic!("env_type called");
}
#![allow(dead_code)]
-use proxmox::api::schema::{self, ApiType, EnumEntry};
use proxmox_api_macro::api;
+use proxmox_schema as schema;
+use proxmox_schema::{ApiType, EnumEntry};
use anyhow::Error;
use serde::Deserialize;
#[test]
fn ok_string() {
- const TEST_SCHEMA: ::proxmox::api::schema::Schema =
- ::proxmox::api::schema::StringSchema::new("A string")
- .format(&schema::ApiStringFormat::Enum(&[
- EnumEntry::new("ok", "Ok"),
- EnumEntry::new("not-ok", "Not OK"),
- ]))
- .schema();
+ const TEST_SCHEMA: ::proxmox_schema::Schema = ::proxmox_schema::StringSchema::new("A string")
+ .format(&schema::ApiStringFormat::Enum(&[
+ EnumEntry::new("ok", "Ok"),
+ EnumEntry::new("not-ok", "Not OK"),
+ ]))
+ .schema();
assert_eq!(TEST_SCHEMA, OkString::API_SCHEMA);
}
#[test]
fn test_struct() {
- pub const TEST_SCHEMA: ::proxmox::api::schema::Schema =
- ::proxmox::api::schema::ObjectSchema::new(
- "An example of a simple struct type.",
- &[
- (
- "another",
- true,
- &::proxmox::api::schema::StringSchema::new(
- "An optional auto-derived value for testing:",
- )
+ pub const TEST_SCHEMA: ::proxmox_schema::Schema = ::proxmox_schema::ObjectSchema::new(
+ "An example of a simple struct type.",
+ &[
+ (
+ "another",
+ true,
+ &::proxmox_schema::StringSchema::new("An optional auto-derived value for testing:")
.schema(),
- ),
- (
- "test_string",
- false,
- &::proxmox::api::schema::StringSchema::new("A test string.").schema(),
- ),
- ],
- )
- .schema();
+ ),
+ (
+ "test_string",
+ false,
+ &::proxmox_schema::StringSchema::new("A test string.").schema(),
+ ),
+ ],
+ )
+ .schema();
assert_eq!(TEST_SCHEMA, TestStruct::API_SCHEMA);
}
#[test]
fn renamed_struct() {
- const TEST_SCHEMA: ::proxmox::api::schema::Schema = ::proxmox::api::schema::ObjectSchema::new(
+ const TEST_SCHEMA: ::proxmox_schema::Schema = ::proxmox_schema::ObjectSchema::new(
"An example of a struct with renamed fields.",
&[
(
"SomeOther",
true,
- &::proxmox::api::schema::StringSchema::new(
- "An optional auto-derived value for testing:",
- )
- .schema(),
+ &::proxmox_schema::StringSchema::new("An optional auto-derived value for testing:")
+ .schema(),
),
(
"test-string",
false,
- &::proxmox::api::schema::StringSchema::new("A test string.").schema(),
+ &::proxmox_schema::StringSchema::new("A test string.").schema(),
),
],
)
#[test]
fn selection_test() {
- const TEST_SCHEMA: ::proxmox::api::schema::Schema = ::proxmox::api::schema::StringSchema::new(
+ const TEST_SCHEMA: ::proxmox_schema::Schema = ::proxmox_schema::StringSchema::new(
"A selection of either \'onekind\', \'another-kind\' or \'selection-number-three\'.",
)
- .format(&::proxmox::api::schema::ApiStringFormat::Enum(&[
+ .format(&::proxmox_schema::ApiStringFormat::Enum(&[
EnumEntry::new("onekind", "The first kind."),
EnumEntry::new("another-kind", "Some other kind."),
EnumEntry::new("selection-number-three", "And yet another."),
#[test]
fn string_check_schema_test() {
- const TEST_METHOD: ::proxmox::api::ApiMethod = ::proxmox::api::ApiMethod::new(
- &::proxmox::api::ApiHandler::Sync(&api_function_string_check),
- &::proxmox::api::schema::ObjectSchema::new(
+ const TEST_METHOD: ::proxmox_router::ApiMethod = ::proxmox_router::ApiMethod::new(
+ &::proxmox_router::ApiHandler::Sync(&api_function_string_check),
+ &::proxmox_schema::ObjectSchema::new(
"Check a string.",
&[
("arg", false, &OkString::API_SCHEMA),
],
),
)
- .returns(::proxmox::api::router::ReturnType::new(
+ .returns(::proxmox_schema::ReturnType::new(
true,
- &::proxmox::api::schema::BooleanSchema::new("Whether the string was \"ok\".").schema(),
+ &::proxmox_schema::BooleanSchema::new("Whether the string was \"ok\".").schema(),
))
.protected(false);
#![allow(dead_code)]
-use proxmox::api::api;
-use proxmox::api::schema::{ApiType, Updater, UpdaterType};
+use proxmox_schema::{api, ApiType, Updater, UpdaterType};
// Helpers for type checks:
struct AssertTypeEq<T>(T);
#[test]
fn test_simple() {
- pub const TEST_SCHEMA: ::proxmox::api::schema::Schema =
- ::proxmox::api::schema::ObjectSchema::new(
- "An example of a simple struct type.",
- &[
- (
- "one-field",
- true,
- &::proxmox::api::schema::StringSchema::new("A test string.").schema(),
- ),
- (
- "opt",
- true,
- &::proxmox::api::schema::StringSchema::new("Another test value.").schema(),
- ),
- ],
- )
- .schema();
+ pub const TEST_SCHEMA: ::proxmox_schema::Schema = ::proxmox_schema::ObjectSchema::new(
+ "An example of a simple struct type.",
+ &[
+ (
+ "one-field",
+ true,
+ &::proxmox_schema::StringSchema::new("A test string.").schema(),
+ ),
+ (
+ "opt",
+ true,
+ &::proxmox_schema::StringSchema::new("Another test value.").schema(),
+ ),
+ ],
+ )
+ .schema();
assert_eq!(TEST_SCHEMA, SimpleUpdater::API_SCHEMA);
}
}
#[test]
fn test_super_complex() {
- pub const TEST_SCHEMA: ::proxmox::api::schema::Schema =
- ::proxmox::api::schema::ObjectSchema::new(
- "One of the baaaad cases.",
- &[
- ("custom", true, &<Option<Custom> as ApiType>::API_SCHEMA),
- (
- "extra",
- true,
- &::proxmox::api::schema::StringSchema::new("An extra field.").schema(),
- ),
- (
- "simple",
- true,
- //&<<Simple as UpdaterType>::Updater as ApiType>::API_SCHEMA,
- &SimpleUpdater::API_SCHEMA,
- ),
- ],
- )
- .schema();
+ pub const TEST_SCHEMA: ::proxmox_schema::Schema = ::proxmox_schema::ObjectSchema::new(
+ "One of the baaaad cases.",
+ &[
+ ("custom", true, &<Option<Custom> as ApiType>::API_SCHEMA),
+ (
+ "extra",
+ true,
+ &::proxmox_schema::StringSchema::new("An extra field.").schema(),
+ ),
+ (
+ "simple",
+ true,
+ //&<<Simple as UpdaterType>::Updater as ApiType>::API_SCHEMA,
+ &SimpleUpdater::API_SCHEMA,
+ ),
+ ],
+ )
+ .schema();
assert_eq!(TEST_SCHEMA, SuperComplexUpdater::API_SCHEMA);
}
unsafe { &(*(std::ptr::null::<$ty>())).$field as *const _ as usize }
};
}
+
+/// Shortcut for generating an `&'static CStr`.
+///
+/// This takes a *string* (*not* a *byte-string*), appends a terminating zero, and calls
+/// `CStr::from_bytes_with_nul_unchecked`.
+///
+/// Shortcut for:
+/// ```no_run
+/// let bytes = concat!("THE TEXT", "\0");
+/// unsafe { ::std::ffi::CStr::from_bytes_with_nul_unchecked(bytes.as_bytes()) }
+/// # ;
+/// ```
+#[macro_export]
+macro_rules! c_str {
+ ($data:expr) => {{
+ let bytes = concat!($data, "\0");
+ unsafe { ::std::ffi::CStr::from_bytes_with_nul_unchecked(bytes.as_bytes()) }
+ }};
+}
--- /dev/null
+[package]
+name = "proxmox-router"
+version = "1.0.0"
+authors = ["Proxmox Support Team <support@proxmox.com>"]
+edition = "2018"
+license = "AGPL-3"
+description = "proxmox API Router and CLI utilities"
+
+exclude = [ "debian" ]
+
+[dependencies]
+anyhow = "1.0"
+http = "0.2"
+hyper = { version = "0.14", features = [ "full" ] }
+percent-encoding = "2.1"
+serde_json = "1.0"
+unicode-width ="0.1.8"
+
+# cli:
+tokio = { version = "1.0", features = [], optional = true }
+rustyline = { version = "7", optional = true }
+libc = { version = "0.2", optional = true }
+
+proxmox-lang = { path = "../proxmox-lang", version = "1.0" }
+proxmox-schema = { path = "../proxmox-schema", version = "1.0" }
+
+[features]
+default = [ "cli" ]
+cli = [ "libc", "rustyline", "tokio" ]
+test-harness = [ "proxmox-schema/test-harness" ]
--- /dev/null
+rust-proxmox-router (1.0.0-1) stable; urgency=medium
+
+ * initial split out of `librust-proxmox-dev`
+
+ -- Proxmox Support Team <support@proxmox.com> Wed, 06 Oct 2021 11:04:36 +0200
--- /dev/null
+Source: rust-proxmox-router
+Section: rust
+Priority: optional
+Build-Depends: debhelper (>= 12),
+ dh-cargo (>= 24),
+ cargo:native <!nocheck>,
+ rustc:native <!nocheck>,
+ libstd-rust-dev <!nocheck>,
+ librust-anyhow-1+default-dev <!nocheck>,
+ librust-http-0.2+default-dev <!nocheck>,
+ librust-hyper-0.14+default-dev <!nocheck>,
+ librust-hyper-0.14+full-dev <!nocheck>,
+ librust-libc-0.2+default-dev <!nocheck>,
+ librust-percent-encoding-2+default-dev (>= 2.1-~~) <!nocheck>,
+ librust-proxmox-lang-1+default-dev <!nocheck>,
+ librust-proxmox-schema-1+default-dev <!nocheck>,
+ librust-rustyline-7+default-dev <!nocheck>,
+ librust-serde-json-1+default-dev <!nocheck>,
+ librust-tokio-1+default-dev <!nocheck>,
+ librust-unicode-width-0.1+default-dev (>= 0.1.8-~~) <!nocheck>
+Maintainer: Proxmox Support Team <support@proxmox.com>
+Standards-Version: 4.5.1
+Vcs-Git: git://git.proxmox.com/git/proxmox.git
+Vcs-Browser: https://git.proxmox.com/?p=proxmox.git
+Rules-Requires-Root: no
+
+Package: librust-proxmox-router-dev
+Architecture: any
+Multi-Arch: same
+Depends:
+ ${misc:Depends},
+ librust-anyhow-1+default-dev,
+ librust-http-0.2+default-dev,
+ librust-hyper-0.14+default-dev,
+ librust-hyper-0.14+full-dev,
+ librust-percent-encoding-2+default-dev (>= 2.1-~~),
+ librust-proxmox-lang-1+default-dev,
+ librust-proxmox-schema-1+default-dev,
+ librust-serde-json-1+default-dev,
+ librust-unicode-width-0.1+default-dev (>= 0.1.8-~~)
+Recommends:
+ librust-proxmox-router+cli-dev (= ${binary:Version})
+Suggests:
+ librust-proxmox-router+libc-dev (= ${binary:Version}),
+ librust-proxmox-router+rustyline-dev (= ${binary:Version}),
+ librust-proxmox-router+test-harness-dev (= ${binary:Version}),
+ librust-proxmox-router+tokio-dev (= ${binary:Version})
+Provides:
+ librust-proxmox-router-1-dev (= ${binary:Version}),
+ librust-proxmox-router-1.0-dev (= ${binary:Version}),
+ librust-proxmox-router-1.0.0-dev (= ${binary:Version})
+Description: Proxmox API Router and CLI utilities - Rust source code
+ This package contains the source for the Rust proxmox-router crate, packaged by
+ debcargo for use with cargo and dh-cargo.
+
+Package: librust-proxmox-router+cli-dev
+Architecture: any
+Multi-Arch: same
+Depends:
+ ${misc:Depends},
+ librust-proxmox-router-dev (= ${binary:Version}),
+ librust-libc-0.2+default-dev,
+ librust-rustyline-7+default-dev,
+ librust-tokio-1+default-dev
+Provides:
+ librust-proxmox-router+default-dev (= ${binary:Version}),
+ librust-proxmox-router-1+cli-dev (= ${binary:Version}),
+ librust-proxmox-router-1+default-dev (= ${binary:Version}),
+ librust-proxmox-router-1.0+cli-dev (= ${binary:Version}),
+ librust-proxmox-router-1.0+default-dev (= ${binary:Version}),
+ librust-proxmox-router-1.0.0+cli-dev (= ${binary:Version}),
+ librust-proxmox-router-1.0.0+default-dev (= ${binary:Version})
+Description: Proxmox API Router and CLI utilities - feature "cli" and 1 more
+ This metapackage enables feature "cli" for the Rust proxmox-router crate, by
+ pulling in any additional dependencies needed by that feature.
+ .
+ Additionally, this package also provides the "default" feature.
+
+Package: librust-proxmox-router+libc-dev
+Architecture: any
+Multi-Arch: same
+Depends:
+ ${misc:Depends},
+ librust-proxmox-router-dev (= ${binary:Version}),
+ librust-libc-0.2+default-dev
+Provides:
+ librust-proxmox-router-1+libc-dev (= ${binary:Version}),
+ librust-proxmox-router-1.0+libc-dev (= ${binary:Version}),
+ librust-proxmox-router-1.0.0+libc-dev (= ${binary:Version})
+Description: Proxmox API Router and CLI utilities - feature "libc"
+ This metapackage enables feature "libc" for the Rust proxmox-router crate, by
+ pulling in any additional dependencies needed by that feature.
+
+Package: librust-proxmox-router+rustyline-dev
+Architecture: any
+Multi-Arch: same
+Depends:
+ ${misc:Depends},
+ librust-proxmox-router-dev (= ${binary:Version}),
+ librust-rustyline-7+default-dev
+Provides:
+ librust-proxmox-router-1+rustyline-dev (= ${binary:Version}),
+ librust-proxmox-router-1.0+rustyline-dev (= ${binary:Version}),
+ librust-proxmox-router-1.0.0+rustyline-dev (= ${binary:Version})
+Description: Proxmox API Router and CLI utilities - feature "rustyline"
+ This metapackage enables feature "rustyline" for the Rust proxmox-router crate,
+ by pulling in any additional dependencies needed by that feature.
+
+Package: librust-proxmox-router+test-harness-dev
+Architecture: any
+Multi-Arch: same
+Depends:
+ ${misc:Depends},
+ librust-proxmox-router-dev (= ${binary:Version}),
+ librust-proxmox-schema-1+test-harness-dev
+Provides:
+ librust-proxmox-router-1+test-harness-dev (= ${binary:Version}),
+ librust-proxmox-router-1.0+test-harness-dev (= ${binary:Version}),
+ librust-proxmox-router-1.0.0+test-harness-dev (= ${binary:Version})
+Description: Proxmox API Router and CLI utilities - feature "test-harness"
+ This metapackage enables feature "test-harness" for the Rust proxmox-router
+ crate, by pulling in any additional dependencies needed by that feature.
+
+Package: librust-proxmox-router+tokio-dev
+Architecture: any
+Multi-Arch: same
+Depends:
+ ${misc:Depends},
+ librust-proxmox-router-dev (= ${binary:Version}),
+ librust-tokio-1+default-dev
+Provides:
+ librust-proxmox-router-1+tokio-dev (= ${binary:Version}),
+ librust-proxmox-router-1.0+tokio-dev (= ${binary:Version}),
+ librust-proxmox-router-1.0.0+tokio-dev (= ${binary:Version})
+Description: Proxmox API Router and CLI utilities - feature "tokio"
+ This metapackage enables feature "tokio" for the Rust proxmox-router crate, by
+ pulling in any additional dependencies needed by that feature.
--- /dev/null
+Copyright (C) 2021 Proxmox Server Solutions GmbH
+
+This software is written by Proxmox Server Solutions GmbH <support@proxmox.com>
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU Affero General Public License as published by
+the Free Software Foundation, either version 3 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU Affero General Public License for more details.
+
+You should have received a copy of the GNU Affero General Public License
+along with this program. If not, see <http://www.gnu.org/licenses/>.
--- /dev/null
+overlay = "."
+crate_src_path = ".."
+maintainer = "Proxmox Support Team <support@proxmox.com>"
+
+[source]
+vcs_git = "git://git.proxmox.com/git/proxmox.git"
+vcs_browser = "https://git.proxmox.com/?p=proxmox.git"
--- /dev/null
+use anyhow::*;
+use serde_json::Value;
+use std::cell::RefCell;
+use std::sync::Arc;
+
+use proxmox_schema::*;
+use proxmox_schema::format::DocumentationFormat;
+
+use super::environment::CliEnvironment;
+use super::getopts;
+use super::{
+ generate_nested_usage, generate_usage_str, print_help, print_nested_usage_error,
+ print_simple_usage_error, CliCommand, CliCommandMap, CommandLineInterface,
+};
+use crate::{ApiFuture, ApiHandler, ApiMethod, RpcEnvironment};
+
+/// Schema definition for ``--output-format`` parameter.
+///
+/// - ``text``: command specific text format.
+/// - ``json``: JSON, single line.
+/// - ``json-pretty``: JSON, human readable.
+///
+pub const OUTPUT_FORMAT: Schema = StringSchema::new("Output format.")
+ .format(&ApiStringFormat::Enum(&[
+ EnumEntry::new("text", "plain text output"),
+ EnumEntry::new("json", "single-line json formatted output"),
+ EnumEntry::new("json-pretty", "pretty-printed json output"),
+ ]))
+ .schema();
+
+fn parse_arguments(prefix: &str, cli_cmd: &CliCommand, args: Vec<String>) -> Result<Value, Error> {
+ let (params, remaining) = match getopts::parse_arguments(
+ &args,
+ cli_cmd.arg_param,
+ &cli_cmd.fixed_param,
+ cli_cmd.info.parameters,
+ ) {
+ Ok((p, r)) => (p, r),
+ Err(err) => {
+ let err_msg = err.to_string();
+ print_simple_usage_error(prefix, cli_cmd, &err_msg);
+ return Err(format_err!("{}", err_msg));
+ }
+ };
+
+ if !remaining.is_empty() {
+ let err_msg = format!("got additional arguments: {:?}", remaining);
+ print_simple_usage_error(prefix, cli_cmd, &err_msg);
+ return Err(format_err!("{}", err_msg));
+ }
+
+ Ok(params)
+}
+
+async fn handle_simple_command_future(
+ prefix: &str,
+ cli_cmd: &CliCommand,
+ args: Vec<String>,
+ mut rpcenv: CliEnvironment,
+) -> Result<(), Error> {
+ let params = parse_arguments(prefix, cli_cmd, args)?;
+
+ match cli_cmd.info.handler {
+ ApiHandler::Sync(handler) => match (handler)(params, &cli_cmd.info, &mut rpcenv) {
+ Ok(value) => {
+ if value != Value::Null {
+ println!("Result: {}", serde_json::to_string_pretty(&value).unwrap());
+ }
+ }
+ Err(err) => {
+ eprintln!("Error: {}", err);
+ return Err(err);
+ }
+ },
+ ApiHandler::Async(handler) => {
+ let future = (handler)(params, &cli_cmd.info, &mut rpcenv);
+
+ match future.await {
+ Ok(value) => {
+ if value != Value::Null {
+ println!("Result: {}", serde_json::to_string_pretty(&value).unwrap());
+ }
+ }
+ Err(err) => {
+ eprintln!("Error: {}", err);
+ return Err(err);
+ }
+ }
+ }
+ ApiHandler::AsyncHttp(_) => {
+ let err_msg = "CliHandler does not support ApiHandler::AsyncHttp - internal error";
+ print_simple_usage_error(prefix, cli_cmd, err_msg);
+ return Err(format_err!("{}", err_msg));
+ }
+ }
+
+ Ok(())
+}
+
+fn handle_simple_command(
+ prefix: &str,
+ cli_cmd: &CliCommand,
+ args: Vec<String>,
+ mut rpcenv: CliEnvironment,
+ run: Option<fn(ApiFuture) -> Result<Value, Error>>,
+) -> Result<(), Error> {
+ let params = parse_arguments(prefix, cli_cmd, args)?;
+
+ match cli_cmd.info.handler {
+ ApiHandler::Sync(handler) => match (handler)(params, &cli_cmd.info, &mut rpcenv) {
+ Ok(value) => {
+ if value != Value::Null {
+ println!("Result: {}", serde_json::to_string_pretty(&value).unwrap());
+ }
+ }
+ Err(err) => {
+ eprintln!("Error: {}", err);
+ return Err(err);
+ }
+ },
+ ApiHandler::Async(handler) => {
+ let future = (handler)(params, &cli_cmd.info, &mut rpcenv);
+ if let Some(run) = run {
+ match (run)(future) {
+ Ok(value) => {
+ if value != Value::Null {
+ println!("Result: {}", serde_json::to_string_pretty(&value).unwrap());
+ }
+ }
+ Err(err) => {
+ eprintln!("Error: {}", err);
+ return Err(err);
+ }
+ }
+ } else {
+ let err_msg = "CliHandler does not support ApiHandler::Async - internal error";
+ print_simple_usage_error(prefix, cli_cmd, err_msg);
+ return Err(format_err!("{}", err_msg));
+ }
+ }
+ ApiHandler::AsyncHttp(_) => {
+ let err_msg = "CliHandler does not support ApiHandler::AsyncHttp - internal error";
+ print_simple_usage_error(prefix, cli_cmd, err_msg);
+ return Err(format_err!("{}", err_msg));
+ }
+ }
+
+ Ok(())
+}
+
+fn parse_nested_command<'a>(
+ prefix: &mut String,
+ def: &'a CliCommandMap,
+ args: &mut Vec<String>,
+) -> Result<&'a CliCommand, Error> {
+ let mut map = def;
+
+ // Note: Avoid async recursive function, because current rust compiler cant handle that
+ loop {
+ replace_aliases(args, &map.aliases);
+
+ if args.is_empty() {
+ let mut cmds: Vec<&String> = map.commands.keys().collect();
+ cmds.sort();
+
+ let list = cmds.iter().fold(String::new(), |mut s, item| {
+ if !s.is_empty() {
+ s += ", ";
+ }
+ s += item;
+ s
+ });
+
+ let err_msg = format!("no command specified.\nPossible commands: {}", list);
+ print_nested_usage_error(&prefix, map, &err_msg);
+ return Err(format_err!("{}", err_msg));
+ }
+
+ let command = args.remove(0);
+
+ let (_, sub_cmd) = match map.find_command(&command) {
+ Some(cmd) => cmd,
+ None => {
+ let err_msg = format!("no such command '{}'", command);
+ print_nested_usage_error(&prefix, map, &err_msg);
+ return Err(format_err!("{}", err_msg));
+ }
+ };
+
+ *prefix = format!("{} {}", prefix, command);
+
+ match sub_cmd {
+ CommandLineInterface::Simple(cli_cmd) => {
+ //return handle_simple_command(&prefix, cli_cmd, args).await;
+ return Ok(&cli_cmd);
+ }
+ CommandLineInterface::Nested(new_map) => map = new_map,
+ }
+ }
+}
+
+const API_METHOD_COMMAND_HELP: ApiMethod = ApiMethod::new(
+ &ApiHandler::Sync(&help_command),
+ &ObjectSchema::new(
+ "Get help about specified command (or sub-command).",
+ &[
+ (
+ "command",
+ true,
+ &ArraySchema::new(
+ "Command. This may be a list in order to spefify nested sub-commands.",
+ &StringSchema::new("Name.").schema(),
+ )
+ .schema(),
+ ),
+ (
+ "verbose",
+ true,
+ &BooleanSchema::new("Verbose help.").schema(),
+ ),
+ ],
+ ),
+);
+
+std::thread_local! {
+ static HELP_CONTEXT: RefCell<Option<Arc<CommandLineInterface>>> = RefCell::new(None);
+}
+
+fn help_command(
+ param: Value,
+ _info: &ApiMethod,
+ _rpcenv: &mut dyn RpcEnvironment,
+) -> Result<Value, Error> {
+ let mut command: Vec<String> = param["command"]
+ .as_array()
+ .unwrap_or(&Vec::new())
+ .iter()
+ .map(|v| v.as_str().unwrap().to_string())
+ .collect();
+
+ let verbose = param["verbose"].as_bool();
+
+ HELP_CONTEXT.with(|ctx| match &*ctx.borrow() {
+ Some(def) => {
+ if let CommandLineInterface::Nested(map) = def.as_ref() {
+ replace_aliases(&mut command, &map.aliases);
+ }
+ print_help(def, String::from(""), &command, verbose);
+ }
+ None => {
+ eprintln!("Sorry, help context not set - internal error.");
+ }
+ });
+
+ Ok(Value::Null)
+}
+
+fn set_help_context(def: Option<Arc<CommandLineInterface>>) {
+ HELP_CONTEXT.with(|ctx| {
+ *ctx.borrow_mut() = def;
+ });
+}
+
+pub(crate) fn help_command_def() -> CliCommand {
+ CliCommand::new(&API_METHOD_COMMAND_HELP).arg_param(&["command"])
+}
+
+fn replace_aliases(args: &mut Vec<String>, aliases: &[(Vec<&'static str>, Vec<&'static str>)]) {
+ for (old, new) in aliases {
+ if args.len() < old.len() {
+ continue;
+ }
+ if old[..] == args[..old.len()] {
+ let new_args: Vec<String> = new.iter().map(|s| String::from(*s)).collect();
+ let rest = args.split_off(old.len());
+ args.truncate(0);
+ args.extend(new_args);
+ for arg in rest.iter() {
+ args.push(arg.clone());
+ }
+ return;
+ }
+ }
+}
+
+/// Handle command invocation.
+///
+/// This command gets the command line ``args`` and tries to invoke
+/// the corresponding API handler.
+pub async fn handle_command_future(
+ def: Arc<CommandLineInterface>,
+ prefix: &str,
+ mut args: Vec<String>,
+ rpcenv: CliEnvironment,
+) -> Result<(), Error> {
+ set_help_context(Some(def.clone()));
+
+ let result = match &*def {
+ CommandLineInterface::Simple(ref cli_cmd) => {
+ handle_simple_command_future(&prefix, &cli_cmd, args, rpcenv).await
+ }
+ CommandLineInterface::Nested(ref map) => {
+ let mut prefix = prefix.to_string();
+ let cli_cmd = parse_nested_command(&mut prefix, &map, &mut args)?;
+ handle_simple_command_future(&prefix, &cli_cmd, args, rpcenv).await
+ }
+ };
+
+ set_help_context(None);
+
+ result
+}
+
+/// Handle command invocation.
+///
+/// This command gets the command line ``args`` and tries to invoke
+/// the corresponding API handler.
+pub fn handle_command(
+ def: Arc<CommandLineInterface>,
+ prefix: &str,
+ mut args: Vec<String>,
+ rpcenv: CliEnvironment,
+ run: Option<fn(ApiFuture) -> Result<Value, Error>>,
+) -> Result<(), Error> {
+ set_help_context(Some(def.clone()));
+
+ let result = match &*def {
+ CommandLineInterface::Simple(ref cli_cmd) => {
+ handle_simple_command(&prefix, &cli_cmd, args, rpcenv, run)
+ }
+ CommandLineInterface::Nested(ref map) => {
+ let mut prefix = prefix.to_string();
+ let cli_cmd = parse_nested_command(&mut prefix, &map, &mut args)?;
+ handle_simple_command(&prefix, &cli_cmd, args, rpcenv, run)
+ }
+ };
+
+ set_help_context(None);
+
+ result
+}
+
+fn prepare_cli_command(def: &CommandLineInterface) -> (String, Vec<String>) {
+ let mut args = std::env::args();
+
+ let prefix = args.next().unwrap();
+ let prefix = prefix.rsplit('/').next().unwrap().to_string(); // without path
+
+ let args: Vec<String> = args.collect();
+
+ if !args.is_empty() {
+ if args[0] == "bashcomplete" {
+ def.print_bash_completion();
+ std::process::exit(0);
+ }
+
+ if args[0] == "printdoc" {
+ let usage = match def {
+ CommandLineInterface::Simple(cli_cmd) => {
+ generate_usage_str(&prefix, &cli_cmd, DocumentationFormat::ReST, "", &[])
+ }
+ CommandLineInterface::Nested(map) => {
+ generate_nested_usage(&prefix, &map, DocumentationFormat::ReST)
+ }
+ };
+ println!("{}", usage);
+ std::process::exit(0);
+ }
+ }
+
+ (prefix, args)
+}
+
+/// Helper to get arguments and invoke the command (async).
+///
+/// This helper reads arguments with ``std::env::args()``. The first
+/// argument is assumed to be the program name, and is passed as ``prefix`` to
+/// ``handle_command()``.
+///
+/// This helper automatically add the help command, and two special
+/// sub-command:
+///
+/// - ``bashcomplete``: Output bash completions instead of running the command.
+/// - ``printdoc``: Output ReST documentation.
+///
+pub async fn run_async_cli_command<C: Into<CommandLineInterface>>(def: C, rpcenv: CliEnvironment) {
+ let def = match def.into() {
+ CommandLineInterface::Simple(cli_cmd) => CommandLineInterface::Simple(cli_cmd),
+ CommandLineInterface::Nested(map) => CommandLineInterface::Nested(map.insert_help()),
+ };
+
+ let (prefix, args) = prepare_cli_command(&def);
+
+ if handle_command_future(Arc::new(def), &prefix, args, rpcenv)
+ .await
+ .is_err()
+ {
+ std::process::exit(-1);
+ }
+}
+
+/// Helper to get arguments and invoke the command.
+///
+/// This is the synchrounous version of run_async_cli_command. You can
+/// pass an optional ``run`` function to execute async commands (else
+/// async commands simply fail).
+pub fn run_cli_command<C: Into<CommandLineInterface>>(
+ def: C,
+ rpcenv: CliEnvironment,
+ run: Option<fn(ApiFuture) -> Result<Value, Error>>,
+) {
+ let def = match def.into() {
+ CommandLineInterface::Simple(cli_cmd) => CommandLineInterface::Simple(cli_cmd),
+ CommandLineInterface::Nested(map) => CommandLineInterface::Nested(map.insert_help()),
+ };
+
+ let (prefix, args) = prepare_cli_command(&def);
+
+ if handle_command(Arc::new(def), &prefix, args, rpcenv, run).is_err() {
+ std::process::exit(-1);
+ }
+}
--- /dev/null
+use std::collections::HashMap;
+
+use proxmox_schema::*;
+
+use super::help_command_def;
+use super::{shellword_split_unclosed, CliCommand, CommandLineInterface, CompletionFunction};
+
+fn record_done_argument(
+ done: &mut HashMap<String, String>,
+ parameters: ParameterSchema,
+ key: &str,
+ value: &str,
+) {
+ if let Some((_, schema)) = parameters.lookup(key) {
+ match schema {
+ Schema::Array(_) => { /* do nothing ?? */ }
+ _ => {
+ done.insert(key.to_owned(), value.to_owned());
+ }
+ }
+ }
+}
+
+fn get_property_completion(
+ schema: &Schema,
+ name: &str,
+ completion_functions: &HashMap<String, CompletionFunction>,
+ arg: &str,
+ param: &HashMap<String, String>,
+) -> Vec<String> {
+ if let Some(callback) = completion_functions.get(name) {
+ let list = (callback)(arg, param);
+ let mut completions = Vec::new();
+ for value in list {
+ if value.starts_with(arg) {
+ completions.push(value);
+ }
+ }
+ return completions;
+ }
+
+ match schema {
+ Schema::String(StringSchema {
+ format: Some(format),
+ ..
+ }) => {
+ if let ApiStringFormat::Enum(variants) = format {
+ let mut completions = Vec::new();
+ for variant in variants.iter() {
+ if variant.value.starts_with(arg) {
+ completions.push(variant.value.to_string());
+ }
+ }
+ return completions;
+ }
+ }
+ Schema::Boolean(BooleanSchema { .. }) => {
+ let mut completions = Vec::new();
+ let mut lowercase_arg = arg.to_string();
+ lowercase_arg.make_ascii_lowercase();
+ for value in ["0", "1", "yes", "no", "true", "false", "on", "off"].iter() {
+ if value.starts_with(&lowercase_arg) {
+ completions.push((*value).to_string());
+ }
+ }
+ return completions;
+ }
+ Schema::Array(ArraySchema { items, .. }) => {
+ if let Schema::String(_) = items {
+ return get_property_completion(&items, name, completion_functions, arg, param);
+ }
+ }
+ _ => {}
+ }
+
+ Vec::new()
+}
+
+fn get_simple_completion(
+ cli_cmd: &CliCommand,
+ done: &mut HashMap<String, String>,
+ arg_param: &[&str], // we remove done arguments
+ args: &[String],
+) -> Vec<String> {
+ //eprintln!("COMPL: {:?} {:?} {}", arg_param, args, args.len());
+
+ if !arg_param.is_empty() {
+ let prop_name = arg_param[0];
+ if let Some((optional, schema)) = cli_cmd.info.parameters.lookup(prop_name) {
+ let is_array_param = if let Schema::Array(_) = schema {
+ true
+ } else {
+ false
+ };
+
+ if (optional || is_array_param) && args[0].starts_with('-') {
+ // argument parameter is optional (or array) , and arg
+ // looks like an option, so assume its empty and
+ // complete the rest
+ } else {
+ record_done_argument(done, cli_cmd.info.parameters, prop_name, &args[0]);
+ if args.len() > 1 {
+ if is_array_param {
+ return get_simple_completion(cli_cmd, done, &arg_param[..], &args[1..]);
+ } else {
+ return get_simple_completion(cli_cmd, done, &arg_param[1..], &args[1..]);
+ }
+ }
+
+ if args.len() == 1 {
+ return get_property_completion(
+ schema,
+ prop_name,
+ &cli_cmd.completion_functions,
+ &args[0],
+ done,
+ );
+ }
+
+ return Vec::new();
+ }
+ } else {
+ // unknown arg_param - should never happen
+ return Vec::new();
+ }
+ }
+ if args.is_empty() {
+ return Vec::new();
+ }
+
+ // Try to parse all argumnets but last, record args already done
+ if args.len() > 1 {
+ let mut errors = ParameterError::new(); // we simply ignore any parsing errors here
+ let (data, _remaining) = super::getopts::parse_argument_list(
+ &args[0..args.len() - 1],
+ cli_cmd.info.parameters,
+ &mut errors,
+ );
+ for (key, value) in &data {
+ record_done_argument(done, cli_cmd.info.parameters, key, value);
+ }
+ }
+
+ let prefix = &args[args.len() - 1]; // match on last arg
+
+ // complete option-name or option-value ?
+ if !prefix.starts_with('-') && args.len() > 1 {
+ let last = &args[args.len() - 2];
+ if last.starts_with("--") && last.len() > 2 {
+ let prop_name = &last[2..];
+ if let Some((_, schema)) = cli_cmd.info.parameters.lookup(prop_name) {
+ return get_property_completion(
+ schema,
+ prop_name,
+ &cli_cmd.completion_functions,
+ &prefix,
+ done,
+ );
+ }
+ return Vec::new();
+ }
+ }
+
+ let mut completions = Vec::new();
+ for (name, _optional, _schema) in cli_cmd.info.parameters.properties() {
+ if done.contains_key(*name) {
+ continue;
+ }
+ if cli_cmd.arg_param.contains(name) {
+ continue;
+ }
+ let option = String::from("--") + name;
+ if option.starts_with(prefix) {
+ completions.push(option);
+ }
+ }
+ completions
+}
+
+impl CommandLineInterface {
+ fn get_help_completion(&self, help_cmd: &CliCommand, args: &[String]) -> Vec<String> {
+ let mut done = HashMap::new();
+
+ match self {
+ CommandLineInterface::Simple(_) => {
+ get_simple_completion(help_cmd, &mut done, &[], args)
+ }
+ CommandLineInterface::Nested(map) => {
+ if args.is_empty() {
+ let mut completions = Vec::new();
+ for cmd in map.commands.keys() {
+ completions.push(cmd.to_string());
+ }
+ return completions;
+ }
+
+ let first = &args[0];
+ if args.len() > 1 {
+ if let Some(sub_cmd) = map.commands.get(first) {
+ // do exact match here
+ return sub_cmd.get_help_completion(help_cmd, &args[1..]);
+ }
+ return Vec::new();
+ }
+
+ if first.starts_with('-') {
+ return get_simple_completion(help_cmd, &mut done, &[], args);
+ }
+
+ let mut completions = Vec::new();
+ for cmd in map.commands.keys() {
+ if cmd.starts_with(first) {
+ completions.push(cmd.to_string());
+ }
+ }
+ completions
+ }
+ }
+ }
+
+ fn get_nested_completion(&self, args: &[String]) -> Vec<String> {
+ match self {
+ CommandLineInterface::Simple(cli_cmd) => {
+ let mut done: HashMap<String, String> = HashMap::new();
+ cli_cmd.fixed_param.iter().for_each(|(key, value)| {
+ record_done_argument(&mut done, cli_cmd.info.parameters, &key, &value);
+ });
+ get_simple_completion(cli_cmd, &mut done, &cli_cmd.arg_param, args)
+ }
+ CommandLineInterface::Nested(map) => {
+ if args.is_empty() {
+ let mut completions = Vec::new();
+ for cmd in map.commands.keys() {
+ completions.push(cmd.to_string());
+ }
+ return completions;
+ }
+ let first = &args[0];
+ if args.len() > 1 {
+ if let Some((_, sub_cmd)) = map.find_command(first) {
+ return sub_cmd.get_nested_completion(&args[1..]);
+ }
+ return Vec::new();
+ }
+ let mut completions = Vec::new();
+ for cmd in map.commands.keys() {
+ if cmd.starts_with(first) {
+ completions.push(cmd.to_string());
+ }
+ }
+ completions
+ }
+ }
+ }
+
+ /// Helper to generate bash completions.
+ ///
+ /// This helper extracts the command line from environment variable
+ /// set by ``bash``, namely ``COMP_LINE`` and ``COMP_POINT``. This is
+ /// passed to ``get_completions()``. Returned values are printed to
+ /// ``stdout``.
+ pub fn print_bash_completion(&self) {
+ let comp_point: usize = match std::env::var("COMP_POINT") {
+ Ok(val) => match usize::from_str_radix(&val, 10) {
+ Ok(i) => i,
+ Err(_) => return,
+ },
+ Err(_) => return,
+ };
+
+ let cmdline = match std::env::var("COMP_LINE") {
+ Ok(mut val) => {
+ if let Some((byte_pos, _)) = val.char_indices().nth(comp_point) {
+ val.truncate(byte_pos);
+ }
+ val
+ }
+ Err(_) => return,
+ };
+
+ let (_start, completions) = self.get_completions(&cmdline, true);
+
+ for item in completions {
+ println!("{}", item);
+ }
+ }
+
+ /// Compute possible completions for a partial command
+ pub fn get_completions(&self, line: &str, skip_first: bool) -> (usize, Vec<String>) {
+ let (mut args, start) = match shellword_split_unclosed(line, false) {
+ (mut args, None) => {
+ args.push("".into());
+ (args, line.len())
+ }
+ (mut args, Some((start, arg, _quote))) => {
+ args.push(arg);
+ (args, start)
+ }
+ };
+
+ if skip_first {
+ if args.is_empty() {
+ return (0, Vec::new());
+ }
+
+ args.remove(0); // no need for program name
+ }
+
+ let completions = if !args.is_empty() && args[0] == "help" {
+ self.get_help_completion(&help_command_def(), &args[1..])
+ } else {
+ self.get_nested_completion(&args)
+ };
+
+ (start, completions)
+ }
+}
+
+#[cfg(test)]
+mod test {
+ use anyhow::Error;
+ use serde_json::Value;
+
+ use proxmox_schema::{BooleanSchema, ObjectSchema, StringSchema};
+
+ use crate::cli::{CliCommand, CliCommandMap, CommandLineInterface};
+ use crate::{ApiHandler, ApiMethod, RpcEnvironment};
+
+ fn dummy_method(
+ _param: Value,
+ _info: &ApiMethod,
+ _rpcenv: &mut dyn RpcEnvironment,
+ ) -> Result<Value, Error> {
+ Ok(Value::Null)
+ }
+
+ const API_METHOD_SIMPLE1: ApiMethod = ApiMethod::new(
+ &ApiHandler::Sync(&dummy_method),
+ &ObjectSchema::new(
+ "Simple API method with one required and one optionl argument.",
+ &[
+ (
+ "optional-arg",
+ true,
+ &BooleanSchema::new("Optional boolean argument.")
+ .default(false)
+ .schema(),
+ ),
+ (
+ "required-arg",
+ false,
+ &StringSchema::new("Required string argument.").schema(),
+ ),
+ ],
+ ),
+ );
+
+ fn get_complex_test_cmddef() -> CommandLineInterface {
+ let sub_def = CliCommandMap::new()
+ .insert("l1c1", CliCommand::new(&API_METHOD_SIMPLE1))
+ .insert("l1c2", CliCommand::new(&API_METHOD_SIMPLE1));
+
+ let cmd_def = CliCommandMap::new()
+ .insert_help()
+ .insert("l0sub", CommandLineInterface::Nested(sub_def))
+ .insert("l0c1", CliCommand::new(&API_METHOD_SIMPLE1))
+ .insert(
+ "l0c2",
+ CliCommand::new(&API_METHOD_SIMPLE1).arg_param(&["required-arg"]),
+ )
+ .insert(
+ "l0c3",
+ CliCommand::new(&API_METHOD_SIMPLE1).arg_param(&["required-arg", "optional-arg"]),
+ );
+
+ cmd_def.into()
+ }
+
+ fn test_completions(cmd_def: &CommandLineInterface, line: &str, start: usize, expect: &[&str]) {
+ let mut expect: Vec<String> = expect.iter().map(|s| s.to_string()).collect();
+ expect.sort();
+
+ let (completion_start, mut completions) = cmd_def.get_completions(line, false);
+ completions.sort();
+
+ assert_eq!((start, expect), (completion_start, completions));
+ }
+
+ #[test]
+ fn test_nested_completion() {
+ let cmd_def = get_complex_test_cmddef();
+
+ test_completions(&cmd_def, "", 0, &["help", "l0c1", "l0c2", "l0c3", "l0sub"]);
+
+ test_completions(&cmd_def, "l0c1 ", 5, &["--optional-arg", "--required-arg"]);
+
+ test_completions(&cmd_def, "l0c1 -", 5, &["--optional-arg", "--required-arg"]);
+
+ test_completions(
+ &cmd_def,
+ "l0c1 --",
+ 5,
+ &["--optional-arg", "--required-arg"],
+ );
+
+ test_completions(&cmd_def, "l0c1 ---", 5, &[]);
+
+ test_completions(&cmd_def, "l0c1 x", 5, &[]);
+
+ test_completions(&cmd_def, "l0c1 --r", 5, &["--required-arg"]);
+
+ test_completions(&cmd_def, "l0c1 --required-arg", 5, &["--required-arg"]);
+
+ test_completions(
+ &cmd_def,
+ "l0c1 --required-arg -",
+ 20,
+ // Note: --required-arg is not finished, so it still pops up
+ &["--required-arg", "--optional-arg"],
+ );
+
+ test_completions(
+ &cmd_def,
+ "l0c1 --required-arg test -",
+ 25,
+ &["--optional-arg"],
+ );
+
+ test_completions(
+ &cmd_def,
+ "l0c1 --required-arg test --optional-arg ",
+ 40,
+ &["0", "1", "false", "no", "true", "yes", "on", "off"],
+ );
+
+ test_completions(
+ &cmd_def,
+ "l0c1 --required-arg test --optional-arg f",
+ 40,
+ &["false"],
+ );
+
+ test_completions(
+ &cmd_def,
+ "l0c1 --required-arg test --optional-arg F",
+ 40,
+ &["false"],
+ );
+
+ test_completions(
+ &cmd_def,
+ "l0c1 --required-arg test --optional-arg Yes",
+ 40,
+ &["yes"],
+ );
+ }
+
+ #[test]
+ fn test_help_completion() {
+ let cmd_def = get_complex_test_cmddef();
+
+ test_completions(&cmd_def, "h", 0, &["help"]);
+
+ test_completions(
+ &cmd_def,
+ "help ",
+ 5,
+ &["help", "l0sub", "l0c1", "l0c3", "l0c2"],
+ );
+
+ test_completions(&cmd_def, "help l0", 5, &["l0sub", "l0c1", "l0c3", "l0c2"]);
+
+ test_completions(&cmd_def, "help -", 5, &["--verbose"]);
+
+ test_completions(&cmd_def, "help l0c2", 5, &["l0c2"]);
+
+ test_completions(&cmd_def, "help l0c2 ", 10, &["--verbose"]);
+
+ test_completions(&cmd_def, "help l0c2 --verbose -", 20, &[]);
+
+ test_completions(&cmd_def, "help l0s", 5, &["l0sub"]);
+
+ test_completions(&cmd_def, "help l0sub ", 11, &["l1c1", "l1c2"]);
+
+ test_completions(&cmd_def, "help l0sub l1c2 -", 16, &["--verbose"]);
+
+ test_completions(&cmd_def, "help l0sub l1c2 --verbose -", 26, &[]);
+
+ test_completions(&cmd_def, "help l0sub l1c3", 11, &[]);
+ }
+}
--- /dev/null
+use serde_json::Value;
+
+use crate::{RpcEnvironment, RpcEnvironmentType};
+
+/// `RpcEnvironmet` implementation for command line tools
+#[derive(Default)]
+pub struct CliEnvironment {
+ result_attributes: Value,
+ auth_id: Option<String>,
+}
+
+impl CliEnvironment {
+ pub fn new() -> Self {
+ Default::default()
+ }
+}
+
+impl RpcEnvironment for CliEnvironment {
+ fn result_attrib_mut(&mut self) -> &mut Value {
+ &mut self.result_attributes
+ }
+
+ fn result_attrib(&self) -> &Value {
+ &self.result_attributes
+ }
+
+ fn env_type(&self) -> RpcEnvironmentType {
+ RpcEnvironmentType::CLI
+ }
+
+ fn set_auth_id(&mut self, auth_id: Option<String>) {
+ self.auth_id = auth_id;
+ }
+
+ fn get_auth_id(&self) -> Option<String> {
+ self.auth_id.clone()
+ }
+}
--- /dev/null
+#![allow(clippy::match_bool)] // just no...
+
+use std::collections::HashSet;
+
+use serde_json::Value;
+
+use proxmox_schema::*;
+use proxmox_schema::format::{
+ get_property_description, get_schema_type_text, DocumentationFormat, ParameterDisplayStyle,
+};
+
+use super::{value_to_text, TableFormatOptions};
+use super::{CliCommand, CliCommandMap, CommandLineInterface};
+
+/// Helper function to format and print result.
+///
+/// This is implemented for machine generatable formats 'json' and
+/// 'json-pretty'. The 'text' format needs to be handled somewhere
+/// else.
+pub fn format_and_print_result(result: &Value, output_format: &str) {
+ if output_format == "json-pretty" {
+ println!("{}", serde_json::to_string_pretty(&result).unwrap());
+ } else if output_format == "json" {
+ println!("{}", serde_json::to_string(&result).unwrap());
+ } else {
+ unimplemented!();
+ }
+}
+
+/// Helper function to format and print result.
+///
+/// This is implemented for machine generatable formats 'json' and
+/// 'json-pretty', and for the 'text' format which generates nicely
+/// formatted tables with borders.
+pub fn format_and_print_result_full(
+ result: &mut Value,
+ return_type: &ReturnType,
+ output_format: &str,
+ options: &TableFormatOptions,
+) {
+ if return_type.optional && result.is_null() {
+ return;
+ }
+
+ if output_format == "json-pretty" {
+ println!("{}", serde_json::to_string_pretty(&result).unwrap());
+ } else if output_format == "json" {
+ println!("{}", serde_json::to_string(&result).unwrap());
+ } else if output_format == "text" {
+ if let Err(err) = value_to_text(std::io::stdout(), result, &return_type.schema, options) {
+ eprintln!("unable to format result: {}", err);
+ }
+ } else {
+ eprintln!("undefined output format '{}'", output_format);
+ }
+}
+
+/// Helper to generate command usage text for simple commands.
+pub fn generate_usage_str(
+ prefix: &str,
+ cli_cmd: &CliCommand,
+ format: DocumentationFormat,
+ indent: &str,
+ skip_options: &[&str],
+) -> String {
+ let arg_param = cli_cmd.arg_param;
+ let fixed_param = &cli_cmd.fixed_param;
+ let schema = cli_cmd.info.parameters;
+
+ let mut done_hash = HashSet::<&str>::new();
+ for option in skip_options {
+ done_hash.insert(option);
+ }
+
+ let mut args = String::new();
+
+ for positional_arg in arg_param {
+ match schema.lookup(positional_arg) {
+ Some((optional, param_schema)) => {
+ args.push(' ');
+
+ let is_array = matches!(param_schema, Schema::Array(_));
+ if optional {
+ args.push('[');
+ }
+ if is_array {
+ args.push('{');
+ }
+ args.push('<');
+ args.push_str(positional_arg);
+ args.push('>');
+ if is_array {
+ args.push('}');
+ }
+ if optional {
+ args.push(']');
+ }
+
+ done_hash.insert(positional_arg);
+ }
+ None => panic!("no such property '{}' in schema", positional_arg),
+ }
+ }
+
+ let mut arg_descr = String::new();
+ for positional_arg in arg_param {
+ let (_optional, param_schema) = schema.lookup(positional_arg).unwrap();
+ let param_descr = get_property_description(
+ positional_arg,
+ param_schema,
+ ParameterDisplayStyle::Fixed,
+ format,
+ );
+ arg_descr.push_str(¶m_descr);
+ }
+
+ let mut options = String::new();
+
+ for (prop, optional, param_schema) in schema.properties() {
+ if done_hash.contains(prop) {
+ continue;
+ }
+ if fixed_param.contains_key(prop) {
+ continue;
+ }
+
+ let type_text = get_schema_type_text(param_schema, ParameterDisplayStyle::Arg);
+
+ let prop_descr =
+ get_property_description(prop, param_schema, ParameterDisplayStyle::Arg, format);
+
+ if *optional {
+ options.push_str(&prop_descr);
+ } else {
+ args.push_str(" --");
+ args.push_str(prop);
+ args.push(' ');
+ args.push_str(&type_text);
+
+ arg_descr.push_str(&prop_descr);
+ }
+
+ done_hash.insert(prop);
+ }
+
+ let option_indicator = if !options.is_empty() {
+ " [OPTIONS]"
+ } else {
+ ""
+ };
+
+ let mut text = match format {
+ DocumentationFormat::Short => {
+ return format!("{}{}{}{}\n", indent, prefix, args, option_indicator);
+ }
+ DocumentationFormat::Long => format!("{}{}{}{}\n", indent, prefix, args, option_indicator),
+ DocumentationFormat::Full => format!(
+ "{}{}{}{}\n\n{}\n\n",
+ indent,
+ prefix,
+ args,
+ option_indicator,
+ schema.description()
+ ),
+ DocumentationFormat::ReST => format!(
+ "``{}{}{}``\n\n{}\n\n",
+ prefix,
+ args,
+ option_indicator,
+ schema.description()
+ ),
+ };
+
+ if !arg_descr.is_empty() {
+ text.push_str(&arg_descr);
+ }
+
+ if !options.is_empty() {
+ text.push_str("Optional parameters:\n\n");
+ text.push_str(&options);
+ }
+ text
+}
+
+/// Print command usage for simple commands to ``stderr``.
+pub fn print_simple_usage_error(prefix: &str, cli_cmd: &CliCommand, err_msg: &str) {
+ let usage = generate_usage_str(prefix, cli_cmd, DocumentationFormat::Long, "", &[]);
+ eprint!("Error: {}\nUsage: {}", err_msg, usage);
+}
+
+/// Print command usage for nested commands to ``stderr``.
+pub fn print_nested_usage_error(prefix: &str, def: &CliCommandMap, err_msg: &str) {
+ let usage = generate_nested_usage(prefix, def, DocumentationFormat::Short);
+ eprintln!("Error: {}\n\nUsage:\n\n{}", err_msg, usage);
+}
+
+/// Helper to generate command usage text for nested commands.
+pub fn generate_nested_usage(
+ prefix: &str,
+ def: &CliCommandMap,
+ format: DocumentationFormat,
+) -> String {
+ let mut cmds: Vec<&String> = def.commands.keys().collect();
+ cmds.sort();
+
+ let skip_options = def.usage_skip_options;
+
+ let mut usage = String::new();
+
+ for cmd in cmds {
+ let new_prefix = if prefix.is_empty() {
+ String::from(cmd)
+ } else {
+ format!("{} {}", prefix, cmd)
+ };
+
+ match def.commands.get(cmd).unwrap() {
+ CommandLineInterface::Simple(cli_cmd) => {
+ if !usage.is_empty() && format == DocumentationFormat::ReST {
+ usage.push_str("----\n\n");
+ }
+ usage.push_str(&generate_usage_str(
+ &new_prefix,
+ cli_cmd,
+ format,
+ "",
+ skip_options,
+ ));
+ }
+ CommandLineInterface::Nested(map) => {
+ usage.push_str(&generate_nested_usage(&new_prefix, map, format));
+ }
+ }
+ }
+
+ usage
+}
+
+/// Print help text to ``stderr``.
+pub fn print_help(
+ top_def: &CommandLineInterface,
+ mut prefix: String,
+ args: &[String],
+ mut verbose: Option<bool>,
+) {
+ let mut iface = top_def;
+
+ for cmd in args {
+ if let CommandLineInterface::Nested(map) = iface {
+ if let Some((full_name, subcmd)) = map.find_command(cmd) {
+ iface = subcmd;
+ if !prefix.is_empty() {
+ prefix.push(' ');
+ }
+ prefix.push_str(&full_name);
+ continue;
+ }
+ }
+ if prefix.is_empty() {
+ eprintln!("no such command '{}'", cmd);
+ } else {
+ eprintln!("no such command '{} {}'", prefix, cmd);
+ }
+ return;
+ }
+
+ if verbose.is_none() {
+ if let CommandLineInterface::Simple(_) = iface {
+ verbose = Some(true);
+ }
+ }
+
+ let format = match verbose.unwrap_or(false) {
+ true => DocumentationFormat::Full,
+ false => DocumentationFormat::Short,
+ };
+
+ match iface {
+ CommandLineInterface::Nested(map) => {
+ println!("Usage:\n\n{}", generate_nested_usage(&prefix, map, format));
+ }
+ CommandLineInterface::Simple(cli_cmd) => {
+ println!(
+ "Usage: {}",
+ generate_usage_str(&prefix, cli_cmd, format, "", &[])
+ );
+ }
+ }
+}
--- /dev/null
+use std::collections::HashMap;
+
+use anyhow::format_err;
+use serde_json::Value;
+
+use proxmox_schema::*;
+
+#[derive(Debug)]
+enum RawArgument {
+ Separator,
+ Argument { value: String },
+ Option { name: String, value: Option<String> },
+}
+
+fn parse_argument(arg: &str) -> RawArgument {
+ let bytes = arg.as_bytes();
+
+ let length = bytes.len();
+
+ if length < 2 || bytes[0] != b'-' {
+ return RawArgument::Argument {
+ value: arg.to_string(),
+ };
+ }
+
+ let first = if bytes[1] == b'-' {
+ if length == 2 {
+ return RawArgument::Separator;
+ }
+ 2
+ } else {
+ 1
+ };
+
+ if let Some(i) = bytes[first..length].iter().position(|b| *b == b'=') {
+ let start = i + first;
+ // Since we take a &str, we know the contents of it are valid utf8.
+ // Since bytes[start] == b'=', we know the byte beginning at start is a single-byte
+ // code pointer. We also know that 'first' points exactly after a single-byte code
+ // point as it points to the first byte after a hyphen.
+ // Therefore we know arg[first..start] is valid utf-8, therefore it is safe to use
+ // get_unchecked() to speed things up.
+ return RawArgument::Option {
+ name: unsafe { arg.get_unchecked(first..start).to_string() },
+ value: Some(unsafe { arg.get_unchecked((start + 1)..).to_string() }),
+ };
+ }
+
+ RawArgument::Option {
+ name: unsafe { arg.get_unchecked(first..).to_string() },
+ value: None,
+ }
+}
+
+/// parse as many arguments as possible into a Vec<String, String>. This does not
+/// verify the schema.
+/// Returns parsed data and the remaining arguments as two separate array
+pub(crate) fn parse_argument_list<T: AsRef<str>>(
+ args: &[T],
+ schema: ParameterSchema,
+ errors: &mut ParameterError,
+) -> (Vec<(String, String)>, Vec<String>) {
+ let mut data: Vec<(String, String)> = vec![];
+ let mut remaining: Vec<String> = vec![];
+
+ let mut pos = 0;
+
+ while pos < args.len() {
+ match parse_argument(args[pos].as_ref()) {
+ RawArgument::Separator => {
+ break;
+ }
+ RawArgument::Option { name, value } => match value {
+ None => {
+ let mut want_bool = false;
+ let mut can_default = false;
+ if let Some((_optional, param_schema)) = schema.lookup(&name) {
+ if let Schema::Boolean(boolean_schema) = param_schema {
+ want_bool = true;
+ match boolean_schema.default {
+ Some(false) | None => can_default = true,
+ Some(true) => (),
+ }
+ }
+ }
+
+ let mut next_is_argument = false;
+ let mut next_is_bool = false;
+
+ if (pos + 1) < args.len() {
+ let next = args[pos + 1].as_ref();
+ if let RawArgument::Argument { .. } = parse_argument(next) {
+ next_is_argument = true;
+ if parse_boolean(next).is_ok() {
+ next_is_bool = true;
+ }
+ }
+ }
+
+ if want_bool {
+ if next_is_bool {
+ pos += 1;
+ data.push((name, args[pos].as_ref().to_string()));
+ } else if can_default {
+ data.push((name, "true".to_string()));
+ } else {
+ errors.push(name.to_string(), format_err!("missing boolean value."));
+ }
+ } else if next_is_argument {
+ pos += 1;
+ data.push((name, args[pos].as_ref().to_string()));
+ } else {
+ errors.push(name.to_string(), format_err!("missing parameter value."));
+ }
+ }
+ Some(v) => {
+ data.push((name, v));
+ }
+ },
+ RawArgument::Argument { value } => {
+ remaining.push(value);
+ }
+ }
+
+ pos += 1;
+ }
+
+ remaining.reserve(args.len() - pos);
+ for i in &args[pos..] {
+ remaining.push(i.as_ref().to_string());
+ }
+
+ (data, remaining)
+}
+
+/// Parses command line arguments using a `Schema`
+///
+/// Returns parsed options as json object, together with the
+/// list of additional command line arguments.
+pub fn parse_arguments<T: AsRef<str>>(
+ args: &[T],
+ arg_param: &[&str],
+ fixed_param: &HashMap<&'static str, String>,
+ schema: ParameterSchema,
+) -> Result<(Value, Vec<String>), ParameterError> {
+ let mut errors = ParameterError::new();
+
+ // first check if all arg_param exists in schema
+
+ let mut last_arg_param_is_optional = false;
+ let mut last_arg_param_is_array = false;
+
+ for i in 0..arg_param.len() {
+ let name = arg_param[i];
+ if let Some((optional, param_schema)) = schema.lookup(&name) {
+ if i == arg_param.len() - 1 {
+ last_arg_param_is_optional = optional;
+ if let Schema::Array(_) = param_schema {
+ last_arg_param_is_array = true;
+ }
+ } else if optional {
+ panic!("positional argument '{}' may not be optional", name);
+ }
+ } else {
+ panic!("no such property '{}' in schema", name);
+ }
+ }
+
+ let (mut data, mut remaining) = parse_argument_list(args, schema, &mut errors);
+
+ for i in 0..arg_param.len() {
+ let name = arg_param[i];
+ let is_last_arg_param = i == (arg_param.len() - 1);
+
+ if remaining.is_empty() {
+ if !(is_last_arg_param && last_arg_param_is_optional) {
+ errors.push(name.to_string(), format_err!("missing argument"));
+ }
+ } else if is_last_arg_param && last_arg_param_is_array {
+ for value in remaining {
+ data.push((name.to_string(), value));
+ }
+ remaining = vec![];
+ } else {
+ data.push((name.to_string(), remaining.remove(0)));
+ }
+ }
+
+ if !errors.is_empty() {
+ return Err(errors);
+ }
+
+ for (name, value) in fixed_param.iter() {
+ data.push((name.to_string(), value.to_string()));
+ }
+
+ let options = parse_parameter_strings(&data, schema, true)?;
+
+ Ok((options, remaining))
+}
+
+#[test]
+fn test_boolean_arg() {
+ const PARAMETERS: ObjectSchema = ObjectSchema::new(
+ "Parameters:",
+ &[("enable", false, &BooleanSchema::new("Enable").schema())],
+ );
+
+ let mut variants: Vec<(Vec<&str>, bool)> = vec![];
+ variants.push((vec!["-enable"], true));
+ variants.push((vec!["-enable=1"], true));
+ variants.push((vec!["-enable", "yes"], true));
+ variants.push((vec!["-enable", "Yes"], true));
+ variants.push((vec!["--enable", "1"], true));
+ variants.push((vec!["--enable", "ON"], true));
+ variants.push((vec!["--enable", "true"], true));
+
+ variants.push((vec!["--enable", "0"], false));
+ variants.push((vec!["--enable", "no"], false));
+ variants.push((vec!["--enable", "off"], false));
+ variants.push((vec!["--enable", "false"], false));
+
+ for (args, expect) in variants {
+ let res = parse_arguments(
+ &args,
+ &vec![],
+ &HashMap::new(),
+ ParameterSchema::from(&PARAMETERS),
+ );
+ assert!(res.is_ok());
+ if let Ok((options, remaining)) = res {
+ assert!(options["enable"] == expect);
+ assert!(remaining.len() == 0);
+ }
+ }
+}
+
+#[test]
+fn test_argument_paramenter() {
+ use proxmox_schema::*;
+
+ const PARAMETERS: ObjectSchema = ObjectSchema::new(
+ "Parameters:",
+ &[
+ ("enable", false, &BooleanSchema::new("Enable.").schema()),
+ ("storage", false, &StringSchema::new("Storage.").schema()),
+ ],
+ );
+
+ let args = vec!["-enable", "local"];
+ let res = parse_arguments(
+ &args,
+ &vec!["storage"],
+ &HashMap::new(),
+ ParameterSchema::from(&PARAMETERS),
+ );
+ assert!(res.is_ok());
+ if let Ok((options, remaining)) = res {
+ assert!(options["enable"] == true);
+ assert!(options["storage"] == "local");
+ assert!(remaining.len() == 0);
+ }
+}
--- /dev/null
+//! Tools to create command line parsers
+//!
+//! This crate provides convenient helpers to create command line
+//! parsers using Schema definitions.
+//!
+//! ## Features
+//!
+//! - Use declarative API schema to define the CLI
+//! - Automatic parameter verification
+//! - Automatically generate documentation and manual pages
+//! - Automatically generate bash completion helpers
+//! - Ability to create interactive commands (using ``rustyline``)
+//! - Supports complex/nested commands
+
+use std::collections::HashMap;
+
+use crate::ApiMethod;
+
+mod environment;
+pub use environment::*;
+
+mod shellword;
+pub use shellword::*;
+
+mod format;
+pub use format::*;
+
+mod text_table;
+pub use text_table::*;
+
+mod completion;
+pub use completion::*;
+
+mod getopts;
+pub use getopts::*;
+
+mod command;
+pub use command::*;
+
+mod readline;
+pub use readline::*;
+
+/// Completion function for single parameters.
+///
+/// Completion functions gets the current parameter value, and should
+/// return a list of all possible values.
+pub type CompletionFunction = fn(&str, &HashMap<String, String>) -> Vec<String>;
+
+/// Define a simple CLI command.
+pub struct CliCommand {
+ /// The Schema definition.
+ pub info: &'static ApiMethod,
+ /// Argument parameter list.
+ ///
+ /// Those parameters are expected to be passed as command line
+ /// arguments in the specified order. All other parameters needs
+ /// to be specified as ``--option <value>`` pairs.
+ pub arg_param: &'static [&'static str],
+ /// Predefined parameters.
+ pub fixed_param: HashMap<&'static str, String>,
+ /// Completion functions.
+ ///
+ /// Each parameter may have an associated completion function,
+ /// which is called by the shell completion handler.
+ pub completion_functions: HashMap<String, CompletionFunction>,
+}
+
+impl CliCommand {
+ /// Create a new instance.
+ pub fn new(info: &'static ApiMethod) -> Self {
+ Self {
+ info,
+ arg_param: &[],
+ fixed_param: HashMap::new(),
+ completion_functions: HashMap::new(),
+ }
+ }
+
+ /// Set argument parameter list.
+ pub fn arg_param(mut self, names: &'static [&'static str]) -> Self {
+ self.arg_param = names;
+ self
+ }
+
+ /// Set fixed parameters.
+ pub fn fixed_param(mut self, key: &'static str, value: String) -> Self {
+ self.fixed_param.insert(key, value);
+ self
+ }
+
+ /// Set completion functions.
+ pub fn completion_cb(mut self, param_name: &str, cb: CompletionFunction) -> Self {
+ self.completion_functions.insert(param_name.into(), cb);
+ self
+ }
+}
+
+/// Define nested CLI commands.
+#[derive(Default)]
+pub struct CliCommandMap {
+ /// Each command has an unique name. The map associates names with
+ /// command definitions.
+ pub commands: HashMap<String, CommandLineInterface>,
+ pub aliases: Vec<(Vec<&'static str>, Vec<&'static str>)>,
+ /// List of options to suppress in generate_usage
+ pub usage_skip_options: &'static [&'static str],
+}
+
+impl CliCommandMap {
+ /// Create a new instance.
+ pub fn new() -> Self {
+ Default::default()
+ }
+
+ /// Insert another command.
+ pub fn insert<C: Into<CommandLineInterface>>(mut self, name: &'static str, cli: C) -> Self {
+ self.commands.insert(name.into(), cli.into());
+ self
+ }
+
+ pub fn alias(mut self, old: &'static [&'static str], new: &'static [&'static str]) -> Self {
+ self.aliases.push((Vec::from(old), Vec::from(new)));
+ self
+ }
+
+ pub fn usage_skip_options(mut self, list: &'static [&'static str]) -> Self {
+ self.usage_skip_options = list;
+ self
+ }
+
+ /// Insert the help command.
+ pub fn insert_help(mut self) -> Self {
+ self.commands
+ .insert(String::from("help"), help_command_def().into());
+ self
+ }
+
+ fn find_command(&self, name: &str) -> Option<(String, &CommandLineInterface)> {
+ if let Some(sub_cmd) = self.commands.get(name) {
+ return Some((name.to_string(), sub_cmd));
+ };
+
+ let mut matches: Vec<&str> = vec![];
+
+ for cmd in self.commands.keys() {
+ if cmd.starts_with(name) {
+ matches.push(cmd);
+ }
+ }
+
+ if matches.len() != 1 {
+ return None;
+ }
+
+ if let Some(sub_cmd) = self.commands.get(matches[0]) {
+ return Some((matches[0].to_string(), sub_cmd));
+ };
+
+ None
+ }
+}
+
+/// Define Complex command line interfaces.
+pub enum CommandLineInterface {
+ Simple(CliCommand),
+ Nested(CliCommandMap),
+}
+
+impl From<CliCommand> for CommandLineInterface {
+ fn from(cli_cmd: CliCommand) -> Self {
+ CommandLineInterface::Simple(cli_cmd)
+ }
+}
+
+impl From<CliCommandMap> for CommandLineInterface {
+ fn from(list: CliCommandMap) -> Self {
+ CommandLineInterface::Nested(list)
+ }
+}
--- /dev/null
+use std::sync::Arc;
+
+use super::*;
+
+/// Helper trait implementation for ``rustyline``.
+///
+/// This can be used to generate interactive commands using
+/// ``rustyline`` (readline implementation).
+///
+pub struct CliHelper {
+ cmd_def: Arc<CommandLineInterface>,
+}
+
+impl CliHelper {
+ pub fn new(cmd_def: CommandLineInterface) -> Self {
+ Self {
+ cmd_def: Arc::new(cmd_def),
+ }
+ }
+
+ pub fn cmd_def(&self) -> Arc<CommandLineInterface> {
+ self.cmd_def.clone()
+ }
+}
+
+impl rustyline::completion::Completer for CliHelper {
+ type Candidate = String;
+
+ fn complete(
+ &self,
+ line: &str,
+ pos: usize,
+ _ctx: &rustyline::Context<'_>,
+ ) -> rustyline::Result<(usize, Vec<Self::Candidate>)> {
+ let line = &line[..pos];
+
+ let (start, completions) = self.cmd_def.get_completions(line, false);
+
+ Ok((start, completions))
+ }
+}
+
+impl rustyline::hint::Hinter for CliHelper {
+ type Hint = String;
+}
+impl rustyline::validate::Validator for CliHelper {}
+impl rustyline::highlight::Highlighter for CliHelper {}
+impl rustyline::Helper for CliHelper {}
--- /dev/null
+use anyhow::*;
+
+/// Shell quote type
+pub use rustyline::completion::Quote;
+
+#[derive(PartialEq)]
+enum ParseMode {
+ Space,
+ DoubleQuote,
+ EscapeNormal,
+ EscapeInDoubleQuote,
+ Normal,
+ SingleQuote,
+}
+
+/// Parsing strings as they would be interpreted by the UNIX Bourne shell.
+///
+/// - ``finalize``: assume this is a complete command line. Set this
+/// to false for the 'completion' helper, which needs to get
+/// information about the last unfinished parameter.
+///
+/// Returns the list of fully parsed words (unescaped and quotes
+/// removed). If there are unclosed quotes, the start of that
+/// parameter, the parameter value (unescaped and quotes removed), and
+/// the quote type are returned.
+pub fn shellword_split_unclosed(
+ s: &str,
+ finalize: bool,
+) -> (Vec<String>, Option<(usize, String, Quote)>) {
+ let char_indices = s.char_indices();
+ let mut args: Vec<String> = Vec::new();
+ let mut field_start = None;
+ let mut field = String::new();
+ let mut mode = ParseMode::Space;
+
+ let space_chars = [' ', '\t', '\n'];
+
+ for (index, c) in char_indices {
+ match mode {
+ ParseMode::Space => match c {
+ '"' => {
+ mode = ParseMode::DoubleQuote;
+ field_start = Some((index, Quote::Double));
+ }
+ '\\' => {
+ mode = ParseMode::EscapeNormal;
+ field_start = Some((index, Quote::None));
+ }
+ '\'' => {
+ mode = ParseMode::SingleQuote;
+ field_start = Some((index, Quote::Single));
+ }
+ c if space_chars.contains(&c) => (), // skip space
+ c => {
+ mode = ParseMode::Normal;
+ field_start = Some((index, Quote::None));
+ field.push(c);
+ }
+ },
+ ParseMode::EscapeNormal => {
+ mode = ParseMode::Normal;
+ field.push(c);
+ }
+ ParseMode::EscapeInDoubleQuote => {
+ // Within double quoted strings, backslashes are only
+ // treated as metacharacters when followed by one of
+ // the following characters: $ ' " \ newline
+ match c {
+ '$' | '\'' | '"' | '\\' | '\n' => (),
+ _ => field.push('\\'),
+ }
+ field.push(c);
+ mode = ParseMode::DoubleQuote;
+ }
+ ParseMode::Normal => match c {
+ '"' => mode = ParseMode::DoubleQuote,
+ '\'' => mode = ParseMode::SingleQuote,
+ '\\' => mode = ParseMode::EscapeNormal,
+ c if space_chars.contains(&c) => {
+ mode = ParseMode::Space;
+ let (_start, _quote) = field_start.take().unwrap();
+ args.push(field.split_off(0));
+ }
+ c => field.push(c), // continue
+ },
+ ParseMode::DoubleQuote => match c {
+ '"' => mode = ParseMode::Normal,
+ '\\' => mode = ParseMode::EscapeInDoubleQuote,
+ c => field.push(c), // continue
+ },
+ ParseMode::SingleQuote => match c {
+ // Note: no escape in single quotes
+ '\'' => mode = ParseMode::Normal,
+ c => field.push(c), // continue
+ },
+ }
+ }
+
+ if finalize && mode == ParseMode::Normal {
+ let (_start, _quote) = field_start.take().unwrap();
+ args.push(field.split_off(0));
+ }
+
+ match field_start {
+ Some((start, quote)) => (args, Some((start, field, quote))),
+ None => (args, None),
+ }
+}
+
+/// Splits a string into a vector of words in the same way the UNIX Bourne shell does.
+///
+/// Return words unescaped and without quotes.
+pub fn shellword_split(s: &str) -> Result<Vec<String>, Error> {
+ let (args, unclosed_field) = shellword_split_unclosed(s, true);
+ if unclosed_field.is_some() {
+ bail!("shellword split failed - found unclosed quote.");
+ }
+ Ok(args)
+}
+
+#[test]
+fn test_shellword_split() {
+ let expect = ["ls", "/etc"];
+ let expect: Vec<String> = expect.iter().map(|v| v.to_string()).collect();
+
+ assert_eq!(expect, shellword_split("ls /etc").unwrap());
+ assert_eq!(expect, shellword_split("ls \"/etc\"").unwrap());
+ assert_eq!(expect, shellword_split("ls '/etc'").unwrap());
+ assert_eq!(expect, shellword_split("ls '/etc'").unwrap());
+
+ assert_eq!(expect, shellword_split("ls /e\"t\"c").unwrap());
+ assert_eq!(expect, shellword_split("ls /e'tc'").unwrap());
+ assert_eq!(expect, shellword_split("ls /e't''c'").unwrap());
+
+ let expect = ["ls", "/etc 08x"];
+ let expect: Vec<String> = expect.iter().map(|v| v.to_string()).collect();
+ assert_eq!(expect, shellword_split("ls /etc\\ \\08x").unwrap());
+
+ let expect = ["ls", "/etc \\08x"];
+ let expect: Vec<String> = expect.iter().map(|v| v.to_string()).collect();
+ assert_eq!(expect, shellword_split("ls \"/etc \\08x\"").unwrap());
+}
+
+#[test]
+fn test_shellword_split_unclosed() {
+ let expect = ["ls".to_string()].to_vec();
+ assert_eq!(
+ (
+ expect,
+ Some((3, "./File1 name with spaces".to_string(), Quote::Single))
+ ),
+ shellword_split_unclosed("ls './File1 name with spaces", false)
+ );
+
+ let expect = ["ls".to_string()].to_vec();
+ assert_eq!(
+ (
+ expect,
+ Some((3, "./File2 name with spaces".to_string(), Quote::Double))
+ ),
+ shellword_split_unclosed("ls \"./File2 \"name\" with spaces", false)
+ );
+}
--- /dev/null
+use std::io::Write;
+
+use anyhow::{bail, Error};
+use serde_json::Value;
+use unicode_width::UnicodeWidthStr;
+
+use proxmox_lang::c_str;
+use proxmox_schema::{ObjectSchemaType, Schema, SchemaPropertyEntry};
+
+/// allows to configure the default output fromat using environment vars
+pub const ENV_VAR_PROXMOX_OUTPUT_FORMAT: &str = "PROXMOX_OUTPUT_FORMAT";
+/// if set, supress borders (and headers) when printing tables
+pub const ENV_VAR_PROXMOX_OUTPUT_NO_BORDER: &str = "PROXMOX_OUTPUT_NO_BORDER";
+/// if set, supress headers when printing tables
+pub const ENV_VAR_PROXMOX_OUTPUT_NO_HEADER: &str = "PROXMOX_OUTPUT_NO_HEADER";
+
+/// Helper to get output format from parameters or environment
+pub fn get_output_format(param: &Value) -> String {
+ let mut output_format = None;
+
+ if let Some(format) = param["output-format"].as_str() {
+ output_format = Some(format.to_owned());
+ } else if let Ok(format) = std::env::var(ENV_VAR_PROXMOX_OUTPUT_FORMAT) {
+ output_format = Some(format);
+ }
+
+ output_format.unwrap_or_else(|| String::from("text"))
+}
+
+/// Helper to get output format from parameters or environment
+/// and removing from parameters
+pub fn extract_output_format(param: &mut Value) -> String {
+ let output_format = get_output_format(param);
+
+ if let Some(param) = param.as_object_mut() {
+ param.remove("output-format");
+ }
+
+ output_format
+}
+
+/// Helper to get TableFormatOptions with default from environment
+pub fn default_table_format_options() -> TableFormatOptions {
+ let no_border = std::env::var(ENV_VAR_PROXMOX_OUTPUT_NO_BORDER)
+ .ok()
+ .is_some();
+ let no_header = std::env::var(ENV_VAR_PROXMOX_OUTPUT_NO_HEADER)
+ .ok()
+ .is_some();
+
+ TableFormatOptions::new()
+ .noborder(no_border)
+ .noheader(no_header)
+}
+
+/// Render function
+///
+/// Should convert the json `value` into a text string. `record` points to
+/// the surrounding data object.
+pub type RenderFunction =
+ fn(/* value: */ &Value, /* record: */ &Value) -> Result<String, Error>;
+
+fn data_to_text(data: &Value, schema: &Schema) -> Result<String, Error> {
+ if data.is_null() {
+ return Ok(String::new());
+ }
+
+ match schema {
+ Schema::Null => {
+ // makes no sense to display Null columns
+ bail!("internal error");
+ }
+ Schema::Boolean(_) => match data.as_bool() {
+ Some(value) => Ok(String::from(if value { "1" } else { "0" })),
+ None => bail!("got unexpected data (expected bool)."),
+ },
+ Schema::Integer(_) => match data.as_i64() {
+ Some(value) => Ok(format!("{}", value)),
+ None => bail!("got unexpected data (expected integer)."),
+ },
+ Schema::Number(_) => match data.as_f64() {
+ Some(value) => Ok(format!("{}", value)),
+ None => bail!("got unexpected data (expected number)."),
+ },
+ Schema::String(_) => match data.as_str() {
+ Some(value) => Ok(value.to_string()),
+ None => bail!("got unexpected data (expected string)."),
+ },
+ Schema::Object(_) => Ok(data.to_string()),
+ Schema::Array(_) => Ok(data.to_string()),
+ Schema::AllOf(_) => Ok(data.to_string()),
+ }
+}
+
+struct TableBorders {
+ column_separator: char,
+ top: String,
+ head: String,
+ middle: String,
+ bottom: String,
+}
+
+impl TableBorders {
+ fn new<I>(column_widths: I, ascii_delimiters: bool) -> Self
+ where
+ I: Iterator<Item = usize>,
+ {
+ let mut top = String::new();
+ let mut head = String::new();
+ let mut middle = String::new();
+ let mut bottom = String::new();
+
+ let column_separator = if ascii_delimiters { '|' } else { '│' };
+
+ for (i, column_width) in column_widths.enumerate() {
+ if ascii_delimiters {
+ top.push('+');
+ head.push('+');
+ middle.push('+');
+ bottom.push('+');
+ } else if i == 0 {
+ top.push('┌');
+ head.push('╞');
+ middle.push('├');
+ bottom.push('└');
+ } else {
+ top.push('┬');
+ head.push('╪');
+ middle.push('┼');
+ bottom.push('┴');
+ }
+
+ for _j in 0..(column_width + 2) {
+ if ascii_delimiters {
+ top.push('=');
+ head.push('=');
+ middle.push('-');
+ bottom.push('=');
+ } else {
+ top.push('─');
+ head.push('═');
+ middle.push('─');
+ bottom.push('─');
+ }
+ }
+ }
+ if ascii_delimiters {
+ top.push('+');
+ head.push('+');
+ middle.push('+');
+ bottom.push('+');
+ } else {
+ top.push('┐');
+ head.push('╡');
+ middle.push('┤');
+ bottom.push('┘');
+ }
+
+ Self {
+ column_separator,
+ top,
+ head,
+ middle,
+ bottom,
+ }
+ }
+}
+
+/// Table Column configuration
+///
+/// This structure can be used to set additional rendering information for a table column.
+pub struct ColumnConfig {
+ pub name: String,
+ pub header: Option<String>,
+ pub right_align: Option<bool>,
+ pub renderer: Option<RenderFunction>,
+}
+
+impl ColumnConfig {
+ pub fn new(name: &str) -> Self {
+ Self {
+ name: name.to_string(),
+ header: None,
+ right_align: None,
+ renderer: None,
+ }
+ }
+
+ pub fn right_align(mut self, right_align: bool) -> Self {
+ self.right_align = Some(right_align);
+ self
+ }
+
+ pub fn renderer(mut self, renderer: RenderFunction) -> Self {
+ self.renderer = Some(renderer);
+ self
+ }
+
+ pub fn header<S: Into<String>>(mut self, header: S) -> Self {
+ self.header = Some(header.into());
+ self
+ }
+}
+
+/// Get the current size of the terminal (for stdout).
+/// # Safety
+///
+/// uses unsafe call to tty_ioctl, see man tty_ioctl(2).
+fn stdout_terminal_size() -> (usize, usize) {
+ let mut winsize = libc::winsize {
+ ws_row: 0,
+ ws_col: 0,
+ ws_xpixel: 0,
+ ws_ypixel: 0,
+ };
+ unsafe { libc::ioctl(libc::STDOUT_FILENO, libc::TIOCGWINSZ, &mut winsize) };
+ (winsize.ws_row as usize, winsize.ws_col as usize)
+}
+
+/// Table formatter configuration
+#[derive(Default)]
+pub struct TableFormatOptions {
+ /// Can be used to sort after a specific columns, if it isn't set
+ /// we sort after the leftmost column (with no undef value in
+ /// $data) this can be turned off by passing and empty array. The
+ /// boolean argument specifies the sort order (false => ASC, true => DESC)
+ pub sortkeys: Option<Vec<(String, bool)>>,
+ /// Print without asciiart border.
+ pub noborder: bool,
+ /// Print without table header.
+ pub noheader: bool,
+ /// Limit output width.
+ pub columns: Option<usize>,
+ /// Use ascii characters for table delimiters (instead of utf8).
+ pub ascii_delimiters: bool,
+ /// Comumn configurations
+ pub column_config: Vec<ColumnConfig>,
+}
+
+impl TableFormatOptions {
+ /// Create a new Instance with reasonable defaults for terminal output
+ ///
+ /// This tests if stdout is a TTY and sets the columns to the terminal width,
+ /// and sets ascii_delimiters to true If the locale CODESET is not UTF-8.
+ pub fn new() -> Self {
+ let mut me = Self::default();
+
+ let is_tty = unsafe { libc::isatty(libc::STDOUT_FILENO) == 1 };
+
+ if is_tty {
+ let (_rows, columns) = stdout_terminal_size();
+ if columns > 0 {
+ me.columns = Some(columns);
+ }
+ }
+
+ let empty_cstr = c_str!("");
+
+ use std::ffi::CStr;
+ let encoding = unsafe {
+ libc::setlocale(libc::LC_CTYPE, empty_cstr.as_ptr());
+ CStr::from_ptr(libc::nl_langinfo(libc::CODESET))
+ };
+
+ if encoding != c_str!("UTF-8") {
+ me.ascii_delimiters = true;
+ }
+
+ me
+ }
+
+ pub fn disable_sort(mut self) -> Self {
+ self.sortkeys = Some(Vec::new());
+ self
+ }
+
+ pub fn sortby<S: Into<String>>(mut self, key: S, sort_desc: bool) -> Self {
+ let key = key.into();
+ match self.sortkeys {
+ None => {
+ let mut list = Vec::new();
+ list.push((key, sort_desc));
+ self.sortkeys = Some(list);
+ }
+ Some(ref mut list) => {
+ list.push((key, sort_desc));
+ }
+ }
+ self
+ }
+
+ pub fn noborder(mut self, noborder: bool) -> Self {
+ self.noborder = noborder;
+ self
+ }
+
+ pub fn noheader(mut self, noheader: bool) -> Self {
+ self.noheader = noheader;
+ self
+ }
+
+ pub fn ascii_delimiters(mut self, ascii_delimiters: bool) -> Self {
+ self.ascii_delimiters = ascii_delimiters;
+ self
+ }
+
+ pub fn columns(mut self, columns: Option<usize>) -> Self {
+ self.columns = columns;
+ self
+ }
+
+ pub fn column_config(mut self, column_config: Vec<ColumnConfig>) -> Self {
+ self.column_config = column_config;
+ self
+ }
+
+ /// Add a single column configuration
+ pub fn column(mut self, column_config: ColumnConfig) -> Self {
+ self.column_config.push(column_config);
+ self
+ }
+
+ fn lookup_column_info(
+ &self,
+ column_name: &str,
+ ) -> (String, Option<bool>, Option<RenderFunction>) {
+ let mut renderer = None;
+
+ let header;
+ let mut right_align = None;
+
+ if let Some(column_config) = self.column_config.iter().find(|v| v.name == *column_name) {
+ renderer = column_config.renderer;
+ right_align = column_config.right_align;
+ if let Some(ref h) = column_config.header {
+ header = h.to_owned();
+ } else {
+ header = column_name.to_string();
+ }
+ } else {
+ header = column_name.to_string();
+ }
+
+ (header, right_align, renderer)
+ }
+}
+
+struct TableCell {
+ lines: Vec<String>,
+}
+
+struct TableColumn {
+ cells: Vec<TableCell>,
+ width: usize,
+ right_align: bool,
+}
+
+fn format_table<W: Write>(
+ output: W,
+ list: &mut Vec<Value>,
+ schema: &dyn ObjectSchemaType,
+ options: &TableFormatOptions,
+) -> Result<(), Error> {
+ let properties_to_print = if options.column_config.is_empty() {
+ extract_properties_to_print(schema.properties())
+ } else {
+ options
+ .column_config
+ .iter()
+ .map(|v| v.name.clone())
+ .collect()
+ };
+
+ let column_count = properties_to_print.len();
+ if column_count == 0 {
+ return Ok(());
+ };
+
+ let sortkeys = if let Some(ref sortkeys) = options.sortkeys {
+ sortkeys.clone()
+ } else {
+ let mut keys = Vec::new();
+ keys.push((properties_to_print[0].clone(), false)); // leftmost, ASC
+ keys
+ };
+
+ let mut sortinfo = Vec::new();
+
+ for (sortkey, sort_order) in sortkeys {
+ let (_optional, sort_prop_schema) = match schema.lookup(&sortkey) {
+ Some(tup) => tup,
+ None => bail!("property {} does not exist in schema.", sortkey),
+ };
+ let numeric_sort = match sort_prop_schema {
+ Schema::Integer(_) => true,
+ Schema::Number(_) => true,
+ _ => false,
+ };
+ sortinfo.push((sortkey, sort_order, numeric_sort));
+ }
+
+ use std::cmp::Ordering;
+ list.sort_unstable_by(move |a, b| {
+ for &(ref sortkey, sort_desc, numeric) in &sortinfo {
+ let res = if numeric {
+ let (v1, v2) = if sort_desc {
+ (b[&sortkey].as_f64(), a[&sortkey].as_f64())
+ } else {
+ (a[&sortkey].as_f64(), b[&sortkey].as_f64())
+ };
+ match (v1, v2) {
+ (None, None) => Ordering::Greater,
+ (Some(_), None) => Ordering::Greater,
+ (None, Some(_)) => Ordering::Less,
+ (Some(a), Some(b)) =>
+ {
+ #[allow(clippy::if_same_then_else)]
+ if a.is_nan() {
+ Ordering::Greater
+ } else if b.is_nan() {
+ Ordering::Less
+ } else if a < b {
+ Ordering::Less
+ } else if a > b {
+ Ordering::Greater
+ } else {
+ Ordering::Equal
+ }
+ }
+ }
+ } else {
+ let (v1, v2) = if sort_desc {
+ (b[sortkey].as_str(), a[sortkey].as_str())
+ } else {
+ (a[sortkey].as_str(), b[sortkey].as_str())
+ };
+ v1.cmp(&v2)
+ };
+
+ if res != Ordering::Equal {
+ return res;
+ }
+ }
+ Ordering::Equal
+ });
+
+ let mut tabledata: Vec<TableColumn> = Vec::new();
+
+ let mut column_names = Vec::new();
+
+ for name in properties_to_print.iter() {
+ let (_optional, prop_schema) = match schema.lookup(name) {
+ Some(tup) => tup,
+ None => bail!("property {} does not exist in schema.", name),
+ };
+
+ let is_numeric = match prop_schema {
+ Schema::Integer(_) => true,
+ Schema::Number(_) => true,
+ Schema::Boolean(_) => true,
+ _ => false,
+ };
+
+ let (header, right_align, renderer) = options.lookup_column_info(name);
+
+ let right_align = right_align.unwrap_or(is_numeric);
+
+ let mut max_width = if options.noheader || options.noborder {
+ 0
+ } else {
+ header.chars().count()
+ };
+
+ column_names.push(header);
+
+ let mut cells = Vec::new();
+ for entry in list.iter() {
+ let result = if let Some(renderer) = renderer {
+ (renderer)(&entry[name], &entry)
+ } else {
+ data_to_text(&entry[name], prop_schema)
+ };
+
+ let text = match result {
+ Ok(text) => text,
+ Err(err) => bail!("unable to format property {} - {}", name, err),
+ };
+
+ let lines: Vec<String> = text
+ .lines()
+ .map(|line| {
+ let width = UnicodeWidthStr::width(line);
+ if width > max_width {
+ max_width = width;
+ }
+ line.to_string()
+ })
+ .collect();
+
+ cells.push(TableCell { lines });
+ }
+
+ tabledata.push(TableColumn {
+ cells,
+ width: max_width,
+ right_align,
+ });
+ }
+
+ render_table(output, &tabledata, &column_names, options)
+}
+
+fn render_table<W: Write>(
+ mut output: W,
+ tabledata: &[TableColumn],
+ column_names: &[String],
+ options: &TableFormatOptions,
+) -> Result<(), Error> {
+ let mut write_line = |line: &str| -> Result<(), Error> {
+ if let Some(columns) = options.columns {
+ let line: String = line.chars().take(columns).collect();
+ output.write_all(line.as_bytes())?;
+ } else {
+ output.write_all(line.as_bytes())?;
+ }
+ output.write_all(b"\n")?;
+ Ok(())
+ };
+
+ let column_widths = tabledata.iter().map(|d| d.width);
+ let borders = TableBorders::new(column_widths, options.ascii_delimiters);
+
+ if !options.noborder {
+ write_line(&borders.top)?;
+ }
+
+ let mut header = String::new();
+ for (i, name) in column_names.iter().enumerate() {
+ let column = &tabledata[i];
+ header.push(borders.column_separator);
+ header.push(' ');
+ if column.right_align {
+ header.push_str(&format!("{:>width$}", name, width = column.width));
+ } else {
+ header.push_str(&format!("{:<width$}", name, width = column.width));
+ }
+ header.push(' ');
+ }
+
+ if !(options.noheader || options.noborder) {
+ header.push(borders.column_separator);
+
+ write_line(&header)?;
+ write_line(&borders.head)?;
+ }
+
+ let rows = tabledata[0].cells.len();
+ for pos in 0..rows {
+ let mut max_lines = 0;
+ for (i, _name) in column_names.iter().enumerate() {
+ let cells = &tabledata[i].cells;
+ let lines = &cells[pos].lines;
+ if lines.len() > max_lines {
+ max_lines = lines.len();
+ }
+ }
+ for line_nr in 0..max_lines {
+ let mut text = String::new();
+ let empty_string = String::new();
+ for (i, _name) in column_names.iter().enumerate() {
+ let column = &tabledata[i];
+ let lines = &column.cells[pos].lines;
+ let line = lines.get(line_nr).unwrap_or(&empty_string);
+
+ if options.noborder {
+ if i > 0 {
+ text.push(' ');
+ }
+ } else {
+ text.push(borders.column_separator);
+ text.push(' ');
+ }
+
+ let padding = column.width - UnicodeWidthStr::width(line.as_str());
+ if column.right_align {
+ text.push_str(&format!("{:>width$}{}", "", line, width = padding));
+ } else {
+ text.push_str(&format!("{}{:<width$}", line, "", width = padding));
+ }
+
+ if !options.noborder {
+ text.push(' ');
+ }
+ }
+ if !options.noborder {
+ text.push(borders.column_separator);
+ }
+ write_line(&text)?;
+ }
+
+ if !options.noborder {
+ if (pos + 1) == rows {
+ write_line(&borders.bottom)?;
+ } else {
+ write_line(&borders.middle)?;
+ }
+ }
+ }
+
+ Ok(())
+}
+
+fn format_object<W: Write>(
+ output: W,
+ data: &Value,
+ schema: &dyn ObjectSchemaType,
+ options: &TableFormatOptions,
+) -> Result<(), Error> {
+ let properties_to_print = if options.column_config.is_empty() {
+ extract_properties_to_print(schema.properties())
+ } else {
+ options
+ .column_config
+ .iter()
+ .map(|v| v.name.clone())
+ .collect()
+ };
+
+ let row_count = properties_to_print.len();
+ if row_count == 0 {
+ return Ok(());
+ };
+
+ const NAME_TITLE: &str = "Name";
+ const VALUE_TITLE: &str = "Value";
+
+ let mut max_name_width = if options.noheader || options.noborder {
+ 0
+ } else {
+ NAME_TITLE.len()
+ };
+ let mut max_value_width = if options.noheader || options.noborder {
+ 0
+ } else {
+ VALUE_TITLE.len()
+ };
+
+ let column_names = vec![NAME_TITLE.to_string(), VALUE_TITLE.to_string()];
+
+ let mut name_cells = Vec::new();
+ let mut value_cells = Vec::new();
+
+ let mut all_right_aligned = true;
+
+ for name in properties_to_print.iter() {
+ let (optional, prop_schema) = match schema.lookup(name) {
+ Some(tup) => tup,
+ None => bail!("property {} does not exist in schema.", name),
+ };
+
+ let is_numeric = match prop_schema {
+ Schema::Integer(_) => true,
+ Schema::Number(_) => true,
+ Schema::Boolean(_) => true,
+ _ => false,
+ };
+
+ let (header, right_align, renderer) = options.lookup_column_info(name);
+
+ let right_align = right_align.unwrap_or(is_numeric);
+
+ if !right_align {
+ all_right_aligned = false;
+ }
+
+ if optional {
+ if let Some(object) = data.as_object() {
+ if object.get(name).is_none() {
+ continue;
+ }
+ }
+ }
+
+ let header_width = header.chars().count();
+ if header_width > max_name_width {
+ max_name_width = header_width;
+ }
+
+ name_cells.push(TableCell {
+ lines: vec![header],
+ });
+
+ let result = if let Some(renderer) = renderer {
+ (renderer)(&data[name], &data)
+ } else {
+ data_to_text(&data[name], prop_schema)
+ };
+
+ let text = match result {
+ Ok(text) => text,
+ Err(err) => bail!("unable to format property {} - {}", name, err),
+ };
+
+ let lines: Vec<String> = text
+ .lines()
+ .map(|line| {
+ let width = line.chars().count();
+ if width > max_value_width {
+ max_value_width = width;
+ }
+ line.to_string()
+ })
+ .collect();
+
+ value_cells.push(TableCell { lines });
+ }
+
+ let name_column = TableColumn {
+ cells: name_cells,
+ width: max_name_width,
+ right_align: false,
+ };
+ let value_column = TableColumn {
+ cells: value_cells,
+ width: max_value_width,
+ right_align: all_right_aligned,
+ };
+
+ let mut tabledata: Vec<TableColumn> = Vec::new();
+ tabledata.push(name_column);
+ tabledata.push(value_column);
+
+ render_table(output, &tabledata, &column_names, options)
+}
+
+fn extract_properties_to_print<I>(properties: I) -> Vec<String>
+where
+ I: Iterator<Item = &'static SchemaPropertyEntry>,
+{
+ let mut result = Vec::new();
+ let mut opt_properties = Vec::new();
+
+ for (name, optional, _prop_schema) in properties {
+ if *optional {
+ opt_properties.push(name.to_string());
+ } else {
+ result.push(name.to_string());
+ }
+ }
+
+ result.extend(opt_properties);
+
+ result
+}
+
+/// Format data using TableFormatOptions
+pub fn value_to_text<W: Write>(
+ output: W,
+ data: &mut Value,
+ schema: &Schema,
+ options: &TableFormatOptions,
+) -> Result<(), Error> {
+ match schema {
+ Schema::Null => {
+ if *data != Value::Null {
+ bail!("got unexpected data (expected null).");
+ }
+ }
+ Schema::Boolean(_boolean_schema) => {
+ unimplemented!();
+ }
+ Schema::Integer(_integer_schema) => {
+ unimplemented!();
+ }
+ Schema::Number(_number_schema) => {
+ unimplemented!();
+ }
+ Schema::String(_string_schema) => {
+ unimplemented!();
+ }
+ Schema::Object(object_schema) => {
+ format_object(output, data, object_schema, options)?;
+ }
+ Schema::Array(array_schema) => {
+ let list = match data.as_array_mut() {
+ Some(list) => list,
+ None => bail!("got unexpected data (expected array)."),
+ };
+ if list.is_empty() {
+ return Ok(());
+ }
+
+ match array_schema.items {
+ Schema::Object(object_schema) => {
+ format_table(output, list, object_schema, options)?;
+ }
+ Schema::AllOf(all_of_schema) => {
+ format_table(output, list, all_of_schema, options)?;
+ }
+ _ => {
+ unimplemented!();
+ }
+ }
+ }
+ Schema::AllOf(all_of_schema) => {
+ format_object(output, data, all_of_schema, options)?;
+ }
+ }
+ Ok(())
+}
--- /dev/null
+use std::fmt;
+
+#[doc(hidden)]
+pub use http::StatusCode;
+
+/// HTTP error including `StatusCode` and message.
+#[derive(Debug)]
+pub struct HttpError {
+ pub code: StatusCode,
+ pub message: String,
+}
+
+impl std::error::Error for HttpError {}
+
+impl HttpError {
+ pub fn new(code: StatusCode, message: String) -> Self {
+ HttpError { code, message }
+ }
+}
+
+impl fmt::Display for HttpError {
+ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+ write!(f, "{}", self.message)
+ }
+}
+
+/// Macro to create a HttpError inside a anyhow::Error
+#[macro_export]
+macro_rules! http_err {
+ ($status:ident, $($fmt:tt)+) => {{
+ ::anyhow::Error::from($crate::HttpError::new(
+ $crate::error::StatusCode::$status,
+ format!($($fmt)+)
+ ))
+ }};
+}
+
+/// Bail with an error generated with the `http_err!` macro.
+#[macro_export]
+macro_rules! http_bail {
+ ($status:ident, $($fmt:tt)+) => {{
+ return Err($crate::http_err!($status, $($fmt)+));
+ }};
+}
--- /dev/null
+//! Module to generate and format API Documenation
+
+use std::io::Write;
+
+use anyhow::Error;
+
+use proxmox_schema::format::*;
+use proxmox_schema::ObjectSchemaType;
+
+use crate::{ApiHandler, ApiMethod};
+
+fn dump_method_definition(method: &str, path: &str, def: Option<&ApiMethod>) -> Option<String> {
+ let style = ParameterDisplayStyle::Config;
+ match def {
+ None => None,
+ Some(api_method) => {
+ let description = wrap_text("", "", &api_method.parameters.description(), 80);
+ let param_descr = dump_properties(&api_method.parameters, "", style, &[]);
+
+ let return_descr = dump_api_return_schema(&api_method.returns, style);
+
+ let mut method = method;
+
+ if let ApiHandler::AsyncHttp(_) = api_method.handler {
+ method = if method == "POST" { "UPLOAD" } else { method };
+ method = if method == "GET" { "DOWNLOAD" } else { method };
+ }
+
+ let res = format!(
+ "**{} {}**\n\n{}{}\n\n{}",
+ method, path, description, param_descr, return_descr
+ );
+ Some(res)
+ }
+ }
+}
+
+/// Generate ReST Documentaion for a complete API defined by a ``Router``.
+pub fn dump_api(
+ output: &mut dyn Write,
+ router: &crate::Router,
+ path: &str,
+ mut pos: usize,
+) -> Result<(), Error> {
+ use crate::SubRoute;
+
+ let mut cond_print = |x| -> Result<_, Error> {
+ if let Some(text) = x {
+ if pos > 0 {
+ writeln!(output, "-----\n")?;
+ }
+ writeln!(output, "{}", text)?;
+ pos += 1;
+ }
+ Ok(())
+ };
+
+ cond_print(dump_method_definition("GET", path, router.get))?;
+ cond_print(dump_method_definition("POST", path, router.post))?;
+ cond_print(dump_method_definition("PUT", path, router.put))?;
+ cond_print(dump_method_definition("DELETE", path, router.delete))?;
+
+ match &router.subroute {
+ None => return Ok(()),
+ Some(SubRoute::MatchAll { router, param_name }) => {
+ let sub_path = if path == "." {
+ format!("<{}>", param_name)
+ } else {
+ format!("{}/<{}>", path, param_name)
+ };
+ dump_api(output, router, &sub_path, pos)?;
+ }
+ Some(SubRoute::Map(dirmap)) => {
+ //let mut keys: Vec<&String> = map.keys().collect();
+ //keys.sort_unstable_by(|a, b| a.cmp(b));
+ for (key, sub_router) in dirmap.iter() {
+ let sub_path = if path == "." {
+ (*key).to_string()
+ } else {
+ format!("{}/{}", path, key)
+ };
+ dump_api(output, sub_router, &sub_path, pos)?;
+ }
+ }
+ }
+
+ Ok(())
+}
--- /dev/null
+//! API Router and Command Line Interface utilities.
+
+pub mod format;
+
+#[cfg(feature = "cli")]
+pub mod cli;
+
+// this is public so the `http_err!` macro can access `http::StatusCode` through it
+#[doc(hidden)]
+pub mod error;
+
+mod permission;
+mod router;
+mod rpc_environment;
+
+#[doc(inline)]
+pub use error::HttpError;
+
+pub use permission::*;
+pub use router::*;
+pub use rpc_environment::{RpcEnvironment, RpcEnvironmentType};
+
+// make list_subdirs_api_method! work without an explicit proxmox-schema dependency:
+#[doc(hidden)]
+pub use proxmox_schema::ObjectSchema as ListSubdirsObjectSchema;
--- /dev/null
+//! Declarative permission system
+//!
+//! A declarative way to define API access permissions.
+
+use std::collections::HashMap;
+use std::fmt;
+use std::ops::Deref;
+
+/// Access permission
+#[cfg_attr(feature = "test-harness", derive(Eq, PartialEq))]
+pub enum Permission {
+ /// Allow Superuser
+ Superuser,
+ /// Allow the whole World, no authentication required
+ World,
+ /// Allow any authenticated user
+ Anybody,
+ /// Allow access for the specified user
+ User(&'static str),
+ /// Allow access if specified param matches logged in user
+ UserParam(&'static str),
+ /// Allow access for the specified group of users
+ Group(&'static str),
+ /// Use a parameter value as userid to run sub-permission tests.
+ WithParam(&'static str, &'static Permission),
+ /// Check privilege/role on the specified path. The boolean
+ /// attribute specifies if you want to allow partial matches (u64
+ /// interpreted as bitmask).
+ Privilege(&'static [&'static str], u64, bool),
+ /// Allow access if all sub-permissions match
+ And(&'static [&'static Permission]),
+ /// Allow access if any sub-permissions match
+ Or(&'static [&'static Permission]),
+}
+
+impl fmt::Debug for Permission {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ match self {
+ Permission::Superuser => f.write_str("Superuser"),
+ Permission::World => f.write_str("World"),
+ Permission::Anybody => f.write_str("Anybody"),
+ Permission::User(ref userid) => write!(f, "User({})", userid),
+ Permission::UserParam(param_name) => write!(f, "UserParam({})", param_name),
+ Permission::Group(ref group) => write!(f, "Group({})", group),
+ Permission::WithParam(param_name, subtest) => {
+ write!(f, "WithParam({}, {:?})", param_name, subtest)
+ }
+ Permission::Privilege(path, privs, partial) => {
+ write!(f, "Privilege({:?}, {:0b}, {})", path, privs, partial)
+ }
+ Permission::And(list) => {
+ f.write_str("And(\n")?;
+ for subtest in list.iter() {
+ writeln!(f, " {:?}", subtest)?;
+ }
+ f.write_str(")\n")
+ }
+ Permission::Or(list) => {
+ f.write_str("Or(\n")?;
+ for subtest in list.iter() {
+ writeln!(f, " {:?}", subtest)?;
+ }
+ f.write_str(")\n")
+ }
+ }
+ }
+}
+
+/// Trait to query user information (used by check_api_permission)
+pub trait UserInformation {
+ fn is_superuser(&self, userid: &str) -> bool;
+ fn is_group_member(&self, userid: &str, group: &str) -> bool;
+ fn lookup_privs(&self, userid: &str, path: &[&str]) -> u64;
+}
+
+impl <T: UserInformation> UserInformation for std::sync::Arc<T> {
+ fn is_superuser(&self, userid: &str) -> bool {
+ self.deref().is_superuser(userid)
+ }
+ fn is_group_member(&self, userid: &str, group: &str) -> bool {
+ self.deref().is_group_member(userid, group)
+ }
+ fn lookup_privs(&self, userid: &str, path: &[&str]) -> u64 {
+ self.deref().lookup_privs(userid, path)
+ }
+}
+
+/// Example implementation to check access permissions
+///
+/// This implementation supports URI variables in Privilege path
+/// components, i.e. '{storage}'. We replace this with actual
+/// parameter values before calling lookup_privs().
+pub fn check_api_permission(
+ perm: &Permission,
+ userid: Option<&str>,
+ param: &HashMap<String, String>,
+ info: &dyn UserInformation,
+) -> bool {
+ if let Some(ref userid) = userid {
+ if info.is_superuser(userid) {
+ return true;
+ }
+ }
+
+ check_api_permission_tail(perm, userid, param, info)
+}
+
+// some of them are deeply nested
+#[allow(clippy::needless_return)]
+fn check_api_permission_tail(
+ perm: &Permission,
+ userid: Option<&str>,
+ param: &HashMap<String, String>,
+ info: &dyn UserInformation,
+) -> bool {
+ match perm {
+ Permission::World => return true,
+ Permission::Anybody => {
+ return userid.is_some();
+ }
+ Permission::Superuser => match userid {
+ None => return false,
+ Some(ref userid) => return info.is_superuser(userid),
+ },
+ Permission::User(expected_userid) => match userid {
+ None => return false,
+ Some(ref userid) => return userid == expected_userid,
+ },
+ Permission::UserParam(param_name) => match (userid, param.get(¶m_name.to_string())) {
+ (None, _) => return false,
+ (_, None) => return false,
+ (Some(ref userid), Some(ref expected)) => return userid == expected,
+ },
+ Permission::Group(expected_group) => match userid {
+ None => return false,
+ Some(ref userid) => return info.is_group_member(userid, expected_group),
+ },
+ Permission::WithParam(param_name, subtest) => {
+ return check_api_permission(
+ subtest,
+ param.get(*param_name).map(|v| v.as_str()),
+ param,
+ info,
+ );
+ }
+ Permission::Privilege(path, expected_privs, partial) => {
+ // replace uri vars
+ let mut new_path: Vec<&str> = Vec::new();
+ for comp in path.iter() {
+ if comp.starts_with('{') && comp.ends_with('}') {
+ let param_name = unsafe { comp.get_unchecked(1..comp.len() - 1) };
+ match param.get(param_name) {
+ None => return false,
+ Some(value) => {
+ new_path.push(value);
+ }
+ }
+ } else {
+ new_path.push(comp);
+ }
+ }
+ match userid {
+ None => return false,
+ Some(userid) => {
+ let privs = info.lookup_privs(userid, &new_path);
+ if privs == 0 {
+ return false;
+ };
+ if *partial {
+ return (expected_privs & privs) != 0;
+ } else {
+ return (*expected_privs & privs) == *expected_privs;
+ }
+ }
+ }
+ }
+ Permission::And(list) => {
+ for subtest in list.iter() {
+ if !check_api_permission_tail(subtest, userid, param, info) {
+ return false;
+ }
+ }
+
+ return true;
+ }
+ Permission::Or(list) => {
+ for subtest in list.iter() {
+ if check_api_permission_tail(subtest, userid, param, info) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+ }
+}
+
+#[cfg(test)]
+mod test {
+ use serde_json::{json, Value};
+
+ use crate::permission::*;
+
+ struct MockedUserInfo {
+ privs: Value,
+ groups: Value,
+ }
+
+ impl UserInformation for MockedUserInfo {
+ fn is_superuser(&self, userid: &str) -> bool {
+ userid == "root"
+ }
+
+ fn is_group_member(&self, userid: &str, group: &str) -> bool {
+ if let Some(groups) = self.groups[userid].as_array() {
+ return groups.contains(&Value::from(group));
+ }
+
+ return false;
+ }
+
+ fn lookup_privs(&self, userid: &str, path: &[&str]) -> u64 {
+ let path = format!("/{}", path.join("/"));
+ if let Some(users) = self.privs.get(path) {
+ if let Some(privilege) = users.get(userid) {
+ return privilege.as_u64().unwrap();
+ }
+ }
+
+ return 0;
+ }
+ }
+
+ #[test]
+ fn test_privileges() {
+ let userinfo = MockedUserInfo {
+ privs: json!({
+ "/": {
+ "user1": 0b10,
+ },
+ "/datastore": {
+ "user1": 0b00,
+ "user2": 0b01,
+ },
+ "/datastore/foo": {
+ "user1": 0b01,
+ },
+ }),
+ groups: json!({
+ "user1": [
+ "group1",
+ ],
+ "user2": [
+ "group2",
+ ],
+ }),
+ };
+
+ let mut param = HashMap::new();
+ param.insert("user".to_string(), "user1".to_string());
+ param.insert("datastore".to_string(), "foo".to_string());
+
+ let test_check = |perm: &Permission, userid: Option<&str>, should_succeed: bool| {
+ println!("{:?} on {:?}: {}", userid, perm, should_succeed);
+ assert_eq!(
+ check_api_permission(perm, userid, ¶m, &userinfo),
+ should_succeed
+ )
+ };
+
+ test_check(&Permission::Superuser, Some("root"), true);
+ test_check(&Permission::Superuser, Some("user1"), false);
+ test_check(&Permission::Superuser, None, false);
+
+ test_check(&Permission::World, Some("root"), true);
+ test_check(&Permission::World, Some("user1"), true);
+ test_check(&Permission::World, None, true);
+
+ test_check(&Permission::Anybody, Some("root"), true);
+ test_check(&Permission::Anybody, Some("user1"), true);
+ test_check(&Permission::Anybody, None, false);
+
+ test_check(&Permission::User("user1"), Some("root"), true);
+ test_check(&Permission::User("user1"), Some("user1"), true);
+ test_check(&Permission::User("user1"), Some("user2"), false);
+ test_check(&Permission::User("user1"), None, false);
+
+ test_check(&Permission::Group("group1"), Some("root"), true);
+ test_check(&Permission::Group("group1"), Some("user1"), true);
+ test_check(&Permission::Group("group1"), Some("user2"), false);
+ test_check(&Permission::Group("group1"), None, false);
+
+ test_check(
+ &Permission::WithParam("user", &Permission::User("root")),
+ Some("root"),
+ true,
+ );
+ test_check(
+ &Permission::WithParam("user", &Permission::User("user1")),
+ Some("user1"),
+ true,
+ );
+ test_check(
+ &Permission::WithParam("user", &Permission::User("user2")),
+ Some("user2"),
+ false,
+ );
+ test_check(
+ &Permission::WithParam("user", &Permission::User("")),
+ None,
+ false,
+ );
+
+ test_check(
+ &Permission::And(&[&Permission::User("user1"), &Permission::Group("group2")]),
+ Some("root"),
+ true,
+ );
+ test_check(
+ &Permission::And(&[&Permission::User("user1"), &Permission::Group("group2")]),
+ Some("user1"),
+ false,
+ );
+ test_check(
+ &Permission::And(&[&Permission::User("user1"), &Permission::Group("group1")]),
+ Some("user1"),
+ true,
+ );
+ test_check(
+ &Permission::And(&[&Permission::User("user1"), &Permission::Group("group2")]),
+ None,
+ false,
+ );
+
+ test_check(
+ &Permission::Or(&[&Permission::User("user1"), &Permission::Group("group2")]),
+ Some("root"),
+ true,
+ );
+ test_check(
+ &Permission::Or(&[&Permission::User("user1"), &Permission::Group("group2")]),
+ Some("user1"),
+ true,
+ );
+ test_check(
+ &Permission::Or(&[&Permission::User("user1"), &Permission::Group("group1")]),
+ Some("user2"),
+ false,
+ );
+ test_check(
+ &Permission::Or(&[&Permission::User("user1"), &Permission::Group("group2")]),
+ None,
+ false,
+ );
+
+ test_check(&Permission::Privilege(&[], 0b11, true), Some("root"), true);
+ test_check(&Permission::Privilege(&[], 0b11, true), Some("user1"), true);
+ test_check(
+ &Permission::Privilege(&[], 0b11, false),
+ Some("user1"),
+ false,
+ );
+ test_check(
+ &Permission::Privilege(&[], 0b11, true),
+ Some("user2"),
+ false,
+ );
+ test_check(
+ &Permission::Privilege(&[], 0b11, false),
+ Some("user2"),
+ false,
+ );
+ test_check(&Permission::Privilege(&[], 0b11, true), None, false);
+ test_check(&Permission::Privilege(&[], 0b11, false), None, false);
+
+ test_check(
+ &Permission::Privilege(&["datastore"], 0b01, true),
+ Some("user1"),
+ false,
+ );
+ test_check(
+ &Permission::Privilege(&["datastore"], 0b01, true),
+ Some("user2"),
+ true,
+ );
+ test_check(
+ &Permission::Privilege(&["datastore"], 0b01, true),
+ None,
+ false,
+ );
+
+ test_check(
+ &Permission::Privilege(&["datastore", "{datastore}"], 0b01, true),
+ Some("user1"),
+ true,
+ );
+ test_check(
+ &Permission::Privilege(&["datastore", "{datastore}"], 0b01, true),
+ Some("user2"),
+ false,
+ );
+ test_check(
+ &Permission::Privilege(&["datastore", "{datastore}"], 0b01, true),
+ None,
+ false,
+ );
+ }
+}
--- /dev/null
+use std::collections::HashMap;
+use std::fmt;
+use std::future::Future;
+use std::pin::Pin;
+
+use anyhow::Error;
+use http::request::Parts;
+use http::{Method, Response};
+use hyper::Body;
+use percent_encoding::percent_decode_str;
+use serde_json::Value;
+
+use proxmox_schema::{ObjectSchema, ParameterSchema, ReturnType, Schema};
+
+use super::Permission;
+use crate::RpcEnvironment;
+
+/// A synchronous API handler gets a json Value as input and returns a json Value as output.
+///
+/// Most API handler are synchronous. Use this to define such handler:
+/// ```
+/// # use anyhow::*;
+/// # use serde_json::{json, Value};
+/// use proxmox_router::{ApiHandler, ApiMethod, RpcEnvironment};
+/// use proxmox_schema::ObjectSchema;
+///
+/// fn hello(
+/// param: Value,
+/// info: &ApiMethod,
+/// rpcenv: &mut dyn RpcEnvironment,
+/// ) -> Result<Value, Error> {
+/// Ok(json!("Hello world!"))
+/// }
+///
+/// const API_METHOD_HELLO: ApiMethod = ApiMethod::new(
+/// &ApiHandler::Sync(&hello),
+/// &ObjectSchema::new("Hello World Example", &[])
+/// );
+/// ```
+pub type ApiHandlerFn = &'static (dyn Fn(Value, &ApiMethod, &mut dyn RpcEnvironment) -> Result<Value, Error>
+ + Send
+ + Sync
+ + 'static);
+
+/// Asynchronous API handlers
+///
+/// Returns a future Value.
+/// ```
+/// # use serde_json::{json, Value};
+/// #
+/// use proxmox_router::{ApiFuture, ApiHandler, ApiMethod, RpcEnvironment};
+/// use proxmox_schema::ObjectSchema;
+///
+///
+/// fn hello_future<'a>(
+/// param: Value,
+/// info: &ApiMethod,
+/// rpcenv: &'a mut dyn RpcEnvironment,
+/// ) -> ApiFuture<'a> {
+/// Box::pin(async move {
+/// let data = json!("hello world!");
+/// Ok(data)
+/// })
+/// }
+///
+/// const API_METHOD_HELLO_FUTURE: ApiMethod = ApiMethod::new(
+/// &ApiHandler::Async(&hello_future),
+/// &ObjectSchema::new("Hello World Example (async)", &[])
+/// );
+/// ```
+pub type ApiAsyncHandlerFn = &'static (dyn for<'a> Fn(Value, &'static ApiMethod, &'a mut dyn RpcEnvironment) -> ApiFuture<'a>
+ + Send
+ + Sync);
+
+pub type ApiFuture<'a> = Pin<Box<dyn Future<Output = Result<Value, anyhow::Error>> + Send + 'a>>;
+
+/// Asynchronous HTTP API handlers
+///
+/// They get low level access to request and response data. Use this
+/// to implement custom upload/download functions.
+/// ```
+/// # use serde_json::{json, Value};
+/// #
+/// use hyper::{Body, Response, http::request::Parts};
+///
+/// use proxmox_router::{ApiHandler, ApiMethod, ApiResponseFuture, RpcEnvironment};
+/// use proxmox_schema::ObjectSchema;
+///
+/// fn low_level_hello(
+/// parts: Parts,
+/// req_body: Body,
+/// param: Value,
+/// info: &ApiMethod,
+/// rpcenv: Box<dyn RpcEnvironment>,
+/// ) -> ApiResponseFuture {
+/// Box::pin(async move {
+/// let response = http::Response::builder()
+/// .status(200)
+/// .body(Body::from("Hello world!"))?;
+/// Ok(response)
+/// })
+/// }
+///
+/// const API_METHOD_LOW_LEVEL_HELLO: ApiMethod = ApiMethod::new(
+/// &ApiHandler::AsyncHttp(&low_level_hello),
+/// &ObjectSchema::new("Hello World Example (low level)", &[])
+/// );
+/// ```
+pub type ApiAsyncHttpHandlerFn = &'static (dyn Fn(
+ Parts,
+ Body,
+ Value,
+ &'static ApiMethod,
+ Box<dyn RpcEnvironment>,
+) -> ApiResponseFuture
+ + Send
+ + Sync
+ + 'static);
+
+/// The output of an asynchronous API handler is a future yielding a `Response`.
+pub type ApiResponseFuture =
+ Pin<Box<dyn Future<Output = Result<Response<Body>, anyhow::Error>> + Send>>;
+
+/// Enum for different types of API handler functions.
+pub enum ApiHandler {
+ Sync(ApiHandlerFn),
+ Async(ApiAsyncHandlerFn),
+ AsyncHttp(ApiAsyncHttpHandlerFn),
+}
+
+#[cfg(feature = "test-harness")]
+impl Eq for ApiHandler {}
+
+#[cfg(feature = "test-harness")]
+impl PartialEq for ApiHandler {
+ fn eq(&self, rhs: &Self) -> bool {
+ unsafe {
+ match (self, rhs) {
+ (ApiHandler::Sync(l), ApiHandler::Sync(r)) => {
+ core::mem::transmute::<_, usize>(l) == core::mem::transmute::<_, usize>(r)
+ }
+ (ApiHandler::Async(l), ApiHandler::Async(r)) => {
+ core::mem::transmute::<_, usize>(l) == core::mem::transmute::<_, usize>(r)
+ }
+ (ApiHandler::AsyncHttp(l), ApiHandler::AsyncHttp(r)) => {
+ core::mem::transmute::<_, usize>(l) == core::mem::transmute::<_, usize>(r)
+ }
+ _ => false,
+ }
+ }
+ }
+}
+
+/// Lookup table to child `Router`s
+///
+/// Stores a sorted list of `(name, router)` tuples:
+///
+/// - `name`: The name of the subdir
+/// - `router`: The router for this subdir
+///
+/// **Note:** The list has to be sorted by name, because we use a binary
+/// search to find items.
+///
+/// This is a workaround unless RUST can const_fn `Hash::new()`
+pub type SubdirMap = &'static [(&'static str, &'static Router)];
+
+/// Classify different types of routers
+pub enum SubRoute {
+ //Hash(HashMap<String, Router>),
+ /// Router with static lookup map.
+ ///
+ /// The first path element is used to lookup a new
+ /// router with `SubdirMap`. If found, the remaining path is
+ /// passed to that router.
+ Map(SubdirMap),
+ /// Router that always match the first path element
+ ///
+ /// The matched path element is stored as parameter
+ /// `param_name`. The remaining path is matched using the `router`.
+ MatchAll {
+ router: &'static Router,
+ param_name: &'static str,
+ },
+}
+
+/// Macro to create an ApiMethod to list entries from SubdirMap
+#[macro_export]
+macro_rules! list_subdirs_api_method {
+ ($map:expr) => {
+ $crate::ApiMethod::new(
+ &$crate::ApiHandler::Sync( & |_, _, _| {
+ let index = ::serde_json::json!(
+ $map.iter().map(|s| ::serde_json::json!({ "subdir": s.0}))
+ .collect::<Vec<::serde_json::Value>>()
+ );
+ Ok(index)
+ }),
+ &$crate::ListSubdirsObjectSchema::new("Directory index.", &[])
+ .additional_properties(true)
+ ).access(None, &$crate::Permission::Anybody)
+ }
+}
+
+/// Define APIs with routing information
+///
+/// REST APIs use hierarchical paths to identify resources. A path
+/// consists of zero or more components, separated by `/`. A `Router`
+/// is a simple data structure to define such APIs. Each `Router` is
+/// responsible for a specific path, and may define `ApiMethod`s for
+/// different HTTP requests (GET, PUT, POST, DELETE). If the path
+/// contains more elements, `subroute` is used to find the correct
+/// endpoint.
+///
+/// Routers are meant to be build a compile time, and you can use
+/// all `const fn(mut self, ..)` methods to configure them.
+///
+///```
+/// # use serde_json::{json, Value};
+/// use proxmox_router::{ApiHandler, ApiMethod, Router};
+/// use proxmox_schema::ObjectSchema;
+///
+/// const API_METHOD_HELLO: ApiMethod = ApiMethod::new(
+/// &ApiHandler::Sync(&|_, _, _| {
+/// Ok(json!("Hello world!"))
+/// }),
+/// &ObjectSchema::new("Hello World Example", &[])
+/// );
+/// const ROUTER: Router = Router::new()
+/// .get(&API_METHOD_HELLO);
+///```
+pub struct Router {
+ /// GET requests
+ pub get: Option<&'static ApiMethod>,
+ /// PUT requests
+ pub put: Option<&'static ApiMethod>,
+ /// POST requests
+ pub post: Option<&'static ApiMethod>,
+ /// DELETE requests
+ pub delete: Option<&'static ApiMethod>,
+ /// Used to find the correct API endpoint.
+ pub subroute: Option<SubRoute>,
+}
+
+impl Router {
+ /// Create a new Router.
+ pub const fn new() -> Self {
+ Self {
+ get: None,
+ put: None,
+ post: None,
+ delete: None,
+ subroute: None,
+ }
+ }
+
+ /// Configure a static map as `subroute`.
+ pub const fn subdirs(mut self, map: SubdirMap) -> Self {
+ self.subroute = Some(SubRoute::Map(map));
+ self
+ }
+
+ /// Configure a `SubRoute::MatchAll` as `subroute`.
+ pub const fn match_all(mut self, param_name: &'static str, router: &'static Router) -> Self {
+ self.subroute = Some(SubRoute::MatchAll { router, param_name });
+ self
+ }
+
+ /// Configure the GET method.
+ pub const fn get(mut self, m: &'static ApiMethod) -> Self {
+ self.get = Some(m);
+ self
+ }
+
+ /// Configure the PUT method.
+ pub const fn put(mut self, m: &'static ApiMethod) -> Self {
+ self.put = Some(m);
+ self
+ }
+
+ /// Configure the POST method.
+ pub const fn post(mut self, m: &'static ApiMethod) -> Self {
+ self.post = Some(m);
+ self
+ }
+
+ /// Same as `post`, but expects an `AsyncHttp` handler.
+ pub const fn upload(mut self, m: &'static ApiMethod) -> Self {
+ // fixme: expect AsyncHttp
+ self.post = Some(m);
+ self
+ }
+
+ /// Same as `get`, but expects an `AsyncHttp` handler.
+ pub const fn download(mut self, m: &'static ApiMethod) -> Self {
+ // fixme: expect AsyncHttp
+ self.get = Some(m);
+ self
+ }
+
+ /// Same as `get`, but expects an `AsyncHttp` handler.
+ pub const fn upgrade(mut self, m: &'static ApiMethod) -> Self {
+ // fixme: expect AsyncHttp
+ self.get = Some(m);
+ self
+ }
+
+ /// Configure the DELETE method
+ pub const fn delete(mut self, m: &'static ApiMethod) -> Self {
+ self.delete = Some(m);
+ self
+ }
+
+ /// Find the router for a specific path.
+ ///
+ /// - `components`: Path, split into individual components.
+ /// - `uri_param`: Mutable hash map to store parameter from `MatchAll` router.
+ pub fn find_route(
+ &self,
+ components: &[&str],
+ uri_param: &mut HashMap<String, String>,
+ ) -> Option<&Router> {
+ if components.is_empty() {
+ return Some(self);
+ };
+
+ let (dir, remaining) = (components[0], &components[1..]);
+
+ let dir = match percent_decode_str(dir).decode_utf8() {
+ Ok(dir) => dir.to_string(),
+ Err(_) => return None,
+ };
+
+ match self.subroute {
+ None => {}
+ Some(SubRoute::Map(dirmap)) => {
+ if let Ok(ind) = dirmap.binary_search_by_key(&dir.as_str(), |(name, _)| name) {
+ let (_name, router) = dirmap[ind];
+ //println!("FOUND SUBDIR {}", dir);
+ return router.find_route(remaining, uri_param);
+ }
+ }
+ Some(SubRoute::MatchAll { router, param_name }) => {
+ //println!("URI PARAM {} = {}", param_name, dir); // fixme: store somewhere
+ uri_param.insert(param_name.to_owned(), dir);
+ return router.find_route(remaining, uri_param);
+ }
+ }
+
+ None
+ }
+
+ /// Lookup the API method for a specific path.
+ /// - `components`: Path, split into individual components.
+ /// - `method`: The HTTP method.
+ /// - `uri_param`: Mutable hash map to store parameter from `MatchAll` router.
+ pub fn find_method(
+ &self,
+ components: &[&str],
+ method: Method,
+ uri_param: &mut HashMap<String, String>,
+ ) -> Option<&ApiMethod> {
+ if let Some(info) = self.find_route(components, uri_param) {
+ return match method {
+ Method::GET => info.get,
+ Method::PUT => info.put,
+ Method::POST => info.post,
+ Method::DELETE => info.delete,
+ _ => None,
+ };
+ }
+ None
+ }
+}
+
+impl Default for Router {
+ #[inline]
+ fn default() -> Self {
+ Self::new()
+ }
+}
+
+const NULL_SCHEMA: Schema = Schema::Null;
+
+fn dummy_handler_fn(
+ _arg: Value,
+ _method: &ApiMethod,
+ _env: &mut dyn RpcEnvironment,
+) -> Result<Value, Error> {
+ // do nothing
+ Ok(Value::Null)
+}
+
+const DUMMY_HANDLER: ApiHandler = ApiHandler::Sync(&dummy_handler_fn);
+
+/// Access permission with description
+#[cfg_attr(feature = "test-harness", derive(Eq, PartialEq))]
+pub struct ApiAccess {
+ pub description: Option<&'static str>,
+ pub permission: &'static Permission,
+}
+
+/// This struct defines a synchronous API call which returns the result as json `Value`
+#[cfg_attr(feature = "test-harness", derive(Eq, PartialEq))]
+pub struct ApiMethod {
+ /// The protected flag indicates that the provides function should be forwarded
+ /// to the daemon running in privileged mode.
+ pub protected: bool,
+ /// This flag indicates that the provided method may change the local timezone, so the server
+ /// should do a tzset afterwards
+ pub reload_timezone: bool,
+ /// Parameter type Schema
+ pub parameters: ParameterSchema,
+ /// Return type Schema
+ pub returns: ReturnType,
+ /// Handler function
+ pub handler: &'static ApiHandler,
+ /// Access Permissions
+ pub access: ApiAccess,
+}
+
+impl std::fmt::Debug for ApiMethod {
+ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+ write!(f, "ApiMethod {{ ")?;
+ write!(f, " parameters: {:?}", self.parameters)?;
+ write!(f, " returns: {:?}", self.returns)?;
+ write!(f, " handler: {:p}", &self.handler)?;
+ write!(f, " permissions: {:?}", &self.access.permission)?;
+ write!(f, "}}")
+ }
+}
+
+impl ApiMethod {
+ pub const fn new_full(handler: &'static ApiHandler, parameters: ParameterSchema) -> Self {
+ Self {
+ parameters,
+ handler,
+ returns: ReturnType::new(false, &NULL_SCHEMA),
+ protected: false,
+ reload_timezone: false,
+ access: ApiAccess {
+ description: None,
+ permission: &Permission::Superuser,
+ },
+ }
+ }
+
+ pub const fn new(handler: &'static ApiHandler, parameters: &'static ObjectSchema) -> Self {
+ Self::new_full(handler, ParameterSchema::Object(parameters))
+ }
+
+ pub const fn new_dummy(parameters: &'static ObjectSchema) -> Self {
+ Self {
+ parameters: ParameterSchema::Object(parameters),
+ handler: &DUMMY_HANDLER,
+ returns: ReturnType::new(false, &NULL_SCHEMA),
+ protected: false,
+ reload_timezone: false,
+ access: ApiAccess {
+ description: None,
+ permission: &Permission::Superuser,
+ },
+ }
+ }
+
+ pub const fn returns(mut self, returns: ReturnType) -> Self {
+ self.returns = returns;
+
+ self
+ }
+
+ pub const fn protected(mut self, protected: bool) -> Self {
+ self.protected = protected;
+
+ self
+ }
+
+ pub const fn reload_timezone(mut self, reload_timezone: bool) -> Self {
+ self.reload_timezone = reload_timezone;
+
+ self
+ }
+
+ pub const fn access(
+ mut self,
+ description: Option<&'static str>,
+ permission: &'static Permission,
+ ) -> Self {
+ self.access = ApiAccess {
+ description,
+ permission,
+ };
+
+ self
+ }
+}
--- /dev/null
+use std::any::Any;
+
+use serde_json::Value;
+
+/// Helper to get around `RpcEnvironment: Sized`
+pub trait AsAny {
+ fn as_any(&self) -> &(dyn Any + Send);
+}
+
+impl<T: Any + Send> AsAny for T {
+ fn as_any(&self) -> &(dyn Any + Send) {
+ self
+ }
+}
+
+/// Abstract Interface for API methods to interact with the environment
+pub trait RpcEnvironment: Any + AsAny + Send {
+ /// Use this to pass additional result data. It is up to the environment
+ /// how the data is used.
+ fn result_attrib_mut(&mut self) -> &mut Value;
+
+ /// Access result attribute immutable
+ fn result_attrib(&self) -> &Value;
+
+ /// The environment type
+ fn env_type(&self) -> RpcEnvironmentType;
+
+ /// Set authentication id
+ fn set_auth_id(&mut self, user: Option<String>);
+
+ /// Get authentication id
+ fn get_auth_id(&self) -> Option<String>;
+
+ /// Set the client IP, should be re-set if a proxied connection was detected
+ fn set_client_ip(&mut self, _client_ip: Option<std::net::SocketAddr>) {
+ // dummy no-op implementation, as most environments don't need this
+ }
+
+ /// Get the (real) client IP
+ fn get_client_ip(&self) -> Option<std::net::SocketAddr> {
+ None // dummy no-op implementation, as most environments don't need this
+ }
+}
+
+/// Environment Type
+///
+/// We use this to enumerate the different environment types. Some methods
+/// needs to do different things when started from the command line interface,
+/// or when executed from a privileged server running as root.
+#[derive(PartialEq, Copy, Clone)]
+pub enum RpcEnvironmentType {
+ /// Command started from command line
+ CLI,
+ /// Access from public accessible server
+ PUBLIC,
+ /// Access from privileged server (run as root)
+ PRIVILEGED,
+}
+
+impl core::ops::Index<&str> for &dyn RpcEnvironment {
+ type Output = Value;
+ fn index(&self, index: &str) -> &Value {
+ &self.result_attrib().index(index)
+ }
+}
+
+impl core::ops::Index<&str> for &mut dyn RpcEnvironment {
+ type Output = Value;
+ fn index(&self, index: &str) -> &Value {
+ &self.result_attrib().index(index)
+ }
+}
+
+impl core::ops::IndexMut<&str> for &mut dyn RpcEnvironment {
+ fn index_mut(&mut self, index: &str) -> &mut Value {
+ self.result_attrib_mut().index_mut(index)
+ }
+}
--- /dev/null
+[package]
+name = "proxmox-schema"
+version = "1.0.0"
+authors = ["Proxmox Support Team <support@proxmox.com>"]
+edition = "2018"
+license = "AGPL-3"
+description = "proxmox api schema and validation"
+
+exclude = [ "debian" ]
+
+[dependencies]
+anyhow = "1.0"
+lazy_static = "1.4"
+regex = "1.2"
+serde = "1.0"
+serde_json = "1.0"
+textwrap = "0.11"
+
+# the upid type needs this for 'getpid'
+libc = { version = "0.2", optional = true }
+nix = { version = "0.19", optional = true }
+
+proxmox-api-macro = { path = "../proxmox-api-macro", optional = true, version = "1.0.0" }
+
+[dev-dependencies]
+url = "2.1"
+serde = { version = "1.0", features = [ "derive" ] }
+proxmox-api-macro = { path = "../proxmox-api-macro", version = "1.0.0" }
+
+[features]
+default = []
+
+api-macro = ["proxmox-api-macro"]
+upid-api-impl = [ "libc", "nix" ]
+
+# Testing only
+test-harness = []
--- /dev/null
+rust-proxmox-schema (1.0.0-1) stable; urgency=medium
+
+ * initial split out of `librust-proxmox-dev`
+
+ -- Proxmox Support Team <support@proxmox.com> Wed, 06 Oct 2021 11:04:36 +0200
--- /dev/null
+Source: rust-proxmox-schema
+Section: rust
+Priority: optional
+Build-Depends: debhelper (>= 12),
+ dh-cargo (>= 24),
+ cargo:native <!nocheck>,
+ rustc:native <!nocheck>,
+ libstd-rust-dev <!nocheck>,
+ librust-anyhow-1+default-dev <!nocheck>,
+ librust-lazy-static-1+default-dev (>= 1.4-~~) <!nocheck>,
+ librust-regex-1+default-dev (>= 1.2-~~) <!nocheck>,
+ librust-serde-1+default-dev <!nocheck>,
+ librust-serde-json-1+default-dev <!nocheck>,
+ librust-textwrap-0.11+default-dev <!nocheck>
+Maintainer: Proxmox Support Team <support@proxmox.com>
+Standards-Version: 4.5.1
+Vcs-Git: git://git.proxmox.com/git/proxmox.git
+Vcs-Browser: https://git.proxmox.com/?p=proxmox.git
+Rules-Requires-Root: no
+
+Package: librust-proxmox-schema-dev
+Architecture: any
+Multi-Arch: same
+Depends:
+ ${misc:Depends},
+ librust-anyhow-1+default-dev,
+ librust-lazy-static-1+default-dev (>= 1.4-~~),
+ librust-regex-1+default-dev (>= 1.2-~~),
+ librust-serde-1+default-dev,
+ librust-serde-json-1+default-dev,
+ librust-textwrap-0.11+default-dev
+Suggests:
+ librust-proxmox-schema+api-macro-dev (= ${binary:Version}),
+ librust-proxmox-schema+libc-dev (= ${binary:Version}),
+ librust-proxmox-schema+nix-dev (= ${binary:Version}),
+ librust-proxmox-schema+upid-api-impl-dev (= ${binary:Version})
+Provides:
+ librust-proxmox-schema+default-dev (= ${binary:Version}),
+ librust-proxmox-schema+test-harness-dev (= ${binary:Version}),
+ librust-proxmox-schema-1-dev (= ${binary:Version}),
+ librust-proxmox-schema-1+default-dev (= ${binary:Version}),
+ librust-proxmox-schema-1+test-harness-dev (= ${binary:Version}),
+ librust-proxmox-schema-1.0-dev (= ${binary:Version}),
+ librust-proxmox-schema-1.0+default-dev (= ${binary:Version}),
+ librust-proxmox-schema-1.0+test-harness-dev (= ${binary:Version}),
+ librust-proxmox-schema-1.0.0-dev (= ${binary:Version}),
+ librust-proxmox-schema-1.0.0+default-dev (= ${binary:Version}),
+ librust-proxmox-schema-1.0.0+test-harness-dev (= ${binary:Version})
+Description: Proxmox api schema and validation - Rust source code
+ This package contains the source for the Rust proxmox-schema crate, packaged by
+ debcargo for use with cargo and dh-cargo.
+
+Package: librust-proxmox-schema+api-macro-dev
+Architecture: any
+Multi-Arch: same
+Depends:
+ ${misc:Depends},
+ librust-proxmox-schema-dev (= ${binary:Version}),
+ librust-proxmox-api-macro-1+default-dev
+Provides:
+ librust-proxmox-schema+proxmox-api-macro-dev (= ${binary:Version}),
+ librust-proxmox-schema-1+api-macro-dev (= ${binary:Version}),
+ librust-proxmox-schema-1+proxmox-api-macro-dev (= ${binary:Version}),
+ librust-proxmox-schema-1.0+api-macro-dev (= ${binary:Version}),
+ librust-proxmox-schema-1.0+proxmox-api-macro-dev (= ${binary:Version}),
+ librust-proxmox-schema-1.0.0+api-macro-dev (= ${binary:Version}),
+ librust-proxmox-schema-1.0.0+proxmox-api-macro-dev (= ${binary:Version})
+Description: Proxmox api schema and validation - feature "api-macro" and 1 more
+ This metapackage enables feature "api-macro" for the Rust proxmox-schema crate,
+ by pulling in any additional dependencies needed by that feature.
+ .
+ Additionally, this package also provides the "proxmox-api-macro" feature.
+
+Package: librust-proxmox-schema+libc-dev
+Architecture: any
+Multi-Arch: same
+Depends:
+ ${misc:Depends},
+ librust-proxmox-schema-dev (= ${binary:Version}),
+ librust-libc-0.2+default-dev
+Provides:
+ librust-proxmox-schema-1+libc-dev (= ${binary:Version}),
+ librust-proxmox-schema-1.0+libc-dev (= ${binary:Version}),
+ librust-proxmox-schema-1.0.0+libc-dev (= ${binary:Version})
+Description: Proxmox api schema and validation - feature "libc"
+ This metapackage enables feature "libc" for the Rust proxmox-schema crate, by
+ pulling in any additional dependencies needed by that feature.
+
+Package: librust-proxmox-schema+nix-dev
+Architecture: any
+Multi-Arch: same
+Depends:
+ ${misc:Depends},
+ librust-proxmox-schema-dev (= ${binary:Version}),
+ librust-nix-0.19+default-dev
+Provides:
+ librust-proxmox-schema-1+nix-dev (= ${binary:Version}),
+ librust-proxmox-schema-1.0+nix-dev (= ${binary:Version}),
+ librust-proxmox-schema-1.0.0+nix-dev (= ${binary:Version})
+Description: Proxmox api schema and validation - feature "nix"
+ This metapackage enables feature "nix" for the Rust proxmox-schema crate, by
+ pulling in any additional dependencies needed by that feature.
+
+Package: librust-proxmox-schema+upid-api-impl-dev
+Architecture: any
+Multi-Arch: same
+Depends:
+ ${misc:Depends},
+ librust-proxmox-schema-dev (= ${binary:Version}),
+ librust-libc-0.2+default-dev,
+ librust-nix-0.19+default-dev
+Provides:
+ librust-proxmox-schema-1+upid-api-impl-dev (= ${binary:Version}),
+ librust-proxmox-schema-1.0+upid-api-impl-dev (= ${binary:Version}),
+ librust-proxmox-schema-1.0.0+upid-api-impl-dev (= ${binary:Version})
+Description: Proxmox api schema and validation - feature "upid-api-impl"
+ This metapackage enables feature "upid-api-impl" for the Rust proxmox-schema
+ crate, by pulling in any additional dependencies needed by that feature.
--- /dev/null
+Copyright (C) 2021 Proxmox Server Solutions GmbH
+
+This software is written by Proxmox Server Solutions GmbH <support@proxmox.com>
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU Affero General Public License as published by
+the Free Software Foundation, either version 3 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU Affero General Public License for more details.
+
+You should have received a copy of the GNU Affero General Public License
+along with this program. If not, see <http://www.gnu.org/licenses/>.
--- /dev/null
+overlay = "."
+crate_src_path = ".."
+maintainer = "Proxmox Support Team <support@proxmox.com>"
+
+[source]
+vcs_git = "git://git.proxmox.com/git/proxmox.git"
+vcs_browser = "https://git.proxmox.com/?p=proxmox.git"
--- /dev/null
+/// Helper macro to generate a simple string type wrapper.
+///
+/// This is meant to be used with an API-type tuple struct containing a single `String` like this:
+///
+/// ```
+/// # use proxmox_schema::{api_string_type, ApiStringFormat};
+/// # use proxmox_api_macro::api;
+/// # const PROXMOX_SAFE_ID_FORMAT: ApiStringFormat = ApiStringFormat::Enum(&[]);
+/// use serde::{Deserialize, Serialize};
+///
+/// api_string_type! {
+/// #[api(format: &PROXMOX_SAFE_ID_FORMAT)]
+/// /// ACME account name.
+/// #[derive(Debug, Clone, Eq, PartialEq, Hash, Deserialize, Serialize)]
+/// #[serde(transparent)]
+/// pub struct AccountName(String);
+/// }
+/// ```
+///
+/// This will automatically implements:
+/// * `Display` as a pass-through to `String`'s `Display`
+/// * `Deref`
+/// * `DerefMut`
+/// * `AsRef<str>`
+/// * `TryFrom<String>`
+/// * `fn into_string(self) -> String`
+/// * `fn as_str(&self) -> &str`
+/// * `fn from_string(inner: String) -> Result<Self, anyhow::Error>` using
+/// `StringSchema::check_constraints`.
+/// * `unsafe fn from_string_unchecked(inner: String) -> Self`
+#[macro_export]
+macro_rules! api_string_type {
+ (
+ $(#[$doc:meta])*
+ $vis:vis struct $name:ident(String);
+ ) => (
+ $(#[$doc])*
+ $vis struct $name(String);
+
+ impl ::std::ops::Deref for $name {
+ type Target = str;
+
+ #[inline]
+ fn deref(&self) -> &str {
+ &self.0
+ }
+ }
+
+ impl ::std::ops::DerefMut for $name {
+ #[inline]
+ fn deref_mut(&mut self) -> &mut str {
+ &mut self.0
+ }
+ }
+
+ impl AsRef<str> for $name {
+ #[inline]
+ fn as_ref(&self) -> &str {
+ self.0.as_ref()
+ }
+ }
+
+ impl ::std::convert::TryFrom<String> for $name {
+ type Error = ::anyhow::Error;
+
+ fn try_from(inner: String) -> Result<Self, ::anyhow::Error> {
+ Self::from_string(inner)
+ }
+ }
+
+ impl $name {
+ /// Get the contained string.
+ pub fn into_string(self) -> String {
+ self.0
+ }
+
+ /// Get the string as slice.
+ pub fn as_str(&self) -> &str {
+ self.0.as_str()
+ }
+
+ /// Create an instance directly from a `String`.
+ ///
+ /// # Safety
+ ///
+ /// It is the caller's job to have validated the contents.
+ /// While there are no memory safety issues, a wrong string can cause API calls to
+ /// fail parameter validation.
+ pub unsafe fn from_string_unchecked(name: String) -> Self {
+ Self(name)
+ }
+
+ /// Create an instance directly from a `String`, validating it using the API schema's
+ /// [`check_constraints`](::proxmox_schema::StringSchema::check_constraints())
+ /// method.
+ pub fn from_string(inner: String) -> Result<Self, ::anyhow::Error> {
+ use $crate::ApiType;
+ match &Self::API_SCHEMA {
+ $crate::Schema::String(s) => s.check_constraints(&inner)?,
+ _ => unreachable!(),
+ }
+ Ok(Self(inner))
+ }
+ }
+
+ impl ::std::fmt::Display for $name {
+ #[inline]
+ fn fmt(&self, f: &mut ::std::fmt::Formatter) -> ::std::fmt::Result {
+ ::std::fmt::Display::fmt(&self.0, f)
+ }
+ }
+ );
+}
--- /dev/null
+use std::fmt;
+
+/// Helper to represent const regular expressions
+///
+/// The current Regex::new() function is not `const_fn`. Unless that
+/// works, we use `ConstRegexPattern` to represent static regular
+/// expressions. Please use the `const_regex` macro to generate
+/// instances of this type (uses lazy_static).
+pub struct ConstRegexPattern {
+ /// This is only used for documentation and debugging
+ pub regex_string: &'static str,
+ /// This function return the the actual Regex
+ pub regex_obj: fn() -> &'static regex::Regex,
+}
+
+impl fmt::Debug for ConstRegexPattern {
+ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+ write!(f, "{:?}", self.regex_string)
+ }
+}
+
+impl std::ops::Deref for ConstRegexPattern {
+ type Target = regex::Regex;
+
+ fn deref(&self) -> &Self::Target {
+ (self.regex_obj)()
+ }
+}
+
+/// Macro to generate a ConstRegexPattern
+///
+/// ```
+/// use proxmox_schema::const_regex;
+///
+/// const_regex!{
+/// FILE_EXTENSION_REGEX = r".*\.([a-zA-Z]+)$";
+/// pub SHA256_HEX_REGEX = r"^[a-f0-9]{64}$";
+/// }
+/// ```
+#[macro_export]
+macro_rules! const_regex {
+ ($(
+ $(#[$attr:meta])*
+ $vis:vis $name:ident = $regex:expr;
+ )+) => { $(
+ $(#[$attr])* $vis const $name: $crate::ConstRegexPattern =
+ $crate::ConstRegexPattern {
+ regex_string: $regex,
+ regex_obj: (|| -> &'static ::regex::Regex {
+ $crate::semver_exempt::lazy_static! {
+ static ref SCHEMA: ::regex::Regex = ::regex::Regex::new($regex).unwrap();
+ }
+ &SCHEMA
+ })
+ };
+ )+ };
+}
+
+#[cfg(feature = "test-harness")]
+impl Eq for ConstRegexPattern {}
+
+#[cfg(feature = "test-harness")]
+impl PartialEq for ConstRegexPattern {
+ fn eq(&self, rhs: &Self) -> bool {
+ self.regex_string == rhs.regex_string
+ }
+}
--- /dev/null
+//! Partial object deserialization by extracting object portions from a Value using an api schema.
+
+use std::fmt;
+
+use serde::de::{self, IntoDeserializer, Visitor};
+use serde_json::Value;
+
+use crate::{ObjectSchemaType, Schema};
+
+pub struct Error {
+ msg: String,
+}
+
+impl fmt::Debug for Error {
+ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+ fmt::Debug::fmt(&self.msg, f)
+ }
+}
+
+impl fmt::Display for Error {
+ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+ fmt::Display::fmt(&self.msg, f)
+ }
+}
+
+impl std::error::Error for Error {}
+
+impl serde::de::Error for Error {
+ fn custom<T: fmt::Display>(msg: T) -> Self {
+ Self {
+ msg: msg.to_string(),
+ }
+ }
+}
+
+impl From<serde_json::Error> for Error {
+ fn from(error: serde_json::Error) -> Self {
+ Error {
+ msg: error.to_string(),
+ }
+ }
+}
+
+pub struct ExtractValueDeserializer<'o> {
+ object: &'o mut serde_json::Map<String, Value>,
+ schema: &'static Schema,
+}
+
+impl<'o> ExtractValueDeserializer<'o> {
+ pub fn try_new(
+ object: &'o mut serde_json::Map<String, Value>,
+ schema: &'static Schema,
+ ) -> Option<Self> {
+ match schema {
+ Schema::Object(_) | Schema::AllOf(_) => Some(Self { object, schema }),
+ _ => None,
+ }
+ }
+}
+
+macro_rules! deserialize_non_object {
+ ($name:ident) => {
+ fn $name<V>(self, _visitor: V) -> Result<V::Value, Self::Error>
+ where
+ V: Visitor<'de>,
+ {
+ Err(de::Error::custom(
+ "deserializing partial object into type which is not an object",
+ ))
+ }
+ };
+ ($name:ident ( $($args:tt)* )) => {
+ fn $name<V>(self, $($args)*, _visitor: V) -> Result<V::Value, Self::Error>
+ where
+ V: Visitor<'de>,
+ {
+ Err(de::Error::custom(
+ "deserializing partial object into type which is not an object",
+ ))
+ }
+ };
+}
+
+impl<'de> de::Deserializer<'de> for ExtractValueDeserializer<'de> {
+ type Error = Error;
+
+ fn deserialize_any<V>(self, visitor: V) -> Result<V::Value, Error>
+ where
+ V: Visitor<'de>,
+ {
+ self.deserialize_map(visitor)
+ }
+
+ fn deserialize_map<V>(self, visitor: V) -> Result<V::Value, Error>
+ where
+ V: Visitor<'de>,
+ {
+ use serde::de::Error;
+
+ match self.schema {
+ Schema::Object(schema) => visitor.visit_map(MapAccess::<'de>::new(
+ self.object,
+ schema.properties().map(|(name, _, _)| *name),
+ )),
+ Schema::AllOf(schema) => visitor.visit_map(MapAccess::<'de>::new(
+ self.object,
+ schema.properties().map(|(name, _, _)| *name),
+ )),
+
+ // The following should be caught by ExtractValueDeserializer::new()!
+ _ => Err(Error::custom(
+ "ExtractValueDeserializer used with invalid schema",
+ )),
+ }
+ }
+
+ fn deserialize_struct<V>(
+ self,
+ _name: &'static str,
+ _fields: &'static [&'static str],
+ visitor: V,
+ ) -> Result<V::Value, Error>
+ where
+ V: Visitor<'de>,
+ {
+ use serde::de::Error;
+
+ match self.schema {
+ Schema::Object(schema) => visitor.visit_map(MapAccess::<'de>::new(
+ self.object,
+ schema.properties().map(|(name, _, _)| *name),
+ )),
+ Schema::AllOf(schema) => visitor.visit_map(MapAccess::<'de>::new(
+ self.object,
+ schema.properties().map(|(name, _, _)| *name),
+ )),
+
+ // The following should be caught by ExtractValueDeserializer::new()!
+ _ => Err(Error::custom(
+ "ExtractValueDeserializer used with invalid schema",
+ )),
+ }
+ }
+
+ deserialize_non_object!(deserialize_i8);
+ deserialize_non_object!(deserialize_i16);
+ deserialize_non_object!(deserialize_i32);
+ deserialize_non_object!(deserialize_i64);
+ deserialize_non_object!(deserialize_u8);
+ deserialize_non_object!(deserialize_u16);
+ deserialize_non_object!(deserialize_u32);
+ deserialize_non_object!(deserialize_u64);
+ deserialize_non_object!(deserialize_f32);
+ deserialize_non_object!(deserialize_f64);
+ deserialize_non_object!(deserialize_char);
+ deserialize_non_object!(deserialize_bool);
+ deserialize_non_object!(deserialize_str);
+ deserialize_non_object!(deserialize_string);
+ deserialize_non_object!(deserialize_bytes);
+ deserialize_non_object!(deserialize_byte_buf);
+ deserialize_non_object!(deserialize_option);
+ deserialize_non_object!(deserialize_seq);
+ deserialize_non_object!(deserialize_unit);
+ deserialize_non_object!(deserialize_identifier);
+ deserialize_non_object!(deserialize_unit_struct(_: &'static str));
+ deserialize_non_object!(deserialize_newtype_struct(_: &'static str));
+ deserialize_non_object!(deserialize_tuple(_: usize));
+ deserialize_non_object!(deserialize_tuple_struct(_: &'static str, _: usize));
+ deserialize_non_object!(deserialize_enum(
+ _: &'static str,
+ _: &'static [&'static str]
+ ));
+
+ fn deserialize_ignored_any<V>(self, visitor: V) -> Result<V::Value, Self::Error>
+ where
+ V: Visitor<'de>,
+ {
+ visitor.visit_unit()
+ }
+}
+
+struct MapAccess<'o, I> {
+ object: &'o mut serde_json::Map<String, Value>,
+ iter: I,
+ value: Option<Value>,
+}
+
+impl<'o, I> MapAccess<'o, I>
+where
+ I: Iterator<Item = &'static str>,
+{
+ fn new(object: &'o mut serde_json::Map<String, Value>, iter: I) -> Self {
+ Self {
+ object,
+ iter,
+ value: None,
+ }
+ }
+}
+
+impl<'de, I> de::MapAccess<'de> for MapAccess<'de, I>
+where
+ I: Iterator<Item = &'static str>,
+{
+ type Error = Error;
+
+ fn next_key_seed<K>(&mut self, seed: K) -> Result<Option<K::Value>, Error>
+ where
+ K: de::DeserializeSeed<'de>,
+ {
+ loop {
+ return match self.iter.next() {
+ Some(key) => match self.object.remove(key) {
+ Some(value) => {
+ self.value = Some(value);
+ seed.deserialize(key.into_deserializer()).map(Some)
+ }
+ None => continue,
+ },
+ None => Ok(None),
+ };
+ }
+ }
+
+ fn next_value_seed<V>(&mut self, seed: V) -> Result<V::Value, Error>
+ where
+ V: de::DeserializeSeed<'de>,
+ {
+ match self.value.take() {
+ Some(value) => seed.deserialize(value).map_err(Error::from),
+ None => Err(de::Error::custom("value is missing")),
+ }
+ }
+}
+
+#[test]
+fn test_extraction() {
+ use serde::Deserialize;
+
+ use crate::{ObjectSchema, StringSchema};
+
+ #[derive(Deserialize)]
+ struct Foo {
+ foo1: String,
+ foo2: String,
+ }
+
+ const SIMPLE_STRING: Schema = StringSchema::new("simple").schema();
+ const FOO_SCHEMA: Schema = ObjectSchema::new(
+ "A Foo",
+ &[
+ ("foo1", false, &SIMPLE_STRING),
+ ("foo2", false, &SIMPLE_STRING),
+ ],
+ )
+ .schema();
+
+ #[derive(Deserialize)]
+ struct Bar {
+ bar1: String,
+ bar2: String,
+ }
+
+ const BAR_SCHEMA: Schema = ObjectSchema::new(
+ "A Bar",
+ &[
+ ("bar1", false, &SIMPLE_STRING),
+ ("bar2", false, &SIMPLE_STRING),
+ ],
+ )
+ .schema();
+
+ let mut data = serde_json::json!({
+ "foo1": "hey1",
+ "foo2": "hey2",
+ "bar1": "there1",
+ "bar2": "there2",
+ });
+
+ let data = data.as_object_mut().unwrap();
+
+ let foo: Foo =
+ Foo::deserialize(ExtractValueDeserializer::try_new(data, &FOO_SCHEMA).unwrap()).unwrap();
+
+ assert!(data.remove("foo1").is_none());
+ assert!(data.remove("foo2").is_none());
+ assert_eq!(foo.foo1, "hey1");
+ assert_eq!(foo.foo2, "hey2");
+
+ let bar =
+ Bar::deserialize(ExtractValueDeserializer::try_new(data, &BAR_SCHEMA).unwrap()).unwrap();
+
+ assert!(data.is_empty());
+ assert_eq!(bar.bar1, "there1");
+ assert_eq!(bar.bar2, "there2");
+}
--- /dev/null
+//! Module to generate and format API Documenation
+
+use anyhow::{bail, Error};
+
+use crate::*;
+
+/// Enumerate different styles to display parameters/properties.
+#[derive(Copy, Clone, PartialEq)]
+pub enum ParameterDisplayStyle {
+ /// Used for properties in configuration files: ``key:``
+ Config,
+ /// Used for PropertyStings properties in configuration files
+ ConfigSub,
+ /// Used for command line options: ``--key``
+ Arg,
+ /// Used for command line options passed as arguments: ``<key>``
+ Fixed,
+}
+
+/// CLI usage information format.
+#[derive(Copy, Clone, PartialEq)]
+pub enum DocumentationFormat {
+ /// Text, command line only (one line).
+ Short,
+ /// Text, list all options.
+ Long,
+ /// Text, include description.
+ Full,
+ /// Like full, but in reStructuredText format.
+ ReST,
+}
+
+/// Line wrapping to form simple list of paragraphs.
+pub fn wrap_text(
+ initial_indent: &str,
+ subsequent_indent: &str,
+ text: &str,
+ columns: usize,
+) -> String {
+ let wrapper1 = textwrap::Wrapper::new(columns)
+ .initial_indent(initial_indent)
+ .subsequent_indent(subsequent_indent);
+
+ let wrapper2 = textwrap::Wrapper::new(columns)
+ .initial_indent(subsequent_indent)
+ .subsequent_indent(subsequent_indent);
+
+ text.split("\n\n")
+ .map(|p| p.trim())
+ .filter(|p| !p.is_empty())
+ .fold(String::new(), |mut acc, p| {
+ if acc.is_empty() {
+ acc.push_str(&wrapper1.wrap(p).join("\n"));
+ } else {
+ acc.push_str(&wrapper2.wrap(p).join("\n"));
+ }
+ acc.push_str("\n\n");
+ acc
+ })
+}
+
+#[test]
+fn test_wrap_text() {
+ let text = "Command. This may be a list in order to spefify nested sub-commands.";
+ let expect = " Command. This may be a list in order to spefify nested sub-\n commands.\n\n";
+
+ let indent = " ";
+ let wrapped = wrap_text(indent, indent, text, 80);
+
+ assert_eq!(wrapped, expect);
+}
+
+fn get_simple_type_text(schema: &Schema, list_enums: bool) -> String {
+ match schema {
+ Schema::Null => String::from("<null>"), // should not happen
+ Schema::Boolean(_) => String::from("<1|0>"),
+ Schema::Integer(_) => String::from("<integer>"),
+ Schema::Number(_) => String::from("<number>"),
+ Schema::String(string_schema) => match string_schema {
+ StringSchema {
+ type_text: Some(type_text),
+ ..
+ } => String::from(*type_text),
+ StringSchema {
+ format: Some(ApiStringFormat::Enum(variants)),
+ ..
+ } => {
+ if list_enums && variants.len() <= 3 {
+ let list: Vec<String> =
+ variants.iter().map(|e| String::from(e.value)).collect();
+ list.join("|")
+ } else {
+ String::from("<enum>")
+ }
+ }
+ _ => String::from("<string>"),
+ },
+ _ => panic!("get_simple_type_text: expected simple type"),
+ }
+}
+
+/// Generate ReST Documentaion for object properties
+pub fn dump_properties(
+ param: &dyn ObjectSchemaType,
+ indent: &str,
+ style: ParameterDisplayStyle,
+ skip: &[&str],
+) -> String {
+ let mut res = String::new();
+ let next_indent = format!(" {}", indent);
+
+ let mut required_list: Vec<String> = Vec::new();
+ let mut optional_list: Vec<String> = Vec::new();
+
+ for (prop, optional, schema) in param.properties() {
+ if skip.iter().find(|n| n == &prop).is_some() {
+ continue;
+ }
+
+ let mut param_descr =
+ get_property_description(prop, &schema, style, DocumentationFormat::ReST);
+
+ if !indent.is_empty() {
+ param_descr = format!("{}{}", indent, param_descr); // indent first line
+ param_descr = param_descr.replace("\n", &format!("\n{}", indent)); // indent rest
+ }
+
+ if style == ParameterDisplayStyle::Config {
+ match schema {
+ Schema::String(StringSchema {
+ format: Some(ApiStringFormat::PropertyString(sub_schema)),
+ ..
+ }) => {
+ match sub_schema {
+ Schema::Object(object_schema) => {
+ let sub_text = dump_properties(
+ object_schema,
+ &next_indent,
+ ParameterDisplayStyle::ConfigSub,
+ &[],
+ );
+ param_descr.push_str(&sub_text);
+ }
+ Schema::Array(_) => {
+ // do nothing - description should explain the list type
+ }
+ _ => unreachable!(),
+ }
+ }
+ _ => { /* do nothing */ }
+ }
+ }
+ if *optional {
+ optional_list.push(param_descr);
+ } else {
+ required_list.push(param_descr);
+ }
+ }
+
+ if !required_list.is_empty() {
+ if style != ParameterDisplayStyle::ConfigSub {
+ res.push_str("\n*Required properties:*\n\n");
+ }
+
+ for text in required_list {
+ res.push_str(&text);
+ res.push('\n');
+ }
+ }
+
+ if !optional_list.is_empty() {
+ if style != ParameterDisplayStyle::ConfigSub {
+ res.push_str("\n*Optional properties:*\n\n");
+ }
+
+ for text in optional_list {
+ res.push_str(&text);
+ res.push('\n');
+ }
+ }
+
+ res
+}
+
+/// Helper to format an object property, including name, type and description.
+pub fn get_property_description(
+ name: &str,
+ schema: &Schema,
+ style: ParameterDisplayStyle,
+ format: DocumentationFormat,
+) -> String {
+ let type_text = get_schema_type_text(schema, style);
+
+ let (descr, default, extra) = match schema {
+ Schema::Null => ("null", None, None),
+ Schema::String(ref schema) => (
+ schema.description,
+ schema.default.map(|v| v.to_owned()),
+ None,
+ ),
+ Schema::Boolean(ref schema) => (
+ schema.description,
+ schema.default.map(|v| v.to_string()),
+ None,
+ ),
+ Schema::Integer(ref schema) => (
+ schema.description,
+ schema.default.map(|v| v.to_string()),
+ None,
+ ),
+ Schema::Number(ref schema) => (
+ schema.description,
+ schema.default.map(|v| v.to_string()),
+ None,
+ ),
+ Schema::Object(ref schema) => (schema.description, None, None),
+ Schema::AllOf(ref schema) => (schema.description, None, None),
+ Schema::Array(ref schema) => (
+ schema.description,
+ None,
+ Some(String::from("Can be specified more than once.")),
+ ),
+ };
+
+ let default_text = match default {
+ Some(text) => format!(" (default={})", text),
+ None => String::new(),
+ };
+
+ let descr = match extra {
+ Some(extra) => format!("{} {}", descr, extra),
+ None => String::from(descr),
+ };
+
+ if format == DocumentationFormat::ReST {
+ let mut text = match style {
+ ParameterDisplayStyle::Config => {
+ // reST definition list format
+ format!("``{}`` : ``{}{}``\n ", name, type_text, default_text)
+ }
+ ParameterDisplayStyle::ConfigSub => {
+ // reST definition list format
+ format!("``{}`` = ``{}{}``\n ", name, type_text, default_text)
+ }
+ ParameterDisplayStyle::Arg => {
+ // reST option list format
+ format!("``--{}`` ``{}{}``\n ", name, type_text, default_text)
+ }
+ ParameterDisplayStyle::Fixed => {
+ format!("``<{}>`` : ``{}{}``\n ", name, type_text, default_text)
+ }
+ };
+
+ text.push_str(&wrap_text("", " ", &descr, 80));
+ text.push('\n');
+
+ text
+ } else {
+ let display_name = match style {
+ ParameterDisplayStyle::Config => format!("{}:", name),
+ ParameterDisplayStyle::ConfigSub => format!("{}=", name),
+ ParameterDisplayStyle::Arg => format!("--{}", name),
+ ParameterDisplayStyle::Fixed => format!("<{}>", name),
+ };
+
+ let mut text = format!(" {:-10} {}{}", display_name, type_text, default_text);
+ let indent = " ";
+ text.push('\n');
+ text.push_str(&wrap_text(indent, indent, &descr, 80));
+
+ text
+ }
+}
+
+/// Helper to format the type text
+///
+/// The result is a short string including important constraints, for
+/// example ``<integer> (0 - N)``.
+pub fn get_schema_type_text(schema: &Schema, _style: ParameterDisplayStyle) -> String {
+ match schema {
+ Schema::Null => String::from("<null>"), // should not happen
+ Schema::String(string_schema) => {
+ match string_schema {
+ StringSchema {
+ type_text: Some(type_text),
+ ..
+ } => String::from(*type_text),
+ StringSchema {
+ format: Some(ApiStringFormat::Enum(variants)),
+ ..
+ } => {
+ let list: Vec<String> =
+ variants.iter().map(|e| String::from(e.value)).collect();
+ list.join("|")
+ }
+ // displaying regex add more confision than it helps
+ //StringSchema { format: Some(ApiStringFormat::Pattern(const_regex)), .. } => {
+ // format!("/{}/", const_regex.regex_string)
+ //}
+ StringSchema {
+ format: Some(ApiStringFormat::PropertyString(sub_schema)),
+ ..
+ } => get_property_string_type_text(sub_schema),
+ _ => String::from("<string>"),
+ }
+ }
+ Schema::Boolean(_) => String::from("<boolean>"),
+ Schema::Integer(integer_schema) => match (integer_schema.minimum, integer_schema.maximum) {
+ (Some(min), Some(max)) => format!("<integer> ({} - {})", min, max),
+ (Some(min), None) => format!("<integer> ({} - N)", min),
+ (None, Some(max)) => format!("<integer> (-N - {})", max),
+ _ => String::from("<integer>"),
+ },
+ Schema::Number(number_schema) => match (number_schema.minimum, number_schema.maximum) {
+ (Some(min), Some(max)) => format!("<number> ({} - {})", min, max),
+ (Some(min), None) => format!("<number> ({} - N)", min),
+ (None, Some(max)) => format!("<number> (-N - {})", max),
+ _ => String::from("<number>"),
+ },
+ Schema::Object(_) => String::from("<object>"),
+ Schema::Array(schema) => get_schema_type_text(schema.items, _style),
+ Schema::AllOf(_) => String::from("<object>"),
+ }
+}
+
+pub fn get_property_string_type_text(schema: &Schema) -> String {
+ match schema {
+ Schema::Object(object_schema) => get_object_type_text(object_schema),
+ Schema::Array(array_schema) => {
+ let item_type = get_simple_type_text(array_schema.items, true);
+ format!("[{}, ...]", item_type)
+ }
+ _ => panic!("get_property_string_type_text: expected array or object"),
+ }
+}
+
+fn get_object_type_text(object_schema: &ObjectSchema) -> String {
+ let mut parts = Vec::new();
+
+ let mut add_part = |name, optional, schema| {
+ let tt = get_simple_type_text(schema, false);
+ let text = if parts.is_empty() {
+ format!("{}={}", name, tt)
+ } else {
+ format!(",{}={}", name, tt)
+ };
+ if optional {
+ parts.push(format!("[{}]", text));
+ } else {
+ parts.push(text);
+ }
+ };
+
+ // add default key first
+ if let Some(ref default_key) = object_schema.default_key {
+ let (optional, schema) = object_schema.lookup(default_key).unwrap();
+ add_part(default_key, optional, schema);
+ }
+
+ // add required keys
+ for (name, optional, schema) in object_schema.properties {
+ if *optional {
+ continue;
+ }
+ if let Some(ref default_key) = object_schema.default_key {
+ if name == default_key {
+ continue;
+ }
+ }
+ add_part(name, *optional, schema);
+ }
+
+ // add options keys
+ for (name, optional, schema) in object_schema.properties {
+ if !*optional {
+ continue;
+ }
+ if let Some(ref default_key) = object_schema.default_key {
+ if name == default_key {
+ continue;
+ }
+ }
+ add_part(name, *optional, schema);
+ }
+
+ let mut type_text = String::new();
+ type_text.push('[');
+ type_text.push_str(&parts.join(" "));
+ type_text.push(']');
+ type_text
+}
+
+/// Generate ReST Documentaion for enumeration.
+pub fn dump_enum_properties(schema: &Schema) -> Result<String, Error> {
+ let mut res = String::new();
+
+ if let Schema::String(StringSchema {
+ format: Some(ApiStringFormat::Enum(variants)),
+ ..
+ }) = schema
+ {
+ for item in variants.iter() {
+ res.push_str(&format!(":``{}``: ", item.value));
+ let descr = wrap_text("", " ", item.description, 80);
+ res.push_str(&descr);
+ res.push('\n');
+ }
+ return Ok(res);
+ }
+
+ bail!("dump_enum_properties failed - not an enum");
+}
+
+pub fn dump_api_return_schema(returns: &ReturnType, style: ParameterDisplayStyle) -> String {
+ let schema = &returns.schema;
+
+ let mut res = if returns.optional {
+ "*Returns* (optionally): ".to_string()
+ } else {
+ "*Returns*: ".to_string()
+ };
+
+ let type_text = get_schema_type_text(schema, style);
+ res.push_str(&format!("**{}**\n\n", type_text));
+
+ match schema {
+ Schema::Null => {
+ return res;
+ }
+ Schema::Boolean(schema) => {
+ let description = wrap_text("", "", schema.description, 80);
+ res.push_str(&description);
+ }
+ Schema::Integer(schema) => {
+ let description = wrap_text("", "", schema.description, 80);
+ res.push_str(&description);
+ }
+ Schema::Number(schema) => {
+ let description = wrap_text("", "", schema.description, 80);
+ res.push_str(&description);
+ }
+ Schema::String(schema) => {
+ let description = wrap_text("", "", schema.description, 80);
+ res.push_str(&description);
+ }
+ Schema::Array(schema) => {
+ let description = wrap_text("", "", schema.description, 80);
+ res.push_str(&description);
+ }
+ Schema::Object(obj_schema) => {
+ let description = wrap_text("", "", obj_schema.description, 80);
+ res.push_str(&description);
+ res.push_str(&dump_properties(obj_schema, "", style, &[]));
+ }
+ Schema::AllOf(all_of_schema) => {
+ let description = wrap_text("", "", all_of_schema.description, 80);
+ res.push_str(&description);
+ res.push_str(&dump_properties(all_of_schema, "", style, &[]));
+ }
+ }
+
+ res.push('\n');
+
+ res
+}
--- /dev/null
+//! Proxmox schema module.
+//!
+//! This provides utilities to define APIs in a declarative way using
+//! Schemas. Primary use case it to define REST/HTTP APIs. Another use case
+//! is to define command line tools using Schemas. Finally, it is
+//! possible to use schema definitions to derive configuration file
+//! parsers.
+
+#[cfg(feature = "api-macro")]
+pub use proxmox_api_macro::api;
+
+mod api_type_macros;
+
+mod const_regex;
+pub use const_regex::ConstRegexPattern;
+
+pub mod de;
+pub mod format;
+
+mod schema;
+pub use schema::*;
+
+pub mod upid;
+
+// const_regex uses lazy_static, but we otherwise don't need it, and don't want to force users to
+// have to write it out in their Cargo.toml as dependency, so we add a hidden re-export here which
+// is semver-exempt!
+#[doc(hidden)]
+pub mod semver_exempt {
+ pub use lazy_static::lazy_static;
+}
--- /dev/null
+//! Data types to decscribe data types.
+//!
+//! This is loosly based on JSON Schema, but uses static RUST data
+//! types. This way we can build completely static API
+//! definitions included with the programs read-only text segment.
+
+use std::fmt;
+
+use anyhow::{bail, format_err, Error};
+use serde_json::{json, Value};
+
+use crate::ConstRegexPattern;
+
+/// Error type for schema validation
+///
+/// The validation functions may produce several error message,
+/// i.e. when validation objects, it can produce one message for each
+/// erroneous object property.
+#[derive(Default, Debug)]
+pub struct ParameterError {
+ error_list: Vec<(String, Error)>,
+}
+
+impl std::error::Error for ParameterError {}
+
+impl ParameterError {
+ pub fn new() -> Self {
+ Self {
+ error_list: Vec::new(),
+ }
+ }
+
+ pub fn push(&mut self, name: String, value: Error) {
+ self.error_list.push((name, value));
+ }
+
+ pub fn len(&self) -> usize {
+ self.error_list.len()
+ }
+
+ pub fn errors(&self) -> &[(String, Error)] {
+ &self.error_list
+ }
+
+ pub fn is_empty(&self) -> bool {
+ self.len() == 0
+ }
+
+ pub fn add_errors(&mut self, prefix: &str, err: Error) {
+ if let Some(param_err) = err.downcast_ref::<ParameterError>() {
+ for (sub_key, sub_err) in param_err.errors().iter() {
+ self.push(
+ format!("{}/{}", prefix, sub_key),
+ format_err!("{}", sub_err),
+ );
+ }
+ } else {
+ self.push(prefix.to_string(), err);
+ }
+ }
+}
+
+impl fmt::Display for ParameterError {
+ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+ let mut msg = String::new();
+
+ if !self.is_empty() {
+ msg.push_str("parameter verification errors\n\n");
+ }
+
+ for (name, err) in self.error_list.iter() {
+ msg.push_str(&format!("parameter '{}': {}\n", name, err));
+ }
+
+ write!(f, "{}", msg)
+ }
+}
+
+/// Data type to describe boolean values
+#[derive(Debug)]
+#[cfg_attr(feature = "test-harness", derive(Eq, PartialEq))]
+pub struct BooleanSchema {
+ pub description: &'static str,
+ /// Optional default value.
+ pub default: Option<bool>,
+}
+
+impl BooleanSchema {
+ pub const fn new(description: &'static str) -> Self {
+ BooleanSchema {
+ description,
+ default: None,
+ }
+ }
+
+ pub const fn default(mut self, default: bool) -> Self {
+ self.default = Some(default);
+ self
+ }
+
+ pub const fn schema(self) -> Schema {
+ Schema::Boolean(self)
+ }
+}
+
+/// Data type to describe integer values.
+#[derive(Debug)]
+#[cfg_attr(feature = "test-harness", derive(Eq, PartialEq))]
+pub struct IntegerSchema {
+ pub description: &'static str,
+ /// Optional minimum.
+ pub minimum: Option<isize>,
+ /// Optional maximum.
+ pub maximum: Option<isize>,
+ /// Optional default.
+ pub default: Option<isize>,
+}
+
+impl IntegerSchema {
+ pub const fn new(description: &'static str) -> Self {
+ IntegerSchema {
+ description,
+ default: None,
+ minimum: None,
+ maximum: None,
+ }
+ }
+
+ pub const fn default(mut self, default: isize) -> Self {
+ self.default = Some(default);
+ self
+ }
+
+ pub const fn minimum(mut self, minimum: isize) -> Self {
+ self.minimum = Some(minimum);
+ self
+ }
+
+ pub const fn maximum(mut self, maximium: isize) -> Self {
+ self.maximum = Some(maximium);
+ self
+ }
+
+ pub const fn schema(self) -> Schema {
+ Schema::Integer(self)
+ }
+
+ fn check_constraints(&self, value: isize) -> Result<(), Error> {
+ if let Some(minimum) = self.minimum {
+ if value < minimum {
+ bail!(
+ "value must have a minimum value of {} (got {})",
+ minimum,
+ value
+ );
+ }
+ }
+
+ if let Some(maximum) = self.maximum {
+ if value > maximum {
+ bail!(
+ "value must have a maximum value of {} (got {})",
+ maximum,
+ value
+ );
+ }
+ }
+
+ Ok(())
+ }
+}
+
+/// Data type to describe (JSON like) number value
+#[derive(Debug)]
+pub struct NumberSchema {
+ pub description: &'static str,
+ /// Optional minimum.
+ pub minimum: Option<f64>,
+ /// Optional maximum.
+ pub maximum: Option<f64>,
+ /// Optional default.
+ pub default: Option<f64>,
+}
+
+impl NumberSchema {
+ pub const fn new(description: &'static str) -> Self {
+ NumberSchema {
+ description,
+ default: None,
+ minimum: None,
+ maximum: None,
+ }
+ }
+
+ pub const fn default(mut self, default: f64) -> Self {
+ self.default = Some(default);
+ self
+ }
+
+ pub const fn minimum(mut self, minimum: f64) -> Self {
+ self.minimum = Some(minimum);
+ self
+ }
+
+ pub const fn maximum(mut self, maximium: f64) -> Self {
+ self.maximum = Some(maximium);
+ self
+ }
+
+ pub const fn schema(self) -> Schema {
+ Schema::Number(self)
+ }
+
+ fn check_constraints(&self, value: f64) -> Result<(), Error> {
+ if let Some(minimum) = self.minimum {
+ if value < minimum {
+ bail!(
+ "value must have a minimum value of {} (got {})",
+ minimum,
+ value
+ );
+ }
+ }
+
+ if let Some(maximum) = self.maximum {
+ if value > maximum {
+ bail!(
+ "value must have a maximum value of {} (got {})",
+ maximum,
+ value
+ );
+ }
+ }
+
+ Ok(())
+ }
+}
+
+#[cfg(feature = "test-harness")]
+impl Eq for NumberSchema {}
+
+#[cfg(feature = "test-harness")]
+impl PartialEq for NumberSchema {
+ fn eq(&self, rhs: &Self) -> bool {
+ fn f64_eq(l: Option<f64>, r: Option<f64>) -> bool {
+ match (l, r) {
+ (None, None) => true,
+ (Some(l), Some(r)) => (l - r).abs() < 0.0001,
+ _ => false,
+ }
+ }
+
+ self.description == rhs.description
+ && f64_eq(self.minimum, rhs.minimum)
+ && f64_eq(self.maximum, rhs.maximum)
+ && f64_eq(self.default, rhs.default)
+ }
+}
+
+/// Data type to describe string values.
+#[derive(Debug)]
+#[cfg_attr(feature = "test-harness", derive(Eq, PartialEq))]
+pub struct StringSchema {
+ pub description: &'static str,
+ /// Optional default value.
+ pub default: Option<&'static str>,
+ /// Optional minimal length.
+ pub min_length: Option<usize>,
+ /// Optional maximal length.
+ pub max_length: Option<usize>,
+ /// Optional microformat.
+ pub format: Option<&'static ApiStringFormat>,
+ /// A text representation of the format/type (used to generate documentation).
+ pub type_text: Option<&'static str>,
+}
+
+impl StringSchema {
+ pub const fn new(description: &'static str) -> Self {
+ StringSchema {
+ description,
+ default: None,
+ min_length: None,
+ max_length: None,
+ format: None,
+ type_text: None,
+ }
+ }
+
+ pub const fn default(mut self, text: &'static str) -> Self {
+ self.default = Some(text);
+ self
+ }
+
+ pub const fn format(mut self, format: &'static ApiStringFormat) -> Self {
+ self.format = Some(format);
+ self
+ }
+
+ pub const fn type_text(mut self, type_text: &'static str) -> Self {
+ self.type_text = Some(type_text);
+ self
+ }
+
+ pub const fn min_length(mut self, min_length: usize) -> Self {
+ self.min_length = Some(min_length);
+ self
+ }
+
+ pub const fn max_length(mut self, max_length: usize) -> Self {
+ self.max_length = Some(max_length);
+ self
+ }
+
+ pub const fn schema(self) -> Schema {
+ Schema::String(self)
+ }
+
+ fn check_length(&self, length: usize) -> Result<(), Error> {
+ if let Some(min_length) = self.min_length {
+ if length < min_length {
+ bail!("value must be at least {} characters long", min_length);
+ }
+ }
+
+ if let Some(max_length) = self.max_length {
+ if length > max_length {
+ bail!("value may only be {} characters long", max_length);
+ }
+ }
+
+ Ok(())
+ }
+
+ pub fn check_constraints(&self, value: &str) -> Result<(), Error> {
+ self.check_length(value.chars().count())?;
+
+ if let Some(ref format) = self.format {
+ match format {
+ ApiStringFormat::Pattern(regex) => {
+ if !(regex.regex_obj)().is_match(value) {
+ bail!("value does not match the regex pattern");
+ }
+ }
+ ApiStringFormat::Enum(variants) => {
+ if variants.iter().find(|&e| e.value == value).is_none() {
+ bail!("value '{}' is not defined in the enumeration.", value);
+ }
+ }
+ ApiStringFormat::PropertyString(subschema) => {
+ parse_property_string(value, subschema)?;
+ }
+ ApiStringFormat::VerifyFn(verify_fn) => {
+ verify_fn(value)?;
+ }
+ }
+ }
+
+ Ok(())
+ }
+}
+
+/// Data type to describe array of values.
+///
+/// All array elements are of the same type, as defined in the `items`
+/// schema.
+#[derive(Debug)]
+#[cfg_attr(feature = "test-harness", derive(Eq, PartialEq))]
+pub struct ArraySchema {
+ pub description: &'static str,
+ /// Element type schema.
+ pub items: &'static Schema,
+ /// Optional minimal length.
+ pub min_length: Option<usize>,
+ /// Optional maximal length.
+ pub max_length: Option<usize>,
+}
+
+impl ArraySchema {
+ pub const fn new(description: &'static str, item_schema: &'static Schema) -> Self {
+ ArraySchema {
+ description,
+ items: item_schema,
+ min_length: None,
+ max_length: None,
+ }
+ }
+
+ pub const fn min_length(mut self, min_length: usize) -> Self {
+ self.min_length = Some(min_length);
+ self
+ }
+
+ pub const fn max_length(mut self, max_length: usize) -> Self {
+ self.max_length = Some(max_length);
+ self
+ }
+
+ pub const fn schema(self) -> Schema {
+ Schema::Array(self)
+ }
+
+ fn check_length(&self, length: usize) -> Result<(), Error> {
+ if let Some(min_length) = self.min_length {
+ if length < min_length {
+ bail!("array must contain at least {} elements", min_length);
+ }
+ }
+
+ if let Some(max_length) = self.max_length {
+ if length > max_length {
+ bail!("array may only contain {} elements", max_length);
+ }
+ }
+
+ Ok(())
+ }
+}
+
+/// Property entry in an object schema:
+///
+/// - `name`: The name of the property
+/// - `optional`: Set when the property is optional
+/// - `schema`: Property type schema
+pub type SchemaPropertyEntry = (&'static str, bool, &'static Schema);
+
+/// Lookup table to Schema properties
+///
+/// Stores a sorted list of `(name, optional, schema)` tuples:
+///
+/// - `name`: The name of the property
+/// - `optional`: Set when the property is optional
+/// - `schema`: Property type schema
+///
+/// **Note:** The list has to be storted by name, because we use
+/// a binary search to find items.
+///
+/// This is a workaround unless RUST can const_fn `Hash::new()`
+pub type SchemaPropertyMap = &'static [SchemaPropertyEntry];
+
+/// Data type to describe objects (maps).
+#[derive(Debug)]
+#[cfg_attr(feature = "test-harness", derive(Eq, PartialEq))]
+pub struct ObjectSchema {
+ pub description: &'static str,
+ /// If set, allow additional properties which are not defined in
+ /// the schema.
+ pub additional_properties: bool,
+ /// Property schema definitions.
+ pub properties: SchemaPropertyMap,
+ /// Default key name - used by `parse_parameter_string()`
+ pub default_key: Option<&'static str>,
+}
+
+impl ObjectSchema {
+ pub const fn new(description: &'static str, properties: SchemaPropertyMap) -> Self {
+ ObjectSchema {
+ description,
+ properties,
+ additional_properties: false,
+ default_key: None,
+ }
+ }
+
+ pub const fn additional_properties(mut self, additional_properties: bool) -> Self {
+ self.additional_properties = additional_properties;
+ self
+ }
+
+ pub const fn default_key(mut self, key: &'static str) -> Self {
+ self.default_key = Some(key);
+ self
+ }
+
+ pub const fn schema(self) -> Schema {
+ Schema::Object(self)
+ }
+
+ pub fn lookup(&self, key: &str) -> Option<(bool, &Schema)> {
+ if let Ok(ind) = self
+ .properties
+ .binary_search_by_key(&key, |(name, _, _)| name)
+ {
+ let (_name, optional, prop_schema) = self.properties[ind];
+ Some((optional, prop_schema))
+ } else {
+ None
+ }
+ }
+}
+
+/// Combines multiple *object* schemas into one.
+///
+/// Note that these are limited to object schemas. Other schemas will produce errors.
+///
+/// Technically this could also contain an `additional_properties` flag, however, in the JSON
+/// Schema, this is not supported, so here we simply assume additional properties to be allowed.
+#[derive(Debug)]
+#[cfg_attr(feature = "test-harness", derive(Eq, PartialEq))]
+pub struct AllOfSchema {
+ pub description: &'static str,
+
+ /// The parameter is checked against all of the schemas in the list. Note that all schemas must
+ /// be object schemas.
+ pub list: &'static [&'static Schema],
+}
+
+impl AllOfSchema {
+ pub const fn new(description: &'static str, list: &'static [&'static Schema]) -> Self {
+ Self { description, list }
+ }
+
+ pub const fn schema(self) -> Schema {
+ Schema::AllOf(self)
+ }
+
+ pub fn lookup(&self, key: &str) -> Option<(bool, &Schema)> {
+ for entry in self.list {
+ match entry {
+ Schema::AllOf(s) => {
+ if let Some(v) = s.lookup(key) {
+ return Some(v);
+ }
+ }
+ Schema::Object(s) => {
+ if let Some(v) = s.lookup(key) {
+ return Some(v);
+ }
+ }
+ _ => panic!("non-object-schema in `AllOfSchema`"),
+ }
+ }
+
+ None
+ }
+}
+
+/// Beside [`ObjectSchema`] we also have an [`AllOfSchema`] which also represents objects.
+pub trait ObjectSchemaType {
+ fn description(&self) -> &'static str;
+ fn lookup(&self, key: &str) -> Option<(bool, &Schema)>;
+ fn properties(&self) -> ObjectPropertyIterator;
+ fn additional_properties(&self) -> bool;
+}
+
+impl ObjectSchemaType for ObjectSchema {
+ fn description(&self) -> &'static str {
+ self.description
+ }
+
+ fn lookup(&self, key: &str) -> Option<(bool, &Schema)> {
+ ObjectSchema::lookup(self, key)
+ }
+
+ fn properties(&self) -> ObjectPropertyIterator {
+ ObjectPropertyIterator {
+ schemas: [].iter(),
+ properties: Some(self.properties.iter()),
+ nested: None,
+ }
+ }
+
+ fn additional_properties(&self) -> bool {
+ self.additional_properties
+ }
+}
+
+impl ObjectSchemaType for AllOfSchema {
+ fn description(&self) -> &'static str {
+ self.description
+ }
+
+ fn lookup(&self, key: &str) -> Option<(bool, &Schema)> {
+ AllOfSchema::lookup(self, key)
+ }
+
+ fn properties(&self) -> ObjectPropertyIterator {
+ ObjectPropertyIterator {
+ schemas: self.list.iter(),
+ properties: None,
+ nested: None,
+ }
+ }
+
+ fn additional_properties(&self) -> bool {
+ true
+ }
+}
+
+#[doc(hidden)]
+pub struct ObjectPropertyIterator {
+ schemas: std::slice::Iter<'static, &'static Schema>,
+ properties: Option<std::slice::Iter<'static, SchemaPropertyEntry>>,
+ nested: Option<Box<ObjectPropertyIterator>>,
+}
+
+impl Iterator for ObjectPropertyIterator {
+ type Item = &'static SchemaPropertyEntry;
+
+ fn next(&mut self) -> Option<&'static SchemaPropertyEntry> {
+ loop {
+ match self.nested.as_mut().and_then(Iterator::next) {
+ Some(item) => return Some(item),
+ None => self.nested = None,
+ }
+
+ match self.properties.as_mut().and_then(Iterator::next) {
+ Some(item) => return Some(item),
+ None => match self.schemas.next()? {
+ Schema::AllOf(o) => self.nested = Some(Box::new(o.properties())),
+ Schema::Object(o) => self.properties = Some(o.properties.iter()),
+ _ => {
+ self.properties = None;
+ continue;
+ }
+ },
+ }
+ }
+ }
+}
+
+/// Schemas are used to describe complex data types.
+///
+/// All schema types implement constant builder methods, and a final
+/// `schema()` method to convert them into a `Schema`.
+///
+/// ```
+/// use proxmox_schema::{Schema, BooleanSchema, IntegerSchema, ObjectSchema};
+///
+/// const SIMPLE_OBJECT: Schema = ObjectSchema::new(
+/// "A very simple object with 2 properties",
+/// &[ // this arrays needs to be storted by name!
+/// (
+/// "property_one",
+/// false /* required */,
+/// &IntegerSchema::new("A required integer property.")
+/// .minimum(0)
+/// .maximum(100)
+/// .schema()
+/// ),
+/// (
+/// "property_two",
+/// true /* optional */,
+/// &BooleanSchema::new("An optional boolean property.")
+/// .default(true)
+/// .schema()
+/// ),
+/// ],
+/// ).schema();
+/// ```
+#[derive(Debug)]
+#[cfg_attr(feature = "test-harness", derive(Eq, PartialEq))]
+pub enum Schema {
+ Null,
+ Boolean(BooleanSchema),
+ Integer(IntegerSchema),
+ Number(NumberSchema),
+ String(StringSchema),
+ Object(ObjectSchema),
+ Array(ArraySchema),
+ AllOf(AllOfSchema),
+}
+
+/// A string enum entry. An enum entry must have a value and a description.
+#[derive(Clone, Debug)]
+#[cfg_attr(feature = "test-harness", derive(Eq, PartialEq))]
+pub struct EnumEntry {
+ pub value: &'static str,
+ pub description: &'static str,
+}
+
+impl EnumEntry {
+ /// Convenience method as long as we only have 2 mandatory fields in an `EnumEntry`.
+ pub const fn new(value: &'static str, description: &'static str) -> Self {
+ Self { value, description }
+ }
+}
+
+/// String microformat definitions.
+///
+/// Strings are probably the most flexible data type, and there are
+/// several ways to define their content.
+///
+/// ## Enumerations
+///
+/// Simple list all possible values.
+///
+/// ```
+/// use proxmox_schema::{ApiStringFormat, EnumEntry};
+///
+/// const format: ApiStringFormat = ApiStringFormat::Enum(&[
+/// EnumEntry::new("vm", "A guest VM run via qemu"),
+/// EnumEntry::new("ct", "A guest container run via lxc"),
+/// ]);
+/// ```
+///
+/// ## Regular Expressions
+///
+/// Use a regular expression to describe valid strings.
+///
+/// ```
+/// use proxmox_schema::{const_regex, ApiStringFormat};
+///
+/// const_regex! {
+/// pub SHA256_HEX_REGEX = r"^[a-f0-9]{64}$";
+/// }
+/// const format: ApiStringFormat = ApiStringFormat::Pattern(&SHA256_HEX_REGEX);
+/// ```
+///
+/// ## Property Strings
+///
+/// Use a schema to describe complex types encoded as string.
+///
+/// Arrays are parsed as comma separated lists, i.e: `"1,2,3"`. The
+/// list may be sparated by comma, semicolon or whitespace.
+///
+/// Objects are parsed as comma (or semicolon) separated `key=value` pairs, i.e:
+/// `"prop1=2,prop2=test"`. Any whitespace is trimmed from key and value.
+///
+///
+/// **Note:** This only works for types which does not allow using the
+/// comma, semicolon or whitespace separator inside the value,
+/// i.e. this only works for arrays of simple data types, and objects
+/// with simple properties (no nesting).
+///
+/// ```
+/// use proxmox_schema::{ApiStringFormat, ArraySchema, IntegerSchema, Schema, StringSchema};
+/// use proxmox_schema::{parse_simple_value, parse_property_string};
+///
+/// const PRODUCT_LIST_SCHEMA: Schema =
+/// ArraySchema::new("Product List.", &IntegerSchema::new("Product ID").schema())
+/// .min_length(1)
+/// .schema();
+///
+/// const SCHEMA: Schema = StringSchema::new("A list of Product IDs, comma separated.")
+/// .format(&ApiStringFormat::PropertyString(&PRODUCT_LIST_SCHEMA))
+/// .schema();
+///
+/// let res = parse_simple_value("", &SCHEMA);
+/// assert!(res.is_err());
+///
+/// let res = parse_simple_value("1,2,3", &SCHEMA); // parse as String
+/// assert!(res.is_ok());
+///
+/// let data = parse_property_string("1,2", &PRODUCT_LIST_SCHEMA); // parse as Array
+/// assert!(data.is_ok());
+/// ```
+pub enum ApiStringFormat {
+ /// Enumerate all valid strings
+ Enum(&'static [EnumEntry]),
+ /// Use a regular expression to describe valid strings.
+ Pattern(&'static ConstRegexPattern),
+ /// Use a schema to describe complex types encoded as string.
+ PropertyString(&'static Schema),
+ /// Use a verification function.
+ VerifyFn(fn(&str) -> Result<(), Error>),
+}
+
+impl std::fmt::Debug for ApiStringFormat {
+ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+ match self {
+ ApiStringFormat::VerifyFn(fnptr) => write!(f, "VerifyFn({:p}", fnptr),
+ ApiStringFormat::Enum(variants) => write!(f, "Enum({:?}", variants),
+ ApiStringFormat::Pattern(regex) => write!(f, "Pattern({:?}", regex),
+ ApiStringFormat::PropertyString(schema) => write!(f, "PropertyString({:?}", schema),
+ }
+ }
+}
+
+#[cfg(feature = "test-harness")]
+impl Eq for ApiStringFormat {}
+
+#[cfg(feature = "test-harness")]
+impl PartialEq for ApiStringFormat {
+ fn eq(&self, rhs: &Self) -> bool {
+ match (self, rhs) {
+ (ApiStringFormat::Enum(l), ApiStringFormat::Enum(r)) => l == r,
+ (ApiStringFormat::Pattern(l), ApiStringFormat::Pattern(r)) => l == r,
+ (ApiStringFormat::PropertyString(l), ApiStringFormat::PropertyString(r)) => l == r,
+ (ApiStringFormat::VerifyFn(l), ApiStringFormat::VerifyFn(r)) => std::ptr::eq(l, r),
+ (_, _) => false,
+ }
+ }
+}
+
+/// Parameters are objects, but we have two types of object schemas, the regular one and the
+/// `AllOf` schema.
+#[derive(Clone, Copy, Debug)]
+#[cfg_attr(feature = "test-harness", derive(Eq, PartialEq))]
+pub enum ParameterSchema {
+ Object(&'static ObjectSchema),
+ AllOf(&'static AllOfSchema),
+}
+
+impl ObjectSchemaType for ParameterSchema {
+ fn description(&self) -> &'static str {
+ match self {
+ ParameterSchema::Object(o) => o.description(),
+ ParameterSchema::AllOf(o) => o.description(),
+ }
+ }
+
+ fn lookup(&self, key: &str) -> Option<(bool, &Schema)> {
+ match self {
+ ParameterSchema::Object(o) => o.lookup(key),
+ ParameterSchema::AllOf(o) => o.lookup(key),
+ }
+ }
+
+ fn properties(&self) -> ObjectPropertyIterator {
+ match self {
+ ParameterSchema::Object(o) => o.properties(),
+ ParameterSchema::AllOf(o) => o.properties(),
+ }
+ }
+
+ fn additional_properties(&self) -> bool {
+ match self {
+ ParameterSchema::Object(o) => o.additional_properties(),
+ ParameterSchema::AllOf(o) => o.additional_properties(),
+ }
+ }
+}
+
+impl From<&'static ObjectSchema> for ParameterSchema {
+ fn from(schema: &'static ObjectSchema) -> Self {
+ ParameterSchema::Object(schema)
+ }
+}
+
+impl From<&'static AllOfSchema> for ParameterSchema {
+ fn from(schema: &'static AllOfSchema) -> Self {
+ ParameterSchema::AllOf(schema)
+ }
+}
+
+/// Helper function to parse boolean values
+///
+/// - true: `1 | on | yes | true`
+/// - false: `0 | off | no | false`
+pub fn parse_boolean(value_str: &str) -> Result<bool, Error> {
+ match value_str.to_lowercase().as_str() {
+ "1" | "on" | "yes" | "true" => Ok(true),
+ "0" | "off" | "no" | "false" => Ok(false),
+ _ => bail!("Unable to parse boolean option."),
+ }
+}
+
+/// Parse a complex property string (`ApiStringFormat::PropertyString`)
+pub fn parse_property_string(value_str: &str, schema: &'static Schema) -> Result<Value, Error> {
+ // helper for object/allof schemas:
+ fn parse_object<T: Into<ParameterSchema>>(
+ value_str: &str,
+ schema: T,
+ default_key: Option<&'static str>,
+ ) -> Result<Value, Error> {
+ let mut param_list: Vec<(String, String)> = vec![];
+ let key_val_list: Vec<&str> = value_str
+ .split(|c: char| c == ',' || c == ';')
+ .filter(|s| !s.is_empty())
+ .collect();
+ for key_val in key_val_list {
+ let kv: Vec<&str> = key_val.splitn(2, '=').collect();
+ if kv.len() == 2 {
+ param_list.push((kv[0].trim().into(), kv[1].trim().into()));
+ } else if let Some(key) = default_key {
+ param_list.push((key.into(), kv[0].trim().into()));
+ } else {
+ bail!("Value without key, but schema does not define a default key.");
+ }
+ }
+
+ parse_parameter_strings(¶m_list, schema, true).map_err(Error::from)
+ }
+
+ match schema {
+ Schema::Object(object_schema) => {
+ parse_object(value_str, object_schema, object_schema.default_key)
+ }
+ Schema::AllOf(all_of_schema) => parse_object(value_str, all_of_schema, None),
+ Schema::Array(array_schema) => {
+ let mut array: Vec<Value> = vec![];
+ let list: Vec<&str> = value_str
+ .split(|c: char| c == ',' || c == ';' || char::is_ascii_whitespace(&c))
+ .filter(|s| !s.is_empty())
+ .collect();
+
+ for value in list {
+ match parse_simple_value(value.trim(), &array_schema.items) {
+ Ok(res) => array.push(res),
+ Err(err) => bail!("unable to parse array element: {}", err),
+ }
+ }
+ array_schema.check_length(array.len())?;
+
+ Ok(array.into())
+ }
+ _ => bail!("Got unexpected schema type."),
+ }
+}
+
+/// Parse a simple value (no arrays and no objects)
+pub fn parse_simple_value(value_str: &str, schema: &Schema) -> Result<Value, Error> {
+ let value = match schema {
+ Schema::Null => {
+ bail!("internal error - found Null schema.");
+ }
+ Schema::Boolean(_boolean_schema) => {
+ let res = parse_boolean(value_str)?;
+ Value::Bool(res)
+ }
+ Schema::Integer(integer_schema) => {
+ let res: isize = value_str.parse()?;
+ integer_schema.check_constraints(res)?;
+ Value::Number(res.into())
+ }
+ Schema::Number(number_schema) => {
+ let res: f64 = value_str.parse()?;
+ number_schema.check_constraints(res)?;
+ Value::Number(serde_json::Number::from_f64(res).unwrap())
+ }
+ Schema::String(string_schema) => {
+ string_schema.check_constraints(value_str)?;
+ Value::String(value_str.into())
+ }
+ _ => bail!("unable to parse complex (sub) objects."),
+ };
+ Ok(value)
+}
+
+/// Parse key/value pairs and verify with object schema
+///
+/// - `test_required`: is set, checks if all required properties are
+/// present.
+pub fn parse_parameter_strings<T: Into<ParameterSchema>>(
+ data: &[(String, String)],
+ schema: T,
+ test_required: bool,
+) -> Result<Value, ParameterError> {
+ do_parse_parameter_strings(data, schema.into(), test_required)
+}
+
+fn do_parse_parameter_strings(
+ data: &[(String, String)],
+ schema: ParameterSchema,
+ test_required: bool,
+) -> Result<Value, ParameterError> {
+ let mut params = json!({});
+
+ let mut errors = ParameterError::new();
+
+ let additional_properties = schema.additional_properties();
+
+ for (key, value) in data {
+ if let Some((_optional, prop_schema)) = schema.lookup(&key) {
+ match prop_schema {
+ Schema::Array(array_schema) => {
+ if params[key] == Value::Null {
+ params[key] = json!([]);
+ }
+ match params[key] {
+ Value::Array(ref mut array) => {
+ match parse_simple_value(value, &array_schema.items) {
+ Ok(res) => array.push(res), // fixme: check_length??
+ Err(err) => errors.push(key.into(), err),
+ }
+ }
+ _ => {
+ errors.push(key.into(), format_err!("expected array - type missmatch"))
+ }
+ }
+ }
+ _ => match parse_simple_value(value, prop_schema) {
+ Ok(res) => {
+ if params[key] == Value::Null {
+ params[key] = res;
+ } else {
+ errors.push(key.into(), format_err!("duplicate parameter."));
+ }
+ }
+ Err(err) => errors.push(key.into(), err),
+ },
+ }
+ } else if additional_properties {
+ match params[key] {
+ Value::Null => {
+ params[key] = Value::String(value.to_owned());
+ }
+ Value::String(ref old) => {
+ params[key] = Value::Array(vec![
+ Value::String(old.to_owned()),
+ Value::String(value.to_owned()),
+ ]);
+ }
+ Value::Array(ref mut array) => {
+ array.push(Value::String(value.to_string()));
+ }
+ _ => errors.push(key.into(), format_err!("expected array - type missmatch")),
+ }
+ } else {
+ errors.push(
+ key.into(),
+ format_err!("schema does not allow additional properties."),
+ );
+ }
+ }
+
+ if test_required && errors.is_empty() {
+ for (name, optional, _prop_schema) in schema.properties() {
+ if !(*optional) && params[name] == Value::Null {
+ errors.push(
+ name.to_string(),
+ format_err!("parameter is missing and it is not optional."),
+ );
+ }
+ }
+ }
+
+ if !errors.is_empty() {
+ Err(errors)
+ } else {
+ Ok(params)
+ }
+}
+
+/// Verify JSON value with `schema`.
+pub fn verify_json(data: &Value, schema: &Schema) -> Result<(), Error> {
+ match schema {
+ Schema::Null => {
+ if !data.is_null() {
+ bail!("Expected Null, but value is not Null.");
+ }
+ }
+ Schema::Object(object_schema) => verify_json_object(data, object_schema)?,
+ Schema::Array(array_schema) => verify_json_array(data, &array_schema)?,
+ Schema::Boolean(boolean_schema) => verify_json_boolean(data, &boolean_schema)?,
+ Schema::Integer(integer_schema) => verify_json_integer(data, &integer_schema)?,
+ Schema::Number(number_schema) => verify_json_number(data, &number_schema)?,
+ Schema::String(string_schema) => verify_json_string(data, &string_schema)?,
+ Schema::AllOf(all_of_schema) => verify_json_object(data, all_of_schema)?,
+ }
+ Ok(())
+}
+
+/// Verify JSON value using a `StringSchema`.
+pub fn verify_json_string(data: &Value, schema: &StringSchema) -> Result<(), Error> {
+ if let Some(value) = data.as_str() {
+ schema.check_constraints(value)
+ } else {
+ bail!("Expected string value.");
+ }
+}
+
+/// Verify JSON value using a `BooleanSchema`.
+pub fn verify_json_boolean(data: &Value, _schema: &BooleanSchema) -> Result<(), Error> {
+ if !data.is_boolean() {
+ bail!("Expected boolean value.");
+ }
+ Ok(())
+}
+
+/// Verify JSON value using an `IntegerSchema`.
+pub fn verify_json_integer(data: &Value, schema: &IntegerSchema) -> Result<(), Error> {
+ if let Some(value) = data.as_i64() {
+ schema.check_constraints(value as isize)
+ } else {
+ bail!("Expected integer value.");
+ }
+}
+
+/// Verify JSON value using an `NumberSchema`.
+pub fn verify_json_number(data: &Value, schema: &NumberSchema) -> Result<(), Error> {
+ if let Some(value) = data.as_f64() {
+ schema.check_constraints(value)
+ } else {
+ bail!("Expected number value.");
+ }
+}
+
+/// Verify JSON value using an `ArraySchema`.
+pub fn verify_json_array(data: &Value, schema: &ArraySchema) -> Result<(), Error> {
+ let list = match data {
+ Value::Array(ref list) => list,
+ Value::Object(_) => bail!("Expected array - got object."),
+ _ => bail!("Expected array - got scalar value."),
+ };
+
+ schema.check_length(list.len())?;
+
+ for (i, item) in list.iter().enumerate() {
+ let result = verify_json(item, &schema.items);
+ if let Err(err) = result {
+ let mut errors = ParameterError::new();
+ errors.add_errors(&format!("[{}]", i), err);
+ return Err(errors.into());
+ }
+ }
+
+ Ok(())
+}
+
+/// Verify JSON value using an `ObjectSchema`.
+pub fn verify_json_object(data: &Value, schema: &dyn ObjectSchemaType) -> Result<(), Error> {
+ let map = match data {
+ Value::Object(ref map) => map,
+ Value::Array(_) => bail!("Expected object - got array."),
+ _ => bail!("Expected object - got scalar value."),
+ };
+
+ let mut errors = ParameterError::new();
+
+ let additional_properties = schema.additional_properties();
+
+ for (key, value) in map {
+ if let Some((_optional, prop_schema)) = schema.lookup(&key) {
+ let result = match prop_schema {
+ Schema::Object(object_schema) => verify_json_object(value, object_schema),
+ Schema::Array(array_schema) => verify_json_array(value, array_schema),
+ _ => verify_json(value, prop_schema),
+ };
+ if let Err(err) = result {
+ errors.add_errors(key, err);
+ };
+ } else if !additional_properties {
+ errors.push(
+ key.to_string(),
+ format_err!("schema does not allow additional properties."),
+ );
+ }
+ }
+
+ for (name, optional, _prop_schema) in schema.properties() {
+ if !(*optional) && data[name] == Value::Null {
+ errors.push(
+ name.to_string(),
+ format_err!("property is missing and it is not optional."),
+ );
+ }
+ }
+
+ if !errors.is_empty() {
+ Err(errors.into())
+ } else {
+ Ok(())
+ }
+}
+
+/// API types should define an "updater type" via this trait in order to support derived "Updater"
+/// structs more easily.
+///
+/// Most trivial types can simply use an `Option<Self>` as updater. For types which do not use the
+/// `#[api]` macro, this will need to be explicitly created (or derived via
+/// `#[derive(UpdaterType)]`.
+pub trait UpdaterType: Sized {
+ type Updater: Updater;
+}
+
+#[cfg(feature = "api-macro")]
+pub use proxmox_api_macro::UpdaterType;
+
+#[cfg(feature = "api-macro")]
+#[doc(hidden)]
+pub use proxmox_api_macro::Updater;
+
+macro_rules! basic_updater_type {
+ ($($ty:ty)*) => {
+ $(
+ impl UpdaterType for $ty {
+ type Updater = Option<Self>;
+ }
+ )*
+ };
+}
+basic_updater_type! { bool u8 u16 u32 u64 i8 i16 i32 i64 usize isize f32 f64 String char }
+
+impl<T> UpdaterType for Option<T>
+where
+ T: UpdaterType,
+{
+ type Updater = T::Updater;
+}
+
+pub trait ApiType {
+ const API_SCHEMA: Schema;
+}
+
+impl<T: ApiType> ApiType for Option<T> {
+ const API_SCHEMA: Schema = T::API_SCHEMA;
+}
+
+/// A helper type for "Updater" structs. This trait is *not* implemented for an api "base" type
+/// when deriving an `Updater` for it, though the generated *updater* type does implement this
+/// trait!
+///
+/// This trait is mostly to figure out if an updater is empty (iow. it should not be applied).
+/// In that, it is useful when a type which should have an updater also has optional fields which
+/// should not be serialized. Instead of `#[serde(skip_serializing_if = "Option::is_none")]`, this
+/// trait's `is_empty` needs to be used via `#[serde(skip_serializing_if = "Updater::is_empty")]`.
+pub trait Updater {
+ /// Check if the updater is "none" or "empty".
+ fn is_empty(&self) -> bool;
+}
+
+impl<T> Updater for Vec<T> {
+ fn is_empty(&self) -> bool {
+ self.is_empty()
+ }
+}
+
+impl<T> Updater for Option<T> {
+ fn is_empty(&self) -> bool {
+ self.is_none()
+ }
+}
+
+#[cfg_attr(feature = "test-harness", derive(Eq, PartialEq))]
+pub struct ReturnType {
+ /// A return type may be optional, meaning the method may return null or some fixed data.
+ ///
+ /// If true, the return type in pseudo openapi terms would be `"oneOf": [ "null", "T" ]`.
+ pub optional: bool,
+
+ /// The method's return type.
+ pub schema: &'static Schema,
+}
+
+impl std::fmt::Debug for ReturnType {
+ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+ if self.optional {
+ write!(f, "optional {:?}", self.schema)
+ } else {
+ write!(f, "{:?}", self.schema)
+ }
+ }
+}
+
+impl ReturnType {
+ pub const fn new(optional: bool, schema: &'static Schema) -> Self {
+ Self { optional, schema }
+ }
+}
--- /dev/null
+use anyhow::{bail, Error};
+
+use crate::{const_regex, ApiStringFormat, ApiType, Schema, StringSchema};
+
+/// Unique Process/Task Identifier
+///
+/// We use this to uniquely identify worker task. UPIDs have a short
+/// string repesentaion, which gives additional information about the
+/// type of the task. for example:
+/// ```text
+/// UPID:{node}:{pid}:{pstart}:{task_id}:{starttime}:{worker_type}:{worker_id}:{userid}:
+/// UPID:elsa:00004F37:0039E469:00000000:5CA78B83:garbage_collection::root@pam:
+/// ```
+/// Please note that we use tokio, so a single thread can run multiple
+/// tasks.
+// #[api] - manually implemented API type
+#[derive(Debug, Clone)]
+pub struct UPID {
+ /// The Unix PID
+ pub pid: i32, // really libc::pid_t, but we don't want this as a dependency for proxmox-schema
+ /// The Unix process start time from `/proc/pid/stat`
+ pub pstart: u64,
+ /// The task start time (Epoch)
+ pub starttime: i64,
+ /// The task ID (inside the process/thread)
+ pub task_id: usize,
+ /// Worker type (arbitrary ASCII string)
+ pub worker_type: String,
+ /// Worker ID (arbitrary ASCII string)
+ pub worker_id: Option<String>,
+ /// The authenticated entity who started the task
+ pub auth_id: String,
+ /// The node name.
+ pub node: String,
+}
+
+const_regex! {
+ pub PROXMOX_UPID_REGEX = concat!(
+ r"^UPID:(?P<node>[a-zA-Z0-9]([a-zA-Z0-9\-]*[a-zA-Z0-9])?):(?P<pid>[0-9A-Fa-f]{8}):",
+ r"(?P<pstart>[0-9A-Fa-f]{8,9}):(?P<task_id>[0-9A-Fa-f]{8,16}):(?P<starttime>[0-9A-Fa-f]{8}):",
+ r"(?P<wtype>[^:\s]+):(?P<wid>[^:\s]*):(?P<authid>[^:\s]+):$"
+ );
+}
+
+pub const PROXMOX_UPID_FORMAT: ApiStringFormat = ApiStringFormat::Pattern(&PROXMOX_UPID_REGEX);
+
+pub const UPID_SCHEMA: Schema = StringSchema::new("Unique Process/Task Identifier")
+ .min_length("UPID:N:12345678:12345678:12345678:::".len())
+ .format(&PROXMOX_UPID_FORMAT)
+ .schema();
+
+impl ApiType for UPID {
+ const API_SCHEMA: Schema = UPID_SCHEMA;
+}
+
+impl std::str::FromStr for UPID {
+ type Err = Error;
+
+ fn from_str(s: &str) -> Result<Self, Self::Err> {
+ if let Some(cap) = PROXMOX_UPID_REGEX.captures(s) {
+ let worker_id = if cap["wid"].is_empty() {
+ None
+ } else {
+ let wid = unescape_id(&cap["wid"])?;
+ Some(wid)
+ };
+
+ Ok(UPID {
+ pid: i32::from_str_radix(&cap["pid"], 16).unwrap(),
+ pstart: u64::from_str_radix(&cap["pstart"], 16).unwrap(),
+ starttime: i64::from_str_radix(&cap["starttime"], 16).unwrap(),
+ task_id: usize::from_str_radix(&cap["task_id"], 16).unwrap(),
+ worker_type: cap["wtype"].to_string(),
+ worker_id,
+ auth_id: cap["authid"].to_string(),
+ node: cap["node"].to_string(),
+ })
+ } else {
+ bail!("unable to parse UPID '{}'", s);
+ }
+ }
+}
+
+impl std::fmt::Display for UPID {
+ fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
+ let wid = if let Some(ref id) = self.worker_id {
+ escape_id(id)
+ } else {
+ String::new()
+ };
+
+ // Note: pstart can be > 32bit if uptime > 497 days, so this can result in
+ // more that 8 characters for pstart
+
+ write!(
+ f,
+ "UPID:{}:{:08X}:{:08X}:{:08X}:{:08X}:{}:{}:{}:",
+ self.node,
+ self.pid,
+ self.pstart,
+ self.task_id,
+ self.starttime,
+ self.worker_type,
+ wid,
+ self.auth_id
+ )
+ }
+}
+
+impl serde::Serialize for UPID {
+ fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
+ where
+ S: serde::ser::Serializer,
+ {
+ serializer.serialize_str(&ToString::to_string(self))
+ }
+}
+
+impl<'de> serde::Deserialize<'de> for UPID {
+ fn deserialize<D>(deserializer: D) -> Result<UPID, D::Error>
+ where
+ D: serde::Deserializer<'de>,
+ {
+ struct ForwardToStrVisitor;
+
+ impl<'a> serde::de::Visitor<'a> for ForwardToStrVisitor {
+ type Value = UPID;
+
+ fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
+ formatter.write_str("a valid UPID")
+ }
+
+ fn visit_str<E: serde::de::Error>(self, v: &str) -> Result<UPID, E> {
+ v.parse::<UPID>().map_err(|_| {
+ serde::de::Error::invalid_value(serde::de::Unexpected::Str(v), &self)
+ })
+ }
+ }
+
+ deserializer.deserialize_str(ForwardToStrVisitor)
+ }
+}
+
+// the following two are copied as they're the only `proxmox-systemd` dependencies in this crate,
+// and this crate has MUCH fewer dependencies without it
+/// non-path systemd-unit compatible escaping
+fn escape_id(unit: &str) -> String {
+ use std::fmt::Write;
+
+ let mut escaped = String::new();
+
+ for (i, &c) in unit.as_bytes().iter().enumerate() {
+ if c == b'/' {
+ escaped.push('-');
+ } else if (i == 0 && c == b'.')
+ || !matches!(c, b'_' | b'.' | b'0'..=b'9' | b'a'..=b'z' | b'A'..=b'Z')
+ {
+ // unwrap: writing to a String
+ write!(escaped, "\\x{:02x}", c).unwrap();
+ } else {
+ escaped.push(char::from(c));
+ }
+ }
+
+ escaped
+}
+
+fn hex_digit(d: u8) -> Result<u8, Error> {
+ match d {
+ b'0'..=b'9' => Ok(d - b'0'),
+ b'A'..=b'F' => Ok(d - b'A' + 10),
+ b'a'..=b'f' => Ok(d - b'a' + 10),
+ _ => bail!("got invalid hex digit"),
+ }
+}
+
+/// systemd-unit compatible escaping
+fn unescape_id(text: &str) -> Result<String, Error> {
+ let mut i = text.as_bytes();
+
+ let mut data: Vec<u8> = Vec::new();
+
+ loop {
+ if i.is_empty() {
+ break;
+ }
+ let next = i[0];
+ if next == b'\\' {
+ if i.len() < 4 || i[1] != b'x' {
+ bail!("error in escape sequence");
+ }
+ let h1 = hex_digit(i[2])?;
+ let h0 = hex_digit(i[3])?;
+ data.push(h1 << 4 | h0);
+ i = &i[4..]
+ } else if next == b'-' {
+ data.push(b'/');
+ i = &i[1..]
+ } else {
+ data.push(next);
+ i = &i[1..]
+ }
+ }
+
+ let text = String::from_utf8(data)?;
+
+ Ok(text)
+}
+
+#[cfg(feature = "upid-api-impl")]
+mod upid_impl {
+ use std::sync::atomic::{AtomicUsize, Ordering};
+
+ use anyhow::{bail, format_err, Error};
+
+ use super::UPID;
+
+ impl UPID {
+ /// Create a new UPID
+ pub fn new(
+ worker_type: &str,
+ worker_id: Option<String>,
+ auth_id: String,
+ ) -> Result<Self, Error> {
+ let pid = unsafe { libc::getpid() };
+
+ let bad: &[_] = &['/', ':', ' '];
+
+ if worker_type.contains(bad) {
+ bail!("illegal characters in worker type '{}'", worker_type);
+ }
+
+ if auth_id.contains(bad) {
+ bail!("illegal characters in auth_id '{}'", auth_id);
+ }
+
+ static WORKER_TASK_NEXT_ID: AtomicUsize = AtomicUsize::new(0);
+
+ let task_id = WORKER_TASK_NEXT_ID.fetch_add(1, Ordering::SeqCst);
+
+ Ok(UPID {
+ pid,
+ pstart: get_pid_start(pid)?,
+ starttime: epoch_i64(),
+ task_id,
+ worker_type: worker_type.to_owned(),
+ worker_id,
+ auth_id,
+ node: nix::sys::utsname::uname()
+ .nodename()
+ .split('.')
+ .next()
+ .ok_or_else(|| format_err!("failed to get nodename from uname()"))?
+ .to_owned(),
+ })
+ }
+ }
+
+ fn get_pid_start(pid: libc::pid_t) -> Result<u64, Error> {
+ let statstr = String::from_utf8(std::fs::read(format!("/proc/{}/stat", pid))?)?;
+ let cmdend = statstr
+ .rfind(')')
+ .ok_or_else(|| format_err!("missing ')' in /proc/PID/stat"))?;
+ let starttime = statstr[cmdend + 1..]
+ .trim_start()
+ .split_ascii_whitespace()
+ .nth(19)
+ .ok_or_else(|| format_err!("failed to find starttime in /proc/{}/stat", pid))?;
+ starttime.parse().map_err(|err| {
+ format_err!(
+ "failed to parse starttime from /proc/{}/stat ({:?}): {}",
+ pid,
+ starttime,
+ err,
+ )
+ })
+ }
+
+ // Copied as this is the only `proxmox-time` dependency in this crate
+ // and this crate has MUCH fewer dependencies without it
+ fn epoch_i64() -> i64 {
+ use std::convert::TryFrom;
+ use std::time::{SystemTime, UNIX_EPOCH};
+
+ let now = SystemTime::now();
+
+ if now > UNIX_EPOCH {
+ i64::try_from(now.duration_since(UNIX_EPOCH).unwrap().as_secs())
+ .expect("epoch_i64: now is too large")
+ } else {
+ -i64::try_from(UNIX_EPOCH.duration_since(now).unwrap().as_secs())
+ .expect("epoch_i64: now is too small")
+ }
+ }
+}
--- /dev/null
+use anyhow::bail;
+use serde_json::Value;
+use url::form_urlencoded;
+
+use proxmox_schema::*;
+
+fn parse_query_string<T: Into<ParameterSchema>>(
+ query: &str,
+ schema: T,
+ test_required: bool,
+) -> Result<Value, ParameterError> {
+ let param_list: Vec<(String, String)> = form_urlencoded::parse(query.as_bytes())
+ .into_owned()
+ .collect();
+
+ parse_parameter_strings(¶m_list, schema.into(), test_required)
+}
+
+#[test]
+fn test_schema1() {
+ let schema = Schema::Object(ObjectSchema {
+ description: "TEST",
+ additional_properties: false,
+ properties: &[],
+ default_key: None,
+ });
+
+ println!("TEST Schema: {:?}", schema);
+}
+
+#[test]
+fn test_query_string() {
+ {
+ const SCHEMA: ObjectSchema = ObjectSchema::new(
+ "Parameters.",
+ &[("name", false, &StringSchema::new("Name.").schema())],
+ );
+
+ let res = parse_query_string("", &SCHEMA, true);
+ assert!(res.is_err());
+ }
+
+ {
+ const SCHEMA: ObjectSchema = ObjectSchema::new(
+ "Parameters.",
+ &[("name", true, &StringSchema::new("Name.").schema())],
+ );
+
+ let res = parse_query_string("", &SCHEMA, true);
+ assert!(res.is_ok());
+ }
+
+ // TEST min_length and max_length
+ {
+ const SCHEMA: ObjectSchema = ObjectSchema::new(
+ "Parameters.",
+ &[(
+ "name",
+ true,
+ &StringSchema::new("Name.")
+ .min_length(5)
+ .max_length(10)
+ .schema(),
+ )],
+ );
+
+ let res = parse_query_string("name=abcd", &SCHEMA, true);
+ assert!(res.is_err());
+
+ let res = parse_query_string("name=abcde", &SCHEMA, true);
+ assert!(res.is_ok());
+
+ let res = parse_query_string("name=abcdefghijk", &SCHEMA, true);
+ assert!(res.is_err());
+
+ let res = parse_query_string("name=abcdefghij", &SCHEMA, true);
+ assert!(res.is_ok());
+ }
+
+ // TEST regex pattern
+ crate::const_regex! {
+ TEST_REGEX = "test";
+ TEST2_REGEX = "^test$";
+ }
+
+ {
+ const SCHEMA: ObjectSchema = ObjectSchema::new(
+ "Parameters.",
+ &[(
+ "name",
+ false,
+ &StringSchema::new("Name.")
+ .format(&ApiStringFormat::Pattern(&TEST_REGEX))
+ .schema(),
+ )],
+ );
+
+ let res = parse_query_string("name=abcd", &SCHEMA, true);
+ assert!(res.is_err());
+
+ let res = parse_query_string("name=ateststring", &SCHEMA, true);
+ assert!(res.is_ok());
+ }
+
+ {
+ const SCHEMA: ObjectSchema = ObjectSchema::new(
+ "Parameters.",
+ &[(
+ "name",
+ false,
+ &StringSchema::new("Name.")
+ .format(&ApiStringFormat::Pattern(&TEST2_REGEX))
+ .schema(),
+ )],
+ );
+
+ let res = parse_query_string("name=ateststring", &SCHEMA, true);
+ assert!(res.is_err());
+
+ let res = parse_query_string("name=test", &SCHEMA, true);
+ assert!(res.is_ok());
+ }
+
+ // TEST string enums
+ {
+ const SCHEMA: ObjectSchema = ObjectSchema::new(
+ "Parameters.",
+ &[(
+ "name",
+ false,
+ &StringSchema::new("Name.")
+ .format(&ApiStringFormat::Enum(&[
+ EnumEntry::new("ev1", "desc ev1"),
+ EnumEntry::new("ev2", "desc ev2"),
+ ]))
+ .schema(),
+ )],
+ );
+
+ let res = parse_query_string("name=noenum", &SCHEMA, true);
+ assert!(res.is_err());
+
+ let res = parse_query_string("name=ev1", &SCHEMA, true);
+ assert!(res.is_ok());
+
+ let res = parse_query_string("name=ev2", &SCHEMA, true);
+ assert!(res.is_ok());
+
+ let res = parse_query_string("name=ev3", &SCHEMA, true);
+ assert!(res.is_err());
+ }
+}
+
+#[test]
+fn test_query_integer() {
+ {
+ const SCHEMA: ObjectSchema = ObjectSchema::new(
+ "Parameters.",
+ &[("count", false, &IntegerSchema::new("Count.").schema())],
+ );
+
+ let res = parse_query_string("", &SCHEMA, true);
+ assert!(res.is_err());
+ }
+
+ {
+ const SCHEMA: ObjectSchema = ObjectSchema::new(
+ "Parameters.",
+ &[(
+ "count",
+ true,
+ &IntegerSchema::new("Count.")
+ .minimum(-3)
+ .maximum(50)
+ .schema(),
+ )],
+ );
+
+ let res = parse_query_string("", &SCHEMA, true);
+ assert!(res.is_ok());
+
+ let res = parse_query_string("count=abc", &SCHEMA, false);
+ assert!(res.is_err());
+
+ let res = parse_query_string("count=30", &SCHEMA, false);
+ assert!(res.is_ok());
+
+ let res = parse_query_string("count=-1", &SCHEMA, false);
+ assert!(res.is_ok());
+
+ let res = parse_query_string("count=300", &SCHEMA, false);
+ assert!(res.is_err());
+
+ let res = parse_query_string("count=-30", &SCHEMA, false);
+ assert!(res.is_err());
+
+ let res = parse_query_string("count=50", &SCHEMA, false);
+ assert!(res.is_ok());
+
+ let res = parse_query_string("count=-3", &SCHEMA, false);
+ assert!(res.is_ok());
+ }
+}
+
+#[test]
+fn test_query_boolean() {
+ {
+ const SCHEMA: ObjectSchema = ObjectSchema::new(
+ "Parameters.",
+ &[("force", false, &BooleanSchema::new("Force.").schema())],
+ );
+
+ let res = parse_query_string("", &SCHEMA, true);
+ assert!(res.is_err());
+ }
+
+ {
+ const SCHEMA: ObjectSchema = ObjectSchema::new(
+ "Parameters.",
+ &[("force", true, &BooleanSchema::new("Force.").schema())],
+ );
+
+ let res = parse_query_string("", &SCHEMA, true);
+ assert!(res.is_ok());
+
+ let res = parse_query_string("a=b", &SCHEMA, true);
+ assert!(res.is_err());
+
+ let res = parse_query_string("force", &SCHEMA, true);
+ assert!(res.is_err());
+
+ let res = parse_query_string("force=yes", &SCHEMA, true);
+ assert!(res.is_ok());
+ let res = parse_query_string("force=1", &SCHEMA, true);
+ assert!(res.is_ok());
+ let res = parse_query_string("force=On", &SCHEMA, true);
+ assert!(res.is_ok());
+ let res = parse_query_string("force=TRUE", &SCHEMA, true);
+ assert!(res.is_ok());
+ let res = parse_query_string("force=TREU", &SCHEMA, true);
+ assert!(res.is_err());
+
+ let res = parse_query_string("force=NO", &SCHEMA, true);
+ assert!(res.is_ok());
+ let res = parse_query_string("force=0", &SCHEMA, true);
+ assert!(res.is_ok());
+ let res = parse_query_string("force=off", &SCHEMA, true);
+ assert!(res.is_ok());
+ let res = parse_query_string("force=False", &SCHEMA, true);
+ assert!(res.is_ok());
+ }
+}
+
+#[test]
+fn test_verify_function() {
+ const SCHEMA: ObjectSchema = ObjectSchema::new(
+ "Parameters.",
+ &[(
+ "p1",
+ false,
+ &StringSchema::new("P1")
+ .format(&ApiStringFormat::VerifyFn(|value| {
+ if value == "test" {
+ return Ok(());
+ };
+ bail!("format error");
+ }))
+ .schema(),
+ )],
+ );
+
+ let res = parse_query_string("p1=tes", &SCHEMA, true);
+ assert!(res.is_err());
+ let res = parse_query_string("p1=test", &SCHEMA, true);
+ assert!(res.is_ok());
+}
+
+#[test]
+fn test_verify_complex_object() {
+ const NIC_MODELS: ApiStringFormat = ApiStringFormat::Enum(&[
+ EnumEntry::new("e1000", "Intel E1000"),
+ EnumEntry::new("virtio", "Paravirtualized ethernet device"),
+ ]);
+
+ const PARAM_SCHEMA: Schema = ObjectSchema::new(
+ "Properties.",
+ &[
+ (
+ "enable",
+ true,
+ &BooleanSchema::new("Enable device.").schema(),
+ ),
+ (
+ "model",
+ false,
+ &StringSchema::new("Ethernet device Model.")
+ .format(&NIC_MODELS)
+ .schema(),
+ ),
+ ],
+ )
+ .default_key("model")
+ .schema();
+
+ const SCHEMA: ObjectSchema = ObjectSchema::new(
+ "Parameters.",
+ &[(
+ "net0",
+ false,
+ &StringSchema::new("First Network device.")
+ .format(&ApiStringFormat::PropertyString(&PARAM_SCHEMA))
+ .schema(),
+ )],
+ );
+
+ let res = parse_query_string("", &SCHEMA, true);
+ assert!(res.is_err());
+
+ let res = parse_query_string("test=abc", &SCHEMA, true);
+ assert!(res.is_err());
+
+ let res = parse_query_string("net0=model=abc", &SCHEMA, true);
+ assert!(res.is_err());
+
+ let res = parse_query_string("net0=model=virtio", &SCHEMA, true);
+ assert!(res.is_ok());
+
+ let res = parse_query_string("net0=model=virtio,enable=1", &SCHEMA, true);
+ assert!(res.is_ok());
+
+ let res = parse_query_string("net0=virtio,enable=no", &SCHEMA, true);
+ assert!(res.is_ok());
+}
+
+#[test]
+fn test_verify_complex_array() {
+ {
+ const PARAM_SCHEMA: Schema =
+ ArraySchema::new("Integer List.", &IntegerSchema::new("Soemething").schema()).schema();
+
+ const SCHEMA: ObjectSchema = ObjectSchema::new(
+ "Parameters.",
+ &[(
+ "list",
+ false,
+ &StringSchema::new("A list on integers, comma separated.")
+ .format(&ApiStringFormat::PropertyString(&PARAM_SCHEMA))
+ .schema(),
+ )],
+ );
+
+ let res = parse_query_string("", &SCHEMA, true);
+ assert!(res.is_err());
+
+ let res = parse_query_string("list=", &SCHEMA, true);
+ assert!(res.is_ok());
+
+ let res = parse_query_string("list=abc", &SCHEMA, true);
+ assert!(res.is_err());
+
+ let res = parse_query_string("list=1", &SCHEMA, true);
+ assert!(res.is_ok());
+
+ let res = parse_query_string("list=2,3,4,5", &SCHEMA, true);
+ assert!(res.is_ok());
+ }
+
+ {
+ const PARAM_SCHEMA: Schema =
+ ArraySchema::new("Integer List.", &IntegerSchema::new("Soemething").schema())
+ .min_length(1)
+ .max_length(3)
+ .schema();
+
+ const SCHEMA: ObjectSchema = ObjectSchema::new(
+ "Parameters.",
+ &[(
+ "list",
+ false,
+ &StringSchema::new("A list on integers, comma separated.")
+ .format(&ApiStringFormat::PropertyString(&PARAM_SCHEMA))
+ .schema(),
+ )],
+ );
+
+ let res = parse_query_string("list=", &SCHEMA, true);
+ assert!(res.is_err());
+
+ let res = parse_query_string("list=1,2,3", &SCHEMA, true);
+ assert!(res.is_ok());
+
+ let res = parse_query_string("list=2,3,4,5", &SCHEMA, true);
+ assert!(res.is_err());
+ }
+}
--- /dev/null
+use anyhow::{bail, Error};
+use serde_json::{json, Value};
+
+use proxmox_schema::*;
+
+static STRING_SCHEMA: Schema = StringSchema::new("A test string").schema();
+
+static SIMPLE_OBJECT_SCHEMA: Schema = ObjectSchema::new(
+ "simple object schema",
+ &[
+ ("prop1", false, &STRING_SCHEMA),
+ ("prop2", true, &STRING_SCHEMA),
+ ("prop3", false, &STRING_SCHEMA),
+ ]
+).schema();
+
+static SIMPLE_PROPERTY_STRING_SCHEMA: Schema = StringSchema::new("simple property string")
+ .format(&ApiStringFormat::PropertyString(&SIMPLE_OBJECT_SCHEMA))
+ .schema();
+
+static SIMPLE_ARRAY_SCHEMA: Schema = ArraySchema::new("String list.", &STRING_SCHEMA).schema();
+
+static NESTED_OBJECT_SCHEMA: Schema = ObjectSchema::new(
+ "nested object schema",
+ &[
+ ("arr1", false, &SIMPLE_ARRAY_SCHEMA),
+ ("obj1", false, &SIMPLE_OBJECT_SCHEMA),
+ ("prop1", false, &STRING_SCHEMA),
+ ]
+).schema();
+
+static NESTED_PROPERTY_SCHEMA: Schema = ObjectSchema::new(
+ "object with property strings",
+ &[
+ ("ps1", false, &SIMPLE_PROPERTY_STRING_SCHEMA),
+ ]
+).schema();
+
+
+fn compare_error(
+ expected: &[(&str, &str)],
+ err: Error,
+) -> Result<(), Error> {
+ let err = match err.downcast_ref::<ParameterError>() {
+ Some(err) => err,
+ None => bail!("unable to downcast error: {}", err),
+ };
+
+ let result = (move || {
+ let errors = err.errors();
+
+ if errors.len() != expected.len() {
+ bail!("error list has different length: {} != {}", expected.len(), errors.len());
+ }
+
+ for i in 0..expected.len() {
+ if expected[i].0 != errors[i].0 {
+ bail!("error {} path differs: '{}' != '{}'", i, expected[i].0, errors[i].0);
+ }
+ if expected[i].1 != errors[i].1.to_string() {
+ bail!("error {} message differs: '{}' != '{}'", i, expected[i].1, errors[i].1);
+ }
+
+ }
+
+ Ok(())
+ })();
+
+ if result.is_err() {
+ println!("GOT: {:?}", err);
+ }
+
+ result
+}
+
+fn test_verify(
+ schema: &Schema,
+ data: &Value,
+ expected_errors: &[(&str, &str)],
+) -> Result<(), Error> {
+ match verify_json(data, schema) {
+ Ok(_) => bail!("expected errors, but got Ok()"),
+ Err(err) => compare_error(expected_errors, err)?,
+ }
+ Ok(())
+}
+
+#[test]
+fn verify_simple_object() -> Result<(), Error> {
+
+ let simple_value = json!({"prop1": 1, "prop4": "abc"});
+
+ test_verify(
+ &SIMPLE_OBJECT_SCHEMA,
+ &simple_value,
+ &[
+ ("prop1", "Expected string value."),
+ ("prop4", "schema does not allow additional properties."),
+ ("prop3", "property is missing and it is not optional."),
+ ],
+ )?;
+
+ Ok(())
+}
+
+#[test]
+fn verify_nested_object1() -> Result<(), Error> {
+
+ let nested_value = json!({"prop1": 1, "prop4": "abc"});
+
+ test_verify(
+ &NESTED_OBJECT_SCHEMA,
+ &nested_value,
+ &[
+ ("prop1", "Expected string value."),
+ ("prop4", "schema does not allow additional properties."),
+ ("arr1", "property is missing and it is not optional."),
+ ("obj1", "property is missing and it is not optional."),
+ ],
+ )?;
+
+ Ok(())
+}
+
+#[test]
+fn verify_nested_object2() -> Result<(), Error> {
+
+ let nested_value = json!({"prop1": 1, "prop4": "abc", "obj1": {}, "arr1": ["abc", 0]});
+
+ test_verify(
+ &NESTED_OBJECT_SCHEMA,
+ &nested_value,
+ &[
+ ("arr1/[1]", "Expected string value."),
+ ("obj1/prop1", "property is missing and it is not optional."),
+ ("obj1/prop3", "property is missing and it is not optional."),
+ ("prop1", "Expected string value."),
+ ("prop4", "schema does not allow additional properties."),
+ ],
+ )?;
+
+ Ok(())
+}
+
+#[test]
+fn verify_nested_property1() -> Result<(), Error> {
+
+ let value = json!({"ps1": "abc"});
+
+ test_verify(
+ &NESTED_PROPERTY_SCHEMA,
+ &value,
+ &[
+ ("ps1", "Value without key, but schema does not define a default key."),
+ ],
+ )?;
+
+ Ok(())
+}
+
+#[test]
+fn verify_nested_property2() -> Result<(), Error> {
+ let value = json!({"ps1": "abc=1"});
+
+ test_verify(
+ &NESTED_PROPERTY_SCHEMA,
+ &value,
+ &[
+ ("ps1/abc", "schema does not allow additional properties."),
+ ],
+ )?;
+
+ Ok(())
+}
+
+#[test]
+fn verify_nested_property3() -> Result<(), Error> {
+ let value = json!({"ps1": ""});
+
+ test_verify(
+ &NESTED_PROPERTY_SCHEMA,
+ &value,
+ &[
+ ("ps1/prop1", "parameter is missing and it is not optional."),
+ ("ps1/prop3", "parameter is missing and it is not optional."),
+ ],
+ )?;
+
+ Ok(())
+}
regex = "1.2"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
-#valgrind_request = { git = "https://github.com/edef1c/libvalgrind_request", version = "1.1.0", optional = true }
-# libc, nix, lazy_static
-
-# sys module:
-# libc, nix, lazy_static
-
-# api module:
-bytes = "1.0"
-futures = { version = "0.3", optional = true }
-http = "0.2"
-hyper = { version = "0.14", features = [ "full" ], optional = true }
-percent-encoding = "2.1"
-rustyline = "7"
-textwrap = "0.11"
tokio = { version = "1.0", features = [], optional = true }
-tokio-stream = { version = "0.1.1", optional = true }
-url = "2.1"
-#regex, serde, serde_json
# Macro crates:
-proxmox-api-macro = { path = "../proxmox-api-macro", optional = true, version = "0.5.1" }
proxmox-sortable-macro = { path = "../proxmox-sortable-macro", optional = true, version = "0.1.1" }
[features]
-default = [ "cli", "router" ]
+default = []
sortable-macro = ["proxmox-sortable-macro"]
-
-# api:
-api-macro = ["proxmox-api-macro"]
-test-harness = []
-cli = [ "router", "hyper", "tokio" ]
-router = [ "futures", "hyper", "tokio" ]
-
-examples = ["tokio/macros"]
-
-# tools:
-#valgrind = ["proxmox-tools/valgrind"]
+++ /dev/null
-/// Helper macro to generate a simple string type wrapper.
-///
-/// This is meant to be used with an API-type tuple struct containing a single `String` like this:
-///
-/// ```ignore
-/// # use proxmox::api::api;
-/// # use proxmox::api::schema::ApiStringFormat;
-/// # const PROXMOX_SAFE_ID_FORMAT: ApiStringFormat = ApiStringFormat::Enum(&[]);
-/// use proxmox::api_string_type;
-/// use serde::{Deserialize, Serialize};
-///
-/// api_string_type! {
-/// #[api(format: &PROXMOX_SAFE_ID_FORMAT)]
-/// /// ACME account name.
-/// #[derive(Debug, Clone, Eq, PartialEq, Hash, Deserialize, Serialize)]
-/// #[serde(transparent)]
-/// pub struct AccountName(String);
-/// }
-/// ```
-///
-/// This will automatically implements:
-/// * `Display` as a pass-through to `String`'s `Display`
-/// * `Deref`
-/// * `DerefMut`
-/// * `AsRef<str>`
-/// * `TryFrom<String>`
-/// * `fn into_string(self) -> String`
-/// * `fn as_str(&self) -> &str`
-/// * `fn from_string(inner: String) -> Result<Self, anyhow::Error>` using
-/// `StringSchema::check_constraints`.
-/// * `unsafe fn from_string_unchecked(inner: String) -> Self`
-#[macro_export]
-macro_rules! api_string_type {
- (
- $(#[$doc:meta])*
- $vis:vis struct $name:ident(String);
- ) => (
- $(#[$doc])*
- $vis struct $name(String);
-
- impl ::std::ops::Deref for $name {
- type Target = str;
-
- #[inline]
- fn deref(&self) -> &str {
- &self.0
- }
- }
-
- impl ::std::ops::DerefMut for $name {
- #[inline]
- fn deref_mut(&mut self) -> &mut str {
- &mut self.0
- }
- }
-
- impl AsRef<str> for $name {
- #[inline]
- fn as_ref(&self) -> &str {
- self.0.as_ref()
- }
- }
-
- impl ::std::convert::TryFrom<String> for $name {
- type Error = ::anyhow::Error;
-
- fn try_from(inner: String) -> Result<Self, ::anyhow::Error> {
- Self::from_string(inner)
- }
- }
-
- impl $name {
- /// Get the contained string.
- pub fn into_string(self) -> String {
- self.0
- }
-
- /// Get the string as slice.
- pub fn as_str(&self) -> &str {
- self.0.as_str()
- }
-
- /// Create an instance directly from a `String`.
- ///
- /// # Safety
- ///
- /// It is the caller's job to have validated the contents.
- /// While there are no memory safety issues, a wrong string can cause API calls to
- /// fail parameter validation.
- pub unsafe fn from_string_unchecked(name: String) -> Self {
- Self(name)
- }
-
- /// Create an instance directly from a `String`, validating it using the API schema's
- /// [`check_constraints`](::proxmox::api::schema::StringSchema::check_constraints())
- /// method.
- pub fn from_string(inner: String) -> Result<Self, ::anyhow::Error> {
- match &Self::API_SCHEMA {
- ::proxmox::api::schema::Schema::String(s) => s.check_constraints(&inner)?,
- _ => unreachable!(),
- }
- Ok(Self(inner))
- }
- }
-
- impl ::std::fmt::Display for $name {
- #[inline]
- fn fmt(&self, f: &mut ::std::fmt::Formatter) -> ::std::fmt::Result {
- ::std::fmt::Display::fmt(&self.0, f)
- }
- }
- );
-}
+++ /dev/null
-use anyhow::*;
-use serde_json::Value;
-use std::cell::RefCell;
-use std::sync::Arc;
-
-use crate::api::format::*;
-use crate::api::schema::*;
-use crate::api::*;
-
-use super::environment::CliEnvironment;
-
-use super::format::*;
-use super::getopts;
-use super::{completion::*, CliCommand, CliCommandMap, CommandLineInterface};
-
-/// Schema definition for ``--output-format`` parameter.
-///
-/// - ``text``: command specific text format.
-/// - ``json``: JSON, single line.
-/// - ``json-pretty``: JSON, human readable.
-///
-pub const OUTPUT_FORMAT: Schema = StringSchema::new("Output format.")
- .format(&ApiStringFormat::Enum(&[
- EnumEntry::new("text", "plain text output"),
- EnumEntry::new("json", "single-line json formatted output"),
- EnumEntry::new("json-pretty", "pretty-printed json output"),
- ]))
- .schema();
-
-fn parse_arguments(prefix: &str, cli_cmd: &CliCommand, args: Vec<String>) -> Result<Value, Error> {
- let (params, remaining) = match getopts::parse_arguments(
- &args,
- cli_cmd.arg_param,
- &cli_cmd.fixed_param,
- cli_cmd.info.parameters,
- ) {
- Ok((p, r)) => (p, r),
- Err(err) => {
- let err_msg = err.to_string();
- print_simple_usage_error(prefix, cli_cmd, &err_msg);
- return Err(format_err!("{}", err_msg));
- }
- };
-
- if !remaining.is_empty() {
- let err_msg = format!("got additional arguments: {:?}", remaining);
- print_simple_usage_error(prefix, cli_cmd, &err_msg);
- return Err(format_err!("{}", err_msg));
- }
-
- Ok(params)
-}
-
-async fn handle_simple_command_future(
- prefix: &str,
- cli_cmd: &CliCommand,
- args: Vec<String>,
- mut rpcenv: CliEnvironment,
-) -> Result<(), Error> {
- let params = parse_arguments(prefix, cli_cmd, args)?;
-
- match cli_cmd.info.handler {
- ApiHandler::Sync(handler) => match (handler)(params, &cli_cmd.info, &mut rpcenv) {
- Ok(value) => {
- if value != Value::Null {
- println!("Result: {}", serde_json::to_string_pretty(&value).unwrap());
- }
- }
- Err(err) => {
- eprintln!("Error: {}", err);
- return Err(err);
- }
- },
- ApiHandler::Async(handler) => {
- let future = (handler)(params, &cli_cmd.info, &mut rpcenv);
-
- match future.await {
- Ok(value) => {
- if value != Value::Null {
- println!("Result: {}", serde_json::to_string_pretty(&value).unwrap());
- }
- }
- Err(err) => {
- eprintln!("Error: {}", err);
- return Err(err);
- }
- }
- }
- ApiHandler::AsyncHttp(_) => {
- let err_msg = "CliHandler does not support ApiHandler::AsyncHttp - internal error";
- print_simple_usage_error(prefix, cli_cmd, err_msg);
- return Err(format_err!("{}", err_msg));
- }
- }
-
- Ok(())
-}
-
-fn handle_simple_command(
- prefix: &str,
- cli_cmd: &CliCommand,
- args: Vec<String>,
- mut rpcenv: CliEnvironment,
- run: Option<fn(ApiFuture) -> Result<Value, Error>>,
-) -> Result<(), Error> {
- let params = parse_arguments(prefix, cli_cmd, args)?;
-
- match cli_cmd.info.handler {
- ApiHandler::Sync(handler) => match (handler)(params, &cli_cmd.info, &mut rpcenv) {
- Ok(value) => {
- if value != Value::Null {
- println!("Result: {}", serde_json::to_string_pretty(&value).unwrap());
- }
- }
- Err(err) => {
- eprintln!("Error: {}", err);
- return Err(err);
- }
- },
- ApiHandler::Async(handler) => {
- let future = (handler)(params, &cli_cmd.info, &mut rpcenv);
- if let Some(run) = run {
- match (run)(future) {
- Ok(value) => {
- if value != Value::Null {
- println!("Result: {}", serde_json::to_string_pretty(&value).unwrap());
- }
- }
- Err(err) => {
- eprintln!("Error: {}", err);
- return Err(err);
- }
- }
- } else {
- let err_msg = "CliHandler does not support ApiHandler::Async - internal error";
- print_simple_usage_error(prefix, cli_cmd, err_msg);
- return Err(format_err!("{}", err_msg));
- }
- }
- ApiHandler::AsyncHttp(_) => {
- let err_msg = "CliHandler does not support ApiHandler::AsyncHttp - internal error";
- print_simple_usage_error(prefix, cli_cmd, err_msg);
- return Err(format_err!("{}", err_msg));
- }
- }
-
- Ok(())
-}
-
-fn parse_nested_command<'a>(
- prefix: &mut String,
- def: &'a CliCommandMap,
- args: &mut Vec<String>,
-) -> Result<&'a CliCommand, Error> {
- let mut map = def;
-
- // Note: Avoid async recursive function, because current rust compiler cant handle that
- loop {
- replace_aliases(args, &map.aliases);
-
- if args.is_empty() {
- let mut cmds: Vec<&String> = map.commands.keys().collect();
- cmds.sort();
-
- let list = cmds.iter().fold(String::new(), |mut s, item| {
- if !s.is_empty() {
- s += ", ";
- }
- s += item;
- s
- });
-
- let err_msg = format!("no command specified.\nPossible commands: {}", list);
- print_nested_usage_error(&prefix, map, &err_msg);
- return Err(format_err!("{}", err_msg));
- }
-
- let command = args.remove(0);
-
- let (_, sub_cmd) = match map.find_command(&command) {
- Some(cmd) => cmd,
- None => {
- let err_msg = format!("no such command '{}'", command);
- print_nested_usage_error(&prefix, map, &err_msg);
- return Err(format_err!("{}", err_msg));
- }
- };
-
- *prefix = format!("{} {}", prefix, command);
-
- match sub_cmd {
- CommandLineInterface::Simple(cli_cmd) => {
- //return handle_simple_command(&prefix, cli_cmd, args).await;
- return Ok(&cli_cmd);
- }
- CommandLineInterface::Nested(new_map) => map = new_map,
- }
- }
-}
-
-const API_METHOD_COMMAND_HELP: ApiMethod = ApiMethod::new(
- &ApiHandler::Sync(&help_command),
- &ObjectSchema::new(
- "Get help about specified command (or sub-command).",
- &[
- (
- "command",
- true,
- &ArraySchema::new(
- "Command. This may be a list in order to spefify nested sub-commands.",
- &StringSchema::new("Name.").schema(),
- )
- .schema(),
- ),
- (
- "verbose",
- true,
- &BooleanSchema::new("Verbose help.").schema(),
- ),
- ],
- ),
-);
-
-std::thread_local! {
- static HELP_CONTEXT: RefCell<Option<Arc<CommandLineInterface>>> = RefCell::new(None);
-}
-
-fn help_command(
- param: Value,
- _info: &ApiMethod,
- _rpcenv: &mut dyn RpcEnvironment,
-) -> Result<Value, Error> {
- let mut command: Vec<String> = param["command"]
- .as_array()
- .unwrap_or(&Vec::new())
- .iter()
- .map(|v| v.as_str().unwrap().to_string())
- .collect();
-
- let verbose = param["verbose"].as_bool();
-
- HELP_CONTEXT.with(|ctx| match &*ctx.borrow() {
- Some(def) => {
- if let CommandLineInterface::Nested(map) = def.as_ref() {
- replace_aliases(&mut command, &map.aliases);
- }
- print_help(def, String::from(""), &command, verbose);
- }
- None => {
- eprintln!("Sorry, help context not set - internal error.");
- }
- });
-
- Ok(Value::Null)
-}
-
-fn set_help_context(def: Option<Arc<CommandLineInterface>>) {
- HELP_CONTEXT.with(|ctx| {
- *ctx.borrow_mut() = def;
- });
-}
-
-pub(crate) fn help_command_def() -> CliCommand {
- CliCommand::new(&API_METHOD_COMMAND_HELP).arg_param(&["command"])
-}
-
-fn replace_aliases(args: &mut Vec<String>, aliases: &[(Vec<&'static str>, Vec<&'static str>)]) {
- for (old, new) in aliases {
- if args.len() < old.len() {
- continue;
- }
- if old[..] == args[..old.len()] {
- let new_args: Vec<String> = new.iter().map(|s| String::from(*s)).collect();
- let rest = args.split_off(old.len());
- args.truncate(0);
- args.extend(new_args);
- for arg in rest.iter() {
- args.push(arg.clone());
- }
- return;
- }
- }
-}
-
-/// Handle command invocation.
-///
-/// This command gets the command line ``args`` and tries to invoke
-/// the corresponding API handler.
-pub async fn handle_command_future(
- def: Arc<CommandLineInterface>,
- prefix: &str,
- mut args: Vec<String>,
- rpcenv: CliEnvironment,
-) -> Result<(), Error> {
- set_help_context(Some(def.clone()));
-
- let result = match &*def {
- CommandLineInterface::Simple(ref cli_cmd) => {
- handle_simple_command_future(&prefix, &cli_cmd, args, rpcenv).await
- }
- CommandLineInterface::Nested(ref map) => {
- let mut prefix = prefix.to_string();
- let cli_cmd = parse_nested_command(&mut prefix, &map, &mut args)?;
- handle_simple_command_future(&prefix, &cli_cmd, args, rpcenv).await
- }
- };
-
- set_help_context(None);
-
- result
-}
-
-/// Handle command invocation.
-///
-/// This command gets the command line ``args`` and tries to invoke
-/// the corresponding API handler.
-pub fn handle_command(
- def: Arc<CommandLineInterface>,
- prefix: &str,
- mut args: Vec<String>,
- rpcenv: CliEnvironment,
- run: Option<fn(ApiFuture) -> Result<Value, Error>>,
-) -> Result<(), Error> {
- set_help_context(Some(def.clone()));
-
- let result = match &*def {
- CommandLineInterface::Simple(ref cli_cmd) => {
- handle_simple_command(&prefix, &cli_cmd, args, rpcenv, run)
- }
- CommandLineInterface::Nested(ref map) => {
- let mut prefix = prefix.to_string();
- let cli_cmd = parse_nested_command(&mut prefix, &map, &mut args)?;
- handle_simple_command(&prefix, &cli_cmd, args, rpcenv, run)
- }
- };
-
- set_help_context(None);
-
- result
-}
-
-fn prepare_cli_command(def: &CommandLineInterface) -> (String, Vec<String>) {
- let mut args = std::env::args();
-
- let prefix = args.next().unwrap();
- let prefix = prefix.rsplit('/').next().unwrap().to_string(); // without path
-
- let args: Vec<String> = args.collect();
-
- if !args.is_empty() {
- if args[0] == "bashcomplete" {
- print_bash_completion(&def);
- std::process::exit(0);
- }
-
- if args[0] == "printdoc" {
- let usage = match def {
- CommandLineInterface::Simple(cli_cmd) => {
- generate_usage_str(&prefix, &cli_cmd, DocumentationFormat::ReST, "", &[])
- }
- CommandLineInterface::Nested(map) => {
- generate_nested_usage(&prefix, &map, DocumentationFormat::ReST)
- }
- };
- println!("{}", usage);
- std::process::exit(0);
- }
- }
-
- (prefix, args)
-}
-
-/// Helper to get arguments and invoke the command (async).
-///
-/// This helper reads arguments with ``std::env::args()``. The first
-/// argument is assumed to be the program name, and is passed as ``prefix`` to
-/// ``handle_command()``.
-///
-/// This helper automatically add the help command, and two special
-/// sub-command:
-///
-/// - ``bashcomplete``: Output bash completions instead of running the command.
-/// - ``printdoc``: Output ReST documentation.
-///
-pub async fn run_async_cli_command<C: Into<CommandLineInterface>>(def: C, rpcenv: CliEnvironment) {
- let def = match def.into() {
- CommandLineInterface::Simple(cli_cmd) => CommandLineInterface::Simple(cli_cmd),
- CommandLineInterface::Nested(map) => CommandLineInterface::Nested(map.insert_help()),
- };
-
- let (prefix, args) = prepare_cli_command(&def);
-
- if handle_command_future(Arc::new(def), &prefix, args, rpcenv)
- .await
- .is_err()
- {
- std::process::exit(-1);
- }
-}
-
-/// Helper to get arguments and invoke the command.
-///
-/// This is the synchrounous version of run_async_cli_command. You can
-/// pass an optional ``run`` function to execute async commands (else
-/// async commands simply fail).
-pub fn run_cli_command<C: Into<CommandLineInterface>>(
- def: C,
- rpcenv: CliEnvironment,
- run: Option<fn(ApiFuture) -> Result<Value, Error>>,
-) {
- let def = match def.into() {
- CommandLineInterface::Simple(cli_cmd) => CommandLineInterface::Simple(cli_cmd),
- CommandLineInterface::Nested(map) => CommandLineInterface::Nested(map.insert_help()),
- };
-
- let (prefix, args) = prepare_cli_command(&def);
-
- if handle_command(Arc::new(def), &prefix, args, rpcenv, run).is_err() {
- std::process::exit(-1);
- }
-}
+++ /dev/null
-use super::*;
-
-use crate::api::schema::*;
-
-fn record_done_argument(
- done: &mut HashMap<String, String>,
- parameters: ParameterSchema,
- key: &str,
- value: &str,
-) {
- if let Some((_, schema)) = parameters.lookup(key) {
- match schema {
- Schema::Array(_) => { /* do nothing ?? */ }
- _ => {
- done.insert(key.to_owned(), value.to_owned());
- }
- }
- }
-}
-
-fn get_property_completion(
- schema: &Schema,
- name: &str,
- completion_functions: &HashMap<String, CompletionFunction>,
- arg: &str,
- param: &HashMap<String, String>,
-) -> Vec<String> {
- if let Some(callback) = completion_functions.get(name) {
- let list = (callback)(arg, param);
- let mut completions = Vec::new();
- for value in list {
- if value.starts_with(arg) {
- completions.push(value);
- }
- }
- return completions;
- }
-
- match schema {
- Schema::String(StringSchema {
- format: Some(format),
- ..
- }) => {
- if let ApiStringFormat::Enum(variants) = format {
- let mut completions = Vec::new();
- for variant in variants.iter() {
- if variant.value.starts_with(arg) {
- completions.push(variant.value.to_string());
- }
- }
- return completions;
- }
- }
- Schema::Boolean(BooleanSchema { .. }) => {
- let mut completions = Vec::new();
- let mut lowercase_arg = arg.to_string();
- lowercase_arg.make_ascii_lowercase();
- for value in ["0", "1", "yes", "no", "true", "false", "on", "off"].iter() {
- if value.starts_with(&lowercase_arg) {
- completions.push((*value).to_string());
- }
- }
- return completions;
- }
- Schema::Array(ArraySchema { items, .. }) => {
- if let Schema::String(_) = items {
- return get_property_completion(&items, name, completion_functions, arg, param);
- }
- }
- _ => {}
- }
-
- Vec::new()
-}
-
-fn get_simple_completion(
- cli_cmd: &CliCommand,
- done: &mut HashMap<String, String>,
- arg_param: &[&str], // we remove done arguments
- args: &[String],
-) -> Vec<String> {
- //eprintln!("COMPL: {:?} {:?} {}", arg_param, args, args.len());
-
- if !arg_param.is_empty() {
- let prop_name = arg_param[0];
- if let Some((optional, schema)) = cli_cmd.info.parameters.lookup(prop_name) {
- let is_array_param = if let Schema::Array(_) = schema {
- true
- } else {
- false
- };
-
- if (optional || is_array_param) && args[0].starts_with('-') {
- // argument parameter is optional (or array) , and arg
- // looks like an option, so assume its empty and
- // complete the rest
- } else {
- record_done_argument(done, cli_cmd.info.parameters, prop_name, &args[0]);
- if args.len() > 1 {
- if is_array_param {
- return get_simple_completion(cli_cmd, done, &arg_param[..], &args[1..]);
- } else {
- return get_simple_completion(cli_cmd, done, &arg_param[1..], &args[1..]);
- }
- }
-
- if args.len() == 1 {
- return get_property_completion(
- schema,
- prop_name,
- &cli_cmd.completion_functions,
- &args[0],
- done,
- );
- }
-
- return Vec::new();
- }
- } else {
- // unknown arg_param - should never happen
- return Vec::new();
- }
- }
- if args.is_empty() {
- return Vec::new();
- }
-
- // Try to parse all argumnets but last, record args already done
- if args.len() > 1 {
- let mut errors = ParameterError::new(); // we simply ignore any parsing errors here
- let (data, _remaining) = getopts::parse_argument_list(
- &args[0..args.len() - 1],
- cli_cmd.info.parameters,
- &mut errors,
- );
- for (key, value) in &data {
- record_done_argument(done, cli_cmd.info.parameters, key, value);
- }
- }
-
- let prefix = &args[args.len() - 1]; // match on last arg
-
- // complete option-name or option-value ?
- if !prefix.starts_with('-') && args.len() > 1 {
- let last = &args[args.len() - 2];
- if last.starts_with("--") && last.len() > 2 {
- let prop_name = &last[2..];
- if let Some((_, schema)) = cli_cmd.info.parameters.lookup(prop_name) {
- return get_property_completion(
- schema,
- prop_name,
- &cli_cmd.completion_functions,
- &prefix,
- done,
- );
- }
- return Vec::new();
- }
- }
-
- let mut completions = Vec::new();
- for (name, _optional, _schema) in cli_cmd.info.parameters.properties() {
- if done.contains_key(*name) {
- continue;
- }
- if cli_cmd.arg_param.contains(name) {
- continue;
- }
- let option = String::from("--") + name;
- if option.starts_with(prefix) {
- completions.push(option);
- }
- }
- completions
-}
-
-fn get_help_completion(
- def: &CommandLineInterface,
- help_cmd: &CliCommand,
- args: &[String],
-) -> Vec<String> {
- let mut done = HashMap::new();
-
- match def {
- CommandLineInterface::Simple(_) => get_simple_completion(help_cmd, &mut done, &[], args),
- CommandLineInterface::Nested(map) => {
- if args.is_empty() {
- let mut completions = Vec::new();
- for cmd in map.commands.keys() {
- completions.push(cmd.to_string());
- }
- return completions;
- }
-
- let first = &args[0];
- if args.len() > 1 {
- if let Some(sub_cmd) = map.commands.get(first) {
- // do exact match here
- return get_help_completion(sub_cmd, help_cmd, &args[1..]);
- }
- return Vec::new();
- }
-
- if first.starts_with('-') {
- return get_simple_completion(help_cmd, &mut done, &[], args);
- }
-
- let mut completions = Vec::new();
- for cmd in map.commands.keys() {
- if cmd.starts_with(first) {
- completions.push(cmd.to_string());
- }
- }
- completions
- }
- }
-}
-
-fn get_nested_completion(def: &CommandLineInterface, args: &[String]) -> Vec<String> {
- match def {
- CommandLineInterface::Simple(cli_cmd) => {
- let mut done: HashMap<String, String> = HashMap::new();
- cli_cmd.fixed_param.iter().for_each(|(key, value)| {
- record_done_argument(&mut done, cli_cmd.info.parameters, &key, &value);
- });
- get_simple_completion(cli_cmd, &mut done, &cli_cmd.arg_param, args)
- }
- CommandLineInterface::Nested(map) => {
- if args.is_empty() {
- let mut completions = Vec::new();
- for cmd in map.commands.keys() {
- completions.push(cmd.to_string());
- }
- return completions;
- }
- let first = &args[0];
- if args.len() > 1 {
- if let Some((_, sub_cmd)) = map.find_command(first) {
- return get_nested_completion(sub_cmd, &args[1..]);
- }
- return Vec::new();
- }
- let mut completions = Vec::new();
- for cmd in map.commands.keys() {
- if cmd.starts_with(first) {
- completions.push(cmd.to_string());
- }
- }
- completions
- }
- }
-}
-
-/// Helper to generate bash completions.
-///
-/// This helper extracts the command line from environment variable
-/// set by ``bash``, namely ``COMP_LINE`` and ``COMP_POINT``. This is
-/// passed to ``get_completions()``. Returned values are printed to
-/// ``stdout``.
-pub fn print_bash_completion(def: &CommandLineInterface) {
- let comp_point: usize = match std::env::var("COMP_POINT") {
- Ok(val) => match usize::from_str_radix(&val, 10) {
- Ok(i) => i,
- Err(_) => return,
- },
- Err(_) => return,
- };
-
- let cmdline = match std::env::var("COMP_LINE") {
- Ok(mut val) => {
- if let Some((byte_pos, _)) = val.char_indices().nth(comp_point) {
- val.truncate(byte_pos);
- }
- val
- }
- Err(_) => return,
- };
-
- let (_start, completions) = super::get_completions(def, &cmdline, true);
-
- for item in completions {
- println!("{}", item);
- }
-}
-
-/// Compute possible completions for a partial command
-pub fn get_completions(
- cmd_def: &CommandLineInterface,
- line: &str,
- skip_first: bool,
-) -> (usize, Vec<String>) {
- let (mut args, start) = match shellword_split_unclosed(line, false) {
- (mut args, None) => {
- args.push("".into());
- (args, line.len())
- }
- (mut args, Some((start, arg, _quote))) => {
- args.push(arg);
- (args, start)
- }
- };
-
- if skip_first {
- if args.is_empty() {
- return (0, Vec::new());
- }
-
- args.remove(0); // no need for program name
- }
-
- let completions = if !args.is_empty() && args[0] == "help" {
- get_help_completion(cmd_def, &help_command_def(), &args[1..])
- } else {
- get_nested_completion(cmd_def, &args)
- };
-
- (start, completions)
-}
-
-#[cfg(test)]
-mod test {
-
- use anyhow::*;
- use serde_json::Value;
-
- use crate::api::{cli::*, schema::*, *};
-
- fn dummy_method(
- _param: Value,
- _info: &ApiMethod,
- _rpcenv: &mut dyn RpcEnvironment,
- ) -> Result<Value, Error> {
- Ok(Value::Null)
- }
-
- const API_METHOD_SIMPLE1: ApiMethod = ApiMethod::new(
- &ApiHandler::Sync(&dummy_method),
- &ObjectSchema::new(
- "Simple API method with one required and one optionl argument.",
- &[
- (
- "optional-arg",
- true,
- &BooleanSchema::new("Optional boolean argument.")
- .default(false)
- .schema(),
- ),
- (
- "required-arg",
- false,
- &StringSchema::new("Required string argument.").schema(),
- ),
- ],
- ),
- );
-
- fn get_complex_test_cmddef() -> CommandLineInterface {
- let sub_def = CliCommandMap::new()
- .insert("l1c1", CliCommand::new(&API_METHOD_SIMPLE1))
- .insert("l1c2", CliCommand::new(&API_METHOD_SIMPLE1));
-
- let cmd_def = CliCommandMap::new()
- .insert_help()
- .insert("l0sub", CommandLineInterface::Nested(sub_def))
- .insert("l0c1", CliCommand::new(&API_METHOD_SIMPLE1))
- .insert(
- "l0c2",
- CliCommand::new(&API_METHOD_SIMPLE1).arg_param(&["required-arg"]),
- )
- .insert(
- "l0c3",
- CliCommand::new(&API_METHOD_SIMPLE1).arg_param(&["required-arg", "optional-arg"]),
- );
-
- cmd_def.into()
- }
-
- fn test_completions(cmd_def: &CommandLineInterface, line: &str, start: usize, expect: &[&str]) {
- let mut expect: Vec<String> = expect.iter().map(|s| s.to_string()).collect();
- expect.sort();
-
- let (completion_start, mut completions) = get_completions(cmd_def, line, false);
- completions.sort();
-
- assert_eq!((start, expect), (completion_start, completions));
- }
-
- #[test]
- fn test_nested_completion() {
- let cmd_def = get_complex_test_cmddef();
-
- test_completions(&cmd_def, "", 0, &["help", "l0c1", "l0c2", "l0c3", "l0sub"]);
-
- test_completions(&cmd_def, "l0c1 ", 5, &["--optional-arg", "--required-arg"]);
-
- test_completions(&cmd_def, "l0c1 -", 5, &["--optional-arg", "--required-arg"]);
-
- test_completions(
- &cmd_def,
- "l0c1 --",
- 5,
- &["--optional-arg", "--required-arg"],
- );
-
- test_completions(&cmd_def, "l0c1 ---", 5, &[]);
-
- test_completions(&cmd_def, "l0c1 x", 5, &[]);
-
- test_completions(&cmd_def, "l0c1 --r", 5, &["--required-arg"]);
-
- test_completions(&cmd_def, "l0c1 --required-arg", 5, &["--required-arg"]);
-
- test_completions(
- &cmd_def,
- "l0c1 --required-arg -",
- 20,
- // Note: --required-arg is not finished, so it still pops up
- &["--required-arg", "--optional-arg"],
- );
-
- test_completions(
- &cmd_def,
- "l0c1 --required-arg test -",
- 25,
- &["--optional-arg"],
- );
-
- test_completions(
- &cmd_def,
- "l0c1 --required-arg test --optional-arg ",
- 40,
- &["0", "1", "false", "no", "true", "yes", "on", "off"],
- );
-
- test_completions(
- &cmd_def,
- "l0c1 --required-arg test --optional-arg f",
- 40,
- &["false"],
- );
-
- test_completions(
- &cmd_def,
- "l0c1 --required-arg test --optional-arg F",
- 40,
- &["false"],
- );
-
- test_completions(
- &cmd_def,
- "l0c1 --required-arg test --optional-arg Yes",
- 40,
- &["yes"],
- );
- }
-
- #[test]
- fn test_help_completion() {
- let cmd_def = get_complex_test_cmddef();
-
- test_completions(&cmd_def, "h", 0, &["help"]);
-
- test_completions(
- &cmd_def,
- "help ",
- 5,
- &["help", "l0sub", "l0c1", "l0c3", "l0c2"],
- );
-
- test_completions(&cmd_def, "help l0", 5, &["l0sub", "l0c1", "l0c3", "l0c2"]);
-
- test_completions(&cmd_def, "help -", 5, &["--verbose"]);
-
- test_completions(&cmd_def, "help l0c2", 5, &["l0c2"]);
-
- test_completions(&cmd_def, "help l0c2 ", 10, &["--verbose"]);
-
- test_completions(&cmd_def, "help l0c2 --verbose -", 20, &[]);
-
- test_completions(&cmd_def, "help l0s", 5, &["l0sub"]);
-
- test_completions(&cmd_def, "help l0sub ", 11, &["l1c1", "l1c2"]);
-
- test_completions(&cmd_def, "help l0sub l1c2 -", 16, &["--verbose"]);
-
- test_completions(&cmd_def, "help l0sub l1c2 --verbose -", 26, &[]);
-
- test_completions(&cmd_def, "help l0sub l1c3", 11, &[]);
- }
-}
+++ /dev/null
-use serde_json::Value;
-
-use crate::api::{RpcEnvironment, RpcEnvironmentType};
-
-/// `RpcEnvironmet` implementation for command line tools
-#[derive(Default)]
-pub struct CliEnvironment {
- result_attributes: Value,
- auth_id: Option<String>,
-}
-
-impl CliEnvironment {
- pub fn new() -> Self {
- Default::default()
- }
-}
-
-impl RpcEnvironment for CliEnvironment {
- fn result_attrib_mut(&mut self) -> &mut Value {
- &mut self.result_attributes
- }
-
- fn result_attrib(&self) -> &Value {
- &self.result_attributes
- }
-
- fn env_type(&self) -> RpcEnvironmentType {
- RpcEnvironmentType::CLI
- }
-
- fn set_auth_id(&mut self, auth_id: Option<String>) {
- self.auth_id = auth_id;
- }
-
- fn get_auth_id(&self) -> Option<String> {
- self.auth_id.clone()
- }
-}
+++ /dev/null
-#![allow(clippy::match_bool)] // just no...
-
-use serde_json::Value;
-
-use std::collections::HashSet;
-
-use crate::api::format::*;
-use crate::api::router::ReturnType;
-use crate::api::schema::*;
-
-use super::{value_to_text, TableFormatOptions};
-use super::{CliCommand, CliCommandMap, CommandLineInterface};
-
-/// Helper function to format and print result.
-///
-/// This is implemented for machine generatable formats 'json' and
-/// 'json-pretty'. The 'text' format needs to be handled somewhere
-/// else.
-pub fn format_and_print_result(result: &Value, output_format: &str) {
- if output_format == "json-pretty" {
- println!("{}", serde_json::to_string_pretty(&result).unwrap());
- } else if output_format == "json" {
- println!("{}", serde_json::to_string(&result).unwrap());
- } else {
- unimplemented!();
- }
-}
-
-/// Helper function to format and print result.
-///
-/// This is implemented for machine generatable formats 'json' and
-/// 'json-pretty', and for the 'text' format which generates nicely
-/// formatted tables with borders.
-pub fn format_and_print_result_full(
- result: &mut Value,
- return_type: &ReturnType,
- output_format: &str,
- options: &TableFormatOptions,
-) {
- if return_type.optional && result.is_null() {
- return;
- }
-
- if output_format == "json-pretty" {
- println!("{}", serde_json::to_string_pretty(&result).unwrap());
- } else if output_format == "json" {
- println!("{}", serde_json::to_string(&result).unwrap());
- } else if output_format == "text" {
- if let Err(err) = value_to_text(std::io::stdout(), result, &return_type.schema, options) {
- eprintln!("unable to format result: {}", err);
- }
- } else {
- eprintln!("undefined output format '{}'", output_format);
- }
-}
-
-/// Helper to generate command usage text for simple commands.
-pub fn generate_usage_str(
- prefix: &str,
- cli_cmd: &CliCommand,
- format: DocumentationFormat,
- indent: &str,
- skip_options: &[&str],
-) -> String {
- let arg_param = cli_cmd.arg_param;
- let fixed_param = &cli_cmd.fixed_param;
- let schema = cli_cmd.info.parameters;
-
- let mut done_hash = HashSet::<&str>::new();
- for option in skip_options {
- done_hash.insert(option);
- }
-
- let mut args = String::new();
-
- for positional_arg in arg_param {
- match schema.lookup(positional_arg) {
- Some((optional, param_schema)) => {
- args.push(' ');
-
- let is_array = matches!(param_schema, Schema::Array(_));
- if optional {
- args.push('[');
- }
- if is_array {
- args.push('{');
- }
- args.push('<');
- args.push_str(positional_arg);
- args.push('>');
- if is_array {
- args.push('}');
- }
- if optional {
- args.push(']');
- }
-
- done_hash.insert(positional_arg);
- }
- None => panic!("no such property '{}' in schema", positional_arg),
- }
- }
-
- let mut arg_descr = String::new();
- for positional_arg in arg_param {
- let (_optional, param_schema) = schema.lookup(positional_arg).unwrap();
- let param_descr = get_property_description(
- positional_arg,
- param_schema,
- ParameterDisplayStyle::Fixed,
- format,
- );
- arg_descr.push_str(¶m_descr);
- }
-
- let mut options = String::new();
-
- for (prop, optional, param_schema) in schema.properties() {
- if done_hash.contains(prop) {
- continue;
- }
- if fixed_param.contains_key(prop) {
- continue;
- }
-
- let type_text = get_schema_type_text(param_schema, ParameterDisplayStyle::Arg);
-
- let prop_descr =
- get_property_description(prop, param_schema, ParameterDisplayStyle::Arg, format);
-
- if *optional {
- options.push_str(&prop_descr);
- } else {
- args.push_str(" --");
- args.push_str(prop);
- args.push(' ');
- args.push_str(&type_text);
-
- arg_descr.push_str(&prop_descr);
- }
-
- done_hash.insert(prop);
- }
-
- let option_indicator = if !options.is_empty() {
- " [OPTIONS]"
- } else {
- ""
- };
-
- let mut text = match format {
- DocumentationFormat::Short => {
- return format!("{}{}{}{}\n", indent, prefix, args, option_indicator);
- }
- DocumentationFormat::Long => format!("{}{}{}{}\n", indent, prefix, args, option_indicator),
- DocumentationFormat::Full => format!(
- "{}{}{}{}\n\n{}\n\n",
- indent,
- prefix,
- args,
- option_indicator,
- schema.description()
- ),
- DocumentationFormat::ReST => format!(
- "``{}{}{}``\n\n{}\n\n",
- prefix,
- args,
- option_indicator,
- schema.description()
- ),
- };
-
- if !arg_descr.is_empty() {
- text.push_str(&arg_descr);
- }
-
- if !options.is_empty() {
- text.push_str("Optional parameters:\n\n");
- text.push_str(&options);
- }
- text
-}
-
-/// Print command usage for simple commands to ``stderr``.
-pub fn print_simple_usage_error(prefix: &str, cli_cmd: &CliCommand, err_msg: &str) {
- let usage = generate_usage_str(prefix, cli_cmd, DocumentationFormat::Long, "", &[]);
- eprint!("Error: {}\nUsage: {}", err_msg, usage);
-}
-
-/// Print command usage for nested commands to ``stderr``.
-pub fn print_nested_usage_error(prefix: &str, def: &CliCommandMap, err_msg: &str) {
- let usage = generate_nested_usage(prefix, def, DocumentationFormat::Short);
- eprintln!("Error: {}\n\nUsage:\n\n{}", err_msg, usage);
-}
-
-/// Helper to generate command usage text for nested commands.
-pub fn generate_nested_usage(
- prefix: &str,
- def: &CliCommandMap,
- format: DocumentationFormat,
-) -> String {
- let mut cmds: Vec<&String> = def.commands.keys().collect();
- cmds.sort();
-
- let skip_options = def.usage_skip_options;
-
- let mut usage = String::new();
-
- for cmd in cmds {
- let new_prefix = if prefix.is_empty() {
- String::from(cmd)
- } else {
- format!("{} {}", prefix, cmd)
- };
-
- match def.commands.get(cmd).unwrap() {
- CommandLineInterface::Simple(cli_cmd) => {
- if !usage.is_empty() && format == DocumentationFormat::ReST {
- usage.push_str("----\n\n");
- }
- usage.push_str(&generate_usage_str(
- &new_prefix,
- cli_cmd,
- format,
- "",
- skip_options,
- ));
- }
- CommandLineInterface::Nested(map) => {
- usage.push_str(&generate_nested_usage(&new_prefix, map, format));
- }
- }
- }
-
- usage
-}
-
-/// Print help text to ``stderr``.
-pub fn print_help(
- top_def: &CommandLineInterface,
- mut prefix: String,
- args: &[String],
- mut verbose: Option<bool>,
-) {
- let mut iface = top_def;
-
- for cmd in args {
- if let CommandLineInterface::Nested(map) = iface {
- if let Some((full_name, subcmd)) = map.find_command(cmd) {
- iface = subcmd;
- if !prefix.is_empty() {
- prefix.push(' ');
- }
- prefix.push_str(&full_name);
- continue;
- }
- }
- if prefix.is_empty() {
- eprintln!("no such command '{}'", cmd);
- } else {
- eprintln!("no such command '{} {}'", prefix, cmd);
- }
- return;
- }
-
- if verbose.is_none() {
- if let CommandLineInterface::Simple(_) = iface {
- verbose = Some(true);
- }
- }
-
- let format = match verbose.unwrap_or(false) {
- true => DocumentationFormat::Full,
- false => DocumentationFormat::Short,
- };
-
- match iface {
- CommandLineInterface::Nested(map) => {
- println!("Usage:\n\n{}", generate_nested_usage(&prefix, map, format));
- }
- CommandLineInterface::Simple(cli_cmd) => {
- println!(
- "Usage: {}",
- generate_usage_str(&prefix, cli_cmd, format, "", &[])
- );
- }
- }
-}
+++ /dev/null
-use std::collections::HashMap;
-
-use anyhow::*;
-use serde_json::Value;
-
-use crate::api::schema::*;
-
-#[derive(Debug)]
-enum RawArgument {
- Separator,
- Argument { value: String },
- Option { name: String, value: Option<String> },
-}
-
-fn parse_argument(arg: &str) -> RawArgument {
- let bytes = arg.as_bytes();
-
- let length = bytes.len();
-
- if length < 2 || bytes[0] != b'-' {
- return RawArgument::Argument {
- value: arg.to_string(),
- };
- }
-
- let first = if bytes[1] == b'-' {
- if length == 2 {
- return RawArgument::Separator;
- }
- 2
- } else {
- 1
- };
-
- if let Some(i) = bytes[first..length].iter().position(|b| *b == b'=') {
- let start = i + first;
- // Since we take a &str, we know the contents of it are valid utf8.
- // Since bytes[start] == b'=', we know the byte beginning at start is a single-byte
- // code pointer. We also know that 'first' points exactly after a single-byte code
- // point as it points to the first byte after a hyphen.
- // Therefore we know arg[first..start] is valid utf-8, therefore it is safe to use
- // get_unchecked() to speed things up.
- return RawArgument::Option {
- name: unsafe { arg.get_unchecked(first..start).to_string() },
- value: Some(unsafe { arg.get_unchecked((start + 1)..).to_string() }),
- };
- }
-
- RawArgument::Option {
- name: unsafe { arg.get_unchecked(first..).to_string() },
- value: None,
- }
-}
-
-/// parse as many arguments as possible into a Vec<String, String>. This does not
-/// verify the schema.
-/// Returns parsed data and the remaining arguments as two separate array
-pub(crate) fn parse_argument_list<T: AsRef<str>>(
- args: &[T],
- schema: ParameterSchema,
- errors: &mut ParameterError,
-) -> (Vec<(String, String)>, Vec<String>) {
- let mut data: Vec<(String, String)> = vec![];
- let mut remaining: Vec<String> = vec![];
-
- let mut pos = 0;
-
- while pos < args.len() {
- match parse_argument(args[pos].as_ref()) {
- RawArgument::Separator => {
- break;
- }
- RawArgument::Option { name, value } => match value {
- None => {
- let mut want_bool = false;
- let mut can_default = false;
- if let Some((_optional, param_schema)) = schema.lookup(&name) {
- if let Schema::Boolean(boolean_schema) = param_schema {
- want_bool = true;
- match boolean_schema.default {
- Some(false) | None => can_default = true,
- Some(true) => (),
- }
- }
- }
-
- let mut next_is_argument = false;
- let mut next_is_bool = false;
-
- if (pos + 1) < args.len() {
- let next = args[pos + 1].as_ref();
- if let RawArgument::Argument { .. } = parse_argument(next) {
- next_is_argument = true;
- if parse_boolean(next).is_ok() {
- next_is_bool = true;
- }
- }
- }
-
- if want_bool {
- if next_is_bool {
- pos += 1;
- data.push((name, args[pos].as_ref().to_string()));
- } else if can_default {
- data.push((name, "true".to_string()));
- } else {
- errors.push(name.to_string(), format_err!("missing boolean value."));
- }
- } else if next_is_argument {
- pos += 1;
- data.push((name, args[pos].as_ref().to_string()));
- } else {
- errors.push(name.to_string(), format_err!("missing parameter value."));
- }
- }
- Some(v) => {
- data.push((name, v));
- }
- },
- RawArgument::Argument { value } => {
- remaining.push(value);
- }
- }
-
- pos += 1;
- }
-
- remaining.reserve(args.len() - pos);
- for i in &args[pos..] {
- remaining.push(i.as_ref().to_string());
- }
-
- (data, remaining)
-}
-
-/// Parses command line arguments using a `Schema`
-///
-/// Returns parsed options as json object, together with the
-/// list of additional command line arguments.
-pub fn parse_arguments<T: AsRef<str>>(
- args: &[T],
- arg_param: &[&str],
- fixed_param: &HashMap<&'static str, String>,
- schema: ParameterSchema,
-) -> Result<(Value, Vec<String>), ParameterError> {
- let mut errors = ParameterError::new();
-
- // first check if all arg_param exists in schema
-
- let mut last_arg_param_is_optional = false;
- let mut last_arg_param_is_array = false;
-
- for i in 0..arg_param.len() {
- let name = arg_param[i];
- if let Some((optional, param_schema)) = schema.lookup(&name) {
- if i == arg_param.len() - 1 {
- last_arg_param_is_optional = optional;
- if let Schema::Array(_) = param_schema {
- last_arg_param_is_array = true;
- }
- } else if optional {
- panic!("positional argument '{}' may not be optional", name);
- }
- } else {
- panic!("no such property '{}' in schema", name);
- }
- }
-
- let (mut data, mut remaining) = parse_argument_list(args, schema, &mut errors);
-
- for i in 0..arg_param.len() {
- let name = arg_param[i];
- let is_last_arg_param = i == (arg_param.len() - 1);
-
- if remaining.is_empty() {
- if !(is_last_arg_param && last_arg_param_is_optional) {
- errors.push(name.to_string(), format_err!("missing argument"));
- }
- } else if is_last_arg_param && last_arg_param_is_array {
- for value in remaining {
- data.push((name.to_string(), value));
- }
- remaining = vec![];
- } else {
- data.push((name.to_string(), remaining.remove(0)));
- }
- }
-
- if !errors.is_empty() {
- return Err(errors);
- }
-
- for (name, value) in fixed_param.iter() {
- data.push((name.to_string(), value.to_string()));
- }
-
- let options = parse_parameter_strings(&data, schema, true)?;
-
- Ok((options, remaining))
-}
-
-#[test]
-fn test_boolean_arg() {
- const PARAMETERS: ObjectSchema = ObjectSchema::new(
- "Parameters:",
- &[("enable", false, &BooleanSchema::new("Enable").schema())],
- );
-
- let mut variants: Vec<(Vec<&str>, bool)> = vec![];
- variants.push((vec!["-enable"], true));
- variants.push((vec!["-enable=1"], true));
- variants.push((vec!["-enable", "yes"], true));
- variants.push((vec!["-enable", "Yes"], true));
- variants.push((vec!["--enable", "1"], true));
- variants.push((vec!["--enable", "ON"], true));
- variants.push((vec!["--enable", "true"], true));
-
- variants.push((vec!["--enable", "0"], false));
- variants.push((vec!["--enable", "no"], false));
- variants.push((vec!["--enable", "off"], false));
- variants.push((vec!["--enable", "false"], false));
-
- for (args, expect) in variants {
- let res = parse_arguments(
- &args,
- &vec![],
- &HashMap::new(),
- ParameterSchema::from(&PARAMETERS),
- );
- assert!(res.is_ok());
- if let Ok((options, remaining)) = res {
- assert!(options["enable"] == expect);
- assert!(remaining.len() == 0);
- }
- }
-}
-
-#[test]
-fn test_argument_paramenter() {
- const PARAMETERS: ObjectSchema = ObjectSchema::new(
- "Parameters:",
- &[
- ("enable", false, &BooleanSchema::new("Enable.").schema()),
- ("storage", false, &StringSchema::new("Storage.").schema()),
- ],
- );
-
- let args = vec!["-enable", "local"];
- let res = parse_arguments(
- &args,
- &vec!["storage"],
- &HashMap::new(),
- ParameterSchema::from(&PARAMETERS),
- );
- assert!(res.is_ok());
- if let Ok((options, remaining)) = res {
- assert!(options["enable"] == true);
- assert!(options["storage"] == "local");
- assert!(remaining.len() == 0);
- }
-}
+++ /dev/null
-//! Tools to create command line parsers
-//!
-//! This crate provides convenient helpers to create command line
-//! parsers using Schema definitions.
-//!
-//! ## Features
-//!
-//! - Use declarative API schema to define the CLI
-//! - Automatic parameter verification
-//! - Automatically generate documentation and manual pages
-//! - Automatically generate bash completion helpers
-//! - Ability to create interactive commands (using ``rustyline``)
-//! - Supports complex/nested commands
-
-mod environment;
-pub use environment::*;
-
-mod shellword;
-pub use shellword::*;
-
-mod format;
-pub use format::*;
-
-mod text_table;
-pub use text_table::*;
-
-mod completion;
-pub use completion::*;
-
-mod getopts;
-pub use getopts::*;
-
-mod command;
-pub use command::*;
-
-mod readline;
-pub use readline::*;
-
-use std::collections::HashMap;
-
-use crate::api::ApiMethod;
-
-/// Completion function for single parameters.
-///
-/// Completion functions gets the current parameter value, and should
-/// return a list of all possible values.
-pub type CompletionFunction = fn(&str, &HashMap<String, String>) -> Vec<String>;
-
-/// Define a simple CLI command.
-pub struct CliCommand {
- /// The Schema definition.
- pub info: &'static ApiMethod,
- /// Argument parameter list.
- ///
- /// Those parameters are expected to be passed as command line
- /// arguments in the specified order. All other parameters needs
- /// to be specified as ``--option <value>`` pairs.
- pub arg_param: &'static [&'static str],
- /// Predefined parameters.
- pub fixed_param: HashMap<&'static str, String>,
- /// Completion functions.
- ///
- /// Each parameter may have an associated completion function,
- /// which is called by the shell completion handler.
- pub completion_functions: HashMap<String, CompletionFunction>,
-}
-
-impl CliCommand {
- /// Create a new instance.
- pub fn new(info: &'static ApiMethod) -> Self {
- Self {
- info,
- arg_param: &[],
- fixed_param: HashMap::new(),
- completion_functions: HashMap::new(),
- }
- }
-
- /// Set argument parameter list.
- pub fn arg_param(mut self, names: &'static [&'static str]) -> Self {
- self.arg_param = names;
- self
- }
-
- /// Set fixed parameters.
- pub fn fixed_param(mut self, key: &'static str, value: String) -> Self {
- self.fixed_param.insert(key, value);
- self
- }
-
- /// Set completion functions.
- pub fn completion_cb(mut self, param_name: &str, cb: CompletionFunction) -> Self {
- self.completion_functions.insert(param_name.into(), cb);
- self
- }
-}
-
-/// Define nested CLI commands.
-#[derive(Default)]
-pub struct CliCommandMap {
- /// Each command has an unique name. The map associates names with
- /// command definitions.
- pub commands: HashMap<String, CommandLineInterface>,
- pub aliases: Vec<(Vec<&'static str>, Vec<&'static str>)>,
- /// List of options to suppress in generate_usage
- pub usage_skip_options: &'static [&'static str],
-}
-
-impl CliCommandMap {
- /// Create a new instance.
- pub fn new() -> Self {
- Default::default()
- }
-
- /// Insert another command.
- pub fn insert<C: Into<CommandLineInterface>>(mut self, name: &'static str, cli: C) -> Self {
- self.commands.insert(name.into(), cli.into());
- self
- }
-
- pub fn alias(mut self, old: &'static [&'static str], new: &'static [&'static str]) -> Self {
- self.aliases.push((Vec::from(old), Vec::from(new)));
- self
- }
-
- pub fn usage_skip_options(mut self, list: &'static [&'static str]) -> Self {
- self.usage_skip_options = list;
- self
- }
-
- /// Insert the help command.
- pub fn insert_help(mut self) -> Self {
- self.commands
- .insert(String::from("help"), help_command_def().into());
- self
- }
-
- fn find_command(&self, name: &str) -> Option<(String, &CommandLineInterface)> {
- if let Some(sub_cmd) = self.commands.get(name) {
- return Some((name.to_string(), sub_cmd));
- };
-
- let mut matches: Vec<&str> = vec![];
-
- for cmd in self.commands.keys() {
- if cmd.starts_with(name) {
- matches.push(cmd);
- }
- }
-
- if matches.len() != 1 {
- return None;
- }
-
- if let Some(sub_cmd) = self.commands.get(matches[0]) {
- return Some((matches[0].to_string(), sub_cmd));
- };
-
- None
- }
-}
-
-/// Define Complex command line interfaces.
-pub enum CommandLineInterface {
- Simple(CliCommand),
- Nested(CliCommandMap),
-}
-
-impl From<CliCommand> for CommandLineInterface {
- fn from(cli_cmd: CliCommand) -> Self {
- CommandLineInterface::Simple(cli_cmd)
- }
-}
-
-impl From<CliCommandMap> for CommandLineInterface {
- fn from(list: CliCommandMap) -> Self {
- CommandLineInterface::Nested(list)
- }
-}
+++ /dev/null
-use std::sync::Arc;
-
-use super::*;
-
-/// Helper trait implementation for ``rustyline``.
-///
-/// This can be used to generate interactive commands using
-/// ``rustyline`` (readline implementation).
-///
-pub struct CliHelper {
- cmd_def: Arc<CommandLineInterface>,
-}
-
-impl CliHelper {
- pub fn new(cmd_def: CommandLineInterface) -> Self {
- Self {
- cmd_def: Arc::new(cmd_def),
- }
- }
-
- pub fn cmd_def(&self) -> Arc<CommandLineInterface> {
- self.cmd_def.clone()
- }
-}
-
-impl rustyline::completion::Completer for CliHelper {
- type Candidate = String;
-
- fn complete(
- &self,
- line: &str,
- pos: usize,
- _ctx: &rustyline::Context<'_>,
- ) -> rustyline::Result<(usize, Vec<Self::Candidate>)> {
- let line = &line[..pos];
-
- let (start, completions) = super::get_completions(&*self.cmd_def, line, false);
-
- Ok((start, completions))
- }
-}
-
-impl rustyline::hint::Hinter for CliHelper {
- type Hint = String;
-}
-impl rustyline::validate::Validator for CliHelper {}
-impl rustyline::highlight::Highlighter for CliHelper {}
-impl rustyline::Helper for CliHelper {}
+++ /dev/null
-use anyhow::*;
-
-/// Shell quote type
-pub use rustyline::completion::Quote;
-
-#[derive(PartialEq)]
-enum ParseMode {
- Space,
- DoubleQuote,
- EscapeNormal,
- EscapeInDoubleQuote,
- Normal,
- SingleQuote,
-}
-
-/// Parsing strings as they would be interpreted by the UNIX Bourne shell.
-///
-/// - ``finalize``: assume this is a complete command line. Set this
-/// to false for the 'completion' helper, which needs to get
-/// information about the last unfinished parameter.
-///
-/// Returns the list of fully parsed words (unescaped and quotes
-/// removed). If there are unclosed quotes, the start of that
-/// parameter, the parameter value (unescaped and quotes removed), and
-/// the quote type are returned.
-pub fn shellword_split_unclosed(
- s: &str,
- finalize: bool,
-) -> (Vec<String>, Option<(usize, String, Quote)>) {
- let char_indices = s.char_indices();
- let mut args: Vec<String> = Vec::new();
- let mut field_start = None;
- let mut field = String::new();
- let mut mode = ParseMode::Space;
-
- let space_chars = [' ', '\t', '\n'];
-
- for (index, c) in char_indices {
- match mode {
- ParseMode::Space => match c {
- '"' => {
- mode = ParseMode::DoubleQuote;
- field_start = Some((index, Quote::Double));
- }
- '\\' => {
- mode = ParseMode::EscapeNormal;
- field_start = Some((index, Quote::None));
- }
- '\'' => {
- mode = ParseMode::SingleQuote;
- field_start = Some((index, Quote::Single));
- }
- c if space_chars.contains(&c) => (), // skip space
- c => {
- mode = ParseMode::Normal;
- field_start = Some((index, Quote::None));
- field.push(c);
- }
- },
- ParseMode::EscapeNormal => {
- mode = ParseMode::Normal;
- field.push(c);
- }
- ParseMode::EscapeInDoubleQuote => {
- // Within double quoted strings, backslashes are only
- // treated as metacharacters when followed by one of
- // the following characters: $ ' " \ newline
- match c {
- '$' | '\'' | '"' | '\\' | '\n' => (),
- _ => field.push('\\'),
- }
- field.push(c);
- mode = ParseMode::DoubleQuote;
- }
- ParseMode::Normal => match c {
- '"' => mode = ParseMode::DoubleQuote,
- '\'' => mode = ParseMode::SingleQuote,
- '\\' => mode = ParseMode::EscapeNormal,
- c if space_chars.contains(&c) => {
- mode = ParseMode::Space;
- let (_start, _quote) = field_start.take().unwrap();
- args.push(field.split_off(0));
- }
- c => field.push(c), // continue
- },
- ParseMode::DoubleQuote => match c {
- '"' => mode = ParseMode::Normal,
- '\\' => mode = ParseMode::EscapeInDoubleQuote,
- c => field.push(c), // continue
- },
- ParseMode::SingleQuote => match c {
- // Note: no escape in single quotes
- '\'' => mode = ParseMode::Normal,
- c => field.push(c), // continue
- },
- }
- }
-
- if finalize && mode == ParseMode::Normal {
- let (_start, _quote) = field_start.take().unwrap();
- args.push(field.split_off(0));
- }
-
- match field_start {
- Some((start, quote)) => (args, Some((start, field, quote))),
- None => (args, None),
- }
-}
-
-/// Splits a string into a vector of words in the same way the UNIX Bourne shell does.
-///
-/// Return words unescaped and without quotes.
-pub fn shellword_split(s: &str) -> Result<Vec<String>, Error> {
- let (args, unclosed_field) = shellword_split_unclosed(s, true);
- if unclosed_field.is_some() {
- bail!("shellword split failed - found unclosed quote.");
- }
- Ok(args)
-}
-
-#[test]
-fn test_shellword_split() {
- let expect = ["ls", "/etc"];
- let expect: Vec<String> = expect.iter().map(|v| v.to_string()).collect();
-
- assert_eq!(expect, shellword_split("ls /etc").unwrap());
- assert_eq!(expect, shellword_split("ls \"/etc\"").unwrap());
- assert_eq!(expect, shellword_split("ls '/etc'").unwrap());
- assert_eq!(expect, shellword_split("ls '/etc'").unwrap());
-
- assert_eq!(expect, shellword_split("ls /e\"t\"c").unwrap());
- assert_eq!(expect, shellword_split("ls /e'tc'").unwrap());
- assert_eq!(expect, shellword_split("ls /e't''c'").unwrap());
-
- let expect = ["ls", "/etc 08x"];
- let expect: Vec<String> = expect.iter().map(|v| v.to_string()).collect();
- assert_eq!(expect, shellword_split("ls /etc\\ \\08x").unwrap());
-
- let expect = ["ls", "/etc \\08x"];
- let expect: Vec<String> = expect.iter().map(|v| v.to_string()).collect();
- assert_eq!(expect, shellword_split("ls \"/etc \\08x\"").unwrap());
-}
-
-#[test]
-fn test_shellword_split_unclosed() {
- let expect = ["ls".to_string()].to_vec();
- assert_eq!(
- (
- expect,
- Some((3, "./File1 name with spaces".to_string(), Quote::Single))
- ),
- shellword_split_unclosed("ls './File1 name with spaces", false)
- );
-
- let expect = ["ls".to_string()].to_vec();
- assert_eq!(
- (
- expect,
- Some((3, "./File2 name with spaces".to_string(), Quote::Double))
- ),
- shellword_split_unclosed("ls \"./File2 \"name\" with spaces", false)
- );
-}
+++ /dev/null
-use std::io::Write;
-
-use anyhow::*;
-use serde_json::Value;
-use unicode_width::UnicodeWidthStr;
-
-use crate::api::schema::*;
-
-/// allows to configure the default output fromat using environment vars
-pub const ENV_VAR_PROXMOX_OUTPUT_FORMAT: &str = "PROXMOX_OUTPUT_FORMAT";
-/// if set, supress borders (and headers) when printing tables
-pub const ENV_VAR_PROXMOX_OUTPUT_NO_BORDER: &str = "PROXMOX_OUTPUT_NO_BORDER";
-/// if set, supress headers when printing tables
-pub const ENV_VAR_PROXMOX_OUTPUT_NO_HEADER: &str = "PROXMOX_OUTPUT_NO_HEADER";
-
-/// Helper to get output format from parameters or environment
-pub fn get_output_format(param: &Value) -> String {
- let mut output_format = None;
-
- if let Some(format) = param["output-format"].as_str() {
- output_format = Some(format.to_owned());
- } else if let Ok(format) = std::env::var(ENV_VAR_PROXMOX_OUTPUT_FORMAT) {
- output_format = Some(format);
- }
-
- output_format.unwrap_or_else(|| String::from("text"))
-}
-
-/// Helper to get output format from parameters or environment
-/// and removing from parameters
-pub fn extract_output_format(param: &mut Value) -> String {
- let output_format = get_output_format(param);
-
- if let Some(param) = param.as_object_mut() {
- param.remove("output-format");
- }
-
- output_format
-}
-
-/// Helper to get TableFormatOptions with default from environment
-pub fn default_table_format_options() -> TableFormatOptions {
- let no_border = std::env::var(ENV_VAR_PROXMOX_OUTPUT_NO_BORDER)
- .ok()
- .is_some();
- let no_header = std::env::var(ENV_VAR_PROXMOX_OUTPUT_NO_HEADER)
- .ok()
- .is_some();
-
- TableFormatOptions::new()
- .noborder(no_border)
- .noheader(no_header)
-}
-
-/// Render function
-///
-/// Should convert the json `value` into a text string. `record` points to
-/// the surrounding data object.
-pub type RenderFunction =
- fn(/* value: */ &Value, /* record: */ &Value) -> Result<String, Error>;
-
-fn data_to_text(data: &Value, schema: &Schema) -> Result<String, Error> {
- if data.is_null() {
- return Ok(String::new());
- }
-
- match schema {
- Schema::Null => {
- // makes no sense to display Null columns
- bail!("internal error");
- }
- Schema::Boolean(_) => match data.as_bool() {
- Some(value) => Ok(String::from(if value { "1" } else { "0" })),
- None => bail!("got unexpected data (expected bool)."),
- },
- Schema::Integer(_) => match data.as_i64() {
- Some(value) => Ok(format!("{}", value)),
- None => bail!("got unexpected data (expected integer)."),
- },
- Schema::Number(_) => match data.as_f64() {
- Some(value) => Ok(format!("{}", value)),
- None => bail!("got unexpected data (expected number)."),
- },
- Schema::String(_) => match data.as_str() {
- Some(value) => Ok(value.to_string()),
- None => bail!("got unexpected data (expected string)."),
- },
- Schema::Object(_) => Ok(data.to_string()),
- Schema::Array(_) => Ok(data.to_string()),
- Schema::AllOf(_) => Ok(data.to_string()),
- }
-}
-
-struct TableBorders {
- column_separator: char,
- top: String,
- head: String,
- middle: String,
- bottom: String,
-}
-
-impl TableBorders {
- fn new<I>(column_widths: I, ascii_delimiters: bool) -> Self
- where
- I: Iterator<Item = usize>,
- {
- let mut top = String::new();
- let mut head = String::new();
- let mut middle = String::new();
- let mut bottom = String::new();
-
- let column_separator = if ascii_delimiters { '|' } else { '│' };
-
- for (i, column_width) in column_widths.enumerate() {
- if ascii_delimiters {
- top.push('+');
- head.push('+');
- middle.push('+');
- bottom.push('+');
- } else if i == 0 {
- top.push('┌');
- head.push('╞');
- middle.push('├');
- bottom.push('└');
- } else {
- top.push('┬');
- head.push('╪');
- middle.push('┼');
- bottom.push('┴');
- }
-
- for _j in 0..(column_width + 2) {
- if ascii_delimiters {
- top.push('=');
- head.push('=');
- middle.push('-');
- bottom.push('=');
- } else {
- top.push('─');
- head.push('═');
- middle.push('─');
- bottom.push('─');
- }
- }
- }
- if ascii_delimiters {
- top.push('+');
- head.push('+');
- middle.push('+');
- bottom.push('+');
- } else {
- top.push('┐');
- head.push('╡');
- middle.push('┤');
- bottom.push('┘');
- }
-
- Self {
- column_separator,
- top,
- head,
- middle,
- bottom,
- }
- }
-}
-
-/// Table Column configuration
-///
-/// This structure can be used to set additional rendering information for a table column.
-pub struct ColumnConfig {
- pub name: String,
- pub header: Option<String>,
- pub right_align: Option<bool>,
- pub renderer: Option<RenderFunction>,
-}
-
-impl ColumnConfig {
- pub fn new(name: &str) -> Self {
- Self {
- name: name.to_string(),
- header: None,
- right_align: None,
- renderer: None,
- }
- }
-
- pub fn right_align(mut self, right_align: bool) -> Self {
- self.right_align = Some(right_align);
- self
- }
-
- pub fn renderer(mut self, renderer: RenderFunction) -> Self {
- self.renderer = Some(renderer);
- self
- }
-
- pub fn header<S: Into<String>>(mut self, header: S) -> Self {
- self.header = Some(header.into());
- self
- }
-}
-
-/// Table formatter configuration
-#[derive(Default)]
-pub struct TableFormatOptions {
- /// Can be used to sort after a specific columns, if it isn't set
- /// we sort after the leftmost column (with no undef value in
- /// $data) this can be turned off by passing and empty array. The
- /// boolean argument specifies the sort order (false => ASC, true => DESC)
- pub sortkeys: Option<Vec<(String, bool)>>,
- /// Print without asciiart border.
- pub noborder: bool,
- /// Print without table header.
- pub noheader: bool,
- /// Limit output width.
- pub columns: Option<usize>,
- /// Use ascii characters for table delimiters (instead of utf8).
- pub ascii_delimiters: bool,
- /// Comumn configurations
- pub column_config: Vec<ColumnConfig>,
-}
-
-impl TableFormatOptions {
- /// Create a new Instance with reasonable defaults for terminal output
- ///
- /// This tests if stdout is a TTY and sets the columns to the terminal width,
- /// and sets ascii_delimiters to true If the locale CODESET is not UTF-8.
- pub fn new() -> Self {
- let mut me = Self::default();
-
- let is_tty = unsafe { libc::isatty(libc::STDOUT_FILENO) == 1 };
-
- if is_tty {
- let (_rows, columns) = crate::sys::linux::tty::stdout_terminal_size();
- if columns > 0 {
- me.columns = Some(columns);
- }
- }
-
- let empty_cstr = crate::c_str!("");
-
- use std::ffi::CStr;
- let encoding = unsafe {
- libc::setlocale(libc::LC_CTYPE, empty_cstr.as_ptr());
- CStr::from_ptr(libc::nl_langinfo(libc::CODESET))
- };
-
- if encoding != crate::c_str!("UTF-8") {
- me.ascii_delimiters = true;
- }
-
- me
- }
-
- pub fn disable_sort(mut self) -> Self {
- self.sortkeys = Some(Vec::new());
- self
- }
-
- pub fn sortby<S: Into<String>>(mut self, key: S, sort_desc: bool) -> Self {
- let key = key.into();
- match self.sortkeys {
- None => {
- let mut list = Vec::new();
- list.push((key, sort_desc));
- self.sortkeys = Some(list);
- }
- Some(ref mut list) => {
- list.push((key, sort_desc));
- }
- }
- self
- }
-
- pub fn noborder(mut self, noborder: bool) -> Self {
- self.noborder = noborder;
- self
- }
-
- pub fn noheader(mut self, noheader: bool) -> Self {
- self.noheader = noheader;
- self
- }
-
- pub fn ascii_delimiters(mut self, ascii_delimiters: bool) -> Self {
- self.ascii_delimiters = ascii_delimiters;
- self
- }
-
- pub fn columns(mut self, columns: Option<usize>) -> Self {
- self.columns = columns;
- self
- }
-
- pub fn column_config(mut self, column_config: Vec<ColumnConfig>) -> Self {
- self.column_config = column_config;
- self
- }
-
- /// Add a single column configuration
- pub fn column(mut self, column_config: ColumnConfig) -> Self {
- self.column_config.push(column_config);
- self
- }
-
- fn lookup_column_info(
- &self,
- column_name: &str,
- ) -> (String, Option<bool>, Option<RenderFunction>) {
- let mut renderer = None;
-
- let header;
- let mut right_align = None;
-
- if let Some(column_config) = self.column_config.iter().find(|v| v.name == *column_name) {
- renderer = column_config.renderer;
- right_align = column_config.right_align;
- if let Some(ref h) = column_config.header {
- header = h.to_owned();
- } else {
- header = column_name.to_string();
- }
- } else {
- header = column_name.to_string();
- }
-
- (header, right_align, renderer)
- }
-}
-
-struct TableCell {
- lines: Vec<String>,
-}
-
-struct TableColumn {
- cells: Vec<TableCell>,
- width: usize,
- right_align: bool,
-}
-
-fn format_table<W: Write>(
- output: W,
- list: &mut Vec<Value>,
- schema: &dyn ObjectSchemaType,
- options: &TableFormatOptions,
-) -> Result<(), Error> {
- let properties_to_print = if options.column_config.is_empty() {
- extract_properties_to_print(schema.properties())
- } else {
- options
- .column_config
- .iter()
- .map(|v| v.name.clone())
- .collect()
- };
-
- let column_count = properties_to_print.len();
- if column_count == 0 {
- return Ok(());
- };
-
- let sortkeys = if let Some(ref sortkeys) = options.sortkeys {
- sortkeys.clone()
- } else {
- let mut keys = Vec::new();
- keys.push((properties_to_print[0].clone(), false)); // leftmost, ASC
- keys
- };
-
- let mut sortinfo = Vec::new();
-
- for (sortkey, sort_order) in sortkeys {
- let (_optional, sort_prop_schema) = match schema.lookup(&sortkey) {
- Some(tup) => tup,
- None => bail!("property {} does not exist in schema.", sortkey),
- };
- let numeric_sort = match sort_prop_schema {
- Schema::Integer(_) => true,
- Schema::Number(_) => true,
- _ => false,
- };
- sortinfo.push((sortkey, sort_order, numeric_sort));
- }
-
- use std::cmp::Ordering;
- list.sort_unstable_by(move |a, b| {
- for &(ref sortkey, sort_desc, numeric) in &sortinfo {
- let res = if numeric {
- let (v1, v2) = if sort_desc {
- (b[&sortkey].as_f64(), a[&sortkey].as_f64())
- } else {
- (a[&sortkey].as_f64(), b[&sortkey].as_f64())
- };
- match (v1, v2) {
- (None, None) => Ordering::Greater,
- (Some(_), None) => Ordering::Greater,
- (None, Some(_)) => Ordering::Less,
- (Some(a), Some(b)) =>
- {
- #[allow(clippy::if_same_then_else)]
- if a.is_nan() {
- Ordering::Greater
- } else if b.is_nan() {
- Ordering::Less
- } else if a < b {
- Ordering::Less
- } else if a > b {
- Ordering::Greater
- } else {
- Ordering::Equal
- }
- }
- }
- } else {
- let (v1, v2) = if sort_desc {
- (b[sortkey].as_str(), a[sortkey].as_str())
- } else {
- (a[sortkey].as_str(), b[sortkey].as_str())
- };
- v1.cmp(&v2)
- };
-
- if res != Ordering::Equal {
- return res;
- }
- }
- Ordering::Equal
- });
-
- let mut tabledata: Vec<TableColumn> = Vec::new();
-
- let mut column_names = Vec::new();
-
- for name in properties_to_print.iter() {
- let (_optional, prop_schema) = match schema.lookup(name) {
- Some(tup) => tup,
- None => bail!("property {} does not exist in schema.", name),
- };
-
- let is_numeric = match prop_schema {
- Schema::Integer(_) => true,
- Schema::Number(_) => true,
- Schema::Boolean(_) => true,
- _ => false,
- };
-
- let (header, right_align, renderer) = options.lookup_column_info(name);
-
- let right_align = right_align.unwrap_or(is_numeric);
-
- let mut max_width = if options.noheader || options.noborder {
- 0
- } else {
- header.chars().count()
- };
-
- column_names.push(header);
-
- let mut cells = Vec::new();
- for entry in list.iter() {
- let result = if let Some(renderer) = renderer {
- (renderer)(&entry[name], &entry)
- } else {
- data_to_text(&entry[name], prop_schema)
- };
-
- let text = match result {
- Ok(text) => text,
- Err(err) => bail!("unable to format property {} - {}", name, err),
- };
-
- let lines: Vec<String> = text
- .lines()
- .map(|line| {
- let width = UnicodeWidthStr::width(line);
- if width > max_width {
- max_width = width;
- }
- line.to_string()
- })
- .collect();
-
- cells.push(TableCell { lines });
- }
-
- tabledata.push(TableColumn {
- cells,
- width: max_width,
- right_align,
- });
- }
-
- render_table(output, &tabledata, &column_names, options)
-}
-
-fn render_table<W: Write>(
- mut output: W,
- tabledata: &[TableColumn],
- column_names: &[String],
- options: &TableFormatOptions,
-) -> Result<(), Error> {
- let mut write_line = |line: &str| -> Result<(), Error> {
- if let Some(columns) = options.columns {
- let line: String = line.chars().take(columns).collect();
- output.write_all(line.as_bytes())?;
- } else {
- output.write_all(line.as_bytes())?;
- }
- output.write_all(b"\n")?;
- Ok(())
- };
-
- let column_widths = tabledata.iter().map(|d| d.width);
- let borders = TableBorders::new(column_widths, options.ascii_delimiters);
-
- if !options.noborder {
- write_line(&borders.top)?;
- }
-
- let mut header = String::new();
- for (i, name) in column_names.iter().enumerate() {
- let column = &tabledata[i];
- header.push(borders.column_separator);
- header.push(' ');
- if column.right_align {
- header.push_str(&format!("{:>width$}", name, width = column.width));
- } else {
- header.push_str(&format!("{:<width$}", name, width = column.width));
- }
- header.push(' ');
- }
-
- if !(options.noheader || options.noborder) {
- header.push(borders.column_separator);
-
- write_line(&header)?;
- write_line(&borders.head)?;
- }
-
- let rows = tabledata[0].cells.len();
- for pos in 0..rows {
- let mut max_lines = 0;
- for (i, _name) in column_names.iter().enumerate() {
- let cells = &tabledata[i].cells;
- let lines = &cells[pos].lines;
- if lines.len() > max_lines {
- max_lines = lines.len();
- }
- }
- for line_nr in 0..max_lines {
- let mut text = String::new();
- let empty_string = String::new();
- for (i, _name) in column_names.iter().enumerate() {
- let column = &tabledata[i];
- let lines = &column.cells[pos].lines;
- let line = lines.get(line_nr).unwrap_or(&empty_string);
-
- if options.noborder {
- if i > 0 {
- text.push(' ');
- }
- } else {
- text.push(borders.column_separator);
- text.push(' ');
- }
-
- let padding = column.width - UnicodeWidthStr::width(line.as_str());
- if column.right_align {
- text.push_str(&format!("{:>width$}{}", "", line, width = padding));
- } else {
- text.push_str(&format!("{}{:<width$}", line, "", width = padding));
- }
-
- if !options.noborder {
- text.push(' ');
- }
- }
- if !options.noborder {
- text.push(borders.column_separator);
- }
- write_line(&text)?;
- }
-
- if !options.noborder {
- if (pos + 1) == rows {
- write_line(&borders.bottom)?;
- } else {
- write_line(&borders.middle)?;
- }
- }
- }
-
- Ok(())
-}
-
-fn format_object<W: Write>(
- output: W,
- data: &Value,
- schema: &dyn ObjectSchemaType,
- options: &TableFormatOptions,
-) -> Result<(), Error> {
- let properties_to_print = if options.column_config.is_empty() {
- extract_properties_to_print(schema.properties())
- } else {
- options
- .column_config
- .iter()
- .map(|v| v.name.clone())
- .collect()
- };
-
- let row_count = properties_to_print.len();
- if row_count == 0 {
- return Ok(());
- };
-
- const NAME_TITLE: &str = "Name";
- const VALUE_TITLE: &str = "Value";
-
- let mut max_name_width = if options.noheader || options.noborder {
- 0
- } else {
- NAME_TITLE.len()
- };
- let mut max_value_width = if options.noheader || options.noborder {
- 0
- } else {
- VALUE_TITLE.len()
- };
-
- let column_names = vec![NAME_TITLE.to_string(), VALUE_TITLE.to_string()];
-
- let mut name_cells = Vec::new();
- let mut value_cells = Vec::new();
-
- let mut all_right_aligned = true;
-
- for name in properties_to_print.iter() {
- let (optional, prop_schema) = match schema.lookup(name) {
- Some(tup) => tup,
- None => bail!("property {} does not exist in schema.", name),
- };
-
- let is_numeric = match prop_schema {
- Schema::Integer(_) => true,
- Schema::Number(_) => true,
- Schema::Boolean(_) => true,
- _ => false,
- };
-
- let (header, right_align, renderer) = options.lookup_column_info(name);
-
- let right_align = right_align.unwrap_or(is_numeric);
-
- if !right_align {
- all_right_aligned = false;
- }
-
- if optional {
- if let Some(object) = data.as_object() {
- if object.get(name).is_none() {
- continue;
- }
- }
- }
-
- let header_width = header.chars().count();
- if header_width > max_name_width {
- max_name_width = header_width;
- }
-
- name_cells.push(TableCell {
- lines: vec![header],
- });
-
- let result = if let Some(renderer) = renderer {
- (renderer)(&data[name], &data)
- } else {
- data_to_text(&data[name], prop_schema)
- };
-
- let text = match result {
- Ok(text) => text,
- Err(err) => bail!("unable to format property {} - {}", name, err),
- };
-
- let lines: Vec<String> = text
- .lines()
- .map(|line| {
- let width = line.chars().count();
- if width > max_value_width {
- max_value_width = width;
- }
- line.to_string()
- })
- .collect();
-
- value_cells.push(TableCell { lines });
- }
-
- let name_column = TableColumn {
- cells: name_cells,
- width: max_name_width,
- right_align: false,
- };
- let value_column = TableColumn {
- cells: value_cells,
- width: max_value_width,
- right_align: all_right_aligned,
- };
-
- let mut tabledata: Vec<TableColumn> = Vec::new();
- tabledata.push(name_column);
- tabledata.push(value_column);
-
- render_table(output, &tabledata, &column_names, options)
-}
-
-fn extract_properties_to_print<I>(properties: I) -> Vec<String>
-where
- I: Iterator<Item = &'static SchemaPropertyEntry>,
-{
- let mut result = Vec::new();
- let mut opt_properties = Vec::new();
-
- for (name, optional, _prop_schema) in properties {
- if *optional {
- opt_properties.push(name.to_string());
- } else {
- result.push(name.to_string());
- }
- }
-
- result.extend(opt_properties);
-
- result
-}
-
-/// Format data using TableFormatOptions
-pub fn value_to_text<W: Write>(
- output: W,
- data: &mut Value,
- schema: &Schema,
- options: &TableFormatOptions,
-) -> Result<(), Error> {
- match schema {
- Schema::Null => {
- if *data != Value::Null {
- bail!("got unexpected data (expected null).");
- }
- }
- Schema::Boolean(_boolean_schema) => {
- unimplemented!();
- }
- Schema::Integer(_integer_schema) => {
- unimplemented!();
- }
- Schema::Number(_number_schema) => {
- unimplemented!();
- }
- Schema::String(_string_schema) => {
- unimplemented!();
- }
- Schema::Object(object_schema) => {
- format_object(output, data, object_schema, options)?;
- }
- Schema::Array(array_schema) => {
- let list = match data.as_array_mut() {
- Some(list) => list,
- None => bail!("got unexpected data (expected array)."),
- };
- if list.is_empty() {
- return Ok(());
- }
-
- match array_schema.items {
- Schema::Object(object_schema) => {
- format_table(output, list, object_schema, options)?;
- }
- Schema::AllOf(all_of_schema) => {
- format_table(output, list, all_of_schema, options)?;
- }
- _ => {
- unimplemented!();
- }
- }
- }
- Schema::AllOf(all_of_schema) => {
- format_object(output, data, all_of_schema, options)?;
- }
- }
- Ok(())
-}
+++ /dev/null
-use std::fmt;
-
-/// Helper to represent const regular expressions
-///
-/// The current Regex::new() function is not `const_fn`. Unless that
-/// works, we use `ConstRegexPattern` to represent static regular
-/// expressions. Please use the `const_regex` macro to generate
-/// instances of this type (uses lazy_static).
-pub struct ConstRegexPattern {
- /// This is only used for documentation and debugging
- pub regex_string: &'static str,
- /// This function return the the actual Regex
- pub regex_obj: fn() -> &'static regex::Regex,
-}
-
-impl fmt::Debug for ConstRegexPattern {
- fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
- write!(f, "{:?}", self.regex_string)
- }
-}
-
-impl std::ops::Deref for ConstRegexPattern {
- type Target = regex::Regex;
-
- fn deref(&self) -> &Self::Target {
- (self.regex_obj)()
- }
-}
-
-/// Macro to generate a ConstRegexPattern
-///
-/// ```
-/// # use proxmox::const_regex;
-/// #
-/// const_regex!{
-/// FILE_EXTENSION_REGEX = r".*\.([a-zA-Z]+)$";
-/// pub SHA256_HEX_REGEX = r"^[a-f0-9]{64}$";
-/// }
-/// ```
-#[macro_export]
-macro_rules! const_regex {
- ($(
- $(#[$attr:meta])*
- $vis:vis $name:ident = $regex:expr;
- )+) => { $(
- $(#[$attr])* $vis const $name: $crate::api::const_regex::ConstRegexPattern =
- $crate::api::const_regex::ConstRegexPattern {
- regex_string: $regex,
- regex_obj: (|| -> &'static ::regex::Regex {
- ::lazy_static::lazy_static! {
- static ref SCHEMA: ::regex::Regex = ::regex::Regex::new($regex).unwrap();
- }
- &SCHEMA
- })
- };
- )+ };
-}
-
-#[cfg(feature = "test-harness")]
-impl Eq for ConstRegexPattern {}
-
-#[cfg(feature = "test-harness")]
-impl PartialEq for ConstRegexPattern {
- fn eq(&self, rhs: &Self) -> bool {
- self.regex_string == rhs.regex_string
- }
-}
+++ /dev/null
-//! Partial object deserialization by extracting object portions from a Value using an api schema.
-
-use std::fmt;
-
-use serde::de::{self, IntoDeserializer, Visitor};
-use serde_json::Value;
-
-use crate::api::schema::{ObjectSchemaType, Schema};
-
-pub struct Error {
- inner: anyhow::Error,
-}
-
-impl fmt::Debug for Error {
- fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
- fmt::Debug::fmt(&self.inner, f)
- }
-}
-
-impl fmt::Display for Error {
- fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
- fmt::Display::fmt(&self.inner, f)
- }
-}
-
-impl std::error::Error for Error {}
-
-impl serde::de::Error for Error {
- fn custom<T: fmt::Display>(msg: T) -> Self {
- Self {
- inner: anyhow::format_err!("{}", msg),
- }
- }
-}
-
-impl From<serde_json::Error> for Error {
- fn from(inner: serde_json::Error) -> Self {
- Error {
- inner: inner.into(),
- }
- }
-}
-
-pub struct ExtractValueDeserializer<'o> {
- object: &'o mut serde_json::Map<String, Value>,
- schema: &'static Schema,
-}
-
-impl<'o> ExtractValueDeserializer<'o> {
- pub fn try_new(
- object: &'o mut serde_json::Map<String, Value>,
- schema: &'static Schema,
- ) -> Option<Self> {
- match schema {
- Schema::Object(_) | Schema::AllOf(_) => Some(Self { object, schema }),
- _ => None,
- }
- }
-}
-
-macro_rules! deserialize_non_object {
- ($name:ident) => {
- fn $name<V>(self, _visitor: V) -> Result<V::Value, Self::Error>
- where
- V: Visitor<'de>,
- {
- Err(de::Error::custom(
- "deserializing partial object into type which is not an object",
- ))
- }
- };
- ($name:ident ( $($args:tt)* )) => {
- fn $name<V>(self, $($args)*, _visitor: V) -> Result<V::Value, Self::Error>
- where
- V: Visitor<'de>,
- {
- Err(de::Error::custom(
- "deserializing partial object into type which is not an object",
- ))
- }
- };
-}
-
-impl<'de> de::Deserializer<'de> for ExtractValueDeserializer<'de> {
- type Error = Error;
-
- fn deserialize_any<V>(self, visitor: V) -> Result<V::Value, Error>
- where
- V: Visitor<'de>,
- {
- self.deserialize_map(visitor)
- }
-
- fn deserialize_map<V>(self, visitor: V) -> Result<V::Value, Error>
- where
- V: Visitor<'de>,
- {
- use serde::de::Error;
-
- match self.schema {
- Schema::Object(schema) => visitor.visit_map(MapAccess::<'de>::new(
- self.object,
- schema.properties().map(|(name, _, _)| *name),
- )),
- Schema::AllOf(schema) => visitor.visit_map(MapAccess::<'de>::new(
- self.object,
- schema.properties().map(|(name, _, _)| *name),
- )),
-
- // The following should be caught by ExtractValueDeserializer::new()!
- _ => Err(Error::custom(
- "ExtractValueDeserializer used with invalid schema",
- )),
- }
- }
-
- fn deserialize_struct<V>(
- self,
- _name: &'static str,
- _fields: &'static [&'static str],
- visitor: V,
- ) -> Result<V::Value, Error>
- where
- V: Visitor<'de>,
- {
- use serde::de::Error;
-
- match self.schema {
- Schema::Object(schema) => visitor.visit_map(MapAccess::<'de>::new(
- self.object,
- schema.properties().map(|(name, _, _)| *name),
- )),
- Schema::AllOf(schema) => visitor.visit_map(MapAccess::<'de>::new(
- self.object,
- schema.properties().map(|(name, _, _)| *name),
- )),
-
- // The following should be caught by ExtractValueDeserializer::new()!
- _ => Err(Error::custom(
- "ExtractValueDeserializer used with invalid schema",
- )),
- }
- }
-
- deserialize_non_object!(deserialize_i8);
- deserialize_non_object!(deserialize_i16);
- deserialize_non_object!(deserialize_i32);
- deserialize_non_object!(deserialize_i64);
- deserialize_non_object!(deserialize_u8);
- deserialize_non_object!(deserialize_u16);
- deserialize_non_object!(deserialize_u32);
- deserialize_non_object!(deserialize_u64);
- deserialize_non_object!(deserialize_f32);
- deserialize_non_object!(deserialize_f64);
- deserialize_non_object!(deserialize_char);
- deserialize_non_object!(deserialize_bool);
- deserialize_non_object!(deserialize_str);
- deserialize_non_object!(deserialize_string);
- deserialize_non_object!(deserialize_bytes);
- deserialize_non_object!(deserialize_byte_buf);
- deserialize_non_object!(deserialize_option);
- deserialize_non_object!(deserialize_seq);
- deserialize_non_object!(deserialize_unit);
- deserialize_non_object!(deserialize_identifier);
- deserialize_non_object!(deserialize_unit_struct(_: &'static str));
- deserialize_non_object!(deserialize_newtype_struct(_: &'static str));
- deserialize_non_object!(deserialize_tuple(_: usize));
- deserialize_non_object!(deserialize_tuple_struct(_: &'static str, _: usize));
- deserialize_non_object!(deserialize_enum(
- _: &'static str,
- _: &'static [&'static str]
- ));
-
- fn deserialize_ignored_any<V>(self, visitor: V) -> Result<V::Value, Self::Error>
- where
- V: Visitor<'de>,
- {
- visitor.visit_unit()
- }
-}
-
-struct MapAccess<'o, I> {
- object: &'o mut serde_json::Map<String, Value>,
- iter: I,
- value: Option<Value>,
-}
-
-impl<'o, I> MapAccess<'o, I>
-where
- I: Iterator<Item = &'static str>,
-{
- fn new(object: &'o mut serde_json::Map<String, Value>, iter: I) -> Self {
- Self {
- object,
- iter,
- value: None,
- }
- }
-}
-
-impl<'de, I> de::MapAccess<'de> for MapAccess<'de, I>
-where
- I: Iterator<Item = &'static str>,
-{
- type Error = Error;
-
- fn next_key_seed<K>(&mut self, seed: K) -> Result<Option<K::Value>, Error>
- where
- K: de::DeserializeSeed<'de>,
- {
- loop {
- return match self.iter.next() {
- Some(key) => match self.object.remove(key) {
- Some(value) => {
- self.value = Some(value);
- seed.deserialize(key.into_deserializer()).map(Some)
- }
- None => continue,
- },
- None => Ok(None),
- };
- }
- }
-
- fn next_value_seed<V>(&mut self, seed: V) -> Result<V::Value, Error>
- where
- V: de::DeserializeSeed<'de>,
- {
- match self.value.take() {
- Some(value) => seed.deserialize(value).map_err(Error::from),
- None => Err(de::Error::custom("value is missing")),
- }
- }
-}
-
-#[test]
-fn test_extraction() {
- use serde::Deserialize;
-
- use crate::api::schema::{ObjectSchema, StringSchema};
-
- #[derive(Deserialize)]
- struct Foo {
- foo1: String,
- foo2: String,
- }
-
- const SIMPLE_STRING: Schema = StringSchema::new("simple").schema();
- const FOO_SCHEMA: Schema = ObjectSchema::new(
- "A Foo",
- &[
- ("foo1", false, &SIMPLE_STRING),
- ("foo2", false, &SIMPLE_STRING),
- ],
- )
- .schema();
-
- #[derive(Deserialize)]
- struct Bar {
- bar1: String,
- bar2: String,
- }
-
- const BAR_SCHEMA: Schema = ObjectSchema::new(
- "A Bar",
- &[
- ("bar1", false, &SIMPLE_STRING),
- ("bar2", false, &SIMPLE_STRING),
- ],
- )
- .schema();
-
- let mut data = serde_json::json!({
- "foo1": "hey1",
- "foo2": "hey2",
- "bar1": "there1",
- "bar2": "there2",
- });
-
- let data = data.as_object_mut().unwrap();
-
- let foo: Foo =
- Foo::deserialize(ExtractValueDeserializer::try_new(data, &FOO_SCHEMA).unwrap()).unwrap();
-
- assert!(data.remove("foo1").is_none());
- assert!(data.remove("foo2").is_none());
- assert_eq!(foo.foo1, "hey1");
- assert_eq!(foo.foo2, "hey2");
-
- let bar =
- Bar::deserialize(ExtractValueDeserializer::try_new(data, &BAR_SCHEMA).unwrap()).unwrap();
-
- assert!(data.is_empty());
- assert_eq!(bar.bar1, "there1");
- assert_eq!(bar.bar2, "there2");
-}
+++ /dev/null
-use std::fmt;
-
-#[doc(hidden)]
-pub use http::StatusCode;
-
-/// HTTP error including `StatusCode` and message.
-#[derive(Debug)]
-pub struct HttpError {
- pub code: StatusCode,
- pub message: String,
-}
-
-impl std::error::Error for HttpError {}
-
-impl HttpError {
- pub fn new(code: StatusCode, message: String) -> Self {
- HttpError { code, message }
- }
-}
-
-impl fmt::Display for HttpError {
- fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
- write!(f, "{}", self.message)
- }
-}
-
-/// Macro to create a HttpError inside a anyhow::Error
-#[macro_export]
-macro_rules! http_err {
- ($status:ident, $($fmt:tt)+) => {{
- ::anyhow::Error::from($crate::api::error::HttpError::new(
- $crate::api::error::StatusCode::$status,
- format!($($fmt)+)
- ))
- }};
-}
-
-/// Bail with an error generated with the `http_err!` macro.
-#[macro_export]
-macro_rules! http_bail {
- ($status:ident, $($fmt:tt)+) => {{
- return Err($crate::http_err!($status, $($fmt)+));
- }};
-}
+++ /dev/null
-//! Module to generate and format API Documenation
-
-use anyhow::{bail, Error};
-
-use std::io::Write;
-
-use crate::api::{
- router::ReturnType, schema::*, section_config::SectionConfig, ApiHandler, ApiMethod,
-};
-
-/// Enumerate different styles to display parameters/properties.
-#[derive(Copy, Clone, PartialEq)]
-pub enum ParameterDisplayStyle {
- /// Used for properties in configuration files: ``key:``
- Config,
- /// Used for PropertyStings properties in configuration files
- ConfigSub,
- /// Used for command line options: ``--key``
- Arg,
- /// Used for command line options passed as arguments: ``<key>``
- Fixed,
-}
-
-/// CLI usage information format.
-#[derive(Copy, Clone, PartialEq)]
-pub enum DocumentationFormat {
- /// Text, command line only (one line).
- Short,
- /// Text, list all options.
- Long,
- /// Text, include description.
- Full,
- /// Like full, but in reStructuredText format.
- ReST,
-}
-
-/// Line wrapping to form simple list of paragraphs.
-pub fn wrap_text(
- initial_indent: &str,
- subsequent_indent: &str,
- text: &str,
- columns: usize,
-) -> String {
- let wrapper1 = textwrap::Wrapper::new(columns)
- .initial_indent(initial_indent)
- .subsequent_indent(subsequent_indent);
-
- let wrapper2 = textwrap::Wrapper::new(columns)
- .initial_indent(subsequent_indent)
- .subsequent_indent(subsequent_indent);
-
- text.split("\n\n")
- .map(|p| p.trim())
- .filter(|p| !p.is_empty())
- .fold(String::new(), |mut acc, p| {
- if acc.is_empty() {
- acc.push_str(&wrapper1.wrap(p).join("\n"));
- } else {
- acc.push_str(&wrapper2.wrap(p).join("\n"));
- }
- acc.push_str("\n\n");
- acc
- })
-}
-
-#[test]
-fn test_wrap_text() {
- let text = "Command. This may be a list in order to spefify nested sub-commands.";
- let expect = " Command. This may be a list in order to spefify nested sub-\n commands.\n\n";
-
- let indent = " ";
- let wrapped = wrap_text(indent, indent, text, 80);
-
- assert_eq!(wrapped, expect);
-}
-
-/// Helper to format the type text
-///
-/// The result is a short string including important constraints, for
-/// example ``<integer> (0 - N)``.
-pub fn get_schema_type_text(schema: &Schema, _style: ParameterDisplayStyle) -> String {
- match schema {
- Schema::Null => String::from("<null>"), // should not happen
- Schema::String(string_schema) => {
- match string_schema {
- StringSchema {
- type_text: Some(type_text),
- ..
- } => String::from(*type_text),
- StringSchema {
- format: Some(ApiStringFormat::Enum(variants)),
- ..
- } => {
- let list: Vec<String> =
- variants.iter().map(|e| String::from(e.value)).collect();
- list.join("|")
- }
- // displaying regex add more confision than it helps
- //StringSchema { format: Some(ApiStringFormat::Pattern(const_regex)), .. } => {
- // format!("/{}/", const_regex.regex_string)
- //}
- StringSchema {
- format: Some(ApiStringFormat::PropertyString(sub_schema)),
- ..
- } => get_property_string_type_text(sub_schema),
- _ => String::from("<string>"),
- }
- }
- Schema::Boolean(_) => String::from("<boolean>"),
- Schema::Integer(integer_schema) => match (integer_schema.minimum, integer_schema.maximum) {
- (Some(min), Some(max)) => format!("<integer> ({} - {})", min, max),
- (Some(min), None) => format!("<integer> ({} - N)", min),
- (None, Some(max)) => format!("<integer> (-N - {})", max),
- _ => String::from("<integer>"),
- },
- Schema::Number(number_schema) => match (number_schema.minimum, number_schema.maximum) {
- (Some(min), Some(max)) => format!("<number> ({} - {})", min, max),
- (Some(min), None) => format!("<number> ({} - N)", min),
- (None, Some(max)) => format!("<number> (-N - {})", max),
- _ => String::from("<number>"),
- },
- Schema::Object(_) => String::from("<object>"),
- Schema::Array(schema) => get_schema_type_text(schema.items, _style),
- Schema::AllOf(_) => String::from("<object>"),
- }
-}
-
-/// Helper to format an object property, including name, type and description.
-pub fn get_property_description(
- name: &str,
- schema: &Schema,
- style: ParameterDisplayStyle,
- format: DocumentationFormat,
-) -> String {
- let type_text = get_schema_type_text(schema, style);
-
- let (descr, default, extra) = match schema {
- Schema::Null => ("null", None, None),
- Schema::String(ref schema) => (schema.description, schema.default.map(|v| v.to_owned()), None),
- Schema::Boolean(ref schema) => (schema.description, schema.default.map(|v| v.to_string()), None),
- Schema::Integer(ref schema) => (schema.description, schema.default.map(|v| v.to_string()), None),
- Schema::Number(ref schema) => (schema.description, schema.default.map(|v| v.to_string()), None),
- Schema::Object(ref schema) => (schema.description, None, None),
- Schema::AllOf(ref schema) => (schema.description, None, None),
- Schema::Array(ref schema) => (schema.description, None, Some(String::from("Can be specified more than once."))),
- };
-
- let default_text = match default {
- Some(text) => format!(" (default={})", text),
- None => String::new(),
- };
-
- let descr = match extra {
- Some(extra) => format!("{} {}", descr, extra),
- None => String::from(descr),
- };
-
- if format == DocumentationFormat::ReST {
- let mut text = match style {
- ParameterDisplayStyle::Config => {
- // reST definition list format
- format!("``{}`` : ``{}{}``\n ", name, type_text, default_text)
- }
- ParameterDisplayStyle::ConfigSub => {
- // reST definition list format
- format!("``{}`` = ``{}{}``\n ", name, type_text, default_text)
- }
- ParameterDisplayStyle::Arg => {
- // reST option list format
- format!("``--{}`` ``{}{}``\n ", name, type_text, default_text)
- }
- ParameterDisplayStyle::Fixed => {
- format!("``<{}>`` : ``{}{}``\n ", name, type_text, default_text)
- }
- };
-
- text.push_str(&wrap_text("", " ", &descr, 80));
- text.push('\n');
-
- text
- } else {
- let display_name = match style {
- ParameterDisplayStyle::Config => format!("{}:", name),
- ParameterDisplayStyle::ConfigSub => format!("{}=", name),
- ParameterDisplayStyle::Arg => format!("--{}", name),
- ParameterDisplayStyle::Fixed => format!("<{}>", name),
- };
-
- let mut text = format!(" {:-10} {}{}", display_name, type_text, default_text);
- let indent = " ";
- text.push('\n');
- text.push_str(&wrap_text(indent, indent, &descr, 80));
-
- text
- }
-}
-
-fn get_simply_type_text(schema: &Schema, list_enums: bool) -> String {
- match schema {
- Schema::Null => String::from("<null>"), // should not happen
- Schema::Boolean(_) => String::from("<1|0>"),
- Schema::Integer(_) => String::from("<integer>"),
- Schema::Number(_) => String::from("<number>"),
- Schema::String(string_schema) => match string_schema {
- StringSchema {
- type_text: Some(type_text),
- ..
- } => String::from(*type_text),
- StringSchema {
- format: Some(ApiStringFormat::Enum(variants)),
- ..
- } => {
- if list_enums && variants.len() <= 3 {
- let list: Vec<String> =
- variants.iter().map(|e| String::from(e.value)).collect();
- list.join("|")
- } else {
- String::from("<enum>")
- }
- }
- _ => String::from("<string>"),
- },
- _ => panic!("get_simply_type_text: expected simply type"),
- }
-}
-
-fn get_object_type_text(object_schema: &ObjectSchema) -> String {
- let mut parts = Vec::new();
-
- let mut add_part = |name, optional, schema| {
- let tt = get_simply_type_text(schema, false);
- let text = if parts.is_empty() {
- format!("{}={}", name, tt)
- } else {
- format!(",{}={}", name, tt)
- };
- if optional {
- parts.push(format!("[{}]", text));
- } else {
- parts.push(text);
- }
- };
-
- // add default key first
- if let Some(ref default_key) = object_schema.default_key {
- let (optional, schema) = object_schema.lookup(default_key).unwrap();
- add_part(default_key, optional, schema);
- }
-
- // add required keys
- for (name, optional, schema) in object_schema.properties {
- if *optional {
- continue;
- }
- if let Some(ref default_key) = object_schema.default_key {
- if name == default_key {
- continue;
- }
- }
- add_part(name, *optional, schema);
- }
-
- // add options keys
- for (name, optional, schema) in object_schema.properties {
- if !*optional {
- continue;
- }
- if let Some(ref default_key) = object_schema.default_key {
- if name == default_key {
- continue;
- }
- }
- add_part(name, *optional, schema);
- }
-
- let mut type_text = String::new();
- type_text.push('[');
- type_text.push_str(&parts.join(" "));
- type_text.push(']');
- type_text
-}
-
-pub fn get_property_string_type_text(schema: &Schema) -> String {
- match schema {
- Schema::Object(object_schema) => get_object_type_text(object_schema),
- Schema::Array(array_schema) => {
- let item_type = get_simply_type_text(array_schema.items, true);
- format!("[{}, ...]", item_type)
- }
- _ => panic!("get_property_string_type_text: expected array or object"),
- }
-}
-
-/// Generate ReST Documentaion for enumeration.
-pub fn dump_enum_properties(schema: &Schema) -> Result<String, Error> {
- let mut res = String::new();
-
- if let Schema::String(StringSchema {
- format: Some(ApiStringFormat::Enum(variants)),
- ..
- }) = schema
- {
- for item in variants.iter() {
- res.push_str(&format!(":``{}``: ", item.value));
- let descr = wrap_text("", " ", item.description, 80);
- res.push_str(&descr);
- res.push('\n');
- }
- return Ok(res);
- }
-
- bail!("dump_enum_properties failed - not an enum");
-}
-
-/// Generate ReST Documentaion for object properties
-pub fn dump_properties(
- param: &dyn ObjectSchemaType,
- indent: &str,
- style: ParameterDisplayStyle,
- skip: &[&str],
-) -> String {
- let mut res = String::new();
- let next_indent = format!(" {}", indent);
-
- let mut required_list: Vec<String> = Vec::new();
- let mut optional_list: Vec<String> = Vec::new();
-
- for (prop, optional, schema) in param.properties() {
- if skip.iter().find(|n| n == &prop).is_some() {
- continue;
- }
-
- let mut param_descr =
- get_property_description(prop, &schema, style, DocumentationFormat::ReST);
-
- if !indent.is_empty() {
- param_descr = format!("{}{}", indent, param_descr); // indent first line
- param_descr = param_descr.replace("\n", &format!("\n{}", indent)); // indent rest
- }
-
- if style == ParameterDisplayStyle::Config {
- match schema {
- Schema::String(StringSchema {
- format: Some(ApiStringFormat::PropertyString(sub_schema)),
- ..
- }) => {
- match sub_schema {
- Schema::Object(object_schema) => {
- let sub_text = dump_properties(
- object_schema,
- &next_indent,
- ParameterDisplayStyle::ConfigSub,
- &[],
- );
- param_descr.push_str(&sub_text);
- }
- Schema::Array(_) => {
- // do nothing - description should explain the list type
- }
- _ => unreachable!(),
- }
- }
- _ => { /* do nothing */ }
- }
- }
- if *optional {
- optional_list.push(param_descr);
- } else {
- required_list.push(param_descr);
- }
- }
-
- if !required_list.is_empty() {
- if style != ParameterDisplayStyle::ConfigSub {
- res.push_str("\n*Required properties:*\n\n");
- }
-
- for text in required_list {
- res.push_str(&text);
- res.push('\n');
- }
- }
-
- if !optional_list.is_empty() {
- if style != ParameterDisplayStyle::ConfigSub {
- res.push_str("\n*Optional properties:*\n\n");
- }
-
- for text in optional_list {
- res.push_str(&text);
- res.push('\n');
- }
- }
-
- res
-}
-
-fn dump_api_return_schema(returns: &ReturnType, style: ParameterDisplayStyle) -> String {
- let schema = &returns.schema;
-
- let mut res = if returns.optional {
- "*Returns* (optionally): ".to_string()
- } else {
- "*Returns*: ".to_string()
- };
-
- let type_text = get_schema_type_text(schema, style);
- res.push_str(&format!("**{}**\n\n", type_text));
-
- match schema {
- Schema::Null => {
- return res;
- }
- Schema::Boolean(schema) => {
- let description = wrap_text("", "", schema.description, 80);
- res.push_str(&description);
- }
- Schema::Integer(schema) => {
- let description = wrap_text("", "", schema.description, 80);
- res.push_str(&description);
- }
- Schema::Number(schema) => {
- let description = wrap_text("", "", schema.description, 80);
- res.push_str(&description);
- }
- Schema::String(schema) => {
- let description = wrap_text("", "", schema.description, 80);
- res.push_str(&description);
- }
- Schema::Array(schema) => {
- let description = wrap_text("", "", schema.description, 80);
- res.push_str(&description);
- }
- Schema::Object(obj_schema) => {
- let description = wrap_text("", "", obj_schema.description, 80);
- res.push_str(&description);
- res.push_str(&dump_properties(obj_schema, "", style, &[]));
- }
- Schema::AllOf(all_of_schema) => {
- let description = wrap_text("", "", all_of_schema.description, 80);
- res.push_str(&description);
- res.push_str(&dump_properties(all_of_schema, "", style, &[]));
- }
- }
-
- res.push('\n');
-
- res
-}
-
-fn dump_method_definition(method: &str, path: &str, def: Option<&ApiMethod>) -> Option<String> {
- let style = ParameterDisplayStyle::Config;
- match def {
- None => None,
- Some(api_method) => {
- let description = wrap_text("", "", &api_method.parameters.description(), 80);
- let param_descr = dump_properties(&api_method.parameters, "", style, &[]);
-
- let return_descr = dump_api_return_schema(&api_method.returns, style);
-
- let mut method = method;
-
- if let ApiHandler::AsyncHttp(_) = api_method.handler {
- method = if method == "POST" { "UPLOAD" } else { method };
- method = if method == "GET" { "DOWNLOAD" } else { method };
- }
-
- let res = format!(
- "**{} {}**\n\n{}{}\n\n{}",
- method, path, description, param_descr, return_descr
- );
- Some(res)
- }
- }
-}
-
-/// Generate ReST Documentaion for a complete API defined by a ``Router``.
-pub fn dump_api(
- output: &mut dyn Write,
- router: &crate::api::Router,
- path: &str,
- mut pos: usize,
-) -> Result<(), Error> {
- use crate::api::SubRoute;
-
- let mut cond_print = |x| -> Result<_, Error> {
- if let Some(text) = x {
- if pos > 0 {
- writeln!(output, "-----\n")?;
- }
- writeln!(output, "{}", text)?;
- pos += 1;
- }
- Ok(())
- };
-
- cond_print(dump_method_definition("GET", path, router.get))?;
- cond_print(dump_method_definition("POST", path, router.post))?;
- cond_print(dump_method_definition("PUT", path, router.put))?;
- cond_print(dump_method_definition("DELETE", path, router.delete))?;
-
- match &router.subroute {
- None => return Ok(()),
- Some(SubRoute::MatchAll { router, param_name }) => {
- let sub_path = if path == "." {
- format!("<{}>", param_name)
- } else {
- format!("{}/<{}>", path, param_name)
- };
- dump_api(output, router, &sub_path, pos)?;
- }
- Some(SubRoute::Map(dirmap)) => {
- //let mut keys: Vec<&String> = map.keys().collect();
- //keys.sort_unstable_by(|a, b| a.cmp(b));
- for (key, sub_router) in dirmap.iter() {
- let sub_path = if path == "." {
- (*key).to_string()
- } else {
- format!("{}/{}", path, key)
- };
- dump_api(output, sub_router, &sub_path, pos)?;
- }
- }
- }
-
- Ok(())
-}
-
-/// Generate ReST Documentaion for ``SectionConfig``
-pub fn dump_section_config(config: &SectionConfig) -> String {
- let mut res = String::new();
-
- let plugin_count = config.plugins().len();
-
- for plugin in config.plugins().values() {
- let name = plugin.type_name();
- let properties = plugin.properties();
- let skip = match plugin.id_property() {
- Some(id) => vec![id],
- None => Vec::new(),
- };
-
- if plugin_count > 1 {
- let description = wrap_text("", "", properties.description(), 80);
- res.push_str(&format!(
- "\n**Section type** \'``{}``\': {}\n\n",
- name, description
- ));
- }
-
- res.push_str(&dump_properties(
- properties,
- "",
- ParameterDisplayStyle::Config,
- &skip,
- ));
- }
-
- res
-}
+++ /dev/null
-//! Proxmox API module.
-//!
-//! This provides utilities to define APIs in a declarative way using
-//! Schemas. Primary use case it to define REST/HTTP APIs. Another use case
-//! is to define command line tools using Schemas. Finally, it is
-//! possible to use schema definitions to derive configuration file
-//! parsers.
-
-#[cfg(feature = "api-macro")]
-pub use proxmox_api_macro::{api, router};
-
-#[macro_use]
-mod api_type_macros;
-
-#[doc(hidden)]
-pub mod const_regex;
-#[doc(hidden)]
-pub mod error;
-pub mod schema;
-pub mod section_config;
-
-mod permission;
-pub use permission::*;
-
-#[doc(inline)]
-pub use const_regex::ConstRegexPattern;
-
-#[doc(inline)]
-pub use error::HttpError;
-
-#[cfg(any(feature = "router", feature = "cli"))]
-#[doc(hidden)]
-pub mod rpc_environment;
-
-#[cfg(any(feature = "router", feature = "cli"))]
-#[doc(inline)]
-pub use rpc_environment::{RpcEnvironment, RpcEnvironmentType};
-
-#[cfg(feature = "router")]
-pub mod format;
-
-#[cfg(feature = "router")]
-#[doc(hidden)]
-pub mod router;
-
-#[cfg(feature = "router")]
-#[doc(inline)]
-pub use router::{
- ApiFuture, ApiHandler, ApiMethod, ApiResponseFuture, Router, SubRoute, SubdirMap,
-};
-
-#[cfg(feature = "cli")]
-pub mod cli;
-
-pub mod de;
-
-pub mod upid;
+++ /dev/null
-//! Declarative permission system
-//!
-//! A declarative way to define API access permissions.
-
-use std::collections::HashMap;
-use std::fmt;
-use std::ops::Deref;
-
-/// Access permission
-#[cfg_attr(feature = "test-harness", derive(Eq, PartialEq))]
-pub enum Permission {
- /// Allow Superuser
- Superuser,
- /// Allow the whole World, no authentication required
- World,
- /// Allow any authenticated user
- Anybody,
- /// Allow access for the specified user
- User(&'static str),
- /// Allow access if specified param matches logged in user
- UserParam(&'static str),
- /// Allow access for the specified group of users
- Group(&'static str),
- /// Use a parameter value as userid to run sub-permission tests.
- WithParam(&'static str, &'static Permission),
- /// Check privilege/role on the specified path. The boolean
- /// attribute specifies if you want to allow partial matches (u64
- /// interpreted as bitmask).
- Privilege(&'static [&'static str], u64, bool),
- /// Allow access if all sub-permissions match
- And(&'static [&'static Permission]),
- /// Allow access if any sub-permissions match
- Or(&'static [&'static Permission]),
-}
-
-impl fmt::Debug for Permission {
- fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
- match self {
- Permission::Superuser => f.write_str("Superuser"),
- Permission::World => f.write_str("World"),
- Permission::Anybody => f.write_str("Anybody"),
- Permission::User(ref userid) => write!(f, "User({})", userid),
- Permission::UserParam(param_name) => write!(f, "UserParam({})", param_name),
- Permission::Group(ref group) => write!(f, "Group({})", group),
- Permission::WithParam(param_name, subtest) => {
- write!(f, "WithParam({}, {:?})", param_name, subtest)
- }
- Permission::Privilege(path, privs, partial) => {
- write!(f, "Privilege({:?}, {:0b}, {})", path, privs, partial)
- }
- Permission::And(list) => {
- f.write_str("And(\n")?;
- for subtest in list.iter() {
- writeln!(f, " {:?}", subtest)?;
- }
- f.write_str(")\n")
- }
- Permission::Or(list) => {
- f.write_str("Or(\n")?;
- for subtest in list.iter() {
- writeln!(f, " {:?}", subtest)?;
- }
- f.write_str(")\n")
- }
- }
- }
-}
-
-/// Trait to query user information (used by check_api_permission)
-pub trait UserInformation {
- fn is_superuser(&self, userid: &str) -> bool;
- fn is_group_member(&self, userid: &str, group: &str) -> bool;
- fn lookup_privs(&self, userid: &str, path: &[&str]) -> u64;
-}
-
-impl <T: UserInformation> UserInformation for std::sync::Arc<T> {
- fn is_superuser(&self, userid: &str) -> bool {
- self.deref().is_superuser(userid)
- }
- fn is_group_member(&self, userid: &str, group: &str) -> bool {
- self.deref().is_group_member(userid, group)
- }
- fn lookup_privs(&self, userid: &str, path: &[&str]) -> u64 {
- self.deref().lookup_privs(userid, path)
- }
-}
-
-/// Example implementation to check access permissions
-///
-/// This implementation supports URI variables in Privilege path
-/// components, i.e. '{storage}'. We replace this with actual
-/// parameter values before calling lookup_privs().
-pub fn check_api_permission(
- perm: &Permission,
- userid: Option<&str>,
- param: &HashMap<String, String>,
- info: &dyn UserInformation,
-) -> bool {
- if let Some(ref userid) = userid {
- if info.is_superuser(userid) {
- return true;
- }
- }
-
- check_api_permission_tail(perm, userid, param, info)
-}
-
-// some of them are deeply nested
-#[allow(clippy::needless_return)]
-fn check_api_permission_tail(
- perm: &Permission,
- userid: Option<&str>,
- param: &HashMap<String, String>,
- info: &dyn UserInformation,
-) -> bool {
- match perm {
- Permission::World => return true,
- Permission::Anybody => {
- return userid.is_some();
- }
- Permission::Superuser => match userid {
- None => return false,
- Some(ref userid) => return info.is_superuser(userid),
- },
- Permission::User(expected_userid) => match userid {
- None => return false,
- Some(ref userid) => return userid == expected_userid,
- },
- Permission::UserParam(param_name) => match (userid, param.get(¶m_name.to_string())) {
- (None, _) => return false,
- (_, None) => return false,
- (Some(ref userid), Some(ref expected)) => return userid == expected,
- },
- Permission::Group(expected_group) => match userid {
- None => return false,
- Some(ref userid) => return info.is_group_member(userid, expected_group),
- },
- Permission::WithParam(param_name, subtest) => {
- return check_api_permission(
- subtest,
- param.get(*param_name).map(|v| v.as_str()),
- param,
- info,
- );
- }
- Permission::Privilege(path, expected_privs, partial) => {
- // replace uri vars
- let mut new_path: Vec<&str> = Vec::new();
- for comp in path.iter() {
- if comp.starts_with('{') && comp.ends_with('}') {
- let param_name = unsafe { comp.get_unchecked(1..comp.len() - 1) };
- match param.get(param_name) {
- None => return false,
- Some(value) => {
- new_path.push(value);
- }
- }
- } else {
- new_path.push(comp);
- }
- }
- match userid {
- None => return false,
- Some(userid) => {
- let privs = info.lookup_privs(userid, &new_path);
- if privs == 0 {
- return false;
- };
- if *partial {
- return (expected_privs & privs) != 0;
- } else {
- return (*expected_privs & privs) == *expected_privs;
- }
- }
- }
- }
- Permission::And(list) => {
- for subtest in list.iter() {
- if !check_api_permission_tail(subtest, userid, param, info) {
- return false;
- }
- }
-
- return true;
- }
- Permission::Or(list) => {
- for subtest in list.iter() {
- if check_api_permission_tail(subtest, userid, param, info) {
- return true;
- }
- }
-
- return false;
- }
- }
-}
-
-#[cfg(test)]
-mod test {
-
- use crate::api::permission::*;
- use serde_json::{json, Value};
-
- struct MockedUserInfo {
- privs: Value,
- groups: Value,
- }
-
- impl UserInformation for MockedUserInfo {
- fn is_superuser(&self, userid: &str) -> bool {
- userid == "root"
- }
-
- fn is_group_member(&self, userid: &str, group: &str) -> bool {
- if let Some(groups) = self.groups[userid].as_array() {
- return groups.contains(&Value::from(group));
- }
-
- return false;
- }
-
- fn lookup_privs(&self, userid: &str, path: &[&str]) -> u64 {
- let path = format!("/{}", path.join("/"));
- if let Some(users) = self.privs.get(path) {
- if let Some(privilege) = users.get(userid) {
- return privilege.as_u64().unwrap();
- }
- }
-
- return 0;
- }
- }
-
- #[test]
- fn test_privileges() {
- let userinfo = MockedUserInfo {
- privs: json!({
- "/": {
- "user1": 0b10,
- },
- "/datastore": {
- "user1": 0b00,
- "user2": 0b01,
- },
- "/datastore/foo": {
- "user1": 0b01,
- },
- }),
- groups: json!({
- "user1": [
- "group1",
- ],
- "user2": [
- "group2",
- ],
- }),
- };
-
- let mut param = HashMap::new();
- param.insert("user".to_string(), "user1".to_string());
- param.insert("datastore".to_string(), "foo".to_string());
-
- let test_check = |perm: &Permission, userid: Option<&str>, should_succeed: bool| {
- println!("{:?} on {:?}: {}", userid, perm, should_succeed);
- assert_eq!(
- check_api_permission(perm, userid, ¶m, &userinfo),
- should_succeed
- )
- };
-
- test_check(&Permission::Superuser, Some("root"), true);
- test_check(&Permission::Superuser, Some("user1"), false);
- test_check(&Permission::Superuser, None, false);
-
- test_check(&Permission::World, Some("root"), true);
- test_check(&Permission::World, Some("user1"), true);
- test_check(&Permission::World, None, true);
-
- test_check(&Permission::Anybody, Some("root"), true);
- test_check(&Permission::Anybody, Some("user1"), true);
- test_check(&Permission::Anybody, None, false);
-
- test_check(&Permission::User("user1"), Some("root"), true);
- test_check(&Permission::User("user1"), Some("user1"), true);
- test_check(&Permission::User("user1"), Some("user2"), false);
- test_check(&Permission::User("user1"), None, false);
-
- test_check(&Permission::Group("group1"), Some("root"), true);
- test_check(&Permission::Group("group1"), Some("user1"), true);
- test_check(&Permission::Group("group1"), Some("user2"), false);
- test_check(&Permission::Group("group1"), None, false);
-
- test_check(
- &Permission::WithParam("user", &Permission::User("root")),
- Some("root"),
- true,
- );
- test_check(
- &Permission::WithParam("user", &Permission::User("user1")),
- Some("user1"),
- true,
- );
- test_check(
- &Permission::WithParam("user", &Permission::User("user2")),
- Some("user2"),
- false,
- );
- test_check(
- &Permission::WithParam("user", &Permission::User("")),
- None,
- false,
- );
-
- test_check(
- &Permission::And(&[&Permission::User("user1"), &Permission::Group("group2")]),
- Some("root"),
- true,
- );
- test_check(
- &Permission::And(&[&Permission::User("user1"), &Permission::Group("group2")]),
- Some("user1"),
- false,
- );
- test_check(
- &Permission::And(&[&Permission::User("user1"), &Permission::Group("group1")]),
- Some("user1"),
- true,
- );
- test_check(
- &Permission::And(&[&Permission::User("user1"), &Permission::Group("group2")]),
- None,
- false,
- );
-
- test_check(
- &Permission::Or(&[&Permission::User("user1"), &Permission::Group("group2")]),
- Some("root"),
- true,
- );
- test_check(
- &Permission::Or(&[&Permission::User("user1"), &Permission::Group("group2")]),
- Some("user1"),
- true,
- );
- test_check(
- &Permission::Or(&[&Permission::User("user1"), &Permission::Group("group1")]),
- Some("user2"),
- false,
- );
- test_check(
- &Permission::Or(&[&Permission::User("user1"), &Permission::Group("group2")]),
- None,
- false,
- );
-
- test_check(&Permission::Privilege(&[], 0b11, true), Some("root"), true);
- test_check(&Permission::Privilege(&[], 0b11, true), Some("user1"), true);
- test_check(
- &Permission::Privilege(&[], 0b11, false),
- Some("user1"),
- false,
- );
- test_check(
- &Permission::Privilege(&[], 0b11, true),
- Some("user2"),
- false,
- );
- test_check(
- &Permission::Privilege(&[], 0b11, false),
- Some("user2"),
- false,
- );
- test_check(&Permission::Privilege(&[], 0b11, true), None, false);
- test_check(&Permission::Privilege(&[], 0b11, false), None, false);
-
- test_check(
- &Permission::Privilege(&["datastore"], 0b01, true),
- Some("user1"),
- false,
- );
- test_check(
- &Permission::Privilege(&["datastore"], 0b01, true),
- Some("user2"),
- true,
- );
- test_check(
- &Permission::Privilege(&["datastore"], 0b01, true),
- None,
- false,
- );
-
- test_check(
- &Permission::Privilege(&["datastore", "{datastore}"], 0b01, true),
- Some("user1"),
- true,
- );
- test_check(
- &Permission::Privilege(&["datastore", "{datastore}"], 0b01, true),
- Some("user2"),
- false,
- );
- test_check(
- &Permission::Privilege(&["datastore", "{datastore}"], 0b01, true),
- None,
- false,
- );
- }
-}
+++ /dev/null
-use std::collections::HashMap;
-use std::fmt;
-use std::future::Future;
-use std::pin::Pin;
-
-use anyhow::Error;
-use http::request::Parts;
-use http::{Method, Response};
-use hyper::Body;
-use percent_encoding::percent_decode_str;
-use serde_json::Value;
-
-use crate::api::schema::{ObjectSchema, ParameterSchema, Schema};
-use crate::api::RpcEnvironment;
-
-use super::Permission;
-
-/// Deprecated reexport:
-pub use super::schema::ReturnType;
-
-/// A synchronous API handler gets a json Value as input and returns a json Value as output.
-///
-/// Most API handler are synchronous. Use this to define such handler:
-/// ```
-/// # use anyhow::*;
-/// # use serde_json::{json, Value};
-/// # use proxmox::api::{*, schema::*};
-/// #
-/// fn hello(
-/// param: Value,
-/// info: &ApiMethod,
-/// rpcenv: &mut dyn RpcEnvironment,
-/// ) -> Result<Value, Error> {
-/// Ok(json!("Hello world!"))
-/// }
-///
-/// const API_METHOD_HELLO: ApiMethod = ApiMethod::new(
-/// &ApiHandler::Sync(&hello),
-/// &ObjectSchema::new("Hello World Example", &[])
-/// );
-/// ```
-pub type ApiHandlerFn = &'static (dyn Fn(Value, &ApiMethod, &mut dyn RpcEnvironment) -> Result<Value, Error>
- + Send
- + Sync
- + 'static);
-
-/// Asynchronous API handlers
-///
-/// Returns a future Value.
-/// ```
-/// # use anyhow::*;
-/// # use serde_json::{json, Value};
-/// # use proxmox::api::{*, schema::*};
-/// #
-/// use futures::*;
-///
-/// fn hello_future<'a>(
-/// param: Value,
-/// info: &ApiMethod,
-/// rpcenv: &'a mut dyn RpcEnvironment,
-/// ) -> ApiFuture<'a> {
-/// async move {
-/// let data = json!("hello world!");
-/// Ok(data)
-/// }.boxed()
-/// }
-///
-/// const API_METHOD_HELLO_FUTURE: ApiMethod = ApiMethod::new(
-/// &ApiHandler::Async(&hello_future),
-/// &ObjectSchema::new("Hello World Example (async)", &[])
-/// );
-/// ```
-pub type ApiAsyncHandlerFn = &'static (dyn for<'a> Fn(Value, &'static ApiMethod, &'a mut dyn RpcEnvironment) -> ApiFuture<'a>
- + Send
- + Sync);
-
-pub type ApiFuture<'a> = Pin<Box<dyn Future<Output = Result<Value, anyhow::Error>> + Send + 'a>>;
-
-/// Asynchronous HTTP API handlers
-///
-/// They get low level access to request and response data. Use this
-/// to implement custom upload/download functions.
-/// ```
-/// # use anyhow::*;
-/// # use serde_json::{json, Value};
-/// # use proxmox::api::{*, schema::*};
-/// #
-/// use futures::*;
-/// use hyper::{Body, Response, http::request::Parts};
-///
-/// fn low_level_hello(
-/// parts: Parts,
-/// req_body: Body,
-/// param: Value,
-/// info: &ApiMethod,
-/// rpcenv: Box<dyn RpcEnvironment>,
-/// ) -> ApiResponseFuture {
-/// async move {
-/// let response = http::Response::builder()
-/// .status(200)
-/// .body(Body::from("Hello world!"))?;
-/// Ok(response)
-/// }.boxed()
-/// }
-///
-/// const API_METHOD_LOW_LEVEL_HELLO: ApiMethod = ApiMethod::new(
-/// &ApiHandler::AsyncHttp(&low_level_hello),
-/// &ObjectSchema::new("Hello World Example (low level)", &[])
-/// );
-/// ```
-pub type ApiAsyncHttpHandlerFn = &'static (dyn Fn(
- Parts,
- Body,
- Value,
- &'static ApiMethod,
- Box<dyn RpcEnvironment>,
-) -> ApiResponseFuture
- + Send
- + Sync
- + 'static);
-
-/// The output of an asynchronous API handler is a future yielding a `Response`.
-pub type ApiResponseFuture =
- Pin<Box<dyn Future<Output = Result<Response<Body>, anyhow::Error>> + Send>>;
-
-/// Enum for different types of API handler functions.
-pub enum ApiHandler {
- Sync(ApiHandlerFn),
- Async(ApiAsyncHandlerFn),
- AsyncHttp(ApiAsyncHttpHandlerFn),
-}
-
-#[cfg(feature = "test-harness")]
-impl Eq for ApiHandler {}
-
-#[cfg(feature = "test-harness")]
-impl PartialEq for ApiHandler {
- fn eq(&self, rhs: &Self) -> bool {
- unsafe {
- match (self, rhs) {
- (ApiHandler::Sync(l), ApiHandler::Sync(r)) => {
- core::mem::transmute::<_, usize>(l) == core::mem::transmute::<_, usize>(r)
- }
- (ApiHandler::Async(l), ApiHandler::Async(r)) => {
- core::mem::transmute::<_, usize>(l) == core::mem::transmute::<_, usize>(r)
- }
- (ApiHandler::AsyncHttp(l), ApiHandler::AsyncHttp(r)) => {
- core::mem::transmute::<_, usize>(l) == core::mem::transmute::<_, usize>(r)
- }
- _ => false,
- }
- }
- }
-}
-
-/// Lookup table to child `Router`s
-///
-/// Stores a sorted list of `(name, router)` tuples:
-///
-/// - `name`: The name of the subdir
-/// - `router`: The router for this subdir
-///
-/// **Note:** The list has to be sorted by name, because we use a binary
-/// search to find items.
-///
-/// This is a workaround unless RUST can const_fn `Hash::new()`
-pub type SubdirMap = &'static [(&'static str, &'static Router)];
-
-/// Classify different types of routers
-pub enum SubRoute {
- //Hash(HashMap<String, Router>),
- /// Router with static lookup map.
- ///
- /// The first path element is used to lookup a new
- /// router with `SubdirMap`. If found, the remaining path is
- /// passed to that router.
- Map(SubdirMap),
- /// Router that always match the first path element
- ///
- /// The matched path element is stored as parameter
- /// `param_name`. The remaining path is matched using the `router`.
- MatchAll {
- router: &'static Router,
- param_name: &'static str,
- },
-}
-
-/// Macro to create an ApiMethod to list entries from SubdirMap
-#[macro_export]
-macro_rules! list_subdirs_api_method {
- ($map:expr) => {
- $crate::api::ApiMethod::new(
- &$crate::api::ApiHandler::Sync( & |_, _, _| {
- let index = ::serde_json::json!(
- $map.iter().map(|s| ::serde_json::json!({ "subdir": s.0}))
- .collect::<Vec<::serde_json::Value>>()
- );
- Ok(index)
- }),
- &$crate::api::schema::ObjectSchema::new("Directory index.", &[])
- .additional_properties(true)
- ).access(None, &$crate::api::Permission::Anybody)
- }
-}
-
-/// Define APIs with routing information
-///
-/// REST APIs use hierarchical paths to identify resources. A path
-/// consists of zero or more components, separated by `/`. A `Router`
-/// is a simple data structure to define such APIs. Each `Router` is
-/// responsible for a specific path, and may define `ApiMethod`s for
-/// different HTTP requests (GET, PUT, POST, DELETE). If the path
-/// contains more elements, `subroute` is used to find the correct
-/// endpoint.
-///
-/// Routers are meant to be build a compile time, and you can use
-/// all `const fn(mut self, ..)` methods to configure them.
-///
-///```
-/// # use anyhow::*;
-/// # use serde_json::{json, Value};
-/// # use proxmox::api::{*, schema::*};
-/// #
-/// const API_METHOD_HELLO: ApiMethod = ApiMethod::new(
-/// &ApiHandler::Sync(&|_, _, _| {
-/// Ok(json!("Hello world!"))
-/// }),
-/// &ObjectSchema::new("Hello World Example", &[])
-/// );
-/// const ROUTER: Router = Router::new()
-/// .get(&API_METHOD_HELLO);
-///```
-pub struct Router {
- /// GET requests
- pub get: Option<&'static ApiMethod>,
- /// PUT requests
- pub put: Option<&'static ApiMethod>,
- /// POST requests
- pub post: Option<&'static ApiMethod>,
- /// DELETE requests
- pub delete: Option<&'static ApiMethod>,
- /// Used to find the correct API endpoint.
- pub subroute: Option<SubRoute>,
-}
-
-impl Router {
- /// Create a new Router.
- pub const fn new() -> Self {
- Self {
- get: None,
- put: None,
- post: None,
- delete: None,
- subroute: None,
- }
- }
-
- /// Configure a static map as `subroute`.
- pub const fn subdirs(mut self, map: SubdirMap) -> Self {
- self.subroute = Some(SubRoute::Map(map));
- self
- }
-
- /// Configure a `SubRoute::MatchAll` as `subroute`.
- pub const fn match_all(mut self, param_name: &'static str, router: &'static Router) -> Self {
- self.subroute = Some(SubRoute::MatchAll { router, param_name });
- self
- }
-
- /// Configure the GET method.
- pub const fn get(mut self, m: &'static ApiMethod) -> Self {
- self.get = Some(m);
- self
- }
-
- /// Configure the PUT method.
- pub const fn put(mut self, m: &'static ApiMethod) -> Self {
- self.put = Some(m);
- self
- }
-
- /// Configure the POST method.
- pub const fn post(mut self, m: &'static ApiMethod) -> Self {
- self.post = Some(m);
- self
- }
-
- /// Same as `post`, but expects an `AsyncHttp` handler.
- pub const fn upload(mut self, m: &'static ApiMethod) -> Self {
- // fixme: expect AsyncHttp
- self.post = Some(m);
- self
- }
-
- /// Same as `get`, but expects an `AsyncHttp` handler.
- pub const fn download(mut self, m: &'static ApiMethod) -> Self {
- // fixme: expect AsyncHttp
- self.get = Some(m);
- self
- }
-
- /// Same as `get`, but expects an `AsyncHttp` handler.
- pub const fn upgrade(mut self, m: &'static ApiMethod) -> Self {
- // fixme: expect AsyncHttp
- self.get = Some(m);
- self
- }
-
- /// Configure the DELETE method
- pub const fn delete(mut self, m: &'static ApiMethod) -> Self {
- self.delete = Some(m);
- self
- }
-
- /// Find the router for a specific path.
- ///
- /// - `components`: Path, split into individual components.
- /// - `uri_param`: Mutable hash map to store parameter from `MatchAll` router.
- pub fn find_route(
- &self,
- components: &[&str],
- uri_param: &mut HashMap<String, String>,
- ) -> Option<&Router> {
- if components.is_empty() {
- return Some(self);
- };
-
- let (dir, remaining) = (components[0], &components[1..]);
-
- let dir = match percent_decode_str(dir).decode_utf8() {
- Ok(dir) => dir.to_string(),
- Err(_) => return None,
- };
-
- match self.subroute {
- None => {}
- Some(SubRoute::Map(dirmap)) => {
- if let Ok(ind) = dirmap.binary_search_by_key(&dir.as_str(), |(name, _)| name) {
- let (_name, router) = dirmap[ind];
- //println!("FOUND SUBDIR {}", dir);
- return router.find_route(remaining, uri_param);
- }
- }
- Some(SubRoute::MatchAll { router, param_name }) => {
- //println!("URI PARAM {} = {}", param_name, dir); // fixme: store somewhere
- uri_param.insert(param_name.to_owned(), dir);
- return router.find_route(remaining, uri_param);
- }
- }
-
- None
- }
-
- /// Lookup the API method for a specific path.
- /// - `components`: Path, split into individual components.
- /// - `method`: The HTTP method.
- /// - `uri_param`: Mutable hash map to store parameter from `MatchAll` router.
- pub fn find_method(
- &self,
- components: &[&str],
- method: Method,
- uri_param: &mut HashMap<String, String>,
- ) -> Option<&ApiMethod> {
- if let Some(info) = self.find_route(components, uri_param) {
- return match method {
- Method::GET => info.get,
- Method::PUT => info.put,
- Method::POST => info.post,
- Method::DELETE => info.delete,
- _ => None,
- };
- }
- None
- }
-}
-
-impl Default for Router {
- #[inline]
- fn default() -> Self {
- Self::new()
- }
-}
-
-const NULL_SCHEMA: Schema = Schema::Null;
-
-fn dummy_handler_fn(
- _arg: Value,
- _method: &ApiMethod,
- _env: &mut dyn RpcEnvironment,
-) -> Result<Value, Error> {
- // do nothing
- Ok(Value::Null)
-}
-
-const DUMMY_HANDLER: ApiHandler = ApiHandler::Sync(&dummy_handler_fn);
-
-/// Access permission with description
-#[cfg_attr(feature = "test-harness", derive(Eq, PartialEq))]
-pub struct ApiAccess {
- pub description: Option<&'static str>,
- pub permission: &'static Permission,
-}
-
-/// This struct defines a synchronous API call which returns the result as json `Value`
-#[cfg_attr(feature = "test-harness", derive(Eq, PartialEq))]
-pub struct ApiMethod {
- /// The protected flag indicates that the provides function should be forwarded
- /// to the daemon running in privileged mode.
- pub protected: bool,
- /// This flag indicates that the provided method may change the local timezone, so the server
- /// should do a tzset afterwards
- pub reload_timezone: bool,
- /// Parameter type Schema
- pub parameters: ParameterSchema,
- /// Return type Schema
- pub returns: ReturnType,
- /// Handler function
- pub handler: &'static ApiHandler,
- /// Access Permissions
- pub access: ApiAccess,
-}
-
-impl std::fmt::Debug for ApiMethod {
- fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
- write!(f, "ApiMethod {{ ")?;
- write!(f, " parameters: {:?}", self.parameters)?;
- write!(f, " returns: {:?}", self.returns)?;
- write!(f, " handler: {:p}", &self.handler)?;
- write!(f, " permissions: {:?}", &self.access.permission)?;
- write!(f, "}}")
- }
-}
-
-impl ApiMethod {
- pub const fn new_full(handler: &'static ApiHandler, parameters: ParameterSchema) -> Self {
- Self {
- parameters,
- handler,
- returns: ReturnType::new(false, &NULL_SCHEMA),
- protected: false,
- reload_timezone: false,
- access: ApiAccess {
- description: None,
- permission: &Permission::Superuser,
- },
- }
- }
-
- pub const fn new(handler: &'static ApiHandler, parameters: &'static ObjectSchema) -> Self {
- Self::new_full(handler, ParameterSchema::Object(parameters))
- }
-
- pub const fn new_dummy(parameters: &'static ObjectSchema) -> Self {
- Self {
- parameters: ParameterSchema::Object(parameters),
- handler: &DUMMY_HANDLER,
- returns: ReturnType::new(false, &NULL_SCHEMA),
- protected: false,
- reload_timezone: false,
- access: ApiAccess {
- description: None,
- permission: &Permission::Superuser,
- },
- }
- }
-
- pub const fn returns(mut self, returns: ReturnType) -> Self {
- self.returns = returns;
-
- self
- }
-
- pub const fn protected(mut self, protected: bool) -> Self {
- self.protected = protected;
-
- self
- }
-
- pub const fn reload_timezone(mut self, reload_timezone: bool) -> Self {
- self.reload_timezone = reload_timezone;
-
- self
- }
-
- pub const fn access(
- mut self,
- description: Option<&'static str>,
- permission: &'static Permission,
- ) -> Self {
- self.access = ApiAccess {
- description,
- permission,
- };
-
- self
- }
-}
+++ /dev/null
-use crate::tools::AsAny;
-
-use serde_json::Value;
-
-/// Abstract Interface for API methods to interact with the environment
-pub trait RpcEnvironment: std::any::Any + AsAny + Send {
- /// Use this to pass additional result data. It is up to the environment
- /// how the data is used.
- fn result_attrib_mut(&mut self) -> &mut Value;
-
- /// Access result attribute immutable
- fn result_attrib(&self) -> &Value;
-
- /// The environment type
- fn env_type(&self) -> RpcEnvironmentType;
-
- /// Set authentication id
- fn set_auth_id(&mut self, user: Option<String>);
-
- /// Get authentication id
- fn get_auth_id(&self) -> Option<String>;
-
- /// Set the client IP, should be re-set if a proxied connection was detected
- fn set_client_ip(&mut self, _client_ip: Option<std::net::SocketAddr>) {
- // dummy no-op implementation, as most environments don't need this
- }
-
- /// Get the (real) client IP
- fn get_client_ip(&self) -> Option<std::net::SocketAddr> {
- None // dummy no-op implementation, as most environments don't need this
- }
-}
-
-/// Environment Type
-///
-/// We use this to enumerate the different environment types. Some methods
-/// needs to do different things when started from the command line interface,
-/// or when executed from a privileged server running as root.
-#[derive(PartialEq, Copy, Clone)]
-pub enum RpcEnvironmentType {
- /// Command started from command line
- CLI,
- /// Access from public accessible server
- PUBLIC,
- /// Access from privileged server (run as root)
- PRIVILEGED,
-}
-
-impl core::ops::Index<&str> for &dyn RpcEnvironment {
- type Output = Value;
- fn index(&self, index: &str) -> &Value {
- &self.result_attrib().index(index)
- }
-}
-
-impl core::ops::Index<&str> for &mut dyn RpcEnvironment {
- type Output = Value;
- fn index(&self, index: &str) -> &Value {
- &self.result_attrib().index(index)
- }
-}
-
-impl core::ops::IndexMut<&str> for &mut dyn RpcEnvironment {
- fn index_mut(&mut self, index: &str) -> &mut Value {
- self.result_attrib_mut().index_mut(index)
- }
-}
+++ /dev/null
-//! Data types to decscribe data types.
-//!
-//! This is loosly based on JSON Schema, but uses static RUST data
-//! types. This way we can build completely static API
-//! definitions included with the programs read-only text segment.
-
-use std::fmt;
-
-use anyhow::{bail, format_err, Error};
-use serde_json::{json, Value};
-use url::form_urlencoded;
-
-use crate::api::const_regex::ConstRegexPattern;
-
-/// Error type for schema validation
-///
-/// The validation functions may produce several error message,
-/// i.e. when validation objects, it can produce one message for each
-/// erroneous object property.
-#[derive(Default, Debug)]
-pub struct ParameterError {
- error_list: Vec<(String, Error)>,
-}
-
-impl std::error::Error for ParameterError {}
-
-impl ParameterError {
- pub fn new() -> Self {
- Self {
- error_list: Vec::new(),
- }
- }
-
- pub fn push(&mut self, name: String, value: Error) {
- self.error_list.push((name, value));
- }
-
- pub fn len(&self) -> usize {
- self.error_list.len()
- }
-
- pub fn errors(&self) -> &[(String, Error)] {
- &self.error_list
- }
-
- pub fn is_empty(&self) -> bool {
- self.len() == 0
- }
-
- pub fn add_errors(&mut self, prefix: &str, err: Error) {
- if let Some(param_err) = err.downcast_ref::<ParameterError>() {
- for (sub_key, sub_err) in param_err.errors().iter() {
- self.push(format!("{}/{}", prefix, sub_key), format_err!("{}", sub_err));
- }
- } else {
- self.push(prefix.to_string(), err);
- }
- }
-
-}
-
-impl fmt::Display for ParameterError {
- fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
- let mut msg = String::new();
-
- if !self.is_empty() {
- msg.push_str("parameter verification errors\n\n");
- }
-
- for (name, err) in self.error_list.iter() {
- msg.push_str(&format!("parameter '{}': {}\n", name, err));
- }
-
- write!(f, "{}", msg)
- }
-}
-
-/// Data type to describe boolean values
-#[derive(Debug)]
-#[cfg_attr(feature = "test-harness", derive(Eq, PartialEq))]
-pub struct BooleanSchema {
- pub description: &'static str,
- /// Optional default value.
- pub default: Option<bool>,
-}
-
-impl BooleanSchema {
- pub const fn new(description: &'static str) -> Self {
- BooleanSchema {
- description,
- default: None,
- }
- }
-
- pub const fn default(mut self, default: bool) -> Self {
- self.default = Some(default);
- self
- }
-
- pub const fn schema(self) -> Schema {
- Schema::Boolean(self)
- }
-}
-
-/// Data type to describe integer values.
-#[derive(Debug)]
-#[cfg_attr(feature = "test-harness", derive(Eq, PartialEq))]
-pub struct IntegerSchema {
- pub description: &'static str,
- /// Optional minimum.
- pub minimum: Option<isize>,
- /// Optional maximum.
- pub maximum: Option<isize>,
- /// Optional default.
- pub default: Option<isize>,
-}
-
-impl IntegerSchema {
- pub const fn new(description: &'static str) -> Self {
- IntegerSchema {
- description,
- default: None,
- minimum: None,
- maximum: None,
- }
- }
-
- pub const fn default(mut self, default: isize) -> Self {
- self.default = Some(default);
- self
- }
-
- pub const fn minimum(mut self, minimum: isize) -> Self {
- self.minimum = Some(minimum);
- self
- }
-
- pub const fn maximum(mut self, maximium: isize) -> Self {
- self.maximum = Some(maximium);
- self
- }
-
- pub const fn schema(self) -> Schema {
- Schema::Integer(self)
- }
-
- fn check_constraints(&self, value: isize) -> Result<(), Error> {
- if let Some(minimum) = self.minimum {
- if value < minimum {
- bail!(
- "value must have a minimum value of {} (got {})",
- minimum,
- value
- );
- }
- }
-
- if let Some(maximum) = self.maximum {
- if value > maximum {
- bail!(
- "value must have a maximum value of {} (got {})",
- maximum,
- value
- );
- }
- }
-
- Ok(())
- }
-}
-
-/// Data type to describe (JSON like) number value
-#[derive(Debug)]
-pub struct NumberSchema {
- pub description: &'static str,
- /// Optional minimum.
- pub minimum: Option<f64>,
- /// Optional maximum.
- pub maximum: Option<f64>,
- /// Optional default.
- pub default: Option<f64>,
-}
-
-impl NumberSchema {
- pub const fn new(description: &'static str) -> Self {
- NumberSchema {
- description,
- default: None,
- minimum: None,
- maximum: None,
- }
- }
-
- pub const fn default(mut self, default: f64) -> Self {
- self.default = Some(default);
- self
- }
-
- pub const fn minimum(mut self, minimum: f64) -> Self {
- self.minimum = Some(minimum);
- self
- }
-
- pub const fn maximum(mut self, maximium: f64) -> Self {
- self.maximum = Some(maximium);
- self
- }
-
- pub const fn schema(self) -> Schema {
- Schema::Number(self)
- }
-
- fn check_constraints(&self, value: f64) -> Result<(), Error> {
- if let Some(minimum) = self.minimum {
- if value < minimum {
- bail!(
- "value must have a minimum value of {} (got {})",
- minimum,
- value
- );
- }
- }
-
- if let Some(maximum) = self.maximum {
- if value > maximum {
- bail!(
- "value must have a maximum value of {} (got {})",
- maximum,
- value
- );
- }
- }
-
- Ok(())
- }
-}
-
-#[cfg(feature = "test-harness")]
-impl Eq for NumberSchema {}
-
-#[cfg(feature = "test-harness")]
-impl PartialEq for NumberSchema {
- fn eq(&self, rhs: &Self) -> bool {
- fn f64_eq(l: Option<f64>, r: Option<f64>) -> bool {
- match (l, r) {
- (None, None) => true,
- (Some(l), Some(r)) => (l - r).abs() < 0.0001,
- _ => false,
- }
- }
-
- self.description == rhs.description
- && f64_eq(self.minimum, rhs.minimum)
- && f64_eq(self.maximum, rhs.maximum)
- && f64_eq(self.default, rhs.default)
- }
-}
-
-/// Data type to describe string values.
-#[derive(Debug)]
-#[cfg_attr(feature = "test-harness", derive(Eq, PartialEq))]
-pub struct StringSchema {
- pub description: &'static str,
- /// Optional default value.
- pub default: Option<&'static str>,
- /// Optional minimal length.
- pub min_length: Option<usize>,
- /// Optional maximal length.
- pub max_length: Option<usize>,
- /// Optional microformat.
- pub format: Option<&'static ApiStringFormat>,
- /// A text representation of the format/type (used to generate documentation).
- pub type_text: Option<&'static str>,
-}
-
-impl StringSchema {
- pub const fn new(description: &'static str) -> Self {
- StringSchema {
- description,
- default: None,
- min_length: None,
- max_length: None,
- format: None,
- type_text: None,
- }
- }
-
- pub const fn default(mut self, text: &'static str) -> Self {
- self.default = Some(text);
- self
- }
-
- pub const fn format(mut self, format: &'static ApiStringFormat) -> Self {
- self.format = Some(format);
- self
- }
-
- pub const fn type_text(mut self, type_text: &'static str) -> Self {
- self.type_text = Some(type_text);
- self
- }
-
- pub const fn min_length(mut self, min_length: usize) -> Self {
- self.min_length = Some(min_length);
- self
- }
-
- pub const fn max_length(mut self, max_length: usize) -> Self {
- self.max_length = Some(max_length);
- self
- }
-
- pub const fn schema(self) -> Schema {
- Schema::String(self)
- }
-
- fn check_length(&self, length: usize) -> Result<(), Error> {
- if let Some(min_length) = self.min_length {
- if length < min_length {
- bail!("value must be at least {} characters long", min_length);
- }
- }
-
- if let Some(max_length) = self.max_length {
- if length > max_length {
- bail!("value may only be {} characters long", max_length);
- }
- }
-
- Ok(())
- }
-
- pub fn check_constraints(&self, value: &str) -> Result<(), Error> {
- self.check_length(value.chars().count())?;
-
- if let Some(ref format) = self.format {
- match format {
- ApiStringFormat::Pattern(regex) => {
- if !(regex.regex_obj)().is_match(value) {
- bail!("value does not match the regex pattern");
- }
- }
- ApiStringFormat::Enum(variants) => {
- if variants.iter().find(|&e| e.value == value).is_none() {
- bail!("value '{}' is not defined in the enumeration.", value);
- }
- }
- ApiStringFormat::PropertyString(subschema) => {
- parse_property_string(value, subschema)?;
- }
- ApiStringFormat::VerifyFn(verify_fn) => {
- verify_fn(value)?;
- }
- }
- }
-
- Ok(())
- }
-}
-
-/// Data type to describe array of values.
-///
-/// All array elements are of the same type, as defined in the `items`
-/// schema.
-#[derive(Debug)]
-#[cfg_attr(feature = "test-harness", derive(Eq, PartialEq))]
-pub struct ArraySchema {
- pub description: &'static str,
- /// Element type schema.
- pub items: &'static Schema,
- /// Optional minimal length.
- pub min_length: Option<usize>,
- /// Optional maximal length.
- pub max_length: Option<usize>,
-}
-
-impl ArraySchema {
- pub const fn new(description: &'static str, item_schema: &'static Schema) -> Self {
- ArraySchema {
- description,
- items: item_schema,
- min_length: None,
- max_length: None,
- }
- }
-
- pub const fn min_length(mut self, min_length: usize) -> Self {
- self.min_length = Some(min_length);
- self
- }
-
- pub const fn max_length(mut self, max_length: usize) -> Self {
- self.max_length = Some(max_length);
- self
- }
-
- pub const fn schema(self) -> Schema {
- Schema::Array(self)
- }
-
- fn check_length(&self, length: usize) -> Result<(), Error> {
- if let Some(min_length) = self.min_length {
- if length < min_length {
- bail!("array must contain at least {} elements", min_length);
- }
- }
-
- if let Some(max_length) = self.max_length {
- if length > max_length {
- bail!("array may only contain {} elements", max_length);
- }
- }
-
- Ok(())
- }
-}
-
-/// Property entry in an object schema:
-///
-/// - `name`: The name of the property
-/// - `optional`: Set when the property is optional
-/// - `schema`: Property type schema
-pub type SchemaPropertyEntry = (&'static str, bool, &'static Schema);
-
-/// Lookup table to Schema properties
-///
-/// Stores a sorted list of `(name, optional, schema)` tuples:
-///
-/// - `name`: The name of the property
-/// - `optional`: Set when the property is optional
-/// - `schema`: Property type schema
-///
-/// **Note:** The list has to be storted by name, because we use
-/// a binary search to find items.
-///
-/// This is a workaround unless RUST can const_fn `Hash::new()`
-pub type SchemaPropertyMap = &'static [SchemaPropertyEntry];
-
-/// Data type to describe objects (maps).
-#[derive(Debug)]
-#[cfg_attr(feature = "test-harness", derive(Eq, PartialEq))]
-pub struct ObjectSchema {
- pub description: &'static str,
- /// If set, allow additional properties which are not defined in
- /// the schema.
- pub additional_properties: bool,
- /// Property schema definitions.
- pub properties: SchemaPropertyMap,
- /// Default key name - used by `parse_parameter_string()`
- pub default_key: Option<&'static str>,
-}
-
-impl ObjectSchema {
- pub const fn new(description: &'static str, properties: SchemaPropertyMap) -> Self {
- ObjectSchema {
- description,
- properties,
- additional_properties: false,
- default_key: None,
- }
- }
-
- pub const fn additional_properties(mut self, additional_properties: bool) -> Self {
- self.additional_properties = additional_properties;
- self
- }
-
- pub const fn default_key(mut self, key: &'static str) -> Self {
- self.default_key = Some(key);
- self
- }
-
- pub const fn schema(self) -> Schema {
- Schema::Object(self)
- }
-
- pub fn lookup(&self, key: &str) -> Option<(bool, &Schema)> {
- if let Ok(ind) = self
- .properties
- .binary_search_by_key(&key, |(name, _, _)| name)
- {
- let (_name, optional, prop_schema) = self.properties[ind];
- Some((optional, prop_schema))
- } else {
- None
- }
- }
-}
-
-/// Combines multiple *object* schemas into one.
-///
-/// Note that these are limited to object schemas. Other schemas will produce errors.
-///
-/// Technically this could also contain an `additional_properties` flag, however, in the JSON
-/// Schema, this is not supported, so here we simply assume additional properties to be allowed.
-#[derive(Debug)]
-#[cfg_attr(feature = "test-harness", derive(Eq, PartialEq))]
-pub struct AllOfSchema {
- pub description: &'static str,
-
- /// The parameter is checked against all of the schemas in the list. Note that all schemas must
- /// be object schemas.
- pub list: &'static [&'static Schema],
-}
-
-impl AllOfSchema {
- pub const fn new(description: &'static str, list: &'static [&'static Schema]) -> Self {
- Self { description, list }
- }
-
- pub const fn schema(self) -> Schema {
- Schema::AllOf(self)
- }
-
- pub fn lookup(&self, key: &str) -> Option<(bool, &Schema)> {
- for entry in self.list {
- match entry {
- Schema::AllOf(s) => {
- if let Some(v) = s.lookup(key) {
- return Some(v);
- }
- }
- Schema::Object(s) => {
- if let Some(v) = s.lookup(key) {
- return Some(v);
- }
- }
- _ => panic!("non-object-schema in `AllOfSchema`"),
- }
- }
-
- None
- }
-}
-
-/// Beside [`ObjectSchema`] we also have an [`AllOfSchema`] which also represents objects.
-pub trait ObjectSchemaType {
- fn description(&self) -> &'static str;
- fn lookup(&self, key: &str) -> Option<(bool, &Schema)>;
- fn properties(&self) -> ObjectPropertyIterator;
- fn additional_properties(&self) -> bool;
-}
-
-impl ObjectSchemaType for ObjectSchema {
- fn description(&self) -> &'static str {
- self.description
- }
-
- fn lookup(&self, key: &str) -> Option<(bool, &Schema)> {
- ObjectSchema::lookup(self, key)
- }
-
- fn properties(&self) -> ObjectPropertyIterator {
- ObjectPropertyIterator {
- schemas: [].iter(),
- properties: Some(self.properties.iter()),
- nested: None,
- }
- }
-
- fn additional_properties(&self) -> bool {
- self.additional_properties
- }
-}
-
-impl ObjectSchemaType for AllOfSchema {
- fn description(&self) -> &'static str {
- self.description
- }
-
- fn lookup(&self, key: &str) -> Option<(bool, &Schema)> {
- AllOfSchema::lookup(self, key)
- }
-
- fn properties(&self) -> ObjectPropertyIterator {
- ObjectPropertyIterator {
- schemas: self.list.iter(),
- properties: None,
- nested: None,
- }
- }
-
- fn additional_properties(&self) -> bool {
- true
- }
-}
-
-#[doc(hidden)]
-pub struct ObjectPropertyIterator {
- schemas: std::slice::Iter<'static, &'static Schema>,
- properties: Option<std::slice::Iter<'static, SchemaPropertyEntry>>,
- nested: Option<Box<ObjectPropertyIterator>>,
-}
-
-impl Iterator for ObjectPropertyIterator {
- type Item = &'static SchemaPropertyEntry;
-
- fn next(&mut self) -> Option<&'static SchemaPropertyEntry> {
- loop {
- match self.nested.as_mut().and_then(Iterator::next) {
- Some(item) => return Some(item),
- None => self.nested = None,
- }
-
- match self.properties.as_mut().and_then(Iterator::next) {
- Some(item) => return Some(item),
- None => match self.schemas.next()? {
- Schema::AllOf(o) => self.nested = Some(Box::new(o.properties())),
- Schema::Object(o) => self.properties = Some(o.properties.iter()),
- _ => {
- self.properties = None;
- continue;
- }
- },
- }
- }
- }
-}
-
-/// Schemas are used to describe complex data types.
-///
-/// All schema types implement constant builder methods, and a final
-/// `schema()` method to convert them into a `Schema`.
-///
-/// ```
-/// # use proxmox::api::{*, schema::*};
-/// #
-/// const SIMPLE_OBJECT: Schema = ObjectSchema::new(
-/// "A very simple object with 2 properties",
-/// &[ // this arrays needs to be storted by name!
-/// (
-/// "property_one",
-/// false /* required */,
-/// &IntegerSchema::new("A required integer property.")
-/// .minimum(0)
-/// .maximum(100)
-/// .schema()
-/// ),
-/// (
-/// "property_two",
-/// true /* optional */,
-/// &BooleanSchema::new("An optional boolean property.")
-/// .default(true)
-/// .schema()
-/// ),
-/// ],
-/// ).schema();
-/// ```
-#[derive(Debug)]
-#[cfg_attr(feature = "test-harness", derive(Eq, PartialEq))]
-pub enum Schema {
- Null,
- Boolean(BooleanSchema),
- Integer(IntegerSchema),
- Number(NumberSchema),
- String(StringSchema),
- Object(ObjectSchema),
- Array(ArraySchema),
- AllOf(AllOfSchema),
-}
-
-/// A string enum entry. An enum entry must have a value and a description.
-#[derive(Clone, Debug)]
-#[cfg_attr(feature = "test-harness", derive(Eq, PartialEq))]
-pub struct EnumEntry {
- pub value: &'static str,
- pub description: &'static str,
-}
-
-impl EnumEntry {
- /// Convenience method as long as we only have 2 mandatory fields in an `EnumEntry`.
- pub const fn new(value: &'static str, description: &'static str) -> Self {
- Self { value, description }
- }
-}
-
-/// String microformat definitions.
-///
-/// Strings are probably the most flexible data type, and there are
-/// several ways to define their content.
-///
-/// ## Enumerations
-///
-/// Simple list all possible values.
-///
-/// ```
-/// # use proxmox::api::{*, schema::*};
-/// const format: ApiStringFormat = ApiStringFormat::Enum(&[
-/// EnumEntry::new("vm", "A guest VM run via qemu"),
-/// EnumEntry::new("ct", "A guest container run via lxc"),
-/// ]);
-/// ```
-///
-/// ## Regular Expressions
-///
-/// Use a regular expression to describe valid strings.
-///
-/// ```
-/// # use proxmox::api::{*, schema::*};
-/// # use proxmox::const_regex;
-/// const_regex! {
-/// pub SHA256_HEX_REGEX = r"^[a-f0-9]{64}$";
-/// }
-/// const format: ApiStringFormat = ApiStringFormat::Pattern(&SHA256_HEX_REGEX);
-/// ```
-///
-/// ## Property Strings
-///
-/// Use a schema to describe complex types encoded as string.
-///
-/// Arrays are parsed as comma separated lists, i.e: `"1,2,3"`. The
-/// list may be sparated by comma, semicolon or whitespace.
-///
-/// Objects are parsed as comma (or semicolon) separated `key=value` pairs, i.e:
-/// `"prop1=2,prop2=test"`. Any whitespace is trimmed from key and value.
-///
-///
-/// **Note:** This only works for types which does not allow using the
-/// comma, semicolon or whitespace separator inside the value,
-/// i.e. this only works for arrays of simple data types, and objects
-/// with simple properties (no nesting).
-///
-/// ```
-/// # use proxmox::api::{*, schema::*};
-/// #
-/// const PRODUCT_LIST_SCHEMA: Schema =
-/// ArraySchema::new("Product List.", &IntegerSchema::new("Product ID").schema())
-/// .min_length(1)
-/// .schema();
-///
-/// const SCHEMA: Schema = StringSchema::new("A list of Product IDs, comma separated.")
-/// .format(&ApiStringFormat::PropertyString(&PRODUCT_LIST_SCHEMA))
-/// .schema();
-///
-/// let res = parse_simple_value("", &SCHEMA);
-/// assert!(res.is_err());
-///
-/// let res = parse_simple_value("1,2,3", &SCHEMA); // parse as String
-/// assert!(res.is_ok());
-///
-/// let data = parse_property_string("1,2", &PRODUCT_LIST_SCHEMA); // parse as Array
-/// assert!(data.is_ok());
-/// ```
-pub enum ApiStringFormat {
- /// Enumerate all valid strings
- Enum(&'static [EnumEntry]),
- /// Use a regular expression to describe valid strings.
- Pattern(&'static ConstRegexPattern),
- /// Use a schema to describe complex types encoded as string.
- PropertyString(&'static Schema),
- /// Use a verification function.
- VerifyFn(fn(&str) -> Result<(), Error>),
-}
-
-impl std::fmt::Debug for ApiStringFormat {
- fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
- match self {
- ApiStringFormat::VerifyFn(fnptr) => write!(f, "VerifyFn({:p}", fnptr),
- ApiStringFormat::Enum(variants) => write!(f, "Enum({:?}", variants),
- ApiStringFormat::Pattern(regex) => write!(f, "Pattern({:?}", regex),
- ApiStringFormat::PropertyString(schema) => write!(f, "PropertyString({:?}", schema),
- }
- }
-}
-
-#[cfg(feature = "test-harness")]
-impl Eq for ApiStringFormat {}
-
-#[cfg(feature = "test-harness")]
-impl PartialEq for ApiStringFormat {
- fn eq(&self, rhs: &Self) -> bool {
- match (self, rhs) {
- (ApiStringFormat::Enum(l), ApiStringFormat::Enum(r)) => l == r,
- (ApiStringFormat::Pattern(l), ApiStringFormat::Pattern(r)) => l == r,
- (ApiStringFormat::PropertyString(l), ApiStringFormat::PropertyString(r)) => l == r,
- (ApiStringFormat::VerifyFn(l), ApiStringFormat::VerifyFn(r)) => std::ptr::eq(l, r),
- (_, _) => false,
- }
- }
-}
-
-/// Parameters are objects, but we have two types of object schemas, the regular one and the
-/// `AllOf` schema.
-#[derive(Clone, Copy, Debug)]
-#[cfg_attr(feature = "test-harness", derive(Eq, PartialEq))]
-pub enum ParameterSchema {
- Object(&'static ObjectSchema),
- AllOf(&'static AllOfSchema),
-}
-
-impl ObjectSchemaType for ParameterSchema {
- fn description(&self) -> &'static str {
- match self {
- ParameterSchema::Object(o) => o.description(),
- ParameterSchema::AllOf(o) => o.description(),
- }
- }
-
- fn lookup(&self, key: &str) -> Option<(bool, &Schema)> {
- match self {
- ParameterSchema::Object(o) => o.lookup(key),
- ParameterSchema::AllOf(o) => o.lookup(key),
- }
- }
-
- fn properties(&self) -> ObjectPropertyIterator {
- match self {
- ParameterSchema::Object(o) => o.properties(),
- ParameterSchema::AllOf(o) => o.properties(),
- }
- }
-
- fn additional_properties(&self) -> bool {
- match self {
- ParameterSchema::Object(o) => o.additional_properties(),
- ParameterSchema::AllOf(o) => o.additional_properties(),
- }
- }
-}
-
-impl From<&'static ObjectSchema> for ParameterSchema {
- fn from(schema: &'static ObjectSchema) -> Self {
- ParameterSchema::Object(schema)
- }
-}
-
-impl From<&'static AllOfSchema> for ParameterSchema {
- fn from(schema: &'static AllOfSchema) -> Self {
- ParameterSchema::AllOf(schema)
- }
-}
-
-/// Helper function to parse boolean values
-///
-/// - true: `1 | on | yes | true`
-/// - false: `0 | off | no | false`
-pub fn parse_boolean(value_str: &str) -> Result<bool, Error> {
- match value_str.to_lowercase().as_str() {
- "1" | "on" | "yes" | "true" => Ok(true),
- "0" | "off" | "no" | "false" => Ok(false),
- _ => bail!("Unable to parse boolean option."),
- }
-}
-
-/// Parse a complex property string (`ApiStringFormat::PropertyString`)
-pub fn parse_property_string(value_str: &str, schema: &'static Schema) -> Result<Value, Error> {
- // helper for object/allof schemas:
- fn parse_object<T: Into<ParameterSchema>>(
- value_str: &str,
- schema: T,
- default_key: Option<&'static str>,
- ) -> Result<Value, Error> {
- let mut param_list: Vec<(String, String)> = vec![];
- let key_val_list: Vec<&str> = value_str
- .split(|c: char| c == ',' || c == ';')
- .filter(|s| !s.is_empty())
- .collect();
- for key_val in key_val_list {
- let kv: Vec<&str> = key_val.splitn(2, '=').collect();
- if kv.len() == 2 {
- param_list.push((kv[0].trim().into(), kv[1].trim().into()));
- } else if let Some(key) = default_key {
- param_list.push((key.into(), kv[0].trim().into()));
- } else {
- bail!("Value without key, but schema does not define a default key.");
- }
- }
-
- parse_parameter_strings(¶m_list, schema, true).map_err(Error::from)
- }
-
- match schema {
- Schema::Object(object_schema) => {
- parse_object(value_str, object_schema, object_schema.default_key)
- }
- Schema::AllOf(all_of_schema) => parse_object(value_str, all_of_schema, None),
- Schema::Array(array_schema) => {
- let mut array: Vec<Value> = vec![];
- let list: Vec<&str> = value_str
- .split(|c: char| c == ',' || c == ';' || char::is_ascii_whitespace(&c))
- .filter(|s| !s.is_empty())
- .collect();
-
- for value in list {
- match parse_simple_value(value.trim(), &array_schema.items) {
- Ok(res) => array.push(res),
- Err(err) => bail!("unable to parse array element: {}", err),
- }
- }
- array_schema.check_length(array.len())?;
-
- Ok(array.into())
- }
- _ => bail!("Got unexpected schema type."),
- }
-}
-
-/// Parse a simple value (no arrays and no objects)
-pub fn parse_simple_value(value_str: &str, schema: &Schema) -> Result<Value, Error> {
- let value = match schema {
- Schema::Null => {
- bail!("internal error - found Null schema.");
- }
- Schema::Boolean(_boolean_schema) => {
- let res = parse_boolean(value_str)?;
- Value::Bool(res)
- }
- Schema::Integer(integer_schema) => {
- let res: isize = value_str.parse()?;
- integer_schema.check_constraints(res)?;
- Value::Number(res.into())
- }
- Schema::Number(number_schema) => {
- let res: f64 = value_str.parse()?;
- number_schema.check_constraints(res)?;
- Value::Number(serde_json::Number::from_f64(res).unwrap())
- }
- Schema::String(string_schema) => {
- string_schema.check_constraints(value_str)?;
- Value::String(value_str.into())
- }
- _ => bail!("unable to parse complex (sub) objects."),
- };
- Ok(value)
-}
-
-/// Parse key/value pairs and verify with object schema
-///
-/// - `test_required`: is set, checks if all required properties are
-/// present.
-pub fn parse_parameter_strings<T: Into<ParameterSchema>>(
- data: &[(String, String)],
- schema: T,
- test_required: bool,
-) -> Result<Value, ParameterError> {
- do_parse_parameter_strings(data, schema.into(), test_required)
-}
-
-fn do_parse_parameter_strings(
- data: &[(String, String)],
- schema: ParameterSchema,
- test_required: bool,
-) -> Result<Value, ParameterError> {
- let mut params = json!({});
-
- let mut errors = ParameterError::new();
-
- let additional_properties = schema.additional_properties();
-
- for (key, value) in data {
- if let Some((_optional, prop_schema)) = schema.lookup(&key) {
- match prop_schema {
- Schema::Array(array_schema) => {
- if params[key] == Value::Null {
- params[key] = json!([]);
- }
- match params[key] {
- Value::Array(ref mut array) => {
- match parse_simple_value(value, &array_schema.items) {
- Ok(res) => array.push(res), // fixme: check_length??
- Err(err) => errors.push(key.into(), err),
- }
- }
- _ => errors.push(key.into(), format_err!("expected array - type missmatch")),
- }
- }
- _ => match parse_simple_value(value, prop_schema) {
- Ok(res) => {
- if params[key] == Value::Null {
- params[key] = res;
- } else {
- errors.push(key.into(), format_err!("duplicate parameter."));
- }
- }
- Err(err) => errors.push(key.into(), err),
- },
- }
- } else if additional_properties {
- match params[key] {
- Value::Null => {
- params[key] = Value::String(value.to_owned());
- }
- Value::String(ref old) => {
- params[key] = Value::Array(vec![
- Value::String(old.to_owned()),
- Value::String(value.to_owned()),
- ]);
- }
- Value::Array(ref mut array) => {
- array.push(Value::String(value.to_string()));
- }
- _ => errors.push(key.into(), format_err!("expected array - type missmatch")),
- }
- } else {
- errors.push(key.into(), format_err!("schema does not allow additional properties."));
- }
- }
-
- if test_required && errors.is_empty() {
- for (name, optional, _prop_schema) in schema.properties() {
- if !(*optional) && params[name] == Value::Null {
- errors.push(name.to_string(), format_err!("parameter is missing and it is not optional."));
- }
- }
- }
-
- if !errors.is_empty() {
- Err(errors)
- } else {
- Ok(params)
- }
-}
-
-/// Parse a `form_urlencoded` query string and verify with object schema
-/// - `test_required`: is set, checks if all required properties are
-/// present.
-pub fn parse_query_string<T: Into<ParameterSchema>>(
- query: &str,
- schema: T,
- test_required: bool,
-) -> Result<Value, ParameterError> {
- let param_list: Vec<(String, String)> = form_urlencoded::parse(query.as_bytes())
- .into_owned()
- .collect();
-
- parse_parameter_strings(¶m_list, schema.into(), test_required)
-}
-
-/// Verify JSON value with `schema`.
-pub fn verify_json(data: &Value, schema: &Schema) -> Result<(), Error> {
- match schema {
- Schema::Null => {
- if !data.is_null() {
- bail!("Expected Null, but value is not Null.");
- }
- }
- Schema::Object(object_schema) => verify_json_object(data, object_schema)?,
- Schema::Array(array_schema) => verify_json_array(data, &array_schema)?,
- Schema::Boolean(boolean_schema) => verify_json_boolean(data, &boolean_schema)?,
- Schema::Integer(integer_schema) => verify_json_integer(data, &integer_schema)?,
- Schema::Number(number_schema) => verify_json_number(data, &number_schema)?,
- Schema::String(string_schema) => verify_json_string(data, &string_schema)?,
- Schema::AllOf(all_of_schema) => verify_json_object(data, all_of_schema)?,
- }
- Ok(())
-}
-
-/// Verify JSON value using a `StringSchema`.
-pub fn verify_json_string(data: &Value, schema: &StringSchema) -> Result<(), Error> {
- if let Some(value) = data.as_str() {
- schema.check_constraints(value)
- } else {
- bail!("Expected string value.");
- }
-}
-
-/// Verify JSON value using a `BooleanSchema`.
-pub fn verify_json_boolean(data: &Value, _schema: &BooleanSchema) -> Result<(), Error> {
- if !data.is_boolean() {
- bail!("Expected boolean value.");
- }
- Ok(())
-}
-
-/// Verify JSON value using an `IntegerSchema`.
-pub fn verify_json_integer(data: &Value, schema: &IntegerSchema) -> Result<(), Error> {
- if let Some(value) = data.as_i64() {
- schema.check_constraints(value as isize)
- } else {
- bail!("Expected integer value.");
- }
-}
-
-/// Verify JSON value using an `NumberSchema`.
-pub fn verify_json_number(data: &Value, schema: &NumberSchema) -> Result<(), Error> {
- if let Some(value) = data.as_f64() {
- schema.check_constraints(value)
- } else {
- bail!("Expected number value.");
- }
-}
-
-/// Verify JSON value using an `ArraySchema`.
-pub fn verify_json_array(data: &Value, schema: &ArraySchema) -> Result<(), Error> {
- let list = match data {
- Value::Array(ref list) => list,
- Value::Object(_) => bail!("Expected array - got object."),
- _ => bail!("Expected array - got scalar value."),
- };
-
- schema.check_length(list.len())?;
-
- for (i, item) in list.iter().enumerate() {
- let result = verify_json(item, &schema.items);
- if let Err(err) = result {
- let mut errors = ParameterError::new();
- errors.add_errors(&format!("[{}]", i), err);
- return Err(errors.into());
- }
- }
-
- Ok(())
-}
-
-/// Verify JSON value using an `ObjectSchema`.
-pub fn verify_json_object(
- data: &Value,
- schema: &dyn ObjectSchemaType,
-) -> Result<(), Error> {
- let map = match data {
- Value::Object(ref map) => map,
- Value::Array(_) => bail!("Expected object - got array."),
- _ => bail!("Expected object - got scalar value."),
- };
-
- let mut errors = ParameterError::new();
-
- let additional_properties = schema.additional_properties();
-
- for (key, value) in map {
- if let Some((_optional, prop_schema)) = schema.lookup(&key) {
- let result = match prop_schema {
- Schema::Object(object_schema) => verify_json_object(value, object_schema),
- Schema::Array(array_schema) => verify_json_array(value, array_schema),
- _ => verify_json(value, prop_schema),
- };
- if let Err(err) = result {
- errors.add_errors(key, err);
- };
- } else if !additional_properties {
- errors.push(key.to_string(), format_err!("schema does not allow additional properties."));
- }
- }
-
- for (name, optional, _prop_schema) in schema.properties() {
- if !(*optional) && data[name] == Value::Null {
- errors.push(name.to_string(), format_err!("property is missing and it is not optional."));
- }
- }
-
- if !errors.is_empty() {
- Err(errors.into())
- } else {
- Ok(())
- }
-}
-
-/// API types should define an "updater type" via this trait in order to support derived "Updater"
-/// structs more easily.
-///
-/// Most trivial types can simply use an `Option<Self>` as updater. For types which do not use the
-/// `#[api]` macro, this will need to be explicitly created (or derived via
-/// `#[derive(UpdaterType)]`.
-pub trait UpdaterType: Sized {
- type Updater: Updater;
-}
-
-#[cfg(feature = "api-macro")]
-pub use proxmox_api_macro::UpdaterType;
-
-#[cfg(feature = "api-macro")]
-#[doc(hidden)]
-pub use proxmox_api_macro::Updater;
-
-macro_rules! basic_updater_type {
- ($($ty:ty)*) => {
- $(
- impl UpdaterType for $ty {
- type Updater = Option<Self>;
- }
- )*
- };
-}
-basic_updater_type! { bool u8 u16 u32 u64 i8 i16 i32 i64 usize isize f32 f64 String char }
-
-impl<T> UpdaterType for Option<T>
-where
- T: UpdaterType,
-{
- type Updater = T::Updater;
-}
-
-pub trait ApiType {
- const API_SCHEMA: Schema;
-}
-
-impl<T: ApiType> ApiType for Option<T> {
- const API_SCHEMA: Schema = T::API_SCHEMA;
-}
-
-/// A helper type for "Updater" structs. This trait is *not* implemented for an api "base" type
-/// when deriving an `Updater` for it, though the generated *updater* type does implement this
-/// trait!
-///
-/// This trait is mostly to figure out if an updater is empty (iow. it should not be applied).
-/// In that, it is useful when a type which should have an updater also has optional fields which
-/// should not be serialized. Instead of `#[serde(skip_serializing_if = "Option::is_none")]`, this
-/// trait's `is_empty` needs to be used via `#[serde(skip_serializing_if = "Updater::is_empty")]`.
-pub trait Updater {
- /// Check if the updater is "none" or "empty".
- fn is_empty(&self) -> bool;
-}
-
-impl<T> Updater for Vec<T> {
- fn is_empty(&self) -> bool {
- self.is_empty()
- }
-}
-
-impl<T> Updater for Option<T> {
- fn is_empty(&self) -> bool {
- self.is_none()
- }
-}
-
-#[cfg_attr(feature = "test-harness", derive(Eq, PartialEq))]
-pub struct ReturnType {
- /// A return type may be optional, meaning the method may return null or some fixed data.
- ///
- /// If true, the return type in pseudo openapi terms would be `"oneOf": [ "null", "T" ]`.
- pub optional: bool,
-
- /// The method's return type.
- pub schema: &'static Schema,
-}
-
-impl std::fmt::Debug for ReturnType {
- fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
- if self.optional {
- write!(f, "optional {:?}", self.schema)
- } else {
- write!(f, "{:?}", self.schema)
- }
- }
-}
-
-impl ReturnType {
- pub const fn new(optional: bool, schema: &'static Schema) -> Self {
- Self { optional, schema }
- }
-}
assert_eq!(raw, created);
}
+
+/// Generate ReST Documentaion for ``SectionConfig``
+pub fn dump_section_config(config: &SectionConfig) -> String {
+ let mut res = String::new();
+
+ let plugin_count = config.plugins().len();
+
+ for plugin in config.plugins().values() {
+ let name = plugin.type_name();
+ let properties = plugin.properties();
+ let skip = match plugin.id_property() {
+ Some(id) => vec![id],
+ None => Vec::new(),
+ };
+
+ if plugin_count > 1 {
+ let description = wrap_text("", "", properties.description(), 80);
+ res.push_str(&format!(
+ "\n**Section type** \'``{}``\': {}\n\n",
+ name, description
+ ));
+ }
+
+ res.push_str(&dump_properties(
+ properties,
+ "",
+ ParameterDisplayStyle::Config,
+ &skip,
+ ));
+ }
+
+ res
+}
+++ /dev/null
-use std::sync::atomic::{AtomicUsize, Ordering};
-
-use anyhow::{bail, Error};
-
-use crate::api::schema::{ApiStringFormat, ApiType, Schema, StringSchema};
-use crate::const_regex;
-use crate::sys::linux::procfs;
-
-/// Unique Process/Task Identifier
-///
-/// We use this to uniquely identify worker task. UPIDs have a short
-/// string repesentaion, which gives additional information about the
-/// type of the task. for example:
-/// ```text
-/// UPID:{node}:{pid}:{pstart}:{task_id}:{starttime}:{worker_type}:{worker_id}:{userid}:
-/// UPID:elsa:00004F37:0039E469:00000000:5CA78B83:garbage_collection::root@pam:
-/// ```
-/// Please note that we use tokio, so a single thread can run multiple
-/// tasks.
-// #[api] - manually implemented API type
-#[derive(Debug, Clone)]
-pub struct UPID {
- /// The Unix PID
- pub pid: libc::pid_t,
- /// The Unix process start time from `/proc/pid/stat`
- pub pstart: u64,
- /// The task start time (Epoch)
- pub starttime: i64,
- /// The task ID (inside the process/thread)
- pub task_id: usize,
- /// Worker type (arbitrary ASCII string)
- pub worker_type: String,
- /// Worker ID (arbitrary ASCII string)
- pub worker_id: Option<String>,
- /// The authenticated entity who started the task
- pub auth_id: String,
- /// The node name.
- pub node: String,
-}
-
-crate::forward_serialize_to_display!(UPID);
-crate::forward_deserialize_to_from_str!(UPID);
-
-const_regex! {
- pub PROXMOX_UPID_REGEX = concat!(
- r"^UPID:(?P<node>[a-zA-Z0-9]([a-zA-Z0-9\-]*[a-zA-Z0-9])?):(?P<pid>[0-9A-Fa-f]{8}):",
- r"(?P<pstart>[0-9A-Fa-f]{8,9}):(?P<task_id>[0-9A-Fa-f]{8,16}):(?P<starttime>[0-9A-Fa-f]{8}):",
- r"(?P<wtype>[^:\s]+):(?P<wid>[^:\s]*):(?P<authid>[^:\s]+):$"
- );
-}
-
-pub const PROXMOX_UPID_FORMAT: ApiStringFormat =
- ApiStringFormat::Pattern(&PROXMOX_UPID_REGEX);
-
-pub const UPID_SCHEMA: Schema = StringSchema::new("Unique Process/Task Identifier")
- .min_length("UPID:N:12345678:12345678:12345678:::".len())
- .format(&PROXMOX_UPID_FORMAT)
- .schema();
-
-impl ApiType for UPID {
- const API_SCHEMA: Schema = UPID_SCHEMA;
-}
-
-impl UPID {
- /// Create a new UPID
- pub fn new(
- worker_type: &str,
- worker_id: Option<String>,
- auth_id: String,
- ) -> Result<Self, Error> {
-
- let pid = unsafe { libc::getpid() };
-
- let bad: &[_] = &['/', ':', ' '];
-
- if worker_type.contains(bad) {
- bail!("illegal characters in worker type '{}'", worker_type);
- }
-
- if auth_id.contains(bad) {
- bail!("illegal characters in auth_id '{}'", auth_id);
- }
-
- static WORKER_TASK_NEXT_ID: AtomicUsize = AtomicUsize::new(0);
-
- let task_id = WORKER_TASK_NEXT_ID.fetch_add(1, Ordering::SeqCst);
-
- Ok(UPID {
- pid,
- pstart: procfs::PidStat::read_from_pid(nix::unistd::Pid::from_raw(pid))?.starttime,
- starttime: crate::tools::time::epoch_i64(),
- task_id,
- worker_type: worker_type.to_owned(),
- worker_id,
- auth_id,
- node: crate::tools::nodename().to_owned(),
- })
- }
-}
-
-
-impl std::str::FromStr for UPID {
- type Err = Error;
-
- fn from_str(s: &str) -> Result<Self, Self::Err> {
- if let Some(cap) = PROXMOX_UPID_REGEX.captures(s) {
-
- let worker_id = if cap["wid"].is_empty() {
- None
- } else {
- let wid = crate::tools::systemd::unescape_unit(&cap["wid"])?;
- Some(wid)
- };
-
- Ok(UPID {
- pid: i32::from_str_radix(&cap["pid"], 16).unwrap(),
- pstart: u64::from_str_radix(&cap["pstart"], 16).unwrap(),
- starttime: i64::from_str_radix(&cap["starttime"], 16).unwrap(),
- task_id: usize::from_str_radix(&cap["task_id"], 16).unwrap(),
- worker_type: cap["wtype"].to_string(),
- worker_id,
- auth_id: cap["authid"].to_string(),
- node: cap["node"].to_string(),
- })
- } else {
- bail!("unable to parse UPID '{}'", s);
- }
-
- }
-}
-
-impl std::fmt::Display for UPID {
-
- fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
-
- let wid = if let Some(ref id) = self.worker_id {
- crate::tools::systemd::escape_unit(id, false)
- } else {
- String::new()
- };
-
- // Note: pstart can be > 32bit if uptime > 497 days, so this can result in
- // more that 8 characters for pstart
-
- write!(f, "UPID:{}:{:08X}:{:08X}:{:08X}:{:08X}:{}:{}:{}:",
- self.node, self.pid, self.pstart, self.task_id, self.starttime, self.worker_type, wid, self.auth_id)
- }
-}
#[macro_use]
pub mod serde_macros;
-pub mod api;
pub mod sys;
pub mod tools;
$crate::c_result!($expr)?
}};
}
-
-/// Shortcut for generating an `&'static CStr`.
-///
-/// This takes a *string* (*not* a *byte-string*), appends a terminating zero, and calls
-/// `CStr::from_bytes_with_nul_unchecked`.
-///
-/// Shortcut for:
-/// ```no_run
-/// let bytes = concat!("THE TEXT", "\0");
-/// unsafe { ::std::ffi::CStr::from_bytes_with_nul_unchecked(bytes.as_bytes()) }
-/// # ;
-/// ```
-#[macro_export]
-macro_rules! c_str {
- ($data:expr) => {{
- let bytes = concat!($data, "\0");
- unsafe { ::std::ffi::CStr::from_bytes_with_nul_unchecked(bytes.as_bytes()) }
- }};
-}
+++ /dev/null
-use anyhow::bail;
-
-use crate::api::schema::*;
-
-#[test]
-fn test_schema1() {
- let schema = Schema::Object(ObjectSchema {
- description: "TEST",
- additional_properties: false,
- properties: &[],
- default_key: None,
- });
-
- println!("TEST Schema: {:?}", schema);
-}
-
-#[test]
-fn test_query_string() {
- {
- const SCHEMA: ObjectSchema = ObjectSchema::new(
- "Parameters.",
- &[("name", false, &StringSchema::new("Name.").schema())],
- );
-
- let res = parse_query_string("", &SCHEMA, true);
- assert!(res.is_err());
- }
-
- {
- const SCHEMA: ObjectSchema = ObjectSchema::new(
- "Parameters.",
- &[("name", true, &StringSchema::new("Name.").schema())],
- );
-
- let res = parse_query_string("", &SCHEMA, true);
- assert!(res.is_ok());
- }
-
- // TEST min_length and max_length
- {
- const SCHEMA: ObjectSchema = ObjectSchema::new(
- "Parameters.",
- &[(
- "name",
- true,
- &StringSchema::new("Name.")
- .min_length(5)
- .max_length(10)
- .schema(),
- )],
- );
-
- let res = parse_query_string("name=abcd", &SCHEMA, true);
- assert!(res.is_err());
-
- let res = parse_query_string("name=abcde", &SCHEMA, true);
- assert!(res.is_ok());
-
- let res = parse_query_string("name=abcdefghijk", &SCHEMA, true);
- assert!(res.is_err());
-
- let res = parse_query_string("name=abcdefghij", &SCHEMA, true);
- assert!(res.is_ok());
- }
-
- // TEST regex pattern
- crate::const_regex! {
- TEST_REGEX = "test";
- TEST2_REGEX = "^test$";
- }
-
- {
- const SCHEMA: ObjectSchema = ObjectSchema::new(
- "Parameters.",
- &[(
- "name",
- false,
- &StringSchema::new("Name.")
- .format(&ApiStringFormat::Pattern(&TEST_REGEX))
- .schema(),
- )],
- );
-
- let res = parse_query_string("name=abcd", &SCHEMA, true);
- assert!(res.is_err());
-
- let res = parse_query_string("name=ateststring", &SCHEMA, true);
- assert!(res.is_ok());
- }
-
- {
- const SCHEMA: ObjectSchema = ObjectSchema::new(
- "Parameters.",
- &[(
- "name",
- false,
- &StringSchema::new("Name.")
- .format(&ApiStringFormat::Pattern(&TEST2_REGEX))
- .schema(),
- )],
- );
-
- let res = parse_query_string("name=ateststring", &SCHEMA, true);
- assert!(res.is_err());
-
- let res = parse_query_string("name=test", &SCHEMA, true);
- assert!(res.is_ok());
- }
-
- // TEST string enums
- {
- const SCHEMA: ObjectSchema = ObjectSchema::new(
- "Parameters.",
- &[(
- "name",
- false,
- &StringSchema::new("Name.")
- .format(&ApiStringFormat::Enum(&[
- EnumEntry::new("ev1", "desc ev1"),
- EnumEntry::new("ev2", "desc ev2"),
- ]))
- .schema(),
- )],
- );
-
- let res = parse_query_string("name=noenum", &SCHEMA, true);
- assert!(res.is_err());
-
- let res = parse_query_string("name=ev1", &SCHEMA, true);
- assert!(res.is_ok());
-
- let res = parse_query_string("name=ev2", &SCHEMA, true);
- assert!(res.is_ok());
-
- let res = parse_query_string("name=ev3", &SCHEMA, true);
- assert!(res.is_err());
- }
-}
-
-#[test]
-fn test_query_integer() {
- {
- const SCHEMA: ObjectSchema = ObjectSchema::new(
- "Parameters.",
- &[("count", false, &IntegerSchema::new("Count.").schema())],
- );
-
- let res = parse_query_string("", &SCHEMA, true);
- assert!(res.is_err());
- }
-
- {
- const SCHEMA: ObjectSchema = ObjectSchema::new(
- "Parameters.",
- &[(
- "count",
- true,
- &IntegerSchema::new("Count.")
- .minimum(-3)
- .maximum(50)
- .schema(),
- )],
- );
-
- let res = parse_query_string("", &SCHEMA, true);
- assert!(res.is_ok());
-
- let res = parse_query_string("count=abc", &SCHEMA, false);
- assert!(res.is_err());
-
- let res = parse_query_string("count=30", &SCHEMA, false);
- assert!(res.is_ok());
-
- let res = parse_query_string("count=-1", &SCHEMA, false);
- assert!(res.is_ok());
-
- let res = parse_query_string("count=300", &SCHEMA, false);
- assert!(res.is_err());
-
- let res = parse_query_string("count=-30", &SCHEMA, false);
- assert!(res.is_err());
-
- let res = parse_query_string("count=50", &SCHEMA, false);
- assert!(res.is_ok());
-
- let res = parse_query_string("count=-3", &SCHEMA, false);
- assert!(res.is_ok());
- }
-}
-
-#[test]
-fn test_query_boolean() {
- {
- const SCHEMA: ObjectSchema = ObjectSchema::new(
- "Parameters.",
- &[("force", false, &BooleanSchema::new("Force.").schema())],
- );
-
- let res = parse_query_string("", &SCHEMA, true);
- assert!(res.is_err());
- }
-
- {
- const SCHEMA: ObjectSchema = ObjectSchema::new(
- "Parameters.",
- &[("force", true, &BooleanSchema::new("Force.").schema())],
- );
-
- let res = parse_query_string("", &SCHEMA, true);
- assert!(res.is_ok());
-
- let res = parse_query_string("a=b", &SCHEMA, true);
- assert!(res.is_err());
-
- let res = parse_query_string("force", &SCHEMA, true);
- assert!(res.is_err());
-
- let res = parse_query_string("force=yes", &SCHEMA, true);
- assert!(res.is_ok());
- let res = parse_query_string("force=1", &SCHEMA, true);
- assert!(res.is_ok());
- let res = parse_query_string("force=On", &SCHEMA, true);
- assert!(res.is_ok());
- let res = parse_query_string("force=TRUE", &SCHEMA, true);
- assert!(res.is_ok());
- let res = parse_query_string("force=TREU", &SCHEMA, true);
- assert!(res.is_err());
-
- let res = parse_query_string("force=NO", &SCHEMA, true);
- assert!(res.is_ok());
- let res = parse_query_string("force=0", &SCHEMA, true);
- assert!(res.is_ok());
- let res = parse_query_string("force=off", &SCHEMA, true);
- assert!(res.is_ok());
- let res = parse_query_string("force=False", &SCHEMA, true);
- assert!(res.is_ok());
- }
-}
-
-#[test]
-fn test_verify_function() {
- const SCHEMA: ObjectSchema = ObjectSchema::new(
- "Parameters.",
- &[(
- "p1",
- false,
- &StringSchema::new("P1")
- .format(&ApiStringFormat::VerifyFn(|value| {
- if value == "test" {
- return Ok(());
- };
- bail!("format error");
- }))
- .schema(),
- )],
- );
-
- let res = parse_query_string("p1=tes", &SCHEMA, true);
- assert!(res.is_err());
- let res = parse_query_string("p1=test", &SCHEMA, true);
- assert!(res.is_ok());
-}
-
-#[test]
-fn test_verify_complex_object() {
- const NIC_MODELS: ApiStringFormat = ApiStringFormat::Enum(&[
- EnumEntry::new("e1000", "Intel E1000"),
- EnumEntry::new("virtio", "Paravirtualized ethernet device"),
- ]);
-
- const PARAM_SCHEMA: Schema = ObjectSchema::new(
- "Properties.",
- &[
- (
- "enable",
- true,
- &BooleanSchema::new("Enable device.").schema(),
- ),
- (
- "model",
- false,
- &StringSchema::new("Ethernet device Model.")
- .format(&NIC_MODELS)
- .schema(),
- ),
- ],
- )
- .default_key("model")
- .schema();
-
- const SCHEMA: ObjectSchema = ObjectSchema::new(
- "Parameters.",
- &[(
- "net0",
- false,
- &StringSchema::new("First Network device.")
- .format(&ApiStringFormat::PropertyString(&PARAM_SCHEMA))
- .schema(),
- )],
- );
-
- let res = parse_query_string("", &SCHEMA, true);
- assert!(res.is_err());
-
- let res = parse_query_string("test=abc", &SCHEMA, true);
- assert!(res.is_err());
-
- let res = parse_query_string("net0=model=abc", &SCHEMA, true);
- assert!(res.is_err());
-
- let res = parse_query_string("net0=model=virtio", &SCHEMA, true);
- assert!(res.is_ok());
-
- let res = parse_query_string("net0=model=virtio,enable=1", &SCHEMA, true);
- assert!(res.is_ok());
-
- let res = parse_query_string("net0=virtio,enable=no", &SCHEMA, true);
- assert!(res.is_ok());
-}
-
-#[test]
-fn test_verify_complex_array() {
- {
- const PARAM_SCHEMA: Schema =
- ArraySchema::new("Integer List.", &IntegerSchema::new("Soemething").schema()).schema();
-
- const SCHEMA: ObjectSchema = ObjectSchema::new(
- "Parameters.",
- &[(
- "list",
- false,
- &StringSchema::new("A list on integers, comma separated.")
- .format(&ApiStringFormat::PropertyString(&PARAM_SCHEMA))
- .schema(),
- )],
- );
-
- let res = parse_query_string("", &SCHEMA, true);
- assert!(res.is_err());
-
- let res = parse_query_string("list=", &SCHEMA, true);
- assert!(res.is_ok());
-
- let res = parse_query_string("list=abc", &SCHEMA, true);
- assert!(res.is_err());
-
- let res = parse_query_string("list=1", &SCHEMA, true);
- assert!(res.is_ok());
-
- let res = parse_query_string("list=2,3,4,5", &SCHEMA, true);
- assert!(res.is_ok());
- }
-
- {
- const PARAM_SCHEMA: Schema =
- ArraySchema::new("Integer List.", &IntegerSchema::new("Soemething").schema())
- .min_length(1)
- .max_length(3)
- .schema();
-
- const SCHEMA: ObjectSchema = ObjectSchema::new(
- "Parameters.",
- &[(
- "list",
- false,
- &StringSchema::new("A list on integers, comma separated.")
- .format(&ApiStringFormat::PropertyString(&PARAM_SCHEMA))
- .schema(),
- )],
- );
-
- let res = parse_query_string("list=", &SCHEMA, true);
- assert!(res.is_err());
-
- let res = parse_query_string("list=1,2,3", &SCHEMA, true);
- assert!(res.is_ok());
-
- let res = parse_query_string("list=2,3,4,5", &SCHEMA, true);
- assert!(res.is_err());
- }
-}
+++ /dev/null
-use anyhow::{bail, Error};
-use serde_json::{json, Value};
-
-use crate::api::schema::*;
-
-static STRING_SCHEMA: Schema = StringSchema::new("A test string").schema();
-
-static SIMPLE_OBJECT_SCHEMA: Schema = ObjectSchema::new(
- "simple object schema",
- &[
- ("prop1", false, &STRING_SCHEMA),
- ("prop2", true, &STRING_SCHEMA),
- ("prop3", false, &STRING_SCHEMA),
- ]
-).schema();
-
-static SIMPLE_PROPERTY_STRING_SCHEMA: Schema = StringSchema::new("simple property string")
- .format(&ApiStringFormat::PropertyString(&SIMPLE_OBJECT_SCHEMA))
- .schema();
-
-static SIMPLE_ARRAY_SCHEMA: Schema = ArraySchema::new("String list.", &STRING_SCHEMA).schema();
-
-static NESTED_OBJECT_SCHEMA: Schema = ObjectSchema::new(
- "nested object schema",
- &[
- ("arr1", false, &SIMPLE_ARRAY_SCHEMA),
- ("obj1", false, &SIMPLE_OBJECT_SCHEMA),
- ("prop1", false, &STRING_SCHEMA),
- ]
-).schema();
-
-static NESTED_PROPERTY_SCHEMA: Schema = ObjectSchema::new(
- "object with property strings",
- &[
- ("ps1", false, &SIMPLE_PROPERTY_STRING_SCHEMA),
- ]
-).schema();
-
-
-fn compare_error(
- expected: &[(&str, &str)],
- err: Error,
-) -> Result<(), Error> {
- let err = match err.downcast_ref::<ParameterError>() {
- Some(err) => err,
- None => bail!("unable to downcast error: {}", err),
- };
-
- let result = crate::try_block!({
- let errors = err.errors();
-
- if errors.len() != expected.len() {
- bail!("error list has different length: {} != {}", expected.len(), errors.len());
- }
-
- for i in 0..expected.len() {
- if expected[i].0 != errors[i].0 {
- bail!("error {} path differs: '{}' != '{}'", i, expected[i].0, errors[i].0);
- }
- if expected[i].1 != errors[i].1.to_string() {
- bail!("error {} message differs: '{}' != '{}'", i, expected[i].1, errors[i].1);
- }
-
- }
-
- Ok(())
- });
-
- if result.is_err() {
- println!("GOT: {:?}", err);
- }
-
- result
-}
-
-fn test_verify(
- schema: &Schema,
- data: &Value,
- expected_errors: &[(&str, &str)],
-) -> Result<(), Error> {
- match verify_json(data, schema) {
- Ok(_) => bail!("expected errors, but got Ok()"),
- Err(err) => compare_error(expected_errors, err)?,
- }
- Ok(())
-}
-
-#[test]
-fn verify_simple_object() -> Result<(), Error> {
-
- let simple_value = json!({"prop1": 1, "prop4": "abc"});
-
- test_verify(
- &SIMPLE_OBJECT_SCHEMA,
- &simple_value,
- &[
- ("prop1", "Expected string value."),
- ("prop4", "schema does not allow additional properties."),
- ("prop3", "property is missing and it is not optional."),
- ],
- )?;
-
- Ok(())
-}
-
-#[test]
-fn verify_nested_object1() -> Result<(), Error> {
-
- let nested_value = json!({"prop1": 1, "prop4": "abc"});
-
- test_verify(
- &NESTED_OBJECT_SCHEMA,
- &nested_value,
- &[
- ("prop1", "Expected string value."),
- ("prop4", "schema does not allow additional properties."),
- ("arr1", "property is missing and it is not optional."),
- ("obj1", "property is missing and it is not optional."),
- ],
- )?;
-
- Ok(())
-}
-
-#[test]
-fn verify_nested_object2() -> Result<(), Error> {
-
- let nested_value = json!({"prop1": 1, "prop4": "abc", "obj1": {}, "arr1": ["abc", 0]});
-
- test_verify(
- &NESTED_OBJECT_SCHEMA,
- &nested_value,
- &[
- ("arr1/[1]", "Expected string value."),
- ("obj1/prop1", "property is missing and it is not optional."),
- ("obj1/prop3", "property is missing and it is not optional."),
- ("prop1", "Expected string value."),
- ("prop4", "schema does not allow additional properties."),
- ],
- )?;
-
- Ok(())
-}
-
-#[test]
-fn verify_nested_property1() -> Result<(), Error> {
-
- let value = json!({"ps1": "abc"});
-
- test_verify(
- &NESTED_PROPERTY_SCHEMA,
- &value,
- &[
- ("ps1", "Value without key, but schema does not define a default key."),
- ],
- )?;
-
- Ok(())
-}
-
-#[test]
-fn verify_nested_property2() -> Result<(), Error> {
- let value = json!({"ps1": "abc=1"});
-
- test_verify(
- &NESTED_PROPERTY_SCHEMA,
- &value,
- &[
- ("ps1/abc", "schema does not allow additional properties."),
- ],
- )?;
-
- Ok(())
-}
-
-#[test]
-fn verify_nested_property3() -> Result<(), Error> {
- let value = json!({"ps1": ""});
-
- test_verify(
- &NESTED_PROPERTY_SCHEMA,
- &value,
- &[
- ("ps1/prop1", "parameter is missing and it is not optional."),
- ("ps1/prop3", "parameter is missing and it is not optional."),
- ],
- )?;
-
- Ok(())
-}
+++ /dev/null
-use std::any::Any;
-
-/// An easy way to convert types to Any
-///
-/// Mostly useful to downcast trait objects (see RpcEnvironment).
-pub trait AsAny {
- fn as_any(&self) -> &dyn Any;
-}
-
-impl<T: Any> AsAny for T {
- fn as_any(&self) -> &dyn Any {
- self
- }
-}
use anyhow::*;
use lazy_static::lazy_static;
-pub mod as_any;
pub mod common_regex;
pub mod email;
pub mod fd;
pub mod serde;
pub mod systemd;
-#[doc(inline)]
-pub use as_any::AsAny;
-
const HEX_CHARS: &[u8; 16] = b"0123456789abcdef";
/// Helper to provide a `Display` for arbitrary byte slices.