plezi 0.10.17 → 0.11.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.yardopts +1 -0
- data/CHANGELOG.md +16 -0
- data/README.md +9 -3
- data/bin/plezi +1 -1
- data/docs/async_helpers.md +195 -0
- data/docs/http_helpers.md +9 -0
- data/docs/logging.md +14 -0
- data/docs/routes.md +37 -0
- data/docs/websockets.md +9 -0
- data/lib/plezi/builders/app_builder.rb +23 -23
- data/lib/plezi/common/api.rb +3 -3
- data/lib/plezi/common/dsl.rb +0 -1
- data/lib/plezi/common/settings.rb +1 -1
- data/lib/plezi/handlers/controller_magic.rb +19 -9
- data/lib/plezi/handlers/http_router.rb +9 -10
- data/lib/plezi/handlers/placebo.rb +2 -3
- data/lib/plezi/handlers/route.rb +4 -4
- data/lib/plezi/handlers/session.rb +21 -12
- data/lib/plezi/helpers/http_sender.rb +1 -1
- data/lib/plezi/helpers/magic_helpers.rb +66 -4
- data/lib/plezi/oauth/auth_controller.rb +3 -3
- data/lib/plezi/version.rb +1 -1
- data/plezi.gemspec +1 -1
- data/resources/config.ru +11 -7
- data/resources/mini_app.rb +9 -10
- data/resources/mini_welcome_page.html +3 -3
- data/test/plezi_tests.rb +7 -3
- data/websocket chatroom.md +26 -27
- metadata +11 -5
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 7faa8f51c3d95810d3e7a315b970d55276d5f3dc
|
4
|
+
data.tar.gz: ac93d924d781fe4dd9c9e388a8405a971b76f623
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 6e8ec22249a50788ace60a4f12af6a589264cd3d91d6e2c2873002915be5c3a0a14ece6301f39306b293de65129f620f64c04c7b38984ac0234187237cef4d06
|
7
|
+
data.tar.gz: 8c23d6ac45147933bb294e97b266ee49b10481ce1747d1649b7866e8ff6e1f087201083607b53557a85b6d60632c0b5d9154e3a2b7028bbc7ae86d3a145c0730
|
data/.yardopts
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
yardoc lib/**/*.rb ext/**/*.c - docs/*.md
|
data/CHANGELOG.md
CHANGED
@@ -2,6 +2,22 @@
|
|
2
2
|
|
3
3
|
***
|
4
4
|
|
5
|
+
Change log v.0.11.0
|
6
|
+
|
7
|
+
**Update**: Requires GRHttp server and GReactor version 0.1.0 or above, adjusted to the updated API.
|
8
|
+
|
9
|
+
**Update**: Better pinging and timout support courtesy of the updated GRHttp server.
|
10
|
+
|
11
|
+
**Update**: The default number of threads is now 30. It seems that once we move beyond 1 thread (which is also supported), the added threads are adding more security against blocking code without effecting performance as much. It is expected that advanced users will consider moving away from multi-threading to muli-processing while avoiding blocking code. All these options are supported by Plezi, GRHttp and GReactor.
|
12
|
+
|
13
|
+
**Fix**: Fixed an issue where requests for folders within the assets folder (folder indexing) would fail with an internal error message (error 500) instead of a not found message (error 404).
|
14
|
+
|
15
|
+
**Fix**: fixed an issue that caused the static file service to fail when using the preferred `:public` vs. the older `:root` option used to set the public folder's path.
|
16
|
+
|
17
|
+
**Minor**: minor adjustments and improvements, such as: auto-setting the `content-type` header when using `render`.
|
18
|
+
|
19
|
+
***
|
20
|
+
|
5
21
|
Change log v.0.10.17
|
6
22
|
|
7
23
|
**Update**: Requires a newer version of the GRHttp server and GReactor, building on it's WSClient and HTTP decoding improvements.
|
data/README.md
CHANGED
@@ -1,6 +1,4 @@
|
|
1
1
|
# [Plezi](https://github.com/boazsegev/plezi), The Ruby framework for realtime web-apps
|
2
|
-
[![Gem Version](https://badge.fury.io/rb/plezi.svg)](http://badge.fury.io/rb/plezi)
|
3
|
-
[![Inline docs](http://inch-ci.org/github/boazsegev/plezi.svg?branch=master)](http://www.rubydoc.info/github/boazsegev/plezi/master)
|
4
2
|
|
5
3
|
Plezi is an easy to use Ruby Websocket Framework, with full RESTful routing support and HTTP streaming support. It's name comes from the word "fun", or "pleasure", since Plezi is a pleasure to work with.
|
6
4
|
|
@@ -14,6 +12,12 @@ With Plezi, you can easily:
|
|
14
12
|
|
15
13
|
Plezi leverages [GRHttp server's](https://github.com/boazsegev/GRHttp) new architecture. GRHttp is a pure Ruby HTTP and Websocket Generic Server built using [GReactor](https://github.com/boazsegev/GReactor) - a multi-threaded pure ruby alternative to EventMachine with basic process forking support (enjoy forking, if your code is scaling ready).
|
16
14
|
|
15
|
+
### Plezi version data
|
16
|
+
[![Gem Version](https://badge.fury.io/rb/plezi.svg)](http://badge.fury.io/rb/plezi)
|
17
|
+
[![Inline docs](http://inch-ci.org/github/boazsegev/plezi.svg?branch=master)](http://www.rubydoc.info/github/boazsegev/plezi/master)
|
18
|
+
|
19
|
+
The `master` branch always refers to the latest edge version, which might also be a broken version. Please refer to the relevent version by using the version's `tag` in the branch selector.
|
20
|
+
|
17
21
|
## Installation
|
18
22
|
|
19
23
|
Add this line to your application's Gemfile:
|
@@ -115,7 +119,9 @@ Remember to connect to the service from at least two browser windows - to truly
|
|
115
119
|
route '/', BroadcastCtrl
|
116
120
|
```
|
117
121
|
|
118
|
-
method names starting with an underscore ('_') will NOT be made public by the router
|
122
|
+
method names starting with an underscore ('_') will NOT be made public by the router.
|
123
|
+
|
124
|
+
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) ).
|
119
125
|
|
120
126
|
## Adding Websockets to your existing Rails/Sinatra/Rack application
|
121
127
|
|
data/bin/plezi
CHANGED
@@ -65,7 +65,7 @@ if ARGV[0] == 'new' || ARGV[0] == 'n' || ARGV[0] == "force" || ARGV[0] == 'mini'
|
|
65
65
|
|
66
66
|
# building
|
67
67
|
template = Plezi::AppBuilder.new
|
68
|
-
(ARGV[0] == 'mini' || ARGV[0] == 'm' ) ? template.build_mini : template.build
|
68
|
+
(ARGV[0] == 'mini' || ARGV[0] == 'm' ) ? template.build_mini(ARGV[1]) : template.build(ARGV[1])
|
69
69
|
elsif ARGV[0] == 'server' || ARGV[0] == 'start' || ARGV[0] == 's'
|
70
70
|
ARGV.shift
|
71
71
|
load File.expand_path(Dir["."][0], (File.expand_path(Dir["."][0]).split(/[\\\/]/).last) ) rescue load( File.expand_path(Dir["."][0], (File.expand_path(Dir["."][0]).split(/[\\\/]/).last ) ) )
|
@@ -0,0 +1,195 @@
|
|
1
|
+
# Plezi's Asynchronous Engine
|
2
|
+
|
3
|
+
(todo: write documentation)
|
4
|
+
|
5
|
+
Inside Plezi's core code is a pure Ruby IO reactor called [GReactor](https://github.com/boazsegev/GReactor) (Generic Reactor), a very powerful Asynchronous Workflow Engine that allows us to enjoy both Multi-Threading and Multi-Processing.
|
6
|
+
|
7
|
+
Although multi-threading is highly regarded, it should be pointed out that using the GReactor with just one thread is both faster and more efficient. But, since some tasks that take more time (blocking tasks) can't be broken down into smaller tasks, using a number of threads (and/or processes) is a better practice.
|
8
|
+
|
9
|
+
You can read more about the [GReactor](https://github.com/boazsegev/GReactor) and it's amazing features in it's [documentation](http://www.rubydoc.info/github/boazsegev/GReactor/master).
|
10
|
+
|
11
|
+
Here we will discuss the methods used for asynchronous processing of different tasks that allow us to break big heavy tasks into smaller bits, allowing our application to 'flow' and stay responsive even while under heavy loads.
|
12
|
+
|
13
|
+
## Asynchronous HTTP responses
|
14
|
+
|
15
|
+
Inside Plezi's core code is a pure Ruby HTTP and Websocket Server (and client) called [GRHttp](https://github.com/boazsegev/GRHttp) (Generic HTTP), which allows for native HTTP streaming.
|
16
|
+
|
17
|
+
Asynchronous HTTP method calls can be nested, but shouldn't be called on after the other.
|
18
|
+
|
19
|
+
i.e.:
|
20
|
+
|
21
|
+
```ruby
|
22
|
+
# right
|
23
|
+
response.stream_async { response.stream_async {'do after'}; 'do first' }
|
24
|
+
# wrong
|
25
|
+
response.stream_async { "who's first?" }
|
26
|
+
response.stream_async { "I don't know..." }
|
27
|
+
```
|
28
|
+
|
29
|
+
Since streaming is done asynchronously, and since Plezi is multi-threaded by default (this can be changed to single threaded, but is less recomended unless you know your code doesn't block - see `Plezi::Settings.max_threads = number`), Asynchronous HTTP method nesting makes sure that the code doesn't conflict and that race conditions don't occure within the same HTTP response.
|
30
|
+
|
31
|
+
|
32
|
+
#### GRHttp's `response.stream_async &block`
|
33
|
+
|
34
|
+
GRHttp's response object, which is accessed by the controller using the `response` method (or the `@response` object), allows easy access to HTTP streaming.
|
35
|
+
|
36
|
+
For example (run this in the terminal using `irb`):
|
37
|
+
|
38
|
+
```ruby
|
39
|
+
require `plezi`
|
40
|
+
|
41
|
+
class MyController
|
42
|
+
def index
|
43
|
+
response.stream_async do
|
44
|
+
response << "This will stream.\n"
|
45
|
+
response.stream_async do
|
46
|
+
response << "Streaming can be nested."
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
listen
|
53
|
+
route '/', MyController
|
54
|
+
|
55
|
+
exit
|
56
|
+
```
|
57
|
+
|
58
|
+
As noted above, `response.stream_async` calls should always be nested and never called in 'parallel'.
|
59
|
+
|
60
|
+
Calling `response.stream_async`
|
61
|
+
|
62
|
+
#### GRHttp's `response.stream_array enum, &block`
|
63
|
+
|
64
|
+
To make nesting easier, GRHttp's response object provides the `response.stream_array enum, &block` method.
|
65
|
+
|
66
|
+
Here's our modified example:
|
67
|
+
|
68
|
+
```ruby
|
69
|
+
require `plezi`
|
70
|
+
|
71
|
+
class MyController
|
72
|
+
def index
|
73
|
+
data = ["This will stream.\n", "Streaming can be nested."]
|
74
|
+
response.stream_array(data) {|s| response << s}
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
listen
|
79
|
+
route '/', MyController
|
80
|
+
|
81
|
+
exit
|
82
|
+
```
|
83
|
+
|
84
|
+
You can also add data to the array while 'looping', which allows you to use the array as a 'flag' for looped streaming. The following is a very limited example, which could be used for "lazy loading" data from a database, in order to save on system resources or send large table data using JSON "packets".
|
85
|
+
|
86
|
+
```ruby
|
87
|
+
require `plezi`
|
88
|
+
|
89
|
+
class MyController
|
90
|
+
def index
|
91
|
+
data = ["This will stream.\n", "Streaming can be nested."]
|
92
|
+
flag = [true]
|
93
|
+
response.stream_array(flag) do
|
94
|
+
response << data.shift
|
95
|
+
flag << true unless data.empty?
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
listen
|
101
|
+
route '/', MyController
|
102
|
+
|
103
|
+
exit
|
104
|
+
```
|
105
|
+
|
106
|
+
## Asynchronous code execution
|
107
|
+
|
108
|
+
[GReactor](https://github.com/boazsegev/GReactor) (Generic Reactor), a very powerful Asynchronous Workflow Engine which offers a very intuitve and easy to use API both for queuing code snippets (blocks / methods) and for schedualing non-persistent timed events (future timed events are discarded during shutdown and need to be re-initiated).
|
109
|
+
|
110
|
+
### The Asynchronous "Queue"
|
111
|
+
|
112
|
+
`GReactor` (in short: `GR`) offers a number of methods that allow us to easily queue code execution.
|
113
|
+
|
114
|
+
|
115
|
+
#### `GR.run_async(arg1, arg2, arg3...) {block}`
|
116
|
+
|
117
|
+
`GR.run_async` takes arguments to be passed to a block of code prior to execution. This allows us to seperate the `Proc` object creation fron the data handling and possibly (but not always) optimize our code.
|
118
|
+
|
119
|
+
For example:
|
120
|
+
|
121
|
+
```ruby
|
122
|
+
require `plezi`
|
123
|
+
|
124
|
+
class MyController
|
125
|
+
def index
|
126
|
+
GR.run_async(Time.now) {|t| puts "Someone poked me at: #{t}"} # maybe send an email?
|
127
|
+
"Hello World"
|
128
|
+
end
|
129
|
+
end
|
130
|
+
|
131
|
+
listen
|
132
|
+
route '/', MyController
|
133
|
+
|
134
|
+
exit
|
135
|
+
```
|
136
|
+
|
137
|
+
#### `GR.callback(object, method, arg1, arg2...) {|returned_value| callback block}`
|
138
|
+
|
139
|
+
Another common method imployed is the `GR.callback`, which allows us to layer asynchronous code:
|
140
|
+
|
141
|
+
```ruby
|
142
|
+
require `plezi`
|
143
|
+
|
144
|
+
class MyController
|
145
|
+
def index
|
146
|
+
GR.callback(self, :print_poke, Time.now) { puts "Printed poke."}
|
147
|
+
"Hello World"
|
148
|
+
end
|
149
|
+
protected
|
150
|
+
def print_poke time
|
151
|
+
puts "Someone poked me at: #{time}"
|
152
|
+
end
|
153
|
+
end
|
154
|
+
|
155
|
+
listen
|
156
|
+
route '/', MyController
|
157
|
+
|
158
|
+
exit
|
159
|
+
```
|
160
|
+
|
161
|
+
#### `GR.queue(proc, arguments_array)`
|
162
|
+
|
163
|
+
GReactor's asynchronous engine is, in it's core, based off this simple method which accepts two elements:
|
164
|
+
|
165
|
+
1. An object that answers to `call`
|
166
|
+
2. An array of arguments to be passed to that object (or `nil`).
|
167
|
+
|
168
|
+
This method allows us to easily reuse Proc objects without constantly creating new Proc objects and releasing them from memory.
|
169
|
+
|
170
|
+
For example:
|
171
|
+
|
172
|
+
```ruby
|
173
|
+
require `plezi`
|
174
|
+
|
175
|
+
class MyController
|
176
|
+
|
177
|
+
POKE_TASK = -> {|time| puts "Someone poked me at: #{time}"}
|
178
|
+
|
179
|
+
def index
|
180
|
+
GR.queue POKE_TASK, [Time.now]
|
181
|
+
"Hello World"
|
182
|
+
end
|
183
|
+
end
|
184
|
+
|
185
|
+
listen
|
186
|
+
route '/', MyController
|
187
|
+
|
188
|
+
exit
|
189
|
+
```
|
190
|
+
|
191
|
+
|
192
|
+
### Timed events
|
193
|
+
|
194
|
+
## The Graceful Shutdown
|
195
|
+
|
@@ -0,0 +1,9 @@
|
|
1
|
+
# HTTP Helpers inherited from GRHttp
|
2
|
+
|
3
|
+
Inside Plezi's core code is a pure Ruby HTTP and Websocket Server (and client) called [GRHttp](https://github.com/boazsegev/GRHttp) (Generic HTTP), a very powerful server that [isn't limited by Rack's API](https://github.com/boazsegev/GRHttp/blob/master/HTTP.md).
|
4
|
+
|
5
|
+
|
6
|
+
(todo: write documentation)
|
7
|
+
|
8
|
+
|
9
|
+
|
data/docs/logging.md
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
# Plezi's Logging
|
2
|
+
|
3
|
+
(todo: write documentation)
|
4
|
+
|
5
|
+
Inside Plezi's core code is a pure Ruby IO reactor called [GReactor](https://github.com/boazsegev/GReactor) (Generic Reactor), a very powerful Asynchronous Workflow Engine that allows us to enjoy both Multi-Threading and Multi-Processing.
|
6
|
+
|
7
|
+
Plezi leverages [GReactor's](https://github.com/boazsegev/GReactor) logging support to help you log to both files and STDOUT (terminal screen) - either one or both
|
8
|
+
|
9
|
+
You can read more about [GReactor](https://github.com/boazsegev/GReactor) and it's amazing features in it's [documentation](http://www.rubydoc.info/github/boazsegev/GReactor/master).
|
10
|
+
|
11
|
+
## Setting up a Logger
|
12
|
+
|
13
|
+
|
14
|
+
## Logging Helpers Methods
|
data/docs/routes.md
ADDED
@@ -0,0 +1,37 @@
|
|
1
|
+
# Plezi's Smart Routing System
|
2
|
+
|
3
|
+
In the core of Plezi's framework is a smart Object Oriented Router which actls like a "virtual folder" with RESTful routing and Websocket support.
|
4
|
+
|
5
|
+
RESTful routing and Websocket callback support both allow us to use conventionally named methods in our Controller to achive common tasks. Such names methods, as will be explored further on, include the `update`, `save` and `show` RESTful method names, as well as the `on_open`, `on_message(data)` and `on_close` Websocket callbacks.
|
6
|
+
|
7
|
+
## Defining a Route
|
8
|
+
|
9
|
+
(todo: write documentation)
|
10
|
+
|
11
|
+
### The `:id` parameter
|
12
|
+
|
13
|
+
(todo: write documentation)
|
14
|
+
|
15
|
+
### More inline parameters
|
16
|
+
|
17
|
+
(todo: write documentation)
|
18
|
+
|
19
|
+
### Re-Write Routes and Proc Controllers
|
20
|
+
|
21
|
+
## The Controller
|
22
|
+
|
23
|
+
(todo: write documentation)
|
24
|
+
|
25
|
+
### A Virtual Folder
|
26
|
+
|
27
|
+
(todo: write documentation)
|
28
|
+
|
29
|
+
### RESTful methods
|
30
|
+
|
31
|
+
(todo: write documentation)
|
32
|
+
|
33
|
+
### Websocket Callbacks
|
34
|
+
|
35
|
+
(todo: write documentation)
|
36
|
+
|
37
|
+
|
data/docs/websockets.md
ADDED
@@ -11,28 +11,28 @@ module Plezi
|
|
11
11
|
def app_tree
|
12
12
|
@app_tree ||= {}
|
13
13
|
end
|
14
|
-
def build_mini
|
14
|
+
def build_mini app_name = ARGV[1]
|
15
15
|
require 'plezi/version'
|
16
|
-
|
17
|
-
|
16
|
+
app_tree["#{app_name}"] ||= IO.read( File.join(@root, "resources" ,"mini_exec.rb")).gsub('appname', app_name)
|
17
|
+
app_tree["#{app_name}.rb"] ||= IO.read( File.join(@root, "resources" ,"mini_app.rb")).gsub('appname', app_name).gsub('appsecret', "#{app_name}_#{SecureRandom.hex}")
|
18
18
|
app_tree["Procfile"] ||= ""
|
19
|
-
app_tree["Procfile"] << "\nweb: bundle exec ruby ./#{
|
19
|
+
app_tree["Procfile"] << "\nweb: bundle exec ruby ./#{app_name} -p $PORT\n"
|
20
20
|
app_tree["Gemfile"] ||= ''
|
21
21
|
app_tree["Gemfile"] << "source 'https://rubygems.org'\n\n####################\n# core gems\n\n# include the basic plezi framework and server\ngem 'plezi', '~> #{Plezi::VERSION}'\n"
|
22
22
|
app_tree["Gemfile"] << "\n\n\nruby '#{RUBY_VERSION}'\n"
|
23
23
|
app_tree["templates"] ||= {}
|
24
24
|
app_tree["templates"]["404.html.erb"] ||= IO.read(File.join(@root, "resources" ,"404.erb"))
|
25
25
|
app_tree["templates"]["500.html.erb"] ||= IO.read(File.join(@root, "resources" ,"500.erb"))
|
26
|
-
app_tree["templates"]["welcome.html.erb"] ||= IO.read(File.join(@root, "resources" ,"mini_welcome_page.html")).gsub('appname',
|
26
|
+
app_tree["templates"]["welcome.html.erb"] ||= IO.read(File.join(@root, "resources" ,"mini_welcome_page.html")).gsub('appname', app_name)
|
27
27
|
app_tree["assets"] ||= {}
|
28
|
-
app_tree["assets"]["websocket.js"] ||= IO.read(File.join(@root, "resources" ,"websockets.js")).gsub('appname',
|
29
|
-
finalize
|
28
|
+
app_tree["assets"]["websocket.js"] ||= IO.read(File.join(@root, "resources" ,"websockets.js")).gsub('appname', app_name)
|
29
|
+
finalize app_name
|
30
30
|
end
|
31
31
|
|
32
|
-
def build
|
32
|
+
def build app_name = ARGV[1]
|
33
33
|
require 'plezi/version'
|
34
34
|
# plezi run script
|
35
|
-
|
35
|
+
app_tree["#{app_name}"] ||= IO.read File.join(@root,"resources" ,"code.rb")
|
36
36
|
|
37
37
|
# set up application files
|
38
38
|
app_tree["app"] ||= {}
|
@@ -55,15 +55,15 @@ module Plezi
|
|
55
55
|
app_tree["assets"] ||= {}
|
56
56
|
app_tree["assets"]["stylesheets"] ||= {}
|
57
57
|
app_tree["assets"]["javascripts"] ||= {}
|
58
|
-
app_tree["assets"]["javascripts"]["websocket.js"] ||= IO.read(File.join(@root,"resources" ,"websockets.js")).gsub('appname',
|
59
|
-
app_tree["assets"]["welcome.html"] ||= IO.read(File.join(@root,"resources" ,"welcome_page.html")).gsub('appname',
|
58
|
+
app_tree["assets"]["javascripts"]["websocket.js"] ||= IO.read(File.join(@root,"resources" ,"websockets.js")).gsub('appname', app_name)
|
59
|
+
app_tree["assets"]["welcome.html"] ||= IO.read(File.join(@root,"resources" ,"welcome_page.html")).gsub('appname', app_name)
|
60
60
|
|
61
61
|
# app core files.
|
62
62
|
app_tree["environment.rb"] ||= IO.read File.join(@root,"resources" ,"environment.rb")
|
63
63
|
app_tree["routes.rb"] ||= IO.read File.join(@root,"resources" ,"routes.rb")
|
64
64
|
app_tree["rakefile"] ||= IO.read File.join(@root,"resources" ,"rakefile")
|
65
65
|
app_tree["Procfile"] ||= ""
|
66
|
-
app_tree["Procfile"] << "\nweb: bundle exec ruby ./#{
|
66
|
+
app_tree["Procfile"] << "\nweb: bundle exec ruby ./#{app_name} -p $PORT\n"
|
67
67
|
app_tree["Gemfile"] ||= ''
|
68
68
|
app_tree["Gemfile"] << "source 'https://rubygems.org'\n\n####################\n# core gems\n\n# include the basic plezi framework and server\ngem 'plezi', '~> #{Plezi::VERSION}'\n"
|
69
69
|
app_tree["Gemfile"] << IO.read( File.join(@root,"resources" ,"Gemfile"))
|
@@ -78,7 +78,7 @@ module Plezi
|
|
78
78
|
app_tree["config"]["haml.rb"] ||= IO.read(File.join(@root,"resources" ,"haml_config.rb"))
|
79
79
|
app_tree["config"]["slim.rb"] ||= IO.read(File.join(@root,"resources" ,"slim_config.rb"))
|
80
80
|
app_tree["config"]["i18n.rb"] ||= IO.read(File.join(@root,"resources" ,"i18n_config.rb"))
|
81
|
-
app_tree["config"]["redis.rb"] ||= (IO.read(File.join(@root,"resources" ,"redis_config.rb"))).gsub('appsecret', "#{
|
81
|
+
app_tree["config"]["redis.rb"] ||= (IO.read(File.join(@root,"resources" ,"redis_config.rb"))).gsub('appsecret', "#{app_name}_#{SecureRandom.hex}")
|
82
82
|
|
83
83
|
#set up database stub folders
|
84
84
|
app_tree["db"] ||= {}
|
@@ -106,29 +106,29 @@ module Plezi
|
|
106
106
|
app_tree["public"]["assets"]["stylesheets"] ||= {}
|
107
107
|
app_tree["public"]["assets"]["javascripts"] ||= {}
|
108
108
|
app_tree["public"]["images"] ||= {}
|
109
|
-
finalize
|
109
|
+
finalize app_name
|
110
110
|
end
|
111
|
-
def finalize
|
111
|
+
def finalize app_name = ARGV[1]
|
112
112
|
begin
|
113
|
-
Dir.mkdir
|
114
|
-
puts "created the #{
|
113
|
+
Dir.mkdir app_name
|
114
|
+
puts "created the #{app_name} application directory.".green
|
115
115
|
rescue Exception => e
|
116
|
-
puts "the #{
|
116
|
+
puts "the #{app_name} application directory exists - trying to rebuild (no overwrite).".pink
|
117
117
|
end
|
118
|
-
Dir.chdir
|
118
|
+
Dir.chdir app_name
|
119
119
|
puts "starting to write template data...".red
|
120
120
|
puts ""
|
121
121
|
Builder.write_files app_tree
|
122
|
-
File.chmod 0775, "#{
|
122
|
+
File.chmod 0775, "#{app_name}"
|
123
123
|
puts "tried to update execution permissions. this is system dependent and might have failed.".pink
|
124
|
-
puts "use: chmod +x ./#{
|
124
|
+
puts "use: chmod +x ./#{app_name} to set execution permissions on Unix machines."
|
125
125
|
puts ""
|
126
126
|
puts "done."
|
127
127
|
puts "\n#{@end_comments.join("\n")}" unless @end_comments.empty?
|
128
128
|
puts ""
|
129
|
-
puts "please change directory into the app directory: cd #{
|
129
|
+
puts "please change directory into the app directory: cd #{app_name}"
|
130
130
|
puts ""
|
131
|
-
puts "run the #{
|
131
|
+
puts "run the #{app_name} app using: ./#{app_name} or using: plezi s"
|
132
132
|
puts ""
|
133
133
|
end
|
134
134
|
end
|
data/lib/plezi/common/api.rb
CHANGED
@@ -109,9 +109,9 @@ module Plezi
|
|
109
109
|
def start_async
|
110
110
|
Object.const_set("NO_PLEZI_AUTO_START", true) unless defined?(NO_PLEZI_AUTO_START)
|
111
111
|
return GReactor.start if GReactor.running?
|
112
|
-
puts "Starting Plezi #{Plezi::VERSION} Services using
|
112
|
+
puts "Starting Plezi #{Plezi::VERSION} Services using GRHttp #{GRHttp::VERSION} and GReactor #{GReactor::VERSION}."
|
113
113
|
GReactor.on_shutdown { puts "Plezi shutdown. It was fun to serve you." }
|
114
|
-
GReactor.start Plezi::Settings.max_threads
|
114
|
+
GReactor.start ::Plezi::Settings.max_threads
|
115
115
|
end
|
116
116
|
# This allows you to run the Plezi framework along side another framework - WITHOUT running the actual server.
|
117
117
|
#
|
@@ -136,4 +136,4 @@ end
|
|
136
136
|
Encoding.default_internal = 'utf-8'
|
137
137
|
Encoding.default_external = 'utf-8'
|
138
138
|
|
139
|
-
NO_PLEZI_AUTO_START
|
139
|
+
Object.const_set("NO_PLEZI_AUTO_START", true) if defined?(::Rack::Builder) && !defined?(NO_PLEZI_AUTO_START)
|
data/lib/plezi/common/dsl.rb
CHANGED
@@ -13,6 +13,12 @@ module Plezi
|
|
13
13
|
# response:: the HTTPResponse **OR** the WSResponse object that formats the response and sends it. use `response << data`. This object can be used to send partial data (such as headers, or partial html content) in blocking mode as well as sending data in the default non-blocking mode.
|
14
14
|
# host_params:: a copy of the parameters used to create the host and service which accepted the request and created this instance of the controller class.
|
15
15
|
#
|
16
|
+
# For Controller Class menthods, please read the documentation about {Plezi::ControllerMagic::ClassMethods}.
|
17
|
+
#
|
18
|
+
# For Controller Instance methods, please read the documentation about {Plezi::ControllerMagic::InstanceMethods}.
|
19
|
+
#
|
20
|
+
# {include: Plezi::ControllerMagic::InstanceMethods}
|
21
|
+
#
|
16
22
|
module ControllerMagic
|
17
23
|
def self.included base
|
18
24
|
base.send :include, InstanceMethods
|
@@ -38,7 +44,7 @@ module Plezi
|
|
38
44
|
#
|
39
45
|
# The first time this method is called, the session object will be created. The session object must be created BEFORE the headers are set , if it is to be used.
|
40
46
|
#
|
41
|
-
# Sessions are not automatically created, because they
|
47
|
+
# Sessions are not automatically created, because they require more resources. The one exception is the Websocket connection that will force a session object into existence, as it's very common to use session data in Websocket connections and the extra connection time is less relevant for a long term connection.
|
42
48
|
def session
|
43
49
|
response.session
|
44
50
|
end
|
@@ -73,14 +79,14 @@ module Plezi
|
|
73
79
|
#
|
74
80
|
def redirect_to url, options = {}
|
75
81
|
return super *[] if defined? super
|
76
|
-
raise 'Cannot redirect
|
82
|
+
raise 'Cannot redirect once a Websocket connection was established.' if response.is_a?(::GRHttp::WSEvent)
|
83
|
+
raise 'Cannot redirect after headers were sent.' if response.headers_sent?
|
77
84
|
url = "#{request.base_url}/#{url.to_s.gsub('_', '/')}" if url.is_a?(Symbol) || ( url.is_a?(String) && url.empty? ) || url.nil?
|
78
85
|
# redirect
|
79
86
|
response.status = options.delete(:status) || 302
|
80
87
|
response['Location'] = url
|
81
88
|
response['content-length'] ||= 0
|
82
89
|
flash.update options
|
83
|
-
response.finish
|
84
90
|
true
|
85
91
|
end
|
86
92
|
|
@@ -123,6 +129,8 @@ module Plezi
|
|
123
129
|
# filename:: sets a filename for the browser to "save as". defaults to empty.
|
124
130
|
#
|
125
131
|
def send_data data, options = {}
|
132
|
+
raise 'Cannot use "send_data" once a Websocket connection was established.' if response.is_a?(::GRHttp::WSEvent)
|
133
|
+
# return response.write(data) if response.is_a?(::GRHttp::WSEvent)
|
126
134
|
raise 'Cannot use "send_data" after headers were sent' if response.headers_sent?
|
127
135
|
Plezi.warn 'HTTP response buffer is cleared by `#send_data`' if response.body && response.body.any? && response.body.clear
|
128
136
|
response << data
|
@@ -131,15 +139,15 @@ module Plezi
|
|
131
139
|
content_disposition = options[:inline] ? 'inline' : 'attachment'
|
132
140
|
content_disposition << "; filename=#{options[:filename]}" if options[:filename]
|
133
141
|
|
134
|
-
response['content-type'] = options[:type] ||= MimeTypeHelper::MIME_DICTIONARY[::File.extname(options[:filename])]
|
142
|
+
response['content-type'] = (options[:type] ||= MimeTypeHelper::MIME_DICTIONARY[::File.extname(options[:filename])])
|
135
143
|
response['content-length'] = data.bytesize rescue true
|
136
144
|
response['content-disposition'] = content_disposition
|
137
|
-
response.finish
|
138
145
|
true
|
139
146
|
end
|
140
147
|
|
141
|
-
#
|
142
|
-
#
|
148
|
+
# 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).
|
149
|
+
#
|
150
|
+
# For example, to render the file `body.html.slim` with the layout `main_layout.html.haml`:
|
143
151
|
# render :body, layout: :main_layout
|
144
152
|
#
|
145
153
|
# or, for example, to render the file `json.js.slim`
|
@@ -171,14 +179,16 @@ module Plezi
|
|
171
179
|
# set up defaults
|
172
180
|
options[:type] ||= 'html'
|
173
181
|
options[:locale] ||= params[:locale].to_sym if params[:locale]
|
182
|
+
#update content-type header
|
183
|
+
response['content-type'] ||= (MimeTypeHelper::MIME_DICTIONARY[options[:type]] + "; charset=utf-8".freeze)
|
174
184
|
# options[:locals] ||= {}
|
175
185
|
I18n.locale = options[:locale] || I18n.default_locale if defined?(I18n) # sets the locale to nil for default behavior even if the locale was set by a previous action - removed: # && options[:locale]
|
176
186
|
# find template and create template object
|
177
187
|
filename = template.is_a?(String) ? File.join( host_params[:templates].to_s, template) : (File.join( host_params[:templates].to_s, *template.to_s.split('_')) + (options[:type].empty? ? '': ".#{options[:type]}") + '.slim')
|
178
188
|
return ( Plezi.cache_needs_update?(filename) ? Plezi.cache_data( filename, ( Slim::Template.new() { IO.binread filename } ) ) : (Plezi.get_cached filename) ).render(self, &block) if defined?(::Slim) && Plezi.file_exists?(filename)
|
179
|
-
filename.
|
189
|
+
filename.sub! /\.slim$/, '.haml'
|
180
190
|
return ( Plezi.cache_needs_update?(filename) ? Plezi.cache_data( filename, ( Haml::Engine.new( IO.binread(filename) ) ) ) : (Plezi.get_cached filename) ).render(self, &block) if defined?(::Haml) && Plezi.file_exists?(filename)
|
181
|
-
filename.
|
191
|
+
filename.sub! /\.haml$/, '.erb'
|
182
192
|
return ( Plezi.cache_needs_update?(filename) ? Plezi.cache_data( filename, ( ERB.new( IO.binread(filename) ) ) ) : (Plezi.get_cached filename) ).result(binding, &block) if defined?(::ERB) && Plezi.file_exists?(filename)
|
183
193
|
return false
|
184
194
|
end
|
@@ -107,42 +107,41 @@ module Plezi
|
|
107
107
|
source_file = File.join(params[:assets], *(request.path.match(/^#{params[:assets_public]}\/(.+)/)[1].split('/')))
|
108
108
|
|
109
109
|
# stop if file name is reserved / has security issues
|
110
|
-
return false if source_file.match(/(scss|sass|coffee|\.\.\/)$/)
|
110
|
+
return false if File.directory?(source_file) || source_file.match(/(scss|sass|coffee|\.\.\/)$/)
|
111
111
|
|
112
112
|
# set where to store the rendered asset
|
113
|
-
target_file =
|
114
|
-
target_file = File.join( params[:root], params[:assets_public], *request.path.match(/^#{params[:assets_public]}\/(.*)/)[1].split('/') ) if params[:root]
|
113
|
+
target_file = File.join( params[:public].to_s, params[:assets_public].to_s, *request.path.match(/^#{params[:assets_public]}\/(.*)/)[1].split('/') )
|
115
114
|
|
116
115
|
# send the file if it exists (no render needed)
|
117
116
|
if File.exists?(source_file)
|
118
|
-
data = Plezi.cache_needs_update?(source_file) ? Plezi.save_file(target_file, Plezi.reload_file(source_file), params[:save_assets]) : Plezi.load_file(source_file)
|
117
|
+
data = Plezi.cache_needs_update?(source_file) ? Plezi.save_file(target_file, Plezi.reload_file(source_file), (params[:public] && params[:save_assets])) : Plezi.load_file(source_file)
|
119
118
|
return (data ? Base::HTTPSender.send_raw_data(request, response, data, MimeTypeHelper::MIME_DICTIONARY[::File.extname(source_file)]) : false)
|
120
119
|
end
|
121
120
|
|
122
121
|
# render supported assets
|
123
122
|
case source_file
|
124
123
|
when /\.css$/
|
125
|
-
sass = source_file.
|
126
|
-
sass.
|
124
|
+
sass = source_file.sub /css$/, 'sass'
|
125
|
+
sass.sub! /sass$/, 'scss' unless Plezi.file_exists?(sass)
|
127
126
|
return false unless Plezi.file_exists?(sass)
|
128
127
|
# review mtime and render sass if necessary
|
129
128
|
if defined?(::Sass) && refresh_sass?(sass)
|
130
129
|
eng = Sass::Engine.for_file(sass, cache_store: @sass_cache)
|
131
130
|
Plezi.cache_data sass, eng.dependencies
|
132
131
|
css, map = eng.render_with_sourcemap(params[:assets_public])
|
133
|
-
Plezi.save_file target_file, css, params[:save_assets]
|
134
|
-
Plezi.save_file (target_file + ".map"), map, params[:save_assets]
|
132
|
+
Plezi.save_file target_file, css, (params[:public] && params[:save_assets])
|
133
|
+
Plezi.save_file (target_file + ".map"), map, (params[:public] && params[:save_assets])
|
135
134
|
end
|
136
135
|
# try to send the cached css file which started the request.
|
137
136
|
return Base::HTTPSender.send_file request, response, target_file
|
138
137
|
when /\.js$/
|
139
|
-
coffee = source_file.
|
138
|
+
coffee = source_file.sub /js$/i, 'coffee'
|
140
139
|
return false unless Plezi.file_exists?(coffee)
|
141
140
|
# review mtime and render coffee if necessary
|
142
141
|
if defined?(::CoffeeScript) && Plezi.cache_needs_update?(coffee)
|
143
142
|
# render coffee to cache
|
144
143
|
Plezi.cache_data coffee, nil
|
145
|
-
Plezi.save_file target_file, CoffeeScript.compile(IO.binread coffee), params[:save_assets]
|
144
|
+
Plezi.save_file target_file, CoffeeScript.compile(IO.binread coffee), (params[:public] && params[:save_assets])
|
146
145
|
end
|
147
146
|
# try to send the cached js file which started the request.
|
148
147
|
return Base::HTTPSender.send_file request, response, target_file
|
@@ -68,7 +68,7 @@ module Plezi
|
|
68
68
|
self.read
|
69
69
|
GR.warn "Placebo IO recieved IO signal - this is unexpected..."
|
70
70
|
end
|
71
|
-
def
|
71
|
+
def on_close
|
72
72
|
@params[:out].close rescue nil
|
73
73
|
@cache[:websocket_handler].on_close if @cache[:websocket_handler]
|
74
74
|
end
|
@@ -86,8 +86,7 @@ module Plezi
|
|
86
86
|
Object.const_set(new_class_name, new_class)
|
87
87
|
end
|
88
88
|
i, o = IO.pipe
|
89
|
-
io = Placebo::Base::PlaceboIO.new i, out: o
|
90
|
-
io = GReactor.add_raw_io i, io
|
89
|
+
io = Placebo::Base::PlaceboIO.new i, out: o, reactor: ::GReactor
|
91
90
|
new_class.new(io)
|
92
91
|
end
|
93
92
|
end
|
data/lib/plezi/handlers/route.rb
CHANGED
@@ -17,7 +17,7 @@ module Plezi
|
|
17
17
|
fill_parameters = match request.path
|
18
18
|
return false unless fill_parameters
|
19
19
|
old_params = request.params.dup
|
20
|
-
fill_parameters.each {|k,v|
|
20
|
+
fill_parameters.each {|k,v| Plezi::Base::Helpers.add_param_to_hash k, v, request.params }
|
21
21
|
ret = false
|
22
22
|
if controller
|
23
23
|
ret = controller.new(request, response)._route_path_to_methods_and_set_the_response_
|
@@ -112,7 +112,7 @@ module Plezi
|
|
112
112
|
param_name = param_name[1].to_sym if param_name
|
113
113
|
|
114
114
|
if param_name && dest[param_name]
|
115
|
-
url <<
|
115
|
+
url << Plezi::Base::Helpers.encode_url(dest.delete(param_name))
|
116
116
|
url << '/'
|
117
117
|
elsif !param_name
|
118
118
|
url << sec
|
@@ -125,7 +125,7 @@ module Plezi
|
|
125
125
|
end
|
126
126
|
unless dest.empty?
|
127
127
|
add = '?'
|
128
|
-
dest.each {|k, v| url << "#{add}#{
|
128
|
+
dest.each {|k, v| url << "#{add}#{Plezi::Base::Helpers.encode_url k}=#{Plezi::Base::Helpers.encode_url v}"; add = '&'}
|
129
129
|
end
|
130
130
|
url
|
131
131
|
|
@@ -229,7 +229,7 @@ module Plezi
|
|
229
229
|
# m = nil
|
230
230
|
# unless @fill_parameters.values.include?("format")
|
231
231
|
# if (m = path.match /([^\.]*)\.([^\.\/]+)$/)
|
232
|
-
#
|
232
|
+
# Plezi::Base::Helpers.add_param_to_hash 'format', m[2], hash
|
233
233
|
# path = m[1]
|
234
234
|
# end
|
235
235
|
# end
|
@@ -5,9 +5,8 @@ module Plezi
|
|
5
5
|
# returns a session object
|
6
6
|
def fetch id
|
7
7
|
return Plezi::Session.new(id) if Plezi.redis # avoid a local cache if Redis is used.
|
8
|
-
|
8
|
+
GRHttp::Base::FileSessionStorage.fetch id # use the tmp-file-session logic if Redis isn't present
|
9
9
|
end
|
10
|
-
@session_cache = {}
|
11
10
|
end
|
12
11
|
end
|
13
12
|
# A hash like interface for storing request session data.
|
@@ -22,9 +21,9 @@ module Plezi
|
|
22
21
|
def initialize id
|
23
22
|
@id = id
|
24
23
|
if id && (conn=Plezi.redis)
|
25
|
-
|
24
|
+
return self
|
26
25
|
end
|
27
|
-
|
26
|
+
failed
|
28
27
|
end
|
29
28
|
# Get a key from the session data store. If a Redis server is supplied, it will be used to synchronize session data.
|
30
29
|
#
|
@@ -34,9 +33,9 @@ module Plezi
|
|
34
33
|
key = key.to_s
|
35
34
|
if conn=Plezi.redis
|
36
35
|
conn.expire @id, SESSION_LIFETIME
|
37
|
-
|
36
|
+
return conn.hget @id, key
|
38
37
|
end
|
39
|
-
|
38
|
+
failed
|
40
39
|
end
|
41
40
|
alias :fetch :[]
|
42
41
|
|
@@ -49,8 +48,9 @@ module Plezi
|
|
49
48
|
if (conn=Plezi.redis)
|
50
49
|
conn.hset @id, key, value
|
51
50
|
conn.expire @id, SESSION_LIFETIME
|
51
|
+
return value
|
52
52
|
end
|
53
|
-
|
53
|
+
failed
|
54
54
|
end
|
55
55
|
alias :store :[]=
|
56
56
|
|
@@ -58,9 +58,9 @@ module Plezi
|
|
58
58
|
def to_h
|
59
59
|
if (conn=Plezi.redis)
|
60
60
|
conn.expire @id, SESSION_LIFETIME
|
61
|
-
return
|
61
|
+
return conn.hgetall(@id)
|
62
62
|
end
|
63
|
-
|
63
|
+
failed
|
64
64
|
end
|
65
65
|
|
66
66
|
# Removes a key from the session's data store.
|
@@ -68,17 +68,26 @@ module Plezi
|
|
68
68
|
key = key.to_s
|
69
69
|
if (conn=Plezi.redis)
|
70
70
|
conn.expire @id, SESSION_LIFETIME
|
71
|
+
ret = conn.hget @id, key
|
71
72
|
conn.hdel @id, key
|
73
|
+
return ret
|
72
74
|
end
|
73
|
-
|
75
|
+
failed
|
74
76
|
end
|
75
77
|
|
76
78
|
# Clears the session's data.
|
77
79
|
def clear
|
78
80
|
if (conn=Plezi.redis)
|
79
|
-
conn.del @id
|
81
|
+
return conn.del @id
|
80
82
|
end
|
81
|
-
|
83
|
+
failed
|
84
|
+
end
|
85
|
+
|
86
|
+
protected
|
87
|
+
|
88
|
+
def failed
|
89
|
+
raise 'Redis connection failed while using Redis Session Storage.'
|
90
|
+
|
82
91
|
end
|
83
92
|
end
|
84
93
|
GRHttp::SessionManager.storage = Plezi::Base::SessionStorage
|
@@ -24,7 +24,7 @@ module Plezi
|
|
24
24
|
elsif Plezi.file_exists?(fn = File.join(base_code_path, "#{code}.html"))
|
25
25
|
return send_file(request, response, fn, code, headers)
|
26
26
|
end
|
27
|
-
return true if send_raw_data(request, response,
|
27
|
+
return true if send_raw_data(request, response, response.class::STATUS_CODES[code], 'text/plain', code, headers)
|
28
28
|
rescue Exception => e
|
29
29
|
Plezi.error e
|
30
30
|
end
|
@@ -1,8 +1,5 @@
|
|
1
1
|
module Plezi
|
2
2
|
|
3
|
-
# use GRHttp's helpers for escaping data etc'.
|
4
|
-
HTTP = GRHttp::HTTP
|
5
|
-
|
6
3
|
module Base
|
7
4
|
# some helper methods used internally.
|
8
5
|
module Helpers
|
@@ -12,7 +9,7 @@ module Plezi
|
|
12
9
|
HASH_SYM_PROC = Proc.new {|h,k| k = (Symbol === k ? k.to_s : k.to_s.to_sym); h[k] if h.has_key?(k) }
|
13
10
|
|
14
11
|
# tweeks a hash object to read both :symbols and strings (similar to Rails but without).
|
15
|
-
def make_hash_accept_symbols hash
|
12
|
+
def self.make_hash_accept_symbols hash
|
16
13
|
@magic_hash_proc ||= Proc.new do |hs,k|
|
17
14
|
if k.is_a?(Symbol) && hs.has_key?( k.to_s)
|
18
15
|
hs[k.to_s]
|
@@ -29,6 +26,71 @@ module Plezi
|
|
29
26
|
end
|
30
27
|
end
|
31
28
|
end
|
29
|
+
|
30
|
+
# encodes URL data
|
31
|
+
def self.encode_url str
|
32
|
+
(str.to_s.gsub(/[^a-z0-9\*\.\_\-]/i) {|m| '%%%02x'.freeze % m.ord }).force_encoding(::Encoding::ASCII_8BIT)
|
33
|
+
end
|
34
|
+
|
35
|
+
# Adds paramaters to a Hash object, according to the GRHttp's server conventions.
|
36
|
+
def self.add_param_to_hash name, value, target
|
37
|
+
begin
|
38
|
+
c = target
|
39
|
+
val = rubyfy! value
|
40
|
+
a = name.chomp('[]'.freeze).split('['.freeze)
|
41
|
+
|
42
|
+
a[0...-1].inject(target) do |h, n|
|
43
|
+
n.chomp!(']'.freeze);
|
44
|
+
n.strip!;
|
45
|
+
raise "malformed parameter name for #{name}" if n.empty?
|
46
|
+
n = (n.to_i.to_s == n) ? n.to_i : n.to_sym
|
47
|
+
c = (h[n] ||= {})
|
48
|
+
end
|
49
|
+
n = a.last
|
50
|
+
n.chomp!(']'); n.strip!;
|
51
|
+
n = n.empty? ? nil : ( (n.to_i.to_s == n) ? n.to_i : n.to_sym )
|
52
|
+
if n
|
53
|
+
if c[n]
|
54
|
+
c[n].is_a?(Array) ? (c[n] << val) : (c[n] = [c[n], val])
|
55
|
+
else
|
56
|
+
c[n] = val
|
57
|
+
end
|
58
|
+
else
|
59
|
+
if c[n]
|
60
|
+
c[n].is_a?(Array) ? (c[n] << val) : (c[n] = [c[n], val])
|
61
|
+
else
|
62
|
+
c[n] = [val]
|
63
|
+
end
|
64
|
+
end
|
65
|
+
val
|
66
|
+
rescue => e
|
67
|
+
GReactor.error e
|
68
|
+
GReactor.error "(Silent): parameters parse error for #{name} ... maybe conflicts with a different set?"
|
69
|
+
target[name] = val
|
70
|
+
end
|
71
|
+
end
|
72
|
+
# Changes String to a Ruby Object, if it's a special string...
|
73
|
+
def self.rubyfy!(string)
|
74
|
+
return string unless string.is_a?(String)
|
75
|
+
try_utf8! string
|
76
|
+
if string == 'true'.freeze
|
77
|
+
string = true
|
78
|
+
elsif string == 'false'.freeze
|
79
|
+
string = false
|
80
|
+
elsif string.to_i.to_s == string
|
81
|
+
string = string.to_i
|
82
|
+
end
|
83
|
+
string
|
84
|
+
end
|
85
|
+
|
86
|
+
# re-encodes a string into UTF-8 unly when the encoding will remail valid.
|
87
|
+
def self.try_utf8!(string, encoding= ::Encoding::UTF_8)
|
88
|
+
return false unless string
|
89
|
+
string.force_encoding(::Encoding::ASCII_8BIT) unless string.force_encoding(encoding).valid_encoding?
|
90
|
+
string
|
91
|
+
end
|
92
|
+
|
93
|
+
|
32
94
|
end
|
33
95
|
end
|
34
96
|
|
@@ -139,7 +139,7 @@ module Plezi
|
|
139
139
|
# @user ||= app_login || OAuth2Ctrl.auth(:facebook, fb_token, self) || OAuth2Ctrl.auth(:google, google_token, self) || ....
|
140
140
|
def self.auth service_name, service_token, controller
|
141
141
|
service = SERVICES[service_name]
|
142
|
-
|
142
|
+
return false unless service
|
143
143
|
# auth_res = controller.cookies[c_name] ? (JSON.parse URI.parse("#{service[:profile_url]}?access_token=#{controller.cookies[c_name]}").read rescue ({}) ) : {}
|
144
144
|
# controller.cookies[c_name] = nil unless auth_res['id']
|
145
145
|
# auth_res['id'] ? controller.instance_exec( service_name, auth_res['id'], auth_res['email'], auth_res, &auth_callback) : ( controller.instance_exec( service_name, nil, nil, auth_res, &auth_callback) && false)
|
@@ -156,7 +156,7 @@ module Plezi
|
|
156
156
|
def update
|
157
157
|
service_name = params[:id].to_s.to_sym
|
158
158
|
service = SERVICES[service_name]
|
159
|
-
|
159
|
+
return false unless service
|
160
160
|
if params[:error]
|
161
161
|
instance_exec( service_name, nil, nil, nil, {}, &self.class.auth_callback)
|
162
162
|
return redirect_to(flash[:redirect_after])
|
@@ -190,7 +190,7 @@ module Plezi
|
|
190
190
|
def _auth_url_for service_name
|
191
191
|
service = SERVICES[service_name]
|
192
192
|
return nil unless service
|
193
|
-
redirect_uri =
|
193
|
+
redirect_uri = Plezi::Base::Helpers.encode_url "#{request.base_url}/auth/#{service_name.to_s}", :url #response_type
|
194
194
|
return "#{service[:auth_url]}?client_id=#{service[:app_id]}&redirect_uri=#{redirect_uri}&scope=#{service[:scope]}&response_type=code"
|
195
195
|
end
|
196
196
|
|
data/lib/plezi/version.rb
CHANGED
data/plezi.gemspec
CHANGED
@@ -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 "grhttp", "~> 0.0
|
21
|
+
spec.add_dependency "grhttp", "~> 0.1.0"
|
22
22
|
spec.add_development_dependency "bundler", "~> 1.7"
|
23
23
|
spec.add_development_dependency "rake", "~> 10.0"
|
24
24
|
|
data/resources/config.ru
CHANGED
@@ -22,7 +22,7 @@
|
|
22
22
|
# 2. you have better control over Middleware then you could have with Plezi.
|
23
23
|
# ("wait", you might say, "there is no Middleware in Plezi!"... "Ahhh", I will answer, "so much to discover...")
|
24
24
|
|
25
|
-
NO_PLEZI_AUTO_START
|
25
|
+
Object.const_set("NO_PLEZI_AUTO_START", true) unless defined?(NO_PLEZI_AUTO_START)
|
26
26
|
|
27
27
|
# load all framework and gems
|
28
28
|
load ::File.expand_path(File.join("..", "environment.rb"), __FILE__)
|
@@ -31,11 +31,15 @@ load ::File.expand_path(File.join("..", "environment.rb"), __FILE__)
|
|
31
31
|
load ::File.expand_path(File.join("..", "routes.rb"), __FILE__)
|
32
32
|
|
33
33
|
# start the plezi EM, to make sure that the plezi async code doesn't break.
|
34
|
-
|
35
|
-
|
34
|
+
if Rack::Handler.default == GRHttp::Base::Rack
|
35
|
+
run(Proc.new { [404, {}, []] })
|
36
|
+
else
|
37
|
+
GReactor.clear_listeners
|
38
|
+
GReactor.start Plezi::Settings.max_threads
|
36
39
|
|
37
|
-
# run the Rack app - not yet supported
|
38
|
-
# run Plezi::Base::Rack
|
40
|
+
# run the Rack app - not yet supported
|
41
|
+
# run Plezi::Base::Rack
|
39
42
|
|
40
|
-
# # exit rack to start the plezi server
|
41
|
-
# Process.kill 'TERM'
|
43
|
+
# # exit rack to start the plezi server
|
44
|
+
# Process.kill 'TERM'
|
45
|
+
end
|
data/resources/mini_app.rb
CHANGED
@@ -6,27 +6,26 @@
|
|
6
6
|
## Set up root object, it might be used by the environment and\or the plezi extension gems.
|
7
7
|
Root ||= Pathname.new(File.dirname(__FILE__)).expand_path
|
8
8
|
## Set a persistent session token id name
|
9
|
-
GRHttp.session_token = 'appname_uui'
|
10
|
-
## make sure all file access and file loading is relative to the application's root folder
|
11
|
-
# Dir.chdir Root.to_s
|
12
|
-
## load code from a subfolder called 'code'
|
13
|
-
# Dir[File.join "{code}", "**" , "*.rb"].each {|file| load File.expand_path(file)}
|
14
|
-
## OR load code from all the ruby files in the main forlder (subfolder inclussion will fail on PaaS)
|
15
|
-
# Dir[File.join File.dirname(__FILE__), "*.rb"].each {|file| load File.expand_path(file) unless file == __FILE__}
|
16
|
-
|
17
9
|
## If this app is independant, use bundler to load gems (including the plezi gem).
|
18
10
|
## Else, use the original app's Gemfile and start Plezi's Rack mode.
|
19
11
|
require 'bundler'
|
20
12
|
Bundler.require(:default, ENV['ENV'].to_s.to_sym)
|
21
13
|
## OR:
|
22
14
|
# Plesi.start_rack # remember
|
15
|
+
GRHttp.session_token = 'appname_uui'
|
16
|
+
## make sure all file access and file loading is relative to the application's root folder
|
17
|
+
# Dir.chdir Root.to_s
|
18
|
+
## load code from a subfolder called 'app'
|
19
|
+
# Dir[File.join "{app}", "**" , "*.rb"].each {|file| load File.expand_path(file)}
|
20
|
+
## OR load code from all the ruby files in the main forlder (subfolder inclussion will fail on PaaS)
|
21
|
+
# Dir[File.join File.dirname(__FILE__), "*.rb"].each {|file| load File.expand_path(file) unless file == __FILE__}
|
23
22
|
|
24
23
|
## Uncomment to create a log file
|
25
24
|
# GReactor.create_logger File.expand_path(Root.join('server.log').to_s)
|
26
25
|
|
27
26
|
## Options for Scaling the app (across processes or machines):
|
28
|
-
## uncomment to set up forking.
|
29
|
-
# GReactor
|
27
|
+
## uncomment to set up forking for 3 more processes (total of 4).
|
28
|
+
# GReactor.forking 3
|
30
29
|
## Redis scaling
|
31
30
|
# Plezi::Settings.redis_channel_name = 'appsecret'
|
32
31
|
# ENV['PL_REDIS_URL'] ||= ENV['REDIS_URL'] || ENV['REDISCLOUD_URL'] || ENV['REDISTOGO_URL'] || "redis://username:password@my.host:6389"
|
@@ -248,10 +248,10 @@ function send_text()
|
|
248
248
|
<p class="bold">Reviewing the sample code and feeding <a href="https://github.com/boazsegev/plezi">Plezi</a> your code:</p>
|
249
249
|
<ul>
|
250
250
|
<li>Review and update MyController in your 'appname.rb'.</li>
|
251
|
-
<li>
|
252
|
-
<li>Write your own controller and code, using a sub-foler as suggested in the 'appname.rb' file.</li>
|
251
|
+
<li>Edit or delete this file (appname/templates/welcome.html.erb).</li>
|
252
|
+
<li>Write your own controller and code, maybe using a sub-foler as suggested in the 'appname.rb' file.</li>
|
253
253
|
<li>Set up your routes in the 'appname.rb' file.</li>
|
254
|
-
<li>Edit the javascript for the client in your 'appname/websockets.js' file.</li>
|
254
|
+
<li>Edit the javascript for the client in your 'appname/websockets.js' file and link to it from your html.</li>
|
255
255
|
</ul>
|
256
256
|
</li>
|
257
257
|
<li>
|
data/test/plezi_tests.rb
CHANGED
@@ -88,7 +88,7 @@ class TestCtrl
|
|
88
88
|
true
|
89
89
|
end
|
90
90
|
def _stream_out
|
91
|
-
response
|
91
|
+
response << "streamed"
|
92
92
|
true
|
93
93
|
end
|
94
94
|
def file_test
|
@@ -303,7 +303,11 @@ module PleziTestTasks
|
|
303
303
|
puts e.message
|
304
304
|
end
|
305
305
|
remote = GRHttp::WSClient.connect_to("wss://echo.websocket.org/") {|ws| puts " * Extra Websocket Remote test (SSL: echo.websocket.org): #{RESULTS[ws.data == 'Hello websockets!']}"; response.close}
|
306
|
-
remote
|
306
|
+
if remote.closed?
|
307
|
+
puts " * Extra Websocket Remote test (SSL: echo.websocket.org): #{RESULTS[false]}"
|
308
|
+
else
|
309
|
+
remote << "Hello websockets!"
|
310
|
+
end
|
307
311
|
sleep 0.5
|
308
312
|
[ws1, ws2, ws3, ws4, remote].each {|ws| ws.close}
|
309
313
|
PL.on_shutdown {puts " * Websocket connection message test: #{RESULTS[connection_test]}" unless connection_test}
|
@@ -475,7 +479,7 @@ Plezi.start_async
|
|
475
479
|
PleziTestTasks.run_tests
|
476
480
|
|
477
481
|
# ENV['PL_REDIS_URL'] ||= ENV['REDIS_URL'] || ENV['REDISCLOUD_URL'] || ENV['REDISTOGO_URL'] || "redis://test:1234@pub-redis-11008.us-east-1-4.5.ec2.garantiadata.com:11008"
|
478
|
-
# GReactor
|
482
|
+
# GReactor.forking 4
|
479
483
|
# GR.run_async { PleziTestTasks.run_tests }
|
480
484
|
# start_services
|
481
485
|
|
data/websocket chatroom.md
CHANGED
@@ -109,7 +109,7 @@ Last, but not least, we tell Plezi to connect the root of our web application to
|
|
109
109
|
route '/', ChatController
|
110
110
|
```
|
111
111
|
|
112
|
-
Plezi controller classes are like virtual folders with special support for RESTful methods (`index`, `new`, `save`, `update`, `delete`), HTTP filters and helpers (`before`, `after`, `redirect_to`, `send_data`), WebSockets methods (`
|
112
|
+
Plezi controller classes are like virtual folders with special support for RESTful methods (`index`, `new`, `save`, `update`, `delete`), HTTP filters and helpers (`before`, `after`, `redirect_to`, `send_data`), WebSockets methods (`on_open`, `on_message(data)`, `on_close`), and WebSockets filters and helpers (`pre-connect`, `broadcast`, `unicast` etc').
|
113
113
|
|
114
114
|
Plezi uses a common special parameter called 'id' to help with all this magic... if we don't define this parameter ourselves, Plezi will try to append this parameter to the end our route's path. So, actually, our route looks like this:
|
115
115
|
|
@@ -213,11 +213,11 @@ end
|
|
213
213
|
To design a chatroom we will need a few things:
|
214
214
|
|
215
215
|
1. We will need to force people identify themselves by choosing nicknames - to do this we will define the `on_connect` method to refuse any connections that don't have a nickname.
|
216
|
-
2. We will want to make sure these nicknames are unique and don't give a wrong sense of authority (nicknames such as 'admin' should be forbidden) - for now, we will simply
|
216
|
+
2. We will want to make sure these nicknames are unique and don't give a wrong sense of authority (nicknames such as 'admin' should be forbidden) - for now, we will simply refuse the 'wrong' type of nicknames and leave uniqieness for another time.
|
217
217
|
3. We will want to push messages we recieve to all the other chatroom members - to do this we will use the `broadcast` method in our `on_message(data)` method.
|
218
218
|
4. We will also want to tell people when someone left the chatroom - to do this we can define an `on_disconnect` method and use the `broadcast` method in there.
|
219
219
|
|
220
|
-
We can use the :id parameter to
|
220
|
+
We can use the :id parameter to set the nickname.
|
221
221
|
|
222
222
|
the :id is an automatic parameter that Plezi appended to our path like already explained and it's perfect for our simple needs.
|
223
223
|
|
@@ -225,9 +225,9 @@ We could probably rewrite our route to something like this: `route '/(:id)/(:nic
|
|
225
225
|
|
226
226
|
####Broadcasting chat (websocket) messages
|
227
227
|
|
228
|
-
When we get a chat message, with `on_message(data)`, we will want to broadcast this message to all
|
228
|
+
When we get a chat message, with `on_message(data)`, we will want to broadcast this message to all the _other_ ChatController connections.
|
229
229
|
|
230
|
-
Using JSON, our new
|
230
|
+
Using JSON, our new `on_message(data)` method can look something like this:
|
231
231
|
|
232
232
|
```ruby
|
233
233
|
def on_message data
|
@@ -239,7 +239,7 @@ def on_message data
|
|
239
239
|
return false
|
240
240
|
end
|
241
241
|
message = {}
|
242
|
-
message[:message] = data[
|
242
|
+
message[:message] = GRHttp.HTTP.escape data['message'] # we should sanitize the data
|
243
243
|
message[:event] = :chat
|
244
244
|
message[:from] = params[:id]
|
245
245
|
message[:at] = Time.now
|
@@ -258,11 +258,11 @@ def on_message data
|
|
258
258
|
response.close
|
259
259
|
return false
|
260
260
|
end
|
261
|
-
broadcast :_send_message, {event: :chat, from: params[:id], message: data[
|
261
|
+
broadcast :_send_message, {event: :chat, from: params[:id], message: GRHttp.HTTP.escape(data['message']), at: Time.now}.to_json
|
262
262
|
end
|
263
263
|
```
|
264
264
|
|
265
|
-
Now that the
|
265
|
+
Now that the boring stuff is condenced, let's look at that last line - the one that calls `broadcast`
|
266
266
|
|
267
267
|
`broadcast` is an interesing Plezi feature that allows us to tell all the _other_ connection to run a method. It is totally asynchroneos, so we don't wait for it to complete.
|
268
268
|
|
@@ -274,9 +274,9 @@ Let's start with the name - why the underscore at the beginning?
|
|
274
274
|
|
275
275
|
Plezi knows that sometimes we will want to create public methods that aren't available as a path - remember the `people` method, it was automatically recognized as an HTTP path...
|
276
276
|
|
277
|
-
Plezi allows us to 'exclude' some methods from this auto-recogntion. protected methods and methods starting with an underscore (\_)
|
277
|
+
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.
|
278
278
|
|
279
|
-
Since
|
279
|
+
Since I was too lzy to write the `protected` keyword, I just added an underscore at the begining of the name.
|
280
280
|
|
281
281
|
This will be our `_send_message` method:
|
282
282
|
|
@@ -288,7 +288,7 @@ end
|
|
288
288
|
|
289
289
|
Did you notice the difference between WebSocket responses and HTTP?
|
290
290
|
|
291
|
-
|
291
|
+
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.
|
292
292
|
|
293
293
|
|
294
294
|
####Telling people that we left the chatroom
|
@@ -322,7 +322,7 @@ If we ever write a real chatroom, our login process will look somewhat different
|
|
322
322
|
First, we will ensure the new connection has a nickname (the connection was made to '/nickname' rather then the root of our application '/'):
|
323
323
|
|
324
324
|
```ruby
|
325
|
-
def
|
325
|
+
def on_open
|
326
326
|
if params[:id].nil?
|
327
327
|
response << {event: :error, from: :system, at: Time.now, message: "Error: cannot connect without a nickname!"}.to_json
|
328
328
|
response.close
|
@@ -331,18 +331,26 @@ def on_connect
|
|
331
331
|
end
|
332
332
|
```
|
333
333
|
|
334
|
-
Easy
|
334
|
+
Easy?
|
335
|
+
|
336
|
+
There's an even easier and safer way to do this, which doesn't send an error message back, it looks like this:
|
337
|
+
|
338
|
+
```ruby
|
339
|
+
def pre_connect
|
340
|
+
return false if params[:id].nil?
|
341
|
+
true
|
342
|
+
end
|
343
|
+
```
|
335
344
|
|
336
|
-
|
345
|
+
Since Websocket connections start as an HTTP GET request, the pre-connect is called while still in 'HTTP mode', allowing us to use HTTP logic and refuse connections even before any websocket data can be sent by the 'client'. This is definitly the safer approach.
|
337
346
|
|
338
|
-
|
347
|
+
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:
|
339
348
|
|
340
349
|
```ruby
|
341
350
|
message = {from: '', at: Time.now}
|
342
|
-
|
343
|
-
if (list + ['admin', 'system', 'sys', 'administrator']).include? params[:id]
|
351
|
+
if params[:id].match /admin|admn|system/i
|
344
352
|
message[:event] = :error
|
345
|
-
message[:message] = "The nickname '#{params[:id]}' is
|
353
|
+
message[:message] = "The nickname '#{params[:id]}' is refused."
|
346
354
|
response << message.to_json
|
347
355
|
params[:id] = false
|
348
356
|
response.close
|
@@ -350,20 +358,11 @@ We will also add some reserved names to this list, to make sure nobody impersona
|
|
350
358
|
end
|
351
359
|
```
|
352
360
|
|
353
|
-
Hmm.. **collect**? what is the `collect` method? - well, this is a little bit of more Plezi magic that allows us to ask and collect information from all the _other_ active connections. This method returns an array of all the responses.
|
354
|
-
|
355
|
-
We will use `collect` to get an array of all the connected nicknames - we will write the `_ask_nickname` method in just a bit.
|
356
|
-
|
357
361
|
Then, if all is good, we will welcome the new connection to our chatroom. We will also tell the new guest who is already connected and broadcast their arrivale to everybody else...:
|
358
362
|
|
359
363
|
```ruby
|
360
364
|
message = {from: '', at: Time.now}
|
361
365
|
message[:event] = :chat
|
362
|
-
if list.empty?
|
363
|
-
message[:message] = "Welcome! You're the first one here."
|
364
|
-
else
|
365
|
-
message[:message] = "Welcome! #{list[0..-2].join(', ')} #{list[1] ? 'and' : ''} #{list.last} #{list[1] ? 'are' : 'is'} already here."
|
366
|
-
end
|
367
366
|
response << message.to_json
|
368
367
|
message[:message] = "#{params[:id]} joined the chatroom."
|
369
368
|
broadcast :_send_message, message.to_json
|
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.
|
4
|
+
version: 0.11.0
|
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
|
+
date: 2015-09-19 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: grhttp
|
@@ -16,14 +16,14 @@ dependencies:
|
|
16
16
|
requirements:
|
17
17
|
- - "~>"
|
18
18
|
- !ruby/object:Gem::Version
|
19
|
-
version: 0.0
|
19
|
+
version: 0.1.0
|
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.0
|
26
|
+
version: 0.1.0
|
27
27
|
- !ruby/object:Gem::Dependency
|
28
28
|
name: bundler
|
29
29
|
requirement: !ruby/object:Gem::Requirement
|
@@ -62,12 +62,18 @@ extensions: []
|
|
62
62
|
extra_rdoc_files: []
|
63
63
|
files:
|
64
64
|
- ".gitignore"
|
65
|
+
- ".yardopts"
|
65
66
|
- CHANGELOG.md
|
66
67
|
- Gemfile
|
67
68
|
- LICENSE.txt
|
68
69
|
- README.md
|
69
70
|
- Rakefile
|
70
71
|
- bin/plezi
|
72
|
+
- docs/async_helpers.md
|
73
|
+
- docs/http_helpers.md
|
74
|
+
- docs/logging.md
|
75
|
+
- docs/routes.md
|
76
|
+
- docs/websockets.md
|
71
77
|
- lib/plezi.rb
|
72
78
|
- lib/plezi/builders/app_builder.rb
|
73
79
|
- lib/plezi/builders/builder.rb
|
@@ -148,7 +154,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
148
154
|
version: '0'
|
149
155
|
requirements: []
|
150
156
|
rubyforge_project:
|
151
|
-
rubygems_version: 2.4.
|
157
|
+
rubygems_version: 2.4.5.1
|
152
158
|
signing_key:
|
153
159
|
specification_version: 4
|
154
160
|
summary: Plezi - the easy way to add Websockets, RESTful routing and HTTP streaming
|