Skip to main content

memstead_schema/
meta_schema.rs

1//! Embedded JSON Schemas (the "meta-schemas") for authoring validation.
2//!
3//! `schema-manifest.schema.json` and `type-definition.schema.json` are
4//! generated from the Rust schema structs and committed under
5//! `generated/`. They are baked into the binary here so the engine can
6//! *publish* them into a workspace's `.memstead/meta-schemas/` at boot —
7//! a well-known location an authored package's
8//! `# yaml-language-server: $schema=…` directive resolves against, giving
9//! IDE-side validation as the YAML is edited. `memstead schema install`
10//! rewrites a package's directive to the installed-location form so the
11//! published copy is what the editor checks against.
12
13use std::path::Path;
14
15/// JSON Schema for a schema package manifest (`schema.yaml`).
16pub const META_SCHEMA_MANIFEST: &str = include_str!("../generated/schema-manifest.schema.json");
17
18/// JSON Schema for a type definition (`types/<type>.yaml`).
19pub const META_SCHEMA_TYPE_DEFINITION: &str =
20    include_str!("../generated/type-definition.schema.json");
21
22/// Directory (under `<workspace>/.memstead/`) the meta-schemas publish to.
23pub const META_SCHEMA_DIR: &str = "meta-schemas";
24
25/// Published filename of the manifest meta-schema.
26pub const META_SCHEMA_MANIFEST_FILE: &str = "schema-manifest.schema.json";
27/// Published filename of the type-definition meta-schema.
28pub const META_SCHEMA_TYPE_DEFINITION_FILE: &str = "type-definition.schema.json";
29
30/// Publish the embedded meta-schemas into
31/// `<workspace_root>/.memstead/meta-schemas/`. Idempotent — rewrites a
32/// file only when its on-disk bytes differ. Best-effort by contract: the
33/// engine-boot caller ignores the error so a read-only or
34/// permission-restricted workspace still boots; the meta-schemas are an
35/// editor convenience, not load-bearing engine state.
36pub fn publish_meta_schemas(workspace_root: &Path) -> std::io::Result<()> {
37    let dir = workspace_root.join(".memstead").join(META_SCHEMA_DIR);
38    write_if_changed(&dir.join(META_SCHEMA_MANIFEST_FILE), META_SCHEMA_MANIFEST)?;
39    write_if_changed(
40        &dir.join(META_SCHEMA_TYPE_DEFINITION_FILE),
41        META_SCHEMA_TYPE_DEFINITION,
42    )?;
43    Ok(())
44}
45
46fn write_if_changed(path: &Path, contents: &str) -> std::io::Result<()> {
47    if let Ok(existing) = std::fs::read_to_string(path)
48        && existing == contents
49    {
50        return Ok(());
51    }
52    if let Some(parent) = path.parent() {
53        std::fs::create_dir_all(parent)?;
54    }
55    std::fs::write(path, contents)
56}
57
58#[cfg(test)]
59mod tests {
60    use super::*;
61
62    #[test]
63    fn embedded_meta_schemas_are_valid_json_objects() {
64        let manifest: serde_json::Value = serde_json::from_str(META_SCHEMA_MANIFEST).unwrap();
65        assert!(
66            manifest.get("properties").is_some(),
67            "manifest meta-schema has properties"
68        );
69        let type_def: serde_json::Value =
70            serde_json::from_str(META_SCHEMA_TYPE_DEFINITION).unwrap();
71        assert!(
72            type_def.get("properties").is_some(),
73            "type meta-schema has properties"
74        );
75    }
76
77    #[test]
78    fn publish_writes_both_and_is_idempotent() {
79        let tmp = tempfile::tempdir().unwrap();
80        publish_meta_schemas(tmp.path()).unwrap();
81        let mdir = tmp.path().join(".memstead").join("meta-schemas");
82        assert_eq!(
83            std::fs::read_to_string(mdir.join(META_SCHEMA_MANIFEST_FILE)).unwrap(),
84            META_SCHEMA_MANIFEST,
85        );
86        assert!(mdir.join(META_SCHEMA_TYPE_DEFINITION_FILE).is_file());
87        // Second publish is a no-op (bytes unchanged) and still succeeds.
88        publish_meta_schemas(tmp.path()).unwrap();
89        assert_eq!(
90            std::fs::read_to_string(mdir.join(META_SCHEMA_MANIFEST_FILE)).unwrap(),
91            META_SCHEMA_MANIFEST,
92        );
93    }
94}