rest-builder 0.9.0
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/.gitignore +2 -0
- data/.gitmodules +6 -0
- data/.travis.yml +14 -0
- data/CHANGES.md +5 -0
- data/Gemfile +24 -0
- data/README.md +577 -0
- data/Rakefile +21 -0
- data/lib/rest-builder.rb +27 -0
- data/lib/rest-builder/builder.rb +164 -0
- data/lib/rest-builder/client.rb +282 -0
- data/lib/rest-builder/engine.rb +57 -0
- data/lib/rest-builder/engine/dry.rb +11 -0
- data/lib/rest-builder/engine/http-client.rb +46 -0
- data/lib/rest-builder/error.rb +4 -0
- data/lib/rest-builder/event_source.rb +137 -0
- data/lib/rest-builder/middleware.rb +147 -0
- data/lib/rest-builder/payload.rb +173 -0
- data/lib/rest-builder/promise.rb +35 -0
- data/lib/rest-builder/test.rb +26 -0
- data/lib/rest-builder/version.rb +4 -0
- data/rest-builder.gemspec +73 -0
- data/task/README.md +54 -0
- data/task/gemgem.rb +316 -0
- data/test/test_builder.rb +45 -0
- data/test/test_client.rb +212 -0
- data/test/test_event_source.rb +152 -0
- data/test/test_future.rb +21 -0
- data/test/test_httpclient.rb +118 -0
- data/test/test_payload.rb +205 -0
- metadata +129 -0
data/Rakefile
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
|
2
|
+
begin
|
3
|
+
require "#{dir = File.dirname(__FILE__)}/task/gemgem"
|
4
|
+
rescue LoadError
|
5
|
+
sh 'git submodule update --init --recursive'
|
6
|
+
exec Gem.ruby, '-S', $PROGRAM_NAME, *ARGV
|
7
|
+
end
|
8
|
+
|
9
|
+
$LOAD_PATH.unshift(File.expand_path("#{dir}/promise_pool/lib"))
|
10
|
+
|
11
|
+
Gemgem.init(dir) do |s|
|
12
|
+
require 'rest-builder/version'
|
13
|
+
s.name = 'rest-builder'
|
14
|
+
s.version = RestBuilder::VERSION
|
15
|
+
%w[promise_pool httpclient mime-types].each do |g|
|
16
|
+
s.add_runtime_dependency(g)
|
17
|
+
end
|
18
|
+
|
19
|
+
# exclude promise_pool
|
20
|
+
s.files.reject!{ |f| f.start_with?('promise_pool/') }
|
21
|
+
end
|
data/lib/rest-builder.rb
ADDED
@@ -0,0 +1,27 @@
|
|
1
|
+
|
2
|
+
require 'rest-builder/builder'
|
3
|
+
|
4
|
+
module RestBuilder
|
5
|
+
REQUEST_METHOD = 'REQUEST_METHOD'
|
6
|
+
REQUEST_PATH = 'REQUEST_PATH'
|
7
|
+
REQUEST_QUERY = 'REQUEST_QUERY'
|
8
|
+
REQUEST_PAYLOAD = 'REQUEST_PAYLOAD'
|
9
|
+
REQUEST_HEADERS = 'REQUEST_HEADERS'
|
10
|
+
REQUEST_URI = 'REQUEST_URI'
|
11
|
+
|
12
|
+
RESPONSE_BODY = 'RESPONSE_BODY'
|
13
|
+
RESPONSE_STATUS = 'RESPONSE_STATUS'
|
14
|
+
RESPONSE_HEADERS = 'RESPONSE_HEADERS'
|
15
|
+
RESPONSE_SOCKET = 'RESPONSE_SOCKET'
|
16
|
+
RESPONSE_KEY = 'RESPONSE_KEY'
|
17
|
+
|
18
|
+
DRY = 'core.dry'
|
19
|
+
FAIL = 'core.fail'
|
20
|
+
LOG = 'core.log'
|
21
|
+
CLIENT = 'core.client'
|
22
|
+
|
23
|
+
ASYNC = 'async.callback'
|
24
|
+
TIMER = 'async.timer'
|
25
|
+
PROMISE = 'async.promise'
|
26
|
+
HIJACK = 'async.hijack'
|
27
|
+
end
|
@@ -0,0 +1,164 @@
|
|
1
|
+
|
2
|
+
require 'thread'
|
3
|
+
require 'weakref'
|
4
|
+
|
5
|
+
require 'promise_pool'
|
6
|
+
|
7
|
+
require 'rest-builder/client'
|
8
|
+
require 'rest-builder/event_source'
|
9
|
+
require 'rest-builder/engine/http-client'
|
10
|
+
|
11
|
+
module RestBuilder
|
12
|
+
class Builder
|
13
|
+
singleton_class.module_eval do
|
14
|
+
attr_writer :default_engine
|
15
|
+
def default_engine
|
16
|
+
@default_engine ||= HttpClient
|
17
|
+
end
|
18
|
+
|
19
|
+
def client *attrs, &block
|
20
|
+
new(&block).to_client(*attrs)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def initialize &block
|
25
|
+
@engine = nil
|
26
|
+
@middles ||= []
|
27
|
+
instance_eval(&block) if block_given?
|
28
|
+
end
|
29
|
+
|
30
|
+
attr_reader :middles
|
31
|
+
attr_writer :default_engine
|
32
|
+
def default_engine
|
33
|
+
@default_engine ||= self.class.default_engine
|
34
|
+
end
|
35
|
+
|
36
|
+
def use middle, *args, &block
|
37
|
+
middles << [middle, args, block]
|
38
|
+
end
|
39
|
+
|
40
|
+
def run engine
|
41
|
+
@engine = engine
|
42
|
+
end
|
43
|
+
|
44
|
+
def members
|
45
|
+
middles.map{ |(middle, _, _)|
|
46
|
+
middle.members if middle.respond_to?(:members)
|
47
|
+
}.flatten.compact
|
48
|
+
end
|
49
|
+
|
50
|
+
def to_app engine=@engine || default_engine
|
51
|
+
# === foldr m.new app middles
|
52
|
+
middles.reverse.inject(engine.new){ |app, (middle, args, block)|
|
53
|
+
begin
|
54
|
+
middle.new(app, *partial_deep_copy(args), &block)
|
55
|
+
rescue ArgumentError => e
|
56
|
+
raise ArgumentError.new("#{middle}: #{e}")
|
57
|
+
end
|
58
|
+
}
|
59
|
+
end
|
60
|
+
|
61
|
+
def to_client *attrs
|
62
|
+
fields = (members + attrs + [:config_engine]).uniq
|
63
|
+
struct = build_struct(fields)
|
64
|
+
client = Class.new(struct)
|
65
|
+
client.const_set('Struct', struct)
|
66
|
+
class_methods = build_class_methods
|
67
|
+
client.const_set('ClassMethods', class_methods)
|
68
|
+
client.singleton_class.send(:include, class_methods)
|
69
|
+
client.send(:include, Client)
|
70
|
+
client.builder = self
|
71
|
+
client.pool_size = 0 # default to no pool
|
72
|
+
client.pool_idle_time = 60 # default to 60 seconds
|
73
|
+
client.event_source_class = EventSource
|
74
|
+
client.promises = []
|
75
|
+
client.mutex = Mutex.new
|
76
|
+
client
|
77
|
+
end
|
78
|
+
|
79
|
+
private
|
80
|
+
def partial_deep_copy obj
|
81
|
+
case obj
|
82
|
+
when Array; obj.map{ |o| partial_deep_copy(o) }
|
83
|
+
when Hash ; obj.inject({}){ |r, (k, v)| r[k] = partial_deep_copy(v); r }
|
84
|
+
else ; obj
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
def build_struct fields
|
89
|
+
if fields.empty?
|
90
|
+
Struct.new(nil)
|
91
|
+
else
|
92
|
+
Struct.new(*fields.uniq)
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
def build_class_methods
|
97
|
+
Module.new do
|
98
|
+
attr_accessor :builder, :event_source_class, :promises, :mutex
|
99
|
+
attr_reader :pool_size, :pool_idle_time
|
100
|
+
|
101
|
+
def inherited sub
|
102
|
+
sub.builder = builder
|
103
|
+
sub.pool_size = pool_size
|
104
|
+
sub.pool_idle_time = pool_idle_time
|
105
|
+
sub.event_source_class = event_source_class
|
106
|
+
sub.promises = []
|
107
|
+
sub.mutex = Mutex.new
|
108
|
+
end
|
109
|
+
|
110
|
+
def pool_size= size
|
111
|
+
@pool_size = size
|
112
|
+
thread_pool.max_size = size
|
113
|
+
end
|
114
|
+
|
115
|
+
def pool_idle_time= time
|
116
|
+
@pool_idle_time = time
|
117
|
+
thread_pool.idle_time = time
|
118
|
+
end
|
119
|
+
|
120
|
+
def thread_pool
|
121
|
+
@thread_pool ||=
|
122
|
+
PromisePool::ThreadPool.new(pool_size, pool_idle_time)
|
123
|
+
end
|
124
|
+
|
125
|
+
def defer
|
126
|
+
raise ArgumentError.new('no block given') unless block_given?
|
127
|
+
promise = PromisePool::Promise.new(thread_pool)
|
128
|
+
give_promise(WeakRef.new(promise))
|
129
|
+
promise.defer{ yield }.future
|
130
|
+
end
|
131
|
+
|
132
|
+
def give_promise weak_promise, ps=promises, m=mutex
|
133
|
+
m.synchronize do
|
134
|
+
ps << weak_promise
|
135
|
+
ps.keep_if(&:weakref_alive?)
|
136
|
+
end
|
137
|
+
end
|
138
|
+
|
139
|
+
# Shutdown the thread pool for this client and wait for all requests
|
140
|
+
def shutdown
|
141
|
+
thread_pool.shutdown
|
142
|
+
wait
|
143
|
+
end
|
144
|
+
|
145
|
+
# Wait for all the requests to be done for this client
|
146
|
+
def wait ps=promises, m=mutex
|
147
|
+
return self if ps.empty?
|
148
|
+
current_promises = nil
|
149
|
+
m.synchronize do
|
150
|
+
current_promises = ps.dup
|
151
|
+
ps.clear
|
152
|
+
end
|
153
|
+
current_promises.each do |p|
|
154
|
+
begin
|
155
|
+
p.weakref_alive? && p.wait
|
156
|
+
rescue WeakRef::RefError # it's gc'ed after we think it's alive
|
157
|
+
end
|
158
|
+
end
|
159
|
+
wait(ps, m)
|
160
|
+
end
|
161
|
+
end
|
162
|
+
end
|
163
|
+
end
|
164
|
+
end
|
@@ -0,0 +1,282 @@
|
|
1
|
+
|
2
|
+
require 'thread'
|
3
|
+
require 'weakref'
|
4
|
+
|
5
|
+
require 'rest-builder/promise'
|
6
|
+
require 'rest-builder/middleware'
|
7
|
+
require 'rest-builder/engine/dry'
|
8
|
+
|
9
|
+
module RestBuilder
|
10
|
+
module Client
|
11
|
+
Unserializable = [Proc, Method, IO]
|
12
|
+
|
13
|
+
def self.included mod
|
14
|
+
# honor default attributes
|
15
|
+
src = mod.members.map{ |name|
|
16
|
+
<<-RUBY
|
17
|
+
def #{name}
|
18
|
+
if (r = super).nil?
|
19
|
+
self.#{name} = default_#{name}
|
20
|
+
else
|
21
|
+
r
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def default_#{name} a=app
|
26
|
+
if self.class.respond_to?("default_#{name}")
|
27
|
+
self.class.default_#{name} # old class default style
|
28
|
+
elsif a.respond_to?(:#{name})
|
29
|
+
a.#{name}({}) # middleware instance value
|
30
|
+
elsif a.respond_to?(:app)
|
31
|
+
default_#{name}(a.app) # walk into next app
|
32
|
+
else
|
33
|
+
nil
|
34
|
+
end
|
35
|
+
end
|
36
|
+
private :default_#{name}
|
37
|
+
RUBY
|
38
|
+
}
|
39
|
+
accessor = Module.new
|
40
|
+
accessor.module_eval(src.join("\n"), __FILE__, __LINE__)
|
41
|
+
mod.const_set('Accessor', accessor)
|
42
|
+
mod.send(:include, accessor)
|
43
|
+
end
|
44
|
+
|
45
|
+
attr_reader :app, :dry, :promises
|
46
|
+
attr_accessor :error_callback
|
47
|
+
def initialize o={}
|
48
|
+
@app ||= self.class.builder.to_app # lighten! would reinitialize anyway
|
49
|
+
@dry ||= self.class.builder.to_app(Dry)
|
50
|
+
@promises = [] # don't record any promises in lighten!
|
51
|
+
@mutex = nil # for locking promises, lazily initialized
|
52
|
+
# for serialization
|
53
|
+
@error_callback = nil
|
54
|
+
o.each{ |key, value| send("#{key}=", value) if respond_to?("#{key}=") }
|
55
|
+
end
|
56
|
+
|
57
|
+
def attributes
|
58
|
+
Hash[each_pair.map{ |k, v| [k, send(k)] }]
|
59
|
+
end
|
60
|
+
|
61
|
+
def inspect
|
62
|
+
fields = if size > 0
|
63
|
+
attributes.map{ |k, v|
|
64
|
+
"#{k}=#{v.inspect.sub(/(?<=.{12}).{4,}/, '...')}"
|
65
|
+
}.join(', ')
|
66
|
+
else
|
67
|
+
''
|
68
|
+
end
|
69
|
+
"#<struct #{self.class.name}#{fields}>"
|
70
|
+
end
|
71
|
+
|
72
|
+
def lighten! o={}
|
73
|
+
attributes.each{ |k, v| vv = case v;
|
74
|
+
when Hash; lighten_hash(v)
|
75
|
+
when Array; lighten_array(v)
|
76
|
+
when *Unserializable; nil
|
77
|
+
else v
|
78
|
+
end
|
79
|
+
send("#{k}=", vv)}
|
80
|
+
initialize(o)
|
81
|
+
@app, @dry = lighten_app(app), lighten_app(dry)
|
82
|
+
self
|
83
|
+
end
|
84
|
+
|
85
|
+
def lighten o={}
|
86
|
+
dup.lighten!(o)
|
87
|
+
end
|
88
|
+
|
89
|
+
def wait
|
90
|
+
self.class.wait(promises, mutex)
|
91
|
+
self
|
92
|
+
end
|
93
|
+
|
94
|
+
def url path, query={}, opts={}
|
95
|
+
dry.call(build_env({
|
96
|
+
REQUEST_PATH => path,
|
97
|
+
REQUEST_QUERY => query,
|
98
|
+
DRY => true}.merge(opts)), &Middleware.method(:request_uri))
|
99
|
+
end
|
100
|
+
|
101
|
+
def get path, query={}, opts={}, &cb
|
102
|
+
request(
|
103
|
+
{REQUEST_METHOD => :get ,
|
104
|
+
REQUEST_PATH => path ,
|
105
|
+
REQUEST_QUERY => query }.merge(opts), &cb)
|
106
|
+
end
|
107
|
+
|
108
|
+
def delete path, query={}, opts={}, &cb
|
109
|
+
request(
|
110
|
+
{REQUEST_METHOD => :delete,
|
111
|
+
REQUEST_PATH => path ,
|
112
|
+
REQUEST_QUERY => query }.merge(opts), &cb)
|
113
|
+
end
|
114
|
+
|
115
|
+
def head path, query={}, opts={}, &cb
|
116
|
+
request(
|
117
|
+
{REQUEST_METHOD => :head ,
|
118
|
+
REQUEST_PATH => path ,
|
119
|
+
REQUEST_QUERY => query ,
|
120
|
+
RESPONSE_KEY => RESPONSE_HEADERS}.merge(opts), &cb)
|
121
|
+
end
|
122
|
+
|
123
|
+
def options path, query={}, opts={}, &cb
|
124
|
+
request(
|
125
|
+
{REQUEST_METHOD => :options,
|
126
|
+
REQUEST_PATH => path ,
|
127
|
+
REQUEST_QUERY => query ,
|
128
|
+
RESPONSE_KEY => RESPONSE_HEADERS}.merge(opts), &cb)
|
129
|
+
end
|
130
|
+
|
131
|
+
def post path, payload={}, query={}, opts={}, &cb
|
132
|
+
request(
|
133
|
+
{REQUEST_METHOD => :post ,
|
134
|
+
REQUEST_PATH => path ,
|
135
|
+
REQUEST_QUERY => query ,
|
136
|
+
REQUEST_PAYLOAD => payload}.merge(opts), &cb)
|
137
|
+
end
|
138
|
+
|
139
|
+
def put path, payload={}, query={}, opts={}, &cb
|
140
|
+
request(
|
141
|
+
{REQUEST_METHOD => :put ,
|
142
|
+
REQUEST_PATH => path ,
|
143
|
+
REQUEST_QUERY => query ,
|
144
|
+
REQUEST_PAYLOAD => payload}.merge(opts), &cb)
|
145
|
+
end
|
146
|
+
|
147
|
+
def patch path, payload={}, query={}, opts={}, &cb
|
148
|
+
request(
|
149
|
+
{REQUEST_METHOD => :patch ,
|
150
|
+
REQUEST_PATH => path ,
|
151
|
+
REQUEST_QUERY => query ,
|
152
|
+
REQUEST_PAYLOAD => payload}.merge(opts), &cb)
|
153
|
+
end
|
154
|
+
|
155
|
+
def event_source path, query={}, opts={}
|
156
|
+
self.class.event_source_class.new(self, path, query, opts)
|
157
|
+
end
|
158
|
+
|
159
|
+
def request env, a=app
|
160
|
+
if block_given?
|
161
|
+
request_full(env, a){ |response| yield(response[response_key(env)]) }
|
162
|
+
else
|
163
|
+
request_full(env, a)[response_key(env)]
|
164
|
+
end
|
165
|
+
end
|
166
|
+
|
167
|
+
def request_full env, a=app, &k
|
168
|
+
response = a.call(build_env({ASYNC => !!k}.merge(env))) do |res|
|
169
|
+
(k || :itself.to_proc).call(request_complete(res))
|
170
|
+
end
|
171
|
+
|
172
|
+
give_promise(response)
|
173
|
+
|
174
|
+
if block_given?
|
175
|
+
self
|
176
|
+
else
|
177
|
+
response
|
178
|
+
end
|
179
|
+
end
|
180
|
+
|
181
|
+
def give_promise response
|
182
|
+
# under ASYNC callback, response might not be a response hash
|
183
|
+
# in that case (maybe in a user created engine), Client#wait
|
184
|
+
# won't work because we have no way to track the promise.
|
185
|
+
if response.kind_of?(Hash) && response[PROMISE]
|
186
|
+
weak_promise = WeakRef.new(response[PROMISE])
|
187
|
+
self.class.give_promise(weak_promise)
|
188
|
+
self.class.give_promise(weak_promise, promises, mutex)
|
189
|
+
end
|
190
|
+
|
191
|
+
response
|
192
|
+
end
|
193
|
+
|
194
|
+
def build_env env={}
|
195
|
+
default_env.merge(
|
196
|
+
Middleware.string_keys(attributes).merge(Middleware.string_keys(env)))
|
197
|
+
end
|
198
|
+
|
199
|
+
def default_env
|
200
|
+
{REQUEST_METHOD => :get,
|
201
|
+
REQUEST_PATH => '/' ,
|
202
|
+
REQUEST_QUERY => {} ,
|
203
|
+
REQUEST_PAYLOAD => {} ,
|
204
|
+
REQUEST_HEADERS => {} ,
|
205
|
+
FAIL => [] ,
|
206
|
+
LOG => [] ,
|
207
|
+
CLIENT => self}
|
208
|
+
end
|
209
|
+
# ------------------------ instance ---------------------
|
210
|
+
|
211
|
+
|
212
|
+
|
213
|
+
private
|
214
|
+
def request_complete res
|
215
|
+
if err = res[FAIL].find{ |f| f.kind_of?(Exception) }
|
216
|
+
Promise.set_backtrace(err) unless err.backtrace
|
217
|
+
error_callback.call(err) if error_callback
|
218
|
+
if res[ASYNC]
|
219
|
+
res.merge(response_key(res) => err)
|
220
|
+
elsif res[PROMISE] # promise would handle the exception for us
|
221
|
+
err
|
222
|
+
else
|
223
|
+
raise err
|
224
|
+
end
|
225
|
+
else
|
226
|
+
res
|
227
|
+
end
|
228
|
+
end
|
229
|
+
|
230
|
+
def mutex
|
231
|
+
@mutex ||= Mutex.new
|
232
|
+
end
|
233
|
+
|
234
|
+
def response_key opts
|
235
|
+
opts[RESPONSE_KEY] ||
|
236
|
+
if opts[HIJACK] then RESPONSE_SOCKET else RESPONSE_BODY end
|
237
|
+
end
|
238
|
+
|
239
|
+
def lighten_hash hash
|
240
|
+
Hash[hash.map{ |(key, value)|
|
241
|
+
case value
|
242
|
+
when Hash; lighten_hash(value)
|
243
|
+
when Array; lighten_array(value)
|
244
|
+
when *Unserializable; [key, nil]
|
245
|
+
else [key, value]
|
246
|
+
end
|
247
|
+
}]
|
248
|
+
end
|
249
|
+
|
250
|
+
def lighten_array array
|
251
|
+
array.map{ |value|
|
252
|
+
case value
|
253
|
+
when Hash; lighten_hash(value)
|
254
|
+
when Array; lighten_array(value)
|
255
|
+
when *Unserializable; nil
|
256
|
+
else value
|
257
|
+
end
|
258
|
+
}.compact
|
259
|
+
end
|
260
|
+
|
261
|
+
def lighten_app app
|
262
|
+
members = if app.class.respond_to?(:members)
|
263
|
+
app.class.members.map{ |key|
|
264
|
+
case value = app.send(key, {})
|
265
|
+
when Hash; lighten_hash(value)
|
266
|
+
when Array; lighten_array(value)
|
267
|
+
when *Unserializable; nil
|
268
|
+
else value
|
269
|
+
end
|
270
|
+
}
|
271
|
+
else
|
272
|
+
[]
|
273
|
+
end
|
274
|
+
|
275
|
+
if app.respond_to?(:app) && app.app
|
276
|
+
app.class.new(lighten_app(app.app), *members)
|
277
|
+
else
|
278
|
+
app.class.new(*members)
|
279
|
+
end
|
280
|
+
end
|
281
|
+
end
|
282
|
+
end
|