fog_pack/validator/enum_set.rs
1use super::*;
2use crate::error::{Error, Result};
3use serde::{Deserialize, Serialize};
4use std::default::Default;
5
6/// "Enum" validator that selects a validator based on the value's enum variant.
7///
8/// This validator expects a serialized Rust enum. A serialized enum consists of either a single
9/// string (a unit variant) or a map with a single key-value pair, where the key is the name of the
10/// enum variant and the value is the associated data. The associated data is validated against the
11/// matching validator in the contained `BTreeMap`. If there is no match, validation fails.
12///
13/// For unit variants, there is no validator, and they pass as long as their name is a key in the
14/// `BTreeMap`.
15///
16/// # Query Checking
17///
18/// The query validator must be an Any or an Enum validator, and the maps are directly checked against
19/// each other. The query validator may use a subset of the enum list. For unit variants, both the
20/// query validator and schema validator must have `None` instead of a validator. As an example,
21/// see the following:
22///
23/// ```
24/// # use fog_pack::{
25/// # validator::*,
26/// # schema::*,
27/// # document::*,
28/// # entry::*,
29/// # query::*,
30/// # types::*,
31/// # };
32/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
33///
34/// // Say we have a enum like this:
35/// enum ExampleEnum {
36/// Empty,
37/// Integer(Integer),
38/// String(String),
39/// }
40///
41/// // Let's use this as our schema-side validator
42/// let entry_validator = EnumValidator::new()
43/// .insert("Empty", None)
44/// .insert("Integer", Some(IntValidator::new().build()))
45/// .insert("String", Some(StrValidator::new().build()))
46/// .build();
47///
48/// // We'll build a full schema, so we can do query validation
49/// let schema_doc = SchemaBuilder::new(Validator::Null)
50/// .entry_add("item", entry_validator, None)
51/// .build()
52/// .unwrap();
53/// let schema = Schema::from_doc(&schema_doc).unwrap();
54///
55/// // This query is accepted because all enum validators match. Note how
56/// // String isn't present, because the query doesn't need to have all
57/// // possible enums.
58/// let query_validator = EnumValidator::new()
59/// .insert("Empty", None)
60/// .insert("Integer", Some(IntValidator::new().build()))
61/// .build();
62/// let query = NewQuery::new("item", query_validator);
63/// assert!(schema.encode_query(query).is_ok());
64///
65/// // This query, however, has a validator for "Empty", so it doesn't work:
66/// let query_validator = EnumValidator::new()
67/// .insert("Empty", Some(Validator::Null))
68/// .insert("Integer", Some(IntValidator::new().build()))
69/// .build();
70/// let query = NewQuery::new("item", query_validator);
71/// assert!(schema.encode_query(query).is_err());
72///
73/// # Ok(())
74/// # }
75/// ```
76///
77#[derive(Clone, Default, Debug, PartialEq, Serialize, Deserialize)]
78pub struct EnumValidator(pub BTreeMap<String, Option<Validator>>);
79
80impl EnumValidator {
81 /// Make a new validator with the default configuration.
82 pub fn new() -> Self {
83 Self::default()
84 }
85
86 /// Add a new enum to the set.
87 pub fn insert(mut self, variant: impl Into<String>, validator: Option<Validator>) -> Self {
88 self.0.insert(variant.into(), validator);
89 self
90 }
91
92 /// Build this into a [`Validator`] enum.
93 pub fn build(self) -> Validator {
94 Validator::Enum(self)
95 }
96
97 /// Iterate over all the enum variants.
98 pub fn iter(&self) -> std::collections::btree_map::Iter<String, Option<Validator>> {
99 self.0.iter()
100 }
101
102 /// Iterate over all the validators in this enum.
103 pub fn values(&self) -> std::collections::btree_map::Values<String, Option<Validator>> {
104 self.0.values()
105 }
106
107 pub(crate) fn validate<'de, 'c>(
108 &'c self,
109 types: &'c BTreeMap<String, Validator>,
110 mut parser: Parser<'de>,
111 checklist: Option<Checklist<'c>>,
112 ) -> Result<(Parser<'de>, Option<Checklist<'c>>)> {
113 // Get the enum itself, which should be a map with 1 key-value pair or a string.
114 let elem = parser
115 .next()
116 .ok_or_else(|| Error::FailValidate("expected a enum".to_string()))??;
117 let (key, has_value) = match elem {
118 Element::Str(v) => (v, false),
119 Element::Map(1) => {
120 let key = parser
121 .next()
122 .ok_or_else(|| Error::FailValidate("expected a string".to_string()))??;
123 if let Element::Str(key) = key {
124 (key, true)
125 } else {
126 return Err(Error::FailValidate("expected a string".to_string()));
127 }
128 }
129 _ => return Err(Error::FailValidate("expected an enum".to_string())),
130 };
131
132 // Find the matching validator and verify the (possible) content against it
133 let validator = self
134 .0
135 .get(key)
136 .ok_or_else(|| Error::FailValidate(format!("{} is not in enum list", key)))?;
137 match (validator, has_value) {
138 (None, false) => Ok((parser, checklist)),
139 (None, true) => Err(Error::FailValidate(format!(
140 "enum {} shouldn't have any associated value",
141 key
142 ))),
143 (Some(_), false) => Err(Error::FailValidate(format!(
144 "enum {} should have an associated value",
145 key
146 ))),
147 (Some(validator), true) => validator.validate(types, parser, checklist),
148 }
149 }
150
151 pub(crate) fn query_check(
152 &self,
153 types: &BTreeMap<String, Validator>,
154 other: &Validator,
155 ) -> bool {
156 match other {
157 Validator::Enum(other) => {
158 // For each entry in the query's enum, make sure it:
159 // 1. Has a corresponding entry in our enum
160 // 2. That our enum's matching validator would allow the query's validator
161 // for that enum.
162 // 3. If both have a "None" instead of a validator, that's also OK
163 other
164 .0
165 .iter()
166 .all(|(other_k, other_v)| match (self.0.get(other_k), other_v) {
167 (Some(Some(validator)), Some(other_v)) => {
168 validator.query_check(types, other_v)
169 }
170 (Some(None), None) => true,
171 _ => false,
172 })
173 }
174 Validator::Any => true,
175 _ => false,
176 }
177 }
178}
179
180#[cfg(test)]
181mod test {
182 use super::*;
183
184 #[test]
185 fn example_schema() {
186 use crate::schema::{Schema, SchemaBuilder};
187 // Let's use this as our schema-side validator, and make a full schema.
188 let entry_validator = EnumValidator::new()
189 .insert("Empty", None)
190 .insert("Integer", Some(IntValidator::new().build()))
191 .insert("String", Some(StrValidator::new().build()))
192 .build();
193 let schema_doc = SchemaBuilder::new(Validator::Null)
194 .entry_add("item", entry_validator, None)
195 .build()
196 .unwrap();
197 Schema::from_doc(&schema_doc).unwrap();
198 }
199}