rusty_vault 0.2.1

RustyVault is a powerful identity-based secrets management software, providing features such as cryptographic key management, encryption as a service, public key cryptography, certificates management, identity credentials management and so forth. RustyVault's RESTful API is designed to be fully compatible with Hashicorp Vault.
Documentation
use std::{collections::HashMap, fmt, sync::Arc};

use super::{request::Request, response::Response, Backend, Field, Operation};
use crate::{context::Context, errors::RvError};

type PathOperationHandler = dyn Fn(&dyn Backend, &mut Request) -> Result<Option<Response>, RvError> + Send + Sync;

#[derive(Debug, Clone)]
pub struct Path {
    pub ctx: Arc<Context>,
    pub pattern: String,
    pub fields: HashMap<String, Arc<Field>>,
    pub operations: Vec<PathOperation>,
    pub help: String,
}

#[derive(Clone)]
pub struct PathOperation {
    pub op: Operation,
    pub handler: Arc<PathOperationHandler>,
}

impl fmt::Debug for PathOperation {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        f.debug_struct("PathOperation").field("op", &self.op).finish()
    }
}

impl Path {
    pub fn new(pattern: &str) -> Self {
        Self {
            ctx: Arc::new(Context::new()),
            pattern: pattern.to_string(),
            fields: HashMap::new(),
            operations: Vec::new(),
            help: String::new(),
        }
    }

    pub fn get_field(&self, key: &str) -> Option<Arc<Field>> {
        self.fields.get(key).cloned()
    }
}

impl PathOperation {
    pub fn new() -> Self {
        Self {
            op: Operation::Read,
            handler: Arc::new(|_backend: &dyn Backend, _req: &mut Request| -> Result<Option<Response>, RvError> {
                Ok(None)
            }),
        }
    }

    pub fn handle_request(&self, backend: &dyn Backend, req: &mut Request) -> Result<Option<Response>, RvError> {
        (self.handler)(backend, req)
    }
}

#[macro_export]
macro_rules! new_fields {
    ($($tt:tt)*) => {
        new_fields_internal!($($tt)*)
    };
}

#[macro_export]
#[doc(hidden)]
macro_rules! new_fields_internal {
    (@object $object:ident field_type: $field_type:expr) => {
        $object.field_type = $field_type;
    };
    (@object $object:ident required: $required:expr) => {
        $object.required = $required;
    };
    (@object $object:ident default: $default:expr) => {
        let val = serde_json::json!($default);
        $object.default = val;
        $object.required = false;
    };
    (@object $object:ident description: $description:expr) => {
        $object.description = $description.to_string();
    };
    (@object $object:ident field_tt: {$($key:ident: $value:expr),*}) => {
        {
            let mut field = Field::new();

            $(
                new_fields_internal!(@object field $key: $value);
            )*

            field
        }
    };
    (@object $object:ident $($field_name:tt: $field_tt:tt),*) => {
        $(
            $object.insert($field_name.to_string(),
                           Arc::new(new_fields_internal!(@object $object field_tt: $field_tt)));
        )*
    };
    ({ $($tt:tt)+ }) => {
        {
            let mut fields = HashMap::new();
            new_fields_internal!(@object fields $($tt)+);
            fields
        }
    };
}

#[macro_export]
macro_rules! new_path {
    ($($tt:tt)*) => {
        new_path_internal!($($tt)*)
    };
}

#[macro_export]
#[doc(hidden)]
macro_rules! new_path_internal {
    (@object $object:ident pattern: $pattern:expr) => {
        $object.pattern = $pattern.to_string();
    };
    (@object $object:ident help: $help:expr) => {
        $object.help = $help.to_string();
    };
    (@object $object:ident
        fields: {
            $($tt:tt)+
        }
    ) => {
        $object.fields = new_fields!({ $($tt)+ });
    };
    (@object $object:ident op: $op:expr) => {
        $object.op = $op;
    };
    (@operation $operation:ident []) => {
    };
    (@operation $object:ident [{op: $op:expr, handler: $handler_obj:ident$(.$handler_method:ident)*} $($rest:tt)*]) => {
        let mut path_op = PathOperation::new();

        path_op.op = $op;
        path_op.handler = Arc::new(move |backend: &dyn Backend, req: &mut Request| -> Result<Option<Response>, RvError> {
            $handler_obj$(.$handler_method)*(backend, req)
        });

        $object.operations.push(path_op);

        new_path_internal!(@operation $object [$($rest)*]);
    };
    (@operation $object:ident [{op: $op:expr, raw_handler: $handler:expr} $($rest:tt)*]) => {
        let mut path_op = PathOperation::new();

        path_op.op = $op;
        path_op.handler = Arc::new($handler);

        $object.operations.push(path_op);

        new_path_internal!(@operation $object [$($rest)*]);
    };
    (@object $object:ident
        operations: [
            $($operation:tt),* $(,)?
        ]
    ) => {
        new_path_internal!(@operation $object [$($operation)*]);
    };
    (@object $object:ident () $($key:ident: $value:tt),*) => {
        $(
            new_path_internal!(@object $object $key: $value);
        )*
    };
    ({ $($tt:tt)+ }) => {
        {
            let mut path = Path {
                ctx: Arc::new(Context::new()),
                pattern: String::new(),
                fields: HashMap::new(),
                operations: Vec::new(),
                help: String::new(),
            };
            new_path_internal!(@object path () $($tt)+);
            path
        }
    };
}

#[cfg(test)]
mod test {
    use super::{super::FieldType, *};

    pub fn my_test_read_handler(_backend: &dyn Backend, _req: &mut Request) -> Result<Option<Response>, RvError> {
        Ok(None)
    }

    #[test]
    fn test_logical_path() {
        let path: Path = new_path!({
            pattern: "/aa",
            fields: {
                "mytype": {
                    field_type: FieldType::Int,
                    description: "haha"
                },
                "mypath": {
                    field_type: FieldType::Str,
                    description: "hehe"
                }
            },
            operations: [
                {op: Operation::Read, handler: my_test_read_handler},
                {op: Operation::Write, raw_handler: |_backend: &dyn Backend, _req: &mut Request| -> Result<Option<Response>, RvError> {
                        Err(RvError::ErrUnknown)
                    }
                }
            ],
            help: "testhelp"
        });

        assert_eq!(&path.pattern, "/aa");
        assert_eq!(&path.help, "testhelp");
        assert!(path.fields.get("mytype").is_some());
        assert_eq!(path.fields["mytype"].field_type, FieldType::Int);
        assert_eq!(path.fields["mytype"].description, "haha");
        assert!(path.fields.get("mypath").is_some());
        assert_eq!(path.fields["mypath"].field_type, FieldType::Str);
        assert_eq!(path.fields["mypath"].description, "hehe");
        assert!(path.fields.get("xxfield").is_none());
        assert_eq!(path.operations[0].op, Operation::Read);
        assert_eq!(path.operations[1].op, Operation::Write);
        assert_eq!(path.operations.len(), 2);
    }
}