berkeley_library-tind 0.4.0 → 0.5.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 (45) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/build.yml +1 -1
  3. data/.idea/inspectionProfiles/Project_Default.xml +18 -0
  4. data/.idea/tind.iml +91 -91
  5. data/.ruby-version +1 -1
  6. data/CHANGES.md +33 -1
  7. data/README.md +15 -1
  8. data/berkeley_library-tind.gemspec +3 -2
  9. data/lib/berkeley_library/tind/api/api.rb +17 -11
  10. data/lib/berkeley_library/tind/api/collection.rb +1 -1
  11. data/lib/berkeley_library/tind/api/search.rb +2 -2
  12. data/lib/berkeley_library/tind/export/exporter.rb +1 -1
  13. data/lib/berkeley_library/tind/export/table.rb +1 -1
  14. data/lib/berkeley_library/tind/export/table_metrics.rb +1 -1
  15. data/lib/berkeley_library/tind/marc/xml_builder.rb +62 -0
  16. data/lib/berkeley_library/tind/marc/xml_reader.rb +32 -19
  17. data/lib/berkeley_library/tind/marc/xml_writer.rb +152 -0
  18. data/lib/berkeley_library/tind/module_info.rb +1 -1
  19. data/lib/berkeley_library/util/files.rb +39 -0
  20. data/lib/berkeley_library/util/ods/spreadsheet.rb +1 -1
  21. data/lib/berkeley_library/util/ods/xml/element_node.rb +1 -1
  22. data/spec/berkeley_library/tind/export/export_spec.rb +3 -1
  23. data/spec/berkeley_library/tind/export/table_spec.rb +2 -0
  24. data/spec/berkeley_library/tind/marc/xml_reader_spec.rb +42 -1
  25. data/spec/berkeley_library/tind/marc/xml_writer_spec.rb +156 -0
  26. data/spec/data/new-records.xml +46 -0
  27. metadata +36 -39
  28. data/Jenkinsfile +0 -18
  29. data/lib/berkeley_library/util/arrays.rb +0 -178
  30. data/lib/berkeley_library/util/logging.rb +0 -1
  31. data/lib/berkeley_library/util/paths.rb +0 -111
  32. data/lib/berkeley_library/util/stringios.rb +0 -30
  33. data/lib/berkeley_library/util/strings.rb +0 -42
  34. data/lib/berkeley_library/util/sys_exits.rb +0 -15
  35. data/lib/berkeley_library/util/times.rb +0 -22
  36. data/lib/berkeley_library/util/uris/appender.rb +0 -162
  37. data/lib/berkeley_library/util/uris/requester.rb +0 -62
  38. data/lib/berkeley_library/util/uris/validator.rb +0 -32
  39. data/lib/berkeley_library/util/uris.rb +0 -44
  40. data/spec/berkeley_library/util/arrays_spec.rb +0 -340
  41. data/spec/berkeley_library/util/paths_spec.rb +0 -90
  42. data/spec/berkeley_library/util/stringios_spec.rb +0 -34
  43. data/spec/berkeley_library/util/strings_spec.rb +0 -27
  44. data/spec/berkeley_library/util/times_spec.rb +0 -39
  45. data/spec/berkeley_library/util/uris_spec.rb +0 -118
@@ -1,6 +1,7 @@
1
1
  require 'nokogiri'
2
2
  require 'marc/xml_parsers'
3
3
  require 'marc_extensions'
4
+ require 'berkeley_library/util/files'
4
5
 
5
6
  module BerkeleyLibrary
6
7
  module TIND
@@ -9,6 +10,7 @@ module BerkeleyLibrary
9
10
  class XMLReader
10
11
  include Enumerable
11
12
  include ::MARC::NokogiriReader
13
+ include BerkeleyLibrary::Util::Files
12
14
 
13
15
  # ############################################################
14
16
  # Constant
@@ -43,10 +45,10 @@ module BerkeleyLibrary
43
45
  # ############################################################
44
46
  # Initializer
45
47
 
