diff --git a/Cargo.toml b/Cargo.toml index e8666910..7048864a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,6 +6,8 @@ members = [ "proxmox-http", "proxmox-io", "proxmox-lang", + "proxmox-router", + "proxmox-schema", "proxmox-sortable-macro", "proxmox-tfa", "proxmox-time", diff --git a/Makefile b/Makefile index a29553dd..ddf5dc1e 100644 --- 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 \ diff --git a/proxmox-api-macro/Cargo.toml b/proxmox-api-macro/Cargo.toml index c9786e61..cba7d1c5 100644 --- a/proxmox-api-macro/Cargo.toml +++ b/proxmox-api-macro/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "proxmox-api-macro" edition = "2018" -version = "0.5.1" +version = "1.0.0" authors = [ "Wolfgang Bumiller " ] 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)! diff --git a/proxmox-api-macro/debian/changelog b/proxmox-api-macro/debian/changelog index edcd62eb..c0b2ddab 100644 --- a/proxmox-api-macro/debian/changelog +++ b/proxmox-api-macro/debian/changelog @@ -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 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 diff --git a/proxmox-api-macro/debian/control b/proxmox-api-macro/debian/control index e70edce7..5e0979e5 100644 --- a/proxmox-api-macro/debian/control +++ b/proxmox-api-macro/debian/control @@ -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. diff --git a/proxmox-api-macro/src/api/enums.rs b/proxmox-api-macro/src/api/enums.rs index 3c35bc09..2797a641 100644 --- a/proxmox-api-macro/src/api/enums.rs +++ b/proxmox-api-macro/src/api/enums.rs @@ -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; } }) diff --git a/proxmox-api-macro/src/api/method.rs b/proxmox-api-macro/src/api/method.rs index edaff0ec..927030f3 100644 --- a/proxmox-api-macro/src/api/method.rs +++ b/proxmox-api-macro/src/api/method.rs @@ -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 #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) }, )) } diff --git a/proxmox-api-macro/src/api/mod.rs b/proxmox-api-macro/src/api/mod.rs index 138b82b5..18ffdac3 100644 --- a/proxmox-api-macro/src/api/mod.rs +++ b/proxmox-api-macro/src/api/mod.rs @@ -148,7 +148,7 @@ impl Schema { fn to_schema_reference(&self) -> Option { 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) => { diff --git a/proxmox-api-macro/src/api/structs.rs b/proxmox-api-macro/src/api/structs.rs index 4bc6b759..49ebf3ad 100644 --- a/proxmox-api-macro/src/api/structs.rs +++ b/proxmox-api-macro/src/api/structs.rs @@ -62,7 +62,7 @@ fn handle_unit_struct(attribs: JSONObject, stru: syn::ItemStruct) -> Result - impl ::proxmox::api::schema::UpdaterType for #name { + impl ::proxmox_schema::UpdaterType for #name { type Updater = Option; } }); @@ -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() { diff --git a/proxmox-api-macro/src/lib.rs b/proxmox-api-macro/src/lib.rs index 9f562627..e8682517 100644 --- a/proxmox-api-macro/src/lib.rs +++ b/proxmox-api-macro/src/lib.rs @@ -69,7 +69,7 @@ fn router_do(item: TokenStream) -> Result { ``` # 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 { ```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(), diff --git a/proxmox-api-macro/src/updater.rs b/proxmox-api-macro/src/updater.rs index 85c671d3..4399e971 100644 --- a/proxmox-api-macro/src/updater.rs +++ b/proxmox-api-macro/src/updater.rs @@ -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; } } diff --git a/proxmox-api-macro/tests/allof.rs b/proxmox-api-macro/tests/allof.rs index f14f6a1e..57089d26 100644 --- a/proxmox-api-macro/tests/allof.rs +++ b/proxmox-api-macro/tests/allof.rs @@ -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( "", &[( "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( "", &[( "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"); } diff --git a/proxmox-api-macro/tests/api1.rs b/proxmox-api-macro/tests/api1.rs index 88adb40a..88eb74ac 100644 --- a/proxmox-api-macro/tests/api1.rs +++ b/proxmox-api-macro/tests/api1.rs @@ -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 { #[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) -> 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"); } diff --git a/proxmox-api-macro/tests/api2.rs b/proxmox-api-macro/tests/api2.rs index 950c5758..a7e92648 100644 --- a/proxmox-api-macro/tests/api2.rs +++ b/proxmox-api-macro/tests/api2.rs @@ -34,14 +34,14 @@ pub async fn number(num: u32) -> Result { #[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(), )], ), ) diff --git a/proxmox-api-macro/tests/ext-schema.rs b/proxmox-api-macro/tests/ext-schema.rs index 9fce967e..4c88de0e 100644 --- a/proxmox-api-macro/tests/ext-schema.rs +++ b/proxmox-api-macro/tests/ext-schema.rs @@ -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 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(), )], ), ) diff --git a/proxmox-api-macro/tests/int-limits.rs b/proxmox-api-macro/tests/int-limits.rs index d8a47391..896a93af 100644 --- a/proxmox-api-macro/tests/int-limits.rs +++ b/proxmox-api-macro/tests/int-limits.rs @@ -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(); diff --git a/proxmox-api-macro/tests/options.rs b/proxmox-api-macro/tests/options.rs index 598f8623..6a7fa1ba 100644 --- a/proxmox-api-macro/tests/options.rs +++ b/proxmox-api-macro/tests/options.rs @@ -40,7 +40,7 @@ pub fn test_default_macro(value: Option) -> Result { } 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"); } diff --git a/proxmox-api-macro/tests/types.rs b/proxmox-api-macro/tests/types.rs index 2fd83808..efba8391 100644 --- a/proxmox-api-macro/tests/types.rs +++ b/proxmox-api-macro/tests/types.rs @@ -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 { #[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); diff --git a/proxmox-api-macro/tests/updater.rs b/proxmox-api-macro/tests/updater.rs index e0662418..bce65e9b 100644 --- a/proxmox-api-macro/tests/updater.rs +++ b/proxmox-api-macro/tests/updater.rs @@ -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); @@ -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, & as ApiType>::API_SCHEMA), - ( - "extra", - true, - &::proxmox::api::schema::StringSchema::new("An extra field.").schema(), - ), - ( - "simple", - true, - //&<::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, & as ApiType>::API_SCHEMA), + ( + "extra", + true, + &::proxmox_schema::StringSchema::new("An extra field.").schema(), + ), + ( + "simple", + true, + //&<::Updater as ApiType>::API_SCHEMA, + &SimpleUpdater::API_SCHEMA, + ), + ], + ) + .schema(); assert_eq!(TEST_SCHEMA, SuperComplexUpdater::API_SCHEMA); } diff --git a/proxmox-lang/src/lib.rs b/proxmox-lang/src/lib.rs index 14b8e6b0..640efc42 100644 --- a/proxmox-lang/src/lib.rs +++ b/proxmox-lang/src/lib.rs @@ -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 index 00000000..b1827bd7 --- /dev/null +++ b/proxmox-router/Cargo.toml @@ -0,0 +1,30 @@ +[package] +name = "proxmox-router" +version = "1.0.0" +authors = ["Proxmox Support Team "] +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 index 00000000..2e0563d5 --- /dev/null +++ b/proxmox-router/debian/changelog @@ -0,0 +1,5 @@ +rust-proxmox-router (1.0.0-1) stable; urgency=medium + + * initial split out of `librust-proxmox-dev` + + -- Proxmox Support Team Wed, 06 Oct 2021 11:04:36 +0200 diff --git a/proxmox-router/debian/control b/proxmox-router/debian/control new file mode 100644 index 00000000..315e036d --- /dev/null +++ b/proxmox-router/debian/control @@ -0,0 +1,137 @@ +Source: rust-proxmox-router +Section: rust +Priority: optional +Build-Depends: debhelper (>= 12), + dh-cargo (>= 24), + cargo:native , + rustc:native , + libstd-rust-dev , + librust-anyhow-1+default-dev , + librust-http-0.2+default-dev , + librust-hyper-0.14+default-dev , + librust-hyper-0.14+full-dev , + librust-libc-0.2+default-dev , + librust-percent-encoding-2+default-dev (>= 2.1-~~) , + librust-proxmox-lang-1+default-dev , + librust-proxmox-schema-1+default-dev , + librust-rustyline-7+default-dev , + librust-serde-json-1+default-dev , + librust-tokio-1+default-dev , + librust-unicode-width-0.1+default-dev (>= 0.1.8-~~) +Maintainer: Proxmox Support Team +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 index 00000000..5661ef60 --- /dev/null +++ b/proxmox-router/debian/copyright @@ -0,0 +1,16 @@ +Copyright (C) 2021 Proxmox Server Solutions GmbH + +This software is written by Proxmox Server Solutions GmbH + +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 . diff --git a/proxmox-router/debian/debcargo.toml b/proxmox-router/debian/debcargo.toml new file mode 100644 index 00000000..b7864cdb --- /dev/null +++ b/proxmox-router/debian/debcargo.toml @@ -0,0 +1,7 @@ +overlay = "." +crate_src_path = ".." +maintainer = "Proxmox Support Team " + +[source] +vcs_git = "git://git.proxmox.com/git/proxmox.git" +vcs_browser = "https://git.proxmox.com/?p=proxmox.git" diff --git a/proxmox/src/api/cli/command.rs b/proxmox-router/src/cli/command.rs similarity index 97% rename from proxmox/src/api/cli/command.rs rename to proxmox-router/src/cli/command.rs index b91aac77..906ec0c3 100644 --- a/proxmox/src/api/cli/command.rs +++ b/proxmox-router/src/cli/command.rs @@ -3,15 +3,16 @@ use serde_json::Value; use std::cell::RefCell; use std::sync::Arc; -use crate::api::format::*; -use crate::api::schema::*; -use crate::api::*; +use proxmox_schema::*; +use proxmox_schema::format::DocumentationFormat; use super::environment::CliEnvironment; - -use super::format::*; use super::getopts; -use super::{completion::*, CliCommand, CliCommandMap, CommandLineInterface}; +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. /// @@ -349,7 +350,7 @@ fn prepare_cli_command(def: &CommandLineInterface) -> (String, Vec) { if !args.is_empty() { if args[0] == "bashcomplete" { - print_bash_completion(&def); + def.print_bash_completion(); std::process::exit(0); } diff --git a/proxmox/src/api/cli/completion.rs b/proxmox-router/src/cli/completion.rs similarity index 69% rename from proxmox/src/api/cli/completion.rs rename to proxmox-router/src/cli/completion.rs index b3182644..42b19e5f 100644 --- a/proxmox/src/api/cli/completion.rs +++ b/proxmox-router/src/cli/completion.rs @@ -1,6 +1,9 @@ -use super::*; +use std::collections::HashMap; -use crate::api::schema::*; +use proxmox_schema::*; + +use super::help_command_def; +use super::{shellword_split_unclosed, CliCommand, CommandLineInterface, CompletionFunction}; fn record_done_argument( done: &mut HashMap, @@ -128,7 +131,7 @@ fn get_simple_completion( // 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( + let (data, _remaining) = super::getopts::parse_argument_list( &args[0..args.len() - 1], cli_cmd.info.parameters, &mut errors, @@ -174,156 +177,154 @@ fn get_simple_completion( completions } -fn get_help_completion( - def: &CommandLineInterface, - help_cmd: &CliCommand, - args: &[String], -) -> Vec { - let mut done = HashMap::new(); +impl CommandLineInterface { + fn get_help_completion(&self, help_cmd: &CliCommand, args: &[String]) -> Vec { + 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); + } - 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()); + if cmd.starts_with(first) { + completions.push(cmd.to_string()); + } } - return completions; + 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 { - match def { - CommandLineInterface::Simple(cli_cmd) => { - let mut done: HashMap = 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() { + fn get_nested_completion(&self, args: &[String]) -> Vec { + match self { + CommandLineInterface::Simple(cli_cmd) => { + let mut done: HashMap = 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() { - completions.push(cmd.to_string()); + if cmd.starts_with(first) { + completions.push(cmd.to_string()); + } } - return completions; + 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, + /// 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, - }, - 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); + 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 } - val + Err(_) => return, + }; + + let (_start, completions) = self.get_completions(&cmdline, true); + + for item in completions { + println!("{}", item); } - 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) { - 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) - }; + /// Compute possible completions for a partial command + pub fn get_completions(&self, line: &str, skip_first: bool) -> (usize, Vec) { + 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) + } + }; - (start, completions) + 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::*; + use anyhow::Error; use serde_json::Value; - use crate::api::{cli::*, schema::*, *}; + use proxmox_schema::{BooleanSchema, ObjectSchema, StringSchema}; + + use crate::cli::{CliCommand, CliCommandMap, CommandLineInterface}; + use crate::{ApiHandler, ApiMethod, RpcEnvironment}; fn dummy_method( _param: Value, @@ -379,7 +380,7 @@ mod test { let mut expect: Vec = expect.iter().map(|s| s.to_string()).collect(); expect.sort(); - let (completion_start, mut completions) = get_completions(cmd_def, line, false); + let (completion_start, mut completions) = cmd_def.get_completions(line, false); completions.sort(); assert_eq!((start, expect), (completion_start, completions)); diff --git a/proxmox/src/api/cli/environment.rs b/proxmox-router/src/cli/environment.rs similarity index 93% rename from proxmox/src/api/cli/environment.rs rename to proxmox-router/src/cli/environment.rs index 60852929..0e9e3bf9 100644 --- a/proxmox/src/api/cli/environment.rs +++ b/proxmox-router/src/cli/environment.rs @@ -1,6 +1,6 @@ use serde_json::Value; -use crate::api::{RpcEnvironment, RpcEnvironmentType}; +use crate::{RpcEnvironment, RpcEnvironmentType}; /// `RpcEnvironmet` implementation for command line tools #[derive(Default)] diff --git a/proxmox/src/api/cli/format.rs b/proxmox-router/src/cli/format.rs similarity index 98% rename from proxmox/src/api/cli/format.rs rename to proxmox-router/src/cli/format.rs index a4fb78db..c36f44bd 100644 --- a/proxmox/src/api/cli/format.rs +++ b/proxmox-router/src/cli/format.rs @@ -1,12 +1,13 @@ #![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 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}; diff --git a/proxmox/src/api/cli/getopts.rs b/proxmox-router/src/cli/getopts.rs similarity index 99% rename from proxmox/src/api/cli/getopts.rs rename to proxmox-router/src/cli/getopts.rs index db22d416..e41766a8 100644 --- a/proxmox/src/api/cli/getopts.rs +++ b/proxmox-router/src/cli/getopts.rs @@ -1,9 +1,9 @@ use std::collections::HashMap; -use anyhow::*; +use anyhow::format_err; use serde_json::Value; -use crate::api::schema::*; +use proxmox_schema::*; #[derive(Debug)] enum RawArgument { @@ -237,6 +237,8 @@ fn test_boolean_arg() { #[test] fn test_argument_paramenter() { + use proxmox_schema::*; + const PARAMETERS: ObjectSchema = ObjectSchema::new( "Parameters:", &[ diff --git a/proxmox/src/api/cli/mod.rs b/proxmox-router/src/cli/mod.rs similarity index 99% rename from proxmox/src/api/cli/mod.rs rename to proxmox-router/src/cli/mod.rs index 9e66979e..1494e319 100644 --- a/proxmox/src/api/cli/mod.rs +++ b/proxmox-router/src/cli/mod.rs @@ -12,6 +12,10 @@ //! - Ability to create interactive commands (using ``rustyline``) //! - Supports complex/nested commands +use std::collections::HashMap; + +use crate::ApiMethod; + mod environment; pub use environment::*; @@ -36,10 +40,6 @@ 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 diff --git a/proxmox/src/api/cli/readline.rs b/proxmox-router/src/cli/readline.rs similarity index 92% rename from proxmox/src/api/cli/readline.rs rename to proxmox-router/src/cli/readline.rs index d35f1cbb..dafb7080 100644 --- a/proxmox/src/api/cli/readline.rs +++ b/proxmox-router/src/cli/readline.rs @@ -34,7 +34,7 @@ impl rustyline::completion::Completer for CliHelper { ) -> rustyline::Result<(usize, Vec)> { let line = &line[..pos]; - let (start, completions) = super::get_completions(&*self.cmd_def, line, false); + let (start, completions) = self.cmd_def.get_completions(line, false); Ok((start, completions)) } diff --git a/proxmox/src/api/cli/shellword.rs b/proxmox-router/src/cli/shellword.rs similarity index 100% rename from proxmox/src/api/cli/shellword.rs rename to proxmox-router/src/cli/shellword.rs diff --git a/proxmox/src/api/cli/text_table.rs b/proxmox-router/src/cli/text_table.rs similarity index 97% rename from proxmox/src/api/cli/text_table.rs rename to proxmox-router/src/cli/text_table.rs index d1366293..19320341 100644 --- a/proxmox/src/api/cli/text_table.rs +++ b/proxmox-router/src/cli/text_table.rs @@ -1,10 +1,11 @@ use std::io::Write; -use anyhow::*; +use anyhow::{bail, Error}; use serde_json::Value; use unicode_width::UnicodeWidthStr; -use crate::api::schema::*; +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"; @@ -201,6 +202,21 @@ impl ColumnConfig { } } +/// 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 { @@ -232,13 +248,13 @@ impl TableFormatOptions { let is_tty = unsafe { libc::isatty(libc::STDOUT_FILENO) == 1 }; if is_tty { - let (_rows, columns) = crate::sys::linux::tty::stdout_terminal_size(); + let (_rows, columns) = stdout_terminal_size(); if columns > 0 { me.columns = Some(columns); } } - let empty_cstr = crate::c_str!(""); + let empty_cstr = c_str!(""); use std::ffi::CStr; let encoding = unsafe { @@ -246,7 +262,7 @@ impl TableFormatOptions { CStr::from_ptr(libc::nl_langinfo(libc::CODESET)) }; - if encoding != crate::c_str!("UTF-8") { + if encoding != c_str!("UTF-8") { me.ascii_delimiters = true; } diff --git a/proxmox/src/api/error.rs b/proxmox-router/src/error.rs similarity index 88% rename from proxmox/src/api/error.rs rename to proxmox-router/src/error.rs index fec36f2b..e285cf7e 100644 --- a/proxmox/src/api/error.rs +++ b/proxmox-router/src/error.rs @@ -28,8 +28,8 @@ impl fmt::Display for HttpError { #[macro_export] macro_rules! http_err { ($status:ident, $($fmt:tt)+) => {{ - ::anyhow::Error::from($crate::api::error::HttpError::new( - $crate::api::error::StatusCode::$status, + ::anyhow::Error::from($crate::HttpError::new( + $crate::error::StatusCode::$status, format!($($fmt)+) )) }}; diff --git a/proxmox-router/src/format.rs b/proxmox-router/src/format.rs new file mode 100644 index 00000000..32c1009f --- /dev/null +++ b/proxmox-router/src/format.rs @@ -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 { + 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 index 00000000..dadb917f --- /dev/null +++ b/proxmox-router/src/lib.rs @@ -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/src/api/permission.rs b/proxmox-router/src/permission.rs similarity index 99% rename from proxmox/src/api/permission.rs rename to proxmox-router/src/permission.rs index b55bcd54..eb93b332 100644 --- a/proxmox/src/api/permission.rs +++ b/proxmox-router/src/permission.rs @@ -197,10 +197,10 @@ fn check_api_permission_tail( #[cfg(test)] mod test { - - use crate::api::permission::*; use serde_json::{json, Value}; + use crate::permission::*; + struct MockedUserInfo { privs: Value, groups: Value, diff --git a/proxmox/src/api/router.rs b/proxmox-router/src/router.rs similarity index 94% rename from proxmox/src/api/router.rs rename to proxmox-router/src/router.rs index cd3bbbed..19d23890 100644 --- a/proxmox/src/api/router.rs +++ b/proxmox-router/src/router.rs @@ -10,13 +10,10 @@ 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 proxmox_schema::{ObjectSchema, ParameterSchema, ReturnType, Schema}; use super::Permission; - -/// Deprecated reexport: -pub use super::schema::ReturnType; +use crate::RpcEnvironment; /// A synchronous API handler gets a json Value as input and returns a json Value as output. /// @@ -24,8 +21,9 @@ pub use super::schema::ReturnType; /// ``` /// # use anyhow::*; /// # use serde_json::{json, Value}; -/// # use proxmox::api::{*, schema::*}; -/// # +/// use proxmox_router::{ApiHandler, ApiMethod, RpcEnvironment}; +/// use proxmox_schema::ObjectSchema; +/// /// fn hello( /// param: Value, /// info: &ApiMethod, @@ -48,21 +46,21 @@ pub type ApiHandlerFn = &'static (dyn Fn(Value, &ApiMethod, &mut dyn RpcEnvironm /// /// Returns a future Value. /// ``` -/// # use anyhow::*; /// # use serde_json::{json, Value}; -/// # use proxmox::api::{*, schema::*}; /// # -/// use futures::*; +/// 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> { -/// async move { +/// Box::pin(async move { /// let data = json!("hello world!"); /// Ok(data) -/// }.boxed() +/// }) /// } /// /// const API_METHOD_HELLO_FUTURE: ApiMethod = ApiMethod::new( @@ -81,13 +79,13 @@ pub type ApiFuture<'a> = Pin = Pin, /// ) -> ApiResponseFuture { -/// async move { +/// Box::pin(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( @@ -189,17 +187,17 @@ pub enum SubRoute { #[macro_export] macro_rules! list_subdirs_api_method { ($map:expr) => { - $crate::api::ApiMethod::new( - &$crate::api::ApiHandler::Sync( & |_, _, _| { + $crate::ApiMethod::new( + &$crate::ApiHandler::Sync( & |_, _, _| { let index = ::serde_json::json!( $map.iter().map(|s| ::serde_json::json!({ "subdir": s.0})) .collect::>() ); Ok(index) }), - &$crate::api::schema::ObjectSchema::new("Directory index.", &[]) + &$crate::ListSubdirsObjectSchema::new("Directory index.", &[]) .additional_properties(true) - ).access(None, &$crate::api::Permission::Anybody) + ).access(None, &$crate::Permission::Anybody) } } @@ -217,10 +215,10 @@ macro_rules! list_subdirs_api_method { /// all `const fn(mut self, ..)` methods to configure them. /// ///``` -/// # use anyhow::*; /// # use serde_json::{json, Value}; -/// # use proxmox::api::{*, schema::*}; -/// # +/// use proxmox_router::{ApiHandler, ApiMethod, Router}; +/// use proxmox_schema::ObjectSchema; +/// /// const API_METHOD_HELLO: ApiMethod = ApiMethod::new( /// &ApiHandler::Sync(&|_, _, _| { /// Ok(json!("Hello world!")) diff --git a/proxmox/src/api/rpc_environment.rs b/proxmox-router/src/rpc_environment.rs similarity index 87% rename from proxmox/src/api/rpc_environment.rs rename to proxmox-router/src/rpc_environment.rs index 0dc06d90..27c71984 100644 --- a/proxmox/src/api/rpc_environment.rs +++ b/proxmox-router/src/rpc_environment.rs @@ -1,9 +1,20 @@ -use crate::tools::AsAny; +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 AsAny for T { + fn as_any(&self) -> &(dyn Any + Send) { + self + } +} + /// Abstract Interface for API methods to interact with the environment -pub trait RpcEnvironment: std::any::Any + AsAny + Send { +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; diff --git a/proxmox-schema/Cargo.toml b/proxmox-schema/Cargo.toml new file mode 100644 index 00000000..e864a40d --- /dev/null +++ b/proxmox-schema/Cargo.toml @@ -0,0 +1,37 @@ +[package] +name = "proxmox-schema" +version = "1.0.0" +authors = ["Proxmox Support Team "] +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 index 00000000..476b1de2 --- /dev/null +++ b/proxmox-schema/debian/changelog @@ -0,0 +1,5 @@ +rust-proxmox-schema (1.0.0-1) stable; urgency=medium + + * initial split out of `librust-proxmox-dev` + + -- Proxmox Support Team Wed, 06 Oct 2021 11:04:36 +0200 diff --git a/proxmox-schema/debian/control b/proxmox-schema/debian/control new file mode 100644 index 00000000..04c67638 --- /dev/null +++ b/proxmox-schema/debian/control @@ -0,0 +1,118 @@ +Source: rust-proxmox-schema +Section: rust +Priority: optional +Build-Depends: debhelper (>= 12), + dh-cargo (>= 24), + cargo:native , + rustc:native , + libstd-rust-dev , + 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 +Maintainer: Proxmox Support Team +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 index 00000000..5661ef60 --- /dev/null +++ b/proxmox-schema/debian/copyright @@ -0,0 +1,16 @@ +Copyright (C) 2021 Proxmox Server Solutions GmbH + +This software is written by Proxmox Server Solutions GmbH + +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 . diff --git a/proxmox-schema/debian/debcargo.toml b/proxmox-schema/debian/debcargo.toml new file mode 100644 index 00000000..b7864cdb --- /dev/null +++ b/proxmox-schema/debian/debcargo.toml @@ -0,0 +1,7 @@ +overlay = "." +crate_src_path = ".." +maintainer = "Proxmox Support Team " + +[source] +vcs_git = "git://git.proxmox.com/git/proxmox.git" +vcs_browser = "https://git.proxmox.com/?p=proxmox.git" diff --git a/proxmox/src/api/api_type_macros.rs b/proxmox-schema/src/api_type_macros.rs similarity index 91% rename from proxmox/src/api/api_type_macros.rs rename to proxmox-schema/src/api_type_macros.rs index 60d5adaa..f3740d14 100644 --- a/proxmox/src/api/api_type_macros.rs +++ b/proxmox-schema/src/api_type_macros.rs @@ -2,11 +2,10 @@ /// /// 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; +/// ``` +/// # use proxmox_schema::{api_string_type, ApiStringFormat}; +/// # use proxmox_api_macro::api; /// # const PROXMOX_SAFE_ID_FORMAT: ApiStringFormat = ApiStringFormat::Enum(&[]); -/// use proxmox::api_string_type; /// use serde::{Deserialize, Serialize}; /// /// api_string_type! { @@ -92,11 +91,12 @@ macro_rules! api_string_type { } /// Create an instance directly from a `String`, validating it using the API schema's - /// [`check_constraints`](::proxmox::api::schema::StringSchema::check_constraints()) + /// [`check_constraints`](::proxmox_schema::StringSchema::check_constraints()) /// method. pub fn from_string(inner: String) -> Result { + use $crate::ApiType; match &Self::API_SCHEMA { - ::proxmox::api::schema::Schema::String(s) => s.check_constraints(&inner)?, + $crate::Schema::String(s) => s.check_constraints(&inner)?, _ => unreachable!(), } Ok(Self(inner)) diff --git a/proxmox/src/api/const_regex.rs b/proxmox-schema/src/const_regex.rs similarity index 88% rename from proxmox/src/api/const_regex.rs rename to proxmox-schema/src/const_regex.rs index f3f16b6d..3a5c2dab 100644 --- a/proxmox/src/api/const_regex.rs +++ b/proxmox-schema/src/const_regex.rs @@ -30,8 +30,8 @@ impl std::ops::Deref for ConstRegexPattern { /// Macro to generate a ConstRegexPattern /// /// ``` -/// # use proxmox::const_regex; -/// # +/// use proxmox_schema::const_regex; +/// /// const_regex!{ /// FILE_EXTENSION_REGEX = r".*\.([a-zA-Z]+)$"; /// pub SHA256_HEX_REGEX = r"^[a-f0-9]{64}$"; @@ -43,11 +43,11 @@ 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 { + $(#[$attr])* $vis const $name: $crate::ConstRegexPattern = + $crate::ConstRegexPattern { regex_string: $regex, regex_obj: (|| -> &'static ::regex::Regex { - ::lazy_static::lazy_static! { + $crate::semver_exempt::lazy_static! { static ref SCHEMA: ::regex::Regex = ::regex::Regex::new($regex).unwrap(); } &SCHEMA diff --git a/proxmox/src/api/de.rs b/proxmox-schema/src/de.rs similarity index 95% rename from proxmox/src/api/de.rs rename to proxmox-schema/src/de.rs index 254e6688..c1a8ae98 100644 --- a/proxmox/src/api/de.rs +++ b/proxmox-schema/src/de.rs @@ -5,21 +5,21 @@ use std::fmt; use serde::de::{self, IntoDeserializer, Visitor}; use serde_json::Value; -use crate::api::schema::{ObjectSchemaType, Schema}; +use crate::{ObjectSchemaType, Schema}; pub struct Error { - inner: anyhow::Error, + msg: String, } impl fmt::Debug for Error { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - fmt::Debug::fmt(&self.inner, f) + fmt::Debug::fmt(&self.msg, f) } } impl fmt::Display for Error { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - fmt::Display::fmt(&self.inner, f) + fmt::Display::fmt(&self.msg, f) } } @@ -28,15 +28,15 @@ impl std::error::Error for Error {} impl serde::de::Error for Error { fn custom(msg: T) -> Self { Self { - inner: anyhow::format_err!("{}", msg), + msg: msg.to_string(), } } } impl From for Error { - fn from(inner: serde_json::Error) -> Self { + fn from(error: serde_json::Error) -> Self { Error { - inner: inner.into(), + msg: error.to_string(), } } } @@ -237,7 +237,7 @@ where fn test_extraction() { use serde::Deserialize; - use crate::api::schema::{ObjectSchema, StringSchema}; + use crate::{ObjectSchema, StringSchema}; #[derive(Deserialize)] struct Foo { diff --git a/proxmox/src/api/format.rs b/proxmox-schema/src/format.rs similarity index 76% rename from proxmox/src/api/format.rs rename to proxmox-schema/src/format.rs index 64fcd0af..104b1c08 100644 --- a/proxmox/src/api/format.rs +++ b/proxmox-schema/src/format.rs @@ -2,11 +2,7 @@ use anyhow::{bail, Error}; -use std::io::Write; - -use crate::api::{ - router::ReturnType, schema::*, section_config::SectionConfig, ApiHandler, ApiMethod, -}; +use crate::*; /// Enumerate different styles to display parameters/properties. #[derive(Copy, Clone, PartialEq)] @@ -74,128 +70,7 @@ fn test_wrap_text() { assert_eq!(wrapped, expect); } -/// Helper to format the type text -/// -/// The result is a short string including important constraints, for -/// example `` (0 - N)``. -pub fn get_schema_type_text(schema: &Schema, _style: ParameterDisplayStyle) -> String { - match schema { - Schema::Null => String::from(""), // 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 = - 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(""), - } - } - Schema::Boolean(_) => String::from(""), - Schema::Integer(integer_schema) => match (integer_schema.minimum, integer_schema.maximum) { - (Some(min), Some(max)) => format!(" ({} - {})", min, max), - (Some(min), None) => format!(" ({} - N)", min), - (None, Some(max)) => format!(" (-N - {})", max), - _ => String::from(""), - }, - Schema::Number(number_schema) => match (number_schema.minimum, number_schema.maximum) { - (Some(min), Some(max)) => format!(" ({} - {})", min, max), - (Some(min), None) => format!(" ({} - N)", min), - (None, Some(max)) => format!(" (-N - {})", max), - _ => String::from(""), - }, - Schema::Object(_) => String::from(""), - Schema::Array(schema) => get_schema_type_text(schema.items, _style), - Schema::AllOf(_) => String::from(""), - } -} - -/// 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 { +fn get_simple_type_text(schema: &Schema, list_enums: bool) -> String { match schema { Schema::Null => String::from(""), // should not happen Schema::Boolean(_) => String::from("<1|0>"), @@ -220,98 +95,10 @@ fn get_simply_type_text(schema: &Schema, list_enums: bool) -> String { } _ => String::from(""), }, - _ => panic!("get_simply_type_text: expected simply type"), + _ => panic!("get_simple_type_text: expected simple 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 { - 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, @@ -395,7 +182,236 @@ pub fn dump_properties( res } -fn dump_api_return_schema(returns: &ReturnType, style: ParameterDisplayStyle) -> String { +/// 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 `` (0 - N)``. +pub fn get_schema_type_text(schema: &Schema, _style: ParameterDisplayStyle) -> String { + match schema { + Schema::Null => String::from(""), // 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 = + 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(""), + } + } + Schema::Boolean(_) => String::from(""), + Schema::Integer(integer_schema) => match (integer_schema.minimum, integer_schema.maximum) { + (Some(min), Some(max)) => format!(" ({} - {})", min, max), + (Some(min), None) => format!(" ({} - N)", min), + (None, Some(max)) => format!(" (-N - {})", max), + _ => String::from(""), + }, + Schema::Number(number_schema) => match (number_schema.minimum, number_schema.maximum) { + (Some(min), Some(max)) => format!(" ({} - {})", min, max), + (Some(min), None) => format!(" ({} - N)", min), + (None, Some(max)) => format!(" (-N - {})", max), + _ => String::from(""), + }, + Schema::Object(_) => String::from(""), + Schema::Array(schema) => get_schema_type_text(schema.items, _style), + Schema::AllOf(_) => String::from(""), + } +} + +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 { + 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 { @@ -447,114 +463,3 @@ fn dump_api_return_schema(returns: &ReturnType, style: ParameterDisplayStyle) -> res } - -fn dump_method_definition(method: &str, path: &str, def: Option<&ApiMethod>) -> Option { - 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-schema/src/lib.rs b/proxmox-schema/src/lib.rs new file mode 100644 index 00000000..a4e3ba4c --- /dev/null +++ b/proxmox-schema/src/lib.rs @@ -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/src/api/schema.rs b/proxmox-schema/src/schema.rs similarity index 96% rename from proxmox/src/api/schema.rs rename to proxmox-schema/src/schema.rs index c9f659e6..34135f45 100644 --- a/proxmox/src/api/schema.rs +++ b/proxmox-schema/src/schema.rs @@ -8,9 +8,8 @@ use std::fmt; use anyhow::{bail, format_err, Error}; use serde_json::{json, Value}; -use url::form_urlencoded; -use crate::api::const_regex::ConstRegexPattern; +use crate::ConstRegexPattern; /// Error type for schema validation /// @@ -50,13 +49,15 @@ impl ParameterError { pub fn add_errors(&mut self, prefix: &str, err: Error) { if let Some(param_err) = err.downcast_ref::() { for (sub_key, sub_err) in param_err.errors().iter() { - self.push(format!("{}/{}", prefix, sub_key), format_err!("{}", sub_err)); + self.push( + format!("{}/{}", prefix, sub_key), + format_err!("{}", sub_err), + ); } } else { self.push(prefix.to_string(), err); } } - } impl fmt::Display for ParameterError { @@ -623,8 +624,8 @@ impl Iterator for ObjectPropertyIterator { /// `schema()` method to convert them into a `Schema`. /// /// ``` -/// # use proxmox::api::{*, 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! @@ -684,7 +685,8 @@ impl EnumEntry { /// Simple list all possible values. /// /// ``` -/// # use proxmox::api::{*, schema::*}; +/// 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"), @@ -696,8 +698,8 @@ impl EnumEntry { /// Use a regular expression to describe valid strings. /// /// ``` -/// # use proxmox::api::{*, schema::*}; -/// # use proxmox::const_regex; +/// use proxmox_schema::{const_regex, ApiStringFormat}; +/// /// const_regex! { /// pub SHA256_HEX_REGEX = r"^[a-f0-9]{64}$"; /// } @@ -721,8 +723,9 @@ impl EnumEntry { /// with simple properties (no nesting). /// /// ``` -/// # use proxmox::api::{*, schema::*}; -/// # +/// 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) @@ -961,7 +964,9 @@ fn do_parse_parameter_strings( Err(err) => errors.push(key.into(), err), } } - _ => errors.push(key.into(), format_err!("expected array - type missmatch")), + _ => { + errors.push(key.into(), format_err!("expected array - type missmatch")) + } } } _ => match parse_simple_value(value, prop_schema) { @@ -992,14 +997,20 @@ fn do_parse_parameter_strings( _ => errors.push(key.into(), format_err!("expected array - type missmatch")), } } else { - errors.push(key.into(), format_err!("schema does not allow additional properties.")); + 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.")); + errors.push( + name.to_string(), + format_err!("parameter is missing and it is not optional."), + ); } } } @@ -1011,21 +1022,6 @@ fn do_parse_parameter_strings( } } -/// 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>( - query: &str, - schema: T, - test_required: bool, -) -> Result { - let param_list: Vec<(String, String)> = form_urlencoded::parse(query.as_bytes()) - .into_owned() - .collect(); - - parse_parameter_strings(¶m_list, schema.into(), test_required) -} - /// Verify JSON value with `schema`. pub fn verify_json(data: &Value, schema: &Schema) -> Result<(), Error> { match schema { @@ -1103,10 +1099,7 @@ pub fn verify_json_array(data: &Value, schema: &ArraySchema) -> Result<(), Error } /// Verify JSON value using an `ObjectSchema`. -pub fn verify_json_object( - data: &Value, - schema: &dyn ObjectSchemaType, -) -> Result<(), Error> { +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."), @@ -1128,13 +1121,19 @@ pub fn verify_json_object( errors.add_errors(key, err); }; } else if !additional_properties { - errors.push(key.to_string(), format_err!("schema does not allow 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.")); + errors.push( + name.to_string(), + format_err!("property is missing and it is not optional."), + ); } } diff --git a/proxmox-schema/src/upid.rs b/proxmox-schema/src/upid.rs new file mode 100644 index 00000000..22d878b6 --- /dev/null +++ b/proxmox-schema/src/upid.rs @@ -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, + /// 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[a-zA-Z0-9]([a-zA-Z0-9\-]*[a-zA-Z0-9])?):(?P[0-9A-Fa-f]{8}):", + r"(?P[0-9A-Fa-f]{8,9}):(?P[0-9A-Fa-f]{8,16}):(?P[0-9A-Fa-f]{8}):", + r"(?P[^:\s]+):(?P[^:\s]*):(?P[^:\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 { + 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(&self, serializer: S) -> Result + where + S: serde::ser::Serializer, + { + serializer.serialize_str(&ToString::to_string(self)) + } +} + +impl<'de> serde::Deserialize<'de> for UPID { + fn deserialize(deserializer: D) -> Result + 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(self, v: &str) -> Result { + v.parse::().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 { + 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 { + let mut i = text.as_bytes(); + + let mut data: Vec = 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, + auth_id: String, + ) -> Result { + 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 { + 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/src/test/schema.rs b/proxmox-schema/tests/schema.rs similarity index 96% rename from proxmox/src/test/schema.rs rename to proxmox-schema/tests/schema.rs index c3f20828..4b4a8371 100644 --- a/proxmox/src/test/schema.rs +++ b/proxmox-schema/tests/schema.rs @@ -1,6 +1,20 @@ use anyhow::bail; +use serde_json::Value; +use url::form_urlencoded; -use crate::api::schema::*; +use proxmox_schema::*; + +fn parse_query_string>( + query: &str, + schema: T, + test_required: bool, +) -> Result { + let param_list: Vec<(String, String)> = form_urlencoded::parse(query.as_bytes()) + .into_owned() + .collect(); + + parse_parameter_strings(¶m_list, schema.into(), test_required) +} #[test] fn test_schema1() { diff --git a/proxmox/src/test/schema_verification.rs b/proxmox-schema/tests/schema_verification.rs similarity index 98% rename from proxmox/src/test/schema_verification.rs rename to proxmox-schema/tests/schema_verification.rs index 4937de8c..2ba4455b 100644 --- a/proxmox/src/test/schema_verification.rs +++ b/proxmox-schema/tests/schema_verification.rs @@ -1,7 +1,7 @@ use anyhow::{bail, Error}; use serde_json::{json, Value}; -use crate::api::schema::*; +use proxmox_schema::*; static STRING_SCHEMA: Schema = StringSchema::new("A test string").schema(); @@ -46,7 +46,7 @@ fn compare_error( None => bail!("unable to downcast error: {}", err), }; - let result = crate::try_block!({ + let result = (move || { let errors = err.errors(); if errors.len() != expected.len() { @@ -64,7 +64,7 @@ fn compare_error( } Ok(()) - }); + })(); if result.is_err() { println!("GOT: {:?}", err); diff --git a/proxmox/Cargo.toml b/proxmox/Cargo.toml index b26b5a09..67fa98af 100644 --- a/proxmox/Cargo.toml +++ b/proxmox/Cargo.toml @@ -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/mod.rs b/proxmox/src/api/mod.rs deleted file mode 100644 index 18b5cd26..00000000 --- a/proxmox/src/api/mod.rs +++ /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/section_config.rs b/proxmox/src/api/section_config.rs index 564a7c77..3fbaf705 100644 --- a/proxmox/src/api/section_config.rs +++ b/proxmox/src/api/section_config.rs @@ -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 index 8f03a204..00000000 --- a/proxmox/src/api/upid.rs +++ /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, - /// 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[a-zA-Z0-9]([a-zA-Z0-9\-]*[a-zA-Z0-9])?):(?P[0-9A-Fa-f]{8}):", - r"(?P[0-9A-Fa-f]{8,9}):(?P[0-9A-Fa-f]{8,16}):(?P[0-9A-Fa-f]{8}):", - r"(?P[^:\s]+):(?P[^:\s]*):(?P[^:\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, - auth_id: String, - ) -> Result { - - 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 { - 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) - } -} diff --git a/proxmox/src/lib.rs b/proxmox/src/lib.rs index 6e959064..8d6bcfcf 100644 --- a/proxmox/src/lib.rs +++ b/proxmox/src/lib.rs @@ -4,7 +4,6 @@ #[macro_use] pub mod serde_macros; -pub mod api; pub mod sys; pub mod tools; diff --git a/proxmox/src/sys/macros.rs b/proxmox/src/sys/macros.rs index da6f7bd7..b5bd982c 100644 --- a/proxmox/src/sys/macros.rs +++ b/proxmox/src/sys/macros.rs @@ -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/tools/as_any.rs b/proxmox/src/tools/as_any.rs deleted file mode 100644 index 39eaedc8..00000000 --- a/proxmox/src/tools/as_any.rs +++ /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 AsAny for T { - fn as_any(&self) -> &dyn Any { - self - } -} diff --git a/proxmox/src/tools/mod.rs b/proxmox/src/tools/mod.rs index 1c426666..0ab8aed3 100644 --- a/proxmox/src/tools/mod.rs +++ b/proxmox/src/tools/mod.rs @@ -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.