/
Snaplet.hs
338 lines (278 loc) · 10.7 KB
/
Snaplet.hs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
{-# LANGUAGE BangPatterns #-}
{-# LANGUAGE ExistentialQuantification #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE Rank2Types #-}
{-# LANGUAGE TypeOperators #-}
{-|
Snaplets allow you to build web applications out of composable parts. This
allows you to build self-contained units and glue them together to make your
overall application.
A snaplet has a few moving parts, some user-defined and some provided by the
snaplet API:
* each snaplet has its own configuration given to it at startup.
* each snaplet is given its own directory on the filesystem, from which it
reads its configuration and in which it can store files.
* each snaplet comes with an 'Initializer' which defines how to create an
instance of the Snaplet at startup. The initializer decides how to
interpret the snaplet configuration, which URLs to handle (and how), sets
up the initial snaplet state, tells the snaplet runtime system how to
clean the snaplet up, etc.
* each snaplet contains some user-defined in-memory state; for instance, a
snaplet that talks to a database might contain a reference to a connection
pool. The snaplet state is an ordinary Haskell record, with a datatype
defined by the snaplet author. The initial state record is created during
initialization and is available to snaplet 'Handler's when serving HTTP
requests.
NOTE: This documentation is written as a prose tutorial of the snaplets
API. Don't be scared by the fact that it's auto-generated and is filled with
type signatures. Just keep reading.
-}
module Snap.Snaplet
(
-- * Snaplet
-- $snapletDoc
Snaplet
, SnapletConfig
-- * Lenses
-- $lenses
-- * Snaplet Helper Functions
-- $snapletHelpers
, snapletConfig
, snapletValue
, subSnaplet
-- * MonadSnaplet
-- $monadSnaplet
, MonadSnaplet(..)
, getSnapletAncestry
, getSnapletFilePath
, getSnapletName
, getSnapletDescription
, getSnapletUserConfig
, getSnapletRootURL
, getRoutePattern
, setRoutePattern
-- * Snaplet state manipulation
-- $snapletState
, getSnapletState
, putSnapletState
, modifySnapletState
, getsSnapletState
-- , wrap
-- , wrapTop
-- * Initializer
-- $initializer
, Initializer
, SnapletInit
, makeSnaplet
, nestSnaplet
, embedSnaplet
, nameSnaplet
, onUnload
, addPostInitHook
, addPostInitHookBase
, printInfo
, getRoutes
-- * Routes
-- $routes
, addRoutes
, wrapSite
-- * Handlers
, Handler
, reloadSite
, bracketHandler
-- * Serving Applications
, runSnaplet
, combineConfig
, serveSnaplet
, loadAppConfig
-- * Snaplet Lenses
, SnapletLens
) where
import Snap.Snaplet.Internal.Initializer
import Snap.Snaplet.Internal.Types
-- $snapletDoc
-- The heart of the snaplets infrastructure is state management. (Note: when
-- we say \"state\" here, we mean in-memory Haskell objects, not external data
-- storage or databases; how you deal with persisted data is up to you.) Most
-- nontrivial pieces of a web application need some kind of runtime state or
-- environment data. The datatype we use to handle this is called 'Snaplet':
-- $snapletHelpers
--
-- Your web application will itself get wrapped in a 'Snaplet', and the
-- top-level user state of your application (which will likely contain other
-- snaplets nested inside it) will look something like this:
--
-- > data App = App
-- > { _foo :: Snaplet Foo
-- > , _bar :: Snaplet Bar
-- > , _someNonSnapletData :: String
-- > }
--
-- Every web application using snaplets has a top-most user state which
-- contains all of the application state; we call this state the \"base\"
-- state.
--
-- We export several helper lenses for working with Snaplet types.
-- $lenses
-- In the example above, the @Foo@ snaplet has to be written to work with any
-- base state (otherwise it wouldn't be reusable!), but functions written to
-- work with the @Foo@ snaplet want to be able to modify the @Foo@ record
-- /within the context/ of the base state. Given that Haskell datatypes are
-- pure, how do you allow for this?
--
-- Our solution is to use /lenses/, as defined in Edward Kmett's @lens@
-- library (<http://hackage.haskell.org/package/lens>). A lens, notated
-- as follows:
--
-- > SimpleLens a b
--
-- is conceptually a \"getter\" and a \"setter\" rolled up into one. The
-- @lens@ library provides the following functions:
--
-- > view :: (SimpleLens a b) -> a -> b
-- > set :: (SimpleLens a b) -> b -> a -> a
-- > over :: (SimpleLens a b) -> (b -> b) -> a -> a
--
-- which allow you to get, set, and modify a value of type @b@ within the
-- context of type @a@. The @lens@ package comes with a Template Haskell
-- function called 'makeLenses', which auto-magically defines a lens for every
-- record field having a name beginning with an underscore. In the @App@
-- example above, adding the declaration:
--
-- > makeLenses ''App
--
-- would define lenses:
--
-- > foo :: SimpleLens App (Snaplet Foo)
-- > bar :: SimpleLens App (Snaplet Bar)
-- > someNonSnapletData :: SimpleLens App String
--
-- The coolest thing about @lens@ lenses is that they /compose/ using the
-- @(.)@ operator. If the @Foo@ type had a field of type @Quux@ within it with
-- a lens @quux :: SimpleLens Foo Quux@, then you could create a lens of type
-- @SimpleLens App Quux@ by composition:
--
-- > import Control.Lens
-- >
-- > data Foo = Foo { _quux :: Quux }
-- > makeLenses ''Foo
-- >
-- > -- snapletValue is defined in the framework:
-- > snapletValue :: SimpleLens (Snaplet a) a
-- >
-- > appQuuxLens :: SimpleLens App Quux
-- > appQuuxLens = foo . snapletValue . quux
--
-- Lens composition is very similar to function composition except it works in
-- the opposite direction (think Java-style System.out.println ordering) and
-- it gives you a composed getter and setter at the same time.
-- $monadSnaplet
-- The primary abstraction in the snaplet infrastructure is a combination of
-- the reader and state monads. The state monad holds the top level
-- application data type (from now on referred to as the base state). The
-- reader monad holds a lens from the base state to the current snaplet's
-- state. This allows quux snaplet functions to access and modify the Quux
-- data structure without knowing anything about the App or Foo data
-- structures. It also lets other snaplets call functions from the quux
-- snaplet if they have the quux snaplet's lens @SimpleLens App (Snaplet Quux)@.
-- We can view our application as a tree of snaplets and other pieces of data.
-- The lenses are like pointers to nodes of the tree. If you have a pointer to
-- a node, you can access the node and all of its children without knowing
-- anything about the rest of the tree.
--
-- Several monads use this infrastructure. These monads need at least three
-- type parameters. Two for the lens type, and the standard \'a\' denoting the
-- monad return value. You will usually see this written in type signatures as
-- \"m b v a\" or some variation. The \'m\' is the type variable of the
-- MonadSnaplet type class. \'b\' is the base state, and \'v\' is the state of
-- the current \"view\" snaplet (or simply, current state).
--
-- The MonadSnaplet type class distills the essence of the operations used
-- with this pattern. Its functions define fundamental methods for navigating
-- snaplet trees.
-- $snapletState
-- MonadSnaplet instances will typically have @MonadState v@ instances. We
-- provide the following convenience functions which give the equivalent to
-- @MonadState (Snaplet v)@ for the less common cases where you need to work
-- with the Snaplet wrapper.
-- $initializer
-- The Initializer monad is where your application's initialization happens.
-- Initializers are run at startup and any time a site reload is triggered.
-- The Initializer's job is to construct a snaplet's routes and initial state,
-- set up filesystem data, read config files, etc.
--
-- In order to initialize its state, a snaplet needs to initialize all the
-- @Snaplet a@ state for each of its subsnaplets. The only way to construct
-- a @Snaplet a@ type is by calling 'nestSnaplet' or 'embedSnaplet' from
-- within an initializer.
-- $writingSnaplets
-- When writing a snaplet, you must define an initializer function. The
-- initializer function for the Foo snaplet (where Foo is the snaplet's
-- state type) must have a return type of @Initializer b Foo Foo@.
-- To create an initializer like this, you have to use the 'makeSnaplet'
-- function. It takes care of the necessary internal bookkeeping needed when
-- initializing a new snaplet. Haskell's strong type system allows us to
-- ensure that calling 'makeSnaplet' is the only way you can construct a
-- Snaplet type.
-- $routes
-- Snaplet initializers are also responsible for setting up any routes defined
-- by the snaplet. To do that you'll usually use either 'addRoutes' or
-- 'wrapSite'.
{-
/FIXME/: finish this section
Discuss:
* lenses and how snaplet apps are built out of parts.
* the initializer type, and what you can do with it
* layout of snaplets on disk, and how on-disk stuff can be auto-populated
from the cabal data directory
* the handler type, and what you can do with it
{FIXME: strike/rewrite these sentences. Components that do not need any kind
of state or environment are probably more appropriate as a standalone
library than as a snaplet.
We start our application by defining a data structure to hold the state.
This data structure includes the state of any snaplets (wrapped in a
Snaplet) we want to use as well as any other state we might want.}
> module MyApp where
> import Control.Lens
> import Snap.Snaplet
> import Snap.Snaplet.Heist
>
> data App = App
> { _heist :: Snaplet (Heist App)
> , _foo :: Snaplet Foo
> , _bar :: Snaplet Bar
> , _companyName :: String
> }
>
> makeLenses ''App
The next thing we need to do is define an initializer.
> app :: Initializer App App App
> app = do
> hs <- nestSnaplet "heist" $ heistInit "templates"
> fs <- nestSnaplet "foo" $ fooInit heist
> bs <- nestSnaplet "" $ nameSnaplet "baz" $ barInit heist
> addRoutes [ ("/hello", writeText "hello world")
> ]
> wrapSite (<|> with heist heistServe)
> return $ App hs fs bs "fooCorp"
Then we define a simple main to run the application.
> main = serveSnaplet defaultConfig app
Snaplet filesystem directory structure:
> snaplet
> |-- snaplet.cabal
> |-- log/
> |-- src/
> ------------------------
> |-- db.cfg
> |-- snaplet.cfg
> |-- public/
> |-- stylesheets/
> |-- images/
> |-- js/
> |-- snaplets
> |-- subsnaplet1/
> |-- subsnaplet2/
> |-- templates/
-}