46
- # Reads MARC records from an XML datasource given either as a file path,
48
+ # Reads MARC records from an XML datasource given either as an XML string, a file path,
47
49
  # or as an IO object.
48
50
  #
49
- # @param source [String, Pathname, IO] the path to a file, or an IO to read from directly
51
+ # @param source [String, Pathname, IO] an XML string, the path to a file, or an IO to read from directly
50
52
  # @param freeze [Boolean] whether to freeze each record after reading
51
53
  def initialize(source, freeze: false)
52
54
  @handle = ensure_io(source)
@@ -55,14 +57,26 @@ module BerkeleyLibrary
55
57
  end
56
58
 
57
59
  class << self
58
- include MARCExtensions::XMLReaderClassExtensions
60
+ # Reads MARC records from an XML datasource given either as an XML string, a file path,
61
+ # or as an IO object.
62
+ #
63
+ # @param source [String, Pathname, IO] an XML string, the path to a file, or an IO to read from directly
64
+ # @param freeze [Boolean] whether to freeze each record after reading
65
+ def read(source, freeze: false)
66
+ new(source, freeze: freeze)
67
+ end
59
68
  end
60
69
 
61
70
  # ############################################################
62
71
  # MARC::GenericPullParser overrides
63
72
 
64
73
  def yield_record
65
- @record[:record].freeze if @freeze
74
+ @record[:record].tap do |record|
75
+ clean_cf_values(record)
76
+ move_cf000_to_leader(record)
77
+ record.freeze if @freeze
78
+ end
79
+
66
80
  super
67
81
  ensure
68
82
  increment_records_yielded!
@@ -113,26 +127,25 @@ module BerkeleyLibrary
113
127
 
114
128
  private
115
129
 
116
- def ensure_io(file)
117
- return file if io_like?(file)
118
- return File.new(file) if file_exists?(file)
119
- return StringIO.new(file) if file =~ /^\s*</x
130
+ # TIND uses <controlfield tag="000"/> instead of <leader/>
131
+ def move_cf000_to_leader(record)
132
+ return unless (cf_000 = record['000'])
120
133
 
121
- raise ArgumentError, "Don't know how to read XML from #{file.inspect}: not an IO, file path, or XML text"
134
+ record.leader = cf_000.value
135
+ record.fields.delete(cf_000)
122
136
  end
123
137
 
124
- # Returns true if `obj` is close enough to an IO object for Nokogiri
125
- # to parse as one.
126
- #
127
- # @param obj [Object] the object that might be an IO
128
- # @see https://github.com/sparklemotion/nokogiri/blob/v1.11.1/lib/nokogiri/xml/sax/parser.rb#L81 Nokogiri::XML::SAX::Parser#parse
129
- def io_like?(obj)
130
- obj.respond_to?(:read) && obj.respond_to?(:close)
138
+ # TIND uses \ (0x5c), not space (0x32), for unspecified values in positional fields
139
+ def clean_cf_values(record)
140
+ record.each_control_field { |cf| cf.value = cf.value&.gsub('\\', ' ') }
131
141
  end
132
142
 
133
- def file_exists?(path)
134
- (path.respond_to?(:exist?) && path.exist?) ||
135
- (path.respond_to?(:to_str) && File.exist?(path))
143
+ def ensure_io(file)
144
+ return file if reader_like?(file)
145
+ return File.new(file) if file_exists?(file)
146
+ return StringIO.new(file) if file =~ /^\s*</x
147
+
148
+ raise ArgumentError, "Don't know how to read XML from #{file.inspect}: not an IO, file path, or XML text"
136
149
  end
137
150
 
138
151
  def increment_records_yielded!
