ro-crate 0.5.1 → 0.5.3

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: 28049e56f761e9b8684970817cc4eace0a60c78aee05b6a16688e589a2088357
4
- data.tar.gz: 3e731b4c6039aa74ea34ab7d672fa665b99d4d030598a16ac593877cb0bf306f
3
+ metadata.gz: 987f4bf5543890ca23264eaa771134f6ec2ba848a84ea32424cf951429141523
4
+ data.tar.gz: f05c5166d95be3cec706fbb5d550c5af916f5f4c444112575ec49d5fe961e12a
5
5
  SHA512:
6
- metadata.gz: ac8be2df8a04041f7cafc51e04c2ee67d0e25599b1c7a16716aa0b61a4db132b5fb1e55a1bef48b7c6c83aea6bfaba1826a1ac294f58e3092a35438e1938433c
7
- data.tar.gz: 13a3780ee9ff9c85ac6829b0f93e393df3917e9286468c54cd127d1fdba704d14ced069fafb7bcb76378c6b120c6598e431e905059983f1dcec704efefc038cf
6
+ metadata.gz: '0848a81d03d8cdf7b4809de8613d90b67950f4339ae20ab5ce4ba6fa8d9e9559033c4a53ca47391b99ed5287ba7a2f2be7976b87c7ea8479051b8e7dfc9da6e8'
7
+ data.tar.gz: c1fc1ffdadafd04c19d81c37befec9eed9bcf9e40db9c39158b20a2a8ec8b8368841c65211d31439ba585f637de9541e2b7d58673da4964c7b8afccbfb9bf6ff
@@ -5,7 +5,7 @@ jobs:
5
5
  runs-on: ubuntu-latest
6
6
  strategy:
7
7
  matrix:
8
- ruby: ['2.6', '2.7', '3.0', '3.1']
8
+ ruby: ['2.6', '2.7', '3.0', '3.1', '3.2', '3.3']
9
9
  fail-fast: false
10
10
  steps:
11
11
  - name: Checkout
data/.gitignore CHANGED
@@ -48,3 +48,5 @@ build-iPhoneSimulator/
48
48
 
49
49
  # unless supporting rvm < 1.11.0 or doing something fancy, ignore this:
50
50
  .rvmrc
51
+
52
+ Gemfile.lock
data/.ruby-version CHANGED
@@ -1 +1 @@
1
- ruby-3.0.4
1
+ ruby-3.2.5
@@ -2,11 +2,6 @@ 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
-
10
5
  def initialize(graph, content = {})
11
6
  @graph = graph
12
7
  super()
@@ -59,7 +59,7 @@ module ROCrate
59
59
  # @return [Hash{String => Entry}>] The files/directories that were populated.
60
60
  # The key is the relative path of the file/directory, and the value is an Entry object where data can be read etc.
61
61
  def populate_entries(source_directory, include_hidden: false)
62
- raise 'Not a directory' unless ::File.directory?(source_directory)
62
+ raise TypeError, 'Not a directory' unless ::File.directory?(source_directory)
63
63
  @directory_entries = {}
64
64
  list_all_files(source_directory, include_hidden: include_hidden).each do |rel_path|
65
65
  source_path = Pathname.new(::File.join(source_directory, rel_path)).expand_path
@@ -0,0 +1,15 @@
1
+ module ROCrate
2
+ class Exception < StandardError
3
+ attr_reader :inner_exception
4
+
5
+ def initialize(message, _inner_exception = nil)
6
+ if _inner_exception
7
+ @inner_exception = _inner_exception
8
+ super("#{message}: #{@inner_exception.class.name} - #{@inner_exception.message}")
9
+ set_backtrace(@inner_exception.backtrace)
10
+ else
11
+ super(message)
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,4 @@
1
+ module ROCrate
2
+ class ReadException < ROCrate::Exception
3
+ end
4
+ end
@@ -9,7 +9,6 @@ module ROCrate
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
- raise "Not a directory!" unless ::File.directory?(target_dir)
13
12
  begin
14
13
  is_dir = ::File.directory?(source)
15
14
  rescue TypeError
@@ -82,6 +81,8 @@ module ROCrate
82
81
  # @param target_dir [String, ::File, Pathname] The target directory where the crate should be unzipped.
83
82
  # @return [Crate] The RO-Crate.
84
83
  def self.read_zip(source, target_dir: Dir.mktmpdir)
84
+ raise ROCrate::ReadException, "Target is not a directory!" unless ::File.directory?(target_dir)
85
+
85
86
  unzip_to(source, target_dir)
86
87
 
87
88
  # Traverse the unzipped directory to try and find the crate's root
@@ -96,7 +97,7 @@ module ROCrate
96
97
  # @param source [String, ::File, Pathname] The location of the directory.
97
98
  # @return [Crate] The RO-Crate.
98
99
  def self.read_directory(source)
99
- raise "Not a directory!" unless ::File.directory?(source)
100
+ raise ROCrate::ReadException, "Source is not a directory!" unless ::File.directory?(source)
100
101
 
101
102
  source = ::File.expand_path(source)
