restify 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/LICENSE.txt +165 -0
- data/README.md +96 -0
- data/doc/file.README.html +107 -0
- data/lib/restify.rb +42 -0
- data/lib/restify/adapter.rb +74 -0
- data/lib/restify/client.rb +106 -0
- data/lib/restify/collection.rb +69 -0
- data/lib/restify/link.rb +100 -0
- data/lib/restify/parser/json.rb +16 -0
- data/lib/restify/relation.rb +33 -0
- data/lib/restify/relations.rb +35 -0
- data/lib/restify/request.rb +37 -0
- data/lib/restify/resource.rb +131 -0
- data/lib/restify/response.rb +142 -0
- data/lib/restify/version.rb +13 -0
- data/restify.gemspec +29 -0
- data/spec/restify/link_spec.rb +53 -0
- data/spec/restify/resource_spec.rb +96 -0
- data/spec/restify_spec.rb +219 -0
- data/spec/spec_helper.rb +21 -0
- metadata +175 -0
@@ -0,0 +1,106 @@
|
|
1
|
+
module Restify
|
2
|
+
#
|
3
|
+
# A {ResponseError} is returned on a non-successful
|
4
|
+
# response from server. Usually it will either be a
|
5
|
+
# {ClientError} or a {ServerError}.
|
6
|
+
#
|
7
|
+
class ResponseError < StandardError
|
8
|
+
attr_reader :response
|
9
|
+
|
10
|
+
def initialize(response)
|
11
|
+
@response = response
|
12
|
+
super "#{response.message} (#{response.code}) for `#{response.url}':\n" \
|
13
|
+
" #{errors.inspect}"
|
14
|
+
end
|
15
|
+
|
16
|
+
# Return response status.
|
17
|
+
#
|
18
|
+
# @return [Symbol] Response status.
|
19
|
+
# @see Response#status
|
20
|
+
#
|
21
|
+
def status
|
22
|
+
response.status
|
23
|
+
end
|
24
|
+
|
25
|
+
# Return response status code.
|
26
|
+
#
|
27
|
+
# @return [Fixnum] Response status code.
|
28
|
+
# @see Response#code
|
29
|
+
#
|
30
|
+
def code
|
31
|
+
response.code
|
32
|
+
end
|
33
|
+
|
34
|
+
# Return hash or array of errors if response included
|
35
|
+
# such a thing otherwise it returns nil.
|
36
|
+
#
|
37
|
+
def errors
|
38
|
+
response.decoded_body['errors'] || response.decoded_body[:errors]
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
# A {ClientError} will be raised when a response has a
|
43
|
+
# 4XX status code.
|
44
|
+
class ClientError < ResponseError; end
|
45
|
+
|
46
|
+
# A {ServerError} will be raised when a response has a
|
47
|
+
# 5XX status code.
|
48
|
+
class ServerError < ResponseError; end
|
49
|
+
|
50
|
+
# @api private
|
51
|
+
#
|
52
|
+
class Client
|
53
|
+
#
|
54
|
+
# Keys that should not be extracted from options
|
55
|
+
# to expand URI templates.
|
56
|
+
RESERVED_KEYS = [:data]
|
57
|
+
|
58
|
+
# @api private
|
59
|
+
#
|
60
|
+
# Request given path with given method.
|
61
|
+
#
|
62
|
+
# Returns an obligation that will return a collection or
|
63
|
+
# resource or fail with a response error depending on
|
64
|
+
# response from server.
|
65
|
+
#
|
66
|
+
def request(method, path, opts = {})
|
67
|
+
data = opts.fetch(:data, opts)
|
68
|
+
request = Request.new method: method, uri: base.join(path.to_s), data: data
|
69
|
+
|
70
|
+
::Restify.adapter.call(request).then do |response|
|
71
|
+
if response.success?
|
72
|
+
handle_success response
|
73
|
+
else
|
74
|
+
handle_error response
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
private
|
80
|
+
|
81
|
+
attr_reader :base
|
82
|
+
|
83
|
+
def initialize(uri)
|
84
|
+
@base = ::Addressable::URI.parse(uri)
|
85
|
+
end
|
86
|
+
|
87
|
+
def handle_success(response)
|
88
|
+
if response.decoded_body.is_a?(Array)
|
89
|
+
Collection.create(self, response.decoded_body, response)
|
90
|
+
else
|
91
|
+
Resource.create(self, response.decoded_body, response)
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
def handle_error(response)
|
96
|
+
case response.code
|
97
|
+
when 400...500
|
98
|
+
raise ClientError.new(response)
|
99
|
+
when 500...600
|
100
|
+
raise ServerError.new(response)
|
101
|
+
else
|
102
|
+
raise RuntimeError.new "Unknown response code: #{response.code}"
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|
@@ -0,0 +1,69 @@
|
|
1
|
+
module Restify
|
2
|
+
#
|
3
|
+
class Collection
|
4
|
+
include Relations
|
5
|
+
|
6
|
+
# @!method [](index)
|
7
|
+
# Retrieve value for given index.
|
8
|
+
#
|
9
|
+
# @param index [Integer] Index.
|
10
|
+
# @return [Object] Data value for given index.
|
11
|
+
#
|
12
|
+
delegate :[], to: :items
|
13
|
+
|
14
|
+
# @!method size
|
15
|
+
# Return size of collection. Only includes size of current
|
16
|
+
# page in paginated resources.
|
17
|
+
#
|
18
|
+
# @return [Integer] Number of items in current collection.
|
19
|
+
#
|
20
|
+
delegate :size, to: :items
|
21
|
+
|
22
|
+
# @!method first
|
23
|
+
# Return first item of collection.
|
24
|
+
#
|
25
|
+
# @return [Resource, Collection, Object] Return first
|
26
|
+
#
|
27
|
+
delegate :first, to: :items
|
28
|
+
|
29
|
+
# @!method each
|
30
|
+
# Iterate over all items or return enumerator.
|
31
|
+
#
|
32
|
+
# @overload each
|
33
|
+
# Calls block once for each item, passing the item as
|
34
|
+
# parameters.
|
35
|
+
#
|
36
|
+
# @yield [item] Yield for each item.
|
37
|
+
# @yieldparam item [Resource, Collection, Object] Collection item.
|
38
|
+
#
|
39
|
+
# @overload each
|
40
|
+
# Return enumerator for each item.
|
41
|
+
#
|
42
|
+
# @return [Enumerator] Enumerator for each item.
|
43
|
+
#
|
44
|
+
delegate :each, to: :items
|
45
|
+
|
46
|
+
# Return parsed resource attributes as hash.
|
47
|
+
#
|
48
|
+
# @return [Array] Collection items.
|
49
|
+
#
|
50
|
+
def items
|
51
|
+
@items ||= []
|
52
|
+
end
|
53
|
+
|
54
|
+
def initialize(client, relations = {}, items = [])
|
55
|
+
@client = client
|
56
|
+
@relations = HashWithIndifferentAccess.new relations
|
57
|
+
@items = Array items
|
58
|
+
end
|
59
|
+
|
60
|
+
class << self
|
61
|
+
def create(client, data, response)
|
62
|
+
data = data.map { |s| Resource.create(client, s, nil) } if data
|
63
|
+
relations = response ? response.relations(client) : nil
|
64
|
+
|
65
|
+
new client, relations, data
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
data/lib/restify/link.rb
ADDED
@@ -0,0 +1,100 @@
|
|
1
|
+
module Restify
|
2
|
+
#
|
3
|
+
# A {Link} represents a single entry from the Link header
|
4
|
+
# of a HTTP response.
|
5
|
+
#
|
6
|
+
class Link
|
7
|
+
#
|
8
|
+
# URI of the link interpreted as a RFC6570 template.
|
9
|
+
#
|
10
|
+
# @return [Addressable::Template] URI template.
|
11
|
+
#
|
12
|
+
attr_reader :uri
|
13
|
+
|
14
|
+
# Link metadata like "rel" if specified.
|
15
|
+
#
|
16
|
+
# @return [HashWithIndifferentAccess<String, String>] Metadata.
|
17
|
+
#
|
18
|
+
attr_reader :metadata
|
19
|
+
|
20
|
+
def initialize(uri, metadata = {})
|
21
|
+
@uri = uri
|
22
|
+
@metadata = HashWithIndifferentAccess.new(metadata)
|
23
|
+
end
|
24
|
+
|
25
|
+
class << self
|
26
|
+
REGEXP_URI = /<[^>]*>\s*/
|
27
|
+
REGEXP_PAR = /;\s*\w+\s*=\s*/i
|
28
|
+
REGEXP_QUT = /"[^"]*"\s*/
|
29
|
+
REGEXP_ARG = /\w+\s*/i
|
30
|
+
|
31
|
+
def parse(string)
|
32
|
+
scanner = StringScanner.new(string.strip)
|
33
|
+
|
34
|
+
catch(:invalid) do
|
35
|
+
return parse_links(scanner)
|
36
|
+
end
|
37
|
+
|
38
|
+
fail ArgumentError,
|
39
|
+
"Invalid token at #{scanner.pos}: '#{scanner.rest}'"
|
40
|
+
end
|
41
|
+
|
42
|
+
private
|
43
|
+
|
44
|
+
def parse_links(scanner)
|
45
|
+
links = []
|
46
|
+
loop do
|
47
|
+
if (link = parse_link(scanner))
|
48
|
+
links << link
|
49
|
+
elsif scanner.eos?
|
50
|
+
return links
|
51
|
+
else
|
52
|
+
throw :invalid
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
def parse_link(scanner)
|
58
|
+
if (m = scanner.scan(REGEXP_URI))
|
59
|
+
uri = Addressable::Template.new(m.strip[1..-2])
|
60
|
+
params = parse_params(scanner)
|
61
|
+
new uri, params
|
62
|
+
else
|
63
|
+
false
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
def parse_params(scanner)
|
68
|
+
params = {}
|
69
|
+
loop do
|
70
|
+
if (p = parse_param(scanner))
|
71
|
+
params[p[0]] = p[1]
|
72
|
+
elsif scanner.scan(/,\s*/) || scanner.eos?
|
73
|
+
return params
|
74
|
+
else
|
75
|
+
throw :invalid
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
def parse_param(scanner)
|
81
|
+
if (m = scanner.scan(REGEXP_PAR))
|
82
|
+
key = m.strip[1..-2].strip
|
83
|
+
[key, parse_value(scanner)]
|
84
|
+
else
|
85
|
+
false
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
def parse_value(scanner)
|
90
|
+
if (m = scanner.scan(REGEXP_QUT))
|
91
|
+
m.strip[1..-2]
|
92
|
+
elsif (m = scanner.scan(REGEXP_ARG))
|
93
|
+
m.strip
|
94
|
+
else
|
95
|
+
throw :invalid
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
module Restify
|
2
|
+
#
|
3
|
+
class Relation
|
4
|
+
def initialize(client, uri_template)
|
5
|
+
@client = client
|
6
|
+
@template = if uri_template.is_a?(Addressable::Template)
|
7
|
+
uri_template
|
8
|
+
else
|
9
|
+
Addressable::Template.new(uri_template)
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
def get(opts = {})
|
14
|
+
request :get, opts
|
15
|
+
end
|
16
|
+
|
17
|
+
def post(opts = {})
|
18
|
+
request :post, opts
|
19
|
+
end
|
20
|
+
|
21
|
+
private
|
22
|
+
|
23
|
+
attr_reader :client, :template
|
24
|
+
|
25
|
+
def request(method, opts = {})
|
26
|
+
keys = template.variables - Client::RESERVED_KEYS
|
27
|
+
params = opts.except!(keys)
|
28
|
+
uri = template.expand(params)
|
29
|
+
|
30
|
+
client.request method, uri, opts
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
module Restify
|
2
|
+
#
|
3
|
+
module Relations
|
4
|
+
#
|
5
|
+
# Check if resource has a relation with given name.
|
6
|
+
#
|
7
|
+
# @param name [String, Symbol] Relation name.
|
8
|
+
# @return [Boolean] True if resource has relation, false otherwise.
|
9
|
+
#
|
10
|
+
def rel?(name)
|
11
|
+
relations.key? name
|
12
|
+
end
|
13
|
+
alias_method :relation?, :rel?
|
14
|
+
alias_method :has_rel?, :rel?
|
15
|
+
alias_method :has_relation?, :rel?
|
16
|
+
|
17
|
+
# Return relation with given name.
|
18
|
+
#
|
19
|
+
# @param name [String, Symbol] Relation name.
|
20
|
+
# @return [Relation] Relation.
|
21
|
+
#
|
22
|
+
def rel(name)
|
23
|
+
relations.fetch name
|
24
|
+
end
|
25
|
+
alias_method :relation, :rel
|
26
|
+
|
27
|
+
# Hash of all known relations.
|
28
|
+
#
|
29
|
+
# @return [HashWithIndifferentAccess<String, Relation>] Relations.
|
30
|
+
#
|
31
|
+
def relations
|
32
|
+
@relations ||= HashWithIndifferentAccess.new
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
module Restify
|
2
|
+
#
|
3
|
+
class Request
|
4
|
+
#
|
5
|
+
# HTTP method.
|
6
|
+
#
|
7
|
+
# @return [String] HTTP method.
|
8
|
+
#
|
9
|
+
attr_reader :method
|
10
|
+
|
11
|
+
# Request URI.
|
12
|
+
#
|
13
|
+
# @return [String] Request URI.
|
14
|
+
#
|
15
|
+
attr_reader :uri
|
16
|
+
|
17
|
+
# Request data.
|
18
|
+
#
|
19
|
+
attr_reader :data
|
20
|
+
|
21
|
+
def initialize(opts = {})
|
22
|
+
@method = opts.fetch(:method, :get)
|
23
|
+
@uri = opts.fetch(:uri) { fail ArgumentError, ':uri required.' }
|
24
|
+
@data = opts.fetch(:data, nil)
|
25
|
+
end
|
26
|
+
|
27
|
+
def body
|
28
|
+
@body ||= MultiJson.dump(data)
|
29
|
+
end
|
30
|
+
|
31
|
+
def headers
|
32
|
+
{
|
33
|
+
'Content-Type' => 'application/json'
|
34
|
+
}
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,131 @@
|
|
1
|
+
module Restify
|
2
|
+
#
|
3
|
+
class Resource
|
4
|
+
include Relations
|
5
|
+
|
6
|
+
# Return content Media-Type.
|
7
|
+
#
|
8
|
+
# @return [MediaType] Resource media type.
|
9
|
+
#
|
10
|
+
attr_reader :media_type
|
11
|
+
|
12
|
+
# @!method [](key)
|
13
|
+
# Retrieve value for given key.
|
14
|
+
#
|
15
|
+
# @param key [String, Symbol] Data key.
|
16
|
+
# @return [Object] Data value for given key.
|
17
|
+
#
|
18
|
+
delegate :[], to: :attributes
|
19
|
+
|
20
|
+
# @!method key?(name)
|
21
|
+
# Check if resource has given key.
|
22
|
+
#
|
23
|
+
# @param name [String, Symbol] Key name.
|
24
|
+
# @return [Boolean] True if resource contains key, false otherwise.
|
25
|
+
#
|
26
|
+
delegate :key?, :has_key?, to: :attributes
|
27
|
+
|
28
|
+
# @!method each
|
29
|
+
# Iterate over keys and values or return enumerator.
|
30
|
+
#
|
31
|
+
# @overload each
|
32
|
+
# Calls block once for each key, passing the key-value pair as
|
33
|
+
# parameters.
|
34
|
+
#
|
35
|
+
# @yield [key, value] Yield for each key-value pair.
|
36
|
+
# @yieldparam key [String] Attribute key.
|
37
|
+
# @yieldparam value [Object] Attribute value.
|
38
|
+
#
|
39
|
+
# @overload each
|
40
|
+
# Return enumerator for each key-value pair.
|
41
|
+
#
|
42
|
+
# @return [Enumerator] Enumerator for each key-value pair.
|
43
|
+
#
|
44
|
+
delegate :each, to: :attributes
|
45
|
+
|
46
|
+
# Return parsed resource attributes as hash.
|
47
|
+
#
|
48
|
+
# @return [HashWithIndifferentAccess] Attribute hash.
|
49
|
+
#
|
50
|
+
def attributes
|
51
|
+
@attributes ||= HashWithIndifferentAccess.new
|
52
|
+
end
|
53
|
+
|
54
|
+
# @!method status
|
55
|
+
# Return response status if available.
|
56
|
+
#
|
57
|
+
# @return [Symbol] Response status.
|
58
|
+
# @see Response#status
|
59
|
+
#
|
60
|
+
delegate :status, to: :@response, allow_nil: true
|
61
|
+
|
62
|
+
# @!method code
|
63
|
+
# Return response status code if available.
|
64
|
+
#
|
65
|
+
# @return [Fixnum] Response status code.
|
66
|
+
# @see Response#code
|
67
|
+
#
|
68
|
+
delegate :code, to: :@response, allow_nil: true
|
69
|
+
|
70
|
+
# Follow the Location header from the response of
|
71
|
+
# this resource if available.
|
72
|
+
#
|
73
|
+
# @return [Obligation<Resource>] Followed resource.
|
74
|
+
#
|
75
|
+
def follow
|
76
|
+
if @response && @response.headers['LOCATION']
|
77
|
+
@client.request :get, @response.headers['LOCATION']
|
78
|
+
else
|
79
|
+
raise RuntimeError.new 'Nothing to follow.'
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
def initialize(client, relations = {}, attributes = {}, response = nil)
|
84
|
+
@client = client
|
85
|
+
@response = response
|
86
|
+
@relations = HashWithIndifferentAccess.new relations
|
87
|
+
@attributes = HashWithIndifferentAccess.new attributes
|
88
|
+
end
|
89
|
+
|
90
|
+
class << self
|
91
|
+
#
|
92
|
+
# Build a resource from given response.
|
93
|
+
#
|
94
|
+
def create(client, data, response)
|
95
|
+
relations = {}
|
96
|
+
relations = response.relations(client) if response
|
97
|
+
|
98
|
+
hash = {}
|
99
|
+
if data
|
100
|
+
data.each do |key, value|
|
101
|
+
hash[key] = case value
|
102
|
+
when Array
|
103
|
+
Collection.create(client, value, nil)
|
104
|
+
when Hash
|
105
|
+
Resource.create(client, value, nil)
|
106
|
+
else
|
107
|
+
value
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
data.keys.each do |key|
|
112
|
+
name = nil
|
113
|
+
if (m = /\A(\w+)_url\z/.match(key))
|
114
|
+
name = m[1]
|
115
|
+
elsif key == 'url'
|
116
|
+
name = :self
|
117
|
+
else
|
118
|
+
next
|
119
|
+
end
|
120
|
+
|
121
|
+
unless relations.key?(name) || data[key].to_s.blank?
|
122
|
+
relations[name] = Relation.new(client, data[key].to_s)
|
123
|
+
end
|
124
|
+
end
|
125
|
+
end
|
126
|
+
|
127
|
+
new client, relations, hash, response
|
128
|
+
end
|
129
|
+
end
|
130
|
+
end
|
131
|
+
end
|