plezi 0.11.0 → 0.11.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +10 -0
- data/lib/plezi.rb +2 -1
- data/lib/plezi/common/cache.rb +2 -2
- data/lib/plezi/common/renderer.rb +138 -0
- data/lib/plezi/handlers/controller_magic.rb +5 -10
- data/lib/plezi/handlers/http_router.rb +19 -40
- data/lib/plezi/helpers/http_sender.rb +10 -11
- data/lib/plezi/helpers/mime_types.rb +1989 -993
- data/lib/plezi/version.rb +1 -1
- data/resources/controller.rb +1 -1
- data/resources/mini_app.rb +10 -7
- data/websocket chatroom.md +65 -70
- metadata +3 -2
data/lib/plezi/version.rb
CHANGED
data/resources/controller.rb
CHANGED
@@ -119,7 +119,7 @@ class SampleController
|
|
119
119
|
# the demo content simply broadcasts the message.
|
120
120
|
def on_message data
|
121
121
|
# broadcast sends an asynchronous message to all sibling instances, but not to self.
|
122
|
-
data =
|
122
|
+
data = ERB::Util.html_escape data
|
123
123
|
broadcast :_print_out, data
|
124
124
|
response << "You said: #{data}"
|
125
125
|
response << (request.ssl? ? "FYI: Yes, This is an SSL connection..." : "FYI: Nope, this isn't an SSL connection (clear text).") if data.match /ssl\?/i
|
data/resources/mini_app.rb
CHANGED
@@ -40,18 +40,21 @@ class MyController
|
|
40
40
|
end
|
41
41
|
# Websockets
|
42
42
|
def on_message data
|
43
|
-
data =
|
44
|
-
|
45
|
-
broadcast :
|
43
|
+
data = ERB::Util.html_escape data
|
44
|
+
print data
|
45
|
+
broadcast :print, data
|
46
46
|
end
|
47
47
|
def on_open
|
48
|
-
|
49
|
-
broadcast :
|
48
|
+
print 'Welcome!'
|
49
|
+
broadcast :print, "Somebody joind us :-)"
|
50
50
|
end
|
51
51
|
def on_close
|
52
|
-
broadcast :
|
52
|
+
broadcast :print, "Somebody left us :-("
|
53
53
|
end
|
54
|
-
|
54
|
+
|
55
|
+
protected
|
56
|
+
|
57
|
+
def print data
|
55
58
|
response << data
|
56
59
|
end
|
57
60
|
end
|
data/websocket chatroom.md
CHANGED
@@ -32,7 +32,7 @@ That's it.
|
|
32
32
|
|
33
33
|
##The Ruby Code (chatroom server)
|
34
34
|
|
35
|
-
We can create an Plezi application using the `$ plezi new myapp`
|
35
|
+
We can create an Plezi application using the `$ plezi new myapp` or `plezi mini myapp` commands, but that's too easy - we want it hardcore.
|
36
36
|
|
37
37
|
Let's create an application folder called `mychat` and save our code in a file called `mychat.rb` in our application folder.
|
38
38
|
|
@@ -73,7 +73,7 @@ A service, in this case, is realy just a nice word for the Plezi server (which m
|
|
73
73
|
|
74
74
|
As you can see, some options are there for later, but are disabled for now.
|
75
75
|
|
76
|
-
- **
|
76
|
+
- **public**: this option defines the folder from which Plezi should serve public static files (html files, images etc'). We will not be serving any static files at the moment, so this option is disabled.
|
77
77
|
|
78
78
|
- **assets**: this option tells plezi where to look for asset files that might need rendering - such as Sass and Coffee-Script files... We will not be using these features either, so that's out as well.
|
79
79
|
|
@@ -83,11 +83,13 @@ As you can see, some options are there for later, but are disabled for now.
|
|
83
83
|
|
84
84
|
- **ssl**: this option, if set to true, will make our service into an SSL/TSL encrypted service (as well as our websocket service)... we can leave this off for now - it's actually hardly ever used since it's usually better to leave that to our production server.
|
85
85
|
|
86
|
+
- **port**: Hardcoding a port would override the default port (which is either 3000 or the default port specified using the `-p <port>`). For this demo, as in most cases, it's best to a avoid setting up a port and preffer the default preferance.
|
87
|
+
|
86
88
|
```ruby
|
87
89
|
service_options = {
|
88
|
-
#
|
90
|
+
# public: Root.join('public').to_s,
|
89
91
|
# assets: Root.join('assets').to_s,
|
90
|
-
# assets_public: '/',
|
92
|
+
# assets_public: '/assets',
|
91
93
|
templates: Root.join('views').to_s,
|
92
94
|
ssl: false
|
93
95
|
}
|
@@ -95,13 +97,13 @@ service_options = {
|
|
95
97
|
|
96
98
|
Next we call the `listen` command - this command actually creates the service.
|
97
99
|
|
98
|
-
The port plezi uses by default is 3000 [http://localhost:3000/](http://localhost:3000/)
|
100
|
+
The port plezi uses by default is either 3000 [http://localhost:3000/](http://localhost:3000/) or the port defined when calling the script (i.e. `./mychat.rb -p 8080`).
|
99
101
|
|
100
102
|
```ruby
|
101
103
|
listen service_options
|
102
104
|
```
|
103
105
|
|
104
|
-
(if you want to force a specific port, i.e. 80, write `listen 80
|
106
|
+
(if you want to force a specific port, i.e. 80, write `listen service_options.merge(port: 80)` - but make sure you are allowed to use this port)
|
105
107
|
|
106
108
|
Last, but not least, we tell Plezi to connect the root of our web application to our ChatController - in other words, make sure the root _path_ ('/') is connected to the ChatController class.
|
107
109
|
|
@@ -109,9 +111,9 @@ Last, but not least, we tell Plezi to connect the root of our web application to
|
|
109
111
|
route '/', ChatController
|
110
112
|
```
|
111
113
|
|
112
|
-
Plezi controller classes are like virtual folders with special support for RESTful methods (`index`, `new`, `save`, `update`, `delete`), HTTP filters and helpers (`before`, `after`, `redirect_to`, `send_data`), WebSockets methods (`on_open`, `on_message(data)`, `on_close`), and WebSockets filters and helpers (`
|
114
|
+
Plezi controller classes are like virtual folders with special support for RESTful methods (`index`, `new`, `save`, `update`, `delete`), HTTP filters and helpers (`before`, `after`, `redirect_to`, `send_data`), WebSockets methods (`on_open`, `on_message(data)`, `on_close`), and WebSockets filters and helpers (`pre_connect`, `broadcast`, `unicast` etc').
|
113
115
|
|
114
|
-
Plezi uses a common special parameter called 'id' to help with all this magic... if we don't define this parameter ourselves, Plezi will try to append this parameter to the end our route's path. So, actually, our route looks like this:
|
116
|
+
Plezi uses a common special parameter called 'id' to help with all this magic... if we don't define this parameter ourselves, Plezi will try to append this parameter as an optional parameter to the end our route's path. So, actually, our route looks like this:
|
115
117
|
|
116
118
|
```ruby
|
117
119
|
route '/(:id)', ChatController
|
@@ -133,7 +135,7 @@ def index
|
|
133
135
|
end
|
134
136
|
```
|
135
137
|
|
136
|
-
Plezi has a really easy method called `render` that creates (and caches) a rendering object with our template file's content and returns a String
|
138
|
+
Plezi has a really easy method called `render` that creates (and caches) a rendering object with our template file's content and returns a String with our rendered template.
|
137
139
|
|
138
140
|
Lets fill in our `index` method:
|
139
141
|
|
@@ -155,36 +157,38 @@ Let's rewrite our `index` method to make it cleaner:
|
|
155
157
|
class ChatController
|
156
158
|
def index
|
157
159
|
response['content-type'] = 'text/html'
|
158
|
-
render(:chat)
|
160
|
+
render(:chat) # since this String is the returned value, it works.
|
159
161
|
end
|
160
162
|
end
|
161
163
|
```
|
162
164
|
|
163
165
|
When someone will visit the root of our application (which is also the '_root_' of our controller), they will get the our ChatController#index method.
|
164
166
|
|
165
|
-
We just need to remember to create a 'chat' template file (`chat.html.erb` or `chat.html.haml`)... but that's for later.
|
167
|
+
We just need to remember to create a 'chat' template file (`chat.html.erb`, `chat.html.slim` or `chat.html.haml`)... but that's for later.
|
166
168
|
|
167
169
|
####Telling people that we made this cool app!
|
168
170
|
|
169
|
-
there is a secret web convention that allows developers to _sign_ their work by answering the `/people` path with plain text and the names of the people who built the site...
|
171
|
+
there is a secret web convention that allows developers to _sign_ their work by answering the `/people.txt` path with plain text and the names of the people who built the site...
|
170
172
|
|
171
173
|
With Plezi, that's super easy.
|
172
174
|
|
173
|
-
Since out ChatController is at the root of
|
175
|
+
Since out ChatController is at the root of our application, let's add a `people.txt` method to our ChatController:
|
176
|
+
|
177
|
+
method names cant normally have the dot in their name, do we will use a helper method for this special name.
|
174
178
|
|
175
179
|
```ruby
|
176
|
-
|
180
|
+
def_special_method "people.txt" do
|
177
181
|
"I wrote this app :)"
|
178
182
|
end
|
179
183
|
```
|
180
184
|
|
181
|
-
Plezi uses the 'id' parameter to recognize special paths as well as for it's RESTful support. Now, anyone visiting '/people' will reach our ChatController#people method.
|
185
|
+
Plezi uses the 'id' parameter to recognize special paths as well as for it's RESTful support. Now, anyone visiting '/people.txt' will reach our ChatController#people method.
|
182
186
|
|
183
|
-
Just like we already discovered, returning a String object (the last line of the `people` method is a String) automatically appends this string to our HTTP response - cool :)
|
187
|
+
Just like we already discovered, returning a String object (the last line of the `people.txt` method is a String) automatically appends this string to our HTTP response - cool :)
|
184
188
|
|
185
189
|
###The Controller - live input and pushing data (WebSockets)
|
186
190
|
|
187
|
-
We are building
|
191
|
+
We are building a somewhat advanced application here - this is _not_ another 'hello world' - lets start exploring the advanced stuff.
|
188
192
|
|
189
193
|
####Supporting WebSockets
|
190
194
|
|
@@ -212,14 +216,14 @@ end
|
|
212
216
|
|
213
217
|
To design a chatroom we will need a few things:
|
214
218
|
|
215
|
-
1. We will need to force people identify themselves by choosing nicknames - to do this we will define the `
|
219
|
+
1. We will need to force people identify themselves by choosing nicknames - to do this we will define the `on_open` method to refuse any connections that don't have a nickname.
|
216
220
|
2. We will want to make sure these nicknames are unique and don't give a wrong sense of authority (nicknames such as 'admin' should be forbidden) - for now, we will simply refuse the 'wrong' type of nicknames and leave uniqieness for another time.
|
217
221
|
3. We will want to push messages we recieve to all the other chatroom members - to do this we will use the `broadcast` method in our `on_message(data)` method.
|
218
|
-
4. We will also want to tell people when someone left the chatroom - to do this we can define an `
|
222
|
+
4. We will also want to tell people when someone left the chatroom - to do this we can define an `on_close` method and use the `broadcast` method in there.
|
219
223
|
|
220
224
|
We can use the :id parameter to set the nickname.
|
221
225
|
|
222
|
-
the :id is an automatic parameter that Plezi appended to our path like already explained and it's perfect for our
|
226
|
+
the :id is an automatic parameter that Plezi appended to our path like already explained and it's perfect for our current needs.
|
223
227
|
|
224
228
|
We could probably rewrite our route to something like this: `route '/(:id)/(:nickname)', ChatController` (or move the `/people` path out of the controller and use `'/(:nickname)'`)... but why work hard when we don't need to?
|
225
229
|
|
@@ -239,7 +243,7 @@ def on_message data
|
|
239
243
|
return false
|
240
244
|
end
|
241
245
|
message = {}
|
242
|
-
message[:message] =
|
246
|
+
message[:message] = data['message'] # should consider sanitizing this
|
243
247
|
message[:event] = :chat
|
244
248
|
message[:from] = params[:id]
|
245
249
|
message[:at] = Time.now
|
@@ -247,7 +251,7 @@ def on_message data
|
|
247
251
|
end
|
248
252
|
```
|
249
253
|
|
250
|
-
let's write it a bit shorter... if our code has nothing important to say, it might as well be quick about it.
|
254
|
+
let's write it a bit shorter... if our code has nothing important to say, it might as well be quick about it and avoid unnecessary intermediate object assignments.
|
251
255
|
|
252
256
|
```ruby
|
253
257
|
def on_message data
|
@@ -258,7 +262,7 @@ def on_message data
|
|
258
262
|
response.close
|
259
263
|
return false
|
260
264
|
end
|
261
|
-
broadcast :_send_message, {event: :chat, from: params[:id], message:
|
265
|
+
broadcast :_send_message, {event: :chat, from: params[:id], message: ERB::Util.html_escape(data['message']), at: Time.now}.to_json
|
262
266
|
end
|
263
267
|
```
|
264
268
|
|
@@ -298,7 +302,7 @@ Another feature we want to put in, is letting people know when someone enters or
|
|
298
302
|
Using the `broadcast` method with the special `on_disconnect` websocket method, makes telling people we left an easy task...
|
299
303
|
|
300
304
|
```ruby
|
301
|
-
def
|
305
|
+
def on_close
|
302
306
|
message = {event: :chat, from: '', at: Time.now}
|
303
307
|
message[:message] = "#{params[:id]} left the chatroom."
|
304
308
|
broadcast :_send_message, message.to_json if params[:id]
|
@@ -310,14 +314,14 @@ We will only tell people that we left the chatroom if our login was successful -
|
|
310
314
|
Let's make it a bit shorter?
|
311
315
|
|
312
316
|
```ruby
|
313
|
-
def
|
317
|
+
def on_close
|
314
318
|
broadcast :_send_message, {event: :chat, from: '', at: Time.now, message: "#{params[:id]} left the chatroom."}.to_json if params[:id]
|
315
319
|
end
|
316
320
|
```
|
317
321
|
|
318
322
|
####The login process and telling people we're here
|
319
323
|
|
320
|
-
If we ever write a real chatroom, our login process will look somewhat different - but the following process is good enough for now and it has a lot to teach us...
|
324
|
+
If we ever write a real chatroom, our login process will look somewhat different, probably using the `pre_connect` callback (which is safer) - but the following process is good enough for now and it has a lot to teach us...
|
321
325
|
|
322
326
|
First, we will ensure the new connection has a nickname (the connection was made to '/nickname' rather then the root of our application '/'):
|
323
327
|
|
@@ -342,15 +346,16 @@ def pre_connect
|
|
342
346
|
end
|
343
347
|
```
|
344
348
|
|
345
|
-
Since Websocket connections start as an HTTP GET request, the pre-connect is called while still in 'HTTP mode', allowing us to use HTTP logic and refuse connections even before any websocket data can be sent by the 'client'. This is definitly the safer approach.
|
349
|
+
Since Websocket connections start as an HTTP GET request, the pre-connect is called while still in 'HTTP mode', allowing us to use HTTP logic and refuse connections even before any websocket data can be sent by the 'client'. This is definitly the safer approach... but it doesn't allow us to send websocket data (such as our pre-close message).
|
346
350
|
|
347
351
|
Next, we will check if the nickname is on the reserved names list, to make sure nobody impersonates a system administrator... let's add this code to our `on_open` method:
|
348
352
|
|
349
353
|
```ruby
|
350
354
|
message = {from: '', at: Time.now}
|
351
|
-
|
355
|
+
name = params[:id]
|
356
|
+
if (name.match(/admin|admn|system|sys|administrator/i))
|
352
357
|
message[:event] = :error
|
353
|
-
message[:message] = "The nickname '#{
|
358
|
+
message[:message] = "The nickname '#{name}' is refused."
|
354
359
|
response << message.to_json
|
355
360
|
params[:id] = false
|
356
361
|
response.close
|
@@ -358,63 +363,50 @@ Next, we will check if the nickname is on the reserved names list, to make sure
|
|
358
363
|
end
|
359
364
|
```
|
360
365
|
|
361
|
-
Then, if all is good, we will welcome the new connection to our chatroom. We will also
|
366
|
+
Then, if all is good, we will welcome the new connection to our chatroom. We will also broadcast the new guest's arrivale to everybody else...:
|
362
367
|
|
363
368
|
```ruby
|
364
369
|
message = {from: '', at: Time.now}
|
365
370
|
message[:event] = :chat
|
371
|
+
message[:message] = "Welcome #{params[:id]}."
|
366
372
|
response << message.to_json
|
367
373
|
message[:message] = "#{params[:id]} joined the chatroom."
|
368
374
|
broadcast :_send_message, message.to_json
|
369
375
|
```
|
370
376
|
|
371
|
-
Let's make it just a bit shorter, most of the code ins't important enough to worry about readability... we can compact our `if` statement to an inline statement like this:
|
372
377
|
|
373
|
-
|
374
|
-
message[:message] = list.empty? ? "You're the first one here." : "#{list[0..-2].join(', ')} #{list[1] ? 'and' : ''} #{list.last} #{list[1] ? 'are' : 'is'} already in the chatroom"
|
375
|
-
```
|
376
|
-
|
377
|
-
We will also want to tweek the code a bit, so the nicknames are case insensative...
|
378
|
-
|
379
|
-
This will be our final `on_connect` method:
|
378
|
+
This will be our final `on_open` method:
|
380
379
|
|
381
380
|
```ruby
|
382
|
-
def
|
381
|
+
def on_open
|
383
382
|
if params[:id].nil?
|
384
383
|
response << {event: :error, from: :system, at: Time.now, message: "Error: cannot connect without a nickname!"}.to_json
|
385
384
|
response.close
|
386
385
|
return false
|
387
386
|
end
|
388
387
|
message = {from: '', at: Time.now}
|
389
|
-
|
390
|
-
if ((
|
388
|
+
name = params[:id]
|
389
|
+
if (name.match(/admin|admn|system|sys|administrator/i))
|
391
390
|
message[:event] = :error
|
392
|
-
message[:message] = "The nickname '#{
|
391
|
+
message[:message] = "The nickname '#{name}' is already taken."
|
393
392
|
response << message.to_json
|
394
393
|
params[:id] = false
|
395
394
|
response.close
|
396
395
|
return
|
397
396
|
end
|
398
397
|
message[:event] = :chat
|
399
|
-
message[:message] =
|
398
|
+
message[:message] = "Welcome #{params[:id]}."
|
399
|
+
# Should you end up storing your connected user names inside a manged list
|
400
|
+
# in redis or a database and then read that into a variable called 'list'
|
401
|
+
# here is some code you can use to write a message to the user based on the
|
402
|
+
# people currently in that list.
|
403
|
+
# message[:message] = list.empty? ? "You're the first one here." : "#{list[0..-2].join(', ')} #{list[1] ? 'and' : ''} #{list.last} #{list[1] ? 'are' : 'is'} already in the chatroom"
|
400
404
|
response << message.to_json
|
401
|
-
message[:message] = "#{
|
405
|
+
message[:message] = "#{name} joined the chatroom."
|
402
406
|
broadcast :_send_message, message.to_json
|
403
407
|
end
|
404
408
|
```
|
405
409
|
|
406
|
-
####The \_ask_nickname method
|
407
|
-
|
408
|
-
Just like the `_send_message` method, this method's name starts with an underscore to make sure it is ignored by the Plezi router.
|
409
|
-
|
410
|
-
Since this message is used by the `collect` method to collect information (which will block our code), it's very important that this method will be short and fast - it might run hundreds of times (or more), depending how many people are connected to our chatroom...
|
411
|
-
|
412
|
-
```ruby
|
413
|
-
def _ask_nickname
|
414
|
-
return params[:id]
|
415
|
-
end
|
416
|
-
```
|
417
|
-
|
418
410
|
###The Complete Ruby Code < (less then) 75 lines
|
419
411
|
|
420
412
|
This is our complete `mychat.rb` Ruby application code:
|
@@ -430,7 +422,7 @@ class ChatController
|
|
430
422
|
response['content-type'] = 'text/html'
|
431
423
|
render(:chat)
|
432
424
|
end
|
433
|
-
|
425
|
+
def_special_method "people.txt" do
|
434
426
|
"I wrote this app :)"
|
435
427
|
end
|
436
428
|
def on_message data
|
@@ -441,40 +433,43 @@ class ChatController
|
|
441
433
|
response.close
|
442
434
|
return false
|
443
435
|
end
|
444
|
-
broadcast :_send_message, {event: :chat, from: params[:id], message: data[
|
436
|
+
broadcast :_send_message, {event: :chat, from: params[:id], message: ERB::Util.html_escape(data['message']), at: Time.now}.to_json
|
445
437
|
end
|
446
438
|
def _send_message data
|
447
439
|
response << data
|
448
440
|
end
|
449
|
-
def
|
441
|
+
def on_open
|
450
442
|
if params[:id].nil?
|
451
|
-
response << {event: :error, from: :system, at: Time.now, message:
|
443
|
+
response << {event: :error, from: :system, at: Time.now, message: "Error: cannot connect without a nickname!"}.to_json
|
452
444
|
response.close
|
453
445
|
return false
|
454
446
|
end
|
455
447
|
message = {from: '', at: Time.now}
|
456
|
-
|
457
|
-
if ((
|
448
|
+
name = params[:id]
|
449
|
+
if (name.match(/admin|admn|system|sys|administrator/i))
|
458
450
|
message[:event] = :error
|
459
|
-
message[:message] = "The nickname '#{
|
451
|
+
message[:message] = "The nickname '#{name}' is already taken."
|
460
452
|
response << message.to_json
|
461
453
|
params[:id] = false
|
462
454
|
response.close
|
463
455
|
return
|
464
456
|
end
|
465
457
|
message[:event] = :chat
|
466
|
-
message[:message] =
|
458
|
+
message[:message] = "Welcome #{params[:id]}."
|
459
|
+
# Should you end up storing your connected user names inside a manged list
|
460
|
+
# in redis or a database and then read that into a variable called 'list'
|
461
|
+
# here is some code you can use to write a message to the user based on the
|
462
|
+
# people currently in that list.
|
463
|
+
# message[:message] = list.empty? ? "You're the first one here." : "#{list[0..-2].join(', ')} #{list[1] ? 'and' : ''} #{list.last} #{list[1] ? 'are' : 'is'} already in the chatroom"
|
467
464
|
response << message.to_json
|
468
|
-
message[:message] = "#{
|
465
|
+
message[:message] = "#{name} joined the chatroom."
|
469
466
|
broadcast :_send_message, message.to_json
|
470
467
|
end
|
471
468
|
|
472
|
-
|
469
|
+
|
470
|
+
def on_close
|
473
471
|
broadcast :_send_message, {event: :chat, from: '', at: Time.now, message: "#{params[:id]} left the chatroom."}.to_json if params[:id]
|
474
472
|
end
|
475
|
-
def _ask_nickname
|
476
|
-
return params[:id]
|
477
|
-
end
|
478
473
|
end
|
479
474
|
|
480
475
|
# Using pathname extentions for setting public folder
|
@@ -494,7 +489,7 @@ service_options = {
|
|
494
489
|
listen service_options
|
495
490
|
|
496
491
|
# this routes the root of the application ('/') to our ChatController
|
497
|
-
route '
|
492
|
+
route '/:id', ChatController
|
498
493
|
```
|
499
494
|
|
500
495
|
##The HTML - a web page with websockets
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: plezi
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.11.
|
4
|
+
version: 0.11.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Boaz Segev
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2015-09-
|
11
|
+
date: 2015-09-22 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: grhttp
|
@@ -83,6 +83,7 @@ files:
|
|
83
83
|
- lib/plezi/common/defer.rb
|
84
84
|
- lib/plezi/common/dsl.rb
|
85
85
|
- lib/plezi/common/redis.rb
|
86
|
+
- lib/plezi/common/renderer.rb
|
86
87
|
- lib/plezi/common/settings.rb
|
87
88
|
- lib/plezi/handlers/controller_core.rb
|
88
89
|
- lib/plezi/handlers/controller_magic.rb
|