102
103
  metadata_file = Dir.entries(source).detect { |entry| entry == ROCrate::Metadata::IDENTIFIER ||
@@ -104,13 +105,18 @@ module ROCrate
104
105
 
105
106
  if metadata_file
106
107
  metadata_json = ::File.read(::File.join(source, metadata_file))
107
- metadata = JSON.parse(metadata_json)
108
+ begin
109
+ metadata = JSON.parse(metadata_json)
110
+ rescue JSON::ParserError => e
111
+ raise ROCrate::ReadException.new("Error parsing metadata", e)
112
+ end
113
+
108
114
  entities = entities_from_metadata(metadata)
109
115
  context = metadata['@context']
110
116
 
111
117
  build_crate(entities, source, context: context)
112
118
  else
113
- raise 'No metadata found!'
119
+ raise ROCrate::ReadException, "No metadata found!"
114
120
  end
115
121
  end
116
122
 
@@ -131,14 +137,14 @@ module ROCrate
131
137
 
132
138
  # Do some normalization...
133
139
  entities[ROCrate::Metadata::IDENTIFIER] = extract_metadata_entity(entities)
134
- raise "No metadata entity found in @graph!" unless entities[ROCrate::Metadata::IDENTIFIER]
140
+ raise ROCrate::ReadException, "No metadata entity found in @graph!" unless entities[ROCrate::Metadata::IDENTIFIER]
135
141
  entities[ROCrate::Preview::IDENTIFIER] = extract_preview_entity(entities)
136
142
  entities[ROCrate::Crate::IDENTIFIER] = extract_root_entity(entities)
137
- raise "No root entity (with @id: #{entities[ROCrate::Metadata::IDENTIFIER].dig('about', '@id')}) found in @graph!" unless entities[ROCrate::Crate::IDENTIFIER]
143
+ raise ROCrate::ReadException, "No root entity (with @id: #{entities[ROCrate::Metadata::IDENTIFIER].dig('about', '@id')}) found in @graph!" unless entities[ROCrate::Crate::IDENTIFIER]
138
144
 
139
145
  entities
140
146
  else
141
- raise "No @graph found in metadata!"
147
+ raise ROCrate::ReadException, "No @graph found in metadata!"
142
148
  end
143
149
  end
144
150
 
@@ -198,7 +204,9 @@ module ROCrate
198
204
  # @param entity_hash [Hash] A Hash containing all the entities in the @graph, mapped by their @id.
199
205
  # @return [Array<ROCrate::File, ROCrate::Directory>] The extracted DataEntity objects.
200
206
  def self.extract_data_entities(crate, source, entity_hash)
201
- crate.raw_properties['hasPart'].map do |ref|
207
+ parts = crate.raw_properties['hasPart'] || []
208
+ parts = [parts] unless parts.is_a?(Array)
209
+ parts.map do |ref|
202
210
  entity_props = entity_hash.delete(ref['@id'])
203
211
  next unless entity_props
204
212
  entity_class = ROCrate::DataEntity.specialize(entity_props)
@@ -234,21 +242,21 @@ module ROCrate
234
242
  # or nil if it referenced a local file that wasn't found.
235
243
  def self.create_data_entity(crate, entity_class, source, entity_props)
236
244
  id = entity_props.delete('@id')
245
+ raise ROCrate::ReadException, "Data Entity missing '@id': #{entity_props.inspect}" unless id
237
246
  decoded_id = URI.decode_www_form_component(id)
238
247
  path = nil
239
248
  uri = URI(id) rescue nil
240
249
  if uri&.absolute?
241
250
  path = uri
242
251
  decoded_id = nil
243
- else
252
+ elsif !id.start_with?('#')
244
253
  [id, decoded_id].each do |i|
245
254
  fullpath = ::File.join(source, i)
246
255
  path = Pathname.new(fullpath) if ::File.exist?(fullpath)
247
256
  end
248
- # unless path
249
- # warn "Missing file/directory: #{id}, skipping..."
250
- # return nil
251
- # end
257
+ if path.nil?
258
+ raise ROCrate::ReadException, "Local Data Entity not found in crate: #{id}"
259
+ end
252
260
  end
253
261
 
254
262
  entity_class.new(crate, path, decoded_id, entity_props)
@@ -290,7 +298,7 @@ module ROCrate
290
298
  # mapped by its @id.
291
299
  def self.extract_root_entity(entities)
292
300
  root_id = entities[ROCrate::Metadata::IDENTIFIER].dig('about', '@id')
293
- raise "Metadata entity does not reference any root entity" unless root_id
301
+ raise ROCrate::ReadException, "Metadata entity does not reference any root entity" unless root_id
294
302
  entities.delete(root_id)
295
303
  end
296
304
 
@@ -300,12 +308,16 @@ module ROCrate
300
308
  # @param source [String, ::File, Pathname] The location of the directory.
301
309
  # @return [Pathname, nil] The path to the root, or nil if not found.
302
310
  def self.detect_root_directory(source)
303
- Pathname(source).find do |entry|
311
+ queue = [source]
312
+ until queue.empty?
313
+ entry = Pathname(queue.shift)
304
314
  if entry.file?
305
315
  name = entry.basename.to_s
306
316
  if name == ROCrate::Metadata::IDENTIFIER || name == ROCrate::Metadata::IDENTIFIER_1_0
307
317
  return entry.parent
308
318
  end
319
+ elsif entry.directory?
320
+ queue += entry.children
309
321
  end
310
322
  end
311
323
 
@@ -1,10 +1,31 @@
1
+ <%
2
+ def entity_to_html(entity)
3
+ if entity.is_a?(Array)
4
+ if entity.length == 1
5
+ entity_to_html(entity.first)
6
+ else
7
+ "<ul><li>#{entity.map { |e| entity_to_html(e) }.join('</li><li>')}</li></ul>"
8
+ end
9
+ elsif entity.is_a?(ROCrate::Entity)
10
+ label = entity['name'] || entity.id
11
+ if entity.external?
12
+ "<a href=\"#{entity.id}\" target=\"_blank\">#{label}</a>"
13
+ else
14
+ label
15
+ end
16
+ else
17
+ entity
18
+ end
19
+ end
20
+ %>
1
21
  <!DOCTYPE html>
2
- <html>
22
+ <html lang="en">
3
23
  <head>
4
24
  <title><%= name || "New RO-Crate" %></title>
5
25
  <script type="application/ld+json"><%= metadata.generate %></script>
6
- <meta name="generator" content="https://github.com/fbacall/ro-crate-ruby">
26
+ <meta name="generator" content="https://github.com/ResearchObject/ro-crate-ruby">
7
27
  <meta name="keywords" content="RO-Crate">
28
+ <meta charset="utf-8">
8
29
  </head>
9
30
  <body>
10
31
  <h1><%= name || "New RO-Crate" %></h1>
@@ -17,36 +38,32 @@
17
38
  <dl>
18
39
  <% if author %>
19
40
  <dt>Author</dt>
20
- <dd><%= author %></dd>
41
+ <dd><%= entity_to_html author %></dd>
21
42
  <% end %>
22
43
  <% if contact_point %>
23
44
  <dt>Contact</dt>
24
- <dd><%= contact_point %></dd>
45
+ <dd><%= entity_to_html contact_point %></dd>
25
46
  <% end %>
26
47
  <% if publisher %>
27
48
  <dt>Publisher</dt>
28
- <dd><%= publisher %></dd>
49
+ <dd><%= entity_to_html publisher %></dd>
29
50
  <% end %>
30
51
  <% if license %>
31
52
  <dt>License</dt>
32
- <dd><%= license %></dd>
53
+ <dd><%= entity_to_html license %></dd>
33
54
  <% end %>
34
55
  </dl>
35
56
 
36
57
  <h2>Contents</h2>
37
58
  <ul>
38
59
  <% data_entities.each do |data_entity| %>
39
- <li id="__data_entity_<%= data_entity.id.gsub(/\s/, '-') %>">
40
- <% if data_entity.external? %>
41
- <strong><a href="<%= data_entity.id %>" target="_blank"><%= data_entity.name || data_entity.id %></a></strong>
42
- <% else %>
43
- <strong><%= data_entity.name || data_entity.id %></strong>
44
- <% end %>
60
+ <li>
61
+ <strong><%= entity_to_html data_entity %></strong>
45
62
  <% if data_entity.content_size %>
46
- <br/>Size: <%= data_entity.content_size %>
63
+ <br/>Size: <%= entity_to_html data_entity.content_size %>
47
64
  <% end %>
48
65
  <% if data_entity.encoding_format %>
49
- <br/>Format: <%= data_entity.encoding_format %>
66
+ <br/>Format: <%= entity_to_html data_entity.encoding_format %>
50
67
  <% end %>
51
68
  </li>
52
69
  <% end %>
@@ -14,9 +14,11 @@ module ROCrate
14
14
  #
15
15
  # @param dir [String] A path for the directory for the crate to be written to. All parent directories will be created.
16
16
  # @param overwrite [Boolean] Whether or not to overwrite existing files.
17
- def write(dir, overwrite: true)
17
+ # @param skip_preview [Boolean] Whether or not to skip generation of the RO-Crate preview HTML file.
18
+ def write(dir, overwrite: true, skip_preview: false)
18
19
  FileUtils.mkdir_p(dir) # Make any parent directories
19
20
  @crate.payload.each do |path, entry|
21
+ next if skip_preview && entry&.source.is_a?(ROCrate::PreviewGenerator)
20
22
  fullpath = ::File.join(dir, path)
21
23
  next if !overwrite && ::File.exist?(fullpath)
22
24
  next if entry.directory?
@@ -40,10 +42,12 @@ module ROCrate
40
42
  # Write the crate to a zip file.
41
43
  #
42
44
  # @param destination [String, ::File] The destination where to write the RO-Crate zip.
43
- def write_zip(destination)
45
+ # @param skip_preview [Boolean] Whether or not to skip generation of the RO-Crate preview HTML file.
46
+ def write_zip(destination, skip_preview: false)
44
47
  Zip::File.open(destination, Zip::File::CREATE) do |zip|
45
48
  @crate.payload.each do |path, entry|
46
49
  next if entry.directory?
50
+ next if skip_preview && entry&.source.is_a?(ROCrate::PreviewGenerator)
47
51
  if entry.symlink?
48
52
  zip.add(path, entry.path) if entry.path
49
53
  else
data/lib/ro_crate.rb CHANGED
@@ -6,6 +6,8 @@ require 'zip/filesystem'
6
6
  require 'addressable'
7
7
  require 'open-uri'
8
8
 
9
+ require 'ro_crate/model/exceptions/exception'
10
+ require 'ro_crate/model/exceptions/read_exception'
9
11
  require 'ro_crate/json_ld_hash'
10
12
  require 'ro_crate/model/entity'
11
13
  require 'ro_crate/model/data_entity'
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.5.1'
3
+ s.version = '0.5.3'
4
4
  s.summary = 'Create, manipulate, read RO-Crates.'
5
5
  s.authors = ['Finn Bacall']
6
6
  s.email = 'finn.bacall@manchester.ac.uk'
data/test/entity_test.rb CHANGED
@@ -72,6 +72,23 @@ class EntityTest < Test::Unit::TestCase
72
72
  assert_equal(person.canonical_id, crate.author.canonical_id)
73
73
  end
74
74
 
75
+ test 'to_json' do
76
+ crate = ROCrate::Crate.new
77
+
78
+ crate['test'] = 'hello'
79
+ crate['test2'] = ['hello']
80
+ crate['test3'] = 123
81
+ crate['test4'] = { a: 'bc' }
82
+
83
+ json = crate.to_json
84
+ parsed = JSON.parse(json)
85
+
86
+ assert_equal 'hello', parsed['test']
87
+ assert_equal ['hello'], parsed['test2']
88
+ assert_equal 123, parsed['test3']
89
+ assert_equal({ 'a' => 'bc' }, parsed['test4'])
90
+ end
91
+
75
92
  test 'format various IDs' do
76
93
  assert_equal "#Hello%20World/Goodbye%20World", ROCrate::ContextualEntity.format_id('#Hello World/Goodbye World')
77
94
  assert_equal "#Hello%20World/Goodbye%20World", ROCrate::ContextualEntity.format_id('Hello World/Goodbye World')
@@ -0,0 +1,110 @@
1
+ # encoding: utf-8
2
+ require 'test_helper'
3
+
4
+ class EntryTest < Test::Unit::TestCase
5
+ setup do
6
+ stub_request(:get, 'http://example.com/dir/file.txt').to_return(status: 200, body: 'file contents')
7
+ stub_request(:get, 'http://example.com/dir/').to_return(status: 200, body: '<html>...')
8
+
9
+ @local_file = ROCrate::Entry.new(fixture_file('info.txt'))
10
+ @local_path = ROCrate::Entry.new(Pathname.new(fixture_dir).join('directory', 'info.txt'))
11
+ @local_io = ROCrate::Entry.new(StringIO.new('stringio'))
12
+ @local_dir = ROCrate::Entry.new(Pathname.new(fixture_dir).join('directory'))
13
+ @local_symlink = ROCrate::Entry.new(Pathname.new(fixture_dir).join('symlink'))
14
+ @local_dir_symlink = ROCrate::Entry.new(Pathname.new(fixture_dir).join('dir_symlink'))
15
+ @remote_file = ROCrate::RemoteEntry.new(URI('http://example.com/dir/file.txt'))
16
+ @remote_dir = ROCrate::RemoteEntry.new(URI('http://example.com/dir/'), directory: true)
17
+ end
18
+
19
+ test 'read' do
20
+ assert_equal "Hello\n", @local_file.read
21
+ assert_equal "1234\n", @local_path.read
22
+ assert_equal "stringio", @local_io.read
23
+ assert_raises(Errno::EISDIR) { @local_dir.read }
24
+ assert_raises(Errno::EISDIR) { @local_dir_symlink.read }
25
+ assert_equal "I have spaces in my name\n", @local_symlink.read
26
+ assert_equal "file contents", @remote_file.read
27
+ assert_equal "<html>...", @remote_dir.read
28
+ end
29
+
30
+ test 'write_to' do
31
+ dest = StringIO.new
32
+ @local_file.write_to(dest)
33
+ dest.rewind
34
+ assert_equal "Hello\n", dest.read
35
+
36
+ dest = StringIO.new
37
+ @local_path.write_to(dest)
38
+ dest.rewind
39
+ assert_equal "1234\n", dest.read
40
+
41
+ dest = StringIO.new
42
+ @local_io.write_to(dest)
43
+ dest.rewind
44
+ assert_equal "stringio", dest.read
45
+
46
+ assert_raises(Errno::EISDIR) { @local_dir.write_to(dest) }
47
+
48
+ assert_raises(Errno::EISDIR) { @local_dir_symlink.write_to(dest) }
49
+
50
+ dest = StringIO.new
51
+ @local_symlink.write_to(dest)
52
+ dest.rewind
53
+ assert_equal "I have spaces in my name\n", dest.read
54
+
55
+ dest = StringIO.new
56
+ @remote_file.write_to(dest)
57
+ dest.rewind
58
+ assert_equal "file contents", dest.read
59
+
60
+ dest = StringIO.new
61
+ @remote_dir.write_to(dest)
62
+ dest.rewind
63
+ assert_equal "<html>...", dest.read
64
+ end
65
+
66
+ test 'directory?' do
67
+ refute @local_file.directory?
68
+ refute @local_path.directory?
69
+ refute @local_io.directory?
70
+ assert @local_dir.directory?
71
+ refute @local_symlink.directory?
72
+ assert @local_dir_symlink.directory?
73
+ refute @remote_file.directory?
74
+ assert @remote_dir.directory?
75
+ end
76
+
77
+ test 'symlink?' do
78
+ refute @local_file.symlink?
79
+ refute @local_path.symlink?
80
+ refute @local_io.symlink?
81
+ refute @local_dir.symlink?
82
+ assert @local_symlink.symlink?
83
+ assert @local_dir_symlink.symlink?
84
+ refute @remote_file.symlink?
85
+ refute @remote_dir.symlink?
86
+ end
87
+
88
+ test 'remote?' do
89
+ refute @local_file.remote?
90
+ refute @local_path.remote?
91
+ refute @local_io.remote?
92
+ refute @local_dir.remote?
93
+ refute @local_symlink.remote?
94
+ refute @local_dir_symlink.remote?
95
+ assert @remote_file.remote?
96
+ assert @remote_dir.remote?
97
+ end
98
+
99
+ test 'path' do
100
+ base = Pathname.new(fixture_dir).expand_path.to_s
101
+ assert_equal "#{base}/info.txt", @local_file.path
102
+ assert_equal "#{base}/directory/info.txt", @local_path.path
103
+ assert_nil @local_io.path
104
+ assert_equal "#{base}/directory", @local_dir.path
105
+ assert_equal "#{base}/symlink", @local_symlink.path
106
+ assert_equal "#{base}/dir_symlink", @local_dir_symlink.path
107
+ assert_nil @remote_file.path
108
+ assert_nil @remote_dir.path
109
+ end
110
+ end
@@ -0,0 +1,21 @@
1
+ {
2
+ "@context": "https://w3id.org/ro/crate/1.1/context",
3
+ "@graph": [
4
+ {
5
+ "@id": "arcp://name,somethingsomething",
6
+ "@type": "Dataset",
7
+ "datePublished": "2024-01-31T10:47:17+00:00"
8
+
9
+ },
10
+ {
11
+ "@id": "ro-crate-metadata.json",
12
+ "@type": "CreativeWork",
13
+ "about": {
14
+ "@id": "arcp://name,somethingsomething"
15
+ },
16
+ "conformsTo": {
17
+ "@id": "https://w3id.org/ro/crate/1.1"
18
+ }
19
+ }
20
+ ]
21
+ }
@@ -0,0 +1,32 @@
1
+ {
2
+ "@context": "https://w3id.org/ro/crate/1.1/context",
3
+ "@graph": [
4
+ {
5
+ "@id": "ro-crate-metadata.json",
6
+ "@type": "CreativeWork",
7
+ "about": {
8
+ "@id": "./"
9
+ }
10
+ },
11
+ {
12
+ "@id": "./",
13
+ "@type": "Dataset",
14
+ "hasPart": [
15
+ {
16
+ "@id": "file1.txt"
17
+ },
18
+ {
19
+ "@id": "file2.txt"
20
+ }
21
+ ]
22
+ },
23
+ {
24
+ "@id": "file1.txt",
25
+ "@type": "File"
26
+ },
27
+ {
28
+ "@id": "file2.txt",
29
+ "@type": "File"
30
+ }
31
+ ]
32
+ }
@@ -0,0 +1,3 @@
1
+ {
2
+ "@context": "https://w3id.org/ro/crate/1.1/context"
3
+ }
@@ -0,0 +1,6 @@
1
+ {
2
+ "@context": "https://w3id.org/ro/crate/1.1/context",
3
+ "@graph": [
4
+
5
+ ]
6
+ }
@@ -0,0 +1,12 @@
1
+ {
2
+ "@context": "https://w3id.org/ro/crate/1.1/context",
3
+ "@graph": [
4
+ {
5
+ "@id": "ro-crate-metadata.json",
6
+ "@type": "CreativeWork",
7
+ "about": {
8
+ "@id": "./"
9
+ }
10
+ }
11
+ ]
12
+ }
@@ -0,0 +1,9 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <title>Error</title>
5
+ </head>
6
+ <body>
7
+ Were you expecting JSON?
8
+ </body>
9
+ </html>
@@ -0,0 +1 @@
1
+ directory
@@ -0,0 +1 @@
1
+ 123
@@ -0,0 +1,57 @@
1
+ {
2
+ "@context": [
3
+ "https://w3id.org/ro/crate/1.1/context",
4
+ "https://w3id.org/ro/terms/workflow-run/context"
5
+ ],
6
+ "@graph": [
7
+ {
8
+ "@id": "./",
9
+ "@type": "Dataset",
10
+ "datePublished": "2025-01-28T02:44:51.523Z",
11
+ "hasPart": {
12
+ "@id": "a_file"
13
+ },
14
+ "dateCreated": "2025-01-28T02:45:05.906Z",
15
+ "dateModified": "2025-01-28T02:44:51.523Z",
16
+ "description": "Something",
17
+ "license": {
18
+ "@id": "https://spdx.org/licenses/MIT"
19
+ },
20
+ "mainEntity": {
21
+ "@id": "a_file"
22
+ },
23
+ "name": "An RO-Crate containing just a single item"
24
+ },
25
+ {
26
+ "@id": "ro-crate-metadata.json",
27
+ "@type": "CreativeWork",
28
+ "conformsTo": [
29
+ {
30
+ "@id": "https://w3id.org/ro/crate/1.1"
31
+ },
32
+ {
33
+ "@id": "https://w3id.org/workflowhub/workflow-ro-crate/1.0"
34
+ }
35
+ ],
36
+ "about": {
37
+ "@id": "./"
38
+ }
39
+ },
40
+ {
41
+ "@id": "a_file",
42
+ "@type": [
43
+ "File",
44
+ "ComputationalWorkflow",
45
+ "SoftwareSourceCode"
46
+ ],
47
+ "contentSize": 4,
48
+ "description": "A workflow",
49
+ "encodingFormat": "text/plain",
50
+ "name": "a_file",
51
+ "programmingLanguage": {
52
+ "@id": "https://example.com/workflow_format"
53
+ }
54
+ }
55
+ ]
56
+ }
57
+
@@ -0,0 +1 @@
1
+ file with spaces.txt
@@ -0,0 +1,48 @@
1
+ # encoding: utf-8
2
+ require 'test_helper'
3
+
4
+ class PreviewTest < Test::Unit::TestCase
5
+ test 'simple attributes' do
6
+ crate = ROCrate::Crate.new
7
+ crate.author = 'Finn'
8
+
9
+ html = crate.preview.source.read
10
+ assert_includes html, '<dd>Finn</dd>'
11
+ end
12
+
13
+ test 'list attributes' do
14
+ crate = ROCrate::Crate.new
15
+ crate.author = ['Finn', 'Josiah']
16
+
17
+ html = crate.preview.source.read
18
+ assert_includes html, '<dd><ul><li>Finn</li><li>Josiah</li></ul></dd>'
19
+ end
20
+
21
+ test 'entity attributes' do
22
+ crate = ROCrate::Crate.new
23
+ crate.author = crate.add_person('https://orcid.org/0000-0002-0048-3300', name: 'Finn')
24
+
25
+ html = crate.preview.source.read
26
+ assert_includes html, '<dd><a href="https://orcid.org/0000-0002-0048-3300" target="_blank">Finn</a></dd>'
27
+ end
28
+
29
+ test 'complex attributes' do
30
+ crate = ROCrate::Crate.new
31
+ crate.author = [crate.add_person('https://orcid.org/0000-0002-0048-3300', name: 'Finn'), 'Josiah']
32
+
33
+ html = crate.preview.source.read
34
+
35
+ assert_includes html, '<dd><ul><li><a href="https://orcid.org/0000-0002-0048-3300" target="_blank">Finn</a></li><li>Josiah</li></ul></dd>'
36
+ end
37
+
38
+ test 'files' do
39
+ crate = ROCrate::Crate.new
40
+ crate.add_file(fixture_file('info.txt'))
41
+ crate.add_external_file('https://raw.githubusercontent.com/ResearchObject/ro-crate-ruby/master/README.md')
42
+
43
+ html = crate.preview.source.read
44
+
45
+ assert_includes html, '<strong>info.txt</strong>'
46
+ assert_includes html, '<strong><a href="https://raw.githubusercontent.com/ResearchObject/ro-crate-ruby/master/README.md" target="_blank">https://raw.githubusercontent.com/ResearchObject/ro-crate-ruby/master/README.md</a></strong>'
47
+ end
48
+ end
data/test/reader_test.rb CHANGED
@@ -116,6 +116,19 @@ class ReaderTest < Test::Unit::TestCase
116
116
  end
117
117
  end
118
118
 
119
+ test 'reading from zip IO' do
120
+ io = StringIO.new(fixture_file('directory.zip').read)
121
+ crate = ROCrate::Reader.read(io)
122
+
123
+ assert crate.payload['fish/info.txt']
124
+ assert_equal '1234', crate.payload['fish/info.txt'].source.read.chomp
125
+ assert crate.payload['fish/root.txt']
126
+ assert crate.payload['fish/data/info.txt']
127
+ assert crate.payload['fish/data/nested.txt']
128
+ assert crate.payload['fish/data/binary.jpg']
129
+ assert_equal ['./', 'fish/', 'ro-crate-metadata.jsonld', 'ro-crate-preview.html'], crate.entities.map(&:id).sort
130
+ end
131
+
119
132
  test 'reading from directory with directories' do
120
133
  crate = ROCrate::Reader.read_directory(fixture_file('directory_crate').path)
121
134
 
@@ -303,4 +316,83 @@ class ReaderTest < Test::Unit::TestCase
303
316
  refute real_file.payload.values.first.remote?
304
317
  refute real_file.payload.values.first.directory?
305
318
  end
319
+
320
+ test 'handles exceptions' do
321
+ e = check_exception(ROCrate::ReadException) do
322
+ ROCrate::Reader.read(fixture_file('broken/no_graph'))
323
+ end
324
+ assert_include e.message, 'No @graph'
325
+
326
+ e = check_exception(ROCrate::ReadException) do
327
+ ROCrate::Reader.read(fixture_file('broken/no_metadata_entity'))
328
+ end
329
+ assert_include e.message, 'No metadata entity'
330
+
331
+ e = check_exception(ROCrate::ReadException) do
332
+ ROCrate::Reader.read(fixture_file('broken/no_metadata_file'))
333
+ end
334
+ assert_include e.message, 'No metadata found'
335
+
336
+ e = check_exception(ROCrate::ReadException) do
337
+ ROCrate::Reader.read(fixture_file('broken/no_root_entity'))
338
+ end
339
+ assert_include e.message, 'No root'
340
+
341
+ e = check_exception(ROCrate::ReadException) do
342
+ ROCrate::Reader.read(fixture_file('broken/not_json'))
343
+ end
344
+ assert_include e.message, 'Error parsing metadata: JSON::ParserError'
345
+ assert_equal JSON::ParserError, e.inner_exception.class
346
+
347
+ e = check_exception(ROCrate::ReadException) do
348
+ ROCrate::Reader.read(fixture_file('workflow-0.2.0.zip'), target_dir: fixture_file('workflow-0.2.0.zip'))
349
+ end
350
+ assert_include e.message, 'Target is not a directory!'
351
+
352
+ e = check_exception(ROCrate::ReadException) do
353
+ ROCrate::Reader.read_directory(fixture_file('workflow-0.2.0.zip'))
354
+ end
355
+ assert_include e.message, 'Source is not a directory!'
356
+
357
+ e = check_exception(ROCrate::ReadException) do
358
+ ROCrate::Reader.read(fixture_file('broken/missing_file'))
359
+ end
360
+ assert_include e.message, 'not found in crate: file1.txt'
361
+ end
362
+
363
+ test 'tolerates arcp identifier on root data entity (and missing hasPart)' do
364
+ crate = ROCrate::Reader.read(fixture_file('arcp').path)
365
+
366
+ assert_equal 'arcp://name,somethingsomething', crate.id
367
+ assert_empty crate.data_entities
368
+ end
369
+
370
+ test 'reads first metadata file it encounters' do
371
+ crate = ROCrate::Reader.read(fixture_file('multi_metadata_crate.crate.zip').path)
372
+
373
+ assert_equal 'At the root', crate.name
374
+ end
375
+
376
+ test 'reads crate with singleton hasPart' do
377
+ crate = ROCrate::Reader.read(fixture_file('singleton-haspart').path)
378
+
379
+ data = crate.data_entities
380
+ assert_equal 1, data.length
381
+ assert_equal 'a_file', data.first.name
382
+ end
383
+
384
+ private
385
+
386
+ def check_exception(exception_class)
387
+ e = nil
388
+ assert_raise(exception_class) do
389
+ begin
390
+ yield
391
+ rescue exception_class => e
392
+ raise e
393
+ end
394
+ end
395
+
396
+ e
397
+ end
306
398
  end
data/test/writer_test.rb CHANGED
@@ -230,36 +230,83 @@ class WriterTest < Test::Unit::TestCase
230
230
 
231
231
  test 'write crate with remote files and directories' do
232
232
  orig = ROCrate::Reader.read(fixture_file('uri_heavy_crate').path)
233
- Tempfile.create do |file|
234
- ROCrate::Writer.new(orig).write_zip(file)
233
+ Tempfile.create do |file|
234
+ ROCrate::Writer.new(orig).write_zip(file)
235
235
 
236
- Zip::File.open(file) do |zipfile|
237
- refute zipfile.find_entry('nih:sha-256;3a2c-8d14-a40b-3755-4abc-5af8-a56d-ba3a-e159-d688-c9b3-f169-6751-4b88-fbd2-6a9f;7')
238
- end
236
+ Zip::File.open(file) do |zipfile|
237
+ refute zipfile.find_entry('nih:sha-256;3a2c-8d14-a40b-3755-4abc-5af8-a56d-ba3a-e159-d688-c9b3-f169-6751-4b88-fbd2-6a9f;7')
238
+ end
239
239
 
240
- file.rewind
241
-
242
- crate = ROCrate::Reader.read(file)
243
-
244
- dir = crate.get('nih:sha-256;f70e-eb2e-89d0-b3dc-5c99-8541-fa4b-6e64-a194-cf9d-ebd8-ca58-24e7-c47a-553f-86fa;c/')
245
- assert dir
246
- assert dir.is_a?(ROCrate::Directory)
247
- assert dir.remote?
248
- assert_empty dir.payload
249
-
250
- file = crate.get('nih:sha-256;3a2c-8d14-a40b-3755-4abc-5af8-a56d-ba3a-e159-d688-c9b3-f169-6751-4b88-fbd2-6a9f;7')
251
- assert file
252
- assert file.is_a?(ROCrate::File)
253
- assert file.remote?
254
- assert_empty file.payload
255
-
256
- real_file = crate.get('main.nf')
257
- assert real_file
258
- assert real_file.is_a?(ROCrate::File)
259
- refute real_file.remote?
260
- assert_not_empty real_file.payload
261
- refute real_file.payload.values.first.remote?
262
- refute real_file.payload.values.first.directory?
240
+ file.rewind
241
+
242
+ crate = ROCrate::Reader.read(file)
243
+
244
+ dir = crate.get('nih:sha-256;f70e-eb2e-89d0-b3dc-5c99-8541-fa4b-6e64-a194-cf9d-ebd8-ca58-24e7-c47a-553f-86fa;c/')
245
+ assert dir
246
+ assert dir.is_a?(ROCrate::Directory)
247
+ assert dir.remote?
248
+ assert_empty dir.payload
249
+
250
+ file = crate.get('nih:sha-256;3a2c-8d14-a40b-3755-4abc-5af8-a56d-ba3a-e159-d688-c9b3-f169-6751-4b88-fbd2-6a9f;7')
251
+ assert file
252
+ assert file.is_a?(ROCrate::File)
253
+ assert file.remote?
254
+ assert_empty file.payload
255
+
256
+ real_file = crate.get('main.nf')
257
+ assert real_file
258
+ assert real_file.is_a?(ROCrate::File)
259
+ refute real_file.remote?
260
+ assert_not_empty real_file.payload
261
+ refute real_file.payload.values.first.remote?
262
+ refute real_file.payload.values.first.directory?
263
+ end
264
+ end
265
+
266
+ test 'write crate with arcp root identifier' do
267
+ crate = ROCrate::Reader.read(fixture_file('arcp').path)
268
+
269
+ assert_equal 'arcp://name,somethingsomething', crate.id
270
+ Dir.mktmpdir do |dir|
271
+ ROCrate::Writer.new(crate).write(dir)
272
+ Dir.chdir(dir) do
273
+ assert File.exist?('ro-crate-metadata.json')
263
274
  end
275
+ end
276
+ end
277
+
278
+ test 'skip generating the preview in a directory' do
279
+ crate = ROCrate::Crate.new
280
+ crate.add_file(fixture_file('info.txt'))
281
+ crate.add_file(StringIO.new('just a string!'), 'notice.txt')
282
+ crate.add_file(fixture_file('data.csv'), 'directory/data.csv')
283
+
284
+ Dir.mktmpdir do |dir|
285
+ ROCrate::Writer.new(crate).write(dir, skip_preview: true)
286
+ assert ::File.exist?(::File.join(dir, ROCrate::Metadata::IDENTIFIER))
287
+ refute ::File.exist?(::File.join(dir, ROCrate::Preview::IDENTIFIER))
288
+ assert_equal 6, ::File.size(::File.join(dir, 'info.txt'))
289
+ assert_equal 14, ::File.size(::File.join(dir, 'notice.txt'))
290
+ assert_equal 20, ::File.size(::File.join(dir, 'directory', 'data.csv'))
291
+ end
292
+ end
293
+
294
+ test 'skip generating the preview in a zip file' do
295
+ crate = ROCrate::Crate.new
296
+ crate.add_directory(fixture_file('directory').path.to_s, 'fish')
297
+
298
+ Tempfile.create do |file|
299
+ ROCrate::Writer.new(crate).write_zip(file, skip_preview: true)
300
+
301
+ Zip::File.open(file) do |zipfile|
302
+ assert zipfile.file.exist?(ROCrate::Metadata::IDENTIFIER)
303
+ refute zipfile.file.exist?(ROCrate::Preview::IDENTIFIER)
304
+ assert zipfile.file.exist? 'fish/info.txt'
305
+ assert zipfile.file.exist? 'fish/root.txt'
306
+ assert zipfile.file.exist? 'fish/data/info.txt'
307
+ assert zipfile.file.exist? 'fish/data/nested.txt'
308
+ assert zipfile.file.exist? 'fish/data/binary.jpg'
309
+ end
310
+ end
264
311
  end
265
312
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ro-crate
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.5.1
4
+ version: 0.5.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Finn Bacall
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2022-12-12 00:00:00.000000000 Z
11
+ date: 2025-01-30 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: addressable
@@ -139,7 +139,6 @@ files:
139
139
  - ".gitignore"
140
140
  - ".ruby-version"
141
141
  - Gemfile
142
- - Gemfile.lock
143
142
  - LICENSE
144
143
  - README.md
145
144
  - Rakefile
@@ -152,6 +151,8 @@ files:
152
151
  - lib/ro_crate/model/directory.rb
153
152
  - lib/ro_crate/model/entity.rb
154
153
  - lib/ro_crate/model/entry.rb
154
+ - lib/ro_crate/model/exceptions/exception.rb
155
+ - lib/ro_crate/model/exceptions/read_exception.rb
155
156
  - lib/ro_crate/model/file.rb
156
157
  - lib/ro_crate/model/metadata.rb
157
158
  - lib/ro_crate/model/organization.rb
@@ -166,12 +167,22 @@ files:
166
167
  - test/crate_test.rb
167
168
  - test/directory_test.rb
168
169
  - test/entity_test.rb
170
+ - test/entry_test.rb
171
+ - test/fixtures/arcp/ro-crate-metadata.json
169
172
  - test/fixtures/biobb_hpc_workflows-condapack.zip
173
+ - test/fixtures/broken/missing_file/file2.txt
174
+ - test/fixtures/broken/missing_file/ro-crate-metadata.json
175
+ - test/fixtures/broken/no_graph/ro-crate-metadata.json
176
+ - test/fixtures/broken/no_metadata_entity/ro-crate-metadata.json
177
+ - test/fixtures/broken/no_metadata_file/test.txt
178
+ - test/fixtures/broken/no_root_entity/ro-crate-metadata.json
179
+ - test/fixtures/broken/not_json/ro-crate-metadata.json
170
180
  - test/fixtures/conflicting_data_directory/info.txt
171
181
  - test/fixtures/conflicting_data_directory/nested.txt
172
182
  - test/fixtures/crate-spec1.1/file with spaces.txt
173
183
  - test/fixtures/crate-spec1.1/ro-crate-metadata.json
174
184
  - test/fixtures/data.csv
185
+ - test/fixtures/dir_symlink
175
186
  - test/fixtures/directory.zip
176
187
  - test/fixtures/directory/.dir/test.txt
177
188
  - test/fixtures/directory/.dotfile
@@ -190,6 +201,7 @@ files:
190
201
  - test/fixtures/file with spaces.txt
191
202
  - test/fixtures/info.txt
192
203
  - test/fixtures/misc_data_entity_crate/ro-crate-metadata.json
204
+ - test/fixtures/multi_metadata_crate.crate.zip
193
205
  - test/fixtures/nested_directory.zip
194
206
  - test/fixtures/ro-crate-galaxy-sortchangecase/LICENSE
195
207
  - test/fixtures/ro-crate-galaxy-sortchangecase/README.md
@@ -198,6 +210,8 @@ files:
198
210
  - test/fixtures/ro-crate-galaxy-sortchangecase/test/test1/input.bed
199
211
  - test/fixtures/ro-crate-galaxy-sortchangecase/test/test1/output_exp.bed
200
212
  - test/fixtures/ro-crate-galaxy-sortchangecase/test/test1/sort-and-change-case-test.yml
213
+ - test/fixtures/singleton-haspart/a_file
214
+ - test/fixtures/singleton-haspart/ro-crate-metadata.json
201
215
  - test/fixtures/spaces/file with spaces.txt
202
216
  - test/fixtures/spaces/ro-crate-metadata.jsonld
203
217
  - test/fixtures/sparse_directory_crate.zip
@@ -210,6 +224,7 @@ files:
210
224
  - test/fixtures/sparse_directory_crate/ro-crate-metadata.jsonld
211
225
  - test/fixtures/sparse_directory_crate/ro-crate-preview.html
212
226
  - test/fixtures/sparse_directory_crate/unlisted_file.txt
227
+ - test/fixtures/symlink
213
228
  - test/fixtures/unlinked_entity_crate/LICENSE
214
229
  - test/fixtures/unlinked_entity_crate/README.md
215
230
  - test/fixtures/unlinked_entity_crate/ro-crate-metadata.json
@@ -2450,6 +2465,7 @@ files:
2450
2465
  - test/fixtures/workflow-test-fixture-symlink/concat_two_files.ga
2451
2466
  - test/fixtures/workflow-test-fixture-symlink/diagram.png
2452
2467
  - test/fixtures/workflow-test-fixture-symlink/images/workflow-diagram.png
2468
+ - test/preview_test.rb
2453
2469
  - test/reader_test.rb
2454
2470
  - test/test_helper.rb
2455
2471
  - test/writer_test.rb
@@ -2472,7 +2488,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
2472
2488
  - !ruby/object:Gem::Version
2473
2489
  version: '0'
2474
2490
  requirements: []
2475
- rubygems_version: 3.2.33
2491
+ rubygems_version: 3.4.19
2476
2492
  signing_key:
2477
2493
  specification_version: 4
2478
2494
  summary: Create, manipulate, read RO-Crates.
data/Gemfile.lock DELETED
@@ -1,50 +0,0 @@
1
- PATH
2
- remote: .
3
- specs:
4
- ro-crate (0.5.1)
5
- addressable (>= 2.7, < 2.9)
6
- rubyzip (~> 2.0.0)
7
-
8
- GEM
9
- remote: https://rubygems.org/
10
- specs:
11
- addressable (2.8.0)
12
- public_suffix (>= 2.0.2, < 5.0)
13
- crack (0.4.3)
14
- safe_yaml (~> 1.0.0)
15
- docile (1.3.5)
16
- hashdiff (1.0.1)
17
- power_assert (2.0.1)
18
- public_suffix (4.0.6)
19
- rake (13.0.0)
20
- rexml (3.2.5)
21
- rubyzip (2.0.0)
22
- safe_yaml (1.0.5)
23
- simplecov (0.21.2)
24
- docile (~> 1.1)
25
- simplecov-html (~> 0.11)
26
- simplecov_json_formatter (~> 0.1)
27
- simplecov-html (0.12.3)
28
- simplecov_json_formatter (0.1.2)
29
- test-unit (3.5.3)
30
- power_assert
31
- webmock (3.8.3)
32
- addressable (>= 2.3.6)
33
- crack (>= 0.3.2)
34
- hashdiff (>= 0.4.0, < 2.0.0)
35
- yard (0.9.25)
36
-
37
- PLATFORMS
38
- ruby
39
-
40
- DEPENDENCIES
41
- rake (~> 13.0.0)
42
- rexml (~> 3.2.5)
43
- ro-crate!
44
- simplecov (~> 0.21.2)
45
- test-unit (~> 3.5.3)
46
- webmock (~> 3.8.3)
47
- yard (~> 0.9.25)
48
-
49
- BUNDLED WITH
50
- 2.3.6