summaryrefslogtreecommitdiff
path: root/desk/lib/rudder.hoon
blob: 45eaf30737e650384b9bb22c881e58b7a03e66d0 (plain)
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
::  rudder: framework for routing & serving simple web frontends
::
::      v1.0.2: newborn helmsman
::
::    the primary usage pattern involves your app calling steer:rudder
::    with a configuration, then calling the resulting gate with an
::    incoming request and relevant context.
::
::      %.  [bowl [eyre-id inbound-request] dat]
::      %-  (steer:rudder _dat cmd)
::      [pages route adlib solve]
::
::    dat    is app state passed into and transformed by the frontend code.
::    cmd    is the type of app actions that the frontend may produce.
::    pages  is a (map term (page _dat cmd)), contains per-view frontend logic.
::    route  is a routing function, turning a url query into a $place.
::    adlib  gets called with the full request when no route is found.
::    solve  is a function that applies a cmd resulting from a POST request.
::
::    the library provides some default implementations for route and adlib,
::    which you can construct using +point and +fours respectively.
::
::    for examples and a more detailed description of handling http requests,
::    see /lib/rudder/poke-example.hoon
::
::    pages implement a bundle of view logic, each implementing a door
::    with three arms.
::
::    +build  gets called for GET requests, producing a $reply to render.
::    +argue  gets called for POST requests, turning it into a cmd.
::    +final  gets called after POST requests, producing a $reply to render.
::
::    for examples and a more detailed description of implementing a page,
::    see /lib/rudder/page-example.hoon
::
::TODO
::  - should rudder really be falling back to generic error messages when
::    calling +final after failure? what if apps/pages want to provide
::    their own generic error message?
::  - in the full-default setup, the behavior of +alert is a little bit
::    awkward. because +point forces routes to omit trailing slashes,
::    you cannot refer to "the current page" in a consistent way.
::    you have to either hardcode the page name, or pass the full url
::    from the inbound-request.
::    a router that forces inclusion of trailing slashes would let you
::    use '.', but has unconventional url semantics, and doesn't mesh
::    nicely with single-level routing.
::  - some inconsistency between the expected output of +adlib and +solve.
::    "briefless" +solve results may be common, so it's nice that they're
::    easy to write. for +adlib that probably isn't as relevant, and
::    the current factoring makes for a nice =^ in the lib code, but...
::    on the other hand, they're still different output types semantically,
::    so inconsistency isn't the end of the world. would have to see how
::    this ends up looking in practice.
::  - +argue is awkward because its function signature doesn't really work
::    if the cmd type is an atom.
::  - maybe unsupported methods should go to the fallback too?
::  - currently ambiguous: do you catch would-fail actions during +argue,
::    or in +solve? might be best to catch earlier, but this splits
::    or duplicates business logic between app and pages...
::
|%
+|  %types  ::  outputs, inputs, function signatures
::
+$  reply
  $%  [%page bod=manx]                                  ::  html page
      [%xtra hed=header-list:http bod=manx]             ::  html page w/ heads
      [%next loc=@t msg=brief]                          ::  303, succeeded
      [%move loc=@t]                                    ::  308, use other
      [%auth loc=@t]                                    ::  307, please log in
      [%code cod=@ud msg=brief]                         ::  error code page
      [%full ful=simple-payload:http]                   ::  full payload
  ==
::
+$  place
  $%  [%page ath=? nom=term]                            ::  serve from pages
      [%away loc=(list @t)]                             ::  308, redirect
  ==
::
+$  query
  $:  trail
      args=(list [key=@t value=@t])
  ==
::
+$  trail
  [ext=(unit @ta) site=(list @t)]
::
+$  order  [id=@ta inbound-request:eyre]
+$  route  $-(trail (unit place))
+$  brief  ?(~ @t)
::
++  page
  |*  [dat=mold cmd=mold]
  $_  ^|
  |_  [bowl:gall order dat]
  ++  build  |~([(list [k=@t v=@t]) (unit [? @t])] *reply)
  ++  argue  |~([header-list:http (unit octs)] *$@(brief cmd))
  ++  final  |~([success=? msg=brief] *reply)
  --