@@ -0,0 +1,152 @@
1
+ require 'nokogiri'
2
+ require 'marc_extensions'
3
+ require 'berkeley_library/tind/marc/xml_builder'
4
+
5
+ module BerkeleyLibrary
6
+ module TIND
7
+ module MARC
8
+ class XMLWriter
9
+ include BerkeleyLibrary::Util::Files
10
+ include BerkeleyLibrary::Logging
11
+
12
+ # ------------------------------------------------------------
13
+ # Constants
14
+
15
+ UTF_8 = Encoding::UTF_8.name
16
+
17
+ EMPTY_COLLECTION_DOC = Nokogiri::XML::Builder.new(encoding: UTF_8) do |xml|
18
+ xml.collection(xmlns: ::MARC::MARC_NS)
19
+ end.doc.freeze
20
+
21
+ COLLECTION_CLOSING_TAG = '</collection>'.freeze
22
+
23
+ DEFAULT_NOKOGIRI_OPTS = { encoding: UTF_8 }.freeze
24
+
25
+ # ------------------------------------------------------------
26
+ # Fields
27
+
28
+ attr_reader :out
29
+ attr_reader :nokogiri_options
30
+
31
+ # ------------------------------------------------------------
32
+ # Initializer
33
+
34
+ # Initializes a new {XMLWriter}.
35
+ #
36
+ # ```ruby
37
+ # File.open('marc.xml', 'wb') do |f|
38
+ # w = XMLWriter.new(f)
39
+ # marc_records.each { |r| w.write(r) }
40
+ # w.close
41
+ # end
42
+ # ```
43
+ #
44
+ # @param out [IO, String] an IO, or the name of a file
45
+ # @param nokogiri_options [Hash] Options passed to
46
+ # {https://nokogiri.org/rdoc/Nokogiri/XML/Node.html#method-i-write_to Nokogiri::XML::Node#write_to}
47
+ # Note that the `encoding` option is ignored, except insofar as
48
+ # passing an encoding other than UTF-8 will raise an `ArgumentError`.
49
+ # @raise ArgumentError if `out` is not an IO or a string, or is a string referencing
50
+ # a file path that cannot be opened for writing; or if an encoding other than UTF-8
51
+ # is specified in `nokogiri-options`
52
+ # @see #open
53
+ def initialize(out, **nokogiri_options)
54
+ @nokogiri_options = valid_nokogiri_options(nokogiri_options)
55
+ @out = ensure_io(out)
56
+ end
57
+
58
+ # ------------------------------------------------------------
59
+ # Class methods
60
+
61
+ class << self
62
+
63
+ # Opens a new {XMLWriter} with the specified output destination and
64
+ # Nokogiri options, writes the XML prolog and opening `<collection>`
65
+ # tag, yields the writer to write one or more MARC records, and closes
66
+ # the writer.
67
+ #
68
+ # ```ruby
69
+ # XMLWriter.open('marc.xml') do |w|
70
+ # marc_records.each { |r| w.write(r) }
71
+ # end
72
+ # ```
73
+ #
74
+ # Note that unlike initializing a writer with {#new} and closing it
75
+ # immediately, this will write an XML document with an empty
76
+ # `<collection></collection>` tag even if no records are written.
77
+ #
78
+ # @yieldparam writer [XMLWriter] the writer
79
+ # @see #new
80
+ # @see #close
81
+ def open(out, **nokogiri_options)
82
+ writer = new(out, **nokogiri_options)
83
+ writer.send(:ensure_open!)
84
+ yield writer if block_given?
85
+ writer.close
86
+ end
87
+ end
88
+
89
+ # ------------------------------------------------------------
90
+ # Instance methods
91
+
92
+ # Writes the specified record to the underlying stream, writing the
93
+ # XML prolog and opening `<collection>` tag if they have not yet
94
+ # been written.
95
+ #
96
+ # @param record [::MARC::Record] the MARC record to write.
97
+ # @raise IOError if the underlying stream has already been closed.
98
+ def write(record)
99
+ ensure_open!
100
+ record_element = XMLBuilder.new(record).build
101
+ record_element.write_to(out, nokogiri_options)
102
+ out.write("\n")
103
+ end
104
+
105
+ # Closes the underlying stream. If the XML prolog and opening `<collection>`
106
+ # tag have already been written, the closing `<collection/>` tag is written
107
+ # first.
108
+ def close
109
+ out.write(COLLECTION_CLOSING_TAG) if @open
110
+ out.close
111
+ end
112
+
113
+ # ------------------------------------------------------------
114
+ # Private
115
+
116
+ private
117
+
118
+ def ensure_open!
119
+ return if @open
120
+
121
+ out.write(prolog_and_opening_tag)
122
+ @open = true
123
+ end
124
+
125
+ def prolog_and_opening_tag
126
+ StringIO.open do |tmp|
127
+ EMPTY_COLLECTION_DOC.write_to(tmp, nokogiri_options)
128
+ result = tmp.string
129
+ result.sub!(%r{/>\s*$}, ">\n")
130
+ result
131
+ end
132
+ end
133
+
134
+ def ensure_io(file)
135
+ return file if writer_like?(file)
136
+ return File.open(file, 'wb') if parent_exists?(file)
137
+
138
+ raise ArgumentError, "Don't know how to write XML to #{file.inspect}: not an IO or file path"
139
+ end
140
+
141
+ def valid_nokogiri_options(opts)
142
+ if (encoding = opts.delete(:encoding)) && encoding != UTF_8
143
+ raise ArgumentError, "#{self.class.name} only supports #{UTF_8}; unable to use specified encoding #{encoding}"
144
+ end
145
+
146
+ DEFAULT_NOKOGIRI_OPTS.merge(opts)
147
+ end
148
+
149
+ end
150
+ end
151
+ end
152
+ end
@@ -7,7 +7,7 @@ module BerkeleyLibrary
7
7
  SUMMARY = 'TIND DA utilities for the UC Berkeley Library'.freeze
