tsjson 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (46) hide show
  1. checksums.yaml +7 -0
  2. data/lib/errors/cant_distinguish_type_error.rb +17 -0
  3. data/lib/errors/index.rb +12 -0
  4. data/lib/errors/list_validation_error.rb +34 -0
  5. data/lib/errors/literal_union_validation_error.rb +18 -0
  6. data/lib/errors/literal_validation_error.rb +16 -0
  7. data/lib/errors/not_enough_discriminators.rb +7 -0
  8. data/lib/errors/object_validation_error.rb +56 -0
  9. data/lib/errors/required_field_error.rb +7 -0
  10. data/lib/errors/scalar_union_validation_error.rb +18 -0
  11. data/lib/errors/scalar_validation_error.rb +16 -0
  12. data/lib/errors/unexpected_field_error.rb +7 -0
  13. data/lib/errors/unexpected_value_error.rb +16 -0
  14. data/lib/errors/validation_error.rb +16 -0
  15. data/lib/language/ast/kind.rb +25 -0
  16. data/lib/language/lexer/lexer.rb +452 -0
  17. data/lib/language/lexer/location.rb +20 -0
  18. data/lib/language/lexer/syntax_error.rb +89 -0
  19. data/lib/language/lexer/token.rb +34 -0
  20. data/lib/language/lexer/token_kind.rb +37 -0
  21. data/lib/language/lexer/utils.rb +32 -0
  22. data/lib/language/parser/parser.rb +437 -0
  23. data/lib/language/source.rb +109 -0
  24. data/lib/schema/schema.rb +48 -0
  25. data/lib/schema/schema_builder.rb +148 -0
  26. data/lib/tsjson.rb +1 -0
  27. data/lib/types/any.rb +15 -0
  28. data/lib/types/base.rb +19 -0
  29. data/lib/types/boolean.rb +17 -0
  30. data/lib/types/discriminator_map.rb +116 -0
  31. data/lib/types/enum.rb +47 -0
  32. data/lib/types/float.rb +17 -0
  33. data/lib/types/index.rb +27 -0
  34. data/lib/types/int.rb +17 -0
  35. data/lib/types/intersection.rb +72 -0
  36. data/lib/types/list.rb +33 -0
  37. data/lib/types/literal.rb +25 -0
  38. data/lib/types/literal_union.rb +48 -0
  39. data/lib/types/merge.rb +21 -0
  40. data/lib/types/null.rb +17 -0
  41. data/lib/types/object.rb +87 -0
  42. data/lib/types/scalar.rb +24 -0
  43. data/lib/types/scalar_union.rb +25 -0
  44. data/lib/types/string.rb +17 -0
  45. data/lib/types/union.rb +61 -0
  46. 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
@@ -0,0 +1 @@
1
+ require_relative './schema/schema.rb'
@@ -0,0 +1,15 @@
1
+ require_relative './scalar.rb'
2
+
3
+ module TSJSON
4
+ class AnyType < ScalarType
5
+ def initialize
6
+ super('Any')
7
+ end
8
+
9
+ def validate(value)
10
+ true
11
+ end
12
+ end
13
+
14
+ TSJSONAny = AnyType.new
15
+ end
@@ -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