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 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 let json = match serde_json::from_str(&data)
192 .map_err(|_| DeviceDetectionConfigError::DeviceDetectionFileWrongFormat)?
193 {
194 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}