Skip to content

Commit

Permalink
Initial dotnet client, proxy creation complete, field invocation is s…
Browse files Browse the repository at this point in the history
…till WIP
  • Loading branch information
Zaid-Ajaj committed May 2, 2018
1 parent d4a7905 commit 4e63294
Show file tree
Hide file tree
Showing 16 changed files with 340 additions and 154 deletions.
13 changes: 7 additions & 6 deletions .paket/Paket.Restore.targets
Original file line number Diff line number Diff line change
Expand Up @@ -48,16 48,16 @@

<!-- Because ReadAllText is slow on osx/linux, try to find shasum and awk -->
<PropertyGroup>
<PaketRestoreCachedHasher Condition="'$(OS)' != 'Windows_NT' And '$(PaketRestoreCachedHasher)' == '' And Exists('/usr/bin/shasum') And Exists('/usr/bin/awk')">/usr/bin/shasum $(PaketRestoreCacheFile) | /usr/bin/awk '{ print $1 }'</PaketRestoreCachedHasher>
<PaketRestoreLockFileHasher Condition="'$(OS)' != 'Windows_NT' And '$(PaketRestoreLockFileHash)' == '' And Exists('/usr/bin/shasum') And Exists('/usr/bin/awk')">/usr/bin/shasum $(PaketLockFilePath) | /usr/bin/awk '{ print $1 }'</PaketRestoreLockFileHasher>
<PaketRestoreCachedHasher Condition="'$(OS)' != 'Windows_NT' And '$(PaketRestoreCachedHasher)' == '' And Exists('/usr/bin/shasum') And Exists('/usr/bin/awk')">/usr/bin/shasum "$(PaketRestoreCacheFile)" | /usr/bin/awk '{ print $1 }'</PaketRestoreCachedHasher>
<PaketRestoreLockFileHasher Condition="'$(OS)' != 'Windows_NT' And '$(PaketRestoreLockFileHash)' == '' And Exists('/usr/bin/shasum') And Exists('/usr/bin/awk')">/usr/bin/shasum "$(PaketLockFilePath)" | /usr/bin/awk '{ print $1 }'</PaketRestoreLockFileHasher>
</PropertyGroup>

<!-- If shasum and awk exist get the hashes -->
<Exec StandardOutputImportance="Low" Condition=" '$(PaketRestoreCachedHasher)' != '' " Command="$(PaketRestoreCachedHasher)" ConsoleToMSBuild='true'>
<Output TaskParameter="ConsoleOutput" PropertyName="PaketRestoreCachedHash" />
<Output TaskParameter="ConsoleOutput" PropertyName="PaketRestoreCachedHash" />
</Exec>
<Exec StandardOutputImportance="Low" Condition=" '$(PaketRestoreLockFileHasher)' != '' " Command="$(PaketRestoreLockFileHasher)" ConsoleToMSBuild='true'>
<Output TaskParameter="ConsoleOutput" PropertyName="PaketRestoreLockFileHash" />
<Output TaskParameter="ConsoleOutput" PropertyName="PaketRestoreLockFileHash" />
</Exec>

<PropertyGroup Condition="Exists('$(PaketRestoreCacheFile)') ">
Expand Down Expand Up @@ -127,6 127,7 @@
<PackageReference Include="%(PaketReferencesFileLinesInfo.PackageName)">
<Version>%(PaketReferencesFileLinesInfo.PackageVersion)</Version>
<PrivateAssets Condition="%(PaketReferencesFileLinesInfo.AllPrivateAssets) == 'true'">All</PrivateAssets>
<ExcludeAssets Condition="%(PaketReferencesFileLinesInfo.AllPrivateAssets) == 'exclude'">runtime</ExcludeAssets>
</PackageReference>
</ItemGroup>

Expand Down Expand Up @@ -183,8 184,8 @@

<ConvertToAbsolutePath Condition="@(_NuspecFiles) != ''" Paths="@(_NuspecFiles)">
<Output TaskParameter="AbsolutePaths" PropertyName="NuspecFileAbsolutePath" />
</ConvertToAbsolutePath>
</ConvertToAbsolutePath>


