plezi 0.12.13 → 0.12.14

Sign up to get free protection for your applications and to get access to all the features.
@@ -62,31 +62,26 @@ module Plezi
62
62
  # this method does two things.
63
63
  #
64
64
  # 1. sets redirection headers for the response.
65
- # 2. sets the `flash` object (short-time cookies) with all the values passed except the :status value.
65
+ # 2. sets the `flash` object (short-time cookies) with all the values passed except the :permanent value.
66
66
  #
67
67
  # use:
68
- # redirect_to 'http://google.com', notice: "foo", status: 302
69
- # # => redirects to 'http://google.com' with status 302 and adds notice: "foo" to the flash
70
- # or simply:
68
+ # redirect_to 'http://google.com', notice: "foo", permanent: true
69
+ # # => redirects to 'http://google.com' with status 301 (permanent redirection) and adds notice: "foo" to the flash
70
+ # or, a simple temporary redirect:
71
71
  # redirect_to 'http://google.com'
72
- # # => redirects to 'http://google.com' with status 302 (default status)
72
+ # # => redirects to 'http://google.com' with status 302 (default temporary redirection)
73
73
  #
74
- # if the url is a symbol, the method will try to format it into a correct url, replacing any
75
- # underscores ('_') with a backslash ('/').
74
+ # if the url is a symbol or a hash, the method will try to format it into a url Srting, using the `url_for` method.
76
75
  #
77
- # if the url is an empty string, the method will try to format it into a correct url
78
- # representing the index of the application (http://server/)
76
+ # if the url is a String, it will be passed along as is.
77
+ #
78
+ # An empty String or `nil` will be replaced with the root path for the request's specific host (i.e. `http://localhost:3000/`).
79
79
  #
80
80
  def redirect_to url, options = {}
81
81
  return super() if defined? super
82
- raise 'Cannot redirect after headers were sent.' if response.headers_sent?
83
- url = "#{request.base_url}/#{url.to_s.gsub('_', '/')}" if url.is_a?(Symbol) || ( url.is_a?(String) && url.empty? ) || url.nil?
82
+ url = full_url_for(url) unless url.is_a?(String) || url.nil?
84
83
  # redirect
85
- response.status = options.delete(:status) || 302
86
- response['location'] = url
87
- response['content-length'] ||= 0
88
- flash.update options
89
- true
84
+ response.redirect_to url, options
90
85
  end
91
86
 
92
87
  # Returns the RELATIVE url for methods in THIS controller (i.e.: "/path_to_controller/restful/params?non=restful&params=foo")
@@ -108,22 +103,22 @@ module Plezi
108
103
  end
109
104
  # same as #url_for, but returns the full URL (protocol:port:://host/path?params=foo)
110
105
  def full_url_for dest
111
- request.base_url + url_for(dest)
106
+ "#{request.base_url}#{self.class.url_for(dest)}"
112
107
  end
113
108
 
114
109
  # Send raw data to be saved as a file or viewed as an attachment. Browser should believe it had recieved a file.
115
110
  #
116
- # this is usful for sending 'attachments' (data to be downloaded) rather then
111
+ # this is useful for sending 'attachments' (data to be downloaded) rather then
117
112
  # a regular response.
118
113
  #
119
- # this is also usful for offering a file name for the browser to "save as".
114
+ # this is also useful for offering a file name for the browser to "save as".
120
115
  #
121
116
  # it accepts:
122
117
  # data:: the data to be sent - this could be a String or an open File handle.
123
118
  # options:: a hash of any of the options listed furtheron.
124
119
  #
125
120
  # the :symbol=>value options are:
126
- # type:: the type of the data to be sent. defaults to empty. if :filename is supplied, an attempt to guess will be made.
121
+ # type:: the mime-type of the data to be sent. defaults to empty. if :filename is supplied, an attempt to guess will be made.
127
122
  # inline:: sets the data to be sent an an inline object (to be viewed rather then downloaded). defaults to false.
128
123
  # filename:: sets a filename for the browser to "save as". defaults to empty.
129
124
  #
@@ -144,16 +139,16 @@ module Plezi
144
139
  true
145
140
  end
146
141
 
147
- # Renders a template file (.slim/.erb/.haml) or an html file (.html) to text and attempts to set the response's 'content-type' header (if it's still empty).
142
+ # Renders a template file (.slim/.erb/.haml) to a String and attempts to set the response's 'content-type' header (if it's still empty).
148
143
  #
149
144
  # For example, to render the file `body.html.slim` with the layout `main_layout.html.haml`:
150
145
  # render :body, layout: :main_layout
151
146
  #
152
147
  # or, for example, to render the file `json.js.slim`
153
- # render :json, type: 'js'
148
+ # render :json, format: 'js'
154
149
  #
155
150
  # or, for example, to render the file `template.haml`
156
- # render :template, type: ''
151
+ # render :template, format: ''
157
152
  #
158
153
  # template:: a Symbol for the template to be used.
159
154
  # options:: a Hash for any options such as `:layout` or `locale`.
@@ -94,6 +94,7 @@ module Plezi
94
94
  end
95
95
 
96
96
  # called immediately after a WebSocket connection has been established.
97
+ # it blocks all the connection's actions until the `on_open` initialization is finished.
97
98
  def on_open
98
99
  true
99
100
  end
@@ -106,11 +107,15 @@ module Plezi
106
107
  _push "your message was sent: #{data.to_s}"
107
108
  end
108
109
 
109
- # called when a disconnect packet has been recieved or the connection has been cut
110
- # (ISN'T called after a disconnect message has been sent).
110
+ # called once, AFTER the connection was closed.
111
111
  def on_close
112
112
  end
113
113
 
114
+ # called once, during **server shutdown**, BEFORE the connection is closed.
115
+ # this will only be called for connections that are open while the server is shutting down.
116
+ def on_shutdown
117
+ end
118
+
114
119
  # a demo event method that recieves a broadcast from instance siblings.
115
120
  #
116
121
  # methods that are protected and methods that start with an underscore are hidden from the router
@@ -3,12 +3,18 @@ module Plezi
3
3
 
4
4
  # Sends common basic HTTP responses.
5
5
  module HTTPSender
6
- class CodeContext
7
- attr_accessor :request
8
- def initialize request
9
- @request = request
6
+ class ErrorCtrl
7
+ include ::Plezi::Base::ControllerCore
8
+ include ::Plezi::ControllerMagic
9
+
10
+ def index
11
+ render(response.status.to_s) || (params[:format] && (params[:format] != 'html'.freeze) && render(response.status.to_s, format: 'html'.freeze)) || ((response['content-type'.freeze] = 'text/plain'.freeze) && response.class::STATUS_CODES[response.status])
12
+ end
13
+ def requested_method
14
+ :index
10
15
  end
11
16
  end
17
+
12
18
  module_function
13
19
 
14
20
  ######
@@ -18,13 +24,10 @@ module Plezi
18
24
  # sends a response for an error code, rendering the relevent file (if exists).
19
25
  def send_by_code request, response, code, headers = {}
20
26
  begin
21
- base_code_path = request[:host_settings][:templates] || File.expand_path('.')
22
- fn = File.join(base_code_path, "#{code}.html")
23
- rendered = ::Plezi::Renderer.render fn, binding #CodeContext.new(request)
24
- return send_raw_data request, response, rendered, 'text/html', code, headers if rendered
25
- return send_file(request, response, fn, code, headers) if Plezi.file_exists?(fn)
26
- return true if send_raw_data(request, response, response.class::STATUS_CODES[code], 'text/plain', code, headers)
27
- rescue Exception => e
27
+ response.status = code
28
+ headers.each {|k, v| response[k] = v}
29
+ return ErrorCtrl.new(request, response).index
30
+ rescue => e
28
31
  Plezi.error e
29
32
  end
30
33
  false
@@ -36,8 +39,8 @@ module Plezi
36
39
  def send_static_file request, response
37
40
  root = request[:host_settings][:public]
38
41
  return false unless root
39
- file_requested = request[:path].to_s.split('/')
40
- unless file_requested.include? '..'
42
+ file_requested = request[:path].to_s.split('/'.freeze)
43
+ unless file_requested.include? '..'.freeze
41
44
  file_requested.shift
42
45
  file_requested = File.join(root, *file_requested)
43
46
  return true if send_file request, response, file_requested
@@ -62,14 +65,12 @@ module Plezi
62
65
  def send_raw_data request, response, data, mime, status_code = 200, headers = {}
63
66
  headers.each {|k, v| response[k] = v}
64
67
  response.status = status_code if response.status == 200 # avoid resetting a manually set status
65
- response['content-type'] = mime
66
- response['cache-control'] ||= 'public, max-age=86400'
68
+ response['content-type'.freeze] = mime
69
+ response['cache-control'.freeze] ||= 'public, max-age=86400'.freeze
67
70
  response.body = data
