├── docs ├── img │ ├── logo.png │ └── nuget-logo.png └── index.fsx ├── tests ├── google.jpg ├── html-reference.jpg ├── html-reference.pdf ├── url-reference.jpg ├── url-reference.pdf ├── paket.references ├── CloudLayer.Tests.fsproj └── ApiSpecs.fs ├── watch.ps1 ├── src ├── paket.references ├── CloudLayer.FSharp.fsproj ├── Directory.Build.props ├── Api.fsi ├── Api.fs └── Http.fs ├── RELEASE_NOTES.md ├── .gitattributes ├── .config └── dotnet-tools.json ├── .gitignore ├── .github └── ISSUE_TEMPLATE.md ├── paket.dependencies ├── FSharp.CloudLayer.sln ├── examples └── Example.fsx ├── README.md └── LICENSE.txt /docs/img/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cloudlayerio/cloudlayerio-fsharp/HEAD/docs/img/logo.png -------------------------------------------------------------------------------- /tests/google.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cloudlayerio/cloudlayerio-fsharp/HEAD/tests/google.jpg -------------------------------------------------------------------------------- /watch.ps1: -------------------------------------------------------------------------------- 1 | $env:TargetPath = "src\CloudLayer.FSharp\bin\Debug\netstandard2.0" 2 | dotnet fsdocs watch -------------------------------------------------------------------------------- /docs/img/nuget-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cloudlayerio/cloudlayerio-fsharp/HEAD/docs/img/nuget-logo.png -------------------------------------------------------------------------------- /tests/html-reference.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cloudlayerio/cloudlayerio-fsharp/HEAD/tests/html-reference.jpg -------------------------------------------------------------------------------- /tests/html-reference.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cloudlayerio/cloudlayerio-fsharp/HEAD/tests/html-reference.pdf -------------------------------------------------------------------------------- /tests/url-reference.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cloudlayerio/cloudlayerio-fsharp/HEAD/tests/url-reference.jpg -------------------------------------------------------------------------------- /tests/url-reference.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cloudlayerio/cloudlayerio-fsharp/HEAD/tests/url-reference.pdf -------------------------------------------------------------------------------- /tests/paket.references: -------------------------------------------------------------------------------- 1 | FSharp.Core 2 | FsCheck 3 | Microsoft.NET.Test.Sdk 4 | NUnit 5 | NUnit.Runners 6 | NUnit3TestAdapter -------------------------------------------------------------------------------- /src/paket.references: -------------------------------------------------------------------------------- 1 | FSharp.Core 2 | System.Net.Http 3 | System.Text.Json 4 | System.Net.Http.Json 5 | Microsoft.Extensions.Http -------------------------------------------------------------------------------- /RELEASE_NOTES.md: -------------------------------------------------------------------------------- 1 | ## 0.1 2 | 3 | - Initial release 4 | - Support fetching images and pdfs 5 | - Support rate limit and account status checks 6 | - Support Http Client Factory -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto 2 | *.cs text eol=lf diff=csharp 3 | *.fs text eol=lf 4 | *.sln text eol=crlf merge=union 5 | *.csproj text merge=union 6 | *.fsproj text merge=union 7 | *.sh text eol=lf -------------------------------------------------------------------------------- /.config/dotnet-tools.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 1, 3 | "isRoot": true, 4 | "tools": { 5 | "paket": { 6 | "version": "5.245.1", 7 | "commands": [ 8 | "paket" 9 | ] 10 | }, 11 | "fake-cli": { 12 | "version": "5.20.1", 13 | "commands": [ 14 | "fake" 15 | ] 16 | }, 17 | "fsharp.formatting.commandtool": { 18 | "version": "7.2.8", 19 | "commands": [ 20 | "fsdocs" 21 | ] 22 | } 23 | } 24 | } -------------------------------------------------------------------------------- /src/CloudLayer.FSharp.fsproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | #ignore thumbnails created by windows 2 | Thumbs.db 3 | #Ignore files build by Visual Studio 4 | *.obj 5 | *.pdb 6 | *.user 7 | *.aps 8 | *.pch 9 | *.vspscc 10 | *_i.c 11 | *_p.c 12 | *.ncb 13 | *.suo 14 | *.tlb 15 | *.tlh 16 | *.bak 17 | *.cache 18 | *.ilk 19 | *.log 20 | *_Spliced.*proj 21 | [Bb]in 22 | [Dd]ebug*/ 23 | *.lib 24 | *.sbr 25 | obj/ 26 | [Rr]elease*/ 27 | _ReSharper*/ 28 | [Tt]est[Rr]esult* 29 | build/ 30 | deploy/ 31 | docs/output 32 | packages/ 33 | temp/ 34 | test/ 35 | tools/ 36 | *.nupkg 37 | AssemblyInfo.fs 38 | .fake/ 39 | .ionide/ 40 | .paket/ 41 | .vs/ 42 | .vscode/ 43 | .fsdocs/ 44 | tmp/ 45 | paket-files/ 46 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ### Description 2 | 3 | Please provide a succinct description of your issue. 4 | 5 | ### Repro steps 6 | 7 | Please provide the steps required to reproduce the problem 8 | 9 | 1. Step A 10 | 11 | 2. Step B 12 | 13 | ### Expected behavior 14 | 15 | Please provide a description of the behavior you expect. 16 | 17 | ### Actual behavior 18 | 19 | Please provide a description of the actual behavior you observe. 20 | 21 | ### Known workarounds 22 | 23 | Please provide a description of any known workarounds. 24 | 25 | ### Related information 26 | 27 | * Operating system 28 | * Branch 29 | * .NET Runtime, CoreCLR or Mono Version 30 | * Performance information, links to performance testing scripts 31 | -------------------------------------------------------------------------------- /paket.dependencies: -------------------------------------------------------------------------------- 1 | version 5.241.2 2 | source http://api.nuget.org/v3/index.json 3 | redirects: on 4 | storage: none 5 | 6 | nuget FSharp.Core 7 | nuget System.Net.Http 8 | nuget System.Net.Http.Json 9 | nuget Microsoft.Extensions.Http 10 | nuget Microsoft.NET.Test.Sdk 11 | nuget NUnit 12 | nuget NUnit.Runners 13 | nuget NUnit3TestAdapter 14 | nuget FsCheck 15 | 16 | // [ FAKE GROUP ] 17 | group Build 18 | source https://api.nuget.org/v3/index.json 19 | source https://ci.appveyor.com/nuget/fsharp-formatting 20 | nuget Fake.Core.ReleaseNotes 21 | nuget Fake.Tools.Git 22 | nuget Fake.IO.FileSystem 23 | nuget Fake.Core.Target 24 | nuget Fake.DotNet.FSFormatting 25 | nuget FSharp.Formatting >= 3 prerelease 26 | nuget FSharp.Formatting.CommandTool >= 3 prerelease 27 | nuget Fake.BuildServer.AppVeyor 28 | nuget Fake.DotNet.Paket 29 | nuget NUnit 30 | nuget FsCheck 31 | -------------------------------------------------------------------------------- /docs/index.fsx: -------------------------------------------------------------------------------- 1 | (** 2 | 3 | # CloudLayer API for F# 4 | 5 | 6 | ## Installation 7 | 8 |
9 | The library can be installed from NuGet: 10 |
PM> Install-Package CloudLayer.FSharp
11 |
12 | 13 | 14 | ## Contributing 15 | 16 | 17 | The project is hosted on [GitHub][gh] where you can [report issues][issues], fork 18 | the project and submit pull requests. If you're adding new public API, please also 19 | consider adding [samples][content] that can be turned into documentation. You might 20 | also want to read [library design notes][readme] to understand how it works. 21 | 22 | ## License 23 | 24 | The library is available under the MIT license, which allows modification and 25 | redistribution for both commercial and non-commercial purposes. For more information, see the 26 | [License file][license] in the GitHub repository. 27 | 28 | *) 29 | -------------------------------------------------------------------------------- /tests/CloudLayer.Tests.fsproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netcoreapp3.1 5 | false 6 | true 7 | 8 | 9 | 10 | 11 | 12 | PreserveNewest 13 | 14 | 15 | PreserveNewest 16 | 17 | 18 | PreserveNewest 19 | 20 | 21 | PreserveNewest 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /FSharp.CloudLayer.sln: -------------------------------------------------------------------------------- 1 | 2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 16 4 | VisualStudioVersion = 16.0.30709.64 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{6EC3EE1D-3C4E-46DD-8F32-0CC8E7565705}") = "CloudLayer.FSharp", "src\CloudLayer.FSharp.fsproj", "{844FAAFB-02C6-4F5D-B79D-7DBB3AD8E345}" 7 | EndProject 8 | Project("{6EC3EE1D-3C4E-46DD-8F32-0CC8E7565705}") = "CloudLayer.Tests", "tests\CloudLayer.Tests.fsproj", "{20CEE79F-4E8D-462B-94A5-1BC634FE8614}" 9 | EndProject 10 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{483A5836-2876-4E01-B578-A7C9101A9EB7}" 11 | ProjectSection(SolutionItems) = preProject 12 | examples\Example.fsx = examples\Example.fsx 13 | paket.dependencies = paket.dependencies 14 | EndProjectSection 15 | EndProject 16 | Global 17 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 18 | Debug|Any CPU = Debug|Any CPU 19 | Release|Any CPU = Release|Any CPU 20 | EndGlobalSection 21 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 22 | {844FAAFB-02C6-4F5D-B79D-7DBB3AD8E345}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 23 | {844FAAFB-02C6-4F5D-B79D-7DBB3AD8E345}.Debug|Any CPU.Build.0 = Debug|Any CPU 24 | {844FAAFB-02C6-4F5D-B79D-7DBB3AD8E345}.Release|Any CPU.ActiveCfg = Release|Any CPU 25 | {844FAAFB-02C6-4F5D-B79D-7DBB3AD8E345}.Release|Any CPU.Build.0 = Release|Any CPU 26 | {20CEE79F-4E8D-462B-94A5-1BC634FE8614}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 27 | {20CEE79F-4E8D-462B-94A5-1BC634FE8614}.Debug|Any CPU.Build.0 = Debug|Any CPU 28 | {20CEE79F-4E8D-462B-94A5-1BC634FE8614}.Release|Any CPU.ActiveCfg = Release|Any CPU 29 | {20CEE79F-4E8D-462B-94A5-1BC634FE8614}.Release|Any CPU.Build.0 = Release|Any CPU 30 | EndGlobalSection 31 | GlobalSection(SolutionProperties) = preSolution 32 | HideSolutionNode = FALSE 33 | EndGlobalSection 34 | GlobalSection(ExtensibilityGlobals) = postSolution 35 | SolutionGuid = {E1F107AB-73B6-4ECA-B2EB-F7E48040FAD3} 36 | EndGlobalSection 37 | EndGlobal 38 | -------------------------------------------------------------------------------- /examples/Example.fsx: -------------------------------------------------------------------------------- 1 | #r "nuget: System.Net.Http" 2 | #r "nuget: System.Text.Json" 3 | #r "nuget: System.Net.Http.Json" 4 | #r "nuget: Microsoft.Extensions.Http" 5 | #r @"..\src\bin\Debug\netstandard2.0\CloudLayer.FSharp.dll" 6 | 7 | open System 8 | open CloudLayerIo 9 | 10 | let connection = { Connection.Defaults with ApiKey = "ca-666907a519df4f84b0db24b822b37c5e" } 11 | 12 | let online = 13 | connection |> CloudLayerApi.isOnline |> Async.RunSynchronously 14 | 15 | let status = 16 | connection |> CloudLayerApi.accountStatus |> Async.RunSynchronously 17 | 18 | match status with 19 | | Ok status -> 20 | $"{status.Remaining} of {status.Limit} remaining. " + 21 | $"Limit resets at {status.ResetsAt.LocalDateTime}" 22 | | Error err -> 23 | match err with 24 | | FailureReason.InvalidApiKey -> "Check your api key" 25 | | FailureReason.InsufficientCredit -> "Buy more credit pls" 26 | | FailureReason.SubscriptionInactive -> "Please activate your account" 27 | | FailureReason.Unauthorized -> "Please check your credentials or proxy" 28 | | other -> $"There was an error: {other}" 29 | |> printfn "%s" 30 | 31 | let image = 32 | connection |> CloudLayerApi.fetchImage (Url "https://google.com") |> Async.RunSynchronously 33 | 34 | match image with 35 | | Ok (stream, status) -> 36 | //do something with stream 37 | () 38 | | Error err -> 39 | failwithf "Something went wrong: %A" err 40 | 41 | connection 42 | |> CloudLayerApi.fetchImage (Url "https://google.com") 43 | |> CloudLayerApi.saveToFile "google.jpg" 44 | |> Async.RunSynchronously 45 | 46 | 47 | connection 48 | |> CloudLayerApi.fetchImageWith 49 | { ImageOptions.Defaults with 50 | Source = Url "https://www.openstreetmap.org#map=13/-6.1918/71.2976" 51 | Timeout = TimeSpan.FromSeconds 60. 52 | Inline = false } 53 | |> CloudLayerApi.saveToFile "eagle-island.jpg" 54 | |> Async.RunSynchronously 55 | 56 | connection 57 | |> CloudLayerApi.fetchPdfWith 58 | { PdfOptions.Defaults with 59 | Source = (Url "https://en.wikipedia.org/wiki/Marine_snow") 60 | PrintBackground = false 61 | Format = "A4" } 62 | |> CloudLayerApi.saveToFile "snow.pdf" 63 | |> Async.RunSynchronously 64 | 65 | -------------------------------------------------------------------------------- /src/Directory.Build.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netstandard2.0 5 | 0.1.0 6 | Asti; 7 | F# API for the CloudLayer service 8 | F# API for the CloudLayer service 9 | https://github.com/cloudlayerio/cloudlayerio-fsharp 10 | nuget-logo.png 11 | LICENSE.txt 12 | F#;FSharp;CloudLayer; 13 | https://github.com/cloudlayerio/cloudlayerio-fsharp/blob/master/LICENSE.txt 14 | https://github.com/cloudlayerio/cloudlayerio-fsharp 15 | true 16 | 17 | 18 | true 19 | 20 | 21 | true 22 | 23 | 24 | true 25 | symbols.nupkg 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | @(ReleaseNoteLines, '%0a') 39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /src/Api.fsi: -------------------------------------------------------------------------------- 1 | namespace CloudLayerIo 2 | 3 | open System 4 | 5 | type DomSelector = 6 | { Selector: string 7 | Options: {| Visible: bool 8 | Hidden: bool 9 | Timeout: TimeSpan |} option 10 | } 11 | 12 | type ImageOptions = 13 | { Delay: TimeSpan 14 | Timeout: TimeSpan 15 | Filename: string option 16 | Inline: bool 17 | WaitForSelector: DomSelector option 18 | Source: Source } 19 | static member Defaults: ImageOptions 20 | 21 | type PdfOptions = 22 | { Format: string 23 | Margin: {| left: string 24 | top: string 25 | right: string 26 | bottom: string |} option 27 | PrintBackground: bool 28 | Timeout: TimeSpan 29 | Delay: TimeSpan 30 | Filename: string option 31 | Inline: bool 32 | WaitForSelector: DomSelector option 33 | Source: Source } 34 | static member Defaults: PdfOptions 35 | 36 | [] 37 | module CloudLayerApi = 38 | /// Check if the service is online. 39 | /// Returns true if online. 40 | val isOnline: connection:Connection -> Async 41 | 42 | /// Returns the account operational status and limits 43 | val accountStatus: connection:Connection -> Async> 44 | 45 | /// Creates an image with the specified options and returns a stream containing the image 46 | val fetchImageWith: 47 | options:ImageOptions -> connection:Connection -> Async> 48 | 49 | /// Creates an image with the default options and returns a stream containing the image 50 | val fetchImage: 51 | source:Source -> connection:Connection -> Async> 52 | 53 | /// Creates a pdf with the specified options and returns a stream containing the pdf file 54 | val fetchPdfWith: 55 | options:PdfOptions -> connection:Connection -> Async> 56 | 57 | /// Creates a pdf with the default options and returns a stream containing the pdf file 58 | val fetchPdf: source:Source -> connection:Connection -> Async> 59 | 60 | /// If generation was successful, saves a generated file to the specified path 61 | val saveToFile: path:string -> result:Async> -> Async> 62 | 63 | /// Reads the entire response into a byte-array 64 | val toByteArray: result:Async> -> Async> 65 | -------------------------------------------------------------------------------- /src/Api.fs: -------------------------------------------------------------------------------- 1 | namespace CloudLayerIo 2 | 3 | open System 4 | open System.IO 5 | open CloudLayerIo.Async 6 | open CloudLayerIo.Http 7 | 8 | 9 | type DomSelector = 10 | { Selector: string 11 | Options: {| Visible: bool 12 | Hidden: bool 13 | Timeout: TimeSpan |} option 14 | } 15 | 16 | type ImageOptions = 17 | { Delay: TimeSpan 18 | Timeout: TimeSpan 19 | Filename: string option 20 | Inline: bool 21 | WaitForSelector: DomSelector option 22 | Source: Source } 23 | static member Defaults = 24 | { Delay = TimeSpan.Zero 25 | Timeout = TimeSpan.FromSeconds 30. 26 | Inline = false 27 | WaitForSelector = None 28 | Filename = None 29 | Source = Html "

