ocfl 0.4.1 → 0.5.0

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 664b853647982b285f37270e111f75fe19fce52217c50eebed68c6e23340c6fc
4
- data.tar.gz: 32fa2fc95e646671dde3f4b0e3349ad84b77816f4087abe63623ae3d59028e20
3
+ metadata.gz: 734c2a614db8c30eb86ddd0c7a32b6c10d813adb9ae5244d50614f35244c64cd
4
+ data.tar.gz: 4b5daffdbd581fe38974975a7f9e6dfcef734d078833b0eb030d750aacc7b18e
5
5
  SHA512:
6
- metadata.gz: 1f023ae4faca8ac12ba46782a68a099c04a9623f9ed11ce26005ae96a1d259dfe7ca6c0cc2039c20154dda3bed64d9faa8f4181327920a5ebec8b6a597f4f7b1
7
- data.tar.gz: 33db1c161b1e24ba2859c98c65a5071745f2815956903ca45ad7faff8f1c8046040156d42d2483fa832379fd4cf880b39880d40c1b231ba0b44c6a85dc6ddb60
6
+ metadata.gz: 8c67987902f4adc67c8c250af66568872b9f3e9ca9cd9a3df60f26702e4ac16bfb6aea9e44628f427fe910af548ebc534436005e857f15cc3d47e4a31cc667a3
7
+ data.tar.gz: 857a0a20ee5a57c6b7087ef43264dbe2d3fd9b258175cc9449885a4b0eb1d56823cfe87eccad9bb86c5efc26ce5c2433e0465d6c5b0e425523668a18a7131e6a
data/.rubocop.yml CHANGED
@@ -1,3 +1,8 @@
1
+ require:
2
+ - rubocop-performance
3
+ - rubocop-rake
4
+ - rubocop-rspec
5
+
1
6
  AllCops:
2
7
  TargetRubyVersion: 3.1
3
8
 
@@ -17,8 +22,14 @@ Metrics/BlockLength:
17
22
  - describe
18
23
  - context
19
24
 
20
- Style/StringConcatenation:
21
- Enabled: false # Too many false positives with Pathname#+
25
+ RSpec/MultipleExpectations:
26
+ Enabled: false
27
+
28
+ RSpec/ExampleLength:
29
+ Max: 10
30
+
31
+ RSpec/NestedGroups:
32
+ Max: 4
22
33
 
23
34
  Gemspec/DeprecatedAttributeAssignment: # new in 1.30
24
35
  Enabled: true
@@ -219,4 +230,148 @@ Style/SuperWithArgsParentheses: # new in 1.58
219
230
  Style/SwapValues: # new in 1.1
220
231
  Enabled: true
221
232
  Style/YAMLFileRead: # new in 1.53
