tsjson 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/lib/errors/cant_distinguish_type_error.rb +17 -0
- data/lib/errors/index.rb +12 -0
- data/lib/errors/list_validation_error.rb +34 -0
- data/lib/errors/literal_union_validation_error.rb +18 -0
- data/lib/errors/literal_validation_error.rb +16 -0
- data/lib/errors/not_enough_discriminators.rb +7 -0
- data/lib/errors/object_validation_error.rb +56 -0
- data/lib/errors/required_field_error.rb +7 -0
- data/lib/errors/scalar_union_validation_error.rb +18 -0
- data/lib/errors/scalar_validation_error.rb +16 -0
- data/lib/errors/unexpected_field_error.rb +7 -0
- data/lib/errors/unexpected_value_error.rb +16 -0
- data/lib/errors/validation_error.rb +16 -0
- data/lib/language/ast/kind.rb +25 -0
- data/lib/language/lexer/lexer.rb +452 -0
- data/lib/language/lexer/location.rb +20 -0
- data/lib/language/lexer/syntax_error.rb +89 -0
- data/lib/language/lexer/token.rb +34 -0
- data/lib/language/lexer/token_kind.rb +37 -0
- data/lib/language/lexer/utils.rb +32 -0
- data/lib/language/parser/parser.rb +437 -0
- data/lib/language/source.rb +109 -0
- data/lib/schema/schema.rb +48 -0
- data/lib/schema/schema_builder.rb +148 -0
- data/lib/tsjson.rb +1 -0
- data/lib/types/any.rb +15 -0
- data/lib/types/base.rb +19 -0
- data/lib/types/boolean.rb +17 -0
- data/lib/types/discriminator_map.rb +116 -0
- data/lib/types/enum.rb +47 -0
- data/lib/types/float.rb +17 -0
- data/lib/types/index.rb +27 -0
- data/lib/types/int.rb +17 -0
- data/lib/types/intersection.rb +72 -0
- data/lib/types/list.rb +33 -0
- data/lib/types/literal.rb +25 -0
- data/lib/types/literal_union.rb +48 -0
- data/lib/types/merge.rb +21 -0
- data/lib/types/null.rb +17 -0
- data/lib/types/object.rb +87 -0
- data/lib/types/scalar.rb +24 -0
- data/lib/types/scalar_union.rb +25 -0
- data/lib/types/string.rb +17 -0
- data/lib/types/union.rb +61 -0
- metadata +85 -0
@@ -0,0 +1,109 @@
|
|
1
|
+
module TSJSON
|
2
|
+
class Source
|
3
|
+
attr_accessor :body, :name, :locationOffset
|
4
|
+
|
5
|
+
def initialize(
|
6
|
+
body, name = 'TSJSON Source', locationOffset = { line: 1, column: 1 }
|
7
|
+
)
|
8
|
+
self.body = body
|
9
|
+
self.name = name
|
10
|
+
self.locationOffset = locationOffset
|
11
|
+
end
|
12
|
+
|
13
|
+
def self.print_location(location)
|
14
|
+
return(
|
15
|
+
print_source_location(
|
16
|
+
location.source,
|
17
|
+
Location.get_location(location.source, location.start)
|
18
|
+
)
|
19
|
+
)
|
20
|
+
end
|
21
|
+
|
22
|
+
def self.print_source_location(source, sourceLocation)
|
23
|
+
firstLineColumnOffset = source.locationOffset[:column] - 1
|
24
|
+
body = whitespace(firstLineColumnOffset) + source.body
|
25
|
+
|
26
|
+
lineIndex = sourceLocation[:line] - 1
|
27
|
+
lineOffset = source.locationOffset[:line] - 1
|
28
|
+
lineNum = sourceLocation[:line] + lineOffset
|
29
|
+
|
30
|
+
columnOffset = sourceLocation[:line] === 1 ? firstLineColumnOffset : 0
|
31
|
+
columnNum = sourceLocation[:column] + columnOffset
|
32
|
+
locationStr = "#{source.name}:#{lineNum}:#{columnNum}\n"
|
33
|
+
|
34
|
+
lines = body.split("\n", -1)
|
35
|
+
locationLine = lines[lineIndex]
|
36
|
+
|
37
|
+
# Special case for minified documents
|
38
|
+
if (locationLine.length > 120)
|
39
|
+
subLineIndex = Math.floor(columnNum / 80)
|
40
|
+
subLineColumnNum = columnNum % 80
|
41
|
+
subLines = []
|
42
|
+
|
43
|
+
i = 0
|
44
|
+
loop do
|
45
|
+
break unless i < locationLine.length
|
46
|
+
subLines.push(locationLine.slice(i, 80))
|
47
|
+
i += 80
|
48
|
+
end
|
49
|
+
|
50
|
+
return(
|
51
|
+
locationStr +
|
52
|
+
print_prefixed_lines(
|
53
|
+
[["#{lineNum}", subLines[0]]].concat(
|
54
|
+
subLines.slice(1, subLineIndex + 1).map do |subLine|
|
55
|
+
['', subLine]
|
56
|
+
end
|
57
|
+
).concat(
|
58
|
+
[
|
59
|
+
[' ', whitespace(subLineColumnNum - 1) + '^'],
|
60
|
+
['', subLines[subLineIndex + 1]]
|
61
|
+
]
|
62
|
+
)
|
63
|
+
)
|
64
|
+
)
|
65
|
+
end
|
66
|
+
|
67
|
+
return(
|
68
|
+
locationStr +
|
69
|
+
print_prefixed_lines(
|
70
|
+
[
|
71
|
+
["#{lineNum - 1}", lineIndex > 1 ? lines[lineIndex - 1] : nil],
|
72
|
+
["#{lineNum}", locationLine],
|
73
|
+
['', whitespace(columnNum - 1) + '^'],
|
74
|
+
["#{lineNum + 1}", lines[lineIndex + 1]]
|
75
|
+
]
|
76
|
+
)
|
77
|
+
)
|
78
|
+
end
|
79
|
+
|
80
|
+
def self.whitespace(len)
|
81
|
+
return Array.new(len + 1, '').join(' ')
|
82
|
+
end
|
83
|
+
|
84
|
+
def self.leftPad(len, str)
|
85
|
+
return whitespace(len - str.length) + str
|
86
|
+
end
|
87
|
+
|
88
|
+
def self.print_prefixed_lines(lines)
|
89
|
+
existingLines =
|
90
|
+
lines.filter do |line_arr|
|
91
|
+
_, line = line_arr
|
92
|
+
line != nil
|
93
|
+
end
|
94
|
+
|
95
|
+
padLen =
|
96
|
+
existingLines.map do |prefix_arr|
|
97
|
+
prefix = prefix_arr[0]
|
98
|
+
prefix.length
|
99
|
+
end.max
|
100
|
+
|
101
|
+
return(
|
102
|
+
existingLines.map do |line_arr|
|
103
|
+
prefix, line = line_arr
|
104
|
+
leftPad(padLen, prefix) + (line.empty? ? ' |' : ' | ' + line)
|
105
|
+
end.join("\n")
|
106
|
+
)
|
107
|
+
end
|
108
|
+
end
|
109
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
require_relative '../types/index.rb'
|
2
|
+
require_relative './schema_builder'
|
3
|
+
|
4
|
+
module TSJSON
|
5
|
+
class Schema
|
6
|
+
attr_reader :types_map
|
7
|
+
|
8
|
+
BUILT_IN_SCALARS = {
|
9
|
+
'string' => TSJSONString,
|
10
|
+
'int' => TSJSONInt,
|
11
|
+
'float' => TSJSONFloat,
|
12
|
+
'number' => TSJSONFloat,
|
13
|
+
'boolean' => TSJSONBoolean,
|
14
|
+
'any' => TSJSONAny,
|
15
|
+
'null' => TSJSONNull
|
16
|
+
}
|
17
|
+
|
18
|
+
def initialize
|
19
|
+
@types_map = {}
|
20
|
+
end
|
21
|
+
|
22
|
+
def type(name)
|
23
|
+
BUILT_IN_SCALARS[name] || @types_map[name]
|
24
|
+
end
|
25
|
+
|
26
|
+
def add_type(name, type)
|
27
|
+
raise 'duplicate type' if type(name)
|
28
|
+
|
29
|
+
@types_map[name] = type
|
30
|
+
end
|
31
|
+
|
32
|
+
def compile
|
33
|
+
@types_map.values.each(&:compile)
|
34
|
+
end
|
35
|
+
|
36
|
+
def self.build(source)
|
37
|
+
unless source.is_a?(Array) || source.is_a?(String)
|
38
|
+
raise 'Schema.build expects string or array of strings'
|
39
|
+
end
|
40
|
+
builder = SchemaBuilder.new
|
41
|
+
(source.is_a?(Array) ? source : [source]).each do |s|
|
42
|
+
builder.parse_source(s)
|
43
|
+
end
|
44
|
+
builder.schema.compile
|
45
|
+
return builder.schema
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
@@ -0,0 +1,148 @@
|
|
1
|
+
require_relative '../language/parser/parser.rb'
|
2
|
+
|
3
|
+
module TSJSON
|
4
|
+
class SchemaBuilder
|
5
|
+
attr_reader :schema
|
6
|
+
|
7
|
+
def initialize
|
8
|
+
@schema = Schema.new
|
9
|
+
@generics = {}
|
10
|
+
end
|
11
|
+
|
12
|
+
def add_generic(name, parameters_ast, type_ast)
|
13
|
+
check_duplicate_type(name)
|
14
|
+
|
15
|
+
parameters = parameters_ast.map { |p| { name: p[:name][:value] } }
|
16
|
+
|
17
|
+
@generics[name] = { parameters: parameters, type_ast: type_ast }
|
18
|
+
end
|
19
|
+
|
20
|
+
def add_type(name, type_ast)
|
21
|
+
check_duplicate_type(name)
|
22
|
+
|
23
|
+
@schema.add_type(name, type_definition(type_ast))
|
24
|
+
end
|
25
|
+
|
26
|
+
def check_duplicate_type(name)
|
27
|
+
if @schema.type(name) || @generics.key?(name)
|
28
|
+
raise "duplicate type #{name}"
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def parse_source(source)
|
33
|
+
parser = Parser.new(source)
|
34
|
+
ast = parser.parse_document
|
35
|
+
|
36
|
+
ast[:definitions].each { |node| definition(node) }
|
37
|
+
end
|
38
|
+
|
39
|
+
def definition(node)
|
40
|
+
case node[:kind]
|
41
|
+
when AST::Kind::TypeAlias
|
42
|
+
name = node[:name][:value]
|
43
|
+
parameters = node[:parameters]
|
44
|
+
type_node = node[:definition]
|
45
|
+
|
46
|
+
if parameters.length > 0
|
47
|
+
add_generic(name, parameters, type_node)
|
48
|
+
else
|
49
|
+
add_type(name, type_node)
|
50
|
+
end
|
51
|
+
return
|
52
|
+
when AST::Kind::Enum
|
53
|
+
name = node[:name][:value]
|
54
|
+
members =
|
55
|
+
node[:members].reduce({}) do |acc, m|
|
56
|
+
acc.merge(
|
57
|
+
{
|
58
|
+
m[:name][:value] =>
|
59
|
+
Literal.new(m.dig(:value, :value) || m.dig(:name, :value)) # use name as value if value is not defined
|
60
|
+
}
|
61
|
+
)
|
62
|
+
end
|
63
|
+
type = Enum.new(members)
|
64
|
+
@schema.add_type(name, type)
|
65
|
+
return
|
66
|
+
end
|
67
|
+
|
68
|
+
raise "Can't build definition. Kind: #{node[:kind]}"
|
69
|
+
end
|
70
|
+
|
71
|
+
def type_definition(node, scope = {})
|
72
|
+
case node[:kind]
|
73
|
+
when AST::Kind::TypeReference
|
74
|
+
return reference(node, scope)
|
75
|
+
when AST::Kind::ArrayType
|
76
|
+
return List.new(type_definition(node[:type], scope))
|
77
|
+
when AST::Kind::StringLiteral
|
78
|
+
return Literal.new(node[:value])
|
79
|
+
when AST::Kind::Int
|
80
|
+
return Literal.new(node[:value])
|
81
|
+
when AST::Kind::Float
|
82
|
+
return Literal.new(node[:value])
|
83
|
+
when AST::Kind::TypeLiteral
|
84
|
+
return(
|
85
|
+
ObjectType.new do
|
86
|
+
node[:properties].reduce({}) do |map, f|
|
87
|
+
map[f[:name][:value]] = {
|
88
|
+
type: type_definition(f[:type], scope),
|
89
|
+
optional: f[:optional]
|
90
|
+
}
|
91
|
+
map
|
92
|
+
end
|
93
|
+
end
|
94
|
+
)
|
95
|
+
when AST::Kind::IntersectionType
|
96
|
+
return(
|
97
|
+
Intersection.new(node[:types].map { |t| type_definition(t, scope) })
|
98
|
+
)
|
99
|
+
when AST::Kind::UnionType
|
100
|
+
types = node[:types].map { |t| type_definition(t, scope) }
|
101
|
+
t = types.first
|
102
|
+
|
103
|
+
if t.is_a?(ObjectType) || t.is_a?(Union) || t.is_a?(Intersection)
|
104
|
+
return Union.new(types)
|
105
|
+
elsif t.is_a?(ScalarType)
|
106
|
+
return ScalarUnion.new(types)
|
107
|
+
elsif t.is_a?(Literal)
|
108
|
+
return LiteralUnion.new(types)
|
109
|
+
end
|
110
|
+
raise "can't create union with type #{t.class.name}"
|
111
|
+
when AST::Kind::ParenthesizedType
|
112
|
+
return type_definition(node[:type], scope)
|
113
|
+
when AST::Kind::PropertyAccess
|
114
|
+
target = type_definition(node[:target], scope)
|
115
|
+
return target.property(node[:property][:value])
|
116
|
+
when AST::Kind::IndexAccess
|
117
|
+
target = type_definition(node[:target], scope)
|
118
|
+
return target.index(node[:index][:value])
|
119
|
+
end
|
120
|
+
|
121
|
+
raise "Can't build type definition. Kind: #{node[:kind]}"
|
122
|
+
end
|
123
|
+
|
124
|
+
def reference(node, scope)
|
125
|
+
name = node[:name][:value]
|
126
|
+
|
127
|
+
if type = scope[name] || @schema.type(name)
|
128
|
+
return type
|
129
|
+
elsif generic = @generics[name]
|
130
|
+
parameters = generic[:parameters]
|
131
|
+
args = node[:args]
|
132
|
+
|
133
|
+
new_scope =
|
134
|
+
generic[:parameters]
|
135
|
+
.each_with_index
|
136
|
+
.reduce(scope) do |memo, (param, idx)|
|
137
|
+
name = param[:name]
|
138
|
+
type_ast = args[idx]
|
139
|
+
raise "argument #{name} was not provided" if (!type_ast)
|
140
|
+
|
141
|
+
memo.merge({ name => type_definition(type_ast, scope) })
|
142
|
+
end
|
143
|
+
return type_definition(generic[:type_ast], new_scope)
|
144
|
+
end
|
145
|
+
raise "Type doesn't exist: #{name}" unless type
|
146
|
+
end
|
147
|
+
end
|
148
|
+
end
|
data/lib/tsjson.rb
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require_relative './schema/schema.rb'
|
data/lib/types/any.rb
ADDED
data/lib/types/base.rb
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
module TSJSON
|
2
|
+
class Base
|
3
|
+
def valid?(value)
|
4
|
+
validate(value)
|
5
|
+
rescue ValidationError
|
6
|
+
false
|
7
|
+
end
|
8
|
+
|
9
|
+
def compile; end
|
10
|
+
|
11
|
+
def property(name)
|
12
|
+
raise "Can't access property #{name} of #{self.class.name}"
|
13
|
+
end
|
14
|
+
|
15
|
+
def index(name)
|
16
|
+
raise "Can't access index #{name} of #{self.class.name}"
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
require_relative './scalar.rb'
|
2
|
+
|
3
|
+
module TSJSON
|
4
|
+
class BooleanType < ScalarType
|
5
|
+
def initialize
|
6
|
+
super('Boolean')
|
7
|
+
end
|
8
|
+
|
9
|
+
def validate(value)
|
10
|
+
super(value) unless value == true || value == false
|
11
|
+
|
12
|
+
true
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
TSJSONBoolean = BooleanType.new
|
17
|
+
end
|
@@ -0,0 +1,116 @@
|
|
1
|
+
require_relative './literal.rb'
|
2
|
+
|
3
|
+
module TSJSON
|
4
|
+
class DiscriminatorMap
|
5
|
+
attr_reader :discriminators
|
6
|
+
|
7
|
+
def initialize(types)
|
8
|
+
# Hash with discriminators.
|
9
|
+
# Key - name of the field
|
10
|
+
# Value - enum with all possible field values
|
11
|
+
discriminators = {}
|
12
|
+
|
13
|
+
# Hash of form H[field_name][field_value]=[...all types that matches field_name=field_value]
|
14
|
+
hash = {}
|
15
|
+
|
16
|
+
types.each do |t|
|
17
|
+
# for each field inside each type
|
18
|
+
t.fields.each do |f|
|
19
|
+
next unless f[:type].is_a?(Literal) || f[:type].is_a?(LiteralUnion) # only literal types may be used as discriminators
|
20
|
+
|
21
|
+
field_name = f[:name]
|
22
|
+
|
23
|
+
field_types = f[:type].is_a?(Literal) ? [f[:type]] : f[:type].types # iterate over all types of LiteralUnion
|
24
|
+
field_types.each do |field_type|
|
25
|
+
# create discriminator enums
|
26
|
+
if discriminators[field_name] == nil
|
27
|
+
discriminators[field_name] = LiteralUnion.new([field_type])
|
28
|
+
else
|
29
|
+
discriminators[field_name] =
|
30
|
+
discriminators[field_name].push_type(field_type)
|
31
|
+
end
|
32
|
+
|
33
|
+
# create array of types for each discriminator
|
34
|
+
if hash[field_name] == nil
|
35
|
+
hash[field_name] = { field_type.value => [t] }
|
36
|
+
else
|
37
|
+
if hash[field_name][field_type.value] == nil
|
38
|
+
hash[field_name][field_type.value] = [t]
|
39
|
+
else
|
40
|
+
hash[field_name][field_type.value].push(t)
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
# delete discriminators that appear in all types or in one only
|
48
|
+
hash.each do |field_name, values_hash|
|
49
|
+
if values_hash.values.length == 1 &&
|
50
|
+
values_hash.values[0].length == types.length
|
51
|
+
discriminators.delete(field_name)
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
# filter valid discriminators
|
56
|
+
hash =
|
57
|
+
hash.reject do |field_name, values_hash|
|
58
|
+
values_hash.values.length == 1 &&
|
59
|
+
values_hash.values[0].length == types.length
|
60
|
+
end.to_h
|
61
|
+
|
62
|
+
# check that all types are mapped
|
63
|
+
mapped_types = hash.values.map(&:values).flatten.uniq
|
64
|
+
if (mapped_types.length < types.length)
|
65
|
+
raise NotEnoughDiscriminators.new(types: types)
|
66
|
+
end
|
67
|
+
|
68
|
+
# replace array of types with single type or nested map
|
69
|
+
@hash =
|
70
|
+
hash.transform_values do |values_hash|
|
71
|
+
values_hash.transform_values do |types|
|
72
|
+
types.length == 1 ? types.first : DiscriminatorMap.new(types)
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
# create discriminators array sorted by appearance count
|
77
|
+
@discriminators =
|
78
|
+
discriminators.map { |k, v| { field: k, value: v } }.sort do |a, b|
|
79
|
+
b[:value].size <=> a[:value].size
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
def validate(object)
|
84
|
+
object = object&.transform_keys!(&:to_s)
|
85
|
+
|
86
|
+
discriminators.each do |discriminator|
|
87
|
+
field = discriminator[:field]
|
88
|
+
expected_value = discriminator[:value]
|
89
|
+
|
90
|
+
value = object[field]
|
91
|
+
unless value.nil?
|
92
|
+
unless expected_value.has?(value)
|
93
|
+
raise UnexpectedValueError.new(
|
94
|
+
field: field,
|
95
|
+
expected_values: expected_value.to_json,
|
96
|
+
received: value
|
97
|
+
)
|
98
|
+
end
|
99
|
+
|
100
|
+
return @hash[field][value].validate(object)
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
raise CantDistinguishTypeError.new(
|
105
|
+
discriminators:
|
106
|
+
discriminators.map { |d|
|
107
|
+
{ field: d[:field], values: d[:value].to_json }
|
108
|
+
}
|
109
|
+
)
|
110
|
+
end
|
111
|
+
|
112
|
+
def [](value)
|
113
|
+
@hash[value]
|
114
|
+
end
|
115
|
+
end
|
116
|
+
end
|