@@ -16,42 +16,85 @@ and field reads and assignments back to the contained object.
1616If this explanation is confusing, don't worry. It's easier to understand a flow
1717in 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 = [[
2436import Flow from require "lapis.flow"
2537
2638class 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
3249The 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 = [[
4175obj = {
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
5091on 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
63106you can use ` expose_assigns ` . It's a class property that tells the flow how to
64107handle 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 = [[
83169import 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
97184flows. In this case, the contained object will be the request instance. You'll
98185call the flow from within your application. Because this is a common pattern,
99186there's a ` flow ` method on the request object that makes instantiating flows
100187easy.
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 = [[
106225import Flow from require "lapis.flow"
226+
107227class 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
120248The 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 = [[
123266class 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
140297my_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