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.
- data/Gemfile +3 -0
- data/LICENSE.md +20 -0
- data/README.md +17 -0
- data/Rakefile +135 -0
- data/SPEC.md +71 -0
- data/example/client.rb +50 -0
- data/example/nigiri.schema.json +47 -0
- data/example/server.rb +114 -0
- data/example/user.schema.json +51 -0
- data/lib/sawyer.rb +14 -0
- data/lib/sawyer/agent.rb +82 -0
- data/lib/sawyer/relation.rb +253 -0
- data/lib/sawyer/resource.rb +69 -0
- data/lib/sawyer/response.rb +42 -0
- data/sawyer.gemspec +78 -0
- data/test/agent_test.rb +60 -0
- data/test/helper.rb +7 -0
- data/test/relation_test.rb +109 -0
- data/test/resource_test.rb +99 -0
- data/test/response_test.rb +54 -0
- metadata +116 -0
@@ -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
|
+
|
data/lib/sawyer.rb
ADDED
data/lib/sawyer/agent.rb
ADDED
@@ -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
|
+
|