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 +4 -4
- data/lib/ALD/api.rb +285 -0
- data/lib/ALD/collection.rb +96 -0
- data/lib/ALD/collection_entry.rb +77 -0
- data/lib/ALD/conditioned.rb +216 -0
- data/lib/ALD/item.rb +186 -0
- data/lib/ALD/item_collection.rb +169 -0
- data/lib/ALD/local_filter.rb +136 -0
- data/lib/ALD/user.rb +94 -0
- data/lib/ALD/user_collection.rb +140 -0
- metadata +12 -4
- data/lib/ALD/schema.xsd +0 -340
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: df517c6ce5077f688d4b97fadcfa88c435007098
|
4
|
+
data.tar.gz: 43279871802cbc8bd078f8cf5b6f07a193988c73
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: a4d738c9afd6f50106cbc3c0422c84e53a7ce1c287c0abe1cd10bb544778c1ac114b3b7dbeeeb285e404054c6c542c6604d1d02fa6b229f839bb7deac70dbc5c
|
7
|
+
data.tar.gz: 4806c2dd60bc0a75ccc8d3b5f1aa7e788bc1063a1bf92810be1d107c3647c0363a007bbeefc82ae4b7b7bc8cbf05e53e262a4c3b7bb2db8156926e4f95dc2d99
|
data/lib/ALD/api.rb
ADDED
@@ -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
|