Skip to content

Commit

Permalink
implement projection expressions; minor bugfixes and api improvements
Browse files Browse the repository at this point in the history
  • Loading branch information
eiriktsarpalis committed Mar 21, 2016
1 parent 56b696b commit 45b02fd
Show file tree
Hide file tree
Showing 11 changed files with 778 additions and 152 deletions.
23 changes: 22 additions & 1 deletion src/FSharp.AWS.DynamoDB/Expression/ExpressionContainers.fs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ open Microsoft.FSharp.Quotations
open FSharp.AWS.DynamoDB.ExprCommon
open FSharp.AWS.DynamoDB.ConditionalExpr
open FSharp.AWS.DynamoDB.UpdateExpr
open FSharp.AWS.DynamoDB.ProjectionExpr

//
// Public converted condition expression wrapper implementations
Expand Down Expand Up @@ -114,4 +115,24 @@ and UpdateExpression =
let msg = sprintf "found conflicting paths '%s' and '%s' being accessed in update expression." p1 p2
invalidArg "expr" msg

new UpdateExpression<'TRecord>({ UpdateOps = uops ; NParams = 0 })
new UpdateExpression<'TRecord>({ UpdateOps = uops ; NParams = 0 })

/// Represents a projection expression for a given record type
[<Sealed; AutoSerializable(false)>]
type ProjectionExpression<'TRecord, 'TProjection> internal (expr : ProjectionExpr) =
let data = lazy(expr.GetDebugData())
/// Internal projection expression object
member internal __.ProjectionExpr = expr
/// DynamoDB projection expression string
member __.Expression = let expr,_ = data.Value in expr
/// DynamoDB attribute names
member __.Names = let _,names = data.Value in names

member internal __.UnPickle(ro : RestObject) = expr.Ctor ro :?> 'TProjection

override __.Equals(other : obj) =
match other with
| :? ProjectionExpression<'TRecord, 'TProjection> as other -> expr.Attributes = other.ProjectionExpr.Attributes
| _ -> false

override __.GetHashCode() = hash expr.Attributes
103 changes: 103 additions & 0 deletions src/FSharp.AWS.DynamoDB/Expression/ProjectionExpr.fs
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
module internal FSharp.AWS.DynamoDB.ProjectionExpr

open System
open System.Collections.Generic
open System.Reflection

open Microsoft.FSharp.Reflection
open Microsoft.FSharp.Quotations
open Microsoft.FSharp.Quotations.Patterns
open Microsoft.FSharp.Quotations.DerivedPatterns
open Microsoft.FSharp.Quotations.ExprShape

open Amazon.DynamoDBv2
open Amazon.DynamoDBv2.Model

open FSharp.AWS.DynamoDB.ExprCommon

///////////////////////////////
//
// Extracts projection expressions from an F# quotation of the form
// <@ fun record -> record.A, record.B, record.B.[0].C @>
//
// c.f. http://docs.aws.amazon.com/amazondynamodb/latest/developerguide/Expressions.AccessingItemAttributes.html
//
///////////////////////////////

type ProjectionExpr =
{
Attributes : AttributeId []
Ctor : Dictionary<string, AttributeValue> -> obj
}

with

static member Extract (recordInfo : RecordInfo) (expr : Expr<'TRecord -> 'Tuple>) =
let invalidExpr () = invalidArg "expr" "supplied expression is not a valid projection."
match expr with
| Lambda(r, body) when r.Type = recordInfo.Type ->
let (|AttributeGet|_|) expr = QuotedAttribute.TryExtract (fun _ -> None) r recordInfo expr

match body with
| AttributeGet qa ->
let pickler = qa.Pickler
let attrId = qa.RootProperty.Name
let attr = qa.Id
let ctor (ro : RestObject) =
let ok, av = ro.TryGetValue attrId
if ok then pickler.UnPickleUntyped av
else pickler.DefaultValueUntyped

{ Attributes = [|attr|] ; Ctor = ctor }

| NewTuple values ->
let qas =
values
|> Seq.map (function AttributeGet qa -> qa | _ -> invalidExpr ())
|> Seq.toArray

