plezi 0.12.22 → 0.14.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (108) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +18 -0
  3. data/LICENSE.txt +17 -18
  4. data/README.md +54 -698
  5. data/Rakefile +7 -4
  6. data/bin/config.ru +22 -0
  7. data/{test → bin}/console +4 -6
  8. data/bin/hello_world +52 -0
  9. data/bin/setup +8 -0
  10. data/exe/plezi +145 -0
  11. data/lib/plezi.rb +24 -137
  12. data/lib/plezi/activation.rb +28 -0
  13. data/lib/plezi/api.rb +62 -0
  14. data/lib/plezi/controller/controller.rb +259 -0
  15. data/lib/plezi/controller/controller_class.rb +176 -0
  16. data/lib/plezi/controller/cookies.rb +40 -0
  17. data/lib/plezi/helpers.rb +60 -0
  18. data/lib/plezi/rake.rb +2 -24
  19. data/lib/plezi/render/erb.rb +34 -0
  20. data/lib/plezi/render/has_cache.rb +36 -0
  21. data/lib/plezi/render/markdown.rb +63 -0
  22. data/lib/plezi/render/render.rb +49 -0
  23. data/lib/plezi/render/sass.rb +55 -0
  24. data/lib/plezi/render/slim.rb +33 -0
  25. data/lib/plezi/router/adclient.rb +23 -0
  26. data/lib/plezi/router/assets.rb +67 -0
  27. data/lib/plezi/router/errors.rb +29 -0
  28. data/lib/plezi/router/route.rb +112 -0
  29. data/lib/plezi/router/router.rb +120 -0
  30. data/lib/plezi/version.rb +1 -1
  31. data/lib/plezi/websockets/message_dispatch.rb +91 -0
  32. data/lib/plezi/websockets/redis.rb +55 -0
  33. data/plezi.gemspec +25 -16
  34. data/resources/404.erb +5 -4
  35. data/resources/500.erb +5 -4
  36. data/resources/{500.html → 503.html} +8 -9
  37. data/resources/client.js +253 -0
  38. data/resources/config.ru +5 -36
  39. data/resources/ctrlr.rb +34 -0
  40. data/resources/gemfile +4 -0
  41. data/resources/mini_app.rb +28 -82
  42. data/resources/mini_exec +7 -0
  43. data/resources/mini_welcome_page.html +0 -0
  44. data/resources/procfile +3 -0
  45. data/resources/rakefile +4 -8
  46. data/resources/routes.rb +9 -26
  47. data/resources/{websockets.js → simple-client.js} +3 -3
  48. metadata +60 -85
  49. data/bin/plezi +0 -104
  50. data/docs/async_helpers.md +0 -245
  51. data/docs/controllers.md +0 -27
  52. data/docs/logging.md +0 -49
  53. data/docs/routes.md +0 -209
  54. data/docs/websockets.md +0 -213
  55. data/lib/plezi/builders/ac_model.rb +0 -59
  56. data/lib/plezi/builders/app_builder.rb +0 -137
  57. data/lib/plezi/builders/builder.rb +0 -43
  58. data/lib/plezi/builders/form_builder.rb +0 -27
  59. data/lib/plezi/common/api.rb +0 -92
  60. data/lib/plezi/common/cache.rb +0 -122
  61. data/lib/plezi/common/defer.rb +0 -21
  62. data/lib/plezi/common/dsl.rb +0 -94
  63. data/lib/plezi/common/redis.rb +0 -65
  64. data/lib/plezi/common/renderer.rb +0 -141
  65. data/lib/plezi/common/settings.rb +0 -52
  66. data/lib/plezi/handlers/controller_core.rb +0 -106
  67. data/lib/plezi/handlers/controller_magic.rb +0 -284
  68. data/lib/plezi/handlers/http_router.rb +0 -205
  69. data/lib/plezi/handlers/placebo.rb +0 -112
  70. data/lib/plezi/handlers/route.rb +0 -216
  71. data/lib/plezi/handlers/session.rb +0 -109
  72. data/lib/plezi/handlers/stubs.rb +0 -156
  73. data/lib/plezi/handlers/ws_identity.rb +0 -253
  74. data/lib/plezi/handlers/ws_object.rb +0 -308
  75. data/lib/plezi/helpers/http_sender.rb +0 -84
  76. data/lib/plezi/helpers/magic_helpers.rb +0 -104
  77. data/lib/plezi/helpers/mime_types.rb +0 -1995
  78. data/lib/plezi/oauth.rb +0 -5
  79. data/lib/plezi/oauth/auth_controller.rb +0 -229
  80. data/logo/dark.png +0 -0
  81. data/logo/light.png +0 -0
  82. data/logo/sign.png +0 -0
  83. data/resources/404.haml +0 -121
  84. data/resources/404.html +0 -124
  85. data/resources/404.slim +0 -120
  86. data/resources/500.haml +0 -120
  87. data/resources/500.slim +0 -120
  88. data/resources/Gemfile +0 -86
  89. data/resources/code.rb +0 -8
  90. data/resources/controller.rb +0 -142
  91. data/resources/database.yml +0 -33
  92. data/resources/db_ac_config.rb +0 -59
  93. data/resources/db_dm_config.rb +0 -51
  94. data/resources/db_sequel_config.rb +0 -33
  95. data/resources/en.yml +0 -204
  96. data/resources/haml_config.rb +0 -6
  97. data/resources/i18n_config.rb +0 -14
  98. data/resources/initialize.rb +0 -49
  99. data/resources/mini_exec.rb +0 -7
  100. data/resources/oauth_config.rb +0 -24
  101. data/resources/plezi_client.js +0 -198
  102. data/resources/plezi_websockets.html +0 -47
  103. data/resources/redis_config.rb +0 -42
  104. data/resources/slim_config.rb +0 -11
  105. data/resources/welcome_page.html +0 -272
  106. data/test/dispatch +0 -58
  107. data/test/hello_world +0 -13
  108. data/test/plezi_tests.rb +0 -581
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 8eab38fb8e80a3c148bfcc217f44e3ade73f9adc
4
- data.tar.gz: 5bcdd05f5c30e0cf177b983d757f21e6c13b7bf8
3
+ metadata.gz: e0ec77845c19054f8f3ded8e4ee36f80b9ef5c16
4
+ data.tar.gz: fca528a70e26a270a600779a5998371f55c83bfd
5
5
  SHA512:
6
- metadata.gz: 60bfb7361f4f50d380f6db994795959ddb611e421e42a1f09ea4a9cd1dcc36c7eac1a5c5edd094ae5bffe52f4176f8ca2cb46af134f718b18ceb6e1725bb5ce4
7
- data.tar.gz: d8ecf48b40a0feacec0cc0b86dae87a00cd19f0523e58a829d0d9d1f19eaf9d82816fea4d4e7f2ae85d407786075f032ccf8c267162f9d8b11465f755f1debc1
6
+ metadata.gz: 8e5d7edbd9c0c078094905ae49bb701e20a71c988a0cd712424bb049b46dba7b3e5225b2d0ef38526b786b976bc39d220fbf272a5393b2f43d6ea370345edc53
7
+ data.tar.gz: 3b6940cb92999d6462db8739b710eb0472e18cfdd2a737b1da91b20300689e57380e3821632f599179643f37c49ed57c76b4862f055c4ddf6004cc883e0dfc14
data/CHANGELOG.md CHANGED
@@ -2,6 +2,24 @@
2
2
 
3
3
  ***
4
4
 
5
+ Change log v.0.14.0
6
+
7
+ Rewrote the whole thing. v. 0.14.0 is a total restart...
8
+
9
+ ...in fact, the changes were so big, we're bumping the developemnt version twice.
10
+
11
+ You might wonder what changed and what stayed the same. Well... we kept the name.
12
+
13
+ API changes ahead.
14
+
15
+ Features were **removed** (I know, features are usually *added*, but Plezi will not become another Sinatra / Rails).
16
+
17
+ ***
18
+
19
+ Pre 0.14.0
20
+
21
+ ***
22
+
5
23
  Change log v.0.12.22
6
24
 
7
25
  **Fix**: fix for issue #17 where unicode characters might cause `erb` rendering to fail. Credit to @davidjuin0519 (Juin Chiu) for reporting the issue and helping resolve it.
data/LICENSE.txt CHANGED
@@ -1,22 +1,21 @@
1
- Copyright (c) 2014 Myst
1
+ The MIT License (MIT)
2
2
 
3
- MIT License
3
+ Copyright (c) 2016 Boaz Segev
4
4
 
5
- Permission is hereby granted, free of charge, to any person obtaining
6
- a copy of this software and associated documentation files (the
7
- "Software"), to deal in the Software without restriction, including
8
- without limitation the rights to use, copy, modify, merge, publish,
9
- distribute, sublicense, and/or sell copies of the Software, and to
10
- permit persons to whom the Software is furnished to do so, subject to
11
- the following conditions:
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
12
11
 
13
- The above copyright notice and this permission notice shall be
14
- included in all copies or substantial portions of the Software.
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
15
14
 
16
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
- EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
- MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
- NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
- LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
- OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
- WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md CHANGED
@@ -1,756 +1,112 @@
1
- # [<img src='https://raw.githubusercontent.com/boazsegev/plezi/master/logo/dark.png' alt='Plezi' style='width:20em;' />](http://www.plezi.io)
1
+ # Plezi - a real-time web application framework for Ruby
2
2
 