222
- Enabled: true
233
+ Enabled: true
234
+ Style/MapIntoArray: # new in 1.63
235
+ Enabled: true
236
+ Performance/AncestorsInclude: # new in 1.7
237
+ Enabled: true
238
+ Performance/BigDecimalWithNumericArgument: # new in 1.7
239
+ Enabled: true
240
+ Performance/BlockGivenWithExplicitBlock: # new in 1.9
241
+ Enabled: true
242
+ Performance/CollectionLiteralInLoop: # new in 1.8
243
+ Enabled: true
244
+ Performance/ConcurrentMonotonicTime: # new in 1.12
245
+ Enabled: true
246
+ Performance/ConstantRegexp: # new in 1.9
247
+ Enabled: true
248
+ Performance/MapCompact: # new in 1.11
249
+ Enabled: true
250
+ Performance/MapMethodChain: # new in 1.19
251
+ Enabled: true
252
+ Performance/MethodObjectAsBlock: # new in 1.9
253
+ Enabled: true
254
+ Performance/RedundantEqualityComparisonBlock: # new in 1.10
255
+ Enabled: true
256
+ Performance/RedundantSortBlock: # new in 1.7
257
+ Enabled: true
258
+ Performance/RedundantSplitRegexpArgument: # new in 1.10
259
+ Enabled: true
260
+ Performance/RedundantStringChars: # new in 1.7
261
+ Enabled: true
262
+ Performance/ReverseFirst: # new in 1.7
263
+ Enabled: true
264
+ Performance/SortReverse: # new in 1.7
265
+ Enabled: true
266
+ Performance/Squeeze: # new in 1.7
267
+ Enabled: true
268
+ Performance/StringIdentifierArgument: # new in 1.13
269
+ Enabled: true
270
+ Performance/StringInclude: # new in 1.7
271
+ Enabled: true
272
+ Performance/Sum: # new in 1.8
273
+ Enabled: true
274
+ Capybara/ClickLinkOrButtonStyle: # new in 2.19
275
+ Enabled: true
276
+ Capybara/MatchStyle: # new in 2.17
277
+ Enabled: true
278
+ Capybara/NegationMatcher: # new in 2.14
279
+ Enabled: true
280
+ Capybara/RedundantWithinFind: # new in 2.20
281
+ Enabled: true
282
+ Capybara/SpecificActions: # new in 2.14
283
+ Enabled: true
284
+ Capybara/SpecificFinders: # new in 2.13
285
+ Enabled: true
286
+ Capybara/SpecificMatcher: # new in 2.12
287
+ Enabled: true
288
+ Capybara/RSpec/HaveSelector: # new in 2.19
289
+ Enabled: true
290
+ Capybara/RSpec/PredicateMatcher: # new in 2.19
291
+ Enabled: true
292
+ FactoryBot/AssociationStyle: # new in 2.23
293
+ Enabled: true
294
+ FactoryBot/ConsistentParenthesesStyle: # new in 2.14
295
+ Enabled: true
296
+ FactoryBot/ExcessiveCreateList: # new in 2.25
297
+ Enabled: true
298
+ FactoryBot/FactoryAssociationWithStrategy: # new in 2.23
299
+ Enabled: true
300
+ FactoryBot/FactoryNameStyle: # new in 2.16
301
+ Enabled: true
302
+ FactoryBot/IdSequence: # new in 2.24
303
+ Enabled: true
304
+ FactoryBot/RedundantFactoryOption: # new in 2.23
305
+ Enabled: true
306
+ FactoryBot/SyntaxMethods: # new in 2.7
307
+ Enabled: true
308
+ RSpecRails/AvoidSetupHook: # new in 2.4
309
+ Enabled: true
310
+ RSpecRails/HaveHttpStatus: # new in 2.12
311
+ Enabled: true
312
+ RSpecRails/InferredSpecType: # new in 2.14
313
+ Enabled: true
314
+ RSpecRails/MinitestAssertions: # new in 2.17
315
+ Enabled: true
316
+ RSpecRails/NegationBeValid: # new in 2.23
317
+ Enabled: true
318
+ RSpecRails/TravelAround: # new in 2.19
319
+ Enabled: true
320
+ RSpec/BeEmpty: # new in 2.20
321
+ Enabled: true
322
+ RSpec/BeEq: # new in 2.9.0
323
+ Enabled: true
324
+ RSpec/BeNil: # new in 2.9.0
325
+ Enabled: true
326
+ RSpec/ChangeByZero: # new in 2.11
327
+ Enabled: true
328
+ RSpec/ContainExactly: # new in 2.19
329
+ Enabled: true
330
+ RSpec/DuplicatedMetadata: # new in 2.16
331
+ Enabled: true
332
+ RSpec/EmptyMetadata: # new in 2.24
333
+ Enabled: true
334
+ RSpec/EmptyOutput: # new in 2.29
335
+ Enabled: true
336
+ RSpec/Eq: # new in 2.24
337
+ Enabled: true
338
+ RSpec/ExcessiveDocstringSpacing: # new in 2.5
339
+ Enabled: true
340
+ RSpec/IdenticalEqualityAssertion: # new in 2.4
341
+ Enabled: true
342
+ RSpec/IndexedLet: # new in 2.20
343
+ Enabled: true
344
+ RSpec/IsExpectedSpecify: # new in 2.27
345
+ Enabled: true
346
+ RSpec/MatchArray: # new in 2.19
347
+ Enabled: true
348
+ RSpec/MetadataStyle: # new in 2.24
349
+ Enabled: true
350
+ RSpec/NoExpectationExample: # new in 2.13
351
+ Enabled: true
352
+ RSpec/PendingWithoutReason: # new in 2.16
353
+ Enabled: true
354
+ RSpec/ReceiveMessages: # new in 2.23
355
+ Enabled: true
356
+ RSpec/RedundantAround: # new in 2.19
357
+ Enabled: true
358
+ RSpec/RedundantPredicateMatcher: # new in 2.26
359
+ Enabled: true
360
+ RSpec/RemoveConst: # new in 2.26
361
+ Enabled: true
362
+ RSpec/RepeatedSubjectCall: # new in 2.27
363
+ Enabled: true
364
+ RSpec/SkipBlockInsideExample: # new in 2.19
365
+ Enabled: true
366
+ RSpec/SortMetadata: # new in 2.14
367
+ Enabled: true
368
+ RSpec/SpecFilePathFormat: # new in 2.24
369
+ Enabled: true
370
+ RSpec/SpecFilePathSuffix: # new in 2.24
371
+ Enabled: true
372
+ RSpec/SubjectDeclaration: # new in 2.5
373
+ Enabled: true
374
+ RSpec/UndescriptiveLiteralsDescription: # new in 2.29
375
+ Enabled: true
376
+ RSpec/VerifiedDoubleReference: # new in 2.10.0
377
+ Enabled: true
data/README.md CHANGED
@@ -35,15 +35,29 @@ new_version.save
35
35
  directory.head