// check for conflicting projection attributes
qas
|> Seq.groupBy (fun qa -> qa.RootProperty.AttrId)
|> Seq.filter (fun (_,rps) -> Seq.length rps > 1)
|> Seq.iter (fun (attr,_) ->
sprintf "Projection expression accessing conflicting property '%s'." attr
|> invalidArg "expr")

let attrs = qas |> Array.map (fun qa -> qa.Id)
let picklers = qas |> Array.map (fun attr -> attr.Pickler)
let attrIds = qas |> Array.map (fun attr -> attr.RootProperty.Name)
let tupleCtor = FSharpValue.PreComputeTupleConstructor typeof<'Tuple>

let ctor (ro : RestObject) =
let values = Array.zeroCreate<obj> picklers.Length
for i = 0 to picklers.Length - 1 do
let id = attrIds.[i]
let ok, av = ro.TryGetValue (attrIds.[i])
if ok then values.[i] <- picklers.[i].UnPickleUntyped av
else values.[i] <- picklers.[i].DefaultValueUntyped

tupleCtor values

{ Attributes = attrs ; Ctor = ctor }

| _ -> invalidArg "expr" "projection type must either be a single property, or tuple of properties."
| _ -> invalidExpr ()

member __.Write (writer : AttributeWriter) =
let sb = new System.Text.StringBuilder()
let inline (!) (x:string) = sb.Append x |> ignore
let mutable isFirst = true
for attr in __.Attributes do
if isFirst then isFirst <- false
else ! ", "

! (writer.WriteAttibute attr)

sb.ToString()

member __.GetDebugData() =
let aw = new AttributeWriter()
let expr = __.Write (aw)
let names = aw.Names |> Seq.map (fun kv -> kv.Key, kv.Value) |> Seq.toList
expr, names
31 changes: 31 additions & 0 deletions src/FSharp.AWS.DynamoDB/Extensions.fs
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
namespace FSharp.AWS.DynamoDB

open Microsoft.FSharp.Quotations

/// Collection of extensions for the public API
[<AutoOpen>]
module Extensions =

/// Precomputes a template expression
let inline template<'TRecord> = RecordTemplate.Define<'TRecord>()

/// A conditional which verifies that given item exists
let inline itemExists<'TRecord> = template<'TRecord>.ItemExists
/// A conditional which verifies that given item does not exist
let inline itemDoesNotExist<'TRecord> = template<'TRecord>.ItemDoesNotExist

