valkyrie 2.1.0 → 3.0.0.pre.beta.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (64) hide show
  1. checksums.yaml +4 -4
  2. data/.circleci/config.yml +71 -36
  3. data/.lando.yml +58 -0
  4. data/.rubocop.yml +11 -1
  5. data/.tool-versions +1 -1
  6. data/CHANGELOG.md +94 -13
  7. data/CONTRIBUTING.md +30 -8
  8. data/README.md +24 -48
  9. data/Rakefile +26 -20
  10. data/db/config.yml +3 -10
  11. data/lib/generators/valkyrie/resource_generator.rb +3 -3
  12. data/lib/valkyrie/change_set.rb +3 -3
  13. data/lib/valkyrie/id.rb +12 -19
  14. data/lib/valkyrie/indexers/access_controls_indexer.rb +17 -17
  15. data/lib/valkyrie/persistence/buffered_persister.rb +2 -2
  16. data/lib/valkyrie/persistence/composite_persister.rb +3 -3
  17. data/lib/valkyrie/persistence/custom_query_container.rb +8 -16
  18. data/lib/valkyrie/persistence/fedora/list_node.rb +43 -43
  19. data/lib/valkyrie/persistence/fedora/metadata_adapter.rb +5 -1
  20. data/lib/valkyrie/persistence/fedora/ordered_list.rb +90 -90
  21. data/lib/valkyrie/persistence/fedora/ordered_reader.rb +5 -5
  22. data/lib/valkyrie/persistence/fedora/permissive_schema.rb +1 -1
  23. data/lib/valkyrie/persistence/fedora/persister/model_converter.rb +15 -16
  24. data/lib/valkyrie/persistence/fedora/persister/orm_converter.rb +14 -19
  25. data/lib/valkyrie/persistence/fedora/persister.rb +83 -83
  26. data/lib/valkyrie/persistence/fedora/query_service.rb +39 -41
  27. data/lib/valkyrie/persistence/memory/persister.rb +51 -35
  28. data/lib/valkyrie/persistence/memory/query_service.rb +26 -30
  29. data/lib/valkyrie/persistence/postgres/orm_converter.rb +52 -52
  30. data/lib/valkyrie/persistence/postgres/persister.rb +4 -1
  31. data/lib/valkyrie/persistence/postgres/query_service.rb +34 -34
  32. data/lib/valkyrie/persistence/shared/json_value_mapper.rb +1 -1
  33. data/lib/valkyrie/persistence/solr/metadata_adapter.rb +15 -3
  34. data/lib/valkyrie/persistence/solr/model_converter.rb +323 -340
  35. data/lib/valkyrie/persistence/solr/orm_converter.rb +4 -4
  36. data/lib/valkyrie/persistence/solr/persister.rb +16 -4
  37. data/lib/valkyrie/persistence/solr/queries/find_by_alternate_identifier_query.rb +1 -1
  38. data/lib/valkyrie/persistence/solr/queries/find_by_id_query.rb +1 -1
  39. data/lib/valkyrie/persistence/solr/queries/find_members_query.rb +1 -1
  40. data/lib/valkyrie/persistence/solr/query_service.rb +12 -12
  41. data/lib/valkyrie/persistence/solr/repository.rb +17 -7
  42. data/lib/valkyrie/resource/access_controls.rb +1 -1
  43. data/lib/valkyrie/resource.rb +0 -1
  44. data/lib/valkyrie/specs/shared_specs/change_set.rb +1 -1
  45. data/lib/valkyrie/specs/shared_specs/file.rb +1 -0
  46. data/lib/valkyrie/specs/shared_specs/persister.rb +22 -4
  47. data/lib/valkyrie/specs/shared_specs/queries.rb +7 -0
  48. data/lib/valkyrie/specs/shared_specs/resource.rb +1 -1
  49. data/lib/valkyrie/specs/shared_specs/storage_adapter.rb +19 -0
  50. data/lib/valkyrie/specs/shared_specs/write_only/metadata_adapter.rb +62 -0
  51. data/lib/valkyrie/specs/shared_specs.rb +2 -0
  52. data/lib/valkyrie/storage/disk.rb +24 -1
  53. data/lib/valkyrie/storage/fedora.rb +17 -17
  54. data/lib/valkyrie/storage_adapter.rb +12 -12
  55. data/lib/valkyrie/types.rb +1 -1
  56. data/lib/valkyrie/version.rb +1 -1
  57. data/lib/valkyrie/vocab/pcdm_use.rb +12 -0
  58. data/lib/valkyrie.rb +13 -27
  59. data/tasks/dev.rake +14 -51
  60. data/valkyrie.gemspec +3 -6
  61. metadata +25 -63
  62. data/.docker-stack/valkyrie-development/docker-compose.yml +0 -53
  63. data/.docker-stack/valkyrie-test/docker-compose.yml +0 -53
  64. data/tasks/docker.rake +0 -31
