Skip to content

Commit dcbc23d

Browse files
committed
write the flow docs
1 parent 6ff0fde commit dcbc23d

File tree

2 files changed

+249
-67
lines changed

2 files changed

+249
-67
lines changed

docs/actions.md

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1050,9 +1050,11 @@ $options_table{
10501050

10511051
### `request:flow(module_name)`
10521052

1053-
Loads a flow by `module_name` with the `flows_prefix` on the current request
1054-
object. If the flow with that name has been previously loaded, the existing
1055-
flow instance is returned.
1053+
Loads a [Flow]($root/reference/flows.html) by `module_name` with the
1054+
`flows_prefix` on the current request object. If the flow with that name has
1055+
been previously loaded, the existing flow instance is returned. See the [Flows
1056+
guide]($root/reference/flows.html) for more information about using flows to
1057+
organize your application.
10561058

10571059
### `request:html(fn)`
10581060

docs/flows.md

Lines changed: 244 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -16,42 +16,85 @@ and field reads and assignments back to the contained object.
1616
If this explanation is confusing, don't worry. It's easier to understand a flow
1717
in example. We'll use the `Flow` class standalone to demonstrate how it works.
1818

19-
```lua
20-
-- todo
21-
```
22-
23-
```moon
19+
$dual_code{
20+
lua = [[
21+
local Flow = require("lapis.flow").Flow
22+
23+
local FormatterFlow = Flow:extend({
24+
format_name = function(self)
25+
-- self.name and self.age are read from the contained object
26+
return self.name .. " (age: " .. self.age .. ")"
27+
end,
28+
29+
print_greeting = function(self)
30+
-- self:get_greeting() calls get_greeting on the contained object
31+
print(self:get_greeting())
32+
end
33+
})
34+
]],
35+
moon = [[
2436
import Flow from require "lapis.flow"
2537

2638
class FormatterFlow extends Flow
2739
format_name: =>
40+
-- @name and @age are read from the contained object
2841
"#{@name} (age: #{@age})"
2942

30-
```
43+
print_greeting: =>
44+
-- @get_greeting! calls get_greeting on the contained object
45+
print @get_greeting!
46+
]]
47+
}
3148

3249
The above flow provides a `format_name` method that reads the `name` and `age`
33-
fields. We can instantiate a flow with an object that provides those fields,
34-
then call that method on the flow instance:
35-
36-
```lua
37-
-- todo
38-
```
50+
fields from the contained object. When you access a field or call a method on
51+
`self` that doesn't exist on the flow, it is automatically proxied to the
52+
contained object. When calling a method on the contained object, the receiver
53+
is the contained object itself, not the flow.
54+
55+
We can instantiate a flow with an object that provides those fields, then call
56+
the flow's method on the flow instance:
57+
58+
$dual_code{
59+
lua = [[
60+
local obj = {
61+
name = "Pizza Zone",
62+
age = "2000 Years",
63+
get_greeting = function(self)
64+
-- self will always be obj, not a flow instance, even if called through a
65+
-- flow
66+
return "Hello from " .. self.name
67+
end
68+
}
3969

40-
```moon
70+
local flow = FormatterFlow(obj)
71+
print(flow:format_name()) --> "Pizza Zone (age: 2000 Years)"
72+
flow:print_greeting() --> "Hello from Pizza Zone"
73+
]],
74+
moon = [[
4175
obj = {
4276
name: "Pizza Zone"
4377
age: "2000 Years"
78+
get_greeting: =>
79+
-- @ will always be obj, not a flow instance, even if called through a
80+
-- flow
81+
"Hello from #{@name}"
4482
}
4583

46-
print FormatterFlow(obj)\format_name!
47-
```
84+
flow = FormatterFlow(obj)
85+
print flow\format_name! --> "Pizza Zone (age: 2000 Years)"
86+
flow\print_greeting! --> "Hello from Pizza Zone"
87+
]]
88+
}
4889

49-
You can think of a flow of a collection of methods that are designed to operate
90+
You can think of a flow as a collection of methods that are designed to operate
5091
on a certain kind of object. Why would we use a flow instead of just making
51-
these methods part of the objects class? A flow lets you encapsulate logic into
52-
a separate namespace. Instead of having classes with many methods, you split
53-
apart your methods into flows and leave the class with a smaller
54-
implementation.
92+
these methods part of the object's class? A flow lets you encapsulate logic
93+
into a separate namespace. Instead of having classes with many methods, you
94+
split apart your methods into flows and leave the class with a smaller
95+
implementation. This can help your code stay more organized and also make it
96+
easier to unit-test individual code paths without having to mock and entire
97+
request.
5598

