diff --git a/src/foundation/filter.rs b/src/foundation/filter.rs index c1cf03f47..e912f2bee 100644 --- a/src/foundation/filter.rs +++ b/src/foundation/filter.rs @@ -8,7 +8,7 @@ use pest::Parser; use std::cmp::Ordering; use std::collections::HashMap; -use crate::model::ItemField; +use crate::model::{ItemField}; use crate::model::{PlaylistItem, PlaylistItemType}; use crate::tools::directed_graph::DirectedGraph; use crate::tuliprox_error::{create_tuliprox_error_result, info_err}; @@ -29,7 +29,7 @@ pub fn get_field_value(pli: &PlaylistItem, field: &ItemField) -> String { value.to_string() } -pub fn set_field_value(pli: &mut PlaylistItem, field: &ItemField, value: String) { +pub fn set_field_value(pli: &mut PlaylistItem, field: &ItemField, value: String) -> bool { let header = &mut pli.header; match field { ItemField::Group => header.group = value, @@ -41,8 +41,9 @@ pub fn set_field_value(pli: &mut PlaylistItem, field: &ItemField, value: String) header.title.clone_from(&value); header.name = value; } - ItemField::Type => {} + ItemField::Type => {}, } + true } pub struct ValueProvider<'a> { @@ -50,20 +51,26 @@ pub struct ValueProvider<'a> { } impl ValueProvider<'_> { - pub fn call(&self, field: &ItemField) -> String { + pub fn get(&self, field: &ItemField) -> String { get_field_value(self.pli, field) } } -pub trait ValueProcessor { - fn process(&mut self, field: &ItemField, value: &str, rewc: &RegexWithCaptures) -> bool; +pub struct ValueAccessor<'a> { + pub pli: &'a mut PlaylistItem, } -pub struct MockValueProcessor {} +impl ValueAccessor<'_> { + pub fn get(&self, field: &ItemField) -> String { + get_field_value(self.pli, field) + } -impl ValueProcessor for MockValueProcessor { - fn process(&mut self, _: &ItemField, _: &str, _: &RegexWithCaptures) -> bool { - false + pub fn set(&mut self, field: &ItemField, value: &str) { + if set_field_value(&mut self.pli, field, value.to_string()) { + trace!("Property {field} set to {value}"); + } else { + error!("Can't set unknown field {field} set to {value}"); + } } } @@ -161,19 +168,19 @@ pub enum Filter { } impl Filter { - pub fn filter(&self, provider: &ValueProvider, processor: &mut dyn ValueProcessor) -> bool { + pub fn filter(&self, provider: &ValueProvider) -> bool { match self { Self::FieldComparison(field, rewc) => { let (is_match, value) = if field == &ItemField::Caption { - let value = provider.call(&ItemField::Title); + let value = provider.get(&ItemField::Title); if rewc.re.is_match(value.as_str()) { (true, value) } else { - let value = provider.call(&ItemField::Name); + let value = provider.get(&ItemField::Name); (rewc.re.is_match(value.as_str()), value) } } else { - let value = provider.call(field); + let value = provider.get(field); (rewc.re.is_match(value.as_str()), value) }; if log_enabled!(Level::Trace) { @@ -183,13 +190,10 @@ impl Filter { debug!("Match failed: {self}: {:?} {} => {}={}", &rewc, &rewc.restr, &field, &value); } } - if is_match { - processor.process(field, &value, rewc); - } is_match } Self::TypeComparison(field, item_type) => { - let value = provider.call(field); + let value = provider.get(field); get_filter_item_type(value.as_str()).is_some_and(|pli_type| { let is_match = pli_type.eq(item_type); if log_enabled!(Level::Trace) { @@ -202,16 +206,16 @@ impl Filter { is_match }) } - Self::Group(expr) => expr.filter(provider, processor), + Self::Group(expr) => expr.filter(provider), Self::UnaryExpression(op, expr) => match op { - UnaryOperator::Not => !expr.filter(provider, processor), + UnaryOperator::Not => !expr.filter(provider), }, Self::BinaryExpression(left, op, right) => match op { BinaryOperator::And => { - left.filter(provider, processor) && right.filter(provider, processor) + left.filter(provider) && right.filter(provider) } BinaryOperator::Or => { - left.filter(provider, processor) || right.filter(provider, processor) + left.filter(provider) || right.filter(provider) } }, } @@ -728,7 +732,7 @@ pub fn apply_templates_to_pattern_single(pattern: &str, templates: &Vec = channels .iter() .filter(|&chan| { let provider = ValueProvider { pli: chan, }; - filter.filter(&provider, &mut processor) + filter.filter(&provider) }) .collect(); assert_eq!(filtered.len(), 2); @@ -845,14 +848,13 @@ mod tests { create_mock_pli("NC", "GA"), create_mock_pli("NA", "GC"), ]; - let mut processor = MockValueProcessor {}; let filtered: Vec<&PlaylistItem> = channels .iter() .filter(|&chan| { let provider = ValueProvider { pli: chan, }; - filter.filter(&provider, &mut processor) + filter.filter(&provider) }) .collect(); assert_eq!(filtered.len(), 1); @@ -907,14 +909,13 @@ mod tests { create_mock_pli("24/7: Cars", "US Channels"), create_mock_pli("Entertainment", "US Channels"), ]; - let mut processor = MockValueProcessor {}; let filtered: Vec<&PlaylistItem> = channels .iter() .filter(|&chan| { let provider = ValueProvider { pli: chan, }; - filter.filter(&provider, &mut processor) + filter.filter(&provider) }) .collect(); assert_eq!(filtered.len(), 1); diff --git a/src/foundation/mapper.rs b/src/foundation/mapper.rs index c7f9b7527..6c7f90601 100644 --- a/src/foundation/mapper.rs +++ b/src/foundation/mapper.rs @@ -1,6 +1,6 @@ #![allow(clippy::empty_docs)] -use crate::foundation::filter::ValueProvider; +use crate::foundation::filter::{ValueAccessor}; use crate::foundation::mapper::EvalResult::{AnyValue, Failure, Named, Undefined, Value}; use crate::model::ItemField; use crate::tuliprox_error::{create_tuliprox_error_result, info_err, TuliproxError, TuliproxErrorKind}; @@ -17,11 +17,12 @@ use std::str::FromStr; WHITESPACE = _{ " " | "\t"} regex_op = _{ "~" } identifier = @{ (ASCII_ALPHANUMERIC | "_")+ } +var_access = { identifier ~ ("." ~ identifier)? } string_literal = @{ "\"" ~ ( "\\\"" | (!"\"" ~ ANY) )* ~ "\"" } field = { ^"group" | ^"title" | ^"name" | ^"url" | ^"input" | ^"caption"} regex_expr = { field ~ regex_op ~ string_literal } -expression = _{ map_block | match_block | function_call | regex_expr | string_literal | identifier } -function_name = { ^"concat" | ^"uppercase" | ^"lowercase" | ^"capitalize" | ^"trim"} +expression = { map_block | match_block | function_call | regex_expr | string_literal | var_access } +function_name = { "concat" | "uppercase" | "lowercase" | "capitalize" | "trim" | "print" } function_call = { function_name ~ "(" ~ (expression ~ ("," ~ expression)*)? ~ ")" } any_match = { "_" } match_case_key = { any_match | identifier } @@ -39,7 +40,6 @@ statement_reparator = _{ ";" | NEWLINE } statements = _{ (statement_reparator* ~ (statement | comment))* ~ statement_reparator* } main = { SOI ~ statements? ~ EOI } "##] - struct MapperParser; #[derive(Debug, Clone)] @@ -79,6 +79,7 @@ enum BuiltInFunction { Lowercase, Capitalize, Trim, + Print, } impl FromStr for BuiltInFunction { @@ -91,6 +92,7 @@ impl FromStr for BuiltInFunction { "lowercase" => Ok(Self::Lowercase), "uppercase" => Ok(Self::Uppercase), "trim" => Ok(Self::Trim), + "print" => Ok(Self::Print), _ => create_tuliprox_error_result!(TuliproxErrorKind::Info, "Unknown function {}", s), } } @@ -100,6 +102,7 @@ impl FromStr for BuiltInFunction { enum Expression { Identifier(String), StringLiteral(String), + VarAccess(String, String), RegexExpr { field: ItemField, pattern: String, re_pattern: Regex }, FunctionCall { name: BuiltInFunction, args: Vec }, MatchBlock(Vec), @@ -116,53 +119,80 @@ enum AssignmentTarget { enum Statement { Assignment { target: AssignmentTarget, expr: Expression }, Expression(Expression), - Comment(String), + Comment//(String), } -#[derive(Debug)] -pub struct MapperProgram { +#[derive(Debug, Clone)] +pub struct MapperScript { statements: Vec, } -impl MapperProgram { - pub fn eval(&self, ctx: &mut Context, provider: &ValueProvider) -> Result<(), TuliproxError> { +impl MapperScript { + pub fn eval(&self, setter: &mut ValueAccessor) -> Result<(), TuliproxError> { + let ctx = &mut Context::new(); + self.eval_with_context(ctx, setter)?; + Ok(()) + } + + pub fn eval_with_context(&self, ctx: &mut Context, setter: &mut ValueAccessor) -> Result<(), TuliproxError> { for stmt in &self.statements { - stmt.eval(ctx, provider)?; + stmt.eval(ctx, setter)?; } Ok(()) } } impl Statement { - pub fn eval(&self, ctx: &mut Context, provider: &ValueProvider) -> Result<(), TuliproxError> { + pub fn eval(&self, ctx: &mut Context, setter: &mut ValueAccessor) -> Result<(), TuliproxError> { match self { Statement::Assignment { target, expr } => { - let val = expr.eval(ctx, provider); + let val = expr.eval(ctx, setter); match target { AssignmentTarget::Identifier(name) => { - ctx.variables.insert(name.clone(), val); + ctx.add_var(name, val); } AssignmentTarget::Field(name) => { - // TODO set fiels value - error!("Set field value not implemented yet. {name} = {val:?}"); + match val { + Undefined => {} + Value(content) => { + setter.set(name, content.as_str()); + } + Named(pairs) => { + let mut result = String::with_capacity(128); + for (i, (key, value)) in pairs.iter().enumerate() { + result.push_str(key); + result.push_str(": "); + result.push_str(value); + if i < pairs.len() - 1 { + result.push_str(", "); + } + } + setter.set(name, &result); + } + AnyValue => {} + Failure(err) => { + error!("Failed to set field {name} value: {err}"); + } + } } } } Statement::Expression(expr) => { - let result = expr.eval(ctx, provider); + let result = expr.eval(ctx, setter); error!("Ignoring result {result:?}"); } - Statement::Comment(_) => {} + Statement::Comment => {} } Ok(()) } } -impl MapperProgram { +impl MapperScript { fn validate_expr<'a>(expr: &Expression, identifiers: &mut HashSet<&'a str>) -> Result<(), TuliproxError> { match expr { - Expression::Identifier(ident) => { + Expression::Identifier(ident) + | Expression::VarAccess(ident, _) => { if !identifiers.contains(ident.as_str()) { return create_tuliprox_error_result!(TuliproxErrorKind::Info, "Identifier unknown {}", ident); } @@ -171,7 +201,7 @@ impl MapperProgram { Expression::RegexExpr { field: _field, pattern: _pattern, re_pattern: _re_pattern } => {} Expression::FunctionCall { name: _name, args } => { for arg in args { - MapperProgram::validate_expr(arg, identifiers)?; + MapperScript::validate_expr(arg, identifiers)?; } } Expression::MatchBlock(cases) => { @@ -201,7 +231,7 @@ impl MapperProgram { return create_tuliprox_error_result!(TuliproxErrorKind::Info, "Duplicate case {}", identifer_key); } case_keys.insert(identifer_key); - MapperProgram::validate_expr(&match_case.expression, identifiers)?; + MapperScript::validate_expr(&match_case.expression, identifiers)?; } } Expression::MapBlock { key, cases } => { @@ -229,7 +259,7 @@ impl MapperProgram { } } } - MapperProgram::validate_expr(&map_case.expression, identifiers)?; + MapperScript::validate_expr(&map_case.expression, identifiers)?; } } } @@ -247,12 +277,12 @@ impl MapperProgram { } AssignmentTarget::Field(_) => {} } - MapperProgram::validate_expr(value, &mut identifiers)?; + MapperScript::validate_expr(value, &mut identifiers)?; } Statement::Expression(expr) => { - MapperProgram::validate_expr(expr, &mut identifiers)?; + MapperScript::validate_expr(expr, &mut identifiers)?; } - Statement::Comment(_) => {} + Statement::Comment => {} } } Ok(()) @@ -267,7 +297,7 @@ impl MapperProgram { statements.push(stmt); } } - MapperProgram::validate(&statements)?; + MapperScript::validate(&statements)?; Ok(Self { statements }) } fn parse_statement(pair: Pair) -> Result, TuliproxError> { @@ -275,12 +305,15 @@ impl MapperProgram { 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::assignment => Ok(Some(MapperScript::parse_assignment(inner)?)), + Rule::expression => Ok(Some(Statement::Expression(MapperScript::parse_expression(inner)?))), + _ => { + error!("Unknown statement rule: {:?}", inner.as_rule()); + Ok(None) + } } } - Rule::comment => Ok(Some(Statement::Comment(pair.as_str().trim().to_string()))), + Rule::comment => Ok(Some(Statement::Comment /*(pair.as_str().trim().to_string())*/)), _ => Ok(None), } } @@ -294,12 +327,12 @@ impl MapperProgram { _ => 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)?; + let value = MapperScript::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(); + let inner = pair.into_inner().next().unwrap(); match inner.as_rule() { Rule::identifier => Ok(MatchCaseKey::Identifier(inner.as_str().to_string())), Rule::any_match => Ok(MatchCaseKey::AnyMatch), @@ -314,13 +347,16 @@ impl MapperProgram { let identifiers = match first.as_rule() { Rule::match_case_key => { - vec![MapperProgram::parse_match_case_key(first)?] + vec![MapperScript::parse_match_case_key(first)?] } Rule::match_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)?); + match MapperScript::parse_match_case_key(arm)? { + MatchCaseKey::Identifier(ident) => matches.push(MatchCaseKey::Identifier(ident)), + MatchCaseKey::AnyMatch => return Err(info_err!("Unexpected match case key: _".to_string())), + } } } matches @@ -328,7 +364,7 @@ impl MapperProgram { _ => return create_tuliprox_error_result!(TuliproxErrorKind::Info, "Unexpected match arm input: {:?}", first.as_rule()), }; - let expr = MapperProgram::parse_expression(inner.next().unwrap())?; + let expr = MapperScript::parse_expression(inner.next().unwrap())?; Ok(MatchCase { keys: identifiers, @@ -337,9 +373,14 @@ impl MapperProgram { } fn parse_map_case_key(pair: Pair) -> Result { - let mut inner = pair.into_inner().next().unwrap(); + let inner = pair.into_inner().next().unwrap(); match inner.as_rule() { - Rule::string_literal => Ok(MapCaseKey::Text(inner.as_str().to_string())), + Rule::string_literal => { + let raw = inner.as_str().to_string(); + // remove quotes + let content = &raw[1..raw.len() - 1]; + Ok(MapCaseKey::Text(content.to_string())) + }, Rule::any_match => Ok(MapCaseKey::AnyMatch), _ => create_tuliprox_error_result!(TuliproxErrorKind::Info, "Unexpected map key: {:?}", inner.as_rule()), } @@ -352,12 +393,12 @@ impl MapperProgram { let identifier = match first.as_rule() { Rule::map_case_key => { - MapperProgram::parse_map_case_key(first)? + MapperScript::parse_map_case_key(first)? } _ => return create_tuliprox_error_result!(TuliproxErrorKind::Info, "Unexpected map case key: {:?}", first.as_rule()), }; - let expr = MapperProgram::parse_expression(inner.next().unwrap())?; + let expr = MapperScript::parse_expression(inner.next().unwrap())?; Ok(MapCase { key: identifier, @@ -367,7 +408,16 @@ impl MapperProgram { fn parse_expression(pair: Pair) -> Result { match pair.as_rule() { - Rule::identifier => Ok(Expression::Identifier(pair.as_str().to_string())), + Rule::var_access => { + let text = pair.as_str().to_string(); + if text.contains(".") { + let splitted: Vec<&str> = text.splitn(2, '.').collect(); + Ok(Expression::VarAccess(splitted[0].to_string(), splitted[1].to_string())) + } else { + Ok(Expression::Identifier(text)) + } + + }, Rule::string_literal => { let raw = pair.as_str(); @@ -392,7 +442,7 @@ impl MapperProgram { let fn_name = inner.next().unwrap().as_str().to_string(); let mut args = vec![]; for arg in inner { - args.push(MapperProgram::parse_expression(arg)?); + args.push(MapperScript::parse_expression(arg)?); } let name = BuiltInFunction::from_str(&fn_name)?; Ok(Expression::FunctionCall { name, args }) @@ -402,7 +452,7 @@ impl MapperProgram { let case_pairs = pair.into_inner(); let mut cases = vec![]; for case in case_pairs { - cases.push(MapperProgram::parse_match_case(case)?); + cases.push(MapperScript::parse_match_case(case)?); } Ok(Expression::MatchBlock(cases)) } @@ -418,11 +468,16 @@ impl MapperProgram { }; let mut cases = vec![]; for case in inner { - cases.push(MapperProgram::parse_map_case(case)?); + cases.push(MapperScript::parse_map_case(case)?); } Ok(Expression::MapBlock { key, cases}) } + Rule::expression => { + let inner = pair.into_inner().next().unwrap(); + MapperScript::parse_expression(inner) + } + _ => create_tuliprox_error_result!(TuliproxErrorKind::Info, "Unknown expression rule: {:?}", pair.as_rule()), } } @@ -439,15 +494,15 @@ impl Context { } } - pub fn add_var(&mut self, name: &str, value: EvalResult) { + fn add_var(&mut self, name: &str, value: EvalResult) { self.variables.insert(name.to_string(), value); } - pub fn has_var(&self, name: &str) -> bool { + fn has_var(&self, name: &str) -> bool { self.variables.contains_key(name) } - pub fn get_var(&self, name: &str) -> &EvalResult { + fn get_var(&self, name: &str) -> &EvalResult { self.variables.get(name).unwrap_or(&Undefined) } } @@ -462,13 +517,26 @@ enum EvalResult { Failure(String), } +fn compare_tuple_vec<'a>( + a: &'a [(String, String)], + b: &'a [(String, String)], +) -> bool { + fn to_map<'a>(vec: &'a [(String, String)]) -> HashMap<&'a str, &'a str> { + vec.iter() + .map(|(k, v)| (k.as_str(), v.as_str())) + .collect() + } + + to_map(a) == to_map(b) +} + 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! + (Named(a), Named(b)) => compare_tuple_vec(&a, &b), (Failure(_), _) | (_, Failure(_)) => false, (Undefined, _) | (_, Undefined) => false, _ => false, @@ -508,17 +576,35 @@ fn concat_args(args: &Vec) -> Vec<&str> { } impl Expression { - pub fn eval(&self, ctx: &mut Context, provider: &ValueProvider) -> EvalResult { + pub fn eval(&self, ctx: &mut Context, accessor: &ValueAccessor) -> EvalResult { match self { Expression::Identifier(name) => { match ctx.variables.get(name) { - None => EvalResult::Failure(format!("Variable with name {name} not found.")), + None => Failure(format!("Variable with name {name} not found.")), Some(value) => value.clone(), } } + Expression::VarAccess(name, field) => { + match ctx.variables.get(name) { + None => Failure(format!("Variable with name {name} not found.")), + Some(value) => match value { + Undefined => Undefined, + Value(_) => Failure(format!("Variable with name {name} has no fields.")), + Named(values) => { + for (key, val) in values { + if key == field { + return Value(val.to_string()) + } + } + Failure(format!("Variable with name {name} has no field {field}.")) + } + AnyValue | Failure(_) => value.clone(), + }, + } + } Expression::StringLiteral(s) => Value(s.clone()), - Expression::RegexExpr { field, pattern, re_pattern } => { - let val = provider.call(field); + Expression::RegexExpr { field, pattern: _pattern, re_pattern } => { + let val = accessor.get(field); let mut values = vec![]; for caps in re_pattern.captures_iter(&val) { // Positional groups @@ -543,7 +629,7 @@ impl Expression { Named(values) } Expression::FunctionCall { name, args } => { - let evaluated_args: Vec = args.iter().map(|a| a.eval(ctx, provider)).collect(); + let evaluated_args: Vec = args.iter().map(|a| a.eval(ctx, accessor)).collect(); for arg in &evaluated_args { if arg.is_error() { return arg.clone(); @@ -556,6 +642,10 @@ impl Expression { 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(" ")), + BuiltInFunction::Print => { + println!("[MapperScript] {}", concat_args(&evaluated_args).join("")); + Undefined + } } } Expression::MatchBlock(cases) => { @@ -585,7 +675,7 @@ impl Expression { } } if match_count == case_keys_len { - return match_case.expression.eval(ctx, provider); + return match_case.expression.eval(ctx, accessor); } } Undefined @@ -607,7 +697,7 @@ impl Expression { }; if matches { - return map_case.expression.eval(ctx, provider); + return map_case.expression.eval(ctx, accessor); } } Undefined @@ -688,29 +778,29 @@ mod tests { "SHD" => "SD", _ => quality, } + # print("quality is:", quality) coast_quality = match { (coast, quality) => concat(capitalize(coast), " ", uppercase(quality)), - (coast, _) => concat(capitalize(coast), " HD"), - (_, quality) => concat("East", uppercase(quality)), + coast => concat(capitalize(coast), " HD"), + quality => concat("East ", uppercase(quality)), } - result = concat("US: TNT ", coast_quality) + Caption = concat("US: TNT ", coast_quality) "#; - let mut mapper = MapperProgram::parse(dsl).expect("Parsing failed"); + let mapper = MapperScript::parse(dsl).expect("Parsing failed"); println!("Program: {mapper:?}"); - let channels: Vec = vec![ + let mut channels: Vec = vec![ ("D", "HD"), ("A", "FHD"), ("Z", ""), ("K", "HD"), ("B", "HD"), ("A", "HD"), ("K", "SHD"), ("C", "LHD"), ("L", "FHD"), ("R", "UHD"), ("T", "SD"), ("A", "FHD"), ].into_iter().map(|(name, quality)| PlaylistItem { header: PlaylistItemHeader { title: format!("Chanel {name} [{quality}]"), ..Default::default() } }).collect::>(); - for pli in channels.iter() { - let mut ctx = Context::new(); - let provider = ValueProvider { - pli + for pli in channels.iter_mut() { + let mut accessor = ValueAccessor { + pli, }; - mapper.eval(&mut ctx, &provider).expect("TODO: panic message"); - println!("Result: {:?}", ctx.variables.get("result")); + mapper.eval(&mut accessor).expect("TODO: panic message"); + println!("Result: {pli:?}"); } diff --git a/src/model/config_target.rs b/src/model/config_target.rs index a25acfe66..d61d7a1c1 100644 --- a/src/model/config_target.rs +++ b/src/model/config_target.rs @@ -1,4 +1,4 @@ -use crate::foundation::filter::{get_filter, Filter, MockValueProcessor, PatternTemplate, ValueProvider}; +use crate::foundation::filter::{get_filter, Filter, PatternTemplate, ValueProvider}; use crate::tuliprox_error::{create_tuliprox_error_result, handle_tuliprox_error_result_list, info_err, TuliproxError, TuliproxErrorKind}; use crate::model::cluster_flags::ClusterFlags; use crate::model::config_rename::ConfigRename; @@ -301,9 +301,8 @@ impl ConfigTarget { } pub fn filter(&self, provider: &ValueProvider) -> bool { - let mut processor = MockValueProcessor {}; if let Some(filter) = self.t_filter.as_ref() { - return filter.filter(provider, &mut processor); + return filter.filter(provider); } true } diff --git a/src/model/mapping.rs b/src/model/mapping.rs index b0a486cf5..59377f6bc 100644 --- a/src/model/mapping.rs +++ b/src/model/mapping.rs @@ -1,20 +1,14 @@ use enum_iterator::Sequence; -use log::{debug, error, trace}; -use regex::Regex; -use std::collections::HashMap; use std::fmt::Display; use std::str::FromStr; use std::sync::atomic::AtomicU32; use std::sync::Arc; -use crate::foundation::filter::{apply_templates_to_pattern_single, get_filter, prepare_templates, Filter, PatternTemplate, RegexWithCaptures, ValueProcessor}; +use crate::foundation::filter::{apply_templates_to_pattern_single, get_filter, prepare_templates, Filter, PatternTemplate}; +use crate::foundation::mapper::MapperScript; use crate::model::valid_property; -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; -use crate::utils::CONSTANTS; pub const COUNTER_FIELDS: &[&str] = &["name", "title", "caption", "chno"]; @@ -162,12 +156,11 @@ impl MapperOperation { #[derive(Debug, Clone, serde::Serialize, serde::Deserialize, Default)] pub struct Mapper { pub filter: String, - pub pattern: Vec, - pub pipeline: Vec, + pub script: String, #[serde(skip_serializing, skip_deserializing)] pub t_filter: Option, #[serde(skip_serializing, skip_deserializing)] - pub t_pattern: Option>, + pub t_script: Option, #[serde(skip_serializing, skip_deserializing)] t_tags: Vec, } @@ -177,196 +170,20 @@ impl Mapper { /// /// Will panic if default `RegEx` gets invalid pub fn prepare(&mut self, templates: Option<&Vec>, tags: Option<&Vec>) -> Result<(), TuliproxError> { - 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::, _>>()?); - + let script = match templates { + None => self.script.to_string(), + Some(tmpls) => apply_templates_to_pattern_single(&self.script, tmpls)?, + }; + self.t_script = Some(MapperScript::parse(&script)?); self.t_tags = tags.map_or_else(Vec::new, std::clone::Clone::clone); - Ok(()) } } -enum TransformModifier { - Lowercase, - Uppercase, - Capitalize, -} - -pub struct MappingValueProcessor<'a> { - pub pli: &'a mut PlaylistItem, - pub mapper: &'a Mapper, -} - -impl MappingValueProcessor<'_> { - fn get_property(&self, key: &str) -> Option { - self.pli.header.get_field(key) - } - - fn set_property(&mut self, key: &str, value: &str) { - if !self.pli.header.set_field(key, value) { - error!("Cant set unknown field {key} set to {value}"); - } - trace!("Property {key} set to {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), - } - } - } - - fn apply_tags(&self, value: &str, captures: &HashMap<&str, &str>) -> Option { - let mut new_value = String::from(value); - let tag_captures = CONSTANTS.re_template_tag.captures_iter(value) - .filter(|caps| caps.len() > 1) - .filter_map(|caps| caps.get(1)) - .map(|caps| caps.as_str()) - .collect::>(); - - let mut captured_tag_values: Vec<&str> = Vec::with_capacity(128); - for tag_capture in tag_captures { - for mapping_tag in &self.mapper.t_tags { - if mapping_tag.name.eq(tag_capture) { - // we have the right tag, now get all captured values - captured_tag_values.clear(); - for cap in &mapping_tag.captures { - if let Some(cap_value) = captures.get(cap.as_str()) { - captured_tag_values.push(cap_value); - } else { - debug!("Cant find any tag match for {tag_capture}"); - return None; - } - } - if !captured_tag_values.is_empty() { - let captured_text = captured_tag_values.join(&mapping_tag.concat); - let replacement = if captured_text.trim().is_empty() { - // nothing found so replace tag with empty string - String::new() - } else { - // Now we have all our captured values, lets create the tag - format!("{}{captured_text}{}", &mapping_tag.prefix, &mapping_tag.suffix) - }; - new_value = new_value.replace(format!("", mapping_tag.name).as_str(), replacement.as_str()); - } - } - } - } - Some(new_value) - } -} - -impl ValueProcessor for MappingValueProcessor<'_> { - fn process<'a>(&mut self, _: &ItemField, value: &str, rewc: &RegexWithCaptures) -> bool { - let mut captured_values = HashMap::new(); - if !rewc.captures.is_empty() { - rewc.re.captures_iter(value) - .filter(|caps| caps.len() > 1) - .for_each(|captures| - for capture_name in &rewc.captures { - let match_opt = captures.name(capture_name.as_str()); - let capture_value = if match_opt.is_some() { - match_opt.map_or("", |m| m.as_str()) - } else { - "" - }; - debug!("match {capture_name}: {capture_value}"); - captured_values.insert(capture_name.as_str(), capture_value); - } - ); - } - - MappingValueProcessor::<'_>::apply_pipeline(self, &captured_values); - true - } -} #[derive(Debug, Clone, serde::Serialize, serde::Deserialize, Default)] pub struct Mapping { diff --git a/src/processing/processor/playlist.rs b/src/processing/processor/playlist.rs index 506184ea1..d584ebeea 100644 --- a/src/processing/processor/playlist.rs +++ b/src/processing/processor/playlist.rs @@ -9,10 +9,10 @@ use std::sync::Arc; use std::thread; use tokio::sync::Mutex; -use crate::foundation::filter::{get_field_value, set_field_value, MockValueProcessor, ValueProvider}; +use crate::foundation::filter::{get_field_value, set_field_value, ValueProvider, ValueAccessor}; use crate::messaging::{send_message, MsgKind}; use crate::model::{ConfigTarget, InputType, ItemField, ProcessTargets, ProcessingOrder}; -use crate::model::{CounterModifier, Mapping, MappingValueProcessor}; +use crate::model::{CounterModifier, Mapping}; use crate::model::{FetchedPlaylist, FieldGetAccessor, FieldSetAccessor, PlaylistEntry, PlaylistGroup, PlaylistItem, UUIDType, XtreamCluster}; use crate::model::{InputStats, PlaylistStats, SourceStats, TargetStats}; use crate::processing::playlist_watch::process_group_watch; @@ -26,7 +26,6 @@ use crate::utils::default_as_default; use deunicode::deunicode; use log::{debug, error, info, log_enabled, trace, warn, Level}; use std::time::Instant; - use crate::model::Epg; use crate::processing::parser::xmltv::flatten_tvguide; use crate::processing::processor::epg::process_playlist_epg; @@ -121,35 +120,24 @@ fn rename_playlist(playlist: &mut [PlaylistGroup], target: &ConfigTarget) -> Opt } } -macro_rules! apply_pattern { - ($pattern:expr, $provider:expr, $processor:expr) => {{ - if let Some(ptrn) = $pattern { - ptrn.filter($provider, $processor); - }; - }}; -} - fn map_channel(mut channel: PlaylistItem, mapping: &Mapping) -> PlaylistItem { if let Some(mapper) = &mapping.mapper { if !mapper.is_empty() { let header = &channel.header; let channel_name = if mapping.match_as_ascii { deunicode(&header.name) } else { header.name.to_string() }; if mapping.match_as_ascii && log_enabled!(Level::Trace) { trace!("Decoded {} for matching to {}", &header.name, &channel_name); } - // let ref_chan = &mut channel; let ref_chan = &mut channel; - let mut mock_processor = MockValueProcessor {}; for m in mapper { - let provider = ValueProvider { pli: &ref_chan.clone() }; - let mut processor = MappingValueProcessor { pli: ref_chan, mapper: m }; - match &m.t_filter { - Some(filter) => { - if filter.filter(&provider, &mut mock_processor) { - //apply_pattern!(&m.t_pattern, &provider, &mut processor); + if let Some(script) = m.t_script.as_ref() { + let provider = ValueProvider { pli: &ref_chan.clone() }; + let mut accessor = ValueAccessor { pli: ref_chan }; + if let Some(filter) = &m.t_filter { + if filter.filter(&provider) { + if let Err(err) = script.eval(&mut accessor) { + error!("Error while evaluating script: {err}"); + } } } - _ => { - //apply_pattern!(&m.t_pattern, &provider, &mut processor); - } } } } @@ -196,7 +184,6 @@ fn map_playlist(playlist: &mut [PlaylistGroup], target: &ConfigTarget) -> Option fn map_playlist_counter(target: &ConfigTarget, playlist: &mut [PlaylistGroup]) { if target.t_mapping.load().is_some() { - let mut mock_processor = MockValueProcessor {}; let guard = target.t_mapping.load(); let mappings = guard.as_ref().unwrap(); for mapping in mappings.iter() { @@ -205,7 +192,7 @@ fn map_playlist_counter(target: &ConfigTarget, playlist: &mut [PlaylistGroup]) { for plg in &mut *playlist { for channel in &mut plg.channels { let provider = ValueProvider { pli: channel }; - if counter.filter.filter(&provider, &mut mock_processor) { + if counter.filter.filter(&provider) { let cntval = counter.value.fetch_add(1, core::sync::atomic::Ordering::SeqCst); let padded_cntval = if counter.padding > 0 { format!("{:0width$}", cntval, width = counter.padding as usize) diff --git a/src/utils/network/request.rs b/src/utils/network/request.rs index 8bbe48d72..88d5aa052 100644 --- a/src/utils/network/request.rs +++ b/src/utils/network/request.rs @@ -17,7 +17,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, Config}; -use crate::model::{ConfigInput, ProxyConfig, InputFetchMethod}; +use crate::model::{ConfigInput, InputFetchMethod}; use crate::repository::storage::{get_input_storage_path, short_hash}; use crate::repository::storage_const; use crate::utils::compression::compression_utils::{is_deflate, is_gzip};