ro-crate 0.4.11 → 0.4.15
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
+
![Tests](https://github.com/ResearchObject/ro-crate-ruby/actions/workflows/tests.yml/badge.svg)
|
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
|