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.
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