Skip to content

Commit 2a954b9

Browse files
authored
fetch example: use servant-client-js, remove Miso.Fetch (#904)
1 parent 64f147b commit 2a954b9

File tree

11 files changed

+56
-179
lines changed

11 files changed

+56
-179
lines changed

README.md

+18
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@
5050
- [Internals](#internals)
5151
- [Examples](#examples)
5252
- [Building examples](#building-examples)
53+
- [Coverage](#interacting-with-http-apis)
5354
- [Coverage](#coverage)
5455
- [Isomorphic](#isomorphic)
5556
- [Benchmarks](#benchmarks)
@@ -578,6 +579,23 @@ cd result/bin/todo-mvc.jsexe && http-sever
578579
Serving HTTP on 0.0.0.0 port 8000 ...
579580
```
580581

582+
## Interacting with HTTP APIs 🔌
583+
584+
If you want to interact with an HTTP API, we recommend one of the following approaches:
585+
586+
1. For a simple JSON-based API, you can use Miso's `fetchJSON` function.
587+
588+
2. In more complex cases, you can define a [Servant](https://www.servant.dev/) API and automatically obtain client functions via `servant-client-js` (or any other `servant-client-core`-based backend).
589+
590+
The Fetch example ([Source](https://github.com/dmjio/miso/blob/master/examples/fetch/Main.hs), [Demo](https://fetch.haskell-miso.org/)) demonstrates the necessary ingredients. Make sure to add the following to your `cabal.project`:
591+
592+
```cabal
593+
source-repository-package
594+
type: git
595+
location: https://github.com/amesgen/servant-client-js
596+
tag: 2853fb4f26175f51ae7b9aaf0ec683c45070d06e
597+
```
598+
581599
## Coverage ✅
582600
583601
The core engine of `miso` is the [diff](https://github.com/dmjio/miso/blob/master/ts/dom.ts) function. It is responsible for all DOM manipulation that occurs in a miso application and has [100% code coverage](http://coverage.haskell-miso.org). Tests and coverage made possible using [bun](https://github.com/oven-sh/bun).

cabal.project

+7-1
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ packages:
55
haskell-miso.org/
66
sample-app/
77

8-
index-state: 2025-04-29T21:50:43Z
8+
index-state: 2025-05-01T20:28:51Z
99

1010
allow-newer:
1111
all:base
@@ -36,3 +36,9 @@ if arch(wasm32)
3636
location: https://github.com/haskell-wasm/foundation.git
3737
tag: 8e6dd48527fb429c1922083a5030ef88e3d58dd3
3838
subdir: basement
39+
40+
-- for the fetch example
41+
source-repository-package
42+
type: git
43+
location: https://github.com/amesgen/servant-client-js
44+
tag: 2853fb4f26175f51ae7b9aaf0ec683c45070d06e

examples/fetch/Main.hs

+11-7
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
{-# LANGUAGE CPP #-}
33
{-# LANGUAGE DataKinds #-}
44
{-# LANGUAGE DeriveGeneric #-}
5+
{-# LANGUAGE LambdaCase #-}
56
{-# LANGUAGE RecordWildCards #-}
67
{-# LANGUAGE TypeApplications #-}
78
{-# LANGUAGE OverloadedStrings #-}
@@ -23,6 +24,7 @@ import GHC.Generics
2324
import Language.Javascript.JSaddle (JSM)
2425
import Data.Proxy
2526
import Servant.API
27+
import Servant.Client.JS (ClientEnv (..), ClientError, parseBaseUrl, client, runClientM)
2628
----------------------------------------------------------------------------
2729
import Miso hiding (defaultOptions)
2830
import Miso.String
@@ -68,15 +70,17 @@ type GithubAPI = Get '[JSON] GitHub
6870
----------------------------------------------------------------------------
6971
-- | Uses servant to reify type-safe calls to the Fetch API
7072
getGithubAPI
71-
:: (GitHub -> JSM ())
72-
-- ^ Successful callback
73-
-> (MisoString -> JSM ())
74-
-- ^ Errorful callback
75-
-> JSM ()
76-
getGithubAPI = fetch (Proxy @GithubAPI) "https://api.github.com"
73+
:: JSM (Either ClientError GitHub)
74+
getGithubAPI = do
75+
baseUrl <- parseBaseUrl "https://api.github.com"
76+
runClientM c (ClientEnv baseUrl)
77+
where
78+
c = Servant.Client.JS.client (Proxy @GithubAPI)
7779
----------------------------------------------------------------------------
7880
updateModel :: Action -> Effect Model Action
79-
updateModel FetchGitHub = withSink $ \snk -> getGithubAPI (snk . SetGitHub) (snk . ErrorHandler)
81+
updateModel FetchGitHub = scheduleIO $ getGithubAPI <&> \case
82+
Right r -> SetGitHub r
83+
Left e -> ErrorHandler $ ms $ show e
8084
updateModel (SetGitHub apiInfo) =
8185
info ?= apiInfo
8286
updateModel (ErrorHandler msg) =

examples/miso-examples.cabal

+2-1
Original file line numberDiff line numberDiff line change
@@ -166,7 +166,8 @@ executable fetch
166166
jsaddle,
167167
miso,
168168
mtl,
169-
servant
169+
servant,
170+
servant-client-js
170171

171172
executable canvas2d
172173
import:

miso.cabal

+1-1
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ common jsaddle
6565

6666
if arch(wasm32)
6767
build-depends:
68-
jsaddle-wasm >= 0.1 && < 0.2
68+
jsaddle-wasm >= 0.1.1 && < 0.2
6969

7070
common client
7171
if impl(ghcjs) || arch(javascript)

nix/haskell/packages/ghc/default.nix

+2
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ self: super:
1717
jsaddle = self.callCabal2nix "jsaddle" "${source.jsaddle}/jsaddle" {};
1818
jsaddle-warp =
1919
dontCheck (self.callCabal2nix "jsaddle-warp" "${source.jsaddle}/jsaddle-warp" {});
20+
servant-client-core = doJailbreak super.servant-client-core;
21+
servant-client-js = self.callCabal2nix "servant-client-js" source.servant-client-js {};
2022

2123
/* cruft */
2224
crypton = dontCheck super.crypton;

nix/haskell/packages/ghcjs/default.nix

+2
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ self: super:
1212
/* examples */
1313
sample-app-js = self.callCabal2nix "app" source.sample-app {};
1414
jsaddle = self.callCabal2nix "jsaddle" "${source.jsaddle}/jsaddle" {};
15+
servant-client-core = doJailbreak super.servant-client-core;
16+
servant-client-js = self.callCabal2nix "servant-client-js" source.servant-client-js {};
1517
flatris = self.callCabal2nix "flatris" source.flatris {};
1618
miso-plane-core = self.callCabal2nix "miso-plane" source.miso-plane {};
1719
miso-plane = pkgs.runCommand "miso-plane" {} ''

nix/legacy/haskell/packages/ghc/default.nix

+1
Original file line numberDiff line numberDiff line change
@@ -12,4 +12,5 @@ self: super:
1212

1313
jsaddle = self.callCabal2nix "jsaddle" "${source.jsaddle}/jsaddle" {};
1414
jsaddle-warp = dontCheck (self.callCabal2nix "jsaddle-warp" "${source.jsaddle}/jsaddle-warp" {});
15+
servant-client-js = self.callCabal2nix "servant-client-js" source.servant-client-js {};
1516
}

nix/legacy/haskell/packages/ghcjs/default.nix

+1
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ self: super:
1010
sample-app-js = self.callCabal2nix "app" source.sample-app {};
1111
jsaddle = self.callCabal2nix "jsaddle" "${source.jsaddle}/jsaddle" {};
1212
jsaddle-warp = dontCheck (self.callCabal2nix "jsaddle-warp" "${source.jsaddle}/jsaddle-warp" {});
13+
servant-client-js = self.callCabal2nix "servant-client-js" source.servant-client-js {};
1314
flatris = self.callCabal2nix "flatris" source.flatris {};
1415
miso-plane =
1516
let

nix/source.nix

+6
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,12 @@ in
3737
rev = "0d5e427cb99391179b143dc93dfbac9c1019237b";
3838
sha256 = "sha256-jyJ7bdz0gNLOSzRxOWcv7eWGIwo3N/O4PcY7HyNF8Fo=";
3939
};
40+
servant-client-js = fetchFromGitHub {
41+
owner = "amesgen";
42+
repo = "servant-client-js";
43+
rev = "3ff9ad6906ebeeae52a7eaa31f7026790a59769a";
44+
hash = "sha256-7x2bxbm2cyuzhotXtdQ0jwfc0aMzjQ/fxDfHjmVvivQ=";
45+
};
4046
flatris = fetchFromGitHub {
4147
owner = "dmjio";
4248
repo = "hs-flatris";

src/Miso/Fetch.hs

+5-169
Original file line numberDiff line numberDiff line change
@@ -20,178 +20,14 @@
2020
-- Portability : non-portable
2121
--
2222
-- Module for interacting with the Fetch API <https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API>
23-
-- with a servant-style interface.
23+
-- manually.
2424
--
25-
-- > import Miso (fetch)
26-
-- > import Data.Proxy
27-
-- > import Servant.API
28-
-- >
29-
-- > data Action
30-
-- > = FetchGitHub
31-
-- > | SetGitHub GitHub
32-
-- > | ErrorHandler MisoString
33-
-- > deriving (Show, Eq)
34-
-- >
35-
-- > type GithubAPI = Get '[JSON] GitHub
36-
-- >
37-
-- > getGithubAPI
38-
-- > :: (GitHub -> JSM ())
39-
-- > -- ^ Successful callback
40-
-- > -> (MisoString -> JSM ())
41-
-- > -- ^ Errorful callback
42-
-- > -> JSM ()
43-
-- > getGithubAPI = fetch (Proxy @GithubAPI) "https://api.github.com"
44-
-- >
45-
-- > updateModel :: Action -> Effect Model Action ()
46-
-- > updateModel FetchGitHub =
47-
-- > withSink $ \snk ->
48-
-- > getGithubAPI (snk . SetGitHub) (snk . ErrorHandler)
49-
-- > updateModel (SetGitHub apiInfo) =
50-
-- > info ?= apiInfo
51-
-- > updateModel (ErrorHandler msg) =
52-
-- > io (consoleError msg)
25+
-- Refer to the miso README if you want to automatically interact with a Servant
26+
-- API.
5327
--
5428
----------------------------------------------------------------------------
5529
module Miso.Fetch
56-
( -- * Class
57-
Fetch (..)
58-
-- ** Simple non-Servant API
59-
, fetchJSON
30+
( fetchJSON
6031
) where
61-
-----------------------------------------------------------------------------
62-
import Data.Aeson
63-
import Data.Kind (Type)
64-
import Data.Proxy (Proxy(..))
65-
import GHC.TypeLits
66-
import Language.Javascript.JSaddle (JSM)
67-
import Servant.API
68-
import Servant.API.Modifiers
69-
-----------------------------------------------------------------------------
70-
import Miso.FFI.Internal (fetchJSON)
71-
import Miso.Lens
72-
import Miso.String (MisoString, ms)
73-
import qualified Miso.String as MS
74-
-----------------------------------------------------------------------------
75-
-- | Internal type used to accumulate options during the type-level traversal
76-
data FetchOptions
77-
= FetchOptions
78-
{ _baseUrl :: MisoString
79-
, _currentPath :: MisoString
80-
, _body :: Maybe MisoString
81-
, _headers :: [(MisoString, MisoString)]
82-
, _queryParams :: [(MisoString,MisoString)]
83-
, _queryFlags :: [MisoString]
84-
}
85-
-----------------------------------------------------------------------------
86-
baseUrl :: Lens FetchOptions MisoString
87-
baseUrl = lens _baseUrl $ \record field -> record { _baseUrl = field }
88-
-----------------------------------------------------------------------------
89-
currentPath :: Lens FetchOptions MisoString
90-
currentPath = lens _currentPath $ \record field -> record { _currentPath = field }
91-
-----------------------------------------------------------------------------
92-
body :: Lens FetchOptions (Maybe MisoString)
93-
body = lens _body $ \record field -> record { _body = field }
94-
-----------------------------------------------------------------------------
95-
headers :: Lens FetchOptions [(MisoString,MisoString)]
96-
headers = lens _headers $ \record field -> record { _headers = field }
97-
-----------------------------------------------------------------------------
98-
queryParams :: Lens FetchOptions [(MisoString,MisoString)]
99-
queryParams = lens _queryParams $ \record field -> record { _queryParams = field }
100-
-----------------------------------------------------------------------------
101-
queryFlags :: Lens FetchOptions [MisoString]
102-
queryFlags = lens _queryFlags $ \record field -> record { _queryFlags = field }
103-
-----------------------------------------------------------------------------
104-
defaultFetchOptions :: FetchOptions
105-
defaultFetchOptions
106-
= FetchOptions
107-
{ _headers = []
108-
, _baseUrl = mempty
109-
, _currentPath = mempty
110-
, _queryParams = []
111-
, _queryFlags = []
112-
, _body = Nothing
113-
}
114-
-----------------------------------------------------------------------------
115-
class Fetch (api :: Type) where
116-
type ToFetch api :: Type
117-
fetch :: Proxy api -> MisoString -> ToFetch api
118-
fetch proxy url = fetchWith proxy (defaultFetchOptions & baseUrl .~ url)
119-
fetchWith :: Proxy api -> FetchOptions -> ToFetch api
120-
-----------------------------------------------------------------------------
121-
instance (Fetch left , Fetch right) => Fetch (left :<|> right) where
122-
type ToFetch (left :<|> right) = ToFetch left :<|> ToFetch right
123-
fetchWith Proxy o = fetchWith (Proxy @left) o :<|> fetchWith (Proxy @right) o
124-
-----------------------------------------------------------------------------
125-
instance (Fetch api, KnownSymbol path) => Fetch (path :> api) where
126-
type ToFetch (path :> api) = ToFetch api
127-
fetchWith Proxy options = fetchWith (Proxy @api) options_
128-
where
129-
path :: MisoString
130-
path = ms $ symbolVal (Proxy @path)
13132

132-
options_ :: FetchOptions
133-
options_ = options & currentPath %~ (<> ms "/" <> path)
134-
-----------------------------------------------------------------------------
135-
instance (ToHttpApiData a, Fetch api, KnownSymbol path) => Fetch (Capture path a :> api) where
136-
type ToFetch (Capture path a :> api) = a -> ToFetch api
137-
fetchWith Proxy options arg = fetchWith (Proxy @api) options_
138-
where
139-
options_ :: FetchOptions
140-
options_ = options & currentPath %~ (<> ms "/" <> ms (toEncodedUrlPiece arg))
141-
-----------------------------------------------------------------------------
142-
instance (ToHttpApiData a, Fetch api, SBoolI (FoldRequired mods), KnownSymbol name) => Fetch (QueryParam' mods name a :> api) where
143-
type ToFetch (QueryParam' mods name a :> api) = RequiredArgument mods a -> ToFetch api
144-
fetchWith Proxy options arg = fetchWith (Proxy @api) options_
145-
where
146-
param (x :: a) = [(ms $ symbolVal (Proxy @name), ms (enc x))]
147-
#if MIN_VERSION_http_api_data(0,5,1)
148-
enc = toEncodedQueryParam
149-
#else
150-
enc = toEncodedUrlPiece
151-
#endif
152-
options_ :: FetchOptions
153-
options_ = options & queryParams <>~ foldRequiredArgument (Proxy @mods) param (foldMap param) arg
154-
-----------------------------------------------------------------------------
155-
instance (Fetch api, KnownSymbol name) => Fetch (QueryFlag name :> api) where
156-
type ToFetch (QueryFlag name :> api) = Bool -> ToFetch api
157-
fetchWith Proxy options flag = fetchWith (Proxy @api) options_
158-
where
159-
options_ :: FetchOptions
160-
options_ = options & queryFlags <>~ [ ms $ symbolVal (Proxy @name) | flag ]
161-
-----------------------------------------------------------------------------
162-
instance (ToJSON a, Fetch api) => Fetch (ReqBody '[JSON] a :> api) where
163-
type ToFetch (ReqBody '[JSON] a :> api) = a -> ToFetch api
164-
fetchWith Proxy options body_ = fetchWith (Proxy @api) (options_ (ms (encode body_)))
165-
where
166-
options_ :: MisoString -> FetchOptions
167-
options_ b = options & body ?~ b
168-
-----------------------------------------------------------------------------
169-
instance (KnownSymbol name, ToHttpApiData a, Fetch api) => Fetch (Header name a :> api) where
170-
type ToFetch (Header name a :> api) = a -> ToFetch api
171-
fetchWith Proxy options value = fetchWith (Proxy @api) o
172-
where
173-
headerName :: MisoString
174-
headerName = ms $ symbolVal (Proxy @name)
175-
176-
o :: FetchOptions
177-
o = options & headers <>~ [ (headerName, ms (toHeader value)) ]
178-
-----------------------------------------------------------------------------
179-
instance (ReflectMethod method, FromJSON a) => Fetch (Verb method code content a) where
180-
type ToFetch (Verb method code content a) = (a -> JSM()) -> (MisoString -> JSM ()) -> JSM ()
181-
fetchWith Proxy options success_ error_ =
182-
fetchJSON url method (options ^. body) (options ^. headers) success_ error_
183-
where
184-
method = ms (reflectMethod (Proxy @method))
185-
params = MS.concat
186-
[ mconcat
187-
[ ms "?"
188-
, MS.intercalate (ms "&")
189-
[ k <> ms "=" <> v
190-
| (k,v) <- options ^. queryParams
191-
]
192-
]
193-
| not $ null (options ^. queryParams)
194-
]
195-
flags = MS.mconcat [ ms "?" <> k | k <- options ^. queryFlags ]
196-
url = options ^. baseUrl <> options ^. currentPath <> params <> flags
197-
-----------------------------------------------------------------------------
33+
import Miso.FFI.Internal (fetchJSON)

0 commit comments

Comments
 (0)