Skip to main content

molframe_query/api/
builder.rs

1//! Compile-time checked Rust builder surface.
2
3use crate::ast::{Column, Expr, GeometricExpr, Macro, Operator};
4use std::ops::{BitAnd, BitOr, Not};
5
6/// A typed selection expression under construction.
7#[derive(Clone, PartialEq, Debug)]
8pub struct Builder {
9    expr: Expr,
10    source: Box<str>,
11}
12
13impl Builder {
14    /// Consumes the builder into the shared selection IR.
15    #[must_use]
16    pub(crate) fn into_parts(self) -> (Expr, Box<str>) {
17        (self.expr, self.source)
18    }
19
20    /// Canonical textual form accepted by the query parser.
21    #[must_use]
22    pub fn source(&self) -> &str {
23        &self.source
24    }
25
26    fn new(expr: Expr, source: impl Into<Box<str>>) -> Self {
27        Self {
28            expr,
29            source: source.into(),
30        }
31    }
32}
33
34impl BitAnd for Builder {
35    type Output = Self;
36
37    fn bitand(self, right: Self) -> Self::Output {
38        let source = format!("({}) and ({})", self.source, right.source);
39        Self::new(Expr::And(Box::new(self.expr), Box::new(right.expr)), source)
40    }
41}
42
43impl BitOr for Builder {
44    type Output = Self;
45
46    fn bitor(self, right: Self) -> Self::Output {
47        let source = format!("({}) or ({})", self.source, right.source);
48        Self::new(Expr::Or(Box::new(self.expr), Box::new(right.expr)), source)
49    }
50}
51
52impl Not for Builder {
53    type Output = Self;
54
55    fn not(self) -> Self::Output {
56        let source = format!("not ({})", self.source);
57        Self::new(Expr::Not(Box::new(self.expr)), source)
58    }
59}
60
61/// A typed column awaiting a predicate.
62#[derive(Clone, Copy, PartialEq, Eq, Debug)]
63pub struct ColumnBuilder(Column, &'static str);
64
65impl ColumnBuilder {
66    /// String or integer membership equality.
67    #[must_use]
68    pub fn eq(self, value: impl Into<Box<str>>) -> Builder {
69        let value = value.into();
70        Builder::new(
71            Expr::Membership {
72                column: self.0,
73                values: vec![value.clone()],
74            },
75            format!("{} {:?}", self.1, value),
76        )
77    }
78
79    /// Numeric less-than comparison.
80    #[must_use]
81    pub fn lt(self, value: f64) -> Builder {
82        self.compare(Operator::Less, value)
83    }
84
85    /// Numeric less-than-or-equal comparison.
86    #[must_use]
87    pub fn le(self, value: f64) -> Builder {
88        self.compare(Operator::LessEqual, value)
89    }
90
91    /// Numeric greater-than comparison.
92    #[must_use]
93    pub fn gt(self, value: f64) -> Builder {
94        self.compare(Operator::Greater, value)
95    }
96
97    /// Numeric greater-than-or-equal comparison.
98    #[must_use]
99    pub fn ge(self, value: f64) -> Builder {
100        self.compare(Operator::GreaterEqual, value)
101    }
102
103    fn compare(self, operator: Operator, value: f64) -> Builder {
104        let symbol = match operator {
105            Operator::Less => "<",
106            Operator::LessEqual => "<=",
107            Operator::Greater => ">",
108            Operator::GreaterEqual => ">=",
109            Operator::Equal => "=",
110            Operator::NotEqual => "!=",
111        };
112        Builder::new(
113            Expr::Comparison {
114                column: self.0,
115                operator,
116                value,
117                absolute: false,
118            },
119            format!("{} {symbol} {value}", self.1),
120        )
121    }
122}
123
124/// Builder functions sharing the textual parser's typed IR.
125pub mod col {
126    use super::{Builder, Column, ColumnBuilder, Expr, GeometricExpr, Macro};
127
128    /// Every atom.
129    #[must_use]
130    pub fn all() -> Builder {
131        Builder::new(Expr::All, "all")
132    }
133
134    /// No atoms.
135    #[must_use]
136    pub fn none() -> Builder {
137        Builder::new(Expr::None, "none")
138    }
139
140    /// Protein atoms.
141    #[must_use]
142    pub fn is_protein() -> Builder {
143        Builder::new(Expr::Macro(Macro::Protein), "protein")
144    }
145
146    macro_rules! molecular_selector {
147        ($name:ident, $variant:ident, $source:literal, $doc:literal) => {
148            #[doc = $doc]
149            #[must_use]
150            pub fn $name() -> Builder {
151                Builder::new(Expr::Macro(Macro::$variant), $source)
152            }
153        };
154    }
155
156    molecular_selector!(backbone, Backbone, "backbone", "Protein backbone atoms.");
157    molecular_selector!(
158        sidechain,
159        Sidechain,
160        "sidechain",
161        "Protein side-chain atoms."
162    );
163    molecular_selector!(nucleic, Nucleic, "nucleic", "Nucleic-acid atoms.");
164    molecular_selector!(
165        nucleic_backbone,
166        NucleicBackbone,
167        "nucleicbackbone",
168        "Nucleic-acid backbone atoms."
169    );
170    molecular_selector!(
171        nucleic_base,
172        NucleicBase,
173        "nucleicbase",
174        "Nucleic-acid base atoms."
175    );
176    molecular_selector!(
177        nucleic_sugar,
178        NucleicSugar,
179        "nucleicsugar",
180        "Nucleic-acid sugar atoms."
181    );
182    molecular_selector!(water, Water, "water", "Water atoms.");
183    molecular_selector!(ions, Ion, "ion", "Ion atoms.");
184    molecular_selector!(lipids, Lipid, "lipid", "Lipid atoms.");
185    molecular_selector!(glycans, Saccharide, "saccharide", "Saccharide atoms.");
186    molecular_selector!(hetero, Hetero, "hetero", "Heterogeneous atoms.");
187    molecular_selector!(hydrogen, Hydrogen, "hydrogen", "Hydrogen atoms.");
188    molecular_selector!(
189        polar_hydrogen,
190        PolarHydrogen,
191        "polar_hydrogen",
192        "Hydrogen atoms bonded to N, O or S in the full topology. Requires available bonds.
193
194```
195use molframe_query::{Query, col};
196let query = Query::from_builder(col::polar_hydrogen());
197assert_eq!(query.source(), \"polar_hydrogen\");
198```"
199    );
200    molecular_selector!(
201        nonpolar_hydrogen,
202        NonpolarHydrogen,
203        "nonpolar_hydrogen",
204        "Hydrogen atoms not bonded to N, O or S, including unbonded H. Requires available bonds.
205
206```
207use molframe_query::{Query, col};
208let query = Query::from_builder(col::nonpolar_hydrogen());
209assert_eq!(query.source(), \"nonpolar_hydrogen\");
210```"
211    );
212    molecular_selector!(heavy, Heavy, "heavy", "Non-hydrogen atoms.");
213    molecular_selector!(polymer, Polymer, "polymer", "Polymer atoms.");
214    molecular_selector!(ligands, Ligand, "ligand", "Non-polymer ligand atoms.");
215    molecular_selector!(aromatic, Aromatic, "aromatic", "Aromatic atoms.");
216    molecular_selector!(
217        helix,
218        Helix,
219        "helix",
220        "All helices, including polyproline-II and deposited other helices."
221    );
222    molecular_selector!(strand, Strand, "strand", "Beta-ladder residues only.");
223    molecular_selector!(sheet, Sheet, "sheet", "Beta strands and isolated bridges.");
224    molecular_selector!(
225        alpha_helix,
226        AlphaHelix,
227        "alpha_helix",
228        "Alpha-helix residues."
229    );
230    molecular_selector!(
231        helix_310,
232        Helix310,
233        "helix_310",
234        "Three-ten helix residues."
235    );
236    molecular_selector!(pi_helix, PiHelix, "pi_helix", "Pi-helix residues.");
237    molecular_selector!(
238        polyproline,
239        Polyproline,
240        "polyproline",
241        "Polyproline-II helix residues."
242    );
243    molecular_selector!(bridge, Bridge, "bridge", "Isolated beta-bridge residues.");
244    molecular_selector!(turn, Turn, "turn", "Hydrogen-bonded turn residues.");
245    molecular_selector!(bend, Bend, "bend", "Bend residues.");
246    molecular_selector!(
247        coil,
248        Coil,
249        "coil",
250        "Assigned coil residues, excluding unknown."
251    );
252    molecular_selector!(
253        plddt_very_high,
254        PlddtVeryHigh,
255        "plddt_very_high",
256        "Atoms whose pLDDT is above 90."
257    );
258    molecular_selector!(
259        plddt_confident,
260        PlddtConfident,
261        "plddt_confident",
262        "Atoms whose pLDDT is above 70 and at most 90."
263    );
264    molecular_selector!(
265        plddt_low,
266        PlddtLow,
267        "plddt_low",
268        "Atoms whose pLDDT is above 50 and at most 70."
269    );
270    molecular_selector!(
271        plddt_very_low,
272        PlddtVeryLow,
273        "plddt_very_low",
274        "Atoms whose pLDDT is at most 50."
275    );
276
277    /// Atom name column.
278    #[must_use]
279    pub const fn name() -> ColumnBuilder {
280        ColumnBuilder(Column::AtomName, "name")
281    }
282
283    /// Residue name column.
284    #[must_use]
285    pub const fn resname() -> ColumnBuilder {
286        ColumnBuilder(Column::ResidueName, "resname")
287    }
288
289    /// Chain identifier column.
290    #[must_use]
291    pub const fn chain() -> ColumnBuilder {
292        ColumnBuilder(Column::Chain, "chain")
293    }
294
295    /// Temperature-factor column.
296    #[must_use]
297    pub const fn bfactor() -> ColumnBuilder {
298        ColumnBuilder(Column::BFactor, "bfactor")
299    }
300
301    /// Occupancy column.
302    #[must_use]
303    pub const fn occupancy() -> ColumnBuilder {
304        ColumnBuilder(Column::Occupancy, "occupancy")
305    }
306
307    /// X coordinate.
308    #[must_use]
309    pub const fn x() -> ColumnBuilder {
310        ColumnBuilder(Column::X, "x")
311    }
312
313    /// Atoms within `radius` of `target`.
314    #[must_use]
315    pub fn within(radius: f32, target: Builder) -> Builder {
316        let source = format!("within {radius} of ({})", target.source);
317        Builder::new(
318            Expr::Geometric(GeometricExpr::Within {
319                radius,
320                target: Box::new(target.expr),
321            }),
322            source,
323        )
324    }
325
326    /// Whole residues containing `target` atoms.
327    #[must_use]
328    pub fn by_residue(target: Builder) -> Builder {
329        let source = format!("byres ({})", target.source);
330        Builder::new(Expr::ByResidue(Box::new(target.expr)), source)
331    }
332
333    /// Whole residues within `radius` of `target`.
334    #[must_use]
335    pub fn residues_within(radius: f32, target: Builder) -> Builder {
336        by_residue(within(radius, target))
337    }
338}
339
340#[cfg(test)]
341#[path = "builder_tests.rs"]
342mod tests;