ALD 0.1.0 → 0.1.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.
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