Skip to content

Commit

Permalink
Merge pull request #80 from fsbolero/pagemodel
Browse files Browse the repository at this point in the history
[#79] Implement page models
  • Loading branch information
Tarmil authored Sep 22, 2019
2 parents 4947383 + c553c60 commit fd1564c
Show file tree
Hide file tree
Showing 7 changed files with 331 additions and 372 deletions.
6 changes: 3 additions & 3 deletions appveyor.yml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
version: build {build}

image:
- Visual Studio 2017
- Visual Studio 2019
# - Ubuntu

build:
Expand All @@ -13,7 +13,7 @@ branches:
skip_tags: true

init:
- cmd: choco install dotnetcore-sdk --version 3.0.100-preview9-014004
- cmd: choco install dotnetcore-sdk --version 3.0.100-rc1-014190
- git config --global core.autocrlf input

before_build:
Expand All @@ -28,7 +28,7 @@ build_script:
# Chrome on AV is not latest
- ps: |
dotnet tool install paket --tool-path .paket
.paket/paket add Selenium.WebDriver.ChromeDriver --version 75.0.3770.140
.paket/paket add Selenium.WebDriver.ChromeDriver --version 76.0.3809.12600
dotnet restore
- ps: '& $env:BUILD_SCRIPT -t pack -c Release -v "$env:SEMVER" --sourceLink /p:GhPages=true'

Expand Down
13 changes: 6 additions & 7 deletions paket.dependencies
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,12 @@ nuget FSharp.Core >= 4.5.0 lowest_matching: true
nuget HtmlAgilityPack >= 1.8.0 lowest_matching: true
nuget Elmish ~> 2.0 lowest_matching: true
nuget TaskBuilder.fs >= 2.1.0 lowest_matching: true
nuget Microsoft.AspNetCore.Blazor 3.0.0-preview9.19424.4
nuget Microsoft.AspNetCore.Blazor.Build 3.0.0-preview9.19424.4
nuget Microsoft.AspNetCore.Blazor.Server 3.0.0-preview9.19424.4
nuget Microsoft.AspNetCore.Blazor.DevServer 3.0.0-preview9.19424.4
nuget Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation 3.0.0-preview9.19424.4
nuget System.Runtime.CompilerServices.Unsafe 4.6.0-preview9.19421.4
nuget Microsoft.AspNetCore.Blazor 3.0.0-preview9.19457.4
nuget Microsoft.AspNetCore.Blazor.Build 3.0.0-preview9.19457.4
nuget Microsoft.AspNetCore.Blazor.Server 3.0.0-preview9.19457.4
nuget Microsoft.AspNetCore.Blazor.DevServer 3.0.0-preview9.19457.4
nuget Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation 3.0.0-rc1.19457.4
nuget System.Runtime.CompilerServices.Unsafe 4.6.0-rc1.19456.4

# Build and test references
nuget FsCheck.NUnit ~> 2.12
Expand All @@ -35,7 +35,6 @@ source https://api.nuget.org/v3/index.json
storage: none
framework: netstandard2.0

nuget FSharp.Core ~> 4.5.0
nuget Fake.Core.Target
nuget Fake.IO.FileSystem
nuget Fake.DotNet.AssemblyInfoFile
Expand Down
509 changes: 198 additions & 311 deletions paket.lock

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion src/Bolero/Components.fs
Original file line number Diff line number Diff line change
Expand Up @@ -167,7 +167,7 @@ type ProgramComponent<'model, 'msg>() =
initModel, []

override this.OnAfterRenderAsync(firstRender) =
if this.Router.IsSome && not navigationInterceptionEnabled && not firstRender then
if this.Router.IsSome && not navigationInterceptionEnabled then
navigationInterceptionEnabled <- true
this.NavigationInterception.EnableNavigationInterceptionAsync()
else
Expand Down
145 changes: 102 additions & 43 deletions src/Bolero/Router.fs
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,6 @@ namespace Bolero
open System
open System.Collections.Generic
open System.Runtime.CompilerServices
open System.Text
open FSharp.Reflection
open System.Runtime.InteropServices

Expand Down Expand Up @@ -96,6 +95,7 @@ type InvalidRouterKind =
| IdenticalPath of UnionCaseInfo * UnionCaseInfo
| RestNotLast of UnionCaseInfo
| InvalidRestType of UnionCaseInfo
| MultiplePageModels of UnionCaseInfo

exception InvalidRouter of kind: InvalidRouterKind with
override this.Message =
Expand Down Expand Up @@ -125,6 +125,11 @@ exception InvalidRouter of kind: InvalidRouterKind with
withCase case "{*rest} parameter must be the last fragment"
| InvalidRouterKind.InvalidRestType case ->
withCase case "{*rest} parameter must have type string, list or array"
| InvalidRouterKind.MultiplePageModels case ->
withCase case "multiple page models on the same case"

[<CLIMutable>]
type PageModel<'T> = { Model: 'T }

