valkyrie 2.1.0 → 3.0.0.pre.beta.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 (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