Skip to main content

fallow_api/
schemas.rs

1//! JSON Schema documents and zero-config rule defaults, re-exported from
2//! `fallow-config` for embedders that must not depend on the config crate
3//! directly (the MCP server's `fallow://schema/*` and `fallow://issue-types`
4//! resources). Each function returns the exact document the matching CLI
5//! command prints (`fallow config-schema`, `fallow plugin-schema`,
6//! `fallow rule-pack-schema`), so a cached resource and a CLI dump agree.
7
8use std::sync::LazyLock;
9
10use fallow_config::{ExternalPluginDef, FallowConfig, RulePackDef, RulesConfig};
11
12/// `RulesConfig::default()` serialized once; the struct is compile-time
13/// constant, so the map never changes within a process.
14static DEFAULT_RULE_SEVERITIES: LazyLock<serde_json::Value> = LazyLock::new(|| {
15    serde_json::to_value(RulesConfig::default())
16        .unwrap_or_else(|_| serde_json::Value::Object(serde_json::Map::new()))
17});
18
19/// JSON Schema of the fallow config file (`fallow config-schema`).
20#[must_use]
21pub fn config_schema() -> serde_json::Value {
22    FallowConfig::json_schema()
23}
24
25/// JSON Schema of a user-authored external plugin (`fallow plugin-schema`).
26#[must_use]
27pub fn plugin_schema() -> serde_json::Value {
28    ExternalPluginDef::json_schema()
29}
30
31/// JSON Schema of a declarative rule pack (`fallow rule-pack-schema`).
32#[must_use]
33pub fn rule_pack_schema() -> serde_json::Value {
34    RulePackDef::json_schema()
35}
36
37/// The zero-config `rules.*` severities as a flat JSON object keyed by config
38/// key (`unused-exports`, `security-sink`, ...), serialized once from
39/// `RulesConfig::default()`. This is the single source of default severities
40/// for `fallow schema` and the MCP issue-type resource. Infallible in
41/// practice (a flat struct of `Severity` enums); the empty-object fallback
42/// keeps callers panic-free and simply yields no default severity if
43/// serialization ever changed shape.
44#[must_use]
45pub fn default_rule_severities() -> serde_json::Value {
46    DEFAULT_RULE_SEVERITIES.clone()
47}
48
49/// Whether `key` names a `rules.*` config field (a kebab-case key of
50/// [`default_rule_severities`]).
51#[must_use]
52pub fn is_rule_severity_key(key: &str) -> bool {
53    DEFAULT_RULE_SEVERITIES
54        .get(key)
55        .is_some_and(serde_json::Value::is_string)
56}
57
58#[cfg(test)]
59mod tests {
60    use super::*;
61
62    #[test]
63    fn schemas_are_json_objects_with_properties() {
64        for (label, schema) in [
65            ("config", config_schema()),
66            ("plugin", plugin_schema()),
67            ("rule-pack", rule_pack_schema()),
68        ] {
69            assert!(
70                schema
71                    .get("properties")
72                    .is_some_and(serde_json::Value::is_object),
73                "{label} schema must be an object schema with properties"
74            );
75        }
76    }
77
78    #[test]
79    fn default_severities_are_keyed_by_config_key() {
80        let defaults = default_rule_severities();
81        assert_eq!(defaults["unused-exports"], "error");
82        assert_eq!(defaults["security-sink"], "off");
83        assert!(
84            defaults.get("unused_exports").is_none(),
85            "keys are kebab-case"
86        );
87        assert!(is_rule_severity_key("coverage-gaps"));
88        assert!(!is_rule_severity_key("code-duplication"));
89    }
90}