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    /// The record as ordered key and value pairs: the one list that `to_json` and
32    /// `to_mmcif` both project, for a caller that writes another format.
33    #[must_use]
34    pub fn entries(&self) -> Vec<(String, String)> {
35        fields(self)
36    }
37
38    /// Serialises the complete record as a standalone mmCIF data block.
39    ///
40    /// The two-column category is deliberately lossless and extensible: older
41    /// readers can preserve fields they do not interpret.
42    #[must_use]
43    pub fn to_mmcif(&self) -> String {
44        let fields = fields(self);
45        let mut output = String::from(
46            "data_molframe_provenance\n#\nloop_\n_molframe_provenance.key\n_molframe_provenance.value\n",
47        );
48        for (key, value) in fields {
49            cif_value(&mut output, &key);
50            output.push(' ');
51            cif_value(&mut output, &value);
52            output.push('\n');
53        }
54        output.push_str("#\n");
55        output
56    }
57}
58
59fn fields(record: &Provenance) -> Vec<(String, String)> {
60    let policy = &record.policy;
61    let mut result = vec![
62        ("molframe_version", record.molframe_version.to_owned()),
63        ("input_source", record.input_source.to_string()),
64        ("input_sha256", optional_display(record.input_digest)),
65        ("policy_fingerprint", record.policy_fingerprint.to_string()),
66        ("profile", optional_profile(record)),
67        (
68            "schema_version",
69            optional_version(record.schema_version.as_ref()),
70        ),
71        (
72            "component_version",
73            optional_version(record.component_version.as_ref()),
74        ),
75        ("timestamp", optional_text(record.timestamp.as_deref())),
76        ("policy.assembly", format!("{:?}", policy.assembly)),
77        ("policy.model", format!("{:?}", policy.model)),
78        ("policy.altloc", format!("{:?}", policy.altloc)),
79        ("policy.identifiers", format!("{:?}", policy.identifiers)),
80        (
81            "policy.missing_atoms",
82            format!("{:?}", policy.missing_atoms),
83        ),
84        ("policy.hydrogens", format!("{:?}", policy.hydrogens)),
85        (
86            "policy.atom_equivalence",
87            format!("{:?}", policy.atom_equivalence),
88        ),
89        ("policy.symmetry", format!("{:?}", policy.symmetry)),
90        ("policy.alignment", format!("{:?}", policy.alignment)),
91        ("policy.precision", format!("{:?}", policy.precision)),
92        ("policy.periodic", format!("{:?}", policy.periodic)),
93        ("policy.vdw_radii", format!("{:?}", policy.vdw_radii)),
94        ("policy.contact_def", format!("{:?}", policy.contact_def)),
95        (
96            "policy.float_tolerance.relative",
97            policy.float_tolerance.relative.to_string(),
98        ),
99        (
100            "policy.float_tolerance.absolute",
101            policy.float_tolerance.absolute.to_string(),
102        ),
103    ]
104    .into_iter()
105    .map(|(key, value)| (key.to_owned(), value))
106    .collect::<Vec<_>>();
107    if let Some(algorithm) = &record.algorithm {
108        result.push(("algorithm.name".to_owned(), algorithm.name().to_owned()));
109        result.push((
110            "algorithm.version".to_owned(),
111            algorithm.version().to_owned(),
112        ));
113    }
114    result.extend(
115        record
116            .parameters
117            .iter()
118            .map(|(name, value)| (format!("parameter.{name}"), value.serialised())),
119    );
120    result
121}
122
123fn optional_display(value: Option<impl std::fmt::Display>) -> String {
124    match value {
125        Some(value) => value.to_string(),
126        None => ".".to_owned(),
127    }
128}
129
130fn optional_profile(record: &Provenance) -> String {
131    match record.profile {
132        Some(profile) => profile.as_str().to_owned(),
133        None => ".".to_owned(),
134    }
135}
136
137fn optional_version(value: Option<&super::provenance::DictionaryVersion>) -> String {
138    match value {
139        Some(value) => value.as_str().to_owned(),
140        None => ".".to_owned(),
141    }
142}
143
144fn optional_text(value: Option<&str>) -> String {
145    match value {
146        Some(value) => value.to_owned(),
147        None => ".".to_owned(),
148    }
149}
150
151/// Appends `value` to `output` as a JSON string literal.
152pub fn push_json_string(output: &mut String, value: &str) {
153    json_string(output, value);
154}
155
156fn json_string(output: &mut String, value: &str) {
157    output.push('"');
158    for character in value.chars() {
159        match character {
160            '"' => output.push_str("\\\""),
161            '\\' => output.push_str("\\\\"),
162            '\n' => output.push_str("\\n"),
163            '\r' => output.push_str("\\r"),
164            '\t' => output.push_str("\\t"),
165            control if control.is_control() => {
166                let _ = write!(output, "\\u{:04x}", u32::from(control));
167            }
168            ordinary => output.push(ordinary),
169        }
170    }
171    output.push('"');
172}
173
174fn cif_value(output: &mut String, value: &str) {
175    if bare_cif(value) {
176        output.push_str(value);
177    } else if !value.contains('\'') && !value.contains('\n') {
178        output.push('\'');
179        output.push_str(value);
180        output.push('\'');
181    } else if !value.contains('"') && !value.contains('\n') {
182        output.push('"');
183        output.push_str(value);
184        output.push('"');
185    } else {
186        output.push_str("\n;");
187        output.push_str(value);
188        output.push_str("\n;");
189    }
190}
191
192fn bare_cif(value: &str) -> bool {
193    !value.is_empty()
194        && value != "."
195        && value != "?"
196        && !value.chars().any(char::is_whitespace)
197        && !value.starts_with(['#', '$', ';', '_', '\'', '"'])
198        && !value.eq_ignore_ascii_case("loop_")
199        && !value.to_ascii_lowercase().starts_with("data_")
200        && !value.to_ascii_lowercase().starts_with("save_")
201}
202
203#[cfg(test)]
204#[path = "serialise_tests.rs"]
205mod tests;