zizia 1.0.1

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 (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