use serde::Serialize;
use std::collections::BTreeMap;
use crate::ast::shortcode::ShortcodeKind;
use crate::contract::components::{COMPONENTS, Status};
use crate::contract::frontmatter::{FrontmatterFieldJson, frontmatter_fields};
use crate::contract::tokens::Tokens;
pub const DESCRIBE_SCHEMA_VERSION: u32 = 6;
pub const MOSS_HTML_VERSION: u32 = 1;
#[derive(Serialize)]
pub struct DescribePayload<'a> {
pub describe_schema_version: u32,
pub moss_html_version: u32,
pub moss_binary_version: &'static str,
pub tokens: BTreeMap<&'a str, Vec<TokenJson<'a>>>,
pub components: Vec<ComponentJson>,
pub custom_properties: Vec<CustomPropJson>,
pub scope_attributes: Vec<ScopeAttrJson>,
pub frontmatter: Vec<FrontmatterFieldJson>,
pub languages: &'static [&'static str],
pub plugin_hooks: Vec<PluginHookInfo>,
pub manifest_fields: Vec<ManifestFieldInfo>,
pub slots: Vec<SlotInfo>,
pub cli_commands: Vec<CliCommandInfo>,
}
#[derive(Serialize)]
pub struct PluginHookInfo {
pub name: &'static str,
pub description: &'static str,
pub arity: &'static str,
pub context: &'static str,
}
#[derive(Serialize)]
pub struct ManifestFieldInfo {
pub name: &'static str,
pub r#type: &'static str,
pub required: bool,
pub description: &'static str,
}
#[derive(Serialize)]
pub struct SlotInfo {
pub name: &'static str,
pub position: &'static str,
pub authorable: bool,
}
#[derive(Serialize)]
pub struct CliCommandInfo {
pub name: &'static str,
pub args: &'static str,
pub description: &'static str,
}
#[derive(Serialize)]
pub struct TokenJson<'a> {
pub name: &'a str,
pub value: &'a str,
#[serde(skip_serializing_if = "Option::is_none")]
pub dark_value: Option<&'a str>,
#[serde(rename = "type", skip_serializing_if = "Option::is_none")]
pub type_hint: Option<&'a str>,
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<&'a str>,
}
#[derive(Serialize)]
pub struct ComponentJson {
pub class: &'static str,
pub kind: &'static str,
pub parent: &'static str,
pub data_attrs: Vec<DataAttrJson>,
pub example_html: &'static str,
pub example_markdown: &'static str,
pub status: &'static str,
pub since: &'static str,
pub description: &'static str,
pub authorable: bool,
}
#[derive(Serialize)]
pub struct DataAttrJson {
pub name: &'static str,
pub values: &'static [&'static str],
pub default: &'static str,
pub description: &'static str,
}
#[derive(Serialize)]
pub struct ScopeAttrJson {
pub selector: &'static str,
pub name: &'static str,
pub values: &'static [&'static str],
pub description: &'static str,
}
#[derive(Serialize)]
pub struct CustomPropJson {
pub name: &'static str,
pub owner: &'static str,
pub default: &'static str,
pub description: &'static str,
}
impl<'a> DescribePayload<'a> {
pub fn new(tokens: &'a Tokens) -> Self {
let mut tokens_map: BTreeMap<&str, Vec<TokenJson>> = BTreeMap::new();
for group in &tokens.groups {
let entries: Vec<TokenJson> = group
.entries
.iter()
.map(|t| TokenJson {
name: &t.name,
value: &t.value,
dark_value: t.dark_value.as_deref(),
type_hint: t.type_hint.as_deref(),
description: t.description.as_deref(),
})
.collect();
tokens_map.insert(&group.name, entries);
}
let authorable: std::collections::HashSet<&'static str> =
ShortcodeKind::all().map(|k| k.root_class()).collect();
let components: Vec<ComponentJson> = COMPONENTS
.iter()
.filter(|c| c.is_public())
.map(|c| ComponentJson {
class: c.class,
kind: c.kind,
parent: c.parent,
data_attrs: c
.data_attrs
.iter()
.map(|a| DataAttrJson {
name: a.name,
values: a.values,
default: a.default,
description: a.description,
})
.collect(),
example_html: c.example_html,
example_markdown: c.example_markdown,
status: match c.status {
Status::Confirmed => "confirmed",
Status::Emerging => "emerging",
Status::Retired => "retired",
},
since: c.since,
description: c.description,
authorable: authorable.contains(c.class),
})
.collect();
DescribePayload {
describe_schema_version: DESCRIBE_SCHEMA_VERSION,
moss_html_version: MOSS_HTML_VERSION,
moss_binary_version: env!("CARGO_PKG_VERSION"),
tokens: tokens_map,
components,
custom_properties: crate::contract::custom_props::CUSTOM_PROPS
.iter()
.map(|p| CustomPropJson {
name: p.name,
owner: p.owner,
default: p.default,
description: p.description,
})
.collect(),
scope_attributes: crate::contract::custom_props::SCOPE_ATTRS
.iter()
.map(|a| ScopeAttrJson {
selector: a.selector,
name: a.name,
values: a.values,
description: a.description,
})
.collect(),
frontmatter: frontmatter_fields(),
languages: crate::home::known_language_codes(),
plugin_hooks: Vec::new(),
manifest_fields: Vec::new(),
slots: Vec::new(),
cli_commands: Vec::new(),
}
}
pub fn with_binary_version(mut self, version: &'static str) -> Self {
self.moss_binary_version = version;
self
}
pub fn with_plugin_contract(
mut self,
plugin_hooks: Vec<PluginHookInfo>,
manifest_fields: Vec<ManifestFieldInfo>,
slots: Vec<SlotInfo>,
cli_commands: Vec<CliCommandInfo>,
) -> Self {
self.plugin_hooks = plugin_hooks;
self.manifest_fields = manifest_fields;
self.slots = slots;
self.cli_commands = cli_commands;
self
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::contract::tokens::load_tokens;
#[test]
fn apply_is_absent_from_public_contract() {
let tokens = load_tokens().expect("tokens");
let payload = DescribePayload::new(&tokens);
assert!(
payload.components.iter().all(|c| !c.class.starts_with("moss-apply")),
"apply classes must be demoted from the public contract"
);
assert!(
!payload.components.iter().any(|c| c.class == "moss-apply" && c.authorable),
":::apply must not be marked authorable"
);
}
}