Files
mqtt2db/src/mapping.rs

242 lines
7.4 KiB
Rust

use influxdb::Type;
use jsonpath::Selector;
use std::{convert::TryFrom, fmt};
use crate::config::{Mapping as ConfigMapping, Payload as ConfigPayload, TagValue as ConfigTagValue};
use crate::interpolate::{InterpolatedName, InterpolatedNamePart};
use crate::value::{ToInfluxType, ValueType};
#[derive(Clone, Debug, PartialEq)]
pub enum TopicLevel {
Literal(String),
SingleWildcard,
MultiWildcard,
}
impl TryFrom<&str> for TopicLevel {
type Error = String;
fn try_from(s: &str) -> Result<Self, Self::Error> {
match s {
"+" => Ok(TopicLevel::SingleWildcard),
"#" => Ok(TopicLevel::MultiWildcard),
s if s.contains("+") || s.contains("#") => {
Err(format!("Topic level '{}' cannot contain '+' or '#'", s))
}
s => Ok(TopicLevel::Literal(s.to_string())),
}
}
}
#[derive(Debug)]
pub enum TagValue {
InterpolatedStr(InterpolatedName),
Literal(Type),
}
impl TryFrom<&ConfigTagValue> for TagValue {
type Error = String;
fn try_from(tag_value: &ConfigTagValue) -> Result<Self, Self::Error> {
match tag_value.r#type {
ValueType::Text => {
let interp = InterpolatedName::try_from(tag_value.value.as_str())?;
match interp.parts.get(0) {
Some(InterpolatedNamePart::Literal(literal)) if interp.parts.len() == 1 => {
Ok(TagValue::Literal(Type::Text(literal.clone())))
}
_ => Ok(TagValue::InterpolatedStr(interp)),
}
}
other => tag_value.value.to_influx_type(other).map(TagValue::Literal),
}
}
}
pub enum Payload {
Raw,
Json {
value_field_selector: Selector,
timestamp_field_selector: Option<Selector>,
},
}
impl fmt::Debug for Payload {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
use Payload::*;
match self {
Raw => write!(f, "Raw"),
Json { .. } => write!(f, "Json {{ ... }}"),
}
}
}
#[derive(Debug)]
pub struct Mapping {
pub topic: Vec<TopicLevel>,
pub payload: Payload,
pub field_name: InterpolatedName,
pub value_type: ValueType,
pub tags: Vec<(String, TagValue)>,
}
impl TryFrom<&ConfigMapping> for Mapping {
type Error = String;
fn try_from(mapping: &ConfigMapping) -> Result<Self, Self::Error> {
let topic = mapping
.topic
.split("/")
.map(|level| TopicLevel::try_from(level))
.collect::<Result<Vec<TopicLevel>, String>>()?;
let pre_multi_levels: Vec<&TopicLevel> = topic
.iter()
.take_while(|level| **level != TopicLevel::MultiWildcard)
.collect();
if pre_multi_levels.len() < topic.len() - 1 {
Err(format!(
"Topic '{}' has '#' wildcard before last topic level",
mapping.topic
))?;
}
let max_interp_ref = topic
.iter()
.filter(|level| **level == TopicLevel::SingleWildcard)
.count();
let field_name = match InterpolatedName::try_from(mapping.field_name.as_str()) {
Ok(name) if find_max_ref(&name) > max_interp_ref => Err(format!(
"Topic '{}' has field name '{}' which has invalid references",
mapping.topic, mapping.field_name
)),
Ok(name) => Ok(name),
Err(err) => Err(err),
}?;
let payload = match &mapping.payload {
None => Payload::Raw,
Some(ConfigPayload::json { value_field_path, timestamp_field_path }) => {
let value_field_selector = Selector::new(&value_field_path)
.map_err(|err| format!("Value field path '{}' is invalid: {}'", value_field_path, err))?;
let timestamp_field_selector = timestamp_field_path.as_ref()
.map(|path| Selector::new(path)
.map_err(|err| format!("Timestamp field path '{}' is invalid: {}'", path, err))
)
.transpose()?;
Payload::Json {
value_field_selector,
timestamp_field_selector,
}
}
};
let tags = mapping
.tags
.iter()
.map(|tag| match TagValue::try_from(tag.1) {
Ok(TagValue::InterpolatedStr(ref name)) if find_max_ref(name) > max_interp_ref => {
Err(format!(
"Topic '{}' has tag value '{:?}' which has invalid references",
mapping.topic, tag.1
))
}
Ok(value) => Ok((tag.0.clone(), value)),
Err(err) => Err(err),
})
.collect::<Result<Vec<(String, TagValue)>, String>>()?;
Ok(Mapping {
topic,
payload,
field_name,
value_type: mapping.value_type,
tags,
})
}
}
fn find_max_ref(name: &InterpolatedName) -> usize {
name.parts.iter().fold(0, |max_ref, part| match part {
InterpolatedNamePart::Reference(num) if *num > max_ref => *num,
_ => max_ref,
})
}
#[cfg(test)]
mod test {
use std::collections::HashMap;
use super::*;
#[test]
fn mapping_parsing() -> Result<(), String> {
use TopicLevel::*;
fn mk_cfg_mapping(topic: &str) -> ConfigMapping {
ConfigMapping {
topic: topic.to_string(),
payload: None,
field_name: "".to_string(),
value_type: ValueType::Text,
tags: HashMap::new(),
}
}
assert_eq!(
vec![Literal("foo".to_string()), Literal("bar".to_string())],
Mapping::try_from(&mk_cfg_mapping("foo/bar"))?.topic
);
assert_eq!(
vec![
Literal("".to_string()),
Literal("foo".to_string()),
Literal("bar".to_string())
],
Mapping::try_from(&mk_cfg_mapping("/foo/bar"))?.topic
);
assert_eq!(
vec![
Literal("foo".to_string()),
Literal("bar".to_string()),
Literal("".to_string())
],
Mapping::try_from(&mk_cfg_mapping("foo/bar/"))?.topic
);
assert_eq!(
vec![
Literal("foo".to_string()),
Literal("bar".to_string()),
MultiWildcard
],
Mapping::try_from(&mk_cfg_mapping("foo/bar/#"))?.topic
);
assert_eq!(
vec![
Literal("foo".to_string()),
SingleWildcard,
Literal("bar".to_string())
],
Mapping::try_from(&mk_cfg_mapping("foo/+/bar"))?.topic
);
assert_eq!(
vec![
Literal("foo".to_string()),
SingleWildcard,
Literal("bar".to_string()),
MultiWildcard
],
Mapping::try_from(&mk_cfg_mapping("foo/+/bar/#"))?.topic
);
assert!(Mapping::try_from(&mk_cfg_mapping("foo/#/bar")).is_err());
assert!(Mapping::try_from(&mk_cfg_mapping("foo/bar#")).is_err());
assert!(Mapping::try_from(&mk_cfg_mapping("foo/bar+baz/quux")).is_err());
assert!(Mapping::try_from(&mk_cfg_mapping("foo/bar#baz/quux")).is_err());
Ok(())
}
}