ro-crate 0.4.6 → 0.4.11

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 4880110308b68d8bc35333b1fc8a8396ea30e36069f92e059b8c57ec3567d33e
4
- data.tar.gz: 77a17a0436f2dea14254a3501f9db349eed5460851d94ab4e87adb7be83e1b38
3
+ metadata.gz: 62f3979b325743d31720f803dbf75690ad6b7b9bcf9f2cbf63d8200a2bb204ba
4
+ data.tar.gz: 3a229e06492a3f9a90721799f43929d6a5ca01fc0a0ff34d917eeebff037d9d4
5
5
  SHA512:
6
- metadata.gz: 711a45d7aaa63f8a5a7bba78e433d82875311472d6f4abfd6d388dd8565a7707036152c81de4059fd92da5b7851925caf9d808f39d115752e68eaddbdc85bac1
7
- data.tar.gz: ce14efaf63e7224adc79cdb11d27bdec933be290ccf60196014cb7ce9380136451265a8f814e1ca7b70286fc32852024d429334bbaab18208d31095cd619dc9e
6
+ metadata.gz: 242b8e326756d51ab583726db7fb94763286f2764bdf1f0523cd9dab5fa560fe4da3016a4a4b84b0cc6bcef4ec096aeae7dc2fd344ef6cb5ee045cf99a954ef9
7
+ data.tar.gz: 4abaece91d997ac398ee627f639fc98c924768c255bac1bb6c9ce7f43fc03f5b018a2fc657008863c65537592261e85f697ef2c7e200c20621ba41ff0a933541
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- ro-crate (0.4.6)
4
+ ro-crate (0.4.11)
5
5
  addressable (~> 2.7.0)
6
6
  rubyzip (~> 2.0.0)
7
7
 
@@ -12,19 +12,19 @@ GEM
12
12
  public_suffix (>= 2.0.2, < 5.0)
13
13
  crack (0.4.3)
14
14
  safe_yaml (~> 1.0.0)
15
- docile (1.3.1)
15
+ docile (1.3.5)
16
16
  hashdiff (1.0.1)
17
- json (2.3.1)
18
17
  power_assert (0.4.1)
19
18
  public_suffix (4.0.3)
20
19
  rake (13.0.0)
21
20
  rubyzip (2.0.0)
22
21
  safe_yaml (1.0.5)
23
- simplecov (0.16.1)
22
+ simplecov (0.21.2)
24
23
  docile (~> 1.1)
25
- json (>= 1.8, < 3)
26
- simplecov-html (~> 0.10.0)
27
- simplecov-html (0.10.2)
24
+ simplecov-html (~> 0.11)
25
+ simplecov_json_formatter (~> 0.1)
26
+ simplecov-html (0.12.3)
27
+ simplecov_json_formatter (0.1.2)
28
28
  test-unit (3.2.3)
29
29
  power_assert
30
30
  webmock (3.8.3)
@@ -39,7 +39,7 @@ PLATFORMS
39
39
  DEPENDENCIES
40
40
  rake (~> 13.0.0)
41
41
  ro-crate!
42
- simplecov (~> 0.16.1)
42
+ simplecov (~> 0.21.2)
43
43
  test-unit (~> 3.2.3)
44
44
  webmock (~> 3.8.3)
45
45
  yard (~> 0.9.25)
data/README.md CHANGED
@@ -17,6 +17,43 @@ and run `bundle install`.
17
17
 
18
18
  ## Usage
19
19
 
