rest-builder 0.9.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,57 @@
1
+
2
+ require 'rest-builder/promise'
3
+ require 'rest-builder/payload'
4
+ require 'rest-builder/middleware'
5
+
6
+ module RestBuilder
7
+ class Engine
8
+ def self.members; [:config_engine]; end
9
+ include Middleware
10
+
11
+ def call env, &k
12
+ promise = Promise.new(env[TIMER])
13
+ req = env.merge(REQUEST_URI => request_uri(env), PROMISE => promise)
14
+
15
+ promise.then do |result|
16
+ case result
17
+ when Exception
18
+ req.merge(FAIL => env[FAIL] + [result])
19
+ else
20
+ req.merge(result)
21
+ end
22
+ end.then(&k)
23
+
24
+ pool_size = env[CLIENT].class.pool_size
25
+ if pool_size < 0
26
+ promise.call{ request(req) }
27
+ elsif pool_size == 0
28
+ promise.defer{ request(req) }
29
+ else
30
+ promise.defer(env[CLIENT].class.thread_pool){ request(req) }
31
+ end
32
+
33
+ req.merge(promise.future_response)
34
+ end
35
+
36
+ private
37
+ def payload_and_headers env
38
+ if has_payload?(env)
39
+ Payload.generate_with_headers(env[REQUEST_PAYLOAD],
40
+ env[REQUEST_HEADERS])
41
+ else
42
+ [{}, env[REQUEST_HEADERS]]
43
+ end
44
+ end
45
+
46
+ def normalize_headers headers
47
+ headers.inject({}){ |r, (k, v)|
48
+ r[k.to_s.upcase.tr('-', '_')] = if v.kind_of?(Array) && v.size == 1
49
+ v.first
50
+ else
51
+ v
52
+ end
53
+ r
54
+ }
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,11 @@
1
+
2
+ require 'rest-builder/middleware'
3
+
4
+ module RestBuilder
5
+ class Dry
6
+ include Middleware
7
+ def call env
8
+ yield(env)
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,46 @@
1
+
2
+ require 'httpclient'
3
+ # httpclient would require something (cookie manager) while initialized,
4
+ # so we should try to force requiring them to avoid require deadlock!
5
+ HTTPClient.new
6
+
7
+ require 'rest-builder/engine'
8
+
9
+ module RestBuilder
10
+ class HttpClient < Engine
11
+ private
12
+ def request env
13
+ client = ::HTTPClient.new
14
+ client.cookie_manager = nil
15
+ client.follow_redirect_count = 0
16
+ client.transparent_gzip_decompression = true
17
+ config = config_engine(env) and config.call(client)
18
+ payload, headers = payload_and_headers(env)
19
+
20
+ if env[HIJACK]
21
+ request_async(client, payload, headers, env)
22
+ else
23
+ request_sync(client, payload, headers, env)
24
+ end
25
+ end
26
+
27
+ def request_sync client, payload, headers, env
28
+ res = client.request(env[REQUEST_METHOD], env[REQUEST_URI], nil,
29
+ payload, {'User-Agent' => 'Ruby'}.merge(headers))
30
+
31
+ {RESPONSE_STATUS => res.status,
32
+ RESPONSE_HEADERS => normalize_headers(res.header.all),
33
+ RESPONSE_BODY => res.content}
34
+ end
35
+
36
+ def request_async client, payload, headers, env
37
+ res = client.request_async(env[REQUEST_METHOD], env[REQUEST_URI], nil,
38
+ payload, {'User-Agent' => 'Ruby'}.merge(headers)).pop
39
+
40
+ {RESPONSE_STATUS => res.status,
41
+ RESPONSE_HEADERS => normalize_headers(res.header.all),
42
+ RESPONSE_BODY => '',
43
+ RESPONSE_SOCKET => res.content}
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,4 @@
1
+
2
+ module RestBuilder
3
+ Error = Class.new(RuntimeError)
4
+ end
@@ -0,0 +1,137 @@
1
+
2
+ require 'thread'
3
+ require 'rest-builder/error'
4
+
5
+ module RestBuilder
6
+ class EventSource < Struct.new(:client, :path, :query, :opts, :socket)
7
+ READ_WAIT = 35
8
+
9
+ def start
10
+ self.mutex = Mutex.new
11
+ self.condv = ConditionVariable.new
12
+ @onopen ||= nil
13
+ @onmessage ||= nil
14
+ @onerror ||= nil
15
+ @onreconnect ||= nil
16
+ @closed ||= false
17
+ reconnect
18
+ self
19
+ end
20
+
21
+ def closed?
22
+ !!(socket && socket.closed?) || @closed
23
+ end
24
+
25
+ def close
26
+ socket && socket.close
27
+ rescue IOError
28
+ end
29
+
30
+ def wait
31
+ raise Error.new("Not yet started for: #{self}") unless mutex
32
+ mutex.synchronize{ condv.wait(mutex) until closed? } unless closed?
33
+ self
34
+ end
35
+
36
+ def onopen sock=nil, &cb
37
+ if block_given?
38
+ @onopen = cb
39
+ else
40
+ self.socket = sock # for you to track the socket
41
+ @onopen.call(sock) if @onopen
42
+ onmessage_for(sock)
43
+ end
44
+ self
45
+ rescue Exception => e
46
+ begin # close the socket since we're going to stop anyway
47
+ sock.close # if we don't close it, client might wait forever
48
+ rescue IOError
49
+ end
50
+ # let the client has a chance to handle this, and make signal
51
+ onerror(e, sock)
52
+ end
53
+
54
+ def onmessage event=nil, data=nil, sock=nil, &cb
55
+ if block_given?
56
+ @onmessage = cb
57
+ elsif @onmessage
58
+ @onmessage.call(event, data, sock)
59
+ end
60
+ self
61
+ end
62
+
63
+ # would also be called upon closing, would always be called at least once
64
+ def onerror error=nil, sock=nil, &cb
65
+ if block_given?
66
+ @onerror = cb
67
+ else
68
+ begin
69
+ @onerror.call(error, sock) if @onerror
70
+ onreconnect(error, sock)
71
+ rescue Exception
72
+ mutex.synchronize do
73
+ @closed = true
74
+ condv.signal # so we can't be reconnecting, need to try to unblock
75
+ end
76
+ raise
77
+ end
78
+ end
79
+ self
80
+ end
81
+
82
+ # would be called upon closing,
83
+ # and would try to reconnect if a callback is set and return true
84
+ def onreconnect error=nil, sock=nil, &cb
85
+ if block_given?
86
+ @onreconnect = cb
87
+ elsif closed? && @onreconnect && @onreconnect.call(error, sock)
88
+ reconnect
89
+ else
90
+ mutex.synchronize do
91
+ @closed = true
92
+ condv.signal # we could be closing, let's try to unblock it
93
+ end
94
+ end
95
+ self
96
+ end
97
+
98
+ protected
99
+ attr_accessor :mutex, :condv
100
+
101
+ private
102
+ # called in requesting thread after the request is done
103
+ def onmessage_for sock
104
+ while IO.select([sock], [], [], READ_WAIT)
105
+ event = sock.readline("\n\n").split("\n").inject({}) do |r, i|
106
+ k, v = i.split(': ', 2)
107
+ r[k] = v
108
+ r
109
+ end
110
+ onmessage(event['event'], event['data'], sock)
111
+ end
112
+ close_sock(sock)
113
+ onerror(EOFError.new, sock)
114
+ rescue IOError, SystemCallError => e
115
+ close_sock(sock)
116
+ onerror(e, sock)
117
+ end
118
+
119
+ def close_sock sock
120
+ sock.close
121
+ rescue IOError => e
122
+ onerror(e, sock)
123
+ end
124
+
125
+ def reconnect
126
+ o = {REQUEST_HEADERS => {'Accept' => 'text/event-stream'},
127
+ HIJACK => true}.merge(opts)
128
+ client.get(path, query, o) do |sock|
129
+ if sock.nil? || sock.kind_of?(Exception)
130
+ onerror(sock)
131
+ else
132
+ onopen(sock)
133
+ end
134
+ end
135
+ end
136
+ end
137
+ end
@@ -0,0 +1,147 @@
1
+
2
+ module RestBuilder
3
+ module Middleware
4
+ METHODS_WITH_PAYLOAD = [:post, :put, :patch]
5
+
6
+ def self.included mod
7
+ mod.send(:attr_reader, :app)
8
+ mem = if mod.respond_to?(:members) then mod.members else [] end
9
+ src = mem.map{ |member| <<-RUBY }
10
+ attr_writer :#{member}
11
+ def #{member} env
12
+ if env.key?('#{member}')
13
+ env['#{member}']
14
+ else
15
+ @#{member}
16
+ end
17
+ end
18
+ RUBY
19
+ args = [:app] + mem
20
+ para_list = args.map{ |a| "#{a}=nil"}.join(', ')
21
+ args_list = args .join(', ')
22
+ ivar_list = args.map{ |a| "@#{a}" }.join(', ')
23
+ src << <<-RUBY
24
+ def initialize #{para_list}
25
+ #{ivar_list} = #{args_list}
26
+ end
27
+ RUBY
28
+ accessor = Module.new
29
+ accessor.module_eval(src.join("\n"), __FILE__, __LINE__)
30
+ mod.const_set(:Accessor, accessor)
31
+ mod.send(:include, accessor)
32
+ end
33
+
34
+ def call env, &k; app.call(env, &(k || :itself.to_proc)); end
35
+ def fail env, obj
36
+ if obj
37
+ env.merge(FAIL => (env[FAIL] || []) + [obj])
38
+ else
39
+ env
40
+ end
41
+ end
42
+ def log env, obj
43
+ if obj
44
+ env.merge(LOG => (env[LOG] || []) + [obj])
45
+ else
46
+ env
47
+ end
48
+ end
49
+ def run a=app
50
+ if a.respond_to?(:app) && a.app
51
+ run(a.app)
52
+ else
53
+ a
54
+ end
55
+ end
56
+ def error_callback res, err
57
+ res[CLIENT].error_callback.call(err) if
58
+ res[CLIENT] && res[CLIENT].error_callback
59
+ end
60
+ def give_promise res
61
+ res[CLIENT].give_promise(res) if res[CLIENT]
62
+ end
63
+
64
+ module_function
65
+ def request_uri env
66
+ # compacting the hash
67
+ if (query = (env[REQUEST_QUERY] || {}).select{ |k, v| v }).empty?
68
+ env[REQUEST_PATH].to_s
69
+ else
70
+ q = if env[REQUEST_PATH] =~ /\?/ then '&' else '?' end
71
+ "#{env[REQUEST_PATH]}#{q}#{percent_encode(query)}"
72
+ end
73
+ end
74
+ public :request_uri
75
+
76
+ def percent_encode query
77
+ query.sort.map{ |(k, v)|
78
+ if v.kind_of?(Array)
79
+ v.map{ |vv| "#{escape(k.to_s)}=#{escape(vv.to_s)}" }.join('&')
80
+ else
81
+ "#{escape(k.to_s)}=#{escape(v.to_s)}"
82
+ end
83
+ }.join('&')
84
+ end
85
+ public :percent_encode
86
+
87
+ UNRESERVED = /[^a-zA-Z0-9\-\.\_\~]+/
88
+ def escape string
89
+ string.gsub(UNRESERVED) do |s|
90
+ "%#{s.unpack('H2' * s.bytesize).join('%')}".upcase
91
+ end
92
+ end
93
+ public :escape
94
+
95
+ def has_payload? env
96
+ METHODS_WITH_PAYLOAD.include?(env[REQUEST_METHOD])
97
+ end
98
+ public :has_payload?
99
+
100
+ def contain_binary? payload
101
+ return false unless payload
102
+ return true if payload.respond_to?(:read)
103
+ return true if payload.find{ |k, v|
104
+ # if payload is an array, then v would be nil
105
+ (v || k).respond_to?(:read) ||
106
+ # if v is an array, it could contain binary data
107
+ (v.kind_of?(Array) && v.any?{ |vv| vv.respond_to?(:read) }) }
108
+ return false
109
+ end
110
+ public :contain_binary?
111
+
112
+ def string_keys hash
113
+ hash.inject({}){ |r, (k, v)|
114
+ if v.kind_of?(Hash)
115
+ r[k.to_s] = case k.to_s
116
+ when REQUEST_QUERY, REQUEST_PAYLOAD, REQUEST_HEADERS
117
+ string_keys(v)
118
+ else; v
119
+ end
120
+ else
121
+ r[k.to_s] = v
122
+ end
123
+ r
124
+ }
125
+ end
126
+ public :string_keys
127
+
128
+ # this method is intended to merge payloads if they are non-empty hashes,
129
+ # but prefer the right most one if they are not hashes.
130
+ def merge_hash *hashes
131
+ hashes.reverse_each.inject do |r, i|
132
+ if r.kind_of?(Hash)
133
+ if i.kind_of?(Hash)
134
+ Middleware.string_keys(i).merge(Middleware.string_keys(r))
135
+ elsif r.empty?
136
+ i # prefer non-empty ones
137
+ else
138
+ r # don't try to merge non-hashes
139
+ end
140
+ else
141
+ r
142
+ end
143
+ end
144
+ end
145
+ public :merge_hash
146
+ end
147
+ end
@@ -0,0 +1,173 @@
1
+
2
+ # stolen and modified from rest-client
3
+
4
+ require 'stringio'
5
+ require 'tempfile'
6
+
7
+ require 'rest-builder/error'
8
+ require 'rest-builder/middleware'
9
+
10
+ begin
11
+ require 'mime/types/columnar'
12
+ rescue LoadError
13
+ require 'mime/types'
14
+ end
15
+
16
+ module RestBuilder
17
+ class Payload
18
+ def self.generate_with_headers payload, headers
19
+ h = if p = generate(payload)
20
+ p.headers.merge(headers)
21
+ else
22
+ headers
23
+ end
24
+ [p, h]
25
+ end
26
+
27
+ def self.generate payload
28
+ if payload.respond_to?(:read)
29
+ Streamed.new(payload)
30
+
31
+ elsif payload.kind_of?(String)
32
+ StreamedString.new(payload)
33
+
34
+ elsif payload.kind_of?(Hash)
35
+ if payload.empty?
36
+ nil
37
+
38
+ elsif Middleware.contain_binary?(payload)
39
+ Multipart.new(payload)
40
+
41
+ else
42
+ UrlEncoded.new(payload)
43
+ end
44
+
45
+ else
46
+ raise Error.new("Payload should be either String, Hash, or" \
47
+ " responding to `read', but: #{payload.inspect}")
48
+ end
49
+ end
50
+
51
+ # Payload API
52
+ attr_reader :io
53
+ alias_method :to_io, :io
54
+
55
+ def initialize payload; @io = payload ; end
56
+ def read bytes=nil; io.read(bytes) ; end
57
+ def close ; io.close unless closed?; end
58
+ def closed? ; io.closed? ; end
59
+ def headers ; {} ; end
60
+
61
+ def size
62
+ if io.respond_to?(:size)
63
+ io.size
64
+ elsif io.respond_to?(:stat)
65
+ io.stat.size
66
+ else
67
+ 0
68
+ end
69
+ end
70
+
71
+ class Streamed < Payload
72
+ def headers
73
+ {'Content-Length' => size.to_s}
74
+ end
75
+ end
76
+
77
+ class StreamedString < Streamed
78
+ def initialize payload
79
+ super(StringIO.new(payload))
80
+ end
81
+ end
82
+
83
+ class UrlEncoded < StreamedString
84
+ def initialize payload
85
+ super(Middleware.percent_encode(payload))
86
+ end
87
+
88
+ def headers
89
+ super.merge('Content-Type' => 'application/x-www-form-urlencoded')
90
+ end
91
+ end
92
+
93
+ class Multipart < Streamed
94
+ EOL = "\r\n"
95
+
96
+ def initialize payload
97
+ super(Tempfile.new("rest-core.payload.#{boundary}"))
98
+
99
+ io.binmode
100
+
101
+ payload.each_with_index do |(k, v), i|
102
+ if v.kind_of?(Array)
103
+ v.each{ |vv| part(k, vv) }
104
+ else
105
+ part(k, v)
106
+ end
107
+ end
108
+ io.write("--#{boundary}--#{EOL}")
109
+ io.rewind
110
+ end
111
+
112
+ def part k, v
113
+ io.write("--#{boundary}#{EOL}Content-Disposition: form-data")
114
+ io.write("; name=\"#{k}\"") if k
115
+ if v.respond_to?(:read)
116
+ part_binary(k, v)
117
+ else
118
+ part_plantext(k, v)
119
+ end
120
+ end
121
+
122
+ def part_plantext k, v
123
+ io.write("#{EOL}#{EOL}#{v}#{EOL}")
124
+ end
125
+
126
+ def part_binary k, v
127
+ if v.respond_to?(:original_filename) # Rails
128
+ io.write("; filename=\"#{v.original_filename}\"#{EOL}")
129
+ elsif v.respond_to?(:path) # files
130
+ io.write("; filename=\"#{File.basename(v.path)}\"#{EOL}")
131
+ else # io
132
+ io.write("; filename=\"#{k}\"#{EOL}")
133
+ end
134
+
135
+ # supply your own content type for regular files, will you?
136
+ if v.respond_to?(:content_type) # Rails
137
+ io.write("Content-Type: #{v.content_type}#{EOL}#{EOL}")
138
+ elsif v.respond_to?(:path) && type = mime_type(v.path) # files
139
+ io.write("Content-Type: #{type}#{EOL}#{EOL}")
140
+ else
141
+ io.write(EOL)
142
+ end
143
+
144
+ while data = v.read(8192)
145
+ io.write(data)
146
+ end
147
+
148
+ io.write(EOL)
149
+
150
+ ensure
151
+ v.close if v.respond_to?(:close)
152
+ end
153
+
154
+ def mime_type path
155
+ mime = MIME::Types.type_for(path)
156
+ mime.first && mime.first.content_type
157
+ end
158
+
159
+ def boundary
160
+ @boundary ||= rand(1_000_000).to_s
161
+ end
162
+
163
+ def headers
164
+ super.merge('Content-Type' =>
165
+ "multipart/form-data; boundary=#{boundary}")
166
+ end
167
+
168
+ def close
169
+ io.close! unless io.closed?
170
+ end
171
+ end
172
+ end
173
+ end