Skip to main content

molframe_core/contract/
serialise.rs

1//! Deterministic provenance interchange formats.
2//!
3//! JSON and mmCIF are projections of one ordered field list. Keeping that list
4//! in one place prevents the two representations from silently recording
5//! different policy decisions.
6
7use super::provenance::Provenance;
8use std::fmt::Write;
9
10impl Provenance {
11    /// Serialises the complete record as deterministic JSON.
12    ///
13    /// Keys follow a fixed order and every policy field is named explicitly.
14    /// The result is independent of map iteration order.
15    #[must_use]
16    pub fn to_json(&self) -> String {
17        let fields = fields(self);
18        let mut output = String::from("{");
19        for (position, (key, value)) in fields.iter().enumerate() {
20            if position != 0 {
21                output.push(',');
22            }
23            json_string(&mut output, key);
24            output.push(':');
25            json_string(&mut output, value);
26        }
27        output.push('}');
28        output
29    }
30
31    /// Serialises the complete record as a standalone mmCIF data block.
32    ///
33    /// The two-column category is deliberately lossless and extensible: older
34    /// readers can preserve fields they do not interpret.
35    #[must_use]
36    pub fn to_mmcif(&self) -> String {
37        let fields = fields(self);
38        let mut output = String::from(
39            "data_molframe_provenance\n#\nloop_\n_molframe_provenance.key\n_molframe_provenance.value\n",
40        );
41        for (key, value) in fields {
42            cif_value(&mut output, &key);
43            output.push(' ');
44            cif_value(&mut output, &value);
45            output.push('\n');
46        }
47        output.push_str("#\n");
48        output
49    }
50}
51
52fn fields(record: &Provenance) -> Vec<(String, String)> {
53    let policy = &record.policy;
54    let mut result = vec![
55        ("molframe_version", record.molframe_version.to_owned()),
56        ("input_source", record.input_source.to_string()),
57        (
58            "input_fingerprint",
59            optional_display(record.input_fingerprint),
60        ),
61        ("policy_fingerprint", record.policy_fingerprint.to_string()),
62        ("profile", optional_profile(record)),
63        (
64            "schema_version",
65            optional_version(record.schema_version.as_ref()),
66        ),
67        (
68            "component_version",
69            optional_version(record.component_version.as_ref()),
70        ),
71        ("timestamp", optional_text(record.timestamp.as_deref())),
72        ("policy.assembly", format!("{:?}", policy.assembly)),
73        ("policy.model", format!("{:?}", policy.model)),
74        ("policy.altloc", format!("{:?}", policy.altloc)),
75        ("policy.identifiers", format!("{:?}", policy.identifiers)),
76        (
77            "policy.missing_atoms",
78            format!("{:?}", policy.missing_atoms),
79        ),
80        ("policy.hydrogens", format!("{:?}", policy.hydrogens)),
81        (
82            "policy.atom_equivalence",
83            format!("{:?}", policy.atom_equivalence),
84        ),
85        ("policy.symmetry", format!("{:?}", policy.symmetry)),
86        ("policy.alignment", format!("{:?}", policy.alignment)),
87        ("policy.precision", format!("{:?}", policy.precision)),
88        ("policy.periodic", format!("{:?}", policy.periodic)),
89        ("policy.vdw_radii", format!("{:?}", policy.vdw_radii)),
90        ("policy.contact_def", format!("{:?}", policy.contact_def)),
91        (
92            "policy.float_tolerance.relative",
93            policy.float_tolerance.relative.to_string(),
94        ),
95        (
96            "policy.float_tolerance.absolute",
97            policy.float_tolerance.absolute.to_string(),
98        ),
99    ]
100    .into_iter()
101    .map(|(key, value)| (key.to_owned(), value))
102    .collect::<Vec<_>>();
103    if let Some(algorithm) = &record.algorithm {
104        result.push(("algorithm.name".to_owned(), algorithm.name().to_owned()));
105        result.push((
106            "algorithm.version".to_owned(),
107            algorithm.version().to_owned(),
108        ));
109    }
110    result.extend(
111        record
112            .parameters
113            .iter()
114            .map(|(name, value)| (format!("parameter.{name}"), value.serialised())),
115    );
116    result
117}
118
119fn optional_display(value: Option<impl std::fmt::Display>) -> String {
120    match value {
121        Some(value) => value.to_string(),
122        None => ".".to_owned(),
123    }
124}
125
126fn optional_profile(record: &Provenance) -> String {
127    match record.profile {
128        Some(profile) => profile.as_str().to_owned(),
129        None => ".".to_owned(),
130    }
131}
132
133fn optional_version(value: Option<&super::provenance::DictionaryVersion>) -> String {
134    match value {
135        Some(value) => value.as_str().to_owned(),
136        None => ".".to_owned(),
137    }
138}
139
140fn optional_text(value: Option<&str>) -> String {
141    match value {
142        Some(value) => value.to_owned(),
143        None => ".".to_owned(),
144    }
145}
146
147fn json_string(output: &mut String, value: &str) {
148    output.push('"');
149    for character in value.chars() {
150        match character {
151            '"' => output.push_str("\\\""),
152            '\\' => output.push_str("\\\\"),
153            '\n' => output.push_str("\\n"),
154            '\r' => output.push_str("\\r"),
155            '\t' => output.push_str("\\t"),
156            control if control.is_control() => {
157                let _ = write!(output, "\\u{:04x}", u32::from(control));
158            }
159            ordinary => output.push(ordinary),
160        }
161    }
162    output.push('"');
163}
164
165fn cif_value(output: &mut String, value: &str) {
166    if bare_cif(value) {
167        output.push_str(value);
168    } else if !value.contains('\'') && !value.contains('\n') {
169        output.push('\'');
170        output.push_str(value);
171        output.push('\'');
172    } else if !value.contains('"') && !value.contains('\n') {
173        output.push('"');
174        output.push_str(value);
175        output.push('"');
176    } else {
177        output.push_str("\n;");
178        output.push_str(value);
179        output.push_str("\n;");
180    }
181}
182
183fn bare_cif(value: &str) -> bool {
184    !value.is_empty()
185        && value != "."
186        && value != "?"
187        && !value.chars().any(char::is_whitespace)
188        && !value.starts_with(['#', '$', ';', '_', '\'', '"'])
189        && !value.eq_ignore_ascii_case("loop_")
190        && !value.to_ascii_lowercase().starts_with("data_")
191        && !value.to_ascii_lowercase().starts_with("save_")
192}
193
194#[cfg(test)]
195#[path = "serialise_tests.rs"]
196mod tests;