20
+ This gem consists a hierarchy of classes to model RO-Crate "entities": the crate itself, data entities
21
+ (files and directory) and contextual entities (with a limited set of specializations, such as `ROCrate::Person`).
22
+ They are all descendents of the `ROCrate::Entity` class, with the `ROCrate::Crate` class representing the crate itself.
23
+
24
+ The `ROCrate::Reader` class handles reading of RO-Crates into the above model, from a Zip file or directory.
25
+
26
+ The `ROCrate::Writer` class can write out an `ROCrate::Crate` instance into a Zip file or directory.
27
+
28
+ **Note:** for performance reasons, the gem is currently not linked-data aware and will allow you to set properties that
29
+ are not semantically valid.
30
+
31
+ ### Entities
32
+ Entities correspond to entries in the `@graph` of the RO-Crate's metadata JSON-LD file. Each entity class is
33
+ basically a wrapper around a set of JSON properties, with some convenience methods for getting/setting some
34
+ commonly used properties (`crate.name = "My first crate"`).
35
+
36
+ These convenience getter/setter methods will automatically handle turning objects into references and adding them to the
37
+ `@graph` if necessary.
38
+
39
+ ##### Getting/Setting Arbitrary Properties of Entities
40
+ As well as using the pre-defined getter/setter methods, you can get/set arbitrary properties like so.
41
+
42
+ To set the "creativeWorkStatus" property of the RO-Crate itself to a string literal:
43
+ ```ruby
44
+ crate['creativeWorkStatus'] = 'work-in-progress'
45
+ ```
46
+
47
+ If you want to reference other entities in the crate, you can get a JSON-LD reference from an entity object by using the `reference` method:
48
+ ```ruby
49
+ joe = crate.add_person('joe', { name: 'Joe Bloggs' }) # Add the entity to the @graph
50
+ crate['copyrightHolder'] = joe.reference # Reference the entity from the "copyrightHolder" property
51
+ ```
52
+ and to resolve those references back to the object, use the `dereference` method:
53
+ ```ruby
54
+ joe = crate['copyrightHolder'].dereference
55
+ ```
56
+
20
57
  ### Documentation
21
58
 
