lusi_api 0.1.11
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 +7 -0
- data/.gitignore +58 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +21 -0
- data/README.md +235 -0
- data/Rakefile +8 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/lib/lusi_api.rb +5 -0
- data/lib/lusi_api/calendar.rb +234 -0
- data/lib/lusi_api/core/api.rb +190 -0
- data/lib/lusi_api/core/code.rb +101 -0
- data/lib/lusi_api/core/exceptions.rb +47 -0
- data/lib/lusi_api/core/lookup.rb +612 -0
- data/lib/lusi_api/core/util.rb +102 -0
- data/lib/lusi_api/core/xml.rb +168 -0
- data/lib/lusi_api/country.rb +111 -0
- data/lib/lusi_api/course.rb +1300 -0
- data/lib/lusi_api/enrolment.rb +247 -0
- data/lib/lusi_api/organisation.rb +291 -0
- data/lib/lusi_api/person/staff.rb +115 -0
- data/lib/lusi_api/person/student.rb +551 -0
- data/lib/lusi_api/service_account.rb +121 -0
- data/lib/lusi_api/service_method.rb +52 -0
- data/lib/lusi_api/version.rb +5 -0
- data/lib/lusi_api/vle.rb +329 -0
- data/lusi_api.gemspec +36 -0
- metadata +182 -0
@@ -0,0 +1,190 @@
|
|
1
|
+
require 'nokogiri'
|
2
|
+
require 'rest-client'
|
3
|
+
|
4
|
+
require 'lusi_api/core/exceptions'
|
5
|
+
require 'lusi_api/service_account'
|
6
|
+
|
7
|
+
|
8
|
+
|
9
|
+
module LUSI
|
10
|
+
module API
|
11
|
+
module Core
|
12
|
+
|
13
|
+
|
14
|
+
# Provides a wrapper for the LUSI web service API
|
15
|
+
class API
|
16
|
+
|
17
|
+
# Initializes a new API instance
|
18
|
+
# @param api_user [String] the service account username
|
19
|
+
# @param api_password [String] the service account password
|
20
|
+
# @param logger [Logger, nil] a Logger instance for optional logging
|
21
|
+
# @param timeout [Integer] the timeout in seconds for LUSI API calls
|
22
|
+
def initialize(api_user: nil, api_password: nil, api_root: nil, logger: nil, timeout: nil)
|
23
|
+
@api_password = api_password
|
24
|
+
@api_root = api_root || 'https://lusiservice.lancs.ac.uk'
|
25
|
+
@api_user = api_user
|
26
|
+
@logger = logger
|
27
|
+
@timeout = timeout.to_i
|
28
|
+
end
|
29
|
+
|
30
|
+
# Calls a LUSI API method and return the parsed XML response
|
31
|
+
# Any undocumented keyword parameters are passed as parameters to the LUSI API call. The LUSI 'Username' and
|
32
|
+
# 'Password' parameters are automatically added.
|
33
|
+
# @param path [String] the path of the API call (e.g. 'LUSIReference')
|
34
|
+
# @param endpoint [String] the endpoint of the API call (e.g. 'General.asmx')
|
35
|
+
# @param method [String] the method name of the API call (e.g. 'GetServiceAccountDetails')
|
36
|
+
# @param headers [Hash<String, String>] optional HTTP headers for the API call
|
37
|
+
# @param options [Hash<String, Object>] options for the REST client
|
38
|
+
# @return [Nokogiri::XML::Node] the parsed XML <body> content from the API response
|
39
|
+
# @raise [LUSIAPI::Core::APITimeoutError] if the LUSI API call times out
|
40
|
+
# @raise [LUSIAPI::Core::APICallError] if the LUSI API call fails for any other reason (e.g. network unavailable)
|
41
|
+
# @raise [LUSIAPI::Core::APIContentError] if the LUSI API call returns an XML document with an error response
|
42
|
+
def call(path, endpoint, method, headers: nil, options: nil, **params)
|
43
|
+
|
44
|
+
# Add the username and password to the params
|
45
|
+
params[:Username] ||= @api_user
|
46
|
+
params[:Password] ||= @api_password
|
47
|
+
|
48
|
+
# Set the REST client headers
|
49
|
+
rest_headers = {}.merge(headers || {})
|
50
|
+
|
51
|
+
# Set the REST client options
|
52
|
+
rest_options = {
|
53
|
+
method: :post,
|
54
|
+
headers: rest_headers,
|
55
|
+
payload: params,
|
56
|
+
url: url(path, endpoint, method),
|
57
|
+
}
|
58
|
+
rest_options[:timeout] = @timeout if @timeout > 0
|
59
|
+
rest_options.merge(options) if options
|
60
|
+
|
61
|
+
RestClient.log = @log if @log
|
62
|
+
|
63
|
+
# Make the API call
|
64
|
+
begin
|
65
|
+
response = RestClient::Request.execute(**rest_options)
|
66
|
+
rescue RestClient::Exceptions::Timeout => e
|
67
|
+
raise APITimeoutError.new(url: rest_options[:url])
|
68
|
+
rescue RestClient::Exception => e
|
69
|
+
raise APICallError.new(url: rest_options[:url])
|
70
|
+
rescue => e
|
71
|
+
raise APIError.new(url: rest_options[:url])
|
72
|
+
end
|
73
|
+
|
74
|
+
# Extract and return the XML content
|
75
|
+
xml(response)
|
76
|
+
|
77
|
+
end
|
78
|
+
|
79
|
+
# Return a ServiceAccount instance representing the API's service user
|
80
|
+
# @return [LUSI::API::ServiceAccount] the ServiceAccount instance representing the API's service user
|
81
|
+
def get_service_account_details
|
82
|
+
# There should only be one instance but get_instance returns an array for consistnecy with other classes
|
83
|
+
result = LUSI::API::ServiceAccount.get_instance(self)
|
84
|
+
result.length == 0 ? nil : result[0]
|
85
|
+
end
|
86
|
+
|
87
|
+
# Make a POST API call
|
88
|
+
# @see #call
|
89
|
+
alias post call
|
90
|
+
|
91
|
+
# Update the service user's password via the LUSI API
|
92
|
+
# @param password [String, nil] the new password (a random 16-character password is generated if this is omitted)
|
93
|
+
# @return [String] the new password
|
94
|
+
def update_service_account_password(password = nil)
|
95
|
+
@api_password = LUSI::API::ServiceAccount.update_service_account_password(self, password)
|
96
|
+
end
|
97
|
+
|
98
|
+
# Construct a full API method URL from its constituents
|
99
|
+
# @param path [String] the path of the API call (e.g. 'LUSIReference')
|
100
|
+
# @param endpoint [String] the endpoint of the API call (e.g. 'General.asmx')
|
101
|
+
# @param method [String] the method name of the API call (e.g. 'GetServiceAccountDetails')
|
102
|
+
# @return [String] the fully-qualified URL of the API method
|
103
|
+
def url(path = nil, endpoint = nil, method = nil)
|
104
|
+
"#{@api_root}/#{path}/#{endpoint}/#{method}"
|
105
|
+
end
|
106
|
+
|
107
|
+
protected
|
108
|
+
|
109
|
+
# Validate the XML response from an API call
|
110
|
+
# @param xml [Nokogiri::XML::Node] the parsed XML response
|
111
|
+
# @return [Nokogiri::XML::Node] the parsed <body> element of the XML response
|
112
|
+
# @raise [LUSI::API::Core::APIContentError] if the XML response indicates an error
|
113
|
+
def validate(xml)
|
114
|
+
header = LUSI::API::Core::XML.xml_at(xml, '//xmlns:Header')
|
115
|
+
status = LUSI::API::Core::XML.xml_content_at(header, 'xmlns:Status')
|
116
|
+
if status == 'Success'
|
117
|
+
# No errors, return the body content
|
118
|
+
LUSI::API::Core::XML.xml_at(xml, '//xmlns:Body')
|
119
|
+
else
|
120
|
+
# Errors present, raise APIContentError with the details
|
121
|
+
error = {
|
122
|
+
error_code: LUSI::API::Core::XML.xml_content_at(header, 'xmlns:ErrorCode'),
|
123
|
+
fault: LUSI::API::Core::XML.xml_content_at(header, 'xmlns:Fault'),
|
124
|
+
status: status
|
125
|
+
}
|
126
|
+
error_message = LUSI::API::Core::XML.xml_content_at(header, 'xmlns:ErrorMessage')
|
127
|
+
case error[:error_code]
|
128
|
+
when 'CLIENT004'
|
129
|
+
raise APIPermissionError.new(error_message, **error)
|
130
|
+
else
|
131
|
+
raise APIContentError.new(error_message, **error)
|
132
|
+
end
|
133
|
+
end
|
134
|
+
end
|
135
|
+
|
136
|
+
# Parse and validate the XML content from a LUSI API call
|
137
|
+
# @param response [String] the string-serialized XML response from the API
|
138
|
+
# @return [Nokogiri::XML::Document] the parsed <body> element of the XML response
|
139
|
+
# @raise [LUSIAPI::Core::APIContentError] if the XML parsing fails or the response indicates an error
|
140
|
+
def xml(response)
|
141
|
+
|
142
|
+
# Parse the HTTP response as XML
|
143
|
+
begin
|
144
|
+
xml = Nokogiri::XML(response.to_s)
|
145
|
+
rescue Nokogiri::SyntaxError => e
|
146
|
+
raise APIContentError('XML syntax error')
|
147
|
+
end
|
148
|
+
|
149
|
+
# Validate the XML and return the <body> element if successful
|
150
|
+
validate(xml)
|
151
|
+
|
152
|
+
end
|
153
|
+
|
154
|
+
end
|
155
|
+
|
156
|
+
|
157
|
+
# Mixin defining properties for a LUSI API endpoint URL, "extend LUSI::API::Core::Endpoint" to include as class
|
158
|
+
# methods
|
159
|
+
module Endpoint
|
160
|
+
|
161
|
+
# Returns the LUSI API endpoint
|
162
|
+
# @return [String] the LUSI API endpoint
|
163
|
+
def lusi_ws_endpoint
|
164
|
+
raise NotImplementedError
|
165
|
+
end
|
166
|
+
|
167
|
+
# Returns the LUSI API method
|
168
|
+
# @return [String] the LUSI API method
|
169
|
+
def lusi_ws_method
|
170
|
+
raise NotImplementedError
|
171
|
+
end
|
172
|
+
|
173
|
+
# Returns the LUSI API URL path
|
174
|
+
# @return [String] the LUSI API URL path
|
175
|
+
def lusi_ws_path
|
176
|
+
raise NotImplementedError
|
177
|
+
end
|
178
|
+
|
179
|
+
# Returns the root element name of the LUSI API XML response
|
180
|
+
# @return [String] the root XML element name
|
181
|
+
def lusi_ws_xml_root
|
182
|
+
raise NotImplementedError
|
183
|
+
end
|
184
|
+
|
185
|
+
end
|
186
|
+
|
187
|
+
|
188
|
+
end
|
189
|
+
end
|
190
|
+
end
|
@@ -0,0 +1,101 @@
|
|
1
|
+
require 'lusi_api/core/xml'
|
2
|
+
|
3
|
+
|
4
|
+
module LUSI
|
5
|
+
module API
|
6
|
+
module Core
|
7
|
+
|
8
|
+
|
9
|
+
# Represents a LUSI code definition (identity code and text description)
|
10
|
+
class BasicCode
|
11
|
+
|
12
|
+
# @!attribute [rw] description
|
13
|
+
# @return [any, nil] the text description
|
14
|
+
attr_accessor :description
|
15
|
+
|
16
|
+
# @!attribute [rw] identity
|
17
|
+
# @return [any, nil] the identity code
|
18
|
+
attr_accessor :identity
|
19
|
+
|
20
|
+
# Initialises a new Code instance
|
21
|
+
# @param xml [Nokogiri::XML::Document, Nokogiri::XML::Node] the XML root of the code from the LUSI API
|
22
|
+
# @param lookup [LUSI::API::Core::Lookup::LookupService, nil] the lookup service for object resolution
|
23
|
+
# @param identity [any, nil] the default identity code
|
24
|
+
# @param description [String, nil] the default text description
|
25
|
+
# @return [void]
|
26
|
+
def initialize(xml = nil, lookup = nil, identity: nil, description: nil)
|
27
|
+
@description = LUSI::API::Core::XML.xml_content_at(xml, 'xmlns:Description', description)
|
28
|
+
@identity = LUSI::API::Core::XML.xml_content_at(xml, 'xmlns:Identity', identity)
|
29
|
+
end
|
30
|
+
|
31
|
+
# Returns a string representation of the code
|
32
|
+
# @return [String, nil] the string representation (text description)
|
33
|
+
def to_s
|
34
|
+
@description
|
35
|
+
end
|
36
|
+
|
37
|
+
# Default get_instance implementation
|
38
|
+
# - Instances cannot be retrieved through the LUSI API
|
39
|
+
def self.get_instance(*args, **kwargs)
|
40
|
+
raise NotImplementedError
|
41
|
+
end
|
42
|
+
|
43
|
+
# Default get_instance_params implementation
|
44
|
+
# - Instances cannot be retrieved through the LUSI API
|
45
|
+
protected
|
46
|
+
|
47
|
+
def self.get_instance_params(*args, **kwargs)
|
48
|
+
{}
|
49
|
+
end
|
50
|
+
|
51
|
+
end
|
52
|
+
|
53
|
+
|
54
|
+
# Represents a LUSI code definition which can be retrieved through the LUSI API
|
55
|
+
class Code < BasicCode
|
56
|
+
|
57
|
+
# Returns a Code instance for the specifiec LUSI API call
|
58
|
+
# @param api [LUSI:API::Core::API] the LUSI API instance
|
59
|
+
# @param lookup [LUSI::API::Core::Lookup::LookupService, nil] the lookup service for object resolution
|
60
|
+
# @param path [String] the LUSI API URL path
|
61
|
+
# @param endpoint [String] the LUSI API URL endpoint
|
62
|
+
# @param method [String] the LUSI API method
|
63
|
+
# @param xml_root [String] the XPath of the root element in the LUSI API response
|
64
|
+
# @param identity [any] the identity to search for
|
65
|
+
# @param description [String, nil] the description to search for
|
66
|
+
# @return [Array<LUSI::API::Core::Code>] the matching Code instances
|
67
|
+
# @yield [obj] Passes the Code instance to the block
|
68
|
+
# @yieldparam obj [LUSI::API::Core::Code] the Code instance
|
69
|
+
def self.get_instance(api = nil, lookup = nil, path = nil, endpoint = nil, method = nil, xml_root = nil, **kwargs)
|
70
|
+
params = get_instance_params(**kwargs)
|
71
|
+
xml = api.call(path, endpoint, method, **params)
|
72
|
+
LUSI::API::Core::XML.xml(xml, xml_root) do |c|
|
73
|
+
obj = new(c, lookup)
|
74
|
+
begin
|
75
|
+
yield(obj) if block_given?
|
76
|
+
rescue StandardError => e
|
77
|
+
puts e
|
78
|
+
end
|
79
|
+
obj
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
protected
|
84
|
+
|
85
|
+
# Returns a hash of parameters for the LUSI API call
|
86
|
+
# @param identity [any, nil] the identity code
|
87
|
+
# @param description [String. nil] the description
|
88
|
+
# @return [Hash<String, any>] the parameter hash for the LUSI API call
|
89
|
+
def self.get_instance_params(**kwargs)
|
90
|
+
{
|
91
|
+
Identity: kwargs[:identity] || '',
|
92
|
+
Description: kwargs[:description] || ''
|
93
|
+
}
|
94
|
+
end
|
95
|
+
|
96
|
+
end
|
97
|
+
|
98
|
+
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
module LUSI
|
2
|
+
module API
|
3
|
+
module Core
|
4
|
+
|
5
|
+
# The root of the LUSI API exception hierarchy
|
6
|
+
class APIError < StandardError
|
7
|
+
|
8
|
+
def initialize(message = nil, url: nil)
|
9
|
+
super(message || 'LUSI API error')
|
10
|
+
@url = url
|
11
|
+
end
|
12
|
+
|
13
|
+
end
|
14
|
+
|
15
|
+
|
16
|
+
# Indicates an error with the LUSI API call (e.g. network error)
|
17
|
+
class APICallError < APIError; end
|
18
|
+
|
19
|
+
|
20
|
+
# Indicates a LUSI API timeout error
|
21
|
+
class APITimeoutError < APICallError; end
|
22
|
+
|
23
|
+
|
24
|
+
# Indicates a successful LUSI API call which returned invalid XML or a document containing an error header
|
25
|
+
class APIResponseError < APIError
|
26
|
+
|
27
|
+
def initialize(message = nil, status: nil, fault: nil, error_code: nil, **kwargs)
|
28
|
+
super(message, **kwargs)
|
29
|
+
@error_code = error_code
|
30
|
+
@fault = fault
|
31
|
+
@status = status
|
32
|
+
end
|
33
|
+
|
34
|
+
end
|
35
|
+
|
36
|
+
|
37
|
+
# Indicates an invalid response from the LUSI API
|
38
|
+
class APIContentError < APIResponseError; end
|
39
|
+
|
40
|
+
|
41
|
+
# Indicates a LUSI API permission-denied error
|
42
|
+
class APIPermissionError < APIResponseError; end
|
43
|
+
|
44
|
+
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
@@ -0,0 +1,612 @@
|
|
1
|
+
require 'lusi_api/core/code'
|
2
|
+
require 'lusi_api/core/exceptions'
|
3
|
+
require 'lusi_api/calendar'
|
4
|
+
require 'lusi_api/country'
|
5
|
+
require 'lusi_api/organisation'
|
6
|
+
require 'lusi_api/person/staff'
|
7
|
+
|
8
|
+
|
9
|
+
module LUSI
|
10
|
+
module API
|
11
|
+
module Core
|
12
|
+
module Lookup
|
13
|
+
# The root of the lookup exception hierarchy
|
14
|
+
class LookupError < ::LUSI::API::Core::APIError
|
15
|
+
|
16
|
+
# @!attribute [rw] key
|
17
|
+
# @return [any] the key which triggered the lookup error
|
18
|
+
attr_accessor :key
|
19
|
+
|
20
|
+
# Initialises a new LookupError instance
|
21
|
+
# @param key [any] the key which triggered the lookup error
|
22
|
+
# @return [void]
|
23
|
+
def initialize(*args, key: nil)
|
24
|
+
super(*args)
|
25
|
+
@key = key
|
26
|
+
end
|
27
|
+
|
28
|
+
end
|
29
|
+
|
30
|
+
# Implements a caching lookup table
|
31
|
+
class LookupTable < Hash
|
32
|
+
|
33
|
+
# Initialises a new LookupTable instance
|
34
|
+
# @param (see Hash)
|
35
|
+
# @return [void]
|
36
|
+
def initialize(*args, &block)
|
37
|
+
|
38
|
+
# Initialise the superclass
|
39
|
+
super(*args)
|
40
|
+
|
41
|
+
# Define a default proc to look up and cache missing keys
|
42
|
+
# Note that if a hash has a default value set, this is lost (reset to nil) when a default_proc is set,
|
43
|
+
# so we preserve the default value here to use in the proc.
|
44
|
+
default_value = default
|
45
|
+
self.default_proc = Proc.new do |hash, key|
|
46
|
+
begin
|
47
|
+
# If the lookup succeeds, add the value to the hash and return the value.
|
48
|
+
self[key] = get_value(key)
|
49
|
+
rescue LookupError => e
|
50
|
+
# Lookup failed
|
51
|
+
if block
|
52
|
+
# Call the block passed by the caller
|
53
|
+
block.call(hash, key)
|
54
|
+
else
|
55
|
+
# Return the default value for the hash
|
56
|
+
default_value
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
end
|
62
|
+
|
63
|
+
# Retrieves the value for a key from some data source
|
64
|
+
# @param key [any] the key to search for
|
65
|
+
# @return [any] the corresponding value
|
66
|
+
# @raise [LookupError] if the data source lookup fails
|
67
|
+
def get_value(key)
|
68
|
+
raise LookupError.new(key)
|
69
|
+
end
|
70
|
+
|
71
|
+
end
|
72
|
+
|
73
|
+
|
74
|
+
# A caching lookup table which retrieves values from the LUSI API
|
75
|
+
class LUSILookupTable < LookupTable
|
76
|
+
|
77
|
+
# @!attribute [rw] api
|
78
|
+
# @return [LUSI::API::Core::API, nil] the LUSI API instance to use for lookups
|
79
|
+
attr_accessor :api
|
80
|
+
|
81
|
+
# Initialises a new LUSILookupTable instance
|
82
|
+
# @param api [LUSI::API::Core::API] the LUSI API instance to use
|
83
|
+
# @param result_class [Class] the class to instantiate from the LUSI API XML response
|
84
|
+
# @param key_attribute [Proc, String, Symbol, nil] an extractor to return the key attribute from the instance
|
85
|
+
# If a Proc is supplied, it will be called with the instance as the sole parameter.
|
86
|
+
# If a String or Symbol are supplied, the named attribute will be returned from the instance
|
87
|
+
# If nil or no extractor is supplied, the attribute 'identity' will be returned if it exists.
|
88
|
+
# @raise [NameError] if the specified attribute cannot be found
|
89
|
+
# @param loadable [Boolean, nil] if true, the lookup table is populated from a single API call; otherwise
|
90
|
+
# the lookup table is populated by individual API calls each time a key lookup fails
|
91
|
+
# @param param [String, nil] the LUSI API parameter to use for the key lookup
|
92
|
+
# @param load_params [Hash<String, any>, nil] default LUSI API parameters passed to the load call
|
93
|
+
# Other positional and keyword parameters are passed to the result class' #get_instance method
|
94
|
+
# @return [void]
|
95
|
+
def initialize(api = nil, result_class = nil, *args, key_attribute: nil, loadable: nil, param: nil,
|
96
|
+
load_params: nil, **kwargs, &block)
|
97
|
+
super(block)
|
98
|
+
|
99
|
+
key_attribute = :identity if key_attribute.nil?
|
100
|
+
if key_attribute.is_a? Proc
|
101
|
+
@key_attribute = key_attribute
|
102
|
+
else
|
103
|
+
@key_attribute = Proc.new { |obj| obj.nil? ? nil : obj.instance_variable_get("@#{key_attribute}") }
|
104
|
+
end
|
105
|
+
|
106
|
+
@api = api
|
107
|
+
@args = args || []
|
108
|
+
@kwargs = kwargs || {}
|
109
|
+
@load_params = load_params
|
110
|
+
@loadable = loadable ? true : false
|
111
|
+
@param = param
|
112
|
+
@result_class = result_class
|
113
|
+
end
|
114
|
+
|
115
|
+
# Retrieves the object corresponding to the key from the LUSI API and instantiates a result from the XML
|
116
|
+
# @param key [any] the key to search
|
117
|
+
# @return [any] an instance of the result_class for the matching object
|
118
|
+
def get_value(key)
|
119
|
+
|
120
|
+
# A loadable lookup table is populated with a single API call, so do regular key lookup
|
121
|
+
raise LookupError.new('lookup table is loadable') if @loadable
|
122
|
+
|
123
|
+
# For non-loadable lookup tables, call the LUSI API to get the key value and create a corresponding instance
|
124
|
+
xml = @api.call(@path, @endpoint, @method, { @param => key })
|
125
|
+
result_class.new(xml, xml_root)
|
126
|
+
|
127
|
+
end
|
128
|
+
|
129
|
+
# Retrieves all objects from the LUSI API and adds them to the lookup table
|
130
|
+
# @param clear [Boolean, nil] if true, clear the lookup table before loading
|
131
|
+
# @param lookup [LUSI::API::Core::Lookup::LookupService] the lookup service for object resolution
|
132
|
+
# @return [Boolean] true if the lookup table was loaded, false if it is not loadable
|
133
|
+
def load(clear = false, lookup = nil, **params)
|
134
|
+
|
135
|
+
# Bail out if this lookup table isn't loadable
|
136
|
+
return false unless @loadable
|
137
|
+
|
138
|
+
# Set up the LUSI API call parameters
|
139
|
+
params ||= @load_params || {}
|
140
|
+
|
141
|
+
# Clear the lookup table before loading if required
|
142
|
+
self.clear if clear
|
143
|
+
|
144
|
+
# Call the LUSI API
|
145
|
+
# - do not use the lookup service to resolve result_class instances
|
146
|
+
#xml = @api.call(@path, @endpoint, @method, **params)
|
147
|
+
#xml.xpath(@xml_root) do |x|
|
148
|
+
params.merge(@kwargs)
|
149
|
+
@result_class.get_instance(@api, lookup, *@args, use_lookup: false, **params) do |obj|
|
150
|
+
# Get the lookup key from the instance and add the instance to the hash
|
151
|
+
begin
|
152
|
+
key = @key_attribute.call(obj)
|
153
|
+
self[key] = obj
|
154
|
+
rescue NameError => e
|
155
|
+
# ?
|
156
|
+
end
|
157
|
+
end
|
158
|
+
|
159
|
+
# Return true to indicate successful loading
|
160
|
+
true
|
161
|
+
|
162
|
+
end
|
163
|
+
|
164
|
+
end
|
165
|
+
|
166
|
+
class LUSILookupTable < LookupTable
|
167
|
+
|
168
|
+
# @!attribute [rw] api
|
169
|
+
# @return [LUSI::API::Core::API, nil] the LUSI API instance to use for lookups
|
170
|
+
attr_accessor :api
|
171
|
+
|
172
|
+
# Initialises a new LUSILookupTable instance
|
173
|
+
# @param api [LUSI::API::Core::API] the LUSI API instance to use
|
174
|
+
# @param result_class [Class] the class to instantiate from the LUSI API XML response
|
175
|
+
# @param key_attribute [Proc, String, Symbol, nil] an extractor to return the key attribute from the instance
|
176
|
+
# If a Proc is supplied, it will be called with the instance as the sole parameter.
|
177
|
+
# If a String or Symbol are supplied, the named attribute will be returned from the instance
|
178
|
+
# If nil or no extractor is supplied, the attribute 'identity' will be returned if it exists.
|
179
|
+
# @raise [NameError] if the specified attribute cannot be found
|
180
|
+
# @param loadable [Boolean, nil] if true, the lookup table is populated from a single API call; otherwise
|
181
|
+
# the lookup table is populated by individual API calls each time a key lookup fails
|
182
|
+
# @param param [String, nil] the LUSI API parameter to use for the key lookup
|
183
|
+
# @param load_params [Hash<String, any>, nil] default LUSI API parameters passed to the load call
|
184
|
+
# Other positional and keyword parameters are passed to the result class' #get_instance method
|
185
|
+
# @return [void]
|
186
|
+
def initialize(api = nil, result_class = nil, *args, key_attribute: nil, loadable: nil, param: nil,
|
187
|
+
load_params: nil, **kwargs, &block)
|
188
|
+
super(block)
|
189
|
+
|
190
|
+
key_attribute = :identity if key_attribute.nil?
|
191
|
+
if key_attribute.is_a? Proc
|
192
|
+
@key_attribute = key_attribute
|
193
|
+
else
|
194
|
+
@key_attribute = Proc.new { |obj| obj.nil? ? nil : obj.instance_variable_get("@#{key_attribute}") }
|
195
|
+
end
|
196
|
+
|
197
|
+
@api = api
|
198
|
+
@args = args || []
|
199
|
+
@kwargs = kwargs || {}
|
200
|
+
@load_params = load_params
|
201
|
+
@loadable = loadable ? true : false
|
202
|
+
@param = param
|
203
|
+
@result_class = result_class
|
204
|
+
end
|
205
|
+
|
206
|
+
# Retrieves the object corresponding to the key from the LUSI API and instantiates a result from the XML
|
207
|
+
# @param key [any] the key to search
|
208
|
+
# @return [any] an instance of the result_class for the matching object
|
209
|
+
def get_value(key)
|
210
|
+
|
211
|
+
# A loadable lookup table is populated with a single API call, so do regular key lookup
|
212
|
+
raise LookupError.new('lookup table is loadable') if @loadable
|
213
|
+
|
214
|
+
# For non-loadable lookup tables, call the LUSI API to get the key value and create a corresponding instance
|
215
|
+
xml = @api.call(@path, @endpoint, @method, { @param => key })
|
216
|
+
result_class.new(xml, xml_root)
|
217
|
+
|
218
|
+
end
|
219
|
+
|
220
|
+
# Retrieves all objects from the LUSI API and adds them to the lookup table
|
221
|
+
# @param clear [Boolean, nil] if true, clear the lookup table before loading
|
222
|
+
# @param lookup [LUSI::API::Core::Lookup::LookupService] the lookup service for object resolution
|
223
|
+
# @return [Boolean] true if the lookup table was loaded, false if it is not loadable
|
224
|
+
def load(clear = false, lookup = nil, **params)
|
225
|
+
|
226
|
+
# Bail out if this lookup table isn't loadable
|
227
|
+
return false unless @loadable
|
228
|
+
|
229
|
+
# Set up the LUSI API call parameters
|
230
|
+
params ||= @load_params || {}
|
231
|
+
|
232
|
+
# Clear the lookup table before loading if required
|
233
|
+
self.clear if clear
|
234
|
+
|
235
|
+
# Call the LUSI API
|
236
|
+
# - do not use the lookup service to resolve result_class instances
|
237
|
+
#xml = @api.call(@path, @endpoint, @method, **params)
|
238
|
+
#xml.xpath(@xml_root) do |x|
|
239
|
+
params.merge(@kwargs)
|
240
|
+
@result_class.get_instance(@api, lookup, *@args, use_lookup: false, **params) do |obj|
|
241
|
+
# Get the lookup key from the instance and add the instance to the hash
|
242
|
+
begin
|
243
|
+
key = @key_attribute.call(obj)
|
244
|
+
self[key] = obj
|
245
|
+
rescue NameError => e
|
246
|
+
# ?
|
247
|
+
end
|
248
|
+
end
|
249
|
+
|
250
|
+
# Return true to indicate successful loading
|
251
|
+
true
|
252
|
+
|
253
|
+
end
|
254
|
+
|
255
|
+
end
|
256
|
+
|
257
|
+
|
258
|
+
# Collects a number of lookup tables into a single service
|
259
|
+
class LookupService
|
260
|
+
|
261
|
+
# Configuration for lookup services
|
262
|
+
# The structure is:
|
263
|
+
# {
|
264
|
+
# service: {
|
265
|
+
# available: include this service in the list of available services if true
|
266
|
+
# class: the LUSI API class returned by the lookup table (default: LUSI::API::Core::Code)
|
267
|
+
# create: create method name
|
268
|
+
# creates: the list of services created by this service
|
269
|
+
# depends: the list of services this service depends on
|
270
|
+
# load: load method name
|
271
|
+
# params: the LUSI API class #get_instance parameters
|
272
|
+
# }
|
273
|
+
# }
|
274
|
+
#
|
275
|
+
# The available option, if specifies, dictates whether the service is included in the list of available
|
276
|
+
# services. The default is true.
|
277
|
+
#
|
278
|
+
# The class option, if specified, is the Class instance of the object class stored in the lookup table.
|
279
|
+
# If no class is specified, the default is LUSI::API::Core::Code
|
280
|
+
#
|
281
|
+
# The create option, if specified, is a symbol (method name on the LookupService instance) or callable.
|
282
|
+
# The create method should accept an arbitrary parameter list (method(*params)) and return a configured
|
283
|
+
# lookup table instance or functional equivalent.
|
284
|
+
# If no create method is specified, the default is to create a LookupTable instance as follows:
|
285
|
+
# LUSILookupTable.new(@api, *params, loadable: true)
|
286
|
+
# where params is the list from the params option (see below)
|
287
|
+
#
|
288
|
+
# The creates option, if specified, is a list of services created by this service. This allows a single
|
289
|
+
# service definition to create multiple related lookup services which can be iterated in the list of
|
290
|
+
# available services.
|
291
|
+
#
|
292
|
+
# The depends option, if specified, specifies a list of lookup services on which the current lookup service
|
293
|
+
# depends. These services are automatically loaded before the current lookup service.
|
294
|
+
#
|
295
|
+
# The load option, if specified, is a symbol (method name on the LookupService instance) or callable.
|
296
|
+
# The load method should accepts two parameters:
|
297
|
+
# If no load method is specified, the default is to call the lookup table's load method.
|
298
|
+
#
|
299
|
+
# The params option specifies the parameters to the create method or LookupTable constructor as a list:
|
300
|
+
# [ lusi-api-url-path, lusi-api-endpoint, lusi-api-method, xml-root, result-class ]
|
301
|
+
#
|
302
|
+
#
|
303
|
+
SERVICES = {
|
304
|
+
absence_reason: {
|
305
|
+
params: ['LUSIReference', 'Lookup.asmx', 'GetAbsenceReasons', 'xmlns:AbsenceReason']
|
306
|
+
},
|
307
|
+
address_country: {
|
308
|
+
class: LUSI::API::Country::AddressCountry,
|
309
|
+
},
|
310
|
+
event_contact_type: {
|
311
|
+
params: ['LUSIReference', 'Lookup.asmx', 'GetEventContactTypes', 'xmlns:EventContactType']
|
312
|
+
},
|
313
|
+
leave_reason: {
|
314
|
+
params: ['LUSIReference', 'Lookup.asmx', 'GetLeaveReasons', 'xmlns:LeaveReason']
|
315
|
+
},
|
316
|
+
location_of_tuition: {
|
317
|
+
params: ['LUSIReference', 'Lookup.asmx', 'GetLocationsOfTuition', 'xmlns:LocationOfTuition']
|
318
|
+
},
|
319
|
+
nationality: {
|
320
|
+
class: LUSI::API::Country::Nationality
|
321
|
+
},
|
322
|
+
organisation: {
|
323
|
+
available: false,
|
324
|
+
create: :create_organisation,
|
325
|
+
creates: [:department, :faculty, :institution]
|
326
|
+
},
|
327
|
+
staff_course_role: {
|
328
|
+
class: LUSI::API::Person::StaffCourseRole
|
329
|
+
},
|
330
|
+
staff_relationship: {
|
331
|
+
params: ['LUSIReference', 'Lookup.asmx', 'GetStaffRelationships', 'xmlns:StaffRelationship']
|
332
|
+
},
|
333
|
+
student_category: {
|
334
|
+
params: ['LUSIReference', 'Lookup.asmx', 'GetStudentCategories', 'xmlns:StudentCategory']
|
335
|
+
},
|
336
|
+
subject_area: {
|
337
|
+
params: ['LUSIReference', 'Lookup.asmx', 'GetSubjectAreas', 'xmlns:SubjectArea']
|
338
|
+
},
|
339
|
+
week: {
|
340
|
+
class: LUSI::API::Calendar::Week,
|
341
|
+
depends: [:year],
|
342
|
+
load: :load_weeks
|
343
|
+
},
|
344
|
+
year: {
|
345
|
+
class: LUSI::API::Calendar::Year
|
346
|
+
}
|
347
|
+
}
|
348
|
+
|
349
|
+
# The list of available services - built lazily
|
350
|
+
@@services = nil
|
351
|
+
|
352
|
+
# Initialises a new LookupService instance
|
353
|
+
# @param api [LUSI::API::Core::API] the LUSI API instance
|
354
|
+
# @param (see #load)
|
355
|
+
# @return [void]
|
356
|
+
def initialize(api = nil, **services)
|
357
|
+
@api = api
|
358
|
+
clear
|
359
|
+
load(**services) unless services.nil? || services.empty?
|
360
|
+
end
|
361
|
+
|
362
|
+
# Clears all lookup tables from the LookupService instance
|
363
|
+
def clear
|
364
|
+
@lookups = {}
|
365
|
+
@organisation = nil
|
366
|
+
end
|
367
|
+
|
368
|
+
# Iterates over each lookup table
|
369
|
+
# @yield [service, lookup_table] passes the service name and corresponding lookup table to the block
|
370
|
+
# @yieldparam service [Symbol] the service name
|
371
|
+
# @yieldparam lookup [LUSI::API::Core::Lookup::LookupTable] the lookup table for the service
|
372
|
+
# @yieldreturn [void]
|
373
|
+
def each
|
374
|
+
@lookups.each { |service, lookup| yield(service, lookup) }
|
375
|
+
end
|
376
|
+
|
377
|
+
# Returns true if all specified services are defined
|
378
|
+
# Parameters are the service names to be checked
|
379
|
+
# @return [Boolean] true if all services are definied, false if any service is undefined
|
380
|
+
def has?(*services)
|
381
|
+
services.each { |service| return false unless @lookups.include?(service) }
|
382
|
+
true
|
383
|
+
end
|
384
|
+
|
385
|
+
# Returns the number of configured lookup tables
|
386
|
+
# @param service [Symbol, nil] the service to check
|
387
|
+
# if present, return the number of entries in the specified lookup table, otherwise return the number of
|
388
|
+
# lookup tables
|
389
|
+
# @return [Integer] the number of configured lookup tables (if service is unspecified) or the number of
|
390
|
+
# entries in the specified lookup table. If an invalid service is specified, 0 is returned.
|
391
|
+
def length(service = nil)
|
392
|
+
if service
|
393
|
+
lookup = @lookups[service]
|
394
|
+
lookup ? lookup.length : 0
|
395
|
+
else
|
396
|
+
@lookups.length
|
397
|
+
end
|
398
|
+
end
|
399
|
+
|
400
|
+
# Loads specified lookup services, or all services if none are specified.
|
401
|
+
# Each named parameter specifies a configuration for the lookup service: service: config
|
402
|
+
# False parameter values cause that service to be ignored.
|
403
|
+
# @return [void]
|
404
|
+
def load(**services)
|
405
|
+
|
406
|
+
# If no services are specified, load all services with default configuration
|
407
|
+
if services.nil? || services.empty?
|
408
|
+
services = {}
|
409
|
+
SERVICES.each_key { |key| services[key] = true }
|
410
|
+
end
|
411
|
+
|
412
|
+
# Create the services
|
413
|
+
services.each do |service, config|
|
414
|
+
# Ignore invalid and unconfigured services
|
415
|
+
create_lookup_service(service, config, services) if config && SERVICES.has_key?(service)
|
416
|
+
end
|
417
|
+
|
418
|
+
end
|
419
|
+
|
420
|
+
# Fetches a key from the specified lookup table
|
421
|
+
# @param service [Symbol] the lookup table to use
|
422
|
+
# @param key [any] the key to search for
|
423
|
+
# @param default [any] the default value if the lookup fails
|
424
|
+
# @return [Object] the value corresponding to the key
|
425
|
+
def lookup(service, key, default = nil)
|
426
|
+
lookup_method = get_callable(service, :lookup)
|
427
|
+
if lookup_method
|
428
|
+
lookup_method.call(service, key, default)
|
429
|
+
else
|
430
|
+
lookup = @lookups[service]
|
431
|
+
if lookup.is_a?(Hash)
|
432
|
+
lookup[key]
|
433
|
+
elsif lookup.is_a?(Method) || lookup.is_a?(Proc)
|
434
|
+
lookup.call(service, key, default)
|
435
|
+
else
|
436
|
+
default
|
437
|
+
end
|
438
|
+
end
|
439
|
+
end
|
440
|
+
|
441
|
+
# Returns the specified lookup table
|
442
|
+
# @param service [Symbol] the lookup table to return
|
443
|
+
# @return [LUSI::API::Core::Lookup::LookupTable] the lookup table
|
444
|
+
def service(service)
|
445
|
+
@lookups[service]
|
446
|
+
end
|
447
|
+
|
448
|
+
# Returns a list of configured services
|
449
|
+
# @param all [Boolean] if true, returns all available services; if false, returns only configured services
|
450
|
+
# @yield [service] Passes the service name to the block
|
451
|
+
# @yieldparam service [Symbol] the service name
|
452
|
+
def services(all = false)
|
453
|
+
if all
|
454
|
+
# Return all services defined in SERVICES
|
455
|
+
return @@services if @@services
|
456
|
+
@@services = []
|
457
|
+
SERVICES.each do |service, config|
|
458
|
+
if config.fetch(:available, true)
|
459
|
+
@@services.push(service)
|
460
|
+
yield(service) if block_given?
|
461
|
+
end
|
462
|
+
config.fetch(:creates, []).each do |created|
|
463
|
+
@@services.push(created)
|
464
|
+
yield(created) if block_given?
|
465
|
+
end
|
466
|
+
end
|
467
|
+
@@services
|
468
|
+
else
|
469
|
+
# Return all services configured in @lookups
|
470
|
+
result = @lookups.keys
|
471
|
+
result.each { |service| yield(service) } if block_given?
|
472
|
+
result
|
473
|
+
end
|
474
|
+
end
|
475
|
+
|
476
|
+
protected
|
477
|
+
|
478
|
+
# Creates a lookup service
|
479
|
+
# - the create method is responsible for loading the table
|
480
|
+
# @param service [Symbol] the lookup service being created
|
481
|
+
# @param config [any] the lookup service configuration
|
482
|
+
# @param services [Hash<Symbol, any>] the configuration hash for all lookup services
|
483
|
+
# @return [void]
|
484
|
+
def create_lookup_service(service, config, services)
|
485
|
+
create_method = get_callable(service, :create)
|
486
|
+
params = SERVICES[service][:params] || []
|
487
|
+
if create_method
|
488
|
+
lookup = create_method.call(service, config, services, *params)
|
489
|
+
else
|
490
|
+
lookup = create_lookup(service, config, services, *params)
|
491
|
+
end
|
492
|
+
end
|
493
|
+
|
494
|
+
# Creates a lookup table
|
495
|
+
# @param service [Symbol] the lookup service being created
|
496
|
+
# @param config [any] the lookup service configuration
|
497
|
+
# @param services [Hash<Symbol, any>] the configuration hash for all lookup services
|
498
|
+
# params is the list of parameters from the service definition in SERVICES
|
499
|
+
# @return [void]
|
500
|
+
def create_lookup(service, config, services, *params)
|
501
|
+
# Create the lookup table
|
502
|
+
result_class = SERVICES[service].fetch(:class, LUSI::API::Core::Code)
|
503
|
+
lookup = LUSILookupTable.new(@api, result_class, *params, loadable: true)
|
504
|
+
# Load the lookup table
|
505
|
+
begin
|
506
|
+
# Load dependencies
|
507
|
+
load_dependencies(service, services) if SERVICES[service][:depends]
|
508
|
+
# Load the table
|
509
|
+
load_lookup(service, config, lookup)
|
510
|
+
# Add the lookup table to the service if successful
|
511
|
+
@lookups[service] = lookup
|
512
|
+
rescue APIPermissionError => e
|
513
|
+
puts("Permission denied for #{service}")
|
514
|
+
end
|
515
|
+
end
|
516
|
+
|
517
|
+
# Creates organisation lookups
|
518
|
+
#
|
519
|
+
# @param service [Symbol] the lookup service being created
|
520
|
+
# @param config [any] the lookup service configuration
|
521
|
+
# @param services [Hash<Symbol, any>] the configuration hash for all lookup services
|
522
|
+
# params is the list of parameters from the service definition in SERVICES
|
523
|
+
# @return [void]
|
524
|
+
def create_organisation(service, config, services, *params)
|
525
|
+
# Create and load the organisation structure
|
526
|
+
@organisation = LUSI::API::Organisation::Organisation.new
|
527
|
+
@organisation.load(@api)
|
528
|
+
# Create lookups for each level of the organisation
|
529
|
+
@organisation.each_unit_type do |unit_type|
|
530
|
+
@lookups[unit_type] = Proc.new do |service, key, default|
|
531
|
+
@organisation.get(key, unit_type)
|
532
|
+
end
|
533
|
+
end
|
534
|
+
end
|
535
|
+
|
536
|
+
# Gets a method or proc of the LookupService instance
|
537
|
+
# @param service [Symbol] the lookup service
|
538
|
+
# @param type [Symbol] the type of method (:create | :load | :lookup)
|
539
|
+
# @return [Method, Proc] the corresponding method or proc, or nil if no method is found or an invalid
|
540
|
+
# parameter is passed
|
541
|
+
def get_callable(service, type)
|
542
|
+
callable = SERVICES[service] ? SERVICES[service][type] : nil
|
543
|
+
case
|
544
|
+
when callable.nil?
|
545
|
+
nil
|
546
|
+
when callable.is_a?(Symbol) || callable.is_a?(String)
|
547
|
+
# callable refers to a method on this LookupService instance
|
548
|
+
method(callable.to_sym)
|
549
|
+
when callable.is_a?(Method) || callable.is_a?(Proc)
|
550
|
+
# callable is already a callable object
|
551
|
+
callable
|
552
|
+
else
|
553
|
+
nil
|
554
|
+
end
|
555
|
+
end
|
556
|
+
|
557
|
+
# Loads service dependencies
|
558
|
+
# @param service [Symbol] the lookup service being configured
|
559
|
+
# @param services [Hash<Symbol, any>] the configuration hash for all lookup services
|
560
|
+
# @return [void]
|
561
|
+
def load_dependencies(service, services)
|
562
|
+
depends = SERVICES[service][:depends]
|
563
|
+
depends.each do |depend|
|
564
|
+
# Skip if the dependency already exists
|
565
|
+
continue if @lookups[depend]
|
566
|
+
# Get the config for the dependency, or use the default configuration if unspecified
|
567
|
+
config = services[depend] || true
|
568
|
+
# Create the lookup service if it doesn't already exist
|
569
|
+
create_lookup_service(depend, config, services)
|
570
|
+
end
|
571
|
+
end
|
572
|
+
|
573
|
+
# Loads a lookup table
|
574
|
+
# @param service [Symbol] the lookup service being configured
|
575
|
+
# @param config [any] the lookup service configuration
|
576
|
+
# @param lookup [LUSILookupTable] the lookup table to be loaded
|
577
|
+
# @return [LUSILookupTable] the lookup table
|
578
|
+
def load_lookup(service, config, lookup)
|
579
|
+
load_method = get_callable(service, :load)
|
580
|
+
if load_method
|
581
|
+
# Call the specified load method
|
582
|
+
load_method.call(service, config, lookup)
|
583
|
+
else
|
584
|
+
# Call the lookup table's load method
|
585
|
+
# - pass this LookupService as the lookup service for object resolution
|
586
|
+
lookup.load(false, self)
|
587
|
+
end
|
588
|
+
lookup
|
589
|
+
end
|
590
|
+
|
591
|
+
# Loads the academic weeks lookup table
|
592
|
+
# @param service [Symbol] the lookup service
|
593
|
+
# @param config [Array<String>] the list of year identity codes to load weeks for (default is all available weeks)
|
594
|
+
# @param lookup [LUSILookupTable] the lookup table being loaded
|
595
|
+
# @return [LUSILookupTable] the lookup table
|
596
|
+
def load_weeks(service, config, lookup)
|
597
|
+
if config.is_a?(Array) && !config.empty?
|
598
|
+
# Load weeks for each specified year (note: clear = false to accumulate results)
|
599
|
+
config.each { |year_identity| lookup.load(false, self, year_identity: year_identity) }
|
600
|
+
elsif config
|
601
|
+
# Load all weeks
|
602
|
+
lookup.load(false, self)
|
603
|
+
end
|
604
|
+
lookup
|
605
|
+
end
|
606
|
+
|
607
|
+
end
|
608
|
+
|
609
|
+
end
|
610
|
+
end
|
611
|
+
end
|
612
|
+
end
|