archival 0.15.0

The simplest CMS in existence
Documentation
use serde::{Deserialize, Serialize};
use std::borrow::Cow;
use std::fmt::{Debug, Display};
use thiserror::Error;

use crate::manifest::EditorTypes;

#[cfg(feature = "json-schema")]
use super::{file::DisplayType, File};

#[derive(Error, Debug, Clone)]
pub enum InvalidFieldError {
    #[error("unrecognized type {0}")]
    UnrecognizedType(String),
    #[error("invalid enum {0} - only string enums supported.")]
    InvalidEnum(String),
    #[error("invalid oneof {0:?} - parse failed")]
    InvalidOneof(String),
    #[error("invalid date {0}")]
    InvalidDate(String),
    #[error(
        "type mismatch for field {field:?} - expected type {field_type:?}, got value {value:?}"
    )]
    TypeMismatch {
        field: String,
        field_type: String,
        value: String,
    },
    #[error(
        "oneof mismatch for field {field:?} - expected type {value:?} to be in {field_type:?}"
    )]
    OneofMismatch {
        field: String,
        field_type: String,
        value: String,
    },
    #[error(
        "enum mismatch for field {field:?} - expected value {value:?} to be in {field_type:?}"
    )]
    EnumMismatch {
        field: String,
        field_type: String,
        value: String,
    },
    #[error("invalid child {key:?}[{index:?}] {child:?}")]
    InvalidChild {
        key: String,
        index: usize,
        child: String,
    },
    #[error("not an array: {key:?} ({value:?})")]
    NotAnArray { key: String, value: String },
    #[error("cannot define an object with reserved name {0}")]
    ReservedObjectNameError(String),
    #[error("cannot create type {0} from a string value")]
    UnsupportedStringValue(String),
    #[error("type {0} was not provided a value and has no default")]
    NoDefaultForType(String),
}

#[cfg(feature = "typescript")]
mod typedefs {
    use typescript_type_def::{
        type_expr::{Ident, NativeTypeInfo, TypeExpr, TypeInfo},
        TypeDef,
    };
    pub struct AliasTypeDef;
    impl TypeDef for AliasTypeDef {
        const INFO: TypeInfo = TypeInfo::Native(NativeTypeInfo {
            r#ref: TypeExpr::ident(Ident("[FieldType, string]")),
        });
    }
    pub struct FieldTypeDef;
    impl TypeDef for FieldTypeDef {
        const INFO: TypeInfo = TypeInfo::Native(NativeTypeInfo {
            r#ref: TypeExpr::ident(Ident("FieldType")),
        });
    }
}

#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "typescript", derive(typescript_type_def::TypeDef))]
pub struct OneofOption {
    pub name: String,
    // Avoid cycle by just inlining the def
    #[cfg_attr(feature = "typescript", type_def(type_of = "typedefs::FieldTypeDef"))]
    pub r#type: FieldType,
}
#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "typescript", derive(typescript_type_def::TypeDef))]
pub enum FieldType {
    String,
    Number,
    Date,
    Enum(Vec<String>),
    Markdown,
    Boolean,
    Image,
    Video,
    Upload,
    Audio,
    Meta,
    Oneof(Vec<OneofOption>),
    Alias(
        #[cfg_attr(feature = "typescript", type_def(type_of = "typedefs::AliasTypeDef"))]
        Box<(FieldType, String)>,
    ),
}

impl FieldType {
    pub fn as_str<'a>(&'a self) -> Cow<'a, str> {
        match self {
            Self::String => "string".into(),
            Self::Number => "number".into(),
            Self::Enum(v) => format!("[{}]", v.join(",")).into(),
            Self::Date => "date".into(),
            Self::Markdown => "markdown".into(),
            Self::Boolean => "boolean".into(),
            Self::Image => "image".into(),
            Self::Video => "video".into(),
            Self::Audio => "audio".into(),
            Self::Upload => "upload".into(),
            Self::Meta => "meta".into(),
            Self::Oneof(v) => v
                .iter()
                .map(|f| format!("{}:{}", f.name, f.r#type.as_str()))
                .collect::<Vec<_>>()
                .join("|")
                .to_string()
                .into(),
            Self::Alias(a) => a.0.as_str(),
        }
    }
    pub fn from_str(
        string: &str,
        editor_types: &EditorTypes,
    ) -> Result<FieldType, InvalidFieldError> {
        match string {
            "string" => Ok(FieldType::String),
            // Note that enums are only supported via direct instantiation
            "number" => Ok(FieldType::Number),
            "date" => Ok(FieldType::Date),
            "markdown" => Ok(FieldType::Markdown),
            "boolean" => Ok(FieldType::Boolean),
            "image" => Ok(FieldType::Image),
            "video" => Ok(FieldType::Video),
            "audio" => Ok(FieldType::Audio),
            "upload" => Ok(FieldType::Upload),
            "meta" => Ok(FieldType::Meta),
            // Note that oneofs are only supported via direct instantiation
            t => {
                if let Some(et) = editor_types.get(t) {
                    Ok(FieldType::Alias(Box::new((
                        FieldType::from_str(&et.alias_of, editor_types)?,
                        t.to_string(),
                    ))))
                } else {
                    Err(InvalidFieldError::UnrecognizedType(string.to_string()))
                }
            }
        }
    }
}

