use {
crate::error::DeviceDetectionConfigError,
serde_json::{Map, Value as SerdeValue},
std::{collections::HashMap, fs, iter::FromIterator, path::Path, path::PathBuf},
};
#[derive(Clone, Debug, Default)]
pub struct DeviceDetection {
mapping: DeviceDetectionMapping,
}
#[derive(Clone, Debug, Default)]
pub enum DeviceDetectionMapping {
#[default]
Empty,
InlineToml {
user_agents: HashMap<String, DeviceDetectionData>,
},
Json {
file: PathBuf,
},
}
#[derive(Clone, Debug)]
pub struct DeviceDetectionData {
data: Map<String, SerdeValue>,
}
impl DeviceDetection {
pub fn new() -> Self {
Self::default()
}
pub fn lookup(&self, user_agent: &str) -> Option<DeviceDetectionData> {
self.mapping.get(user_agent).or(None)
}
}
mod deserialization {
use serde_json::{Map, Number};
use {
super::{DeviceDetection, DeviceDetectionData, DeviceDetectionMapping},
crate::error::{DeviceDetectionConfigError, FastlyConfigError},
serde_json::Value as SerdeValue,
std::path::PathBuf,
std::{collections::HashMap, convert::TryFrom},
toml::value::{Table, Value},
};
impl TryFrom<Table> for DeviceDetection {
type Error = FastlyConfigError;
fn try_from(toml: Table) -> Result<Self, Self::Error> {
fn process_config(
mut toml: Table,
) -> Result<DeviceDetection, DeviceDetectionConfigError> {
let mapping = match toml.remove("format") {
Some(Value::String(value)) => match value.as_str() {
"inline-toml" => process_inline_toml_dictionary(&mut toml)?,
"json" => process_json_entries(&mut toml)?,
"" => return Err(DeviceDetectionConfigError::EmptyFormatEntry),
format => {
return Err(
DeviceDetectionConfigError::InvalidDeviceDetectionMappingFormat(
format.to_string(),
),
);
}
},
Some(_) => return Err(DeviceDetectionConfigError::InvalidFormatEntry),
None => DeviceDetectionMapping::Empty,
};
Ok(DeviceDetection { mapping })
}
process_config(toml).map_err(|err| {
FastlyConfigError::InvalidDeviceDetectionDefinition {
name: "device_detection_mapping".to_string(),
err,
}
})
}
}
fn process_inline_toml_dictionary(
toml: &mut Table,
) -> Result<DeviceDetectionMapping, DeviceDetectionConfigError> {
fn convert_value_to_json(value: Value) -> Option<SerdeValue> {
match value {
Value::String(value) => Some(SerdeValue::String(value)),
Value::Integer(value) => Some(SerdeValue::Number(Number::from(value))),
Value::Float(value) => Number::from_f64(value).map(SerdeValue::Number),
Value::Boolean(value) => Some(SerdeValue::Bool(value)),
Value::Table(value) => {
let mut map = Map::new();
for (k, v) in value {
map.insert(k, convert_value_to_json(v)?);
}
Some(SerdeValue::Object(map))
}
_ => None,
}
}
let toml = match toml
.remove("user_agents")
.ok_or(DeviceDetectionConfigError::MissingUserAgents)?
{
Value::Table(table) => table,
_ => return Err(DeviceDetectionConfigError::InvalidUserAgentsType),
};
let mut user_agents = HashMap::<String, DeviceDetectionData>::with_capacity(toml.len());
for (user_agent, value) in toml {
let user_agent = user_agent.to_string();
let table = value
.as_table()
.ok_or(DeviceDetectionConfigError::InvalidInlineEntryType)?
.to_owned();
let mut device_detection_data = DeviceDetectionData::new();
for (field, value) in table {
let value = convert_value_to_json(value)
.ok_or(DeviceDetectionConfigError::InvalidInlineEntryType)?;
device_detection_data.insert(field, value);
}
user_agents.insert(user_agent, device_detection_data);
}
Ok(DeviceDetectionMapping::InlineToml { user_agents })
}
fn process_json_entries(
toml: &mut Table,
) -> Result<DeviceDetectionMapping, DeviceDetectionConfigError> {
let file: PathBuf = match toml
.remove("file")
.ok_or(DeviceDetectionConfigError::MissingFile)?
{
Value::String(file) => {
if file.is_empty() {
return Err(DeviceDetectionConfigError::EmptyFileEntry);
} else {
file.into()
}
}
_ => return Err(DeviceDetectionConfigError::InvalidFileEntry),
};
DeviceDetectionMapping::read_json_contents(&file)?;
Ok(DeviceDetectionMapping::Json { file })
}
}
impl DeviceDetectionMapping {
pub fn get(&self, user_agent: &str) -> Option<DeviceDetectionData> {
match self {
Self::Empty => None,
Self::InlineToml { user_agents } => user_agents
.get(user_agent)
.map(|device_detection_data| device_detection_data.to_owned()),
Self::Json { file } => Self::read_json_contents(file)
.ok()
.map(|user_agents| {
user_agents
.get(user_agent)
.map(|device_detection_data| device_detection_data.to_owned())
})
.unwrap(),
}
}
pub fn read_json_contents(
file: &Path,
) -> Result<HashMap<String, DeviceDetectionData>, DeviceDetectionConfigError> {
let data = fs::read_to_string(file).map_err(DeviceDetectionConfigError::IoError)?;
let json = match serde_json::from_str(&data)
.map_err(|_| DeviceDetectionConfigError::DeviceDetectionFileWrongFormat)?
{
serde_json::Value::Object(obj) => obj,
_ => {
return Err(DeviceDetectionConfigError::DeviceDetectionFileWrongFormat);
}
};
let mut user_agents = HashMap::<String, DeviceDetectionData>::with_capacity(json.len());
for (user_agent, value) in json {
let user_agent = user_agent.to_string();
let table = value
.as_object()
.ok_or(DeviceDetectionConfigError::InvalidInlineEntryType)?
.to_owned();
let device_detection_data = DeviceDetectionData::from(&table);
user_agents.insert(user_agent, device_detection_data);
}
Ok(user_agents)
}
}
impl Default for DeviceDetectionData {
fn default() -> Self {
Self::from(HashMap::new())
}
}
impl From<HashMap<&str, SerdeValue>> for DeviceDetectionData {
fn from(value: HashMap<&str, SerdeValue>) -> Self {
let entries = value
.iter()
.map(|(&field, value)| (field.to_string(), value.to_owned()));
Self {
data: Map::from_iter(entries),
}
}
}
impl From<&Map<String, SerdeValue>> for DeviceDetectionData {
fn from(data: &Map<String, SerdeValue>) -> Self {
Self {
data: data.to_owned(),
}
}
}
impl DeviceDetectionData {
pub fn new() -> Self {
Self { data: Map::new() }
}
pub fn insert(&mut self, field: String, value: SerdeValue) {
self.data.insert(field, value);
}
}
impl std::fmt::Display for DeviceDetectionData {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match serde_json::to_string(&self.data) {
Ok(s) => write!(f, "{}", s),
Err(_) => Ok(()),
}
}
}