<!-- Call Pack -->
<PackTask Condition="$(UseNewPack)"
Expand Down
21 changes: 21 additions & 0 deletions Fable.Remoting.DotnetClient/Fable.Remoting.DotnetClient.fsproj
Original file line number Diff line number Diff line change
@@ -0,0 1,21 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<Description>Dotnet client proxy that is compatible with Fable.Remoting webservers</Description>
<PackageProjectUrl>https://github.com/Zaid-Ajaj/Fable.Remoting</PackageProjectUrl>
<RepositoryUrl>https://github.com/Zaid-Ajaj/Fable.Remoting.git</RepositoryUrl>
<PackageLicenseUrl>https://github.com/Zaid-Ajaj/Fable.Remoting/blob/master/LICENSE</PackageLicenseUrl>
<PackageIconUrl></PackageIconUrl>
<PackageTags>fsharp;fable;remoting;rpc;webserver;json</PackageTags>
<Authors>Zaid Ajaj</Authors>
<Version>1.0.0</Version>
<TargetFrameworks>netstandard2.0</TargetFrameworks>
</PropertyGroup>
<ItemGroup>
<Compile Include="Proxy.fs" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Fable.Remoting.Json\Fable.Remoting.Json.fsproj" />
</ItemGroup>
<Import Project="..\.paket\Paket.Restore.targets" />
</Project>
100 changes: 100 additions & 0 deletions Fable.Remoting.DotnetClient/Proxy.fs
Original file line number Diff line number Diff line change
@@ -0,0 1,100 @@
namespace Fable.Remoting.DotnetClient

open FSharp.Reflection
open System.Reflection
open Fable.Remoting.Json
open System
open System.Text
open HttpFs.Client
open Newtonsoft.Json
open Newtonsoft.Json.Linq
open Hopac

[<RequireQualifiedAccess>]
module Proxy =

[<RequireQualifiedAccess>]
type ProxyField =
| Function of name:string * funcType:Type * returnType: Type
| AsyncValue of name:string * genericType:Type
| IgnoredInvalidField of name:string * Type

/// Flattens functions of `(type1 -> type2 -> ... -> typeN)` types to `[type1; type2; typeN]`. Intermediate functions types are expanded as well.
let rec flattenFunction (functionType: System.Type) =
[ if FSharpType.IsFunction functionType then
let domain, range = FSharpType.GetFunctionElements functionType
yield! flattenFunction domain
yield! flattenFunction range
else
yield functionType ]

/// Extracts proxy fields with their relevant data from a record type
let proxyFieldsOf<'t>() =
FSharpType.GetRecordFields typeof<'t>
|> Array.map (fun propInfo ->
let fieldName = propInfo.Name
let fieldType = propInfo.PropertyType
if FSharpType.IsFunction fieldType
then
let returnType = List.last (flattenFunction fieldType)
if returnType.Name = "FSharpAsync`1"
then ProxyField.Function(fieldName, fieldType, returnType.GetGenericArguments().[0])
else ProxyField.IgnoredInvalidField(fieldName, fieldType)
elif fieldType.Name = "FSharpAsync`1"
then ProxyField.AsyncValue(fieldName, fieldType.GetGenericArguments().[0])
else ProxyField.IgnoredInvalidField(fieldName, fieldType))
|> List.ofSeq

let private converter = FableJsonConverter()
let private serializer = JsonSerializer()
serializer.Converters.Add converter

/// Parses a JSON iput string to a .NET type using Fable JSON converter
let parseDynamicallyAs (valueType: Type) (json: string) =
JToken.Parse(json).ToObject(valueType, serializer)

/// Sends a POST request to the calulated url with the arguments of serialized to an input list
let proxyPost (functionArguments: obj list) url (returnType: Type) =
let serializedInputArgs = JsonConvert.SerializeObject(functionArguments, converter)
Request.createUrl Post url
|> Request.bodyStringEncoded serializedInputArgs (Encoding.UTF8)
|> getResponse
|> Job.bind Response.readBodyAsString
|> Job.map (parseDynamicallyAs returnType)
|> Job.toAsync

let createField (serverType: Type) endpoint routeBuilder = function
| ProxyField.Function(funcName, funcType, returnType) ->
let route = routeBuilder serverType.Name funcName
let argCount = List.length (flattenFunction funcType) - 1
let url = sprintf "%s%s" endpoint route
printfn "Mapping record field '%s' to route %s" funcName url
match argCount with
| 1 -> FSharpValue.MakeFunction(funcType, fun a -> box (proxyPost [a] url returnType))
| 2 -> box (fun a b -> proxyPost [a; b] url returnType)
| 3 -> box (fun a b c -> proxyPost [a; b; c] url returnType)
| 4 -> box (fun a b c d e -> proxyPost [a; b; c; d; e] url returnType)
| 5 -> box (fun a b c d e f -> proxyPost [a; b; c; d; e; f] url returnType)
| 6 -> box (fun a b c d e f g -> proxyPost [a; b; c; d; e; f; g] url returnType)
| n -> failwith "Only up to 6 paramters are supported"
|> Some
| ProxyField.AsyncValue(name, returnType) ->
let customRoute = routeBuilder serverType.Name name
let url = sprintf "%s%s" endpoint customRoute
box (proxyPost [] url returnType)
|> Some
| ProxyField.IgnoredInvalidField(name, _) ->
printfn "Record field '%s' is not a valid proxy field and will be ignored" name
None