36
36
  # => 'v2'
37
37
 
38
- directory.path("v2", "ocfl.rbs")
38
+ # List file names that were part of a given version
39
+ directory.versions['v2'].file_names
40
+ # => ["ocfl.rbs"]
41
+
42
+ # Get the path of a file in a given version
43
+ directory.path(filepath: "ocfl.rbs", version: "v2")
44
+ # => <Pathname:/files/[object_root]/v2/content/ocfl.rbs>
45
+
46
+ # Get the path of a file in the head version
47
+ directory.path(filepath: "ocfl.rbs", version: :head)
39
48
  # => <Pathname:/files/[object_root]/v2/content/ocfl.rbs>
40
49
 
41
- directory.path(:head, "ocfl.rbs")
50
+ # Get the path of a file from the latest version in which it was changed
51
+ directory.path(filepath: "ocfl.rbs")
42
52
  # => <Pathname:/files/[object_root]/v2/content/ocfl.rbs>
43
53
 
44
54
  new_version = directory.overwrite_current_version
45
55
  new_version.copy_file('sig/ocfl.rbs')
46
56
  new_version.save
57
+
58
+ new_version = directory.clone_current_version
59
+ new_version.copy_file('Gemfile')
60
+ new_version.save
47
61
  ```
48
62
 
49
63
  ## Development
@@ -1,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "digest"
4
-
5
3
  module OCFL
6
4
  module Object
7
5
  # An OCFL Directory layout for a particular object.
@@ -19,15 +17,22 @@ module OCFL
19
17
 
20
18
  delegate :head, :versions, :manifest, to: :inventory
21
19
 
22
- def path(version, filename)
20
+ def path(filepath:, version: nil)
23
21
  version = head if version == :head
24
- relative_path = version_inventory(version).path(filename)
25
- object_root + relative_path
22
+ relative_path = if version
23
+ version_inventory(version).path(filepath)
24
+ else
25
+ inventory.path(filepath)
26
+ end
27
+
28
+ raise FileNotFound, "Path '#{filepath}' not found in #{version || "object"} inventory" if relative_path.nil?
29
+
30
+ object_root / relative_path
26
31
  end
27
32
 
28
33
  def inventory
29
34
  @inventory ||= begin
30
- data = InventoryLoader.load(object_root + "inventory.json")
35
+ data = InventoryLoader.load(object_root / "inventory.json")
31
36
  if data.success?
32
37
  Inventory.new(data: data.value!)
33
38
  else
@@ -44,7 +49,7 @@ module OCFL
44
49
 
45
50
  def version_inventory(version)
46
51
  @version_inventory[version] ||= begin
47
- data = InventoryLoader.load(object_root + version + "inventory.json")
52
+ data = InventoryLoader.load(object_root / version / "inventory.json")
48
53
  if data.success?
49
54
  Inventory.new(data: data.value!)
50
55
  else
@@ -60,12 +65,17 @@ module OCFL
60
65
  @inventory = nil
61
66
  @errors = nil
62
67
  @version_inventory_errors = {}
68
+ true
63
69
  end
64
70
 
65
71
  def begin_new_version
66
72
  DraftVersion.new(object_directory: self)
67
73
  end
68
74
 
75
+ def clone_current_version
76
+ DraftVersion.new(object_directory: self, state: head_inventory.state)
77
+ end
78
+
69
79
  def overwrite_current_version
70
80
  DraftVersion.new(object_directory: self, overwrite_head: true)
71
81
  end
@@ -82,7 +92,7 @@ module OCFL
82
92
  end
83
93
 
84
94
  def head_directory_valid?
85
- InventoryValidator.new(directory: object_root + inventory.head).valid? &&
95
+ InventoryValidator.new(directory: object_root / inventory.head).valid? &&
86
96
  !head_inventory.nil? # Ensures it could be loaded
87
97
  end
88
98
 
@@ -91,9 +101,8 @@ module OCFL
91
101
  end
92
102
 
93
103
  def namaste_file
94
- object_root + "0=ocfl_object_1.1"
104
+ object_root / "0=ocfl_object_1.1"
95
105
  end
96
106
  end
97
- # rubocop:enable Style/StringConcatenation
98
107
  end
99
108
  end
@@ -1,21 +1,25 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "digest"
4
-
5
3
  module OCFL
6
4
  module Object
7
5
  # Creates a OCFL Directory layout for a particular object.
8
6
  class DirectoryBuilder
9
7
  class ObjectExists < Error; end
10
8
 
11
- def initialize(object_root:, id:)
9
+ def initialize(object_root:, id:, content_directory: nil)
12
10
  @object_root = Pathname.new(object_root)
13
11
  raise ObjectExists, "The directory `#{object_root}' already exists" if @object_root.exist?
