tsjson 1.0.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/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
|