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