berkeley_library-tind 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (162) hide show
  1. checksums.yaml +7 -0
  2. data/.github/workflows/build.yml +18 -0
  3. data/.gitignore +388 -0
  4. data/.idea/inspectionProfiles/Project_Default.xml +20 -0
  5. data/.idea/misc.xml +4 -0
  6. data/.idea/modules.xml +8 -0
  7. data/.idea/tind.iml +138 -0
  8. data/.idea/vcs.xml +6 -0
  9. data/.rubocop.yml +334 -0
  10. data/.ruby-version +1 -0
  11. data/.simplecov +8 -0
  12. data/.yardopts +1 -0
  13. data/CHANGES.md +58 -0
  14. data/Dockerfile +57 -0
  15. data/Gemfile +3 -0
  16. data/Jenkinsfile +18 -0
  17. data/LICENSE.md +21 -0
  18. data/README.md +73 -0
  19. data/Rakefile +20 -0
  20. data/berkeley_library-tind.gemspec +50 -0
  21. data/bin/tind-export +14 -0
  22. data/docker-compose.yml +15 -0
  23. data/lib/berkeley_library/tind.rb +3 -0
  24. data/lib/berkeley_library/tind/api.rb +1 -0
  25. data/lib/berkeley_library/tind/api/api.rb +132 -0
  26. data/lib/berkeley_library/tind/api/api_exception.rb +131 -0
  27. data/lib/berkeley_library/tind/api/collection.rb +82 -0
  28. data/lib/berkeley_library/tind/api/date_range.rb +67 -0
  29. data/lib/berkeley_library/tind/api/format.rb +32 -0
  30. data/lib/berkeley_library/tind/api/search.rb +100 -0
  31. data/lib/berkeley_library/tind/config.rb +103 -0
  32. data/lib/berkeley_library/tind/export.rb +1 -0
  33. data/lib/berkeley_library/tind/export/column.rb +54 -0
  34. data/lib/berkeley_library/tind/export/column_group.rb +144 -0
  35. data/lib/berkeley_library/tind/export/column_group_list.rb +131 -0
  36. data/lib/berkeley_library/tind/export/column_width_calculator.rb +76 -0
  37. data/lib/berkeley_library/tind/export/config.rb +154 -0
  38. data/lib/berkeley_library/tind/export/csv_exporter.rb +29 -0
  39. data/lib/berkeley_library/tind/export/export.rb +47 -0
  40. data/lib/berkeley_library/tind/export/export_command.rb +168 -0
  41. data/lib/berkeley_library/tind/export/export_exception.rb +8 -0
  42. data/lib/berkeley_library/tind/export/export_format.rb +67 -0
  43. data/lib/berkeley_library/tind/export/exporter.rb +105 -0
  44. data/lib/berkeley_library/tind/export/filter.rb +52 -0
  45. data/lib/berkeley_library/tind/export/no_results_error.rb +7 -0
  46. data/lib/berkeley_library/tind/export/ods_exporter.rb +138 -0
  47. data/lib/berkeley_library/tind/export/row.rb +24 -0
  48. data/lib/berkeley_library/tind/export/row_metrics.rb +18 -0
  49. data/lib/berkeley_library/tind/export/table.rb +175 -0
  50. data/lib/berkeley_library/tind/export/table_metrics.rb +116 -0
  51. data/lib/berkeley_library/tind/marc.rb +1 -0
  52. data/lib/berkeley_library/tind/marc/xml_reader.rb +144 -0
  53. data/lib/berkeley_library/tind/module_info.rb +14 -0
  54. data/lib/berkeley_library/util/arrays.rb +178 -0
  55. data/lib/berkeley_library/util/logging.rb +1 -0
  56. data/lib/berkeley_library/util/ods/spreadsheet.rb +170 -0
  57. data/lib/berkeley_library/util/ods/xml/content_doc.rb +26 -0
  58. data/lib/berkeley_library/util/ods/xml/document_node.rb +57 -0
  59. data/lib/berkeley_library/util/ods/xml/element_node.rb +106 -0
  60. data/lib/berkeley_library/util/ods/xml/loext/table_protection.rb +26 -0
  61. data/lib/berkeley_library/util/ods/xml/manifest/file_entry.rb +42 -0
  62. data/lib/berkeley_library/util/ods/xml/manifest/manifest.rb +73 -0
  63. data/lib/berkeley_library/util/ods/xml/manifest_doc.rb +26 -0
  64. data/lib/berkeley_library/util/ods/xml/namespace.rb +46 -0
  65. data/lib/berkeley_library/util/ods/xml/office/automatic_styles.rb +181 -0
  66. data/lib/berkeley_library/util/ods/xml/office/body.rb +17 -0
  67. data/lib/berkeley_library/util/ods/xml/office/document_content.rb +98 -0
  68. data/lib/berkeley_library/util/ods/xml/office/document_styles.rb +39 -0
  69. data/lib/berkeley_library/util/ods/xml/office/font_face_decls.rb +30 -0
  70. data/lib/berkeley_library/util/ods/xml/office/scripts.rb +17 -0
  71. data/lib/berkeley_library/util/ods/xml/office/spreadsheet.rb +37 -0
  72. data/lib/berkeley_library/util/ods/xml/office/styles.rb +39 -0
  73. data/lib/berkeley_library/util/ods/xml/style/cell_style.rb +58 -0
  74. data/lib/berkeley_library/util/ods/xml/style/column_style.rb +36 -0
  75. data/lib/berkeley_library/util/ods/xml/style/default_style.rb +31 -0
  76. data/lib/berkeley_library/util/ods/xml/style/family.rb +85 -0
  77. data/lib/berkeley_library/util/ods/xml/style/font_face.rb +46 -0
  78. data/lib/berkeley_library/util/ods/xml/style/paragraph_properties.rb +30 -0
  79. data/lib/berkeley_library/util/ods/xml/style/row_style.rb +37 -0
  80. data/lib/berkeley_library/util/ods/xml/style/style.rb +44 -0
  81. data/lib/berkeley_library/util/ods/xml/style/table_cell_properties.rb +40 -0
  82. data/lib/berkeley_library/util/ods/xml/style/table_column_properties.rb +30 -0
  83. data/lib/berkeley_library/util/ods/xml/style/table_properties.rb +25 -0
  84. data/lib/berkeley_library/util/ods/xml/style/table_row_properties.rb +28 -0
  85. data/lib/berkeley_library/util/ods/xml/style/table_style.rb +27 -0
  86. data/lib/berkeley_library/util/ods/xml/style/text_properties.rb +52 -0
  87. data/lib/berkeley_library/util/ods/xml/styles_doc.rb +26 -0
  88. data/lib/berkeley_library/util/ods/xml/table/named_expressions.rb +17 -0
  89. data/lib/berkeley_library/util/ods/xml/table/repeatable.rb +38 -0
  90. data/lib/berkeley_library/util/ods/xml/table/table.rb +193 -0
  91. data/lib/berkeley_library/util/ods/xml/table/table_cell.rb +46 -0
  92. data/lib/berkeley_library/util/ods/xml/table/table_column.rb +43 -0
  93. data/lib/berkeley_library/util/ods/xml/table/table_row.rb +136 -0
  94. data/lib/berkeley_library/util/ods/xml/text/p.rb +118 -0
  95. data/lib/berkeley_library/util/paths.rb +111 -0
  96. data/lib/berkeley_library/util/stringios.rb +30 -0
  97. data/lib/berkeley_library/util/strings.rb +42 -0
  98. data/lib/berkeley_library/util/sys_exits.rb +15 -0
  99. data/lib/berkeley_library/util/times.rb +22 -0
  100. data/lib/berkeley_library/util/uris.rb +44 -0
  101. data/lib/berkeley_library/util/uris/appender.rb +162 -0
  102. data/lib/berkeley_library/util/uris/requester.rb +62 -0
  103. data/lib/berkeley_library/util/uris/validator.rb +32 -0
  104. data/rakelib/bundle.rake +8 -0
  105. data/rakelib/coverage.rake +11 -0
  106. data/rakelib/gem.rake +54 -0
  107. data/rakelib/rubocop.rake +18 -0
  108. data/rakelib/spec.rake +2 -0
  109. data/spec/.rubocop.yml +40 -0
  110. data/spec/berkeley_library/tind/api/api_exception_spec.rb +91 -0
  111. data/spec/berkeley_library/tind/api/api_spec.rb +143 -0
  112. data/spec/berkeley_library/tind/api/collection_spec.rb +74 -0
  113. data/spec/berkeley_library/tind/api/date_range_spec.rb +110 -0
  114. data/spec/berkeley_library/tind/api/format_spec.rb +54 -0
  115. data/spec/berkeley_library/tind/api/search_spec.rb +364 -0
  116. data/spec/berkeley_library/tind/config_spec.rb +86 -0
  117. data/spec/berkeley_library/tind/export/column_group_spec.rb +29 -0
  118. data/spec/berkeley_library/tind/export/column_spec.rb +43 -0
  119. data/spec/berkeley_library/tind/export/config_spec.rb +206 -0
  120. data/spec/berkeley_library/tind/export/export_command_spec.rb +169 -0
  121. data/spec/berkeley_library/tind/export/export_format_spec.rb +59 -0
  122. data/spec/berkeley_library/tind/export/export_matcher.rb +112 -0
  123. data/spec/berkeley_library/tind/export/export_spec.rb +150 -0
  124. data/spec/berkeley_library/tind/export/exporter_spec.rb +125 -0
  125. data/spec/berkeley_library/tind/export/row_spec.rb +118 -0
  126. data/spec/berkeley_library/tind/export/table_spec.rb +322 -0
  127. data/spec/berkeley_library/tind/marc/xml_reader_spec.rb +93 -0
  128. data/spec/berkeley_library/util/arrays_spec.rb +340 -0
  129. data/spec/berkeley_library/util/ods/spreadsheet_spec.rb +124 -0
  130. data/spec/berkeley_library/util/ods/xml/content_doc_spec.rb +121 -0
  131. data/spec/berkeley_library/util/ods/xml/manifest/file_entry_spec.rb +27 -0
  132. data/spec/berkeley_library/util/ods/xml/manifest/manifest_spec.rb +33 -0
  133. data/spec/berkeley_library/util/ods/xml/office/document_content_spec.rb +60 -0
  134. data/spec/berkeley_library/util/ods/xml/style/automatic_styles_spec.rb +37 -0
  135. data/spec/berkeley_library/util/ods/xml/style/family_spec.rb +57 -0
  136. data/spec/berkeley_library/util/ods/xml/table/table_row_spec.rb +179 -0
  137. data/spec/berkeley_library/util/ods/xml/table/table_spec.rb +218 -0
  138. data/spec/berkeley_library/util/paths_spec.rb +90 -0
  139. data/spec/berkeley_library/util/stringios_spec.rb +34 -0
  140. data/spec/berkeley_library/util/strings_spec.rb +27 -0
  141. data/spec/berkeley_library/util/times_spec.rb +39 -0
  142. data/spec/berkeley_library/util/uris_spec.rb +118 -0
  143. data/spec/data/collection-names.txt +438 -0
  144. data/spec/data/collections.json +4827 -0
  145. data/spec/data/disjoint-records.xml +187 -0
  146. data/spec/data/record-184453.xml +58 -0
  147. data/spec/data/record-184458.xml +63 -0
  148. data/spec/data/record-187888.xml +78 -0
  149. data/spec/data/records-api-search-cjk-p1.xml +6381 -0
  150. data/spec/data/records-api-search-cjk-p2.xml +5 -0
  151. data/spec/data/records-api-search-p1.xml +4506 -0
  152. data/spec/data/records-api-search-p2.xml +4509 -0
  153. data/spec/data/records-api-search-p3.xml +4506 -0
  154. data/spec/data/records-api-search-p4.xml +4509 -0
  155. data/spec/data/records-api-search-p5.xml +4506 -0
  156. data/spec/data/records-api-search-p6.xml +2436 -0
  157. data/spec/data/records-api-search-p7.xml +5 -0
  158. data/spec/data/records-api-search.xml +234 -0
  159. data/spec/data/records-manual-search.xml +547 -0
  160. data/spec/spec_helper.rb +30 -0
  161. data/test/profile/table_from_records_profile.rb +46 -0
  162. metadata +585 -0
