]> git.proxmox.com Git - proxmox.git/commitdiff
add proxmox-schema and proxmox-router crates
authorWolfgang Bumiller <w.bumiller@proxmox.com>
Thu, 7 Oct 2021 07:36:06 +0000 (09:36 +0200)
committerWolfgang Bumiller <w.bumiller@proxmox.com>
Mon, 11 Oct 2021 09:39:59 +0000 (11:39 +0200)
Signed-off-by: Wolfgang Bumiller <w.bumiller@proxmox.com>
82 files changed:
Cargo.toml
Makefile
proxmox-api-macro/Cargo.toml
proxmox-api-macro/debian/changelog
proxmox-api-macro/debian/control
proxmox-api-macro/src/api/enums.rs
proxmox-api-macro/src/api/method.rs
proxmox-api-macro/src/api/mod.rs
proxmox-api-macro/src/api/structs.rs
proxmox-api-macro/src/lib.rs
proxmox-api-macro/src/updater.rs
proxmox-api-macro/tests/allof.rs
proxmox-api-macro/tests/api1.rs
proxmox-api-macro/tests/api2.rs
proxmox-api-macro/tests/ext-schema.rs
proxmox-api-macro/tests/int-limits.rs
proxmox-api-macro/tests/options.rs
proxmox-api-macro/tests/types.rs
proxmox-api-macro/tests/updater.rs
proxmox-lang/src/lib.rs
proxmox-router/Cargo.toml [new file with mode: 0644]
proxmox-router/debian/changelog [new file with mode: 0644]
proxmox-router/debian/control [new file with mode: 0644]
proxmox-router/debian/copyright [new file with mode: 0644]
proxmox-router/debian/debcargo.toml [new file with mode: 0644]
proxmox-router/src/cli/command.rs [new file with mode: 0644]
proxmox-router/src/cli/completion.rs [new file with mode: 0644]
proxmox-router/src/cli/environment.rs [new file with mode: 0644]
proxmox-router/src/cli/format.rs [new file with mode: 0644]
proxmox-router/src/cli/getopts.rs [new file with mode: 0644]
proxmox-router/src/cli/mod.rs [new file with mode: 0644]
proxmox-router/src/cli/readline.rs [new file with mode: 0644]
proxmox-router/src/cli/shellword.rs [new file with mode: 0644]
proxmox-router/src/cli/text_table.rs [new file with mode: 0644]
proxmox-router/src/error.rs [new file with mode: 0644]
proxmox-router/src/format.rs [new file with mode: 0644]
proxmox-router/src/lib.rs [new file with mode: 0644]
proxmox-router/src/permission.rs [new file with mode: 0644]
proxmox-router/src/router.rs [new file with mode: 0644]
proxmox-router/src/rpc_environment.rs [new file with mode: 0644]
proxmox-schema/Cargo.toml [new file with mode: 0644]
proxmox-schema/debian/changelog [new file with mode: 0644]
proxmox-schema/debian/control [new file with mode: 0644]
proxmox-schema/debian/copyright [new file with mode: 0644]
proxmox-schema/debian/debcargo.toml [new file with mode: 0644]
proxmox-schema/src/api_type_macros.rs [new file with mode: 0644]
proxmox-schema/src/const_regex.rs [new file with mode: 0644]
proxmox-schema/src/de.rs [new file with mode: 0644]
proxmox-schema/src/format.rs [new file with mode: 0644]
proxmox-schema/src/lib.rs [new file with mode: 0644]
proxmox-schema/src/schema.rs [new file with mode: 0644]
proxmox-schema/src/upid.rs [new file with mode: 0644]
proxmox-schema/tests/schema.rs [new file with mode: 0644]
proxmox-schema/tests/schema_verification.rs [new file with mode: 0644]
proxmox/Cargo.toml
proxmox/src/api/api_type_macros.rs [deleted file]
proxmox/src/api/cli/command.rs [deleted file]
proxmox/src/api/cli/completion.rs [deleted file]
proxmox/src/api/cli/environment.rs [deleted file]
proxmox/src/api/cli/format.rs [deleted file]
proxmox/src/api/cli/getopts.rs [deleted file]
proxmox/src/api/cli/mod.rs [deleted file]
proxmox/src/api/cli/readline.rs [deleted file]
proxmox/src/api/cli/shellword.rs [deleted file]
proxmox/src/api/cli/text_table.rs [deleted file]
proxmox/src/api/const_regex.rs [deleted file]
proxmox/src/api/de.rs [deleted file]
proxmox/src/api/error.rs [deleted file]
proxmox/src/api/format.rs [deleted file]
proxmox/src/api/mod.rs [deleted file]
proxmox/src/api/permission.rs [deleted file]
proxmox/src/api/router.rs [deleted file]
proxmox/src/api/rpc_environment.rs [deleted file]
proxmox/src/api/schema.rs [deleted file]
proxmox/src/api/section_config.rs
proxmox/src/api/upid.rs [deleted file]
proxmox/src/lib.rs
proxmox/src/sys/macros.rs
proxmox/src/test/schema.rs [deleted file]
proxmox/src/test/schema_verification.rs [deleted file]
proxmox/src/tools/as_any.rs [deleted file]
proxmox/src/tools/mod.rs

