viceroy_lib/config/
device_detection.rs

1use {
2    crate::error::DeviceDetectionConfigError,
3    serde_json::{Map, Value as SerdeValue},
4    std::{collections::HashMap, fs, iter::FromIterator, path::Path, path::PathBuf},
5};
6
7#[derive(Clone, Debug, Default)]
8pub struct DeviceDetection {
9    mapping: DeviceDetectionMapping,
10}
11
12#[derive(Clone, Debug)]
13pub enum DeviceDetectionMapping {
14    Empty,
15    InlineToml {
16        user_agents: HashMap<String, DeviceDetectionData>,
17    },
18    Json {
19        file: PathBuf,
20    },
21}
22
23#[derive(Clone, Debug)]
24pub struct DeviceDetectionData {
25    data: Map<String, SerdeValue>,
26}
27
28impl DeviceDetection {
29    pub fn new() -> Self {
30        Self::default()
31    }
32
33    pub fn lookup(&self, user_agent: &str) -> Option<DeviceDetectionData> {
34        self.mapping.get(user_agent).or(None)
35    }
36}
37
38mod deserialization {
39    use serde_json::{Map, Number};
40
41    use {
42        super::{DeviceDetection, DeviceDetectionData, DeviceDetectionMapping},
43        crate::error::{DeviceDetectionConfigError, FastlyConfigError},
44        serde_json::Value as SerdeValue,
45        std::path::PathBuf,
46        std::{collections::HashMap, convert::TryFrom},
47        toml::value::{Table, Value},
48    };
49
50    impl TryFrom<Table> for DeviceDetection {
51        type Error = FastlyConfigError;
52
53        fn try_from(toml: Table) -> Result<Self, Self::Error> {
54            fn process_config(
55                mut toml: Table,
56            ) -> Result<DeviceDetection, DeviceDetectionConfigError> {
57                let mapping = match toml.remove("format") {
58                    Some(Value::String(value)) => match value.as_str() {
59                        "inline-toml" => process_inline_toml_dictionary(&mut toml)?,
60                        "json" => process_json_entries(&mut toml)?,
61                        "" => return Err(DeviceDetectionConfigError::EmptyFormatEntry),
62                        format => {
63                            return Err(
64                                DeviceDetectionConfigError::InvalidDeviceDetectionMappingFormat(
65                                    format.to_string(),
66                                ),
67                            )
68                        }
69                    },
70                    Some(_) => return Err(DeviceDetectionConfigError::InvalidFormatEntry),
71                    None => DeviceDetectionMapping::Empty,
72                };
73
74                Ok(DeviceDetection { mapping })
75            }
76
77            process_config(toml).map_err(|err| {
78                FastlyConfigError::InvalidDeviceDetectionDefinition {
79                    name: "device_detection_mapping".to_string(),
80                    err,
81                }
82            })
83        }
84    }
85
86    fn process_inline_toml_dictionary(
87        toml: &mut Table,
88    ) -> Result<DeviceDetectionMapping, DeviceDetectionConfigError> {
89        fn convert_value_to_json(value: Value) -> Option<SerdeValue> {
90            match value {
91                Value::String(value) => Some(SerdeValue::String(value)),
92                Value::Integer(value) => Number::try_from(value).ok().map(SerdeValue::Number),
93                Value::Float(value) => Number::from_f64(value).map(SerdeValue::Number),
94                Value::Boolean(value) => Some(SerdeValue::Bool(value)),
95                Value::Table(value) => {
96                    let mut map = Map::new();
97                    for (k, v) in value {
98                        map.insert(k, convert_value_to_json(v)?);
99                    }
100                    Some(SerdeValue::Object(map))
101                }
102                _ => None,
103            }
104        }
105
106        // Take the `user_agents` field from the provided TOML table.
107        let toml = match toml
108            .remove("user_agents")
109            .ok_or(DeviceDetectionConfigError::MissingUserAgents)?
110        {
111            Value::Table(table) => table,
112            _ => return Err(DeviceDetectionConfigError::InvalidUserAgentsType),
113        };
114
115        let mut user_agents = HashMap::<String, DeviceDetectionData>::with_capacity(toml.len());
116
117        for (user_agent, value) in toml {
118            let user_agent = user_agent.to_string();
119            let table = value
120                .as_table()
121                .ok_or(DeviceDetectionConfigError::InvalidInlineEntryType)?
122                .to_owned();
123
124            let mut device_detection_data = DeviceDetectionData::new();
125
126            for (field, value) in table {
127                let value = convert_value_to_json(value)
128                    .ok_or(DeviceDetectionConfigError::InvalidInlineEntryType)?;
129                device_detection_data.insert(field, value);
130            }
131
132            user_agents.insert(user_agent, device_detection_data);
133        }
134
135        Ok(DeviceDetectionMapping::InlineToml { user_agents })
136    }
137
138    fn process_json_entries(
139        toml: &mut Table,
140    ) -> Result<DeviceDetectionMapping, DeviceDetectionConfigError> {
141        let file: PathBuf = match toml
142            .remove("file")
143            .ok_or(DeviceDetectionConfigError::MissingFile)?
144        {
145            Value::String(file) => {
146                if file.is_empty() {
147                    return Err(DeviceDetectionConfigError::EmptyFileEntry);
148                } else {
149                    file.into()
150                }
151            }
152            _ => return Err(DeviceDetectionConfigError::InvalidFileEntry),
153        };
154
155        DeviceDetectionMapping::read_json_contents(&file)?;
156
157        Ok(DeviceDetectionMapping::Json { file })
158    }
159}
160
161impl Default for DeviceDetectionMapping {
162    fn default() -> Self {
163        Self::Empty
164    }
165}
166
167impl DeviceDetectionMapping {
168    pub fn get(&self, user_agent: &str) -> Option<DeviceDetectionData> {
169        match self {
170            Self::Empty => None,
171            Self::InlineToml { user_agents } => user_agents
172                .get(user_agent)
173                .map(|device_detection_data| device_detection_data.to_owned()),
174            Self::Json { file } => Self::read_json_contents(file)
175                .ok()
176                .map(|user_agents| {
177                    user_agents
178                        .get(user_agent)
179                        .map(|device_detection_data| device_detection_data.to_owned())
180                })
181                .unwrap(),
182        }
183    }
184
185    pub fn read_json_contents(
186        file: &Path,
187    ) -> Result<HashMap<String, DeviceDetectionData>, DeviceDetectionConfigError> {
188        let data = fs::read_to_string(file).map_err(DeviceDetectionConfigError::IoError)?;
189
190        // Deserialize the contents of the given JSON file.
191        let json = match serde_json::from_str(&data)
192            .map_err(|_| DeviceDetectionConfigError::DeviceDetectionFileWrongFormat)?
193        {
194            // Check that we were given an object.
195            serde_json::Value::Object(obj) => obj,
196            _ => {
197                return Err(DeviceDetectionConfigError::DeviceDetectionFileWrongFormat);
198            }
199        };
200
201        let mut user_agents = HashMap::<String, DeviceDetectionData>::with_capacity(json.len());
202
203        for (user_agent, value) in json {
204            let user_agent = user_agent.to_string();
205            let table = value
206                .as_object()
207                .ok_or(DeviceDetectionConfigError::InvalidInlineEntryType)?
208                .to_owned();
209
210            let device_detection_data = DeviceDetectionData::from(&table);
211
212            user_agents.insert(user_agent, device_detection_data);
213        }
214
215        Ok(user_agents)
216    }
217}
218
219impl Default for DeviceDetectionData {
220    fn default() -> Self {
221        Self::from(HashMap::new())
222    }
223}
224
225impl From<HashMap<&str, SerdeValue>> for DeviceDetectionData {
226    fn from(value: HashMap<&str, SerdeValue>) -> Self {
227        let entries = value
228            .iter()
229            .map(|(&field, value)| (field.to_string(), value.to_owned()));
230
231        Self {
232            data: Map::from_iter(entries),
233        }
234    }
235}
236
237impl From<&Map<String, SerdeValue>> for DeviceDetectionData {
238    fn from(data: &Map<String, SerdeValue>) -> Self {
239        Self {
240            data: data.to_owned(),
241        }
242    }
243}
244
245impl DeviceDetectionData {
246    pub fn new() -> Self {
247        Self { data: Map::new() }
248    }
249
250    pub fn insert(&mut self, field: String, value: SerdeValue) {
251        self.data.insert(field, value);
252    }
253}
254
255impl ToString for DeviceDetectionData {
256    fn to_string(&self) -> String {
257        serde_json::to_string(&self.data).unwrap_or_else(|_| "".to_string())
258    }
259}