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