From c3ba38dc8276ce0af37ff35f833fd875f5d4ff67 Mon Sep 17 00:00:00 2001 From: Loic Denuziere Date: Sat, 19 Jan 2019 15:32:36 +0100 Subject: [PATCH] [#10] UnionParser: constant paths in a dictionary --- src/Bolero/Router.fs | 227 ++++++++++++++++++---------------- tests/Unit/Web/App.Routing.fs | 10 +- 2 files changed, 126 insertions(+), 111 deletions(-) diff --git a/src/Bolero/Router.fs b/src/Bolero/Router.fs index cd78a086..9543017d 100644 --- a/src/Bolero/Router.fs +++ b/src/Bolero/Router.fs @@ -103,18 +103,17 @@ module private RouterImpl = let fail : SegmentParserResult = None let ok x : SegmentParserResult = Some x - let inline tryParseBaseType<'T when 'T : (static member TryParse : string * byref<'T> -> bool)> () = - fun s -> - let mutable out = Unchecked.defaultof<'T> - if (^T : (static member TryParse : string * byref<'T> -> bool) (s, &out)) then - Some (box out) - else - None + let inline tryParseBaseType<'T when 'T : (static member TryParse : string * byref<'T> -> bool)> s = + let mutable out = Unchecked.defaultof<'T> + if (^T : (static member TryParse : string * byref<'T> -> bool) (s, &out)) then + Some (box out) + else + None let inline defaultBaseTypeParser<'T when 'T : (static member TryParse : string * byref<'T> -> bool)> = function | [] -> fail | x :: rest -> - match tryParseBaseType<'T>() x with + match tryParseBaseType<'T> x with | Some x -> ok (box x, rest) | None -> fail @@ -214,9 +213,12 @@ module private RouterImpl = [] type ParameterModifier = + /// No modifier: "/{parameter}" | Basic - //| Optional + /// Rest of the path: "/{*parameter}" | Rest of (seq -> obj) * (obj -> seq) + /// Optional segment: "/{?parameter}" (TODO) + //| Optional interface IEquatable with member this.Equals(that) = @@ -225,23 +227,34 @@ module private RouterImpl = | Rest _, Rest _ -> true | _ -> false - type UnionParserSegment = - | Constant of string - /// A UnionParserSegment can be common among multiple union cases. - /// fieldIndex lists these cases, and for each of them, its total number of fields and the index of the field for this segment. - | Parameter of fieldIndex: list * fieldType: Type * fieldSegment: Segment * ParameterModifier + /// A {parameter} path segment. + type Parameter = + { + /// A parameter can be common among multiple union cases. + /// `index` lists these cases, and for each of them, its total number of fields and the index of the field for this segment. + index: list + ``type``: Type + segment: Segment + modifier: ParameterModifier + } + /// The parser for a union type at a given point in the path. type UnionParser = { - /// Parser for the first segment. - head: UnionParserSegment - /// Parsers for the remaining segments, each item in the list for a different union case. - tails: list - /// If Some, there is a case for which this is the final segment, and this is its constructor. + /// All recognized "/constant" segments, associated with the parser for the rest of the path. + constants: IDictionary + /// The recognized "/{parameter}" segment, if any. + parameter: option + /// The union case that parses correctly if the path ends here, if any. finalize: option obj)> } - let parseEndPointCasePath (case: UnionCaseInfo) = + /// Intermediate representation of a path segment. + type UnionParserSegment = + | Constant of string + | Parameter of Parameter + + let parseEndPointCasePath (case: UnionCaseInfo) : list = case.GetCustomAttributes() |> Array.tryPick (function | :? EndPointAttribute as e -> Some (List.ofSeq e.Path) @@ -302,7 +315,12 @@ module private RouterImpl = fields |> Array.mapi (fun i p -> let ty = p.PropertyType - Parameter([case, fields.Length, i], ty, getSegment ty, Basic)) + Parameter { + index = [case, fields.Length, i] + ``type`` = ty + segment = getSegment ty + modifier = Basic + }) |> List.ofSeq match parseEndPointCasePath case with // EndPoint "/" @@ -331,7 +349,12 @@ module private RouterImpl = | "" -> ty, Basic | "*" -> restModifierFor ty | s -> failwithf "Invalid parameter modifier: %s" s - Parameter([case, fields.Length, i], ty, getSegment eltTy, modifier) + Parameter { + index = [case, fields.Length, i] + ``type`` = ty + segment = getSegment eltTy + modifier = modifier + } else failwithf "Union case %s.%s has endpoint definition with duplicate field %s" case.DeclaringType.FullName case.Name fieldName @@ -348,11 +371,14 @@ module private RouterImpl = let caseCtor (case: UnionCaseInfo) : UnionCaseInfo * (obj[] -> obj) = case, FSharpValue.PreComputeUnionConstructor(case, true) - let rec mergeEndPointCaseFragments (cases: seq>) : list * option obj)> = + let rec mergeEndPointCaseFragments (cases: seq>) : UnionParser = let constants = Dictionary() let mutable parameter = None let mutable final = None cases |> Seq.iter (fun (case, p) -> + let conflict (case': UnionCaseInfo) s = + failwithf "Union %s's cases %s and %s have conflicting endpoint definitions (%s)" + case.DeclaringType.FullName case.Name case'.Name s match p with | Constant s :: rest -> let existing = @@ -360,75 +386,57 @@ module private RouterImpl = | true, x -> x | false, _ -> [] constants.[s] <- (case, rest) :: existing - | Parameter(n, ty, seg, modif) :: rest -> + | Parameter param :: rest -> match parameter with - | Some (n', ty', seg, ps, modif') -> - if ty <> ty' then - failwithf "[1] Union %s has cases with conflicting endpoint definitions" case.DeclaringType.FullName - elif modif <> modif' then - failwithf "[2] Union %s has cases with conflicting endpoint definitions" case.DeclaringType.FullName - else - parameter <- Some (n @ n', ty, seg, (case, rest) :: ps, modif) + | Some (case', param', _) when param.``type`` <> param'.``type`` -> + conflict case' "type mismatch on the same parameter" + | Some (case', param', _) when param.modifier <> param'.modifier -> + conflict case' "different modifiers on the same parameter" + | Some (case', param', ps) -> + let param = { param with index = param.index @ param'.index } + parameter <- Some (case', param, (case, rest) :: ps) | None -> - parameter <- Some (n, ty, seg, [case, rest], modif) + parameter <- Some (case, param, [case, rest]) | [] -> match final with - | Some _ -> - failwithf "[3] Union %s has cases with conflicting endpoint definitions" case.DeclaringType.FullName - | None -> - final <- Some (caseCtor case) + | Some (case', _) -> conflict case' "same full path" + | None -> final <- Some (case, caseCtor case) ) - [ - for KeyValue(s, cases) in constants do - let tails, final = mergeEndPointCaseFragments cases - yield { - head = Constant s - tails = tails - finalize = final - } - match parameter with - | None -> () - | Some (n, ty, seg, cases, modif) -> - let tails, final = mergeEndPointCaseFragments cases - yield { - head = Parameter(n, ty, seg, modif) - tails = tails - finalize = final - } - ], final + { + constants = dict [ + for KeyValue(s, cases) in constants do + yield s, mergeEndPointCaseFragments cases + ] + parameter = parameter |> Option.map (fun (_, param, cases) -> + param, mergeEndPointCaseFragments cases) + finalize = final |> Option.map snd + } let parseUnion cases : SegmentParser = - let parsers, final = mergeEndPointCaseFragments cases + let parser = mergeEndPointCaseFragments cases fun l -> let d = Dictionary() - let rec run parsers final l = - parsers - |> Seq.tryPick (fun p -> - match p.head, l with - | Constant s, s' :: rest when s = s' -> - run p.tails p.finalize rest - | Constant _, _ -> - None - | Parameter(n, _, seg, Basic), l -> - match seg.parse l with - | None -> None - | Some (o, rest) -> - for (case, fieldCount, i) in n do - let a = - match d.TryGetValue(case) with - | true, a -> a - | false, _ -> - let a = Array.zeroCreate fieldCount - d.[case] <- a - a - a.[i] <- o - run p.tails p.finalize rest - | Parameter(n, _, seg, Rest(restBuild, _)), l -> - let restValues = ResizeArray() - let rec parse l = - match seg.parse l, l with - | None, [] -> - for (case, fieldCount, i) in n do + let rec run (parser: UnionParser) l = + let finalize rest = + parser.finalize |> Option.map (fun (case, ctor) -> + let args = + match d.TryGetValue(case) with + | true, args -> args + | false, _ -> [||] + (ctor args, rest)) + let mutable constant = Unchecked.defaultof<_> + match l with + | [] -> finalize [] + | s :: rest when parser.constants.TryGetValue(s, &constant) -> + run constant rest + | l -> + parser.parameter + |> Option.bind (function + | { modifier = Basic } as param, nextParser -> + match param.segment.parse l with + | None -> None + | Some (o, rest) -> + for (case, fieldCount, i) in param.index do let a = match d.TryGetValue(case) with | true, a -> a @@ -436,24 +444,31 @@ module private RouterImpl = let a = Array.zeroCreate fieldCount d.[case] <- a a - a.[i] <- restBuild restValues - run p.tails p.finalize [] - | None, _::_ -> None - | Some (o, rest), _ -> - restValues.Add(o) - parse rest - parse l - ) - |> Option.orElseWith (fun () -> - final |> Option.map (fun (case, ctor) -> - let args = - match d.TryGetValue(case) with - | true, args -> args - | false, _ -> [||] - ctor args, l + a.[i] <- o + run nextParser rest + | { modifier = Rest(restBuild, _) } as param, nextParser -> + let restValues = ResizeArray() + let rec parse l = + match param.segment.parse l, l with + | None, [] -> + for (case, fieldCount, i) in param.index do + let a = + match d.TryGetValue(case) with + | true, a -> a + | false, _ -> + let a = Array.zeroCreate fieldCount + d.[case] <- a + a + a.[i] <- restBuild restValues + run nextParser [] + | None, _::_ -> None + | Some (o, rest), _ -> + restValues.Add(o) + parse rest + parse l ) - ) - run parsers final l + |> Option.orElseWith (fun () -> finalize l) + run parser l let parseConsecutiveTypes getSegment (tys: Type[]) (ctor: obj[] -> obj) : SegmentParser = let fields = Array.map getSegment tys @@ -485,12 +500,12 @@ module private RouterImpl = let vals = dector o path |> List.collect (function | Constant s -> [s] - | Parameter(n, _, seg, Basic) -> - let (_, _, i) = n |> List.find (fun (case', _, _) -> case' = case) - seg.write vals.[i] - | Parameter(n, _, seg, Rest(_, decons)) -> - let (_, _, i) = n |> List.find (fun (case', _, _) -> case' = case) - [ for x in decons vals.[i] do yield! seg.write x ] + | Parameter({ modifier = Basic } as param) -> + let (_, _, i) = param.index |> List.find (fun (case', _, _) -> case' = case) + param.segment.write vals.[i] + | Parameter({ modifier = Rest(_, decons) } as param) -> + let (_, _, i) = param.index |> List.find (fun (case', _, _) -> case' = case) + [ for x in decons vals.[i] do yield! param.segment.write x ] ) let unionSegment (getSegment: Type -> Segment) (ty: Type) : Segment = diff --git a/tests/Unit/Web/App.Routing.fs b/tests/Unit/Web/App.Routing.fs index ca5e40e3..9dcc2be8 100644 --- a/tests/Unit/Web/App.Routing.fs +++ b/tests/Unit/Web/App.Routing.fs @@ -108,7 +108,7 @@ let router = eprintfn "ROUTER ERROR: %A" e reraise() -let matchInnerPage = function +let innerPageClass = function | InnerHome -> "home" | InnerNoArg -> "noarg" | InnerWithArg x -> sprintf "witharg-%s" x @@ -119,11 +119,11 @@ let rec pageClass = function | NoArg -> "noarg" | WithArg x -> sprintf "witharg-%s" x | WithArgs(x, y) -> sprintf "withargs-%s-%i" x y - | WithUnion u -> "withunion-" + matchInnerPage u - | WithUnionNotTerminal(u, s) -> sprintf "withunion2-%s-%s" (matchInnerPage u) s + | WithUnion u -> "withunion-" + innerPageClass u + | WithUnionNotTerminal(u, s) -> sprintf "withunion2-%s-%s" (innerPageClass u) s | WithNestedUnion u -> sprintf "withnested-%s" (pageClass u) | WithTuple(x, y, z) -> sprintf "withtuple-%i-%s-%b" x y z - | WithRecord { x = x; y = y; z = z } -> sprintf "withrecord-%i-%s-%b" x (matchInnerPage y) z + | WithRecord { x = x; y = y; z = z } -> sprintf "withrecord-%i-%s-%b" x (innerPageClass y) z | WithList l -> sprintf "withlist-%s" (String.concat "-" [for i, s in l -> sprintf "%i-%s" i s]) | WithArray a -> sprintf "witharray-%s" (String.concat "-" [for i, s in a -> sprintf "%i-%s" i s]) | WithPath s -> sprintf "withpath-%s" s @@ -131,7 +131,7 @@ let rec pageClass = function | WithPathAndSuffix2(s, i) -> sprintf "withpathsuffix2-%s-%i" s i | WithPathAndSuffix3 s -> sprintf "withpathsuffix3-%s" s | WithPathConstant -> "withpathconstant" - | WithPathRecord { x = x; y = y; z = z } -> sprintf "withpathrecord-%i-%s-%b" x (matchInnerPage y) z + | WithPathRecord { x = x; y = y; z = z } -> sprintf "withpathrecord-%i-%s-%b" x (innerPageClass y) z let innerlinks = [