forked from SuaveIO/suave
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
6 changed files
with
293 additions
and
50 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,138 @@ | ||
module Suave.DotLiquid | ||
|
||
open System | ||
open System.IO | ||
open DotLiquid | ||
open Microsoft.FSharp.Reflection | ||
open Suave.Utils | ||
open Suave.Types | ||
open Suave.Http | ||
open Suave.Http.Files | ||
|
||
// ------------------------------------------------------------------------------------------------- | ||
// Registering things with DotLiquid | ||
// ------------------------------------------------------------------------------------------------- | ||
|
||
/// Represents a local file system relative to the specified 'root' | ||
let private localFileSystem root = | ||
{ new DotLiquid.FileSystems.IFileSystem with | ||
member this.ReadTemplateFile(context, templateName) = | ||
let templatePath = context.[templateName] :?> string | ||
let fullPath = Path.Combine(root, templatePath) | ||
if not (File.Exists(fullPath)) then failwithf "File not found: %s" fullPath | ||
File.ReadAllText(fullPath) } | ||
|
||
/// Protects accesses to various DotLiquid internal things | ||
let private safe = | ||
let o = obj() | ||
fun f -> lock o f | ||
|
||
/// Given a type which is an F# record containing seq<_>, list<_> and other | ||
/// records, register the type with DotLiquid so that its fields are accessible | ||
let private registerTypeTree = | ||
let registered = System.Collections.Generic.Dictionary<_, _>() | ||
let rec loop ty = | ||
if not (registered.ContainsKey ty) then | ||
if FSharpType.IsRecord ty then | ||
let fields = FSharpType.GetRecordFields(ty) | ||
Template.RegisterSafeType(ty, [| for f in fields -> f.Name |]) | ||
for f in fields do loop f.PropertyType | ||
elif ty.IsGenericType && | ||
( let t = ty.GetGenericTypeDefinition() | ||
in t = typedefof<seq<_>> || t = typedefof<list<_>> ) then | ||
loop (ty.GetGenericArguments().[0]) | ||
registered.[ty] <- true | ||
fun ty -> safe (fun () -> loop ty) | ||
|
||
/// Use the ruby naming convention by default | ||
do Template.NamingConvention <- DotLiquid.NamingConventions.RubyNamingConvention() | ||
|
||
// ------------------------------------------------------------------------------------------------- | ||
// Parsing and loading DotLiquid templates and caching the results | ||
// ------------------------------------------------------------------------------------------------- | ||
|
||
/// Memoize asynchronous function. An item is recomputed when `isValid` returns `false` | ||
let private asyncMemoize isValid f = | ||
let cache = Collections.Concurrent.ConcurrentDictionary<_ , _>() | ||
fun x -> async { | ||
match cache.TryGetValue(x) with | ||
| true, res when isValid x res -> return res | ||
| _ -> | ||
let! res = f x | ||
cache.[x] <- res | ||
return res } | ||
|
||
/// Parse the specified template & register the type that we want to use as "model" | ||
let private parseTemplate template ty = | ||
registerTypeTree ty | ||
let t = Template.Parse(template) | ||
fun k v -> t.Render(Hash.FromDictionary(dict [k, v])) | ||
|
||
/// Asynchronously loads a template & remembers the last write time | ||
/// (so that we can automatically reload the template when file changes) | ||
let private loadTemplate (ty, fileName) = async { | ||
let writeTime = File.GetLastWriteTime(fileName) | ||
use file = new FileStream(fileName, FileMode.Open, FileAccess.Read, FileShare.ReadWrite) | ||
use reader = new StreamReader(file) | ||
let! razorTemplate = reader.ReadToEndAsync() |> Async.AwaitTask | ||
return writeTime, parseTemplate razorTemplate ty } | ||
|
||
/// Load template & memoize & automatically reload when the file changes | ||
let private loadTemplateCached = | ||
loadTemplate |> asyncMemoize (fun (_, templatePath) (lastWrite, _) -> | ||
File.GetLastWriteTime(templatePath) <= lastWrite ) | ||
|
||
// ------------------------------------------------------------------------------------------------- | ||
// Public API | ||
// ------------------------------------------------------------------------------------------------- | ||
|
||
let mutable private templatesDir = None | ||
|
||
/// Set the root directory where DotLiquid is looking for templates. For example, you can | ||
/// write something like this: | ||
/// | ||
/// DotLiquid.setTemplatesDir (__SOURCE_DIRECTORY__ + "/templates") | ||
/// | ||
/// The current directory is a global variable and so it should not change between | ||
/// multiple HTTP requests. This is a DotLiquid limitation. | ||
let setTemplatesDir dir = | ||
if templatesDir <> Some dir then | ||
templatesDir <- Some dir | ||
safe (fun () -> Template.FileSystem <- localFileSystem dir) | ||
|
||
/// Render a page using DotLiquid template. Takes a path (relative to the directory specified | ||
/// using `setTemplatesDir` and a value that is exposed as the "model" variable. You can use | ||
/// any F# record type, seq<_> and list<_> without having to explicitly register the fields. | ||
/// | ||
/// type Page = { Total : int } | ||
/// let app = page "index.html" { Total = 42 } | ||
/// | ||
let page<'T> path (model : 'T) r = async { | ||
let path = | ||
match templatesDir with | ||
| None -> resolvePath r.runtime.homeDirectory path | ||
| Some root -> Path.Combine(root, path) | ||
let! writeTime, renderer = loadTemplateCached (typeof<'T>, path) | ||
let content = renderer "model" (box model) | ||
return! Response.response HTTP_200 (UTF8.bytes content) r } | ||
|
||
/// Register functions from a module as filters available in DotLiquid templates. | ||
/// For example, the following snippet lets you write `{{ model.Total | nuce_num }}`: | ||
/// | ||
/// module MyFilters = | ||
/// let niceNum i = if i > 10 then "lot" else "not much" | ||
/// | ||
/// do registerFiltersByName "MyFilters" | ||
/// | ||
let registerFiltersByName name = | ||
let asm = System.Reflection.Assembly.GetExecutingAssembly() | ||
let typ = | ||
[ for t in asm.GetTypes() do | ||
if t.FullName.EndsWith(name) && not(t.FullName.Contains("<StartupCode")) then yield t ] | ||
|> Seq.last | ||
Template.RegisterFilter(typ) | ||
|
||
/// Similar to `registerFiltersByName`, but the module is speicfied by its `System.Type` | ||
/// (This is more cumbersome, but safer alternative.) | ||
let registerFiltersByType typ = | ||
Template.RegisterFilter(typ) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,94 @@ | ||
<?xml version="1.0" encoding="utf-8"?> | ||
<Project ToolsVersion="4.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> | ||
<PropertyGroup> | ||
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration> | ||
<Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform> | ||
<ProjectGuid>{4e3fe86d-fdb7-4daa-8d58-13f06f75d240}</ProjectGuid> | ||
<OutputType>Library</OutputType> | ||
<RootNamespace>Suave.Razor</RootNamespace> | ||
<AssemblyName>Suave.Razor</AssemblyName> | ||
<TargetFrameworkVersion>v4.5</TargetFrameworkVersion> | ||
<Name>Suave.Razor</Name> | ||
</PropertyGroup> | ||
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' "> | ||
<DebugSymbols>true</DebugSymbols> | ||
<Optimize>false</Optimize> | ||
<Tailcalls>false</Tailcalls> | ||
<OutputPath>bin\Debug\</OutputPath> | ||
<DefineConstants>DEBUG;TRACE</DefineConstants> | ||
<WarningLevel>3</WarningLevel> | ||
<DocumentationFile>bin\Debug\Suave.Razor.xml</DocumentationFile> | ||
</PropertyGroup> | ||
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' "> | ||
<DebugType>pdbonly</DebugType> | ||
<Optimize>true</Optimize> | ||
<Tailcalls>true</Tailcalls> | ||
<OutputPath>bin\Release\</OutputPath> | ||
<DefineConstants>TRACE</DefineConstants> | ||
<WarningLevel>3</WarningLevel> | ||
<DocumentationFile>bin\Release\Suave.Razor.xml</DocumentationFile> | ||
<DebugSymbols>true</DebugSymbols> | ||
</PropertyGroup> | ||
<Choose> | ||
<When Condition="'$(VisualStudioVersion)' == '11.0'"> | ||
<PropertyGroup> | ||
<FSharpTargetsPath>$(MSBuildExtensionsPath32)\..\Microsoft SDKs\F#\3.0\Framework\v4.0\Microsoft.FSharp.Targets</FSharpTargetsPath> | ||
</PropertyGroup> | ||
</When> | ||
<Otherwise> | ||
<PropertyGroup> | ||
<FSharpTargetsPath>$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)\FSharp\Microsoft.FSharp.Targets</FSharpTargetsPath> | ||
</PropertyGroup> | ||
</Otherwise> | ||
</Choose> | ||
<Import Project="$(FSharpTargetsPath)" Condition="Exists('$(FSharpTargetsPath)')" /> | ||
<ItemGroup> | ||
<ProjectReference Include="..\Suave\Suave.fsproj"> | ||
<Project>{3DC9193E-BD0C-4486-9C58-56B630C36623}</Project> | ||
<Name>Suave</Name> | ||
</ProjectReference> | ||
<Reference Include="mscorlib" /> | ||
<Reference Include="System" /> | ||
<Reference Include="System.Core" /> | ||
<Reference Include="System.Numerics" /> | ||
<Reference Include="FSharp.Core"> | ||
<HintPath>..\..\packages\FSharp.Core\lib\net40\FSharp.Core.dll</HintPath> | ||
</Reference> | ||
</ItemGroup> | ||
<ItemGroup> | ||
<Compile Include="Library.fs" /> | ||
<None Include="paket.references" /> | ||
</ItemGroup> | ||
<PropertyGroup> | ||
<MinimumVisualStudioVersion Condition="'$(MinimumVisualStudioVersion)' == ''">11</MinimumVisualStudioVersion> | ||
</PropertyGroup> | ||
<Choose> | ||
<When Condition="$(TargetFrameworkIdentifier) == '.NETFramework' And $(TargetFrameworkVersion) == 'v3.5'"> | ||
<ItemGroup> | ||
<Reference Include="DotLiquid"> | ||
<HintPath>..\..\packages\DotLiquid\lib\NET35\DotLiquid.dll</HintPath> | ||
<Private>True</Private> | ||
<Paket>True</Paket> | ||
</Reference> | ||
</ItemGroup> | ||
</When> | ||
<When Condition="$(TargetFrameworkIdentifier) == '.NETFramework' And ($(TargetFrameworkVersion) == 'v4.0')"> | ||
<ItemGroup> | ||
<Reference Include="DotLiquid"> | ||
<HintPath>..\..\packages\DotLiquid\lib\NET40\DotLiquid.dll</HintPath> | ||
<Private>True</Private> | ||
<Paket>True</Paket> | ||
</Reference> | ||
</ItemGroup> | ||
</When> | ||
<When Condition="($(TargetFrameworkIdentifier) == '.NETFramework' And ($(TargetFrameworkVersion) == 'v4.5' Or $(TargetFrameworkVersion) == 'v4.5.1' Or $(TargetFrameworkVersion) == 'v4.5.2' Or $(TargetFrameworkVersion) == 'v4.5.3' Or $(TargetFrameworkVersion) == 'v4.6')) Or ($(TargetFrameworkIdentifier) == 'MonoAndroid') Or ($(TargetFrameworkIdentifier) == 'MonoTouch')"> | ||
<ItemGroup> | ||
<Reference Include="DotLiquid"> | ||
<HintPath>..\..\packages\DotLiquid\lib\NET45\DotLiquid.dll</HintPath> | ||
<Private>True</Private> | ||
<Paket>True</Paket> | ||
</Reference> | ||
</ItemGroup> | ||
</When> | ||
</Choose> | ||
</Project> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
DotLiquid |
Oops, something went wrong.