8
8
  DESCRIPTION = 'UC Berkeley Library utility gem for working with the TIND DA digital archive.'.freeze
9
9
  LICENSE = 'MIT'.freeze
10
- VERSION = '0.4.0'.freeze
10
+ VERSION = '0.5.0'.freeze
11
11
  HOMEPAGE = 'https://github.com/BerkeleyLibrary/tind'.freeze
12
12
  end
13
13
  end
@@ -0,0 +1,39 @@
1
+ module BerkeleyLibrary
2
+ module Util
3
+ # TODO: Move this to `berkeley_library-util`
4
+ module Files
5
+ class << self
6
+ include Files
7
+ end
8
+
9
+ def file_exists?(path)
10
+ (path.respond_to?(:exist?) && path.exist?) ||
11
+ (path.respond_to?(:to_str) && File.exist?(path))
12
+ end
13
+
14
+ def parent_exists?(path)
15
+ path.respond_to?(:parent) && path.parent.exist? ||
16
+ path.respond_to?(:to_str) && Pathname.new(path).parent.exist?
17
+ end
18
+
19
+ # Returns true if `obj` is close enough to an IO object for Nokogiri
20
+ # to parse as one.
21
+ #
22
+ # @param obj [Object] the object that might be an IO
23
+ # @see https://github.com/sparklemotion/nokogiri/blob/v1.11.1/lib/nokogiri/xml/sax/parser.rb#L81 Nokogiri::XML::SAX::Parser#parse
24
+ def reader_like?(obj)
25
+ obj.respond_to?(:read) && obj.respond_to?(:close)
26
+ end
27
+
28
+ # Returns true if `obj` is close enough to an IO object for Nokogiri
29
+ # to write to.
30
+ #
31
+ # @param obj [Object] the object that might be an IO
32
+ def writer_like?(obj)
33
+ # TODO: is it possible/desirable to loosen this? how strict is libxml2?
34
+ obj.is_a?(IO) || obj.is_a?(StringIO)
35
+ end
36
+
37
+ end
38
+ end
39
+ end
@@ -1,6 +1,6 @@
1
1
  require 'fileutils'
2
2
  require 'zip'
3
- require 'berkeley_library/util/logging'
3
+ require 'berkeley_library/logging'
4
4
  require 'berkeley_library/util/ods/xml/content_doc'
5
5
  require 'berkeley_library/util/ods/xml/styles_doc'
6
6
  require 'berkeley_library/util/ods/xml/manifest_doc'
@@ -1,5 +1,5 @@
1
1
  require 'nokogiri'
2
- require 'berkeley_library/util/logging'
2
+ require 'berkeley_library/logging'
3
3
  require 'berkeley_library/util/ods/xml/namespace'
