Mapper script built-in function template

This commit is contained in:
euzu
2025-06-12 16:20:15 +02:00
parent ef0c68ea0c
commit dd2dd0b052
7 changed files with 124 additions and 67 deletions
+7
View File
@@ -6,6 +6,13 @@
- more logs sanitized
- added session key for session management
- added sleep timer `sleep_timer_mins` to config.yml
- added mapper script builtin function `template` to access template definitions.
```
station_prefix = template(concat("US_", station, "_PREFIX")),
```
If we assume the variable `station` contains the value `WINK`,
this script receives the template with the concatenated name `US_WINK_PREFIX` which should be defined in `templates` section,
and assigns it to the variable `station_prefix`.
# 3.1.3 (2025-06-06)
- Fixed xtream codes series info duplicate fields problem.
+42 -43
View File
@@ -8,7 +8,6 @@ use pest::iterators::Pair;
use pest::Parser;
use std::cmp::Ordering;
use std::collections::HashMap;
use crate::model::{FieldGetAccessor, FieldSetAccessor, ItemField};
use crate::model::{PlaylistItem, PlaylistItemType};
use crate::tools::directed_graph::DirectedGraph;
@@ -286,7 +285,7 @@ fn get_parser_item_field(expr: &Pair<Rule>) -> Result<ItemField, TuliproxError>
fn get_parser_regexp(
expr: &Pair<Rule>,
templates: &Vec<PatternTemplate>,
templates: Option<&Vec<PatternTemplate>>,
) -> Result<CompiledRegex, TuliproxError> {
if expr.as_rule() == Rule::regexp {
let mut parsed_text = String::from(expr.as_str());
@@ -311,7 +310,7 @@ fn get_parser_regexp(
fn get_parser_field_comparison(
expr: Pair<Rule>,
templates: &Vec<PatternTemplate>,
templates: Option<&Vec<PatternTemplate>>,
) -> Result<Filter, TuliproxError> {
let mut expr_inner = expr.into_inner();
match get_parser_item_field(&expr_inner.next().unwrap()) {
@@ -372,7 +371,7 @@ macro_rules! handle_expr {
fn get_parser_expression(
expr: Pair<Rule>,
templates: &Vec<PatternTemplate>,
templates: Option<&Vec<PatternTemplate>>,
errors: &mut Vec<String>,
) -> Result<Filter, String> {
let mut stmts = Vec::with_capacity(128);
@@ -450,9 +449,7 @@ pub fn get_filter(
filter_text: &str,
templates: Option<&Vec<PatternTemplate>>,
) -> Result<Filter, TuliproxError> {
let empty_list = Vec::with_capacity(0);
let template_list: &Vec<PatternTemplate> = templates.unwrap_or(&empty_list);
let source = apply_templates_to_pattern_single(filter_text, template_list)?;
let source = apply_templates_to_pattern_single(filter_text, templates)?;
match FilterParser::parse(Rule::main, &source) {
Ok(pairs) => {
@@ -465,7 +462,7 @@ pub fn get_filter(
for expr in pair.into_inner() {
match expr.as_rule() {
Rule::expr => {
match get_parser_expression(expr, template_list, &mut errors) {
match get_parser_expression(expr, templates, &mut errors) {
Ok(expr) => {
match &op {
Some(binop) => {
@@ -652,59 +649,61 @@ pub fn prepare_templates(templates: &mut Vec<PatternTemplate>) -> Result<Vec<Pat
pub fn apply_templates_to_pattern(
pattern: &str,
templates: &Vec<PatternTemplate>,
templates_list: Option<&Vec<PatternTemplate>>,
allow_multi: bool,
) -> Result<TemplateValue, TuliproxError> {
let mut new_pattern = TemplateValue::Single(pattern.to_string());
for template in templates {
match &template.value {
TemplateValue::Single(val) => {
match new_pattern {
TemplateValue::Single(ref mut pat) => {
let replaced = pat.replace(&template.placeholder, val);
if replaced != *pat {
*pat = replaced;
}
}
TemplateValue::Multi(ref mut pats) => {
for pat in pats.iter_mut() {
if let Some(templates) = templates_list {
for template in templates {
match &template.value {
TemplateValue::Single(val) => {
match new_pattern {
TemplateValue::Single(ref mut pat) => {
let replaced = pat.replace(&template.placeholder, val);
if replaced != *pat {
*pat = replaced;
}
}
}
}
}
TemplateValue::Multi(ref multi_vals) => {
new_pattern = match &new_pattern {
TemplateValue::Single(pat) => {
let mut new_values = IndexSet::new();
for val in multi_vals {
if pat.contains(&template.placeholder) {
new_values.insert(pat.replace(&template.placeholder, val));
} else {
new_values.insert(pat.clone());
TemplateValue::Multi(ref mut pats) => {
for pat in pats.iter_mut() {
let replaced = pat.replace(&template.placeholder, val);
if replaced != *pat {
*pat = replaced;
}
}
}
TemplateValue::Multi(new_values.into_iter().collect())
}
TemplateValue::Multi(pats) => {
let mut new_values = IndexSet::new();
for val in multi_vals {
for pat in pats {
}
TemplateValue::Multi(ref multi_vals) => {
new_pattern = match &new_pattern {
TemplateValue::Single(pat) => {
let mut new_values = IndexSet::new();
for val in multi_vals {
if pat.contains(&template.placeholder) {
new_values.insert(pat.replace(&template.placeholder, val));
} else {
new_values.insert(pat.clone());
}
}
TemplateValue::Multi(new_values.into_iter().collect())
}
TemplateValue::Multi(new_values.into_iter().collect())
}
};
TemplateValue::Multi(pats) => {
let mut new_values = IndexSet::new();
for val in multi_vals {
for pat in pats {
if pat.contains(&template.placeholder) {
new_values.insert(pat.replace(&template.placeholder, val));
} else {
new_values.insert(pat.clone());
}
}
}
TemplateValue::Multi(new_values.into_iter().collect())
}
};
}
}
}
}
@@ -731,7 +730,7 @@ pub fn apply_templates_to_pattern(
Ok(new_pattern)
}
pub fn apply_templates_to_pattern_single(pattern: &str, templates: &Vec<PatternTemplate>) -> Result<String, TuliproxError> {
pub fn apply_templates_to_pattern_single(pattern: &str, templates: Option<&Vec<PatternTemplate>>) -> Result<String, TuliproxError> {
match apply_templates_to_pattern(pattern, templates, false)? {
TemplateValue::Single(value) => Ok(value),
TemplateValue::Multi(_) => create_tuliprox_error_result!(TuliproxErrorKind::Info, "Multi value templates are not supported for pattern!"),
+59 -9
View File
@@ -1,6 +1,6 @@
#![allow(clippy::empty_docs)]
use crate::foundation::filter::ValueAccessor;
use crate::foundation::filter::{PatternTemplate, TemplateValue, ValueAccessor};
use crate::foundation::mapper::EvalResult::{AnyValue, Failure, Named, Number, Undefined, Value};
use crate::tuliprox_error::{create_tuliprox_error_result, info_err, TuliproxError, TuliproxErrorKind};
use crate::utils::Capitalize;
@@ -35,7 +35,7 @@ block_expr = { "{" ~ statements ~ "}" }
condition = { function_call | var_access | field_access }
assignment = { (field_access | identifier) ~ "=" ~ expression }
expression = { assignment | map_block | match_block | function_call | regex_expr | string_literal | number | var_access | field_access | null | block_expr }
function_name = { "concat" | "uppercase" | "lowercase" | "capitalize" | "trim" | "print" | "number" | "first" }
function_name = { "concat" | "uppercase" | "lowercase" | "capitalize" | "trim" | "print" | "number" | "first" | "template" }
function_call = { function_name ~ "(" ~ (expression ~ ("," ~ expression)*)? ~ ")" }
any_match = { "_" }
match_case_key = { any_match | identifier }
@@ -104,6 +104,7 @@ enum BuiltInFunction {
Print,
ToNumber,
First,
Template,
}
impl FromStr for BuiltInFunction {
@@ -119,6 +120,7 @@ impl FromStr for BuiltInFunction {
"print" => Ok(Self::Print),
"number" => Ok(Self::ToNumber),
"first" => Ok(Self::First),
"template" => Ok(Self::Template),
_ => create_tuliprox_error_result!(TuliproxErrorKind::Info, "Unknown function {}", s),
}
}
@@ -165,8 +167,8 @@ pub struct MapperScript {
}
impl MapperScript {
pub fn eval(&self, setter: &mut ValueAccessor) {
let ctx = &mut MapperContext::new(&self.expressions);
pub fn eval(&self, setter: &mut ValueAccessor, templates: Option<&Vec<PatternTemplate>>) {
let ctx = &mut MapperContext::new(&self.expressions, templates);
self.eval_with_context(ctx, setter);
}
@@ -201,8 +203,8 @@ impl Statement {
}
impl MapperScript {
fn validate(expressions: &Vec<Expression>, statements: &Vec<Statement>) -> Result<(), TuliproxError> {
let ctx = &mut MapperContext::new(expressions);
fn validate(expressions: &Vec<Expression>, statements: &Vec<Statement>, templates: Option<&Vec<PatternTemplate>>) -> Result<(), TuliproxError> {
let ctx = &mut MapperContext::new(expressions, templates);
let mut identifiers: HashSet<String> = HashSet::new();
for stmt in statements {
@@ -216,7 +218,7 @@ impl MapperScript {
Ok(())
}
pub fn parse(input: &str) -> Result<Self, TuliproxError> {
pub fn parse(input: &str, templates: Option<&Vec<PatternTemplate>>) -> Result<Self, TuliproxError> {
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();
@@ -227,7 +229,7 @@ impl MapperScript {
}
}
MapperScript::validate(&expressions, &statements)?;
MapperScript::validate(&expressions, &statements, templates)?;
Ok(Self { expressions, statements })
}
fn parse_statement(pair: Pair<Rule>, expressions: &mut Vec<Expression>) -> Result<Option<Statement>, TuliproxError> {
@@ -536,13 +538,37 @@ impl MapperScript {
pub struct MapperContext<'a> {
expressions: &'a Vec<Expression>,
variables: HashMap<String, EvalResult>,
templates: Option<HashMap<String, &'a PatternTemplate>>,
}
impl<'a> MapperContext<'a> {
fn new(expressions: &'a Vec<Expression>) -> Self {
fn new(expressions: &'a Vec<Expression>, templates: Option<&'a Vec<PatternTemplate>>) -> Self {
Self {
expressions,
variables: HashMap::new(),
templates: templates.and_then(|vec_templates| {
if vec_templates.is_empty() {
None
} else {
let mut hash_map = HashMap::new();
for template in vec_templates {
hash_map.insert(template.name.to_string(), template);
}
Some(hash_map)
}
})
}
}
fn get_template(&self, name: &str) -> Option<&str> {
match self.templates.as_ref() {
None => None,
Some(templates) => templates.get(name).and_then(|&template| {
match &template.value {
TemplateValue::Single(v) => Some(v.as_str()),
TemplateValue::Multi(_) => None,
}
})
}
}
@@ -601,6 +627,7 @@ impl<'a> MapperContext<'a> {
}
match name {
BuiltInFunction::ToNumber
| BuiltInFunction::Template
| BuiltInFunction::First => {
if args.len() > 1 {
return create_tuliprox_error_result!(TuliproxErrorKind::Info, "Function accepts only one argument {:?}, {} given", name, args.len());
@@ -995,6 +1022,29 @@ impl Expression {
None => Undefined,
}
}
BuiltInFunction::Template => {
let evaluated_arg = &evaluated_args[0];
let value = match evaluated_arg {
Value(value) => {
Some(value)
}
Named(values) => {
match values.first() {
None => None,
Some((_key, val)) => Some(val),
}
}
_ => None
};
if let Some(val) = value {
match ctx.get_template(val) {
Some(v) => Value(v.to_string()),
None => Undefined,
}
} else {
Undefined
}
}
}
}
}
+1 -3
View File
@@ -14,9 +14,7 @@ pub struct ConfigRename {
impl ConfigRename {
pub fn prepare(&mut self, templates: Option<&Vec<PatternTemplate>>) -> Result<(), TuliproxError> {
if let Some(templ) = templates {
self.pattern = apply_templates_to_pattern_single(&self.pattern, templ)?;
}
self.pattern = apply_templates_to_pattern_single(&self.pattern, templates)?;
match regex::Regex::new(&self.pattern) {
Ok(pattern) => {
self.re = Some(pattern);
+3 -5
View File
@@ -41,7 +41,7 @@ impl ConfigSortGroup {
(Some(seqs), Some(tmpls)) => {
let mut result = Vec::new();
for s in seqs {
match apply_templates_to_pattern(s, tmpls, true)? {
match apply_templates_to_pattern(s, Some(tmpls), true)? {
TemplateValue::Single(val) => result.push(val),
TemplateValue::Multi(vals) => result.extend(vals),
}
@@ -75,9 +75,7 @@ pub struct ConfigSortChannel {
impl ConfigSortChannel {
pub fn prepare(&mut self, templates: Option<&Vec<PatternTemplate>>) -> Result<(), TuliproxError> {
if let Some(templ) = templates {
self.group_pattern = apply_templates_to_pattern_single(&self.group_pattern, templ)?;
}
self.group_pattern = apply_templates_to_pattern_single(&self.group_pattern, templates)?;
// Compile group_pattern
self.t_re_group_pattern = Some(
Regex::new(&self.group_pattern).map_err(|err| {
@@ -90,7 +88,7 @@ impl ConfigSortChannel {
(Some(seqs), Some(tmpls)) => {
let mut result = Vec::new();
for s in seqs {
match apply_templates_to_pattern(s, tmpls, true)? {
match apply_templates_to_pattern(s, Some(tmpls), true)? {
TemplateValue::Single(val) => result.push(val),
TemplateValue::Multi(vals) => result.extend(vals),
}
+10 -6
View File
@@ -129,8 +129,8 @@ impl MapperOperation {
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)?;
if templates.is_some() {
*value = apply_templates_to_pattern_single(value, templates)?;
}
}
}
@@ -158,12 +158,13 @@ impl Mapper {
Ok(filter) => self.t_filter = Some(filter),
Err(err) => return Err(err),
}
let script = match templates {
None => self.script.to_string(),
Some(tmpls) => apply_templates_to_pattern_single(&self.script, tmpls)?,
let script = if templates.is_some() {
apply_templates_to_pattern_single(&self.script, templates)?
} else {
self.script.to_string()
};
trace!("Mapper script: {script}");
self.t_script = Some(MapperScript::parse(&script)?);
self.t_script = Some(MapperScript::parse(&script, templates)?);
Ok(())
}
}
@@ -178,10 +179,13 @@ pub struct Mapping {
pub counter: Option<Vec<MappingCounterDefinition>>,
#[serde(skip_serializing, skip_deserializing)]
pub t_counter: Option<Vec<MappingCounter>>,
#[serde(skip_serializing, skip_deserializing)]
pub(crate) templates: Option<Vec<PatternTemplate>>
}
impl Mapping {
pub fn prepare(&mut self, templates: Option<&Vec<PatternTemplate>>) -> Result<(), TuliproxError> {
self.templates = templates.map(|t| t.iter().map(PatternTemplate::clone).collect::<Vec<_>>());
if let Some(mapper_list) = &mut self.mapper {
for mapper in mapper_list {
mapper.prepare(templates)?;
+2 -1
View File
@@ -126,13 +126,14 @@ fn map_channel(mut channel: PlaylistItem, mapping: &Mapping) -> PlaylistItem {
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 templates = mapping.templates.as_ref();
for m in mapper {
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) {
script.eval(&mut accessor);
script.eval(&mut accessor, templates);
}
}
}