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.
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