index e8666910b0b1f2838d7a8673f6e3c497ab0f6bd0..7048864a53e2df1d38667123479447528c0dc104 100644 (file)
@@ -6,6 +6,8 @@ members = [
     "proxmox-http",
     "proxmox-io",
     "proxmox-lang",
+    "proxmox-router",
+    "proxmox-schema",
     "proxmox-sortable-macro",
     "proxmox-tfa",
     "proxmox-time",
index a29553ddc57c832a41dffbdad862837925c57e63..ddf5dc1ed2dbb2ee0d693c1e1b5329b5f59d920b 100644 (file)
--- a/Makefile
+++ b/Makefile
@@ -7,6 +7,8 @@ CRATES = \
         proxmox-http \
         proxmox-io \
         proxmox-lang \
+        proxmox-router \
+        proxmox-schema \
         proxmox-sortable-macro \
         proxmox-tfa \
         proxmox-time \
index c9786e61cc87ced24b328cd32aee13b56acfe51b..cba7d1c51531ae2eaefc12e91d6a4fa564f75197 100644 (file)
@@ -1,7 +1,7 @@
 [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"
@@ -19,11 +19,19 @@ syn = { version = "1.0", features = [ "extra-traits", "full", "visit-mut" ] }
 
 [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)!
index edcd62eb111a3d5343ed61b06d23a3cc5fcd0e6a..c0b2ddab32e5f0c145dad8fb92ecee208f958c59 100644 (file)
@@ -1,3 +1,9 @@
+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
index e70edce7eddc1c256255ec28b7406138eca0135a..5e0979e539993708e1caa1dbebfeae93155d658a 100644 (file)
@@ -33,12 +33,12 @@ Depends:
  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.
index 3c35bc095482723d07589a523ae66a7d71f4be2e..2797a641511cf327e160d1d2824c8e5b09fcc845 100644 (file)
@@ -65,7 +65,7 @@ pub fn handle_enum(
         };
 
         variants.extend(quote_spanned! { variant.ident.span() =>
-            ::proxmox::api::schema::EnumEntry {
+            ::proxmox_schema::EnumEntry {
                 value: #variant_string,
                 description: #comment,
             },
@@ -78,14 +78,14 @@ pub fn handle_enum(
         #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>;
         }
     })
index edaff0ecbf321e57f8e70ad287bb98aa6b6ae7ac..927030f3a2c3a500cd53121f4d99fcd92161fd36 100644 (file)
@@ -77,7 +77,7 @@ impl ReturnSchema {
         self.schema.to_schema(&mut out)?;
 
         ts.extend(quote! {
-            ::proxmox::api::router::ReturnType::new( #optional , &#out )
+            ::proxmox_schema::ReturnType::new( #optional , &#out )
         });
         Ok(())
     }
@@ -218,16 +218,16 @@ pub fn handle_method(mut attribs: JSONObject, mut func: syn::ItemFn) -> Result<T
     }
 
     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,
             )
@@ -525,13 +525,13 @@ fn create_wrapper_function(
         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
                 //}
@@ -545,8 +545,8 @@ fn create_wrapper_function(
         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
             }
@@ -650,7 +650,7 @@ fn extract_normal_parameter(
                 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,
                         )
@@ -703,10 +703,10 @@ fn serialize_input_schema(
         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)
             },
         ));
     }
@@ -758,7 +758,7 @@ fn serialize_input_schema(
 
         (
             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,),
         )
