aspire 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +59 -0
- data/.rbenv-gemsets +1 -0
- data/.travis.yml +5 -0
- data/CODE_OF_CONDUCT.md +74 -0
- data/Dockerfile +20 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +21 -0
- data/README.md +851 -0
- data/Rakefile +10 -0
- data/aspire.gemspec +40 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/entrypoint.sh +11 -0
- data/exe/build-cache +13 -0
- data/lib/aspire.rb +11 -0
- data/lib/aspire/api.rb +2 -0
- data/lib/aspire/api/base.rb +198 -0
- data/lib/aspire/api/json.rb +195 -0
- data/lib/aspire/api/linked_data.rb +214 -0
- data/lib/aspire/caching.rb +4 -0
- data/lib/aspire/caching/builder.rb +356 -0
- data/lib/aspire/caching/cache.rb +365 -0
- data/lib/aspire/caching/cache_entry.rb +296 -0
- data/lib/aspire/caching/cache_logger.rb +63 -0
- data/lib/aspire/caching/util.rb +210 -0
- data/lib/aspire/cli/cache_builder.rb +123 -0
- data/lib/aspire/cli/command.rb +20 -0
- data/lib/aspire/enumerator/base.rb +29 -0
- data/lib/aspire/enumerator/json_enumerator.rb +130 -0
- data/lib/aspire/enumerator/linked_data_uri_enumerator.rb +32 -0
- data/lib/aspire/enumerator/report_enumerator.rb +64 -0
- data/lib/aspire/exceptions.rb +36 -0
- data/lib/aspire/object.rb +7 -0
- data/lib/aspire/object/base.rb +155 -0
- data/lib/aspire/object/digitisation.rb +43 -0
- data/lib/aspire/object/factory.rb +87 -0
- data/lib/aspire/object/list.rb +590 -0
- data/lib/aspire/object/module.rb +36 -0
- data/lib/aspire/object/resource.rb +371 -0
- data/lib/aspire/object/time_period.rb +47 -0
- data/lib/aspire/object/user.rb +46 -0
- data/lib/aspire/properties.rb +20 -0
- data/lib/aspire/user_lookup.rb +103 -0
- data/lib/aspire/util.rb +185 -0
- data/lib/aspire/version.rb +3 -0
- data/lib/retry.rb +197 -0
- metadata +274 -0
@@ -0,0 +1,63 @@
|
|
1
|
+
require 'logger'
|
2
|
+
|
3
|
+
require 'aspire/exceptions'
|
4
|
+
|
5
|
+
module Aspire
|
6
|
+
# Tools for building a caching from the Aspire APIs
|
7
|
+
module Caching
|
8
|
+
# A wrapper class for Logger adding utility methods
|
9
|
+
class CacheLogger
|
10
|
+
# @!attribute [rw] logger
|
11
|
+
# @return [Logger] the logger
|
12
|
+
attr_accessor :logger
|
13
|
+
|
14
|
+
# Delegates missing methods to the logger
|
15
|
+
# @param method [Symbol] the method name
|
16
|
+
# @param args [Array] the method arguments
|
17
|
+
# @param block [Proc] the method code block
|
18
|
+
# @return [Object] the method result
|
19
|
+
def method_missing(method, *args, &block)
|
20
|
+
# Do not fail if logger is undefined
|
21
|
+
return nil unless logger
|
22
|
+
# Fail if logger does not respond to this method
|
23
|
+
super unless logger.respond_to?(method)
|
24
|
+
# Delegate to the logger method
|
25
|
+
logger.public_send(method, *args, &block)
|
26
|
+
end
|
27
|
+
|
28
|
+
# Delegates missing method respond_to? to the wrapped logger
|
29
|
+
# @param method [Symbol] the method name
|
30
|
+
# @return [Boolean] true if the wrapped logger responds to the method
|
31
|
+
def respond_to_missing?(method)
|
32
|
+
# If logger is undefined, all missing methods are accepted
|
33
|
+
logger ? logger.respond_to?(method) : true
|
34
|
+
end
|
35
|
+
|
36
|
+
# Initialises a new CacheLogger instance
|
37
|
+
# @param logger [Logger] the logger
|
38
|
+
def initialize(logger = nil)
|
39
|
+
self.logger = logger
|
40
|
+
end
|
41
|
+
|
42
|
+
# Logs and raises an exception
|
43
|
+
# @param message [String] the error message
|
44
|
+
# @param exception [Class] the class of the exception to be raised
|
45
|
+
# @param level [Symbol] the logger level (default: Logger::ERROR)
|
46
|
+
# @raise [Aspire::Caching::Exceptions::Error]
|
47
|
+
def log_exception(message, exception = nil, level: nil)
|
48
|
+
log(level || Logger::ERROR, message)
|
49
|
+
raise exception || Aspire::Exceptions::Error, message
|
50
|
+
end
|
51
|
+
|
52
|
+
# Logs an event and returns its first argument
|
53
|
+
# - allows for compact code such as 'return log_return(result, msg,...)'
|
54
|
+
# @param result [Object] the return value of the method
|
55
|
+
# @param (see #log)
|
56
|
+
# @return [Object] the result argument
|
57
|
+
def log_return(result, *args, **kwargs)
|
58
|
+
log(*args, **kwargs)
|
59
|
+
result
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
@@ -0,0 +1,210 @@
|
|
1
|
+
require 'fileutils'
|
2
|
+
require 'json'
|
3
|
+
require 'uri'
|
4
|
+
|
5
|
+
require 'aspire/enumerator/linked_data_uri_enumerator'
|
6
|
+
require 'aspire/exceptions'
|
7
|
+
require 'aspire/util'
|
8
|
+
|
9
|
+
module Aspire
|
10
|
+
# Tools for building a caching from the Aspire APIs
|
11
|
+
module Caching
|
12
|
+
# Cache utility methods
|
13
|
+
module Util
|
14
|
+
include Aspire::Util
|
15
|
+
|
16
|
+
# Rules for determining whether an object URL is cacheable
|
17
|
+
# Each rule is a Proc which accepts a parsed URL from #parse_url and the
|
18
|
+
# CacheEntry instance, and returns true if the object is cacheable or
|
19
|
+
# false if not. Rules are applied in the order specified and all rules
|
20
|
+
# must return true for an object to be cacheable.
|
21
|
+
CACHEABLE = [
|
22
|
+
# The URL must be set and the host must mach the canonical tenancy host
|
23
|
+
proc { |u, e| u && u[:tenancy_host] == e.cache.tenancy_host },
|
24
|
+
# Catalog objects are not cacheable
|
25
|
+
proc { |u, _e| u[:type] != 'catalog' },
|
26
|
+
# User objects themselves are not cacheable but child objects e.g. notes
|
27
|
+
# are cacheable
|
28
|
+
proc { |u, _e| u[:type] != 'users' || !u[:child_type].nil? },
|
29
|
+
# Importance URI values are not cacheable
|
30
|
+
proc do |u, _e|
|
31
|
+
u[:type] != 'config' || !u[:id].to_s.start_with?('importance')
|
32
|
+
end
|
33
|
+
].freeze
|
34
|
+
|
35
|
+
# Adds a prefix to a filename
|
36
|
+
# @param filename [String] the filename
|
37
|
+
# @param prefix [String] the prefix
|
38
|
+
# @return [String] the filename with prefix
|
39
|
+
def add_filename_prefix(filename, prefix)
|
40
|
+
filename = filename.rpartition(File.basename(filename))
|
41
|
+
filename[1] = "#{prefix}#{filename[1]}"
|
42
|
+
filename.join
|
43
|
+
end
|
44
|
+
|
45
|
+
# Adds a suffix to a filename preserving any file extension
|
46
|
+
# e.g. add_filename_suffix('file.txt', '-suffix') == 'file-suffix.txt'
|
47
|
+
# @param filename [String] the filename
|
48
|
+
# @param suffix [String] the suffix
|
49
|
+
# @return [String] the filename with suffix
|
50
|
+
def add_filename_suffix(filename, suffix)
|
51
|
+
f = filename.split(File::SEPARATOR)
|
52
|
+
# If the filename is '.' or '..' add the suffix to the parent path,
|
53
|
+
# otherwise add it to the basename
|
54
|
+
i = %w[. ..].include?(f[-1]) ? -2 : -1
|
55
|
+
# Split the basename around the file extension and prepend the suffix
|
56
|
+
# to the extension
|
57
|
+
if f[i]
|
58
|
+
file_ext = f[i].rpartition(File.extname(f[i]))
|
59
|
+
file_ext[1] = "#{suffix}#{file_ext[1]}"
|
60
|
+
f[i] = file_ext.join
|
61
|
+
end
|
62
|
+
# Reconstruct the filename, preserving any trailing path separator
|
63
|
+
f.push('') if filename.end_with?(File::SEPARATOR)
|
64
|
+
File.join(f)
|
65
|
+
end
|
66
|
+
|
67
|
+
# Parses the URL and checks that it is cacheable
|
68
|
+
# @param u [String] the URL of the API object
|
69
|
+
# @return [MarchData] the parsed URL
|
70
|
+
# @raise [Aspire::Exceptions::NotCacheable] if the URL is not
|
71
|
+
# cacheable
|
72
|
+
def cacheable_url(u)
|
73
|
+
# All rules must return true for the URL to be cacheable
|
74
|
+
u = parse_url(u)
|
75
|
+
CACHEABLE.each do |r|
|
76
|
+
raise Aspire::Exceptions::NotCacheable unless r.call(u, self)
|
77
|
+
end
|
78
|
+
# Return the parsed URL
|
79
|
+
u
|
80
|
+
end
|
81
|
+
|
82
|
+
# Returns true if the directory path has no more parents, false otherwise
|
83
|
+
# @param dir [String] the directory path
|
84
|
+
# @param root [String] the directory root - paths above this are ignored
|
85
|
+
# @return [Boolean] true if there are no more parents, false otherwise
|
86
|
+
def end_of_path?(dir, root = nil)
|
87
|
+
dir.nil? || dir.empty? || dir == '.' || dir == root
|
88
|
+
end
|
89
|
+
|
90
|
+
# Creates a directory and its parents, logs errors
|
91
|
+
# @param dir [String] the directory name
|
92
|
+
# @param logger [Aspire::Caching::CacheLogger] the logger for messages
|
93
|
+
# @param failure [String] the error message on failure
|
94
|
+
# @return [void]
|
95
|
+
# @raise [ArgumentError] if the directory is not specified
|
96
|
+
# @raise [Aspire::Cache::Exceptions::WriteError] if the operation fails
|
97
|
+
def mkdir(dir, logger = nil, success = nil, failure = nil)
|
98
|
+
raise ArgumentError, 'Directory expected' if dir.nil? || dir.empty?
|
99
|
+
FileUtils.mkdir_p(dir, mode: mode)
|
100
|
+
return if logger.nil? || success.nil? || success.empty?
|
101
|
+
logger.log(Logger::DEBUG, success)
|
102
|
+
rescue SystemCallError => e
|
103
|
+
failure ||= "Create directory #{dir} failed"
|
104
|
+
message = "#{failure}: #{e}"
|
105
|
+
raise WriteError, message if logger.nil?
|
106
|
+
logger.log_exception(message, WriteError)
|
107
|
+
end
|
108
|
+
|
109
|
+
# Returns the list of URI references from a linked data API object
|
110
|
+
# @param url [String] the URL of the API object
|
111
|
+
# @param data [Hash] the parsed JSON data for the object
|
112
|
+
# @return [Array<String>] the list of URIs referenced by the object
|
113
|
+
def references(url, data = nil)
|
114
|
+
return [] if data.nil? || data.empty?
|
115
|
+
# Enumerate the URIs and add them as keys of a hash to de-duplicate
|
116
|
+
enum = Aspire::Enumerator::LinkedDataURIEnumerator.new.enumerator(url, data)
|
117
|
+
uris = {}
|
118
|
+
enum.each { |_k, hash, _i| uris[hash['value']] = true }
|
119
|
+
# Return the list of URIs
|
120
|
+
uris.keys
|
121
|
+
end
|
122
|
+
|
123
|
+
# Removes the specified files
|
124
|
+
# @param glob [String] the file pattern to be removed
|
125
|
+
# @param logger [Aspire::Caching::CacheLogger] the logger for messages
|
126
|
+
# @param success [String] the text for success log messages
|
127
|
+
# @param failure [String] the text for failure exception/log messages
|
128
|
+
# @return [void]
|
129
|
+
# @raise [Aspire::Cache::Exceptions::RemoveError] if the removal fails
|
130
|
+
def rm(glob, logger = nil, success = nil, failure = nil)
|
131
|
+
raise ArgumentError, 'file path required' if glob.nil? || glob.empty?
|
132
|
+
FileUtils.rm_rf(Dir.glob(glob), secure: true)
|
133
|
+
return if logger.nil? || success.nil? || success.empty?
|
134
|
+
logger.log(Logger::INFO, success)
|
135
|
+
rescue SystemCallError => e
|
136
|
+
failure ||= "Remove #{glob} failed"
|
137
|
+
message = "#{failure}: #{e}"
|
138
|
+
raise RemoveError, message if logger.nil?
|
139
|
+
logger.log_exception("#{failure}: #{e}", RemoveError)
|
140
|
+
end
|
141
|
+
|
142
|
+
# Remove empty directories in a directory path
|
143
|
+
# @param path [String] the starting file or directory
|
144
|
+
# @param root
|
145
|
+
# @return [void]
|
146
|
+
# @raise [Aspire::Exceptions::RemoveError] if the operation fails
|
147
|
+
def rmdir_empty(path, root)
|
148
|
+
# The starting path is assumed to be a filename, so we append a dummy
|
149
|
+
# filename if it's a directory
|
150
|
+
path = File.directory?(path) ? File.join(path, '.') : path
|
151
|
+
loop do
|
152
|
+
# Get the parent of the current directory/file
|
153
|
+
path = File.dirname(path)
|
154
|
+
# Stop at the end of the directory path or a non-empty directory
|
155
|
+
break if end_of_path?(path, root) || !Dir.empty?(path)
|
156
|
+
# Remove the directory
|
157
|
+
Dir.rmdir(path)
|
158
|
+
end
|
159
|
+
rescue Errno::ENOTEMPTY, Errno::ENOTDIR
|
160
|
+
# Stop without error if the directory is not empty or not a directory
|
161
|
+
nil
|
162
|
+
rescue SystemCallError => e
|
163
|
+
raise RemoveError, "Rmdir #{dir} failed: #{e}"
|
164
|
+
end
|
165
|
+
|
166
|
+
# Removes the file extension from a path
|
167
|
+
# @param path [String] the file path
|
168
|
+
# @return [String] the file path with any extension removed
|
169
|
+
def strip_ext(path)
|
170
|
+
path.rpartition(File.extname(path))[0]
|
171
|
+
end
|
172
|
+
|
173
|
+
# Removes a prefix from a filename
|
174
|
+
# @param filename [String] the filename
|
175
|
+
# @param prefix [String] the prefix
|
176
|
+
# @return [String] the filename without prefix
|
177
|
+
def strip_filename_prefix(filename, prefix)
|
178
|
+
f = filename.rpartition(File.basename(filename))
|
179
|
+
f[1] = strip_prefix(f[1], prefix)
|
180
|
+
f.join
|
181
|
+
end
|
182
|
+
|
183
|
+
# Removes a suffix from a filename
|
184
|
+
# @param filename [String] the filename
|
185
|
+
# @param suffix [String] the suffix
|
186
|
+
# @return [String] the filename without suffix
|
187
|
+
def strip_filename_suffix(filename, suffix)
|
188
|
+
f = filename.rpartition(File.extname(filename))
|
189
|
+
f[0] = strip_suffix(f[0], suffix)
|
190
|
+
f.join
|
191
|
+
end
|
192
|
+
|
193
|
+
# Removes a prefix from a string
|
194
|
+
# @param str [String] the string to remove the prefix from
|
195
|
+
# @param prefix [String] the prefix to remove
|
196
|
+
# @return [String] the string with the prefix removed
|
197
|
+
def strip_prefix(str, prefix)
|
198
|
+
str.start_with?(prefix) ? str.slice(prefix.length..-1) : str
|
199
|
+
end
|
200
|
+
|
201
|
+
# Removes a suffix from a string
|
202
|
+
# @param str [String] the string to remove the suffix from
|
203
|
+
# @param suffix [String] the suffix to remove
|
204
|
+
# @return [String] the string with the suffix removed
|
205
|
+
def strip_suffix(str, suffix)
|
206
|
+
str.end_with?(suffix) ? str.slice(0...-suffix.length) : str
|
207
|
+
end
|
208
|
+
end
|
209
|
+
end
|
210
|
+
end
|
@@ -0,0 +1,123 @@
|
|
1
|
+
require 'aspire/cli/command'
|
2
|
+
require 'aspire/enumerator/report_enumerator'
|
3
|
+
require 'aspire/util'
|
4
|
+
require 'logglier'
|
5
|
+
require 'dotenv'
|
6
|
+
|
7
|
+
module Aspire
|
8
|
+
module CLI
|
9
|
+
|
10
|
+
class CacheBuilder < Command
|
11
|
+
|
12
|
+
def execute
|
13
|
+
|
14
|
+
unless (env_file.nil? || env_file.empty?)
|
15
|
+
Dotenv.load(env_file)
|
16
|
+
end
|
17
|
+
|
18
|
+
@json_api = json_api
|
19
|
+
@linked_data_api = linked_data_api
|
20
|
+
@logger = create_logger log_to_file?
|
21
|
+
@cache_path = ENV['ASPIRE_CACHE_PATH']
|
22
|
+
@list_report = ENV['ASPIRE_LIST_REPORT']
|
23
|
+
@mode = ENV['ASPIRE_CACHE_MODE']
|
24
|
+
@mode = @mode.nil? || @mode.empty? ? 0o700 : @mode.to_i(8)
|
25
|
+
cache = Aspire::Caching::Cache.new(@linked_data_api, @json_api, @cache_path,
|
26
|
+
logger: @logger)
|
27
|
+
@builder = Aspire::Caching::Builder.new(cache)
|
28
|
+
|
29
|
+
if list_uri.nil? || list_uri.empty?
|
30
|
+
|
31
|
+
raise ArgumentError if privacy_control.nil? || privacy_control.empty?
|
32
|
+
|
33
|
+
puts "Caching all lists that match arguments"
|
34
|
+
|
35
|
+
lists = list_enumerator time_period_list, status, privacy_control
|
36
|
+
|
37
|
+
@builder.build(lists)
|
38
|
+
|
39
|
+
puts "Finished caching all lists that match arguments"
|
40
|
+
|
41
|
+
else
|
42
|
+
puts "Caching list #{list_uri}"
|
43
|
+
@builder.write_list(list_uri)
|
44
|
+
puts "Finished caching list"
|
45
|
+
end
|
46
|
+
|
47
|
+
end
|
48
|
+
|
49
|
+
private
|
50
|
+
|
51
|
+
def list_enumerator(time_periods=nil, status=nil, privacy_control=nil)
|
52
|
+
|
53
|
+
filters = []
|
54
|
+
|
55
|
+
if time_periods.nil? || time_periods.empty? || time_periods == ['']
|
56
|
+
time_periods = [nil, '']
|
57
|
+
end
|
58
|
+
|
59
|
+
filters.push(proc { |row| time_periods.include?(row['Time Period']) })
|
60
|
+
|
61
|
+
unless status.nil? || status.empty?
|
62
|
+
filters.push(proc { |row| row['Status'].to_s.start_with?(status) })
|
63
|
+
end
|
64
|
+
|
65
|
+
unless privacy_control.nil? || status.empty?
|
66
|
+
filters.push(proc { |row| row['Privacy Control'] == privacy_control })
|
67
|
+
end
|
68
|
+
|
69
|
+
Aspire::Enumerator::ReportEnumerator.new(@list_report, filters)
|
70
|
+
.enumerator
|
71
|
+
end
|
72
|
+
|
73
|
+
def json_api
|
74
|
+
@api_available = ENV['ASPIRE_API_AVAILABLE'] == 'true'
|
75
|
+
@api_client_id = ENV['ASPIRE_API_CLIENT_ID']
|
76
|
+
@api_secret = ENV['ASPIRE_API_SECRET']
|
77
|
+
@tenant = ENV['ASPIRE_TENANT']
|
78
|
+
Aspire::API::JSON.new(@api_client_id, @api_secret, @tenant,
|
79
|
+
**api_opts)
|
80
|
+
end
|
81
|
+
|
82
|
+
def api_opts
|
83
|
+
@ssl_ca_file = ENV['SSL_CA_FILE']
|
84
|
+
@ssl_ca_path = ENV['SSL_CA_PATH']
|
85
|
+
{
|
86
|
+
ssl_ca_file: @ssl_ca_file,
|
87
|
+
ssl_ca_path: @ssl_ca_path
|
88
|
+
}
|
89
|
+
end
|
90
|
+
|
91
|
+
def linked_data_api
|
92
|
+
@api_available = ENV['ASPIRE_API_AVAILABLE'] == 'true'
|
93
|
+
@linked_data_root = ENV['ASPIRE_LINKED_DATA_ROOT']
|
94
|
+
@tenant = ENV['ASPIRE_TENANT']
|
95
|
+
@tenancy_host_aliases = ENV['ASPIRE_TENANCY_HOST_ALIASES'].to_s.split(';')
|
96
|
+
@tenancy_root = ENV['ASPIRE_TENANCY_ROOT']
|
97
|
+
Aspire::API::LinkedData.new(@tenant,
|
98
|
+
linked_data_root: @linked_data_root,
|
99
|
+
tenancy_host_aliases: @tenancy_host_aliases,
|
100
|
+
tenancy_root: @tenancy_root,
|
101
|
+
**api_opts)
|
102
|
+
end
|
103
|
+
|
104
|
+
def create_logger log_to_file
|
105
|
+
|
106
|
+
@log_file = ENV['ASPIRE_LOG']
|
107
|
+
|
108
|
+
if log_to_file
|
109
|
+
logger = Logger.new("| tee #{@log_file}") # @log_file || STDOUT)
|
110
|
+
logger.datetime_format = '%Y-%m-%d %H:%M:%S'
|
111
|
+
logger.formatter = proc do |severity, datetime, _program, msg|
|
112
|
+
"#{datetime} [#{severity}]: #{msg}\n"
|
113
|
+
end
|
114
|
+
return logger
|
115
|
+
end
|
116
|
+
|
117
|
+
Logglier.new("https://logs-01.loggly.com/inputs/#{ENV['LOGGLIER_TOKEN']}/tag/#{ENV['LOGGLIER_TAG']}/", :threaded => true, :format => :json)
|
118
|
+
end
|
119
|
+
|
120
|
+
end
|
121
|
+
|
122
|
+
end
|
123
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
require 'clamp'
|
2
|
+
|
3
|
+
module Aspire
|
4
|
+
module CLI
|
5
|
+
|
6
|
+
class Command < Clamp::Command
|
7
|
+
|
8
|
+
# option ['-c', '--hierarchy-code'], 'HIERARCHY_CODE', 'the hierarchy code (module etc.)'
|
9
|
+
option ['-e', '--env-file'], 'ENV_FILE', 'file containing env variable key value pairs'
|
10
|
+
option ['-l', '--list-uri'], 'LIST_URI', 'the list URI'
|
11
|
+
option ['-t', '--time-period'], 'TIME_PERIOD', 'the time period (2016-17 etc.)', :multivalued => true
|
12
|
+
option ['-p', '--privacy-control'], 'PRIVACY_CONTROL', 'the list privacy control (Public etc)'
|
13
|
+
option ['-s', '--status'], 'STATUS', 'the list status control (Published etc)'
|
14
|
+
option ['-c', '--clear-cache'], :flag, 'clear cache before running', default: false
|
15
|
+
option ['-f', '--log-to-file'], :flag, 'log output to file', default: false
|
16
|
+
|
17
|
+
end
|
18
|
+
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
module Aspire
|
2
|
+
# Enumerator classes for Aspire reading list processing
|
3
|
+
module Enumerator
|
4
|
+
# The abstract base class for enumerator classes
|
5
|
+
# @abstract Subclasses must implement #enumerate accepting the parameters
|
6
|
+
# passed to #enumerator and yielding values to self.yielder
|
7
|
+
class Base
|
8
|
+
# The Enumerator::Yielder instance from an Enumerator.new call
|
9
|
+
# @!attribute [rw] yielder
|
10
|
+
# @return [Enumerator::Yielder] the yielder instance from an Enumerator
|
11
|
+
attr_accessor :yielder
|
12
|
+
|
13
|
+
# Enumerates the data passed in its arguments
|
14
|
+
# @abstract Subclasses must implement this method
|
15
|
+
def enumerate(*args, **kwargs)
|
16
|
+
raise NotImplementedError
|
17
|
+
end
|
18
|
+
|
19
|
+
# Returns an enumerator enumerating property/value pairs of JSON data
|
20
|
+
# @return [Enumerator] the enumerator
|
21
|
+
def enumerator(*args, **kwargs)
|
22
|
+
::Enumerator.new do |yielder|
|
23
|
+
self.yielder = yielder
|
24
|
+
enumerate(*args, **kwargs)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|