rdf-tabular 0.1.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/AUTHORS +1 -0
- data/README.md +73 -0
- data/UNLICENSE +24 -0
- data/VERSION +1 -0
- data/etc/csvw.jsonld +1507 -0
- data/etc/doap.csv +5 -0
- data/etc/doap.csv-metadata.json +34 -0
- data/etc/doap.ttl +35 -0
- data/lib/rdf/tabular.rb +34 -0
- data/lib/rdf/tabular/csvw.rb +477 -0
- data/lib/rdf/tabular/format.rb +46 -0
- data/lib/rdf/tabular/json.rb +0 -0
- data/lib/rdf/tabular/literal.rb +38 -0
- data/lib/rdf/tabular/metadata.rb +2038 -0
- data/lib/rdf/tabular/reader.rb +591 -0
- data/lib/rdf/tabular/utils.rb +33 -0
- data/lib/rdf/tabular/version.rb +18 -0
- data/spec/format_spec.rb +30 -0
- data/spec/matchers.rb +134 -0
- data/spec/metadata_spec.rb +1716 -0
- data/spec/reader_spec.rb +221 -0
- data/spec/spec_helper.rb +47 -0
- data/spec/suite_helper.rb +161 -0
- data/spec/suite_spec.rb +76 -0
- metadata +269 -0
@@ -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
|
data/spec/format_spec.rb
ADDED
@@ -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
|