::
+$  card  card:agent:gall
::  pilot: core server logic
::
+|  %pilot
::
++  steer  ::  main helper constructor
  |*  [dat=mold cmd=mold]
  |^  serve
  +$  page   (^page dat cmd)
  +$  adlib  $-(order [[(unit reply) (list card)] dat])
  +$  solve  $-(cmd $@(brief [brief (list card) dat]))
  ::
  ++  serve  ::  main helper
    |=  [pages=(map @ta page) =route =adlib =solve]
    |=  [=bowl:gall =order =dat]
    ^-  (quip card _dat)
    =*  id  id.order
    =+  (purse url.request.order)
    =/  target=(unit place)
      (route -<)
    ::  if there is no route, fall back to adlib
    ::
    ?~  target
      =^  [res=(unit reply) caz=(list card)]  dat
        (adlib order)
      :_  dat
      ?~  res  caz
      (weld (spout id (paint u.res)) caz)
    ::  route might be a redirect
    ::
    ?:  ?=(%away -.u.target)
      =+  (rap 3 '/' (join '/' loc.u.target))
      [(spout id (paint %move -)) dat]
    ::  route might require authentication
    ::
    ?:  &(ath.u.target !authenticated.order)
      [(spout id (paint %auth url.request.order)) dat]
    ::  route might have messed up and pointed to nonexistent page
    ::
    ?.  (~(has by pages) nom.u.target)
      [(spout id (issue 404 (cat 3 'no such page: ' nom.u.target))) dat]
    ::
    %.  [bowl order dat]
    (apply (~(got by pages) nom.u.target) solve)
  ::
  ++  apply  ::  page usage helper
    |=  [=page =solve]
    |=  [=bowl:gall =order =dat]
    ^-  (quip card _dat)
    =.  page  ~(. page bowl order dat)
    =*  id    id.order
    ?+  method.request.order
      [(spout id (issue 405 ~)) dat]
    ::
        %'GET'
      :_  dat
      =+  (purse url.request.order)
      =^  msg  args
        ::NOTE  as set by %next replies
        ?~  msg=(get-header:http 'rmsg' args)  [~ args]
        [`[& u.msg] (delete-header:http 'rmsg' args)]
      %+  spout  id
      (paint (build:page args msg))
    ::
        %'POST'
      ?@  act=(argue:page [header-list body]:request.order)
        :_  dat
        =?  act  ?=(~ act)  'failed to parse request'
        (spout id (paint (final:page | act)))
      ?@  res=(solve act)
        :_  dat
        =?  act  ?=(~ act)  'failed to process request'
        (spout id (paint (final:page | res)))
      :_  +>.res
      =.  +<+>.page  +>.res
      (weld (spout id (paint (final:page & -.res))) +<.res)
    ==
  --
::  easy: hands-off steering behavior
::
+|  %easy
::
++  point  ::  simple single-level routing, +route
  |=  [base=(lest @t) auth=? have=(set term)]
  ^-  route
  |=  trail
  ^-  (unit place)
  ?~  site=(decap base site)  ~
  ?-  u.site
    ~           `[%page auth %index]
    [~ ~]       `[%away (snip ^site)]
    [%index ~]  `[%away (snip ^site)]
    [@ ~]       ?:((~(has in have) i.u.site) `[%page auth i.u.site] ~)
    [@ ~ ~]     `[%away (snip ^site)]
    *           ~
  ==
::
++  fours  ::  simple 404 responses, +adlib
  |*  dat=*
  ::  ^-  adlib:(rest * _dat)
  |=  *
  [[`[%code 404 'no route found'] ~] dat]
::
++  alert  ::  simple redirecting +final handler
  |=  [next=@t build=$-([(list [@t @t]) (unit [? @t])] reply)]
  |=  [done=? =brief]
  ^-  reply
  ?:  done  [%next next brief]
  (build ~ `[| `@t`brief])
::  cargo: payload generation
::
+|  %cargo
::
++  paint  ::  render response
  |=  =reply
  ^-  simple-payload:http
  ?-  -.reply
    %page  [[200 ['content-type' 'text/html']~] `(press bod.reply)]
    %xtra  =?  hed.reply  ?=(~ (get-header:http 'content-type' hed.reply))
             ['content-type'^'text/html' hed.reply]
           [[200 hed.reply] `(press bod.reply)]
    %next  =;  loc  [[303 ['location' loc]~] ~]
           ?~  msg.reply  loc.reply
           %+  rap  3
           :~  loc.reply
               ?:(?=(^ (find "?" (trip loc.reply))) '&' '?')
               'rmsg='
               (crip (en-urlt:html (trip msg.reply)))
           ==
    %move  [[308 ['location' loc.reply]~] ~]
    %auth  =/  loc  (crip (en-urlt:html (trip loc.reply)))
           [[307 ['location' (cat 3 '/~/login?redirect=' loc)]~] ~]
    %code  (issue +.reply)
    %full  ful.reply
  ==
::
++  issue  ::  render status code page
  |=  [cod=@ud msg=brief]
  ^-  simple-payload:http
  :-  [cod ~]
  =;  nom=@t
    `(as-octs:mimes:html (rap 3 ~[(scot %ud cod) ': ' nom '\0a' msg]))
  ?+  cod  ''
    %400  'bad request'
    %404  'not found'
    %405  'method not allowed'
    %500  'internal server error'
  ==
::  utils: fidgeting
::
+|  %utils
::
++  decap  ::  strip leading base from full site path
  |=  [base=(list @t) site=(list @t)]
  ^-  (unit (list @t))
  ?~  base  `site
  ?~  site  ~
  ?.  =(i.base i.site)  ~
  $(base t.base, site t.site)
::
++  frisk  ::  parse url-encoded form args
  |=  body=@t
  %-  ~(gas by *(map @t @t))
  (fall (rush body yquy:de-purl:html) ~)
::
::NOTE  the below (and $query) are also available in /lib/server.hoon,
::      but we reimplement them here for independence's sake.
::
++  purse  ::  url cord to query
  |=  url=@t
  ^-  query
  (fall (rush url ;~(plug apat:de-purl:html yque:de-purl:html)) [[~ ~] ~])
::
++  press  ::  manx to octs
  (cork en-xml:html as-octt:mimes:html)
::
++  spout  ::  build full response cards
  |=  [eyre-id=@ta simple-payload:http]
  ^-  (list card)
  =/  =path  /http-response/[eyre-id]
  :~  [%give %fact ~[path] [%http-response-header !>(response-header)]]
      [%give %fact ~[path] [%http-response-data !>(data)]]
      [%give %kick ~[path] ~]
  ==
--