memstead_schema/
builtins.rs1use std::sync::Arc;
10
11use include_dir::{Dir, include_dir};
12
13use crate::loader::{self, SchemaLoadError};
14use crate::schema::Schema;
15
16static BUILTIN_SCHEMAS: Dir<'_> = include_dir!("$CARGO_MANIFEST_DIR/builtins/schemas");
17
18pub(crate) fn builtin_schemas_dir() -> &'static Dir<'static> {
25 &BUILTIN_SCHEMAS
26}
27
28pub fn builtin_mem_template(name: &str) -> Option<serde_json::Value> {
41 let file = BUILTIN_SCHEMAS.get_file(format!("{name}/mem-template.json").as_str())?;
42 serde_json::from_slice(file.contents()).ok()
43}
44
45pub struct BuiltinPackage {
54 pub name: String,
55 pub version: String,
56 pub files: Vec<(String, &'static [u8])>,
58}
59
60pub fn builtin_packages() -> Vec<BuiltinPackage> {
64 fn collect_files(dir: &Dir<'static>, root: &str, out: &mut Vec<(String, &'static [u8])>) {
65 for file in dir.files() {
66 let rel = file
67 .path()
68 .strip_prefix(root)
69 .unwrap_or(file.path())
70 .display()
71 .to_string();
72 out.push((rel, file.contents()));
73 }
74 for sub in dir.dirs() {
75 collect_files(sub, root, out);
76 }
77 }
78
79 let mut out = Vec::new();
80 for dir in BUILTIN_SCHEMAS.dirs() {
81 let root = dir.path().display().to_string();
82 let manifest = dir
83 .get_file(format!("{root}/schema.yaml").as_str())
84 .and_then(|f| f.contents_utf8());
85 let Some(manifest) = manifest else { continue };
86 let header: Option<(String, String)> =
87 serde_yaml_ng::from_str::<serde_yaml_ng::Value>(manifest)
88 .ok()
89 .and_then(|v| {
90 let name = v.get("name")?.as_str()?.to_string();
91 let version = v.get("version")?.as_str()?.to_string();
92 Some((name, version))
93 });
94 let Some((name, version)) = header else {
95 continue;
96 };
97 let mut files = Vec::new();
98 collect_files(dir, &root, &mut files);
99 files.sort_by(|a, b| a.0.cmp(&b.0));
100 out.push(BuiltinPackage {
101 name,
102 version,
103 files,
104 });
105 }
106 out
107}
108
109pub fn load_builtin_schemas() -> Result<Vec<Arc<Schema>>, SchemaLoadError> {
111 let mut out = Vec::new();
112 for dir in BUILTIN_SCHEMAS.dirs() {
113 let schema = load_builtin_dir(dir)?;
114 out.push(Arc::new(schema));
115 }
116 Ok(out)
117}
118
119fn load_builtin_dir(dir: &Dir<'_>) -> Result<Schema, SchemaLoadError> {
120 let manifest_file = dir.get_file(format!("{}/schema.yaml", dir.path().display()).as_str());
121 let manifest_text = manifest_file
122 .and_then(|f| f.contents_utf8())
123 .ok_or_else(|| SchemaLoadError::Io {
124 path: dir.path().join("schema.yaml"),
125 source: std::io::Error::new(
126 std::io::ErrorKind::NotFound,
127 "embedded schema.yaml missing or not utf-8",
128 ),
129 })?;
130
131 let mut types: Vec<(String, String)> = Vec::new();
132 let types_path = format!("{}/types", dir.path().display());
133 if let Some(types_dir) = dir.get_dir(types_path.as_str()) {
134 for file in types_dir.files() {
135 if file.path().extension().and_then(|s| s.to_str()) != Some("yaml") {
136 continue;
137 }
138 let Some(stem) = file
139 .path()
140 .file_stem()
141 .and_then(|s| s.to_str())
142 .map(str::to_owned)
143 else {
144 continue;
145 };
146 let Some(contents) = file.contents_utf8() else {
147 return Err(SchemaLoadError::Io {
148 path: file.path().to_path_buf(),
149 source: std::io::Error::new(
150 std::io::ErrorKind::InvalidData,
151 "embedded type yaml is not utf-8",
152 ),
153 });
154 };
155 types.push((stem, contents.to_string()));
156 }
157 }
158
159 let marker_path = format!(
162 "{}/{}",
163 dir.path().display(),
164 loader::SCHEMA_FORMAT_MARKER_FILE
165 );
166 let format = if dir.get_file(marker_path.as_str()).is_some() {
167 loader::MetadataPolarityFormat::RequiredOptIn
168 } else {
169 loader::MetadataPolarityFormat::Legacy
170 };
171 loader::load_schema_from_memory_with_format(manifest_text, &types, format)
172}
173
174#[cfg(test)]
175mod tests {
176 use super::*;
177
178 #[test]
183 fn builtin_mem_templates_carry_instance_keys_only() {
184 let cases = [
185 ("planning", "phase_context"),
186 ("project", "scope"),
187 ("software", "stack"),
188 ];
189 for (name, instance_key) in cases {
190 let tpl = builtin_mem_template(name)
191 .unwrap_or_else(|| panic!("{name} must ship a mem-template.json"));
192 assert!(
193 tpl["language"].is_string(),
194 "{name}: template carries language"
195 );
196 let wg = &tpl["writeGuidance"];
197 assert!(
198 wg.get(instance_key).is_some(),
199 "{name}: template carries instance key {instance_key}",
200 );
201 assert!(
202 wg.get("goal").is_none() && wg.get("avoid").is_none(),
203 "{name}: template must not carry the deprecated literal goal/avoid (schema owns those)",
204 );
205 }
206 }
207
208 #[test]
210 fn builtin_mem_template_absent_is_none() {
211 assert!(builtin_mem_template("default").is_none());
212 assert!(builtin_mem_template("ingest").is_none());
213 assert!(builtin_mem_template("not-a-builtin").is_none());
214 }
215
216 #[test]
219 fn all_builtins_still_load_with_templates_present() {
220 let schemas = load_builtin_schemas().expect("built-ins load");
221 assert!(
222 schemas.iter().any(|s| s.manifest.name == "planning"),
223 "planning still loads alongside its mem-template.json",
224 );
225 }
226}