@@ -771,8 +771,8 @@ fn serialize_input_schema(
         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
@@ -781,7 +781,7 @@ fn serialize_input_schema(
                 );
         },
         quote_spanned! { func_sig_span =>
-            ::proxmox::api::schema::ParameterSchema::AllOf(&#input_schema_name)
+            ::proxmox_schema::ParameterSchema::AllOf(&#input_schema_name)
         },
     ))
 }
index 138b82b530a3c19b7aba74bee319f3a2c6c3e220..18ffdac3905791a0f5782b490f140725e750468d 100644 (file)
@@ -148,7 +148,7 @@ impl Schema {
     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,
@@ -323,31 +323,31 @@ impl SchemaItem {
             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) => {
@@ -355,7 +355,7 @@ impl SchemaItem {
                 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) => {
@@ -363,7 +363,7 @@ impl SchemaItem {
                 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) => {
@@ -375,7 +375,7 @@ impl SchemaItem {
                     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) => {
index 4bc6b75902df0f72f27f5cf76bdc722c82509b2b..49ebf3ada0c9104b906e3a542a6aef93072b99d1 100644 (file)
@@ -62,7 +62,7 @@ fn handle_unit_struct(attribs: JSONObject, stru: syn::ItemStruct) -> Result<Toke
     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>;
         }
     });
@@ -85,8 +85,8 @@ fn finish_schema(
         #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;
         }
     })
 }
@@ -337,7 +337,7 @@ fn finish_all_of_struct(
 
         (
             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,),
         )
@@ -354,9 +354,9 @@ fn finish_all_of_struct(
         }
 
         #[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
@@ -444,7 +444,7 @@ fn derive_updater(
     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
                 }
@@ -453,7 +453,7 @@ fn derive_updater(
     }
 
     output.extend(quote::quote!(
-        impl ::proxmox::api::schema::UpdaterType for #original_name {
+        impl ::proxmox_schema::UpdaterType for #original_name {
             type Updater = #updater_name;
         }
     ));
@@ -505,15 +505,11 @@ fn handle_updater_field(
         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
@@ -530,7 +526,7 @@ fn handle_updater_field(
     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() {
index 9f562627b033ef5add05520cac8c53c12815d9c4..e868251760e023b5ceb8e60d7320ec52198b8e03 100644 (file)
@@ -69,7 +69,7 @@ fn router_do(item: TokenStream) -> Result<TokenStream, Error> {
 
     ```
     # use proxmox_api_macro::api;
-    # use proxmox::api::{ApiMethod, RpcEnvironment};
+    # use proxmox_router::{ApiMethod, RpcEnvironment};
 
     use anyhow::Error;
     use serde_json::Value;
@@ -178,19 +178,19 @@ fn router_do(item: TokenStream) -> Result<TokenStream, Error> {
     ```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(),
index 85c671d39f80155fb2a4496e6f5d704af3379c2d..4399e971bcf27ac93a405137fba5540d56fa4986 100644 (file)
@@ -28,7 +28,7 @@ fn derive_updater_type(full_span: Span, ident: Ident, generics: syn::Generics) -
     no_generics(generics);
 
     quote_spanned! { full_span =>
-        impl ::proxmox::api::schema::UpdaterType for #ident {
+        impl ::proxmox_schema::UpdaterType for #ident {
             type Updater = Option<Self>;
         }
     }
index f14f6a1e040d9984b92b741e87f19652f3978850..57089d2657903877a168975708f7e06c5373b9a8 100644 (file)
@@ -4,8 +4,9 @@ use anyhow::Error;
 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();
@@ -56,17 +57,16 @@ pub struct Nvit {
 
 #[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],
     )
@@ -96,17 +96,17 @@ struct WithExtra {
 
 #[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,
@@ -134,9 +134,9 @@ pub fn hello(it: IndexText, nv: NameValue) -> Result<(NameValue, IndexText), Err
 
 #[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],
         )),
@@ -164,19 +164,19 @@ pub fn with_extra(
 
 #[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,
@@ -189,7 +189,7 @@ fn with_extra_schema_check() {
 }
 
 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");
     }
@@ -199,7 +199,7 @@ impl proxmox::api::RpcEnvironment for RpcEnv {
     }
 
     /// The environment type
-    fn env_type(&self) -> proxmox::api::RpcEnvironmentType {
+    fn env_type(&self) -> proxmox_router::RpcEnvironmentType {
         panic!("env_type called");
     }
 
index 88adb40a65982665d2a26d5b1a2ff415618b8104..88eb74acaa869e03a9eeb2f77042b456cd3af21a 100644 (file)
@@ -3,7 +3,7 @@ use proxmox_api_macro::api;
 use anyhow::Error;
 use serde_json::{json, Value};
 
-use proxmox::api::Permission;
+use proxmox_router::Permission;
 
 #[api(
     input: {
@@ -59,15 +59,15 @@ pub fn create_ticket(param: Value) -> Result<Value, Error> {
 
 #[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(),
@@ -75,22 +75,22 @@ fn create_ticket_schema_check() {
                 (
                     "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(),
@@ -98,12 +98,12 @@ fn create_ticket_schema_check() {
                 (
                     "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(),
                 ),
             ],
         )
@@ -162,15 +162,15 @@ pub fn create_ticket_direct(username: String, password: String) -> Result<&'stat
 
 #[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(),
@@ -178,22 +178,22 @@ fn create_ticket_direct_schema_check() {
                 (
                     "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(),
@@ -201,12 +201,12 @@ fn create_ticket_direct_schema_check() {
                 (
                     "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(),
                 ),
             ],
         )
@@ -258,14 +258,14 @@ pub fn func_with_option(verbose: Option<bool>) -> Result<(), Error> {
 
 #[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(),
             )],
         ),
     )
@@ -275,7 +275,7 @@ fn func_with_option_schema_check() {
 }
 
 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");
     }
@@ -285,7 +285,7 @@ impl proxmox::api::RpcEnvironment for RpcEnv {
     }
 
     /// The environment type
-    fn env_type(&self) -> proxmox::api::RpcEnvironmentType {
+    fn env_type(&self) -> proxmox_router::RpcEnvironmentType {
         panic!("env_type called");
     }
 
index 950c5758306019bac695f0a5c848b7dd01e72497..a7e92648b20432085ca4749a76795c0059b0937b 100644 (file)
@@ -34,14 +34,14 @@ pub async fn number(num: u32) -> Result<u32, Error> {
 
 #[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(),
@@ -75,20 +75,20 @@ pub async fn more_async_params(param: Value) -> Result<(), Error> {
 
 #[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(),
                 ),
             ],
         ),
@@ -116,14 +116,14 @@ pub async fn keyword_named_parameters(r#type: String) -> Result<(), Error> {
 
 #[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(),
             )],
         ),
     )
