disposition_lsp 0.4.0

Language server for editing `disposition` `InputDiagram` YAML.
Documentation
//! Loads and walks the committed `InputDiagram` JSON schema.

use std::sync::OnceLock;

use serde_json::Value;

/// The committed `InputDiagram` JSON schema, regenerated by the `schema_gen`
/// binary (`cargo run -p disposition_lsp --features schema-gen --bin
/// schema_gen`).
const SCHEMA_JSON: &str = include_str!("input_diagram_schema.json");

/// The `InputDiagram` JSON schema, parsed once and walked to derive
/// completions.
///
/// The schema is a JSON Schema 2020-12 document: the root is an object with
/// `properties` (the top-level [`InputDiagram`] fields), and `$defs` holds
/// named type definitions referenced via `$ref`. Map types are modelled as
/// objects with `additionalProperties`; enums as `oneOf` arrays of `{ const,
/// description }`.
///
/// [`InputDiagram`]: disposition_input_model::InputDiagram
pub struct DiagramSchema {
    /// The parsed root schema value.
    root: Value,
}

impl DiagramSchema {
    /// Returns the shared, lazily-parsed schema.
    pub fn get() -> &'static DiagramSchema {
        static SCHEMA: OnceLock<DiagramSchema> = OnceLock::new();
        SCHEMA.get_or_init(|| {
            let root = serde_json::from_str(SCHEMA_JSON)
                .expect("`input_diagram_schema.json` is not valid JSON.");
            DiagramSchema { root }
        })
    }

    /// Returns the root schema node (the `InputDiagram` object schema).
    pub fn root(&self) -> &Value {
        &self.root
    }

    /// Resolves a `$ref` (or any node) to the concrete schema it points at.
    ///
    /// Follows a chain of `$ref`s (e.g. `ThingId` -> `Id`) until a node without
    /// a `$ref` is reached. Non-`$ref` nodes are returned unchanged.
    pub fn deref<'schema>(&'schema self, node: &'schema Value) -> &'schema Value {
        let mut node = node;
        while let Some(ref_name) = Self::ref_name(node) {
            match self.def(ref_name) {
                Some(def) => node = def,
                None => break,
            }
        }
        node
    }

    /// Returns the `$defs` entry with the given name, if present.
    pub fn def(&self, name: &str) -> Option<&Value> {
        self.root.get("$defs")?.get(name)
    }

    /// Returns the def name a `$ref` node points at, e.g. `"ThingId"` for
    /// `{ "$ref": "#/$defs/ThingId" }`.
    pub fn ref_name(node: &Value) -> Option<&str> {
        node.get("$ref")?.as_str()?.strip_prefix("#/$defs/")
    }

    /// Walks the schema from the root along `path` (a chain of map keys),
    /// returning the schema node for the value at that path.
    ///
    /// At each step the current node is dereferenced, then descended either via
    /// `properties[key]` (a known struct field) or `additionalProperties` (an
    /// arbitrary map entry). Returns `None` if the path cannot be followed.
    pub fn schema_at(&self, path: &[String]) -> Option<&Value> {
        let mut node = &self.root;
        for key in path {
            let container = self.deref(node);
            node = self.field_schema(container, key)?;
        }
        Some(node)
    }

    /// Returns the value schema for `key` within an already-dereferenced
    /// `container` object: its `properties[key]`, falling back to
    /// `additionalProperties` (map entry value), if either is present.
    pub fn field_schema<'schema>(
        &self,
        container: &'schema Value,
        key: &str,
    ) -> Option<&'schema Value> {
        if let Some(field) = container
            .get("properties")
            .and_then(|properties| properties.get(key))
        {
            return Some(field);
        }

        // `additionalProperties` may be a bool (`true`/`false`) or a schema
        // object; only the object form describes a value type.
        container
            .get("additionalProperties")
            .filter(|additional_properties| additional_properties.is_object())
    }

    /// Returns the `(name, description)` of each known field of an object
    /// schema's `properties`.
    pub fn property_entries<'schema>(
        &'schema self,
        node: &'schema Value,
    ) -> Vec<PropertyEntry<'schema>> {
        let Some(properties) = self
            .deref(node)
            .get("properties")
            .and_then(Value::as_object)
        else {
            return Vec::new();
        };

        properties
            .iter()
            .map(|(name, schema)| PropertyEntry {
                name,
                description: schema.get("description").and_then(Value::as_str),
            })
            .collect()
    }

    /// Returns the `(value, description)` of each `const` in an enum schema.
    ///
    /// Handles the `oneOf` form schemars emits for fieldless enums
    /// (`oneOf: [{ const: "row", description: .. }, ..]`). Variants without a
    /// `const` (e.g. a `Custom(Id)` freeform string variant) are skipped.
    pub fn enum_entries<'schema>(&'schema self, node: &'schema Value) -> Vec<EnumEntry<'schema>> {
        let node = self.deref(node);

        let Some(one_of) = node.get("oneOf").and_then(Value::as_array) else {
            return Vec::new();
        };

        one_of
            .iter()
            .filter_map(|variant| {
                let value = variant.get("const")?.as_str()?;
                Some(EnumEntry {
                    value,
                    description: variant.get("description").and_then(Value::as_str),
                })
            })
            .collect()
    }

    /// Returns the `items` schema of an array node, if it is an array.
    pub fn array_items<'schema>(&'schema self, node: &'schema Value) -> Option<&'schema Value> {
        let node = self.deref(node);
        if node.get("type").and_then(Value::as_str) == Some("array") {
            node.get("items")
        } else {
            None
        }
    }
}

/// A known field of an object schema.
pub struct PropertyEntry<'schema> {
    /// The field (YAML key) name.
    pub name: &'schema str,
    /// The field's doc-comment, used as completion detail.
    pub description: Option<&'schema str>,
}

/// A `const` value of an enum schema.
pub struct EnumEntry<'schema> {
    /// The serialized enum value, e.g. `"row"`.
    pub value: &'schema str,
    /// The variant's doc-comment, used as completion detail.
    pub description: Option<&'schema str>,
}