14
12
 
15
13
  @id = id
14
+ inventory = Inventory.new(
15
+ data: Inventory::InventoryStruct.new(
16
+ new_inventory_attrs.tap { |attrs| attrs[:contentDirectory] = content_directory if content_directory }
17
+ )
18
+ )
19
+ @object_directory = Directory.new(object_root:, inventory:)
16
20
  end
17
21
 
18
- attr_reader :id, :object_root, :object_directory
22
+ attr_reader :id, :inventory, :object_root, :object_directory
19
23
 
20
24
  def copy_file(...)
21
25
  create_object_directory
@@ -33,28 +37,33 @@ module OCFL
33
37
 
34
38
  # @return [Directory]
35
39
  def save
36
- version_path = object_root + "v1"
37
- FileUtils.mkdir_p(version_path) unless version_path.exist? # in case no files were added
38
-
39
- FileUtils.touch(object_root + "0=ocfl_object_1.1")
40
+ FileUtils.mkdir_p(object_root)
41
+ FileUtils.touch(object_directory.namaste_file)
40
42
  write_inventory
41
43
  object_directory
42
44
  end
43
45
 
44
46
  def version
45
- @version ||= begin
46
- data = Inventory::InventoryStruct.new(id:, version: "v0", type: Inventory::URI_1_1, digestAlgorithm: "sha512",
47
- head: "v0", versions: {}, manifest: {})
48
- inventory = Inventory.new(data:)
49
- @object_directory = Directory.new(object_root: @object_root, inventory:)
50
- DraftVersion.new(object_directory:)
51
- end
47
+ @version ||= DraftVersion.new(object_directory:)
52
48
  end
53
49
 
