zizia 1.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (65) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +15 -0
  3. data/.rubocop.yml +65 -0
  4. data/.rubocop_todo.yml +21 -0
  5. data/.solr_wrapper +8 -0
  6. data/.travis.yml +11 -0
  7. data/Gemfile +12 -0
  8. data/README.md +77 -0
  9. data/Rakefile +34 -0
  10. data/docs/_config.yml +1 -0
  11. data/docs/index.md +98 -0
  12. data/lib/zizia/always_invalid_validator.rb +17 -0
  13. data/lib/zizia/hash_mapper.rb +44 -0
  14. data/lib/zizia/hyrax_basic_metadata_mapper.rb +149 -0
  15. data/lib/zizia/hyrax_record_importer.rb +261 -0
  16. data/lib/zizia/importer.rb +61 -0
  17. data/lib/zizia/input_record.rb +65 -0
  18. data/lib/zizia/log_stream.rb +43 -0
  19. data/lib/zizia/metadata_mapper.rb +83 -0
  20. data/lib/zizia/metadata_only_stack.rb +70 -0
  21. data/lib/zizia/parser.rb +132 -0
  22. data/lib/zizia/parsers/csv_parser.rb +45 -0
  23. data/lib/zizia/record_importer.rb +57 -0
  24. data/lib/zizia/spec/fakes/fake_parser.rb +22 -0
  25. data/lib/zizia/spec/shared_examples/a_mapper.rb +32 -0
  26. data/lib/zizia/spec/shared_examples/a_message_stream.rb +11 -0
  27. data/lib/zizia/spec/shared_examples/a_parser.rb +73 -0
  28. data/lib/zizia/spec/shared_examples/a_validator.rb +46 -0
  29. data/lib/zizia/spec.rb +15 -0
  30. data/lib/zizia/streams/formatted_message_stream.rb +70 -0
  31. data/lib/zizia/validator.rb +117 -0
  32. data/lib/zizia/validators/csv_format_validator.rb +26 -0
  33. data/lib/zizia/validators/title_validator.rb +30 -0
  34. data/lib/zizia/version.rb +5 -0
  35. data/lib/zizia.rb +73 -0
  36. data/log/.keep +0 -0
  37. data/spec/fixtures/bad_example.csv +2 -0
  38. data/spec/fixtures/example.csv +4 -0
  39. data/spec/fixtures/hyrax/example.csv +3 -0
  40. data/spec/fixtures/images/animals/cat.png +0 -0
  41. data/spec/fixtures/images/zizia.png +0 -0
  42. data/spec/fixtures/zizia.png +0 -0
  43. data/spec/integration/import_csv_spec.rb +28 -0
  44. data/spec/integration/import_hyrax_csv.rb +71 -0
  45. data/spec/spec_helper.rb +18 -0
  46. data/spec/stdout_stream_spec.rb +9 -0
  47. data/spec/support/hyrax/basic_metadata.rb +30 -0
  48. data/spec/support/hyrax/core_metadata.rb +15 -0
  49. data/spec/support/shared_contexts/with_work_type.rb +101 -0
  50. data/spec/zizia/csv_format_validator_spec.rb +38 -0
  51. data/spec/zizia/csv_parser_spec.rb +73 -0
  52. data/spec/zizia/formatted_message_stream_spec.rb +35 -0
  53. data/spec/zizia/hash_mapper_spec.rb +8 -0
  54. data/spec/zizia/hyrax_basic_metadata_mapper_spec.rb +190 -0
  55. data/spec/zizia/hyrax_record_importer_spec.rb +178 -0
  56. data/spec/zizia/importer_spec.rb +46 -0
  57. data/spec/zizia/input_record_spec.rb +71 -0
  58. data/spec/zizia/parser_spec.rb +47 -0
  59. data/spec/zizia/record_importer_spec.rb +70 -0
  60. data/spec/zizia/title_validator_spec.rb +23 -0
  61. data/spec/zizia/validator_spec.rb +9 -0
  62. data/spec/zizia/version_spec.rb +7 -0
  63. data/spec/zizia_spec.rb +19 -0
  64. data/zizia.gemspec +34 -0
  65. metadata +246 -0