@@ -48,7 +48,7 @@ module Valkyrie::Persistence::Solr
48
48
  # Construct a Time object from the datestamp for the resource creation date indexed in Solr
49
49
  # @return [Time]
50
50
  def created_at
51
- DateTime.parse(solr_document.fetch("created_at_dtsi").to_s).utc
51
+ DateTime.parse(solr_document.fetch("created_at_dtsi").to_s).new_offset(0)
52
52
  end
53
53
 
54
54
  # Construct a Time object from the datestamp for the date of the last resource update indexed in Solr
@@ -79,7 +79,7 @@ module Valkyrie::Persistence::Solr
79
79
 
80
80
  # Construct the Hash containing the Valkyrie Resource attributes using the Solr Document
81
81
  # @note this filters for attributes which have been indexed as stored multivalued texts (tsim)
82
- # @see https://github.com/samvera-labs/valkyrie/blob/master/solr/config/schema.xml
82
+ # @see https://github.com/samvera-labs/valkyrie/blob/main/solr/config/schema.xml
83
83
  # @see https://lucene.apache.org/solr/guide/defining-fields.html#defining-fields
84
84
  # @return [Hash]
85
85
  def attribute_hash
@@ -445,7 +445,7 @@ module Valkyrie::Persistence::Solr
445
445
  # @return [Boolean]
446
446
  def self.handles?(value)
447
447
  return false unless value.to_s.start_with?("datetime-")
448
- DateTime.iso8601(value.sub(/^datetime-/, '')).utc
448
+ DateTime.iso8601(value.sub(/^datetime-/, '')).new_offset(0)
449
449
  rescue
450
450
  false
451
451
  end
@@ -453,7 +453,7 @@ module Valkyrie::Persistence::Solr
453
453
  # Parses and casts the Solr field value into a UTC DateTime value
454
454
  # @return [Time]
455
455
  def result
456
- DateTime.parse(value.sub(/^datetime-/, '')).utc
456
+ DateTime.parse(value.sub(/^datetime-/, '')).new_offset(0)
457
457
  end
458
458
  end
459
459
  end
@@ -6,7 +6,7 @@ module Valkyrie::Persistence::Solr
6
6
  # Most methods are delegated to {Valkyrie::Persistence::Solr::Repository}
7
7
  class Persister
8
8
  attr_reader :adapter
9
- delegate :connection, :resource_factory, to: :adapter
9
+ delegate :connection, :query_service, :resource_factory, :write_only?, :soft_commit?, to: :adapter
10
10
 
11
11
  # @param adapter [Valkyrie::Persistence::Solr::MetadataAdapter] The adapter with the
12
12
  # configured solr connection.
@@ -15,11 +15,23 @@ module Valkyrie::Persistence::Solr
15
15
  end
16
16
 
17
17
  # (see Valkyrie::Persistence::Memory::Persister#save)
18
- def save(resource:)
19
- repository([resource]).persist.first
18
+ # @return [Boolean] If write_only, whether saving succeeded.
19
+ def save(resource:, external_resource: false)
20
+ if write_only?
21
+ repository([resource]).persist
22
+ else
23
+ raise Valkyrie::Persistence::ObjectNotFoundError, "The object #{resource.id} is previously persisted but not found at save time." unless external_resource || valid_for_save?(resource)
24
+ repository([resource]).persist.first
25
+ end
26
+ end
27
+
28
+ def valid_for_save?(resource)
29
+ return true unless resource.persisted? # a new resource
30
+ query_service.find_by(id: resource.id).present? # a persisted resource must be found
20
31
  end
21
32
 
22
33
  # (see Valkyrie::Persistence::Memory::Persister#save_all)
34
+ # @return [Boolean] If write_only, whether saving succeeded.
23
35
  def save_all(resources:)
24
36
  repository(resources).persist
25
37
  end
@@ -39,7 +51,7 @@ module Valkyrie::Persistence::Solr
39
51
  # @param [Array<Valkyrie::Resource>] resources
40
52
  # @return [Valkyrie::Persistence::Solr::Repository]
41
53
  def repository(resources)
