ALD 0.1.0 → 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 445063876e8fc4783ef501709c428536506a6dd2
4
- data.tar.gz: dada4d739c9e78df7e9d5137ba84fe041fe97303
3
+ metadata.gz: df517c6ce5077f688d4b97fadcfa88c435007098
4
+ data.tar.gz: 43279871802cbc8bd078f8cf5b6f07a193988c73
5
5
  SHA512:
6
- metadata.gz: bda2ebf48adbcf07d5a9e8b7daea6f82d46d5d4e9d26bf2863b2e9fd73c41c0be14e4836b22b655ad3aebef1cb461129f31f8f67e380fbca9eb173a17a1aed30
7
- data.tar.gz: e4d7807db45aba618920471ae6406193e268ebfdd905bd2bb4f51b237c6538589e8740231c4efec5818690fc250c81ed26797b76203077a1e8fb842250dbd22f
6
+ metadata.gz: a4d738c9afd6f50106cbc3c0422c84e53a7ce1c287c0abe1cd10bb544778c1ac114b3b7dbeeeb285e404054c6c542c6604d1d02fa6b229f839bb7deac70dbc5c
7
+ data.tar.gz: 4806c2dd60bc0a75ccc8d3b5f1aa7e788bc1063a1bf92810be1d107c3647c0363a007bbeefc82ae4b7b7bc8cbf05e53e262a4c3b7bb2db8156926e4f95dc2d99
@@ -0,0 +1,285 @@
1
+ require_relative 'item_collection'
2
+ require_relative 'user_collection'
3
+ require_relative 'item'
4
+ require_relative 'user'
5
+ require_relative 'exceptions'
6
+
7
+ require 'net/http'
8
+ require 'net/http/digest_auth'
9
+ require 'json'
10
+
11
+ module ALD
12
+ # Public: Access the ALD API programatically.
13
+ class API
14
+
15
+ # Public: Create a new instance to access an ALD server.
16
+ #
17
+ # root_url - a String pointing to the root URL of the server's API.
18
+ #
19
+ # Example
20
+ #
21
+ # api = ALD::API.new('http://api.my_ald_server.com/v1/')
22
+ def initialize(root_url)
23
+ @root_url = URI(root_url)
24
+ @item_store, @user_store = {}, {}
25
+ end
26
+
27
+ # Public: Get current authentication information Hash (see #auth=)
28
+ attr_reader :auth
29
+
30
+ # Public: Set authentication information for future requests.
31
+ #
32
+ # auth - a Hash containing the authentication information:
33
+ # :name - the user name to use
34
+ # :password - the plaintext password to use
35
+ #
36
+ # Returns the hash that was passed.
37
+ #
38
+ # Raises ArgumentError if the passed hash does not have the specified keys.
39
+ def auth=(auth)
40
+ raise ArgumentError unless valid_auth?(auth)
41
+ @auth = auth
42
+ end
43
+
44
+ # Public: Get a collection of items from this server. This calls
45
+ # ItemCollection#where on the collection of all items. This method might
46
+ # trigger a HTTP request.
47
+ #
48
+ # conditions - a Hash of conditions the items should meet.
49
+ # Valid conditions are documented at ItemCollection#where.
50
+ #
51
+ # Example
52
+ #
53
+ # api.items.each { |i| puts i.name }
54
+ # api.items(name: 'MyApp') # equivalent to api.items.where(name: 'MyApp')
55
+ #
56
+ # Returns an ALD::API::ItemCollection containing the items.
57
+ #
58
+ # Raises ArgumentError if the specified conditions are invalid.
59
+ def items(conditions = nil)
60
+ @all_items ||= ItemCollection.new(self)
61
+ @all_items.where(conditions)
62
+ end
63
+
64
+ # Public: Get a collection of users on this server. This calls
65
+ # UserCollection#where on the collection of all users. This method might
66
+ # trigger a HTTP request.
67
+ #
68
+ # conditions - a Hash of conditions the users should meet.
69
+ # Valid conditions are documented at UserCollection#where.
70
+ #
71
+ # Returns an ALD::API::UserCollection containing the users.
72
+ def users(conditions = nil)
73
+ @all_users ||= UserCollection.new(self)
74
+ @all_users.where(conditions)
75
+ end
76
+
77
+ # Public: Get the API version supported by the server. This method triggers a HTTP
78
+ # request.
79
+ #
80
+ # Returns the semver version string of the API.
81
+ def version
82
+ @version ||= request('/version')['version']
83
+ end
84
+
85
+ # Public: Get an individual item. This method is roughly equivalent to calling
86
+ # ItemCollection#[] on API#items. Calling this method might trigger a HTTP
87
+ # request.
88
+ #
89
+ # Examples
90
+ #
91
+ # api.item('185d265f24654545aad3f88e8a383339')
92
+ # api.item('MyApp', '0.9.5')
93
+ #
94
+ # # unlike ItemCollection#[], this also supports passing a Hash (and a Boolean):
95
+ # api.item({'id' => '185d265f24654545aad3f88e8a383339',
96
+ # 'name' => 'MyApp',
97
+ # 'version' => '4.5.6'})
98
+ # # However, this last form is only meant to be used internally and should
99
+ # # never be called by library consumers.
100
+ #
101
+ # Returns the ALD::API::Item instance representing the item, or nil if not
102
+ # found.
103
+ #
104
+ # Raises ArgumentError if the arguments are not of one of the supported forms.
105
+ #
106
+ # Signature
107
+ #
108
+ # item(id)
109
+ # item(name, version)
110
+ #
111
+ # id - the GUID String of the item to return
112
+ # name - a String containing the item's name
113
+ # version - a String containing the item's semver version
114
+ def item(*args)
115
+ if args.first.is_a? Hash # used internally to avoid multiple Item instances
116
+ args.first['id'] = normalize_id(args.first['id'])
117
+ @item_store[args.first['id']] ||= Item.new(self, *args)
118
+ elsif args.length == 1 && args.first.is_a?(String) # GUID
119
+ @item_store[normalize_id(args.first)] || items[args.first]
120
+ elsif args.length == 2 # name and version
121
+ items[*args]
122
+ else
123
+ raise ArgumentError
124
+ end
125
+ end
126
+
127
+ # Public: Get an individual user. This method is roughly equivalent to calling
128
+ # UserCollection#[] on API#users. Calling this method might trigger a HTTP
129
+ # request.
130
+ #
131
+ # Examples
132
+ #
133
+ # api.user('6a309ac8a4304f5cb1e6a2982f680ca5')
134
+ # api.user('Bob')
135
+ #
136
+ # # As #item, this method also supports being passed a Hash (and a Boolean),
137
+ # # which should only be used internally.
138
+ #
139
+ # Returns the ALD::API::User instance representing the user, or nil if not
140
+ # found.
141
+ #
142
+ # Raises ArgumentError if the arguments are not of one of the supported forms.
143
+ #
144
+ # Signature
145
+ #
146
+ # user(id)
147
+ # user(name)
148
+ #
149
+ # id - a 32-character GUID string containing the user's ID
150
+ # name - a String containing the user's name
151
+ def user(*args)
152
+ if args.first.is_a? Hash # used internally to avoid multiple User instances
153
+ args.first['id'] = normalize_id(args.first['id'])
154
+ @user_store[args.first['id']] ||= User.new(self, *args)
155
+ elsif args.length == 1 && args.first.is_a?(String)
156
+ @user_store[normalize_id(args.first)] || users[args.first]
157
+ else
158
+ raise ArgumentError
159
+ end
160
+ end
161
+
162
+ # Internal: Given a GUID string, bring it into a standardized form. This
163
+ # is used internally to make comparing GUIDs easier.
164
+ #
165
+ # id - the GUID String to normalize
166
+ #
167
+ # Returns the normalized GUID String.
168
+ def normalize_id(id)
169
+ id.upcase.gsub(/[^0-9A-F]/, '')
170
+ end
171
+
172
+ # Internal: The default headers to be used in #request.
173
+ DEFAULT_HEADERS = {
174
+ 'Accept' => 'application/json'
175
+ }
176
+
177
+ # Internal: Make a raw request to the ALD server. This is used internally,
178
+ # and library consumers should only call it if they are familiar with the
179
+ # ALD API.
180
+ #
181
+ # url - the URL String, relative to the root URL, to request against.
182
+ # method - a Symbol indicating the HTTP method to use. Supported: :get, :post
183
+ # headers - a Hash containing additional headers (for the defaults see
184
+ # ::DEFAULT_HEADERS).
185
+ # body - If method is :post, a request body to use.
186
+ #
187
+ # Returns The response body; as Hash / Array if the 'Content-type' header
188
+ # indicates a JSON response; as raw String otherwise.
189
+ #
190
+ # Raises ArgumentError is method is not supported.
191
+ #
192
+ # Raises API::RequestError if the response code is not in (200...300).
193
+ def request(url, method = :get, headers = {}, body = nil)
194
+ Net::HTTP.start(@root_url.host, @root_url.port) do |http|
195
+ url = @root_url + url
196
+
197
+ request = create_request(method, url)
198
+ DEFAULT_HEADERS.merge(headers).each do |k, v|
199
+ request[k] = v
200
+ end
201
+
202
+ response = http.request(request)
203
+ response = request_with_auth(http, request, url, response) if response.code.to_i == 401
204
+
205
+ raise RequestError unless (200...300).include?(response.code.to_i)
206
+ if response['Content-type'].include?('application/json')
207
+ JSON.parse(response.body)
208
+ else
209
+ response.body
210
+ end
211
+ end
212
+ end
213
+
214
+ private
215
+
216
+ # Internal: Create a new Net::HTTPRequest for the given method.
217
+ #
218
+ # method - a Symbol indicating the HTTP verb (lowercase
219
+ # url - the URI to request
220
+ #
221
+ # Returns a Net::HTTPRequest for the given method.
222
+ #
223
+ # Raises ArgumentError if the verb is not supported.
224
+ def create_request(method, url)
225
+ case method
226
+ when :get
227
+ Net::HTTP::Get.new url.request_uri
228
+ when :post
229
+ Net::HTTP::Post.new url.request_uri
230
+ else
231
+ raise ArgumentError
232
+ end
233
+ end
234
+
235
+ # Internal: Retry a request with authentication
236
+ #
237
+ # http - a Net::HTTP object to use for the request
238
+ # request - the Net::HTTPRequest to use
239
+ # url - the URI that is requested
240
+ # failed_response - the response that was given when requesting without auth
241
+ #
242
+ # Returns a successful Net::HTTPResponse for the request.
243
+ #
244
+ # Raises NoAuthError if @auth is not set.
245
+ #
246
+ # Raises UnsupportedAuthMethodError if the server uses a not supported auth
247
+ # method.
248
+ #
249
+ # Raises InvalidAuthError if the authenticated request yields a 401 response.
250
+ def request_with_auth(http, request, url, failed_response)
251
+ raise NoAuthError if @auth.nil?
252
+ case auth_method(failed_response)
253
+ when :basic
254
+ request.basic_auth(@auth[:name], @auth[:password])
255
+ when :digest
256
+ url.user, url.password = @auth[:name], @auth[:password]
257
+ request.add_field 'Authorization', Net::HTTP::DigestAuth.new.auth_header(url, failed_response['WWW-Authenticate'], request.method)
258
+ else
259
+ raise UnsupportedAuthMethodError
260
+ end
261
+
262
+ response = http.request(request)
263
+ raise InvalidAuthError if response.code.to_i == 401
264
+ response
265
+ end
266
+
267
+ # Internal: Get the authentication method used by a server
268
+ #
269
+ # response - the Net::HTTPResponse to examine
270
+ #
271
+ # Returns the used method as Symbol, e.g. :digest or :basic
272
+ def auth_method(response)
273
+ response['WWW-Authenticate'].strip.split(' ')[0].downcase.to_sym
274
+ end
275
+
276
+ # Internal: Check if a Hash contains valid auth data
277
+ #
278
+ # auth - the Hash to check
279
+ #
280
+ # Returns true, if the Hash contains the necessary keys, false otherwise.
281
+ def valid_auth?(auth)
282
+ auth.is_a?(Hash) && %w[name password].all? { |k| auth.key?(k.to_sym) }
283
+ end
284
+ end
285
+ end
@@ -0,0 +1,96 @@
1
+ module ALD
2
+ class API
3
+ # Internal: Base class for collections of entries returned by a request to
4
+ # the ALD API.
5
+ #
6
+ # Child classes inheriting from this class must support:
7
+ #
8
+ # @data - Array of Hashes necessary to create a new
9
+ # entry of this collection, initially nil
10
+ # #entry_filter - From the given argument array, make a filter
11
+ # Hash that identifies the entry indicated by
12
+ # the arguments.
13
+ # #entry(hash, complete) - create a new entry from the given Hash
14
+ # #request - load the @data Array
15
+ # #request_entry(filter) - From the given filter Hash, load all
16
+ # information regarding the entry identified by
17
+ # it.
18
+ class Collection
19
+
20
+ # This class includes the Enumerable module.
21
+ include Enumerable
22
+
23
+ # Internal: Create a new Collection
24
+ #
25
+ # api - the ALD::API instance this collection belongs to
26
+ # conditions - a Hash of conditions entries in this collection must meet
27
+ # data - an Array of Hashes for @data. May be nil.
28
+ def initialize(api, conditions = {}, data = nil)
29
+ @api, @conditions, @data = api, conditions, data
30
+ end
31
+
32
+ # Public: Iterate over the entries in this collection
33
+ #
34
+ # Yields an entry, as returned by #entry
35
+ def each
36
+ request unless initialized?
37
+ @data.each do |hash|
38
+ yield entry(hash)
39
+ end
40
+ end
41
+
42
+ # Internal: Access an entry in the collection.
43
+ #
44
+ # Actual arguments and behaviour depends on child classes.
45
+ #
46
+ # Returns an entry of the collection, or nil if none is found.
47
+ #
48
+ # Raises ArgumentError if the given arguments are invalid.
49
+ def [](*args)
50
+ if args.length == 1 && args.first.is_a?(Integer)
51
+ request unless initialized?
52
+ entry(@data[args.first])
53
+ else
54
+ filter = entry_filter(args)
55
+
56
+ if initialized?
57
+ entry = @data.find { |hash| filter.keys.all? { |k| hash[k.to_s] == filter[k] } }
58
+ full_entry = false
59
+ else
60
+ entry = request_entry(filter)
61
+ full_entry = true
62
+ end
63
+
64
+ entry.nil? ? nil : entry(entry, full_entry)
65
+ end
66
+ end
67
+
68
+ # Public: Indicate if all data in this collection is present. If false,
69
+ # accessing an entry or iterating over entries in this collection may
70
+ # trigger a HTTP request.
71
+ #
72
+ # Returns a Boolean; true if all data is present, false otherwise
73
+ def initialized?
74
+ !@data.nil?
75
+ end
76
+
77
+ private
78
+
79
+ # Internal: Get filter conditions for an entry. Used by #[] to get an
80
+ # entry based on the given arguments.
81
+ #
82
+ # This method is a mere placeholder. Child classes must override it to
83
+ # implement their access semantics for entries.
84
+ #
85
+ # args - an Array of arguments to convert into conditions
86
+ #
87
+ # Returns the Hash of conditions, where each key represents a property
88
+ # of the entry to be found that must equal the corresponding value.
89
+ #
90
+ # Raises ArgumentError if the arguments cannot be converted.
91
+ def entry_filter(args)
92
+ {}
93
+ end
94
+ end
95
+ end
96
+ end
@@ -0,0 +1,77 @@
1
+ module ALD
2
+ class API
3
+ # Internal: Base class for entries in a collection returned by the ALD API.
4
+ # This class is used internally and should not be called by library
5
+ # consumers.
6
+ #
7
+ # Child classes inheriting from this class must support:
8
+ #
9
+ # @data - a Hash containing the entry's data
10
+ # @initialized - a Boolean indicating whether @data is yet complete or
11
+ # not
12
+ # #request - load missing information into @data
13
+ class CollectionEntry
14
+ # Internal: Create a new entry with the given data
15
+ #
16
+ # api - the ALD:API instance this entry belongs to
17
+ # data - the initial, possibly uncomplete @data hash
18
+ # initialized - a Boolean indicating whether data is already complete or
19
+ # not
20
+ def initialize(api, data, initialized = false)
21
+ @api, @data, @initialized = api, data, initialized
22
+ self.class.define_attributes!
23
+ end
24
+
25
+ # Public: Indicate whether all data concerning this entry is available
26
+ # or not. If false, a property retrieval from this entry *may* trigger a
27
+ # HTTP request.
28
+ #
29
+ # Returns a Boolean, true if all data is present, false otherwise.
30
+ def initialized?
31
+ @initialized
32
+ end
33
+
34
+ # Internal: Child classes override this to specify attributes that are
35
+ # always present in @data. For each such attribute, a retrieval method is
36
+ # dynamically defined.
37
+ #
38
+ # Returns an Array of attribute names (String)
39
+ def self.initialized_attributes
40
+ []
41
+ end
42
+
43
+ # Internal: Child classes override this to specify attributes that are
44
+ # *not* always present in @data. For each such attribute, a retrieval
45
+ # method including a call to #request is dynamically defined.
46
+ #
47
+ # Returns an Array of attribute names (String)
48
+ def self.requested_attributes
49
+ []
50
+ end
51
+
52
+ # Internal: Dynamically define attributes determined by child classes.
53
+ # This is called by ::new to define the attributes child classes define
54
+ # in ::initialized_attributes and ::requested_attributes.
55
+ #
56
+ # Returns nothing.
57
+ def self.define_attributes!
58
+ return if @attributes_defined
59
+
60
+ initialized_attributes.each do |attr|
61
+ self.send(:define_method, attr.to_sym) do
62
+ @data[attr]
63
+ end
64
+ end
65
+
66
+ requested_attributes.each do |attr|
67
+ self.send(:define_method, attr.to_sym) do
68
+ request unless initialized?
69
+ @data[attr]
70
+ end
71
+ end
72
+
73
+ @attributes_defined = true
74
+ end
75
+ end
76
+ end
77
+ end