restify 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
@@ -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,16 @@
1
+ module Restify
2
+ #
3
+ module Processors
4
+ #
5
+ # Decode JSON responses.
6
+ class JSON
7
+ def parse(str)
8
+ MultiJSON.load(str)
9
+ end
10
+
11
+ def format
12
+ :json
13
+ end
14
+ end
15
+ end
16
+ 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