42
- Valkyrie::Persistence::Solr::Repository.new(resources: resources, connection: connection, resource_factory: resource_factory)
54
+ Valkyrie::Persistence::Solr::Repository.new(resources: resources, persister: self)
43
55
  end
44
56
  end
45
57
  end
@@ -32,7 +32,7 @@ module Valkyrie::Persistence::Solr::Queries
32
32
  # @note the field used here is alternate_ids_ssim and the value is prefixed by "id-"
33
33
  # @return [Hash]
34
34
  def resource
35
- connection.get("select", params: { q: "alternate_ids_ssim:\"id-#{alternate_identifier}\"", fl: "*", rows: 1 })["response"]["docs"].first
35
+ @resource ||= connection.get("select", params: { q: "alternate_ids_ssim:\"id-#{alternate_identifier}\"", fl: "*", rows: 1 })["response"]["docs"].first
36
36
  end
37
37
  end
38
38
  end
@@ -31,7 +31,7 @@ module Valkyrie::Persistence::Solr::Queries
31
31
  # Query Solr for for the first document with the ID in a field
32
32
  # @return [Hash]
33
33
  def resource
34
- connection.get("select", params: { q: "id:\"#{id}\"", fl: "*", rows: 1 })["response"]["docs"].first
34
+ @resource ||= connection.get("select", params: { q: "id:\"#{id}\"", fl: "*", rows: 1 })["response"]["docs"].first
35
35
  end
36
36
  end
37
37
  end
@@ -27,7 +27,7 @@ module Valkyrie::Persistence::Solr::Queries
27
27
  # Results are ordered by the member IDs specified in the Valkyrie Resource attribute
28
28
  # @yield [Valkyrie::Resource]
29
29
  def each
30
- return [] unless resource.id.present?
30
+ return [] if resource.id.blank?
31
31
  member_ids.map { |id| unordered_members.find { |member| member.id == id } }.reject(&:nil?).each do |member|
32
32
  yield member
33
33
  end
@@ -97,19 +97,19 @@ module Valkyrie::Persistence::Solr
97
97
 
98
98
  private
99
99
 
100
- # (see Valkyrie::Persistence::Memory::QueryService#validate_id)
101
- def validate_id(id)
102
- raise ArgumentError, 'id must be a Valkyrie::ID' unless id.is_a? Valkyrie::ID
103
- end
100
+ # (see Valkyrie::Persistence::Memory::QueryService#validate_id)
101
+ def validate_id(id)
102
+ raise ArgumentError, 'id must be a Valkyrie::ID' unless id.is_a? Valkyrie::ID
103
+ end
104
104
 
105
- # (see Valkyrie::Persistence::Memory::QueryService#ensure_persisted)
106
- def ensure_persisted(resource)
107
- raise ArgumentError, 'resource is not saved' unless resource.persisted?
108
- end
105
+ # (see Valkyrie::Persistence::Memory::QueryService#ensure_persisted)
106
+ def ensure_persisted(resource)
107
+ raise ArgumentError, 'resource is not saved' unless resource.persisted?
108
+ end
109
109
 
110
- # (see Valkyrie::Persistence::Memory::QueryService#ordered_property?)
111
- def ordered_property?(resource:, property:)
112
- resource.ordered_attribute?(property)
113
- end
110
+ # (see Valkyrie::Persistence::Memory::QueryService#ordered_property?)
111
+ def ordered_property?(resource:, property:)
112
+ resource.ordered_attribute?(property)
113
+ end
114
114
  end
115
115
  end
@@ -3,17 +3,18 @@ module Valkyrie::Persistence::Solr
3
3
  # Responsible for handling the logic for persisting or deleting multiple
4
4
  # objects into or out of solr.
5
5
  class Repository
6
- COMMIT_PARAMS = { softCommit: true, versions: true }.freeze
6
+ SOFT_COMMIT_PARAMS = { softCommit: true, versions: true }.freeze
7
+ NO_COMMIT_PARAMS = { versions: true }.freeze
7
8
 
8
- attr_reader :resources, :connection, :resource_factory
9
+ attr_reader :resources, :persister
10
+ delegate :connection, :resource_factory, :write_only?, :soft_commit?, to: :persister
9
11
 
10
12
  # @param [Array<Valkyrie::Resource>] resources
11
13
  # @param [RSolr::Client] connection
12
14
  # @param [ResourceFactory] resource_factory
13
- def initialize(resources:, connection:, resource_factory:)
15
+ def initialize(resources:, persister:)
14
16
  @resources = resources
