sawyer 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,51 @@
1
+ {
2
+ "type": "object",
3
+ "relations": [
4
+ {"rel": "all", "href": "/users"},
5
+ {"rel": "create", "href": "/users", "method": "post"},
6
+ {"rel": "favorites", "schema": "/schema/nigiri"},
7
+ {"rel": "favorites/create", "method": "post"}
8
+ ],
9
+ "properties": {
10
+ "id": {
11
+ "type": "integer",
12
+ "minimum": 1,
13
+ "readonly": true
14
+ },
15
+ "login": {
16
+ "type": "string"
17
+ },
18
+ "created_at": {
19
+ "type": "string",
20
+ "pattern": "\\d{8}T\\d{6}Z",
21
+ "readonly": true
22
+ },
23
+ "_links": {
24
+ "type": "array",
25
+ "items": {
26
+ "type": "object",
27
+ "properties": {
28
+ "rel": {
29
+ "type": "string"
30
+ },
31
+ "href": {
32
+ "type": "string",
33
+ "optional": true
34
+ },
35
+ "method": {
36
+ "type": "string",
37
+ "default": "get"
38
+ },
39
+ "schema": {
40
+ "type": "string",
41
+ "optional": true
42
+ }
43
+ },
44
+ "additionalProperties": false
45
+ },
46
+ "additionalProperties": false,
47
+ "readonly": true
48
+ }
49
+ }
50
+ }
51
+
@@ -0,0 +1,14 @@
1
+ module Sawyer
2
+ VERSION = "0.0.1"
3
+
4
+ class Error < StandardError; end
5
+ end
6
+
7
+ require 'set'
8
+
9
+ %w(
10
+ resource
11
+ relation
12
+ response
13
+ agent
14
+ ).each { |f| require File.expand_path("../sawyer/#{f}", __FILE__) }
@@ -0,0 +1,82 @@
1
+ require 'faraday'
2
+ require 'uri_template'
3
+
4
+ module Sawyer
5
+ class Agent
6
+ NO_BODY = Set.new [:get, :head]
7
+
8
+ # Agents handle making the requests, and passing responses to
9
+ # Sawyer::Response.
10
+ #
11
+ # endpoint - String URI of the API entry point.
12
+ def initialize(endpoint)
13
+ @endpoint = endpoint
14
+ @conn = Faraday.new endpoint
15
+ yield @conn if block_given?
16
+ end
17
+
18
+ # Public: Hits the root of the API to get the initial actions.
19
+ #
20
+ # Returns a Sawyer::Response.
21
+ def start
22
+ call :get, @endpoint
23
+ end
24
+
25
+ # Makes a request through Faraday.
26
+ #
27
+ # method - The Symbol name of an HTTP method.
28
+ # url - The String URL to access. This can be relative to the Agent's
29
+ # endpoint.
30
+ # data - The Optional Hash or Resource body to be sent. :get or :head
31
+ # requests can have no body, so this can be the options Hash
32
+ # instead.
33
+ # options - Hash of option to configure the API request.
34
+ # :headers - Hash of API headers to set.
35
+ # :query - Hash of URL query params to set.
36
+ #
37
+ # Returns a Sawyer::Response.
38
+ def call(method, url, data = nil, options = nil)
39
+ if NO_BODY.include?(method)
40
+ options ||= data
41
+ data = nil
42
+ end
43
+
44
+ options ||= {}
45
+ url = URITemplate.new(url).expand(options[:uri] || {})
46
+ res = @conn.send method, url do |req|
47
+ req.body = encode_body(data) if data
48
+ if params = options[:query]
49
+ req.params.update params
50
+ end
51
+ if headers = options[:headers]
52
+ req.headers.update headers
53
+ end
54
+ end
55
+
56
+ Response.new self, res
57
+ end
58
+
59
+ # Encodes an object to a string for the API request.
60
+ #
61
+ # data - The Hash or Resource that is being sent.
62
+ #
63
+ # Returns a String.
64
+ def encode_body(data)
65
+ Yajl.dump data
66
+ end
67
+
68
+ # Decodes a String response body to a resource.
69
+ #
70
+ # str - The String body from the response.
71
+ #
72
+ # Returns an Object resource (Hash by default).
73
+ def decode_body(str)
74
+ Yajl.load str, :symbolize_keys => true
75
+ end
76
+
77
+ def inspect
78
+ %(<#{self.class} #{@endpoint}>)
79
+ end
80
+ end
81
+ end
82
+
@@ -0,0 +1,253 @@
1
+ module Sawyer
2
+ class Relation
3
+ class Map
4
+ # Tracks the available next actions for a resource, and
5
+ # issues requests for them.
6
+ def initialize
7
+ @map = {}
8
+ end
9
+
10
+ # Adds a Relation to the map.
11
+ #
12
+ # rel - A Relation.
13
+ #
14
+ # Returns nothing.
15
+ def <<(rel)
16
+ @map[rel.name] = rel
17
+ end
18
+
19
+ # Gets the raw Relation by its name.
20
+ #
21
+ # key - The Symbol name of the Relation.
22
+ #
23
+ # Returns a Relation.
24
+ def [](key)
25
+ @map[key.to_sym]
26
+ end
27
+
28
+ # Gets the number of mapped Relations.
29
+ #
30
+ # Returns an Integer.
31
+ def size
32
+ @map.size
33
+ end
34
+
35
+ # Gets a list of the Relation names.
36
+ #
37
+ # Returns an Array of Symbols in no specific order.
38
+ def keys
39
+ @map.keys
40
+ end
41
+
42
+ def inspect
43
+ %(#<#{self.class}: #{@map.keys.inspect}>)
44
+ end
45
+ end
46
+
47
+ attr_reader :agent,
48
+ :name,
49
+ :href,
50
+ :method,
51
+ :available_methods
52
+
53
+ # Public: Builds an index of Relations from the value of a `_links`
54
+ # property in a resource. :get is the default method. Any links with
55
+ # multiple specified methods will get multiple relations created.
56
+ #
57
+ # index - The Hash mapping Relation names to the Hash Relation
58
+ # options.
59
+ # rels - A Relation::Map to store the Relations.
60
+ #
61
+ # Returns a Relation::Map
62
+ def self.from_links(agent, index, rels = Map.new)
63
+ if index.is_a?(Array)
64
+ raise ArgumentError, "Links must be a hash of rel => {_href => '...'}: #{index.inspect}"
65
+ end
66
+
67
+ index.each do |name, options|
68
+ rels << from_link(agent, name, options)
69
+ end if index
70
+
71
+ rels
72
+ end
73
+
74
+ # Public: Builds a single Relation from the given options. These are
75
+ # usually taken from a `_links` property in a resource.
76
+ #
77
+ # agent - The Sawyer::Agent that made the request.
78
+ # name - The Symbol name of the Relation.
79
+ # options - A Hash containing the other Relation properties.
80
+ # :href - The String URL of the next action's location.
81
+ # :method - The optional String HTTP method.
82
+ #
83
+ # Returns a Relation.
84
+ def self.from_link(agent, name, options)
85
+ new agent, name, options[:href], options[:method]
86
+ end
87
+
88
+ # A Relation represents an available next action for a resource.
89
+ #
90
+ # agent - The Sawyer::Agent that made the request.
91
+ # name - The Symbol name of the relation.
92
+ # href - The String URL of the location of the next action.
93
+ # method - The Symbol HTTP method. Default: :get
94
+ def initialize(agent, name, href, method = nil)
95
+ @agent = agent
96
+ @name = name.to_sym
97
+ @href = href.to_s
98
+
99
+ methods = nil
100
+
101
+ if method.is_a? String
102
+ if method.size.zero?
103
+ method = nil
104
+ else
105
+ method.downcase!
106
+ methods = method.split(',').map! do |m|
107
+ m.strip!
108
+ m.to_sym
109
+ end
110
+ method = methods.first
111
+ end
112
+ end
113
+
114
+ @method = (method || :get).to_sym
115
+ @available_methods = Set.new methods || [@method]
116
+ end
117
+
118
+ # Public: Makes an API request with the curent Relation using HEAD.
119
+ #
120
+ # data - The Optional Hash or Resource body to be sent. :get or :head
121
+ # requests can have no body, so this can be the options Hash
122
+ # instead.
123
+ # options - Hash of option to configure the API request.
124
+ # :headers - Hash of API headers to set.
125
+ # :query - Hash of URL query params to set.
126
+ # :method - Symbol HTTP method.
127
+ #
128
+ # Returns a Sawyer::Response.
129
+ def head(options = nil)
130
+ options ||= {}
131
+ options[:method] = :head
132
+ call options
133
+ end
134
+
135
+ # Public: Makes an API request with the curent Relation using GET.
136
+ #
137
+ # data - The Optional Hash or Resource body to be sent. :get or :head
138
+ # requests can have no body, so this can be the options Hash
139
+ # instead.
140
+ # options - Hash of option to configure the API request.
141
+ # :headers - Hash of API headers to set.
142
+ # :query - Hash of URL query params to set.
143
+ # :method - Symbol HTTP method.
144
+ #
145
+ # Returns a Sawyer::Response.
146
+ def get(options = nil)
147
+ options ||= {}
148
+ options[:method] = :get
149
+ call options
150
+ end
151
+
152
+ # Public: Makes an API request with the curent Relation using POST.
153
+ #
154
+ # data - The Optional Hash or Resource body to be sent.
155
+ # options - Hash of option to configure the API request.
156
+ # :headers - Hash of API headers to set.
157
+ # :query - Hash of URL query params to set.
158
+ # :method - Symbol HTTP method.
159
+ #
160
+ # Returns a Sawyer::Response.
161
+ def post(data = nil, options = nil)
162
+ options ||= {}
163
+ options[:method] = :post
164
+ call data, options
165
+ end
166
+
167
+ # Public: Makes an API request with the curent Relation using PUT.
168
+ #
169
+ # data - The Optional Hash or Resource body to be sent.
170
+ # options - Hash of option to configure the API request.
171
+ # :headers - Hash of API headers to set.
172
+ # :query - Hash of URL query params to set.
173
+ # :method - Symbol HTTP method.
174
+ #
175
+ # Returns a Sawyer::Response.
176
+ def put(data = nil, options = nil)
177
+ options ||= {}
178
+ options[:method] = :put
179
+ call data, options
180
+ end
181
+
182
+ # Public: Makes an API request with the curent Relation using PATCH.
183
+ #
184
+ # data - The Optional Hash or Resource body to be sent.
185
+ # options - Hash of option to configure the API request.
186
+ # :headers - Hash of API headers to set.
187
+ # :query - Hash of URL query params to set.
188
+ # :method - Symbol HTTP method.
189
+ #
190
+ # Returns a Sawyer::Response.
191
+ def patch(data = nil, options = nil)
192
+ options ||= {}
193
+ options[:method] = :patch
194
+ call data, options
195
+ end
196
+
197
+ # Public: Makes an API request with the curent Relation using DELETE.
198
+ #
199
+ # data - The Optional Hash or Resource body to be sent.
200
+ # options - Hash of option to configure the API request.
201
+ # :headers - Hash of API headers to set.
202
+ # :query - Hash of URL query params to set.
203
+ # :method - Symbol HTTP method.
204
+ #
205
+ # Returns a Sawyer::Response.
206
+ def delete(data = nil, options = nil)
207
+ options ||= {}
208
+ options[:method] = :delete
209
+ call data, options
210
+ end
211
+
212
+ # Public: Makes an API request with the curent Relation using OPTIONS.
213
+ #
214
+ # data - The Optional Hash or Resource body to be sent.
215
+ # options - Hash of option to configure the API request.
216
+ # :headers - Hash of API headers to set.
217
+ # :query - Hash of URL query params to set.
218
+ # :method - Symbol HTTP method.
219
+ #
220
+ # Returns a Sawyer::Response.
221
+ def options(data = nil, opt = nil)
222
+ opt ||= {}
223
+ opt[:method] = :options
224
+ call data, opt
225
+ end
226
+
227
+ # Public: Makes an API request with the curent Relation.
228
+ #
229
+ # data - The Optional Hash or Resource body to be sent. :get or :head
230
+ # requests can have no body, so this can be the options Hash
231
+ # instead.
232
+ # options - Hash of option to configure the API request.
233
+ # :headers - Hash of API headers to set.
234
+ # :query - Hash of URL query params to set.
235
+ # :method - Symbol HTTP method.
236
+ #
237
+ # Raises ArgumentError if the :method value is not in @available_methods.
238
+ # Returns a Sawyer::Response.
239
+ def call(data = nil, options = nil)
240
+ m = options && options[:method]
241
+ if m && !@available_methods.include?(m == :head ? :get : m)
242
+ raise ArgumentError, "method #{m.inspect} is not available: #{@available_methods.to_a.inspect}"
243
+ end
244
+
245
+ @agent.call m || @method, @href, data, options
246
+ end
247
+
248
+ def inspect
249
+ %(#<#{self.class}: #{@name}: #{@method} #{@href}>)
250
+ end
251
+ end
252
+ end
253
+
@@ -0,0 +1,69 @@
1
+ module Sawyer
2
+ class Resource
3
+ SPECIAL_METHODS = Set.new %w(agent rels fields)
4
+ attr_reader :_agent, :_rels, :_fields
5
+
6
+ # Initializes a Resource with the given data.
7
+ #
8
+ # agent - The Sawyer::Agent that made the API request.
9
+ # data - Hash of key/value properties.
10
+ def initialize(agent, data)
11
+ @_agent = agent
12
+ @_rels = Relation.from_links(agent, data.delete(:_links))
13
+ @_fields = Set.new []
14
+ data.each do |key, value|
15
+ @_fields << key
16
+ instance_variable_set "@#{key}", process_value(value)
17
+ end
18
+ end
19
+
20
+ # Processes an individual value of this resource. Hashes get exploded
21
+ # into another Resource, and Arrays get their values processed too.
22
+ #
23
+ # value - An Object value of a Resource's data.
24
+ #
25
+ # Returns an Object to set as the value of a Resource key.
26
+ def process_value(value)
27
+ case value
28
+ when Hash then self.class.new(@_agent, value)
29
+ when Array then value.map { |v| process_value(v) }
30
+ else value
31
+ end
32
+ end
33
+
34
+ # Checks to see if the given key is in this resource.
35
+ #
36
+ # key - A Symbol key.
37
+ #
38
+ # Returns true if the key exists, or false.
39
+ def key?(key)
40
+ @_fields.include? key
41
+ end
42
+
43
+ ATTR_SETTER = '='.freeze
44
+ ATTR_PREDICATE = '?'.freeze
45
+
46
+ # Provides access to a resource's attributes.
47
+ def method_missing(method, *args)
48
+ attr_name, suffix = method.to_s.scan(/([a-z0-9\_]+)(\?|\=)?$/i).first
49
+ if suffix == ATTR_SETTER
50
+ (class << self; self; end).send :attr_accessor, attr_name
51
+ @_fields << attr_name.to_sym
52
+ instance_variable_set "@#{attr_name}", args.first
53
+ elsif @_fields.include?(attr_name.to_sym)
54
+ value = instance_variable_get("@#{attr_name}")
55
+ case suffix
56
+ when nil
57
+ (class << self; self; end).send :attr_accessor, attr_name
58
+ value
59
+ when ATTR_PREDICATE then !!value
60
+ end
61
+ elsif suffix.nil? && SPECIAL_METHODS.include?(attr_name)
62
+ instance_variable_get "@_#{attr_name}"
63
+ else
64
+ super
65
+ end
66
+ end
67
+ end
68
+ end
69
+