5699
## Assigning Fields Within a Flow
57100

@@ -63,89 +106,226 @@ If you want assignments on `self` to be sent back to the original class then
63106
you can use `expose_assigns`. It's a class property that tells the flow how to
64107
handle assignments to self.
65108

66-
`expose_assigns` can take two types of values:
109+
`expose_assigns` can take two types of values:
67110

68111
* `true` -- all assignments are proxied back to the contained object
69112
* An array of strings -- any field name contained in this array is proxied back to the object
70113

114+
Here's an example using an array to selectively expose certain fields:
115+
116+
$dual_code{
117+
lua = [[
118+
local Flow = require("lapis.flow").Flow
119+
120+
local MyFlow = Flow:extend({
121+
expose_assigns = {"user", "session"},
122+
123+
setup = function(self)
124+
self.user = fetch_user() -- proxied to contained object
125+
self.session = get_session() -- proxied to contained object
126+
self.cache = {} -- stored on flow instance (private)
127+
end
128+
})
129+
]],
130+
moon = [[
131+
import Flow from require "lapis.flow"
132+
133+
class MyFlow extends Flow
134+
@expose_assigns: {"user", "session"}
135+
136+
setup: =>
137+
@user = fetch_user! -- proxied to contained object
138+
@session = get_session! -- proxied to contained object
139+
@cache = {} -- stored on flow instance (private)
140+
]]
141+
}
142+
143+
This pattern is helpful when you have a Flow operating on a Lapis Request
144+
object where you want to set up fields on the request that may be made
145+
available to views or other parts of the request handler.
146+
71147
## Accessing The Contained Object
72148

73-
The contained object is stored on `self` with the name `_` (an underscore). You
74-
should avoid writing to this field since the flow expects it to exist.
149+
The contained object is stored on `self` with the name `_` (an underscore).
150+
Consider it a reserved field for the flow to operate correctly, don't replace
151+
it it, but you can access it.
75152

76-
For example, you can call `tostring` on the contained object like this:
153+
For example, if you need to access the metatable on the contained object for
154+
some reason:
77155

78-
```lua
79-
-- todo
80-
```
156+
$dual_code{
157+
lua = [[
158+
local Flow = require("lapis.flow").Flow
81159

82-
```moon
160+
local MetatableFlow = Flow:extend({
161+
get_metatable = function(self)
162+
return getmetatable(self._)
163+
end
164+
})
165+
166+
print(MetatableFlow({}):get_metatable())
167+
]],
168+
moon = [[
83169
import Flow from require "lapis.flow"
84170

85-
class StringFlow extends Flow
86-
address: =>
87-
tostring @_
171+
class MetatableFlow extends Flow
172+
get_metatable: =>
173+
getmetatable @_
88174

89-
print StringFlow({})\address!
90-
```
175+
print MetatableFlow({})\get_metatable!
176+
]]
177+
}
91178

92179
## Organizing Your Application With Flows
93180

94-
in lapis, an application class is where you define routes and your request
95-
logic. since there are so many responsibilities it easy for an applicatoin
96-
class to get too large to maintain. a good way of separating concerns is to use
181+
In Lapis, an application class is where you define routes and your request
182+
logic. Since there are so many responsibilities it's easy for an application
183+
class to get too large to maintain. A good way of separating concerns is to use
97184
flows. In this case, the contained object will be the request instance. You'll
98185
call the flow from within your application. Because this is a common pattern,
99186
there's a `flow` method on the request object that makes instantiating flows
100187
easy.
101188