15
- @connection = connection
16
- @resource_factory = resource_factory
17
+ @persister = persister
17
18
  end
18
19
 
19
20
  # Persist the resources into Solr
@@ -24,6 +25,7 @@ module Valkyrie::Persistence::Solr
24
25
  solr_document(resource)
25
26
  end
26
27
  results = add_documents(documents)
28
+ return true if write_only?
27
29
  versions = results["adds"]&.each_slice(2)&.to_h
28
30
  documents.map do |document|
29
31
  document["_version_"] = versions.fetch(document[:id])
@@ -35,7 +37,7 @@ module Valkyrie::Persistence::Solr
35
37
  # @return [RSolr::HashWithResponse]
36
38
  # rubocop:disable Style/IfUnlessModifier
37
39
  def add_documents(documents)
38
- connection.add documents, params: COMMIT_PARAMS
40
+ connection.add documents, params: commit_params
39
41
  rescue RSolr::Error::Http => exception
40
42
  # Error 409 conflict is returned when versions do not match
41
43
  if exception.response&.fetch(:status) == 409
@@ -48,7 +50,7 @@ module Valkyrie::Persistence::Solr
48
50
  # Deletes a Solr Document using the ID
49
51
  # @return [Array<Valkyrie::Resource>] resources which have been deleted from Solr
50
52
  def delete
51
- connection.delete_by_id resources.map { |resource| resource.id.to_s }, params: COMMIT_PARAMS
53
+ connection.delete_by_id resources.map { |resource| resource.id.to_s }, params: commit_params
52
54
  resources
53
55
  end
54
56
 
@@ -75,5 +77,13 @@ module Valkyrie::Persistence::Solr
75
77
  raise Valkyrie::Persistence::StaleObjectError, "One or more resources have been updated by another process." if resources.count > 1
76
78
  raise Valkyrie::Persistence::StaleObjectError, "The object #{resources.first.id} has been updated by another process."
77
79
  end
80
+
81
+ def commit_params
82
+ if persister.soft_commit?
83
+ SOFT_COMMIT_PARAMS
84
+ else
85
+ NO_COMMIT_PARAMS
86
+ end
87
+ end
78
88
  end
79
89
  end
@@ -11,7 +11,7 @@ module Valkyrie
11
11
  # attribute :nested_resource
12
12
  # end
13
13
  #
14
- # @see https://github.com/samvera/hydra-head/tree/master/hydra-access-controls
14
+ # @see https://github.com/samvera/hydra-head/tree/main/hydra-access-controls
15
15
  # @see lib/valkyrie/indexers/access_controls_indexer/rb
16
16
  module AccessControls
17
17
  def self.included(klass)
@@ -15,7 +15,6 @@ module Valkyrie
15
15
  # @see lib/valkyrie/specs/shared_specs/resource.rb
16
16
  # rubocop:disable Metrics/ClassLength
17
17
  class Resource < Dry::Struct
18
- include Draper::Decoratable
19
18
  # Allows a Valkyrie::Resource to be instantiated without providing every
20
19
  # available key, and makes sure the defaults are set up if no value is
21
20
  # given.
@@ -75,7 +75,7 @@ RSpec.shared_examples 'a Valkyrie::ChangeSet' do |*_flags|
75
75
 
76
76
  describe "#optimistic_locking_enabled?" do
77
77
  it "delegates down to the resource" do
78
- expect(change_set.optimistic_locking_enabled?).to eq false
78
+ expect(change_set.optimistic_locking_enabled?).to eq change_set.resource.optimistic_locking_enabled?
79
79
  end
80
80
  end
81
81
  end
@@ -9,6 +9,7 @@ RSpec.shared_examples 'a Valkyrie::StorageAdapter::File' do
9
9
  it { is_expected.to respond_to(:read) }
10
10
  it { is_expected.to respond_to(:rewind) }
11
11
  it { is_expected.to respond_to(:id) }
12
+ it { is_expected.to respond_to(:close) }
12
13
  describe "#disk_path" do
13
14
  it "returns an existing disk path" do
14
15
  expect(File.exist?(file.disk_path)).to eq true
@@ -50,6 +50,24 @@ RSpec.shared_examples 'a Valkyrie::Persister' do |*flags|
50
50
  expect(reloaded.nested_resource.first.title).to eq ["Nested"]
51
51
  end
52
52
 