@@ -0,0 +1,132 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Zizia
4
+ ##
5
+ # A generic parser.
6
+ #
7
+ # `Parser` implementations provide a stream of `InputRecord`s, derived from an
8
+ # input object (`file`), through `Parser#records`. This method should be
9
+ # implemented efficiently for repeated access, generating records lazily if
10
+ # possible, and caching if appropriate.
11
+ #
12
+ # Input validation is handled by an array of `#validators`, which are run in
13
+ # sequence when `#validate` (or `#validate!`) is called. Errors caught in
14
+ # validation are accessible via `#errors`, and inputs generating errors result
15
+ # in `#valid? # => false`.
16
+ #
17
+ # A factory method `.for` is provided, and each implementation should
18
+ # provides a `.match?(**)` which returns `true` if the options passed
19
+ # indicate the parser can handle the given input. Parsers are checked for
20
+ # `#match?` in the reverse of load order (i.e. the most recently loaded
21
+ # `Parser` classes are given precedence).
22
+ #
23
+ # @example Getting a parser for a file input
24
+ # file = File.open('path/to/import/manifest.csv')
25
+ #
26
+ # Parser.for(file: file).records
27
+ #
28
+ # @example Validating a parser
29
+ # parser = Parser.for(file: invalid_input)
30
+ #
31
+ # parser.valid? # => true (always true before validation)
32
+ # parser.validate # => false
33
+ # parser.valid? # => false
34
+ # parser.errors # => an array of Validation::Error-like structs
35
+ #
36
+ # parser.validate! # ValidationError
37
+ #
38
+ # rubocop:disable Style/ClassVars
39
+ class Parser
40
+ DEFAULT_VALIDATORS = [].freeze
41
+ @@subclasses = [] # @private
42
+
43
+ ##
44
+ # @!attribute [rw] file
45
+ # @return [File]
46
+ # @!attribute [rw] validators
47
+ # @return [Array<Validator>]
48
+ # @!attribute [r] errors
49
+ # @return [Array]
50
+ attr_accessor :file, :validators
51
+ attr_reader :errors
52
+
53
+ ##
54
+ # @param file [File]
55
+ def initialize(file:, **_opts)
56
+ self.file = file
57
+ @errors = []
58
+ @validators ||= self.class::DEFAULT_VALIDATORS
59
+
60
+ yield self if block_given?
61
+ end
62
+
63
+ class << self
64
+ ##
65
+ # @param file [Object]
66
+ #
67
+ # @return [Zizia::Parser] a parser instance appropriate for
68
+ # the arguments
69
+ #
70
+ # @raise [NoParserError]
71
+ def for(file:)
72
+ klass =
73
+ @@subclasses.find { |k| k.match?(file: file) } ||
74
+ raise(NoParserError)
75
+
76
+ klass.new(file: file)
77
+ end
78
+
79
+ ##
80
+ # @abstract
81
+ # @return [Boolean]
82
+ def match?(**_opts); end
83
+
84
+ private
85
+
86
+ ##
87
+ # @private Register a new class when inherited
88
+ def inherited(subclass)
89
+ @@subclasses.unshift subclass
90
+ super
91
+ end
92
+ end
93
+
94
+ ##
95
+ # @abstract
96
+ #
97
+ # @yield [record] gives each record in the file to the block
98
+ # @yieldparam record [ImportRecord]
99
+ #
100
+ # @return [Enumerable<ImportRecord>]
101
+ def records
102
+ raise NotImplementedError
103
+ end
104
+
105
+ ##
106
+ # @return [Boolean] true if the file input is valid
107
+ def valid?
108
+ errors.empty?
109
+ end
110
+
111
+ ##
112
+ # @return [Boolean] true if the file input is valid
113
+ def validate
114
+ validators.each_with_object(errors) do |validator, errs|
115
+ errs.concat(validator.validate(parser: self))
116
+ end
117
+
118
+ valid?
119
+ end
120
+
121
+ ##
122
+ # @return [true] always true, unless an error is raised.
123
+ #
124
+ # @raise [ValidationError] if the file to parse is invalid
125
+ def validate!
126
+ validate || raise(ValidationError)
127
+ end
128
+
129
+ class NoParserError < TypeError; end
130
+ class ValidationError < RuntimeError; end
131
+ end # rubocop:enable Style/ClassVars
132
+ end
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'csv'
4
+
5
+ module Zizia
6
+ ##
7
+ # A parser for CSV files. A single `InputRecord` is returned for each row
8
+ # parsed from the input.
9
+ #
10
+ # Validates the format of the CSV, generating a single error the file is
11
+ # malformed. This error gives the line number and a message for the first
12
+ # badly formatted row.
13
+ #
14
+ # @see CsvFormatValidator
15
+ class CsvParser < Parser
16
+ DEFAULT_VALIDATORS = [CsvFormatValidator.new].freeze
17
+ EXTENSION = '.csv'
18
+
19
+ class << self
20
+ ##
21
+ # Matches all '.csv' filenames.
22
+ def match?(file:, **_opts)
23
+ File.extname(file) == EXTENSION
24
+ rescue TypeError
25
+ false
26
+ end
27
+ end
28
+
29
+ ##
30
+ # Gives a record for each line in the .csv
31
+ #
32
+ # @see Parser#records
33
+ def records
34
+ return enum_for(:records) unless block_given?
35
+
36
+ file.rewind
37
+
38
+ CSV.parse(file.read, headers: true).each do |row|
39
+ yield InputRecord.from(metadata: row)
40
+ end
41
+ rescue CSV::MalformedCSVError
42
+ []
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,57 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Zizia
4
+ class RecordImporter
5
+ ##
6
+ # @!attribute [rw] error_stream
7
+ # @return [#<<]
8
+ # @!attribute [rw] info_stream
9
+ # @return [#<<]
10
+ # @!attribute [rw] batch_id
11
+ # @return [String] an optional batch id for this import run
12
+ # @!attribute [rw] success_count
13
+ # @return [Integer] a count of the records that were successfully created
14
+ # @!attribute [rw] failure_count
15
+ # @return [Integer] a count of the records that failed import
16
+ attr_accessor :error_stream, :info_stream, :batch_id, :success_count, :failure_count
17
+
18
+ ##
19
+ # @param error_stream [#<<]
20
+ def initialize(error_stream: Zizia.config.default_error_stream,
21
+ info_stream: Zizia.config.default_info_stream)
22
+ self.error_stream = error_stream
23
+ self.info_stream = info_stream
24
+ end
25
+
26
+ ##
27
+ # @param record [ImportRecord]
28
+ #
29
+ # @return [void]
30
+ def import(record:)
31
+ create_for(record: record)
32
+ rescue Faraday::ConnectionFailed, Ldp::HttpError => e
33
+ error_stream << e
34
+ rescue RuntimeError => e
35
+ error_stream << e
36
+ raise e
37
+ end
38
+
39
+ def import_type
40
+ raise 'No curation_concern found for import' unless
41
+ defined?(Hyrax) && Hyrax&.config&.curation_concerns&.any?
42
+
43
+ Hyrax.config.curation_concerns.first
44
+ end
45
+
46
+ private
47
+
48
+ def create_for(record:)
49
+ info_stream << 'Creating record: ' \
50
+ "#{record.respond_to?(:title) ? record.title : record}."
51
+
52
+ created = import_type.create(record.attributes)
53
+
54
+ info_stream << "Record created at: #{created.id}"
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ class FakeParser < Zizia::Parser
4
+ METADATA = [{ 'title' => '1' }, { 'title' => '2' }, { 'title' => '3' }].freeze
5
+
6
+ def initialize(file: METADATA)
7
+ super
8
+ end
9
+
10
+ def records
11
+ return enum_for(:records) unless block_given?
12
+
13
+ file.each { |hsh| yield Zizia::InputRecord.from(metadata: hsh) }
14
+ end
15
+ end
16
+
17
+ describe FakeParser do
18
+ it_behaves_like 'a Zizia::Parser' do
19
+ subject(:parser) { described_class.new }
20
+ let(:record_count) { 3 }
21
+ end
22
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ shared_examples 'a Zizia::Mapper' do
4
+ subject(:mapper) { described_class.new }
5
+
6
+ before { mapper.metadata = metadata }
7
+
8
+ describe '#metadata' do
9
+ it 'can be set' do
10
+ expect { mapper.metadata = nil }
11
+ .to change { mapper.metadata }
12
+ end
13
+ end
14
+
15
+ describe '#field?' do
16
+ it 'does not have bogus fields' do
17
+ expect(mapper.field?(:NOT_A_REAL_FIELD)).to be_falsey
18
+ end
19
+
20
+ it 'has fields that are expected' do
21
+ if defined?(expected_fields)
22
+ expected_fields.each do |field|
23
+ expect(mapper.field?(field)).to be_truthy
24
+ end
25
+ end
26
+ end
27
+ end
28
+
29
+ describe '#fields' do
30
+ it { expect(mapper.fields).to contain_exactly(*expected_fields) }
31
+ end
32
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ shared_examples 'a Zizia::MessageStream' do
4
+ describe '#<<' do
5
+ it { is_expected.to respond_to(:<<) }
6
+
7
+ it 'accepts a string argument' do
8
+ expect { stream << 'some string' }.not_to raise_error
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,73 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'zizia/always_invalid_validator'
4
+
5
+ shared_examples 'a Zizia::Parser' do
6
+ describe '#file' do
7
+ it 'is an accessor' do
8
+ expect { parser.file = :a_new_file }
9
+ .to change { parser.file }
10
+ .to(:a_new_file)
11
+ end
12
+ end
13
+
14
+ describe '#records' do
15
+ it 'yields records' do
16
+ unless described_class == Zizia::Parser
17
+ expect { |b| parser.records(&b) }
18
+ .to yield_control.exactly(record_count).times
19
+ end
20
+ end
21
+ end
22
+
23
+ describe '#valid?' do
24
+ it 'is valid' do
25
+ expect(parser).to be_valid
26
+ end
27
+
28
+ context 'when not valid' do
29
+ before do
30
+ parser.validators = [Zizia::AlwaysInvalidValidator.new]
31
+ end
32
+
33
+ it 'is invalid' do
34
+ expect { parser.validate }
35
+ .to change { parser.valid? }
36
+ .to be_falsey
37
+ end
38
+ end
39
+ end
40
+
41
+ describe '#validate' do
42
+ it 'is true when valid' do
43
+ expect(parser.validate).to be_truthy
44
+ end
45
+
46
+ context 'when not valid' do
47
+ before do
48
+ parser.validators = [Zizia::AlwaysInvalidValidator.new]
49
+ end
50
+
51
+ it 'is invalid' do
52
+ expect(parser.validate).to be_falsey
53
+ end
54
+ end
55
+ end
56
+
57
+ describe '#validate!' do
58
+ it 'is true when valid' do
59
+ expect(parser.validate).to be_truthy
60
+ end
61
+
62
+ context 'when not valid' do
63
+ before do
64
+ parser.validators = [Zizia::AlwaysInvalidValidator.new]
65
+ end
66
+
67
+ it 'raises a ValidationError' do
68
+ expect { parser.validate! }
69
+ .to raise_error Zizia::Parser::ValidationError
70
+ end
71
+ end
72
+ end
73
+ end
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ shared_examples 'a Zizia::Validator' do
4
+ subject(:validator) { described_class.new(error_stream: error_stream) }
5
+ let(:error_stream) { [] }
6
+
7
+ define :be_a_validator_error do # |expected|
8
+ match { false } # { |actual| some_condition }
9
+ end
10
+
11
+ describe '#validate' do
12
+ context 'without a parser' do
13
+ it 'raises ArgumentError' do
14
+ expect { validator.validate }.to raise_error ArgumentError
15
+ end
16
+ end
17
+
18
+ it 'gives an empty error collection for a valid parser' do
19
+ expect(validator.validate(parser: valid_parser)).not_to be_any if
20
+ defined?(valid_parser)
21
+ end
22
+
23
+ context 'for an invalid parser' do
24
+ it 'gives an non-empty error collection' do
25
+ expect(validator.validate(parser: invalid_parser)).to be_any if
26
+ defined?(invalid_parser)
27
+ end
28
+
29
+ it 'gives usable errors' do
30
+ pending 'we need to clarify the error type and usage'
31
+
32
+ validator.validate(parser: invalid_parser).each do |error|
33
+ expect(error).to be_a_validator_error
34
+ end
35
+ end
36
+
37
+ it 'writes errors to the error stream' do
38
+ if defined?(invalid_parser)
39
+ expect { validator.validate(parser: invalid_parser) }
40
+ .to change { error_stream }
41
+ .to include(an_instance_of(Zizia::Validator::Error))
42
+ end
43
+ end
44
+ end
45
+ end
46
+ end
data/lib/zizia/spec.rb ADDED
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Zizia
4
+ ##
5
+ # RSpec test support for {Zizia} importers.
6
+ #
7
+ # @see https://relishapp.com/rspec/rspec-core/docs/
8
+ module Spec
9
+ require 'zizia/spec/shared_examples/a_mapper'
10
+ require 'zizia/spec/shared_examples/a_message_stream'
11
+ require 'zizia/spec/shared_examples/a_parser'
12
+ require 'zizia/spec/shared_examples/a_validator'
13
+ require 'zizia/spec/fakes/fake_parser'
14
+ end
15
+ end
@@ -0,0 +1,70 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Zizia
4
+ ##
5
+ # A message stream that formats a message before forwarding it to an
6
+ # underlying {#stream} (STDOUT by default). Messages are formatted using
7
+ # the `#%` method; the formatter can be a string format specification like
8
+ # "Message received: %s".
9
+ #
10
+ # @example Using a simple formatter
11
+ # formatter = "Message received: %s\n"
12
+ # stream = Zizia::FormattedMessageStream.new(formatter: formatter)
13
+ #
14
+ # stream << "a message"
15
+ # # Message received: a message
16
+ # # => #<IO:<STDOUT>>
17
+ #
18
+ # @example A more complex formatter use case
19
+ # class MyFormatter
20
+ # def %(arg)
21
+ # "#{Time.now}: %s\n" % arg
22
+ # end
23
+ # end
24
+ #
25
+ # formatter = MyFormatter.new
26
+ # stream = Zizia::FormattedMessageStream.new(formatter: formatter)
27
+ #
28
+ # stream << 'a message'
29
+ # # 2018-02-02 16:10:52 -0800: a message
30
+ # # => #<IO:<STDOUT>>
31
+ #
32
+ # stream << 'another message'
33
+ # # 2018-02-02 16:10:55 -0800: another message
34
+ # # => #<IO:<STDOUT>>
35
+ #
36
+ class FormattedMessageStream
37
+ ##
38
+ # @!attribute [rw] formatter
39
+ # @return [#%] A format specification
40
+ # @see https://ruby-doc.org/core-2.4.0/String.html#method-i-25
41
+ # @!attribute [rw] stream
42
+ # @return [#<<] an underlying stream to forward messages to after
43
+ # formatting
44
+ attr_accessor :formatter, :stream
45
+
46
+ ##
47
+ # @param formatter [#%] A format specification
48
+ # @param stream [#<<] an underlying stream to forward messages to after
49
+ # formatting
50
+ #
51
+ # @see https://ruby-doc.org/core-2.4.0/String.html#method-i-25
52
+ def initialize(stream: STDOUT, formatter: "%s\n")
53
+ self.formatter = formatter
54
+ self.stream = stream
55
+ end
56
+
57
+ ##
58
+ def <<(msg)
59
+ stream << format_message(msg)
60
+ end
61
+
62
+ ##
63
+ # @param msg [#to_s]
64
+ #
65
+ # @return [String] the input, cast to a string and formatted using
66
+ def format_message(msg)
67
+ formatter % msg
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,117 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Zizia
4
+ ##
5
+ # @abstract A null validator; always returns an empty error collection
6
+ #
7
+ # Validators are used to ensure the correctness of input to a parser. Each
8
+ # validator must respond to `#validate` and return a collection of errors
9
+ # found during validation. If the input is valid, this collection must be
10
+ # empty. Otherwise, it contains any number of `Validator::Error` structs
11
+ # which should be sent to the `#error_stream` by the validator.
12
+ #
13
+ # The validation process accepts an entire `Parser` and is free to inspect
14
+ # the input `#file` content, or view its individual `#records`.
15
+ #
16
+ # The base class provides infrastructure for the key behavior, relying on a
17
+ # private `#run_validation` method to provide the core behavior. In most cases
18
+ # implementers will want to simply override this method.
19
+ #
20
+ # @example validating a parser
21
+ # validator = MyValidator.new
22
+ # validator.validate(parser: myParser)
23
+ #
24
+ # @example validating an invalid parser
25
+ # validator = MyValidator.new
26
+ # validator.validate(parser: invalidParser)
27
+ # # => Error<#... validator: MyValidator,
28
+ # name: 'An Error Name',
29
+ # description: '...'
30
+ # lineno: 37>
31
+ #
32
+ # @example Implementing a custom Validator and using it in a Parser
33
+ # # Validator checks that the title, when downcased is equal to `moomin`
34
+ # class TitleIsMoominValidator
35
+ # def run_validation(parser:)
36
+ # parser.records.each_with_object([]) do |record, errors|
37
+ # errors << Error.new(self, :title_is_not_moomin) unless
38
+ # title_is_moomin?(record)
39
+ # end
40
+ # end
41
+ #
42
+ # def title_is_moomin?(record)
43
+ # return false unless record.respond_to?(:title)
44
+ # return true if record.title.downcase == 'moomin
45
+ # true
46
+ # end
47
+ # end
48
+ #
49
+ # parser = MyParser.new(some_content)
50
+ # parser.validations << TitleIsMoominvalidator.new
51
+ # parser.validate
52
+ # parser.valid? # => false (unless all the records match the title)
53
+ #
54
+ # @see Parser#validate
55
+ class Validator
56
+ ##
57
+ # A representation of an error encountered in validation.
58
+ Error = Struct.new(:validator, :name, :description, :lineno) do
59
+ ##
60
+ # @!attribute [rw] validator
61
+ # @return [#to_s] the validator that generated this error
62
+ # @!attribute [rw] name
63
+ # @return [#to_s] a short descriptive name for the given error
64
+ # @!attribute [rw] description
65
+ # @return [#to_s] a long form description or message
66
+ # @!attribute [rw] lineno
67
+ # @return [#to_s] the line number, or other indication of the location
68
+ # where the error was encountered
69
+
70
+ ##
71
+ # @return [Boolean]
72
+ def validator_error?
73
+ true
74
+ end
75
+
76
+ ##
77
+ # @return [String]
78
+ def to_s
79
+ "#{name}: #{description} (#{validator})"
80
+ end
81
+ end
82
+
83
+ ##
84
+ # @!attribute [rw] error_stream
85
+ # @return [#<<]
86
+ attr_accessor :error_stream
87
+
88
+ ##
89
+ # @param error_stream [#<<]
90
+ def initialize(error_stream: Zizia.config.default_error_stream)
91
+ self.error_stream = error_stream
92
+ end
93
+
94
+ ##
95
+ # @param parser [Parser]
96
+ #
97
+ # @return [Enumerator<Error>] a collection of errors found in validation
98
+ def validate(parser:)
99
+ run_validation(parser: parser).tap do |errors|
100
+ errors.map { |error| error_stream << error }
101
+ end
102
+ end
103
+
104
+ private
105
+
106
+ # rubocop:disable Lint/UnusedMethodArgument
107
+
108
+ ##
109
+ # @return [Enumerator<Error>]
110
+ #
111
+ def run_validation(parser:)
112
+ [].to_enum
113
+ end
114
+
115
+ # rubocop:enable Lint/UnusedMethodArgument
116
+ end
117
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Zizia
4
+ ##
5
+ # A validator for correctly formatted CSV.
6
+ #
7
+ # @example
8
+ # parser = Parser.new(file: File.open('path/to/my.csv'))
9
+ #
10
+ # CsvFormatValidator.new.validate(parser: parser)
11
+ #
12
+ # @see http://ruby-doc.org/stdlib-2.0.0/libdoc/csv/rdoc/CSV/MalformedCSVError.html
13
+ class CsvFormatValidator < Validator
14
+ ##
15
+ # @private
16
+ #
17
+ # @see Validator#validate
18
+ def run_validation(parser:, **)
19
+ return [] if CSV.parse(parser.file.read)
20
+ rescue CSV::MalformedCSVError => e
21
+ [Error.new(self.class, e.class, e.message)]
22
+ ensure
23
+ parser.file.rewind
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Zizia
4
+ class TitleValidator < Validator
5
+ ##
6
+ # @private
7
+ #
8
+ # @see Validator#validate
9
+ def run_validation(parser:, **)
10
+ parser.records.each_with_object([]) do |record, errors|
11
+ titles = record.respond_to?(:title) ? record.title : []
12
+
13
+ errors << error_for(record: record) if Array(titles).empty?
14
+ end
15
+ end
16
+
17
+ protected
18
+
19
+ ##
20
+ # @private
21
+ # @param record [InputRecord]
22
+ #
23
+ # @return [Error]
24
+ def error_for(record:)
25
+ Error.new(self,
26
+ :missing_title,
27
+ "Title is required; got #{record.mapper.metadata}")
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Zizia
4
+ VERSION = '1.0.1'
5
+ end