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