Hello World

" } 30 | 31 | type PdfOptions = 32 | { Format: string 33 | Margin: {| left: string 34 | top: string 35 | right: string 36 | bottom: string |} option 37 | PrintBackground: bool 38 | Timeout: TimeSpan 39 | Delay: TimeSpan 40 | Filename: string option 41 | Inline: bool 42 | WaitForSelector: DomSelector option 43 | Source: Source 44 | } 45 | static member Defaults = 46 | { Format = "A4" 47 | Margin = None 48 | PrintBackground = true 49 | Delay = TimeSpan.Zero 50 | Timeout = TimeSpan.FromSeconds 30. 51 | Filename = None 52 | Inline = false 53 | WaitForSelector = None 54 | Source = Html "

Hello World

" } 55 | 56 | [] 57 | module CloudLayerApi = 58 | 59 | 60 | let isOnline connection = 61 | connection |> fetch Head "/oapi" |> map (fun res -> res.IsSuccessStatusCode) 62 | 63 | 64 | let accountStatus connection = 65 | connection |> fetch Get "/v1/getStatus" |> bind tryParseResponse 66 | 67 | let fetchImageWith (options : ImageOptions) connection = 68 | let uri = 69 | match options.Source with 70 | | Url _ -> "/v1/url/image" 71 | | Html _ -> "/v1/html/image" 72 | 73 | connection |> fetch (Post options) uri |> bind tryReadStream 74 | 75 | let fetchImage source connection = 76 | connection |> fetchImageWith { ImageOptions.Defaults with Source = source } 77 | 78 | let fetchPdfWith (options : PdfOptions) connection = 79 | let uri = 80 | match options.Source with 81 | | Url _ -> "/v1/url/pdf" 82 | | Html _ -> "/v1/html/pdf" 83 | 84 | connection |> fetch (Post options) uri |> bind tryReadStream 85 | 86 | 87 | let fetchPdf source connection = 88 | connection |> fetchPdfWith { PdfOptions.Defaults with Source = source } 89 | 90 | let saveToFile (path: string) result = 91 | result 92 | |> Result.mapAsync(fun (stream : Stream, resp) -> 93 | async { 94 | let! token = Async.CancellationToken 95 | use file = File.OpenWrite(path) 96 | do! stream.CopyToAsync(file, 1024, token) |> Async.AwaitTask 97 | return resp 98 | }) 99 | 100 | let toByteArray result = 101 | result 102 | |> Result.mapAsync(fun (stream : Stream, _) -> 103 | async { 104 | let! token = Async.CancellationToken 105 | use memstream = new MemoryStream() 106 | do! stream.CopyToAsync(memstream, 1024, token) |> Async.AwaitTask 107 | return memstream.ToArray() 108 | }) 109 | -------------------------------------------------------------------------------- /tests/ApiSpecs.fs: -------------------------------------------------------------------------------- 1 | module CloudLayer.Testing.ApiSpecs 2 | 3 | open System 4 | open NUnit.Framework 5 | open CloudLayerIo 6 | open System.Security.Cryptography 7 | open System.IO 8 | 9 | let conn = Connection.Defaults 10 | let badconn = { conn with ApiKey = "unauthorized-key" } 11 | 12 | let isOk = function | Ok _ -> true | Error _ -> false 13 | let run async' = Async.RunSynchronously(async', 5000000) 14 | 15 | [] 16 | let ``API key exists`` () = 17 | Assert.That(not (String.IsNullOrWhiteSpace conn.ApiKey)) 18 | 19 | [] 20 | let ``Service is online`` () = 21 | Assert.That(conn |> CloudLayerApi.isOnline |> run) 22 | 23 | [] 24 | let ``Account is active`` () = 25 | Assert.That(conn |> CloudLayerApi.accountStatus |> run |> isOk) 26 | 27 | [] 28 | let ``Has valid rate-limits`` () = 29 | let result = conn |> CloudLayerApi.accountStatus |> run 30 | match result with 31 | | Ok limit -> 32 | Assert.That(limit.Limit > 0) 33 | Assert.That(limit.Remaining > 0) 34 | Assert.That(limit.ResetsAt > DateTimeOffset.UnixEpoch) 35 | | Error err -> 36 | Assert.Fail(string err) 37 | 38 | [] 39 | let ``Fails on bad API key`` () = 40 | let response = badconn |> CloudLayerApi.accountStatus |> run 41 | let isInvalid = response = Error FailureReason.InvalidApiKey 42 | Assert.That(isInvalid) 43 | 44 | let referenceUrl = Url "http://acid2.acidtests.org/reference.html" 45 | let referenceHtml = Html "

