ro-crate 0.4.11 → 0.4.15
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 +4 -4
- data/.github/workflows/tests.yml +5 -0
- data/.ruby-version +1 -1
- data/Gemfile.lock +7 -7
- data/README.md +2 -0
- data/lib/ro_crate/json_ld_hash.rb +5 -0
- data/lib/ro_crate/model/crate.rb +53 -6
- data/lib/ro_crate/model/data_entity.rb +20 -7
- data/lib/ro_crate/model/directory.rb +5 -5
- data/lib/ro_crate/model/entity.rb +43 -4
- data/lib/ro_crate/model/entry.rb +2 -2
- data/lib/ro_crate/model/file.rb +6 -2
- data/lib/ro_crate/model/remote_entry.rb +2 -13
- data/lib/ro_crate/reader.rb +11 -8
- data/lib/ro_crate/writer.rb +4 -4
- data/lib/ro_crate.rb +1 -1
- data/ro_crate.gemspec +2 -2
- data/test/crate_test.rb +74 -3
- data/test/directory_test.rb +21 -21
- data/test/entity_test.rb +117 -3
- data/test/fixtures/biobb_hpc_workflows-condapack.zip +0 -0
- data/test/fixtures/conflicting_data_directory/info.txt +1 -0
- data/test/fixtures/conflicting_data_directory/nested.txt +1 -0
- data/test/fixtures/misc_data_entity_crate/ro-crate-metadata.json +33 -0
- data/test/fixtures/ro-crate-galaxy-sortchangecase/ro-crate-metadata.json +10 -3
- data/test/fixtures/unlinked_entity_crate/LICENSE +176 -0
- data/test/fixtures/unlinked_entity_crate/README.md +2 -0
- data/test/fixtures/unlinked_entity_crate/ro-crate-metadata.json +150 -0
- data/test/fixtures/unlinked_entity_crate/sort-and-change-case.ga +118 -0
- data/test/fixtures/unlinked_entity_crate/test/test1/input.bed +3 -0
- data/test/fixtures/unlinked_entity_crate/test/test1/output_exp.bed +3 -0
- data/test/fixtures/unlinked_entity_crate/test/test1/sort-and-change-case-test.yml +8 -0
- data/test/fixtures/workflow-0.2.0/ro-crate-metadata.jsonld +5 -5
- data/test/fixtures/workflow-0.2.0.zip +0 -0
- data/test/reader_test.rb +86 -58
- data/test/test_helper.rb +5 -1
- data/test/writer_test.rb +48 -0
- metadata +24 -7
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 92f4b8cd765215082edea1be4e3c065a29ddcdbbc43a79576a4782ba8d797f07
|
4
|
+
data.tar.gz: d8936210124988edd8fd942c978a93ea6ee6f17fa1bcbfd3b6528e7475143ca1
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: cf88d846278c037fd4437db9f1cd5f3fd31d6901eeb161a38417a5919edaf9e013f28cfadea7a59bf77a687c5946f540ee6cfa28ccc001d473dac41eaf3c5f3e
|
7
|
+
data.tar.gz: ab8413b46ad23145ca6d68f2554cad6eacb9f2ecd349e890b6380295029da5426110e447c863170d997071a6dba3f76c72006ca6a90b9a456ac96858e114aa86
|
data/.github/workflows/tests.yml
CHANGED
@@ -3,6 +3,10 @@ on: [push, pull_request]
|
|
3
3
|
jobs:
|
4
4
|
run-tests:
|
5
5
|
runs-on: ubuntu-latest
|
6
|
+
strategy:
|
7
|
+
matrix:
|
8
|
+
ruby: ['2.6', '2.7']
|
9
|
+
fail-fast: false
|
6
10
|
steps:
|
7
11
|
- name: Checkout
|
8
12
|
uses: actions/checkout@v2.3.1 # If you're using actions/checkout@v2 you must set persist-credentials to false in most cases for the deployment to work correctly.
|
@@ -11,6 +15,7 @@ jobs:
|
|
11
15
|
- name: Setup Ruby
|
12
16
|
uses: ruby/setup-ruby@v1
|
13
17
|
with:
|
18
|
+
ruby-version: ${{ matrix.ruby }}
|
14
19
|
bundler-cache: true # runs 'bundle install' and caches installed gems automatically
|
15
20
|
- name: Run tests
|
16
21
|
run: bundle exec rake test
|
data/.ruby-version
CHANGED
@@ -1 +1 @@
|
|
1
|
-
ruby-2.
|
1
|
+
ruby-2.7.5
|
data/Gemfile.lock
CHANGED
@@ -1,21 +1,21 @@
|
|
1
1
|
PATH
|
2
2
|
remote: .
|
3
3
|
specs:
|
4
|
-
ro-crate (0.4.
|
5
|
-
addressable (
|
4
|
+
ro-crate (0.4.15)
|
5
|
+
addressable (>= 2.7, < 2.9)
|
6
6
|
rubyzip (~> 2.0.0)
|
7
7
|
|
8
8
|
GEM
|
9
9
|
remote: https://rubygems.org/
|
10
10
|
specs:
|
11
|
-
addressable (2.
|
11
|
+
addressable (2.8.0)
|
12
12
|
public_suffix (>= 2.0.2, < 5.0)
|
13
13
|
crack (0.4.3)
|
14
14
|
safe_yaml (~> 1.0.0)
|
15
15
|
docile (1.3.5)
|
16
16
|
hashdiff (1.0.1)
|
17
|
-
power_assert (
|
18
|
-
public_suffix (4.0.
|
17
|
+
power_assert (1.1.3)
|
18
|
+
public_suffix (4.0.6)
|
19
19
|
rake (13.0.0)
|
20
20
|
rubyzip (2.0.0)
|
21
21
|
safe_yaml (1.0.5)
|
@@ -25,7 +25,7 @@ GEM
|
|
25
25
|
simplecov_json_formatter (~> 0.1)
|
26
26
|
simplecov-html (0.12.3)
|
27
27
|
simplecov_json_formatter (0.1.2)
|
28
|
-
test-unit (3.2.
|
28
|
+
test-unit (3.2.9)
|
29
29
|
power_assert
|
30
30
|
webmock (3.8.3)
|
31
31
|
addressable (>= 2.3.6)
|
@@ -45,4 +45,4 @@ DEPENDENCIES
|
|
45
45
|
yard (~> 0.9.25)
|
46
46
|
|
47
47
|
BUNDLED WITH
|
48
|
-
|
48
|
+
2.3.6
|
data/README.md
CHANGED
@@ -1,5 +1,7 @@
|
|
1
1
|
# ro-crate-ruby
|
2
2
|
|
3
|
+

|
4
|
+
|
3
5
|
This is a WIP gem for creating, manipulating and reading RO-Crates (conforming to version 1.1 of the specification).
|
4
6
|
|
5
7
|
* RO-Crate - https://researchobject.github.io/ro-crate/
|
@@ -2,6 +2,11 @@ module ROCrate
|
|
2
2
|
##
|
3
3
|
# A wrapper class for Hash that adds methods to dereference Entities within an RO-Crate.
|
4
4
|
class JSONLDHash < ::Hash
|
5
|
+
def self.[](graph, content = {})
|
6
|
+
@graph = graph
|
7
|
+
super(stringified(content))
|
8
|
+
end
|
9
|
+
|
5
10
|
def initialize(graph, content = {})
|
6
11
|
@graph = graph
|
7
12
|
super()
|
data/lib/ro_crate/model/crate.rb
CHANGED
@@ -25,6 +25,15 @@ module ROCrate
|
|
25
25
|
super(self, nil, id, properties)
|
26
26
|
end
|
27
27
|
|
28
|
+
##
|
29
|
+
# Lookup an Entity using the given ID (in this Entity's crate).
|
30
|
+
#
|
31
|
+
# @param id [String] The ID to query.
|
32
|
+
# @return [Entity, nil]
|
33
|
+
def dereference(id)
|
34
|
+
entities.detect { |e| e.canonical_id == crate.resolve_id(id) } if id
|
35
|
+
end
|
36
|
+
|
28
37
|
##
|
29
38
|
# Create a new file and add it to the crate.
|
30
39
|
#
|
@@ -229,32 +238,70 @@ module ROCrate
|
|
229
238
|
entity.class.new(crate, entity.id, entity.raw_properties)
|
230
239
|
end
|
231
240
|
|
232
|
-
alias_method :
|
241
|
+
alias_method :own_payload, :payload
|
233
242
|
##
|
234
|
-
#
|
235
|
-
# key is the
|
243
|
+
# The file payload of the RO-Crate - a map of all the files/directories contained in the RO-Crate, where the
|
244
|
+
# key is the path relative to the crate's root, and the value is an Entry where the source data can be read.
|
236
245
|
#
|
237
246
|
# @return [Hash{String => Entry}>]
|
238
|
-
def
|
247
|
+
def payload
|
239
248
|
# Gather a map of entries, starting from the crate itself, then any directory data entities, then finally any
|
240
249
|
# file data entities. This ensures in the case of a conflict, the more "specific" data entities take priority.
|
241
|
-
entries =
|
250
|
+
entries = own_payload
|
242
251
|
non_self_entities = default_entities.reject { |e| e == self }
|
243
252
|
sorted_entities = (non_self_entities | data_entities).sort_by { |e| e.is_a?(ROCrate::Directory) ? 0 : 1 }
|
244
253
|
|
245
254
|
sorted_entities.each do |entity|
|
246
|
-
entity.
|
255
|
+
entity.payload.each do |path, entry|
|
247
256
|
entries[path] = entry
|
248
257
|
end
|
249
258
|
end
|
250
259
|
|
251
260
|
entries
|
252
261
|
end
|
262
|
+
alias_method :entries, :payload
|
253
263
|
|
254
264
|
def get_binding
|
255
265
|
binding
|
256
266
|
end
|
257
267
|
|
268
|
+
##
|
269
|
+
# Remove the entity from the RO-Crate.
|
270
|
+
#
|
271
|
+
# @param entity [Entity, String] The entity or ID of an entity to remove from the crate.
|
272
|
+
# @param remove_orphaned [Boolean] Should linked contextual entities also be removed from the crate they are left
|
273
|
+
# dangling (nothing else is linked to them)?
|
274
|
+
#
|
275
|
+
# @return [Entity, nil] The entity that was deleted, or nil if nothing was deleted.
|
276
|
+
def delete(entity, remove_orphaned: true)
|
277
|
+
entity = dereference(entity) if entity.is_a?(String)
|
278
|
+
return unless entity
|
279
|
+
|
280
|
+
deleted = data_entities.delete(entity) || contextual_entities.delete(entity)
|
281
|
+
|
282
|
+
if deleted && remove_orphaned
|
283
|
+
crate_entities = crate.linked_entities(deep: true)
|
284
|
+
to_remove = (entity.linked_entities(deep: true) - crate_entities)
|
285
|
+
to_remove.each(&:delete)
|
286
|
+
end
|
287
|
+
|
288
|
+
deleted
|
289
|
+
end
|
290
|
+
|
291
|
+
##
|
292
|
+
# Remove any contextual entities that are not linked from any other entity.
|
293
|
+
# Optionally takes a block to decide whether the given entity should be removed or not, otherwise removes all
|
294
|
+
# unlinked entities.
|
295
|
+
# @yieldparam [ContextualEntity] entity An unlinked contextual entity.
|
296
|
+
# @yieldparam [Boolean] remove Should this entity be removed?
|
297
|
+
#
|
298
|
+
# @return [Array<ContextualEntity>] The entities that were removed.
|
299
|
+
def gc(&block)
|
300
|
+
unlinked_entities = contextual_entities - metadata.linked_entities(deep: true)
|
301
|
+
|
302
|
+
unlinked_entities.select(&block).each { |e| e.delete(remove_orphaned: false) }
|
303
|
+
end
|
304
|
+
|
258
305
|
private
|
259
306
|
|
260
307
|
def full_entry_path(relative_path)
|
@@ -5,10 +5,6 @@ module ROCrate
|
|
5
5
|
class DataEntity < Entity
|
6
6
|
properties(%w[name contentSize dateModified encodingFormat identifier sameAs author])
|
7
7
|
|
8
|
-
def self.format_local_id(id)
|
9
|
-
super.chomp('/')
|
10
|
-
end
|
11
|
-
|
12
8
|
##
|
13
9
|
# Return an appropriate specialization of DataEntity for the given properties.
|
14
10
|
# @param props [Hash] Set of properties to try and infer the type from.
|
@@ -18,18 +14,35 @@ module ROCrate
|
|
18
14
|
type = [type] unless type.is_a?(Array)
|
19
15
|
if type.include?('Dataset')
|
20
16
|
ROCrate::Directory
|
21
|
-
|
17
|
+
elsif type.include?('File')
|
22
18
|
ROCrate::File
|
19
|
+
else
|
20
|
+
self
|
23
21
|
end
|
24
22
|
end
|
25
23
|
|
26
24
|
##
|
27
|
-
#
|
25
|
+
# Create a new ROCrate::DataEntity. This entity represents something that is neither a file or directory, but
|
26
|
+
# still constitutes part of the crate.
|
27
|
+
# PLEASE NOTE, the new data entity will not be added to the crate. To do this, call Crate#add_data_entity.
|
28
|
+
#
|
29
|
+
# @param crate [Crate] The RO-Crate that owns this directory.
|
30
|
+
# @param source [nil] Ignored. For compatibility with the File and Directory constructor signatures.
|
31
|
+
# @param id [String, nil] An ID to identify this DataEntity, or nil to auto-generate an appropriate one,
|
32
|
+
# (or determine via the properties param)
|
33
|
+
# @param properties [Hash{String => Object}] A hash of JSON-LD properties to associate with this DataEntity.
|
34
|
+
def initialize(crate, source = nil, id = nil, properties = {})
|
35
|
+
super(crate, id, properties)
|
36
|
+
end
|
37
|
+
|
38
|
+
##
|
39
|
+
# The payload of all the files/directories associated with this DataEntity, mapped by their relative file path.
|
28
40
|
#
|
29
41
|
# @return [Hash{String => Entry}>] The key is the location within the crate, and the value is an Entry.
|
30
|
-
def
|
42
|
+
def payload
|
31
43
|
{}
|
32
44
|
end
|
45
|
+
alias_method :entries, :payload
|
33
46
|
|
34
47
|
##
|
35
48
|
# A disk-safe filepath based on the ID of this DataEntity.
|
@@ -3,11 +3,11 @@ module ROCrate
|
|
3
3
|
# A data entity that represents a directory of potentially many files and subdirectories (or none).
|
4
4
|
class Directory < DataEntity
|
5
5
|
def self.format_local_id(id)
|
6
|
-
super + '/'
|
6
|
+
super.chomp('/') + '/'
|
7
7
|
end
|
8
8
|
|
9
9
|
##
|
10
|
-
# Create a new Directory. PLEASE NOTE, the new directory will not be added to the crate. To do this, call
|
10
|
+
# Create a new ROCrate::Directory. PLEASE NOTE, the new directory will not be added to the crate. To do this, call
|
11
11
|
# Crate#add_data_entity, or just use Crate#add_directory.
|
12
12
|
#
|
13
13
|
# @param crate [Crate] The RO-Crate that owns this directory.
|
@@ -24,15 +24,15 @@ module ROCrate
|
|
24
24
|
crate_path = source_directory.basename.to_s if crate_path.nil?
|
25
25
|
end
|
26
26
|
|
27
|
-
super(crate, crate_path, properties)
|
27
|
+
super(crate, nil, crate_path, properties)
|
28
28
|
end
|
29
29
|
|
30
30
|
##
|
31
|
-
# The
|
31
|
+
# The payload of this directory - a map of all the files/directories, where the key is the destination path
|
32
32
|
# within the crate and the value is an Entry where the source data can be read.
|
33
33
|
#
|
34
34
|
# @return [Hash{String => Entry}>]
|
35
|
-
def
|
35
|
+
def payload
|
36
36
|
entries = {}
|
37
37
|
entries[filepath.chomp('/')] = @entry if @entry
|
38
38
|
|
@@ -129,16 +129,26 @@ module ROCrate
|
|
129
129
|
# @param id [String] The ID to query.
|
130
130
|
# @return [Entity, nil]
|
131
131
|
def dereference(id)
|
132
|
-
crate.
|
132
|
+
crate.dereference(id)
|
133
133
|
end
|
134
|
-
|
135
134
|
alias_method :get, :dereference
|
136
135
|
|
136
|
+
##
|
137
|
+
# Remove this entity from the RO-Crate.
|
138
|
+
#
|
139
|
+
# @param remove_orphaned [Boolean] Should linked contextual entities also be removed from the crate (if nothing else is linked to them)?
|
140
|
+
#
|
141
|
+
# @return [Entity, nil] This entity, or nil if nothing was deleted.
|
142
|
+
def delete(remove_orphaned: true)
|
143
|
+
crate.delete(self, remove_orphaned: remove_orphaned)
|
144
|
+
end
|
145
|
+
|
137
146
|
def id
|
138
147
|
@properties['@id']
|
139
148
|
end
|
140
149
|
|
141
150
|
def id=(id)
|
151
|
+
@canonical_id = nil
|
142
152
|
@properties['@id'] = self.class.format_id(id)
|
143
153
|
end
|
144
154
|
|
@@ -190,13 +200,13 @@ module ROCrate
|
|
190
200
|
#
|
191
201
|
# @return [Addressable::URI]
|
192
202
|
def canonical_id
|
193
|
-
crate.resolve_id(id)
|
203
|
+
@canonical_id ||= crate.resolve_id(id)
|
194
204
|
end
|
195
205
|
|
196
206
|
##
|
197
207
|
# Is this entity local to the crate or an external reference?
|
198
208
|
#
|
199
|
-
# @return [
|
209
|
+
# @return [Boolean]
|
200
210
|
def external?
|
201
211
|
crate.canonical_id.host != canonical_id.host
|
202
212
|
end
|
@@ -226,6 +236,35 @@ module ROCrate
|
|
226
236
|
@properties.has_type?(type)
|
227
237
|
end
|
228
238
|
|
239
|
+
##
|
240
|
+
# Gather a list of entities linked to this one through its properties.
|
241
|
+
# @param deep [Boolean] If false, only consider direct links, otherwise consider transitive links.
|
242
|
+
# @param linked [Hash{String => Entity}] Discovered entities, mapped by their ID, to avoid loops when recursing.
|
243
|
+
# @return [Array<Entity>]
|
244
|
+
def linked_entities(deep: false, linked: {})
|
245
|
+
properties.each_key do |key|
|
246
|
+
value = properties[key] # We're doing this to call the JSONLDHash#[] method which wraps
|
247
|
+
value = [value] if value.is_a?(JSONLDHash)
|
248
|
+
|
249
|
+
if value.is_a?(Array)
|
250
|
+
value.each do |v|
|
251
|
+
if v.is_a?(JSONLDHash) && !linked.key?(v['@id'])
|
252
|
+
entity = v.dereference
|
253
|
+
next unless entity
|
254
|
+
linked[entity.id] = entity
|
255
|
+
if deep
|
256
|
+
entity.linked_entities(deep: true, linked: linked).each do |e|
|
257
|
+
linked[e.id] = e
|
258
|
+
end
|
259
|
+
end
|
260
|
+
end
|
261
|
+
end
|
262
|
+
end
|
263
|
+
end
|
264
|
+
|
265
|
+
linked.values.compact
|
266
|
+
end
|
267
|
+
|
229
268
|
private
|
230
269
|
|
231
270
|
def default_properties
|
data/lib/ro_crate/model/entry.rb
CHANGED
@@ -14,10 +14,10 @@ module ROCrate
|
|
14
14
|
end
|
15
15
|
|
16
16
|
##
|
17
|
-
# Write the source to the destination via a buffer.
|
17
|
+
# Write the entry's source to the destination via a buffer.
|
18
18
|
#
|
19
19
|
# @param dest [#write] An IO-like destination to write to.
|
20
|
-
def
|
20
|
+
def write_to(dest)
|
21
21
|
input = source
|
22
22
|
input = input.open('rb') if input.is_a?(Pathname)
|
23
23
|
while (buff = input.read(4096))
|
data/lib/ro_crate/model/file.rb
CHANGED
@@ -2,6 +2,10 @@ module ROCrate
|
|
2
2
|
##
|
3
3
|
# A data entity that represents a single file.
|
4
4
|
class File < DataEntity
|
5
|
+
def self.format_local_id(id)
|
6
|
+
super.chomp('/')
|
7
|
+
end
|
8
|
+
|
5
9
|
##
|
6
10
|
# Create a new ROCrate::File. PLEASE NOTE, the new file will not be added to the crate. To do this, call
|
7
11
|
# Crate#add_data_entity, or just use Crate#add_file.
|
@@ -32,7 +36,7 @@ module ROCrate
|
|
32
36
|
crate_path = source.to_s if source.is_a?(URI) && source.absolute?
|
33
37
|
end
|
34
38
|
|
35
|
-
super(crate, crate_path, properties)
|
39
|
+
super(crate, nil, crate_path, properties)
|
36
40
|
|
37
41
|
if source.is_a?(URI) && source.absolute?
|
38
42
|
@entry = RemoteEntry.new(source)
|
@@ -56,7 +60,7 @@ module ROCrate
|
|
56
60
|
# (for compatibility with Directory#entries)
|
57
61
|
#
|
58
62
|
# @return [Hash{String => Entry}>] The key is the location within the crate, and the value is an Entry.
|
59
|
-
def
|
63
|
+
def payload
|
60
64
|
remote? ? {} : { filepath => source }
|
61
65
|
end
|
62
66
|
|
@@ -2,7 +2,7 @@ module ROCrate
|
|
2
2
|
##
|
3
3
|
# A class to represent a reference within an RO-Crate, to a remote file held on the internet somewhere.
|
4
4
|
# It handles the actual reading/writing of bytes.
|
5
|
-
class RemoteEntry
|
5
|
+
class RemoteEntry < Entry
|
6
6
|
attr_reader :uri
|
7
7
|
|
8
8
|
##
|
@@ -13,22 +13,11 @@ module ROCrate
|
|
13
13
|
@uri = uri
|
14
14
|
end
|
15
15
|
|
16
|
-
def write(dest)
|
17
|
-
raise 'Cannot write to a remote entry!'
|
18
|
-
end
|
19
|
-
|
20
|
-
##
|
21
|
-
# Read from the source.
|
22
|
-
#
|
23
|
-
def read
|
24
|
-
source.read
|
25
|
-
end
|
26
|
-
|
27
16
|
##
|
28
17
|
# @return [IO] An IO object for the remote resource.
|
29
18
|
#
|
30
19
|
def source
|
31
|
-
open
|
20
|
+
uri.open
|
32
21
|
end
|
33
22
|
|
34
23
|
##
|
data/lib/ro_crate/reader.rb
CHANGED
@@ -181,9 +181,10 @@ module ROCrate
|
|
181
181
|
crate.metadata.properties = entity_hash.delete(ROCrate::Metadata::IDENTIFIER)
|
182
182
|
crate.metadata.context = context
|
183
183
|
preview_properties = entity_hash.delete(ROCrate::Preview::IDENTIFIER)
|
184
|
-
|
185
|
-
|
186
|
-
|
184
|
+
preview_path = ::File.join(source, ROCrate::Preview::IDENTIFIER)
|
185
|
+
preview_path = ::File.exists?(preview_path) ? Pathname.new(preview_path) : nil
|
186
|
+
if preview_properties || preview_path
|
187
|
+
crate.preview = ROCrate::Preview.new(crate, preview_path, preview_properties || {})
|
187
188
|
end
|
188
189
|
crate.add_all(source, false)
|
189
190
|
end
|
@@ -244,10 +245,10 @@ module ROCrate
|
|
244
245
|
fullpath = ::File.join(source, i)
|
245
246
|
path = Pathname.new(fullpath) if ::File.exist?(fullpath)
|
246
247
|
end
|
247
|
-
unless path
|
248
|
-
|
249
|
-
|
250
|
-
end
|
248
|
+
# unless path
|
249
|
+
# warn "Missing file/directory: #{id}, skipping..."
|
250
|
+
# return nil
|
251
|
+
# end
|
251
252
|
end
|
252
253
|
|
253
254
|
entity_class.new(crate, path, decoded_id, entity_props)
|
@@ -261,7 +262,9 @@ module ROCrate
|
|
261
262
|
# mapped by its @id, or nil if nothing is found.
|
262
263
|
def self.extract_metadata_entity(entities)
|
263
264
|
key = entities.detect do |_, props|
|
264
|
-
props
|
265
|
+
conforms = props['conformsTo']
|
266
|
+
conforms = [conforms] unless conforms.is_a?(Array)
|
267
|
+
conforms.compact.any? { |c| c['@id']&.start_with?(ROCrate::Metadata::RO_CRATE_BASE) }
|
265
268
|
end&.first
|
266
269
|
|
267
270
|
return entities.delete(key) if key
|
data/lib/ro_crate/writer.rb
CHANGED
@@ -16,14 +16,14 @@ module ROCrate
|
|
16
16
|
# @param overwrite [Boolean] Whether or not to overwrite existing files.
|
17
17
|
def write(dir, overwrite: true)
|
18
18
|
FileUtils.mkdir_p(dir) # Make any parent directories
|
19
|
-
@crate.
|
19
|
+
@crate.payload.each do |path, entry|
|
20
20
|
fullpath = ::File.join(dir, path)
|
21
21
|
next if !overwrite && ::File.exist?(fullpath)
|
22
22
|
next if entry.directory?
|
23
23
|
FileUtils.mkdir_p(::File.dirname(fullpath))
|
24
24
|
temp = Tempfile.new('ro-crate-temp')
|
25
25
|
begin
|
26
|
-
entry.
|
26
|
+
entry.write_to(temp)
|
27
27
|
temp.close
|
28
28
|
FileUtils.mv(temp, fullpath)
|
29
29
|
ensure
|
@@ -38,9 +38,9 @@ module ROCrate
|
|
38
38
|
# @param destination [String, ::File] The destination where to write the RO-Crate zip.
|
39
39
|
def write_zip(destination)
|
40
40
|
Zip::File.open(destination, Zip::File::CREATE) do |zip|
|
41
|
-
@crate.
|
41
|
+
@crate.payload.each do |path, entry|
|
42
42
|
next if entry.directory?
|
43
|
-
zip.get_output_stream(path) { |s| entry.
|
43
|
+
zip.get_output_stream(path) { |s| entry.write_to(s) }
|
44
44
|
end
|
45
45
|
end
|
46
46
|
end
|
data/lib/ro_crate.rb
CHANGED
@@ -10,8 +10,8 @@ require 'ro_crate/json_ld_hash'
|
|
10
10
|
require 'ro_crate/model/entity'
|
11
11
|
require 'ro_crate/model/data_entity'
|
12
12
|
require 'ro_crate/model/file'
|
13
|
-
require 'ro_crate/model/remote_entry'
|
14
13
|
require 'ro_crate/model/entry'
|
14
|
+
require 'ro_crate/model/remote_entry'
|
15
15
|
require 'ro_crate/model/directory'
|
16
16
|
require 'ro_crate/model/metadata'
|
17
17
|
require 'ro_crate/model/preview_generator'
|
data/ro_crate.gemspec
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
Gem::Specification.new do |s|
|
2
2
|
s.name = 'ro-crate'
|
3
|
-
s.version = '0.4.
|
3
|
+
s.version = '0.4.15'
|
4
4
|
s.summary = 'Create, manipulate, read RO-Crates.'
|
5
5
|
s.authors = ['Finn Bacall']
|
6
6
|
s.email = 'finn.bacall@manchester.ac.uk'
|
@@ -8,7 +8,7 @@ Gem::Specification.new do |s|
|
|
8
8
|
s.homepage = 'https://github.com/ResearchObject/ro-crate-ruby'
|
9
9
|
s.require_paths = ['lib']
|
10
10
|
s.licenses = ['MIT']
|
11
|
-
s.add_runtime_dependency 'addressable', '
|
11
|
+
s.add_runtime_dependency 'addressable', '>= 2.7', '< 2.9'
|
12
12
|
s.add_runtime_dependency 'rubyzip', '~> 2.0.0'
|
13
13
|
s.add_development_dependency 'rake', '~> 13.0.0'
|
14
14
|
s.add_development_dependency 'test-unit', '~> 3.2.3'
|
data/test/crate_test.rb
CHANGED
@@ -215,7 +215,7 @@ class CrateTest < Test::Unit::TestCase
|
|
215
215
|
crate = ROCrate::Crate.new
|
216
216
|
entities = crate.add_all(fixture_file('directory').path, include_hidden: true)
|
217
217
|
|
218
|
-
paths = crate.
|
218
|
+
paths = crate.payload.keys
|
219
219
|
assert_equal 11, paths.length
|
220
220
|
assert_includes paths, 'data'
|
221
221
|
assert_includes paths, 'root.txt'
|
@@ -250,7 +250,7 @@ class CrateTest < Test::Unit::TestCase
|
|
250
250
|
|
251
251
|
assert_empty entities
|
252
252
|
|
253
|
-
paths = crate.
|
253
|
+
paths = crate.payload.keys
|
254
254
|
assert_equal 11, paths.length
|
255
255
|
assert_includes paths, 'data'
|
256
256
|
assert_includes paths, 'root.txt'
|
@@ -278,7 +278,7 @@ class CrateTest < Test::Unit::TestCase
|
|
278
278
|
crate = ROCrate::Crate.new
|
279
279
|
entities = crate.add_all(fixture_file('directory').path)
|
280
280
|
|
281
|
-
paths = crate.
|
281
|
+
paths = crate.payload.keys
|
282
282
|
assert_equal 8, paths.length
|
283
283
|
assert_includes paths, 'data'
|
284
284
|
assert_includes paths, 'root.txt'
|
@@ -305,4 +305,75 @@ class CrateTest < Test::Unit::TestCase
|
|
305
305
|
|
306
306
|
assert_equal "5678\n", crate.dereference('data/info.txt').source.read
|
307
307
|
end
|
308
|
+
|
309
|
+
test 'can delete entities' do
|
310
|
+
crate = ROCrate::Crate.new
|
311
|
+
file = crate.add_file(StringIO.new(''), 'file')
|
312
|
+
person = crate.add_person('#bob', { name: 'Bob' })
|
313
|
+
file.author = person
|
314
|
+
|
315
|
+
assert crate.delete(file)
|
316
|
+
assert_not_include crate.entities, file
|
317
|
+
assert_not_include crate.entities, person
|
318
|
+
end
|
319
|
+
|
320
|
+
test 'can delete entities by id' do
|
321
|
+
crate = ROCrate::Crate.new
|
322
|
+
file = crate.add_file(StringIO.new(''), 'file')
|
323
|
+
person = crate.add_person('#bob', { name: 'Bob' })
|
324
|
+
file.author = person
|
325
|
+
|
326
|
+
assert crate.delete('file')
|
327
|
+
assert_not_include crate.entities, file
|
328
|
+
assert_not_include crate.entities, person
|
329
|
+
end
|
330
|
+
|
331
|
+
test 'legacy entries method still returns same result as payload' do
|
332
|
+
crate = ROCrate::Crate.new
|
333
|
+
crate.add_all(fixture_file('directory').path)
|
334
|
+
|
335
|
+
paths = crate.entries.keys
|
336
|
+
assert_equal 8, paths.length
|
337
|
+
assert_includes paths, 'data'
|
338
|
+
assert_includes paths, 'root.txt'
|
339
|
+
assert_includes paths, 'info.txt'
|
340
|
+
assert_includes paths, 'data/binary.jpg'
|
341
|
+
assert_includes paths, 'data/info.txt'
|
342
|
+
assert_includes paths, 'data/nested.txt'
|
343
|
+
assert_not_includes paths, '.dotfile'
|
344
|
+
assert_not_includes paths, '.dir'
|
345
|
+
assert_not_includes paths, '.dir/test.txt'
|
346
|
+
assert_includes paths, 'ro-crate-metadata.json'
|
347
|
+
assert_includes paths, 'ro-crate-preview.html'
|
348
|
+
|
349
|
+
assert_equal crate.payload.keys, crate.entries.keys
|
350
|
+
end
|
351
|
+
|
352
|
+
test 'can garbage collect unlinked entities' do
|
353
|
+
crate = ROCrate::Reader.read(fixture_file('unlinked_entity_crate').path)
|
354
|
+
|
355
|
+
unlinked = crate.gc
|
356
|
+
assert_equal 2, unlinked.length
|
357
|
+
ids = unlinked.map(&:id)
|
358
|
+
assert_includes ids, '#joe'
|
359
|
+
assert_includes ids, '#joehouse'
|
360
|
+
assert_not_includes crate.contextual_entities, unlinked.first
|
361
|
+
assert_not_includes crate.contextual_entities, unlinked.last
|
362
|
+
assert_nil crate.get('#joe')
|
363
|
+
assert_nil crate.get('#joehouse')
|
364
|
+
end
|
365
|
+
|
366
|
+
test 'can conditionally garbage collect unlinked entities using a block' do
|
367
|
+
crate = ROCrate::Reader.read(fixture_file('unlinked_entity_crate').path)
|
368
|
+
|
369
|
+
unlinked = crate.gc do |entity|
|
370
|
+
entity.is_a?(ROCrate::Person)
|
371
|
+
end
|
372
|
+
assert_equal 1, unlinked.length
|
373
|
+
ids = unlinked.map(&:id)
|
374
|
+
assert_includes ids, '#joe'
|
375
|
+
assert_not_includes crate.contextual_entities, unlinked.first
|
376
|
+
assert_nil crate.get('#joe')
|
377
|
+
assert crate.get('#joehouse')
|
378
|
+
end
|
308
379
|
end
|