nestful 0.0.8 → 1.0.0.pre
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +17 -0
- data/Gemfile +4 -0
- data/README.markdown +11 -37
- data/Rakefile +1 -14
- data/examples/resource.rb +6 -0
- data/lib/nestful/.DS_Store +0 -0
- data/lib/nestful/connection.rb +107 -245
- data/lib/nestful/endpoint.rb +42 -0
- data/lib/nestful/exceptions.rb +1 -1
- data/lib/nestful/formats/form_format.rb +3 -3
- data/lib/nestful/formats/json_format.rb +10 -9
- data/lib/nestful/formats/multipart_format.rb +9 -8
- data/lib/nestful/formats.rb +19 -16
- data/lib/nestful/helpers.rb +41 -0
- data/lib/nestful/request.rb +73 -96
- data/lib/nestful/resource.rb +144 -27
- data/lib/nestful/response/headers.rb +31 -0
- data/lib/nestful/response.rb +47 -0
- data/lib/nestful/version.rb +3 -0
- data/lib/nestful.rb +17 -30
- data/nestful.gemspec +14 -54
- metadata +21 -28
- data/lib/nestful/formats/blank_format.rb +0 -13
- data/lib/nestful/formats/text_format.rb +0 -17
- data/lib/nestful/formats/xml_format.rb +0 -34
- data/lib/nestful/oauth.rb +0 -24
- data/lib/nestful/request/callbacks.rb +0 -28
@@ -0,0 +1,42 @@
|
|
1
|
+
module Nestful
|
2
|
+
class Endpoint
|
3
|
+
def self.[](url)
|
4
|
+
self.new(url)
|
5
|
+
end
|
6
|
+
|
7
|
+
attr_reader :url
|
8
|
+
|
9
|
+
def initialize(url, options = {})
|
10
|
+
@url = url
|
11
|
+
@options = options
|
12
|
+
end
|
13
|
+
|
14
|
+
def [](suburl)
|
15
|
+
return self if suburl.nil?
|
16
|
+
suburl = suburl.to_s
|
17
|
+
base = url
|
18
|
+
base += "/" unless base =~ /\/$/
|
19
|
+
self.class.new(URI.join(base, suburl).to_s, @options)
|
20
|
+
end
|
21
|
+
|
22
|
+
def get(params = {}, options = {})
|
23
|
+
request(options.merge(:method => :get, :params => params))
|
24
|
+
end
|
25
|
+
|
26
|
+
def put(params = {}, options = {})
|
27
|
+
request(options.merge(:method => :put, :params => params))
|
28
|
+
end
|
29
|
+
|
30
|
+
def post(params = {}, options = {})
|
31
|
+
request(options.merge(:method => :post, :params => params))
|
32
|
+
end
|
33
|
+
|
34
|
+
def delete(params = {}, options = {})
|
35
|
+
request(options.merge(:method => :delete, :params => params))
|
36
|
+
end
|
37
|
+
|
38
|
+
def request(options = {})
|
39
|
+
Request.new(url, options.merge(@options)).execute
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
data/lib/nestful/exceptions.rb
CHANGED
@@ -1,24 +1,25 @@
|
|
1
|
-
|
2
|
-
require '
|
1
|
+
begin
|
2
|
+
require 'json'
|
3
|
+
rescue LoadError => e
|
4
|
+
require 'json/pure'
|
5
|
+
end
|
3
6
|
|
4
7
|
module Nestful
|
5
8
|
module Formats
|
6
|
-
class
|
7
|
-
def extension
|
8
|
-
'json'
|
9
|
-
end
|
10
|
-
|
9
|
+
class JSONFormat < Format
|
11
10
|
def mime_type
|
12
11
|
'application/json'
|
13
12
|
end
|
14
13
|
|
15
14
|
def encode(hash, options = nil)
|
16
|
-
|
15
|
+
hash.to_json(options)
|
17
16
|
end
|
18
17
|
|
19
18
|
def decode(json)
|
20
|
-
|
19
|
+
JSON.parse(json)
|
21
20
|
end
|
22
21
|
end
|
22
|
+
|
23
|
+
JsonFormat = JSONFormat
|
23
24
|
end
|
24
25
|
end
|
@@ -1,12 +1,13 @@
|
|
1
1
|
require 'securerandom'
|
2
|
+
require 'tempfile'
|
2
3
|
|
3
4
|
module Nestful
|
4
5
|
module Formats
|
5
6
|
class MultipartFormat < Format
|
6
7
|
EOL = "\r\n"
|
7
|
-
|
8
|
+
|
8
9
|
attr_reader :boundary, :stream
|
9
|
-
|
10
|
+
|
10
11
|
def initialize(*args)
|
11
12
|
super
|
12
13
|
@boundary = SecureRandom.hex(10)
|
@@ -29,7 +30,7 @@ module Nestful
|
|
29
30
|
def decode(body)
|
30
31
|
body
|
31
32
|
end
|
32
|
-
|
33
|
+
|
33
34
|
protected
|
34
35
|
def to_multipart(params, namespace = nil)
|
35
36
|
params.each do |key, value|
|
@@ -43,14 +44,14 @@ module Nestful
|
|
43
44
|
|
44
45
|
stream.write("--" + boundary + EOL)
|
45
46
|
|
46
|
-
if value.
|
47
|
+
if value.respond_to?(:read)
|
47
48
|
create_file_field(key, value)
|
48
49
|
else
|
49
50
|
create_field(key, value)
|
50
51
|
end
|
51
|
-
end
|
52
|
+
end
|
52
53
|
end
|
53
|
-
|
54
|
+
|
54
55
|
def create_file_field(key, value)
|
55
56
|
stream.write(%Q{Content-Disposition: form-data; name="#{key}"; filename="#{filename(value)}"} + EOL)
|
56
57
|
stream.write(%Q{Content-Type: application/octet-stream} + EOL)
|
@@ -61,14 +62,14 @@ module Nestful
|
|
61
62
|
end
|
62
63
|
stream.write(EOL)
|
63
64
|
end
|
64
|
-
|
65
|
+
|
65
66
|
def create_field(key, value)
|
66
67
|
stream.write(%Q{Content-Disposition: form-data; name="#{key}"} + EOL)
|
67
68
|
stream.write(EOL)
|
68
69
|
stream.write(value)
|
69
70
|
stream.write(EOL)
|
70
71
|
end
|
71
|
-
|
72
|
+
|
72
73
|
def filename(body)
|
73
74
|
return body.original_filename if body.respond_to?(:original_filename)
|
74
75
|
return File.basename(body.path) if body.respond_to?(:path)
|
data/lib/nestful/formats.rb
CHANGED
@@ -1,32 +1,35 @@
|
|
1
1
|
module Nestful
|
2
2
|
module Formats
|
3
3
|
class Format
|
4
|
-
def extension
|
5
|
-
end
|
6
|
-
|
7
4
|
def mime_type
|
8
5
|
end
|
9
|
-
|
6
|
+
|
10
7
|
def encode(*args)
|
11
8
|
end
|
12
|
-
|
9
|
+
|
13
10
|
def decode(*args)
|
14
11
|
end
|
15
12
|
end
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
13
|
+
|
14
|
+
MAPPING = {
|
15
|
+
'application/json' => :json
|
16
|
+
}
|
17
|
+
|
18
|
+
def self.for(type)
|
19
|
+
format = MAPPING[type]
|
20
|
+
format && self[format]
|
21
|
+
end
|
22
|
+
|
24
23
|
# Lookup the format class from a mime type reference symbol. Example:
|
25
24
|
#
|
26
|
-
# Nestful::Formats[:xml] # => Nestful::Formats::XmlFormat
|
27
25
|
# Nestful::Formats[:json] # => Nestful::Formats::JsonFormat
|
28
26
|
def self.[](mime_type_reference)
|
29
|
-
Nestful::Formats.const_get(
|
27
|
+
Nestful::Formats.const_get(Helpers.camelize(mime_type_reference.to_s) + 'Format')
|
30
28
|
end
|
31
|
-
end
|
29
|
+
end
|
32
30
|
end
|
31
|
+
|
32
|
+
Dir[File.dirname(__FILE__) + '/formats/*.rb'].sort.each do |path|
|
33
|
+
filename = File.basename(path)
|
34
|
+
require "nestful/formats/#{filename}"
|
35
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
require 'cgi'
|
2
|
+
|
3
|
+
module Nestful
|
4
|
+
module Helpers extend self
|
5
|
+
def to_query(key, value)
|
6
|
+
case key
|
7
|
+
when Hash
|
8
|
+
"#{CGI.escape(to_param(key))}=#{CGI.escape(to_param(value))}"
|
9
|
+
|
10
|
+
when Array
|
11
|
+
prefix = "#{key}[]"
|
12
|
+
value.collect { |v| to_query(prefix, v) }.join('&')
|
13
|
+
|
14
|
+
else
|
15
|
+
value
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
def to_param(object, namespace = nil)
|
20
|
+
case object
|
21
|
+
when Hash
|
22
|
+
object.map do |key, value|
|
23
|
+
key = "#{namespace}[#{key}]" if namespace
|
24
|
+
to_query(key, value)
|
25
|
+
end.join('&')
|
26
|
+
|
27
|
+
when Array
|
28
|
+
object.each do |value|
|
29
|
+
to_param(value)
|
30
|
+
end.join('/')
|
31
|
+
|
32
|
+
else
|
33
|
+
value
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def camelize(value)
|
38
|
+
value.to_s.split('_').map {|w| w.capitalize }.join
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
data/lib/nestful/request.rb
CHANGED
@@ -1,22 +1,16 @@
|
|
1
1
|
module Nestful
|
2
2
|
class Request
|
3
|
-
def self.callbacks(type = nil) #:nodoc:
|
4
|
-
@callbacks ||= {}
|
5
|
-
return @callbacks unless type
|
6
|
-
@callbacks[type] ||= []
|
7
|
-
end
|
8
|
-
|
9
3
|
attr_reader :options, :format, :url
|
10
|
-
attr_accessor :params, :body, :
|
11
|
-
|
4
|
+
attr_accessor :params, :body, :method, :headers
|
5
|
+
|
12
6
|
# Connection options
|
13
7
|
attr_accessor :proxy, :user, :password, :auth_type, :timeout, :ssl_options
|
14
|
-
|
8
|
+
|
15
9
|
def initialize(url, options = {})
|
16
10
|
@url = url.to_s
|
17
|
-
|
11
|
+
|
18
12
|
@options = options
|
19
|
-
@options.each do |key, val|
|
13
|
+
@options.each do |key, val|
|
20
14
|
method = "#{key}="
|
21
15
|
send(method, val) if respond_to?(method)
|
22
16
|
end
|
@@ -24,127 +18,110 @@ module Nestful
|
|
24
18
|
self.method ||= :get
|
25
19
|
self.params ||= {}
|
26
20
|
self.headers ||= {}
|
27
|
-
self.
|
28
|
-
self.format ||= :blank
|
21
|
+
self.format ||= :form
|
29
22
|
end
|
30
|
-
|
23
|
+
|
31
24
|
def format=(mime_or_format)
|
32
25
|
@format = mime_or_format.is_a?(Symbol) ?
|
33
|
-
Formats[mime_or_format]
|
34
|
-
end
|
35
|
-
|
36
|
-
def connection
|
37
|
-
conn = Connection.new(uri, format)
|
38
|
-
conn.proxy = proxy if proxy
|
39
|
-
conn.user = user if user
|
40
|
-
conn.password = password if password
|
41
|
-
conn.auth_type = auth_type if auth_type
|
42
|
-
conn.timeout = timeout if timeout
|
43
|
-
conn.ssl_options = ssl_options if ssl_options
|
44
|
-
conn
|
26
|
+
Formats[mime_or_format] : mime_or_format
|
45
27
|
end
|
46
|
-
|
28
|
+
|
47
29
|
def url=(value)
|
48
30
|
@url = value
|
49
31
|
@uri = nil
|
50
32
|
@url
|
51
33
|
end
|
52
|
-
|
34
|
+
|
53
35
|
def uri
|
54
36
|
return @uri if @uri
|
55
|
-
|
37
|
+
|
56
38
|
url = @url.match(/^http/) ? @url : "http://#{@url}"
|
57
|
-
|
39
|
+
|
58
40
|
@uri = URI.parse(url)
|
59
41
|
@uri.path = "/" if @uri.path.empty?
|
60
|
-
|
61
|
-
if extension
|
62
|
-
extension = format.extension if extension.is_a?(Boolean)
|
63
|
-
@uri.path += ".#{extension}"
|
64
|
-
end
|
65
|
-
|
42
|
+
|
66
43
|
@uri.query.split("&").each do |res|
|
67
44
|
key, value = res.split("=")
|
68
45
|
@params[key] = value
|
69
46
|
end if @uri.query
|
70
|
-
|
47
|
+
|
71
48
|
@uri
|
72
49
|
end
|
73
|
-
|
50
|
+
|
74
51
|
def path
|
75
52
|
uri.path
|
76
53
|
end
|
77
|
-
|
78
|
-
def query_path
|
79
|
-
query_path = path
|
80
|
-
if params.any?
|
81
|
-
query_path += "?"
|
82
|
-
query_path += params.to_param
|
83
|
-
end
|
84
|
-
query_path
|
85
|
-
end
|
86
|
-
|
54
|
+
|
87
55
|
def execute
|
88
|
-
callback(:before_request, self)
|
89
56
|
result = nil
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
result
|
94
|
-
result.response = res
|
57
|
+
|
58
|
+
if encoded?
|
59
|
+
connection.send(method, path, encoded, build_headers) do |res|
|
60
|
+
result = res
|
95
61
|
end
|
96
62
|
else
|
97
|
-
connection.send(method, query_path,
|
98
|
-
result =
|
99
|
-
result.class_eval { attr_accessor :response }
|
100
|
-
result.response = res
|
63
|
+
connection.send(method, query_path, build_headers) do |res|
|
64
|
+
result = res
|
101
65
|
end
|
102
66
|
end
|
103
|
-
|
104
|
-
result
|
67
|
+
|
68
|
+
Response.new(result)
|
69
|
+
|
105
70
|
rescue Redirection => error
|
106
71
|
self.url = error.response['Location']
|
107
72
|
execute
|
108
73
|
end
|
109
|
-
|
74
|
+
|
110
75
|
protected
|
111
|
-
def encoded
|
112
|
-
params.any? ? format.encode(params) : body
|
113
|
-
end
|
114
76
|
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
else
|
129
|
-
return result if raw
|
130
|
-
data = result.body
|
131
|
-
format ? format.decode(data) : data
|
132
|
-
end
|
77
|
+
def connection
|
78
|
+
Connection.new(uri,
|
79
|
+
:proxy => proxy,
|
80
|
+
:timeout => timeout,
|
81
|
+
:ssl_options => ssl_options
|
82
|
+
)
|
83
|
+
end
|
84
|
+
|
85
|
+
def content_type_headers
|
86
|
+
if encoded?
|
87
|
+
{'Content-Type' => format.mime_type}
|
88
|
+
else
|
89
|
+
{}
|
133
90
|
end
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
@
|
91
|
+
end
|
92
|
+
|
93
|
+
def auth_headers
|
94
|
+
if auth_type == :bearer
|
95
|
+
{ 'Authorization' => "Bearer #{@password}" }
|
96
|
+
elsif auth_type == :basic
|
97
|
+
{ 'Authorization' => 'Basic ' + ["#{@user}:#{@password}"].pack('m').delete("\r\n") }
|
98
|
+
else
|
99
|
+
{ }
|
139
100
|
end
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
101
|
+
end
|
102
|
+
|
103
|
+
def build_headers
|
104
|
+
auth_headers
|
105
|
+
.merge(content_type_headers)
|
106
|
+
.merge(headers)
|
107
|
+
end
|
108
|
+
|
109
|
+
def query_path
|
110
|
+
query_path = path
|
111
|
+
|
112
|
+
if params.any?
|
113
|
+
query_path += '?' + Helpers.to_param(params)
|
144
114
|
end
|
115
|
+
|
116
|
+
query_path
|
117
|
+
end
|
118
|
+
|
119
|
+
def encoded?
|
120
|
+
[:post, :put].include?(method)
|
121
|
+
end
|
122
|
+
|
123
|
+
def encoded
|
124
|
+
params.any? ? format.encode(params) : body
|
125
|
+
end
|
145
126
|
end
|
146
|
-
|
147
|
-
class Request
|
148
|
-
include Callbacks
|
149
|
-
end
|
150
|
-
end
|
127
|
+
end
|