index 9fce967efed707e238847f01ebe57ae649e1925d..4c88de0e4995ded24abbaa14eac17ffd8e4d0630 100644 (file)
@@ -1,8 +1,9 @@
 //! 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};
@@ -27,9 +28,9 @@ pub fn get_archive(archive_name: String) {
 
 #[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)],
         ),
@@ -56,9 +57,9 @@ pub fn get_archive_2(param: Value, rpcenv: &mut dyn RpcEnvironment) -> Result<Va
 
 #[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)],
         ),
@@ -88,14 +89,14 @@ pub fn get_data(param: Value) -> Result<(), Error> {
 
 #[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(),
             )],
         ),
     )
index d8a47391e05746702b07c563a45c94c82c337b3b..896a93af01c798fd74584c04dfe354e246ae9414 100644 (file)
@@ -1,6 +1,6 @@
 //! 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.
@@ -9,8 +9,8 @@ pub struct AnI16(i16);
 
 #[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();
@@ -24,8 +24,8 @@ pub struct I16G50(i16);
 
 #[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();
@@ -39,8 +39,8 @@ pub struct AnI32(i32);
 
 #[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();
@@ -54,8 +54,8 @@ pub struct AnU32(u32);
 
 #[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();
@@ -69,8 +69,8 @@ pub struct AnI64(i64);
 
 #[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);
 }
@@ -81,8 +81,8 @@ pub struct AnU64(u64);
 
 #[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();
 
index 598f86236ef88c1e693cc57b13e38cab1740d8aa..6a7fa1ba15fa11bbd8f29a9781b2edc303b9cc4b 100644 (file)
@@ -40,7 +40,7 @@ pub fn test_default_macro(value: Option<isize>) -> Result<isize, Error> {
 }
 
 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");
     }
@@ -50,7 +50,7 @@ impl proxmox::api::RpcEnvironment for RpcEnv {
     }
 
     /// The environment type