Hello World!

" 46 | 47 | let fileContents fileName = 48 | File.ReadAllBytes(fileName) |> Ok 49 | 50 | let isSame actual reference = 51 | match actual, reference with 52 | | Ok a, Ok b -> 53 | let headerLength = 10 54 | 55 | // same header 56 | let headerMatches = 57 | (a |> Array.take headerLength) = (b |> Array.take headerLength) 58 | let (la,lb) = Array.length a, Array.length b 59 | 60 | // file sizes match to within 1% 61 | let similarSize = (abs la - lb) <= (lb / 100) 62 | 63 | headerMatches && similarSize 64 | | _ -> false 65 | 66 | [] 67 | let ``Captures an image from a url`` () = 68 | let res = 69 | conn 70 | |> CloudLayerApi.fetchImage referenceUrl 71 | |> CloudLayerApi.toByteArray 72 | |> Async.RunSynchronously 73 | 74 | let imgRef = fileContents "url-reference.jpg" 75 | 76 | Assert.That(isSame res imgRef) 77 | 78 | [] 79 | let ``Captures an image from html`` () = 80 | let res = 81 | conn 82 | |> CloudLayerApi.fetchImage referenceHtml 83 | |> CloudLayerApi.toByteArray 84 | |> Async.RunSynchronously 85 | 86 | let imgRef = fileContents "html-reference.jpg" 87 | 88 | Assert.That(isSame res imgRef) 89 | 90 | [] 91 | let ``Captures a pdf from a url`` () = 92 | let res = 93 | conn 94 | |> CloudLayerApi.fetchPdf referenceUrl 95 | |> CloudLayerApi.toByteArray 96 | |> Async.RunSynchronously 97 | 98 | let pdfRef = fileContents "url-reference.pdf" 99 | 100 | Assert.That(isSame res pdfRef) 101 | 102 | [] 103 | let ``Captures a pdf from html`` () = 104 | let res = 105 | conn 106 | |> CloudLayerApi.fetchPdf referenceHtml 107 | |> CloudLayerApi.toByteArray 108 | |> Async.RunSynchronously 109 | 110 | let pdfRef = fileContents "html-reference.pdf" 111 | 112 | Assert.That(isSame res pdfRef) 113 | 114 | [] 115 | let ``Saves a file to disk`` () = 116 | let filename = "eagle-island.jpg" 117 | let result = 118 | conn 119 | |> CloudLayerApi.fetchImageWith 120 | { ImageOptions.Defaults with 121 | Source = Url "https://www.openstreetmap.org#map=13/-6.1918/71.2976" 122 | Timeout = TimeSpan.FromSeconds 60. 123 | Inline = false } 124 | |> CloudLayerApi.saveToFile filename 125 | |> Async.RunSynchronously 126 | 127 | Assert.That(isOk result) 128 | Assert.That(File.Exists filename) -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # CloudLayer for F# 2 | This is the [CloudLayer](https://cloudlayer.io) API for easy access to our REST based API services using F#. 3 | 4 | To read about how to get started using CloudLayer, see [documentation here](https://cloudlayer.io/docs/getstarted). 5 | 6 | # Installation 7 | 8 | You can reference it directly from Nuget or Paket. 9 | 10 | ```powershell 11 | PS> Install-Package CloudLayer.FSharp 12 | ``` 13 | 14 | The assembly targets `NetStandard 2.0`. 15 | 16 | # Usage 17 | 18 | To begin, create an API key from the [dashboard](https://cloudlayer.io/dashboard/account/api). 19 | 20 | 21 | 22 | ## Basics 23 | 24 | All API calls take in a `Connection`: 25 | 26 | ```fsharp 27 | let connection = 28 | { Connection.Defaults with ApiKey = "ca-644907a519df4f84b0db24b822b37c5e" } 29 | ``` 30 | 31 | If you are using this from an Asp.Net Core app, you can specify a `IHttpClientFactory` to be used (this is usually available through Dependency Injection), 32 | 33 | ```fsharp 34 | let connection' = { connection with ClientFactory = factory } 35 | ``` 36 | 37 | `IHttpClientFactory` avoids socket exhaustion problems and maintains a pool of `HttpClient` instances for reuse. See [this article](https://docs.microsoft.com/en-us/dotnet/architecture/microservices/implement-resilient-applications/use-httpclientfactory-to-implement-resilient-http-requests) for more. 38 | 39 | API calls have, as the last argument a `Connection`, and they take the shape: 40 | 41 | ```fsharp 42 | connection |> CloudLayerApi.apiCall : Async> 43 | ``` 44 | 45 | All API calls return the `Result` type, and they follow the [railway-oriented approach](https://fsharpforfunandprofit.com/posts/recipe-part2/). 46 | 47 | ## Account Status 48 | 49 | You can check the status of your account with 50 | 51 | ```fsharp 52 | let status = 53 | connection |> CloudLayerApi.accountStatus |> Async.RunSynchronously 54 | ``` 55 | 56 | The results can be pattern matched. 57 | 58 | ```fsharp 59 | match status with 60 | | Ok status -> 61 | $"{status.Remaining} of {status.Limit} remaining. " + 62 | $"Limit resets at {status.ResetsAt.LocalDateTime}" 63 | | Error err -> 64 | match err with 65 | | FailureReason.InvalidApiKey -> "Check your api key" 66 | | FailureReason.InsufficientCredit -> "Buy more credit pls" 67 | | FailureReason.SubscriptionInactive -> "Please activate your account" 68 | | FailureReason.Unauthorized -> "Please check your credentials or proxy" 69 | | other -> $"There was an error: {other}" 70 | |> printfn "%s" 71 | ``` 72 | 73 | ## Creating Images 74 | 75 | CloudLayer can create images of public URLs: 76 | 77 | ```fsharp 78 | let image = 79 | connection |> CloudLayerApi.fetchImage (Url "https://google.com") 80 | ``` 81 | 82 | and raw html: 83 | 84 | ```fsharp 85 | let image = 86 | connection |> CloudLayerApi.fetchImage (Html "