54
50
  def write_inventory
55
51
  version.save
56
52
  end
53
+
54
+ private
55
+
56
+ def new_inventory_attrs
57
+ {
58
+ id:,
59
+ version: "v0",
60
+ type: Inventory::URI_1_1,
61
+ digestAlgorithm: "sha512",
62
+ head: "v0",
63
+ versions: {},
64
+ manifest: {}
65
+ }
66
+ end
57
67
  end
58
- # rubocop:enable Style/StringConcatenation
59
68
  end
60
69
  end
@@ -1,16 +1,14 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "digest"
4
-
5
3
  module OCFL
6
4
  module Object
7
5
  # A new OCFL version
8
6
  class DraftVersion
9
7
  # @params [Directory] object_directory
10
- def initialize(object_directory:, overwrite_head: false)
8
+ def initialize(object_directory:, overwrite_head: false, state: {})
11
9
  @object_directory = object_directory
12
10
  @manifest = object_directory.inventory.manifest.dup
13
- @state = {}
11
+ @state = state
14
12
 
15
13
  number = object_directory.head.delete_prefix("v").to_i
16
14
  @version_number = "v#{overwrite_head ? number : number + 1}"
@@ -40,27 +38,31 @@ module OCFL
40
38
  end
41
39
 
42
40
  def save
43
- inventory = build_inventory
44
- InventoryWriter.new(inventory:, path:).write
45
- FileUtils.cp(path + "inventory.json", object_directory.object_root)
46
- FileUtils.cp(path + "inventory.json.sha512", object_directory.object_root)
41
+ prepare_directory # only necessary if the version has no new content (deletes only)
42
+ write_inventory(build_inventory)
47
43
  object_directory.reload
48
44
  end
49
45
 
50
46
  private
51
47
 
48
+ def write_inventory(inventory)
49
+ InventoryWriter.new(inventory:, path:).write
50
+ FileUtils.cp(path / "inventory.json", object_directory.object_root)
51
+ FileUtils.cp(path / "inventory.json.sha512", object_directory.object_root)
52
+ end
53
+
52
54
  def copy_one(logical_file_path, incoming_path, destination_path)
53
55
  logical_file_path = File.join(destination_path, logical_file_path) unless destination_path.empty?
54
56
  add(incoming_path, logical_file_path:)
55
- parent_dir = (content_path + logical_file_path).parent
57
+ parent_dir = (content_path / logical_file_path).parent
56
58
  FileUtils.mkdir_p(parent_dir) unless parent_dir == content_path
57
- FileUtils.cp(incoming_path, content_path + logical_file_path)
59
+ FileUtils.cp(incoming_path, content_path / logical_file_path)
58
60
  end
59
61
 
60
62
  def add(incoming_path, logical_file_path: File.basename(incoming_path))
61
63
  digest = Digest::SHA512.file(incoming_path).to_s
62
64
  version_content_path = content_path.relative_path_from(object_directory.object_root)
63
- file_path_relative_to_root = (version_content_path + logical_file_path).to_s
65
+ file_path_relative_to_root = (version_content_path / logical_file_path).to_s
64
66
  @manifest[digest] = [file_path_relative_to_root]
65
67
  @state[digest] = [logical_file_path]
66
68
  end
@@ -81,11 +83,11 @@ module OCFL
81
83
  end
82
84
 
83
85
  def content_path
84
- path + "content"
86
+ path / object_directory.inventory.content_directory
85
87
  end
86
88
 
87
89
  def path
88
- object_directory.object_root + version_number
90
+ object_directory.object_root / version_number
89
91
  end
90
92
 
91
93
  def build_inventory
@@ -26,18 +26,25 @@ module OCFL
26
26
  attr_reader :errors, :data
27
27
 
28
28
  delegate :id, :head, :versions, :manifest, to: :data
29
+ delegate :state, to: :head_version
29
30
 
30
31
  def content_directory
31
32
  data.contentDirectory || "content"
32
33
  end
33
34
 
