Skip to main content

use_wordpress_block/
lib.rs

1#![forbid(unsafe_code)]
2#![doc = include_str!("../README.md")]
3
4use core::{fmt, str::FromStr};
5use std::error::Error;
6
7macro_rules! block_text_newtype {
8    ($name:ident) => {
9        #[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
10        pub struct $name(String);
11
12        impl $name {
13            pub fn new(input: &str) -> Result<Self, WordPressBlockError> {
14                let trimmed = input.trim();
15                if trimmed.is_empty() {
16                    Err(WordPressBlockError::Empty)
17                } else {
18                    Ok(Self(trimmed.to_string()))
19                }
20            }
21
22            pub fn as_str(&self) -> &str {
23                &self.0
24            }
25        }
26
27        impl fmt::Display for $name {
28            fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
29                formatter.write_str(self.as_str())
30            }
31        }
32
33        impl FromStr for $name {
34            type Err = WordPressBlockError;
35
36            fn from_str(input: &str) -> Result<Self, Self::Err> {
37                Self::new(input)
38            }
39        }
40    };
41}
42
43block_text_newtype!(WordPressBlockCategory);
44block_text_newtype!(WordPressBlockAttributeName);
45block_text_newtype!(WordPressBlockAssetPath);
46
47/// WordPress block name metadata in `namespace/block` form.
48#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
49pub struct WordPressBlockName(String);
50
51impl WordPressBlockName {
52    pub fn new(input: &str) -> Result<Self, WordPressBlockError> {
53        let trimmed = input.trim();
54        let Some((namespace, block)) = trimmed.split_once('/') else {
55            return Err(WordPressBlockError::InvalidBlockName);
56        };
57        if namespace.is_empty() || block.is_empty() || block.contains('/') {
58            return Err(WordPressBlockError::InvalidBlockName);
59        }
60        Ok(Self(trimmed.to_string()))
61    }
62
63    pub fn as_str(&self) -> &str {
64        &self.0
65    }
66}
67
68impl fmt::Display for WordPressBlockName {
69    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
70        formatter.write_str(self.as_str())
71    }
72}
73
74impl FromStr for WordPressBlockName {
75    type Err = WordPressBlockError;
76
77    fn from_str(input: &str) -> Result<Self, Self::Err> {
78        Self::new(input)
79    }
80}
81
82/// WordPress block attribute type metadata.
83#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
84pub enum WordPressBlockAttributeType {
85    String,
86    Boolean,
87    Number,
88    Integer,
89    Object,
90    Array,
91}
92
93impl WordPressBlockAttributeType {
94    pub const fn as_str(self) -> &'static str {
95        match self {
96            Self::String => "string",
97            Self::Boolean => "boolean",
98            Self::Number => "number",
99            Self::Integer => "integer",
100            Self::Object => "object",
101            Self::Array => "array",
102        }
103    }
104}
105
106/// WordPress block attribute metadata.
107#[derive(Clone, Debug, Eq, PartialEq)]
108pub struct WordPressBlockAttribute {
109    name: WordPressBlockAttributeName,
110    attribute_type: WordPressBlockAttributeType,
111}
112
113impl WordPressBlockAttribute {
114    pub const fn new(
115        name: WordPressBlockAttributeName,
116        attribute_type: WordPressBlockAttributeType,
117    ) -> Self {
118        Self {
119            name,
120            attribute_type,
121        }
122    }
123
124    pub const fn name(&self) -> &WordPressBlockAttributeName {
125        &self.name
126    }
127
128    pub const fn attribute_type(&self) -> WordPressBlockAttributeType {
129        self.attribute_type
130    }
131}
132
133/// WordPress block support metadata.
134#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
135pub enum WordPressBlockSupport {
136    Align,
137    Color,
138    Typography,
139    Spacing,
140    Html,
141    Multiple,
142}
143
144impl WordPressBlockSupport {
145    pub const fn as_str(self) -> &'static str {
146        match self {
147            Self::Align => "align",
148            Self::Color => "color",
149            Self::Typography => "typography",
150            Self::Spacing => "spacing",
151            Self::Html => "html",
152            Self::Multiple => "multiple",
153        }
154    }
155}
156
157pub type WordPressBlockSupports = Vec<WordPressBlockSupport>;
158
159/// WordPress `block.json`-oriented metadata.
160#[derive(Clone, Debug, Eq, PartialEq)]
161pub struct WordPressBlockJson {
162    name: WordPressBlockName,
163    category: Option<WordPressBlockCategory>,
164    attributes: Vec<WordPressBlockAttribute>,
165    supports: WordPressBlockSupports,
166    editor_script: Option<WordPressBlockAssetPath>,
167    style: Option<WordPressBlockAssetPath>,
168    render: Option<WordPressBlockAssetPath>,
169}
170
171impl WordPressBlockJson {
172    pub fn new(name: WordPressBlockName) -> Self {
173        Self {
174            name,
175            category: None,
176            attributes: Vec::new(),
177            supports: Vec::new(),
178            editor_script: None,
179            style: None,
180            render: None,
181        }
182    }
183
184    pub fn with_category(mut self, category: WordPressBlockCategory) -> Self {
185        self.category = Some(category);
186        self
187    }
188
189    pub fn with_attribute(mut self, attribute: WordPressBlockAttribute) -> Self {
190        self.attributes.push(attribute);
191        self
192    }
193
194    pub fn with_support(mut self, support: WordPressBlockSupport) -> Self {
195        self.supports.push(support);
196        self
197    }
198
199    pub fn with_render(mut self, render: WordPressBlockAssetPath) -> Self {
200        self.render = Some(render);
201        self
202    }
203
204    pub const fn name(&self) -> &WordPressBlockName {
205        &self.name
206    }
207
208    pub const fn category(&self) -> Option<&WordPressBlockCategory> {
209        self.category.as_ref()
210    }
211
212    pub fn attributes(&self) -> &[WordPressBlockAttribute] {
213        &self.attributes
214    }
215
216    pub fn supports(&self) -> &[WordPressBlockSupport] {
217        &self.supports
218    }
219
220    pub const fn render(&self) -> Option<&WordPressBlockAssetPath> {
221        self.render.as_ref()
222    }
223}
224
225/// Error returned when WordPress block metadata is invalid.
226#[derive(Clone, Copy, Debug, Eq, PartialEq)]
227pub enum WordPressBlockError {
228    Empty,
229    InvalidBlockName,
230}
231
232impl fmt::Display for WordPressBlockError {
233    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
234        match self {
235            Self::Empty => formatter.write_str("WordPress block metadata cannot be empty"),
236            Self::InvalidBlockName => {
237                formatter.write_str("WordPress block names must look like namespace/block")
238            },
239        }
240    }
241}
242
243impl Error for WordPressBlockError {}
244
245#[cfg(test)]
246mod tests {
247    use super::{
248        WordPressBlockAttribute, WordPressBlockAttributeName, WordPressBlockAttributeType,
249        WordPressBlockError, WordPressBlockJson, WordPressBlockName, WordPressBlockSupport,
250    };
251
252    #[test]
253    fn builds_block_json_metadata() -> Result<(), WordPressBlockError> {
254        let block = WordPressBlockJson::new(WordPressBlockName::new("acme/book-card")?)
255            .with_attribute(WordPressBlockAttribute::new(
256                WordPressBlockAttributeName::new("title")?,
257                WordPressBlockAttributeType::String,
258            ))
259            .with_support(WordPressBlockSupport::Color);
260
261        assert_eq!(block.name().as_str(), "acme/book-card");
262        assert_eq!(block.attributes()[0].name().as_str(), "title");
263        assert_eq!(block.supports(), &[WordPressBlockSupport::Color]);
264        Ok(())
265    }
266}