Skip to main content

scarb_manifest_schema/
lib.rs

1//! Dynamic JSON Schema for Scarb manifest.
2//!
3//! Provides a way to generate and traverse the Scarb manifest JSON Schema and retrieve the
4//! definition for a specific TOML path, such as `package.dependencies`.
5
6use anyhow::{Result, anyhow};
7use serde_json::Value;
8use std::sync::OnceLock;
9
10static GLOBAL_TRAVERSER: OnceLock<SchemaTraverser> = OnceLock::new();
11
12pub const SCARB_SCHEMA_JSON: &str = include_str!("../schema.json");
13
14/// Returns a lazily initialised, shared, globally accessible instance of the [`SchemaTraverser`].
15pub fn get_shared_traverser() -> &'static SchemaTraverser {
16    GLOBAL_TRAVERSER.get_or_init(|| {
17        let manifest_schema = get_manifest_schema();
18        SchemaTraverser::new(manifest_schema)
19    })
20}
21
22///  Serializes full JSON Schema for the TomlManifest it into a serde_json::Value.
23pub fn get_manifest_schema() -> Value {
24    serde_json::from_str(SCARB_SCHEMA_JSON).expect("Failed to serialize Manifest schema")
25}
26
27/// Traverses the JSON Schema and returns the definition for a specific TOML path, such as `package.dependencies`.
28pub struct SchemaTraverser {
29    root: Value,
30}
31
32impl SchemaTraverser {
33    /// Creates a new SchemaTraverser from the given JSON Schema.
34    pub fn new(schema: Value) -> Self {
35        Self { root: schema }
36    }
37
38    /// Accepts a sequence of keys (e.g. ["package", "dependencies"]) and returns the specific schema node representing that field.
39    pub fn traverse<I, S>(&self, path: I) -> Result<&Value>
40    where
41        I: IntoIterator<Item = S>,
42        S: AsRef<str>,
43    {
44        let path_vec: Vec<String> = path.into_iter().map(|s| s.as_ref().to_owned()).collect();
45
46        let mut current = &self.root;
47        let mut iter = path_vec.iter().map(String::as_str).peekable();
48
49        while let Some(key) = iter.next() {
50            current = self.resolve_node(current)?;
51
52            let properties = current.get("properties").and_then(|v| v.as_object());
53
54            if let Some(props) = properties {
55                current = props.get(key).ok_or_else(|| {
56                    anyhow!(
57                        "Couldn't resolve '{}' at key '{}'.",
58                        path_vec.join("."),
59                        key
60                    )
61                })?;
62            } else if iter.peek().is_none() {
63                // The last node in the path is valid but does not have properties
64                // (e.g. package.edition.workspace)
65                return Ok(current);
66            } else {
67                return Err(anyhow!(
68                    "Couldn't resolve '{}' at key '{}'.",
69                    path_vec.join("."),
70                    key
71                ));
72            }
73        }
74        Ok(current)
75    }
76
77    /// Handles $ref and anyOf to find the actual object definition
78    fn resolve_node<'a>(&'a self, node: &'a Value) -> Result<&'a Value> {
79        if let Some(ref_path) = node.get("$ref").and_then(|r| r.as_str()) {
80            return self.resolve_ref(ref_path);
81        }
82
83        if let Some(any_of) = node.get("anyOf").and_then(|a| a.as_array()) {
84            for option in any_of {
85                if option.get("properties").is_some() || option.get("$ref").is_some() {
86                    return self.resolve_node(option);
87                }
88            }
89        }
90
91        Ok(node)
92    }
93
94    /// Basic resolver for "#/$defs/TypeName"
95    fn resolve_ref(&self, ref_path: &str) -> Result<&Value> {
96        // Instead of repeating a complex object definition multiple times,
97        // schemars puts the definition in a central "lookup table" (under $defs) and points to it.
98        // Syntax: "$ref": "#/$defs/TypeName"
99        let parts: Vec<&str> = ref_path.split('/').collect();
100        if parts[0] == "#" && parts[1] == "$defs" {
101            return self
102                .root
103                .get("$defs")
104                .and_then(|d| d.get(parts[2]))
105                .ok_or_else(|| anyhow!("Definition {} not found", ref_path));
106        }
107        Err(anyhow!("Unsupported ref format: {}", ref_path))
108    }
109}