Hello World!

") 87 | ``` 88 | 89 | and returns either a `System.IO.Stream` or a `FailureReason`. 90 | 91 | ```fsharp 92 | match image with 93 | | Ok (stream, status) -> 94 | //do something with stream 95 | | Error err -> 96 | failwithf "Something went wrong: %A" err 97 | ``` 98 | 99 | You can save the result to a file with `saveToFile`, or read directly to memory with `toByteArray`. 100 | 101 | ```fsharp 102 | connection 103 | |> CloudLayerApi.fetchImage (Url "https://google.com") 104 | |> CloudLayerApi.saveToFile "google.jpg" 105 | |> Async.RunSynchronously 106 | ``` 107 | 108 | ![HtmlImage](https://raw.githubusercontent.com/cloudlayerio/cloudlayerio-fsharp/main/tests/google.jpg) 109 | 110 | To use more configuration options, use `fetchImageWith`. Options are specified by the `ImageOptions` record. 111 | 112 | ```fsharp 113 | 114 | connection 115 | |> CloudLayerApi.fetchImageWith 116 | { ImageOptions.Defaults with 117 | Source = Url "https://www.openstreetmap.org#map=13/-6.1918/71.2976" 118 | Timeout = TimeSpan.FromSeconds 60. 119 | Inline = false } 120 | |> CloudLayerApi.saveToFile "eagle-island.jpg" 121 | |> Async.RunSynchronously 122 | ``` 123 | 124 | ## Creating PDFs 125 | 126 | Creating PDFs is similar to the API for creating images. 127 | 128 | ```fsharp 129 | connection |> CloudLayerApi.fetchPdf (Url "https://en.wikipedia.org/wiki/Marine_snow") 130 | connection |> CloudLayerApi.fetchPdf (Html "