22
59
  [Click here for API documentation](https://www.researchobject.org/ro-crate-ruby/).
data/lib/ro_crate.rb CHANGED
@@ -14,6 +14,7 @@ require 'ro_crate/model/remote_entry'
14
14
  require 'ro_crate/model/entry'
15
15
  require 'ro_crate/model/directory'
16
16
  require 'ro_crate/model/metadata'
17
+ require 'ro_crate/model/preview_generator'
17
18
  require 'ro_crate/model/preview'
18
19
  require 'ro_crate/model/crate'
19
20
  require 'ro_crate/model/contextual_entity'
@@ -3,21 +3,9 @@ module ROCrate
3
3
  # A class to represent a "Contextual Entity" within an RO-Crate.
4
4
  # Contextual Entities are used to describe and provide context to the Data Entities within the crate.
5
5
  class ContextualEntity < Entity
6
- def self.format_id(id)
6
+ def self.format_local_id(id)
7
7
  i = super
8
- begin
9
- uri = URI(id)
10
- rescue ArgumentError
11
- uri = nil
12
- end
13
-
14
- if uri&.absolute?
15
- i
16
- elsif i.start_with?('#')
17
- i
18
- else
19
- "##{i}"
20
- end
8
+ i.start_with?('#') ? i : "##{i}"
21
9
  end
22
10
 
23
11
  ##
@@ -8,6 +8,11 @@ module ROCrate
8
8
  properties(%w[name datePublished author license identifier distribution contactPoint publisher description url hasPart])
9
9
 
10
10
  def self.format_id(id)
11
+ i = super(id)
12
+ i.end_with?('/') ? i : "#{i}/"
13
+ end
14
+
15
+ def self.format_local_id(id)
11
16
  return id if id == IDENTIFIER
12
17
  super
13
18
  end
@@ -163,6 +168,15 @@ module ROCrate
163
168
  @preview ||= ROCrate::Preview.new(self)
164
169
  end
165
170
 
171
+ ##
172
+ # Set the RO-Crate preview file
173
+ # @param preview [Preview] the preview to set.
174
+ #
175
+ # @return [Preview]
176
+ def preview=(preview)
177
+ @preview = claim(preview)
178
+ end
179
+
166
180
  ##
167
181
  # All the entities within the crate. Includes contextual entities, data entities, the crate itself and its metadata file.
168
182
  #
@@ -3,7 +3,9 @@ module ROCrate
3
3
  # A class to represent a "Data Entity" within an RO-Crate.
4
4
  # Data Entities are the actual physical files and directories within the Crate.
5
5
  class DataEntity < Entity
6
- def self.format_id(id)
6
+ properties(%w[name contentSize dateModified encodingFormat identifier sameAs author])
7
+
8
+ def self.format_local_id(id)
7
9
  super.chomp('/')
8
10
  end
9
11
 
@@ -13,8 +15,6 @@ module ROCrate
13
15
  # @return [Class]
14
16
  def self.specialize(props)
15
17
  type = props['@type']
16
- id = props['@id']
17
- abs = URI(id)&.absolute? rescue false
18
18
  type = [type] unless type.is_a?(Array)
19
19
  if type.include?('Dataset')
20
20
  ROCrate::Directory
@@ -2,9 +2,7 @@ module ROCrate
2
2
  ##
3
3
  # A data entity that represents a directory of potentially many files and subdirectories (or none).
4
4
  class Directory < DataEntity
5
- properties(%w[name contentSize dateModified encodingFormat identifier sameAs])
6
-
7
- def self.format_id(id)
5
+ def self.format_local_id(id)
8
6
  super + '/'
9
7
  end
10
8
 
@@ -29,12 +29,34 @@ module ROCrate
29
29
  end
30
30
 
31
31
  ##
32
- # Format the given ID with rules appropriate for this type.
32
+ # Format the given ID with rules appropriate for this type if it is local/relative, leave as-is if absolute.
33
+ #
34
+ # @param id [String] The candidate ID to be formatted.
35
+ # @return [String] The formatted ID.
36
+ def self.format_id(id)
37
+ begin
38
+ uri = URI(id)
39
+ rescue ArgumentError, URI::InvalidURIError
40
+ uri = nil
41
+ end
42
+
43
+ if uri&.absolute?
44
+ id
45
+ else
46
+ format_local_id(id)
47
+ end
48
+ end
49
+
50
+ ##
51
+ # Format the given local ID with rules appropriate for this type.
33
52
  # For example:
34
53
  # * contextual entities MUST be absolute URIs, or begin with: #
35
54
  # * files MUST NOT begin with ./
36
55
  # * directories MUST NOT begin with ./ (except for the crate itself), and MUST end with /
37
- def self.format_id(id)
56
+ #
57
+ # @param id [String] The candidate local ID to be formatted.
58
+ # @return [String] The formatted local ID.
59
+ def self.format_local_id(id)
38
60
  Addressable::URI.escape(id.sub(/\A\.\//, '')) # Remove initial ./ if present
39
61
  end
40
62
 
@@ -2,14 +2,12 @@ module ROCrate
2
2
  ##
3
3
  # A data entity that represents a single file.
4
4
  class File < DataEntity
5
- properties(%w[name contentSize dateModified encodingFormat identifier sameAs])
6
-
7
5
  ##
8
6
  # Create a new ROCrate::File. PLEASE NOTE, the new file will not be added to the crate. To do this, call
9
7
  # Crate#add_data_entity, or just use Crate#add_file.
10
8
  #
11
9
  # @param crate [Crate] The RO-Crate that owns this file.
12
- # @param source [String, Pathname, ::File, #read, URI, nil] The source on the disk (or on the internet if a URI) where this file will be read.
10
+ # @param source [String, Pathname, ::File, URI, nil, #read] The source on the disk (or on the internet if a URI) where this file will be read.
13
11
  # @param crate_path [String] The relative path within the RO-Crate where this file will be written.
14
12
  # @param properties [Hash{String => Object}] A hash of JSON-LD properties to associate with this file.
15
13
  def initialize(crate, source, crate_path = nil, properties = {})
@@ -17,7 +17,15 @@ module ROCrate
17
17
  # @return [String] The rendered JSON-LD as a "prettified" string.
18
18
  def generate
19
19
  graph = crate.entities.map(&:properties).reject(&:empty?)
20
- JSON.pretty_generate('@context' => CONTEXT, '@graph' => graph)
20
+ JSON.pretty_generate('@context' => context, '@graph' => graph)
21
+ end
22
+
23
+ def context
24
+ @context || CONTEXT
25
+ end
26
+
27
+ def context= c
28
+ @context = c
21
29
  end
22
30
 
23
31
  private
@@ -2,7 +2,7 @@ module ROCrate
2
2
  ##
3
3
  # A contextual entity that represents an organization.
4
4
  class Organization < ContextualEntity
5
- properties(['name'])
5
+ properties(%w[name])
6
6
 
7
7
  private
8
8
 
@@ -12,26 +12,14 @@ module ROCrate
12
12
  # @return [String]
13
13
  attr_accessor :template
14
14
 
15
- def initialize(crate, properties = {})
15
+ def initialize(crate, source = nil, properties = {})
16
+ source ||= PreviewGenerator.new(self)
16
17
  @template = nil
17
- super(crate, nil, IDENTIFIER, properties)
18
- end
19
-
20
- ##
21
- # Generate the crate's `ro-crate-preview.html`.
22
- # @return [String] The rendered HTML as a string.
23
- def generate
24
- b = crate.get_binding
25
- renderer = ERB.new(template || ::File.read(DEFAULT_TEMPLATE))
26
- renderer.result(b)
18
+ super(crate, source, IDENTIFIER, properties)
27
19
  end
28
20
 
29
21
  private
30
22
 
31
- def source
32
- Entry.new(StringIO.new(generate))
33
- end
34
-
35
23
  def default_properties
36
24
  {
37
25
  '@id' => IDENTIFIER,
@@ -0,0 +1,40 @@
1
+ require 'erb'
2
+
3
+ module ROCrate
4
+ ##
5
+ # A class to handle generation of an RO-Crate's preview HTML in an IO-like way (to fit into an Entry).
6
+ class PreviewGenerator
7
+ ##
8
+ # @param preview [Preview] The RO-Crate preview object.
9
+ def initialize(preview)
10
+ @preview = preview
11
+ end
12
+
13
+ def read(*args)
14
+ io.read(*args)
15
+ end
16
+
17
+ ##
18
+ # Generate the crate's `ro-crate-preview.html`.
19
+ # @return [String] The rendered HTML as a string.
20
+ def generate
21
+ b = crate.get_binding
22
+ renderer = ERB.new(template)
23
+ renderer.result(b)
24
+ end
25
+
26
+ def template
27
+ @preview.template || ::File.read(Preview::DEFAULT_TEMPLATE)
28
+ end
29
+
30
+ def crate
31
+ @preview.crate
32
+ end
33
+
34
+ private
35
+
36
+ def io
37
+ @io ||= StringIO.new(generate)
38
+ end
39
+ end
40
+ end
@@ -3,29 +3,67 @@ module ROCrate
3
3
  # A class to handle reading of RO-Crates from Zip files or directories.
4
4
  class Reader
5
5
  ##
6
- # Reads an RO-Crate from a directory of zip file.
6
+ # Reads an RO-Crate from a directory or zip file.
7
7
  #
8
- # @param source [String, ::File, Pathname] The source location for the crate.
8
+ # @param source [String, ::File, Pathname, #read] The location of the zip or directory, or an IO-like object containing a zip.
9
9
  # @param target_dir [String, ::File, Pathname] The target directory where the crate should be unzipped (if its a Zip file).
10
10
  # @return [Crate] The RO-Crate.
11
11
  def self.read(source, target_dir: Dir.mktmpdir)
12
12
  raise "Not a directory!" unless ::File.directory?(target_dir)
13
- if ::File.directory?(source)
13
+ begin
14
+ is_dir = ::File.directory?(source)
15
+ rescue TypeError
16
+ is_dir = false
17
+ end
18
+
19
+ if is_dir
14
20
  read_directory(source)
15
21
  else
16
22
  read_zip(source, target_dir: target_dir)
17
23
  end
18
24
  end
19
25
 
26
+ ##
27
+ # Extract the contents of the given Zip file/data to the given directory.
28
+ #
29
+ # @param source [String, ::File, Pathname, #read] The location of the zip file, or an IO-like object.
30
+ # @param target [String, ::File, Pathname] The target directory where the file should be unzipped.
31
+ def self.unzip_to(source, target)
32
+ source = Pathname.new(::File.expand_path(source)) if source.is_a?(String)
33
+
34
+ if source.is_a?(Pathname) || source.respond_to?(:path)
35
+ unzip_file_to(source, target)
36
+ else
37
+ unzip_io_to(source, target)
38
+ end
39
+ end
40
+
41
+ ##
42
+ # Extract the given Zip file data to the given directory.
43
+ #
44
+ # @param source [#read] An IO-like object containing a Zip file.
45
+ # @param target [String, ::File, Pathname] The target directory where the file should be unzipped.
46
+ def self.unzip_io_to(io, target)
47
+ Dir.chdir(target) do
48
+ Zip::InputStream.open(io) do |input|
49
+ while (entry = input.get_next_entry)
50
+ unless ::File.exist?(entry.name) || entry.name_is_directory?
51
+ FileUtils::mkdir_p(::File.dirname(entry.name))
52
+ ::File.binwrite(entry.name, input.read)
53
+ end
54
+ end
55
+ end
56
+ end
57
+ end
58
+
20
59
  ##
21
60
  # Extract the contents of the given Zip file to the given directory.
22
61
  #
23
62
  # @param source [String, ::File, Pathname] The location of the zip file.
24
63
  # @param target [String, ::File, Pathname] The target directory where the file should be unzipped.
25
- def self.unzip_to(source, target)
26
- source = ::File.expand_path(source)
64
+ def self.unzip_file_to(file_or_path, target)
27
65
  Dir.chdir(target) do
28
- Zip::File.open(source) do |zipfile|
66
+ Zip::File.open(file_or_path) do |zipfile|
29
67
  zipfile.each do |entry|
30
68
  unless ::File.exist?(entry.name)
31
69
  FileUtils::mkdir_p(::File.dirname(entry.name))
@@ -40,13 +78,16 @@ module ROCrate
40
78
  # Reads an RO-Crate from a zip file. It first extracts the Zip file to a temporary directory, and then calls
41
79
  # #read_directory.
42
80
  #
43
- # @param source [String, ::File, Pathname] The location of the zip file.
81
+ # @param source [String, ::File, Pathname, #read] The location of the zip file, or an IO-like object.
44
82
  # @param target_dir [String, ::File, Pathname] The target directory where the crate should be unzipped.
45
83
  # @return [Crate] The RO-Crate.
46
84
  def self.read_zip(source, target_dir: Dir.mktmpdir)
47
85
  unzip_to(source, target_dir)
48
86
 
49
- read_directory(target_dir)
87
+ # Traverse the unzipped directory to try and find the crate's root
88
+ root_dir = detect_root_directory(target_dir)
89
+
90
+ read_directory(root_dir)
50
91
  end
51
92
 
52
93
  ##
@@ -55,13 +96,19 @@ module ROCrate
55
96
  # @param source [String, ::File, Pathname] The location of the directory.
56
97
  # @return [Crate] The RO-Crate.
57
98
  def self.read_directory(source)
99
+ raise "Not a directory!" unless ::File.directory?(source)
100
+
58
101
  source = ::File.expand_path(source)
59
102
  metadata_file = Dir.entries(source).detect { |entry| entry == ROCrate::Metadata::IDENTIFIER ||
60
103
  entry == ROCrate::Metadata::IDENTIFIER_1_0 }
61
104
 
62
105
  if metadata_file
63
- entities = entities_from_metadata(::File.read(::File.join(source, metadata_file)))
64
- build_crate(entities, source)
106
+ metadata_json = ::File.read(::File.join(source, metadata_file))
107
+ metadata = JSON.parse(metadata_json)
108
+ entities = entities_from_metadata(metadata)
109
+ context = metadata['@context']
110
+
111
+ build_crate(entities, source, context: context)
65
112
  else
66
113
  raise 'No metadata found!'
67
114
  end
@@ -70,10 +117,9 @@ module ROCrate
70
117
  ##
71
118
  # Extracts all the entities from the @graph of the RO-Crate Metadata.
72
119
  #
73
- # @param metadata_json [String] A string containing the metadata JSON.
120
+ # @param metadata [Hash] A Hash containing the parsed metadata JSON.
74
121
  # @return [Hash{String => Hash}] A Hash of all the entities, mapped by their @id.
75
- def self.entities_from_metadata(metadata_json)
76
- metadata = JSON.parse(metadata_json)
122
+ def self.entities_from_metadata(metadata)
77
123
  graph = metadata['@graph']
78
124
 
79
125
  if graph
@@ -86,6 +132,7 @@ module ROCrate
86
132
  # Do some normalization...
87
133
  entities[ROCrate::Metadata::IDENTIFIER] = extract_metadata_entity(entities)
88
134
  raise "No metadata entity found in @graph!" unless entities[ROCrate::Metadata::IDENTIFIER]
135
+ entities[ROCrate::Preview::IDENTIFIER] = extract_preview_entity(entities)
89
136
  entities[ROCrate::Crate::IDENTIFIER] = extract_root_entity(entities)
90
137
  raise "No root entity (with @id: #{entities[ROCrate::Metadata::IDENTIFIER].dig('about', '@id')}) found in @graph!" unless entities[ROCrate::Crate::IDENTIFIER]
91
138
 
@@ -96,25 +143,49 @@ module ROCrate
96
143
  end
97
144
 
98
145
  ##
99
- # Create a crate from the given set of entities.
146
+ # Create and populate crate from the given set of entities.
100
147
  #
101
148
  # @param entity_hash [Hash{String => Hash}] A Hash containing all the entities in the @graph, mapped by their @id.
102
149
  # @param source [String, ::File, Pathname] The location of the RO-Crate being read.
150
+ # @param crate_class [Class] The class to use to instantiate the crate,
151
+ # useful if you have created a subclass of ROCrate::Crate that you want to use. (defaults to ROCrate::Crate).
152
+ # @param context [nil, String, Array, Hash] A custom JSON-LD @context (parsed), or nil to use default.
103
153
  # @return [Crate] The RO-Crate.
104
- def self.build_crate(entity_hash, source)
105
- ROCrate::Crate.new.tap do |crate|
154
+ def self.build_crate(entity_hash, source, crate_class: ROCrate::Crate, context:)
155
+ crate = initialize_crate(entity_hash, source, crate_class: crate_class, context: context)
156
+
157
+ extract_data_entities(crate, source, entity_hash).each do |entity|
158
+ crate.add_data_entity(entity)
159
+ end
160
+
161
+ # The remaining entities in the hash must be contextual.
162
+ extract_contextual_entities(crate, entity_hash).each do |entity|
163
+ crate.add_contextual_entity(entity)
164
+ end
165
+
166
+ crate
167
+ end
168
+
169
+ ##
170
+ # Initialize a crate from the given set of entities.
171
+ #
172
+ # @param entity_hash [Hash{String => Hash}] A Hash containing all the entities in the @graph, mapped by their @id.
173
+ # @param source [String, ::File, Pathname] The location of the RO-Crate being read.
174
+ # @param crate_class [Class] The class to use to instantiate the crate,
175
+ # useful if you have created a subclass of ROCrate::Crate that you want to use. (defaults to ROCrate::Crate).
176
+ # @param context [nil, String, Array, Hash] A custom JSON-LD @context (parsed), or nil to use default.
177
+ # @return [Crate] The RO-Crate.
178
+ def self.initialize_crate(entity_hash, source, crate_class: ROCrate::Crate, context:)
179
+ crate_class.new.tap do |crate|
106
180
  crate.properties = entity_hash.delete(ROCrate::Crate::IDENTIFIER)
107
181
  crate.metadata.properties = entity_hash.delete(ROCrate::Metadata::IDENTIFIER)
182
+ crate.metadata.context = context
108
183
  preview_properties = entity_hash.delete(ROCrate::Preview::IDENTIFIER)
109
- crate.preview.properties = preview_properties if preview_properties
110
- crate.add_all(source, false)
111
- extract_data_entities(crate, source, entity_hash).each do |entity|
112
- crate.add_data_entity(entity)
113
- end
114
- # The remaining entities in the hash must be contextual.
115
- extract_contextual_entities(crate, entity_hash).each do |entity|
116
- crate.add_contextual_entity(entity)
184
+ if preview_properties
185
+ preview_path = ::File.join(source, ROCrate::Preview::IDENTIFIER)
186
+ crate.preview = ROCrate::Preview.new(crate, ::File.exists?(preview_path) ? Pathname.new(preview_path) : nil, preview_properties)
117
187
  end
188
+ crate.add_all(source, false)
118
189
  end
119
190
  end
120
191
 
@@ -186,8 +257,8 @@ module ROCrate
186
257
  ##
187
258
  # Extract the metadata entity from the entity hash, according to the rules defined here:
188
259
  # https://www.researchobject.org/ro-crate/1.1/root-data-entity.html#finding-the-root-data-entity
189
- # @return [Hash{String => Hash}] A Hash containing (hopefully) one value, the metadata entity's properties,
190
- # mapped by its @id.
260
+ # @return [nil, Hash{String => Hash}] A Hash containing (hopefully) one value, the metadata entity's properties
261
+ # mapped by its @id, or nil if nothing is found.
191
262
  def self.extract_metadata_entity(entities)
192
263
  key = entities.detect do |_, props|
193
264
  props.dig('conformsTo', '@id')&.start_with?(ROCrate::Metadata::RO_CRATE_BASE)
@@ -202,6 +273,13 @@ module ROCrate
202
273
  entities.delete(ROCrate::Metadata::IDENTIFIER_1_0))
203
274
  end
204
275
 
276
+ ##
277
+ # Extract the ro-crate-preview entity from the entity hash.
278
+ # @return [Hash{String => Hash}] A Hash containing the preview entity's properties mapped by its @id, or nil if nothing is found.
279
+ def self.extract_preview_entity(entities)
280
+ entities.delete("./#{ROCrate::Preview::IDENTIFIER}") || entities.delete(ROCrate::Preview::IDENTIFIER)
281
+ end
282
+
205
283
  ##
206
284
  # Extract the root entity from the entity hash, according to the rules defined here:
207
285
  # https://www.researchobject.org/ro-crate/1.1/root-data-entity.html#finding-the-root-data-entity
@@ -212,5 +290,23 @@ module ROCrate
212
290
  raise "Metadata entity does not reference any root entity" unless root_id
213
291
  entities.delete(root_id)
214
292
  end
293
+
294
+ ##
295
+ # Finds an RO-Crate's root directory (where `ro-crate-metdata.json` is located) within a given directory.
296
+ #
297
+ # @param source [String, ::File, Pathname] The location of the directory.
298
+ # @return [Pathname, nil] The path to the root, or nil if not found.
299
+ def self.detect_root_directory(source)
300
+ Pathname(source).find do |entry|
301
+ if entry.file?
302
+ name = entry.basename.to_s
303
+ if name == ROCrate::Metadata::IDENTIFIER || name == ROCrate::Metadata::IDENTIFIER_1_0
304
+ return entry.parent
305
+ end
306
+ end
307
+ end
308
+
309
+ nil
310
+ end
215
311
  end
216
312
  end