4
4
 
5
5
  module BerkeleyLibrary
@@ -130,7 +130,9 @@ module BerkeleyLibrary
130
130
 
131
131
  before(:each) do
132
132
  search = instance_double(BerkeleyLibrary::TIND::API::Search)
133
- empty_enumerator = Enumerator.new({})
133
+ # rubocop:disable Lint/EmptyBlock
134
+ empty_enumerator = Enumerator.new {}
135
+ # rubocop:enable Lint/EmptyBlock
134
136
  allow(search).to receive(:each_result).and_return(empty_enumerator)
135
137
  allow(BerkeleyLibrary::TIND::API::Search).to receive(:new).with(collection: collection).and_return(search)
136
138
  end
@@ -24,6 +24,8 @@ module BerkeleyLibrary
24
24
  tag = '245'
25
25
  ind_bad = '!'
26
26
 
27
+ records = MARC::XMLReader.read('spec/data/records-manual-search.xml', freeze: false).to_a
28
+
27
29
  record = records.first
28
30
  record[tag].indicator1 = ind_bad
29
31
  expect { table << record }.to raise_error(Export::ExportException) do |e|
@@ -16,6 +16,25 @@ module BerkeleyLibrary
16
16
  expect(record0['024']['a']).to eq('BANC PIC 1982.078:15--ALB')
17
17
  end
18
18
 
19
+ describe 'freeze: true' do
20
+ it 'freezes the records' do
21
+ reader = XMLReader.new('spec/data/records-api-search.xml', freeze: true)
22
+ records = reader.to_a
23
+ expect(records).not_to be_empty # just to be sure
24
+ records.each { |record| expect(record).to be_frozen }
25
+ end
26
+ end
27
+
28
+ describe :records_yielded do
29
+ it 'counts the records' do
30
+ reader = XMLReader.new('spec/data/records-api-search.xml')
31
+ reader.each_with_index do |_, i|
32
+ expect(reader.records_yielded).to eq(i)
33
+ end
34
+ expect(reader.records_yielded).to eq(5)
35
+ end
36
+ end
37
+
19
38
  describe :new do
20
39
  it 'accepts a string path' do
21
40
  path = 'spec/data/records-api-search.xml'
@@ -59,7 +78,7 @@ module BerkeleyLibrary
59
78
 
60
79
  it 'raises ArgumentError if passed something random' do
61
80
  non_xml = Object.new
62
- # noinspection RubyYardParamTypeMatch
81
+ # noinspection RubyMismatchedArgumentType
63
82
  expect { XMLReader.new(non_xml) }.to raise_error(ArgumentError)
64
83
  end
65
84
  end
@@ -87,6 +106,28 @@ module BerkeleyLibrary
87
106
  end
88
107
  end
89
108
 
109
+ describe 'TIND peculiarities' do
110
+ attr_reader :record
111
+
112
+ before(:each) do
113
+ reader = XMLReader.new('spec/data/new-records.xml')
114
+ records = reader.to_a
115
+ expect(records.size).to eq(1) # just to be sure
116
+ @record = records.first
117
+ end
118
+
119
+ it 'converts backslashes in control fields to spaces' do
120
+ cf_008 = record['008']
121
+ expect(cf_008).to be_a(::MARC::ControlField)
122
+ expect(cf_008.value).to eq('190409s2015 xx eng ')
123
+ end
124
+
125
+ it 'parses CF 000 as the leader' do
126
+ expect(record.leader).to eq('00287cam a2200313 4500')
127
+ expect(record['000']).to be_nil
128
+ end
129
+ end
130
+
90
131
  end
91
132
  end
92
133
  end