3
- [![Gem Version](https://badge.fury.io/rb/plezi.svg)](http://badge.fury.io/rb/plezi)
4
- [![Inline docs](http://inch-ci.org/github/boazsegev/plezi.svg?branch=master)](http://www.rubydoc.info/github/boazsegev/plezi/master)
3
+ [![Gem Version](https://badge.fury.io/rb/plezi.svg)](https://badge.fury.io/rb/plezi)
4
+ [![Inline docs](http://inch-ci.org/github/boazsegev/plezi.svg?branch=master)](http://www.rubydoc.info/github/boazsegev/plezi/master/frames)
5
5
  [![GitHub](https://img.shields.io/badge/GitHub-Open%20Source-blue.svg)](https://github.com/boazsegev/plezi)
6
6
 
7
- ## Special notice - pre version 13
7
+ Are microservices on your mind? Do you dream of a an SPA that's easy to scale? Did you wonder if you could write a whole Websockets, RESTful AJAX back-end with just a few lines of code (application logic not included)?
8
8
 
9
- One of the best compliments Plezi keeps receiving is the feedback about how easy it is to set up a websocket application using Plezi.
9
+ Welcome to your new home with [plezi.io](http://www.plezi.io), the Ruby real-time framework that assumes the application's logic is *not* part of the web service.
10
10
 
11
- On the other hand, Plezi's server, Iodine 0.1.x was written in Ruby and allowed a very fast and easy development...
11
+ **NOTICE**: Plezi 0.14.0 (this branch) is NOT an update, it's a total rewrite. Features were _removed_ as well as altered. For example, Plezi is now a Rack framework, with the limitations of CGI design and the advantages of using existing middleware. API changes abound.
12
12
 
13
- ...but with time and experience we all discovered the Ruby's `select` was limited to ~1024 connections, and that Plezi developers wanted more - more connections, more performance, more ease of deployment and more integration with Rails/Sinatra.
13
+ ## What does Plezi have to offer?
14
14
 
15
- I'm working very hard on rewriting the core server in C. Iodine 0.2.x is written in C and it supports only Unix based systems with `kqueue` or `epoll` support (BSD/Linux/MacOSX).
15
+ Plezi is a Rack based framework with support for native (server side implemented) Websockets.
16
16
 
17
- This means that if your machine has the memory and the resources (open file descriptor limits apply), Iodine could support tens of thousands of concurrent connections.
17
+ Plezi will provide the following features over plain Rack:
18
18
 
19
- Also, a lot of the API is changing for better integration with Rack based frameworks (Rails/Sinatra).
19
+ * Object Oriented (M)VC design, BYO (Bring Your Own) models.
20
20
 
21
- This also means that some sacrifices will be made. i.e. more Rack integration means that we loose HTTP streaming (Rack's specifications have their limits).
21
+ * A case sensitive RESTful router to map HTTP requests to your Controllers.
22
22
 
23
- This is where you come in. **Now** is the time to push those changes you wanted to integrate into Plezi. Send in your thoughts and feedback. You can open an issue or email me. Just write about how you use Plezi and what features you think are super important to keep and which once bother you.
23
+ Non-RESTful public Controller methods will be automatically published as valid HTTP routes, allowing the Controller to feel like an intuitive "virtual folder" with RESTful features.
24
24
 
25
- Even if I cannot answer everyone, it will all go into the next version's design and I'll do my best.
25
+ * Raw Websocket connections.
26
26
 
27
- ## Plezi
27
+ Non-RESTful public Controller methods will be automatically published as valid HTTP routes, allowing the Controller to feel like an intuitive "virtual folder" with RESTful features.
28
28
 
29
- Plezi is a Ruby framework for realtime web applications. It's name comes from the word "pleasure", since Plezi is a pleasure to work with.
29
+ * An (optional) Auto-Dispatch to map JSON websocket "events" to Controller functions (handlers).
30
30
 
31
- With Plezi, you can easily:
31
+ * Automatic (optional) scaling using Redis.
32
32
 
33
- 1. Create a Ruby web application, taking full advantage of RESTful routing, HTTP streaming and scalable Websocket features;
33
+ * An extensible template rendering abstraction engine, supports Slim, Markdown (using RedCarpet) and ERB out of the box.
34
34
 
35
- 2. Add Websocket services and RESTful HTTP Streaming to your existing Web-App, (Rails/Sinatra or any other Rack based Ruby app);
35
+ * Belated, extensible, asset baking (a fallback for when the application's assets weren't baked before deployment).
36
36
 
37
- 3. Create an easily scalable backend for your SPA.
37
+ It's possible to define an asset route (this isn't the default) to bake assets on the fly.
38
38
 
39
- ## Guides and documentation
39
+ In production mode, assets will be baked directly to the public folder supplied to Iodine (the web server) with a matching path. This allows the static file server to serve future requests.
40
40
 
41
- You can find [tutorials and guides at Plezi.io](http://www.plezi.io/docs).
41
+ However, during development, baking will save the files to the asset's folder, so that the Ruby layer will be the one serving the content and dynamic updates could be supported.
42
42
 
43
- Plezi leverages Ruby's mixins and meta-programming, so the YARD documentation is hard to navigate... I started writing guides for Plezi and would really appreciate any help you can offer.
43
+ Things Plezi **doesn't** do (anymore / ever):
44
44
 
45
- Please feel free to [contribute to Plezi's guides](https://github.com/boazsegev/plezi-website), or even just observe the [plezi.io website's code](https://github.com/boazsegev/plezi-website) (implemented using Plezi).
46
-
47
- ## Installation
48
-
49
- Add this line to your application's Gemfile:
50
-
51
- ```ruby
52
- gem 'plezi'
53
- ```
54
-
55
- Or install it yourself as:
56
-
57
- $ gem install plezi
58
-
59
- ## Quick start
60
-
61
- Plezi is super easy. Please read our [Getting Started guide](http://www.plezi.io/docs/basics) for more information.
62
-
63
- Here's a super quick intro:
64
-
65
- ### A running start
66
-
67
- Get a jump start by typing (in your terminal):
68
-
69
- $ plezi mini appname
70
- OR
71
- $ plezi new appname
72
-
73
- Next, simply double click the `appname` script file to start the server. Or, from the terminal:
74
-
75
- $ cd appname
76
- $ ruby ./appname
77
-
78
- See it work: [http://localhost:3000/](http://localhost:3000/)
79
-
80
- ### Hello world
81
-
82
- The Plezi framework was designed with intuitive ease of use in mind.
83
-
84
- Open the `irb` terminal and type:
85
-
86
- require 'plezi'
87
- route('*') { "Hello World!" }
88
- exit # <- this exits the terminal and starts the server
89
-
90
- A Hello World web application using three lines of code (one line is the actual code)... see it at [localhost:3000](http://localhost:3000/).
91
-
92
- ### Hello Object Oriented design
93
-
94
- Plezi really shines when we use Controller classes. Try this in your `irb` terminal:
95
-
96
- require 'plezi'
97
- class MyDemo
98
- # the index will answer '/'
99
- def index
100
- "Hello World!"
101
- end
102
- # a regular method will answer it's own name i.e. '/foo'
103
- def foo
104
- "Bar!"
105
- end
106
- # show is RESTful, it will answer any request looking like: '/(:id)'
107
- def show
108
- "Are you looking for: #{params[:id]}?"
109
- end
110
- end
111
-
112
- route '/', MyDemo
113
- exit
114
-
115
- Now visit [index](http://localhost:3000/) and [foo](http://localhost:3000/foo) or request an id, i.e. [http://localhost:3000/1](http://localhost:3000/1).
116
-
117
- ### Quick, websockets!
118
-
119
- Plezi was designed for websockets from the ground up. If your controller class defines an `on_message(data)` callback, plezi will automatically enable websocket connections for that route.
120
-
121
- Here's a Websocket echo server using Plezi:
122
-
123
- require 'plezi'
124
- class MyDemo
125
- def on_message data
126
- # sanitize the data and write it to the websocket.
127
- write ">> #{ERB::Util.html_escape data}"
128
- end
129
- end
130
-
131
- route '/', MyDemo
132
- exit
133
-
134
- Each controller is also a "channel" which can broadcast to everyone who's connected to it.
135
-
136
- Here's a websocket chat-room server using Plezi, comeplete with minor authentication (requires a chat handle):
137
-
138
- require 'plezi'
139
- class MyDemo
140
- def on_open
141
- close unless params[:id]
142
- end
143
- def on_message data
144
- # broadcast to everyone else (NOT ourselves):
145
- broadcast :chat_message, "#{params[:id]}: #{data}"
146
- # write to our own websocket:
147
- chat_message "Me: #{data}"
148
- end
149
- protected
150
- # implement the broadcast event
151
- def chat_message data
152
- write ERB::Util.html_escape(data)
153
- end
154
- end
155
-
156
- route '/', MyDemo
157
- # You can connect to this chatroom by going to ws://localhost:3000/any_nickname
158
- # but you need to write a websocket client too...
159
- # try two browsers with the client provided by http://www.websocket.org/echo.html
160
- exit
161
-
162
- Broadcasting isn't the only tool Plezi offers, we can also send a message to a specific connection using `unicast`, or send a message to everyone (no matter what controller is handling their connection) using `multicast`...
163
-
164
- ...It's even possible to register a unique identity, such as a specific user or even a `session.id`, so their messages are waiting for them even when they're off-line (you decide how long they wait)! We simply use `register_as @user.id` in our `on_open` callback, and than the user can get notifications sent by `notify user.id, :evet_method, *args`.
165
-
166
- ### Scaling? easy!
167
-
168
- Scale your Websocket application with one line of code:
169
-
170
- # REDIS_URL is where Heroku-Redis stores it's URL
171
- ENV['PL_REDIS_URL'] ||= ENV['REDIS_URL'] || "redis://:password@my.host:6389/0"
172
-
173
- Websocket messages (broadcasts, unicasts, etc') and even session data (Plezi keeps it away from the client) will now sync using Redis throughout all your server instances.
174
-
175
-
176
- ### Hosts, template rendering, assets...?
177
-
178
- Plezi allows us to use different host-names for different routes. i.e.:
179
-
180
- require 'plezi'
181
-
182
- host # this is the default host, it's always last to be checked.
183
- route('/') {"this is localhost"}
184
-
185
- host host: '127.0.0.1' # special host, for the IP name
186
- route('/') {"this is only for the IP!"}
187
- exit
188
-
189
- Each host has it's own settings for a public folder, asset rendering, templates etc'. For example:
190
-
191
- require 'plezi'
192
-
193
- class MyDemo
194
- def index
195
- # to make this work, create a template and set the correct template folder
196
- render :index
197
- end
198
- end
199
-
200
- host public: File.join('my', 'public', 'folder'),
201
- templates: File.join('my', 'templates', 'folder'),
202
- assets: File.join('my', 'assets', 'folder')
203
-
204
- route '/', MyDemo
205
- exit
206
-
207
- Plezi supports ERB (i.e. `template.html.erb`), Slim (i.e. `template.html.slim`), Haml (i.e. `template.html.haml`), CoffeeScript (i.e. `asset.js.coffee`) and Sass (i.e. `asset.css.scss`) right out of the box... and it's even extendible using the `Plezi::Renderer.register` and `Plezi::AssetManager.register`
45
+ * No DSL. Plezi won't clutter the global namespace.
208
46
 
47
+ * No application logic inside.
209
48
 
210
- ## Longer version - Plezi Controller classes
49
+ Conneting your application logic to Plezi is easy, however, application logic should really be *independent*, **reusable** and secure. There are plenty of gems that support independent application logic authoring.
211
50
 
212
- One of the best things about the Plezi is it's ability to take in any class as a controller class and route to the classes methods with special support for RESTful methods (`index`, `show`, `new`, `save`, `update`, `delete`, `before` and `after`) and for WebSockets (`pre_connect`, `on_open`, `on_message(data)`, `on_close`, `broadcast`, `unicast`, `multicast`, `on_broadcast(data)`, `register_as(identity)`, `notify`).
51
+ * No native session support. If you *must* have session support, Rack middleware gems provide a lot of options. Pick one... However...
213
52
 
214
- Here is a Hello World using a Controller class (run in `irb`):
53
+ Session have been proved over and over to be insecure and resource draining.
215
54
 
216
- require 'plezi'
55
+ Why use a session when you can save server resources and add security by using a persistent connection, i.e. a Websocket? If you really feel like storing unimportant stuff, why not use javascript's `local storage` on the *client's* machine? (if you need to save important stuff, you probably shouldn't be using sessions anyway).
217
56
 
218
- class Controller
219
- def index
220
- "Hello World!"
221
- end
222
- end
57
+ * No code refresh / development mode. If you want to restart the application automatically whenever you update the code, there are probably plenty of gems that will take care of that.
223
58
 
59
+ Do notice, Websockets require Iodine (the server), since (currently) it's the only Ruby server known to support native Websockets using a Websocket Callback Object.
224
60
 
225
- route '*' , Controller
226
-
227
- exit # Plezi will autostart once you exit irb.
228
-
229
- Except when using WebSockets, returning a String will automatically add the string to the response before sending the response - which makes for cleaner code. It's also possible to use the `response` object to set the response or stream HTTP (return true instead of a stream when you're done).
230
-
231
- It's also possible to define a number of controllers for a similar route. The controllers will answer in the order in which the routes are defined (this allows to group code by logic instead of url).
232
-
233
- \* please read the demo code for Plezi::StubRESTCtrl and Plezi::StubWSCtrl to learn more. Also, read more about the [Iodine's Websocket and HTTP server](https://github.com/boazsegev/iodine) at the core of Plezi to get more information about the amazing [Request](http://www.rubydoc.info/github/boazsegev/iodine/master/Iodine/Http/Request) and [Response](http://www.rubydoc.info/github/boazsegev/iodine/master/Iodine/Http/Response) objects.
234
-
235
- ## Native Websocket and Redis support
236
-
237
- Plezi Controllers have access to native websocket support through the `pre_connect`, `on_open`, `on_message(data)`, `on_close`, `multicast`, `broadcast`, `unicast` and the Identity API (`register_as` and `notify` methods).
238
-
239
- Here is some demo code for a simple Websocket broadcasting server, where messages sent to the server will be broadcasted back to all the **other** active connections (the connection sending the message will not recieve the broadcast).
240
-
241
- As a client side, we will use the WebSockets echo demo page - we will simply put in ws://localhost:3000/ as the server, instead of the default websocket server (ws://echo.websocket.org).
242
-
243
- Remember to connect to the service from at least two browser windows - to truly experience the `broadcast`ed websocket messages.
244
-
245
- ```ruby
246
- require 'plezi'
247
-
248
- # do you need automated redis support?
249
- # require 'redis'
250
- # ENV['PL_REDIS_URL'] = "redis://:password@localhost:6379/0"
251
-
252
- class BroadcastCtrl
253
- def index
254
- redirect_to 'http://www.websocket.org/echo.html'
255
- end
256
- def on_message data
257
- # try replacing the following two lines are with:
258
- # self.class.broadcast :_send_message, data
259
- broadcast :_send_message, data
260
- response << "sent."
261
- end
262
- def _send_message data
263
- response << data
264
- end
265
- def hello
266
- 'Hello!'
267
- end
268
- def_special_method "humans.txt" do
269
- 'I made this :)'
270
- end
271
- end
272
-
273
- route '/', BroadcastCtrl
274
- ```
275
-
276
- method names starting with an underscore ('_') are protected from the Http router, even when they are public.
277
-
278
- This is why even though both '/hello' and '/humans.txt' are public ( [try it](http://localhost:3000/humans.txt) ), '/_send_message' will return a 404 not found error ( [try it](http://localhost:3000/_send_message) ).
279
-
280
- ## Adding Websockets to your existing Rails/Sinatra/Rack application
281
-
282
- You already have an amazing WebApp, but now you want to add websocket broadcasting and unicasting support - Plezi makes connecting your existing WebApp with your Plezi Websocket backend as easy as it gets.
283
-
284
-
285
- There are two easy ways to add Plezi websockets to your existing WebApp, depending on your needs and preferences:
286
-
287
- 1. **The super easy way - a Hybrid app**:
288
-
289
- Plezi plays well with others, so you can add Plezi to your existing framework and let it catch any incoming websocket connections. Your application will still handle anything you didn't ask Plezi to handle (Plezi Websockets and routes will recieve priority, so your app can keep handling the 404 response).
290
-
291
-
292
- 2. **The Placebo API**:
293
-
294
- Plezi has a Placebo API, allowing you to add Plezi features without running a Plezi app.
295
-
296
- By adding the Plezi Placebo to your app, you can easily communicate between your existing app and a remote Plezi process/server. So, although websocket connections are made to a different server, your app can still send and recieve data through the websocket connection (using Redis).
297
-
298
- ### The super easy way - a Hybrid app
299
-
300
- The easiest way to add Plezi websockets to your existing application is to use [Iodine's](https://github.com/boazsegev/iodine) Rack adapter to run your Rack app, while Plezi will use Iodine's native features (such as Websockets and HTTP streaming).
301
-
302
- You can eaither use your existing Plezi application or create a new mini plezi application inside your existing app folder using:
303
-
304
- $ plezi mini appname
61
+ ## Installation
305
62
 
306
- Next, add the `plezi` gem to your `Gemfile` and add the following line somewhere in your apps code:
63
+ Add this line to your application's Gemfile:
307
64
 
308
65
  ```ruby
309
- require './appname/appname.rb'
66
+ gem 'plezi'
310
67
  ```
311
68
 
312
- That's it! Now you can use the Plezi API and your existing application's API at the same time and they are both running on the same server.
313
-
314
- Plezi's routes will be attempted first, so that your app can keep handling the 404 (not found) error page.
315
-
316
- \* just remember to remove any existing servers, such as `thin` of `puma` from your gemfile, otherwise they might take precedence over Plezi's choice of server (Iodine).
69
+ And then execute:
317
70
 
318
- ### The Plezi Placebo API - talking from afar
319
-
320
- To use Plezi and your App on different processes, without mixing them together, simply include the Plezi App in your existing app and call `Plezi.start_placebo` - now you can access all the websocket API that you want from your existing WebApp, but Plezi will not interfere with your WebApp in any way.
321
-
322
- For instance, add the following code to your environment setup on a Rails or Sinatra app:
323
-
324
- ```ruby
71
+ $ bundle
325
72
 
326
- require './my_plezi_app/environment.rb'
327
- require './my_plezi_app/routes.rb'
73
+ Or install it yourself as:
328
74
 
329
- # # Make sure the following is already in your 'my_plezi_app/environment.rb' file:
330
- # ENV['PL_REDIS_URL'] = "redis://:password@my.host:6379/0"
331
- # Plezi::Settings.redis_channel_name = 'unique_channel_name_for_app_b24270e2'
75
+ $ gem install plezi
332
76
 
333
- Plezi.start_placebo
334
- ```
77
+ ## Usage
335
78
 
336
- That's it!
79
+ A new application:
337
80
 
338
- Plezi will automatically set up the Redis connections and pub/sub to connect your existing WebApp with Plezi's Websocket backend - which you can safely scale over processes or machines.
81
+ $ plezi new app_name
339
82
 
340
- Now you can use Plezi from withing your existing App's code. For example, if your Plezi app has a controller named `ClientPleziCtrl`, you might use:
83
+ A simple hello world from `irb`:
341
84
 
342
85
  ```ruby
343
- # Demo a Rails Controller:
344
- class ClientsController < ApplicationController
345
- def update
346
- #... your original logic here
347
- @client = Client.find(params[:id])
348
-
349
- # now unicast data to your client on the websocket
350
- # (assume his websocket uuid was saved in @client.ws_uuid)
351
-
352
- ClientPleziCtrl.unicast @client.ws_uuid, :method_name, @client.attributes
353
-
354
- # or broadcast data to your all your the clients currently connected
355
-
356
- ClientPleziCtrl.broadcast :method_name, @client.attributes
86
+ require 'plezi'
357
87
 
88
+ class HelloWorld
89
+ def index
90
+ "Hello World!"
358
91
  end
359
92
  end
360
- ```
361
-
362
- Easy.
363
-
364
- \- "But wait...", you might say to me, "How do we get information back FROM the back end?"
365
-
366
- Oh, that's easy too.
367
-
368
- With a few more lines of code, we can have the websocket connections _broadcast_ back to us using the `Plezi::Placebo` API.
369
-
370
- In your Rails app, add the logic:
371
-
372
- ```ruby
373
- class MyReciever
374
- def my_reciever_method arg1, arg2, arg3, arg4...
375
- # your app's logic
376
- end
377
- end
378
- Plezi.start_placebo MyReciever
379
- ```
380
-
381
- Plezi will now take your class and add mimick an IO connection (the Placebo connection) on it's Iodine serever. This Placebo connection will answer the Redis broadcasts just as if your class was a websocket controller...
382
-
383
- On the Plezi side, use multicasting or unicasting (but not broadcasting), from ANY controller:
384
-
385
- ```ruby
386
-
387
- class ClientPleziCtrl
388
- def on_message data
389
- # app logic here
390
- multicast :my_reciever_method, arg1, arg2, arg3, arg4...
391
- end
392
- end
393
- ```
394
-
395
- That's it! Now you have your listening object... but be aware - to safely scale up this communication you might consider using unicasting instead of broadcasting.
396
93
 
397
- We recommend saving the uuid of the Rails process to a Redis key and picking it up from there.
398
-
399
- On your Rails app, add:
400
-
401
- ```ruby
402
- #...
403
- class MyReciever
404
- def my_reciever_method arg1, arg2, arg3, arg4...
405
- # ...
406
- end
407
- end
408
-
409
- pl = Plezi.start_placebo MyReciever
410
-
411
- Plezi.redis_connection.set 'MainUUIDs', pl.uuid
412
-
413
- ```
414
- In your Plezi app, use unicasting when possible:
415
-
416
- ```ruby
417
- class ClientPleziCtrl
418
- def on_message data
419
- # app logic here
420
- main_uuid = Plezi.redis_connection.get 'MainUUIDs'
421
- unicast main_uuid, :my_reciever_method, arg1, arg2, arg3, arg4... if main_uuid
422
- end
423
- end
94
+ Plezi.route '*', HelloWorld
424
95
 
96
+ exit # <= if running from terminal, this will start the server
425
97
  ```
426
98
 
427
- ## Native HTTP streaming with Asynchronous events
428
-
429
- Plezi comes with native HTTP streaming support (Http will use chuncked encoding unless experimental Http/2 is in use), alowing you to use Plezi Events and Timers to send an Asynchronous response.
430
-
431
- Let's make the classic 'Hello World' use HTTP Streaming:
432
-
433
- ```ruby
434
- require 'plezi'
435
-
436
- class Controller
437
- def index
438
- response.stream_async do
439
- sleep 0.5
440
- response << "Hello ";
441
- response.stream_async{ sleep 0.5; response << "World" }
442
- end
443
- true
444
- end
445
- end
446
-
447
- route '*' , Controller
448
- ```
449
-
450
- Notice you can nest calls to the `response.stream_async` method, allowing you to breakdown big blocking tasks into smaller chunks. `response.stream_async` will return immediately, scheduling the task for background processing.
451
-
452
- You can also handle other tasks asynchronously using the [Iodine's API](http://www.rubydoc.info/gems/iodine).
453
-
454
- More on asynchronous events and timers later.
455
-
456
- ## Plezi Routes
457
-
458
- Plezi supports magic routes, in similar formats found in other systems, such as: `route "/:required/(:optional_with_format){[\\d]*}/(:optional)", Plezi::StubRESTCtrl`.
459
-
460
- Plezi assummes all simple routes to be RESTful routes with the parameter `:id` ( `"/user" == "/user/(:id)"` ).
461
-
462
- require 'plezi'
463
-
464
- # this route demos a route for listing/showing posts,
465
- # with or without revision numbers or page-control....
466
- # notice the single quotes (otherwise the '\' would need to be escaped).
467
- route '/post/(:id)/(:revision){[\d]+\.[\d]+}/(:page_number)', Plezi::StubRESTCtrl
468
-
469
- now visit:
470
-
471
- * [http://localhost:3000/post/12/1.3/1](http://localhost:3000/post/12/1.3/1)
472
- * [http://localhost:3000/post/12/1](http://localhost:3000/post/12/1)
473
-
474
- **[please see the `route` documentation for more information on routes](./docs/routes.md)**.
475
-
476
- ## Plezi Virtual Hosts
477
-
478
- Plezi can be used to create virtual hosts for the same service, allowing you to handle different domains and subdomains with one app:
479
-
480
- require 'plezi'
481
-
482
- # define a named host.
483
- host 'localhost', alias: 'localhost2', public: File.join('my', 'public', 'folder')
484
-
485
- shared_route '/shared' do |req, res|
486
- res << "shared by all existing hosts.... but the default host doesn't exist yet, so we're only on localhost and localhost2."
487
- end
488
-
489
- # define a default (catch-all) host.
490
- host
491
-
492
- shared_route '/humans.txt' do |req, res|
493
- res << "we are people - we're in every existing hosts."
494
- end
495
-
496
-
497
- route('*') do |req, res|
498
- res << "this is a 'catch-all' host. you got here by putting in the IP adderess."
499
- end
500
-
501
- # get's the existing named host
502
- host 'localhost'
503
-
504
- route('*') do |req, res|
505
- res << "this is localhost or localhost 2"
506
- end
507
-
508
- Now visit:
509
-
510
- * [http://127.0.0.1:3000/]( http://127.0.0.1:3000/ )
511
- * [http://localhost:3000/]( http://localhost:3000/ )
512
- * [http://127.0.0.1:3000/shared]( http://127.0.0.1:3000/shared ) - won't show, becuse this host was created AFTER the route was declered.
513
- * [http://localhost:3000/shared]( http://localhost:3000/shared )
514
- * [http://127.0.0.1:3000/humans.txt]( http://127.0.0.1:3000/humans.txt )
515
- * [http://localhost:3000/humans.txt]( http://localhost:3000/humans.txt )
516
- * notice: `localhost2` will only work if it was defined in your OS's `hosts` file.
517
-
518
- ## Plezi Logging
519
-
520
- The Plezi module (also `PL`) delegates to the Iodine methods, helping with logging as well as the support you already noticed for dynamic routes, dynamic services and more.
521
-
522
- Logging:
523
-
524
- require 'plezi'
525
-
526
- # simple logging of strings
527
- PL.info 'log info'
528
- Iodine.info 'This is the same, but more direct.'
529
- PL.warn 'log warning'
530
- PL.error 'log error'
531
- PL.fatal "log a fatal error (shuoldn't be needed)."
532
- PL.log_raw "Write raw strings to the logger."
533
-
534
- # the logger accepts exceptions as well.
535
- begin
536
- raise "hell"
537
- rescue Exception => e
538
- PL.error e
539
- end
540
-
541
- Please notice it is faster to use the Iodine's API directly when using API that is delegated to Iodine.
542
-
543
- ## Plezi Events and Timers
544
-
545
- The Plezi module (also `PL`) also delegates to the [Iodine's API](http://www.rubydoc.info/gems/greactor/iodine) to help with asynchronous tasking, callbacks, timers and customized shutdown cleanup.
546
-
547
- Asynchronous callbacks (works only while services are active and running):
548
-
549
- require 'plezi'
550
-
551
- def my_shutdown_proc time_start
552
- puts "Services were running for #{Time.now - time_start} seconds."
553
- end
554
-
555
- # shutdown callbacks
556
- Iodine.on_shutdown(Kernel, :my_shutdown_proc, Time.now) { puts "this will run after shutdown." }
557
- Iodine.on_shutdown() { puts "this will run too." }
558
-
559
- # a timer
560
- Iodine.run_after(2) {puts "this will wait 2 seconds to run... too late. for this example"}
99
+ ## Development
561
100
 
562
- Iodine.run {puts "notice that the background tasks will only start once the Plezi's engine is running."}
563
- Iodine.run {puts "exit Plezi to observe the shutdown callbacks."}
101
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
564
102
 
565
- ## Re-write Routes
566
-
567
- Plezi supports special routes used to re-write the request and extract parameters for all future routes.
568
-
569
- This allows you to create path prefixes which will be removed once their information is extracted.
570
-
571
- This is great for setting global information such as internationalization (I18n) locales.
572
-
573
- By using a route with the a 'false' controller, the parameters extracted are automatically retained.
574
-
575
- *(Older versions of Plezi allowed this behavior for all routes, but it was deprecated starting version 0.7.4).
576
-
577
- require 'plezi'
578
-
579
- class Controller
580
- def index
581
- return "Bonjour le monde!" if params[:locale] == 'fr'
582
- "Hello World!\n #{params}"
583
- end
584
- def show
585
- return "Vous êtes à la recherche d' : #{params[:id]}" if params[:locale] == 'fr'
586
- "You're looking for: #{params[:id]}"
587
- end
588
- def debug
589
- # binding.pry
590
- # do you use pry for debuging?
591
- # no? oh well, let's ignore this.
592
- false
593
- end
594
- def delete
595
- return "Mon Dieu! Mon français est mauvais!" if params[:locale] == 'fr'
596
- "did you try #{request.base_url + request.original_path}?_method=delete or does your server support a native DELETE method?"
597
- end
598
- end
599
-
600
- # this is our re-write route.
601
- # it will extract the locale and re-write the request.
602
- route '/:locale{fr|en}/*', false
603
-
604
- # this route takes a regular expression that is a simple math calculation
605
- # (calculator)
606
- #
607
- # it is an example for a Proc controller, which can replace the Class controller.
608
- route /^\/[\d\+\-\*\/\(\)\.]+$/ do |request, response|
609
- message = (request.params[:locale] == 'fr') ? "La solution est" : "My Answer is"
610
- response << "#{message}: #{eval( request.path[1..-1] )}"
611
- end
612
-
613
- route "/users" , Controller
614
-
615
- route "/" , Controller
616
-
617
- try:
618
-
619
- * [http://localhost:3000/](http://localhost:3000/)
620
- * [http://localhost:3000/fr](http://localhost:3000/fr)
621
- * [http://localhost:3000/users/hello](http://localhost:3000/users/hello)
622
- * [http://localhost:3000/users/(5+5*20-15)/9.0](http://localhost:3000/users/(5+5*20-15)/9.0) - should return a 404 not found message.
623
- * [http://localhost:3000/(5+5*20-15)/9.0](http://localhost:3000/(5+5*20-15)/9)
624
- * [http://localhost:3000/fr/(5+5*20-15)/9.0](http://localhost:3000/fr/(5+5*20-15)/9)
625
- * [http://localhost:3000/users/hello?_method=delete](http://localhost:3000/users/hello?_method=delete)
626
-
627
- As you can see in the example above, Plezi supports Proc routes as well as Class controller routes.
628
-
629
- Please notice that there are some differences between the two. Proc routes less friedly, but plenty powerful and are great for custom 404 error handling.
630
-
631
- ## OAuth2 and other Helpers
632
-
633
- Plezi has a few helpers that help with common tasks.
634
-
635
- For instance, Plezi has a built in controller that allows you to add social authentication using Google, Facebook
636
- and and other OAuth2 authentication service. For example:
637
-
638
- require 'plezi'
639
-
640
- class Controller
641
- def index
642
- flash[:login] ? "You are logged in as #{flash[:login]}" : "You aren't logged in. Please visit one of the following:\n\n* #{request.base_url}#{Plezi::OAuth2Ctrl.url_for :google}\n\n* #{request.base_url}#{Plezi::OAuth2Ctrl.url_for :facebook}"
643
- end
644
- end
645
-
646
- # set up the common social authentication variables for automatic Plezi::OAuth2Ctrl service recognition.
647
- ENV["FB_APP_ID"] ||= "facebook_app_id / facebook_client_id"
648
- ENV["FB_APP_SECRET"] ||= "facebook_app_secret / facebook_client_secret"
649
- ENV['GOOGLE_APP_ID'] = "google_app_id / google_client_id"
650
- ENV['GOOGLE_APP_SECRET'] = "google_app_secret / google_client_secret"
651
-
652
- require 'plezi/oauth'
653
-
654
- # manually setup any OAuth2 service (we'll re-setup facebook as an example):
655
- Plezi::OAuth2Ctrl.register_service(:facebook, app_id: ENV['FB_APP_ID'],
656
- app_secret: ENV['FB_APP_SECRET'],
657
- auth_url: "https://www.facebook.com/dialog/oauth",
658
- token_url: "https://graph.facebook.com/v2.3/oauth/access_token",
659
- profile_url: "https://graph.facebook.com/v2.3/me",
660
- scope: "public_profile,email") if ENV['FB_APP_ID'] && ENV['FB_APP_SECRET']
661
-
662
-
663
- create_auth_shared_route do |service_name, token, remote_user_id, remote_user_email, remote_response|
664
- # we will create a temporary cookie storing a login message. replace this code with your app's logic
665
- flash[:login] = "#{remote_response['name']} (#{remote_user_email}) from #{service_name}"
666
- end
667
-
668
- route "/" , Controller
669
-
670
- exit
671
-
672
- Plezi has a some more goodies under the hood.
673
-
674
- Whether such goodies are part of the Plezi-App Template (such as rake tasks for ActiveRecord without Rails) or part of the Plezi Framework core (such as descried in the Plezi::ControllerMagic documentation: #flash, #url_for, #render, #send_data, etc'), these goodies are fun to work with and make completion of common tasks a breeze.
675
-
676
-
677
- ## Plezi Settings
678
-
679
- Plezi leverages [Iodine's server](https://github.com/boazsegev/iodine) new architecture. Iodine is a pure Ruby HTTP and Websocket Server built using [Iodine's](https://github.com/boazsegev/iodine) core library - a multi-threaded pure ruby alternative to EventMachine with process forking support (enjoy forking, if your code is scaling ready).
680
-
681
- Plezi and Iodine are meant to be very effective, allowing for much flexability where needed.
682
-
683
- Settings for the Iodine's core allow you to change different things, such as the level of concurrency you want (`Iodine.threads = ` or `Iodine.processes = `), logging destination (such as logging to a file) and more.
684
-
685
- Settings for Iodine's Http and Websockets server, allow you to change upload limits (which can be super important for security) using `Iodine::Http.max_http_buffer =`, limit websocket message sizes using `Iodine::Http::Websockets.message_size_limit =`, change the Websocket's auto-ping interval using `Iodine::Http::Websockets.default_timeout =` or `Plezi::Settings.ws_message_size_limit` and more... Poke around ;-)
686
-
687
- Plezi and Iodine are written for Ruby versions 2.1.0 or greater (or API compatible variants). Version 2.2.3 is the currently recommended version.
688
-
689
- ## Who's afraid of multi-threading?
690
-
691
- Let's start with the obvious, **if** your code is short and efficient (no blocking tasks), it is best to run Plezi (Iodine) in a single threaded mode - you get better performance AND safer code, **as long as there are no blocking tasks**:
692
-
693
- Plezi.threads = 1
694
-
695
- But... most applications will naturally have blocking tasks, such as database queries etc'. This is why...:
696
-
697
- Plezi builds on Iodine's concept of "connection locking", meaning that your controllers shouldn't be acessed by more than one thread at the same time.
698
-
699
- This allows you to run Plezi as a multi-threaded (and even multi-process) application as long as your controllers don't change or set any global data... Readeing global data after it was set during initialization is totally fine, just not changing or setting it...
700
-
701
- But wait, global data is super important, right?
702
-
703
- Well, sometimes it is. And although it's a better practice to avoide storing any global data in global variables (databases are usually thread safe storage places), sometimes storing stuff in the global space is exactly what we need.
704
-
705
- The solution is simple - if you can't use persistent databases with thread-safe libraries (i.e. Sequel / ActiveRecord / Redis, etc'), use Plezi's global cache storage (see Plezi::Cache).
706
-
707
- Plezi's global cache storage is a local memory based storage protected by a mutex whenever reading or writing from the cache.
708
-
709
- So... these are protected:
710
-
711
- # set data
712
- Plezi.cache_data :my_global_variable, 32
713
- # get data
714
- Plezi.get_cached :my_global_variable # => 32
715
-
716
- However, although Ruby seems innocent, it's super powerful when it comes to using pointers and references behind the scenes. This could allow you to change a protected object in an unprotected way... consider this:
717
-
718
- a = []
719
- b = a
720
- b << '1'
721
- # we changed `a` without noticing
722
- a # => [1]
723
-
724
- For this reason, it's important that Strings, Arrays and Hashes will be protected if they are to be manipulated in any way.
725
-
726
- The following is safe:
727
-
728
- # set data
729
- Plezi.cache_data :global_hash, Hash.new
730
- # manipulate data
731
- Plezi.get_cached :global_hash do |global_hash|
732
- global_hash[:change] = "safe"
733
- end
734
-
735
- However, the following is unsafe:
736
-
737
- # set data
738
- Plezi.cache_data :global_hash, Hash.new
739
- # manipulate data
740
- global_hash = Plezi.get_cached :global_hash do |global_hash|
741
- global_hash[:change] = "NOT safe"
742
-
743
-
744
- \* be aware, if using Plezi in as a multi-process application, that each process has it's own cache and that processes can't share the cache. The different threads in each of the processes will be able to access their process's cache, but each process runs in a different memory space, so they can't share.
103
+ To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
745
104
 
746
105
  ## Contributing
747
106
 
748
- Feel free to fork or contribute. right now I am one person, but together we can make something exciting that will help us enjoy Ruby in this brave new world and (hopefully) set an example that will induce progress in the popular mainstream frameworks such as Rails and Sinatra.
107
+ Bug reports and pull requests are welcome on GitHub at https://github.com/boazsegev/plezi.
108
+
749
109
 
750
- 1. Fork it ( https://github.com/boazsegev/plezi/fork )
751
- 2. Create your feature branch (`git checkout -b my-new-feature`)
752
- 3. Commit your changes (`git commit -am 'Add some feature'`)
753
- 4. Push to the branch (`git push origin my-new-feature`)
754
- 5. Create a new Pull Request
110
+ ## License
755
111
 
756
- # [<img src='https://raw.githubusercontent.com/boazsegev/plezi/master/logo/sign.png' alt='Plezi' style='width:20em;' />](http://www.plezi.io)
112
+ The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).