hyper-resource 1.0.0.lap34
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/LICENSE +21 -0
- data/README.md +109 -0
- data/hyper-resource.gemspec +30 -0
- data/lib/hyper-resource.rb +20 -0
- data/lib/hyper_record.rb +21 -0
- data/lib/hyper_record/class_methods.rb +413 -0
- data/lib/hyper_record/client_instance_methods.rb +300 -0
- data/lib/hyper_record/collection.rb +39 -0
- data/lib/hyper_record/dummy_value.rb +23 -0
- data/lib/hyper_record/server_class_methods.rb +19 -0
- data/lib/hyperloop/resource/client_drivers.rb +109 -0
- data/lib/hyperloop/resource/config.rb +15 -0
- data/lib/hyperloop/resource/http.rb +310 -0
- data/lib/hyperloop/resource/pub_sub.rb +146 -0
- data/lib/hyperloop/resource/rails/controller_templates/methods_controller.rb +63 -0
- data/lib/hyperloop/resource/rails/controller_templates/properties_controller.rb +21 -0
- data/lib/hyperloop/resource/rails/controller_templates/relations_controller.rb +123 -0
- data/lib/hyperloop/resource/rails/controller_templates/scopes_controller.rb +37 -0
- data/lib/hyperloop/resource/security_guards.rb +37 -0
- data/lib/hyperloop/resource/version.rb +5 -0
- data/lib/pusher/source/pusher.js +4183 -0
- metadata +246 -0
@@ -0,0 +1,15 @@
|
|
1
|
+
if RUBY_ENGINE == 'opal'
|
2
|
+
module Hyperloop
|
3
|
+
def self.current_user_id
|
4
|
+
Hyperloop::Resource::ClientDrivers.opts[:current_user_id]
|
5
|
+
end
|
6
|
+
end
|
7
|
+
else
|
8
|
+
module Hyperloop
|
9
|
+
define_setting(:pusher, {})
|
10
|
+
define_setting(:redis_instance, nil)
|
11
|
+
define_setting(:resource_api_base_path, '/api')
|
12
|
+
define_setting(:resource_transport, :pusher)
|
13
|
+
define_setting(:valid_record_class_params, [])
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,310 @@
|
|
1
|
+
module Hyperloop
|
2
|
+
module Resource
|
3
|
+
# {HTTP} is used to perform a `XMLHttpRequest` in ruby. It is a simple wrapper
|
4
|
+
# around `XMLHttpRequest`
|
5
|
+
#
|
6
|
+
# # Making requests
|
7
|
+
#
|
8
|
+
# To create a simple request, {HTTP} exposes class level methods to specify
|
9
|
+
# the HTTP action you wish to perform. Each action accepts the url for the
|
10
|
+
# request, as well as optional arguments passed as a hash:
|
11
|
+
#
|
12
|
+
# HTTP.get("/users/1.json")
|
13
|
+
# HTTP.post("/users", payload: data)
|
14
|
+
#
|
15
|
+
# The supported `HTTP` actions are:
|
16
|
+
#
|
17
|
+
# * {HTTP.get}
|
18
|
+
# * {HTTP.post}
|
19
|
+
# * {HTTP.put}
|
20
|
+
# * {HTTP.delete}
|
21
|
+
# * {HTTP.patch}
|
22
|
+
# * {HTTP.head}
|
23
|
+
#
|
24
|
+
# # Handling responses
|
25
|
+
#
|
26
|
+
# Responses can be handled using either a simple block callback, or using a
|
27
|
+
# {Promise} returned by the request.
|
28
|
+
#
|
29
|
+
# ## Using a block
|
30
|
+
#
|
31
|
+
# All HTTP action methods accept a block which can be used as a simple
|
32
|
+
# handler for the request. The block will be called for both successful as well
|
33
|
+
# as unsuccessful requests.
|
34
|
+
#
|
35
|
+
# HTTP.get("/users/1") do |request|
|
36
|
+
# puts "the request has completed!"
|
37
|
+
# end
|
38
|
+
#
|
39
|
+
# This `request` object will simply be the instance of the {HTTP} class which
|
40
|
+
# wraps the native `XMLHttpRequest`. {HTTP#ok?} can be used to quickly determine
|
41
|
+
# if the request was successful.
|
42
|
+
#
|
43
|
+
# HTTP.get("/users/1") do |request|
|
44
|
+
# if request.ok?
|
45
|
+
# puts "request was success"
|
46
|
+
# else
|
47
|
+
# puts "something went wrong with request"
|
48
|
+
# end
|
49
|
+
# end
|
50
|
+
#
|
51
|
+
# The {HTTP} instance will always be the only object passed to the block.
|
52
|
+
#
|
53
|
+
# ## Using a Promise
|
54
|
+
#
|
55
|
+
# If no block is given to one of the action methods, then a {Promise} is
|
56
|
+
# returned instead. See the standard library for more information on Promises.
|
57
|
+
#
|
58
|
+
# HTTP.get("/users/1").then do |req|
|
59
|
+
# puts "response ok!"
|
60
|
+
# end.fail do |req|
|
61
|
+
# puts "response was not ok"
|
62
|
+
# end
|
63
|
+
#
|
64
|
+
# When using a {Promise}, both success and failure handlers will be passed the
|
65
|
+
# {HTTP} instance.
|
66
|
+
#
|
67
|
+
# # Accessing Response Data
|
68
|
+
#
|
69
|
+
# All data returned from an HTTP request can be accessed via the {HTTP} object
|
70
|
+
# passed into the block or promise handlers.
|
71
|
+
#
|
72
|
+
# - {#ok?} - returns `true` or `false`, if request was a success (or not).
|
73
|
+
# - {#body} - returns the raw text response of the request
|
74
|
+
# - {#status_code} - returns the raw {HTTP} status code as integer
|
75
|
+
# - {#json} - tries to convert the body response into a JSON object
|
76
|
+
class HTTP
|
77
|
+
# All valid {HTTP} action methods this class accepts.
|
78
|
+
#
|
79
|
+
# @see HTTP.get
|
80
|
+
# @see HTTP.post
|
81
|
+
# @see HTTP.put
|
82
|
+
# @see HTTP.delete
|
83
|
+
# @see HTTP.patch
|
84
|
+
# @see HTTP.head
|
85
|
+
ACTIONS = %w[get post put delete patch head]
|
86
|
+
|
87
|
+
# @!method self.get(url, options = {}, &block)
|
88
|
+
#
|
89
|
+
# Create a {HTTP} `get` request.
|
90
|
+
#
|
91
|
+
# @example
|
92
|
+
# HTTP.get("/foo") do |req|
|
93
|
+
# puts "got data: #{req.data}"
|
94
|
+
# end
|
95
|
+
#
|
96
|
+
# @param url [String] url for request
|
97
|
+
# @param options [Hash] any request options
|
98
|
+
# @yield [self] optional block to handle response
|
99
|
+
# @return [Promise, nil] optionally returns a promise
|
100
|
+
|
101
|
+
# @!method self.post(url, options = {}, &block)
|
102
|
+
#
|
103
|
+
# Create a {HTTP} `post` request. Post data can be supplied using the
|
104
|
+
# `payload` options. Usually this will be a hash which will get serialized
|
105
|
+
# into a native javascript object.
|
106
|
+
#
|
107
|
+
# @example
|
108
|
+
# HTTP.post("/bar", payload: data) do |req|
|
109
|
+
# puts "got response"
|
110
|
+
# end
|
111
|
+
#
|
112
|
+
# @param url [String] url for request
|
113
|
+
# @param options [Hash] optional request options
|
114
|
+
# @yield [self] optional block to yield for response
|
115
|
+
# @return [Promise, nil] returns a {Promise} unless block given
|
116
|
+
|
117
|
+
# @!method self.put(url, options = {}, &block)
|
118
|
+
|
119
|
+
# @!method self.delete(url, options = {}, &block)
|
120
|
+
|
121
|
+
# @!method self.patch(url, options = {}, &block)
|
122
|
+
|
123
|
+
# @!method self.head(url, options = {}, &block)
|
124
|
+
|
125
|
+
ACTIONS.each do |action|
|
126
|
+
define_singleton_method(action) do |url, options = {}, &block|
|
127
|
+
new.send(action, url, options, block)
|
128
|
+
end
|
129
|
+
|
130
|
+
define_method(action) do |url, options = {}, &block|
|
131
|
+
send(action, url, options, block)
|
132
|
+
end
|
133
|
+
end
|
134
|
+
|
135
|
+
attr_reader :body, :error_message, :method, :status_code, :url, :xhr
|
136
|
+
|
137
|
+
def initialize
|
138
|
+
@ok = true
|
139
|
+
end
|
140
|
+
|
141
|
+
def self.active?
|
142
|
+
jquery_active_requests = 0
|
143
|
+
%x{
|
144
|
+
if (typeof jQuery !== "undefined" && typeof jQuery.active !== "undefined" && jQuery.active !== null) {
|
145
|
+
jquery_active_requests = jQuery.active;
|
146
|
+
}
|
147
|
+
}
|
148
|
+
(jquery_active_requests + @active_requests) > 0
|
149
|
+
end
|
150
|
+
|
151
|
+
def self.active_requests
|
152
|
+
@active_requests ||= 0
|
153
|
+
@active_requests
|
154
|
+
end
|
155
|
+
|
156
|
+
def self.incr_active_requests
|
157
|
+
@active_requests ||= 0
|
158
|
+
@active_requests += 1
|
159
|
+
end
|
160
|
+
|
161
|
+
def self.decr_active_requests
|
162
|
+
@active_requests ||= 0
|
163
|
+
@active_requests -= 1
|
164
|
+
if @active_requests < 0
|
165
|
+
`console.log("Ooops, Hyperloop::HTTP active_requests out of sync!")`
|
166
|
+
@active_requests = 0
|
167
|
+
end
|
168
|
+
end
|
169
|
+
|
170
|
+
def send(method, url, options, block)
|
171
|
+
@method = method
|
172
|
+
@url = url
|
173
|
+
@payload = options.delete :payload
|
174
|
+
@handler = block
|
175
|
+
%x{
|
176
|
+
var payload_to_send = null;
|
177
|
+
var content_type = null;
|
178
|
+
if (typeof(this.payload) === 'string') {
|
179
|
+
payload_to_send = this.payload;
|
180
|
+
}
|
181
|
+
else if (this.payload != nil) {
|
182
|
+
payload_to_send = this.payload.$to_json();
|
183
|
+
content_type = 'application/json';
|
184
|
+
}
|
185
|
+
|
186
|
+
var xhr = new XMLHttpRequest();
|
187
|
+
|
188
|
+
xhr.onreadystatechange = function() {
|
189
|
+
if(xhr.readyState === XMLHttpRequest.DONE) {
|
190
|
+
self.$class().$decr_active_requests();
|
191
|
+
if ((xhr.status >= 200 && xhr.status < 300) || xhr.status == 304) {
|
192
|
+
return #{succeed(`xhr.responseText`, `xhr.status`, `xhr`)};
|
193
|
+
} else {
|
194
|
+
return #{fail(`xhr`, `xhr.status`, `xhr.statusText`)};
|
195
|
+
}
|
196
|
+
}
|
197
|
+
}
|
198
|
+
xhr.open(this.method.toUpperCase(), this.url);
|
199
|
+
if (payload_to_send !== null && content_type !== null) {
|
200
|
+
xhr.setRequestHeader("Content-Type", content_type);
|
201
|
+
}
|
202
|
+
if (options["$has_key?"]("headers")) {
|
203
|
+
var headers = options['$[]']("headers");
|
204
|
+
var keys = headers.$keys();
|
205
|
+
var keys_length = keys.length;
|
206
|
+
for (var i=0; i < keys_length; i++) {
|
207
|
+
xhr.setRequestHeader( keys[i], headers['$[]'](keys[i]) );
|
208
|
+
}
|
209
|
+
}
|
210
|
+
if (payload_to_send !== null) {
|
211
|
+
self.$class().$incr_active_requests();
|
212
|
+
xhr.send(payload_to_send);
|
213
|
+
} else {
|
214
|
+
self.$class().$incr_active_requests();
|
215
|
+
xhr.send();
|
216
|
+
}
|
217
|
+
}
|
218
|
+
|
219
|
+
@handler ? self : promise
|
220
|
+
end
|
221
|
+
|
222
|
+
# Parses the http response body through json. If the response is not
|
223
|
+
# valid JSON then an error will very likely be thrown.
|
224
|
+
#
|
225
|
+
# @example Getting JSON content
|
226
|
+
# HTTP.get("api.json") do |response|
|
227
|
+
# puts response.json
|
228
|
+
# end
|
229
|
+
#
|
230
|
+
# # => {"key" => 1, "bar" => 2, ... }
|
231
|
+
#
|
232
|
+
# @return [Hash, Array] returns the parsed json
|
233
|
+
def json
|
234
|
+
@json ||= JSON.parse(@body)
|
235
|
+
end
|
236
|
+
|
237
|
+
# Returns true if the request succeeded, false otherwise.
|
238
|
+
#
|
239
|
+
# @example
|
240
|
+
# HTTP.get("/some/url") do |response|
|
241
|
+
# if response.ok?
|
242
|
+
# alert "Yay!"
|
243
|
+
# else
|
244
|
+
# alert "Aww :("
|
245
|
+
# end
|
246
|
+
#
|
247
|
+
# @return [true, false] true if request was successful
|
248
|
+
def ok?
|
249
|
+
@ok
|
250
|
+
end
|
251
|
+
|
252
|
+
# Returns the value of the specified response header.
|
253
|
+
#
|
254
|
+
# @param key [String] name of the header to get
|
255
|
+
# @return [String] value of the header
|
256
|
+
# @return [nil] if the header +key+ was not in the response
|
257
|
+
def get_header(key)
|
258
|
+
%x{
|
259
|
+
var value = #@xhr.getResponseHeader(#{key});
|
260
|
+
return (value === null) ? nil : value;
|
261
|
+
}
|
262
|
+
end
|
263
|
+
|
264
|
+
def inspect
|
265
|
+
"#<HTTP @url=#{@url} @method=#{@method}>"
|
266
|
+
end
|
267
|
+
|
268
|
+
private
|
269
|
+
|
270
|
+
def promise
|
271
|
+
return @promise if @promise
|
272
|
+
|
273
|
+
@promise = Promise.new.tap { |promise|
|
274
|
+
@handler = proc { |res|
|
275
|
+
if res.ok?
|
276
|
+
promise.resolve res
|
277
|
+
else
|
278
|
+
promise.reject res
|
279
|
+
end
|
280
|
+
}
|
281
|
+
}
|
282
|
+
end
|
283
|
+
|
284
|
+
def succeed(data, status, xhr)
|
285
|
+
%x{
|
286
|
+
#@body = data;
|
287
|
+
#@xhr = xhr;
|
288
|
+
#@status_code = xhr.status;
|
289
|
+
|
290
|
+
if (typeof(data) === 'object') {
|
291
|
+
#@json = #{ JSON.from_object `data` };
|
292
|
+
}
|
293
|
+
}
|
294
|
+
|
295
|
+
@handler.call self if @handler
|
296
|
+
end
|
297
|
+
|
298
|
+
def fail(xhr, status, error)
|
299
|
+
%x{
|
300
|
+
#@body = xhr.responseText;
|
301
|
+
#@xhr = xhr;
|
302
|
+
#@status_code = xhr.status;
|
303
|
+
}
|
304
|
+
|
305
|
+
@ok = false
|
306
|
+
@handler.call self if @handler
|
307
|
+
end
|
308
|
+
end
|
309
|
+
end
|
310
|
+
end
|
@@ -0,0 +1,146 @@
|
|
1
|
+
module Hyperloop
|
2
|
+
module Resource
|
3
|
+
module PubSub
|
4
|
+
def self.included(base)
|
5
|
+
base.extend(Hyperloop::Resource::PubSub::ClassMethods)
|
6
|
+
end
|
7
|
+
|
8
|
+
module ClassMethods
|
9
|
+
def _pusher_client
|
10
|
+
@pusher_client ||= Pusher::Client.new(
|
11
|
+
app_id: Hyperloop.pusher[:app_id],
|
12
|
+
key: Hyperloop.pusher[:key],
|
13
|
+
secret: Hyperloop.pusher[:secret],
|
14
|
+
cluster: Hyperloop.pusher[:cluster]
|
15
|
+
)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
def publish_relation(base_record, relation_name, record = nil)
|
20
|
+
subscribers = Hyperloop.redis_instance.hgetall("HRPS__#{base_record.class}__#{base_record.id}__#{relation_name}")
|
21
|
+
time_now = Time.now.to_f
|
22
|
+
scrub_time = time_now - 24.hours.to_f
|
23
|
+
message = {
|
24
|
+
record_type: base_record.class.to_s,
|
25
|
+
id: base_record.id,
|
26
|
+
updated_at: base_record.updated_at,
|
27
|
+
relation: relation_name
|
28
|
+
}
|
29
|
+
if record
|
30
|
+
message[:cause] = {}
|
31
|
+
message[:cause][:record_type] = record.class.to_s
|
32
|
+
message[:cause][:id] = record.id
|
33
|
+
message[:cause][:updated_at] = record.updated_at
|
34
|
+
end
|
35
|
+
subscribers.each do |session_id, last_requested|
|
36
|
+
if last_requested.to_f < scrub_time
|
37
|
+
Hyperloop.redis_instance.hdel("HRPS__#{base_record.class}__#{base_record.id}__#{relation_name}", session_id)
|
38
|
+
next
|
39
|
+
end
|
40
|
+
if Hyperloop.resource_transport == :pusher
|
41
|
+
self.class._pusher_client.trigger("hyper-record-update-channel-#{session_id}", 'update', message)
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
def publish_record(record)
|
47
|
+
subscribers = Hyperloop.redis_instance.hgetall("HRPS__#{record.class}__#{record.id}")
|
48
|
+
time_now = Time.now.to_f
|
49
|
+
scrub_time = time_now - 24.hours.to_f
|
50
|
+
|
51
|
+
message = {
|
52
|
+
record_type: record.class.to_s,
|
53
|
+
id: record.id,
|
54
|
+
updated_at: record.updated_at
|
55
|
+
}
|
56
|
+
message[:destroyed] = true if record.destroyed?
|
57
|
+
|
58
|
+
subscribers.each_slice(50) do |slice|
|
59
|
+
channel_array= []
|
60
|
+
slice.each do |session_id, last_requested|
|
61
|
+
if last_requested.to_f < scrub_time
|
62
|
+
Hyperloop.redis_instance.hdel("HRPS__#{record.class}__#{record.id}", session_id)
|
63
|
+
next
|
64
|
+
end
|
65
|
+
channel_array << "hyper-record-update-channel-#{session_id}"
|
66
|
+
end
|
67
|
+
if Hyperloop.resource_transport == :pusher && channel_array.size > 0
|
68
|
+
self.class._pusher_client.trigger(channel_array, 'update', message)
|
69
|
+
end
|
70
|
+
end
|
71
|
+
Hyperloop.redis_instance.del("HRPS__#{record.class}__#{record.id}") if record.destroyed?
|
72
|
+
end
|
73
|
+
|
74
|
+
def publish_scope(record_class, scope_name)
|
75
|
+
subscribers = Hyperloop.redis_instance.hgetall("HRPS__#{record_class}__scope__#{scope_name}")
|
76
|
+
time_now = Time.now.to_f
|
77
|
+
scrub_time = time_now - 24.hours.to_f
|
78
|
+
subscribers.each do |session_id, last_requested|
|
79
|
+
if last_requested.to_f < scrub_time
|
80
|
+
Hyperloop.redis_instance.hdel("HRPS__#{record_class}__scope__#{scope_name}", session_id)
|
81
|
+
next
|
82
|
+
end
|
83
|
+
message = {
|
84
|
+
record_type: record_class.to_s,
|
85
|
+
scope: scope_name
|
86
|
+
}
|
87
|
+
if Hyperloop.resource_transport == :pusher
|
88
|
+
self.class._pusher_client.trigger("hyper-record-update-channel-#{session_id}", 'update', message)
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
def subscribe_relation(relation, base_record = nil, relation_name = nil)
|
94
|
+
return unless session.id
|
95
|
+
time_now = Time.now.to_f.to_s
|
96
|
+
session_id = session.id.to_s
|
97
|
+
Hyperloop.redis_instance.pipelined do
|
98
|
+
if relation.is_a?(Enumerable)
|
99
|
+
# has_many
|
100
|
+
relation.each do |record|
|
101
|
+
Hyperloop.redis_instance.hset("HRPS__#{record.class}__#{record.id}", session_id, time_now)
|
102
|
+
end
|
103
|
+
elsif !relation.nil?
|
104
|
+
# has_one, belongs_to
|
105
|
+
Hyperloop.redis_instance.hset("HRPS__#{relation.class}__#{relation.id}", session_id, time_now)
|
106
|
+
end
|
107
|
+
Hyperloop.redis_instance.hset("HRPS__#{base_record.class}__#{base_record.id}__#{relation_name}", session_id, time_now) if base_record && relation_name
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
def subscribe_record(record)
|
112
|
+
return unless session.id
|
113
|
+
Hyperloop.redis_instance.hset "HRPS__#{record.class}__#{record.id}", session.id.to_s, Time.now.to_f.to_s
|
114
|
+
end
|
115
|
+
|
116
|
+
def subscribe_scope(collection, record_class = nil, scope_name = nil)
|
117
|
+
return unless session.id
|
118
|
+
time_now = Time.now.to_f.to_s
|
119
|
+
session_id = session.id.to_s
|
120
|
+
Hyperloop.redis_instance.pipelined do
|
121
|
+
if collection.is_a?(Enumerable)
|
122
|
+
collection.each do |record|
|
123
|
+
Hyperloop.redis_instance.hset("HRPS__#{record.class}__#{record.id}", session_id, time_now)
|
124
|
+
end
|
125
|
+
end
|
126
|
+
Hyperloop.redis_instance.hset("HRPS__#{record_class}__scope__#{scope_name}", session_id, time_now) if record_class && scope_name
|
127
|
+
end
|
128
|
+
end
|
129
|
+
|
130
|
+
def pub_sub_relation(relation, base_record, relation_name, causing_record = nil)
|
131
|
+
subscribe_relation(relation, base_record, relation_name)
|
132
|
+
publish_relation(base_record, relation_name, causing_record)
|
133
|
+
end
|
134
|
+
|
135
|
+
def pub_sub_record(record)
|
136
|
+
subscribe_record(record)
|
137
|
+
publish_record(record)
|
138
|
+
end
|
139
|
+
|
140
|
+
def pub_sub_scope(collection, record_class, scope_name)
|
141
|
+
subscribe_scope(collection, record_class, scope_name)
|
142
|
+
publish_scope(record_class, scope_name)
|
143
|
+
end
|
144
|
+
end
|
145
|
+
end
|
146
|
+
end
|