rdf-tabular 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,33 @@
1
+ module RDF::Tabular
2
+ module Utils
3
+ # Add debug event to debug array, if specified
4
+ #
5
+ # param [String] message
6
+ # yieldreturn [String] appended to message, to allow for lazy-evaulation of message
7
+ def debug(*args)
8
+ options = args.last.is_a?(Hash) ? args.pop : {}
9
+ return unless options[:debug] || @options[:debug] || RDF::Tabular.debug?
10
+ depth = options[:depth] || @options[:depth]
11
+ d_str = depth > 100 ? ' ' * 100 + '+' : ' ' * depth
12
+ list = args
13
+ list << yield if block_given?
14
+ message = d_str + (list.empty? ? "" : list.join(": "))
15
+ options[:debug] << message if options[:debug].is_a?(Array)
16
+ @options[:debug] << message if @options[:debug].is_a?(Array)
17
+ $stderr.puts(message) if RDF::Tabular.debug? || @options[:debug] == TrueClass
18
+ end
19
+ module_function :debug
20
+
21
+ # Increase depth around a method invocation
22
+ # @yield
23
+ # Yields with no arguments
24
+ # @yieldreturn [Object] returns the result of yielding
25
+ # @return [Object]
26
+ def depth
27
+ @options[:depth] += 1
28
+ ret = yield
29
+ @options[:depth] -= 1
30
+ ret
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,18 @@
1
+ module RDF::Tabular::VERSION
2
+ VERSION_FILE = File.join(File.expand_path(File.dirname(__FILE__)), "..", "..", "..", "VERSION")
3
+ MAJOR, MINOR, TINY, EXTRA = File.read(VERSION_FILE).chop.split(".")
4
+
5
+ STRING = [MAJOR, MINOR, TINY, EXTRA].compact.join('.')
6
+
7
+ ##
8
+ # @return [String]
9
+ def self.to_s() STRING end
10
+
11
+ ##
12
+ # @return [String]
13
+ def self.to_str() STRING end
14
+
15
+ ##
16
+ # @return [Array(Integer, Integer, Integer)]
17
+ def self.to_a() STRING.split(".") end
18
+ end
@@ -0,0 +1,30 @@
1
+ # coding: utf-8
2
+ $:.unshift "."
3
+ require 'spec_helper'
4
+ require 'rdf/spec/format'
5
+
6
+ describe RDF::Tabular::Format do
7
+ before :each do
8
+ @format_class = described_class
9
+ end
10
+
11
+ include RDF_Format
12
+
13
+ describe ".for" do
14
+ formats = [
15
+ :tabular,
16
+ 'etc/doap.csv',
17
+ {:file_name => 'etc/doap.csv'},
18
+ {:file_extension => 'csv'},
19
+ {:content_type => 'text/csv'},
20
+ ].each do |arg|
21
+ it "discovers with #{arg.inspect}" do
22
+ expect(RDF::Tabular::Format).to include RDF::Format.for(arg)
23
+ end
24
+ end
25
+ end
26
+
27
+ describe "#to_sym" do
28
+ specify {expect(@format_class.to_sym).to eq :tabular}
29
+ end
30
+ end
data/spec/matchers.rb ADDED
@@ -0,0 +1,134 @@
1
+ require 'rdf/isomorphic'
2
+ require 'rspec/matchers'
3
+ require 'rdf/rdfa'
4
+
5
+ def normalize(graph)
6
+ case graph
7
+ when RDF::Queryable then graph
8
+ when IO, StringIO
9
+ RDF::Graph.new.load(graph, base_uri: @info.action)
10
+ else
11
+ # Figure out which parser to use
12
+ g = RDF::Repository.new
13
+ reader_class = detect_format(graph)
14
+ reader_class.new(graph, base_uri: @info.action).each {|s| g << s}
15
+ g
16
+ end
17
+ end
18
+
19
+ Info = Struct.new(:id, :debug, :action, :result, :metadata)
20
+
21
+ RSpec::Matchers.define :be_equivalent_graph do |expected, info|
22
+ match do |actual|
23
+ @info = if (info.id rescue false)
24
+ info
25
+ elsif info.is_a?(Hash)
26
+ Info.new(info[:id], info[:debug], info[:action], info[:result], info[:metadata])
27
+ else
28
+ Info.new(info, info.to_s)
29
+ end
30
+ @info.debug = Array(@info.debug).join("\n")
31
+ @expected = normalize(expected)
32
+ @actual = normalize(actual)
33
+ @actual.isomorphic_with?(@expected) rescue false
34
+ end
35
+
36
+ failure_message do |actual|
37
+ prefixes = {
38
+ '' => @info.action + '#',
39
+ oa: "http://www.w3.org/ns/oa#",
40
+ geo: "http://www.geonames.org/ontology#",
41
+ }
42
+ "#{@info.inspect + "\n"}" +
43
+ if @expected.is_a?(RDF::Enumerable) && @actual.size != @expected.size
44
+ "Graph entry count differs:\nexpected: #{@expected.size}\nactual: #{@actual.size}\n"
45
+ elsif @expected.is_a?(Array) && @actual.size != @expected.length
46
+ "Graph entry count differs:\nexpected: #{@expected.length}\nactual: #{@actual.size}\n"
47
+ else
48
+ "Graph differs\n"
49
+ end +
50
+ "Expected:\n#{@expected.dump(:ttl, standard_prefixes: true, prefixes: prefixes)}" +
51
+ "Results:\n#{@actual.dump(:ttl, standard_prefixes: true, prefixes: prefixes)}" +
52
+ (@info.metadata ? "\nMetadata:\n#{@info.metadata.to_json(JSON_STATE)}\n" : "") +
53
+ (@info.metadata && !@info.metadata.errors.empty? ? "\nMetadata Errors:\n#{@info.metadata.errors.join("\n")}\n" : "") +
54
+ (@info.debug ? "\nDebug:\n#{@info.debug}" : "")
55
+ end
56
+ end
57
+
58
+ RSpec::Matchers.define :pass_query do |expected, info|
59
+ match do |actual|
60
+ @info = if (info.id rescue false)
61
+ info
62
+ elsif info.is_a?(Hash)
63
+ Info.new(info[:id], info[:debug], info[:action], info.fetch(:result, RDF::Literal::TRUE), info[:metadata])
64
+ end
65
+ @info.debug = Array(@info.debug).join("\n")
66
+
67
+ @expected = expected.respond_to?(:read) ? expected.read : expected
68
+
69
+ require 'sparql'
70
+ query = SPARQL.parse(@expected)
71
+ @results = actual.query(query)
72
+
73
+ @results == @info.result
74
+ end
75
+
76
+ failure_message do |actual|
77
+ "#{@info.inspect + "\n"}" +
78
+ if @results.nil?
79
+ "Query failed to return results"
80
+ elsif !@results.is_a?(RDF::Literal::Boolean)
81
+ "Query returned non-boolean results"
82
+ elsif @info.result != @results
83
+ "Query returned false (expected #{@info.result})"
84
+ else
85
+ "Query returned true (expected #{@info.result})"
86
+ end +
87
+ "\n#{@expected}" +
88
+ "\nResults:\n#{@actual.dump(:ttl, standard_prefixes: true, prefixes: {'' => @info.action + '#'})}" +
89
+ (@info.metadata ? "\nMetadata:\n#{@info.metadata.to_json(JSON_STATE)}\n" : "") +
90
+ (@info.metadata && !@info.metadata.errors.empty? ? "\nMetadata Errors:\n#{@info.metadata.errors.join("\n")}\n" : "") +
91
+ "\nDebug:\n#{@info.debug}"
92
+ end
93
+
94
+ failure_message_when_negated do |actual|
95
+ "#{@info.inspect + "\n"}" +
96
+ if @results.nil?
97
+ "Query failed to return results"
98
+ elsif !@results.is_a?(RDF::Literal::Boolean)
99
+ "Query returned non-boolean results"
100
+ elsif @info.expectedResults != @results
101
+ "Query returned false (expected #{@info.result})"
102
+ else
103
+ "Query returned true (expected #{@info.result})"
104
+ end +
105
+ "\n#{@expected}" +
106
+ "\nResults:\n#{@actual.dump(:ttl, standard_prefixes: true, prefixes: {'' => @info.action + '#'})}" +
107
+ (@info.metadata ? "\nMetadata:\n#{@info.metadata.to_json(JSON_STATE)}\n" : "") +
108
+ (@info.metadata && !@info.metadata.errors.empty? ? "\nMetadata Errors:\n#{@info.metadata.errors.join("\n")}\n" : "") +
109
+ "\nDebug:\n#{@info.debug}"
110
+ end
111
+ end
112
+
113
+ RSpec::Matchers.define :produce do |expected, info = []|
114
+ match do |actual|
115
+ @info = if (info.id rescue false)
116
+ info
117
+ elsif info.is_a?(Hash)
118
+ Info.new(info[:id], info[:debug], info[:action], info[:result], info[:metadata])
119
+ elsif info.is_a?(Array)
120
+ Info.new("", info)
121
+ end
122
+ @info.debug = Array(@info.debug).join("\n")
123
+ expect(actual).to eq expected
124
+ end
125
+
126
+ failure_message do |actual|
127
+ "#{@info.inspect + "\n"}" +
128
+ "Expected: #{expected.is_a?(String) ? expected : expected.to_json(JSON_STATE)}\n" +
129
+ "Actual : #{actual.is_a?(String) ? actual : actual.to_json(JSON_STATE)}\n" +
130
+ (@info.metadata ? "\nMetadata:\n#{@info.metadata.to_json(JSON_STATE)}\n" : "") +
131
+ (@info.metadata && !@info.metadata.errors.empty? ? "\nMetadata Errors:\n#{@info.metadata.errors.join("\n")}\n" : "") +
132
+ "Debug:\n#{@info.debug}"
133
+ end
134
+ end
@@ -0,0 +1,1716 @@
1
+ # coding: utf-8
2
+ $:.unshift "."
3
+ require 'spec_helper'
4
+
5
+ describe RDF::Tabular::Metadata do
6
+ before(:each) do
7
+ WebMock.stub_request(:any, %r(.*example.org.*)).
8
+ to_return(lambda {|request|
9
+ file = request.uri.to_s.split('/').last
10
+ content_type = case file
11
+ when /\.json/ then 'application/json'
12
+ when /\.csv/ then 'text/csv'
13
+ else 'text/plain'
14
+ end
15
+
16
+ case file
17
+ when "metadata.json", "country-codes-and-names.csv-metadata.json"
18
+ {status: 401}
19
+ else
20
+ {
21
+ body: File.read(File.expand_path("../data/#{file}", __FILE__)),
22
+ status: 200,
23
+ headers: {'Content-Type' => content_type}
24
+ }
25
+ end
26
+ })
27
+ @debug = []
28
+ end
29
+
30
+ shared_examples "inherited properties" do |allowed = true|
31
+ {
32
+ null: {
33
+ valid: ["foo", %w(foo bar)],
34
+ invalid: [1, true, {}]
35
+ },
36
+ lang: {
37
+ valid: %w(en en-US),
38
+ invalid: %w(1 foo)
39
+ },
40
+ "textDirection" => {
41
+ valid: %w(rtl ltr),
42
+ invalid: %w(foo default)
43
+ },
44
+ separator: {
45
+ valid: %w(, a | :) + [nil],
46
+ invalid: [1, false] + %w(foo ::)
47
+ },
48
+ ordered: {
49
+ valid: [true, false],
50
+ invalid: [nil, "foo", 1, 0, "true", "false", "TrUe", "fAlSe", "1", "0"],
51
+ },
52
+ default: {
53
+ valid: ["foo"],
54
+ invalid: [1, %w(foo bar), true, nil]
55
+ },
56
+ datatype: {
57
+ valid: (%w(anyAtomicType string token language Name NCName boolean gYear number binary datetime any xml html json) +
58
+ [{"base" => "string"}]
59
+ ),
60
+ invalid: [1, true, "foo", "anyType", "anySimpleType", "IDREFS"]
61
+ },
62
+ aboutUrl: {
63
+ valid: ["http://example.org/example.csv#row={_row}", "http://example.org/tree/{on%2Dstreet}/{GID}", "#row.{_row}"],
64
+ invalid: [1, true, nil, %w(foo bar)]
65
+ },
66
+ propertyUrl: {
67
+ valid: [
68
+ "http://example.org/example.csv#col={_name}",
69
+ "http://example.org/tree/{on%2Dstreet}/{GID}",
70
+ "#row.{_row}"
71
+ ],
72
+ invalid: [1, true, %w(foo bar)]
73
+ },
74
+ valueUrl: {
75
+ valid: [
76
+ "http://example.org/example.csv#row={_row}",
77
+ "http://example.org/tree/{on%2Dstreet}/{GID}",
78
+ "#row.{_row}"
79
+ ],
80
+ invalid: [1, true, nil, %w(foo bar)]
81
+ },
82
+ }.each do |prop, params|
83
+ context prop.to_s do
84
+ if allowed
85
+ it "validates" do
86
+ params[:valid].each do |v|
87
+ subject.send("#{prop}=".to_sym, v)
88
+ expect(subject.errors).to be_empty
89
+ end
90
+ end
91
+ it "invalidates" do
92
+ params[:invalid].each do |v|
93
+ subject.send("#{prop}=".to_sym, v)
94
+ subject.valid?
95
+ expect(subject.errors).not_to be_empty
96
+ end
97
+ end
98
+ else
99
+ it "does not allow" do
100
+ params[:valid].each do |v|
101
+ subject.send("#{prop}=".to_sym, v)
102
+ expect(subject.errors).not_to be_empty
103
+ end
104
+ end
105
+ end
106
+ end
107
+ end
108
+ end
109
+
110
+ shared_examples "common properties" do |allowed = true|
111
+ let(:valid) {%w(dc:description dcat:keyword http://schema.org/copyrightHolder)}
112
+ let(:invalid) {%w(foo bar:baz)}
113
+ if allowed
114
+ context "valid JSON-LD" do
115
+ it "allows defined prefixed names and absolute URIs" do
116
+ valid.each do |v|
117
+ subject[v.to_sym] = "foo"
118
+ expect(subject.errors).to be_empty
119
+ end
120
+ end
121
+
122
+ {
123
+ "value object" => %({"@value": "foo"}),
124
+ "value with type" => %({"@value": "1", "@type": "xsd:integer"}),
125
+ "value with language" => %({"@value": "foo", "@language": "en"}),
126
+ "node" => %({"@id": "http://example/foo"}),
127
+ "node with pname type" => %({"@type": "foaf:Person"}),
128
+ "node with URL type" => %({"@type": "http://example/Person"}),
129
+ "node with array type" => %({"@type": ["schema:Person", "foaf:Person"]}),
130
+ "node with term type" => %({"@type": "Table"}),
131
+ "node with term property" => %({"csvw:name": "foo"}),
132
+ "boolean value" => true,
133
+ "integer value" => 1,
134
+ "double value" => 1.1,
135
+ }.each do |name, value|
136
+ specify(name) {
137
+ subject["dc:object"] = value.is_a?(String) ? ::JSON.parse(value) : value
138
+ expect(subject.errors).to be_empty
139
+ }
140
+ end
141
+ end
142
+
143
+ context "invalid JSON-LD" do
144
+ it "Does not allow unknown prefxies or unprefixed names" do
145
+ invalid.each do |v|
146
+ subject[v.to_sym] = "foo"
147
+ expect(subject.errors).not_to be_empty
148
+ end
149
+ end
150
+
151
+ {
152
+ "value with type and language" => %({"@value": "foo", "@type": "xsd:token", "@language": "en"}),
153
+ "@id and @value" => %({"@id": "http://example/", "@value": "foo"}),
154
+ "value with BNode @id" => %({"@id": "_:foo"}),
155
+ "value with BNode @type" => %({"@type": "_:foo"}),
156
+ "value with BNode property" => %({"_:foo": "bar"}),
157
+ "value with @context" => %({"@context": {}, "@id": "http://example/"}),
158
+ "value with @graph" => %({"@graph": {}}),
159
+ }.each do |name, value|
160
+ specify(name) {
161
+ subject["dc:object"] = ::JSON.parse(value)
162
+ expect(subject.errors).not_to be_empty
163
+ }
164
+ end
165
+ end
166
+ else
167
+ it "Does not allow defined prefixed names and absolute URIs" do
168
+ (valid + invalid).each do |v|
169
+ subject[v.to_sym] = "foo"
170
+ expect(subject.errors).not_to be_empty
171
+ end
172
+ end
173
+ end
174
+ end
175
+
176
+ describe RDF::Tabular::Column do
177
+ subject {described_class.new({"name" => "foo"}, base: RDF::URI("http://example.org/base"), debug: @debug)}
178
+ specify {is_expected.to be_valid}
179
+ it_behaves_like("inherited properties")
180
+ it_behaves_like("common properties")
181
+
182
+ it "allows valid name" do
183
+ %w(
184
+ name abc.123 _col.1
185
+ ).each {|v| expect(described_class.new("name" => v)).to be_valid}
186
+ end
187
+
188
+ it "detects invalid names" do
189
+ [1, true, nil, "_foo", "_col=1"].each {|v| expect(described_class.new("name" => v)).not_to be_valid}
190
+ end
191
+
192
+ it "allows absence of name" do
193
+ expect(described_class.new("@type" => "Column")).to be_valid
194
+ expect(described_class.new("@type" => "Column").name).to eql '_col.0'
195
+ end
196
+
197
+ its(:type) {is_expected.to eql :Column}
198
+
199
+ {
200
+ title: {
201
+ valid: ["foo", %w(foo bar), {"en" => "foo", "de" => "bar"}],
202
+ invalid: [1, true, nil]
203
+ },
204
+ required: {
205
+ valid: [true, false],
206
+ invalid: [nil, "foo", 1, 0, "true", "false", "TrUe", "fAlSe", "1", "0"],
207
+ },
208
+ suppressOutput: {
209
+ valid: [true, false],
210
+ invalid: [nil, "foo", 1, 0, "true", "false", "TrUe", "fAlSe", "1", "0"],
211
+ },
212
+ virtual: {
213
+ valid: [true, false],
214
+ invalid: [nil, 1, 0, "true", "false", "TrUe", "fAlSe", "1", "0", "foo"],
215
+ },
216
+ }.each do |prop, params|
217
+ context prop.to_s do
218
+ it "validates" do
219
+ params[:valid].each do |v|
220
+ subject.send("#{prop}=".to_sym, v)
221
+ expect(subject).to be_valid
222
+ end
223
+ end
224
+ it "invalidates" do
225
+ params[:invalid].each do |v|
226
+ subject.send("#{prop}=".to_sym, v)
227
+ expect(subject).not_to be_valid
228
+ end
229
+ end
230
+ end
231
+ end
232
+
233
+ context "title" do
234
+ {
235
+ string: ["foo", {"und" => ["foo"]}],
236
+ }.each do |name, (input, output)|
237
+ it name do
238
+ subject.title = input
239
+ expect(subject.title).to produce(output)
240
+ end
241
+ end
242
+ end
243
+ end
244
+
245
+ describe RDF::Tabular::Schema do
246
+ subject {described_class.new({}, base: RDF::URI("http://example.org/base"), debug: @debug)}
247
+ specify {is_expected.to be_valid}
248
+ it_behaves_like("inherited properties")
249
+ it_behaves_like("common properties")
250
+ its(:type) {is_expected.to eql :Schema}
251
+
252
+ describe "columns" do
253
+ let(:column) {{"name" => "foo"}}
254
+ subject {described_class.new({"columns" => []}, base: RDF::URI("http://example.org/base"), debug: @debug)}
255
+ its(:errors) {is_expected.to be_empty}
256
+
257
+ its(:type) {is_expected.to eql :Schema}
258
+
259
+ it "allows a valid column" do
260
+ v = described_class.new({"columns" => [column]}, base: RDF::URI("http://example.org/base"), debug: @debug)
261
+ expect(v.errors).to be_empty
262
+ end
263
+
264
+ it "is invalid with an invalid column" do
265
+ v = described_class.new({"columns" => [{"name" => "_invalid"}]}, base: RDF::URI("http://example.org/base"), debug: @debug)
266
+ expect(v.errors).not_to be_empty
267
+ end
268
+
269
+ it "is invalid with an non-unique columns" do
270
+ v = described_class.new({"columns" => [column, column]}, base: RDF::URI("http://example.org/base"), debug: @debug)
271
+ expect(v.errors).not_to be_empty
272
+ end
273
+ end
274
+
275
+ describe "primaryKey" do
276
+ let(:column) {{"name" => "foo"}}
277
+ let(:column2) {{"name" => "bar"}}
278
+ subject {described_class.new({"columns" => [column], "primaryKey" => column["name"]}, base: RDF::URI("http://example.org/base"), debug: @debug)}
279
+ specify {is_expected.to be_valid}
280
+
281
+ its(:type) {is_expected.to eql :Schema}
282
+
283
+ it "is invalid if referenced column does not exist" do
284
+ subject[:columns] = []
285
+ expect(subject).not_to be_valid
286
+ end
287
+
288
+ it "is valid with multiple names" do
289
+ v = described_class.new({
290
+ "columns" => [column, column2],
291
+ "primaryKey" => [column["name"], column2["name"]]},
292
+ base: RDF::URI("http://example.org/base"),
293
+ debug: @debug)
294
+ expect(v).to be_valid
295
+ end
296
+
297
+ it "is invalid with multiple names if any column missing" do
298
+ v = described_class.new({
299
+ "columns" => [column],
300
+ "primaryKey" => [column["name"], column2["name"]]},
301
+ base: RDF::URI("http://example.org/base",
302
+ debug: @debug))
303
+ expect(v).not_to be_valid
304
+ end
305
+ end
306
+
307
+ describe "foreignKeys" do
308
+ subject {
309
+ RDF::Tabular::TableGroup.new({
310
+ resources: [{
311
+ url: "a",
312
+ tableSchema: {
313
+ "@id" => "a_s",
314
+ columns: [{name: "a1"}, {name: "a2"}],
315
+ foreignKeys: []
316
+ }
317
+ }, {
318
+ url: "b",
319
+ tableSchema: {
320
+ "@id" => "b_s",
321
+ columns: [{name: "b1"}, {name: "b2"}],
322
+ foreignKeys: []
323
+ }
324
+ }]},
325
+ base: RDF::URI("http://example.org/base"), debug: @debug
326
+ )
327
+ }
328
+ context "valid" do
329
+ {
330
+ "references single column with resource" => {
331
+ "columnReference" => "a1",
332
+ "reference" => {
333
+ "resource" => "b",
334
+ "columnReference" => "b1"
335
+ }
336
+ },
337
+ "references multiple columns with resource" => {
338
+ "columnReference" => ["a1", "a2"],
339
+ "reference" => {
340
+ "resource" => "b",
341
+ "columnReference" => ["b1", "b2"]
342
+ }
343
+ },
344
+ "references single column with tableSchema" => {
345
+ "columnReference" => "a1",
346
+ "reference" => {
347
+ "tableSchema" => "b_s",
348
+ "columnReference" => "b1"
349
+ }
350
+ }
351
+ }.each do |name, fk|
352
+ it name do
353
+ subject.resources.first.tableSchema.foreignKeys << fk
354
+ expect(subject.normalize!.errors).to be_empty
355
+ end
356
+ end
357
+ end
358
+
359
+ context "invalid" do
360
+ {
361
+ "missing source column" => {
362
+ "columnReference" => "not_here",
363
+ "reference" => {
364
+ "resource" => "b",
365
+ "columnReference" => "b1"
366
+ }
367
+ },
368
+ "one missing source column" => {
369
+ "columnReference" => ["a1", "not_here"],
370
+ "reference" => {
371
+ "resource" => "b",
372
+ "columnReference" => ["b1", "b2"]
373
+ }
374
+ },
375
+ "missing destination column" => {
376
+ "columnReference" => "a1",
377
+ "reference" => {
378
+ "resource" => "b",
379
+ "columnReference" => "not_there"
380
+ }
381
+ },
382
+ "missing resource" => {
383
+ "columnReference" => "a1",
384
+ "reference" => {
385
+ "resource" => "not_here",
386
+ "columnReference" => "b1"
387
+ }
388
+ },
389
+ "missing tableSchema" => {
390
+ "columnReference" => "a1",
391
+ "reference" => {
392
+ "schemaReference" => "not_here",
393
+ "columnReference" => "b1"
394
+ }
395
+ },
396
+ "both resource and tableSchema" => {
397
+ "columnReference" => "a1",
398
+ "reference" => {
399
+ "resource" => "b",
400
+ "schemaReference" => "b_s",
401
+ "columnReference" => "b1"
402
+ }
403
+ },
404
+ }.each do |name, fk|
405
+ it name do
406
+ subject.resources.first.tableSchema.foreignKeys << fk
407
+ expect(subject.normalize!.errors).not_to be_empty
408
+ end
409
+ end
410
+ end
411
+ end
412
+ end
413
+
414
+ describe RDF::Tabular::Transformation do
415
+ let(:targetFormat) {"http://example.org/targetFormat"}
416
+ let(:scriptFormat) {"http://example.org/scriptFormat"}
417
+ subject {described_class.new({"url" => "http://example/", "targetFormat" => targetFormat, "scriptFormat" => scriptFormat}, base: RDF::URI("http://example.org/base"), debug: @debug)}
418
+ specify {is_expected.to be_valid}
419
+ it_behaves_like("inherited properties", false)
420
+ it_behaves_like("common properties")
421
+ its(:type) {is_expected.to eql :Transformation}
422
+
423
+ {
424
+ source: {
425
+ valid: %w(json rdf) + [nil],
426
+ invalid: [1, true, {}]
427
+ },
428
+ }.each do |prop, params|
429
+ context prop.to_s do
430
+ it "validates" do
431
+ params[:valid].each do |v|
432
+ subject.send("#{prop}=".to_sym, v)
433
+ expect(subject).to be_valid
434
+ end
435
+ end
436
+ it "invalidates" do
437
+ params[:invalid].each do |v|
438
+ subject.send("#{prop}=".to_sym, v)
439
+ expect(subject).not_to be_valid
440
+ end
441
+ end
442
+ end
443
+ end
444
+
445
+ context "title" do
446
+ {
447
+ string: ["foo", {"und" => ["foo"]}],
448
+ }.each do |name, (input, output)|
449
+ it name do
450
+ subject.title = input
451
+ expect(subject.title).to produce(output)
452
+ end
453
+ end
454
+ end
455
+ end
456
+
457
+ describe RDF::Tabular::Dialect do
458
+ subject {described_class.new({}, base: RDF::URI("http://example.org/base"), debug: @debug)}
459
+ specify {is_expected.to be_valid}
460
+ it_behaves_like("inherited properties", false)
461
+ it_behaves_like("common properties", false)
462
+ its(:type) {is_expected.to eql :Dialect}
463
+
464
+ described_class.const_get(:DIALECT_DEFAULTS).each do |p, v|
465
+ context "#{p}" do
466
+ it "retrieves #{v.inspect} by default" do
467
+ expect(subject.send(p)).to eql v
468
+ end
469
+
470
+ it "retrieves set value" do
471
+ subject[p] = "foo"
472
+ expect(subject.send(p)).to eql "foo"
473
+ end
474
+ end
475
+ end
476
+
477
+ describe "#embedded_metadata" do
478
+ {
479
+ "with defaults" => {
480
+ input: "https://example.org/countries.csv",
481
+ result: %({
482
+ "@context": "http://www.w3.org/ns/csvw",
483
+ "@type": "Table",
484
+ "url": "https://example.org/countries.csv",
485
+ "tableSchema": {
486
+ "@type": "Schema",
487
+ "columns": [
488
+ {"title": {"und": ["countryCode"]}},
489
+ {"title": {"und": ["latitude"]}},
490
+ {"title": {"und": ["longitude"]}},
491
+ {"title": {"und": ["name"]}}
492
+ ]
493
+ }
494
+ })
495
+ },
496
+ "with skipRows" => {
497
+ input: "https://example.org/countries.csv",
498
+ dialect: {skipRows: 1},
499
+ result: %({
500
+ "@context": "http://www.w3.org/ns/csvw",
501
+ "@type": "Table",
502
+ "url": "https://example.org/countries.csv",
503
+ "tableSchema": {
504
+ "@type": "Schema",
505
+ "columns": [
506
+ {"title": {"und": ["AD"]}},
507
+ {"title": {"und": ["42.546245"]}},
508
+ {"title": {"und": ["1.601554"]}},
509
+ {"title": {"und": ["Andorra"]}}
510
+ ]
511
+ },
512
+ "rdfs:comment": ["countryCode,latitude,longitude,name"]
513
+ })
514
+ },
515
+ "delimiter" => {
516
+ input: "https://example.org/tree-ops.tsv",
517
+ dialect: {delimiter: "\t"},
518
+ result: %({
519
+ "@context": "http://www.w3.org/ns/csvw",
520
+ "@type": "Table",
521
+ "url": "https://example.org/tree-ops.tsv",
522
+ "tableSchema": {
523
+ "@type": "Schema",
524
+ "columns": [
525
+ {"title": {"und": ["GID"]}},
526
+ {"title": {"und": ["On Street"]}},
527
+ {"title": {"und": ["Species"]}},
528
+ {"title": {"und": ["Trim Cycle"]}},
529
+ {"title": {"und": ["Inventory Date"]}}
530
+ ]
531
+ }
532
+ })
533
+ },
534
+ "headerColumnCount" => {
535
+ input: "https://example.org/tree-ops.csv",
536
+ dialect: {headerColumnCount: 1},
537
+ result: %({
538
+ "@context": "http://www.w3.org/ns/csvw",
539
+ "@type": "Table",
540
+ "url": "https://example.org/tree-ops.csv",
541
+ "tableSchema": {
542
+ "@type": "Schema",
543
+ "columns": [
544
+ {"title": {"und": ["On Street"]}},
545
+ {"title": {"und": ["Species"]}},
546
+ {"title": {"und": ["Trim Cycle"]}},
547
+ {"title": {"und": ["Inventory Date"]}}
548
+ ]
549
+ }
550
+ })
551
+ },
552
+ }.each do |name, props|
553
+ it name do
554
+ dialect = if props[:dialect]
555
+ described_class.new(props[:dialect], base: RDF::URI("http://example.org/base"), debug: @debug)
556
+ else
557
+ subject
558
+ end
559
+
560
+ result = dialect.embedded_metadata(props[:input])
561
+ expect(::JSON.parse(result.to_json(JSON_STATE))).to produce(::JSON.parse(props[:result]), @debug)
562
+ end
563
+ end
564
+ end
565
+ end
566
+
567
+ describe RDF::Tabular::Table do
568
+ subject {described_class.new({"url" => "http://example.org/table.csv"}, base: RDF::URI("http://example.org/base"), debug: @debug)}
569
+ specify {is_expected.to be_valid}
570
+ it_behaves_like("inherited properties")
571
+ it_behaves_like("common properties")
572
+ its(:type) {is_expected.to eql :Table}
573
+
574
+ {
575
+ tableSchema: {
576
+ valid: [RDF::Tabular::Schema.new({})],
577
+ invalid: [1, true, nil]
578
+ },
579
+ notes: {
580
+ valid: [{}, [{}]],
581
+ invalid: [1, true, nil]
582
+ },
583
+ tableDirection: {
584
+ valid: %w(rtl ltr default),
585
+ invalid: %w(foo true 1)
586
+ },
587
+ transformations: {
588
+ valid: [[RDF::Tabular::Transformation.new(url: "http://example", targetFormat: "http://example", scriptFormat: "http://example/")]],
589
+ invalid: [RDF::Tabular::Transformation.new(url: "http://example", targetFormat: "http://example", scriptFormat: "http://example/")] +
590
+ %w(foo true 1)
591
+ },
592
+ dialect: {
593
+ valid: [{skipRows: 1}],
594
+ invalid: ["http://location-of-dialect", "foo"]
595
+ },
596
+ suppressOutput: {
597
+ valid: [true, false],
598
+ invalid: [nil, "foo", 1, 0, "true", "false", "TrUe", "fAlSe", "1", "0"],
599
+ },
600
+ }.each do |prop, params|
601
+ context prop.to_s do
602
+ it "validates" do
603
+ params[:valid].each do |v|
604
+ subject.send("#{prop}=".to_sym, v)
605
+ expect(subject).to be_valid
606
+ end
607
+ end
608
+ it "invalidates" do
609
+ params[:invalid].each do |v|
610
+ subject.send("#{prop}=".to_sym, v)
611
+ expect(subject).not_to be_valid
612
+ end
613
+ end
614
+ end
615
+ end
616
+ end
617
+
618
+ describe RDF::Tabular::TableGroup do
619
+ let(:table) {{"url" => "http://example.org/table.csv"}}
620
+ subject {described_class.new({"resources" => [table]}, base: RDF::URI("http://example.org/base"), debug: @debug)}
621
+ specify {is_expected.to be_valid}
622
+
623
+ it_behaves_like("inherited properties")
624
+ it_behaves_like("common properties")
625
+ its(:type) {is_expected.to eql :TableGroup}
626
+
627
+
628
+ {
629
+ tableSchema: {
630
+ valid: [RDF::Tabular::Schema.new({})],
631
+ invalid: [1, true, nil]
632
+ },
633
+ tableDirection: {
634
+ valid: %w(rtl ltr default),
635
+ invalid: %w(foo true 1)
636
+ },
637
+ dialect: {
638
+ valid: [{skipRows: 1}],
639
+ invalid: ["http://location-of-dialect", "foo"]
640
+ },
641
+ transformations: {
642
+ valid: [[RDF::Tabular::Transformation.new(url: "http://example", targetFormat: "http://example", scriptFormat: "http://example/")]],
643
+ invalid: [RDF::Tabular::Transformation.new(url: "http://example", targetFormat: "http://example", scriptFormat: "http://example/")] +
644
+ %w(foo true 1)
645
+ },
646
+ notes: {
647
+ valid: [{}, [{}]],
648
+ invalid: [1, true, nil]
649
+ },
650
+ }.each do |prop, params|
651
+ context prop.to_s do
652
+ it "validates" do
653
+ params[:valid].each do |v|
654
+ subject.send("#{prop}=".to_sym, v)
655
+ expect(subject).to be_valid
656
+ end
657
+ end
658
+ it "invalidates" do
659
+ params[:invalid].each do |v|
660
+ subject.send("#{prop}=".to_sym, v)
661
+ expect(subject).not_to be_valid
662
+ end
663
+ end
664
+ end
665
+ end
666
+ end
667
+
668
+ context "parses example metadata" do
669
+ Dir.glob(File.expand_path("../data/*.json", __FILE__)).each do |filename|
670
+ next if filename =~ /-(atd|standard|minimal|roles).json/
671
+ context filename do
672
+ subject {RDF::Tabular::Metadata.open(filename)}
673
+ its(:errors) {is_expected.to be_empty}
674
+ its(:filenames) {is_expected.to include("file:#{filename}")}
675
+ end
676
+ end
677
+ end
678
+
679
+ context "parses invalid metadata" do
680
+ Dir.glob(File.expand_path("../invalid_data/*.json", __FILE__)).each do |filename|
681
+ context filename do
682
+ subject {RDF::Tabular::Metadata.open(filename)}
683
+ File.foreach(filename.sub(".json", "-errors.txt")) do |err|
684
+ its(:errors) {is_expected.to include(err)}
685
+ end
686
+ end
687
+ end
688
+ end
689
+
690
+ context "object properties" do
691
+ let(:table) {{"url" => "http://example.org/table.csv", "@type" => "Table"}}
692
+ it "loads referenced schema" do
693
+ table[:tableSchema] = "http://example.org/schema"
694
+ expect(described_class).to receive(:open).with(table[:tableSchema], kind_of(Hash)).and_return(RDF::Tabular::Schema.new({"@type" => "Schema"}))
695
+ described_class.new(table, base: RDF::URI("http://example.org/base"), debug: @debug)
696
+ end
697
+ it "loads referenced dialect" do
698
+ table[:dialect] = "http://example.org/dialect"
699
+ expect(described_class).to receive(:open).with(table[:dialect], kind_of(Hash)).and_return(RDF::Tabular::Dialect.new({}))
700
+ described_class.new(table, base: RDF::URI("http://example.org/base"), debug: @debug)
701
+ end
702
+ end
703
+
704
+ context "inherited properties" do
705
+ let(:table) {{"url" => "http://example.org/table.csv", "tableSchema" => {"@type" => "Schema"}, "@type" => "Table"}}
706
+ subject {described_class.new(table, base: RDF::URI("http://example.org/base"), debug: @debug)}
707
+
708
+ it "inherits properties from parent" do
709
+ subject.lang = "en"
710
+ expect(subject.tableSchema.lang).to eql "en"
711
+ end
712
+
713
+ it "overrides properties in parent" do
714
+ subject.lang = "en"
715
+ subject.tableSchema.lang = "de"
716
+ expect(subject.tableSchema.lang).to eql "de"
717
+ end
718
+ end
719
+
720
+ describe ".open" do
721
+ context "validates example metadata" do
722
+ Dir.glob(File.expand_path("../data/*.json", __FILE__)).each do |filename|
723
+ next if filename =~ /-(atd|standard|minimal|roles).json/
724
+ context filename do
725
+ subject {RDF::Tabular::Metadata.open(filename, debug: @debug)}
726
+ its(:errors) {is_expected.to produce([], @debug)}
727
+ its(:filenames) {is_expected.to include("file:#{filename}")}
728
+ end
729
+ end
730
+ end
731
+ end
732
+
733
+ describe ".from_input" do
734
+ it "FIXME"
735
+ end
736
+
737
+ describe ".new" do
738
+ context "intuits subclass" do
739
+ {
740
+ ":type TableGroup" => [{}, {type: :TableGroup}, RDF::Tabular::TableGroup],
741
+ ":type Table" => [{}, {type: :Table}, RDF::Tabular::Table],
742
+ ":type Transformation" => [{}, {type: :Transformation}, RDF::Tabular::Transformation],
743
+ ":type Schema" => [{}, {type: :Schema}, RDF::Tabular::Schema],
744
+ ":type Column" => [{}, {type: :Column}, RDF::Tabular::Column],
745
+ ":type Dialect" => [{}, {type: :Dialect}, RDF::Tabular::Dialect],
746
+ "@type TableGroup" => [{"@type" => "TableGroup"}, RDF::Tabular::TableGroup],
747
+ "@type Table" => [{"@type" => "Table"}, RDF::Tabular::Table],
748
+ "@type Transformation" => [{"@type" => "Transformation"}, RDF::Tabular::Transformation],
749
+ "@type Schema" => [{"@type" => "Schema"}, RDF::Tabular::Schema],
750
+ "@type Column" => [{"@type" => "Column"}, RDF::Tabular::Column],
751
+ "@type Dialect" => [{"@type" => "Dialect"}, RDF::Tabular::Dialect],
752
+ "resources TableGroup" => [{"resources" => []}, RDF::Tabular::TableGroup],
753
+ "dialect Table" => [{"dialect" => {}}, RDF::Tabular::Table],
754
+ "tableSchema Table" => [{"tableSchema" => {}}, RDF::Tabular::Table],
755
+ "transformations Table" => [{"transformations" => []}, RDF::Tabular::Table],
756
+ "targetFormat Transformation" => [{"targetFormat" => "foo"}, RDF::Tabular::Transformation],
757
+ "scriptFormat Transformation" => [{"scriptFormat" => "foo"}, RDF::Tabular::Transformation],
758
+ "source Transformation" => [{"source" => "foo"}, RDF::Tabular::Transformation],
759
+ "columns Schema" => [{"columns" => []}, RDF::Tabular::Schema],
760
+ "primaryKey Schema" => [{"primaryKey" => "foo"}, RDF::Tabular::Schema],
761
+ "foreignKeys Schema" => [{"foreignKeys" => []}, RDF::Tabular::Schema],
762
+ "urlTemplate Schema" => [{"urlTemplate" => "foo"}, RDF::Tabular::Schema],
763
+ "commentPrefix Dialect" => [{"commentPrefix" => "#"}, RDF::Tabular::Dialect],
764
+ "delimiter Dialect" => [{"delimiter" => ","}, RDF::Tabular::Dialect],
765
+ "doubleQuote Dialect" => [{"doubleQuote" => true}, RDF::Tabular::Dialect],
766
+ "encoding Dialect" => [{"encoding" => "utf-8"}, RDF::Tabular::Dialect],
767
+ "header Dialect" => [{"header" => true}, RDF::Tabular::Dialect],
768
+ "headerColumnCount Dialect" => [{"headerColumnCount" => 0}, RDF::Tabular::Dialect],
769
+ "headerRowCount Dialect" => [{"headerRowCount" => 1}, RDF::Tabular::Dialect],
770
+ "lineTerminator Dialect" => [{"lineTerminator" => "\r\n"}, RDF::Tabular::Dialect],
771
+ "quoteChar Dialect" => [{"quoteChar" => "\""}, RDF::Tabular::Dialect],
772
+ "skipBlankRows Dialect" => [{"skipBlankRows" => true}, RDF::Tabular::Dialect],
773
+ "skipColumns Dialect" => [{"skipColumns" => 0}, RDF::Tabular::Dialect],
774
+ "skipInitialSpace Dialect" => [{"skipInitialSpace" => "start"}, RDF::Tabular::Dialect],
775
+ "skipRows Dialect" => [{"skipRows" => 1}, RDF::Tabular::Dialect],
776
+ "trim Dialect" => [{"trim" => true}, RDF::Tabular::Dialect],
777
+ }.each do |name, args|
778
+ it name do
779
+ klass = args.pop
780
+ expect(described_class.new(*args)).to be_a(klass)
781
+ end
782
+ end
783
+ end
784
+ end
785
+
786
+ describe "#each_row" do
787
+ subject {
788
+ described_class.new(JSON.parse(%({
789
+ "url": "https://example.org/countries.csv",
790
+ "@type": "Table",
791
+ "tableSchema": {
792
+ "@type": "Schema",
793
+ "columns": [{
794
+ "name": "countryCode",
795
+ "title": "countryCode",
796
+ "propertyUrl": "https://example.org/countries.csv#countryCode"
797
+ }, {
798
+ "name": "latitude",
799
+ "title": "latitude",
800
+ "propertyUrl": "https://example.org/countries.csv#latitude"
801
+ }, {
802
+ "name": "longitude",
803
+ "title": "longitude",
804
+ "propertyUrl": "https://example.org/countries.csv#longitude"
805
+ }, {
806
+ "name": "name",
807
+ "title": "name",
808
+ "propertyUrl": "https://example.org/countries.csv#name"
809
+ }]
810
+ }
811
+ })), base: RDF::URI("http://example.org/base"), debug: @debug)
812
+ }
813
+ let(:input) {RDF::Util::File.open_file("https://example.org/countries.csv")}
814
+
815
+ specify {expect {|b| subject.each_row(input, &b)}.to yield_control.exactly(3)}
816
+
817
+ it "returns consecutive row numbers" do
818
+ nums = subject.to_enum(:each_row, input).map(&:number)
819
+ expect(nums).to eql([1, 2, 3])
820
+ end
821
+
822
+ it "returns cells" do
823
+ subject.each_row(input) do |row|
824
+ expect(row).to be_a(RDF::Tabular::Row)
825
+ expect(row.values.length).to eql 4
826
+ expect(row.values.map(&:class).compact).to include(RDF::Tabular::Row::Cell)
827
+ end
828
+ end
829
+
830
+ it "has nil aboutUrls" do
831
+ subject.each_row(input) do |row|
832
+ expect(row.values[0].aboutUrl).to be_nil
833
+ expect(row.values[1].aboutUrl).to be_nil
834
+ expect(row.values[2].aboutUrl).to be_nil
835
+ expect(row.values[3].aboutUrl).to be_nil
836
+ end
837
+ end
838
+
839
+ it "has expected propertyUrls" do
840
+ subject.each_row(input) do |row|
841
+ expect(row.values[0].propertyUrl).to eq "https://example.org/countries.csv#countryCode"
842
+ expect(row.values[1].propertyUrl).to eq "https://example.org/countries.csv#latitude"
843
+ expect(row.values[2].propertyUrl).to eq "https://example.org/countries.csv#longitude"
844
+ expect(row.values[3].propertyUrl).to eq "https://example.org/countries.csv#name"
845
+ end
846
+ end
847
+
848
+ it "has expected valueUrls" do
849
+ subject.each_row(input) do |row|
850
+ expect(row.values[0].valueUrl).to be_nil
851
+ expect(row.values[1].valueUrl).to be_nil
852
+ expect(row.values[2].valueUrl).to be_nil
853
+ expect(row.values[3].valueUrl).to be_nil
854
+ end
855
+ end
856
+
857
+ it "has expected values" do
858
+ rows = subject.to_enum(:each_row, input).to_a
859
+ expect(rows[0].values.map(&:to_s)).to produce(%w(AD 42.546245 1.601554 Andorra), @debug)
860
+ expect(rows[1].values.map(&:to_s)).to produce((%w(AE 23.424076 53.847818) << "United Arab Emirates"), @debug)
861
+ expect(rows[2].values.map(&:to_s)).to produce(%w(AF 33.93911 67.709953 Afghanistan), @debug)
862
+ end
863
+
864
+ context "URL expansion" do
865
+ subject {
866
+ described_class.new(JSON.parse(%({
867
+ "url": "https://example.org/countries.csv",
868
+ "tableSchema": {
869
+ "columns": [
870
+ {"title": "addressCountry"},
871
+ {"title": "latitude"},
872
+ {"title": "longitude"},
873
+ {"title": "name"}
874
+ ]
875
+ }
876
+ })), base: RDF::URI("http://example.org/base"), debug: @debug)
877
+ }
878
+ let(:input) {RDF::Util::File.open_file("https://example.org/countries.csv")}
879
+
880
+ {
881
+ "default title" => {
882
+ aboutUrl: [RDF::Node, RDF::Node, RDF::Node, RDF::Node],
883
+ propertyUrl: [nil, nil, nil, nil],
884
+ valueUrl: [nil, nil, nil, nil],
885
+ md: {"url" => "https://example.org/countries.csv", "tableSchema" => {
886
+ "columns" => [
887
+ {"title" => "addressCountry"},
888
+ {"title" => "latitude"},
889
+ {"title" => "longitude"},
890
+ {"title" => "name"}
891
+ ]
892
+ }
893
+ }
894
+ },
895
+ "schema transformations" => {
896
+ aboutUrl: %w(#addressCountry #latitude #longitude #name),
897
+ propertyUrl: %w(?_name=addressCountry ?_name=latitude ?_name=longitude ?_name=name),
898
+ valueUrl: %w(addressCountry latitude longitude name),
899
+ md: {
900
+ "url" => "https://example.org/countries.csv",
901
+ "tableSchema" => {
902
+ "aboutUrl" => "{#_name}",
903
+ "propertyUrl" => '{?_name}',
904
+ "valueUrl" => '{_name}',
905
+ "columns" => [
906
+ {"title" => "addressCountry"},
907
+ {"title" => "latitude"},
908
+ {"title" => "longitude"},
909
+ {"title" => "name"}
910
+ ]
911
+ }
912
+ }
913
+ },
914
+ "PNames" => {
915
+ aboutUrl: [RDF::SCHEMA.addressCountry, RDF::SCHEMA.latitude, RDF::SCHEMA.longitude, RDF::SCHEMA.name],
916
+ propertyUrl: [RDF::SCHEMA.addressCountry, RDF::SCHEMA.latitude, RDF::SCHEMA.longitude, RDF::SCHEMA.name],
917
+ valueUrl: [RDF::SCHEMA.addressCountry, RDF::SCHEMA.latitude, RDF::SCHEMA.longitude, RDF::SCHEMA.name],
918
+ md: {
919
+ "url" => "https://example.org/countries.csv",
920
+ "tableSchema" => {
921
+ "aboutUrl" => 'http://schema.org/{_name}',
922
+ "propertyUrl" => 'schema:{_name}',
923
+ "valueUrl" => 'schema:{_name}',
924
+ "columns" => [
925
+ {"title" => "addressCountry"},
926
+ {"title" => "latitude"},
927
+ {"title" => "longitude"},
928
+ {"title" => "name"}
929
+ ]
930
+ }
931
+ }
932
+ },
933
+ }.each do |name, props|
934
+ context name do
935
+ let(:md) {RDF::Tabular::Table.new(props[:md]).merge(subject).resources.first}
936
+ let(:cells) {md.to_enum(:each_row, input).to_a.first.values}
937
+ let(:aboutUrls) {props[:aboutUrl].map {|u| u.is_a?(String) ? md.url.join(u) : u}}
938
+ let(:propertyUrls) {props[:propertyUrl].map {|u| u.is_a?(String) ? md.url.join(u) : u}}
939
+ let(:valueUrls) {props[:valueUrl].map {|u| u.is_a?(String) ? md.url.join(u) : u}}
940
+ it "aboutUrl is #{props[:aboutUrl]}" do
941
+ if aboutUrls.first == RDF::Node
942
+ expect(cells.map(&:aboutUrl)).to all(be_nil)
943
+ else
944
+ expect(cells.map(&:aboutUrl)).to include(*aboutUrls)
945
+ end
946
+ end
947
+ it "propertyUrl is #{props[:propertyUrl]}" do
948
+ expect(cells.map(&:propertyUrl)).to include(*propertyUrls)
949
+ end
950
+ it "valueUrl is #{props[:valueUrl]}" do
951
+ expect(cells.map(&:valueUrl)).to include(*valueUrls)
952
+ end
953
+ end
954
+ end
955
+ end
956
+ it "expands aboutUrl in cells"
957
+
958
+ context "variations" do
959
+ {
960
+ "skipRows" => {dialect: {skipRows: 1}},
961
+ "headerRowCount" => {dialect: {headerRowCount: 0}},
962
+ "skipRows + headerRowCount" => {dialect: {skipRows: 1, headerRowCount: 0}},
963
+ "skipColumns" => {dialect: {skipColumns: 1}},
964
+ "headerColumnCount" => {dialect: {headerColumnCount: 0}},
965
+ "skipColumns + headerColumnCount" => {dialect: {skipColumns: 1, headerColumnCount: 0}},
966
+ }.each do |name, props|
967
+ context name do
968
+ subject {
969
+ raw = JSON.parse(%({
970
+ "url": "https://example.org/countries.csv",
971
+ "@type": "Table",
972
+ "tableSchema": {
973
+ "@type": "Schema",
974
+ "columns": [{
975
+ "name": "countryCode",
976
+ "title": "countryCode",
977
+ "propertyUrl": "https://example.org/countries.csv#countryCode"
978
+ }, {
979
+ "name": "latitude",
980
+ "title": "latitude",
981
+ "propertyUrl": "https://example.org/countries.csv#latitude"
982
+ }, {
983
+ "name": "longitude",
984
+ "title": "longitude",
985
+ "propertyUrl": "https://example.org/countries.csv#longitude"
986
+ }, {
987
+ "name": "name",
988
+ "title": "name",
989
+ "propertyUrl": "https://example.org/countries.csv#name"
990
+ }]
991
+ }
992
+ }))
993
+ raw["dialect"] = props[:dialect]
994
+ described_class.new(raw, base: RDF::URI("http://example.org/base"), debug: @debug)
995
+ }
996
+ let(:rows) {subject.to_enum(:each_row, input).to_a}
997
+ let(:rowOffset) {props[:dialect].fetch(:skipRows, 0) + props[:dialect].fetch(:headerRowCount, 1)}
998
+ let(:columnOffset) {props[:dialect].fetch(:skipColumns, 0) + props[:dialect].fetch(:headerColumnCount, 0)}
999
+ it "has expected number attributes" do
1000
+ nums = [1, 2, 3, 4]
1001
+ nums = nums.first(nums.length - rowOffset)
1002
+ expect(rows.map(&:number)).to eql nums
1003
+ end
1004
+ it "has expected sourceNumber attributes" do
1005
+ nums = [1, 2, 3, 4].map {|n| n + rowOffset}
1006
+ nums = nums.first(nums.length - rowOffset)
1007
+ expect(rows.map(&:sourceNumber)).to eql nums
1008
+ end
1009
+ it "has expected column.number attributes" do
1010
+ nums = [1, 2, 3, 4]
1011
+ nums = nums.first(nums.length - columnOffset)
1012
+ expect(rows.first.values.map {|c| c.column.number}).to eql nums
1013
+ end
1014
+ it "has expected column.sourceNumber attributes" do
1015
+ nums = [1, 2, 3, 4].map {|n| n + columnOffset}
1016
+ nums = nums.first(nums.length - columnOffset)
1017
+ expect(rows.first.values.map {|c| c.column.sourceNumber}).to eql nums
1018
+ end
1019
+ end
1020
+ end
1021
+ end
1022
+
1023
+ context "datatypes" do
1024
+ {
1025
+ # Strings
1026
+ "string with no constraints" => {base: "string", value: "foo", result: "foo"},
1027
+ "string with matching length" => {base: "string", value: "foo", length: 3, result: "foo"},
1028
+ "string with wrong length" => {
1029
+ base: "string",
1030
+ value: "foo",
1031
+ length: 4,
1032
+ errors: ["foo does not have length 4"]
1033
+ },
1034
+ "string with wrong maxLength" => {
1035
+ base: "string",
1036
+ value: "foo",
1037
+ maxLength: 2,
1038
+ errors: ["foo does not have length <= 2"]
1039
+ },
1040
+ "string with wrong minLength" => {
1041
+ base: "string",
1042
+ value: "foo",
1043
+ minLength: 4,
1044
+ errors: ["foo does not have length >= 4"]
1045
+ },
1046
+
1047
+ # Numbers
1048
+ "decimal with no constraints" => {
1049
+ base: "decimal",
1050
+ value: "4"
1051
+ },
1052
+ "decimal with matching pattern" => {
1053
+ base: "decimal",
1054
+ pattern: '\d{3}',
1055
+ value: "123"
1056
+ },
1057
+ "decimal with wrong pattern" => {
1058
+ base: "decimal",
1059
+ pattern: '\d{4}',
1060
+ value: "123",
1061
+ errors: [/123 does not match pattern/]
1062
+ },
1063
+ "decimal with implicit groupChar" => {
1064
+ base: "decimal",
1065
+ value: %("123,456.789"),
1066
+ result: "123456.789"
1067
+ },
1068
+ "decimal with explicit groupChar" => {
1069
+ base: "decimal",
1070
+ groupChar: ";",
1071
+ value: "123;456.789",
1072
+ result: "123456.789"
1073
+ },
1074
+ "decimal with repeated groupChar" => {
1075
+ base: "decimal",
1076
+ groupChar: ";",
1077
+ value: "123;;456.789",
1078
+ result: "123;;456.789",
1079
+ errors: [/has repeating/]
1080
+ },
1081
+ "decimal with explicit decimalChar" => {
1082
+ base: "decimal",
1083
+ decimalChar: ";",
1084
+ value: "123456;789",
1085
+ result: "123456.789"
1086
+ },
1087
+ "invalid decimal" => {
1088
+ base: "decimal",
1089
+ value: "123456.789e10",
1090
+ result: "123456.789e10",
1091
+ errors: ["123456.789e10 is not a valid decimal"]
1092
+ },
1093
+ "decimal with percent" => {
1094
+ base: "decimal",
1095
+ value: "123456.789%",
1096
+ result: "1234.56789"
1097
+ },
1098
+ "decimal with per-mille" => {
1099
+ base: "decimal",
1100
+ value: "123456.789‰",
1101
+ result: "123.456789"
1102
+ },
1103
+ "valid integer" => {base: "integer", value: "1234"},
1104
+ "invalid integer" => {base: "integer", value: "1234.56", errors: ["1234.56 is not a valid integer"]},
1105
+ "valid long" => {base: "long", value: "1234"},
1106
+ "invalid long" => {base: "long", value: "1234.56", errors: ["1234.56 is not a valid long"]},
1107
+ "valid short" => {base: "short", value: "1234"},
1108
+ "invalid short" => {base: "short", value: "1234.56", errors: ["1234.56 is not a valid short"]},
1109
+ "valid byte" => {base: "byte", value: "123"},
1110
+ "invalid byte" => {base: "byte", value: "1234", errors: ["1234 is not a valid byte"]},
1111
+ "valid unsignedLong" => {base: "unsignedLong", value: "1234"},
1112
+ "invalid unsignedLong" => {base: "unsignedLong", value: "-1234", errors: ["-1234 is not a valid unsignedLong"]},
1113
+ "valid unsignedShort" => {base: "unsignedShort", value: "1234"},
1114
+ "invalid unsignedShort" => {base: "unsignedShort", value: "-1234", errors: ["-1234 is not a valid unsignedShort"]},
1115
+ "valid unsignedByte" => {base: "unsignedByte", value: "123"},
1116
+ "invalid unsignedByte" => {base: "unsignedByte", value: "-123", errors: ["-123 is not a valid unsignedByte"]},
1117
+ "valid positiveInteger" => {base: "positiveInteger", value: "123"},
1118
+ "invalid positiveInteger" => {base: "positiveInteger", value: "-123", errors: ["-123 is not a valid positiveInteger"]},
1119
+ "valid negativeInteger" => {base: "negativeInteger", value: "-123"},
1120
+ "invalid negativeInteger" => {base: "negativeInteger", value: "123", errors: ["123 is not a valid negativeInteger"]},
1121
+ "valid nonPositiveInteger" => {base: "nonPositiveInteger", value: "0"},
1122
+ "invalid nonPositiveInteger" => {base: "nonPositiveInteger", value: "1", errors: ["1 is not a valid nonPositiveInteger"]},
1123
+ "valid nonNegativeInteger" => {base: "nonNegativeInteger", value: "0"},
1124
+ "invalid nonNegativeInteger" => {base: "nonNegativeInteger", value: "-1", errors: ["-1 is not a valid nonNegativeInteger"]},
1125
+ "valid double" => {base: "double", value: "1234.456E789"},
1126
+ "invalid double" => {base: "double", value: "1z", errors: ["1z is not a valid double"]},
1127
+ "NaN double" => {base: "double", value: "NaN"},
1128
+ "INF double" => {base: "double", value: "INF"},
1129
+ "-INF double" => {base: "double", value: "-INF"},
1130
+ "valid number" => {base: "number", value: "1234.456E789"},
1131
+ "invalid number" => {base: "number", value: "1z", errors: ["1z is not a valid number"]},
1132
+ "NaN number" => {base: "number", value: "NaN"},
1133
+ "INF number" => {base: "number", value: "INF"},
1134
+ "-INF number" => {base: "number", value: "-INF"},
1135
+ "valid float" => {base: "float", value: "1234.456E789"},
1136
+ "invalid float" => {base: "float", value: "1z", errors: ["1z is not a valid float"]},
1137
+ "NaN float" => {base: "float", value: "NaN"},
1138
+ "INF float" => {base: "float", value: "INF"},
1139
+ "-INF float" => {base: "float", value: "-INF"},
1140
+
1141
+ # Booleans
1142
+ "valid boolean true" => {base: "boolean", value: "true"},
1143
+ "valid boolean false" => {base: "boolean", value: "false"},
1144
+ "valid boolean 1" => {base: "boolean", value: "1", result: "true"},
1145
+ "valid boolean 0" => {base: "boolean", value: "0", result: "false"},
1146
+ "valid boolean Y|N Y" => {base: "boolean", value: "Y", format: "Y|N", result: "true"},
1147
+ "valid boolean Y|N N" => {base: "boolean", value: "N", format: "Y|N", result: "false"},
1148
+
1149
+ # Dates
1150
+ "validate date yyyy-MM-dd" => {base: "date", value: "2015-03-22", format: "yyyy-MM-dd", result: "2015-03-22"},
1151
+ "validate date yyyyMMdd" => {base: "date", value: "20150322", format: "yyyyMMdd", result: "2015-03-22"},
1152
+ "validate date dd-MM-yyyy" => {base: "date", value: "22-03-2015", format: "dd-MM-yyyy", result: "2015-03-22"},
1153
+ "validate date d-M-yyyy" => {base: "date", value: "22-3-2015", format: "d-M-yyyy", result: "2015-03-22"},
1154
+ "validate date MM-dd-yyyy" => {base: "date", value: "03-22-2015", format: "MM-dd-yyyy", result: "2015-03-22"},
1155
+ "validate date M/d/yyyy" => {base: "date", value: "3/22/2015", format: "M-d-yyyy", result: "2015-03-22"},
1156
+ "validate date dd/MM/yyyy" => {base: "date", value: "22/03/2015", format: "dd/MM/yyyy", result: "2015-03-22"},
1157
+ "validate date d/M/yyyy" => {base: "date", value: "22/3/2015", format: "d/M/yyyy", result: "2015-03-22"},
1158
+ "validate date MM/dd/yyyy" => {base: "date", value: "03/22/2015", format: "MM/dd/yyyy", result: "2015-03-22"},
1159
+ "validate date M/d/yyyy" => {base: "date", value: "3/22/2015", format: "M/d/yyyy", result: "2015-03-22"},
1160
+ "validate date dd.MM.yyyy" => {base: "date", value: "22.03.2015", format: "dd.MM.yyyy", result: "2015-03-22"},
1161
+ "validate date d.M.yyyy" => {base: "date", value: "22.3.2015", format: "d.M.yyyy", result: "2015-03-22"},
1162
+ "validate date MM.dd.yyyy" => {base: "date", value: "03.22.2015", format: "MM.dd.yyyy", result: "2015-03-22"},
1163
+ "validate date M.d.yyyy" => {base: "date", value: "3.22.2015", format: "M.d.yyyy", result: "2015-03-22"},
1164
+
1165
+ # Times
1166
+ "valid time HH:mm:ss" => {base: "time", value: "15:02:37", format: "HH:mm:ss", result: "15:02:37"},
1167
+ "valid time HHmmss" => {base: "time", value: "150237", format: "HHmmss", result: "15:02:37"},
1168
+ "valid time HH:mm" => {base: "time", value: "15:02", format: "HH:mm", result: "15:02:00"},
1169
+ "valid time HHmm" => {base: "time", value: "1502", format: "HHmm", result: "15:02:00"},
1170
+
1171
+ # DateTimes
1172
+ "valid dateTime yyyy-MM-ddTHH:mm:ss" => {base: "dateTime", value: "2015-03-15T15:02:37", format: "yyyy-MM-ddTHH:mm:ss", result: "2015-03-15T15:02:37"},
1173
+ "valid dateTime yyyy-MM-dd HH:mm:ss" => {base: "dateTime", value: "2015-03-15 15:02:37", format: "yyyy-MM-dd HH:mm:ss", result: "2015-03-15T15:02:37"},
1174
+ "valid dateTime yyyyMMdd HHmmss" => {base: "dateTime", value: "20150315 150237", format: "yyyyMMdd HHmmss", result: "2015-03-15T15:02:37"},
1175
+ "valid dateTime dd-MM-yyyy HH:mm" => {base: "dateTime", value: "15-03-2015 15:02", format: "dd-MM-yyyy HH:mm", result: "2015-03-15T15:02:00"},
1176
+ "valid dateTime d-M-yyyy HHmm" => {base: "dateTime", value: "15-3-2015 1502", format: "d-M-yyyy HHmm", result: "2015-03-15T15:02:00"},
1177
+ "valid dateTime yyyy-MM-ddTHH:mm" => {base: "dateTime", value: "2015-03-15T15:02", format: "yyyy-MM-ddTHH:mm", result: "2015-03-15T15:02:00"},
1178
+ "valid dateTimeStamp d-M-yyyy HHmm X" => {base: "dateTimeStamp", value: "15-3-2015 1502 Z", format: "d-M-yyyy HHmm X", result: "2015-03-15T15:02:00Z"},
1179
+ "valid datetime yyyy-MM-ddTHH:mm:ss" => {base: "datetime", value: "2015-03-15T15:02:37", format: "yyyy-MM-ddTHH:mm:ss", result: "2015-03-15T15:02:37"},
1180
+ "valid datetime yyyy-MM-dd HH:mm:ss" => {base: "datetime", value: "2015-03-15 15:02:37", format: "yyyy-MM-dd HH:mm:ss", result: "2015-03-15T15:02:37"},
1181
+ "valid datetime yyyyMMdd HHmmss" => {base: "datetime", value: "20150315 150237", format: "yyyyMMdd HHmmss", result: "2015-03-15T15:02:37"},
1182
+ "valid datetime dd-MM-yyyy HH:mm" => {base: "datetime", value: "15-03-2015 15:02", format: "dd-MM-yyyy HH:mm", result: "2015-03-15T15:02:00"},
1183
+ "valid datetime d-M-yyyy HHmm" => {base: "datetime", value: "15-3-2015 1502", format: "d-M-yyyy HHmm", result: "2015-03-15T15:02:00"},
1184
+ "valid datetime yyyy-MM-ddTHH:mm" => {base: "datetime", value: "2015-03-15T15:02", format: "yyyy-MM-ddTHH:mm", result: "2015-03-15T15:02:00"},
1185
+
1186
+ # Timezones
1187
+ "valid w/TZ yyyy-MM-ddX" => {base: "date", value: "2015-03-22Z", format: "yyyy-MM-ddX", result: "2015-03-22Z"},
1188
+ "valid w/TZ dd.MM.yyyy XXXXX" => {base: "date", value: "22.03.2015 Z", format: "dd.MM.yyyy XXXXX", result: "2015-03-22Z"},
1189
+ "valid w/TZ HH:mm:ssX" => {base: "time", value: "15:02:37-05:00", format: "HH:mm:ssX", result: "15:02:37-05:00"},
1190
+ "valid w/TZ HHmm XX" => {base: "time", value: "1502 +08:00", format: "HHmm XX", result: "15:02:00+08:00"},
1191
+ "valid w/TZ yyyy-MM-ddTHH:mm:ssXXX" => {base: "dateTime", value: "2015-03-15T15:02:37-05:00", format: "yyyy-MM-ddTHH:mm:ssXXX", result: "2015-03-15T15:02:37-05:00"},
1192
+ "valid w/TZ yyyy-MM-dd HH:mm:ss X" => {base: "dateTimeStamp", value: "2015-03-15 15:02:37 +08:00", format: "yyyy-MM-dd HH:mm:ss X", result: "2015-03-15T15:02:37+08:00"},
1193
+ "valid gDay" => {base: "gDay", value: "---31"},
1194
+ "valid gMonth" => {base: "gMonth", value: "--02"},
1195
+ "valid gMonthDay" => {base: "gMonthDay", value: "--02-21"},
1196
+ "valid gYear" => {base: "gYear", value: "9999"},
1197
+ "valid gYearMonth" => {base: "gYearMonth", value: "1999-05"},
1198
+
1199
+ # Durations
1200
+ "valid duration PT130S" => {base: "duration", value: "PT130S"},
1201
+ "valid duration PT130M" => {base: "duration", value: "PT130M"},
1202
+ "valid duration PT130H" => {base: "duration", value: "PT130H"},
1203
+ "valid duration P130D" => {base: "duration", value: "P130D"},
1204
+ "valid duration P130M" => {base: "duration", value: "P130M"},
1205
+ "valid duration P130Y" => {base: "duration", value: "P130Y"},
1206
+ "valid duration PT2M10S" => {base: "duration", value: "PT2M10S"},
1207
+ "valid duration P0Y20M0D" => {base: "duration", value: "P0Y20M0D"},
1208
+ "valid duration -P60D" => {base: "duration", value: "-P60D"},
1209
+ "valid dayTimeDuration P1DT2H" => {base: "dayTimeDuration", value: "P1DT2H"},
1210
+ "valid yearMonthDuration P0Y20M" => {base: "yearMonthDuration", value: "P0Y20M"},
1211
+
1212
+ # Other datatypes
1213
+ "valid anyAtomicType" => {base: "anyAtomicType", value: "some thing", result: RDF::Literal("some thing", datatype: RDF::XSD.anyAtomicType)},
1214
+ "valid anyURI" => {base: "anyURI", value: "http://example.com/", result: RDF::Literal("http://example.com/", datatype: RDF::XSD.anyURI)},
1215
+ "valid base64Binary" => {base: "base64Binary", value: "Tm93IGlzIHRoZSB0aW1lIGZvciBhbGwgZ29vZCBjb2RlcnMKdG8gbGVhcm4g", result: RDF::Literal("Tm93IGlzIHRoZSB0aW1lIGZvciBhbGwgZ29vZCBjb2RlcnMKdG8gbGVhcm4g", datatype: RDF::XSD.base64Binary)},
1216
+ "valid hexBinary" => {base: "hexBinary", value: "0FB7", result: RDF::Literal("0FB7", datatype: RDF::XSD.hexBinary)},
1217
+ "valid QName" => {base: "QName", value: "foo:bar", result: RDF::Literal("foo:bar", datatype: RDF::XSD.QName)},
1218
+ "valid normalizedString" => {base: "normalizedString", value: "some thing", result: RDF::Literal("some thing", datatype: RDF::XSD.normalizedString)},
1219
+ "valid token" => {base: "token", value: "some thing", result: RDF::Literal("some thing", datatype: RDF::XSD.token)},
1220
+ "valid language" => {base: "language", value: "en", result: RDF::Literal("en", datatype: RDF::XSD.language)},
1221
+ "valid Name" => {base: "Name", value: "someThing", result: RDF::Literal("someThing", datatype: RDF::XSD.Name)},
1222
+ "valid NMTOKEN" => {base: "NMTOKEN", value: "someThing", result: RDF::Literal("someThing", datatype: RDF::XSD.NMTOKEN)},
1223
+
1224
+ # Unsupported datatypes
1225
+ "anyType not allowed" => {base: "anyType", value: "some thing", errors: [/unsupported datatype/]},
1226
+ "anySimpleType not allowed" => {base: "anySimpleType", value: "some thing", errors: [/unsupported datatype/]},
1227
+ "ENTITIES not allowed" => {base: "ENTITIES", value: "some thing", errors: [/unsupported datatype/]},
1228
+ "IDREFS not allowed" => {base: "IDREFS", value: "some thing", errors: [/unsupported datatype/]},
1229
+ "NMTOKENS not allowed" => {base: "NMTOKENS", value: "some thing", errors: [/unsupported datatype/]},
1230
+ "ENTITY not allowed" => {base: "ENTITY", value: "something", errors: [/unsupported datatype/]},
1231
+ "ID not allowed" => {base: "ID", value: "something", errors: [/unsupported datatype/]},
1232
+ "IDREF not allowed" => {base: "IDREF", value: "something", errors: [/unsupported datatype/]},
1233
+ "NOTATION not allowed" => {base: "NOTATION", value: "some:thing", errors: [/unsupported datatype/]},
1234
+
1235
+ # Aliases
1236
+ "number is alias for double" => {base: "number", value: "1234.456E789", result: RDF::Literal("1234.456E789", datatype: RDF::XSD.double)},
1237
+ "binary is alias for base64Binary" => {base: "binary", value: "Tm93IGlzIHRoZSB0aW1lIGZvciBhbGwgZ29vZCBjb2RlcnMKdG8gbGVhcm4g", result: RDF::Literal("Tm93IGlzIHRoZSB0aW1lIGZvciBhbGwgZ29vZCBjb2RlcnMKdG8gbGVhcm4g", datatype: RDF::XSD.base64Binary)},
1238
+ "datetime is alias for dateTime" => {base: "dateTime", value: "15-3-2015 1502", format: "d-M-yyyy HHmm", result: RDF::Literal("2015-03-15T15:02:00", datatype: RDF::XSD.dateTime)},
1239
+ "any is alias for anyAtomicType" => {base: "any", value: "some thing", result: RDF::Literal("some thing", datatype: RDF::XSD.anyAtomicType)},
1240
+ "xml is alias for rdf:XMLLiteral" => {base: "xml", value: "<foo></foo>", result: RDF::Literal("<foo></foo>", datatype: RDF.XMLLiteral)},
1241
+ "html is alias for rdf:HTML" => {base: "html", value: "<foo></foo>", result: RDF::Literal("<foo></foo>", datatype: RDF.HTML)},
1242
+ #"json is alias for csvw:JSON" => {base: "json", value: %({""foo"": ""bar""}), result: RDF::Literal(%({"foo": "bar"}), datatype: RDF::Tabular::CSVW.json)},
1243
+ }.each do |name, props|
1244
+ context name do
1245
+ let(:value) {props[:value]}
1246
+ let(:result) {
1247
+ if props[:errors]
1248
+ RDF::Literal(props.fetch(:result, value))
1249
+ else
1250
+ RDF::Literal(props.fetch(:result, value), datatype: md.context.expand_iri(props[:base], vocab: true))
1251
+ end
1252
+ }
1253
+ let(:md) {
1254
+ RDF::Tabular::Table.new({
1255
+ url: "http://example.com/table.csv",
1256
+ dialect: {header: false},
1257
+ tableSchema: {
1258
+ columns: [{
1259
+ name: "name",
1260
+ datatype: props.dup.delete_if {|k, v| [:value, :valid, :result].include?(k)}
1261
+ }]
1262
+ }
1263
+ }, debug: @debug)
1264
+ }
1265
+ subject {md.to_enum(:each_row, "#{value}\n").to_a.first.values.first}
1266
+
1267
+ if props[:errors]
1268
+ it {is_expected.not_to be_valid}
1269
+ it "has expected errors" do
1270
+ props[:errors].each do |e|
1271
+ expect(subject.errors.to_s).to match(e)
1272
+ end
1273
+ end
1274
+ else
1275
+ it {is_expected.to be_valid}
1276
+ it "has no errors" do
1277
+ expect(subject.errors).to be_empty
1278
+ end
1279
+ end
1280
+
1281
+ specify {expect(subject.value).to eql result}
1282
+ end
1283
+ end
1284
+ end
1285
+ end
1286
+
1287
+ describe "#common_properties" do
1288
+ describe "#normalize!" do
1289
+ {
1290
+ "string with no language" => [
1291
+ %({
1292
+ "dc:title": "foo"
1293
+ }),
1294
+ %({
1295
+ "@context": "http://www.w3.org/ns/csvw",
1296
+ "dc:title": {"@value": "foo"}
1297
+ })
1298
+ ],
1299
+ "string with language" => [
1300
+ %({
1301
+ "@context": {"@language": "en"},
1302
+ "dc:title": "foo"
1303
+ }),
1304
+ %({
1305
+ "@context": "http://www.w3.org/ns/csvw",
1306
+ "dc:title": {"@value": "foo", "@language": "en"}
1307
+ })
1308
+ ],
1309
+ "relative URL" => [
1310
+ %({
1311
+ "dc:source": {"@id": "foo"}
1312
+ }),
1313
+ %({
1314
+ "@context": "http://www.w3.org/ns/csvw",
1315
+ "dc:source": {"@id": "http://example.com/foo"}
1316
+ })
1317
+ ],
1318
+ "array of values" => [
1319
+ %({
1320
+ "@context": {"@language": "en"},
1321
+ "dc:title": [
1322
+ "foo",
1323
+ {"@value": "bar"},
1324
+ {"@value": "baz", "@language": "de"},
1325
+ 1,
1326
+ true,
1327
+ {"@value": 1},
1328
+ {"@value": true},
1329
+ {"@value": "1", "@type": "xsd:integer"},
1330
+ {"@id": "foo"}
1331
+ ]
1332
+ }),
1333
+ %({
1334
+ "@context": "http://www.w3.org/ns/csvw",
1335
+ "dc:title": [
1336
+ {"@value": "foo", "@language": "en"},
1337
+ {"@value": "bar"},
1338
+ {"@value": "baz", "@language": "de"},
1339
+ 1,
1340
+ true,
1341
+ {"@value": 1},
1342
+ {"@value": true},
1343
+ {"@value": "1", "@type": "xsd:integer"},
1344
+ {"@id": "http://example.com/foo"}
1345
+ ]
1346
+ })
1347
+ ],
1348
+ }.each do |name, (input, result)|
1349
+ it name do
1350
+ a = RDF::Tabular::Table.new(input, base: "http://example.com/A")
1351
+ b = RDF::Tabular::Table.new(result, base: "http://example.com/A")
1352
+ expect(a.normalize!).to eq b
1353
+ end
1354
+ end
1355
+ end
1356
+
1357
+ context "transformation" do
1358
+ it "FIXME"
1359
+ end
1360
+ end
1361
+
1362
+ describe "#merge" do
1363
+ {
1364
+ "two tables with same id" => {
1365
+ A: %({
1366
+ "@type": "Table",
1367
+ "url": "http://example.org/table"
1368
+ }),
1369
+ B: [%({
1370
+ "@type": "Table",
1371
+ "url": "http://example.org/table"
1372
+ })],
1373
+ R: %({
1374
+ "@type": "TableGroup",
1375
+ "resources": [{
1376
+ "@type": "Table",
1377
+ "url": "http://example.org/table"
1378
+ }],
1379
+ "@context": "http://www.w3.org/ns/csvw"
1380
+ })
1381
+ },
1382
+ "two tables with different id" => {
1383
+ A: %({
1384
+ "@type": "Table",
1385
+ "url": "http://example.org/table1"
1386
+ }),
1387
+ B: [%({
1388
+ "@type": "Table",
1389
+ "url": "http://example.org/table2"
1390
+ })],
1391
+ R: %({
1392
+ "@type": "TableGroup",
1393
+ "resources": [{
1394
+ "@type": "Table",
1395
+ "url": "http://example.org/table1"
1396
+ }, {
1397
+ "@type": "Table",
1398
+ "url": "http://example.org/table2"
1399
+ }],
1400
+ "@context": "http://www.w3.org/ns/csvw"
1401
+ })
1402
+ },
1403
+ "table and table-group" => {
1404
+ A: %({
1405
+ "@type": "Table",
1406
+ "url": "http://example.org/table1"
1407
+ }),
1408
+ B: [%({
1409
+ "@type": "TableGroup",
1410
+ "resources": [{
1411
+ "@type": "Table",
1412
+ "url": "http://example.org/table2"
1413
+ }]
1414
+ })],
1415
+ R: %({
1416
+ "@type": "TableGroup",
1417
+ "resources": [{
1418
+ "@type": "Table",
1419
+ "url": "http://example.org/table1"
1420
+ }, {
1421
+ "@type": "Table",
1422
+ "url": "http://example.org/table2"
1423
+ }],
1424
+ "@context": "http://www.w3.org/ns/csvw"
1425
+ })
1426
+ },
1427
+ "table-group and table" => {
1428
+ A: %({
1429
+ "@type": "TableGroup",
1430
+ "resources": [{
1431
+ "@type": "Table",
1432
+ "url": "http://example.org/table1"
1433
+ }]
1434
+ }),
1435
+ B: [%({
1436
+ "@type": "Table",
1437
+ "url": "http://example.org/table2"
1438
+ })],
1439
+ R: %({
1440
+ "@type": "TableGroup",
1441
+ "resources": [{
1442
+ "@type": "Table",
1443
+ "url": "http://example.org/table1"
1444
+ }, {
1445
+ "@type": "Table",
1446
+ "url": "http://example.org/table2"
1447
+ }],
1448
+ "@context": "http://www.w3.org/ns/csvw"
1449
+ })
1450
+ },
1451
+ "table-group and two tables" => {
1452
+ A: %({
1453
+ "@type": "TableGroup",
1454
+ "resources": [{
1455
+ "@type": "Table",
1456
+ "url": "http://example.org/table1"
1457
+ }]
1458
+ }),
1459
+ B: [%({
1460
+ "@type": "Table",
1461
+ "url": "http://example.org/table2",
1462
+ "dc:label": "foo"
1463
+ }), %({
1464
+ "@type": "Table",
1465
+ "url": "http://example.org/table2",
1466
+ "dc:label": "bar"
1467
+ })],
1468
+ R: %({
1469
+ "@type": "TableGroup",
1470
+ "resources": [{
1471
+ "@type": "Table",
1472
+ "url": "http://example.org/table1"
1473
+ }, {
1474
+ "@type": "Table",
1475
+ "url": "http://example.org/table2",
1476
+ "dc:label": {"@value": "foo"}
1477
+ }],
1478
+ "@context": "http://www.w3.org/ns/csvw"
1479
+ })
1480
+ },
1481
+ }.each do |name, props|
1482
+ it name do
1483
+ a = described_class.new(::JSON.parse(props[:A]))
1484
+ b = props[:B].map {|md| described_class.new(::JSON.parse(md))}
1485
+ r = described_class.new(::JSON.parse(props[:R]))
1486
+ expect(a.merge(*b)).to produce(r, @debug)
1487
+ end
1488
+ end
1489
+
1490
+ %w(Transformation Schema Transformation Column Dialect).each do |t|
1491
+ it "does not merge into a #{t}" do
1492
+ a = described_class.new({}, type: t.to_sym)
1493
+ b = described_class.new({}, type: :TableGroup)
1494
+ expect {a.merge(b)}.to raise_error
1495
+ end
1496
+
1497
+ it "does not merge from a #{t}" do
1498
+ a = described_class.new({}, type: :TableGroup)
1499
+ b = described_class.new({}, type: t.to_sym)
1500
+ expect {a.merge(b)}.to raise_error
1501
+ end
1502
+ end
1503
+ end
1504
+
1505
+ describe "#merge!" do
1506
+ {
1507
+ "TableGroup with and without @id" => {
1508
+ A: %({"@id": "http://example.org/foo", "resources": [], "@type": "TableGroup"}),
1509
+ B: %({"resources": [], "@type": "TableGroup"}),
1510
+ R: %({"@id": "http://example.org/foo", "resources": [], "@type": "TableGroup"})
1511
+ },
1512
+ "TableGroup with and without @type" => {
1513
+ A: %({"resources": []}),
1514
+ B: %({"resources": [], "@type": "TableGroup"}),
1515
+ R: %({"resources": [], "@type": "TableGroup"})
1516
+ },
1517
+ "TableGroup with matching resources" => {
1518
+ A: %({"resources": [{"url": "http://example.org/foo", "dc:title": "foo"}]}),
1519
+ B: %({"resources": [{"url": "http://example.org/foo", "dc:description": "bar"}]}),
1520
+ R: %({"resources": [{
1521
+ "url": "http://example.org/foo",
1522
+ "dc:title": {"@value": "foo"},
1523
+ "dc:description": {"@value": "bar"}
1524
+ }]})
1525
+ },
1526
+ "TableGroup with differing resources" => {
1527
+ A: %({"resources": [{"url": "http://example.org/foo", "dc:title": "foo"}]}),
1528
+ B: %({"resources": [{"url": "http://example.org/bar", "dc:description": "bar"}]}),
1529
+ R: %({
1530
+ "resources": [
1531
+ {"url": "http://example.org/foo", "dc:title": {"@value": "foo"}},
1532
+ {"url": "http://example.org/bar", "dc:description": {"@value": "bar"}}
1533
+ ]})
1534
+ },
1535
+ "Table with tableDirection always takes A" => {
1536
+ A: %({"@type": "Table", "url": "http://example.com/foo", "tableDirection": "ltr"}),
1537
+ B: %({"@type": "Table", "url": "http://example.com/foo", "tableDirection": "rtl"}),
1538
+ R: %({"@type": "Table", "url": "http://example.com/foo", "tableDirection": "ltr"}),
1539
+ },
1540
+ "Table with dialect merges A and B" => {
1541
+ A: %({"@type": "Table", "url": "http://example.com/foo", "dialect": {"encoding": "utf-8"}}),
1542
+ B: %({"@type": "Table", "url": "http://example.com/foo", "dialect": {"skipRows": 0}}),
1543
+ R: %({"@type": "Table", "url": "http://example.com/foo", "dialect": {"encoding": "utf-8", "skipRows": 0}}),
1544
+ },
1545
+ "Table with equivalent transformations uses A" => {
1546
+ A: %({
1547
+ "@type": "Table",
1548
+ "url": "http://example.com/foo",
1549
+ "transformations": [{
1550
+ "url": "http://example.com/foo",
1551
+ "targetFormat": "http://example.com/target",
1552
+ "scriptFormat": "http://example.com/template",
1553
+ "source": "json"
1554
+ }]
1555
+ }),
1556
+ B: %({
1557
+ "@type": "Table",
1558
+ "url": "http://example.com/foo",
1559
+ "transformations": [{
1560
+ "url": "http://example.com/foo",
1561
+ "targetFormat": "http://example.com/target",
1562
+ "scriptFormat": "http://example.com/template",
1563
+ "source": "html"
1564
+ }]
1565
+ }),
1566
+ R: %({
1567
+ "@type": "Table",
1568
+ "url": "http://example.com/foo",
1569
+ "transformations": [{
1570
+ "url": "http://example.com/foo",
1571
+ "targetFormat": "http://example.com/target",
1572
+ "scriptFormat": "http://example.com/template",
1573
+ "source": "json"
1574
+ }]
1575
+ }),
1576
+ },
1577
+ "Table with differing transformations appends B to A" => {
1578
+ A: %({
1579
+ "@type": "Table",
1580
+ "url": "http://example.com/foo",
1581
+ "transformations": [{
1582
+ "url": "http://example.com/foo",
1583
+ "targetFormat": "http://example.com/target",
1584
+ "scriptFormat": "http://example.com/template"
1585
+ }]
1586
+ }),
1587
+ B: %({
1588
+ "@type": "Table",
1589
+ "url": "http://example.com/foo",
1590
+ "transformations": [{
1591
+ "url": "http://example.com/bar",
1592
+ "targetFormat": "http://example.com/targetb",
1593
+ "scriptFormat": "http://example.com/templateb"
1594
+ }]
1595
+ }),
1596
+ R: %({
1597
+ "@type": "Table",
1598
+ "url": "http://example.com/foo",
1599
+ "transformations": [{
1600
+ "url": "http://example.com/foo",
1601
+ "targetFormat": "http://example.com/target",
1602
+ "scriptFormat": "http://example.com/template"
1603
+ }, {
1604
+ "url": "http://example.com/bar",
1605
+ "targetFormat": "http://example.com/targetb",
1606
+ "scriptFormat": "http://example.com/templateb"
1607
+ }]
1608
+ }),
1609
+ },
1610
+ "Table with common properties keeps A" => {
1611
+ A: %({"@type": "Table", "url": "http://example.com/foo", "rdfs:label": "foo"}),
1612
+ B: %({"@type": "Table", "url": "http://example.com/foo", "rdfs:label": "bar"}),
1613
+ R: %({
1614
+ "@type": "Table",
1615
+ "url": "http://example.com/foo",
1616
+ "rdfs:label": {"@value": "foo"}
1617
+ }),
1618
+ },
1619
+ "Table with common properties in different languages keeps A" => {
1620
+ A: %({
1621
+ "@context": {"@language": "en"},
1622
+ "@type": "Table",
1623
+ "url": "http://example.com/foo",
1624
+ "rdfs:label": "foo"
1625
+ }),
1626
+ B: %({
1627
+ "@context": {"@language": "fr"},
1628
+ "@type": "Table",
1629
+ "url": "http://example.com/foo",
1630
+ "rdfs:label": "foo"
1631
+ }),
1632
+ R: %({
1633
+ "@context": "http://www.w3.org/ns/csvw",
1634
+ "@type": "Table",
1635
+ "url": "http://example.com/foo",
1636
+ "rdfs:label": {"@value": "foo", "@language": "en"}
1637
+ }),
1638
+ },
1639
+ "Table with different languages merges A and B" => {
1640
+ A: %({
1641
+ "@context": {"@language": "en"},
1642
+ "@type": "Table",
1643
+ "url": "http://example.com/foo",
1644
+ "tableSchema": {
1645
+ "columns": [{"title": "foo"}]
1646
+ }
1647
+ }),
1648
+ B: %({
1649
+ "@type": "Table",
1650
+ "url": "http://example.com/foo",
1651
+ "tableSchema": {
1652
+ "columns": [{"title": "foo"}]
1653
+ }
1654
+ }),
1655
+ R: %({
1656
+ "@context": "http://www.w3.org/ns/csvw",
1657
+ "@type": "Table",
1658
+ "url": "http://example.com/foo",
1659
+ "tableSchema": {
1660
+ "columns": [{"title": {"en": ["foo"]}}]
1661
+ }
1662
+ }),
1663
+ },
1664
+ "Schema with matching columns merges A and B" => {
1665
+ A: %({"@type": "Schema", "columns": [{"name": "foo", "required": true}]}),
1666
+ B: %({"@type": "Schema", "columns": [{"name": "foo", "required": false}]}),
1667
+ R: %({"@type": "Schema", "columns": [{"name": "foo", "required": true}]}),
1668
+ },
1669
+ "Schema with matching column titles" => {
1670
+ A: %({"@type": "Schema", "columns": [{"title": "Foo"}]}),
1671
+ B: %({"@type": "Schema", "columns": [{"name": "foo", "title": "Foo"}]}),
1672
+ R: %({"@type": "Schema", "columns": [{"name": "foo", "title": {"und": ["Foo"]}}]}),
1673
+ },
1674
+ "Schema with primaryKey always takes A" => {
1675
+ A: %({"@type": "Schema", "primaryKey": "foo"}),
1676
+ B: %({"@type": "Schema", "primaryKey": "bar"}),
1677
+ R: %({"@type": "Schema", "primaryKey": "foo"}),
1678
+ },
1679
+ "Schema with matching foreignKey uses A" => {
1680
+ A: %({"@type": "Schema", "columns": [{"name": "foo"}], "foreignKeys": [{"columnReference": "foo", "reference": {"columnReference": "foo"}}]}),
1681
+ B: %({"@type": "Schema", "columns": [{"name": "foo"}], "foreignKeys": [{"columnReference": "foo", "reference": {"columnReference": "foo"}}]}),
1682
+ R: %({"@type": "Schema", "columns": [{"name": "foo"}], "foreignKeys": [{"columnReference": "foo", "reference": {"columnReference": "foo"}}]}),
1683
+ },
1684
+ "Schema with differing foreignKey uses A and B" => {
1685
+ A: %({"@type": "Schema", "columns": [{"name": "foo"}, {"name": "bar"}], "foreignKeys": [{"columnReference": "foo", "reference": {"columnReference": "foo"}}]}),
1686
+ B: %({"@type": "Schema", "columns": [{"name": "foo"}, {"name": "bar"}], "foreignKeys": [{"columnReference": "bar", "reference": {"columnReference": "bar"}}]}),
1687
+ R: %({"@type": "Schema", "columns": [{"name": "foo"}, {"name": "bar"}], "foreignKeys": [{"columnReference": "foo", "reference": {"columnReference": "foo"}}, {"columnReference": "bar", "reference": {"columnReference": "bar"}}]}),
1688
+ },
1689
+ "Schema with urlTemplate always takes A" => {
1690
+ A: %({"@type": "Schema", "urlTemplate": "foo"}),
1691
+ B: %({"@type": "Schema", "urlTemplate": "bar"}),
1692
+ R: %({"@type": "Schema", "urlTemplate": "foo"}),
1693
+ },
1694
+ }.each do |name, props|
1695
+ it name do
1696
+ a = described_class.new(::JSON.parse(props[:A]), debug: @debug)
1697
+ b = described_class.new(::JSON.parse(props[:B]))
1698
+ r = described_class.new(::JSON.parse(props[:R]))
1699
+ m = a.merge!(b)
1700
+ expect(m).to produce(r, @debug)
1701
+ expect(a).to equal m
1702
+ end
1703
+ end
1704
+
1705
+ %w(TableGroup Table Transformation Schema Transformation Column Dialect).each do |ta|
1706
+ %w(TableGroup Table Transformation Schema Transformation Column Dialect).each do |tb|
1707
+ next if ta == tb
1708
+ it "does not merge #{tb} into #{ta}" do
1709
+ a = described_class.new({}, type: ta.to_sym)
1710
+ b = described_class.new({}, type: tb.to_sym)
1711
+ expect {a.merge!(b)}.to raise_error
1712
+ end
1713
+ end
1714
+ end
1715
+ end
1716
+ end