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