Skip to main content

pest_toml_merge/
lib.rs

1use std::sync::Once;
2
3use ast_merge::{
4    ConformanceFamilyPlanContext, ConformanceFeatureProfileView, Diagnostic, DiagnosticCategory,
5    DiagnosticSeverity, MergeResult, ParseResult, PolicyReference, PolicySurface,
6};
7use pest::Parser;
8use pest_grammars::toml::{Rule as PestTomlRule, TomlParser as PestTomlParser};
9use toml_merge::{
10    TomlAnalysis, TomlDialect, TomlFeatureProfile, TomlOwnerMatchResult, analyze_toml_source,
11    match_toml_owners as match_toml_owners_with_substrate, merge_toml_with_parser,
12    toml_feature_profile,
13};
14use tree_haver::{BackendReference, register_backend};
15
16pub const PACKAGE_NAME: &str = "pest-toml-merge";
17pub const BACKEND_ID: &str = "pest";
18
19fn ensure_backend_registered() {
20    static ONCE: Once = Once::new();
21    ONCE.call_once(|| {
22        register_backend(BackendReference {
23            id: BACKEND_ID.to_string(),
24            family: "peg".to_string(),
25        });
26    });
27}
28
29fn unsupported_feature(message: &str) -> Diagnostic {
30    Diagnostic {
31        severity: DiagnosticSeverity::Error,
32        category: DiagnosticCategory::UnsupportedFeature,
33        message: message.to_string(),
34        path: None,
35        review: None,
36    }
37}
38
39fn parse_error(message: &str) -> Diagnostic {
40    Diagnostic {
41        severity: DiagnosticSeverity::Error,
42        category: DiagnosticCategory::ParseError,
43        message: message.to_string(),
44        path: None,
45        review: None,
46    }
47}
48
49pub fn available_toml_backends() -> Vec<String> {
50    ensure_backend_registered();
51    vec![BACKEND_ID.to_string()]
52}
53
54pub fn toml_backend_feature_profile() -> std::collections::BTreeMap<String, serde_json::Value> {
55    ensure_backend_registered();
56    let mut profile = serde_json::Map::new();
57    profile.insert("family".to_string(), serde_json::Value::String("toml".to_string()));
58    profile.insert(
59        "supported_dialects".to_string(),
60        serde_json::Value::Array(vec![serde_json::Value::String("toml".to_string())]),
61    );
62    profile.insert(
63        "supported_policies".to_string(),
64        serde_json::Value::Array(vec![serde_json::json!({
65            "surface": "array",
66            "name": "destination_wins_array",
67        })]),
68    );
69    profile.insert("backend".to_string(), serde_json::Value::String(BACKEND_ID.to_string()));
70    profile.insert(
71        "backend_ref".to_string(),
72        serde_json::json!({
73            "id": BACKEND_ID,
74            "family": "peg",
75        }),
76    );
77    profile.into_iter().collect()
78}
79
80pub fn toml_plan_context() -> ConformanceFamilyPlanContext {
81    ensure_backend_registered();
82    ConformanceFamilyPlanContext {
83        family_profile: ast_merge::FamilyFeatureProfile {
84            family: "toml".to_string(),
85            supported_dialects: vec!["toml".to_string()],
86            supported_policies: vec![PolicyReference {
87                surface: PolicySurface::Array,
88                name: "destination_wins_array".to_string(),
89            }],
90        },
91        feature_profile: Some(ConformanceFeatureProfileView {
92            backend: BACKEND_ID.to_string(),
93            supports_dialects: false,
94            supported_policies: vec![PolicyReference {
95                surface: PolicySurface::Array,
96                name: "destination_wins_array".to_string(),
97            }],
98        }),
99    }
100}
101
102pub fn provider_toml_feature_profile() -> TomlFeatureProfile {
103    toml_feature_profile()
104}
105
106pub fn parse_toml(
107    source: &str,
108    dialect: TomlDialect,
109    backend: Option<&str>,
110) -> ParseResult<TomlAnalysis> {
111    ensure_backend_registered();
112    let requested = backend.unwrap_or(BACKEND_ID);
113    if requested != BACKEND_ID {
114        return ParseResult {
115            ok: false,
116            diagnostics: vec![unsupported_feature(&format!(
117                "Unsupported TOML backend {requested}."
118            ))],
119            analysis: None,
120            policies: vec![],
121        };
122    }
123
124    if let Err(error) = PestTomlParser::parse(PestTomlRule::toml, source) {
125        return ParseResult {
126            ok: false,
127            diagnostics: vec![parse_error(&error.to_string())],
128            analysis: None,
129            policies: vec![],
130        };
131    }
132
133    analyze_toml_source(source, dialect)
134}
135
136pub fn match_toml_owners(
137    template: &TomlAnalysis,
138    destination: &TomlAnalysis,
139) -> TomlOwnerMatchResult {
140    match_toml_owners_with_substrate(template, destination)
141}
142
143pub fn merge_toml(
144    template_source: &str,
145    destination_source: &str,
146    dialect: TomlDialect,
147    backend: Option<&str>,
148) -> MergeResult<String> {
149    ensure_backend_registered();
150    let requested = backend.unwrap_or(BACKEND_ID);
151    if requested != BACKEND_ID {
152        return MergeResult {
153            ok: false,
154            diagnostics: vec![unsupported_feature(&format!(
155                "Unsupported TOML backend {requested}."
156            ))],
157            output: None,
158            policies: vec![],
159        };
160    }
161
162    merge_toml_with_parser(template_source, destination_source, dialect, |source, parse_dialect| {
163        parse_toml(source, parse_dialect, None)
164    })
165}