ro-crate 0.4.6 → 0.4.11

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: 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