Hello from PDF!

") 131 | ``` 132 | 133 | For more options, use `fetchPdfWith`. Options are specified by the `PdfOptions` record. 134 | 135 | ```fsharp 136 | connection 137 | |> CloudLayerApi.fetchPdfWith 138 | { PdfOptions.Defaults with 139 | Source = (Url "https://en.wikipedia.org/wiki/Marine_snow") 140 | PrintBackground = false 141 | Format = "A4" } 142 | |> CloudLayerApi.saveToFile "snow.pdf" 143 | |> Async.RunSynchronously 144 | 145 | ``` 146 | 147 | 148 | 149 | ### Note 150 | 151 | This library is specifically for F#, if you are using C# you should [use our C# library](github.com/cloudlayerio/cloudlayerio-csharp). We did this because we wanted to give F# developers first class support instead of wrapping a C# library. -------------------------------------------------------------------------------- /src/Http.fs: -------------------------------------------------------------------------------- 1 | namespace CloudLayerIo 2 | 3 | open System 4 | open System.Net 5 | open System.Net.Http 6 | open System.Net.Http.Json 7 | open System.Text 8 | open System.Text.Json 9 | open System.Text.Json.Serialization 10 | 11 | type ApiStatus = 12 | { Limit: int 13 | Remaining: int 14 | ResetsAt: DateTimeOffset } 15 | 16 | [] 17 | type FailureReason = 18 | | InvalidApiKey 19 | | InsufficientCredit 20 | | SubscriptionInactive 21 | | InvalidRequest 22 | | Unauthorized 23 | | ServerError 24 | | ErrorCode of statusCode: HttpStatusCode * content: string 25 | | Faulted of Exception 26 | 27 | type CloudLayerResponse = 28 | { mutable reason: string 29 | mutable allowed: bool } 30 | 31 | type Source = 32 | | Url of string 33 | | Html of string 34 | 35 | module Service = 36 | 37 | let [] Uriv1 = 38 | #if DEV 39 | "https://dev-api.cloudlayer.io/oapi" 40 | #else 41 | "https://api.cloudlayer.io" 42 | #endif 43 | 44 | let [] ClientName = "cloudlayerio" 45 | let [] UserAgent = "cloudlayerio-fsharp" 46 | let [] ContentType = "application/json" 47 | 48 | let BaseUri = Uri Uriv1 49 | 50 | let internal ClientFactory = { 51 | new IHttpClientFactory with 52 | member _.CreateClient(_name) = new HttpClient() 53 | } 54 | 55 | type Connection = 56 | { ClientFactory: IHttpClientFactory 57 | ApiKey: string } 58 | static member Defaults = 59 | { ClientFactory = Service.ClientFactory 60 | ApiKey = Environment.GetEnvironmentVariable("CLOUDLAYER_API_KEY") } 61 | 62 | module internal Async = 63 | 64 | let bind fn asyncInstance = 65 | async.Bind(asyncInstance, fn) 66 | 67 | let map fn asyncInstance = async { 68 | let! value = asyncInstance 69 | return fn value 70 | } 71 | 72 | module internal Result = 73 | let mapAsync mapping asyncResult = 74 | asyncResult 75 | |> Async.bind(function 76 | | Ok value -> 77 | mapping value |> Async.map Ok 78 | | Error err -> 79 | async.Return(Error err) 80 | ) 81 | 82 | module internal Http = 83 | 84 | open Service 85 | open Async 86 | 87 | type 'a Verb = 88 | | Head 89 | | Get 90 | | Post of data: 'a 91 | 92 | 93 | let parseRateLimit (response: HttpResponseMessage) = 94 | let getValue key = 95 | response.Headers.GetValues key 96 | |> Seq.map Int64.TryParse 97 | |> Seq.tryHead 98 | |> Option.bind(function true, v -> Some v | _ -> None) 99 | |> Option.defaultValue 0L 100 | 101 | { 102 | Limit = getValue "X-RateLimit-Limit" |> int 103 | Remaining = getValue "X-RateLimit-Remaining" |> int 104 | ResetsAt = getValue "X-RateLimit-Reset" |> DateTimeOffset.FromUnixTimeSeconds 105 | } 106 | 107 | let parseResponse (resp: HttpResponseMessage) = async { 108 | let! token = Async.CancellationToken 109 | let! response = 110 | if resp.Content.Headers.ContentType.MediaType = ContentType then 111 | resp.Content.ReadFromJsonAsync(cancellationToken = token) 112 | |> Async.AwaitTask 113 | |> map Some 114 | else 115 | async.Return None 116 | 117 | return 118 | match resp.StatusCode with 119 | | HttpStatusCode.OK 120 | | HttpStatusCode.Created -> 121 | Ok (parseRateLimit resp) 122 | | HttpStatusCode.Unauthorized -> 123 | match response with 124 | | Some res -> 125 | match res.reason with 126 | | "Invalid API key." -> FailureReason.InvalidApiKey 127 | | "Insufficient credit." -> FailureReason.InsufficientCredit 128 | | "Subscription inactive." -> FailureReason.SubscriptionInactive 129 | | _ -> FailureReason.Unauthorized 130 | | None -> FailureReason.Unauthorized 131 | |> Error 132 | | HttpStatusCode.BadRequest -> 133 | Error FailureReason.InvalidRequest 134 | | HttpStatusCode.InternalServerError -> 135 | Error FailureReason.ServerError 136 | | other -> 137 | let content = resp.Content.ReadAsStringAsync() 138 | do content.Wait(token) 139 | Error (FailureReason.ErrorCode (other, content.Result)) 140 | } 141 | 142 | let tryParseResponse response = async { 143 | try 144 | return! parseResponse response 145 | with ex -> 146 | return Error (FailureReason.Faulted ex) 147 | } 148 | 149 | let tryReadStream response = 150 | response 151 | |> tryParseResponse 152 | |> Result.mapAsync(fun status -> 153 | response.Content.ReadAsStreamAsync() 154 | |> Async.AwaitTask 155 | |> map (fun stream -> stream, status) 156 | ) 157 | 158 | let notSupported () = raise (NotSupportedException()) 159 | 160 | type OptionValueConverter<'T>() = 161 | inherit JsonConverter<'T option>() 162 | 163 | override _.Write (writer, value: 'T option, options: JsonSerializerOptions) = 164 | match value with 165 | | Some value -> JsonSerializer.Serialize(writer, value, options) 166 | | None -> writer.WriteNullValue () 167 | 168 | override _.Read (_r, _t, _o) = 169 | notSupported () 170 | 171 | let serializerOptions = 172 | let opts = JsonSerializerOptions(PropertyNamingPolicy = JsonNamingPolicy.CamelCase) 173 | 174 | opts.Converters.Add <| { 175 | new Serialization.JsonConverter() with 176 | override _.Write(writer, timespan, _options) = 177 | writer.WriteNumberValue(int timespan.TotalMilliseconds) 178 | override _.Read(_r, _t, _opts) = 179 | notSupported () 180 | } 181 | 182 | opts.Converters.Add <| { 183 | new Serialization.JsonConverter() with 184 | override _.Write(writer, source, _options) = 185 | match source with 186 | | Url url -> 187 | writer.WriteStringValue("url") 188 | writer.WriteString("url", url) 189 | | Html html -> 190 | writer.WriteStringValue("html") 191 | writer.WriteString("html", html |> Encoding.UTF8.GetBytes |> Convert.ToBase64String) 192 | 193 | override _.Read(r, t, opts) = 194 | notSupported () 195 | } 196 | 197 | opts.Converters.Add <| { new Serialization.JsonConverterFactory() with 198 | override _.CanConvert(t: Type) : bool = 199 | t.IsGenericType && 200 | t.GetGenericTypeDefinition() = typedefof> 201 | 202 | override _.CreateConverter(t: Type, _options) : JsonConverter = 203 | let typ = t.GetGenericArguments() |> Array.head 204 | let converterType = typedefof>.MakeGenericType(typ) 205 | Activator.CreateInstance(converterType) :?> JsonConverter 206 | } 207 | 208 | opts 209 | 210 | let fetch (content: 'content Verb) (path: string) (connection: Connection) = async { 211 | let! token = Async.CancellationToken 212 | let client = connection.ClientFactory.CreateClient(ClientName) 213 | client.BaseAddress <- Service.BaseUri 214 | client.DefaultRequestHeaders.Add ("X-API-Key", connection.ApiKey) 215 | client.DefaultRequestHeaders.Add ("User-Agent", UserAgent) 216 | 217 | return! 218 | match content with 219 | | Post obj -> 220 | #if DEBUG 221 | let json = JsonSerializer.Serialize(obj, serializerOptions) 222 | do System.Diagnostics.Debug.Print $"POST:{path}\n{json}" 223 | #endif 224 | client.PostAsJsonAsync(path, obj, serializerOptions, token) 225 | | Get -> 226 | #if DEBUG 227 | do System.Diagnostics.Debug.Print $"GET:{path}" 228 | #endif 229 | client.GetAsync(path, token) 230 | | Head -> 231 | client.GetAsync(path, HttpCompletionOption.ResponseHeadersRead, token) 232 | |> Async.AwaitTask 233 | } 234 | 235 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. --------------------------------------------------------------------------------