berkeley_library-tind 0.4.0

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.
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,118 @@
1
+ require 'spec_helper'
2
+
3
+ module BerkeleyLibrary
4
+ module TIND
5
+ # noinspection RubyYardParamTypeMatch
6
+ module Export
7
+ describe Table do
8
+ describe Row do
9
+ describe :values do
10
+
11
+ it 'returns the values for the specified row' do
12
+ records = MARC::XMLReader.read('spec/data/records-manual-search.xml', freeze: true).to_a
13
+ table = Table.from_records(records)
14
+
15
+ record = records[0]
16
+ expected_values = record.data_fields.map(&:subfields).flatten.map(&:value)
17
+
18
+ table << record
19
+ values = table.rows[0].values
20
+
21
+ # match_array gives a better error message than #eq() but doesn't enforce order
22
+ expect(values).to match_array(expected_values)
23
+ expect(values).to eq(expected_values)
24
+ end
25
+
26
+ it 'handles adding records with extra fields' do
27
+ records = %w[184453 184458].map { |n| MARC::XMLReader.read("spec/data/record-#{n}.xml", freeze: true).first }
28
+ table = Table.from_records(records, freeze: true)
29
+
30
+ vv_actual = (0..1).map { |row| table.rows[row].values }
31
+
32
+ vv_expected = records.map { |r| r.data_fields.map(&:subfields).flatten.map(&:value) }
33
+ vv_expected.each_with_index do |expected, index|
34
+ expect(vv_actual[index].compact).to eq(expected)
35
+ end
36
+
37
+ # 184458 has 260 & 269, 184453 doesn't
38
+ vv_184453 = vv_actual[0]
39
+ vv_184458 = vv_actual[1]
40
+ expect(vv_184453.size).to eq(vv_184458.size)
41
+ expect(vv_184453[3..4]).to contain_exactly(nil, nil)
42
+ end
43
+
44
+ it 'handles records with missing fields' do
45
+ records = %w[184458 184453].map { |n| MARC::XMLReader.read("spec/data/record-#{n}.xml", freeze: true).first }
46
+ table = Table.from_records(records, freeze: true)
47
+
48
+ vv_actual = (0..1).map { |row| table.rows[row].values }
49
+
50
+ vv_expected = records.map { |r| r.data_fields.map(&:subfields).flatten.map(&:value) }
51
+ vv_expected.each_with_index do |expected, index|
52
+ expect(vv_actual[index].compact).to eq(expected)
53
+ end
54
+
55
+ # 184458 has 260 & 269, 184453 doesn't
56
+ vv_184458 = vv_actual[0]
57
+ vv_184453 = vv_actual[1]
58
+ expect(vv_184453.size).to eq(vv_184458.size)
59
+ expect(vv_184453[3..4]).to contain_exactly(nil, nil)
60
+ end
61
+
62
+ it 'handles records with disjoint fields' do
63
+ records = MARC::XMLReader.read('spec/data/disjoint-records.xml', freeze: true).to_a
64
+ table = Table.from_records(records, freeze: true)
65
+
66
+ vv_actual = (0...records.size).map { |row| table.rows[row].values }
67
+
68
+ vv_expected = records.map { |r| r.data_fields.map(&:subfields).flatten.map(&:value) }
69
+ vv_expected.each_with_index do |expected, index|
70
+ expect(vv_actual[index]).not_to be_nil
71
+ expect(vv_actual[index].compact).to eq(expected)
72
+ end
73
+ end
74
+
75
+ end
76
+
77
+ describe :each_value do
78
+ it 'yields the values' do
79
+ records = MARC::XMLReader.read('spec/data/disjoint-records.xml', freeze: true).to_a
80
+ table = Table.from_records(records, freeze: true)
81
+
82
+ vv_actual = table.each_row.with_object([]) do |row, vv_all|
83
+ vv_row = [].tap do |vv|
84
+ row.each_value { |v| vv << v }
85
+ end
86
+ vv_all << vv_row
87
+ end
88
+
89
+ vv_expected = records.map { |r| r.data_fields.map(&:subfields).flatten.map(&:value) }
90
+ vv_expected.each_with_index do |expected, index|
91
+ expect(vv_actual[index]).not_to be_nil
92
+ expect(vv_actual[index].compact).to eq(expected)
93
+ end
94
+ end
95
+
96
+ it 'is indexable' do
97
+ records = MARC::XMLReader.read('spec/data/disjoint-records.xml', freeze: true).to_a
98
+ table = Table.from_records(records, freeze: true)
99
+
100
+ vv_actual = table.each_row.with_object([]) do |row, vv_all|
101
+ vv_row = [].tap do |vv|
102
+ row.each_value.with_index { |v, i| vv[i] = v }
103
+ end
104
+ vv_all << vv_row
105
+ end
106
+
107
+ vv_expected = records.map { |r| r.data_fields.map(&:subfields).flatten.map(&:value) }
108
+ vv_expected.each_with_index do |expected, index|
109
+ expect(vv_actual[index]).not_to be_nil
110
+ expect(vv_actual[index].compact).to eq(expected)
111
+ end
112
+ end
113
+ end
114
+ end
115
+ end
116
+ end
117
+ end
118
+ end
@@ -0,0 +1,322 @@
1
+ require 'spec_helper'
2
+
3
+ module BerkeleyLibrary
4
+ module TIND
5
+ # noinspection RubyYardParamTypeMatch
6
+ module Export
7
+ describe Table do
8
+
9
+ describe :<< do
10
+ let(:table) { Table.new }
11
+ let(:records) { MARC::XMLReader.read('spec/data/records-manual-search.xml', freeze: true).to_a }
12
+
13
+ it 'adds one record' do
14
+ table << (record = records.first)
15
+ expect(table.marc_records).to contain_exactly(record)
16
+ end
17
+
18
+ it 'adds multiple records' do
19
+ records.each { |r| table << r }
20
+ expect(table.marc_records).to eq(records)
21
+ end
22
+
23
+ it 'logs the MARC record ID and data field in the event of a bad indicator' do
24
+ tag = '245'
25
+ ind_bad = '!'
26
+
27
+ record = records.first
28
+ record[tag].indicator1 = ind_bad
29
+ expect { table << record }.to raise_error(Export::ExportException) do |e|
30
+ expect(e.message).to include('184458')
31
+ expect(e.message).to include(tag)
32
+ expect(e.message).to include(ind_bad)
33
+ end
34
+ end
35
+ end
36
+
37
+ describe :headers do
38
+ it 'aligns with the correct values' do
39
+ records = MARC::XMLReader.read('spec/data/disjoint-records.xml', freeze: true).to_a
40
+ table = Table.from_records(records)
41
+
42
+ headers = table.headers
43
+ records.each_with_index do |record, row|
44
+ values = table.rows[row].values
45
+ expect(values).not_to be_nil, "No values found for row #{row}"
46
+
47
+ # uncomment this to debug:
48
+
49
+ # puts "#{row}) #{record['245']['rows']}: #{record['500']['rows']}"
50
+ # headers.each_with_index { |h, i| puts "\t#{h}\t#{values[i]}" }
51
+ # puts "\n" if row + 1 < records.size
52
+
53
+ aggregate_failures 'headers' do
54
+ last_col = -1
55
+ record.data_fields_by_tag.each do |tag, dff|
56
+ dff.each do |df|
57
+ ind1 = df.indicator1 == ' ' ? '_' : df.indicator1
58
+ ind2 = df.indicator2 == ' ' ? '_' : df.indicator2
59
+ df_prefix = "#{tag}#{ind1}#{ind2}"
60
+
61
+ expected_suffix = nil
62
+ df.subfields.each do |sf|
63
+ col = values.find_index(sf.value)
64
+ expect(col).to be > last_col
65
+ last_col = col
66
+
67
+ expected_prefix = "#{df_prefix}#{sf.code}"
68
+
69
+ header = headers[col]
70
+ expect(header).to start_with(expected_prefix)
71
+
72
+ header_suffix = header.scan(/(?<=-)[0-9]+$/).first.to_i
73
+ if expected_suffix
74
+ expect(header_suffix).to eq(expected_suffix), "#{row}, #{col}: (#{sf.value}) Expected #{expected_prefix}-#{expected_suffix}, was #{header}"
75
+ else
76
+ expected_suffix = header_suffix
77
+ end
78
+ end
79
+ end
80
+ end
81
+ end
82
+ end
83
+ end
84
+ end
85
+
86
+ describe :freeze do
87
+ it 'prevents adding new records' do
88
+ records = MARC::XMLReader.read('spec/data/records-manual-search.xml', freeze: true).to_a
89
+ table = records[0...3].each_with_object(Table.new) { |r, t| t << r }
90
+ table.freeze
91
+ expect(table.row_count).to eq(3) # just to be sure
92
+ original_headers = table.headers.dup
93
+
94
+ # noinspection RubyModifiedFrozenObject
95
+ expect { table << records.last }.to raise_error(FrozenError)
96
+ expect(table.row_count).to eq(3)
97
+ expect(table.headers).to eq(original_headers)
98
+ end
99
+
100
+ it 'freezes the MARC records array' do
101
+ records = MARC::XMLReader.read('spec/data/records-manual-search.xml', freeze: true).to_a
102
+ table = records.each_with_object(Table.new) { |r, t| t << r }
103
+ table.freeze
104
+ expect { table.marc_records << records.last }.to raise_error(FrozenError)
105
+ end
106
+
107
+ it 'freezes the columns' do
108
+ records = MARC::XMLReader.read('spec/data/records-manual-search.xml', freeze: true).to_a
109
+ table = records.each_with_object(Table.new) { |r, t| t << r }
110
+ table.freeze
111
+
112
+ expect { table.columns << Object.new }.to raise_error(FrozenError)
113
+ end
114
+
115
+ it 'freezes the rows' do
116
+ records = MARC::XMLReader.read('spec/data/records-manual-search.xml', freeze: true).to_a
117
+ table = records.each_with_object(Table.new) { |r, t| t << r }
118
+ table.freeze
119
+
120
+ expect { table.rows << Object.new }.to raise_error(FrozenError)
121
+ end
122
+
123
+ it 'returns self' do
124
+ records = MARC::XMLReader.read('spec/data/records-manual-search.xml', freeze: true).to_a
125
+ table = records.each_with_object(Table.new) { |r, t| t << r }
126
+ expect(table.freeze).to be(table)
127
+ end
128
+ end
129
+
130
+ describe :from_records do
131
+ let(:records) { MARC::XMLReader.read('spec/data/records-manual-search.xml', freeze: true).to_a }
132
+ it 'reads the records' do
133
+ table = Table.from_records(records)
134
+ expect(table.row_count).to eq(records.size)
135
+ end
136
+
137
+ it 'optionally freezes the table' do
138
+ table = Table.from_records(records, freeze: true)
139
+ expect(table.frozen?).to eq(true)
140
+ end
141
+
142
+ it 'parses the data fields into rows' do
143
+ table = Table.from_records(records, freeze: true)
144
+
145
+ aggregate_failures 'row parsing' do
146
+ records.each_with_index do |marc_record, row|
147
+ expected_headers = []
148
+ expected_values = []
149
+
150
+ marc_record.each_data_field.each do |df|
151
+ prefix = ColumnGroup.prefix_for(df)
152
+ df.subfields.each do |sf|
153
+ expected_headers << "#{prefix}#{sf.code}"
154
+ expected_values << sf.value
155
+ end
156
+ end
157
+
158
+ expected_index = 0
159
+ table.rows[row].values.each_with_index do |actual_value, index|
160
+ next unless actual_value
161
+
162
+ expected_header = expected_headers[expected_index]
163
+ actual_header = table.headers[index]
164
+ expect(actual_header).to start_with(expected_header)
165
+
166
+ expected_value = expected_values[expected_index]
167
+ expect(actual_value).to eq(expected_value)
168
+
169
+ expected_index += 1
170
+ end
171
+ end
172
+ end
173
+ end
174
+
175
+ describe :exportable_only do
176
+ it 'filters out non-exportable values' do
177
+ excluded_prefixes = (Filter::DO_NOT_EXPORT_FIELDS + Filter::DO_NOT_EXPORT_SUBFIELDS).map { |h| h.gsub(' ', '_') }
178
+ table = Table.from_records(records, freeze: true, exportable_only: true)
179
+
180
+ aggregate_failures 'row parsing' do
181
+ records.each_with_index do |marc_record, row|
182
+ expected_headers = []
183
+ expected_values = []
184
+
185
+ marc_record.each_data_field.each do |df|
186
+ prefix = ColumnGroup.prefix_for(df)
187
+ df.subfields.each do |sf|
188
+ header = "#{prefix}#{sf.code}"
189
+ next if excluded_prefixes.any? { |h| header.start_with?(h) }
190
+
191
+ expected_headers << header
192
+ expected_values << sf.value
193
+ end
194
+ end
195
+
196
+ expected_index = 0
197
+ table.rows[row].values.each_with_index do |actual_value, index|
198
+ next unless actual_value
199
+
200
+ expected_header = expected_headers[expected_index]
201
+ actual_header = table.headers[index]
202
+ expect(actual_header).to start_with(expected_header)
203
+
204
+ expected_value = expected_values[expected_index]
205
+ expect(actual_value).to eq(expected_value)
206
+
207
+ expected_index += 1
208
+ end
209
+ end
210
+ end
211
+ end
212
+ end
213
+ end
214
+
215
+ describe :rows do
216
+
217
+ it 'returns the rows' do
218
+ records = MARC::XMLReader.read('spec/data/records-manual-search.xml', freeze: true).to_a
219
+ table = Table.from_records(records)
220
+ rows = table.rows
221
+ expect(rows.size).to eq(records.size)
222
+ expect(rows.all? { |r| r.is_a?(Row) }).to eq(true)
223
+ end
224
+
225
+ it 'is not cached when table is not frozen' do
226
+ records = MARC::XMLReader.read('spec/data/records-manual-search.xml', freeze: true).to_a
227
+ some_records = records[0...3]
228
+ table = Table.from_records(some_records)
229
+ rows1 = table.rows
230
+ expect(rows1.size).to eq(some_records.size)
231
+ expect(rows1.all? { |r| r.is_a?(Row) }).to eq(true)
232
+
233
+ table << records.last
234
+ rows2 = table.rows
235
+ expect(rows2).not_to be(rows1)
236
+ expect(rows1.size).to eq(some_records.size) # just to be sure
237
+ expect(rows2.size).to eq(rows1.size + 1)
238
+ end
239
+
240
+ it 'is cached when table is frozen' do
241
+ records = MARC::XMLReader.read('spec/data/records-manual-search.xml', freeze: true).to_a
242
+ some_records = records[0...3]
243
+ table = Table.from_records(some_records, freeze: true)
244
+ rows = table.rows
245
+ expect(rows.size).to eq(some_records.size)
246
+ expect(table.rows).to be(rows)
247
+ end
248
+
249
+ end
250
+
251
+ describe :each_row do
252
+ let(:records) { MARC::XMLReader.read('spec/data/records-manual-search.xml', freeze: true).to_a }
253
+ let(:table) { Table.from_records(records) }
254
+
255
+ it 'yields each row' do
256
+ rows = []
257
+ table.each_row { |r| rows << r }
258
+ expect(rows.size).to eq(records.size)
259
+ expect(rows.all? { |r| r.is_a?(Row) }).to eq(true)
260
+ end
261
+
262
+ it 'returns an enumerator' do
263
+ enum = table.each_row
264
+ expect(enum).to be_an(Enumerator)
265
+ rows = enum.each_with_object([]) { |r, a| a << r }
266
+ expect(rows.size).to eq(records.size)
267
+ expect(rows.all? { |r| r.is_a?(Row) }).to eq(true)
268
+ end
269
+
270
+ it 'is indexable' do
271
+ rows = []
272
+ table.each_row.with_index do |row, i|
273
+ expect(rows[i]).to be_nil # just to be sure
274
+ rows[i] = row
275
+ end
276
+ expect(rows.size).to eq(records.size)
277
+ end
278
+ end
279
+
280
+ describe :to_csv do
281
+ let(:records) { MARC::XMLReader.read('spec/data/records-manual-search.xml', freeze: true) }
282
+ let(:table) { Table.from_records(records, freeze: true) }
283
+
284
+ it 'outputs CSV' do
285
+ csv_str = table.to_csv
286
+ expect(csv_str).to be_a(String)
287
+
288
+ CSV.parse(csv_str, headers: true).each_with_index do |csv_row, row|
289
+ expect(csv_row.headers).to eq(table.headers)
290
+ expect(csv_row.fields).to eq(table.rows[row].values)
291
+ end
292
+ end
293
+
294
+ it 'accepts an IO object' do
295
+ csv_str = StringIO.new.tap { |out| table.to_csv(out) }.string
296
+
297
+ aggregate_failures 'rows' do
298
+ CSV.parse(csv_str, headers: true).each_with_index do |csv_row, row|
299
+ expect(csv_row.headers).to eq(table.headers)
300
+ expect(csv_row.fields).to eq(table.rows[row].values)
301
+ end
302
+ end
303
+ end
304
+
305
+ it 'accepts a filename' do
306
+ Dir.mktmpdir(File.basename(__FILE__, '.rb')) do |dir|
307
+ out_path = File.join(dir, 'out.csv')
308
+ table.to_csv(out_path)
309
+ csv_str = File.read(out_path)
310
+
311
+ CSV.parse(csv_str, headers: true).each_with_index do |csv_row, row|
312
+ expect(csv_row.headers).to eq(table.headers)
313
+ expect(csv_row.fields).to eq(table.rows[row].values)
314
+ end
315
+ end
316
+ end
317
+ end
318
+
319
+ end
320
+ end
321
+ end
322
+ end
@@ -0,0 +1,93 @@
1
+ require 'spec_helper'
2
+
3
+ module BerkeleyLibrary
4
+ module TIND
5
+ module MARC
6
+ describe XMLReader do
7
+ let(:basename) { File.basename(__FILE__, '.rb') }
8
+
9
+ it 'reads MARC records' do
10
+ reader = XMLReader.new('spec/data/records-api-search.xml')
11
+ records = reader.to_a
12
+ expect(records.size).to eq(5)
13
+
14
+ record0 = records[0]
15
+ expect(record0).to be_a(::MARC::Record)
16
+ expect(record0['024']['a']).to eq('BANC PIC 1982.078:15--ALB')
17
+ end
18
+
19
+ describe :new do
20
+ it 'accepts a string path' do
21
+ path = 'spec/data/records-api-search.xml'
22
+ reader = XMLReader.new(path)
23
+ records = reader.to_a
24
+ expect(records.size).to eq(5)
25
+ end
26
+
27
+ it 'accepts a Pathname' do
28
+ path = Pathname.new('spec/data/records-api-search.xml')
29
+ reader = XMLReader.new(path)
30
+ records = reader.to_a
31
+ expect(records.size).to eq(5)
32
+ end
33
+
34
+ it 'accepts an XML string' do
35
+ xml = File.read('spec/data/records-api-search.xml')
36
+ reader = XMLReader.new(xml)
37
+ records = reader.to_a
38
+ expect(records.size).to eq(5)
39
+ end
40
+
41
+ it 'accepts an IO' do
42
+ File.open('spec/data/records-api-search.xml', 'rb') do |f|
43
+ reader = XMLReader.new(f)
44
+ records = reader.to_a
45
+ expect(records.size).to eq(5)
46
+ end
47
+ end
48
+
49
+ it 'raises ArgumentError if passed a nonexistent file' do
50
+ output_path = Dir.mktmpdir(basename) { |dir| File.join(dir, "#{basename}-missing.xml") }
51
+ expect(File.exist?(output_path)).to be_falsey # just to be sure
52
+ expect { XMLReader.new(output_path) }.to raise_error(ArgumentError)
53
+ end
54
+
55
+ it 'raises ArgumentError if passed a non-XML string' do
56
+ non_xml = File.read('spec/data/collections.json')
57
+ expect { XMLReader.new(non_xml) }.to raise_error(ArgumentError)
58
+ end
59
+
60
+ it 'raises ArgumentError if passed something random' do
61
+ non_xml = Object.new
62
+ # noinspection RubyYardParamTypeMatch
63
+ expect { XMLReader.new(non_xml) }.to raise_error(ArgumentError)
64
+ end
65
+ end
66
+
67
+ describe :total do
68
+ it 'parses <total/>' do
69
+ reader = XMLReader.new('spec/data/records-api-search.xml')
70
+ reader.take_while { true } # make sure we've parsed something
71
+ expect(reader.total).to eq(5)
72
+ end
73
+
74
+ it 'parses "Search-Engine-Total-Number-Of-Results"' do
75
+ reader = XMLReader.new('spec/data/records-manual-search.xml')
76
+ reader.take_while { true } # make sure we've parsed something
77
+ expect(reader.total).to eq(10)
78
+ end
79
+ end
80
+
81
+ describe :search_id do
82
+ it 'parses <search_id>' do
83
+ expected = 'DnF1ZXJ5VGhlbkZldG'
84
+ reader = XMLReader.new('spec/data/records-api-search-p1.xml')
85
+ reader.take_while { true } # make sure we've parsed something
86
+ expect(reader.search_id).to eq(expected)
87
+ end
88
+ end
89
+
90
+ end
91
+ end
92
+ end
93
+ end