53
+ context "when a persisted resource is not in the database" do
54
+ it "throws an ObjectNotFoundError" do
55
+ expect(resource).not_to be_persisted
56
+ saved = persister.save(resource: resource)
57
+
58
+ expect(saved).to be_persisted
59
+ persister.delete(resource: saved)
60
+
61
+ expect { persister.save(resource: saved) }.to raise_error(Valkyrie::Persistence::ObjectNotFoundError)
62
+ end
63
+ it "is okay if it's from another persister" do
64
+ memory_adapter = Valkyrie::Persistence::Memory::MetadataAdapter.new
65
+ saved = memory_adapter.persister.save(resource: resource)
66
+
67
+ expect { persister.save(resource: saved, external_resource: true) }.not_to raise_error
68
+ end
69
+ end
70
+
53
71
  it "can persist single values" do
54
72
  resource.single_value = "A single value"
55
73
 
@@ -194,9 +212,9 @@ RSpec.shared_examples 'a Valkyrie::Persister' do |*flags|
194
212
  reloaded = query_service.find_by(id: book.id)
195
213
 
196
214
  expect(reloaded.title.first.to_i).to eq(time1.to_i)
197
- expect(reloaded.title.first.zone).to eq('UTC')
215
+ expect(reloaded.title.first.zone).to eq('+00:00')
198
216
  expect(reloaded.author.first.to_i).to eq(time2.to_i)
199
- expect(reloaded.author.first.zone).to eq('UTC')
217
+ expect(reloaded.author.first.zone).to eq('+00:00')
200
218
  expect(reloaded.other_author.first).to eq "2019-01"
201
219
  end
202
220
 
@@ -351,7 +369,7 @@ RSpec.shared_examples 'a Valkyrie::Persister' do |*flags|
351
369
  # update the resource in the datastore to make its token stale
352
370
  persister.save(resource: resource)
353
371
 
354
- expect { persister.save(resource: resource) }.to raise_error(Valkyrie::Persistence::StaleObjectError, "The object #{resource.id} has been updated by another process.")
372
+ expect { persister.save(resource: resource) }.to raise_error(Valkyrie::Persistence::StaleObjectError)
355
373
  end
356
374
  end
357
375
 
@@ -425,7 +443,7 @@ RSpec.shared_examples 'a Valkyrie::Persister' do |*flags|
425
443
  persister.save(resource: resource2)
426
444
 
427
445
  expect { persister.save_all(resources: [resource1, resource2, resource3]) }
428
- .to raise_error(Valkyrie::Persistence::StaleObjectError, "One or more resources have been updated by another process.")
446
+ .to raise_error(Valkyrie::Persistence::StaleObjectError)
429
447
  end
430
448
  end
431
449
  end
@@ -464,6 +464,13 @@ RSpec.shared_examples 'a Valkyrie query provider' do
464
464
  end
465
465
  end
466
466
 
467
+ describe ".custom_queries" do
468
+ it "raises NoMethodError when the custom query does not exist" do
469
+ expect(query_service.custom_queries).not_to respond_to :very_fake_query
470
+ expect { query_service.custom_queries.very_fake_query }.to raise_error(NoMethodError)
471
+ end
472
+ end
473
+
467
474
  describe ".register_query_handler" do
468
475
  it "can register a query handler" do
469
476
  class QueryHandler
@@ -31,7 +31,7 @@ RSpec.shared_examples 'a Valkyrie::Resource' do
31
31
  end
32
32
  end
33
33
 
34
- describe "#fields" do
34
+ describe ".fields" do
35
35
  it "returns a set of fields" do
36
36
  expect(meta_klass).to respond_to(:fields).with(0).arguments
37
37
  expect(meta_klass.fields).to include(:id)
@@ -30,6 +30,25 @@ RSpec.shared_examples 'a Valkyrie::StorageAdapter' do
30
30
  expect(uploaded_file.valid?(digests: { sha1: sha1 })).to be true
31
31
  end
32
32
 
33
+ it "doesn't leave a file handle open on upload/find_by" do
34
+ # No file handle left open from upload.
35
+ resource = Valkyrie::Specs::CustomResource.new(id: "testdiscovery")
36
+ pre_open_files = open_files
37
+ uploaded_file = storage_adapter.upload(file: file, original_filename: 'foo.jpg', resource: resource, fake_upload_argument: true)
38
+ file.close
39
+ expect(pre_open_files.size).to eq open_files.size
40
+
41
+ # No file handle left open from find_by
42
+ pre_open_files = open_files
43
+ the_file = storage_adapter.find_by(id: uploaded_file.id)
44
+ expect(the_file).to be_kind_of Valkyrie::StorageAdapter::File
45
+ expect(pre_open_files.size).to eq open_files.size
46
+ end
47
+
48
+ def open_files
49
+ `lsof +D . | awk '{print $9}'`.split.uniq[1..-1]
50
+ end
51
+
33
52
  it "can upload, validate, re-fetch, and delete a file" do