34
- # @returns [String] the path to the file relative to the object root. (e.g. v2/content/image.tiff)
35
+ # @return [String,nil] the path to the file relative to the object root. (e.g. v2/content/image.tiff)
35
36
  def path(logical_path)
36
- sha = versions[head].state.find { |_sha, file_names| file_names.include?(logical_path) }&.first
37
+ matching_paths = manifest.values.flatten.select do |path|
38
+ path.match(%r{\Av\d+/#{content_directory}/#{logical_path}\z})
39
+ end
37
40
 
38
- return unless sha
41
+ return if matching_paths.empty?
39
42
 
40
- manifest.fetch(sha).first
43
+ matching_paths.max_by { |path| path[/\d+/].to_i }
44
+ end
45
+
46
+ def head_version
47
+ versions[head]
41
48
  end
42
49
  end
43
50
  end
@@ -1,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "digest"
4
-
5
3
  module OCFL
6
4
  module Object
7
5
  # Checks to see that the inventory.json and it's checksum in a direcotory are valid
@@ -29,7 +27,7 @@ module OCFL
29
27
  end
30
28
 
31
29
  def inventory_checksum_file
32
- directory + "inventory.json.sha512"
30
+ directory / "inventory.json.sha512"
33
31
  end
34
32
 
35
33
  def inventory_file_checksum
@@ -37,9 +35,8 @@ module OCFL
37
35
  end
38
36
 
39
37
  def inventory_file
40
- directory + "inventory.json"
38
+ directory / "inventory.json"
41
39
  end
42
40
  end
43
- # rubocop:enable Style/StringConcatenation
44
41
  end
45
42
  end
@@ -1,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "digest"
4
-
5
3
  module OCFL
6
4
  module Object
7
5
  # Writes a OCFL Inventory to json on disk
@@ -23,11 +21,11 @@ module OCFL
23
21
  end
24
22
 
25
23
  def inventory_file
26
- path + "inventory.json"
24
+ path / "inventory.json"
27
25
  end
28
26
 
29
27
  def checksum_file
30
- path + "inventory.json.sha512"
28
+ path / "inventory.json.sha512"
31
29
  end
32
30
 
33
31
  def update_inventory_checksum
@@ -35,6 +33,5 @@ module OCFL
35
33
  File.write(checksum_file, "#{digest} inventory.json")
36
34
  end
37
35
  end
38
- # rubocop:enable Style/StringConcatenation
39
36
  end
40
37
  end
@@ -17,6 +17,10 @@ module OCFL
17
17
  attribute :state, Types::Hash.map(Types::String, Types::Array.of(Types::String))
18
18
  attribute? :message, Types::String
19
19
  attribute? :user, User
20
+
21
+ def file_names
22
+ state.values.flatten
23
+ end
20
24
  end
21
25
  end
22
26
  end
data/lib/ocfl/object.rb CHANGED
@@ -3,5 +3,7 @@
3
3
  module OCFL
4
4
  # An OCFL Object is a group of one or more content files and administrative information
5
5
  # https://ocfl.io/1.1/spec/#object-spec
6
- module Object; end
6
+ module Object
7
+ class FileNotFound < RuntimeError; end
8
+ end
7
9
  end
data/lib/ocfl/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module OCFL
4
- VERSION = "0.4.1"
4
+ VERSION = "0.5.0"
5
5
  end
data/lib/ocfl.rb CHANGED
@@ -1,12 +1,13 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "zeitwerk"
4
- require "json"
4
+ require "active_support"
5
+ require "active_support/core_ext/module/delegation"
6
+ require "digest"
5
7
  require "dry/monads"
6
8
  require "dry-schema"
7
9
  require "dry-struct"
8
- require "active_support"
9
- require "active_support/core_ext/module/delegation"
10
+ require "json"
10
11
 
11
12
  loader = Zeitwerk::Loader.for_gem
12
13
  loader.inflector.inflect("ocfl" => "OCFL")
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ocfl
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.4.1
4
+ version: 0.5.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Justin Coyne
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2024-05-07 00:00:00.000000000 Z
11
+ date: 2024-05-23 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activesupport