rest-builder 0.9.0

Sign up to get free protection for your applications and to get access to all the features.
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
@@ -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