34
53
  resource = Valkyrie::Specs::CustomResource.new(id: "test")
35
54
  sha1 = Digest::SHA1.file(file).to_s
@@ -0,0 +1,62 @@
1
+ # frozen_string_literal: true
2
+ RSpec.shared_examples 'a write-only Valkyrie::MetadataAdapter' do |passed_adapter|
3
+ before do
4
+ raise 'adapter must be set with `let(:adapter)`' unless
5
+ defined? adapter
6
+ end
7
+ subject { passed_adapter || adapter }
8
+ let(:persister) { adapter.persister }
9
+ it { is_expected.to respond_to(:persister).with(0).arguments }
10
+ it { is_expected.to respond_to(:id).with(0).arguments }
11
+
12
+ describe "#id" do
13
+ it "is a valid string representation of an MD5 hash" do
14
+ expect(adapter.id).to be_a Valkyrie::ID
15
+ expect(adapter.id.to_s.length).to eq 32
16
+ expect(adapter.id.to_s).to match(/^[a-f,0-9]+$/)
17
+ end
18
+ end
19
+
20
+ describe "#write_only?" do
21
+ it "returns true" do
22
+ expect(adapter).to be_write_only
23
+ end
24
+ end
25
+
26
+ describe "persister" do
27
+ before do
28
+ class WriteOnlyCustomResource < Valkyrie::Resource
29
+ include Valkyrie::Resource::AccessControls
30
+ attribute :title
31
+ attribute :author
32
+ attribute :other_author
33
+ attribute :member_ids
34
+ attribute :nested_resource
35
+ attribute :single_value, Valkyrie::Types::String.optional
36
+ attribute :ordered_authors, Valkyrie::Types::Array.of(Valkyrie::Types::Anything).meta(ordered: true)
37
+ attribute :ordered_nested, Valkyrie::Types::Array.of(WriteOnlyCustomResource).meta(ordered: true)
38
+ end
39
+ end
40
+ after do
41
+ Object.send(:remove_const, :WriteOnlyCustomResource)
42
+ end
43
+
44
+ subject { persister }
45
+ let(:resource_class) { WriteOnlyCustomResource }
46
+ let(:resource) { resource_class.new }
47
+
48
+ it { is_expected.to respond_to(:save).with_keywords(:resource) }
49
+ it { is_expected.to respond_to(:save_all).with_keywords(:resources) }
50
+ it { is_expected.to respond_to(:delete).with_keywords(:resource) }
51
+
52
+ it "can save a resource" do
53
+ expect(persister.save(resource: resource)).to eq true
54
+ end
55
+
56
+ it "can save multiple resources at once" do
57
+ resource2 = resource_class.new
58
+ results = persister.save_all(resources: [resource, resource2])
59
+ expect(results).to eq true
60
+ end
61
+ end
62
+ end
@@ -13,3 +13,5 @@ require 'valkyrie/specs/shared_specs/change_set_persister.rb'
13
13
  require 'valkyrie/specs/shared_specs/file.rb'
14
14
  require 'valkyrie/specs/shared_specs/change_set.rb'
15
15
  require 'valkyrie/specs/shared_specs/solr_indexer.rb'
16
+ # Write-only tests.
17
+ require 'valkyrie/specs/shared_specs/write_only/metadata_adapter.rb'
@@ -36,11 +36,34 @@ module Valkyrie::Storage
36
36
  # @return [Valkyrie::StorageAdapter::File]
37
37
  # @raise Valkyrie::StorageAdapter::FileNotFound if nothing is found
38
38
  def find_by(id:)
39
- Valkyrie::StorageAdapter::File.new(id: Valkyrie::ID.new(id.to_s), io: ::File.open(file_path(id), 'rb'))
39
+ Valkyrie::StorageAdapter::File.new(id: Valkyrie::ID.new(id.to_s), io: LazyFile.open(file_path(id), 'rb'))
40
40
  rescue Errno::ENOENT
41
41
  raise Valkyrie::StorageAdapter::FileNotFound
42
42
  end
43
43
 
