use std::fmt::{Debug, Display};
use serde::{Deserialize, Serialize};
use tera::{Context, Tera};
use thiserror::Error;
use uuid::Uuid;
use super::rendered_storage::RenderedStorage;
use super::StorageAccessMode;
use crate::catlib_service::entities::ContainerPath;
use crate::catlib_service::error::CatlibError;
use crate::validator::ValidatedTemplateData;
use crate::ErrContext;
pub const CONTAINER_NAME_PARAM: &str = "CONTAINER_NAME";
pub const OWNER_PARAM: &str = "OWNER";
pub const ACCESS_MODE_PARAM: &str = "ACCESS_MODE"; pub const CONTAINER_UUID_PARAM: &str = "CONTAINER_UUID";
pub const PATH_PARAM: &str = "PATH";
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TemplateContext {
#[serde(rename = "CONTAINER_NAME")]
pub container_name: String,
#[serde(rename = "OWNER")]
pub owner: String,
#[serde(rename = "ACCESS_MODE")]
pub access_mode: StorageAccessMode,
#[serde(rename = "CONTAINER_UUID")]
pub container_uuid: Uuid,
#[serde(rename = "PATH")]
pub path: ContainerPath,
}
#[derive(Debug, Error, Clone)]
#[repr(C)]
pub enum StorageTemplateError {
#[error("Ser/deserialization error: {0}")]
SerdeErr(String),
#[error("Template engine error: {0}")]
TemplateEngineErr(String),
#[error("Catlib error: {0}: {1}")]
CatlibErr(String, CatlibError),
}
impl<T> ErrContext<StorageTemplateError, T> for Result<T, serde_json::Error> {
fn context(self, ctx: impl Display) -> Result<T, StorageTemplateError> {
self.map_err(|e| StorageTemplateError::SerdeErr(Self::format(e, ctx)))
}
}
impl<T> ErrContext<StorageTemplateError, T> for Result<T, serde_yaml::Error> {
fn context(self, ctx: impl Display) -> Result<T, StorageTemplateError> {
self.map_err(|e| StorageTemplateError::SerdeErr(Self::format(e, ctx)))
}
}
impl<T> ErrContext<StorageTemplateError, T> for Result<T, tera::Error> {
fn context(self, ctx: impl Display) -> Result<T, StorageTemplateError> {
self.map_err(|e| StorageTemplateError::TemplateEngineErr(Self::format(e, ctx)))
}
}
impl<T> ErrContext<StorageTemplateError, T> for Result<T, CatlibError> {
fn context(self, ctx: impl Display) -> Result<T, StorageTemplateError> {
self.map_err(|e| StorageTemplateError::CatlibErr(ctx.to_string(), e))
}
}
pub const BACKEND_TYPE_KEY: &str = "backend_type";
pub const TEMPLATE_KEY: &str = "template";
pub const VERSION_KEY: &str = "version";
pub const NAME_KEY: &str = "name";
#[derive(Serialize, Deserialize, Clone, Debug)]
#[serde(tag = "version")]
pub enum StorageTemplate {
#[serde(rename = "1")]
V1(StorageTemplateV1),
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct StorageTemplateV1 {
#[serde(skip)]
catlib_uuid: Option<Uuid>,
name: Option<String>,
backend_type: String,
template: ValidatedTemplateData,
}
impl StorageTemplate {
pub fn new(
backend_type: impl ToString,
template: ValidatedTemplateData,
name: Option<String>,
) -> Self {
Self::V1(StorageTemplateV1 {
name,
catlib_uuid: None,
backend_type: backend_type.to_string(),
template,
})
}
pub(crate) fn with_catlib_uuid(mut self, uuid: Uuid) -> Self {
match &mut self {
StorageTemplate::V1(template) => {
template.catlib_uuid = Some(uuid);
self
}
}
}
pub fn set_catlib_uuid(&mut self, uuid: Uuid) {
match self {
StorageTemplate::V1(template) => {
template.catlib_uuid = Some(uuid);
}
}
}
pub fn name(&self) -> Option<String> {
match self {
StorageTemplate::V1(template) => template.name.clone(),
}
}
pub fn set_name(&mut self, name: String) {
match self {
StorageTemplate::V1(template) => template.name = Some(name),
}
}
pub fn to_json(&self) -> Result<String, StorageTemplateError> {
serde_json::to_string(&self).context("Error while converting template to json")
}
pub fn to_yaml(&self) -> Result<String, StorageTemplateError> {
serde_yaml::to_string(&self).context("Error while converting template to yaml")
}
pub fn backend_type(&self) -> String {
match self {
StorageTemplate::V1(template) => template.backend_type.clone(),
}
}
pub fn template(&self) -> &serde_json::Value {
match self {
StorageTemplate::V1(tv1) => &tv1.template.0,
}
}
pub fn catlib_uuid(&self) -> Option<Uuid> {
match self {
StorageTemplate::V1(template) => template.catlib_uuid,
}
}
pub fn render(&self, params: TemplateContext) -> Result<RenderedStorage, StorageTemplateError> {
let template_str = serde_json::to_string(&self.template())
.context("Error deserializing template while rendering")?;
let filled_template = Tera::one_off(
&template_str,
&Context::from_serialize(params)
.context("Error while deserializing template params")?,
true,
)
.context("Error while filling template with params")?;
let storage_data: serde_json::Value = serde_json::from_str(&filled_template)
.context("Deserialize Storage Error while rendering template")?;
Ok(RenderedStorage {
name: self.name(),
backend_type: self.backend_type(),
data: storage_data,
})
}
}
#[cfg(test)]
mod tests {
use std::collections::HashMap;
use std::str::FromStr;
use std::sync::Arc;
use pretty_assertions::assert_eq;
use uuid::Uuid;
use crate::template_factory::TemplateFactory;
use crate::{
GenericValidator,
StorageTemplate,
StorageTemplateV1,
TemplateContext,
ValidatedTemplateData,
};
#[test]
fn parse_generic_json_template() {
let json_str = serde_json::json!({
"version": "1",
"template": {
"access":
[
{
"user": "*"
}
],
"credentials":
{
"access-key": "NOT_SO_SECRET",
"secret-key": "VERY_SECRET"
},
"manifest-pattern":
{
"path": "/{path}.yaml",
"type": "glob"
},
"read-only": true,
"s3_url": "s3://michal-afc03a81-307c-4b41-b9dd-771835617900/{{ CONTAINER_UUID }}",
"with-index": false
},
"backend_type": "s3",
})
.to_string();
let template_factory = TemplateFactory::new(Arc::new(HashMap::from([(
"s3".into(),
Box::<GenericValidator<HashMap<String, serde_json::Value>>>::default() as _,
)])));
let mut tpl = template_factory
.storage_template_from_json(json_str.as_bytes())
.unwrap();
assert_eq!(tpl.name(), None);
tpl.set_name("random name".to_string());
assert_eq!(tpl.name(), Some("random name".to_string()));
}
#[test]
fn parse_generic_yaml_template() {
let yaml_content = "
version: '1'
template:
access:
- user1: '*'
credentials:
access-key: NOT_SO_SECRET
secret-key: VERY_SECRET
manifest-pattern:
path: /{path}.yaml
type: glob
read-only: true
s3_url: s3://michal-afc03a81-307c-4b41-b9dd-771835617900/{{ CONTAINER_UUID }}
with-index: false
backend_type: s3
";
let template_factory = TemplateFactory::new(Arc::new(HashMap::from([(
"s3".into(),
Box::<GenericValidator<HashMap<String, serde_json::Value>>>::default() as _,
)])));
let mut tpl = template_factory
.storage_template_from_yaml(yaml_content.as_bytes())
.unwrap();
assert_eq!(tpl.name(), None);
tpl.set_name("random name".to_string());
assert_eq!(tpl.name(), Some("random name".to_string()));
}
#[test]
fn test_rendering_template() {
let storage_template = StorageTemplate::new(
"InvalidTemplate",
ValidatedTemplateData(
serde_json::to_value(HashMap::from([
(
"field1".to_owned(),
"Some value with container name: {{ CONTAINER_NAME }}".to_owned(),
),
(
"parameter in key: {{ OWNER }}".to_owned(),
"enum: {{ ACCESS_MODE }}".to_owned(),
),
("uuid".to_owned(), "{{ CONTAINER_UUID }}".to_owned()),
("path".to_owned(), "{{ PATH }}".to_owned()),
]))
.unwrap(),
),
None,
);
let params = TemplateContext {
container_name: "Books".to_owned(),
owner: "John Doe".to_owned(),
access_mode: crate::StorageAccessMode::ReadOnly,
container_uuid: Uuid::from_str("00000000-0000-0000-0000-000000001111").unwrap(),
path: "path1".into(),
};
let rendered_storage = storage_template.render(params).unwrap();
let expected_storage_toml = r#"backend_type = "InvalidTemplate"
[data]
field1 = "Some value with container name: Books"
"parameter in key: John Doe" = "enum: ReadOnly"
path = "path1"
uuid = "00000000-0000-0000-0000-000000001111"
"#;
assert_eq!(
toml::Value::try_from(rendered_storage).unwrap(),
toml::Value::from_str(expected_storage_toml).unwrap()
);
}
#[test]
fn test_to_json() {
let template = StorageTemplate::V1(StorageTemplateV1 {
catlib_uuid: Some(Uuid::from_u128(1)),
name: Some("name".into()),
backend_type: "backend type".into(),
template: ValidatedTemplateData(
serde_json::to_value(HashMap::from([("a", "b")])).unwrap(),
),
});
let json = serde_json::to_value(template).unwrap();
let expected = serde_json::json!({
"version": "1",
"name": "name",
"backend_type": "backend type",
"template": {
"a": "b"
}
});
assert_eq!(json, expected);
}
#[test]
fn test_to_yaml() {
let template = StorageTemplate::V1(StorageTemplateV1 {
catlib_uuid: Some(Uuid::from_u128(1)),
name: Some("name".into()),
backend_type: "backend type".into(),
template: ValidatedTemplateData(
serde_json::to_value(serde_json::to_value(HashMap::from([("a", "b")])).unwrap())
.unwrap(),
),
});
let yaml = serde_yaml::to_value(template).unwrap();
let expected: serde_yaml::Value = serde_yaml::from_str(
r#"
version: '1'
name: name
backend_type: 'backend type'
template:
a: b
"#,
)
.unwrap();
assert_eq!(yaml, expected);
}
}