102-
In this example we declare a flow class for handling logging in and registering
103-
on a website. From our applicaton we call the flow:
104-
105-
```moon
189+
In this example, we declare a flow class for handling logging in and
190+
registering on a website. Logging in and registering an account may share code,
191+
so we can use additional flow methods to encapsulate our logic without
192+
repeating ourselves.
193+
194+
From our application we call the flow:
195+
196+
$dual_code{
197+
lua = [[
198+
local Flow = require("lapis.flow").Flow
199+
200+
local AccountsFlow = Flow:extend({
201+
check_params = function(self)
202+
-- validate self.params...
203+
end,
204+
205+
write_session = function(self, user)
206+
-- store user in session...
207+
end,
208+
209+
login = function(self)
210+
self:check_params()
211+
-- load user from database...
212+
self:write_session(user)
213+
return { redirect_to = self:url_for("homepage") }
214+
end,
215+
216+
register = function(self)
217+
self:check_params()
218+
-- create user in database...
219+
self:write_session(user)
220+
return { redirect_to = self:url_for("homepage") }
221+
end
222+
})
223+
]],
224+
moon = [[
106225
import Flow from require "lapis.flow"
226+
107227
class AccountsFlow extends Flow
228+
check_params: =>
229+
-- validate @params...
230+
231+
write_session: (user) =>
232+
-- store user in session...
233+
108234
login: =>
109-
-- check parameters
110-
-- create the session
111-
redirect_to: @url_for("homepage")
235+
@check_params!
236+
-- load user from database...
237+
@write_session user
238+
redirect_to: @url_for "homepage"
112239

113240
register: =>
114-
-- check parameters
115-
-- create the account, or return error
116-
-- create a session
117-
redirect_to: @url_for("homepage")
118-
```
241+
@check_params!
242+
-- create user in database...
243+
@write_session user
244+
redirect_to: @url_for "homepage"
245+
]]
246+
}
119247

120248
The structure of your application could then be:
121249

122-
```moon
250+
$dual_code{
251+
lua = [[
252+
local lapis = require("lapis")
253+
local capture_errors = require("lapis.application").capture_errors
254+
255+
local app = lapis.Application()
256+
257+
app:match("login", "/login", capture_errors(function(self)
258+
return self:flow("accounts"):login()
259+
end))
260+
261+
app:match("register", "/register", capture_errors(function(self)
262+
return self:flow("accounts"):register()
263+
end))
264+
]],
265+
moon = [[
123266
class App extends lapis.Application
124267
[login: "/login"]: capture_errors => @flow("accounts")\login!
125268
[register: "/register"]: capture_errors => @flow("accounts")\register!
126-
```
269+
]]
270+
}
127271

128272
## Nested Flows
129273

130-
When you instantiate a flow from within a flow, the backing object is wrapped
131-
directly by the the new flow. This means that the current flow's methods are
132-
not made available to the new flow.
274+
When you instantiate a flow and pass an existing flow as the argument, the
275+
backing object is passed directly into the new flow. This means that the
276+
current flow's methods are not made available to the new flow.
277+
278+
$dual_code{
279+
lua = [[
280+
local Flow = require("lapis.flow").Flow
281+
282+
local my_object = { color = "blue" }
133283

134-
```lua
135-
-- todo
136-
```
284+
local FlowA = Flow:extend({})
285+
local FlowB = Flow:extend({})
137286

287+
local flow_a = FlowA(my_object)
288+
local flow_b = FlowB(flow_a) -- passing flow_a, not my_object
289+
290+
-- flow_a and flow_b both point to my_object
291+
assert(flow_a._ == my_object)
292+
assert(flow_b._ == my_object)
293+
]],
294+
moon = [[
295+
import Flow from require "lapis.flow"
138296

139-
```moon
140297
my_object = { color: "blue" }
141298

142-
class SubFlow extends flow
143-
check_object: =>
144-
assert my_object == @_
299+
class FlowA extends Flow
300+
class FlowB extends Flow
301+
302+
flow_a = FlowA my_object
303+
flow_b = FlowB flow_a -- passing flow_a, not my_object
304+
305+
-- flow_a and flow_b both point to my_object
306+
assert(flow_a._ == my_object)
307+
assert(flow_b._ == my_object)
308+
]]
309+
}
145310

146-
class OuterFlow extends flow
147-
get_sub: => subflow @
311+
## Utility Functions
148312

149-
OuterFlow(my_object)\get_sub!\check_object!
150-
```
313+
### `is_flow(cls)`
151314

315+
The `is_flow` function checks if a class or instance is a Flow:
316+
317+
$dual_code{
318+
lua = [[
319+
local is_flow = require("lapis.flow").is_flow
320+
321+
if is_flow(some_object) then
322+
-- handle flow...
323+
end
324+
]],
325+
moon = [[
326+
import is_flow from require "lapis.flow"
327+
328+
if is_flow some_object
329+
-- handle flow...
330+
]]
331+
}

0 commit comments

Comments
 (0)