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/// JSON Schema of the `inspect_similar_code` candidate snapshot handoff, the
38/// object `find_similar_code` hands back and `inspect` reads from stdin.
39#[cfg(feature = "schema")]
40#[must_use]
41pub fn similar_code_snapshot_schema() -> serde_json::Value {
42    fallow_output::SimilarCodeCandidateSnapshot::json_schema()
43}
44
45/// The zero-config `rules.*` severities as a flat JSON object keyed by config
46/// key (`unused-exports`, `security-sink`, ...), serialized once from
47/// `RulesConfig::default()`. This is the single source of default severities
48/// for `fallow schema` and the MCP issue-type resource. Infallible in
49/// practice (a flat struct of `Severity` enums); the empty-object fallback
50/// keeps callers panic-free and simply yields no default severity if
51/// serialization ever changed shape.
52#[must_use]
53pub fn default_rule_severities() -> serde_json::Value {
54    DEFAULT_RULE_SEVERITIES.clone()
55}
56
57/// Whether `key` names a `rules.*` config field (a kebab-case key of
58/// [`default_rule_severities`]).
59#[must_use]
60pub fn is_rule_severity_key(key: &str) -> bool {
61    DEFAULT_RULE_SEVERITIES
62        .get(key)
63        .is_some_and(serde_json::Value::is_string)
64}
65
66#[cfg(test)]
67mod tests {
68    use super::*;
69
70    #[test]
71    fn schemas_are_json_objects_with_properties() {
72        for (label, schema) in [
73            ("config", config_schema()),
74            ("plugin", plugin_schema()),
75            ("rule-pack", rule_pack_schema()),
76        ] {
77            assert!(
78                schema
79                    .get("properties")
80                    .is_some_and(serde_json::Value::is_object),
81                "{label} schema must be an object schema with properties"
82            );
83        }
84    }
85
86    #[test]
87    fn default_severities_are_keyed_by_config_key() {
88        let defaults = default_rule_severities();
89        assert_eq!(defaults["unused-exports"], "error");
90        assert_eq!(defaults["security-sink"], "off");
91        assert!(
92            defaults.get("unused_exports").is_none(),
93            "keys are kebab-case"
94        );
95        assert!(is_rule_severity_key("coverage-gaps"));
96        assert!(!is_rule_severity_key("code-duplication"));
97    }
98}