@@ -0,0 +1,131 @@
1
+ require 'net/http/status'
2
+
3
+ module BerkeleyLibrary
4
+ module TIND
5
+ module API
6
+ # Wrapper for network-related exceptions.
7
+ class APIException < StandardError
8
+ # @return [String, nil] the request URI, if any
9
+ attr_reader :url
10
+
11
+ # @return [Hash, nil] the API query parameters, if any
12
+ attr_reader :params
13
+
14
+ # @return [Integer, nil] the numeric HTTP status code, if any
15
+ attr_reader :status_code
16
+
17
+ # @return [String, nil] the HTTP status message, if any
18
+ attr_reader :status_message
19
+
20
+ # @return [RestClient::Response, nil] the response, if any
21
+ attr_reader :response
22
+
23
+ # Initializes a new APIException.
24
+ #
25
+ # @option opts [String] :msg the exception message (if not present, a default message will be constructed)
26
+ # @option opts [String] :url the request URL, if any
27
+ # @option opts [Hash] :params the query or form parameters, if any
28
+ # @option opts [Integer] :status_code the numeric HTTP status code, if any
29
+ # @option opts [String] :status_message a human-readable string representation of the HTTP status
30
+ # (if not present, a default will be constructed)
31
+ # @option opts [RestClient::Response] :response the HTTP response, if any
32
+ def initialize(msg, **opts)
33
+ super(msg)
34
+
35
+ @url = opts[:url].to_s if opts.key?(:url)
36
+ @params = opts[:params]
37
+ @status_code, default_status_message = format_status(opts[:status_code])
38
+ @status_message = opts[:status_message] || default_status_message
39
+ @response = opts[:response]
40
+ end
41
+
42
+ def body
43
+ return @body if instance_variable_defined?(:@body)
44
+
45
+ @body = response && response.body
46
+ end
47
+
48
+ private
49
+
50
+ def format_status(status_code)
51
+ return unless (numeric_status = Integer(status_code, exception: false))
52
+
53
+ status_name = Net::HTTP::STATUS_CODES[numeric_status]
54
+ default_status_message = [numeric_status, status_name].compact.join(' ')
55
+
56
+ [numeric_status, default_status_message]
57
+ end
58
+
59
+ class << self
60
+ # @param ex [Exception] the exception to wrap
61
+ # @option opts [String] :msg the exception message (if not present, a default message will be constructed)
62
+ # @option opts [String] :url the request URL, if any
63
+ # @option opts [Hash] :params the query or form parameters, if any
64
+ # @option opts [String] :msg_context context information to prepend to the default message
65
+ def wrap(ex, **opts)
66
+ raise ArgumentError, "Can't wrap a nil error" unless ex
67
+
68
+ msg = opts[:msg] || message_from(ex, opts[:url], opts[:params], opts[:detail])
69
+ options = format_options(ex, opts[:url], opts[:params])
70
+ APIException.new(msg, **options)
71
+ end
72
+
73
+ private
74
+
75
+ def format_options(ex, url, params)
76
+ {}.tap do |opts|
77
+ opts[:url] = url if url
78
+ opts[:params] = params if params
79
+ next unless %i[http_code message response].all? { |f| ex.respond_to?(f) }
80
+
81
+ opts[:status_code] = ex.http_code
82
+ opts[:status_message] = ex.message
83
+ opts[:response] = ex.response
84
+ end
85
+ end
86
+
87
+ def message_from(ex, url, params, detail)
88
+ ''.tap do |msg|
89
+ msg << "#{detail}: " if detail
90
+ msg << (url ? "#{API.format_request(url, params)} returned #{ex}" : ex.to_s)
91
+ end
92
+ end
93
+ end
94
+ end
95
+
96
+ # Exception raised when the API key is nil or blank.
97
+ #
98
+ # NOTE: TIND incorrectly returns 403 Forbidden in this case, but we don't even bother
99
+ # to ask, we just simulate a 401.
100
+ class APIKeyNotSet < APIException
101
+ # @param endpoint_uri [URI] the endpoint URI
102
+ # @param params [Hash, nil] the query parameters
103
+ def initialize(endpoint_uri, params)
104
+ request_str = API.format_request(endpoint_uri, params)
105
+ super("#{request_str} failed; API key not set", status_code: 401)
106
+ end
107
+ end
108
+
109
+ # Exception raised when the TIND base URI is nil or blank.
110
+ class BaseURINotSet < APIException
111
+ # @param endpoint [String, Symbol] the endpoint
112
+ # @param params [Hash, nil] the query parameters
113
+ def initialize(endpoint, params)
114
+ msg = BaseURINotSet.format_message(endpoint, params)
115
+ super(msg, status_code: 404)
116
+ end
117
+
118
+ class << self
119
+ def format_message(endpoint, params)
120
+ "request to endpoint #{endpoint.inspect}".tap do |msg|
121
+ if (query_string = API.format_query(params))
122
+ msg << " with query #{query_string}"
123
+ end
124
+ msg << ' failed; base URI not set'
125
+ end
126
+ end
127
+ end
128
+ end
129
+ end
130
+ end
131
+ end
@@ -0,0 +1,82 @@
1
+ require 'json'
2
+ require 'berkeley_library/util/logging'
3
+ require 'berkeley_library/tind/api/api_exception'
4
+
5
+ module BerkeleyLibrary
6
+ module TIND
7
+ module API
8
+ class Collection
9
+ attr_reader :name, :nb_rec, :children, :translations
10
+ alias size nb_rec
11
+
12
+ def initialize(name, nb_rec, children, translations)
13
+ @name = name
14
+ @nb_rec = nb_rec
15
+ @children = children
16
+ @translations = translations
17
+ end
18
+
19
+ def name_en
20
+ return unless (names = translations['name'])
21
+
22
+ names['en']
23
+ end
24
+
25
+ def each_descendant(include_self: false, &block)
26
+ yield self if include_self
27
+
28
+ children.each { |c| c.each_descendant(include_self: include_self, &block) }
29
+ end
30
+
31
+ class << self
32
+ include BerkeleyLibrary::Logging
33
+
34
+ ENDPOINT = 'collections'.freeze
35
+
36
+ def all
37
+ json = API.get(ENDPOINT, depth: 100)
38
+ all_from_json(json)
39
+ rescue API::APIException => e
40
+ logger.error(e)
41
+ []
42
+ end
43
+
44
+ def each_collection(&block)
45
+ return to_enum(:each_collection) unless block_given?
46
+
47
+ all.each { |c| c.each_descendant(include_self: true, &block) }
48
+ end
49
+
50
+ # Returns an array of collection tree roots, which can be traversed
51
+ # with {Collection#each_descendant}.
52
+ #
53
+ # @return [Array<Collection>] an array of top-level collections
54
+ def all_from_json(json)
55
+ ensure_hash(json).map do |name, attrs|
56
+ translations = attrs['translations']
57
+ Collection.new(
58
+ name,
59
+ attrs['nb_rec'],
60
+ all_from_json(attrs['children']),
61
+ translations
62
+ )
63
+ end
64
+ end
65
+
66
+ private
67
+
68
+ def ensure_hash(json)
69
+ return {} unless json
70
+ return json if hash_like?(json)
71
+
72
+ JSON.parse(json)
73
+ end
74
+
75
+ def hash_like?(h)
76
+ h.respond_to?(:each_key) && h.respond_to?(:each_value)
77
+ end
78
+ end
79
+ end
80
+ end
81
+ end
82
+ end
@@ -0,0 +1,67 @@
1
+ require 'berkeley_library/util/times'
2
+ require 'berkeley_library/tind/config'
3
+
4
+ module BerkeleyLibrary
5
+ module TIND
6
+ module API
7
+ class DateRange
8
+ FORMAT = '%Y-%m-%d %H:%M:%S'.freeze
9
+
10
+ attr_reader :from_time, :until_time
11
+
12
+ def initialize(from_time:, until_time:, mtime: false)
13
+ @from_time, @until_time = DateRange.ensure_valid_range(from_time, until_time)
14
+ @mtime = mtime
15
+ end
16
+
17
+ def mtime?
18
+ @mtime
19
+ end
20
+
21
+ def to_params
22
+ { d1: format_param(from_time), d2: format_param(until_time) }.tap do |params|
23
+ params[:dt] = 'm' if mtime?
24
+ end
25
+ end
26
+
27
+ alias eql ==
28
+
29
+ def ==(other)
30
+ return false unless other.class == self.class
31
+
32
+ [from_time, until_time] == [other.from_time, other.until_time]
33
+ end
34
+
35
+ class << self
36
+ def from_range(range)
37
+ DateRange.new(from_time: range.first, until_time: range.last)
38
+ end
39
+
40
+ def ensure_date_range(date_range)
41
+ return unless date_range
42
+ return date_range if date_range.is_a?(DateRange)
43
+ return DateRange.from_range(date_range) if date_range.respond_to?(:first) && date_range.respond_to?(:last)
44
+
45
+ raise ArgumentError, "Can't convert #{date_range.inspect} to #{DateRange}"
46
+ end
47
+
48
+ def ensure_valid_range(from_time, until_time)
49
+ ftime, utime = [from_time, until_time].map { |t| BerkeleyLibrary::Util::Times.ensure_utc(t) }
50
+ return [ftime, utime] if ftime <= utime
51
+
52
+ raise ArgumentError, "Not a valid range: #{from_time.inspect}..#{until_time.inspect}"
53
+ end
54
+ end
55
+
56
+ private
57
+
58
+ def format_param(t)
59
+ tz = BerkeleyLibrary::TIND::Config.timezone
60
+ t_utc = BerkeleyLibrary::Util::Times.ensure_utc(t) # just to be sure
61
+ t_local = tz.utc_to_local(t_utc)
62
+ t_local.strftime(FORMAT)
63
+ end
64
+ end
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,32 @@
1
+ require 'typesafe_enum'
2
+
3
+ module BerkeleyLibrary
4
+ module TIND
5
+ module API
6
+ class Format < TypesafeEnum::Base
7
+ %i[ID XML FILES JSON].each { |fmt| new(fmt) }
8
+
9
+ def to_s
10
+ # noinspection RubyYardReturnMatch
11
+ value
12
+ end
13
+
14
+ def to_str
15
+ value
16
+ end
17
+
18
+ class << self
19
+ def ensure_format(format)
20
+ return unless format
21
+ return format if format.is_a?(Format)
22
+
23
+ fmt = Format.find_by_value(format.to_s.downcase)
24
+ return fmt if fmt
25
+
26
+ raise ArgumentError, "Unknown #{Format}: #{format.inspect}"
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,100 @@
1
+ require 'berkeley_library/util/logging'
2
+ require 'berkeley_library/tind/marc/xml_reader'
3
+ require 'berkeley_library/tind/api/date_range'
4
+ require 'berkeley_library/tind/api/format'
5
+
6
+ module BerkeleyLibrary
7
+ module TIND
8
+ module API
9
+ class Search
10
+ include BerkeleyLibrary::Logging
11
+
12
+ attr_reader :collection, :pattern, :index, :date_range, :format
13
+
14
+ def initialize(collection: nil, pattern: nil, index: nil, date_range: nil, format: Format::XML)
15
+ raise ArgumentError, 'Search requires a collection' unless collection
16
+
17
+ @collection = collection
18
+ @pattern = pattern
19
+ @index = index
20
+ @date_range = DateRange.ensure_date_range(date_range)
21
+ @format = Format.ensure_format(format)
22
+ end
23
+
24
+ # rubocop: disable Metrics/AbcSize
25
+ def params
26
+ @params ||= {}.tap do |params|
27
+ params[:c] = collection if collection
28
+ params[:p] = pattern if pattern
29
+ params[:f] = index if index
30
+ params.merge!(date_range.to_params) if date_range
31
+ params[:format] = self.format.to_s if self.format
32
+ end
33
+ end
34
+ # rubocop: enable Metrics/AbcSize
35
+
36
+ # Performs this search and returns the results as array.
37
+ # @return [Array<MARC::Record>] the results
38
+ def results
39
+ each_result.to_a
40
+ end
41
+
42
+ # Iterates over the records returned by this search.
43
+ # @overload each_result(freeze: false, &block)
44
+ # Yields each record to the provided block.
45
+ # @param freeze [Boolean] whether to freeze each record before yielding.
46
+ # @yieldparam marc_record [MARC::Record] each record
47
+ # @return [self]
48
+ # @overload each_result(freeze: false)
49
+ # Returns an enumerator of the records.
50
+ # @param freeze [Boolean] whether to freeze each record before yielding.
51
+ # @return [Enumerable<MARC::Record>] the records
52
+ def each_result(freeze: false, &block)
53
+ return to_enum(:each_result, freeze: freeze) unless block_given?
54
+
55
+ perform_search(freeze: freeze, &block)
56
+ self
57
+ end
58
+
59
+ private
60
+
61
+ def perform_search(search_id: nil, freeze: false, &block)
62
+ logger.info("perform_search(search_id: #{search_id.inspect})")
63
+ params = search_id ? self.params.merge(search_id: search_id) : self.params
64
+ next_search_id = perform_single_search(params, freeze, &block)
65
+ perform_search(search_id: next_search_id, freeze: freeze, &block) if next_search_id
66
+ end
67
+
68
+ def perform_single_search(params, freeze, &block)
69
+ API.get(:search, params) do |body|
70
+ process_body(body, freeze, &block)
71
+ ensure
72
+ body.close
73
+ end
74
+ rescue APIException => e
75
+ return nil if empty_result?(e)
76
+
77
+ raise
78
+ end
79
+
80
+ def process_body(body, freeze, &block)
81
+ xml_reader = BerkeleyLibrary::TIND::MARC::XMLReader.read(body, freeze: freeze)
82
+ xml_reader.each(&block)
83
+ logger.debug("yielded #{xml_reader.records_yielded} of #{xml_reader.total} records")
84
+ logger.debug("next search ID: #{xml_reader.search_id}")
85
+ xml_reader.search_id if xml_reader.records_yielded > 0
86
+ end
87
+
88
+ def empty_result?(api_ex)
89
+ return false unless api_ex.status_code == 500
90
+ return false unless (body = api_ex.body)
91
+ return false unless (result = JSON.parse(body, symbolize_names: true))
92
+
93
+ result[:success] == false && result[:error].include?('0 values')
94
+ rescue JSON::ParserError
95
+ false
96
+ end
97
+ end
98
+ end
99
+ end
100
+ end
@@ -0,0 +1,103 @@
1
+ require 'berkeley_library/util/uris'
2
+ require 'berkeley_library/tind/module_info'
3
+
4
+ require 'tzinfo'
5
+
6
+ module BerkeleyLibrary
7
+ module TIND
8
+ module Config
9
+
10
+ # The environment variable from which to read the TIND API key.
11
+ ENV_TIND_API_KEY = 'LIT_TIND_API_KEY'.freeze
12
+
13
+ # The root URL for the TIND installation
14
+ ENV_TIND_BASE_URL = 'LIT_TIND_BASE_URL'.freeze
15
+
16
+ DEFAULT_TZID = 'America/Los_Angeles'.freeze
17
+ DEFAULT_USER_AGENT = "#{ModuleInfo::NAME} #{ModuleInfo::VERSION} (#{ModuleInfo::HOMEPAGE})".freeze
18
+
19
+ class << self
20
+ include BerkeleyLibrary::Util::URIs
21
+
22
+ # Sets the TIND API key.
23
+ # @param value [String] the API key.
24
+ attr_writer :api_key
25
+
26
+ # Gets the TIND API key.
27
+ # @return [String, nil] the TIND API key, or `nil` if not set.
28
+ def api_key
29
+ @api_key ||= default_tind_api_key
30
+ end
31
+
32
+ def base_uri
33
+ @base_uri ||= default_tind_base_uri
34
+ end
35
+
36
+ def base_uri=(value)
37
+ @base_uri = uri_or_nil(value)
38
+ end
39
+
40
+ def timezone
41
+ @timezone ||= default_timezone
42
+ end
43
+
44
+ def timezone=(value)
45
+ raise ArgumentError, "Not a #{TZInfo::Timezone}" unless value.respond_to?(:utc_to_local)
46
+
47
+ @timezone = value
48
+ end
49
+
50
+ def user_agent
51
+ @user_agent || DEFAULT_USER_AGENT
52
+ end
53
+
54
+ def user_agent=(value)
55
+ raise ArgumentError, 'TIND firewall rules require a user agent' if blank?(value)
56
+
57
+ @user_agent = value
58
+ end
59
+
60
+ def blank?(v)
61
+ v.nil? || v.to_s.strip.empty?
62
+ end
63
+
64
+ private
65
+
66
+ def default_timezone
67
+ TZInfo::Timezone.get(Config::DEFAULT_TZID)
68
+ end
69
+
70
+ def default_tind_api_key
71
+ ENV[Config::ENV_TIND_API_KEY] || rails_tind_api_key
72
+ end
73
+
74
+ def default_tind_base_uri
75
+ return unless (base_url = ENV[Config::ENV_TIND_BASE_URL] || rails_tind_base_uri)
76
+
77
+ uri_or_nil(base_url)
78
+ end
79
+
80
+ def rails_tind_base_uri
81
+ return unless (rails_config = self.rails_config)
82
+ return unless rails_config.respond_to?(:tind_base_uri)
83
+
84
+ rails_config.tind_base_uri
85
+ end
86
+
87
+ def rails_tind_api_key
88
+ return unless (rails_config = self.rails_config)
89
+ return unless rails_config.respond_to?(:tind_api_key)
90
+
91
+ rails_config.tind_api_key
92
+ end
93
+
94
+ def rails_config
95
+ return unless defined?(Rails)
96
+ return unless (app = Rails.application)
97
+
98
+ app.config
99
+ end
100
+ end
101
+ end
102
+ end
103
+ end