From 4bc65aa6bd9d7cb3d86a1e289f532c3965ae29c4 Mon Sep 17 00:00:00 2001 From: Milan Djurdjevic Date: Sat, 4 Jan 2025 20:39:47 +0100 Subject: [PATCH] Refactoring --- src/Analog.Tests/Analog.Tests.fsproj | 5 +- src/Analog.Tests/EntryParserTest.fs | 42 +++++ src/Analog.Tests/ExtractTest.fs | 37 ----- src/Analog.Tests/FilterEvaluatorTest.fs | 111 +++++++++++++ src/Analog.Tests/FilterParserTest.fs | 60 +++++++ src/Analog.Tests/FilterTest.fs | 11 -- src/Analog/Analog.fsproj | 6 +- src/Analog/Core.fs | 208 ++++++++++++++++++++++++ src/Analog/Extract.fs | 30 ---- src/Analog/Filter.fs | 106 ------------ src/Analog/Print.fs | 14 -- src/Analog/Program.fs | 45 +++-- 12 files changed, 447 insertions(+), 228 deletions(-) create mode 100644 src/Analog.Tests/EntryParserTest.fs delete mode 100644 src/Analog.Tests/ExtractTest.fs create mode 100644 src/Analog.Tests/FilterEvaluatorTest.fs create mode 100644 src/Analog.Tests/FilterParserTest.fs delete mode 100644 src/Analog.Tests/FilterTest.fs create mode 100644 src/Analog/Core.fs delete mode 100644 src/Analog/Extract.fs delete mode 100644 src/Analog/Filter.fs delete mode 100644 src/Analog/Print.fs diff --git a/src/Analog.Tests/Analog.Tests.fsproj b/src/Analog.Tests/Analog.Tests.fsproj index 19d33eb..7a9ee78 100644 --- a/src/Analog.Tests/Analog.Tests.fsproj +++ b/src/Analog.Tests/Analog.Tests.fsproj @@ -9,8 +9,9 @@ - - + + + diff --git a/src/Analog.Tests/EntryParserTest.fs b/src/Analog.Tests/EntryParserTest.fs new file mode 100644 index 0000000..2c3ce3d --- /dev/null +++ b/src/Analog.Tests/EntryParserTest.fs @@ -0,0 +1,42 @@ +module Analog.Tests.EntryParserTest + +open System +open Analog +open FsUnit.Xunit +open Xunit + +let parse txt = + EntryParser.value |> EntryParser.parse txt + +let stringOf = Literal.String +let timestampOf = DateTimeOffset.Parse >> Literal.Timestamp + +[] +let ``parse single log line`` () = + let expected = + [ "loglevel", stringOf "INFO" + "message", stringOf "User logged in successfully" + "timestamp", timestampOf "2024-11-17 14:30:55" ] + |> Map.ofList + |> List.singleton + + "[2024-11-17 14:30:55] [INFO] User logged in successfully" + |> parse + |> should equal expected + +[] +let ``parse two log lines`` () = + let expected = + [ [ "loglevel", stringOf "INFO" + "message", stringOf "User logged in successfully" + "timestamp", timestampOf "2024-11-17 14:30:55" ] + |> Map.ofList + [ "loglevel", stringOf "ERROR" + "message", stringOf "Failed to authenticate user" + "timestamp", timestampOf "2024-11-17 14:31:00" ] + |> Map.ofList ] + |> List.ofSeq + + "[2024-11-17 14:30:55] [INFO] User logged in successfully\n[2024-11-17 14:31:00] [ERROR] Failed to authenticate user" + |> parse + |> should equal expected diff --git a/src/Analog.Tests/ExtractTest.fs b/src/Analog.Tests/ExtractTest.fs deleted file mode 100644 index 37e1b91..0000000 --- a/src/Analog.Tests/ExtractTest.fs +++ /dev/null @@ -1,37 +0,0 @@ -module Analog.Tests.ExtractTest - -open Analog -open FsUnit.Xunit -open Xunit - -let parse txt = Extract.def |> Extract.eval txt - -[] -let ``parse single log line`` () = - let expected = - [ "loglevel", "INFO" - "message", "User logged in successfully" - "timestamp", "2024-11-17 14:30:55" ] - |> Map.ofList - |> List.singleton - - "[2024-11-17 14:30:55] [INFO] User logged in successfully" - |> parse - |> should equal expected - -[] -let ``parse two log lines`` () = - let expected = - [ [ "loglevel", "INFO" - "message", "User logged in successfully" - "timestamp", "2024-11-17 14:30:55" ] - |> Map.ofList - [ "loglevel", "ERROR" - "message", "Failed to authenticate user" - "timestamp", "2024-11-17 14:31:00" ] - |> Map.ofList ] - |> List.ofSeq - - "[2024-11-17 14:30:55] [INFO] User logged in successfully\n[2024-11-17 14:31:00] [ERROR] Failed to authenticate user" - |> parse - |> should equal expected diff --git a/src/Analog.Tests/FilterEvaluatorTest.fs b/src/Analog.Tests/FilterEvaluatorTest.fs new file mode 100644 index 0000000..985087a --- /dev/null +++ b/src/Analog.Tests/FilterEvaluatorTest.fs @@ -0,0 +1,111 @@ +module FilterEvaluatorTest + +open System +open Xunit +open FsUnit.Xunit +open Analog + +[] +let ``Evaluate Const filter with Boolean literal`` () = + let entry = Map.empty + let filter = Filter.Const (Literal.Boolean true) + let result = FilterEvaluator.evaluate entry filter + result |> should equal true + +[] +let ``Evaluate Field filter with matching field in entry`` () = + let entry = Map.ofList [ "key", Literal.Boolean true ] + let filter = Filter.Field "key" + let result = FilterEvaluator.evaluate entry filter + result |> should equal true + +[] +let ``Evaluate Field filter with non-matching field in entry`` () = + let entry = Map.ofList [ "key", Literal.Boolean true ] + let filter = Filter.Field "missingKey" + let result = FilterEvaluator.evaluate entry filter + result |> should equal false + +[] +let ``Evaluate Binary Equal filter with matching String literals`` () = + let entry = Map.empty + let filter = + Filter.Binary( + Filter.Const (Literal.String "test"), + Operator.Equal, + Filter.Const (Literal.String "test") + ) + let result = FilterEvaluator.evaluate entry filter + result |> should equal true + +[] +let ``Evaluate Binary NotEqual filter with non-matching Number literals`` () = + let entry = Map.empty + let filter = + Filter.Binary( + Filter.Const (Literal.Number 1.0), + Operator.NotEqual, + Filter.Const (Literal.Number 2.0) + ) + let result = FilterEvaluator.evaluate entry filter + result |> should equal true + +[] +let ``Evaluate Binary GreaterThan filter with Timestamp literals`` () = + let entry = Map.empty + let filter = + Filter.Binary( + Filter.Const (Literal.Timestamp (DateTimeOffset(2025, 1, 1, 0, 0, 0, TimeSpan.Zero))), + Operator.GreaterThan, + Filter.Const (Literal.Timestamp (DateTimeOffset(2024, 1, 1, 0, 0, 0, TimeSpan.Zero))) + ) + let result = FilterEvaluator.evaluate entry filter + result |> should equal true + +[] +let ``Evaluate Binary And filter with Boolean literals`` () = + let entry = Map.empty + let filter = + Filter.Binary( + Filter.Const (Literal.Boolean true), + Operator.And, + Filter.Const (Literal.Boolean true) + ) + let result = FilterEvaluator.evaluate entry filter + result |> should equal true + +[] +let ``Evaluate Binary Or filter with one Boolean literal true`` () = + let entry = Map.empty + let filter = + Filter.Binary( + Filter.Const (Literal.Boolean false), + Operator.Or, + Filter.Const (Literal.Boolean true) + ) + let result = FilterEvaluator.evaluate entry filter + result |> should equal true + +[] +let ``Evaluate Binary Equal filter with mismatched Literal types`` () = + let entry = Map.empty + let filter = + Filter.Binary( + Filter.Const (Literal.String "test"), + Operator.Equal, + Filter.Const (Literal.Number 42.0) + ) + let result = FilterEvaluator.evaluate entry filter + result |> should equal false + +[] +let ``Evaluate Binary GreaterThan filter with invalid field in entry`` () = + let entry = Map.ofList [ "key", Literal.Number 10.0 ] + let filter = + Filter.Binary( + Filter.Field "key", + Operator.GreaterThan, + Filter.Const (Literal.Number 20.0) + ) + let result = FilterEvaluator.evaluate entry filter + result |> should equal false diff --git a/src/Analog.Tests/FilterParserTest.fs b/src/Analog.Tests/FilterParserTest.fs new file mode 100644 index 0000000..aeeb423 --- /dev/null +++ b/src/Analog.Tests/FilterParserTest.fs @@ -0,0 +1,60 @@ +module Analog.Tests.FilterParserTest + +open System + +open FsUnitTyped +open Xunit + +open Analog + +let parse = FilterParser.expression |> ParserRunner.run + +[] +let ``parse should correctly parse a constant string`` () = + parse "'hello'" |> shouldEqual (Result.Ok(Const(String "hello"))) + +[] +let ``parse should correctly parse a constant number`` () = + parse "42.5" |> shouldEqual (Result.Ok(Const(Literal.Number 42.5))) + +[] +let ``parse should correctly parse a constant boolean (true)`` () = + parse "true" |> shouldEqual (Result.Ok(Const(Boolean true))) + +[] +let ``parse should correctly parse a constant boolean (false)`` () = + parse "false" |> shouldEqual (Result.Ok(Const(Boolean false))) + +[] +let ``parse should correctly parse a field identifier`` () = + parse "fieldName" |> shouldEqual (Result.Ok(Field "fieldName")) + +[] +let ``parse should correctly parse a simple binary expression`` () = + parse "'value' = fieldName" + |> shouldEqual (Result.Ok(Binary(Const(String "value"), Equal, Field "fieldName"))) + +[] +let ``parse should correctly parse a complex binary expression`` () = + parse "'value' = fieldName & 42 > 10" + |> shouldEqual ( + Result.Ok( + Binary( + Binary(Const(String "value"), Equal, Field "fieldName"), + And, + Binary(Const(Literal.Number 42.0), GreaterThan, Const(Literal.Number 10.0)) + ) + ) + ) + +[] +let ``parse should correctly parse a timestamp`` () = + let input = "2024-01-03T12:34:56+00:00" + let result = parse input + result |> shouldEqual (Result.Ok(Const(Timestamp(DateTimeOffset.Parse input)))) + +[] +let ``parse should return an error for invalid input`` () = + match parse "'unterminated string" with + | Ok _ -> failwith "parse should return an error for invalid input" + | Error _ -> () diff --git a/src/Analog.Tests/FilterTest.fs b/src/Analog.Tests/FilterTest.fs deleted file mode 100644 index 7e3ef4e..0000000 --- a/src/Analog.Tests/FilterTest.fs +++ /dev/null @@ -1,11 +0,0 @@ -module Analog.Tests.FilterTest - -open Analog.Filter -open Xunit -open FsUnit.Xunit - -[] -let ``Evaluate expression against a log entry`` () = - let entry = Map["name", "value" :> obj] - let expression = Binary(Field("name"), Equal, Const(String "value")) - Evaluator.eval expression entry |> should equal true diff --git a/src/Analog/Analog.fsproj b/src/Analog/Analog.fsproj index 14a7cfa..648c82a 100644 --- a/src/Analog/Analog.fsproj +++ b/src/Analog/Analog.fsproj @@ -6,9 +6,7 @@ - - - + @@ -18,8 +16,6 @@ - - diff --git a/src/Analog/Core.fs b/src/Analog/Core.fs new file mode 100644 index 0000000..46f89ca --- /dev/null +++ b/src/Analog/Core.fs @@ -0,0 +1,208 @@ +namespace Analog + +open System + +type Literal = + | String of string + | Number of float + | Boolean of bool + | Timestamp of DateTimeOffset + +type Entry = Map + +type Operator = + | Equal + | NotEqual + | GreaterThan + | GreaterThanOrEqual + | LessThan + | LessThanOrEqual + | And + | Or + +type Filter = + | Const of Literal + | Field of string + | Binary of Filter * Operator * Filter + +module ParserRunner = + open FParsec + + let run parser = + run parser + >> function + | Success(value, _, _) -> Result.Ok value + | Failure(error, _, _) -> Result.Error error + + let tryRun parser = + run parser + >> function + | Result.Ok value -> Option.Some value + | Result.Error _ -> Option.None + +module LiteralParser = + open FParsec + + let number: Parser<_, unit> = + pfloat + >>= fun res -> + if Double.IsInfinity res || Double.IsNaN res then + fail "Number cannot be infinite or NaN" + else + preturn res + |> attempt + |>> Literal.Number + + let boolean: Parser<_, unit> = + choice [ pstringCI "true" >>% true; pstringCI "false" >>% false ] + |>> Literal.Boolean + + let timestamp: Parser<_, unit> = + restOfLine false + >>= fun input -> + try + DateTimeOffset.Parse input |> preturn |>> Literal.Timestamp + with err -> + fail err.Message + |> attempt + + let string: Parser<_, unit> = restOfLine true |>> Literal.String + + let literal: Parser<_, unit> = choice [ timestamp; number; boolean; string ] + +module EntryParser = + open GrokNet + + type private RawEntry = Map + + let create txt = + try + Grok txt |> Result.Ok + with err -> + Result.Error err.Message + + let value = + "\[%{TIMESTAMP_ISO8601:timestamp}\] \[%{LOGLEVEL:loglevel}\] %{GREEDYDATA:message}" + |> Grok + + let private group list (key, value) = + match list with + | [] -> [ Map([ key, value ]) ] + | head :: tail -> + if head |> Map.containsKey key then + [ Map([ key, value ]); head ] @ tail + else + (head |> Map.add key value) :: tail + + let private parseRaw (entry: RawEntry) = + entry + |> Map.map (fun _ -> ParserRunner.tryRun LiteralParser.literal) + |> Map.filter (fun _ value -> value |> Option.isSome) + |> Map.map (fun _ value -> value |> Option.get) + + let parse text (grok: Grok) : Entry list = + grok.Parse text + |> Seq.map (fun i -> i.Key, i.Value.ToString()) + |> Seq.fold group List.empty + |> List.map parseRaw + |> List.rev + +module FilterParser = + open FParsec + + let string: Parser<_, unit> = + skipChar '\'' >>. manyCharsTill anyChar (skipChar '\'') |>> String .>> spaces + + let number: Parser<_, unit> = pfloat |>> Literal.Number .>> spaces + + let boolean: Parser<_, unit> = LiteralParser.boolean .>> spaces + + let timestamp: Parser<_, unit> = LiteralParser.timestamp .>> spaces + + let constant: Parser<_, unit> = + choice [ string; timestamp; number; boolean ] |>> Const + + let field: Parser<_, unit> = many1Chars (letter <|> digit) |>> Field .>> spaces + + let term: Parser<_, unit> = choice [ constant; field ] + + let expression: Parser<_, unit> = + let precedence = OperatorPrecedenceParser() + precedence.TermParser <- choice [ constant; field ] + let add = precedence.AddOperator + let binary operator left right = Binary(left, operator, right) + add (InfixOperator("&", spaces, 1, Associativity.Left, binary And)) + add (InfixOperator("|", spaces, 2, Associativity.Left, binary Or)) + add (InfixOperator(">", spaces, 3, Associativity.None, binary GreaterThan)) + add (InfixOperator(">=", spaces, 4, Associativity.None, binary GreaterThanOrEqual)) + add (InfixOperator("<", spaces, 5, Associativity.None, binary LessThan)) + add (InfixOperator("<=", spaces, 6, Associativity.None, binary LessThanOrEqual)) + add (InfixOperator("=", spaces, 7, Associativity.None, binary Equal)) + add (InfixOperator("<>", spaces, 8, Associativity.None, binary NotEqual)) + precedence.ExpressionParser + +module FilterEvaluator = + + type private Eval = + | Temp of Literal option + | Final of bool + + let private compareLiteral (left: Literal option) (right: Literal option) comparer = + match left, right with + | Some left, Some right -> + match left, right with + | String _, String _ -> comparer left right + | Number _, Number _ -> comparer left right + | Boolean _, Boolean _ -> comparer left right + | Timestamp _, Timestamp _ -> comparer left right + | _ -> false + | _ -> false + + let private combineLiteral (left: Literal option) (right: Literal option) combiner = + match left, right with + | Some left, Some right -> + match left, right with + | Boolean left, Boolean right -> combiner left right + | _ -> false + | _ -> false + + let private wrapFinal = Literal.Boolean >> Option.Some + + let private compareEvaluation (left: Eval) (right: Eval) comparer = + match left, right with + | Temp left, Temp right -> compareLiteral left right comparer + | Temp left, Final right -> compareLiteral left (wrapFinal right) comparer + | Final left, Temp right -> compareLiteral (wrapFinal left) right comparer + | Final left, Final right -> compareLiteral (wrapFinal left) (wrapFinal right) comparer + + let private combineEvaluation (left: Eval) (right: Eval) combiner = + match left, right with + | Temp left, Temp right -> combineLiteral left right combiner + | Temp left, Final right -> combineLiteral left (wrapFinal right) combiner + | Final left, Temp right -> combineLiteral (wrapFinal left) right combiner + | Final left, Final right -> combineLiteral (wrapFinal left) (wrapFinal right) combiner + + let private evalOperator (left: Eval) (operator: Operator) (right: Eval) = + match operator with + | Equal -> compareEvaluation left right (=) + | NotEqual -> compareEvaluation left right (<>) + | GreaterThan -> compareEvaluation left right (>) + | GreaterThanOrEqual -> compareEvaluation left right (>=) + | LessThan -> compareEvaluation left right (<) + | LessThanOrEqual -> compareEvaluation left right (<=) + | And -> combineEvaluation left right (&&) + | Or -> combineEvaluation left right (||) + + let rec private eval (expression: Filter) (entry: Entry) : Eval = + match expression with + | Filter.Const right -> right |> Option.Some |> Eval.Temp + | Filter.Field field -> entry |> Map.tryFind field |> Eval.Temp + | Filter.Binary(left, operator, right) -> + let left = eval left entry + let right = eval right entry + evalOperator left operator right |> Eval.Final + + let evaluate entry expression = + match eval expression entry with + | Temp temp -> temp |> Option.isSome + | Final final -> final \ No newline at end of file diff --git a/src/Analog/Extract.fs b/src/Analog/Extract.fs deleted file mode 100644 index 918ac70..0000000 --- a/src/Analog/Extract.fs +++ /dev/null @@ -1,30 +0,0 @@ -module Analog.Extract - -open GrokNet - -type Result = Map - -let init txt = - try - Grok txt |> Result.Ok - with err -> - Result.Error err.Message - -let def = - "\[%{TIMESTAMP_ISO8601:timestamp}\] \[%{LOGLEVEL:loglevel}\] %{GREEDYDATA:message}" - |> Grok - -let private group list (key, value) = - match list with - | [] -> [ Map([ key, value ]) ] - | head :: tail -> - if head |> Map.containsKey key then - [ Map([ key, value ]); head ] @ tail - else - (head |> Map.add key value) :: tail - -let eval txt (grok: Grok) = - grok.Parse txt - |> Seq.map (fun i -> i.Key, i.Value.ToString()) - |> Seq.fold group List.empty> - |> List.rev diff --git a/src/Analog/Filter.fs b/src/Analog/Filter.fs deleted file mode 100644 index 5f4378c..0000000 --- a/src/Analog/Filter.fs +++ /dev/null @@ -1,106 +0,0 @@ -module Analog.Filter - -open System - -type Operator = - | Equal - | NotEqual - | GreaterThan - | GreaterThanOrEqual - | LessThan - | LessThanOrEqual - | And - | Or - -type Constant = - | String of string - | Integer of int - | Float of float - | Boolean of bool - | Null - -type Expression = - | Const of Constant - | Field of string - | Binary of Expression * Operator * Expression - -module Evaluator = - let private box (obj: obj) : Constant = - match obj with - | :? string as x -> String x - | :? int as x -> Integer x - | :? float as x -> Float x - | :? bool as x -> Boolean x - | _ -> Null - - let rec private next (expression: Expression) (entry: Map) : obj = - match expression with - | Const literal -> literal - | Field identifier -> entry[identifier] |> box :> obj - | Binary(left, operator, right) -> - let lEval = next left entry - let rEval = next right entry - // After evaluating left and right expressions, compare them using the operator. - match operator with - | Equal -> lEval = rEval - | NotEqual -> lEval <> rEval - | GreaterThan -> (lEval :?> IComparable) > (rEval :?> IComparable) - | GreaterThanOrEqual -> (lEval :?> IComparable) >= (rEval :?> IComparable) - | LessThan -> (lEval :?> IComparable) < (rEval :?> IComparable) - | LessThanOrEqual -> (lEval :?> IComparable) <= (rEval :?> IComparable) - | And -> (lEval :?> bool) && (rEval :?> bool) - | Or -> (lEval :?> bool) || (rEval :?> bool) - - let eval expression entry = next expression entry :?> bool - -module Parser = - open FParsec - - let private quote: Parser<_, unit> = skipChar '\'' - - let private literal: Parser<_, unit> = - let str: Parser<_, unit> = - quote >>. manyCharsTill anyChar quote |>> String .>> spaces - - let int: Parser<_, unit> = - numberLiteral NumberLiteralOptions.DefaultInteger "integer" - |>> fun number -> int number.String - |>> Integer - .>> spaces - - let flt: Parser<_, unit> = - numberLiteral NumberLiteralOptions.DefaultFloat "float" - |>> fun number -> float number.String - |>> Float - .>> spaces - - let bin: Parser<_, unit> = - choice [ pstringCI "true" >>% Boolean true; pstringCI "false" >>% Boolean false ] - .>> spaces - - let nil: Parser<_, unit> = pstringCI "null" >>% Null .>> spaces - - choice [ str; int; flt; bin; nil ] |>> Const - - let private identifier: Parser<_, unit> = - many1Chars (letter <|> digit) |>> Field .>> spaces - - let private expression = - let precedence = OperatorPrecedenceParser() - precedence.TermParser <- choice [ literal; identifier ] - let add = precedence.AddOperator - let binary operator left right = Binary(left, operator, right) - add (InfixOperator("&", spaces, 1, Associativity.Left, binary And)) - add (InfixOperator("|", spaces, 2, Associativity.Left, binary Or)) - add (InfixOperator(">", spaces, 3, Associativity.None, binary GreaterThan)) - add (InfixOperator(">=", spaces, 4, Associativity.None, binary GreaterThanOrEqual)) - add (InfixOperator("<", spaces, 5, Associativity.None, binary LessThan)) - add (InfixOperator("<=", spaces, 6, Associativity.None, binary LessThanOrEqual)) - add (InfixOperator("=", spaces, 7, Associativity.None, binary Equal)) - add (InfixOperator("<>", spaces, 8, Associativity.None, binary NotEqual)) - precedence.ExpressionParser - - let parse input = - match run expression input with - | Success(result, _, _) -> Result.Ok result - | Failure(errorMsg, _, _) -> Result.Error errorMsg diff --git a/src/Analog/Print.fs b/src/Analog/Print.fs deleted file mode 100644 index 2fb641a..0000000 --- a/src/Analog/Print.fs +++ /dev/null @@ -1,14 +0,0 @@ -module Analog.Print - -open Spectre.Console -open Spectre.Console.Json -open System.Text.Json - -let json: obj -> unit = - JsonSerializer.Serialize - >> JsonText - >> AnsiConsole.Write - >> AnsiConsole.WriteLine - -let error message = - Text(message, Style(foreground = Color.Red)) |> AnsiConsole.Write diff --git a/src/Analog/Program.fs b/src/Analog/Program.fs index 2601299..24111ba 100644 --- a/src/Analog/Program.fs +++ b/src/Analog/Program.fs @@ -5,15 +5,15 @@ open Argu type Argument = | [] File of string - | [] Extraction of string - | [] Transformation of string + | [] Pattern of string + | [] Filter of string interface IArgParserTemplate with member this.Usage = match this with | File _ -> "Log file path." - | Extraction _ -> "Extraction pattern." - | Transformation _ -> "Transformation expression." + | Pattern _ -> "GROK pattern." + | Filter _ -> "Filter expression." let handle (args: ParseResults) = let text = @@ -21,22 +21,22 @@ let handle (args: ParseResults) = |> List.map File.ReadAllText |> List.reduce (fun all next -> all + next) - let extract = - args.TryGetResult Argument.Extraction - |> Option.map Extract.init - |> Option.defaultValue (Extract.def |> Result.Ok) - - let transform = - args.TryGetResult Argument.Transformation |> Option.map Filter.Parser.parse - - match extract, transform with - | Ok extract, None -> Result.Ok(extract, None, text) - | Ok extract, Some transform -> - match transform with - | Ok transform -> Result.Ok(extract, Some transform, text) - | Error error -> Result.Error error - | Error errorValue, None -> Result.Error errorValue - | Error error, Some _ -> Result.Error error + let entries = + args.TryGetResult Argument.Pattern + |> Option.map EntryParser.create + |> Option.defaultValue (EntryParser.value |> Result.Ok) + |> Result.map (EntryParser.parse text) + + args.TryGetResult Argument.Filter + |> Option.map (ParserRunner.run FilterParser.expression) + |> Option.map (fun res -> + res + |> Result.bind (fun filter -> entries |> Result.map (fun entries -> filter, entries))) + |> Option.map (fun result -> + result + |> Result.map (fun (filter, entries) -> + entries |> List.filter (fun entry -> FilterEvaluator.evaluate entry filter))) + |> Option.defaultValue entries let args = try @@ -47,7 +47,6 @@ let args = with err -> Result.Error err.Message - match args |> Result.bind handle with -| Ok resultValue -> failwith "not implemented" -| Error errorValue -> failwith "not implemented" +| Ok entries -> entries.Length |> printf "%i" +| Error error -> error |> eprintf "%s"