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