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