1#![forbid(unsafe_code)]
2#![allow(clippy::module_name_repetitions)]
3#![doc = include_str!("../README.md")]
4
5mod catalyst;
8mod chemical_reaction;
9mod error;
10mod product;
11mod reactant;
12mod reaction_arrow;
13mod reaction_condition;
14mod reaction_condition_set;
15mod reaction_direction;
16mod reaction_equation;
17mod reaction_kind;
18mod reaction_term;
19mod solvent;
20
21pub use catalyst::Catalyst;
22pub use chemical_reaction::ChemicalReaction;
23pub use error::ReactionValidationError;
24pub use product::Product;
25pub use reactant::Reactant;
26pub use reaction_arrow::ReactionArrow;
27pub use reaction_condition::ReactionCondition;
28pub use reaction_condition_set::ReactionConditionSet;
29pub use reaction_direction::ReactionDirection;
30pub use reaction_equation::ReactionEquation;
31pub use reaction_kind::ReactionKind;
32pub use reaction_term::ReactionTerm;
33pub use solvent::Solvent;
34
35#[cfg(test)]
36mod tests {
37 use use_chemical_formula::ChemicalFormula;
38 use use_stoichiometry::StoichiometryValidationError;
39
40 use super::{
41 Catalyst, ChemicalReaction, Product, Reactant, ReactionArrow, ReactionCondition,
42 ReactionConditionSet, ReactionEquation, ReactionKind, ReactionTerm,
43 ReactionValidationError, Solvent,
44 };
45
46 fn formula(input: &str) -> ChemicalFormula {
47 ChemicalFormula::parse(input).expect("test formula should parse")
48 }
49
50 fn term(coefficient: u32, input: &str) -> ReactionTerm {
51 ReactionTerm::new(formula(input))
52 .with_coefficient(coefficient)
53 .expect("coefficient should be valid")
54 }
55
56 #[test]
57 fn creates_simple_synthesis_reaction() {
58 let reaction = ChemicalReaction::new()
59 .with_reactant(term(2, "H2"))
60 .with_reactant(term(1, "O2"))
61 .with_product(term(2, "H2O"))
62 .with_kind(ReactionKind::Synthesis);
63
64 assert_eq!(reaction.to_string(), "2H2 + O2 -> 2H2O");
65 assert_eq!(reaction.kinds(), &[ReactionKind::Synthesis]);
66 assert_eq!(reaction.validate(), Ok(()));
67 }
68
69 #[test]
70 fn creates_decomposition_reaction() {
71 let reaction = ChemicalReaction::new()
72 .with_reactant(term(1, "CaCO3"))
73 .with_product(term(1, "CaO"))
74 .with_product(term(1, "CO2"))
75 .with_kind(ReactionKind::Decomposition);
76
77 assert_eq!(reaction.to_string(), "CaCO3 -> CaO + CO2");
78 assert_eq!(reaction.validate(), Ok(()));
79 }
80
81 #[test]
82 fn creates_combustion_reaction() {
83 let reaction = ChemicalReaction::new()
84 .with_reactant(term(1, "CH4"))
85 .with_reactant(term(2, "O2"))
86 .with_product(term(1, "CO2"))
87 .with_product(term(2, "H2O"))
88 .with_kind(ReactionKind::Combustion);
89
90 assert_eq!(reaction.to_string(), "CH4 + 2O2 -> CO2 + 2H2O");
91 assert_eq!(reaction.kinds(), &[ReactionKind::Combustion]);
92 }
93
94 #[test]
95 fn creates_reactants_and_products() {
96 let reactant = Reactant::new(term(3, "H2"));
97 let product = Product::new(term(2, "NH3"));
98
99 assert_eq!(reactant.to_string(), "3H2");
100 assert_eq!(product.to_string(), "2NH3");
101 assert_eq!(reactant.as_term().coefficient().value(), 3);
102 assert_eq!(product.as_term().formula().to_string(), "NH3");
103 }
104
105 #[test]
106 fn displays_reaction_terms_and_omits_one() {
107 let oxygen = term(1, "O2");
108 let ammonia = term(2, "NH3");
109
110 assert_eq!(oxygen.to_string(), "O2");
111 assert_eq!(ammonia.to_string(), "2NH3");
112 assert_eq!(oxygen.coefficient().value(), 1);
113 assert!(oxygen.coefficient().is_one());
114 }
115
116 #[test]
117 fn displays_reaction_arrows() {
118 assert_eq!(ReactionArrow::Forward.to_string(), "->");
119 assert_eq!(ReactionArrow::Reverse.to_string(), "<-");
120 assert_eq!(ReactionArrow::Reversible.to_string(), "<->");
121 assert_eq!(ReactionArrow::Equilibrium.to_string(), "⇌");
122 assert!(ReactionArrow::Reversible.is_reversible());
123 assert!(ReactionArrow::Equilibrium.is_reversible());
124 }
125
126 #[test]
127 fn stores_catalyst_and_solvent_conditions() {
128 let catalyst = Catalyst::new("Pt").expect("catalyst should be valid");
129 let solvent = Solvent::new("water").expect("solvent should be valid");
130 let conditions = ReactionConditionSet::new()
131 .with_condition(ReactionCondition::Catalyst(catalyst.clone()))
132 .with_condition(ReactionCondition::Solvent(solvent.clone()));
133
134 assert_eq!(catalyst.to_string(), "Pt");
135 assert_eq!(solvent.to_string(), "water");
136 assert_eq!(conditions.len(), 2);
137 assert_eq!(conditions.to_string(), "catalyst: Pt, solvent: water");
138 assert_eq!(conditions.validate(), Ok(()));
139 }
140
141 #[test]
142 fn stores_heat_and_light_condition_labels() {
143 let conditions = ReactionConditionSet::new()
144 .with_condition(ReactionCondition::Heat)
145 .with_condition(ReactionCondition::Light);
146
147 assert_eq!(ReactionCondition::Heat.to_string(), "heat");
148 assert_eq!(ReactionCondition::Light.to_string(), "light");
149 assert_eq!(conditions.to_string(), "heat, light");
150 }
151
152 #[test]
153 fn assigns_reaction_kind_once() {
154 let reaction = ChemicalReaction::new()
155 .with_kind(ReactionKind::AcidBase)
156 .with_kind(ReactionKind::AcidBase)
157 .with_kind(ReactionKind::Neutralization);
158
159 assert_eq!(ReactionKind::AcidBase.to_string(), "acid-base");
160 assert_eq!(
161 reaction.kinds(),
162 &[ReactionKind::AcidBase, ReactionKind::Neutralization]
163 );
164 }
165
166 #[test]
167 fn validates_empty_and_incomplete_reactions() {
168 assert_eq!(
169 ChemicalReaction::new().validate(),
170 Err(ReactionValidationError::EmptyReaction)
171 );
172 assert_eq!(
173 ChemicalReaction::new()
174 .with_product(term(1, "H2O"))
175 .validate(),
176 Err(ReactionValidationError::MissingReactants)
177 );
178 assert_eq!(
179 ChemicalReaction::new()
180 .with_reactant(term(1, "H2"))
181 .validate(),
182 Err(ReactionValidationError::MissingProducts)
183 );
184 }
185
186 #[test]
187 fn returns_structured_validation_errors() {
188 assert_eq!(
189 ReactionTerm::new(formula("H2")).with_coefficient(0),
190 Err(ReactionValidationError::InvalidStoichiometry(
191 StoichiometryValidationError::ZeroCoefficient
192 ))
193 );
194 assert_eq!(
195 Catalyst::new(" "),
196 Err(ReactionValidationError::EmptyCatalystLabel)
197 );
198 assert_eq!(
199 ReactionCondition::temperature(" "),
200 Err(ReactionValidationError::EmptyTemperatureLabel)
201 );
202 assert_eq!(
203 ReactionCondition::custom(" ", None),
204 Err(ReactionValidationError::EmptyConditionLabel)
205 );
206 }
207
208 #[test]
209 fn validates_reaction_equations() {
210 let equation = ReactionEquation::new()
211 .with_reactant(term(1, "AgNO3"))
212 .with_reactant(term(1, "NaCl"))
213 .with_product(term(1, "AgCl"))
214 .with_product(term(1, "NaNO3"));
215
216 assert_eq!(equation.to_string(), "AgNO3 + NaCl -> AgCl + NaNO3");
217 assert_eq!(equation.validate(), Ok(()));
218 }
219}