use serde::{Deserialize, Deserializer, Serialize};
use std::collections::HashMap;
use super::Table;
use super::metadata::KeyValueAttribute;
use super::metadata::Metadata;
#[derive(Debug, Clone, Default, Serialize)]
pub struct DjotAttributeGroup {
pub identifier: String,
pub attributes: Attributes,
}
#[derive(Deserialize)]
#[serde(untagged)]
enum DjotAttributeGroupWire {
Positional((String, Attributes)),
Named { identifier: String, attributes: Attributes },
}
impl<'de> Deserialize<'de> for DjotAttributeGroup {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
Ok(match DjotAttributeGroupWire::deserialize(deserializer)? {
DjotAttributeGroupWire::Positional(group) => group.into(),
DjotAttributeGroupWire::Named { identifier, attributes } => Self { identifier, attributes },
})
}
}
#[cfg(feature = "api")]
impl utoipa::PartialSchema for DjotAttributeGroup {
fn schema() -> utoipa::openapi::RefOr<utoipa::openapi::schema::Schema> {
use utoipa::openapi::schema::{Object, ObjectBuilder, Type};
ObjectBuilder::new()
.property("identifier", Object::with_type(Type::String))
.required("identifier")
.property("attributes", <Attributes as utoipa::PartialSchema>::schema())
.required("attributes")
.into()
}
}
#[cfg(feature = "api")]
impl utoipa::ToSchema for DjotAttributeGroup {
fn schemas(schemas: &mut Vec<(String, utoipa::openapi::RefOr<utoipa::openapi::schema::Schema>)>) {
<Attributes as utoipa::ToSchema>::schemas(schemas);
}
}
impl From<(String, Attributes)> for DjotAttributeGroup {
fn from((identifier, attributes): (String, Attributes)) -> Self {
Self { identifier, attributes }
}
}
impl From<DjotAttributeGroup> for (String, Attributes) {
fn from(group: DjotAttributeGroup) -> Self {
(group.identifier, group.attributes)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "api", derive(utoipa::ToSchema))]
#[cfg_attr(feature = "api", schema(no_recursion))]
pub struct DjotContent {
pub plain_text: String,
pub blocks: Vec<FormattedBlock>,
pub metadata: Metadata,
pub tables: Vec<Table>,
pub images: Vec<DjotImage>,
pub links: Vec<DjotLink>,
pub footnotes: Vec<Footnote>,
#[serde(skip_serializing_if = "Vec::is_empty", default)]
pub attributes: Vec<DjotAttributeGroup>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "api", derive(utoipa::ToSchema))]
#[cfg_attr(feature = "api", schema(no_recursion))]
pub struct FormattedBlock {
pub block_type: BlockType,
#[serde(skip_serializing_if = "Option::is_none")]
pub level: Option<usize>,
pub inline_content: Vec<InlineElement>,
#[serde(skip_serializing_if = "Option::is_none")]
pub attributes: Option<Attributes>,
#[serde(skip_serializing_if = "Option::is_none")]
pub language: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub code: Option<String>,
#[serde(skip_serializing_if = "Vec::is_empty", default)]
pub children: Vec<FormattedBlock>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
#[cfg_attr(feature = "api", derive(utoipa::ToSchema))]
pub enum BlockType {
Paragraph,
Heading,
Blockquote,
CodeBlock,
ListItem,
OrderedList,
BulletList,
TaskList,
DefinitionList,
DefinitionTerm,
DefinitionDescription,
Div,
Section,
ThematicBreak,
RawBlock,
MathDisplay,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "api", derive(utoipa::ToSchema))]
pub struct InlineElement {
pub element_type: InlineType,
pub content: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub attributes: Option<Attributes>,
#[serde(skip_serializing_if = "Option::is_none")]
pub metadata: Option<HashMap<String, String>>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
#[cfg_attr(feature = "api", derive(utoipa::ToSchema))]
pub enum InlineType {
Text,
Strong,
Emphasis,
Highlight,
Subscript,
Superscript,
Insert,
Delete,
Code,
Link,
Image,
Span,
Math,
RawInline,
FootnoteRef,
Symbol,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
#[cfg_attr(feature = "api", derive(utoipa::ToSchema))]
pub struct Attributes {
#[serde(skip_serializing_if = "Option::is_none")]
pub id: Option<String>,
#[serde(skip_serializing_if = "Vec::is_empty", default)]
pub classes: Vec<String>,
#[serde(skip_serializing_if = "Vec::is_empty", default)]
pub key_values: Vec<KeyValueAttribute>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "api", derive(utoipa::ToSchema))]
pub struct DjotImage {
pub src: String,
pub alt: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub title: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub attributes: Option<Attributes>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "api", derive(utoipa::ToSchema))]
pub struct DjotLink {
pub url: String,
pub text: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub title: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub attributes: Option<Attributes>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "api", derive(utoipa::ToSchema))]
pub struct Footnote {
pub label: String,
pub content: Vec<FormattedBlock>,
}
#[cfg(test)]
mod binding_value_serde_tests {
use super::{Attributes, DjotAttributeGroup};
use serde_json::json;
#[cfg(feature = "api")]
#[test]
fn should_describe_djot_attribute_group_as_named_object_schema() {
let schema = serde_json::to_value(<DjotAttributeGroup as utoipa::PartialSchema>::schema())
.expect("schema must serialize");
assert_eq!(schema["type"], "object");
assert_eq!(schema["properties"]["identifier"]["type"], "string");
assert_eq!(schema["properties"]["attributes"]["type"], "object");
let required = schema["required"].as_array().expect("schema must have required fields");
assert_eq!(required.len(), 2);
assert!(required.contains(&json!("identifier")));
assert!(required.contains(&json!("attributes")));
}
#[test]
fn should_still_accept_legacy_positional_array_for_djot_attribute_group() {
let legacy = json!(["section", {
"id": "intro",
"classes": ["lead"],
"key_values": [["role", "doc-introduction"]]
}]);
let group: DjotAttributeGroup =
serde_json::from_value(legacy).expect("legacy attribute group must deserialize");
assert_eq!(group.identifier, "section");
assert_eq!(group.attributes.id.as_deref(), Some("intro"));
assert_eq!(group.attributes.classes, vec!["lead".to_string()]);
assert_eq!(group.attributes.key_values[0].key, "role");
assert_eq!(group.attributes.key_values[0].value, "doc-introduction");
}
#[test]
fn should_serialize_djot_attribute_group_as_named_object() {
let named: DjotAttributeGroup = serde_json::from_value(json!({
"identifier": "section",
"attributes": {
"id": "intro",
"classes": ["lead"],
"key_values": [{"key": "role", "value": "doc-introduction"}]
}
}))
.expect("named attribute group must deserialize");
assert_eq!(named.identifier, "section");
assert_eq!(named.attributes.id.as_deref(), Some("intro"));
assert_eq!(
serde_json::to_value(named).expect("named attribute group must serialize"),
json!({
"identifier": "section",
"attributes": {
"id": "intro",
"classes": ["lead"],
"key_values": [{"key": "role", "value": "doc-introduction"}]
}
})
);
}
#[test]
fn should_still_accept_legacy_attributes_array_and_serialize_key_values_as_objects() {
let legacy = json!({"key_values": [["lang", "en"]]});
let attributes: Attributes = serde_json::from_value(legacy).expect("legacy attributes must deserialize");
assert_eq!(attributes.key_values[0].key, "lang");
assert_eq!(attributes.key_values[0].value, "en");
assert_eq!(
serde_json::to_value(attributes).expect("attributes must serialize"),
json!({"key_values": [{"key": "lang", "value": "en"}]})
);
}
}