-    fn env_type(&self) -> proxmox::api::RpcEnvironmentType {
+    fn env_type(&self) -> proxmox_router::RpcEnvironmentType {
         panic!("env_type called");
     }
 
index 2fd83808820d5e54cbcee7c465672241c03c30b1..efba8391a9dc93f056a8cd32deb25e1be4c511ac 100644 (file)
@@ -3,8 +3,9 @@
 
 #![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;
@@ -23,13 +24,12 @@ pub struct OkString(String);
 
 #[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);
 }
 
@@ -45,26 +45,23 @@ pub struct TestStruct {
 
 #[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);
 }
@@ -84,21 +81,19 @@ pub struct RenamedStruct {
 
 #[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(),
             ),
         ],
     )
@@ -123,10 +118,10 @@ pub enum Selection {
 
 #[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."),
@@ -157,9 +152,9 @@ pub fn string_check(arg: Value, selection: Selection) -> Result<bool, Error> {
 
 #[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),
@@ -167,9 +162,9 @@ fn string_check_schema_test() {
             ],
         ),
     )
-    .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);
 
index e06624180cecf6b3449b6f50e17fdf93c6bdad5b..bce65e9b4faff75c222aa4439677bc8d5add367e 100644 (file)
@@ -1,7 +1,6 @@
 #![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);
@@ -41,23 +40,22 @@ pub struct Simple {
 
 #[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);
 }
@@ -103,25 +101,24 @@ pub struct SuperComplex {
 }
 #[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);
 }
index 14b8e6b05c63f0d75f07e597cde61a5dcac65f01..640efc422de395bf81953ddd94b3ca2a72beb965 100644 (file)
@@ -90,3 +90,22 @@ macro_rules! offsetof {
         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()) }
