taipo 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/.gitignore +12 -0
- data/Gemfile +4 -0
- data/LICENSE.md +9 -0
- data/README.md +110 -0
- data/Rakefile +9 -0
- data/lib/taipo.rb +39 -0
- data/lib/taipo/cache.rb +49 -0
- data/lib/taipo/check.rb +117 -0
- data/lib/taipo/exceptions.rb +6 -0
- data/lib/taipo/exceptions/name_error.rb +8 -0
- data/lib/taipo/exceptions/syntax_error.rb +9 -0
- data/lib/taipo/exceptions/type_error.rb +9 -0
- data/lib/taipo/parser.rb +171 -0
- data/lib/taipo/parser/syntax_state.rb +234 -0
- data/lib/taipo/parser/validater.rb +226 -0
- data/lib/taipo/type_element.rb +200 -0
- data/lib/taipo/type_element/child_type.rb +21 -0
- data/lib/taipo/type_element/constraint.rb +152 -0
- data/lib/taipo/version.rb +3 -0
- data/taipo.gemspec +27 -0
- metadata +150 -0
@@ -0,0 +1,234 @@
|
|
1
|
+
module Taipo
|
2
|
+
module Parser
|
3
|
+
|
4
|
+
# A state machine
|
5
|
+
#
|
6
|
+
# @since 1.0.0
|
7
|
+
# @api private
|
8
|
+
class SyntaxState
|
9
|
+
|
10
|
+
# Initialize a new state machine
|
11
|
+
#
|
12
|
+
# @param state_names [Array<Symbol>] an array of symbols designating the
|
13
|
+
# particular states to be used
|
14
|
+
# @param counter_names_and_closers [Array<Array<Symbol>,Hash<Symbol,
|
15
|
+
# String>>] an array of two collections: an array of symbols
|
16
|
+
# designating the names the state machine will use for counting
|
17
|
+
# brackets and a hash of closing characters used for each bracket (the
|
18
|
+
# key for each closing character should have the same name as the name
|
19
|
+
# used for the counter)
|
20
|
+
#
|
21
|
+
# @since 1.0.0
|
22
|
+
# @api private
|
23
|
+
#
|
24
|
+
# @example
|
25
|
+
# status_array = [ :foo, :bar ]
|
26
|
+
# counter_array = [ [ :angle ], { angle: '>' } ]
|
27
|
+
# state = SyntaxState.new(status_array, counter_array)
|
28
|
+
def initialize(state_names, counter_names_and_closers = nil)
|
29
|
+
@status = Hash[state_names.map { |s| [s, :prohibited] }]
|
30
|
+
if counter_names_and_closers.nil?
|
31
|
+
@counter = Array.new
|
32
|
+
@closers = Array.new
|
33
|
+
else
|
34
|
+
@counter = Hash[counter_names_and_closers[0].map { |c| [c, 0] }]
|
35
|
+
@closers = counter_names_and_closers[1]
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
# Check if the counter for the given +key+ has been incremented
|
40
|
+
#
|
41
|
+
# @param key [Symbol] the counter to check
|
42
|
+
#
|
43
|
+
# @return [Boolean] the result
|
44
|
+
#
|
45
|
+
# @since 1.0.0
|
46
|
+
# @api private
|
47
|
+
def active?(key)
|
48
|
+
@counter[key] > 0
|
49
|
+
end
|
50
|
+
|
51
|
+
# Set the status for the given +key+ to be +:allowed+
|
52
|
+
#
|
53
|
+
# @param key [Symbol] the key to set
|
54
|
+
#
|
55
|
+
# @since 1.0.0
|
56
|
+
# @api private
|
57
|
+
def allow(key)
|
58
|
+
@status[key] = :allowed
|
59
|
+
end
|
60
|
+
|
61
|
+
# Set all statuses to be +:allowed+ except those specified in the
|
62
|
+
# +except+ array
|
63
|
+
#
|
64
|
+
# @note Statuses which have been set to +:disabled+ will not be updated
|
65
|
+
#
|
66
|
+
# @param except [Array<Symbol>] keys not to update to +:allowed+ (will
|
67
|
+
# instead be set to +:prohibited+)
|
68
|
+
#
|
69
|
+
# @since 1.0.0
|
70
|
+
# @api private
|
71
|
+
def allow_all(except: [])
|
72
|
+
set_all :allowed, except: { exceptions: except, status: :prohibited }
|
73
|
+
end
|
74
|
+
|
75
|
+
# Check if the given +key+ is allowed
|
76
|
+
#
|
77
|
+
# @param key [Symbol] the key to check
|
78
|
+
#
|
79
|
+
# @return [Boolean] the result
|
80
|
+
#
|
81
|
+
# @since 1.0.0
|
82
|
+
# @api private
|
83
|
+
def allowed?(key)
|
84
|
+
@status[key] == :allowed
|
85
|
+
end
|
86
|
+
|
87
|
+
# Get the count for the given +key+
|
88
|
+
#
|
89
|
+
# @param key [Symbol] the key for the counter
|
90
|
+
#
|
91
|
+
# @return [Integer] the count
|
92
|
+
#
|
93
|
+
# @since 1.0.0
|
94
|
+
# @api private
|
95
|
+
def count(key)
|
96
|
+
@counter[key]
|
97
|
+
end
|
98
|
+
|
99
|
+
# Decrement the count for the given +key+ by 1
|
100
|
+
#
|
101
|
+
# @param key [Symbol] the key for the counter
|
102
|
+
#
|
103
|
+
# @since 1.0.0
|
104
|
+
# @api private
|
105
|
+
def decrement(key)
|
106
|
+
msg = 'Trying to reduce count below zero.'
|
107
|
+
raise RangeError, msg if @counter[key] == 0
|
108
|
+
@counter[key] -= 1
|
109
|
+
end
|
110
|
+
|
111
|
+
# Disable the status of the given +key+ (by setting it to +:disabled+)
|
112
|
+
#
|
113
|
+
# @param key [Symbol] the +key+ to disable
|
114
|
+
#
|
115
|
+
# @since 1.0.0
|
116
|
+
# @api private
|
117
|
+
def disable(key)
|
118
|
+
@status[key] = :disabled
|
119
|
+
end
|
120
|
+
|
121
|
+
# Enable the status of the given +key+ (by setting it to +:prohibited+)
|
122
|
+
#
|
123
|
+
# @param key [Symbol] the +key+ to disable
|
124
|
+
#
|
125
|
+
# @since 1.0.0
|
126
|
+
# @api private
|
127
|
+
def enable(key)
|
128
|
+
@status[key] = :prohibited
|
129
|
+
end
|
130
|
+
|
131
|
+
# Increment the counter for the given +key+ by 1
|
132
|
+
#
|
133
|
+
# @param key [Symbol] the key for the counter
|
134
|
+
#
|
135
|
+
# @since 1.0.0
|
136
|
+
# @api private
|
137
|
+
def increment(key)
|
138
|
+
@counter[key] += 1
|
139
|
+
end
|
140
|
+
|
141
|
+
# Check if we are 'inside' a set of brackets (eg. a pair of parentheses)
|
142
|
+
# for a given +key+
|
143
|
+
#
|
144
|
+
# @param key [Symbol] the key for the counter
|
145
|
+
#
|
146
|
+
# @return [Boolean] the result
|
147
|
+
#
|
148
|
+
# @since 1.0.0
|
149
|
+
# @api private
|
150
|
+
def inside?(key)
|
151
|
+
@counter[key] > 0
|
152
|
+
end
|
153
|
+
|
154
|
+
# Check if we are 'outside' a set of brackets (eg. a pair of parentheses)
|
155
|
+
# for a given +key+
|
156
|
+
#
|
157
|
+
# @param key [Symbol] the key for the counter
|
158
|
+
#
|
159
|
+
# @return [Boolean] the result
|
160
|
+
#
|
161
|
+
# @since 1.0.0
|
162
|
+
# @api private
|
163
|
+
def outside?(key)
|
164
|
+
@counter[key] == 0
|
165
|
+
end
|
166
|
+
|
167
|
+
# Set the status for the given +key+ to be +:prohibited+
|
168
|
+
#
|
169
|
+
# @param key [Symbol] the key to set
|
170
|
+
#
|
171
|
+
# @since 1.0.0
|
172
|
+
# @api private
|
173
|
+
def prohibit(key)
|
174
|
+
@status[key] = :prohibited
|
175
|
+
end
|
176
|
+
|
177
|
+
# Set all statuses to be +:prohibited+ except those specified in the
|
178
|
+
# +except+ array
|
179
|
+
#
|
180
|
+
# @note Statuses which have been set to +:disabled+ will not be updated
|
181
|
+
#
|
182
|
+
# @param except [Array<Symbol>] keys not to update to +:prohibited+ (will
|
183
|
+
# instead be set to +:allowed+)
|
184
|
+
#
|
185
|
+
# @since 1.0.0
|
186
|
+
# @api private
|
187
|
+
def prohibit_all(except: [])
|
188
|
+
set_all :prohibited, except: { exceptions: except, status: :allowed }
|
189
|
+
end
|
190
|
+
|
191
|
+
# Check if the given +key+ is allowed
|
192
|
+
#
|
193
|
+
# @param key [Symbol] the key to check
|
194
|
+
#
|
195
|
+
# @since 1.0.0
|
196
|
+
# @api private
|
197
|
+
def prohibited?(key)
|
198
|
+
@status[key] == :prohibited
|
199
|
+
end
|
200
|
+
|
201
|
+
# Set all statuses to be +status+ except those specified in the +except+
|
202
|
+
# array
|
203
|
+
#
|
204
|
+
# @note Statuses which have been set to +:disabled+ will not be updated
|
205
|
+
#
|
206
|
+
# @param status [Symbol] the value for all statuses
|
207
|
+
# @param except [Hash<Array<Symbol>, Symbol>] the exceptions
|
208
|
+
# @option except [Array<Symbol>] :exceptions keys not to update to +status+
|
209
|
+
# @option except [Symbol] :status the value for exceptions
|
210
|
+
#
|
211
|
+
# @since 1.0.0
|
212
|
+
# @api private
|
213
|
+
def set_all(status, except: {})
|
214
|
+
@status.transform_values! { |v| v = status unless v == :disabled }
|
215
|
+
except[:exceptions].each do |k|
|
216
|
+
@status[k] = except[:status] unless @status[k] == :disabled
|
217
|
+
end
|
218
|
+
end
|
219
|
+
|
220
|
+
# Get the names of the unbalanced brackets
|
221
|
+
#
|
222
|
+
# @return [Array<Symbol>] an array of the names of the unbalanced
|
223
|
+
# brackets
|
224
|
+
#
|
225
|
+
# @since 1.0.0
|
226
|
+
# @api
|
227
|
+
def unbalanced()
|
228
|
+
@counter.reduce(Array.new) do |memo, c|
|
229
|
+
(c[1] == 0) ? memo : memo.push(@closers[c[0]])
|
230
|
+
end
|
231
|
+
end
|
232
|
+
end
|
233
|
+
end
|
234
|
+
end
|
@@ -0,0 +1,226 @@
|
|
1
|
+
require 'taipo/exceptions'
|
2
|
+
require 'taipo/parser/syntax_state'
|
3
|
+
|
4
|
+
module Taipo
|
5
|
+
module Parser
|
6
|
+
|
7
|
+
# A validater of Taipo type definitions
|
8
|
+
# @since 1.0.0
|
9
|
+
module Validater
|
10
|
+
|
11
|
+
# Check +str+ is a valid type definition
|
12
|
+
#
|
13
|
+
# @param str [String] a type definition
|
14
|
+
#
|
15
|
+
# @raise [::TypeError] if +str+ is not a String
|
16
|
+
# @raise [Taipo::SyntaxError] if +str+ is not a valid type definition
|
17
|
+
#
|
18
|
+
# @since 1.0.0
|
19
|
+
def self.validate(str)
|
20
|
+
msg = "The argument to this method must be of type String."
|
21
|
+
raise ::TypeError, msg unless str.is_a? String
|
22
|
+
msg = "The string to be checked was empty."
|
23
|
+
raise Taipo::SyntaxError, msg if str.empty?
|
24
|
+
|
25
|
+
status_array = [ :bar, :lab, :rab, :lpr, :rpr, :hsh, :cln, :sls, :qut,
|
26
|
+
:cma, :spc, :oth, :end ]
|
27
|
+
counter_array = [ [ :angle, :paren, :const ],
|
28
|
+
{ angle: '>', paren: ')', const: ":' or '#" } ]
|
29
|
+
state = Taipo::Parser::SyntaxState.new(status_array, counter_array)
|
30
|
+
|
31
|
+
i = 0
|
32
|
+
chars = str.chars
|
33
|
+
str_length = chars.size
|
34
|
+
|
35
|
+
state.prohibit_all except: [ :hsh, :oth ]
|
36
|
+
|
37
|
+
while (i < str_length)
|
38
|
+
msg = "The string '#{str}' has an error here: #{str[0, i+1]}"
|
39
|
+
case chars[i]
|
40
|
+
when '|' # bar
|
41
|
+
conditions = [ state.allowed?(:bar) ]
|
42
|
+
raise Taipo::SyntaxError, msg unless conditions.all?
|
43
|
+
state.enable :lab
|
44
|
+
state.enable :lpr
|
45
|
+
state.prohibit_all except: [ :hsh, :oth ]
|
46
|
+
when '<' # lab
|
47
|
+
conditions = [ state.allowed?(:lab) ]
|
48
|
+
raise Taipo::SyntaxError, msg unless conditions.all?
|
49
|
+
state.prohibit_all except: [ :hsh, :oth ]
|
50
|
+
state.increment :angle
|
51
|
+
when '>' # rab
|
52
|
+
conditions = [ state.allowed?(:rab), state.inside?(:angle) ]
|
53
|
+
raise Taipo::SyntaxError, msg unless conditions.all?
|
54
|
+
state.prohibit_all except: [ :bar, :rab, :lpr, :end ]
|
55
|
+
state.decrement :angle
|
56
|
+
when '(' # lpr
|
57
|
+
conditions = [ state.allowed?(:lpr), state.outside?(:paren) ]
|
58
|
+
raise Taipo::SyntaxError, msg unless conditions.all?
|
59
|
+
state.prohibit_all except: [ :hsh, :oth ]
|
60
|
+
state.increment :paren
|
61
|
+
state.increment :const
|
62
|
+
when ')' # rpr
|
63
|
+
conditions = [ state.allowed?(:rpr), state.inside?(:paren) ]
|
64
|
+
raise Taipo::SyntaxError, msg unless conditions.all?
|
65
|
+
state.prohibit_all except: [ :bar, :rab, :end ]
|
66
|
+
state.decrement :paren
|
67
|
+
when '#' # hsh
|
68
|
+
conditions = [ state.allowed?(:hsh) ]
|
69
|
+
raise Taipo::SyntaxError, msg unless conditions.all?
|
70
|
+
if state.outside? :paren
|
71
|
+
state.disable :lab
|
72
|
+
state.disable :lpr
|
73
|
+
state.prohibit_all except: [ :oth ]
|
74
|
+
else
|
75
|
+
state.prohibit_all except: [ :oth ]
|
76
|
+
state.decrement :const
|
77
|
+
end
|
78
|
+
when ':' # cln
|
79
|
+
conditions = [ state.allowed?(:cln), state.inside?(:paren) ]
|
80
|
+
raise Taipo::SyntaxError, msg unless conditions.all?
|
81
|
+
state.prohibit_all except: [ :sls, :qut, :spc, :oth ]
|
82
|
+
state.decrement :const
|
83
|
+
when '/' #sls
|
84
|
+
conditions = [ state.allowed?(:sls), state.inside?(:paren),
|
85
|
+
state.outside?(:const) ]
|
86
|
+
raise Taipo::SyntaxError, msg unless conditions.all?
|
87
|
+
i = Taipo::Parser::Validater.validate_regex(str, start: i+1)
|
88
|
+
state.prohibit_all except: [ :rpr, :cma ]
|
89
|
+
when '"' #qut
|
90
|
+
conditions = [ state.allowed?(:qut), state.inside?(:paren),
|
91
|
+
state.outside?(:const) ]
|
92
|
+
raise Taipo::SyntaxError, msg unless conditions.all?
|
93
|
+
i = Taipo::Parser::Validater.validate_string(str, start: i+1)
|
94
|
+
state.prohibit_all except: [ :rpr, :cma ]
|
95
|
+
when ',' # cma
|
96
|
+
conditions = [ state.allowed?(:cma),
|
97
|
+
state.inside?(:angle) || state.inside?(:paren) ]
|
98
|
+
raise Taipo::SyntaxError, msg unless conditions.all?
|
99
|
+
state.prohibit_all except: [ :spc, :oth ]
|
100
|
+
state.increment :const if state.inside?(:paren)
|
101
|
+
when ' ' # spc
|
102
|
+
conditions = [ state.allowed?(:spc) ]
|
103
|
+
raise Taipo::SyntaxError, msg unless conditions.all?
|
104
|
+
state.prohibit_all except: [ :hsh, :sls, :qut, :oth ]
|
105
|
+
else # oth
|
106
|
+
conditions = [ state.allowed?(:oth) ]
|
107
|
+
raise Taipo::SyntaxError, msg unless conditions.all?
|
108
|
+
state.allow_all except: [ :hsh, :spc ]
|
109
|
+
end
|
110
|
+
i += 1
|
111
|
+
end
|
112
|
+
msg_end = "The string '#{str}' ends with an illegal character."
|
113
|
+
raise Taipo::SyntaxError, msg_end unless state.allowed?(:end)
|
114
|
+
|
115
|
+
missing = state.unbalanced
|
116
|
+
msg_bal = "The string '#{str}' is missing a '#{missing.first}'."
|
117
|
+
raise Taipo::SyntaxError, msg_bal unless missing.size == 0
|
118
|
+
end
|
119
|
+
|
120
|
+
# Check +str+ is a valid regular expression
|
121
|
+
#
|
122
|
+
# @param str [String] a regular expression delimited by '/'
|
123
|
+
# @param start [Integer] the index within the type definition where this
|
124
|
+
# regex begins
|
125
|
+
#
|
126
|
+
# @return [Integer] the index within the type definition where this regex
|
127
|
+
# ends
|
128
|
+
#
|
129
|
+
# @raise [Taipo::SyntaxError] if +str+ is not a valid regular expression
|
130
|
+
#
|
131
|
+
# @since 1.0.0
|
132
|
+
# @api private
|
133
|
+
def self.validate_regex(str, start: 0)
|
134
|
+
status_array = [ :bsl, :sls, :opt, :oth ]
|
135
|
+
counter_array = [ [ :backslash ], { backslash: '/' } ]
|
136
|
+
|
137
|
+
state = SyntaxState.new(status_array, counter_array)
|
138
|
+
state.prohibit_all except: [ :bsl, :oth ]
|
139
|
+
finish = start
|
140
|
+
|
141
|
+
str[start, str.length-start].each_char.with_index(start) do |c, i|
|
142
|
+
if state.active?(:backslash) # The preceding character was a backslash.
|
143
|
+
state.decrement(:backslash)
|
144
|
+
next # Any character after a backslash is allowed.
|
145
|
+
end
|
146
|
+
|
147
|
+
msg = "The string '#{str}' has an error here: #{str[0, i+1]}"
|
148
|
+
|
149
|
+
case c
|
150
|
+
when 'i', 'o', 'x', 'm', 'u', 'e', 's', 'n'
|
151
|
+
next # We're either in the regex or in the options that follow.
|
152
|
+
when '/'
|
153
|
+
raise Taipo::SyntaxError, msg unless state.allowed?(:sls)
|
154
|
+
state.prohibit_all except: [ :opt ]
|
155
|
+
when '\\'
|
156
|
+
raise Taipo::SyntaxError, msg unless state.allowed?(:bsl)
|
157
|
+
state.increment(:backslash)
|
158
|
+
when ',', ')'
|
159
|
+
next if state.allowed?(:oth)
|
160
|
+
finish = i
|
161
|
+
break # The string has ended.
|
162
|
+
else
|
163
|
+
raise Taipo::SyntaxError, msg unless state.allowed?(:oth)
|
164
|
+
state.allow_all
|
165
|
+
end
|
166
|
+
end
|
167
|
+
|
168
|
+
msg = "The string '#{str}' is missing a '/'."
|
169
|
+
raise Taipo::SyntaxError, msg if finish == start
|
170
|
+
|
171
|
+
finish - 1
|
172
|
+
end
|
173
|
+
|
174
|
+
# Check +str+ is a valid string
|
175
|
+
#
|
176
|
+
# @param str [String] a string delimited by '"'
|
177
|
+
# @param start [Integer] the index within the type definition where this
|
178
|
+
# string begins
|
179
|
+
#
|
180
|
+
# @return [Integer] the index within the type definition where this
|
181
|
+
# string ends
|
182
|
+
# @raise [Taipo::SyntaxError] if +str+ is not a valid string
|
183
|
+
#
|
184
|
+
# @since 1.0.0
|
185
|
+
# @api private
|
186
|
+
def self.validate_string(str, start: 0)
|
187
|
+
status_array = [ :bsl, :qut, :oth ]
|
188
|
+
counter_array = [ [ :backslash ], { backslash: '/' } ]
|
189
|
+
|
190
|
+
state = SyntaxState.new(status_array, counter_array)
|
191
|
+
state.prohibit_all except: [ :bsl, :oth ]
|
192
|
+
finish = start
|
193
|
+
|
194
|
+
str[start, str.length-start].each_char.with_index(start) do |c, i|
|
195
|
+
if state.active?(:backslash) # The preceding character was a backslash.
|
196
|
+
state.decrement :backslash
|
197
|
+
next # Any character after a backslash is allowed.
|
198
|
+
end
|
199
|
+
|
200
|
+
msg = "The string '#{str}' has an error here: #{str[0, i+1]}"
|
201
|
+
|
202
|
+
case c
|
203
|
+
when '"'
|
204
|
+
raise Taipo::SyntaxError, msg unless state.allowed?(:qut)
|
205
|
+
state.prohibit_all
|
206
|
+
when '\\'
|
207
|
+
raise Taipo::SyntaxError, msg unless state.allowed?(:bsl)
|
208
|
+
state.increment :backslash
|
209
|
+
when ',', ')'
|
210
|
+
next if state.allowed?(:oth)
|
211
|
+
finish = i
|
212
|
+
break # The string has ended.
|
213
|
+
else
|
214
|
+
raise Taipo::SyntaxError, msg unless state.allowed?(:oth)
|
215
|
+
state.allow_all
|
216
|
+
end
|
217
|
+
end
|
218
|
+
|
219
|
+
msg = "The string '#{str}' is missing a '\"'."
|
220
|
+
raise Taipo::SyntaxError, msg if finish == start
|
221
|
+
|
222
|
+
finish - 1
|
223
|
+
end
|
224
|
+
end
|
225
|
+
end
|
226
|
+
end
|