berkeley_library-tind 0.4.0 → 0.5.0

Sign up to get free protection for your applications and to get access to all the features.
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>