@@ -0,0 +1,156 @@
1
+ require 'spec_helper'
2
+ require 'equivalent-xml'
3
+
4
+ module BerkeleyLibrary
5
+ module TIND
6
+ module MARC
7
+ describe XMLWriter do
8
+ let(:input_path) { 'spec/data/new-records.xml' }
9
+ attr_reader :record
10
+
11
+ before(:each) do
12
+ reader = XMLReader.new(input_path)
13
+ @record = reader.first
14
+ end
15
+
16
+ describe :open do
17
+
18
+ it 'writes a MARC record to a file as XML' do
19
+ Dir.mktmpdir(File.basename(__FILE__, '.rb')) do |dir|
20
+ output_path = File.join(dir, 'marc.xml')
21
+ XMLWriter.open(output_path) { |w| w.write(record) }
22
+
23
+ expected = File.open(input_path) { |f| Nokogiri::XML(f) }
24
+ actual = File.open(output_path) { |f| Nokogiri::XML(f) }
25
+
26
+ aggregate_failures do
27
+ EquivalentXml.equivalent?(expected, actual) do |n1, n2, result|
28
+ expect(n2.to_s).to eq(n1.to_s) unless result
29
+ end
30
+ end
31
+ end
32
+ end
33
+
34
+ it 'writes a MARC record to a StringIO' do
35
+ out = StringIO.new
36
+ XMLWriter.open(out) { |w| w.write(record) }
37
+ expected = File.open(input_path) { |f| Nokogiri::XML(f) }
38
+ actual = Nokogiri::XML(out.string)
39
+ aggregate_failures do
40
+ EquivalentXml.equivalent?(expected, actual) do |n1, n2, result|
41
+ expect(n2.to_s).to eq(n1.to_s) unless result
42
+ end
43
+ end
44
+ end
45
+
46
+ it 'accepts Nokogiri options' do
47
+ Dir.mktmpdir(File.basename(__FILE__, '.rb')) do |dir|
48
+ expected_path = File.join(dir, 'expected.xml')
49
+ XMLWriter.open(expected_path) { |w| w.write(record) }
50
+
51
+ actual_path = File.join(dir, 'actual.xml')
52
+ XMLWriter.open(actual_path, indent_text: "\t") { |w| w.write(record) }
53
+
54
+ expected = File.read(expected_path).gsub(%r{ (?= *<)(?!/)}, "\t")
55
+ actual = File.read(actual_path)
56
+ expect(actual).to eq(expected)
57
+ end
58
+ end
59
+
60
+ it 'accepts an explicit UTF-8 argument' do
61
+ Dir.mktmpdir(File.basename(__FILE__, '.rb')) do |dir|
62
+ output_path = File.join(dir, 'marc.xml')
63
+ XMLWriter.open(output_path, encoding: 'UTF-8') { |w| w.write(record) }
64
+
65
+ expected = File.open(input_path) { |f| Nokogiri::XML(f) }
66
+ actual = File.open(output_path) { |f| Nokogiri::XML(f) }
67
+
68
+ aggregate_failures do
69
+ EquivalentXml.equivalent?(expected, actual) do |n1, n2, result|
70
+ expect(n2.to_s).to eq(n1.to_s) unless result
71
+ end
72
+ end
73
+ end
74
+ end
75
+
76
+ it 'only writes UTF-8' do
77
+ Dir.mktmpdir(File.basename(__FILE__, '.rb')) do |dir|
78
+ output_path = File.join(dir, 'marc.xml')
79
+ expect { XMLWriter.open(output_path, encoding: 'UTF-16') }.to raise_error(ArgumentError)
80
+ expect(File.exist?(output_path)).to eq(false)
81
+ end
82
+ end
83
+
84
+ it 'rejects an invalid file path' do
85
+ bad_directory = Dir.mktmpdir(File.basename(__FILE__, '.rb')) { |dir| dir }
86
+ expect(File.directory?(bad_directory)).to eq(false)
87
+ output_path = File.join(bad_directory, 'marc.xml')
88
+ expect { XMLWriter.open(output_path) }.to raise_error(ArgumentError)
89
+ end
90
+
91
+ it 'rejects a non-IO, non-String argument' do
92
+ invalid_target = Object.new
93
+ expect { XMLWriter.open(invalid_target) }.to raise_error(ArgumentError)
94
+ end
95
+ end
96
+
97
+ describe :close do
98
+ it 'closes without writing the closing tag if nothing has been written' do
99
+ Dir.mktmpdir(File.basename(__FILE__, '.rb')) do |dir|
100
+ output_path = File.join(dir, 'marc.xml')
101
+ w = XMLWriter.new(output_path)
102
+ w.close
103
+
104
+ stat = File.stat(output_path)
105
+ expect(stat.size).to eq(0)
106
+ end
107
+ end
108
+
109
+ it 'writes the closing tag if the opening tag has been written' do
110
+ Dir.mktmpdir(File.basename(__FILE__, '.rb')) do |dir|
111
+ output_path = File.join(dir, 'marc.xml')
112
+ XMLWriter.open(output_path)
113
+ expect(File.exist?(output_path)).to eq(true)
114
+
115
+ doc = File.open(output_path) { |f| Nokogiri::XML(f) }
116
+ expect(doc.root.name).to eq('collection')
117
+ end
118
+ end
119
+ end
120
+
121
+ describe :write do
122
+ it 'raises an IOError if the writer has already been closed' do
123
+ Dir.mktmpdir(File.basename(__FILE__, '.rb')) do |dir|
124
+ output_path = File.join(dir, 'marc.xml')
125
+ w = XMLWriter.new(output_path)
126
+ w.close
127
+
128
+ expect { w.write(record) }.to raise_error(IOError)
129
+
130
+ stat = File.stat(output_path)
131
+ expect(stat.size).to eq(0)
132
+ end
133
+ end
134
+
135
+ it 'does not write a nil leader' do
136
+ record.leader = nil
137
+ marc_xml = StringIO.open do |out|
138
+ XMLWriter.open(out) { |w| w.write(record) }
139
+ out.string
140
+ end
141
+ expect(marc_xml).not_to include('leader')
142
+ end
143
+
144
+ it 'does not write a blank leader' do
145
+ record.leader = ''
146
+ marc_xml = StringIO.open do |out|
147
+ XMLWriter.open(out) { |w| w.write(record) }
148
+ out.string
149
+ end
150
+ expect(marc_xml).not_to include('leader')
151
+ end
152
+ end
153
+ end
154
+ end
155
+ end
156
+ end
@@ -0,0 +1,46 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <!--
3
+ Source: "Batch Uploader: Caveats, common errors and example metadata files", docs.tind.io
4
+ -->
5
+ <collection xmlns="http://www.loc.gov/MARC21/slim">
6
+ <record>
7
+
8
+ <!-- The Leader is encoded in the `000` control field.
9
+ If you want to edit the leader in software such as
10
+ MarcEdit, you will need to change these fields to a
11
+ leader tag. Then, before import into the repository
12
+ you will need to change the fields back to controlfields
13
+ with tag `000`.
14
+ -->
15
+ <controlfield tag="000">00287cam\a2200313\\\4500</controlfield>
16
+
17
+ <!-- All whitespace in control fields need to be replaced with
18
+ backspaces.
19
+ -->
20
+ <controlfield tag="008">190409s2015\\\\xx\\\\\\\\\\\\\\\\\\eng\\</controlfield>
21
+
22
+ <!-- Regular fields are encoded in datafield elements. -->
23
+ <datafield tag="100" ind1="0" ind2=" ">
24
+ <subfield code="a">Aristotle</subfield>
25
+ <subfield code="0">580897</subfield>
26
+ </datafield>
27
+
28
+ <datafield tag="245" ind1="0" ind2="0">
29
+ <subfield code="a">Metaphysics</subfield>
30
+ <subfield code="c">Aristotle</subfield>
31
+ </datafield>
32
+
33
+ <datafield tag="260" ind1=" " ind2=" ">
34
+ <subfield code="a">Narnia</subfield>
35
+ <subfield code="b">Fictive Books</subfield>
36
+ <subfield code="c">2015</subfield>
37
+ </datafield>
38
+
39
+ <!-- Make sure to include a collection when uploading new
40
+ records, so that the record will be searchable.
41
+ -->
42
+ <datafield tag="980" ind1=" " ind2=" ">
43
+ <subfield code="a">BIB</subfield>
44
+ </datafield>
45
+ </record>
46
+ </collection>