+    }};
+}
diff --git a/proxmox-router/Cargo.toml b/proxmox-router/Cargo.toml
new file mode 100644 (file)
index 0000000..b1827bd
--- /dev/null
@@ -0,0 +1,30 @@
+[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" ]
diff --git a/proxmox-router/debian/changelog b/proxmox-router/debian/changelog
new file mode 100644 (file)
index 0000000..2e0563d
--- /dev/null
@@ -0,0 +1,5 @@
+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
diff --git a/proxmox-router/debian/control b/proxmox-router/debian/control
new file mode 100644 (file)
index 0000000..315e036
--- /dev/null
@@ -0,0 +1,137 @@
+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.
diff --git a/proxmox-router/debian/copyright b/proxmox-router/debian/copyright
new file mode 100644 (file)
index 0000000..5661ef6
--- /dev/null
@@ -0,0 +1,16 @@
+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/>.
diff --git a/proxmox-router/debian/debcargo.toml b/proxmox-router/debian/debcargo.toml
new file mode 100644 (file)
index 0000000..b7864cd
--- /dev/null
@@ -0,0 +1,7 @@
+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"
diff --git a/proxmox-router/src/cli/command.rs b/proxmox-router/src/cli/command.rs
new file mode 100644 (file)
index 0000000..906ec0c
--- /dev/null
@@ -0,0 +1,422 @@
+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);
+    }
+}
diff --git a/proxmox-router/src/cli/completion.rs b/proxmox-router/src/cli/completion.rs
new file mode 100644 (file)
index 0000000..42b19e5
--- /dev/null
@@ -0,0 +1,491 @@
+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, &[]);
+    }
+}
diff --git a/proxmox-router/src/cli/environment.rs b/proxmox-router/src/cli/environment.rs
new file mode 100644 (file)
index 0000000..0e9e3bf
--- /dev/null
@@ -0,0 +1,38 @@
+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()
+    }
+}
diff --git a/proxmox-router/src/cli/format.rs b/proxmox-router/src/cli/format.rs
new file mode 100644 (file)
index 0000000..c36f44b
--- /dev/null
@@ -0,0 +1,289 @@
+#![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(&param_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, "", &[])
+            );
+        }
+    }
+}
diff --git a/proxmox-router/src/cli/getopts.rs b/proxmox-router/src/cli/getopts.rs
new file mode 100644 (file)
index 0000000..e41766a
--- /dev/null
@@ -0,0 +1,263 @@
+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);
+    }
+}
diff --git a/proxmox-router/src/cli/mod.rs b/proxmox-router/src/cli/mod.rs
new file mode 100644 (file)
index 0000000..1494e31
--- /dev/null
@@ -0,0 +1,179 @@
+//! 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)
+    }
+}
diff --git a/proxmox-router/src/cli/readline.rs b/proxmox-router/src/cli/readline.rs
new file mode 100644 (file)
index 0000000..dafb708
--- /dev/null
@@ -0,0 +1,48 @@
+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 {}
diff --git a/proxmox-router/src/cli/shellword.rs b/proxmox-router/src/cli/shellword.rs
new file mode 100644 (file)
index 0000000..4adf51d
--- /dev/null
@@ -0,0 +1,163 @@
+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)
+    );
+}
diff --git a/proxmox-router/src/cli/text_table.rs b/proxmox-router/src/cli/text_table.rs
new file mode 100644 (file)
index 0000000..1932034
--- /dev/null
@@ -0,0 +1,810 @@
+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(())
+}
diff --git a/proxmox-router/src/error.rs b/proxmox-router/src/error.rs
new file mode 100644 (file)
index 0000000..e285cf7
--- /dev/null
@@ -0,0 +1,44 @@
+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)+));
+    }};
+}
diff --git a/proxmox-router/src/format.rs b/proxmox-router/src/format.rs
new file mode 100644 (file)
index 0000000..32c1009
--- /dev/null
@@ -0,0 +1,88 @@
+//! 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(())
+}
diff --git a/proxmox-router/src/lib.rs b/proxmox-router/src/lib.rs
new file mode 100644 (file)
index 0000000..dadb917
--- /dev/null
@@ -0,0 +1,25 @@
+//! 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;
diff --git a/proxmox-router/src/permission.rs b/proxmox-router/src/permission.rs
new file mode 100644 (file)
index 0000000..eb93b33
--- /dev/null
@@ -0,0 +1,408 @@
+//! 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(&param_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, &param, &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,
+        );
+    }
+}
diff --git a/proxmox-router/src/router.rs b/proxmox-router/src/router.rs
new file mode 100644 (file)
index 0000000..19d2389
--- /dev/null
@@ -0,0 +1,495 @@
+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
+    }
+}
diff --git a/proxmox-router/src/rpc_environment.rs b/proxmox-router/src/rpc_environment.rs
new file mode 100644 (file)
index 0000000..27c7198
--- /dev/null
@@ -0,0 +1,78 @@
+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)
+    }
+}
diff --git a/proxmox-schema/Cargo.toml b/proxmox-schema/Cargo.toml
new file mode 100644 (file)
index 0000000..e864a40
--- /dev/null
@@ -0,0 +1,37 @@
+[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 = []
diff --git a/proxmox-schema/debian/changelog b/proxmox-schema/debian/changelog
new file mode 100644 (file)
index 0000000..476b1de
--- /dev/null
@@ -0,0 +1,5 @@
+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
diff --git a/proxmox-schema/debian/control b/proxmox-schema/debian/control
new file mode 100644 (file)
index 0000000..04c6763
--- /dev/null
@@ -0,0 +1,118 @@
+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.
diff --git a/proxmox-schema/debian/copyright b/proxmox-schema/debian/copyright
new file mode 100644 (file)
index 0000000..5661ef6
--- /dev/null
@@ -0,0 +1,16 @@
+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/>.
diff --git a/proxmox-schema/debian/debcargo.toml b/proxmox-schema/debian/debcargo.toml
new file mode 100644 (file)
index 0000000..b7864cd
--- /dev/null
@@ -0,0 +1,7 @@
+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"
diff --git a/proxmox-schema/src/api_type_macros.rs b/proxmox-schema/src/api_type_macros.rs
new file mode 100644 (file)
index 0000000..f3740d1
--- /dev/null
@@ -0,0 +1,113 @@
+/// 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)
+            }
+        }
+    );
+}
diff --git a/proxmox-schema/src/const_regex.rs b/proxmox-schema/src/const_regex.rs
new file mode 100644 (file)
index 0000000..3a5c2da
--- /dev/null
@@ -0,0 +1,67 @@
+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
+    }
+}
diff --git a/proxmox-schema/src/de.rs b/proxmox-schema/src/de.rs
new file mode 100644 (file)
index 0000000..c1a8ae9
--- /dev/null
@@ -0,0 +1,296 @@
+//! 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");
+}
diff --git a/proxmox-schema/src/format.rs b/proxmox-schema/src/format.rs
new file mode 100644 (file)
index 0000000..104b1c0
--- /dev/null
@@ -0,0 +1,465 @@
+//! 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
+}
diff --git a/proxmox-schema/src/lib.rs b/proxmox-schema/src/lib.rs
new file mode 100644 (file)
index 0000000..a4e3ba4
--- /dev/null
@@ -0,0 +1,31 @@
+//! 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;
+}
diff --git a/proxmox-schema/src/schema.rs b/proxmox-schema/src/schema.rs
new file mode 100644 (file)
index 0000000..34135f4
--- /dev/null
@@ -0,0 +1,1240 @@
+//! 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(&param_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 }
+    }
+}
diff --git a/proxmox-schema/src/upid.rs b/proxmox-schema/src/upid.rs
new file mode 100644 (file)
index 0000000..22d878b
--- /dev/null
@@ -0,0 +1,295 @@
+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")
+        }
+    }
+}
diff --git a/proxmox-schema/tests/schema.rs b/proxmox-schema/tests/schema.rs
new file mode 100644 (file)
index 0000000..4b4a837
--- /dev/null
@@ -0,0 +1,395 @@
+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(&param_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());
+    }
+}
diff --git a/proxmox-schema/tests/schema_verification.rs b/proxmox-schema/tests/schema_verification.rs
new file mode 100644 (file)
index 0000000..2ba4455
--- /dev/null
@@ -0,0 +1,190 @@
+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(())
+}
index b26b5a094c866a7194040ab4b208908a686d4605..67fa98af80ef0f2599c68c5303dbb217dcf929b8 100644 (file)
@@ -25,40 +25,11 @@ endian_trait = { version = "0.6", features = ["arrays"] }
 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"]