44
+ ## LazyFile takes File.open parameters but doesn't leave a file handle open on
45
+ # instantiation. This way StorageAdapter#find_by doesn't open a handle
46
+ # silently and never clean up after itself.
47
+ class LazyFile
48
+ def self.open(path, mode)
49
+ # Open the file regularly and close it, so it can error if it doesn't
50
+ # exist.
51
+ File.open(path, mode).close
52
+ new(path, mode)
53
+ end
54
+
55
+ delegate(*(File.instance_methods - Object.instance_methods), to: :_inner_file)
56
+
57
+ def initialize(path, mode)
58
+ @__path = path
59
+ @__mode = mode
60
+ end
61
+
62
+ def _inner_file
63
+ @_inner_file ||= File.open(@__path, @__mode)
64
+ end
65
+ end
66
+
44
67
  # Delete the file on disk associated with the given identifier.
45
68
  # @param id [Valkyrie::ID]
46
69
  def delete(id:)
@@ -37,7 +37,7 @@ module Valkyrie::Storage
37
37
  def upload(file:, original_filename:, resource:, content_type: "application/octet-stream", # rubocop:disable Metrics/ParameterLists
38
38
  resource_uri_transformer: default_resource_uri_transformer, **_extra_arguments)
39
39
  identifier = resource_uri_transformer.call(resource, base_url) + '/original'
40
- sha1 = fedora_version == 5 ? "sha" : "sha1"
40
+ sha1 = [5, 6].include?(fedora_version) ? "sha" : "sha1"
41
41
  connection.http.put do |request|
42
42
  request.url identifier
43
43
  request.headers['Content-Type'] = content_type
@@ -83,24 +83,24 @@ module Valkyrie::Storage
83
83
 
84
84
  private
85
85
 
86
- # @return [IOProxy]
87
- def response(id:)
88
- response = connection.http.get(fedora_identifier(id: id))
89
- raise Valkyrie::StorageAdapter::FileNotFound unless response.success?
90
- IOProxy.new(response.body)
91
- end
86
+ # @return [IOProxy]
87
+ def response(id:)
88
+ response = connection.http.get(fedora_identifier(id: id))
89
+ raise Valkyrie::StorageAdapter::FileNotFound unless response.success?
90
+ IOProxy.new(response.body)
91
+ end
92
92
 
93
- def default_resource_uri_transformer
94
- lambda do |resource, base_url|
95
- id = CGI.escape(resource.id.to_s)
96
- RDF::URI.new(base_url + id)
97
- end
93
+ def default_resource_uri_transformer
94
+ lambda do |resource, base_url|
95
+ id = CGI.escape(resource.id.to_s)
96
+ RDF::URI.new(base_url + id)
98
97
  end
98
+ end
99
99
 
100
- def base_url
101
- pre_divider = base_path.starts_with?(SLASH) ? '' : SLASH
102
- post_divider = base_path.ends_with?(SLASH) ? '' : SLASH
103
- "#{connection.http.url_prefix}#{pre_divider}#{base_path}#{post_divider}"
104
- end
100
+ def base_url
101
+ pre_divider = base_path.starts_with?(SLASH) ? '' : SLASH
102
+ post_divider = base_path.ends_with?(SLASH) ? '' : SLASH
103
+ "#{connection.http.url_prefix}#{pre_divider}#{base_path}#{post_divider}"
104
+ end
105
105
  end
106
106
  end
@@ -67,7 +67,7 @@ module Valkyrie
67
67
  class File < Dry::Struct
68
68
  attribute :id, Valkyrie::Types::Any
69
69
  attribute :io, Valkyrie::Types::Any
70
- delegate :size, :read, :rewind, to: :io
70
+ delegate :size, :read, :rewind, :close, to: :io
71
71
  def stream
72
72
  io
73
73
  end
@@ -106,20 +106,20 @@ module Valkyrie
106
106
 
107
107
  private
108
108
 
109
- def tmp_file_name
110
- id.to_s.tr(':/', '__')
111
- end
109
+ def tmp_file_name
110
+ id.to_s.tr(':/', '__')
111
+ end
112
112
 
113
- def tmp_file_path
114
- ::File.join(Dir.tmpdir, tmp_file_name)
115
- end
113
+ def tmp_file_path
114
+ ::File.join(Dir.tmpdir, tmp_file_name)
115
+ end
116
116
 
117
- def tmp_file
118
- @tmp_file ||= ::File.open(tmp_file_path, 'w+b') do |f|
119
- IO.copy_stream(io, f)
120
- f
121
- end
117
+ def tmp_file
118
+ @tmp_file ||= ::File.open(tmp_file_path, 'w+b') do |f|
119
+ IO.copy_stream(io, f)
120
+ f
122
121
  end
