Module: Taipo::Parser::Validater

Defined in:
lib/taipo/parser/validater.rb

Overview

A validater of Taipo type definitions

Taipo’s type definition syntax has four components: (1) names; (2) collections; (3) constraints; and (4) sums.

Names

'String', 'Numeric', 'Foo::Bar'

A name should be the name of a class or a module. A name can include a namespace.

The validater does not check whether the name represents a valid name in the current context nor does it check whether the name complies with Ruby’s requirements for names.

One special case is where the name is left blank. The validater will accept this as valid. Taipo::Parser will implictly add the name ‘Object’ when parsing the type definition. This allows a clean syntax for duck types (discussed in further detail below).

Duck Types

'#to_s', '(#foo, #bar)'

As noted above, duck types can be specified by using a blank name. Duck types are really constraints (discussed in further detail below) on the class Object. While normally constraints need to be enclosed in parentheses, if there is a blank name and only one method constraint, the parentheses can be omitted. For defining duck types that respond to multiple methods, the parentheses are required.

Optional Types

'String?', 'Array<Integer?>', 'Symbol?|String?'

It is possible to specify an ‘optional’ type by appending a question mark to the name of the type. This shorthand functions similarly to defining a sum type with NilClass (the implementation of how optional types are checked is slightly different, however; see TypeElement#match?). It is not possible to define an optional duck type. For that, either the implicit Object class should be specified (and then made optional), or a sum type should be used.

Collections

'Array<Integer>', 'Hash<Symbol, String>', 'Array<Array<Float>>'

A collection should be the type definiton for elements returned by Enumerator#each (the child type) called on the collecting object (the parent type).

A collection is demarcated by the angle brackets < and >. These come immediately after the name of the parent (ie. without a space). The type definition for the child comes immediately after the opening angle bracket.

If Enumerator#each returns multiple values (eg. such as with Hash), the type definition for each value is delimited by a comma. It is optional whether a space follows the comma.

The type definition for a child element can contain all the components of a type definition (ie. name, collection, constraint, sum) allowing for collections that contain collections and so on.

Constraints

'Array(len: 5)', 'Integer(min: 0, max: 10)', 'String(format: /a{3}/)',
'String(val: "Hello world!")', 'Foo(#bar)'

A constraint should be a list of identifiers and values.

A constraint is demarcated by parentheses (ie. ( and )). These come immediately after the name or collection (ie. without a space). The first identifier comes immediately after the opening parenthesis.

An identifier and a value are separated by a colon (and an optional space). Multiple identifier-value pairs are delimited by a comma. It is optional whether a space follows the comma.

The permitted identifiers and their values are as follows:

  • format: takes a regular expression demarcated by /

  • len: takes an integer

  • max: takes an integer

  • min: takes an integer

  • val: takes a number or a string demarcated by

The validater does not check whether the identifiers and values are acceptable, merely that they conform to the grammar. parse will raise an exception when it parses the definition if the values are not acceptable for the relevant identifier. Similarly, while the repetition of an identifier is technically invalid, the exception will not be raised until parse is called.

One special case is where the identifier begins with a #. For this identifier, no value is provided and the constraint instead results in Check#check and Check#review checking whether the given object returns true for Object#respond_to? with the identifier as the symbol.

Sums

'String|Float',
'Boolean|Array<String|Hash<Symbol,Point>|Array<String>>',
'Integer(max: 100)|Float(max: 100)'

A sum is a combination of two or more type definitions.

The sum comprises two or more type definitions, each separated by a bar (ie. |).

Enums

':foo|:bar', ':one|:two|:three'

It’s possible to approximate the enum idiom available in many languages by creating a sum type consisting of Symbols. As a convenience, Taipo parses these values as constraints on the Object class. In other words, the :foo|:bar is really Object(val: :foo)|Object(val: :bar).

Since:

  • 1.0.0

Class Method Summary collapse

Class Method Details

.validate(str) ⇒ NilClass

Check str is a valid type definition

Parameters:

  • str (String)

    a type definition

Returns:

  • (NilClass)

Raises:

  • (::TypeError)

    if str is not a String

  • (Taipo::SyntaxError)

    if str is not a valid type definition

Since:

  • 1.0.0



140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
# File 'lib/taipo/parser/validater.rb', line 140

def self.validate(str)
  msg = "The argument to this method must be of type String."
  raise ::TypeError, msg unless str.is_a? String
  msg = "The string to be checked was empty."
  raise Taipo::SyntaxError, msg if str.empty?

  status_array = [ :bar, :lab, :rab, :lpr, :hsh, :cln, :cma, :spc_bar,
                   :spc_rab, :spc_rpr, :spc_cma, :spc_oth, :mth, :sym,
                   :nme, :end ]
  counter_array = [ [ :angle ], { angle: '>' } ]

  state = Taipo::Parser::SyntaxState.new(status_array, counter_array)
  state.prohibit_all except: [ :lpr, :hsh, :cln, :nme ]

  i = 0
  chars = str.chars

  while (i < chars.size)
    msg = "The string '#{str}' has an error here: #{str[0, i+1]}"
    case chars[i]
    when ')', '/', '"'
      raise Taipo::SyntaxError, msg
    when '|' # bar
      conditions = [ state.allowed?(:bar) ]
      raise Taipo::SyntaxError, msg unless conditions.all?
      state.prohibit_all except: [ :lpr, :hsh, :cln, :spc_bar, :nme ]
    when '<' # lab
      conditions = [ state.allowed?(:lab) ]
      raise Taipo::SyntaxError, msg unless conditions.all?
      state.prohibit_all except: [ :lpr, :hsh, :cln, :nme ]
      state.increment :angle
    when '>' # rab
      conditions = [ state.allowed?(:rab), state.inside?(:angle) ]
      raise Taipo::SyntaxError, msg unless conditions.all?
      state.prohibit_all except: [ :bar, :rab, :lpr, :spc_rab, :end ]
      state.decrement :angle
    when '(' # lpr
      conditions = [ state.allowed?(:lpr) ]
      raise Taipo::SyntaxError, msg unless conditions.all?
      i = Taipo::Parser::Validater.validate_constraints(str, start: i+1)
      state.prohibit_all except: [ :bar, :rab, :spc_rpr, :end ]
    when '#' # hsh
      conditions = [ state.allowed?(:hsh) ]
      raise Taipo::SyntaxError, msg unless conditions.all?
      state.prohibit_all except: [ :mth ]
    when ':' # cln
      if chars[i+1] == ':' && chars[i+2] != ':'
        conditions = [ state.allowed?(:nme) ]
        raise Taipo::SyntaxError, msg unless conditions.all?
        state.prohibit_all except: [ :nme ]
        i = i + 1
      else
        conditions = [ state.allowed?(:cln) ]
        raise Taipo::SyntaxError, msg unless conditions.all?
        state.prohibit_all except: [ :sym ]
      end
    when ',' # cma
      conditions = [ state.allowed?(:cma), state.inside?(:angle)]
      raise Taipo::SyntaxError, msg unless conditions.all?
      state.prohibit_all except: [ :hsh, :cln, :spc_cma, :nme ]
    when ' ' # spc
      conditions = [ state.allowed?(:spc_bar), state.allowed?(:spc_cma),
                     state.allowed?(:spc_oth) ]
      raise Taipo::SyntaxError, msg unless conditions.any?
      if state.allowed?(:spc_bar) || state.allowed?(:spc_cma)
        state.prohibit_all except: [ :hsh, :cln, :nme ]
      elsif state.allowed?(:spc_rab) || state.allowed?(:spc_rpr)
        state.prohibit_all except: [ :bar, :hsh, :cln, :nme ]
      elsif state.allowed?(:spc_oth)
        state.prohibit_all except: [ :bar ]
      end
    else # oth
      conditions = [ state.allowed?(:mth), state.allowed?(:sym),
                     state.allowed?(:nme) ]
      raise Taipo::SyntaxError, msg unless conditions.any?
      if state.allowed?(:mth)
        state.prohibit_all except: [ :bar, :rab, :cma, :spc_oth, :mth,
                                     :end ]
      elsif state.allowed?(:sym)
        state.prohibit_all except: [ :bar, :rab, :cma, :spc_oth, :sym,
                                     :end ]
      elsif state.allowed?(:nme)
        state.prohibit_all except: [ :bar, :lab, :rab, :lpr, :cma,
                                     :spc_oth, :nme, :end ]
      end
    end
    i += 1
  end

  msg_end = "The string '#{str}' ends with an illegal character."
  raise Taipo::SyntaxError, msg_end unless state.allowed?(:end)

  missing = state.unbalanced
  msg_bal = "The string '#{str}' is missing a '#{missing.first}'."
  raise Taipo::SyntaxError, msg_bal unless missing.size == 0
end

.validate_constraints(str, start: 0) ⇒ Integer

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Check str is a valid set of constraints

Parameters:

  • str (String)

    the type definition

  • start (Integer) (defaults to: 0)

    the index within the type definition where this set of constraints begins

Returns:

  • (Integer)

    the index within the type definition where this set of set of constraints end

Raises:

Since:

  • 1.4.0



250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
# File 'lib/taipo/parser/validater.rb', line 250

def self.validate_constraints(str, start: 0)
  status_array = [ :rpr, :hsh, :cln, :sls, :qut, :cma, :spc, :oth ]
  counter_array = [ [ :const ], { const: ":' or '#" } ]

  state = SyntaxState.new(status_array, counter_array)
  state.prohibit_all except: [ :hsh, :oth ]
  state.increment(:const)

  i = start
  chars = str.chars

  while (i < chars.size)
    msg = "The string '#{str}' has an error here: #{str[0, i+1]}"
    case chars[i]
    when '|', '<', '>', '('
      raise Taipo::SyntaxError, msg
    when ')' # rpr
      conditions = [ state.allowed?(:rpr) ]
      raise Taipo::SyntaxError, msg unless conditions.all?
      break # The constraints have ended.
    when '#' # hsh
      conditions = [ state.allowed?(:hsh) ]
      raise Taipo::SyntaxError, msg unless conditions.all?
      state.prohibit_all except: [ :oth ]
      state.decrement :const
    when ':' # cln
      conditions = [ state.allowed?(:cln) ]
      raise Taipo::SyntaxError, msg unless conditions.all?
      if state.count(:const) == 0 # This is a symbol.
        state.prohibit_all except: [ :qut, :oth ]
      else
        state.prohibit_all except: [ :cln, :sls, :qut, :spc, :oth ]
        state.decrement :const
      end
    when '/' #sls
      conditions = [ state.allowed?(:sls), state.outside?(:const) ]
      raise Taipo::SyntaxError, msg unless conditions.all?
      i = Taipo::Parser::Validater.validate_regex(str, start: i+1)
      state.prohibit_all except: [ :rpr, :cma ]
    when '"' #qut
      conditions = [ state.allowed?(:qut), state.outside?(:const) ]
      raise Taipo::SyntaxError, msg unless conditions.all?
      i = Taipo::Parser::Validater.validate_string(str, start: i+1)
      state.prohibit_all except: [ :rpr, :cma ]
    when ',' # cma
      conditions = [ state.allowed?(:cma) ]
      raise Taipo::SyntaxError, msg unless conditions.all?
      state.prohibit_all except: [ :spc, :oth ]
      state.increment :const
    when ' ' # spc
      conditions = [ state.allowed?(:spc) ]
      raise Taipo::SyntaxError, msg unless conditions.all?
      state.prohibit_all except: [ :hsh, :cln, :sls, :qut, :oth ]
    else # oth
      conditions = [ state.allowed?(:oth) ]
      raise Taipo::SyntaxError, msg unless conditions.all?
      state.allow_all except: [ :hsh, :spc ]
    end
    i += 1
  end

  msg = "The string '#{str}' is missing a ')'."
  raise Taipo::SyntaxError, msg if i == chars.size

  missing = state.unbalanced
  msg_bal = "The string '#{str}' is missing a '#{missing.first}'."
  raise Taipo::SyntaxError, msg_bal unless missing.size == 0

  i
end

.validate_regex(str, start: 0) ⇒ Integer

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Check str is a valid regular expression

Parameters:

  • str (String)

    the type definition

  • start (Integer) (defaults to: 0)

    the index within the type definition where this regex begins

Returns:

  • (Integer)

    the index within the type definition where this regex ends

Raises:

Since:

  • 1.0.0



334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
# File 'lib/taipo/parser/validater.rb', line 334

def self.validate_regex(str, start: 0)
  status_array = [ :bsl, :sls, :opt, :oth ]
  counter_array = [ [ :backslash ], { backslash: '/' } ]

  state = SyntaxState.new(status_array, counter_array)
  state.prohibit_all except: [ :bsl, :oth ]
  finish = start

  str[start, str.length-start].each_char.with_index(start) do |c, i|
    if state.active?(:backslash) # The preceding character was a backslash.
      state.decrement(:backslash)
      next # Any character after a backslash is allowed.
    end

    msg = "The string '#{str}' has an error here: #{str[0, i+1]}"

    case c
    when 'i', 'o', 'x', 'm', 'u', 'e', 's', 'n'
      next # We're either in the regex or in the options that follow.
    when '/'
      raise Taipo::SyntaxError, msg unless state.allowed?(:sls)
      state.prohibit_all except: [ :opt ]
    when '\\'
      raise Taipo::SyntaxError, msg unless state.allowed?(:bsl)
      state.increment(:backslash)
    when ',', ')'
      next if state.allowed?(:oth)
      finish = i
      break # The string has ended.
    else
      raise Taipo::SyntaxError, msg unless state.allowed?(:oth)
      state.allow_all
    end
  end

  msg = "The string '#{str}' is missing a '/'."
  raise Taipo::SyntaxError, msg if finish == start

  finish - 1
end

.validate_string(str, start: 0) ⇒ Integer

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Check str is a valid string

Parameters:

  • str (String)

    the type definition

  • start (Integer) (defaults to: 0)

    the index within the type definition where this string begins

Returns:

  • (Integer)

    the index within the type definition where this string ends

Raises:

Since:

  • 1.0.0



388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
# File 'lib/taipo/parser/validater.rb', line 388

def self.validate_string(str, start: 0)
  status_array = [ :bsl, :qut, :oth ]
  counter_array = [ [ :backslash ], { backslash: '/' } ]

  state = SyntaxState.new(status_array, counter_array)
  state.prohibit_all except: [ :bsl, :oth ]
  finish = start

  str[start, str.length-start].each_char.with_index(start) do |c, i|
    if state.active?(:backslash) # The preceding character was a backslash.
      state.decrement :backslash
      next # Any character after a backslash is allowed.
    end

    msg = "The string '#{str}' has an error here: #{str[0, i+1]}"

    case c
    when '"'
      raise Taipo::SyntaxError, msg unless state.allowed?(:qut)
      state.prohibit_all
    when '\\'
      raise Taipo::SyntaxError, msg unless state.allowed?(:bsl)
      state.increment :backslash
    when ',', ')'
      next if state.allowed?(:oth)
      finish = i
      break # The string has ended.
    else
      raise Taipo::SyntaxError, msg unless state.allowed?(:oth)
      state.allow_all
    end
  end

  msg = "The string '#{str}' is missing a '\"'."
  raise Taipo::SyntaxError, msg if finish == start

  finish - 1
end