Skip to content

Commit

Permalink
Add DotLiquid integration
Browse files Browse the repository at this point in the history
  • Loading branch information
tpetricek committed Jun 27, 2015
1 parent ca8a9a8 commit 69f9519
Show file tree
Hide file tree
Showing 6 changed files with 293 additions and 50 deletions.
3 changes: 2 additions & 1 deletion paket.dependencies
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,5 @@ nuget FsPickler
nuget Fuchu-suave.FsCheck
nuget Fuchu-suave.PerfUtil ~> 0.6
nuget WebSocketSharp
nuget openssl.redist
nuget openssl.redist
nuget DotLiquid
1 change: 1 addition & 0 deletions paket.lock
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
NUGET
remote: https://nuget.org/api/v2
specs:
DotLiquid (1.8.0)
FsCheck (2.0.1)
FSharp.Core (>= 3.1.2.1)
FSharp.Charting (0.90.10)
Expand Down
138 changes: 138 additions & 0 deletions src/Suave.DotLiquid/Library.fs
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)
94 changes: 94 additions & 0 deletions src/Suave.DotLiquid/Suave.DotLiquid.fsproj
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>
1 change: 1 addition & 0 deletions src/Suave.DotLiquid/paket.references
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
DotLiquid
Loading

0 comments on commit 69f9519

Please sign in to comment.