68
71
  # response['content-length'] = data.bytesize #this one is automated by the server and should be avoided to support Range requests.
69
72
  true
70
- end##########
71
-
72
-
73
+ end
73
74
  end
74
75
 
75
76
  end
@@ -1,3 +1,3 @@
1
1
  module Plezi
2
- VERSION = "0.12.13"
2
+ VERSION = "0.12.14"
3
3
  end
@@ -18,7 +18,7 @@ Gem::Specification.new do |spec|
18
18
  spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
19
19
  spec.require_paths = ["lib"]
20
20
 
21
- spec.add_dependency "iodine", "~> 0.1.13"
21
+ spec.add_dependency "iodine", "~> 0.1.14"
22
22
  spec.add_development_dependency "bundler", "~> 1.7"
23
23
  spec.add_development_dependency "rake", "~> 10.0"
24
24
 
@@ -1,4 +1,9 @@
1
- <!DOCTYPE html>
1
+ <%
2
+ # # consider replacing the Html with the following Ruby code:
3
+ # response.redirect_to "/", notice: "Sory, we couldn't find #{request.original_path}", status: 404
4
+ # # OR
5
+ # render :template, layout: :layout
6
+ %><!DOCTYPE html>
2
7
  <head>
3
8
  <style>
4
9
  /*
@@ -1,4 +1,9 @@
1
- <!DOCTYPE html>
1
+ <%
2
+ # # consider replacing the Html with the following Ruby code:
3
+ # response.redirect_to "/", notice: "Internal error for #{request.original_path}", status: 500
4
+ # # OR
5
+ # render :template, layout: :layout
6
+ %><!DOCTYPE html>
2
7
  <head>
3
8
  <style>
4
9
  /*
@@ -344,7 +344,7 @@ module PleziTestTasks
344
344
  puts " **** #url_for test FAILED TO RUN!!!"
345
345
  puts e
346
346
  end
347
- def test_placebo
347
+ def placebo_test
348
348
  puts " * Starting placebo tests..."
349
349
  ws = Iodine::Http::WebsocketClient.connect("ws://localhost:3000/ws/placebo") {|ws| 'ME?'}
350
350
  ws << " * Placebo WS connected."
@@ -355,15 +355,15 @@ module PleziTestTasks
355
355
  puts e
356
356
  end
357
357
  def test_websocket
358
- connection_test = broadcast_test = echo_test = unicast_test = false
358
+ connection_test = broadcast_test = echo_test = unicast_test = nil
359
359
  begin
360
- ws4 = Iodine::Http::WebsocketClient.connect("ws://localhost:3000") do |data|
360
+ ws4 = Iodine::Http::WebsocketClient.connect("ws://localhost:3000/") do |data|
361
361
  if data == "unicast"
362
362
  puts " * Websocket unicast testing: #{RESULTS[false]}"
363
363
  unicast_test = :failed
364
364
  end
365
365
  end
366
- ws2 = Iodine::Http::WebsocketClient.connect("ws://localhost:3000") do |data|
366
+ ws2 = Iodine::Http::WebsocketClient.connect("ws://localhost:3000/") do |data|
367
367
  next unless @is_connected || !( (@is_connected = true) )
368
368
  if data == "unicast"
369
369
  puts " * Websocket unicast message test: #{RESULTS[false]}"
@@ -374,17 +374,17 @@ module PleziTestTasks
374
374
  go_test = false
375
375
  end
376
376
  end
377
- ws3 = Iodine::Http::WebsocketClient.connect("ws://localhost:3000", on_open: -> { write 'get uuid' } ) do |data|
377
+ ws3 = Iodine::Http::WebsocketClient.connect("ws://localhost:3000/", on_open: -> { write 'get uuid' } ) do |data|
378
378
  if data.match /uuid: ([^s]*)/
379
379
  ws2 << "to: #{data.match(/^uuid: ([^s]*)/)[1]}"
380
380
  puts " * Websocket UUID for unicast testing: #{data.match(/^uuid: ([^s]*)/)[1]}"
381
381
  elsif data == "unicast"
382
- puts " * Websocket unicast testing: #{RESULTS[:waiting]}"
382
+ puts " * Websocket unicast testing: #{RESULTS[:waiting]} (target received data)"
383
383
  unicast_test ||= true
384
384
  end
385
385
  end
386
386
  puts " * Websocket client test: #{RESULTS[ws2 && true]}"
387
- ws1 = Iodine::Http::WebsocketClient.connect("ws://localhost:3000") do |data|
387
+ ws1 = Iodine::Http::WebsocketClient.connect("ws://localhost:3000/") do |data|
388
388
  unless @connected
389
389
  puts " * Websocket connection message test: #{RESULTS[connection_test = (data == 'connected')]}"
390
390
  @connected = true
@@ -409,8 +409,7 @@ module PleziTestTasks
409
409
  else
410
410
  remote << "Hello websockets!"
411
411
  end
412
- sleep 0.5
413
- [ws1, ws2, ws3, ws4, remote].each {|ws| ws.close}
412
+ Iodine.run_after(30) { [ws1, ws2, ws3, ws4, remote].each {|ws| ws.close} }
414
413
  PL.on_shutdown {puts " * Websocket connection message test: #{RESULTS[connection_test]}" unless connection_test}
415
414
  PL.on_shutdown {puts " * Websocket echo message test: #{RESULTS[echo_test]}" unless echo_test}
416
415
  PL.on_shutdown {puts " * Websocket broadcast message test: #{RESULTS[broadcast_test]}" unless broadcast_test}
@@ -422,7 +421,7 @@ module PleziTestTasks
422
421
  if should_disconnect
423
422
  puts " * Websocket size disconnection test: #{RESULTS[false]}"
424
423
  else
425
- puts " * Websocket message size test: got #{data.bytesize} bytes"
424
+ puts " * Websocket message size test: got #{data.bytesize} bytes starting with #{data[0..10]}"
426
425
  end
427
426
  end
428
427
  ws.on_close do
@@ -566,15 +565,17 @@ Plezi.run do
566
565
  puts " --- Starting tests"
567
566
  puts " --- Failed tests should read: #{PleziTestTasks::RESULTS[false]}"
568
567
 
568
+ PleziTestTasks.run_tests
569
+
569
570
  r = Plezi::Placebo.new PlaceboCtrl
570
571
  puts " * Create Placebo test: #{PleziTestTasks::RESULTS[r && true]}"
571
572
  puts " * Placebo admists to being placebo: #{PleziTestTasks::RESULTS[PlaceboCtrl.placebo?]}"
572
573
  puts " * Regular controller answers placebo: #{PleziTestTasks::RESULTS[!PlaceboTestCtrl.placebo?]}"
573
-
574
- PleziTestTasks.run_tests
574
+ PleziTestTasks.placebo_test
575
575
 
576
576
  shoutdown_test = false
577
577
  Plezi.on_shutdown { puts " * Shutdown test: #{ PleziTestTasks::RESULTS[shoutdown_test] }" }
578
578
  Plezi.on_shutdown { shoutdown_test = true }
579
+ puts "Press ^C to exit."
579
580
 
580
581
  end
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.12.13
4
+ version: 0.12.14
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-11-13 00:00:00.000000000 Z
11
+ date: 2015-11-14 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: iodine
@@ -16,14 +16,14 @@ dependencies:
16
16
  requirements:
17
17
  - - "~>"
18
18
  - !ruby/object:Gem::Version
19
- version: 0.1.13
19
+ version: 0.1.14
20
20
  type: :runtime
21
21
  prerelease: false
22
22
  version_requirements: !ruby/object:Gem::Requirement
23
23
  requirements:
24
24
  - - "~>"
25
25
  - !ruby/object:Gem::Version
26
- version: 0.1.13
26
+ version: 0.1.14
27
27
  - !ruby/object:Gem::Dependency
28
28
  name: bundler
29
29
  requirement: !ruby/object:Gem::Requirement
@@ -75,6 +75,7 @@ files:
75
75
  - docs/routes.md
76
76
  - docs/websockets.md
77
77
  - lib/plezi.rb
78
+ - lib/plezi/builders/ac_model.rb
78
79
  - lib/plezi/builders/app_builder.rb
79
80
  - lib/plezi/builders/builder.rb
80
81
  - lib/plezi/builders/form_builder.rb
@@ -137,7 +138,6 @@ files:
137
138
  - resources/welcome_page.html
138
139
  - test/console
139
140
  - test/plezi_tests.rb
140
- - websocket chatroom.md
141
141
  homepage: http://www.plezi.io/
142
142
  licenses:
143
143
  - MIT
@@ -1,629 +0,0 @@
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` or `plezi mini myapp` commands, 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 Plezi's Http Host parameters.
71
-
72
- A host, in this case, refers to the domain name that the request belongs to. The default domain name is a catch-all (answers requests with any domain name). Setting up hosts is a great way to manage sub-domains (i.e. serving two different home pages for `www.example.com` and `admin.example.com`).
73
-
74
- We will have only one one host for this application, so it's very easy to set up.
75
-
76
- As you can see, some options are there for later, but are disabled for now. here are some of the common options:
77
-
78
- - **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.
79
-
80
- - **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.
81
-
82
- - **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.
83
-
84
- - **_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.
85
-
86
- - **host**: a host name, if we need one (can also be a Regexp object).
87
-
88
- ```ruby
89
- host_options = {
90
- # public: Root.join('public').to_s,
91
- # assets: Root.join('assets').to_s,
92
- # assets_public: '/assets',
93
- templates: Root.join('views').to_s
94
- }
95
- ```
96
-
97
- Next we call the `host` command - this command sets up some of the host options we will need, such as the templates folder. We will use only the main host for now (the catch-all main host is the `:default` host)
98
-
99
- 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`).
100
-
101
- ```ruby
102
- host host_options
103
- ```
104
-
105
- 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.
106
-
107
- ```ruby
108
- route '/', ChatController
109
- ```
110
-
111
- 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').
112
-
113
- 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:
114
-
115
- ```ruby
116
- route '/(:id)', ChatController
117
- ```
118
-
119
- ###The Controller - serving regular data (HTTP)
120
-
121
- Let's take a deeper look into our controller and start filling it in...
122
-
123
- ####serving the main html template file (index)
124
-
125
- 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.
126
-
127
- Since controllers can work like virtual folders with support for RESTful methods, we can define an `index` method to do this simple task:
128
-
129
- ```ruby
130
- def index
131
- #... later
132
- end
133
- ```
134
-
135
- 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.
136
-
137
- Lets fill in our `index` method:
138
-
139
- ```ruby
140
- class ChatController
141
- def index
142
- response['content-type'] = 'text/html'
143
- response << render(:chat)
144
- true
145
- end
146
- end
147
- ```
148
-
149
- 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.
150
-
151
- Let's rewrite our `index` method to make it cleaner:
152
-
153
- ```ruby
154
- class ChatController
155
- def index
156
- response['content-type'] = 'text/html'
157
- render(:chat) # since this String is the returned value, it works.
158
- end
159
- end
160
- ```
161
-
162
- 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.
163
-
164
- 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.
165
-
166
- ####Telling people that we made this cool app!
167
-
168
- 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...
169
-
170
- With Plezi, that's super easy.
171
-
172
- Since out ChatController is at the root of our application, let's add a `people.txt` method to our ChatController:
173
-
174
- method names cant normally have the dot in their name, do we will use a helper method for this special name.
175
-
176
- ```ruby
177
- def_special_method "people.txt" do
178
- "I wrote this app :)"
179
- end
180
- ```
181
-
182
- 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.
183
-
184
- 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 :)
185
-
186
- ###The Controller - live input and pushing data (WebSockets)
187
-
188
- We are building a somewhat advanced application here - this is _not_ another 'hello world' - lets start exploring the advanced stuff.
189
-
190
- ####Supporting WebSockets
191
-
192
- To accept WebSockets connections, our controller must define an `on_message(data)` method.
193
-
194
- Plezi will recognize this method and allow websocket connections for our controller's path (which is at the root of our application).
195
-
196
- 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.
197
-
198
- We will start by formatting our data to JSON (or closing the connection if someone is sending corrupt data):
199
-
200
- ```ruby
201
- def on_message data
202
- begin
203
- data = JSON.parse data
204
- rescue Exception => e
205
- response << {event: :error, message: "Unknown Error"}.to_json
206
- response.close
207
- return false
208
- end
209
- end
210
- ```
211
-
212
- ####Pausing for software design - the Chatroom challange
213
-
214
- To design a chatroom we will need a few things:
215
-
216
- 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.
217
- 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.
218
- 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.
219
- 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.
220
-
221
- We can use the :id parameter to set the nickname.
222
-
223
- the :id is an automatic parameter that Plezi appended to our path like already explained and it's perfect for our current needs.
224
-
225
- 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?
226
-
227
- ####Broadcasting chat (websocket) messages
228
-
229
- When we get a chat message, with `on_message(data)`, we will want to broadcast this message to all the _other_ ChatController connections.
230
-
231
- Using JSON, our new `on_message(data)` method can look something like this:
232
-
233
- ```ruby
234
- def on_message data
235
- begin
236
- data = JSON.parse data
237
- rescue Exception => e
238
- response << {event: :error, message: "Unknown Error"}.to_json
239
- response.close
240
- return false
241
- end
242
- message = {}
243
- message[:message] = data['message'] # should consider sanitizing this
244
- message[:event] = :chat
245
- message[:from] = params[:id]
246
- message[:at] = Time.now
247
- broadcast :_send_message, message.to_json
248
- end
249
- ```
250
-
251
- 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.
252
-
253
- ```ruby
254
- def on_message data
255
- begin
256
- data = JSON.parse data
257
- rescue Exception => e
258
- response << {event: :error, message: "Unknown Error"}.to_json
259
- response.close
260
- return false
261
- end
262
- broadcast :_send_message, {event: :chat, from: params[:id], message: ERB::Util.html_escape(data['message']), at: Time.now}.to_json
263
- end
264
- ```
265
-
266
- Now that the boring stuff is condenced, let's look at that last line - the one that calls `broadcast`
267
-
268
- `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.
269
-
270
- 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!
271
-
272
- ####The \_send_message method
273
-
274
- Let's start with the name - why the underscore at the beginning?
275
-
276
- 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...
277
-
278
- Plezi allows us to 'exclude' some methods from this auto-recogntion. protected methods and methods starting with an underscore (\_) are ignored by the Plezi router.
279
-
280
- Since I was too lzy to write the `protected` keyword, I just added an underscore at the begining of the name.
281
-
282
- This will be our `_send_message` method:
283
-
284
- ```ruby
285
- def _send_message data
286
- response << data
287
- end
288
- ```
289
-
290
- Did you notice the difference between WebSocket responses and HTTP?
291
-
292
- Many times, Websockets are used to do internal work. This is why information is safeguarded and isn't automatically sent back (unlike HTTP, where a response is expected). In WebSockets, we must use the `<<` method to add data to the response stream.
293
-
294
-
295
- ####Telling people that we left the chatroom
296
-
297
- Another feature we want to put in, is letting people know when someone enters or leaves the chatroom.
298
-
299
- Using the `broadcast` method with the special `on_disconnect` websocket method, makes telling people we left an easy task...
300
-
301
- ```ruby
302
- def on_close
303
- message = {event: :chat, from: '', at: Time.now}
304
- message[:message] = "#{params[:id]} left the chatroom."
305
- broadcast :_send_message, message.to_json if params[:id]
306
- end
307
- ```
308
-
309
- 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.
310
-
311
- Let's make it a bit shorter?
312
-
313
- ```ruby
314
- def on_close
315
- broadcast :_send_message, {event: :chat, from: '', at: Time.now, message: "#{params[:id]} left the chatroom."}.to_json if params[:id]
316
- end
317
- ```
318
-
319
- ####The login process and telling people we're here
320
-
321
- 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...
322
-
323
- First, we will ensure the new connection has a nickname (the connection was made to '/nickname' rather then the root of our application '/'):
324
-
325
- ```ruby
326
- def on_open
327
- if params[:id].nil?
328
- response << {event: :error, from: :system, at: Time.now, message: "Error: cannot connect without a nickname!"}.to_json
329
- response.close
330
- return false
331
- end
332
- end
333
- ```
334
-
335
- Easy?
336
-
337
- There's an even easier and safer way to do this, which doesn't send an error message back, it looks like this:
338
-
339
- ```ruby
340
- def pre_connect
341
- return false if params[:id].nil?
342
- true
343
- end
344
- ```
345
-
346
- 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).
347
-
348
- 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:
349
-
350
- ```ruby
351
- message = {from: '', at: Time.now}
352
- name = params[:id]
353
- if (name.match(/admin|admn|system|sys|administrator/i))
354
- message[:event] = :error
355
- message[:message] = "The nickname '#{name}' is refused."
356
- response << message.to_json
357
- params[:id] = false
358
- response.close
359
- return
360
- end
361
- ```
362
-
363
- 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...:
364
-
365
- ```ruby
366
- message = {from: '', at: Time.now}
367
- message[:event] = :chat
368
- message[:message] = "Welcome #{params[:id]}."
369
- response << message.to_json
370
- message[:message] = "#{params[:id]} joined the chatroom."
371
- broadcast :_send_message, message.to_json
372
- ```
373
-
374
-
375
- This will be our final `on_open` method:
376
-
377
- ```ruby
378
- def on_open
379
- if params[:id].nil?
380
- response << {event: :error, from: :system, at: Time.now, message: "Error: cannot connect without a nickname!"}.to_json
381
- response.close
382
- return false
383
- end
384
- message = {from: '', at: Time.now}
385
- name = params[:id]
386
- if (name.match(/admin|admn|system|sys|administrator/i))
387
- message[:event] = :error
388
- message[:message] = "The nickname '#{name}' is already taken."
389
- response << message.to_json
390
- params[:id] = false
391
- response.close
392
- return
393
- end
394
- message[:event] = :chat
395
- message[:message] = "Welcome #{params[:id]}."
396
- # Should you end up storing your connected user names inside a manged list
397
- # in redis or a database and then read that into a variable called 'list'
398
- # here is some code you can use to write a message to the user based on the
399
- # people currently in that list.
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] = "#{name} joined the chatroom."
403
- broadcast :_send_message, message.to_json
404
- end
405
- ```
406
-
407
- ###The Complete Ruby Code < (less then) 75 lines
408
-
409
- This is our complete `mychat.rb` Ruby application code:
410
-
411
- ```ruby
412
- #!/usr/bin/env ruby
413
- # encoding: UTF-8
414
-
415
- require 'plezi'
416
-
417
- class ChatController
418
- def index
419
- response['content-type'] = 'text/html'
420
- render(:chat)
421
- end
422
- def_special_method "people.txt" do
423
- "I wrote this app :)"
424
- end
425
- def on_message data
426
- begin
427
- data = JSON.parse data
428
- rescue Exception => e
429
- response << {event: :error, message: "Unknown Error"}.to_json
430
- response.close
431
- return false
432
- end
433
- broadcast :_send_message, {event: :chat, from: params[:id], message: ERB::Util.html_escape(data['message']), at: Time.now}.to_json
434
- end
435
- def _send_message data
436
- response << data
437
- end
438
- def on_open
439
- if params[:id].nil?
440
- response << {event: :error, from: :system, at: Time.now, message: "Error: cannot connect without a nickname!"}.to_json
441
- response.close
442
- return false
443
- end
444
- message = {from: '', at: Time.now}
445
- name = params[:id]
446
- if (name.match(/admin|admn|system|sys|administrator/i))
447
- message[:event] = :error
448
- message[:message] = "The nickname '#{name}' is already taken."
449
- response << message.to_json
450
- params[:id] = false
451
- response.close
452
- return
453
- end
454
- message[:event] = :chat
455
- message[:message] = "Welcome #{params[:id]}."
456
- # Should you end up storing your connected user names inside a manged list
457
- # in redis or a database and then read that into a variable called 'list'
458
- # here is some code you can use to write a message to the user based on the
459
- # people currently in that list.
460
- # 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"
461
- response << message.to_json
462
- message[:message] = "#{name} joined the chatroom."
463
- broadcast :_send_message, message.to_json
464
- end
465
-
466
-
467
- def on_close
468
- broadcast :_send_message, {event: :chat, from: '', at: Time.now, message: "#{params[:id]} left the chatroom."}.to_json if params[:id]
469
- end
470
- end
471
-
472
- # Using pathname extentions for setting public folder
473
- require 'pathname'
474
- # set up the Root object for easy path access.
475
- Root = Pathname.new(File.dirname(__FILE__)).expand_path
476
-
477
- # set up the Plezi service options
478
- host_options = {
479
- # root: Root.join('public').to_s,
480
- # assets: Root.join('assets').to_s,
481
- # assets_public: '/',
482
- templates: Root.join('views').to_s
483
- }
484
-
485
- host host_options
486
-
487
- # this routes the root of the application ('/') to our ChatController
488
- route '/:id', ChatController
489
- ```
490
-
491
- ##The HTML - a web page with websockets
492
-
493
- The [official websockets page](https://www.websocket.org) has great info about websockets and some tips about creating web pages with WebSocket features.
494
-
495
- 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...
496
-
497
- ...**this is probably the hardest part in the code** (maybe because it isn't Ruby).
498
-
499
- 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`.
500
-
501
- `.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.
502
-
503
- 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?
504
-
505
- Anyway, here's the HTML code, copy it and I'll explain the code in a bit:
506
-
507
- ```html
508
- <!DOCTYPE html>
509
- <head>
510
- <meta charset='UTF-8'>
511
- <style>
512
- html, body {width: 100%; height:100%;}
513
- body {font-size: 1.5em; background-color: #eee;}
514
- p {padding: 0.2em; margin: 0;}
515
- .received { color: #00f;}
516
- .sent { color: #80f;}
517
- input, #output, #status {font-size: 1em; width: 60%; margin: 0.5em 19%; padding: 0.5em 1%;}
518
- input[type=submit] { margin: 0.5em 20%; padding: 0;}
519
- #output {height: 60%; overflow: auto; background-color: #fff;}
520
- .connected {background-color: #efe;}
521
- .disconnected {background-color: #fee;}
522
- </style>
523
- <script>
524
- var websocket = NaN;
525
- var last_msg = NaN;
526
- function Connect() {
527
- websocket = new WebSocket( (window.location.protocol.indexOf('https') < 0 ? 'ws' : 'wss') + '://' + window.location.hostname + (window.location.port == '' ? '' : (':' + window.location.port) ) + "/" + document.getElementById("input").value );
528
- }
529
- function Init()
530
- {
531
- Connect()
532
- websocket.onopen = function(e) { update_status(); WriteStatus({'message':'Connected :)'})};
533
- websocket.onclose = function(e) { websocket = NaN; update_status(); };
534
- websocket.onmessage = function(e) {
535
- var msg = JSON.parse(e.data)
536
- last_msg = msg
537
- if(msg.event == 'chat') WriteMessage(msg, 'received')
538
- if(msg.event == 'error') WriteStatus(msg)
539
- };
540
- websocket.onerror = function(e) { websocket = NaN; update_status(); };
541
- }
542
- function WriteMessage( message, message_type )
543
- {
544
- if (!message_type) message_type = 'received'
545
- var msg = document.createElement("p");
546
- msg.className = message_type;
547
- msg.innerHTML = message.from + ": " + message.message;
548
- document.getElementById("output").appendChild(msg);
549
- }
550
- function WriteStatus( message )
551
- {
552
- document.getElementById("status").innerHTML = message.message;
553
- }
554
- function Send()
555
- {
556
- var msg = {'event':'chat', 'from':'me', 'message':document.getElementById("input").value}
557
- WriteMessage(msg, 'sent');
558
- websocket.send(JSON.stringify(msg));
559
- }
560
- function update_status()
561
- {
562
- if(websocket)
563
- {
564
- document.getElementById("submit").value = "Send"
565
- document.getElementById("input").placeholder = "your message goes here"
566
- document.getElementById("status").className = "connected"
567
- }
568
- else
569
- {
570
- document.getElementById("submit").value = "Connect"
571
- document.getElementById("input").placeholder = "your nickname"
572
- document.getElementById("status").className = "disconnected"
573
- if(last_msg.event != 'error') document.getElementById("status").innerHTML = "Please choose your nickname and join in..."
574
- }
575
- }
576
- function on_submit()
577
- {
578
- if(websocket)
579
- {
580
- Send()
581
- }
582
- else
583
- {
584
- Init()
585
- }
586
- document.getElementById("input").value = ""
587
- }
588
- </script>
589
- </head>
590
- <body>
591
- <div id='status' class='disconnected'>Please choose your nickname and join in...</div>
592
- <div id='output'></div>
593
- <form onsubmit='on_submit(); return false'>
594
- <input id='input' type='text' placeholder='your nickname.' value='' />
595
- <input type='submit' value='Connect' id='submit' />
596
- </form>
597
- </body>
598
- ```
599
-
600
- 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.
601
-
602
- All the interesting bits are in the Javascript.
603
-
604
- 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.
605
-
606
- The CSS is just a bit of styling so the page doesn't look too bad.
607
-
608
- 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').
609
-
610
- I will go over some of the JavaScript highlights very quickly, as there are a lot of tutorials out there regarding websockets and javascript.
611
-
612
- The main javascript functions we are using are:
613
-
614
- * `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`.
615
- * `init` - this is a very interesting function that defines all the callbacks we might need for the websocket to actually work.
616
- * `WriteMessage` - this simple function adds text to the `output` element, adding the different styles as needed.
617
- * `WriteStatus` - this function is used to update the status line.
618
- * `update_status` - we use this function to update the status line when the websocket connects and disconnects from the server.
619
- * `Send` - this simple function sends the data from the input element to the websocket connection.
620
-
621
- ##Congratulations!
622
-
623
- Congratulations! You wrote your first Plezi chatroom :-)
624
-
625
- Using this example we discovered that Plezi is a powerful Ruby framework that has easy and native support for both RESTful HTTP and WebSockets.
626
-
627
- Plezi allowed us to easily write a very advanced application, while exploring exciting new features and discovering how Plezi could help our workflow.
628
-
629
- There's a lot more to explore - enjoy :-)