diff --git a/src/foundation/filter.rs b/src/foundation/filter.rs index 13286b414..c1cf03f47 100644 --- a/src/foundation/filter.rs +++ b/src/foundation/filter.rs @@ -50,7 +50,7 @@ pub struct ValueProvider<'a> { } impl ValueProvider<'_> { - fn call(&self, field: &ItemField) -> String { + pub fn call(&self, field: &ItemField) -> String { get_field_value(self.pli, field) } } diff --git a/src/foundation/mapper.pest b/src/foundation/mapper.pest new file mode 100644 index 000000000..2a2ee6c2b --- /dev/null +++ b/src/foundation/mapper.pest @@ -0,0 +1,24 @@ +WHITESPACE = _{ " " | "\t"} +regex_op = _{ "~" } +identifier = @{ (ASCII_ALPHANUMERIC | "_")+ } +string_literal = @{ "\"" ~ ( "\\\"" | (!"\"" ~ ANY) )* ~ "\"" } +field = { ^"group" | ^"title" | ^"name" | ^"url" | ^"input" | ^"caption"} +regex_expr = { field ~ regex_op ~ string_literal } +expression = _{ match_block | function_call | regex_expr | string_literal | identifier } +function_name = { ^"concat" | ^"uppercase" | ^"lowercase" | ^"capitalize" | ^"trim"} +function_call = { function_name ~ "(" ~ (expression ~ ("," ~ expression)*)? ~ ")" } +any_match = { "_" } +match_key = { any_match | identifier } +match_key_list = { match_key ~ ("," ~ match_key)* } +match_case = { match_key_list ~ "=>" ~ expression | "(" ~ match_key_list ~ ")" ~ "=>" ~ expression } +match_block = { "match" ~ "{" ~ NEWLINE* ~ (match_case ~ ("," ~ NEWLINE* ~ match_case)*)? ~ ","? ~ NEWLINE* ~ "}" } +map_case_key = { any_match | string_literal} +map_case = { map_case_key ~ "=>" ~ expression } +map_key = { identifier } +map_block = { "map" ~ map_key ~ "{" ~ NEWLINE* ~ (map_case ~ ("," ~ NEWLINE* ~ map_case)*)? ~ ","? ~ NEWLINE* ~ "}" } +assignment = { (field | identifier) ~ "=" ~ expression } +statement = { assignment | expression } +comment = _{ "#" ~ (!NEWLINE ~ ANY)* } +statement_reparator = _{ ";" | NEWLINE } +statements = _{ (statement_reparator* ~ (statement | comment))* ~ statement_reparator* } +main = { SOI ~ statements? ~ EOI } diff --git a/src/foundation/mapper.rs b/src/foundation/mapper.rs new file mode 100644 index 000000000..12c65b5c8 --- /dev/null +++ b/src/foundation/mapper.rs @@ -0,0 +1,608 @@ +#![allow(clippy::empty_docs)] + +use crate::foundation::filter::{ValueProvider}; +use crate::foundation::mapper::EvalResult::{Named, Value, Undefined, Failure, AnyValue}; +use crate::model::ItemField; +use crate::tuliprox_error::{create_tuliprox_error_result, info_err, TuliproxError, TuliproxErrorKind}; +use log::error; +use pest::iterators::Pair; +use pest::Parser; +use regex::{Regex}; +use std::collections::{HashMap, HashSet}; +use std::str::FromStr; +use crate::utils::Capitalize; + +#[derive(Parser)] +#[grammar_inline = r##" +WHITESPACE = _{ " " | "\t"} +regex_op = _{ "~" } +identifier = @{ (ASCII_ALPHANUMERIC | "_")+ } +string_literal = @{ "\"" ~ ( "\\\"" | (!"\"" ~ ANY) )* ~ "\"" } +field = { ^"group" | ^"title" | ^"name" | ^"url" | ^"input" | ^"caption"} +regex_expr = { field ~ regex_op ~ string_literal } +expression = _{ match_block | function_call | regex_expr | string_literal | identifier } +function_name = { ^"concat" | ^"uppercase" | ^"lowercase" | ^"capitalize" | ^"trim"} +function_call = { function_name ~ "(" ~ (expression ~ ("," ~ expression)*)? ~ ")" } +any_match = { "_" } +match_key = { any_match | identifier } +match_key_list = { match_key ~ ("," ~ match_key)* } +match_case = { match_key_list ~ "=>" ~ expression | "(" ~ match_key_list ~ ")" ~ "=>" ~ expression } +match_block = { "match" ~ "{" ~ NEWLINE* ~ (match_case ~ ("," ~ NEWLINE* ~ match_case)*)? ~ ","? ~ NEWLINE* ~ "}" } +map_case_key = { any_match | string_literal} +map_case = { map_case_key ~ "=>" ~ expression } +map_key = { identifier } +map_block = { "map" ~ map_key ~ "{" ~ NEWLINE* ~ (map_case ~ ("," ~ NEWLINE* ~ map_case)*)? ~ ","? ~ NEWLINE* ~ "}" } +assignment = { (field | identifier) ~ "=" ~ expression } +statement = { assignment | expression } +comment = _{ "#" ~ (!NEWLINE ~ ANY)* } +statement_reparator = _{ ";" | NEWLINE } +statements = _{ (statement_reparator* ~ (statement | comment))* ~ statement_reparator* } +main = { SOI ~ statements? ~ EOI } +"##] + +struct MapperParser; + +#[derive(Debug, Clone)] +enum MatchCaseKey { + Identifier(String), + String(String), + AnyMatch, +} + +#[derive(Debug, Clone)] +struct MatchCase { + pub identifiers: Vec, + pub expression: Expression, +} + +#[derive(Debug, Clone)] +enum MatchKey { + Identifier(String), +} + + +#[derive(Debug, Clone)] +enum BuiltInFunction { + Concat, + Uppercase, + Lowercase, + Capitalize, + Trim, +} + +impl FromStr for BuiltInFunction { + type Err = TuliproxError; + + fn from_str(s: &str) -> Result { + match s.to_lowercase().as_str() { + "concat" => Ok(Self::Concat), + "capitalize" => Ok(Self::Capitalize), + "lowercase" => Ok(Self::Lowercase), + "uppercase" => Ok(Self::Uppercase), + "trim" => Ok(Self::Trim), + _ => create_tuliprox_error_result!(TuliproxErrorKind::Info, "Unknown function {}", s), + } + } +} + +#[derive(Debug, Clone)] +enum Expression { + Identifier(String), + StringLiteral(String), + RegexExpr { field: ItemField, pattern: String, re_pattern: Regex }, + FunctionCall { name: BuiltInFunction, args: Vec }, + MatchBlock { keys: Vec, cases: Vec }, +} + +#[derive(Debug, Clone)] +enum AssignmentTarget { + Identifier(String), + Field(ItemField), +} + +#[derive(Debug, Clone)] +enum Statement { + Assignment { target: AssignmentTarget, expr: Expression }, + Expression(Expression), + Comment(String), +} + +#[derive(Debug)] +pub struct MapperProgram { + statements: Vec, +} + +impl Statement { + pub fn eval(&self, ctx: &mut Context, provider: &ValueProvider) -> Result<(), TuliproxError> { + match self { + Statement::Assignment { target, expr } => { + let val = expr.eval(ctx, provider); + match target { + AssignmentTarget::Identifier(name) => { + ctx.variables.insert(name.clone(), val); + } + AssignmentTarget::Field(name) => { + // TODO set fiels value + error!("Set field value not implemented yet. {name} = {val:?}"); + } + } + } + Statement::Expression(expr) => { + let result = expr.eval(ctx, provider); + error!("Ignoring result {result:?}"); + } + Statement::Comment(_) => {} + } + Ok(()) + } +} + +impl MapperProgram { + fn validate_expr<'a>(expr: &Expression, identifiers: &mut HashSet<&'a str>) -> Result<(), TuliproxError> { + match expr { + Expression::Identifier(ident) => { + if !identifiers.contains(ident.as_str()) { + return create_tuliprox_error_result!(TuliproxErrorKind::Info, "Identifier unknown {}", ident); + } + } + Expression::StringLiteral(_) => {} + Expression::RegexExpr { field: _field, pattern: _pattern, re_pattern: _re_pattern } => {} + Expression::FunctionCall { name: _name, args } => { + for arg in args { + MapperProgram::validate_expr(arg, identifiers)?; + } + } + Expression::MatchBlock {keys, cases} => { + let mut key_count = 0; + for key in keys { + match key { + MatchKey::Identifier(ident) => { + if !identifiers.contains(ident.as_str()) { + return create_tuliprox_error_result!(TuliproxErrorKind::Info, "Identifier unknown {}", ident); + } + key_count += 1; + } + } + } + for match_case in cases { + let mut any_match_count = 0; + if match_case.identifiers.len() != key_count { + return create_tuliprox_error_result!(TuliproxErrorKind::Info, "Match key count does not match case key count"); + } + for identifier in &match_case.identifiers { + match identifier { + MatchCaseKey::Identifier(ident) => { + if !identifiers.contains(ident.as_str()) { + return create_tuliprox_error_result!(TuliproxErrorKind::Info, "Identifier unknown {}", ident); + } + } + MatchCaseKey::String(_) => {} + MatchCaseKey::AnyMatch => { + any_match_count += 1; + if any_match_count > 1 { + return create_tuliprox_error_result!(TuliproxErrorKind::Info, "Match arm can only have one '_'"); + } + } + } + } + MapperProgram::validate_expr(&match_case.expression, identifiers)?; + } + } + } + Ok(()) + } + + fn validate(statements: &Vec) -> Result<(), TuliproxError> { + let mut identifiers: HashSet<&str> = HashSet::new(); + for stmt in statements { + match stmt { + Statement::Assignment { target, expr: value } => { + match target { + AssignmentTarget::Identifier(ident) => { + identifiers.insert(ident.as_str()); + } + AssignmentTarget::Field(_) => {} + } + MapperProgram::validate_expr(value, &mut identifiers)?; + } + Statement::Expression(expr) => { + MapperProgram::validate_expr(expr, &mut identifiers)?; + } + Statement::Comment(_) => {} + } + } + Ok(()) + } + + pub fn parse(input: &str) -> Result { + let mut parsed = MapperParser::parse(Rule::main, input).map_err(|e| info_err!(e.to_string()))?; + let program_pair = parsed.next().unwrap(); + let mut statements = Vec::new(); + for stmt_pair in program_pair.into_inner() { + if let Some(stmt) = Self::parse_statement(stmt_pair)? { + statements.push(stmt); + } + } + MapperProgram::validate(&statements)?; + Ok(Self { statements }) + } + fn parse_statement(pair: Pair) -> Result, TuliproxError> { + match pair.as_rule() { + Rule::statement => { + let inner = pair.into_inner().next().unwrap(); + match inner.as_rule() { + Rule::assignment => Ok(Some(MapperProgram::parse_assignment(inner)?)), + Rule::expression => Ok(Some(Statement::Expression(MapperProgram::parse_expression(inner)?))), + _ => Ok(None), + } + } + Rule::comment => Ok(Some(Statement::Comment(pair.as_str().trim().to_string()))), + _ => Ok(None), + } + } + + fn parse_assignment(pair: Pair) -> Result { + let mut inner = pair.into_inner(); + let name = inner.next().unwrap(); + let target = match name.as_rule() { + Rule::identifier => AssignmentTarget::Identifier(name.as_str().to_string()), + Rule::field => AssignmentTarget::Field(ItemField::from_str(name.as_str())?), + _ => return create_tuliprox_error_result!(TuliproxErrorKind::Info, "Assignment target not supported {}", name.as_str()), + }; + let next = inner.next().unwrap(); + let value = MapperProgram::parse_expression(next)?; + Ok(Statement::Assignment { target, expr: value }) + } + + fn parse_match_case_key(pair: Pair) -> Result { + let mut inner = pair.into_inner().next().unwrap(); + match inner.as_rule() { + Rule::identifier => Ok(MatchCaseKey::Identifier(inner.as_str().to_string())), + Rule::string_literal => Ok(MatchCaseKey::String(inner.as_str().to_string())), + Rule::any_match => Ok(MatchCaseKey::AnyMatch), + _ => create_tuliprox_error_result!(TuliproxErrorKind::Info, "Unexpected match_key: {:?}", inner.as_rule()), + } + } + + fn parse_match_case(pair: Pair) -> Result { + let mut inner = pair.into_inner(); + + let first = inner.next().unwrap(); + + let identifiers = match first.as_rule() { + Rule::case_key => { + vec![MapperProgram::parse_match_case_key(first)?] + } + Rule::case_key_list => { + let mut matches = vec![]; + for arm in first.into_inner() { + if arm.as_rule() != Rule::WHITESPACE { + matches.push(MapperProgram::parse_match_case_key(arm)?); + } + } + matches + } + _ => return create_tuliprox_error_result!(TuliproxErrorKind::Info, "Unexpected match arm input: {:?}", first.as_rule()), + }; + + let expr = MapperProgram::parse_expression(inner.next().unwrap())?; + + Ok(MatchCase { + identifiers, + expression: expr, + }) + } + + fn parse_expression(pair: Pair) -> Result { + match pair.as_rule() { + Rule::identifier => Ok(Expression::Identifier(pair.as_str().to_string())), + + Rule::string_literal => { + let raw = pair.as_str(); + // remove quotes + let content = &raw[1..raw.len() - 1]; + Ok(Expression::StringLiteral(content.to_string())) + } + + Rule::regex_expr => { + let mut inner = pair.into_inner(); + let field = ItemField::from_str(inner.next().unwrap().as_str())?; + let pattern_raw = inner.next().unwrap().as_str(); + let pattern = &pattern_raw[1..pattern_raw.len() - 1]; // Strip quotes + match Regex::new(pattern) { + Ok(re) => Ok(Expression::RegexExpr { field, pattern: pattern.to_string(), re_pattern: re }), + Err(_) => create_tuliprox_error_result!(TuliproxErrorKind::Info, "Invalid regex {}", pattern), + } + } + + Rule::function_call => { + let mut inner = pair.into_inner(); + let fn_name = inner.next().unwrap().as_str().to_string(); + let mut args = vec![]; + for arg in inner { + args.push(MapperProgram::parse_expression(arg)?); + } + let name = BuiltInFunction::from_str(&fn_name)?; + Ok(Expression::FunctionCall { name, args }) + } + + Rule::match_block => { + let case_pairs = pair.into_inner(); + let mut cases = vec![]; + for arm in case_pairs { + cases.push(MapperProgram::parse_match_case(arm)?); + } + Err(info_err!("Failed".to_string())) + //Ok(Expression::MatchBlock { cases}) + } + + _ => create_tuliprox_error_result!(TuliproxErrorKind::Info, "Unknown expression rule: {:?}", pair.as_rule()), + } + } +} + +pub struct Context { + variables: HashMap, +} + +impl Context { + pub fn new() -> Self { + Self { + variables: HashMap::new(), + } + } + + pub fn add_var(&mut self, name: &str, value: EvalResult) { + self.variables.insert(name.to_string(), value); + } + + pub fn has_var(&self, name: &str) -> bool { + self.variables.contains_key(name) + } + + pub fn get_var(&self, name: &str) -> &EvalResult { + self.variables.get(name).unwrap_or(&Undefined) + } + +} + +#[derive(Debug)] +#[derive(Clone)] +enum EvalResult { + Undefined, + Value(String), + Named(Vec<(String, String)>), + AnyValue, + Failure(String), +} + +impl EvalResult { + fn matches(&self, other: &EvalResult) -> bool { + match (self, other) { + (AnyValue, _) + | (_, AnyValue) => true, + (Value(a), Value(b)) => a == b, + (Named(a), Named(b)) => a == b, // Oder eigene Logik! + (Failure(_), _) | (_, Failure(_)) => false, + (Undefined, _) | (_, Undefined) => false, + _ => false, + } + } + pub fn is_error(&self) -> bool { + match self { + Failure(_) => true, + _ => false, + } + } +} + +fn concat_args(args: &Vec) -> Vec<&str> { + let mut result = vec![]; + + for arg in args { + match arg { + Undefined => {} + Value(value) => result.push(value.as_str()), + Named(pairs) => { + for (i, (key, value)) in pairs.iter().enumerate() { + result.push(key.as_str()); + result.push(": "); + result.push(value.as_str()); + if i < pairs.len() - 1 { + result.push(", "); + } + } + } + AnyValue => {} + Failure(_) => {} + } + } + + result +} + +impl Expression { + + pub fn eval(&self, ctx: &mut Context, provider: &ValueProvider) -> EvalResult { + match self { + Expression::Identifier(name) => { + match ctx.variables.get(name) { + None => EvalResult::Failure(format!("Variable with name {name} not found.")), + Some(value) => value.clone(), + } + } + Expression::StringLiteral(s) => Value(s.clone()), + Expression::RegexExpr { field, pattern, re_pattern } => { + let val = provider.call(field); + let mut values = vec![]; + for caps in re_pattern.captures_iter(&val) { + for name in re_pattern.capture_names().flatten() { + if let Some(m) = caps.name(name) { + values.push((name.to_string(), m.as_str().to_string())); + } + } + } + if values.is_empty() { + return Undefined; + } + Named(values) + } + Expression::FunctionCall { name, args } => { + let evaluated_args: Vec = args.iter().map(|a| a.eval(ctx, provider)).collect(); + for arg in &evaluated_args { + if arg.is_error() { + return arg.clone(); + } + } + + match name { + BuiltInFunction::Concat => Value(concat_args(&evaluated_args).join("")), + BuiltInFunction::Uppercase => Value(concat_args(&evaluated_args).join(" ").to_uppercase()), + BuiltInFunction::Trim => Value(concat_args(&evaluated_args).iter().map(|&s| s.trim()).collect::>().join(" ").trim().to_string()), + BuiltInFunction::Lowercase => Value(concat_args(&evaluated_args).join(" ").to_lowercase()), + BuiltInFunction::Capitalize => Value(concat_args(&evaluated_args).iter().map(|&s| s.capitalize()).collect::>().join(" ")), + } + } + Expression::MatchBlock{ keys, cases} => { + let mut match_keys = vec![]; + for key in keys { + match key { + MatchKey::Identifier(ident) => { + if !ctx.has_var(ident) { + return EvalResult::Failure(format!("Match expression invalid! Variable with name {ident} not found.")); + } + match_keys.push(ctx.get_var(ident)); + } + } + } + + for match_case in cases { + let mut case_keys = vec![]; + for case_key in &match_case.identifiers{ + match case_key { + MatchCaseKey::Identifier(ident) => { + if !ctx.has_var(&ident) { + return Failure(format!("Match case invalid! Variable with name {ident} not found.")); + } + case_keys.push(ctx.get_var(&ident).clone()); + } + MatchCaseKey::String(value) => case_keys.push(EvalResult::Value(value.to_string())), + MatchCaseKey::AnyMatch => case_keys.push(AnyValue), + } + } + + let mut match_count = 0; + for (case_key, &match_key) in case_keys.iter().zip(&match_keys) { + if !match_key.matches(case_key) { + match_count += 1; + } + } + if match_count == case_keys.len() { + return match_case.expression.eval(ctx, provider); + } + } + Undefined + } + } + } +} +// +// pub fn eval_expression(expr: &Expression, ctx: &Context) -> Option { +// match expr { +// Expression::StringLiteral(s) => Some(s.clone()), +// Expression::Identifier(name) => ctx.get(name), +// Expression::FunctionCall { name, args } => { +// let eval_args: Vec = args.iter() +// .filter_map(|a| eval_expression(a, ctx)) +// .collect(); +// match name.as_str() { +// "uppercase" => eval_args.get(0).map(|s| s.to_uppercase()), +// "lowercase" => eval_args.get(0).map(|s| s.to_lowercase()), +// "capitalize" => eval_args.get(0).map(|s| { +// let mut c = s.chars(); +// match c.next() { +// None => String::new(), +// Some(f) => f.to_uppercase().collect::() + c.as_str(), +// } +// }), +// "concat" => Some(eval_args.join("")), +// _ => None, +// } +// } +// } +// } +// +// pub fn eval_statement(stmt: &Statement, ctx: &mut Context) { +// match stmt { +// Statement::Assignment { target, value } => { +// if let Some(val) = eval_expression(value, ctx) { +// ctx.set(target, val); +// } +// } +// Statement::MatchAssignment { target, arms } => { +// let values: Vec> = arms[0].pattern.iter().map(|v| ctx.get(v)).collect(); +// for arm in arms { +// let match_ok = arm.pattern.iter().enumerate().all(|(i, name)| { +// name == "_" || ctx.get(name).is_some() +// }); +// if match_ok { +// if let Some(result) = eval_expression(&arm.result, ctx) { +// ctx.set(target, result); +// break; +// } +// } +// } +// } +// } +// } +// +// pub fn eval_program(prog: &Program, ctx: &mut Context) { +// for stmt in &prog.statements { +// eval_statement(stmt, ctx); +// } +// } + + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_mapper_dsl_eval() { + let dsl = r#" + coast = Caption ~ "(?i)(East|West)" + quality = Caption ~ "(?i)(HD|FHD|LHD)" + quality = uppercase(quality) + quality = map quality { + "LHD" => "HD", + "SHD" => "SD", + _ => quality, + }, + _ => quality + } + + coast_quality = match { + (coast, quality) => concat(capitalize(coast), " ", uppercase(quality)), + (coast, _) => concat(capitalize(coast), " HD"), + (_, quality) => concat("East ", uppercase(quality)), + } + + result = concat("US: TNT ", coast_quality) + "#; + + let mut program = MapperProgram::parse(dsl).expect("Parsing failed"); + println!("Program: {program:?}"); + + // let mut ctx = Context::new(); + // // Beispiel-Felder für Caption + // ctx.fields.insert("Caption".to_string(), "US: TNT East LHD bubble".to_string()); + // + // for stmt in &program.statements { + // //let res = stmt.eval(&mut ctx); + // println!("Statement Result: {:?}", res); + // } + // + // println!("Result variable: {:?}", ctx.variables.get("result")); + // assert_eq!(ctx.variables.get("result").unwrap(), "US: TNT East HD"); + } +} diff --git a/src/foundation/mod.rs b/src/foundation/mod.rs index ff67a7631..9606fb213 100644 --- a/src/foundation/mod.rs +++ b/src/foundation/mod.rs @@ -1 +1,2 @@ pub(crate) mod filter; +pub(crate) mod mapper; diff --git a/src/model/config.rs b/src/model/config.rs index 404b525f9..422d9162d 100644 --- a/src/model/config.rs +++ b/src/model/config.rs @@ -4,6 +4,7 @@ use enum_iterator::Sequence; use std::collections::{HashMap, HashSet}; use std::fmt::Display; use std::path::PathBuf; +use std::str::FromStr; use std::sync::Arc; use log::{debug, error, warn}; @@ -80,6 +81,31 @@ impl Display for ItemField { } } +impl FromStr for ItemField { + type Err = TuliproxError; + + fn from_str(s: &str) -> Result { + if s.eq_ignore_ascii_case(Self::GROUP) { + Ok(Self::Group) + } else if s.eq_ignore_ascii_case(Self::NAME) { + Ok(Self::Name) + } else if s.eq_ignore_ascii_case(Self::TITLE) { + Ok(Self::Title) + } else if s.eq_ignore_ascii_case(Self::CAPTION) { + Ok(Self::Caption) + } else if s.eq_ignore_ascii_case(Self::URL) { + Ok(Self::Url) + } else if s.eq_ignore_ascii_case(Self::INPUT) { + Ok(Self::Input) + } else if s.eq_ignore_ascii_case(Self::TYPE) { + Ok(Self::Type) + } else { + create_tuliprox_error_result!(TuliproxErrorKind::Info, "Unknown InputType: {}", s) + } + } +} + + #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] pub enum FilterMode { #[serde(rename = "discard")] diff --git a/src/model/mapping.rs b/src/model/mapping.rs index 5af424e48..b0a486cf5 100644 --- a/src/model/mapping.rs +++ b/src/model/mapping.rs @@ -1,7 +1,6 @@ use enum_iterator::Sequence; use log::{debug, error, trace}; use regex::Regex; -use std::borrow::Cow; use std::collections::HashMap; use std::fmt::Display; use std::str::FromStr; @@ -10,8 +9,8 @@ use std::sync::Arc; use crate::foundation::filter::{apply_templates_to_pattern_single, get_filter, prepare_templates, Filter, PatternTemplate, RegexWithCaptures, ValueProcessor}; use crate::model::valid_property; -use crate::model::{FieldGetAccessor, FieldSetAccessor, PlaylistItem}; use crate::model::ItemField; +use crate::model::{FieldGetAccessor, FieldSetAccessor, PlaylistItem}; use crate::tuliprox_error::{create_tuliprox_error_result, info_err}; use crate::tuliprox_error::{TuliproxError, TuliproxErrorKind}; use crate::utils::Capitalize; @@ -19,9 +18,7 @@ use crate::utils::CONSTANTS; pub const COUNTER_FIELDS: &[&str] = &["name", "title", "caption", "chno"]; -pub const AFFIX_FIELDS: &[&str] = &["name", "title", "caption", "group"]; - -pub const MAPPER_ATTRIBUTE_FIELDS: &[&str] = &[ +pub const MAPPER_FIELDS: &[&str] = &[ "name", "title", "caption", "group", "id", "chno", "logo", "logo_small", "parent_code", "audio_track", "time_shift", "rec", "url", "epg_channel_id", "epg_id" @@ -113,103 +110,64 @@ pub struct MappingCounter { pub padding: u8, } -#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, Sequence, PartialEq, Eq)] -pub enum TransformModifier { - #[serde(rename = "lowercase")] - Lowercase, - #[serde(rename = "uppercase")] - Uppercase, - #[serde(rename = "capitalize")] - Capitalize, -} - -impl Default for TransformModifier { - fn default() -> Self { - Self::Lowercase - } -} - -impl TransformModifier { - const LOWERCASE: &'static str = "lowercase"; - const UPPERCASE: &'static str = "uppercase"; - const CAPITALIZE: &'static str = "capitalize"; -} - -impl Display for TransformModifier { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{}", match self { - Self::Lowercase => Self::LOWERCASE, - Self::Uppercase => Self::UPPERCASE, - Self::Capitalize => Self::CAPITALIZE, - }) - } -} - -impl FromStr for TransformModifier { - type Err = TuliproxError; - - fn from_str(s: &str) -> Result { - if s.eq("lowercase") { - Ok(Self::Lowercase) - } else if s.eq("uppercase") { - Ok(Self::Uppercase) - } else if s.eq("capitalize") { - Ok(Self::Capitalize) - } else { - create_tuliprox_error_result!(TuliproxErrorKind::Info, "Unknown TransformModifier: {}", s) - } - } -} - #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] -pub struct MapperTransform { - pub field: String, - pub modifier: TransformModifier, - pub pattern: Option, - #[serde(skip_serializing, skip_deserializing)] - pub t_pattern: Option, +#[serde(tag = "modifier", rename_all = "snake_case")] +pub enum MapperOperation { + Lowercase { field: String }, + Uppercase { field: String }, + Capitalize { field: String }, + Suffix { field: String, value: String }, + Prefix { field: String, value: String }, + Set { field: String, value: String }, + Copy { field: String, source: String }, } -impl MapperTransform { +impl MapperOperation { pub fn prepare(&mut self, templates: Option<&Vec>) -> Result<(), TuliproxError> { - match &self.pattern { - None => self.t_pattern = None, - Some(pattern) => { - let mut new_pattern = pattern.to_string(); - match templates { - None => {} - Some(template_list) => { - new_pattern = apply_templates_to_pattern_single(pattern, template_list)?; - } + match self { + MapperOperation::Lowercase { field } + | MapperOperation::Uppercase { field } + | MapperOperation::Capitalize { field } => { + if !valid_property!(field.as_str(), MAPPER_FIELDS) { + return Err(info_err!(format!("Invalid mapper attribute field {field}"))); } - match Regex::new(&new_pattern) { - Ok(pattern) => self.t_pattern = Some(pattern), - Err(err) => return create_tuliprox_error_result!(TuliproxErrorKind::Info, "cant parse regex: {new_pattern} {err}"), + } + + MapperOperation::Copy { field, source } => { + if !valid_property!(field.as_str(), MAPPER_FIELDS) { + return Err(info_err!(format!("Invalid mapper attribute field {field}"))); + } + if !valid_property!(source.as_str(), MAPPER_FIELDS) { + return Err(info_err!(format!("Invalid mapper source field {source}"))); + } + } + + MapperOperation::Suffix { field, value } + | MapperOperation::Prefix { field, value } + | MapperOperation::Set { field, value } => { + if !valid_property!(field.as_str(), MAPPER_FIELDS) { + return Err(info_err!(format!("Invalid mapper attribute field {field}"))); + } + + if let Some(template_list) = templates { + *value = apply_templates_to_pattern_single(value, template_list)?; } } } + Ok(()) } } #[derive(Debug, Clone, serde::Serialize, serde::Deserialize, Default)] pub struct Mapper { - pub filter: Option, - pub pattern: String, - #[serde(default)] - attributes: HashMap, - #[serde(default)] - suffix: HashMap, - #[serde(default)] - prefix: HashMap, - #[serde(default)] - assignments: HashMap, - #[serde(default)] - transform: Option>, + pub filter: String, + pub pattern: Vec, + pub pipeline: Vec, #[serde(skip_serializing, skip_deserializing)] pub t_filter: Option, #[serde(skip_serializing, skip_deserializing)] - pub t_pattern: Option, + pub t_pattern: Option>, #[serde(skip_serializing, skip_deserializing)] t_tags: Vec, } @@ -219,71 +177,38 @@ impl Mapper { /// /// Will panic if default `RegEx` gets invalid pub fn prepare(&mut self, templates: Option<&Vec>, tags: Option<&Vec>) -> Result<(), TuliproxError> { - for (key, value) in &self.attributes.clone() { - if !valid_property!(key.as_str(), MAPPER_ATTRIBUTE_FIELDS) { - return Err(info_err!(format!("Invalid mapper attribute field {key}"))); - } + for op in &mut self.pipeline { + op.prepare(templates)?; + } + match get_filter(&self.filter, templates) { + Ok(filter) => self.t_filter = Some(filter), + Err(err) => return Err(err), + } + self.t_pattern = Some(self + .pattern + .iter() + .map(|pattern| { + let processed = match templates { + None => pattern.to_string(), + Some(tmpls) => apply_templates_to_pattern_single(pattern, tmpls)?, + }; + Regex::new(&processed) + .map_err(|err| info_err!(format!("Invalid regex: {processed} {err}"))) + }) + .collect::, _>>()?); - if let Some(template_list) = templates { - let new_value = apply_templates_to_pattern_single(value, template_list)?; - if new_value != *value { - self.attributes.insert(key.clone(), new_value); - } - } - } + self.t_tags = tags.map_or_else(Vec::new, std::clone::Clone::clone); - for key in self.suffix.keys() { - if !valid_property!(key.as_str(), AFFIX_FIELDS) { - return Err(info_err!(format!("Invalid mapper suffix field {key}"))); - } - } - for key in self.prefix.keys() { - if !valid_property!(key.as_str(), AFFIX_FIELDS) { - return Err(info_err!(format!("Invalid mapper prefix field {key}"))); - } - } - for (key, value) in &self.assignments { - if !valid_property!(key.as_str(), MAPPER_ATTRIBUTE_FIELDS) { - return Err(info_err!(format!("Invalid mapper assignment field {key}"))); - } - if !valid_property!(value.as_str(), MAPPER_ATTRIBUTE_FIELDS) { - return Err(info_err!(format!("Invalid mapper assignment field {value}"))); - } - } - - match &mut self.transform { - None => {} - Some(transforms) => { - for t in transforms { - let field = t.field.as_str(); - if !valid_property!(field, AFFIX_FIELDS) { - return Err(info_err!(format!("Invalid mapper transform field {field}"))); - } - t.prepare(templates)?; - } - } - } - - match get_filter(&self.pattern, templates) { - Ok(pattern) => { - self.t_pattern = Some(pattern); - match &self.filter { - Some(flt) => { - match get_filter(flt, templates) { - Ok(filter) => self.t_filter = Some(filter), - Err(err) => return Err(err), - } - } - _ => self.t_filter = None - } - self.t_tags = tags.map_or_else(std::vec::Vec::new, std::clone::Clone::clone); - Ok(()) - } - Err(err) => Err(err) - } + Ok(()) } } +enum TransformModifier { + Lowercase, + Uppercase, + Capitalize, +} + pub struct MappingValueProcessor<'a> { pub pli: &'a mut PlaylistItem, pub mapper: &'a Mapper, @@ -301,18 +226,79 @@ impl MappingValueProcessor<'_> { trace!("Property {key} set to {value}"); } - fn apply_attributes(&mut self, captured_names: &HashMap<&str, &str>) { - let mapper = self.mapper; - let attributes = &mapper.attributes; - for (key, value) in attributes { - if value.contains('<') { // possible replacement - let replaced = CONSTANTS.re_template_attribute.replace_all(value, |captures: ®ex::Captures| { - let capture_name = &captures[1]; - (*captured_names.get(&capture_name).unwrap_or(&&captures[0])).to_string() - }); - self.set_property(key, &replaced); - } else { - self.set_property(key, value); + fn apply_affix(&mut self, field: &str, value: &str, captured_names: &HashMap<&str, &str>, prefix: bool) { + if let Some(affix) = self.apply_tags(value, captured_names) { + if let Some(old_value) = self.get_property(field) { + let mut new_value = String::with_capacity(old_value.len() + affix.len()); + if prefix { + new_value.push_str(&affix); + new_value.push_str(&old_value); + } else { + new_value.push_str(&old_value); + new_value.push_str(&affix); + } + self.set_property(field, &new_value); + } + } + } + + fn apply_assignment(&mut self, field: &str, source: &str) { + if let Some(prop_value) = self.get_property(source) { + self.set_property(field, &prop_value); + } + } + + fn apply_attributes(&mut self, field: &str, value: &str, captured_names: &HashMap<&str, &str>) { + if value.contains('<') { // possible replacement + let replaced = CONSTANTS.re_template_attribute.replace_all(value, |captures: ®ex::Captures| { + let capture_name = &captures[1]; + (*captured_names.get(&capture_name).unwrap_or(&&captures[0])).to_string() + }); + self.set_property(field, &replaced); + } else { + self.set_property(field, value); + } + } + + fn apply_transform_modifier(modifier: &TransformModifier, value: &str) -> String { + match modifier { + TransformModifier::Uppercase => value.to_uppercase(), + TransformModifier::Lowercase => value.to_lowercase(), + TransformModifier::Capitalize => value.capitalize(), + } + } + + fn apply_transform(&mut self, modifier: TransformModifier, field: &str, captured_names: &HashMap<&str, &str>) { + match modifier { + TransformModifier::Lowercase => {} + TransformModifier::Uppercase => {} + TransformModifier::Capitalize => {} + + // None => {} + // Some(transform_list) => { + // for transform in transform_list { + // if let Some(prop_value) = self.get_property(&transform.field) { + // let value = transform.t_pattern.as_ref().map_or_else(|| Cow::from(Self::apply_transform_modifier(&transform.modifier, prop_value.as_str())), |regex| regex.replace_all(&prop_value, |caps: ®ex::Captures| { + // Self::apply_transform_modifier(&transform.modifier, &caps[0]) + // })); + // self.set_property(&transform.field, &value); + // } + // } + // } + } + } + + fn apply_pipeline(&mut self, captured_names: &HashMap<&str, &str>) { + let pipeline = &self.mapper.pipeline; + for op in pipeline { + match op { + MapperOperation::Lowercase { field } => self.apply_transform(TransformModifier::Lowercase, field, captured_names), + MapperOperation::Uppercase { field } => self.apply_transform(TransformModifier::Uppercase, field, captured_names), + MapperOperation::Capitalize { field } => self.apply_transform(TransformModifier::Capitalize, field, captured_names), + MapperOperation::Suffix { field, value } => self.apply_affix(field, value, captured_names, false), + MapperOperation::Prefix { field, value } => self.apply_affix(field, value, captured_names, true), + MapperOperation::Set { field, value } => self.apply_attributes(field, value, captured_names), + MapperOperation::Copy { field, source } => self.apply_assignment(field, source), } } } @@ -355,68 +341,6 @@ impl MappingValueProcessor<'_> { } Some(new_value) } - - fn apply_suffix(&mut self, captures: &HashMap<&str, &str>) { - let mapper = self.mapper; - let suffixes = &mapper.suffix; - - for (key, value) in suffixes { - if let Some(suffix) = self.apply_tags(value, captures) { - if let Some(old_value) = self.get_property(key) { - let new_value = format!("{old_value}{suffix}"); - self.set_property(key, &new_value); - } - } - } - } - - fn apply_prefix(&mut self, captures: &HashMap<&str, &str>) { - let mapper = self.mapper; - let prefix = &mapper.prefix; - for (key, value) in prefix { - if let Some(prefix) = self.apply_tags(value, captures) { - if let Some(old_value) = self.get_property(key) { - let new_value = format!("{}{}", prefix, &old_value); - self.set_property(key, &new_value); - } - } - } - } - - fn apply_assignments(&mut self) { - let mapper = self.mapper; - let assignments = &mapper.assignments; - for (key, value) in assignments { - if let Some(prop_value) = self.get_property(value) { - self.set_property(key, &prop_value); - } - } - } - - fn apply_transform_modifier(modifier: &TransformModifier, value: &str) -> String { - match modifier { - TransformModifier::Uppercase => value.to_uppercase(), - TransformModifier::Lowercase => value.to_lowercase(), - TransformModifier::Capitalize => value.capitalize(), - } - } - - fn apply_transform(&mut self) { - let mapper = self.mapper; - match &mapper.transform { - None => {} - Some(transform_list) => { - for transform in transform_list { - if let Some(prop_value) = self.get_property(&transform.field) { - let value = transform.t_pattern.as_ref().map_or_else(|| Cow::from(Self::apply_transform_modifier(&transform.modifier, prop_value.as_str())), |regex| regex.replace_all(&prop_value, |caps: ®ex::Captures| { - Self::apply_transform_modifier(&transform.modifier, &caps[0]) - })); - self.set_property(&transform.field, &value); - } - } - } - } - } } impl ValueProcessor for MappingValueProcessor<'_> { @@ -438,11 +362,8 @@ impl ValueProcessor for MappingValueProcessor<'_> { } ); } - MappingValueProcessor::<'_>::apply_attributes(self, &captured_values); - MappingValueProcessor::<'_>::apply_suffix(self, &captured_values); - MappingValueProcessor::<'_>::apply_prefix(self, &captured_values); - MappingValueProcessor::<'_>::apply_assignments(self); - MappingValueProcessor::<'_>::apply_transform(self); + + MappingValueProcessor::<'_>::apply_pipeline(self, &captured_values); true } } diff --git a/src/processing/processor/affix.rs b/src/processing/processor/affix.rs index 64f3661ef..e31d7a8fa 100644 --- a/src/processing/processor/affix.rs +++ b/src/processing/processor/affix.rs @@ -1,4 +1,4 @@ -use crate::model::{ConfigInput, InputAffix, AFFIX_FIELDS, valid_property}; +use crate::model::{ConfigInput, InputAffix, valid_property, MAPPER_FIELDS}; use crate::model::{FetchedPlaylist, FieldGetAccessor, FieldSetAccessor, PlaylistItem}; use crate::utils::{debug_if_enabled}; @@ -19,7 +19,7 @@ fn create_affix_processor(affix: &InputAffix, is_prefix: bool) -> AffixProcessor fn validate_and_create_affix_processor(affix: Option<&InputAffix>, is_prefix: bool) -> Option { if let Some(affix_def) = affix { - if (valid_property!(&affix_def.field.as_str(), AFFIX_FIELDS) && !affix_def.value.is_empty()) { + if (valid_property!(&affix_def.field.as_str(), MAPPER_FIELDS) && !affix_def.value.is_empty()) { return Some(create_affix_processor(affix_def, is_prefix)); } } diff --git a/src/processing/processor/playlist.rs b/src/processing/processor/playlist.rs index 241999244..506184ea1 100644 --- a/src/processing/processor/playlist.rs +++ b/src/processing/processor/playlist.rs @@ -144,11 +144,11 @@ fn map_channel(mut channel: PlaylistItem, mapping: &Mapping) -> PlaylistItem { match &m.t_filter { Some(filter) => { if filter.filter(&provider, &mut mock_processor) { - apply_pattern!(&m.t_pattern, &provider, &mut processor); + //apply_pattern!(&m.t_pattern, &provider, &mut processor); } } _ => { - apply_pattern!(&m.t_pattern, &provider, &mut processor); + //apply_pattern!(&m.t_pattern, &provider, &mut processor); } } } diff --git a/src/utils/network/request.rs b/src/utils/network/request.rs index 3a141a594..c1e116f1f 100644 --- a/src/utils/network/request.rs +++ b/src/utils/network/request.rs @@ -16,7 +16,7 @@ use url::Url; use crate::tuliprox_error::create_tuliprox_error_result; use crate::tuliprox_error::{str_to_io_error, TuliproxError, TuliproxErrorKind}; -use crate::model::format_elapsed_time; +use crate::model::{format_elapsed_time, Config}; use crate::model::{ConfigInput, ProxyConfig, InputFetchMethod}; use crate::repository::storage::{get_input_storage_path, short_hash}; use crate::repository::storage_const; @@ -505,9 +505,9 @@ pub fn get_base_url_from_str(url: &str) -> Option { } } -pub fn create_client(proxy_config: Option<&ProxyConfig>) -> reqwest::ClientBuilder { +pub fn create_client(cfg: &Config) -> reqwest::ClientBuilder { let client = reqwest::Client::builder(); - if let Some(proxy_cfg) = proxy_config { + if let Some(proxy_cfg) = cfg.proxy.as_ref() { let proxy = match reqwest::Proxy::all(&proxy_cfg.url) { Ok(proxy) => { if let (Some(username), Some(password)) = (&proxy_cfg.username, &proxy_cfg.password) {