diff --git a/proxmox/src/api/api_type_macros.rs b/proxmox/src/api/api_type_macros.rs
deleted file mode 100644 (file)
index 60d5ada..0000000
+++ /dev/null
@@ -1,113 +0,0 @@
-/// 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)
-            }
-        }
-    );
-}
diff --git a/proxmox/src/api/cli/command.rs b/proxmox/src/api/cli/command.rs
deleted file mode 100644 (file)
index b91aac7..0000000
+++ /dev/null
@@ -1,421 +0,0 @@
-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);
-    }
-}
diff --git a/proxmox/src/api/cli/completion.rs b/proxmox/src/api/cli/completion.rs
deleted file mode 100644 (file)
index b318264..0000000
+++ /dev/null
@@ -1,490 +0,0 @@
-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, &[]);
-    }
-}
diff --git a/proxmox/src/api/cli/environment.rs b/proxmox/src/api/cli/environment.rs
deleted file mode 100644 (file)
index 6085292..0000000
+++ /dev/null
@@ -1,38 +0,0 @@
-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()
-    }
-}
diff --git a/proxmox/src/api/cli/format.rs b/proxmox/src/api/cli/format.rs
deleted file mode 100644 (file)
index a4fb78d..0000000
+++ /dev/null
@@ -1,288 +0,0 @@
-#![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(&param_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, "", &[])
-            );
-        }
-    }
-}
diff --git a/proxmox/src/api/cli/getopts.rs b/proxmox/src/api/cli/getopts.rs
deleted file mode 100644 (file)
index db22d41..0000000
+++ /dev/null
@@ -1,261 +0,0 @@
-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);
-    }
-}
diff --git a/proxmox/src/api/cli/mod.rs b/proxmox/src/api/cli/mod.rs
deleted file mode 100644 (file)
index 9e66979..0000000
+++ /dev/null
@@ -1,179 +0,0 @@
-//! 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)
-    }
-}
diff --git a/proxmox/src/api/cli/readline.rs b/proxmox/src/api/cli/readline.rs
deleted file mode 100644 (file)
index d35f1cb..0000000
+++ /dev/null
@@ -1,48 +0,0 @@
-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 {}
diff --git a/proxmox/src/api/cli/shellword.rs b/proxmox/src/api/cli/shellword.rs
deleted file mode 100644 (file)
index 4adf51d..0000000
+++ /dev/null
@@ -1,163 +0,0 @@
-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)
-    );
-}
diff --git a/proxmox/src/api/cli/text_table.rs b/proxmox/src/api/cli/text_table.rs
deleted file mode 100644 (file)
index d136629..0000000
+++ /dev/null
@@ -1,794 +0,0 @@
-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(())
-}
diff --git a/proxmox/src/api/const_regex.rs b/proxmox/src/api/const_regex.rs
deleted file mode 100644 (file)
index f3f16b6..0000000
+++ /dev/null
@@ -1,67 +0,0 @@
-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
-    }
-}
diff --git a/proxmox/src/api/de.rs b/proxmox/src/api/de.rs
deleted file mode 100644 (file)
index 254e668..0000000
+++ /dev/null
@@ -1,296 +0,0 @@
-//! 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");
-}
diff --git a/proxmox/src/api/error.rs b/proxmox/src/api/error.rs
deleted file mode 100644 (file)
index fec36f2..0000000
+++ /dev/null
@@ -1,44 +0,0 @@
-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)+));
-    }};
-}
diff --git a/proxmox/src/api/format.rs b/proxmox/src/api/format.rs
deleted file mode 100644 (file)
index 64fcd0a..0000000
+++ /dev/null
@@ -1,560 +0,0 @@
-//! 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
-}
diff --git a/proxmox/src/api/mod.rs b/proxmox/src/api/mod.rs
deleted file mode 100644 (file)
index 18b5cd2..0000000
+++ /dev/null
@@ -1,57 +0,0 @@
-//! 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;
diff --git a/proxmox/src/api/permission.rs b/proxmox/src/api/permission.rs
deleted file mode 100644 (file)
index b55bcd5..0000000
+++ /dev/null
@@ -1,408 +0,0 @@
-//! 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(&param_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, &param, &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,
-        );
-    }
-}
diff --git a/proxmox/src/api/router.rs b/proxmox/src/api/router.rs
deleted file mode 100644 (file)
index cd3bbbe..0000000
+++ /dev/null
@@ -1,497 +0,0 @@
-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
-    }
-}
diff --git a/proxmox/src/api/rpc_environment.rs b/proxmox/src/api/rpc_environment.rs
deleted file mode 100644 (file)
index 0dc06d9..0000000
+++ /dev/null
@@ -1,67 +0,0 @@
-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)
-    }
-}
diff --git a/proxmox/src/api/schema.rs b/proxmox/src/api/schema.rs
deleted file mode 100644 (file)
index c9f659e..0000000
+++ /dev/null
@@ -1,1241 +0,0 @@
-//! 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(&param_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(&param_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 }
-    }
-}
index 564a7c77f7ca415d3c128fbf03493692a858cb6b..3fbaf7059c12ea195d9b9848f2371d347a4b95ee 100644 (file)
@@ -883,3 +883,36 @@ lvmthin: local-lvm2
 
     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