[<AutoOpen>]
module private RouterImpl =
Expand Down Expand Up @@ -282,6 +287,19 @@ module private RouterImpl =
name: string
}

/// Intermediate representation of a path segment.
type UnionParserSegment =
| Constant of string
| Parameter of Parameter

type UnionCase =
{
info: UnionCaseInfo
ctor: obj[] -> obj
argCount: int
segments: UnionParserSegment list
}

/// The parser for a union type at a given point in the path.
type UnionParser =
{
Expand All @@ -290,14 +308,9 @@ module private RouterImpl =
/// The recognized "/{parameter}" segment, if any.
parameter: option<Parameter * UnionParser>
/// The union case that parses correctly if the path ends here, if any.
finalize: option<UnionCaseInfo * (obj[] -> obj)>
finalize: option<UnionCase>
}

/// Intermediate representation of a path segment.
type UnionParserSegment =
| Constant of string
| Parameter of Parameter

let parseEndPointCasePath (case: UnionCaseInfo) : list<string> =
case.GetCustomAttributes()
|> Array.tryPick (function
Expand Down Expand Up @@ -352,28 +365,64 @@ module private RouterImpl =

let fragmentParameterRE = Regex(@"^\{([?*]?)([a-zA-Z0-9_]+)\}$", RegexOptions.Compiled)

let parseEndPointCase getSegment (case: UnionCaseInfo) =
let isPageModel (ty: Type) =
ty.IsGenericType && ty.GetGenericTypeDefinition() = typedefof<PageModel<_>>

let findPageModel (case: UnionCaseInfo) =
((0, None), case.GetFields())
||> Array.fold (fun (i, found) field ->
i + 1,
if isPageModel field.PropertyType then
match found with
| None -> Some (i, field.PropertyType)
| Some _ -> fail (InvalidRouterKind.MultiplePageModels case)
else
found)
|> snd

let getCtor (defaultPageModel: obj -> unit) (case: UnionCaseInfo) =
let ctor = FSharpValue.PreComputeUnionConstructor(case, true)
match findPageModel case with
| None -> ctor
| Some (i, ty) ->
let dummyArgs = Array.zeroCreate (case.GetFields().Length)
let model = FSharpValue.MakeRecord(ty, [|null|])
dummyArgs.[i] <- model
let dummy = ctor dummyArgs
defaultPageModel dummy
fun vals ->
vals.[i] <- model
ctor vals

let parseEndPointCase getSegment (defaultPageModel: obj -> unit) (case: UnionCaseInfo) =
let ctor = getCtor defaultPageModel case
let fields = case.GetFields()
let defaultFrags() =
fields
|> Array.mapi (fun i p ->
let ty = p.PropertyType
Parameter {
if isPageModel ty then None else
Some <| Parameter {
index = [case, fields.Length, i]
``type`` = ty
segment = getSegment ty
modifier = Basic
name = p.Name
})
|> Array.choose id
|> List.ofSeq
match parseEndPointCasePath case with
// EndPoint "/"
| [] -> defaultFrags()
| [] -> { info = case; ctor = ctor; argCount = fields.Length; segments = defaultFrags() }
// EndPoint "/const"
| [root] when isConstantFragment root -> Constant root :: defaultFrags()
| [root] when isConstantFragment root ->
{ info = case; ctor = ctor; argCount = fields.Length; segments = Constant root :: defaultFrags() }
// EndPoint <complex_path>
| frags ->
let unboundFields = HashSet(fields |> Array.map (fun f -> f.Name))
let unboundFields =
fields
|> Array.choose (fun f -> if isPageModel f.PropertyType then None else Some f.Name)
|> HashSet
let fragCount = frags.Length
let res =
frags
Expand Down Expand Up @@ -409,38 +458,35 @@ module private RouterImpl =
)
if unboundFields.Count > 0 then
fail (InvalidRouterKind.MissingField(case, Seq.head unboundFields))
res
{ info = case; ctor = ctor; argCount = fields.Length; segments = res }

let caseCtor (case: UnionCaseInfo) : UnionCaseInfo * (obj[] -> obj) =
case, FSharpValue.PreComputeUnionConstructor(case, true)

let rec mergeEndPointCaseFragments (cases: seq<UnionCaseInfo * list<UnionParserSegment>>) : UnionParser =
let rec mergeEndPointCaseFragments (cases: seq<UnionCase>) : UnionParser =
let constants = Dictionary<string, _>()
let mutable parameter = None
let mutable final = None
cases |> Seq.iter (fun (case, p) ->
match p with
cases |> Seq.iter (fun case ->
match case.segments with
| Constant s :: rest ->
let existing =
match constants.TryGetValue(s) with
| true, x -> x
| false, _ -> []
constants.[s] <- (case, rest) :: existing
constants.[s] <- { case with segments = rest } :: existing
| Parameter param :: rest ->
match parameter with
| Some (case', param', ps) ->
if param.``type`` <> param'.``type`` then
fail (InvalidRouterKind.ParameterTypeMismatch(case', param'.name, case, param.name))
fail (InvalidRouterKind.ParameterTypeMismatch(case', param'.name, case.info, param.name))
if param.modifier <> param'.modifier then
fail (InvalidRouterKind.ModifierMismatch(case', param'.name, case, param.name))
fail (InvalidRouterKind.ModifierMismatch(case', param'.name, case.info, param.name))
let param = { param with index = param.index @ param'.index }
parameter <- Some (case', param, (case, rest) :: ps)
parameter <- Some (case', param, { case with segments = rest } :: ps)
| None ->
parameter <- Some (case, param, [case, rest])
parameter <- Some (case.info, param, [{ case with segments = rest }])
| [] ->
match final with
| Some (case', _) -> fail (InvalidRouterKind.IdenticalPath(case, case'))
| None -> final <- Some (case, caseCtor case)
| Some case' -> fail (InvalidRouterKind.IdenticalPath(case.info, case'.info))
| None -> final <- Some case
)
{
constants = dict [
Expand All @@ -449,7 +495,7 @@ module private RouterImpl =
]
parameter = parameter |> Option.map (fun (_, param, cases) ->
param, mergeEndPointCaseFragments cases)
finalize = final |> Option.map snd
finalize = final
}

let parseUnion cases : SegmentParser =
Expand All @@ -458,12 +504,12 @@ module private RouterImpl =
let d = Dictionary<UnionCaseInfo, obj[]>()
let rec run (parser: UnionParser) l =
let finalize rest =
parser.finalize |> Option.map (fun (case, ctor) ->
parser.finalize |> Option.map (fun case ->
let args =
match d.TryGetValue(case) with
match d.TryGetValue(case.info) with
| true, args -> args
| false, _ -> [||]
(ctor args, rest))
| false, _ -> Array.zeroCreate case.argCount
(case.ctor args, rest))
let mutable constant = Unchecked.defaultof<_>
match l with
| s :: rest when parser.constants.TryGetValue(s, &constant) ->
Expand Down Expand Up @@ -533,24 +579,24 @@ module private RouterImpl =
let caseDector (case: UnionCaseInfo) : obj -> obj[] =
FSharpValue.PreComputeUnionReader(case, true)

let writeUnionCase (case: UnionCaseInfo, path: list<UnionParserSegment>) =
let dector = caseDector case
let writeUnionCase (case: UnionCase) =
let dector = caseDector case.info
fun o ->
let vals = dector o
path |> List.collect (function
case.segments |> List.collect (function
| Constant s -> [s]
| Parameter({ modifier = Basic } as param) ->
let (_, _, i) = param.index |> List.find (fun (case', _, _) -> case' = case)
let (_, _, i) = param.index |> List.find (fun (case', _, _) -> case' = case.info)
param.segment.write vals.[i]
| Parameter({ modifier = Rest(_, decons) } as param) ->
let (_, _, i) = param.index |> List.find (fun (case', _, _) -> case' = case)
let (_, _, i) = param.index |> List.find (fun (case', _, _) -> case' = case.info)
[ for x in decons vals.[i] do yield! param.segment.write x ]
)

let unionSegment (getSegment: Type -> Segment) (ty: Type) : Segment =
let unionSegment (getSegment: Type -> Segment) (defaultPageModel: obj -> unit) (ty: Type) : Segment =
let cases =
FSharpType.GetUnionCases(ty, true)
|> Array.map (fun c -> c, parseEndPointCase getSegment c)
|> Array.map (parseEndPointCase getSegment defaultPageModel)
let write =
let writers = Array.map writeUnionCase cases
let tagReader = FSharpValue.PreComputeUnionTagReader(ty, true)
Expand All @@ -576,7 +622,7 @@ module private RouterImpl =
write = writeConsecutiveTypes getSegment tys dector
}

let rec getSegment (cache: Dictionary<Type, Segment>) (ty: Type) : Segment =
let rec getSegment (cache: Dictionary<Type, Segment>) (defaultPageModel: obj -> unit) (ty: Type) : Segment =
match cache.TryGetValue(ty) with
| true, x -> unbox x
| false, _ ->
Expand All @@ -586,14 +632,14 @@ module private RouterImpl =
write = fun x -> (!segment).write x
}
cache.[ty] <- !segment
let getSegment = getSegment cache
let getSegment = getSegment cache ignore
segment :=
if ty.IsArray && ty.GetArrayRank() = 1 then
arraySegment getSegment (ty.GetElementType())
elif ty.IsGenericType && ty.GetGenericTypeDefinition() = typedefof<list<_>> then
listSegment getSegment (ty.GetGenericArguments().[0])
elif FSharpType.IsUnion(ty, true) then
unionSegment getSegment ty
unionSegment getSegment defaultPageModel ty
elif FSharpType.IsTuple(ty) then
tupleSegment getSegment ty
elif FSharpType.IsRecord(ty, true) then
Expand All @@ -609,11 +655,13 @@ module Router =
/// Infer a router constructed around an endpoint type `'ep`.
/// This type must be an F# union type, and its cases should use `EndPointAttribute`
/// to declare how they match to a URI.
let infer<'ep, 'model, 'msg> (makeMessage: 'ep -> 'msg) (getEndPoint: 'model -> 'ep) =
/// Inside `defaultPageModel`, call `Router.definePageModel` to indicate the page model to use
/// when switching to a new page.
let inferWithModel<'ep, 'model, 'msg> (makeMessage: 'ep -> 'msg) (getEndPoint: 'model -> 'ep) (defaultPageModel: 'ep -> unit) =
let ty = typeof<'ep>
let cache = Dictionary()
for KeyValue(k, v) in baseTypes do cache.Add(k, v)
let frag = getSegment cache ty
let frag = getSegment cache (unbox >> defaultPageModel) ty
{
getEndPoint = getEndPoint
getRoute = fun ep ->
Expand All @@ -629,6 +677,17 @@ module Router =
| _ -> None)
}

/// Infer a router constructed around an endpoint type `'ep`.
/// This type must be an F# union type, and its cases should use `EndPointAttribute`
/// to declare how they match to a URI.
let infer<'ep, 'model, 'msg> (makeMessage: 'ep -> 'msg) (getEndPoint: 'model -> 'ep) =
inferWithModel makeMessage getEndPoint ignore

let noModel<'T> = { Model = Unchecked.defaultof<'T> }

let definePageModel (pageModel: PageModel<'T>) (value: 'T) =
pageModel.GetType().GetProperty("Model").SetValue(pageModel, value)

[<Extension>]
type RouterExtensions =

Expand Down
Loading

0 comments on commit fd1564c

Please sign in to comment.