impl Display for FieldType {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "{}", self.as_str())
    }
}

#[cfg(feature = "json-schema")]
impl FieldType {
    fn maybe_file_type(&self) -> Option<DisplayType> {
        match self {
            FieldType::Image => Some(DisplayType::Image),
            FieldType::Video => Some(DisplayType::Video),
            FieldType::Audio => Some(DisplayType::Audio),
            FieldType::Upload => Some(DisplayType::Download),
            _ => None,
        }
    }

    pub fn to_json_schema_property(
        &self,
        description: &str,
        field_path: &crate::ValuePath,
        options: &mut crate::json_schema::ObjectSchemaOptions,
    ) -> crate::json_schema::ObjectSchema {
        match self {
            Self::Alias(a) => {
                a.0.to_json_schema_property(description, field_path, options)
            }
            field_type => {
                if let Some(display_type) = self.maybe_file_type() {
                    File::to_json_schema_property(
                        description,
                        field_path,
                        field_type,
                        display_type,
                        options,
                    )
                } else if matches!(self, Self::Date) {
                    let mut schema = serde_json::Map::new();
                    schema.insert("description".into(), description.into());
                    schema.insert("type".into(), "string".into());
                    if let Some(date) = options.set_dates_to {
                        let fmt = time::format_description::parse(
                            "[year]-[month]-[day] [hour]:[minute]:[second]",
                        )
                        .unwrap();
                        let date_str = date.with_time(time::Time::MIDNIGHT).format(&fmt).unwrap();
                        schema.insert("const".into(), date_str.into());
                    } else {
                        schema.insert("format".into(), "date".into());
                    }
                    options.decorate(field_path, field_type, &mut schema);
                    schema
                } else if let Self::Enum(valid_values) = self {
                    use serde_json::json;

                    let mut schema = serde_json::Map::new();
                    schema.insert("description".into(), description.into());
                    schema.insert("type".into(), "string".into());
                    schema.insert("enum".into(), json!(valid_values));
                    options.decorate(field_path, field_type, &mut schema);
                    schema
                } else if let Self::Oneof(field_types) = self {
                    let mut schema = serde_json::Map::new();
                    schema.insert("description".into(), description.into());
                    schema.insert(
                        if options.anyof_for_unions {
                            "anyOf"
                        } else {
                            "oneOf"
                        }.into(),
                        field_types
                            .iter()
                            .map(|t| {
                                let mut schema = serde_json::Map::new();
                                schema.insert("type".into(), "object".into());
                                schema.insert("additionalProperties".into(), false.into());
                                schema.insert("properties".into(), serde_json::json!({
                                    "type": {
                                        "type": "string",
                                        "const": t.name
                                    },
                                    "value": t.r#type.to_json_schema_property(&format!("{} - {}", description, t.name), &field_path.clone().append("value".into()), options)
                                }));
                                schema.insert("required".into(), serde_json::json!(["type", "value"]));
                                schema
                            })
                            .collect(),
                    );
                    options.decorate(field_path, field_type, &mut schema);
                    schema
                } else {
                    let mut schema = serde_json::Map::new();
                    schema.insert("description".into(), description.into());
                    // Simple types
                    let mut is_object = false;
                    schema.insert(
                        "type".into(),
                        match self {
                            Self::String => "string".into(),
                            Self::Number => "number".into(),
                            Self::Markdown => "string".into(),
                            Self::Boolean => "boolean".into(),
                            // At some point, we should support providing a
                            // schema for meta types, which would require
                            // either inferring types based on validation or
                            // allowing the template to directly provide a
                            // schema for a given type.
                            Self::Meta => {
                                is_object = true;
                                "object".into()
                            }
                            _ => panic!("don't know how to parse a schema from {:?}", self),
                        },
                    );
                    if is_object {
                        schema.insert("additionalProperties".into(), false.into());
                        schema.insert("properties".into(), serde_json::json!({}));
                        schema.insert("required".into(), serde_json::json!([]));
                    }
                    options.decorate(field_path, field_type, &mut schema);
                    schema
                }
            }
        }
    }
}