├── .gitignore ├── README.md ├── cabal.project ├── fourmolu.yaml ├── servant-jsonrpc-client ├── LICENSE ├── Setup.hs ├── changelog.md ├── servant-jsonrpc-client.cabal └── src │ └── Servant │ └── Client │ └── JsonRpc.hs ├── servant-jsonrpc-examples ├── CHANGELOG.md ├── LICENSE ├── Setup.hs ├── client │ └── Main.hs ├── servant-jsonrpc-examples.cabal ├── server │ └── Main.hs └── src │ └── Servant │ └── JsonRpc │ └── Example.hs ├── servant-jsonrpc-server ├── LICENSE ├── Setup.hs ├── changelog.md ├── servant-jsonrpc-server.cabal └── src │ └── Servant │ └── Server │ └── JsonRpc.hs ├── servant-jsonrpc ├── LICENSE ├── Setup.hs ├── changelog.md ├── servant-jsonrpc.cabal └── src │ └── Servant │ └── JsonRpc.hs ├── stack.yaml └── stack.yaml.lock /.gitignore: -------------------------------------------------------------------------------- 1 | dist-newstyle/ 2 | *.sw[op] 3 | cabal.project.local 4 | .ghc.environment.* 5 | .stack-work/ 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | servant-jsonrpc 2 | ==== 3 | 4 | This module extends [servant][1] to make it easy to define [JSON-RPC][2] servers and 5 | clients. 6 | 7 | [1]: https://haskell-servant.readthedocs.io/en/stable/ 8 | [2]: https://www.jsonrpc.org 9 | 10 | 11 | Notes 12 | ---- 13 | 14 | * Does not enforce the `jsonrpc` key in the response 15 | * Does not enforce `id` key on error responses 16 | * We allow for server messages with `null` for both `error` and `result` keys 17 | * The client interface hides the `id` key since the semantics of HTTP determine 18 | which server responses correspond to which client requests. 19 | 20 | 21 | Examples 22 | ---- 23 | 24 | See the package `./servant-jsonrpc-example` for client, server, and API definition examples. 25 | -------------------------------------------------------------------------------- /cabal.project: -------------------------------------------------------------------------------- 1 | packages: */*.cabal 2 | 3 | package * 4 | ghc-options: -Wall 5 | -------------------------------------------------------------------------------- /fourmolu.yaml: -------------------------------------------------------------------------------- 1 | # Number of spaces per indentation step 2 | indentation: 2 3 | 4 | # Styling of arrows in type signatures (choices: trailing, leading, or leading-args) 5 | function-arrows: trailing 6 | 7 | # How to place commas in multi-line lists, records, etc. (choices: leading or trailing) 8 | comma-style: leading 9 | 10 | # Styling of import/export lists (choices: leading, trailing, or diff-friendly) 11 | import-export-style: diff-friendly 12 | 13 | # Whether to full-indent or half-indent 'where' bindings past the preceding body 14 | indent-wheres: false 15 | 16 | # Whether to leave a space before an opening record brace 17 | record-brace-space: false 18 | 19 | # Number of spaces between top-level declarations 20 | newlines-between-decls: 1 21 | 22 | # How to print Haddock comments (choices: single-line, multi-line, or multi-line-compact) 23 | haddock-style: multi-line 24 | 25 | # How to print module docstring 26 | haddock-style-module: null 27 | 28 | # Styling of let blocks (choices: auto, inline, newline, or mixed) 29 | let-style: auto 30 | 31 | # How to align the 'in' keyword with respect to the 'let' keyword (choices: left-align, right-align, or no-space) 32 | in-style: right-align 33 | 34 | # Whether to put parentheses around a single constraint (choices: auto, always, or never) 35 | single-constraint-parens: always 36 | 37 | # Whether to put parentheses around a single deriving class (choices: auto, always, or never) 38 | single-deriving-parens: always 39 | 40 | # Output Unicode syntax (choices: detect, always, or never) 41 | unicode: never 42 | 43 | # Give the programmer more choice on where to insert blank lines 44 | respectful: true 45 | 46 | # Fixity information for operators 47 | fixities: [] 48 | 49 | # Module reexports Fourmolu should know about 50 | reexports: [] 51 | -------------------------------------------------------------------------------- /servant-jsonrpc-client/LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2020 Bitnomial, Inc. 2 | 3 | Redistribution and use in source and binary forms, with or without 4 | modification, are permitted provided that the following conditions are 5 | met: 6 | 7 | 1. Redistributions of source code must retain the above copyright 8 | notice, this list of conditions and the following disclaimer. 9 | 10 | 2. Redistributions in binary form must reproduce the above copyright 11 | notice, this list of conditions and the following disclaimer in the 12 | documentation and/or other materials provided with the distribution. 13 | 14 | 3. Neither the name of the copyright holder nor the names of its 15 | contributors may be used to endorse or promote products derived from 16 | this software without specific prior written permission. 17 | 18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 19 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 20 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 21 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 22 | HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 23 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 24 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 25 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 26 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 27 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 28 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -------------------------------------------------------------------------------- /servant-jsonrpc-client/Setup.hs: -------------------------------------------------------------------------------- 1 | import Distribution.Simple 2 | 3 | main = defaultMain 4 | -------------------------------------------------------------------------------- /servant-jsonrpc-client/changelog.md: -------------------------------------------------------------------------------- 1 | # 1.2.0 2 | 3 | * Support for user-configured content-type in generated client 4 | 5 | # 1.1.0 6 | 7 | * Relax upper version bounds for `aeson` to `(>= 1.3 && < 1.6)` 8 | * Relax upper version bounds for `base` to `(>= 4.11 && < 5.0)` 9 | * Relax upper version bounds for `servant` to `(>= 0.14 && < 0.19)` 10 | * Relax upper version bounds for `servant-client-core` to `(>= 0.14 && < 0.19)` 11 | 12 | # 1.0.1 13 | 14 | Adds a `HasClient` instance for `RawJsonRpc` 15 | 16 | # 1.0.0 17 | 18 | First release 19 | -------------------------------------------------------------------------------- /servant-jsonrpc-client/servant-jsonrpc-client.cabal: -------------------------------------------------------------------------------- 1 | cabal-version: 2.4 2 | 3 | name: servant-jsonrpc-client 4 | version: 1.2.0 5 | author: Ian Shipman 6 | maintainer: Ian Shipman 7 | 8 | synopsis: Generate JSON-RPC servant clients 9 | description: 10 | Use this package to generate servant client functions that interact with a 11 | remote server via JSON-RPC over HTTP. 12 | 13 | homepage: https://github.com/bitnomial/servant-jsonrpc 14 | license: BSD-3-Clause 15 | license-file: LICENSE 16 | category: Web 17 | build-type: Simple 18 | 19 | extra-source-files: changelog.md 20 | 21 | source-repository head 22 | type: git 23 | location: https://github.com/bitnomial/servant-jsonrpc.git 24 | 25 | library 26 | default-language: Haskell2010 27 | hs-source-dirs: src 28 | 29 | exposed-modules: 30 | Servant.Client.JsonRpc 31 | 32 | build-depends: 33 | aeson >= 1.3 && < 2.3 34 | , base >= 4.11 && < 5.0 35 | , servant >= 0.14 && < 0.21 36 | , servant-client-core >= 0.14 && < 0.21 37 | , servant-jsonrpc >= 1.2 && < 1.3 38 | -------------------------------------------------------------------------------- /servant-jsonrpc-client/src/Servant/Client/JsonRpc.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE FlexibleContexts #-} 2 | {-# LANGUAGE FlexibleInstances #-} 3 | {-# LANGUAGE MultiParamTypeClasses #-} 4 | {-# LANGUAGE ScopedTypeVariables #-} 5 | {-# LANGUAGE TypeApplications #-} 6 | {-# LANGUAGE TypeFamilies #-} 7 | {-# LANGUAGE TypeOperators #-} 8 | {-# LANGUAGE UndecidableInstances #-} 9 | {-# OPTIONS_GHC -fno-warn-orphans #-} 10 | 11 | {- | 12 | Module: Servant.Client.JsonRpc 13 | 14 | This module provides support for generating JSON-RPC clients in the Servant framework. 15 | 16 | > type Mul = JsonRpc "mul" (Int, Int) String Int 17 | > mul :: (Int, Int) -> ClientM (JsonRpcResponse String Int) 18 | > mul = client $ Proxy @Mul 19 | 20 | Note: This client implementation runs over HTTP and the semantics of HTTP 21 | remove the need for the message id. 22 | -} 23 | module Servant.Client.JsonRpc ( 24 | module Servant.JsonRpc, 25 | ) where 26 | 27 | import Data.Proxy (Proxy (..)) 28 | import GHC.TypeLits (KnownSymbol, symbolVal) 29 | import Servant.API (MimeRender, MimeUnrender, NoContent, (:<|>)) 30 | import Servant.Client.Core (HasClient (..), RunClient) 31 | 32 | import Servant.JsonRpc 33 | 34 | -- | The 'RawJsonRpc' construct is completely transparent to clients 35 | instance 36 | (RunClient m, HasClient m (RawJsonRpc ctype apiL), HasClient m (RawJsonRpc ctype apiR)) => 37 | HasClient m (RawJsonRpc ctype (apiL :<|> apiR)) 38 | where 39 | type Client m (RawJsonRpc ctype (apiL :<|> apiR)) = Client m (RawJsonRpc ctype apiL :<|> RawJsonRpc ctype apiR) 40 | clientWithRoute pxm _ = clientWithRoute pxm (Proxy @(RawJsonRpc ctype apiL :<|> RawJsonRpc ctype apiR)) 41 | hoistClientMonad pxm _ = hoistClientMonad pxm (Proxy @(RawJsonRpc ctype apiL :<|> RawJsonRpc ctype apiR)) 42 | 43 | instance 44 | ( RunClient m 45 | , KnownSymbol method 46 | , MimeRender ctype (Request p) 47 | , MimeUnrender ctype (JsonRpcResponse e r) 48 | ) => 49 | HasClient m (RawJsonRpc ctype (JsonRpc method p e r)) 50 | where 51 | type 52 | Client m (RawJsonRpc ctype (JsonRpc method p e r)) = 53 | p -> m (JsonRpcResponse e r) 54 | 55 | clientWithRoute _ _ req p = 56 | client req jsonRpcRequest 57 | where 58 | client = clientWithRoute (Proxy @m) endpoint 59 | jsonRpcRequest = Request (symbolVal $ Proxy @method) p (Just 0) 60 | 61 | endpoint = Proxy @(JsonRpcEndpoint ctype (JsonRpc method p e r)) 62 | 63 | hoistClientMonad _ _ f x p = f $ x p 64 | 65 | instance 66 | ( RunClient m 67 | , KnownSymbol method 68 | , MimeRender ctype (Request p) 69 | ) => 70 | HasClient m (RawJsonRpc ctype (JsonRpcNotification method p)) 71 | where 72 | type 73 | Client m (RawJsonRpc ctype (JsonRpcNotification method p)) = 74 | p -> m NoContent 75 | 76 | clientWithRoute _ _ req p = 77 | client req jsonRpcRequest 78 | where 79 | client = clientWithRoute (Proxy @m) endpoint 80 | jsonRpcRequest = Request (symbolVal $ Proxy @method) p Nothing 81 | 82 | endpoint = Proxy @(JsonRpcEndpoint ctype (JsonRpcNotification method p)) 83 | 84 | hoistClientMonad _ _ f x p = f $ x p 85 | -------------------------------------------------------------------------------- /servant-jsonrpc-examples/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Revision history for servant-jsonrpc-examples 2 | 3 | ## 0.1.0.0 -- YYYY-mm-dd 4 | 5 | * First version. Released on an unsuspecting world. 6 | -------------------------------------------------------------------------------- /servant-jsonrpc-examples/LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2020 Bitnomial, Inc. 2 | 3 | Redistribution and use in source and binary forms, with or without 4 | modification, are permitted provided that the following conditions are 5 | met: 6 | 7 | 1. Redistributions of source code must retain the above copyright 8 | notice, this list of conditions and the following disclaimer. 9 | 10 | 2. Redistributions in binary form must reproduce the above copyright 11 | notice, this list of conditions and the following disclaimer in the 12 | documentation and/or other materials provided with the distribution. 13 | 14 | 3. Neither the name of the copyright holder nor the names of its 15 | contributors may be used to endorse or promote products derived from 16 | this software without specific prior written permission. 17 | 18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 19 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 20 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 21 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 22 | HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 23 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 24 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 25 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 26 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 27 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 28 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -------------------------------------------------------------------------------- /servant-jsonrpc-examples/Setup.hs: -------------------------------------------------------------------------------- 1 | import Distribution.Simple 2 | 3 | main = defaultMain 4 | -------------------------------------------------------------------------------- /servant-jsonrpc-examples/client/Main.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE TypeApplications #-} 2 | {-# OPTIONS_GHC -Wno-unused-do-bind #-} 3 | 4 | module Main where 5 | 6 | import Control.Monad (void) 7 | import Control.Monad.IO.Class (liftIO) 8 | import Data.Proxy (Proxy (..)) 9 | import Network.HTTP.Client (defaultManagerSettings, newManager) 10 | import Servant.API (NoContent, (:<|>) (..)) 11 | import Servant.Client ( 12 | ClientM, 13 | client, 14 | mkClientEnv, 15 | parseBaseUrl, 16 | runClientM, 17 | ) 18 | import Servant.Client.JsonRpc (JsonRpcResponse) 19 | import Servant.JsonRpc.Example (API, NonEndpoint) 20 | 21 | main :: IO () 22 | main = do 23 | env <- mkClientEnv <$> newManager defaultManagerSettings <*> parseBaseUrl "http://localhost:8080" 24 | void . flip runClientM env $ do 25 | jsonRpcPrint "Starting RPC calls" 26 | liftIO . print =<< add (2, 10) 27 | liftIO . print =<< multiply (2, 10) 28 | 29 | printMessage "Starting REST calls" 30 | liftIO . print =<< getTime 31 | 32 | -- A JSON-RPC error response 33 | print =<< runClientM (launchMissiles 100) env 34 | 35 | add, multiply :: (Int, Int) -> ClientM (JsonRpcResponse String Int) 36 | jsonRpcPrint :: String -> ClientM NoContent 37 | getTime :: ClientM String 38 | printMessage :: String -> ClientM NoContent 39 | (add :<|> multiply :<|> jsonRpcPrint) :<|> (getTime :<|> printMessage) = client $ Proxy @API 40 | 41 | launchMissiles :: Int -> ClientM (JsonRpcResponse String Bool) 42 | launchMissiles = client $ Proxy @NonEndpoint 43 | -------------------------------------------------------------------------------- /servant-jsonrpc-examples/servant-jsonrpc-examples.cabal: -------------------------------------------------------------------------------- 1 | cabal-version: 2.4 2 | 3 | name: servant-jsonrpc-examples 4 | version: 1.1.0 5 | author: Ian Shipman 6 | maintainer: Ian Shipman 7 | synopsis: Example client and server for servant-jsonrpc 8 | homepage: https://github.com/bitnomial/servant-jsonrpc 9 | license: BSD-3-Clause 10 | license-file: LICENSE 11 | copyright: Bitnomial, Inc. (c) 2020 12 | category: Web 13 | build-type: Simple 14 | extra-source-files: CHANGELOG.md 15 | 16 | source-repository head 17 | type: git 18 | location: https://github.com/bitnomial/servant-jsonrpc.git 19 | 20 | 21 | common deps 22 | default-language: Haskell2010 23 | build-depends: 24 | aeson >= 1.3 && < 2.3 25 | , base >= 4.11 && < 5.0 26 | , servant >= 0.14 && < 0.21 27 | , servant-jsonrpc 28 | 29 | library 30 | import: deps 31 | hs-source-dirs: src 32 | exposed-modules: 33 | Servant.JsonRpc.Example 34 | 35 | executable servant-jsonrpc-example-server 36 | import: deps 37 | main-is: Main.hs 38 | hs-source-dirs: server 39 | build-depends: 40 | servant-jsonrpc-examples 41 | , servant-jsonrpc-server 42 | , servant-server 43 | , time 44 | , warp 45 | 46 | executable servant-jsonrpc-example-client 47 | import: deps 48 | main-is: Main.hs 49 | hs-source-dirs: client 50 | build-depends: 51 | servant-jsonrpc-examples 52 | , servant-jsonrpc-client 53 | , http-client 54 | , servant-client 55 | -------------------------------------------------------------------------------- /servant-jsonrpc-examples/server/Main.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE TypeApplications #-} 2 | 3 | module Main where 4 | 5 | import Control.Monad.IO.Class (liftIO) 6 | import Data.Proxy (Proxy (..)) 7 | import Data.Time ( 8 | defaultTimeLocale, 9 | formatTime, 10 | getCurrentTime, 11 | ) 12 | import Network.Wai.Handler.Warp (run) 13 | import Servant.API (NoContent (..), (:<|>) (..)) 14 | import Servant.Server (Handler, Server, serve) 15 | 16 | import Servant.JsonRpc.Example (API) 17 | import Servant.Server.JsonRpc (JsonRpcErr) 18 | 19 | main :: IO () 20 | main = run 8080 $ serve (Proxy @API) server 21 | 22 | server :: Server API 23 | server = (add :<|> multiply :<|> printMessage) :<|> (getTime :<|> printMessage) 24 | 25 | add :: (Int, Int) -> Handler (Either (JsonRpcErr String) Int) 26 | add = return . Right . uncurry (+) 27 | 28 | multiply :: (Int, Int) -> Handler (Either (JsonRpcErr String) Int) 29 | multiply = return . Right . uncurry (*) 30 | 31 | printMessage :: String -> Handler NoContent 32 | printMessage msg = NoContent <$ liftIO (putStrLn msg) 33 | 34 | getTime :: Handler String 35 | getTime = timeString <$> liftIO getCurrentTime 36 | where 37 | timeString = formatTime defaultTimeLocale "%T" 38 | -------------------------------------------------------------------------------- /servant-jsonrpc-examples/src/Servant/JsonRpc/Example.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE DataKinds #-} 2 | {-# LANGUAGE TypeOperators #-} 3 | 4 | module Servant.JsonRpc.Example ( 5 | API, 6 | NonEndpoint, 7 | ) where 8 | 9 | import Servant.API ( 10 | Get, 11 | NoContent, 12 | PlainText, 13 | Post, 14 | ReqBody, 15 | (:<|>) (..), 16 | (:>), 17 | ) 18 | import Servant.JsonRpc (JSONRPC, JsonRpc, JsonRpcNotification, RawJsonRpc) 19 | 20 | type Add = JsonRpc "add" (Int, Int) String Int 21 | type Multiply = JsonRpc "multiply" (Int, Int) String Int 22 | type Print = JsonRpcNotification "print" String 23 | 24 | type RpcAPI = Add :<|> Multiply :<|> Print 25 | 26 | type JsonRpcAPI = "json-rpc" :> RawJsonRpc JSONRPC RpcAPI 27 | 28 | type GetTime = "time" :> Get '[PlainText] String 29 | type PrintMessage = "print" :> ReqBody '[PlainText] String :> Post '[PlainText] NoContent 30 | 31 | type RestAPI = "rest" :> (GetTime :<|> PrintMessage) 32 | 33 | type API = JsonRpcAPI :<|> RestAPI 34 | 35 | type NonEndpoint = "json-rpc" :> RawJsonRpc JSONRPC (JsonRpc "launch-missles" Int String Bool) 36 | -------------------------------------------------------------------------------- /servant-jsonrpc-server/LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2020 Bitnomial, Inc. 2 | 3 | Redistribution and use in source and binary forms, with or without 4 | modification, are permitted provided that the following conditions are 5 | met: 6 | 7 | 1. Redistributions of source code must retain the above copyright 8 | notice, this list of conditions and the following disclaimer. 9 | 10 | 2. Redistributions in binary form must reproduce the above copyright 11 | notice, this list of conditions and the following disclaimer in the 12 | documentation and/or other materials provided with the distribution. 13 | 14 | 3. Neither the name of the copyright holder nor the names of its 15 | contributors may be used to endorse or promote products derived from 16 | this software without specific prior written permission. 17 | 18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 19 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 20 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 21 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 22 | HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 23 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 24 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 25 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 26 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 27 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 28 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -------------------------------------------------------------------------------- /servant-jsonrpc-server/Setup.hs: -------------------------------------------------------------------------------- 1 | import Distribution.Simple 2 | 3 | main = defaultMain 4 | -------------------------------------------------------------------------------- /servant-jsonrpc-server/changelog.md: -------------------------------------------------------------------------------- 1 | # 2.2.0 2 | 3 | * Accept either `application/json` or `application/json-rpc` from the client and let the client choose the content type for a response 4 | 5 | # 2.1.1 6 | 7 | * Relax version bounds 8 | * Remove dependency on `mtl` 9 | 10 | # 2.1.0 11 | 12 | * Relax upper version bounds for `aeson` to `(>= 1.3 && < 1.6)` 13 | * Relax upper version bounds for `base` to `(>= 4.11 && < 5.0)` 14 | * Relax upper version bounds for `servant` to `(>= 0.14 && < 0.19)` 15 | * Relax upper version bounds for `servant-server` to `(>= 0.14 && < 0.19)` 16 | 17 | # 2.0.0 18 | 19 | The previous version was hopelessly broken. This completely replaces it. 20 | 21 | # 1.0.0 22 | 23 | First release 24 | -------------------------------------------------------------------------------- /servant-jsonrpc-server/servant-jsonrpc-server.cabal: -------------------------------------------------------------------------------- 1 | cabal-version: 2.4 2 | 3 | name: servant-jsonrpc-server 4 | version: 2.2.0 5 | author: Ian Shipman 6 | maintainer: Ian Shipman 7 | 8 | synopsis: JSON-RPC servant servers 9 | description: 10 | Use this package to define a servant server which exposes JSON-RPC over HTTP endpoints. 11 | 12 | homepage: https://github.com/bitnomial/servant-jsonrpc 13 | license: BSD-3-Clause 14 | license-file: LICENSE 15 | copyright: Bitnomial, Inc. (c) 2020 16 | category: Web 17 | build-type: Simple 18 | 19 | extra-source-files: changelog.md 20 | 21 | source-repository head 22 | type: git 23 | location: https://github.com/bitnomial/servant-jsonrpc.git 24 | 25 | library 26 | default-language: Haskell2010 27 | hs-source-dirs: src 28 | 29 | exposed-modules: 30 | Servant.Server.JsonRpc 31 | 32 | build-depends: 33 | aeson >= 1.3 && < 2.3 34 | , base >= 4.11 && < 5.0 35 | , containers >= 0.5 && < 0.8 36 | , servant >= 0.14 && < 0.21 37 | , servant-jsonrpc >= 1.2 && < 1.3 38 | , servant-server >= 0.14 && < 0.21 39 | -------------------------------------------------------------------------------- /servant-jsonrpc-server/src/Servant/Server/JsonRpc.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE CPP #-} 2 | {-# LANGUAGE DataKinds #-} 3 | {-# LANGUAGE FlexibleContexts #-} 4 | {-# LANGUAGE FlexibleInstances #-} 5 | {-# LANGUAGE LambdaCase #-} 6 | {-# LANGUAGE MultiParamTypeClasses #-} 7 | {-# LANGUAGE RankNTypes #-} 8 | {-# LANGUAGE ScopedTypeVariables #-} 9 | {-# LANGUAGE TypeApplications #-} 10 | {-# LANGUAGE TypeFamilies #-} 11 | {-# LANGUAGE TypeOperators #-} 12 | 13 | #if MIN_VERSION_servant_server(0,18,0) 14 | {-# LANGUAGE UndecidableInstances #-} 15 | #endif 16 | 17 | {-# OPTIONS_GHC -fno-warn-orphans #-} 18 | 19 | {- | 20 | Module: Servant.Server.JsonRpc 21 | 22 | This module provides support for writing handlers for JSON-RPC endpoints 23 | 24 | > type Mul = JsonRpc "mul" (Int, Int) String Int 25 | > mulHandler :: (Int, Int) -> Handler (Either (JsonRpcErr String) Int) 26 | > mulHandler = _ 27 | 28 | > type Add = JsonRpc "add" (Int, Int) String Int 29 | > addHandler :: (Int, Int) -> Handler (Either (JsonRpcErr String) Int) 30 | > addHandler = _ 31 | 32 | > type API = Add :<|> Mul 33 | > server :: Application 34 | > server = serve (Proxy @(RawJsonRpc API)) $ addHandler :<|> mulHandler 35 | -} 36 | module Servant.Server.JsonRpc ( 37 | serveJsonRpc, 38 | RouteJsonRpc (..), 39 | module Servant.JsonRpc, 40 | PossibleContent, 41 | PossibleJsonRpcResponse, 42 | ) where 43 | 44 | import Data.Aeson (FromJSON (..), ToJSON (..), Value) 45 | import Data.Aeson.Types (parseEither) 46 | import Data.Bifunctor (bimap) 47 | import Data.Kind (Type) 48 | import Data.Map.Strict (Map) 49 | import qualified Data.Map.Strict as Map 50 | import Data.Proxy (Proxy (..)) 51 | import GHC.TypeLits (KnownSymbol, symbolVal) 52 | import Servant.API ( 53 | NoContent (..), 54 | Post, 55 | ReqBody, 56 | (:<|>) (..), 57 | (:>), 58 | JSON, 59 | ) 60 | import Servant.API.ContentTypes (AllCTRender (..)) 61 | 62 | #if MIN_VERSION_servant_server(0,18,0) 63 | import Servant.Server ( 64 | DefaultErrorFormatters, 65 | ErrorFormatters, 66 | Handler, 67 | HasContextEntry, 68 | HasServer (..), 69 | type (.++), 70 | ) 71 | #elif MIN_VERSION_servant_server(0,14,0) 72 | import Servant.Server (Handler, HasServer (..)) 73 | #endif 74 | 75 | import Servant.JsonRpc 76 | 77 | {- | Since we collapse an entire JSON RPC api down to a single Servant 78 | endpoint, we need a type that /can/ return content but might not. 79 | -} 80 | data PossibleContent a = SomeContent a | EmptyContent 81 | 82 | instance (ToJSON a) => AllCTRender '[JSONRPC] (PossibleContent a) where 83 | handleAcceptH px h = \case 84 | SomeContent x -> handleAcceptH px h x 85 | EmptyContent -> handleAcceptH px h NoContent 86 | 87 | type PossibleJsonRpcResponse = PossibleContent (JsonRpcResponse Value Value) 88 | 89 | instance ToJSON a => ToJSON (PossibleContent a) where 90 | toJSON = \case 91 | SomeContent x -> toJSON x 92 | EmptyContent -> toJSON () 93 | 94 | type RawJsonRpcEndpoint = 95 | ReqBody '[JSONRPC, JSON] (Request Value) 96 | :> Post '[JSONRPC, JSON] PossibleJsonRpcResponse 97 | 98 | #if MIN_VERSION_servant_server(0,18,0) 99 | instance 100 | (RouteJsonRpc api, HasContextEntry (context .++ DefaultErrorFormatters) ErrorFormatters) => 101 | HasServer (RawJsonRpc ctype api) context 102 | where 103 | #elif MIN_VERSION_servant_server(0,14,0) 104 | instance (RouteJsonRpc api) => HasServer (RawJsonRpc ctype api) context where 105 | #endif 106 | type ServerT (RawJsonRpc ctype api) m = RpcHandler api m 107 | route _ cx = route endpoint cx . fmap (serveJsonRpc pxa pxh) 108 | where 109 | endpoint = Proxy @RawJsonRpcEndpoint 110 | pxa = Proxy @api 111 | pxh = Proxy @Handler 112 | 113 | hoistServerWithContext _ _ f x = hoistRpcRouter (Proxy @api) f x 114 | 115 | -- | This internal class is how we accumulate a map of handlers for dispatch 116 | class RouteJsonRpc a where 117 | type RpcHandler a (m :: Type -> Type) 118 | jsonRpcRouter :: 119 | (Monad m) => 120 | Proxy a -> 121 | Proxy m -> 122 | RpcHandler a m -> 123 | Map String (Value -> m (PossibleContent (Either (JsonRpcErr Value) Value))) 124 | hoistRpcRouter :: Proxy a -> (forall x. m x -> n x) -> RpcHandler a m -> RpcHandler a n 125 | 126 | generalizeResponse :: 127 | (ToJSON e, ToJSON r) => 128 | Either (JsonRpcErr e) r -> 129 | Either (JsonRpcErr Value) Value 130 | generalizeResponse = bimap repack toJSON 131 | where 132 | repack e = e{errorData = toJSON <$> errorData e} 133 | 134 | onDecodeFail :: String -> JsonRpcErr e 135 | onDecodeFail msg = JsonRpcErr invalidParamsCode msg Nothing 136 | 137 | instance (KnownSymbol method, FromJSON p, ToJSON e, ToJSON r) => RouteJsonRpc (JsonRpc method p e r) where 138 | type RpcHandler (JsonRpc method p e r) m = p -> m (Either (JsonRpcErr e) r) 139 | 140 | jsonRpcRouter _ _ h = Map.fromList [(methodName, h')] 141 | where 142 | methodName = symbolVal $ Proxy @method 143 | onDecode = fmap generalizeResponse . h 144 | 145 | h' = 146 | fmap SomeContent 147 | . either (return . Left . onDecodeFail) onDecode 148 | . parseEither parseJSON 149 | 150 | hoistRpcRouter _ f x = f . x 151 | 152 | instance (KnownSymbol method, FromJSON p) => RouteJsonRpc (JsonRpcNotification method p) where 153 | type RpcHandler (JsonRpcNotification method p) m = p -> m NoContent 154 | 155 | jsonRpcRouter _ _ h = Map.fromList [(methodName, h')] 156 | where 157 | methodName = symbolVal $ Proxy @method 158 | onDecode x = EmptyContent <$ h x 159 | 160 | h' = 161 | either (return . SomeContent . Left . onDecodeFail) onDecode 162 | . parseEither parseJSON 163 | 164 | hoistRpcRouter _ f x = f . x 165 | 166 | instance (RouteJsonRpc a, RouteJsonRpc b) => RouteJsonRpc (a :<|> b) where 167 | type RpcHandler (a :<|> b) m = RpcHandler a m :<|> RpcHandler b m 168 | 169 | jsonRpcRouter _ pxm (ha :<|> hb) = jsonRpcRouter pxa pxm ha <> jsonRpcRouter pxb pxm hb 170 | where 171 | pxa = Proxy @a 172 | pxb = Proxy @b 173 | 174 | hoistRpcRouter _ f (x :<|> y) = hoistRpcRouter (Proxy @a) f x :<|> hoistRpcRouter (Proxy @b) f y 175 | 176 | {- | This function is the glue required to convert a collection of 177 | handlers in servant standard style to the handler that 'RawJsonRpc' 178 | expects. 179 | -} 180 | serveJsonRpc :: 181 | (Monad m, RouteJsonRpc a) => 182 | Proxy a -> 183 | Proxy m -> 184 | RpcHandler a m -> 185 | Request Value -> 186 | m PossibleJsonRpcResponse 187 | serveJsonRpc px pxm hs (Request m v ix') 188 | | Just h <- Map.lookup m hmap = 189 | h v >>= \case 190 | SomeContent (Right x) 191 | | Just ix <- ix' -> return . SomeContent $ Result ix x 192 | | otherwise -> return . SomeContent $ Errors ix' invalidRequest 193 | SomeContent (Left e) -> return . SomeContent $ Errors ix' e 194 | EmptyContent -> return EmptyContent 195 | | otherwise = return . SomeContent $ Errors ix' missingMethod 196 | where 197 | missingMethod = JsonRpcErr methodNotFoundCode ("Unknown method: " <> m) Nothing 198 | hmap = jsonRpcRouter px pxm hs 199 | invalidRequest = JsonRpcErr invalidRequestCode "Missing id" Nothing 200 | -------------------------------------------------------------------------------- /servant-jsonrpc/LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2020 Bitnomial, Inc. 2 | 3 | Redistribution and use in source and binary forms, with or without 4 | modification, are permitted provided that the following conditions are 5 | met: 6 | 7 | 1. Redistributions of source code must retain the above copyright 8 | notice, this list of conditions and the following disclaimer. 9 | 10 | 2. Redistributions in binary form must reproduce the above copyright 11 | notice, this list of conditions and the following disclaimer in the 12 | documentation and/or other materials provided with the distribution. 13 | 14 | 3. Neither the name of the copyright holder nor the names of its 15 | contributors may be used to endorse or promote products derived from 16 | this software without specific prior written permission. 17 | 18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 19 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 20 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 21 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 22 | HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 23 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 24 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 25 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 26 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 27 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 28 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -------------------------------------------------------------------------------- /servant-jsonrpc/Setup.hs: -------------------------------------------------------------------------------- 1 | import Distribution.Simple 2 | 3 | main = defaultMain 4 | -------------------------------------------------------------------------------- /servant-jsonrpc/changelog.md: -------------------------------------------------------------------------------- 1 | # 1.2.0 2 | 3 | * Adds a type parameter to the `JsonRpc` endpoint wrapper to enable the user to choose the content-type in the derived client. 4 | 5 | # 1.1.1 6 | 7 | * Allow "application/json-rpc" as the content type 8 | * Accept string for the `id` field 9 | 10 | # 1.1.0 11 | 12 | * Relax upper version bounds for `aeson` to `(>= 1.3 && < 1.6)` 13 | * Relax upper version bounds for `base` to `(>= 4.11 && < 5.0)` 14 | * Relax upper version bounds for `servant` to `(>= 0.14 && < 0.19)` 15 | 16 | # 1.0.1 17 | 18 | Adds standard error codes to the library. 19 | 20 | # 1.0.0 21 | 22 | First release 23 | -------------------------------------------------------------------------------- /servant-jsonrpc/servant-jsonrpc.cabal: -------------------------------------------------------------------------------- 1 | cabal-version: 2.4 2 | 3 | name: servant-jsonrpc 4 | version: 1.2.0 5 | author: Ian Shipman 6 | maintainer: Ian Shipman 7 | 8 | synopsis: JSON-RPC messages and endpoints 9 | description: 10 | This module contains types that a programmer can use for JSON-RPC requests, 11 | server responses, and errors. It also contains two types which should be 12 | used in concert with servant to compose type level API specifications for 13 | JSON-RPC endpoints. 14 | 15 | homepage: https://github.com/bitnomial/servant-jsonrpc 16 | license: BSD-3-Clause 17 | license-file: LICENSE 18 | copyright: Bitnomial, Inc. (c) 2020 19 | category: Web 20 | build-type: Simple 21 | 22 | extra-source-files: changelog.md 23 | 24 | source-repository head 25 | type: git 26 | location: https://github.com/bitnomial/servant-jsonrpc.git 27 | 28 | library 29 | default-language: Haskell2010 30 | hs-source-dirs: src 31 | 32 | exposed-modules: 33 | Servant.JsonRpc 34 | 35 | build-depends: 36 | aeson >= 1.3 && < 2.3 37 | , base >= 4.11 && < 5.0 38 | , http-media >= 0.7.1.3 && < 0.9 39 | , servant >= 0.14 && < 0.21 40 | , text >= 1.2 && < 2.2 41 | -------------------------------------------------------------------------------- /servant-jsonrpc/src/Servant/JsonRpc.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE DataKinds #-} 2 | {-# LANGUAGE FlexibleInstances #-} 3 | {-# LANGUAGE MultiParamTypeClasses #-} 4 | {-# LANGUAGE OverloadedStrings #-} 5 | {-# LANGUAGE ScopedTypeVariables #-} 6 | {-# LANGUAGE TypeApplications #-} 7 | {-# LANGUAGE TypeFamilies #-} 8 | {-# LANGUAGE TypeOperators #-} 9 | {-# LANGUAGE UndecidableInstances #-} 10 | 11 | {- | 12 | Module: Servant.JsonRpc 13 | 14 | Work with JSON-RPC protocol messages at both type and value level. 15 | 16 | > type Mul = JsonRpc "mul" (Int, Int) String Int 17 | > 18 | > req :: Request (Int, Int) 19 | > req = Request "mul" (3, 5) (Just 0) 20 | > 21 | > rsp :: JsonRpcResponse String Int 22 | > rsp = Result 0 15 23 | -} 24 | module Servant.JsonRpc ( 25 | -- * API specification types 26 | RawJsonRpc, 27 | JsonRpc, 28 | JsonRpcNotification, 29 | JSONRPC, 30 | 31 | -- * JSON-RPC messages 32 | Request (..), 33 | JsonRpcResponse (..), 34 | JsonRpcErr (..), 35 | 36 | -- ** Standard error codes 37 | parseErrorCode, 38 | invalidRequestCode, 39 | methodNotFoundCode, 40 | invalidParamsCode, 41 | internalErrorCode, 42 | 43 | -- * Type rewriting 44 | JsonRpcEndpoint, 45 | ) where 46 | 47 | import Control.Applicative (liftA3, (<|>)) 48 | import Data.Aeson ( 49 | FromJSON (..), 50 | ToJSON (..), 51 | Value (Null), 52 | object, 53 | withObject, 54 | (.:), 55 | (.:?), 56 | (.=), 57 | ) 58 | import Data.Aeson.Types (Parser) 59 | import Data.List.NonEmpty (NonEmpty (..)) 60 | import Data.Maybe (isNothing) 61 | import Data.Proxy (Proxy (..)) 62 | import Data.Text.Read (decimal) 63 | import Data.Word (Word64) 64 | import GHC.TypeLits (Symbol) 65 | import Network.HTTP.Media ((//)) 66 | import Servant.API ( 67 | Accept (..), 68 | JSON, 69 | MimeRender (..), 70 | MimeUnrender (..), 71 | NoContent, 72 | Post, 73 | ReqBody, 74 | (:>), 75 | ) 76 | 77 | -- | Client messages 78 | data Request p 79 | = Request 80 | { method :: String 81 | , params :: p 82 | , requestId :: Maybe Word64 83 | -- ^ should be omitted only if the message is a notification, with no response content 84 | } 85 | deriving (Eq, Show) 86 | 87 | instance (ToJSON p) => ToJSON (Request p) where 88 | toJSON (Request m p ix) = 89 | object 90 | . maybe id (onValue "id") ix 91 | $ [ "jsonrpc" .= ("2.0" :: String) 92 | , "method" .= m 93 | , "params" .= p 94 | ] 95 | where 96 | onValue n v = ((n .= v) :) 97 | 98 | instance (FromJSON p) => FromJSON (Request p) where 99 | parseJSON = withObject "JsonRpc Request" $ \obj -> do 100 | ix <- obj .:? "id" 101 | m <- obj .: "method" 102 | p <- obj .: "params" 103 | v <- obj .: "jsonrpc" 104 | 105 | versionGuard v . pure $ Request m p ix 106 | 107 | versionGuard :: Maybe String -> Parser a -> Parser a 108 | versionGuard v x 109 | | v == Just "2.0" = x 110 | | isNothing v = x 111 | | otherwise = fail "unknown version" 112 | 113 | {- | Server messages. An 'Ack' is a message which refers to a 'Request' but 114 | both its "errors" and "result" keys are null 115 | -} 116 | data JsonRpcResponse e r 117 | = Result Word64 r 118 | | Ack Word64 119 | | Errors (Maybe Word64) (JsonRpcErr e) 120 | deriving (Eq, Show) 121 | 122 | data JsonRpcErr e = JsonRpcErr 123 | { errorCode :: Int 124 | , errorMessage :: String 125 | , errorData :: Maybe e 126 | } 127 | deriving (Eq, Show) 128 | 129 | parseErrorCode :: Int 130 | parseErrorCode = -32700 131 | 132 | invalidRequestCode :: Int 133 | invalidRequestCode = -32600 134 | 135 | methodNotFoundCode :: Int 136 | methodNotFoundCode = -32601 137 | 138 | invalidParamsCode :: Int 139 | invalidParamsCode = -32602 140 | 141 | internalErrorCode :: Int 142 | internalErrorCode = -32603 143 | 144 | instance (FromJSON e, FromJSON r) => FromJSON (JsonRpcResponse e r) where 145 | parseJSON = withObject "Response" $ \obj -> do 146 | ix <- obj .: "id" <|> (obj .: "id" >>= parseDecimalString) 147 | version <- obj .:? "jsonrpc" 148 | result <- obj .:? "result" 149 | err <- obj .:? "error" 150 | versionGuard version $ pack ix result err 151 | where 152 | parseDecimalString = either fail (pure . fmap fst) . traverse decimal 153 | 154 | pack (Just ix) (Just r) Nothing = pure $ Result ix r 155 | pack ix Nothing (Just e) = Errors ix <$> parseErr e 156 | pack (Just ix) Nothing Nothing = pure $ Ack ix 157 | pack _ _ _ = fail "invalid response" 158 | 159 | parseErr = 160 | withObject "Error" $ 161 | liftA3 JsonRpcErr <$> (.: "code") <*> (.: "message") <*> (.:? "data") 162 | 163 | instance (ToJSON e, ToJSON r) => ToJSON (JsonRpcResponse e r) where 164 | toJSON (Result ix r) = 165 | object 166 | [ "jsonrpc" .= ("2.0" :: String) 167 | , "result" .= r 168 | , "id" .= ix 169 | ] 170 | toJSON (Ack ix) = 171 | object 172 | [ "jsonrpc" .= ("2.0" :: String) 173 | , "id" .= ix 174 | , "result" .= Null 175 | , "error" .= Null 176 | ] 177 | toJSON (Errors ix (JsonRpcErr c msg err)) = 178 | object 179 | [ "jsonrpc" .= ("2.0" :: String) 180 | , "id" .= ix 181 | , "error" .= detail 182 | ] 183 | where 184 | detail = 185 | object 186 | [ "code" .= c 187 | , "message" .= msg 188 | , "data" .= err 189 | ] 190 | 191 | -- | A JSON RPC server handles any number of methods. Represent this at the type level using this type. 192 | data RawJsonRpc ctype api 193 | 194 | -- | JSON-RPC endpoints which respond with a result 195 | data JsonRpc (method :: Symbol) p e r 196 | 197 | -- | JSON-RPC endpoints which do not respond 198 | data JsonRpcNotification (method :: Symbol) p 199 | 200 | type family JsonRpcEndpoint ctype a where 201 | JsonRpcEndpoint ctype (JsonRpc m p e r) = 202 | ReqBody '[ctype] (Request p) :> Post '[ctype] (JsonRpcResponse e r) 203 | JsonRpcEndpoint ctype (JsonRpcNotification m p) = 204 | ReqBody '[ctype] (Request p) :> Post '[ctype] NoContent 205 | 206 | -- | The JSON-RPC content type 207 | data JSONRPC 208 | 209 | instance Accept JSONRPC where 210 | contentTypes _ = "application" // "json-rpc" :| ["application" // "json"] 211 | 212 | instance (ToJSON a) => MimeRender JSONRPC a where 213 | mimeRender _ = mimeRender (Proxy @JSON) 214 | 215 | instance (FromJSON a) => MimeUnrender JSONRPC a where 216 | mimeUnrender _ = mimeUnrender (Proxy @JSON) 217 | -------------------------------------------------------------------------------- /stack.yaml: -------------------------------------------------------------------------------- 1 | resolver: lts-21.22 2 | 3 | packages: 4 | - servant-jsonrpc 5 | - servant-jsonrpc-client 6 | - servant-jsonrpc-examples 7 | - servant-jsonrpc-server 8 | -------------------------------------------------------------------------------- /stack.yaml.lock: -------------------------------------------------------------------------------- 1 | # This file was autogenerated by Stack. 2 | # You should not edit this file by hand. 3 | # For more information, please see the documentation at: 4 | # https://docs.haskellstack.org/en/stable/lock_files 5 | 6 | packages: [] 7 | snapshots: 8 | - completed: 9 | sha256: afd5ba64ab602cabc2d3942d3d7e7dd6311bc626dcb415b901eaf576cb62f0ea 10 | size: 640060 11 | url: https://raw.githubusercontent.com/commercialhaskell/stackage-snapshots/master/lts/21/22.yaml 12 | original: lts-21.22 13 | --------------------------------------------------------------------------------