csvlint 0.2.6 → 0.3.0
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 +8 -8
- data/.gitattributes +1 -1
- data/.gitignore +1 -1
- data/csvlint.gemspec +2 -0
- data/features/fixtures/w3.org/.well-known/csvm +4 -0
- data/features/step_definitions/validation_errors_steps.rb +1 -1
- data/features/support/earl_formatter.rb +66 -0
- data/features/support/load_tests.rb +20 -15
- data/lib/csvlint/csvw/column.rb +103 -55
- data/lib/csvlint/csvw/date_format.rb +49 -16
- data/lib/csvlint/csvw/number_format.rb +36 -21
- data/lib/csvlint/csvw/property_checker.rb +90 -45
- data/lib/csvlint/csvw/table.rb +57 -39
- data/lib/csvlint/csvw/table_group.rb +9 -9
- data/lib/csvlint/schema.rb +2 -2
- data/lib/csvlint/validate.rb +10 -13
- data/lib/csvlint/version.rb +1 -1
- data/spec/csvw/column_spec.rb +6 -6
- data/spec/csvw/date_format_spec.rb +10 -10
- data/spec/csvw/number_format_spec.rb +29 -0
- metadata +34 -2
@@ -70,39 +70,72 @@ module Csvlint
|
|
70
70
|
# STDERR.puts(@regexp)
|
71
71
|
# STDERR.puts(value)
|
72
72
|
# STDERR.puts(match.inspect)
|
73
|
+
value = {}
|
74
|
+
match.names.each do |field|
|
75
|
+
unless match[field].nil?
|
76
|
+
case field
|
77
|
+
when "timezone"
|
78
|
+
tz = match["timezone"]
|
79
|
+
tz = "+00:00" if tz == 'Z'
|
80
|
+
tz += ':00' if tz.length == 3
|
81
|
+
tz = "#{tz[0..2]}:#{tz[3..4]}" unless tz =~ /:/
|
82
|
+
value[:timezone] = tz
|
83
|
+
when "second"
|
84
|
+
value[:second] = match["second"].to_f
|
85
|
+
else
|
86
|
+
value[field.to_sym] = match[field].to_i
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
73
90
|
case @type
|
74
91
|
when "http://www.w3.org/2001/XMLSchema#date"
|
75
92
|
begin
|
76
|
-
|
93
|
+
value[:dateTime] = Date.new(match["year"].to_i, match["month"].to_i, match["day"].to_i)
|
77
94
|
rescue ArgumentError
|
78
95
|
return nil
|
79
96
|
end
|
80
97
|
when "http://www.w3.org/2001/XMLSchema#dateTime"
|
81
98
|
begin
|
82
|
-
|
99
|
+
value[:dateTime] = DateTime.new(match["year"].to_i, match["month"].to_i, match["day"].to_i, match["hour"].to_i, match["minute"].to_i, (match.names.include?("second") ? match["second"].to_f : 0), match.names.include?("timezone") && match["timezone"] ? match["timezone"] : '')
|
83
100
|
rescue ArgumentError
|
84
101
|
return nil
|
85
102
|
end
|
86
103
|
else
|
87
|
-
value =
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
tz = "#{tz[0..2]}:#{tz[3..4]}" unless tz =~ /:/
|
96
|
-
value["timezone"] = tz
|
97
|
-
when "second"
|
98
|
-
value["second"] = match["second"].to_f
|
104
|
+
value[:dateTime] = DateTime.new(value[:year] || 0, value[:month] || 1, value[:day] || 1, value[:hour] || 0, value[:minute] || 0, value[:second] || 0, value[:timezone] || "+00:00")
|
105
|
+
end
|
106
|
+
if value[:year]
|
107
|
+
if value[:month]
|
108
|
+
if value[:day]
|
109
|
+
if value[:hour]
|
110
|
+
# dateTime
|
111
|
+
value[:string] = "#{format('%04d', value[:year])}-#{format('%02d', value[:month])}-#{format('%02d', value[:day])}T#{format('%02d', value[:hour])}:#{format('%02d', value[:minute] || 0)}:#{format('%02g', value[:second] || 0)}#{value[:timezone] ? value[:timezone].sub("+00:00", "Z") : ''}"
|
99
112
|
else
|
100
|
-
|
113
|
+
# date
|
114
|
+
value[:string] = "#{format('%04d', value[:year])}-#{format('%02d', value[:month])}-#{format('%02d', value[:day])}#{value[:timezone] ? value[:timezone].sub("+00:00", "Z") : ''}"
|
101
115
|
end
|
116
|
+
else
|
117
|
+
# gYearMonth
|
118
|
+
value[:string] = "#{format('%04d', value[:year])}-#{format('%02d', value[:month])}#{value[:timezone] ? value[:timezone].sub("+00:00", "Z") : ''}"
|
102
119
|
end
|
120
|
+
else
|
121
|
+
# gYear
|
122
|
+
value[:string] = "#{format('%04d', value[:year])}#{value[:timezone] ? value[:timezone].sub("+00:00", "Z") : ''}"
|
103
123
|
end
|
104
|
-
|
124
|
+
elsif value[:month]
|
125
|
+
if value[:day]
|
126
|
+
# gMonthDay
|
127
|
+
value[:string] = "--#{format('%02d', value[:month])}-#{format('%02d', value[:day])}#{value[:timezone] ? value[:timezone].sub("+00:00", "Z") : ''}"
|
128
|
+
else
|
129
|
+
# gMonth
|
130
|
+
value[:string] = "--#{format('%02d', value[:month])}#{value[:timezone] ? value[:timezone].sub("+00:00", "Z") : ''}"
|
131
|
+
end
|
132
|
+
elsif value[:day]
|
133
|
+
# gDay
|
134
|
+
value[:string] = "---#{format('%02d', value[:day])}#{value[:timezone] ? value[:timezone].sub("+00:00", "Z") : ''}"
|
135
|
+
else
|
136
|
+
value[:string] = "#{format('%02d', value[:hour])}:#{format('%02d', value[:minute])}:#{format('%02g', value[:second] || 0)}#{value[:timezone] ? value[:timezone].sub("+00:00", "Z") : ''}"
|
105
137
|
end
|
138
|
+
return value
|
106
139
|
end
|
107
140
|
|
108
141
|
private
|
@@ -2,16 +2,28 @@ module Csvlint
|
|
2
2
|
module Csvw
|
3
3
|
class NumberFormat
|
4
4
|
|
5
|
-
attr_reader :pattern, :prefix, :numeric_part, :suffix, :grouping_separator, :decimal_separator, :primary_grouping_size, :secondary_grouping_size, :fractional_grouping_size
|
5
|
+
attr_reader :integer, :pattern, :prefix, :numeric_part, :suffix, :grouping_separator, :decimal_separator, :primary_grouping_size, :secondary_grouping_size, :fractional_grouping_size
|
6
6
|
|
7
|
-
def initialize(pattern=nil, grouping_separator=nil, decimal_separator=".")
|
7
|
+
def initialize(pattern=nil, grouping_separator=nil, decimal_separator=".", integer=nil)
|
8
8
|
@pattern = pattern
|
9
|
+
@integer = integer
|
10
|
+
if @integer.nil?
|
11
|
+
if @pattern.nil?
|
12
|
+
@integer = nil
|
13
|
+
else
|
14
|
+
@integer = !@pattern.include?(decimal_separator)
|
15
|
+
end
|
16
|
+
end
|
9
17
|
@grouping_separator = grouping_separator || (@pattern.nil? ? nil : ",")
|
10
18
|
@decimal_separator = decimal_separator || "."
|
11
19
|
if pattern.nil?
|
12
|
-
|
20
|
+
if integer
|
21
|
+
@regexp = INTEGER_REGEXP
|
22
|
+
else
|
23
|
+
@regexp = Regexp.new("^(([-+]?[0-9]+(\\.[0-9]+)?([Ee][-+]?[0-9]+)?[%‰]?)|NaN|INF|-INF)$")
|
24
|
+
end
|
13
25
|
else
|
14
|
-
numeric_part_regexp = Regexp.new("(?<numeric_part>([0#Ee]|#{Regexp.escape(@grouping_separator)}|#{Regexp.escape(@decimal_separator)})+)")
|
26
|
+
numeric_part_regexp = Regexp.new("(?<numeric_part>[-+]?([0#Ee]|#{Regexp.escape(@grouping_separator)}|#{Regexp.escape(@decimal_separator)})+)")
|
15
27
|
number_format_regexp = Regexp.new("^(?<prefix>.*?)#{numeric_part_regexp}(?<suffix>.*?)$")
|
16
28
|
match = number_format_regexp.match(pattern)
|
17
29
|
raise Csvw::NumberFormatError, "invalid number format" if match.nil?
|
@@ -28,7 +40,12 @@ module Csvlint
|
|
28
40
|
integer_part = mantissa_parts[0]
|
29
41
|
fractional_part = mantissa_parts[1] || ""
|
30
42
|
|
31
|
-
|
43
|
+
if ["+", "-"].include?(integer_part[0])
|
44
|
+
numeric_part_regexp = "\\#{integer_part[0]}"
|
45
|
+
integer_part = integer_part[1..-1]
|
46
|
+
else
|
47
|
+
numeric_part_regexp = "[-+]?"
|
48
|
+
end
|
32
49
|
|
33
50
|
min_integer_digits = integer_part.gsub(@grouping_separator, "").gsub("#", "").length
|
34
51
|
min_fraction_digits = fractional_part.gsub(@grouping_separator, "").gsub("#", "").length
|
@@ -43,8 +60,6 @@ module Csvlint
|
|
43
60
|
fractional_parts = fractional_part.split(@grouping_separator)[0..-2]
|
44
61
|
@fractional_grouping_size = fractional_parts[0].length rescue 0
|
45
62
|
|
46
|
-
numeric_part_regexp = "[-+]?"
|
47
|
-
|
48
63
|
if @primary_grouping_size == 0
|
49
64
|
integer_regexp = "[0-9]*[0-9]{#{min_integer_digits}}"
|
50
65
|
else
|
@@ -160,16 +175,8 @@ module Csvlint
|
|
160
175
|
if @pattern.nil?
|
161
176
|
return nil if !@grouping_separator.nil? && value =~ Regexp.new("((^#{Regexp.escape(@grouping_separator)})|#{Regexp.escape(@grouping_separator)}{2})")
|
162
177
|
value.gsub!(@grouping_separator, "") unless @grouping_separator.nil?
|
163
|
-
|
164
|
-
|
165
|
-
when "%"
|
166
|
-
return value.to_f / 100
|
167
|
-
when "‰"
|
168
|
-
return value.to_f / 1000
|
169
|
-
else
|
170
|
-
return value.to_i
|
171
|
-
end
|
172
|
-
elsif value =~ @regexp
|
178
|
+
value.gsub!(@decimal_separator, ".") unless @decimal_separator.nil?
|
179
|
+
if value =~ @regexp
|
173
180
|
case value
|
174
181
|
when "NaN"
|
175
182
|
return Float::NAN
|
@@ -184,7 +191,11 @@ module Csvlint
|
|
184
191
|
when "‰"
|
185
192
|
return value.to_f / 1000
|
186
193
|
else
|
187
|
-
|
194
|
+
if @integer.nil?
|
195
|
+
return value.include?(".") ? value.to_f : value.to_i
|
196
|
+
else
|
197
|
+
return @integer ? value.to_i : value.to_f
|
198
|
+
end
|
188
199
|
end
|
189
200
|
end
|
190
201
|
else
|
@@ -193,9 +204,13 @@ module Csvlint
|
|
193
204
|
else
|
194
205
|
match = @regexp.match(value)
|
195
206
|
return nil if match.nil?
|
196
|
-
number = match["numeric_part"]
|
197
|
-
|
198
|
-
|
207
|
+
number = match["numeric_part"]
|
208
|
+
number.gsub!(@grouping_separator, "") unless @grouping_separator.nil?
|
209
|
+
number.gsub!(@decimal_separator, ".") unless @decimal_separator.nil?
|
210
|
+
number = @integer ? number.to_i : number.to_f
|
211
|
+
number = number.to_f / 100 if match["prefix"].include?("%") || match["suffix"].include?("%")
|
212
|
+
number = number.to_f / 1000 if match["prefix"].include?("‰") || match["suffix"].include?("‰")
|
213
|
+
return number
|
199
214
|
end
|
200
215
|
end
|
201
216
|
|
@@ -58,15 +58,29 @@ module Csvlint
|
|
58
58
|
else
|
59
59
|
if p[0] == "@"
|
60
60
|
raise Csvlint::Csvw::MetadataError.new(), "common property has property other than @id, @type, @value or @language beginning with @ (#{p})"
|
61
|
+
else
|
62
|
+
v, w = check_common_property_value(v, base_url, lang)
|
63
|
+
warnings += Array(w)
|
61
64
|
end
|
62
65
|
end
|
63
|
-
if v.instance_of? Hash
|
64
|
-
v, w = check_common_property_value(v, base_url, lang)
|
65
|
-
warnings += Array(w)
|
66
|
-
end
|
67
66
|
value[p] = v
|
68
67
|
end
|
69
68
|
return value, warnings
|
69
|
+
when String
|
70
|
+
if lang == "und"
|
71
|
+
return value, nil
|
72
|
+
else
|
73
|
+
return { "@value" => value, "@language" => lang }, nil
|
74
|
+
end
|
75
|
+
when Array
|
76
|
+
values = []
|
77
|
+
warnings = []
|
78
|
+
value.each do |v|
|
79
|
+
v, w = check_common_property_value(v, base_url, lang)
|
80
|
+
warnings += Array(w)
|
81
|
+
values << v
|
82
|
+
end
|
83
|
+
return values, warnings
|
70
84
|
else
|
71
85
|
return value, nil
|
72
86
|
end
|
@@ -114,6 +128,13 @@ module Csvlint
|
|
114
128
|
}
|
115
129
|
end
|
116
130
|
|
131
|
+
def uri_template_property(type)
|
132
|
+
return lambda { |value, base_url, lang|
|
133
|
+
return URITemplate.new(value), nil, type if value.instance_of? String
|
134
|
+
return URITemplate.new(""), :invalid_value, type
|
135
|
+
}
|
136
|
+
end
|
137
|
+
|
117
138
|
def numeric_property(type)
|
118
139
|
return lambda { |value, base_url, lang|
|
119
140
|
return value, nil, type if value.kind_of?(Integer) && value >= 0
|
@@ -192,8 +213,43 @@ module Csvlint
|
|
192
213
|
"@base" => link_property(:context),
|
193
214
|
# common properties
|
194
215
|
"@id" => link_property(:common),
|
195
|
-
"notes" =>
|
216
|
+
"notes" => lambda { |value, base_url, lang|
|
217
|
+
return false, :invalid_value, :common unless value.instance_of? Array
|
218
|
+
values = []
|
219
|
+
warnings = []
|
220
|
+
value.each do |v|
|
221
|
+
v, w = check_common_property_value(v, base_url, lang)
|
222
|
+
values << v
|
223
|
+
warnings += w
|
224
|
+
end
|
225
|
+
return values, warnings, :common
|
226
|
+
},
|
196
227
|
"suppressOutput" => boolean_property(:common),
|
228
|
+
"dialect" => lambda { |value, base_url, lang|
|
229
|
+
if value.instance_of? Hash
|
230
|
+
value = value.clone
|
231
|
+
warnings = []
|
232
|
+
value.each do |p,v|
|
233
|
+
if p == "@id"
|
234
|
+
raise Csvlint::Csvw::MetadataError.new("dialect.@id"), "@id starts with _:" if v =~ /^_:/
|
235
|
+
elsif p == "@type"
|
236
|
+
raise Csvlint::Csvw::MetadataError.new("dialect.@type"), "@type of dialect is not 'Dialect'" if v != 'Dialect'
|
237
|
+
else
|
238
|
+
v, warning, type = check_property(p, v, base_url, lang)
|
239
|
+
if type == :dialect && (warning.nil? || warning.empty?)
|
240
|
+
value[p] = v
|
241
|
+
else
|
242
|
+
value.delete(p)
|
243
|
+
warnings << :invalid_property unless type == :dialect
|
244
|
+
warnings += Array(warning)
|
245
|
+
end
|
246
|
+
end
|
247
|
+
end
|
248
|
+
return value, warnings, :common
|
249
|
+
else
|
250
|
+
return {}, :invalid_value, :common
|
251
|
+
end
|
252
|
+
},
|
197
253
|
# inherited properties
|
198
254
|
"null" => lambda { |value, base_url, lang|
|
199
255
|
case value
|
@@ -270,12 +326,17 @@ module Csvlint
|
|
270
326
|
warnings += convert_value_facet(value, "maxInclusive", value["base"])
|
271
327
|
warnings += convert_value_facet(value, "maxExclusive", value["base"])
|
272
328
|
|
273
|
-
|
274
|
-
|
275
|
-
|
276
|
-
|
277
|
-
|
278
|
-
raise Csvlint::Csvw::MetadataError.new(""), "datatype
|
329
|
+
minInclusive = value["minInclusive"].is_a?(Hash) ? value["minInclusive"][:dateTime] : value["minInclusive"]
|
330
|
+
maxInclusive = value["maxInclusive"].is_a?(Hash) ? value["maxInclusive"][:dateTime] : value["maxInclusive"]
|
331
|
+
minExclusive = value["minExclusive"].is_a?(Hash) ? value["minExclusive"][:dateTime] : value["minExclusive"]
|
332
|
+
maxExclusive = value["maxExclusive"].is_a?(Hash) ? value["maxExclusive"][:dateTime] : value["maxExclusive"]
|
333
|
+
|
334
|
+
raise Csvlint::Csvw::MetadataError.new(""), "datatype cannot specify both minimum/minInclusive (#{minInclusive}) and minExclusive (#{minExclusive}" if minInclusive && minExclusive
|
335
|
+
raise Csvlint::Csvw::MetadataError.new(""), "datatype cannot specify both maximum/maxInclusive (#{maxInclusive}) and maxExclusive (#{maxExclusive}" if maxInclusive && maxExclusive
|
336
|
+
raise Csvlint::Csvw::MetadataError.new(""), "datatype minInclusive (#{minInclusive}) cannot be more than maxInclusive (#{maxInclusive}" if minInclusive && maxInclusive && minInclusive > maxInclusive
|
337
|
+
raise Csvlint::Csvw::MetadataError.new(""), "datatype minInclusive (#{minInclusive}) cannot be more than or equal to maxExclusive (#{maxExclusive}" if minInclusive && maxExclusive && minInclusive >= maxExclusive
|
338
|
+
raise Csvlint::Csvw::MetadataError.new(""), "datatype minExclusive (#{minExclusive}) cannot be more than or equal to maxExclusive (#{maxExclusive}" if minExclusive && maxExclusive && minExclusive > maxExclusive
|
339
|
+
raise Csvlint::Csvw::MetadataError.new(""), "datatype minExclusive (#{minExclusive}) cannot be more than maxInclusive (#{maxInclusive}" if minExclusive && maxInclusive && minExclusive >= maxInclusive
|
279
340
|
|
280
341
|
raise Csvlint::Csvw::MetadataError.new(""), "datatype length (#{value["length"]}) cannot be less than minLength (#{value["minLength"]}" if value["length"] && value["minLength"] && value["length"] < value["minLength"]
|
281
342
|
raise Csvlint::Csvw::MetadataError.new(""), "datatype length (#{value["length"]}) cannot be more than maxLength (#{value["maxLength"]}" if value["length"] && value["maxLength"] && value["length"] > value["maxLength"]
|
@@ -292,9 +353,9 @@ module Csvlint
|
|
292
353
|
elsif NUMERIC_FORMAT_DATATYPES.include?(value["base"])
|
293
354
|
value["format"] = { "pattern" => value["format"] } if value["format"].instance_of? String
|
294
355
|
begin
|
295
|
-
value["format"] = Csvlint::Csvw::NumberFormat.new(value["format"]["pattern"], value["format"]["groupChar"], value["format"]["decimalChar"] || ".")
|
356
|
+
value["format"] = Csvlint::Csvw::NumberFormat.new(value["format"]["pattern"], value["format"]["groupChar"], value["format"]["decimalChar"] || ".", INTEGER_FORMAT_DATATYPES.include?(value["base"]))
|
296
357
|
rescue Csvlint::Csvw::NumberFormatError
|
297
|
-
value["format"] = Csvlint::Csvw::NumberFormat.new(nil, value["format"]["groupChar"], value["format"]["decimalChar"] || ".")
|
358
|
+
value["format"] = Csvlint::Csvw::NumberFormat.new(nil, value["format"]["groupChar"], value["format"]["decimalChar"] || ".", INTEGER_FORMAT_DATATYPES.include?(value["base"]))
|
298
359
|
warnings << :invalid_number_format
|
299
360
|
end
|
300
361
|
elsif value["base"] == "http://www.w3.org/2001/XMLSchema#boolean"
|
@@ -326,9 +387,9 @@ module Csvlint
|
|
326
387
|
},
|
327
388
|
"required" => boolean_property(:inherited),
|
328
389
|
"ordered" => boolean_property(:inherited),
|
329
|
-
"aboutUrl" =>
|
330
|
-
"propertyUrl" =>
|
331
|
-
"valueUrl" =>
|
390
|
+
"aboutUrl" => uri_template_property(:inherited),
|
391
|
+
"propertyUrl" => uri_template_property(:inherited),
|
392
|
+
"valueUrl" => uri_template_property(:inherited),
|
332
393
|
"textDirection" => lambda { |value, base_url, lang|
|
333
394
|
value = value.to_sym
|
334
395
|
return value, nil, :inherited if [:ltr, :rtl, :auto, :inherit].include? value
|
@@ -421,31 +482,6 @@ module Csvlint
|
|
421
482
|
return schema, warnings, :table
|
422
483
|
},
|
423
484
|
"url" => link_property(:table),
|
424
|
-
"dialect" => lambda { |value, base_url, lang|
|
425
|
-
if value.instance_of? Hash
|
426
|
-
value = value.clone
|
427
|
-
warnings = []
|
428
|
-
value.each do |p,v|
|
429
|
-
if p == "@id"
|
430
|
-
raise Csvlint::Csvw::MetadataError.new("dialect.@id"), "@id starts with _:" if v =~ /^_:/
|
431
|
-
elsif p == "@type"
|
432
|
-
raise Csvlint::Csvw::MetadataError.new("dialect.@type"), "@type of dialect is not 'Dialect'" if v != 'Dialect'
|
433
|
-
else
|
434
|
-
v, warning, type = check_property(p, v, base_url, lang)
|
435
|
-
if type == :dialect && (warning.nil? || warning.empty?)
|
436
|
-
value[p] = v
|
437
|
-
else
|
438
|
-
value.delete(p)
|
439
|
-
warnings << :invalid_property unless type == :dialect
|
440
|
-
warnings += Array(warning)
|
441
|
-
end
|
442
|
-
end
|
443
|
-
end
|
444
|
-
return value, warnings, :table
|
445
|
-
else
|
446
|
-
return {}, :invalid_value, :table
|
447
|
-
end
|
448
|
-
},
|
449
485
|
# dialect properties
|
450
486
|
"commentPrefix" => string_property(:dialect),
|
451
487
|
"delimiter" => string_property(:dialect),
|
@@ -566,6 +602,12 @@ module Csvlint
|
|
566
602
|
"xhv" => "http://www.w3.org/1999/xhtml/vocab#",
|
567
603
|
"xml" => "http://www.w3.org/XML/1998/namespace",
|
568
604
|
"xsd" => "http://www.w3.org/2001/XMLSchema#",
|
605
|
+
"csvw" => "http://www.w3.org/ns/csvw#",
|
606
|
+
"cnt" => "http://www.w3.org/2008/content",
|
607
|
+
"earl" => "http://www.w3.org/ns/earl#",
|
608
|
+
"ht" => "http://www.w3.org/2006/http#",
|
609
|
+
"oa" => "http://www.w3.org/ns/oa#",
|
610
|
+
"ptr" => "http://www.w3.org/2009/pointers#",
|
569
611
|
"cc" => "http://creativecommons.org/ns#",
|
570
612
|
"ctag" => "http://commontag.org/ns#",
|
571
613
|
"dc" => "http://purl.org/dc/terms/",
|
@@ -638,8 +680,7 @@ module Csvlint
|
|
638
680
|
"http://www.w3.org/2001/XMLSchema#hexBinary"
|
639
681
|
]
|
640
682
|
|
641
|
-
|
642
|
-
"http://www.w3.org/2001/XMLSchema#decimal",
|
683
|
+
INTEGER_FORMAT_DATATYPES = [
|
643
684
|
"http://www.w3.org/2001/XMLSchema#integer",
|
644
685
|
"http://www.w3.org/2001/XMLSchema#long",
|
645
686
|
"http://www.w3.org/2001/XMLSchema#int",
|
@@ -652,10 +693,14 @@ module Csvlint
|
|
652
693
|
"http://www.w3.org/2001/XMLSchema#unsignedShort",
|
653
694
|
"http://www.w3.org/2001/XMLSchema#unsignedByte",
|
654
695
|
"http://www.w3.org/2001/XMLSchema#nonPositiveInteger",
|
655
|
-
"http://www.w3.org/2001/XMLSchema#negativeInteger"
|
696
|
+
"http://www.w3.org/2001/XMLSchema#negativeInteger"
|
697
|
+
]
|
698
|
+
|
699
|
+
NUMERIC_FORMAT_DATATYPES = [
|
700
|
+
"http://www.w3.org/2001/XMLSchema#decimal",
|
656
701
|
"http://www.w3.org/2001/XMLSchema#double",
|
657
702
|
"http://www.w3.org/2001/XMLSchema#float"
|
658
|
-
]
|
703
|
+
] + INTEGER_FORMAT_DATATYPES
|
659
704
|
|
660
705
|
DATE_FORMAT_DATATYPES = [
|
661
706
|
"http://www.w3.org/2001/XMLSchema#date",
|
data/lib/csvlint/csvw/table.rb
CHANGED
@@ -4,9 +4,9 @@ module Csvlint
|
|
4
4
|
|
5
5
|
include Csvlint::ErrorCollector
|
6
6
|
|
7
|
-
attr_reader :columns, :dialect, :table_direction, :foreign_keys, :foreign_key_references, :id, :notes, :primary_key, :schema, :suppress_output, :transformations, :url, :annotations
|
7
|
+
attr_reader :columns, :dialect, :table_direction, :foreign_keys, :foreign_key_references, :id, :notes, :primary_key, :row_title_columns, :schema, :suppress_output, :transformations, :url, :annotations
|
8
8
|
|
9
|
-
def initialize(url, columns: [], dialect: {}, table_direction: :auto, foreign_keys: [], id: nil, notes: [], primary_key: nil, schema: nil, suppress_output: false, transformations: [], annotations: [], warnings: [])
|
9
|
+
def initialize(url, columns: [], dialect: {}, table_direction: :auto, foreign_keys: [], id: nil, notes: [], primary_key: nil, row_title_columns: [], schema: nil, suppress_output: false, transformations: [], annotations: [], warnings: [])
|
10
10
|
@url = url
|
11
11
|
@columns = columns
|
12
12
|
@dialect = dialect
|
@@ -19,6 +19,7 @@ module Csvlint
|
|
19
19
|
@notes = notes
|
20
20
|
@primary_key = primary_key
|
21
21
|
@primary_key_values = {}
|
22
|
+
@row_title_columns = row_title_columns
|
22
23
|
@schema = schema
|
23
24
|
@suppress_output = suppress_output
|
24
25
|
@transformations = transformations
|
@@ -29,54 +30,59 @@ module Csvlint
|
|
29
30
|
@warnings += columns.map{|c| c.warnings}.flatten
|
30
31
|
end
|
31
32
|
|
32
|
-
def validate_header(headers)
|
33
|
+
def validate_header(headers, strict)
|
33
34
|
reset
|
34
35
|
headers.each_with_index do |header,i|
|
35
36
|
if columns[i]
|
36
|
-
columns[i].validate_header(header)
|
37
|
+
columns[i].validate_header(header, strict)
|
37
38
|
@errors += columns[i].errors
|
38
39
|
@warnings += columns[i].warnings
|
39
|
-
|
40
|
+
elsif strict
|
40
41
|
build_errors(:malformed_header, :schema, 1, nil, header, nil)
|
42
|
+
else
|
43
|
+
build_warnings(:malformed_header, :schema, 1, nil, header, nil)
|
41
44
|
end
|
42
|
-
end unless columns.empty?
|
45
|
+
end # unless columns.empty?
|
43
46
|
return valid?
|
44
47
|
end
|
45
48
|
|
46
|
-
def validate_row(values, row=nil)
|
49
|
+
def validate_row(values, row=nil, validate=false)
|
47
50
|
reset
|
48
51
|
values.each_with_index do |value,i|
|
49
52
|
column = columns[i]
|
50
53
|
if column
|
51
|
-
column.validate(value, row)
|
54
|
+
v = column.validate(value, row)
|
55
|
+
values[i] = v
|
52
56
|
@errors += column.errors
|
53
57
|
@warnings += column.warnings
|
54
58
|
else
|
55
59
|
build_errors(:too_many_values, :schema, row, nil, value, nil)
|
56
60
|
end
|
57
61
|
end unless columns.empty?
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
62
|
+
if validate
|
63
|
+
unless @primary_key.nil?
|
64
|
+
key = @primary_key.map { |column| column.validate(values[column.number - 1], row) }
|
65
|
+
build_errors(:duplicate_key, :schema, row, nil, key.join(","), @primary_key_values[key]) if @primary_key_values.include?(key)
|
66
|
+
@primary_key_values[key] = row
|
67
|
+
end
|
68
|
+
# build a record of the unique values that are referenced by foreign keys from other tables
|
69
|
+
# so that later we can check whether those foreign keys reference these values
|
70
|
+
@foreign_key_references.each do |foreign_key|
|
71
|
+
referenced_columns = foreign_key["referenced_columns"]
|
72
|
+
key = referenced_columns.map{ |column| column.validate(values[column.number - 1], row) }
|
73
|
+
known_values = @foreign_key_reference_values[foreign_key] = @foreign_key_reference_values[foreign_key] || {}
|
74
|
+
known_values[key] = known_values[key] || []
|
75
|
+
known_values[key] << row
|
76
|
+
end
|
77
|
+
# build a record of the references from this row to other tables
|
78
|
+
# we can't check yet whether these exist in the other tables because
|
79
|
+
# we might not have parsed those other tables
|
80
|
+
@foreign_keys.each do |foreign_key|
|
81
|
+
referencing_columns = foreign_key["referencing_columns"]
|
82
|
+
key = referencing_columns.map{ |column| column.validate(values[column.number - 1], row) }
|
83
|
+
known_values = @foreign_key_values[foreign_key] = @foreign_key_values[foreign_key] || []
|
84
|
+
known_values << key unless known_values.include?(key)
|
85
|
+
end
|
80
86
|
end
|
81
87
|
return valid?
|
82
88
|
end
|
@@ -107,19 +113,16 @@ module Csvlint
|
|
107
113
|
return valid?
|
108
114
|
end
|
109
115
|
|
110
|
-
def self.from_json(table_desc, base_url=nil, lang="und", inherited_properties={})
|
116
|
+
def self.from_json(table_desc, base_url=nil, lang="und", common_properties={}, inherited_properties={})
|
111
117
|
annotations = {}
|
112
118
|
warnings = []
|
113
|
-
table_properties = {}
|
114
119
|
columns = []
|
115
|
-
|
120
|
+
table_properties = common_properties.clone
|
116
121
|
inherited_properties = inherited_properties.clone
|
117
122
|
|
118
123
|
table_desc.each do |property,value|
|
119
124
|
if property =="@type"
|
120
125
|
raise Csvlint::Csvw::MetadataError.new("$.tables[?(@.url = '#{table_desc["url"]}')].@type"), "@type of table is not 'Table'" unless value == 'Table'
|
121
|
-
elsif property == "notes"
|
122
|
-
notes = value
|
123
126
|
else
|
124
127
|
v, warning, type = Csvw::PropertyChecker.check_property(property, value, base_url, lang)
|
125
128
|
warnings += Array(warning).map{ |w| Csvlint::ErrorMessage.new(w, :metadata, nil, nil, "#{property}: #{value}", nil) } unless warning.nil? || warning.empty?
|
@@ -140,7 +143,17 @@ module Csvlint
|
|
140
143
|
foreign_keys = []
|
141
144
|
primary_key = nil
|
142
145
|
if table_schema
|
143
|
-
|
146
|
+
unless table_schema["columns"].instance_of? Array
|
147
|
+
table_schema["columns"] = []
|
148
|
+
warnings << Csvlint::ErrorMessage.new(:invalid_value, :metadata, nil, nil, "columns", nil)
|
149
|
+
end
|
150
|
+
|
151
|
+
table_schema.each do |p,v|
|
152
|
+
unless ["columns", "primaryKey", "foreignKeys", "rowTitles"].include? p
|
153
|
+
inherited_properties[p] = v
|
154
|
+
end
|
155
|
+
end
|
156
|
+
|
144
157
|
virtual_columns = false
|
145
158
|
table_schema["columns"].each_with_index do |column_desc,i|
|
146
159
|
if column_desc.instance_of? Hash
|
@@ -180,8 +193,11 @@ module Csvlint
|
|
180
193
|
end if foreign_keys
|
181
194
|
|
182
195
|
row_titles = table_schema["rowTitles"]
|
183
|
-
|
184
|
-
|
196
|
+
row_title_columns = []
|
197
|
+
row_titles.each_with_index do |row_title|
|
198
|
+
i = column_names.index(row_title)
|
199
|
+
raise Csvlint::Csvw::MetadataError.new("$.tables[?(@.url = '#{table_desc["url"]}')].tableSchema.rowTitles[#{i}]"), "rowTitles references non-existant column" unless i
|
200
|
+
row_title_columns << columns[i]
|
185
201
|
end if row_titles
|
186
202
|
|
187
203
|
end
|
@@ -191,9 +207,11 @@ module Csvlint
|
|
191
207
|
columns: columns,
|
192
208
|
dialect: table_properties["dialect"],
|
193
209
|
foreign_keys: foreign_keys || [],
|
194
|
-
notes: notes,
|
210
|
+
notes: table_properties["notes"] || [],
|
195
211
|
primary_key: primary_key_valid && !primary_key_columns.empty? ? primary_key_columns : nil,
|
212
|
+
row_title_columns: row_title_columns,
|
196
213
|
schema: table_schema ? table_schema["@id"] : nil,
|
214
|
+
suppress_output: table_properties["suppressOutput"] ? table_properties["suppressOutput"] : false,
|
197
215
|
annotations: annotations,
|
198
216
|
warnings: warnings
|
199
217
|
)
|