FF-1404 - Add new flow element Matches

This commit is contained in:
John Andrews
2024-04-15 19:22:02 +12:00
parent bb2a698d68
commit 05458a30de
8 changed files with 381 additions and 4 deletions

View File

@@ -3,28 +3,40 @@ using FileFlows.Plugin;
using FileFlows.Plugin.Attributes;
using System.ComponentModel.DataAnnotations;
namespace FileFlows.BasicNodes.Functions;
/// <summary>
/// A flow element that executes custom code
/// </summary>
public class Function : Node
{
/// <inheritdoc />
public override int Inputs => 1;
/// <inheritdoc />
public override FlowElementType Type => FlowElementType.Logic;
/// <inheritdoc />
public override string Icon => "fas fa-code";
/// <inheritdoc />
public override bool FailureNode => true;
/// <inheritdoc />
public override string HelpUrl => "https://fileflows.com/docs/plugins/basic-nodes/function";
/// <summary>
/// Gets or sets the number of outputs
/// </summary>
[DefaultValue(1)]
[NumberInt(1)]
public new int Outputs { get; set; }
/// <summary>
/// Gets or sets the code to execute
/// </summary>
[Required]
[DefaultValue("// Custom javascript code that you can run against the flow file.\n// Flow contains helper functions for the Flow.\n// Variables contain variables available to this node from previous nodes.\n// Logger lets you log messages to the flow output.\n\n// return 0 to complete the flow.\n// return -1 to signal an error in the flow\n// return 1+ to select which output node will be processed next\n\nif(Variables.file.Size === 0)\n\treturn -1;\n\nreturn 1;")]
[Code(2)]
public string Code { get; set; }
delegate void LogDelegate(params object[] values);
/// <inheritdoc />
public override int Execute(NodeParameters args)
{
if (string.IsNullOrEmpty(Code))

View File

@@ -0,0 +1,81 @@
using FileFlows.Plugin;
using FileFlows.Plugin.Attributes;
using System.ComponentModel.DataAnnotations;
using System.Text.RegularExpressions;
using FileFlows.BasicNodes.Helpers;
namespace FileFlows.BasicNodes.Functions;
/// <summary>
/// A flow element that matches different conditions
/// </summary>
public class Matches : Node
{
/// <inheritdoc />
public override int Inputs => 1;
/// <inheritdoc />
public override FlowElementType Type => FlowElementType.Logic;
/// <inheritdoc />
public override string Icon => "fas fa-equals";
/// <inheritdoc />
public override bool FailureNode => true;
/// <inheritdoc />
public override string HelpUrl => "https://fileflows.com/docs/plugins/basic-nodes/matches";
/// <summary>
/// Gets or sets replacements to replace
/// </summary>
[KeyValue(1, showVariables: true, allowDuplicates: true)]
[Required]
public List<KeyValuePair<string, string>> MatchConditions { get; set; }
/// <inheritdoc />
public override int Execute(NodeParameters args)
{
if (MatchConditions?.Any() != true)
{
args.FailureReason = "No matches defined";
args.Logger.ELog(args.FailureReason);
return -1;
}
int output = 0;
foreach (var match in MatchConditions)
{
output++;
try
{
var value = args.ReplaceVariables(match.Key, stripMissing: true);
if (GeneralHelper.IsRegex(match.Value))
{
if (Regex.IsMatch(value, match.Value, RegexOptions.IgnoreCase | RegexOptions.CultureInvariant))
{
args.Logger?.ILog($"Match found '{match.Value}' = {value}");
return output;
}
}
if (match.Value == value)
{
args.Logger?.ILog($"Match found '{match.Value}' = {value}");
return output;
}
if (MathHelper.IsMathOperation(match.Value))
{
if (MathHelper.IsTrue(value, match.Value))
{
args.Logger?.ILog($"Match found '{match.Value}' = {value}");
return output;
}
}
}
catch (Exception)
{
}
}
args.Logger?.ILog("No matches found");
return MatchConditions.Count + 1;
}
}

View File

@@ -0,0 +1,17 @@
namespace FileFlows.BasicNodes.Helpers;
/// <summary>
/// General helper
/// </summary>
public class GeneralHelper
{
/// <summary>
/// Checks if the input string represents a regular expression.
/// </summary>
/// <param name="input">The input string to check.</param>
/// <returns>True if the input is a regular expression, otherwise false.</returns>
public static bool IsRegex(string input)
{
return new[] { "?", "|", "^", "$", "*" }.Any(ch => input.Contains(ch));
}
}

View File

@@ -0,0 +1,166 @@
using System.Globalization;
namespace FileFlows.BasicNodes.Helpers;
/// <summary>
/// Helper for math operations
/// </summary>
public class MathHelper
{
/// <summary>
/// Checks if the comparison string represents a mathematical operation.
/// </summary>
/// <param name="comparison">The comparison string to check.</param>
/// <returns>True if the comparison is a mathematical operation, otherwise false.</returns>
public static bool IsMathOperation(string comparison)
{
// Check if the comparison string starts with <=, <, >, >=, ==, or =
return new[] { "<=", "<", ">", ">=", "==", "=" }.Any(comparison.StartsWith);
}
/// <summary>
/// Tests if a math operation is true
/// </summary>
/// <param name="value">The value to apply the operation to.</param>
/// <param name="operation">The operation string representing the mathematical operation.</param>
/// <returns>True if the mathematical operation is successful, otherwise false.</returns>
public static bool IsTrue(string value, string operation)
{
// This is a basic example; you may need to handle different operators
switch (operation[..2])
{
case "<=":
return Convert.ToDouble(value) <= Convert.ToDouble(AdjustComparisonValue(operation[2..].Trim()));
case ">=":
return Convert.ToDouble(value) >= Convert.ToDouble(AdjustComparisonValue(operation[2..].Trim()));
case "==":
return Math.Abs(Convert.ToDouble(value) - Convert.ToDouble(AdjustComparisonValue(operation[2..].Trim()))) < 0.05f;
case "!=":
case "<>":
return Math.Abs(Convert.ToDouble(value) - Convert.ToDouble(AdjustComparisonValue(operation[2..].Trim()))) > 0.05f;
}
switch (operation[..1])
{
case "<":
return Convert.ToDouble(value) < Convert.ToDouble(AdjustComparisonValue(operation[1..].Trim()));
case ">":
return Convert.ToDouble(value) > Convert.ToDouble(AdjustComparisonValue(operation[1..].Trim()));
case "=":
return Math.Abs(Convert.ToDouble(value) - Convert.ToDouble(AdjustComparisonValue(operation[1..].Trim()))) < 0.05f;
}
return false;
}
/// <summary>
/// Adjusts the comparison string by handling common mistakes in units and converting them into full numbers.
/// </summary>
/// <param name="comparisonValue">The original comparison string to be adjusted.</param>
/// <returns>The adjusted comparison string with corrected units or the original comparison if no adjustments are made.</returns>
private static string AdjustComparisonValue(string comparisonValue)
{
if (string.IsNullOrWhiteSpace(comparisonValue))
return string.Empty;
string adjustedComparison = comparisonValue.ToLower().Trim();
// Handle common mistakes in units
if (adjustedComparison.EndsWith("mbps"))
{
// Make an educated guess for Mbps to kbps conversion
return adjustedComparison[..^4] switch
{
{ } value when double.TryParse(value, out var numericValue) => (numericValue * 1_000_000)
.ToString(CultureInfo.InvariantCulture),
_ => comparisonValue
};
}
if (adjustedComparison.EndsWith("kbps"))
{
// Make an educated guess for kbps to bps conversion
return adjustedComparison[..^4] switch
{
{ } value when double.TryParse(value, out var numericValue) => (numericValue * 1_000)
.ToString(CultureInfo.InvariantCulture),
_ => comparisonValue
};
}
if (adjustedComparison.EndsWith("kb"))
{
return adjustedComparison[..^2] switch
{
{ } value when double.TryParse(value, out var numericValue) => (numericValue * 1_000 )
.ToString(CultureInfo.InvariantCulture),
_ => comparisonValue
};
}
if (adjustedComparison.EndsWith("mb"))
{
return adjustedComparison[..^2] switch
{
{ } value when double.TryParse(value, out var numericValue) => (numericValue * 1_000_000 )
.ToString(CultureInfo.InvariantCulture),
_ => comparisonValue
};
}
if (adjustedComparison.EndsWith("gb"))
{
return adjustedComparison[..^2] switch
{
{ } value when double.TryParse(value, out var numericValue) => (numericValue * 1_000_000_000 )
.ToString(CultureInfo.InvariantCulture),
_ => comparisonValue
};
}
if (adjustedComparison.EndsWith("tb"))
{
return adjustedComparison[..^2] switch
{
{ } value when double.TryParse(value, out var numericValue) => (numericValue * 1_000_000_000_000)
.ToString(CultureInfo.InvariantCulture),
_ => comparisonValue
};
}
if (adjustedComparison.EndsWith("kib"))
{
return adjustedComparison[..^3] switch
{
{ } value when double.TryParse(value, out var numericValue) => (numericValue * 1_024 )
.ToString(CultureInfo.InvariantCulture),
_ => comparisonValue
};
}
if (adjustedComparison.EndsWith("mib"))
{
return adjustedComparison[..^3] switch
{
{ } value when double.TryParse(value, out var numericValue) => (numericValue * 1_048_576 )
.ToString(CultureInfo.InvariantCulture),
_ => comparisonValue
};
}
if (adjustedComparison.EndsWith("gib"))
{
return adjustedComparison[..^3] switch
{
{ } value when double.TryParse(value, out var numericValue) => (numericValue * 1_099_511_627_776 )
.ToString(CultureInfo.InvariantCulture),
_ => comparisonValue
};
}
if (adjustedComparison.EndsWith("tib"))
{
return adjustedComparison[..^3] switch
{
{ } value when double.TryParse(value, out var numericValue) => (numericValue * 1_000_000_000_000)
.ToString(CultureInfo.InvariantCulture),
_ => comparisonValue
};
}
return comparisonValue;
}
}

View File

@@ -0,0 +1,92 @@
#if(DEBUG)
using FileFlows.BasicNodes.Functions;
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace BasicNodes.Tests;
[TestClass]
public class MatchesTests
{
[TestMethod]
public void Matches_Math()
{
var logger = new TestLogger();
Matches ele = new ();
ele.MatchConditions = new()
{
new("{file.Size}", "<=100KB"),
new("{file.Size}", ">100KB"),
new("{file.Size}", ">10MB"),
};
var args = new FileFlows.Plugin.NodeParameters(null, logger,
false, string.Empty, new LocalFileService());
args.Variables["file.Size"] = 120_000; // 120KB
var result = ele.Execute(args);
var log = logger.ToString();
Assert.AreEqual(2, result);
}
[TestMethod]
public void Matches_String()
{
var logger = new TestLogger();
Matches ele = new ();
ele.MatchConditions = new()
{
new("{file.Name}", "triggerthis"),
new("{file.Name}", "DontTriggerThis"),
new("{file.Name}", "TriggerThis"),
};
var args = new FileFlows.Plugin.NodeParameters(null, logger,
false, string.Empty, new LocalFileService());
args.Variables["file.Name"] = "TriggerThis";
var result = ele.Execute(args);
var log = logger.ToString();
Assert.AreEqual(3, result);
}
[TestMethod]
public void Matches_NoMatch()
{
var logger = new TestLogger();
Matches ele = new ();
ele.MatchConditions = new()
{
new("{file.Name}", "triggerthis"),
new("{file.Name}", "DontTriggerThis"),
new("{file.Name}", "TriggerThis"),
};
var args = new FileFlows.Plugin.NodeParameters(null, logger,
false, string.Empty, new LocalFileService());
args.Variables["file.Name"] = "Nothing";
var result = ele.Execute(args);
var log = logger.ToString();
Assert.AreEqual(4, result);
}
[TestMethod]
public void Matches_Regex()
{
var logger = new TestLogger();
Matches ele = new ();
ele.MatchConditions = new()
{
new("{file.Name}", "triggerthis"),
new("{file.Name}", ".*batman.*"),
new("{file.Name}", "TriggerThis"),
};
var args = new FileFlows.Plugin.NodeParameters(null, logger,
false, string.Empty, new LocalFileService());
args.Variables["file.Name"] = "Superman vs Batman (2017)";
var result = ele.Execute(args);
var log = logger.ToString();
Assert.AreEqual(2, result);
}
}
#endif

View File

@@ -212,6 +212,15 @@
"Message": "Message"
}
},
"Matches": {
"Description": "Compares a set of values and matches conditions to see which output should be called",
"Fields": {
"MatchConditions": "",
"MatchConditionsKey": "Value",
"MatchConditionsValue": "Expression",
"MatchConditions-Help": "The matches to test which output should be called."
}
},
"MoveFile": {
"Description": "Moves a file to the destination folder",
"Outputs": {

Binary file not shown.

Binary file not shown.