sawyer 0.0.1

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,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
+