rdf-tabular 0.1.1

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.
@@ -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