jsontableschema 0.1.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 +7 -0
- data/.gitignore +9 -0
- data/.rspec +2 -0
- data/.travis.yml +13 -0
- data/CHANGELOG.md +17 -0
- data/CODE_OF_CONDUCT.md +49 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +21 -0
- data/README.md +233 -0
- data/Rakefile +6 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/etc/schemas/geojson.json +209 -0
- data/etc/schemas/json-table-schema.json +102 -0
- data/jsontableschema.gemspec +32 -0
- data/lib/jsontableschema.rb +41 -0
- data/lib/jsontableschema/constraints/constraints.rb +76 -0
- data/lib/jsontableschema/constraints/enum.rb +14 -0
- data/lib/jsontableschema/constraints/max_length.rb +15 -0
- data/lib/jsontableschema/constraints/maximum.rb +14 -0
- data/lib/jsontableschema/constraints/min_length.rb +15 -0
- data/lib/jsontableschema/constraints/minimum.rb +14 -0
- data/lib/jsontableschema/constraints/pattern.rb +14 -0
- data/lib/jsontableschema/constraints/required.rb +32 -0
- data/lib/jsontableschema/data.rb +57 -0
- data/lib/jsontableschema/exceptions.rb +28 -0
- data/lib/jsontableschema/helpers.rb +48 -0
- data/lib/jsontableschema/infer.rb +142 -0
- data/lib/jsontableschema/model.rb +73 -0
- data/lib/jsontableschema/schema.rb +35 -0
- data/lib/jsontableschema/table.rb +50 -0
- data/lib/jsontableschema/types/any.rb +23 -0
- data/lib/jsontableschema/types/array.rb +37 -0
- data/lib/jsontableschema/types/base.rb +54 -0
- data/lib/jsontableschema/types/boolean.rb +35 -0
- data/lib/jsontableschema/types/date.rb +56 -0
- data/lib/jsontableschema/types/datetime.rb +63 -0
- data/lib/jsontableschema/types/geojson.rb +38 -0
- data/lib/jsontableschema/types/geopoint.rb +56 -0
- data/lib/jsontableschema/types/integer.rb +35 -0
- data/lib/jsontableschema/types/null.rb +37 -0
- data/lib/jsontableschema/types/number.rb +60 -0
- data/lib/jsontableschema/types/object.rb +37 -0
- data/lib/jsontableschema/types/string.rb +64 -0
- data/lib/jsontableschema/types/time.rb +55 -0
- data/lib/jsontableschema/validate.rb +54 -0
- data/lib/jsontableschema/version.rb +3 -0
- metadata +230 -0
@@ -0,0 +1,35 @@
|
|
1
|
+
# Hack to check against one type from http://stackoverflow.com/a/3028378/452684
|
2
|
+
# because Ruby doesn't have a single boolean class
|
3
|
+
module Boolean; end
|
4
|
+
class TrueClass; include Boolean; end
|
5
|
+
class FalseClass; include Boolean; end
|
6
|
+
|
7
|
+
module JsonTableSchema
|
8
|
+
module Types
|
9
|
+
class Boolean < Base
|
10
|
+
|
11
|
+
def name
|
12
|
+
'boolean'
|
13
|
+
end
|
14
|
+
|
15
|
+
def self.supported_constraints
|
16
|
+
[
|
17
|
+
'required',
|
18
|
+
'pattern',
|
19
|
+
'enum',
|
20
|
+
]
|
21
|
+
end
|
22
|
+
|
23
|
+
def type
|
24
|
+
::Boolean
|
25
|
+
end
|
26
|
+
|
27
|
+
def cast_default(value)
|
28
|
+
value = convert_to_boolean(value)
|
29
|
+
raise JsonTableSchema::InvalidCast.new("#{value} is not a #{name}") if value.nil?
|
30
|
+
value
|
31
|
+
end
|
32
|
+
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,56 @@
|
|
1
|
+
module JsonTableSchema
|
2
|
+
module Types
|
3
|
+
class Date < Base
|
4
|
+
|
5
|
+
def name
|
6
|
+
'date'
|
7
|
+
end
|
8
|
+
|
9
|
+
def self.supported_constraints
|
10
|
+
[
|
11
|
+
'required',
|
12
|
+
'pattern',
|
13
|
+
'enum',
|
14
|
+
'minimum',
|
15
|
+
'maximum',
|
16
|
+
]
|
17
|
+
end
|
18
|
+
|
19
|
+
def type
|
20
|
+
::Date
|
21
|
+
end
|
22
|
+
|
23
|
+
def iso8601
|
24
|
+
'%Y-%m-%d'
|
25
|
+
end
|
26
|
+
|
27
|
+
def cast_default(value)
|
28
|
+
@format_string = iso8601
|
29
|
+
cast_fmt(value)
|
30
|
+
end
|
31
|
+
|
32
|
+
def cast_any(value)
|
33
|
+
return value if value.is_a?(type)
|
34
|
+
|
35
|
+
date = ::Date._parse(value)
|
36
|
+
if date.values.count == 3
|
37
|
+
::Date.parse(value)
|
38
|
+
else
|
39
|
+
raise JsonTableSchema::InvalidDateType.new("#{value} is not a valid date")
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
def cast_fmt(value)
|
44
|
+
return value if value.is_a?(type)
|
45
|
+
|
46
|
+
begin
|
47
|
+
return ::Date.strptime(value, @format_string)
|
48
|
+
rescue ArgumentError
|
49
|
+
raise JsonTableSchema::InvalidDateType.new("#{value} is not a valid date")
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
@@ -0,0 +1,63 @@
|
|
1
|
+
module JsonTableSchema
|
2
|
+
module Types
|
3
|
+
class DateTime < Base
|
4
|
+
|
5
|
+
def name
|
6
|
+
'datetime'
|
7
|
+
end
|
8
|
+
|
9
|
+
def self.supported_constraints
|
10
|
+
[
|
11
|
+
'required',
|
12
|
+
'pattern',
|
13
|
+
'enum',
|
14
|
+
'minimum',
|
15
|
+
'maximum'
|
16
|
+
]
|
17
|
+
end
|
18
|
+
|
19
|
+
def type
|
20
|
+
::DateTime
|
21
|
+
end
|
22
|
+
|
23
|
+
def iso8601
|
24
|
+
'%Y-%m-%dT%H:%M:%SZ'
|
25
|
+
end
|
26
|
+
|
27
|
+
# raw_formats = ['DD/MM/YYYYThh/mm/ss']
|
28
|
+
# py_formats = ['%Y/%m/%dT%H:%M:%S']
|
29
|
+
# format_map = dict(zip(raw_formats, py_formats))
|
30
|
+
|
31
|
+
def cast_default(value)
|
32
|
+
@format_string = iso8601
|
33
|
+
cast_fmt(value)
|
34
|
+
end
|
35
|
+
|
36
|
+
def cast_any(value)
|
37
|
+
return value if value.is_a?(type)
|
38
|
+
|
39
|
+
begin
|
40
|
+
date = ::DateTime._parse(value)
|
41
|
+
if date.values.count >= 4
|
42
|
+
::DateTime.parse(value)
|
43
|
+
else
|
44
|
+
raise JsonTableSchema::InvalidDateTimeType.new("#{value} is not a valid datetime")
|
45
|
+
end
|
46
|
+
rescue ArgumentError
|
47
|
+
raise JsonTableSchema::InvalidDateTimeType.new("#{value} is not a valid datetime")
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
def cast_fmt(value)
|
52
|
+
return value if value.is_a?(type)
|
53
|
+
|
54
|
+
begin
|
55
|
+
return ::DateTime.strptime(value, @format_string)
|
56
|
+
rescue ArgumentError
|
57
|
+
raise JsonTableSchema::InvalidDateTimeType.new("#{value} is not a valid date")
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
module JsonTableSchema
|
2
|
+
module Types
|
3
|
+
class GeoJSON < Base
|
4
|
+
|
5
|
+
def name
|
6
|
+
'geojson'
|
7
|
+
end
|
8
|
+
|
9
|
+
def self.supported_constraints
|
10
|
+
[
|
11
|
+
'required',
|
12
|
+
'pattern',
|
13
|
+
'enum'
|
14
|
+
]
|
15
|
+
end
|
16
|
+
|
17
|
+
def type
|
18
|
+
::Hash
|
19
|
+
end
|
20
|
+
|
21
|
+
def cast_default(value)
|
22
|
+
value = JSON.parse(value) if !value.is_a?(type)
|
23
|
+
JSON::Validator.validate!(geojson_schema, value)
|
24
|
+
value
|
25
|
+
rescue JSON::Schema::ValidationError, JSON::ParserError
|
26
|
+
raise JsonTableSchema::InvalidGeoJSONType.new("#{value} is not valid GeoJSON")
|
27
|
+
end
|
28
|
+
|
29
|
+
private
|
30
|
+
|
31
|
+
def geojson_schema
|
32
|
+
path = File.join( File.dirname(__FILE__), "..", "..", "..", "etc", "schemas", "geojson.json" )
|
33
|
+
@geojson_schema ||= JSON.parse File.read(path)
|
34
|
+
end
|
35
|
+
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,56 @@
|
|
1
|
+
module JsonTableSchema
|
2
|
+
module Types
|
3
|
+
class GeoPoint < Base
|
4
|
+
|
5
|
+
def name
|
6
|
+
'geopoint'
|
7
|
+
end
|
8
|
+
|
9
|
+
def self.supported_constraints
|
10
|
+
[
|
11
|
+
'required',
|
12
|
+
'pattern',
|
13
|
+
'enum'
|
14
|
+
]
|
15
|
+
end
|
16
|
+
|
17
|
+
def types
|
18
|
+
[::String, ::Array, ::Hash]
|
19
|
+
end
|
20
|
+
|
21
|
+
def cast_default(value)
|
22
|
+
latlng = value.split(',', 2)
|
23
|
+
cast_array([latlng[0], latlng[1]])
|
24
|
+
end
|
25
|
+
|
26
|
+
def cast_object(value)
|
27
|
+
value = JSON.parse(value) if value.is_a?(::String)
|
28
|
+
cast_array([value['longitude'], value['latitude']])
|
29
|
+
rescue JSON::ParserError
|
30
|
+
raise JsonTableSchema::InvalidGeoPointType.new("#{value} is not a valid geopoint")
|
31
|
+
end
|
32
|
+
|
33
|
+
def cast_array(value)
|
34
|
+
value = JSON.parse(value) if value.is_a?(::String)
|
35
|
+
value = [Float(value[0]), Float(value[1])]
|
36
|
+
check_latlng_range(value)
|
37
|
+
value
|
38
|
+
rescue JSON::ParserError, ArgumentError, TypeError
|
39
|
+
raise JsonTableSchema::InvalidGeoPointType.new("#{value} is not a valid geopoint")
|
40
|
+
end
|
41
|
+
|
42
|
+
private
|
43
|
+
|
44
|
+
def check_latlng_range(geopoint)
|
45
|
+
longitude = geopoint[0]
|
46
|
+
latitude = geopoint[1]
|
47
|
+
if longitude >= 180 or longitude <= -180
|
48
|
+
raise JsonTableSchema::InvalidGeoPointType.new("longtitude should be between -180 and 180, found `#{longitude}`")
|
49
|
+
elsif latitude >= 90 or latitude <= -90
|
50
|
+
raise JsonTableSchema::InvalidGeoPointType.new("longtitude should be between -90 and 90, found `#{latitude}`")
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
module JsonTableSchema
|
2
|
+
module Types
|
3
|
+
class Integer < Base
|
4
|
+
|
5
|
+
def name
|
6
|
+
'integer'
|
7
|
+
end
|
8
|
+
|
9
|
+
def self.supported_constraints
|
10
|
+
[
|
11
|
+
'required',
|
12
|
+
'pattern',
|
13
|
+
'enum',
|
14
|
+
'minimum',
|
15
|
+
'maximum',
|
16
|
+
]
|
17
|
+
end
|
18
|
+
|
19
|
+
def type
|
20
|
+
::Integer
|
21
|
+
end
|
22
|
+
|
23
|
+
def cast_default(value)
|
24
|
+
if value.is_a?(type)
|
25
|
+
value
|
26
|
+
else
|
27
|
+
Integer(value)
|
28
|
+
end
|
29
|
+
rescue ArgumentError
|
30
|
+
raise JsonTableSchema::InvalidCast.new("#{value} is not a #{name}")
|
31
|
+
end
|
32
|
+
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
module JsonTableSchema
|
2
|
+
module Types
|
3
|
+
class Null < Base
|
4
|
+
|
5
|
+
def name
|
6
|
+
'null'
|
7
|
+
end
|
8
|
+
|
9
|
+
def self.supported_constraints
|
10
|
+
[
|
11
|
+
'required',
|
12
|
+
'pattern',
|
13
|
+
'enum',
|
14
|
+
]
|
15
|
+
end
|
16
|
+
|
17
|
+
def type
|
18
|
+
::NilClass
|
19
|
+
end
|
20
|
+
|
21
|
+
def null_values
|
22
|
+
['null', 'none', 'nil', 'nan', '-', '']
|
23
|
+
end
|
24
|
+
|
25
|
+
def cast_default(value)
|
26
|
+
if value.is_a?(type)
|
27
|
+
return value
|
28
|
+
elsif null_values.include?(value.to_s.downcase)
|
29
|
+
nil
|
30
|
+
else
|
31
|
+
raise JsonTableSchema::InvalidCast.new("#{value} is not a #{name}")
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,60 @@
|
|
1
|
+
module JsonTableSchema
|
2
|
+
module Types
|
3
|
+
class Number < Base
|
4
|
+
|
5
|
+
def name
|
6
|
+
'number'
|
7
|
+
end
|
8
|
+
|
9
|
+
def self.supported_constraints
|
10
|
+
[
|
11
|
+
'required',
|
12
|
+
'pattern',
|
13
|
+
'enum',
|
14
|
+
'minimum',
|
15
|
+
'maximum',
|
16
|
+
]
|
17
|
+
end
|
18
|
+
|
19
|
+
def type
|
20
|
+
::Float
|
21
|
+
end
|
22
|
+
|
23
|
+
def currency_symbols
|
24
|
+
ISO4217::Currency.currencies.to_a.map { |c| Regexp.escape(c.last.symbol) rescue nil }.delete_if { |s| s.nil? }
|
25
|
+
end
|
26
|
+
|
27
|
+
def cast_default(value)
|
28
|
+
return value if value.class == type
|
29
|
+
return Float(value) if value.class == ::Fixnum
|
30
|
+
|
31
|
+
value = preprocess_value(value)
|
32
|
+
return Float(value)
|
33
|
+
rescue ArgumentError
|
34
|
+
raise JsonTableSchema::InvalidCast.new("#{value} is not a #{name}")
|
35
|
+
end
|
36
|
+
|
37
|
+
def cast_currency(value)
|
38
|
+
cast_default(value)
|
39
|
+
rescue JsonTableSchema::InvalidCast
|
40
|
+
value = preprocess_value(value)
|
41
|
+
re = Regexp.new currency_symbols.join('|')
|
42
|
+
value.gsub!(re, '')
|
43
|
+
cast_default(value)
|
44
|
+
end
|
45
|
+
|
46
|
+
private
|
47
|
+
|
48
|
+
def preprocess_value(value)
|
49
|
+
group_char = @field.fetch('groupChar', ',')
|
50
|
+
decimal_char = @field.fetch('decimalChar', '.')
|
51
|
+
percent_char = /%|‰|‱|%|﹪|٪/
|
52
|
+
value.gsub(group_char, '')
|
53
|
+
.gsub(decimal_char, '.')
|
54
|
+
.gsub(percent_char, '')
|
55
|
+
.gsub(Regexp.new(currency_symbols.join '|'), '')
|
56
|
+
end
|
57
|
+
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
module JsonTableSchema
|
2
|
+
module Types
|
3
|
+
class Object < Base
|
4
|
+
|
5
|
+
def name
|
6
|
+
'object'
|
7
|
+
end
|
8
|
+
|
9
|
+
def self.supported_constraints
|
10
|
+
[
|
11
|
+
'required',
|
12
|
+
'pattern',
|
13
|
+
'enum',
|
14
|
+
'minLength',
|
15
|
+
'maxLength',
|
16
|
+
]
|
17
|
+
end
|
18
|
+
|
19
|
+
def type
|
20
|
+
::Hash
|
21
|
+
end
|
22
|
+
|
23
|
+
def cast_default(value)
|
24
|
+
return value if value.is_a?(type)
|
25
|
+
parsed = JSON.parse(value)
|
26
|
+
if parsed.is_a?(Hash)
|
27
|
+
return parsed
|
28
|
+
else
|
29
|
+
raise JsonTableSchema::InvalidObjectType.new("#{value} is not a valid object")
|
30
|
+
end
|
31
|
+
rescue
|
32
|
+
raise JsonTableSchema::InvalidObjectType.new("#{value} is not a valid object")
|
33
|
+
end
|
34
|
+
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,64 @@
|
|
1
|
+
module JsonTableSchema
|
2
|
+
module Types
|
3
|
+
class String < Base
|
4
|
+
|
5
|
+
def name
|
6
|
+
'string'
|
7
|
+
end
|
8
|
+
|
9
|
+
def self.supported_constraints
|
10
|
+
[
|
11
|
+
'required',
|
12
|
+
'pattern',
|
13
|
+
'enum',
|
14
|
+
'minLength',
|
15
|
+
'maxLength',
|
16
|
+
]
|
17
|
+
end
|
18
|
+
|
19
|
+
def type
|
20
|
+
::String
|
21
|
+
end
|
22
|
+
|
23
|
+
def email_pattern
|
24
|
+
/[^@]+@[^@]+\.[^@]+/
|
25
|
+
end
|
26
|
+
|
27
|
+
def cast_default(value)
|
28
|
+
if value.is_a?(type)
|
29
|
+
return value
|
30
|
+
else
|
31
|
+
raise JsonTableSchema::InvalidCast.new("#{value} is not a #{name}")
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def cast_email(value)
|
36
|
+
value = cast_default(value)
|
37
|
+
if (value =~ email_pattern) != nil
|
38
|
+
value
|
39
|
+
else
|
40
|
+
raise JsonTableSchema::InvalidEmail.new("#{value} is not a valid email")
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
def cast_uri(value)
|
45
|
+
value = cast_default(value)
|
46
|
+
if (value =~ URI::regexp) != nil
|
47
|
+
value
|
48
|
+
else
|
49
|
+
raise JsonTableSchema::InvalidURI.new("#{value} is not a valid uri")
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
def cast_uuid(value)
|
54
|
+
value = cast_default(value)
|
55
|
+
if UUID.validate(value)
|
56
|
+
value
|
57
|
+
else
|
58
|
+
raise JsonTableSchema::InvalidUUID.new("#{value} is not a valid UUID")
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|