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