plezi 0.7.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (68) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +22 -0
  3. data/CHANGELOG.md +450 -0
  4. data/Gemfile +4 -0
  5. data/KNOWN_ISSUES.md +13 -0
  6. data/LICENSE.txt +22 -0
  7. data/README.md +341 -0
  8. data/Rakefile +2 -0
  9. data/TODO.md +19 -0
  10. data/bin/plezi +301 -0
  11. data/lib/plezi.rb +125 -0
  12. data/lib/plezi/base/cache.rb +77 -0
  13. data/lib/plezi/base/connections.rb +33 -0
  14. data/lib/plezi/base/dsl.rb +177 -0
  15. data/lib/plezi/base/engine.rb +85 -0
  16. data/lib/plezi/base/events.rb +84 -0
  17. data/lib/plezi/base/io_reactor.rb +41 -0
  18. data/lib/plezi/base/logging.rb +62 -0
  19. data/lib/plezi/base/rack_app.rb +89 -0
  20. data/lib/plezi/base/services.rb +57 -0
  21. data/lib/plezi/base/timers.rb +71 -0
  22. data/lib/plezi/handlers/controller_magic.rb +383 -0
  23. data/lib/plezi/handlers/http_echo.rb +27 -0
  24. data/lib/plezi/handlers/http_host.rb +215 -0
  25. data/lib/plezi/handlers/http_router.rb +69 -0
  26. data/lib/plezi/handlers/magic_helpers.rb +43 -0
  27. data/lib/plezi/handlers/route.rb +272 -0
  28. data/lib/plezi/handlers/stubs.rb +143 -0
  29. data/lib/plezi/server/README.md +33 -0
  30. data/lib/plezi/server/helpers/http.rb +169 -0
  31. data/lib/plezi/server/helpers/mime_types.rb +999 -0
  32. data/lib/plezi/server/protocols/http_protocol.rb +318 -0
  33. data/lib/plezi/server/protocols/http_request.rb +133 -0
  34. data/lib/plezi/server/protocols/http_response.rb +294 -0
  35. data/lib/plezi/server/protocols/websocket.rb +208 -0
  36. data/lib/plezi/server/protocols/ws_response.rb +92 -0
  37. data/lib/plezi/server/services/basic_service.rb +224 -0
  38. data/lib/plezi/server/services/no_service.rb +196 -0
  39. data/lib/plezi/server/services/ssl_service.rb +193 -0
  40. data/lib/plezi/version.rb +3 -0
  41. data/plezi.gemspec +26 -0
  42. data/resources/404.erb +68 -0
  43. data/resources/404.haml +64 -0
  44. data/resources/404.html +67 -0
  45. data/resources/404.slim +63 -0
  46. data/resources/500.erb +68 -0
  47. data/resources/500.haml +63 -0
  48. data/resources/500.html +67 -0
  49. data/resources/500.slim +63 -0
  50. data/resources/Gemfile +85 -0
  51. data/resources/anorexic_gray.png +0 -0
  52. data/resources/anorexic_websockets.html +47 -0
  53. data/resources/code.rb +8 -0
  54. data/resources/config.ru +39 -0
  55. data/resources/controller.rb +139 -0
  56. data/resources/db_ac_config.rb +58 -0
  57. data/resources/db_dm_config.rb +51 -0
  58. data/resources/db_sequel_config.rb +42 -0
  59. data/resources/en.yml +204 -0
  60. data/resources/environment.rb +41 -0
  61. data/resources/haml_config.rb +6 -0
  62. data/resources/i18n_config.rb +14 -0
  63. data/resources/rakefile.rb +22 -0
  64. data/resources/redis_config.rb +35 -0
  65. data/resources/routes.rb +26 -0
  66. data/resources/welcome_page.html +72 -0
  67. data/websocket chatroom.md +639 -0
  68. metadata +141 -0
