1use serde::Serialize;
8use std::collections::BTreeMap;
9
10use crate::ast::shortcode::ShortcodeKind;
11use crate::contract::components::{COMPONENTS, Status};
12use crate::contract::frontmatter::{FrontmatterFieldJson, frontmatter_fields};
13use crate::contract::tokens::Tokens;
14
15pub const DESCRIBE_SCHEMA_VERSION: u32 = 5;
16pub const MOSS_HTML_VERSION: u32 = 1;
17
18#[derive(Serialize)]
19pub struct DescribePayload<'a> {
20 pub describe_schema_version: u32,
21 pub moss_html_version: u32,
22 pub moss_binary_version: &'static str,
23 pub tokens: BTreeMap<&'a str, Vec<TokenJson<'a>>>,
24 pub components: Vec<ComponentJson>,
25 pub frontmatter: Vec<FrontmatterFieldJson>,
26 pub plugin_hooks: Vec<PluginHookInfo>,
28 pub manifest_fields: Vec<ManifestFieldInfo>,
30 pub slots: Vec<SlotInfo>,
32 pub cli_commands: Vec<CliCommandInfo>,
35}
36
37#[derive(Serialize)]
42pub struct PluginHookInfo {
43 pub name: &'static str,
45 pub description: &'static str,
47 pub arity: &'static str,
49 pub context: &'static str,
51}
52
53#[derive(Serialize)]
58pub struct ManifestFieldInfo {
59 pub name: &'static str,
61 pub r#type: &'static str,
63 pub required: bool,
65 pub description: &'static str,
67}
68
69#[derive(Serialize)]
74pub struct SlotInfo {
75 pub name: &'static str,
77 pub position: &'static str,
79 pub authorable: bool,
81}
82
83#[derive(Serialize)]
88pub struct CliCommandInfo {
89 pub name: &'static str,
91 pub args: &'static str,
93 pub description: &'static str,
95}
96
97#[derive(Serialize)]
98pub struct TokenJson<'a> {
99 pub name: &'a str,
100 pub value: &'a str,
101 #[serde(skip_serializing_if = "Option::is_none")]
102 pub dark_value: Option<&'a str>,
103 #[serde(rename = "type", skip_serializing_if = "Option::is_none")]
104 pub type_hint: Option<&'a str>,
105 #[serde(skip_serializing_if = "Option::is_none")]
106 pub description: Option<&'a str>,
107}
108
109#[derive(Serialize)]
110pub struct ComponentJson {
111 pub class: &'static str,
112 pub kind: &'static str,
113 pub parent: &'static str,
114 pub data_attrs: Vec<DataAttrJson>,
115 pub example_html: &'static str,
116 pub example_markdown: &'static str,
117 pub status: &'static str,
118 pub since: &'static str,
119 pub description: &'static str,
120 pub authorable: bool,
125}
126
127#[derive(Serialize)]
128pub struct DataAttrJson {
129 pub name: &'static str,
130 pub values: &'static [&'static str],
131 pub default: &'static str,
132 pub description: &'static str,
133}
134
135impl<'a> DescribePayload<'a> {
136 pub fn new(tokens: &'a Tokens) -> Self {
137 let mut tokens_map: BTreeMap<&str, Vec<TokenJson>> = BTreeMap::new();
138 for group in &tokens.groups {
139 let entries: Vec<TokenJson> = group
140 .entries
141 .iter()
142 .map(|t| TokenJson {
143 name: &t.name,
144 value: &t.value,
145 dark_value: t.dark_value.as_deref(),
146 type_hint: t.type_hint.as_deref(),
147 description: t.description.as_deref(),
148 })
149 .collect();
150 tokens_map.insert(&group.name, entries);
151 }
152
153 let authorable: std::collections::HashSet<&'static str> =
154 ShortcodeKind::all().map(|k| k.root_class()).collect();
155
156 let components: Vec<ComponentJson> = COMPONENTS
157 .iter()
158 .filter(|c| c.is_public())
159 .map(|c| ComponentJson {
160 class: c.class,
161 kind: c.kind,
162 parent: c.parent,
163 data_attrs: c
164 .data_attrs
165 .iter()
166 .map(|a| DataAttrJson {
167 name: a.name,
168 values: a.values,
169 default: a.default,
170 description: a.description,
171 })
172 .collect(),
173 example_html: c.example_html,
174 example_markdown: c.example_markdown,
175 status: match c.status {
176 Status::Confirmed => "confirmed",
177 Status::Emerging => "emerging",
178 Status::Retired => "retired",
179 },
180 since: c.since,
181 description: c.description,
182 authorable: authorable.contains(c.class),
183 })
184 .collect();
185
186 DescribePayload {
187 describe_schema_version: DESCRIBE_SCHEMA_VERSION,
188 moss_html_version: MOSS_HTML_VERSION,
189 moss_binary_version: env!("CARGO_PKG_VERSION"),
190 tokens: tokens_map,
191 components,
192 frontmatter: frontmatter_fields(),
193 plugin_hooks: Vec::new(),
198 manifest_fields: Vec::new(),
199 slots: Vec::new(),
200 cli_commands: Vec::new(),
201 }
202 }
203
204 pub fn with_plugin_contract(
208 mut self,
209 plugin_hooks: Vec<PluginHookInfo>,
210 manifest_fields: Vec<ManifestFieldInfo>,
211 slots: Vec<SlotInfo>,
212 cli_commands: Vec<CliCommandInfo>,
213 ) -> Self {
214 self.plugin_hooks = plugin_hooks;
215 self.manifest_fields = manifest_fields;
216 self.slots = slots;
217 self.cli_commands = cli_commands;
218 self
219 }
220}
221
222#[cfg(test)]
223mod tests {
224 use super::*;
225 use crate::contract::tokens::load_tokens;
226
227 #[test]
228 fn apply_is_absent_from_public_contract() {
229 let tokens = load_tokens().expect("tokens");
230 let payload = DescribePayload::new(&tokens);
231 assert!(
232 payload.components.iter().all(|c| !c.class.starts_with("moss-apply")),
233 "apply classes must be demoted from the public contract"
234 );
235 assert!(
236 !payload.components.iter().any(|c| c.class == "moss-apply" && c.authorable),
237 ":::apply must not be marked authorable"
238 );
239 }
240}