zizia 4.0.2.alpha.01 → 4.0.3.alpha.01

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,70 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Zizia
4
- class MetadataOnlyStack
5
- def self.build_stack
6
- ActionDispatch::MiddlewareStack.new.tap do |middleware|
7
- # Wrap everything in a database transaction, if the save of the resource
8
- # fails then roll back any database AdminSetChangeSet
9
- middleware.use Hyrax::Actors::TransactionalRequest
10
-
11
- # Ensure you are mutating the most recent version
12
- # middleware.use Hyrax::Actors::OptimisticLockValidator
13
-
14
- # Attach files from a URI (for BrowseEverything)
15
- # middleware.use Hyrax::Actors::CreateWithRemoteFilesActor
16
-
17
- # Attach files uploaded in the form to the UploadsController
18
- # In Californica, for command line import,
19
- # we are using the CreateWithFilesActor to attach
20
- # local files with a file:// url, not via the UploadsController,
21
- # so we never use the CreateWithFilesActor
22
- # middleware.use Hyrax::Actors::CreateWithFilesActor
23
-
24
- # Add/remove the resource to/from a collection
25
- middleware.use Hyrax::Actors::CollectionsMembershipActor
26
-
27
- # Add/remove to parent work
28
- # middleware.use Hyrax::Actors::AddToWorkActor
29
-
30
- # Add/remove children (works or file_sets)
31
- # middleware.use Hyrax::Actors::AttachMembersActor
32
-
33
- # Set the order of the children (works or file_sets)
34
- # middleware.use Hyrax::Actors::ApplyOrderActor
35
-
36
- # Sets the default admin set if they didn't supply one
37
- # middleware.use Hyrax::Actors::DefaultAdminSetActor
38
-
39
- # Decode the private/public/institution on the form into permisisons on
40
- # the model
41
- # We aren't using this in Californica, we're just setting the visibility
42
- # at import time
43
- # middleware.use Hyrax::Actors::InterpretVisibilityActor
44
-
45
- # Handles transfering ownership of works from one user to another
46
- # We aren't using this in Californica
47
- # middleware.use Hyrax::Actors::TransferRequestActor
48
-
49
- # Copies default permissions from the PermissionTemplate to the work
50
- # middleware.use Hyrax::Actors::ApplyPermissionTemplateActor
51
-
52
- # Remove attached FileSets when destroying a work
53
- # middleware.use Hyrax::Actors::CleanupFileSetsActor
54
-
55
- # Destroys the trophies in the database when the work is destroyed
56
- # middleware.use Hyrax::Actors::CleanupTrophiesActor
57
-
58
- # Destroys the feature tag in the database when the work is destroyed
59
- # middleware.use Hyrax::Actors::FeaturedWorkActor
60
-
61
- # Persist the metadata changes on the resource
62
- middleware.use Hyrax::Actors::ModelActor
63
-
64
- # Start the workflow for this work
65
- # middleware.use Hyrax::Actors::InitializeWorkflowActor
66
- end
67
- end
68
- # rubocop:enable Metrics/MethodLength
69
- end
70
- end
@@ -1,58 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Zizia
4
- ##
5
- # The chief entry point for bulk import of records. `Importer` accepts a
6
- # {Parser} on initialization and iterates through its {Parser#records}, importing
7
- # each using a given {RecordImporter}.
8
- #
9
- # @example Importing in bulk from a CSV file
10
- # parser = Zizia::Parser.for(file: File.new('path/to/import.csv'))
11
- #
12
- # Zizia::Importer.new(parser: parser).import if parser.validate
13
- #
14
- class Importer
15
- extend Forwardable
16
-
17
- ##
18
- # @!attribute [rw] parser
19
- # @return [Parser]
20
- # @!attribute [rw] record_importer
21
- # @return [RecordImporter]
22
- attr_accessor :parser, :record_importer
23
-
24
- ##
25
- # @!method records()
26
- # @see Parser#records
27
- def_delegator :parser, :records, :records
28
-
29
- ##
30
- # @param parser [Parser] The parser to use as the source for import
31
- # records.
32
- # @param record_importer [RecordImporter] An object to handle import of
33
- # each record
34
- def initialize(parser:, record_importer: RecordImporter.new)
35
- self.parser = parser
36
- self.record_importer = record_importer
37
- end
38
-
39
- # Do not attempt to run an import if there are no records. Instead, just write to the log.
40
- def no_records_message
41
- Rails.logger.error "[zizia] event: empty_import, batch_id: #{record_importer.batch_id}"
42
- end
43
-
44
- ##
45
- # Import each record in {#records}.
46
- #
47
- # @return [void]
48
- def import
49
- no_records_message && return unless records.count.positive?
50
- start_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
51
- Rails.logger.info "[zizia] event: start_import, batch_id: #{record_importer.batch_id}, expecting to import #{records.count} records."
52
- records.each { |record| record_importer.import(record: record) }
53
- end_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
54
- elapsed_time = end_time - start_time
55
- Rails.logger.info "[zizia] event: finish_import, batch_id: #{record_importer.batch_id}, successful_record_count: #{record_importer.success_count}, failed_record_count: #{record_importer.failure_count}, elapsed_time: #{elapsed_time}, elapsed_time_per_record: #{elapsed_time / records.count}"
56
- end
57
- end
58
- end
@@ -1,65 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Zizia
4
- ##
5
- # @example Building an importer with the factory
6
- # record = InputRecord.from({some: :metadata}, mapper: MyMapper.new)
7
- # record.some # => :metadata
8
- #
9
- class InputRecord
10
- ##
11
- # @!attribute [rw] mapper
12
- # @return [#map_fields]
13
- attr_accessor :mapper
14
-
15
- ##
16
- # @param mapper [#map_fields]
17
- def initialize(mapper: Zizia.config.metadata_mapper_class.new)
18
- self.mapper = mapper
19
- end
20
-
21
- class << self
22
- ##
23
- # @param metadata [Object]
24
- # @param mapper [#map_fields]
25
- #
26
- # @return [InputRecord] an input record mapping metadata with the given
27
- # mapper
28
- def from(metadata:, mapper: Zizia.config.metadata_mapper_class.new)
29
- mapper.metadata = metadata
30
- new(mapper: mapper)
31
- end
32
- end
33
-
34
- ##
35
- # @return [Hash<Symbol, Object>]
36
- def attributes
37
- mapper.fields.each_with_object({}) do |field, attrs|
38
- attrs[field] = public_send(field)
39
- end
40
- end
41
-
42
- ##
43
- # @return [String, nil] an identifier for the representative file; nil if
44
- # none is given.
45
- def representative_file
46
- return mapper.representative_file if
47
- mapper.respond_to?(:representative_file)
48
-
49
- nil
50
- end
51
-
52
- ##
53
- # Respond to methods matching mapper fields
54
- def method_missing(method_name, *args, &block)
55
- return super unless mapper.field?(method_name)
56
- mapper.public_send(method_name)
57
- end
58
-
59
- ##
60
- # @see #method_missing
61
- def respond_to_missing?(method_name, include_private = false)
62
- mapper.field?(method_name) || super
63
- end
64
- end
65
- end
@@ -1,83 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Zizia
4
- ##
5
- # A null/base mapper that maps no fields.
6
- #
7
- # Real mapper implementations need to provide `#fields`, enumerating over
8
- # `Symbols` that represent the fields on the target object (e.g. an
9
- # `ActiveFedora::Base`/`Hyrax::WorkBehavior`) that the mapper can handle.
10
- # For each field in `#fields`, the mapper must respond to a matching method
11
- # (i.e. `:title` => `#title`), and return the value(s) that should be set to
12
- # the target's attributes upon mapping.
13
- #
14
- # To ease the implementation of field methods, this base class provides
15
- # a `#method_missing` that forwards missing method names to a `#map_field`
16
- # method. `#map_field` can be implemented to provide a generalized field
17
- # mapping when a common pattern will be used for many methods. Callers should
18
- # avoid relying on this protected method directly, since mappers may implement
19
- # individual field methods in any other way (e.g. `def title; end`) to route
20
- # around `#map_field`. Implementations are also free to override
21
- # `#method_missing` if desired.
22
- #
23
- # Mappers generally operate over some input `#metadata`. Example metadata
24
- # types that mappers could be implemented over include `Hash`, `CSV`, `XML`,
25
- # `RDF::Graph`, etc...; mappers are free to interpret or ignore the contents
26
- # of their underlying metadata data structures at their leisure. Values for
27
- # fields are /usually/ derived from the `#metadata`, but can also be generated
28
- # from complex logic or even hard-coded.
29
- #
30
- # @example Using a MetadataMapper
31
- # mapper = MyMapper.new
32
- # mapper.metadata = some_metadata_object
33
- # mapper.fields # => [:title, :author, :description]
34
- #
35
- # mapper.title # => 'Some Title'
36
- #
37
- # mapper.fields.map { |field| mapper.send(field) }
38
- #
39
- # @see ImportRecord#attributes for the canonical usage of a `MetadataMapper`.
40
- # @see HashMapper for an example implementation with dynamically generated
41
- # fields
42
- class MetadataMapper
43
- ##
44
- # @!attribute [rw] metadata
45
- # @return [Object]
46
- attr_accessor :metadata
47
-
48
- ##
49
- # @param name [Symbol]
50
- #
51
- # @return [Boolean]
52
- def field?(name)
53
- fields.include?(name)
54
- end
55
-
56
- ##
57
- # @return [Enumerable<Symbol>] The fields the mapper can process
58
- def fields
59
- []
60
- end
61
-
62
- def method_missing(method_name, *args, &block)
63
- return map_field(method_name) if fields.include?(method_name)
64
- super
65
- end
66
-
67
- def respond_to_missing?(method_name, include_private = false)
68
- field?(method_name) || super
69
- end
70
-
71
- protected
72
-
73
- ##
74
- # @private
75
- #
76
- # @param name [Symbol]
77
- #
78
- # @return [Object]
79
- def map_field(_name)
80
- raise NotImplementedError
81
- end
82
- end
83
- end
data/lib/zizia/parser.rb DELETED
@@ -1,132 +0,0 @@
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
@@ -1,45 +0,0 @@
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 => e
42
- Rails.logger.error "[zizia] The file #{file} could not be parsed as CSV: #{e}"
43
- end
44
- end
45
- end
@@ -1,44 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Zizia
4
- class RecordImporter
5
- ##
6
- # @!attribute [rw] batch_id
7
- # @return [String] an optional batch id for this import run
8
- # @!attribute [rw] success_count
9
- # @return [Integer] a count of the records that were successfully created
10
- # @!attribute [rw] failure_count
11
- # @return [Integer] a count of the records that failed import
12
- attr_accessor :batch_id, :success_count, :failure_count
13
-
14
- ##
15
- # @param record [ImportRecord]
16
- #
17
- # @return [void]
18
- def import(record:)
19
- create_for(record: record)
20
- rescue Faraday::ConnectionFailed, Ldp::HttpError => e
21
- Rails.logger.error "[zizia] #{e}"
22
- rescue RuntimeError => e
23
- Rails.logger.error "[zizia] #{e}"
24
- raise e
25
- end
26
-
27
- def import_type
28
- raise 'No curation_concern found for import' unless
29
- defined?(Hyrax) && Hyrax&.config&.curation_concerns&.any?
30
-
31
- Hyrax.config.curation_concerns.first
32
- end
33
-
34
- private
35
-
36
- def create_for(record:)
37
- Rails.logger.info "[zizia] Creating record: #{record.respond_to?(:title) ? record.title : record}."
38
-
39
- created = import_type.create(record.attributes)
40
-
41
- Rails.logger.info "[zizia] Record created at: #{created.id}"
42
- end
43
- end
44
- end
@@ -1,22 +0,0 @@
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
@@ -1,32 +0,0 @@
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
@@ -1,73 +0,0 @@
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
@@ -1,29 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- shared_examples 'a Zizia::Validator' do
4
- subject(:validator) { described_class.new }
5
-
6
- define :be_a_validator_error do # |expected|
7
- match { false } # { |actual| some_condition }
8
- end
9
-
10
- describe '#validate' do
11
- context 'without a parser' do
12
- it 'raises ArgumentError' do
13
- expect { validator.validate }.to raise_error ArgumentError
14
- end
15
- end
16
-
17
- it 'gives an empty error collection for a valid parser' do
18
- expect(validator.validate(parser: valid_parser)).not_to be_any if
19
- defined?(valid_parser)
20
- end
21
-
22
- context 'for an invalid parser' do
23
- it 'gives an non-empty error collection' do
24
- expect(validator.validate(parser: invalid_parser)).to be_any if
25
- defined?(invalid_parser)
26
- end
27
- end
28
- end
29
- end
data/lib/zizia/spec.rb DELETED
@@ -1,14 +0,0 @@
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_parser'
11
- require 'zizia/spec/shared_examples/a_validator'
12
- require 'zizia/spec/fakes/fake_parser'
13
- end
14
- end