let createAn<'t> (endpoint: string) routeBuilder =
let serverType = typeof<'t>
let fieldCreator = createField serverType endpoint routeBuilder
let fields =
proxyFieldsOf<'t>()
|> List.choose fieldCreator
|> List.map unbox<obj>
|> Array.ofList

FSharpValue.MakeRecord(serverType, fields, false)
|> unbox<'t>
2 changes: 2 additions & 0 deletions Fable.Remoting.DotnetClient/paket.references
Original file line number Diff line number Diff line change
@@ -0,0 1,2 @@
FSharp.Core
Http.fs
18 changes: 18 additions & 0 deletions Fable.Remoting.IntegrationTests/DotnetClient/DotnetClient.fsproj
Original file line number Diff line number Diff line change
@@ -0,0 1,18 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>netcoreapp2.0</TargetFramework>
</PropertyGroup>

<ItemGroup>
<Compile Include="..\Shared\SharedTypes.fs" />
<Compile Include="Program.fs" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="../../Fable.Remoting.DotnetClient/Fable.Remoting.DotnetClient.fsproj" />
</ItemGroup>

<Import Project="..\..\.paket\Paket.Restore.targets" />
</Project>
26 changes: 26 additions & 0 deletions Fable.Remoting.IntegrationTests/DotnetClient/Program.fs
Original file line number Diff line number Diff line change
@@ -0,0 1,26 @@
// Learn more about F# at http://fsharp.org

open System
open SharedTypes
open Fable.Remoting.DotnetClient
open Expecto
open Expecto.Logging
let dotnetClientTests =
testList "Dotnet Client tests" [
testCase "Proxy can be created" <| fun _ ->
let server = Proxy.createAn<ISimpleServer> "http://localhost:8080" (sprintf "/api/%s/%s")
()

testCaseAsync "Calling server works" <| async {
let server = Proxy.createAn<ISimpleServer> "http://localhost:8080" (sprintf "/api/%s/%s")
let! (result : int) = server.getLength "hello"
Expect.equal 5 result "Length returned is correct"
}
]

let testConfig = { Expecto.Tests.defaultConfig with
parallelWorkers = 4
verbosity = LogLevel.Debug }

[<EntryPoint>]
let main argv = runTests testConfig dotnetClientTests
2 changes: 2 additions & 0 deletions Fable.Remoting.IntegrationTests/DotnetClient/paket.references
Original file line number Diff line number Diff line change
@@ -0,0 1,2 @@
FSharp.Core
Expecto
8 changes: 7 additions & 1 deletion Fable.Remoting.IntegrationTests/Server.Suave/Program.fs
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 31,11 @@ let versionTestWebPart =
use_custom_handler_for "v2" (isVersion "2")
}

let simpleServerWebPart = remoting simpleServer {
use_logger (printfn "%s")
use_route_builder routeBuilder
}

let contextTestWebApp =
remoting {callWithCtx = fun (ctx:HttpContext) -> async{return ctx.request.path}} {
use_logger (printfn "%s")
Expand All @@ -41,7 46,8 @@ let webApp =
choose [ GET >=> browseHome
fableWebPart
versionTestWebPart
contextTestWebApp ]
contextTestWebApp
simpleServerWebPart ]

let rec findRoot dir =
if File.Exists(System.IO.Path.Combine(dir, "paket.dependencies"))
Expand Down
6 changes: 6 additions & 0 deletions Fable.Remoting.IntegrationTests/Shared/ServerImpl.fs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 6,12 @@ open System
module Async =
let result<'a> (x: 'a) : Async<'a> =
async { return x }


let simpleServer : ISimpleServer = {
getLength = fun input -> Async.result input.Length
}

// Async.result : 'a -> Async<'a>
// a simple implementation, just return whatever value you get (echo the input)
let server : IServer = {
Expand Down
4 changes: 4 additions & 0 deletions Fable.Remoting.IntegrationTests/Shared/SharedTypes.fs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 21,10 @@ type GenericRecord<'t> = {

type SingleCase = SingleCase of int

type ISimpleServer = {
getLength : string -> Async<int>
}

type IServer = {
// primitive types
simpleUnit : unit -> Async<int>
Expand Down
2 changes: 1 addition & 1 deletion Fable.Remoting.IntegrationTests/client-dist/bundle.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion Fable.Remoting.IntegrationTests/client-dist/bundle.js.map

Large diffs are not rendered by default.

Loading

0 comments on commit 4e63294

Please sign in to comment.