/// Precomputes a conditional expression
let inline cond (expr : Expr<'TRecord -> bool>) : ConditionExpression<'TRecord> =
template<'TRecord>.PrecomputeConditionalExpr expr

/// Precomputes an update expression
let inline update (expr : Expr<'TRecord -> 'TRecord>) : UpdateExpression<'TRecord> =
template<'TRecord>.PrecomputeUpdateExpr expr

/// Precomputes an update operation expression
let inline updateOp (expr : Expr<'TRecord -> UpdateOp>) : UpdateExpression<'TRecord> =
template<'TRecord>.PrecomputeUpdateExpr expr

/// Precomputes a projection expression
let inline proj (expr : Expr<'TRecord -> 'TProjection>) : ProjectionExpression<'TRecord, 'TProjection> =
template<'TRecord>.PrecomputeProjectionExpr<'TProjection> expr
4 changes: 3 additions & 1 deletion src/FSharp.AWS.DynamoDB/FSharp.AWS.DynamoDB.fsproj
Original file line number Diff line number Diff line change
Expand Up @@ -59,10 +59,12 @@
<Compile Include="Expression\ExprCommon.fs" />
<Compile Include="Expression\ConditionalExpr.fs" />
<Compile Include="Expression\UpdateExpr.fs" />
<Compile Include="Expression\ProjectionExpr.fs" />
<Compile Include="Expression\ExpressionContainers.fs" />
<Compile Include="KeySchema.fs" />
<Compile Include="RecordTemplate.fs" />
<Compile Include="TableContext.fs" />
<Compile Include="Extensions.fs" />
<None Include="Script.fsx" />
<None Include="paket.references" />
<None Include="paket.template" />
Expand Down Expand Up @@ -751,4 +753,4 @@
</ItemGroup>
</When>
</Choose>
</Project>
</Project>
13 changes: 6 additions & 7 deletions src/FSharp.AWS.DynamoDB/Picklers/RecordPickler.fs
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ type AttributeType =
type RecordInfo =
{
Type : Type
ConstructorInfo : ConstructorInfo
Constructor : obj[] -> obj
Properties : RecordPropertyInfo []
}
with
Expand Down Expand Up @@ -96,10 +96,10 @@ with
and IRecordPickler =
abstract RecordInfo : RecordInfo

type RecordPickler<'T>(ctorInfo : ConstructorInfo, properties : RecordPropertyInfo []) =
type RecordPickler<'T>(ctor : obj[] -> obj, properties : RecordPropertyInfo []) =
inherit Pickler<'T> ()

let recordInfo = { Type = typeof<'T> ; Properties = properties ; ConstructorInfo = ctorInfo }
let recordInfo = { Type = typeof<'T> ; Properties = properties ; Constructor = ctor }

member __.RecordInfo = recordInfo
member __.OfRecord (value : 'T) : RestObject =
Expand All @@ -122,7 +122,7 @@ type RecordPickler<'T>(ctorInfo : ConstructorInfo, properties : RecordPropertyIn
elif prop.NoDefaultValue then notFound()
else values.[i] <- prop.Pickler.DefaultValueUntyped

ctorInfo.Invoke values :?> 'T
ctor values :?> 'T

interface IRecordPickler with
member __.RecordInfo = recordInfo
Expand All @@ -143,12 +143,11 @@ type RecordPickler<'T>(ctorInfo : ConstructorInfo, properties : RecordPropertyIn


let mkTuplePickler<'T> (resolver : IPicklerResolver) =
let ctor, rest = FSharpValue.PreComputeTupleConstructorInfo(typeof<'T>)
if Option.isSome rest then invalidArg (string typeof<'T>) "Tuples of arity > 7 not supported"
let ctor = FSharpValue.PreComputeTupleConstructor typeof<'T>
let properties = typeof<'T>.GetProperties() |> Array.mapi (RecordPropertyInfo.FromPropertyInfo resolver)
new RecordPickler<'T>(ctor, properties)

let mkFSharpRecordPickler<'T> (resolver : IPicklerResolver) =
let ctor = FSharpValue.PreComputeRecordConstructorInfo(typeof<'T>, true)
let ctor = FSharpValue.PreComputeRecordConstructor(typeof<'T>, true)
let properties = FSharpType.GetRecordFields(typeof<'T>, true) |> Array.mapi (RecordPropertyInfo.FromPropertyInfo resolver)
new RecordPickler<'T>(ctor, properties)
10 changes: 10 additions & 0 deletions src/FSharp.AWS.DynamoDB/RecordTemplate.fs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ open Amazon.DynamoDBv2.Model
open FSharp.AWS.DynamoDB.KeySchema
open FSharp.AWS.DynamoDB.ConditionalExpr
open FSharp.AWS.DynamoDB.UpdateExpr
open FSharp.AWS.DynamoDB.ProjectionExpr

/// DynamoDB table template defined by provided F# record type
[<Sealed; AutoSerializable(false)>]
Expand Down Expand Up @@ -222,6 +223,15 @@ type RecordTemplate<'TRecord> internal () =
let uops = UpdateOperations.ExtractOpExpr pickler.RecordInfo expr
fun i1 i2 i3 i4 i5 -> new UpdateExpression<'TRecord>(uops.Apply(i1, i2, i3, i4, i5))

/// <summary>
/// Precomputes a DynamoDB projection expression using
/// supplied quoted projection expression.
/// </summary>
/// <param name="expr">Quoted record projection expression.</param>
member __.PrecomputeProjectionExpr(expr : Expr<'TRecord -> 'TProjection>) : ProjectionExpression<'TRecord, 'TProjection> =
let pexpr = ProjectionExpr.Extract pickler.RecordInfo expr
new ProjectionExpression<'TRecord, 'TProjection>(pexpr)

/// Convert table key to attribute values
member internal __.ToAttributeValues(key : TableKey) =
KeyStructure.ExtractKey(keyStructure, key)
Expand Down
Loading

0 comments on commit 45b02fd

Please sign in to comment.