+}
diff --git a/proxmox/src/api/upid.rs b/proxmox/src/api/upid.rs
deleted file mode 100644 (file)
index 8f03a20..0000000
+++ /dev/null
@@ -1,148 +0,0 @@
-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)
-    }
-}
index 6e9590643f02d7b7147775035be37831bf8f8b7b..8d6bcfcfa52c57c6c7ebef6a3dcffead46ff1037 100644 (file)
@@ -4,7 +4,6 @@
 #[macro_use]
 pub mod serde_macros;
 
-pub mod api;
 pub mod sys;
 pub mod tools;
 
index da6f7bd7d6d833fcb87b1913236a550a410d618d..b5bd982ce9e063de537a59a64972a25fa60eec58 100644 (file)
@@ -64,22 +64,3 @@ macro_rules! c_try {
         $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()) }
-    }};
-}
diff --git a/proxmox/src/test/schema.rs b/proxmox/src/test/schema.rs
deleted file mode 100644 (file)
index c3f2082..0000000
+++ /dev/null
@@ -1,381 +0,0 @@
-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());
-    }
-}
diff --git a/proxmox/src/test/schema_verification.rs b/proxmox/src/test/schema_verification.rs
deleted file mode 100644 (file)
index 4937de8..0000000
+++ /dev/null
@@ -1,190 +0,0 @@
-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(())
-}
diff --git a/proxmox/src/tools/as_any.rs b/proxmox/src/tools/as_any.rs
deleted file mode 100644 (file)
index 39eaedc..0000000
+++ /dev/null
@@ -1,14 +0,0 @@
-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
-    }
-}
index 1c426666d1567441c78544e27e06a098aa04b13a..0ab8aed34f17d1a7b438d38f8975a0dc23e33080 100644 (file)
@@ -5,7 +5,6 @@ use std::fmt;
 use anyhow::*;
 use lazy_static::lazy_static;
 
-pub mod as_any;
 pub mod common_regex;
 pub mod email;
 pub mod fd;
@@ -15,9 +14,6 @@ pub mod parse;
 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.