122
+ end
123
123
  end
124
124
  end
125
125
  end
@@ -84,7 +84,7 @@ module Valkyrie
84
84
  Set = Array.constructor do |value|
85
85
  value = Array[value]
86
86
  clean_values = value.reject do |val|
87
- val == '' || (val.is_a?(Valkyrie::ID) && val.to_s == '')
87
+ (val.is_a?(Valkyrie::ID) && val.to_s == '') || val == ''
88
88
  end.reject(&:nil?).uniq
89
89
 
90
90
  clean_values.map do |val|
@@ -1,4 +1,4 @@
1
1
  # frozen_string_literal: true
2
2
  module Valkyrie
3
- VERSION = "2.1.0"
3
+ VERSION = "3.0.0-beta.1"
4
4
  end
@@ -39,10 +39,22 @@ module Valkyrie::Vocab
39
39
  "rdf:subClassOf": %(http://pcdm.org/resources#File),
40
40
  "rdfs:isDefinedBy": %(pcdmuse:),
41
41
  type: "rdfs:Class"
42
+ term :PreservationFile,
43
+ comment: %(Best quality representation of the Object appropriate for long-term
44
+ preservation.),
45
+ label: "preservation file",
46
+ "dct:replaces": %(http://pcdm.org/use#PreservationMasterFile),
47
+ "rdf:subClassOf": %(http://pcdm.org/resources#File),
48
+ "rdfs:isDefinedBy": %(pcdmuse:),
49
+ type: "rdfs:Class"
50
+ warn "[DEPRECATION] PCDM is deprecating '#{self.class}#PreservationMasterFile'. Use #{self.class}#PreservationFile instead."
51
+ # @deprecated
42
52
  term :PreservationMasterFile,
43
53
  comment: %(Best quality representation of the Object appropriate for long-term
44
54
  preservation.),
45
55
  label: "preservation master file",
56
+ "dct:isReplacedBy": %(http://pcdm.org/use#PreservationFile),
57
+ "owl:deprecated": true,
46
58
  "rdf:subClassOf": %(http://pcdm.org/resources#File),
47
59
  "rdfs:isDefinedBy": %(pcdmuse:),
48
60
  type: "rdfs:Class"
data/lib/valkyrie.rb CHANGED
@@ -5,7 +5,6 @@ require 'active_support'
5
5
  require 'active_support/core_ext'
6
6
  require 'dry-types'
7
7
  require 'dry-struct'
8
- require 'draper'
9
8
  require 'reform'
10
9
  require 'rdf'
11
10
  require 'valkyrie/rdf_patches'
@@ -87,19 +86,6 @@ module Valkyrie
87
86
  Valkyrie::StorageAdapter.find(super.to_sym)
88
87
  end
89
88
 
90
- # @api public
91
- # Configure id_string_equality to be true in order to make Valkyrie::ID
92
- # equal to the string value they contain. This will be the default behavior
93
- # in v3.0.0.
94
- #
95
- # @return [Boolean] Whether `Valkyrie::ID` should be equal to their string counterpart.
96
- def id_string_equality
97
- super
98
- end
99
-
100
- # @!attribute [w] id_string_equality=
101
- # The setter for #id_string_equality; see it's implementation
102
-
103
89
  # @api public
104
90
  #
105
91
  # The returned anonymous method (e.g. responds to #call) has a signature of
@@ -119,19 +105,19 @@ module Valkyrie
119
105
 
120
106
  private
121
107
 
122
- def defaults
123
- {
124
- resource_class_resolver: method(:default_resource_class_resolver)
125
- }
126
- end
127
-
128
- # String constantize is a "by convention" factory. This works, but assumes
129
- # the ruby class once used to persist is the model used to now reify.
130
- #
131
- # @param [String] class_name
132
- def default_resource_class_resolver(class_name)
133
- class_name.constantize
134
- end
108
+ def defaults
109
+ {
110
+ resource_class_resolver: method(:default_resource_class_resolver)
111
+ }
112
+ end
113
+
114
+ # String constantize is a "by convention" factory. This works, but assumes
115
+ # the ruby class once used to persist is the model used to now reify.
116
+ #
117
+ # @param [String] class_name
118
+ def default_resource_class_resolver(class_name)
119
+ class_name.constantize
120
+ end
135
121
  end
136
122
 
137
123
  module_function :config, :logger, :logger=, :config_root_path, :environment, :config_file, :config_hash