@@ -0,0 +1,41 @@
1
+ # encoding: UTF-8
2
+
3
+ # this file sets up the basic framework.
4
+
5
+ # Using pathname extentions for setting public folder
6
+ require 'pathname'
7
+ #set up root object, it might be used by the environment and\or the plezi extension gems.
8
+ Root ||= Pathname.new(File.dirname(__FILE__)).expand_path
9
+
10
+ # make sure all file access and file loading is relative to the application's root folder
11
+ Dir.chdir Root.to_s
12
+
13
+ # ensure development mode? (comment before production, environment dependent)
14
+ ENV["RACK_ENV"] ||= "development"
15
+
16
+ # save the process id (pid) to file - notice Heroku doesn't allow to write files.
17
+ (IO.write File.expand_path(File.join 'tmp','pid'), Process.pid unless ENV["DYNO"]) rescue true
18
+
19
+ # using bundler to load gems (including the plezi gem)
20
+ require 'bundler'
21
+ Bundler.require
22
+
23
+ # set up Plezi logs - Heroku logs to STDOUT, this machine logs to log file
24
+ Plezi.create_logger File.expand_path(File.join 'logs','server.log'), ENV["RACK_ENV"]=="development" unless ENV['DYNO']
25
+
26
+ # load all config files
27
+ Dir[File.join "{config}", "**" , "*.rb"].each {|file| load File.expand_path(file)}
28
+
29
+ # load all library files
30
+ Dir[File.join "{lib}", "**" , "*.rb"].each {|file| load File.expand_path(file)}
31
+
32
+ # load all application files
33
+ Dir[File.join "{app}", "**" , "*.rb"].each {|file| load File.expand_path(file)}
34
+
35
+ # start a web service to listen on the first default port (3000 or the port set by the command-line).
36
+ # you can change some of the default settings here.
37
+ listen root: Root.join('public').to_s,
38
+ assets: Root.join('assets').to_s,
39
+ assets_public: '/assets',
40
+ templates: Root.join('app','views').to_s,
41
+ ssl: false
@@ -0,0 +1,6 @@
1
+ # encoding: UTF-8
2
+
3
+ if defined? Haml
4
+ # set some options
5
+ Haml::Options.defaults[:format] = :html5
6
+ end
@@ -0,0 +1,14 @@
1
+ # encoding: UTF-8
2
+
3
+ if defined? I18n
4
+ # set up i18n locales paths
5
+ I18n.load_path = Dir[Root.join('locales', '*.{rb,yml}').to_s]
6
+
7
+ # This will prevent certain errors from showing up.
8
+ I18n.enforce_available_locales = false
9
+
10
+ # set default locale, if not english
11
+ # I18n.default_locale = :ru
12
+
13
+ end
14
+
@@ -0,0 +1,22 @@
1
+ # encoding: UTF-8
2
+ BUILDING_PLEZI_TEMPLATE = true
3
+ NO_PLEZI_AUTO_START = true
4
+
5
+ require 'rubygems'
6
+ require 'rake'
7
+
8
+ app_name = ::File.expand_path(::Dir["."][0]).split(/[\\\/]/).last
9
+ require ::File.expand_path(::Dir["."][0], ( app_name + ".rb") )
10
+
11
+ namespace :app do
12
+
13
+ desc "adds and framework files that might be missing (use after adding plezi gems).\nnotice: this will not update rakefile.rb or Gemfile(!)."
14
+ task :rebuild do
15
+ Dir.chdir '..'
16
+ puts `plezi force #{app_name}`
17
+ end
18
+ end
19
+
20
+ task :default do
21
+ puts `rake -T`
22
+ end
@@ -0,0 +1,35 @@
1
+ # encoding: UTF-8
2
+
3
+ if defined? Redis
4
+
5
+ # ## Plezi Redis Automation
6
+ # ## ====
7
+ # ##
8
+ # ## sets up Plezi to use Radis broadcast.
9
+ # ## this is less recommended then writing your own tailored solution
10
+ # ##
11
+ # ## If Plezi Redis Automation is enabled:
12
+ # ## Plezi creates is own listening thread for each Controller class that broadcasts using Redis.
13
+ # ## (using the Controller.redis_connection and Controller.redis_channel_name class methods)
14
+ # ##
15
+ # ## this overrides the default Controller#broadcast method which is very powerful but
16
+ # ## is limited to one process.
17
+ # ##
18
+ # ENV['PL_REDIS_URL'] = ENV['REDISCLOUD_URL'] ||= ENV["REDISTOGO_URL"] ||= "redis://username:password@my.host:6389"
19
+
20
+
21
+ # ## create a listening thread - rewrite the following code for your own Redis tailored solution.
22
+ # ##
23
+ # ## the following is only sample code for you to change:
24
+ # RADIS_CHANNEL = appsecret
25
+ # RADIS_URI = URI.parse(ENV['REDISCLOUD_URL'] || "redis://username:password@my.host:6389")
26
+ # RADIS_CONNECTION = Redis.new(host: RADIS_URI.host, port: RADIS_URI.port, password: RADIS_URI.password)
27
+ # RADIS_THREAD = Thread.new do
28
+ # Redis.new(host: RADIS_URI.host, port: RADIS_URI.port, password: RADIS_URI.password).subscribe(RADIS_CHANNEL) do |on|
29
+ # on.message do |channel, msg|
30
+ # msg = JSON.parse(msg)
31
+ # # do stuff
32
+ # end
33
+ # end
34
+ # end
35
+ end
@@ -0,0 +1,26 @@
1
+ #!/usr/bin/env ruby
2
+ # encoding: UTF-8
3
+
4
+ #########
5
+ ##
6
+ ## This file holds the routes for your application
7
+ ##
8
+ ## use the `host`, `route` and `shared_route` functions
9
+ ##
10
+
11
+
12
+ # This is an optional re-write route for I18n.
13
+ # i.e.: `/en/home` will be rewriten as `/home`, while setting params[:locale] to "en"
14
+ route "/(:locale){#{I18n.available_locales.join "|"}}/*" , false if defined? I18n
15
+
16
+ ###
17
+ # add your routes here:
18
+
19
+
20
+ # remove this demo route and the SampleController once you want to feed Plezi your code.
21
+ route '/', SampleController
22
+
23
+
24
+ # this is a catch all route with a stub controller.
25
+ # un comment the following line and replace the controller if you want a catch-all route.
26
+ # route '*', Plezi::StubRESTCtrl
@@ -0,0 +1,72 @@
1
+ <!DOCTYPE html><head><title>appname - Feed Me!</title><style type="text/css">body, html
2
+ {
3
+ background-color: #eee;
4
+ padding: 0; margin: 0;
5
+ width: 100%;
6
+ font-size: 1em;
7
+ }
8
+ body, html, h1, #wrapper, #wrapper h2
9
+ {
10
+ background-image: url(/images/plezi_gray.png);
11
+ background-position: center top;
12
+ background-origin: inherit;
13
+ background-repeat: no-repeat;
14
+ background-size: 88% auto;
15
+ background-attachment: fixed;
16
+
17
+ }
18
+
19
+ h1
20
+ {
21
+ background-color: #ddd;
22
+ color: #00a;
23
+ text-align: center;
24
+ border-bottom: 1px solid #000;
25
+ margin: 0 0 1em 0;
26
+ padding: 0.5em 0;
27
+ width: 100%;
28
+ }
29
+ p
30
+ {
31
+ font-size: 1em;
32
+ padding: 0 1em;
33
+ margin: 0.5em 0;
34
+ }
35
+ a
36
+ {
37
+ color: #a04;
38
+ text-decoration: none;
39
+ }
40
+ a:hover
41
+ {
42
+ color: #70f;
43
+ text-decoration: underline;
44
+ }
45
+ #wrapper
46
+ {
47
+ background-color: #fff;
48
+ margin: 1em 5%;
49
+ padding: 0 0 2%;
50
+ border-radius: 20px;
51
+ min-height: 50%;
52
+ color: #007;
53
+ }
54
+ #wrapper h2
55
+ {
56
+ background-color: #ddd;
57
+ color: #008;
58
+ text-align: left;
59
+ margin: 0 0 1em 0;
60
+ padding: 0.5em 5%;
61
+ border-radius: 20px;
62
+ }
63
+ #wrapper p{ padding: 0 2%;}
64
+ .bold { font-weight: bold; }
65
+ #wrapper ol li{ padding: 0 2%;}
66
+ pre
67
+ {
68
+ border-radius: 20px;
69
+ padding: 0.5em 0;
70
+ background-color: #444;
71
+ color: #ddd;
72
+ }</style></head><body><h1>Welcome to <a href="https://github.com/boazsegev/plezi">Plezi</a></h1><div id="wrapper"><h2>Congratulations, appname is running - What's next?</h2><p><span class="bold">Congratulations</span>, you're now running appname and it has so much potential!</p><p>appname started out <a href="https://github.com/boazsegev/plezi">Plezi</a> and it's your quest to feed it your code and make sure appname grows strong and happy.</p><p>You're the master of this quest, and your next steps might include:</p><ol><li><p class="bold">Deciding which gems to use:</p><ul><li>edit Gemfile in the appname folder.</li></ul></li><li><p class="bold">Reviewing the sample code and feeding <a href="https://github.com/boazsegev/plezi">Plezi</a> your code:</p><ul><li>Review the 'sample_controller.rb' file in the appname/app/controllers folder.</li><li>Delete the 'sample_controller.rb' file and this file (appname/assets/welcome.html).</li><li>Write your own controller and code.</li><li>Edit the 'routes.rb' file to set up your routes.</li></ul></li><li><p><span class="bold">Having fun</span> and submitting any issues you discover to the <a href="https://github.com/boazsegev/plezi">Plezi Github Project.</a></p></li></ol><p class="bold">Good Luck!</p></div></body>
@@ -0,0 +1,639 @@
1
+ # The Ruby Chatroom - Websockets with Plezi
2
+
3
+ Using Plezi, anyone can easily create a web application that has advanced features such as **websockets**, data pushing and callbacks.
4
+
5
+ The chatroom application is a great way to discover these advanced features and the Plezi framework's native WebSocket support.
6
+
7
+ ###Coding is the way to discover Plezi
8
+
9
+ When I was little, my father tried to teach me to swim... in other words, he throw me in the pool and let the chips fall where they may.
10
+
11
+ I was on the verge of drowning for the first few weeks, but looking back I am very thankful for the experience. You can hardly learn anything about swimming without entering the pool...
12
+
13
+ So let's start with getting wet - writing the code - and then maybe refine our understanding a bit by taking the code apart.
14
+
15
+ ###Before we start - installing Plezi
16
+
17
+ I assume that you have already [installed Ruby with RubyGems](https://www.ruby-lang.org/en/installation/), if not, do it now. I recommend [installing Ruby and RubyGems using rvm](http://rvm.io/rvm/install).
18
+
19
+ once ruby and rubygems are installed, it's time to install Plezi. in your terminal window, run:
20
+
21
+ ```
22
+ $ gem install plezi
23
+ ```
24
+
25
+ depending on your system and setup, you might need to enter a password or use the sudo command to install new gems:
26
+
27
+ ```
28
+ $ sudo gem install plezi
29
+ ```
30
+
31
+ That's it.
32
+
33
+ ##The Ruby Code (chatroom server)
34
+
35
+ We can create an Plezi application using the `$ plezi new myapp` command, but that's too easy - we want it hardcore.
36
+
37
+ Let's create an application folder called `mychat` and save our code in a file called `mychat.rb` in our application folder.
38
+
39
+ The first bit of code tells the Unix bash to run this file as a ruby file, just in case we want to make this file into a Unix executable (for us Unix and BSD people).
40
+
41
+ ```ruby
42
+ #!/usr/bin/env ruby
43
+ # encoding: UTF-8
44
+ ```
45
+
46
+ This next bit of code imports Plezi into our program and allows us to use the Plezi framework in our application.
47
+
48
+ ```ruby
49
+ require 'plezi'
50
+ ```
51
+
52
+ Then there is the part where we define the `ChatController` class... We'll talk about this piece of code later on. for now, I will just point out that this class doesn't inherit any special controller class.
53
+
54
+ Let's write a short stub which we will fill in later.
55
+
56
+ ```ruby
57
+ class ChatController
58
+ # ...we'll fill this in later...
59
+ end
60
+ ```
61
+ Next, we set find the root folder where our application exists - we will use this to tell plezi where our html files, templates and assets are stored (once we write any of them).
62
+
63
+ ```ruby
64
+ # Using pathname extentions for setting public folder
65
+ require 'pathname'
66
+ # set up the Root object for easy path access.
67
+ Root = Pathname.new(File.dirname(__FILE__)).expand_path
68
+ ```
69
+
70
+ Then, we set up the Plezi service's parameters - parameters which Plezi will use to create our main service and host.
71
+
72
+ A service, in this case, is realy just a nice word for the Plezi server (which might have a number of services or hosts). We will have only one service and one host, so it's very easy to set up.
73
+
74
+ As you can see, some options are there for later, but are disabled for now.
75
+
76
+ - **root**: this option defines the folder from which Plezi should serve static files (html files, images etc'). We will not be serving any static files at the moment, so this option is disabled.
77
+
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
+
80
+ - **assets_public**: this option tells plezi which route is the one where assets are attached to (it defaults to '/assets'). We aren't using assets, so that's really not important.
81
+
82
+ - **_templates_**: this option tells Plezi where to look for template files (.haml / .erb files). Since we will use a template file for our HTML, let's go ahead and create a subfolder called `views` and set that as our templates source folder.
83
+
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
+
86
+ ```ruby
87
+ service_options = {
88
+ # root: Root.join('public').to_s,
89
+ # assets: Root.join('assets').to_s,
90
+ # assets_public: '/',
91
+ templates: Root.join('views').to_s,
92
+ ssl: false
93
+ }
94
+ ```
95
+
96
+ Next we call the `listen` command - this command actually creates the service.
97
+
98
+ The port plezi uses by default is 3000 [http://localhost:3000/](http://localhost:3000/). By not defining a port, we allowed ourselves to either use the default port (3000) or decide the port when we run our application (i.e. `./mychat.rb -p 8080`).
99
+
100
+ ```ruby
101
+ listen service_options
102
+ ```
103
+
104
+ (if you want to force a specific port, i.e. 80, write `listen 80, service_options` - but make sure you are allowed to use this port)
105
+
106
+ 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
+
108
+ ```ruby
109
+ route '/', ChatController
110
+ ```
111
+
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_connect`, `on_message(data)`, `on_disconnect`), and WebSockets filters and helpers (`pre-connect`, `broadcast`, `collect`).
113
+
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:
115
+
116
+ ```ruby
117
+ route '/(:id)', ChatController
118
+ ```
119
+
120
+ ###The Controller - serving regular data (HTTP)
121
+
122
+ Let's take a deeper look into our controller and start filling it in...
123
+
124
+ ####serving the main html template file (index)
125
+
126
+ The first thing we want our controller to do, is to serve the HTML template we will write later on. We will use a template so we can add stuff later, maybe.
127
+
128
+ Since controllers can work like virtual folders with support for RESTful methods, we can define an `index` method to do this simple task:
129
+
130
+ ```ruby
131
+ def index
132
+ #... later
133
+ end
134
+ ```
135
+
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 of our rendered template.
137
+
138
+ Lets fill in our `index` method:
139
+
140
+ ```ruby
141
+ class ChatController
142
+ def index
143
+ response['content-type'] = 'text/html'
144
+ response << render(:chat)
145
+ true
146
+ end
147
+ end
148
+ ```
149
+
150
+ Actually, some tasks are so common - like sending text in our HTTP response - that Plezi can helps us along. If our method should return a String object, that String will be appended to the response.
151
+
152
+ Let's rewrite our `index` method to make it cleaner:
153
+
154
+ ```ruby
155
+ class ChatController
156
+ def index
157
+ response['content-type'] = 'text/html'
158
+ render(:chat)
159
+ end
160
+ end
161
+ ```
162
+
163
+ 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
+
165
+ We just need to remember to create a 'chat' template file (`chat.html.erb` or `chat.html.haml`)... but that's for later.
166
+
167
+ ####Telling people that we made this cool app!
168
+
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...
170
+
171
+ With Plezi, that's super easy.
172
+
173
+ Since out ChatController is at the root of ou application, let's add a `people` method to our ChatController:
174
+
175
+ ```ruby
176
+ def people
177
+ "I wrote this app :)"
178
+ end
179
+ ```
180
+
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.
182
+
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 :)
184
+
185
+ ###The Controller - live input and pushing data (WebSockets)
186
+
187
+ We are building an advanced application here - this is _not_ another 'hello world' - lets start exploring the advanced stuff.
188
+
189
+ ####Supporting WebSockets
190
+
191
+ To accept WebSockets connections, our controller must define an `on_message(data)` method.
192
+
193
+ Plezi will recognize this method and allow websocket connections for our controller's path (which is at the root of our application).
194
+
195
+ We will also want to transport some data between the browser (the client) and our server. To do this, we will use [JSON](http://en.wikipedia.org/wiki/JSON), which is really easy to use and is the same format used by socket.io.
196
+
197
+ We will start by formatting our data to JSON (or closing the connection if someone is sending corrupt data):
198
+
199
+ ```ruby
200
+ def on_message data
201
+ begin
202
+ data = JSON.parse data
203
+ rescue Exception => e
204
+ response << {event: :error, message: "Unknown Error"}.to_json
205
+ response.close
206
+ return false
207
+ end
208
+ end
209
+ ```
210
+
211
+ ####Pausing for software design - the Chatroom challange
212
+
213
+ To design a chatroom we will need a few things:
214
+
215
+ 1. We will need to force people identify themselves by choosing nicknames - to do this we will define the `on_connect` method to refuse any connections that don't have a nickname.
216
+ 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 collect the nicknames from all the other active connections using the `collect` method and use that in our `on_connect` method.
217
+ 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 `on_disconnect` method and use the `broadcast` method in there.
219
+
220
+ We can use the :id parameter to collect the nickname.
221
+
222
+ the :id is an automatic parameter that Plezi appended to our path like already explained and it's perfect for our simple needs.
223
+
224
+ 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
+
226
+ ####Broadcasting chat (websocket) messages
227
+
228
+ When we get a chat message, with `on_message(data)`, we will want to broadcast this message to all the _other_ ChatController connections.
229
+
230
+ Using JSON, our new `on_message(data)` method can look something like this:
231
+
232
+ ```ruby
233
+ def on_message data
234
+ begin
235
+ data = JSON.parse data
236
+ rescue Exception => e
237
+ response << {event: :error, message: "Unknown Error"}.to_json
238
+ response.close
239
+ return false
240
+ end
241
+ message = {}
242
+ message[:message] = data["message"]
243
+ message[:event] = :chat
244
+ message[:from] = params[:id]
245
+ message[:at] = Time.now
246
+ broadcast :_send_message, message.to_json
247
+ end
248
+ ```
249
+
250
+ let's write it a bit shorter... if our code has nothing important to say, it might as well be quick about it.
251
+
252
+ ```ruby
253
+ def on_message data
254
+ begin
255
+ data = JSON.parse data
256
+ rescue Exception => e
257
+ response << {event: :error, message: "Unknown Error"}.to_json
258
+ response.close
259
+ return false
260
+ end
261
+ broadcast :_send_message, {event: :chat, from: params[:id], message: data["message"], at: Time.now}.to_json
262
+ end
263
+ ```
264
+
265
+ Now that the code is shorter, let's look at that last line - the one that calls `broadcast`
266
+
267
+ `broadcast` is an interesing Plezi feature that allows us to tell all the _other_ connection to run a method. It is totally asynchroneos, so we don't wait for it to complete.
268
+
269
+ Here, we tell all the other websocket instances of our ChatController to run their `_send_message(msg)` method on their own connections - it even passes a message as an argument... but wait, we didn't write the `_send_message(msg)` method yet!
270
+
271
+ ####The \_send_message method
272
+
273
+ Let's start with the name - why the underscore at the beginning?
274
+
275
+ Plezi knows that sometimes we will want to create public methods that aren't available as a path - remember the `people` method, it was automatically recognized as an HTTP path...
276
+
277
+ Plezi allows us to 'exclude' some methods from this auto-recogntion. protected methods and methods starting with an underscore (\_) aren't recognized by the Plezi router.
278
+
279
+ Since we want the `_send_message` to be called by the `broadcast` method - it must be a public method (otherwise, we will not be able to call it for _other_ connections, only for our own connection).
280
+
281
+ This will be our `_send_message` method:
282
+
283
+ ```ruby
284
+ def _send_message data
285
+ response << data
286
+ end
287
+ ```
288
+
289
+ Did you notice the difference between WebSocket responses and HTTP?
290
+
291
+ In WebSockets, we don't automatically send string data (this is an important safeguard) and we must use the `<<` method to add data to the response stream.
292
+
293
+
294
+ ####Telling people that we left the chatroom
295
+
296
+ Another feature we want to put in, is letting people know when someone enters or leaves the chatroom.
297
+
298
+ Using the `broadcast` method with the special `on_disconnect` websocket method, makes telling people we left an easy task...
299
+
300
+ ```ruby
301
+ def on_disconnect
302
+ message = {event: :chat, from: '', at: Time.now}
303
+ message[:message] = "#{params[:id]} left the chatroom."
304
+ broadcast :_send_message, message.to_json if params[:id]
305
+ end
306
+ ```
307
+
308
+ We will only tell people that we left the chatroom if our login was successful - this is why we use the `if params[:id]` statement - if the login fails, we will set the `params[:id]` to false.
309
+
310
+ Let's make it a bit shorter?
311
+
312
+ ```ruby
313
+ def on_disconnect
314
+ broadcast :_send_message, {event: :chat, from: '', at: Time.now, message: "#{params[:id]} left the chatroom."}.to_json if params[:id]
315
+ end
316
+ ```
317
+
318
+ ####The login process and telling people we're here
319
+
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...
321
+
322
+ First, we will ensure the new connection has a nickname (the connection was made to '/nickname' rather then the root of our application '/'):
323
+
324
+ ```ruby
325
+ def on_connect
326
+ if params[:id].nil?
327
+ response << {event: :error, from: :system, at: Time.now, message: "Error: cannot connect without a nickname!"}.to_json
328
+ response.close
329
+ return false
330
+ end
331
+ end
332
+ ```
333
+
334
+ Easy.
335
+
336
+ Next, we will ask everybody else who is connected to tell us their nicknames - we will test the new nickname against this list and make sure the nickname is unique.
337
+
338
+ We will also add some reserved names to this list, to make sure nobody impersonates a system administrator... let's add this code to our `on_connect` method:
339
+
340
+ ```ruby
341
+ message = {from: '', at: Time.now}
342
+ list = collect(:_ask_nickname)
343
+ if (list + ['admin', 'system', 'sys', 'administrator']).include? params[:id]
344
+ message[:event] = :error
345
+ message[:message] = "The nickname '#{params[:id]}' is already taken."
346
+ response << message.to_json
347
+ params[:id] = false
348
+ response.close
349
+ return
350
+ end
351
+ ```
352
+
353
+ Hmm.. **collect**? what is the `collect` method? - well, this is a little bit of more Plezi magic that allows us to ask and collect information from all the _other_ active connections. This method returns an array of all the responses.
354
+
355
+ We will use `collect` to get an array of all the connected nicknames - we will write the `_ask_nickname` method in just a bit.
356
+
357
+ Then, if all is good, we will welcome the new connection to our chatroom. We will also tell the new guest who is already connected and broadcast their arrivale to everybody else...:
358
+
359
+ ```ruby
360
+ message = {from: '', at: Time.now}
361
+ message[:event] = :chat
362
+ if list.empty?
363
+ message[:message] = "Welcome! You're the first one here."
364
+ else
365
+ message[:message] = "Welcome! #{list[0..-2].join(', ')} #{list[1] ? 'and' : ''} #{list.last} #{list[1] ? 'are' : 'is'} already here."
366
+ end
367
+ response << message.to_json
368
+ message[:message] = "#{params[:id]} joined the chatroom."
369
+ broadcast :_send_message, message.to_json
370
+ ```
371
+
372
+ 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:
373
+
374
+ ```ruby
375
+ 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"
376
+ ```
377
+
378
+ We will also want to tweek the code a bit, so the nicknames are case insensative...
379
+
380
+ This will be our final `on_connect` method:
381
+
382
+ ```ruby
383
+ def on_connect
384
+ if params[:id].nil?
385
+ response << {event: :error, from: :system, at: Time.now, message: "Error: cannot connect without a nickname!"}.to_json
386
+ response.close
387
+ return false
388
+ end
389
+ message = {from: '', at: Time.now}
390
+ list = collect(:_ask_nickname)
391
+ if ((list.map {|n| n.downcase}) + ['admin', 'system', 'sys', 'administrator']).include? params[:id].downcase
392
+ message[:event] = :error
393
+ message[:message] = "The nickname '#{params[:id]}' is already taken."
394
+ response << message.to_json
395
+ params[:id] = false
396
+ response.close
397
+ return
398
+ end
399
+ message[:event] = :chat
400
+ 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"
401
+ response << message.to_json
402
+ message[:message] = "#{params[:id]} joined the chatroom."
403
+ broadcast :_send_message, message.to_json
404
+ end
405
+ ```
406
+
407
+ ####The \_ask_nickname method
408
+
409
+ Just like the `_send_message` method, this method's name starts with an underscore to make sure it is ignored by the Plezi router.
410
+
411
+ 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...
412
+
413
+ ```ruby
414
+ def _ask_nickname
415
+ return params[:id]
416
+ end
417
+ ```
418
+
419
+ ###The Complete Ruby Code < (less then) 75 lines
420
+
421
+ This is our complete `mychat.rb` Ruby application code:
422
+
423
+ ```ruby
424
+ #!/usr/bin/env ruby
425
+ # encoding: UTF-8
426
+
427
+ require 'plezi'
428
+
429
+ class ChatController
430
+ def index
431
+ response['content-type'] = 'text/html'
432
+ render(:chat)
433
+ end
434
+ def people
435
+ "I wrote this app :)"
436
+ end
437
+ def on_message data
438
+ begin
439
+ data = JSON.parse data
440
+ rescue Exception => e
441
+ response << {event: :error, message: "Unknown Error"}.to_json
442
+ response.close
443
+ return false
444
+ end
445
+ broadcast :_send_message, {event: :chat, from: params[:id], message: data["message"], at: Time.now}.to_json
446
+ end
447
+ def _send_message data
448
+ response << data
449
+ end
450
+ def on_connect
451
+ if params[:id].nil?
452
+ response << {event: :error, from: :system, at: Time.now, message: "Error: cannot connect without a nickname!"}.to_json
453
+ response.close
454
+ return false
455
+ end
456
+ message = {from: '', at: Time.now}
457
+ list = collect(:_ask_nickname)
458
+ if ((list.map {|n| n.downcase}) + ['admin', 'system', 'sys', 'administrator']).include? params[:id].downcase
459
+ message[:event] = :error
460
+ message[:message] = "The nickname '#{params[:id]}' is already taken."
461
+ response << message.to_json
462
+ params[:id] = false
463
+ response.close
464
+ return
465
+ end
466
+ message[:event] = :chat
467
+ 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"
468
+ response << message.to_json
469
+ message[:message] = "#{params[:id]} joined the chatroom."
470
+ broadcast :_send_message, message.to_json
471
+ end
472
+
473
+ def on_disconnect
474
+ broadcast :_send_message, {event: :chat, from: '', at: Time.now, message: "#{params[:id]} left the chatroom."}.to_json if params[:id]
475
+ end
476
+ def _ask_nickname
477
+ return params[:id]
478
+ end
479
+ end
480
+
481
+ # Using pathname extentions for setting public folder
482
+ require 'pathname'
483
+ # set up the Root object for easy path access.
484
+ Root = Pathname.new(File.dirname(__FILE__)).expand_path
485
+
486
+ # set up the Plezi service options
487
+ service_options = {
488
+ # root: Root.join('public').to_s,
489
+ # assets: Root.join('assets').to_s,
490
+ # assets_public: '/',
491
+ templates: Root.join('views').to_s,
492
+ ssl: false
493
+ }
494
+
495
+ listen service_options
496
+
497
+ # this routes the root of the application ('/') to our ChatController
498
+ route '/', ChatController
499
+ ```
500
+
501
+ ##The HTML - a web page with websockets
502
+
503
+ The [official websockets page](https://www.websocket.org) has great info about websockets and some tips about creating web pages with WebSocket features.
504
+
505
+ Since this isn't really a tutorial about HTML, Javascript or CSS, we will make it a very simple web page and explain just a few things about the websocket javascript...
506
+
507
+ ...**this is probably the hardest part in the code** (maybe because it isn't Ruby).
508
+
509
+ Let us create a new file, and save it at `views/chat.html.erb` - this is our template file and Plezi will find it when we call `render :chat`.
510
+
511
+ `.erb` files allow us to write HTML like files with Ruby code inside. We could also use Haml (which has a nicer syntax), but for now we will keep things symple... so simple, in fact, we will start with no Ruby code inside.
512
+
513
+ Copy and paste the following into your `views/chat.html.erb` file - the `views` folder is the one we defined for the `templates` in the Plezi service options - remember?
514
+
515
+ Anyway, here's the HTML code, copy it and I'll explain the code in a bit:
516
+
517
+ ```html
518
+ <!DOCTYPE html>
519
+ <head>
520
+ <meta charset='UTF-8'>
521
+ <style>
522
+ html, body {width: 100%; height:100%;}
523
+ body {font-size: 1.5em; background-color: #eee;}
524
+ p {padding: 0.2em; margin: 0;}
525
+ .received { color: #00f;}
526
+ .sent { color: #80f;}
527
+ input, #output, #status {font-size: 1em; width: 60%; margin: 0.5em 19%; padding: 0.5em 1%;}
528
+ input[type=submit] { margin: 0.5em 20%; padding: 0;}
529
+ #output {height: 60%; overflow: auto; background-color: #fff;}
530
+ .connected {background-color: #efe;}
531
+ .disconnected {background-color: #fee;}
532
+ </style>
533
+ <script>
534
+ var websocket = NaN;
535
+ var last_msg = NaN;
536
+ function Connect() {
537
+ websocket = new WebSocket( (window.location.protocol.indexOf('https') < 0 ? 'ws' : 'wss') + '://' + window.location.hostname + (window.location.port == '' ? '' : (':' + window.location.port) ) + "/" + document.getElementById("input").value );
538
+ }
539
+ function Init()
540
+ {
541
+ Connect()
542
+ websocket.onopen = function(e) { update_status(); WriteStatus({'message':'Connected :)'})};
543
+ websocket.onclose = function(e) { websocket = NaN; update_status(); };
544
+ websocket.onmessage = function(e) {
545
+ var msg = JSON.parse(e.data)
546
+ last_msg = msg
547
+ if(msg.event == 'chat') WriteMessage(msg, 'received')
548
+ if(msg.event == 'error') WriteStatus(msg)
549
+ };
550
+ websocket.onerror = function(e) { websocket = NaN; update_status(); };
551
+ }
552
+ function WriteMessage( message, message_type )
553
+ {
554
+ if (!message_type) message_type = 'received'
555
+ var msg = document.createElement("p");
556
+ msg.className = message_type;
557
+ msg.innerHTML = message.from + ": " + message.message;
558
+ document.getElementById("output").appendChild(msg);
559
+ }
560
+ function WriteStatus( message )
561
+ {
562
+ document.getElementById("status").innerHTML = message.message;
563
+ }
564
+ function Send()
565
+ {
566
+ var msg = {'event':'chat', 'from':'me', 'message':document.getElementById("input").value}
567
+ WriteMessage(msg, 'sent');
568
+ websocket.send(JSON.stringify(msg));
569
+ }
570
+ function update_status()
571
+ {
572
+ if(websocket)
573
+ {
574
+ document.getElementById("submit").value = "Send"
575
+ document.getElementById("input").placeholder = "your message goes here"
576
+ document.getElementById("status").className = "connected"
577
+ }
578
+ else
579
+ {
580
+ document.getElementById("submit").value = "Connect"
581
+ document.getElementById("input").placeholder = "your nickname"
582
+ document.getElementById("status").className = "disconnected"
583
+ if(last_msg.event != 'error') document.getElementById("status").innerHTML = "Please choose your nickname and join in..."
584
+ }
585
+ }
586
+ function on_submit()
587
+ {
588
+ if(websocket)
589
+ {
590
+ Send()
591
+ }
592
+ else
593
+ {
594
+ Init()
595
+ }
596
+ document.getElementById("input").value = ""
597
+ }
598
+ </script>
599
+ </head>
600
+ <body>
601
+ <div id='status' class='disconnected'>Please choose your nickname and join in...</div>
602
+ <div id='output'></div>
603
+ <form onsubmit='on_submit(); return false'>
604
+ <input id='input' type='text' placeholder='your nickname.' value='' />
605
+ <input type='submit' value='Connect' id='submit' />
606
+ </form>
607
+ </body>
608
+ ```
609
+
610
+ Our smart web page has three main components: the CSS (the stuff in the `style` tag), the Javascript (in the `script` tag) and the actual HTML.
611
+
612
+ All the interesting bits are in the Javascript.
613
+
614
+ The Javascript allows us to request a nickname, send a connection request to 'ws://localhost:3000/nickname' (where we pick up the nickname using the RESTful 'id' parameter), and send/recieve chat messages.
615
+
616
+ The CSS is just a bit of styling so the page doesn't look too bad.
617
+
618
+ The HTML is also very simple. We have one `div` element called `output`, one text input, a status bar (on top) and a submit button (with the word 'Send' / 'Connect').
619
+
620
+ I will go over some of the JavaScript highlights very quickly, as there are a lot of tutorials out there regarding websockets and javascript.
621
+
622
+ The main javascript functions we are using are:
623
+
624
+ * `connect` - this creates a new websockets object. this is fairly simple, even if a bit hard to read. there is a part there where instead of writing `ws://localhost:3000/nickname` we are dynamically producing the same string - it's harder to read but it will work also when we move the webpage to a real domain where the string might end up being `wss://www.mydomain.com/nickname`.
625
+ * `init` - this is a very interesting function that defines all the callbacks we might need for the websocket to actually work.
626
+ * `WriteMessage` - this simple function adds text to the `output` element, adding the different styles as needed.
627
+ * `WriteStatus` - this function is used to update the status line.
628
+ * `update_status` - we use this function to update the status line when the websocket connects and disconnects from the server.
629
+ * `Send` - this simple function sends the data from the input element to the websocket connection.
630
+
631
+ ##Congratulations!
632
+
633
+ Congratulations! You wrote your first Plezi chatroom :-)
634
+
635
+ Using this example we discovered that Plezi is a powerful Ruby framework that has easy and native support for both RESTful HTTP and WebSockets.
636
+
637
+ Plezi allowed us to easily write a very advanced application, while exploring exciting new features and discovering how Plezi could help our workflow.
638
+
639
+ There's a lot more to explore - enjoy :-)