taipo 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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