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
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 80a49dd630a757254866642ae69f3fd46f8804ca
|
4
|
+
data.tar.gz: ac833851465c9174ffecdb1cd0c42e2f149bed75
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: a89531bc4c8f93e77badf0b3c6ca411c5eaf6bc67dde11816f74de816d6eb394a031c27b3de0d3537ca3567afa4f8215d4ef38d05a1a84654defa38b347b0f7e
|
7
|
+
data.tar.gz: a18955f94b5744fc33c7739dac871fac1b94424d22f11119f16ee895dd3d7191206e5276eb9cf80df1064fb0b572c8f1126092c7fbcdf329a64198e4ad43c0a3
|
data/.gitignore
ADDED
data/Gemfile
ADDED
data/LICENSE.md
ADDED
@@ -0,0 +1,9 @@
|
|
1
|
+
This is free and unencumbered software released into the public domain.
|
2
|
+
|
3
|
+
Anyone is free to copy, modify, publish, use, compile, sell, or distribute this software, either in source code form or as a compiled binary, for any purpose, commercial or non-commerciaL, and by any means.
|
4
|
+
|
5
|
+
In jurisdictions that recognize copyright laws, the author or authors of this software dedicate any and all copyright interest in the software to the public domain. We make this dedication for the benefit of the public at large and to the detriment of our heirs and successors. We intend this dedication to be an overt act of relinquishment in perpetuity of all present and future rights to this software under copyright law.
|
6
|
+
|
7
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
8
|
+
|
9
|
+
For more information, please refer to <http://unlicense.org/>
|
data/README.md
ADDED
@@ -0,0 +1,110 @@
|
|
1
|
+
# Taipo
|
2
|
+
|
3
|
+
Taipo is a simple library for checking the types of variables.
|
4
|
+
|
5
|
+
## Overview
|
6
|
+
|
7
|
+
When we deal with variables in our code, we have certain expectations about what those variables can and can’t do.
|
8
|
+
|
9
|
+
Taipo provides a simple way to make those expectations explicit. If an expectation isn’t met, Taipo can either raise an exception or return the problematic variables for us to handle.
|
10
|
+
|
11
|
+
## Installation
|
12
|
+
|
13
|
+
Run `gem install taipo` or add `gem 'taipo'` to your `Gemfile`.
|
14
|
+
|
15
|
+
## Usage
|
16
|
+
|
17
|
+
Taipo provides two methods that we can mix into our classes: `#check` and `#review`.
|
18
|
+
|
19
|
+
### #check
|
20
|
+
|
21
|
+
```
|
22
|
+
require ‘taipo’
|
23
|
+
|
24
|
+
class Foo
|
25
|
+
include Taipo::Check
|
26
|
+
|
27
|
+
def double(val)
|
28
|
+
check types, val: ‘Integer’
|
29
|
+
val * 2
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
foo = Foo.new
|
34
|
+
foo.double 5 #=> 10
|
35
|
+
foo.double ‘Oops’ #=> Taipo::TypeError
|
36
|
+
```
|
37
|
+
|
38
|
+
The method `#check` will raise an exception as soon as one of its arguments doesn’t match its type definition.
|
39
|
+
|
40
|
+
### #review
|
41
|
+
|
42
|
+
```
|
43
|
+
require ‘taipo’
|
44
|
+
|
45
|
+
class Foo
|
46
|
+
include Taipo::Check
|
47
|
+
|
48
|
+
def add(x, y)
|
49
|
+
errors = review types, x: ‘Integer’, y: ‘Integer’
|
50
|
+
if errors.empty?
|
51
|
+
x + y
|
52
|
+
else
|
53
|
+
‘Oops’
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
foo = Foo.new
|
59
|
+
foo.add 4, 5 #=> 9
|
60
|
+
foo.add 2, ‘OK’ #=> ‘Oops’
|
61
|
+
```
|
62
|
+
|
63
|
+
The method `#review` will put the invalid arguments into an array and return that to the user. If there are no errors, the array is empty.
|
64
|
+
|
65
|
+
## Syntax
|
66
|
+
|
67
|
+
Type definitions are passed as Strings with a straightforward syntax.
|
68
|
+
|
69
|
+
The simplest case is to write the name of a class. For example, `’String’`. Taipo supports more complex type definitions should you need them. Here are some more examples.
|
70
|
+
|
71
|
+
```
|
72
|
+
'String'
|
73
|
+
'Array<String>'
|
74
|
+
'Hash<Symbol,String>'
|
75
|
+
'String|Float'
|
76
|
+
'Boolean|Array<String|Hash<Symbol,Point>|Array<String>>'
|
77
|
+
'Array(len: 5)'
|
78
|
+
'String(format: /woo/)'
|
79
|
+
'String(#size)'
|
80
|
+
'String(#size, #to_s)'
|
81
|
+
'Integer(min: 1, max: 10)'
|
82
|
+
'String(val: "This is a test.")'
|
83
|
+
'#to_s'
|
84
|
+
'#to_s|#to_i'
|
85
|
+
```
|
86
|
+
|
87
|
+
## Requirements
|
88
|
+
|
89
|
+
Taipo has been tested with Ruby version 2.4.2.
|
90
|
+
|
91
|
+
## Bugs
|
92
|
+
|
93
|
+
Found a bug? I’d love to know about it. The best way is to report them on GitHub.
|
94
|
+
|
95
|
+
## Contributions
|
96
|
+
|
97
|
+
If you’re interested in contributing to Taipo, feel free to fork and submit a pull request.
|
98
|
+
|
99
|
+
## Colophon
|
100
|
+
|
101
|
+
Taipo began as an exercise to improve my programming skills. If you want something more comprehensive, consider some of the other options, such as [Contracts][1], [Rtype][2], [Rubype][3] or [Sig][4].
|
102
|
+
|
103
|
+
[1]: https://github.com/egonSchiele/contracts.ruby
|
104
|
+
[2]: https://github.com/sputnikgugja/rtype
|
105
|
+
[3]: https://github.com/gogotanaka/Rubype
|
106
|
+
[4]: https://github.com/janlelis/sig
|
107
|
+
|
108
|
+
# Licence
|
109
|
+
|
110
|
+
Taipo is released into the public domain. See LICENSE.md for more details.
|
data/Rakefile
ADDED
data/lib/taipo.rb
ADDED
@@ -0,0 +1,39 @@
|
|
1
|
+
require 'taipo/version'
|
2
|
+
require 'taipo/check'
|
3
|
+
require 'taipo/parser'
|
4
|
+
|
5
|
+
# A library for checking the types of objects
|
6
|
+
#
|
7
|
+
# @since 1.0.0
|
8
|
+
# @see https://github.com/pyrmont/shakushi
|
9
|
+
module Taipo
|
10
|
+
|
11
|
+
# Check if a string is the name of an instance method
|
12
|
+
#
|
13
|
+
# @note All this does is check whether the given string begins with a hash
|
14
|
+
# symbol.
|
15
|
+
#
|
16
|
+
# @param str [String] the method name to check
|
17
|
+
#
|
18
|
+
# @return [Boolean] the result
|
19
|
+
#
|
20
|
+
# @since 1.0.0
|
21
|
+
# @api private
|
22
|
+
def self.instance_method?(str)
|
23
|
+
str[0] == '#'
|
24
|
+
end
|
25
|
+
|
26
|
+
# Convert an array into a type definition for the types of the elements in the array
|
27
|
+
#
|
28
|
+
# @param arg [Array] the array of elements
|
29
|
+
#
|
30
|
+
# @return [String] a type definition of the types in the array
|
31
|
+
#
|
32
|
+
# @since 1.0.0
|
33
|
+
# @api private
|
34
|
+
def self.child_types_string(arg)
|
35
|
+
child_types = Hash.new
|
36
|
+
arg.each { |a| child_types[a.class.name] = true }
|
37
|
+
'<' + child_types.keys.join('|') + '>'
|
38
|
+
end
|
39
|
+
end
|
data/lib/taipo/cache.rb
ADDED
@@ -0,0 +1,49 @@
|
|
1
|
+
module Taipo
|
2
|
+
# A cache of Taipo::TypeElement created from parsed type definitions
|
3
|
+
#
|
4
|
+
# @since 1.0.0
|
5
|
+
# @api private
|
6
|
+
module Cache
|
7
|
+
|
8
|
+
# The hash that acts as the cache
|
9
|
+
#
|
10
|
+
# @since 1.0.0
|
11
|
+
# @api private
|
12
|
+
@@Cache = {}
|
13
|
+
|
14
|
+
# Retrieve the Taipo::TypeElement object described by the type definition from the cache
|
15
|
+
#
|
16
|
+
# @param k [String] the type definition
|
17
|
+
#
|
18
|
+
# @return [Taipo::TypeElement] if the type definition has been saved
|
19
|
+
# @return [NilClass] if the type definition has not been saved
|
20
|
+
#
|
21
|
+
# @since 1.0.0
|
22
|
+
# @api private
|
23
|
+
def self.[](k)
|
24
|
+
@@Cache[k]
|
25
|
+
end
|
26
|
+
|
27
|
+
# Save the Taipo::TypeElement object described by the type definition in the cache
|
28
|
+
#
|
29
|
+
# @param k [String] the type definition
|
30
|
+
# @param v [Taipo::TypeElement] the object to be saved
|
31
|
+
#
|
32
|
+
# @return [Taipo::TypeElement] the object to be saved
|
33
|
+
#
|
34
|
+
# @since 1.0.0
|
35
|
+
# @api private
|
36
|
+
def self.[]=(k,v)
|
37
|
+
@@Cache[k] = v
|
38
|
+
end
|
39
|
+
|
40
|
+
# Reset the cache
|
41
|
+
#
|
42
|
+
# @since 1.0.0
|
43
|
+
# @api private
|
44
|
+
def self.reset()
|
45
|
+
@@Cache = {}
|
46
|
+
return nil
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
data/lib/taipo/check.rb
ADDED
@@ -0,0 +1,117 @@
|
|
1
|
+
require 'taipo/cache'
|
2
|
+
require 'taipo/exceptions'
|
3
|
+
require 'taipo/parser'
|
4
|
+
require 'taipo/type_element'
|
5
|
+
|
6
|
+
module Taipo
|
7
|
+
|
8
|
+
# A dedicated namespace for methods meant to be included by the user
|
9
|
+
#
|
10
|
+
# @since 1.0.0
|
11
|
+
module Check
|
12
|
+
|
13
|
+
# Syntactic sugar to allow a user to write +check types,...+ and +review
|
14
|
+
# types,...+
|
15
|
+
#
|
16
|
+
# @since 1.0.0
|
17
|
+
alias types binding
|
18
|
+
|
19
|
+
# Check whether the given arguments match the given type definition in the
|
20
|
+
# given context
|
21
|
+
#
|
22
|
+
# @param context [Binding] the context in which the arguments to be checked
|
23
|
+
# are defined
|
24
|
+
# @param collect_invalids [Boolean] whether to raise an exception for, or
|
25
|
+
# collect, an argument that doesn't match its type definition
|
26
|
+
# @param checks [Hash] the arguments to be checked written as +Symbol:
|
27
|
+
# String+ pairs with the Symbol being the name of the argument and the
|
28
|
+
# String being its type definition
|
29
|
+
#
|
30
|
+
# @return [Array] the arguments which don't match (ie. an empty array if
|
31
|
+
# all arguments match)
|
32
|
+
#
|
33
|
+
# @raise [::TypeError] if the context is not a Binding
|
34
|
+
# @raise [Taipo::SyntaxError] if the type definitions in +checks+ are
|
35
|
+
# invalid
|
36
|
+
# @raise [Taipo::TypeError] if the arguments in +checks+ don't match the
|
37
|
+
# given type definition
|
38
|
+
#
|
39
|
+
# @since 1.0.0
|
40
|
+
#
|
41
|
+
# @example
|
42
|
+
# require 'taipo/taipo'
|
43
|
+
#
|
44
|
+
# class A
|
45
|
+
# include Taipo::Check
|
46
|
+
#
|
47
|
+
# def foo(str)
|
48
|
+
# check types, str: 'String'
|
49
|
+
# puts str
|
50
|
+
# end
|
51
|
+
#
|
52
|
+
# def bar(str)
|
53
|
+
# check types, str: 'Integer'
|
54
|
+
# puts str
|
55
|
+
# end
|
56
|
+
# end
|
57
|
+
#
|
58
|
+
# a = A.new()
|
59
|
+
# a.foo('Hello world!') #=> "Hello world!"
|
60
|
+
# a.bar('Goodbye world!') #=> raise Taipo::TypeError
|
61
|
+
def check(context, collect_invalids = false, **checks)
|
62
|
+
msg = "The first argument to this method must be of type Binding."
|
63
|
+
raise TypeError, msg unless context.is_a? Binding
|
64
|
+
|
65
|
+
checks.reduce(Array.new) do |memo,(k,v)|
|
66
|
+
arg = if k[0] == '@' && self.instance_variable_defined?(k)
|
67
|
+
self.instance_variable_get k
|
68
|
+
elsif k[0] != '@' && context.local_variable_defined?(k)
|
69
|
+
context.local_variable_get k
|
70
|
+
else
|
71
|
+
msg = "Argument '#{k}' is not defined."
|
72
|
+
raise Taipo::NameError, msg
|
73
|
+
end
|
74
|
+
|
75
|
+
types = if hit = Taipo::Cache[v]
|
76
|
+
hit
|
77
|
+
else
|
78
|
+
Taipo::Cache[v] = Taipo::Parser.parse v
|
79
|
+
end
|
80
|
+
|
81
|
+
is_match = types.any? { |t| t.match? arg }
|
82
|
+
|
83
|
+
unless collect_invalids || is_match
|
84
|
+
if Taipo::instance_method? v
|
85
|
+
msg = "Object '#{k}' does not respond to #{v}."
|
86
|
+
elsif arg.is_a? Enumerable
|
87
|
+
type_string = arg.class.name + Taipo.child_types_string(arg)
|
88
|
+
msg = "Object '#{k}' is #{type_string} but expected #{v}."
|
89
|
+
else
|
90
|
+
msg = "Object '#{k}' is #{arg.class.name} but expected #{v}."
|
91
|
+
end
|
92
|
+
raise Taipo::TypeError, msg
|
93
|
+
end
|
94
|
+
|
95
|
+
(is_match) ? memo : memo.push(k)
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
# Review whether the given arguments match the given type definition in the
|
100
|
+
# given context
|
101
|
+
#
|
102
|
+
# This is a convenience method for calling {#check} with +collect_invalids+
|
103
|
+
# set to true.
|
104
|
+
#
|
105
|
+
# @param context (see #check)
|
106
|
+
# @param checks (see #check)
|
107
|
+
#
|
108
|
+
# @return (see #check)
|
109
|
+
#
|
110
|
+
# @raise (see #check)
|
111
|
+
#
|
112
|
+
# @since 1.0.0
|
113
|
+
def review(context, **checks)
|
114
|
+
self.check(context, true, checks)
|
115
|
+
end
|
116
|
+
end
|
117
|
+
end
|
data/lib/taipo/parser.rb
ADDED
@@ -0,0 +1,171 @@
|
|
1
|
+
require 'taipo/exceptions'
|
2
|
+
require 'taipo/parser/validater'
|
3
|
+
require 'taipo/type_element'
|
4
|
+
|
5
|
+
module Taipo
|
6
|
+
|
7
|
+
# A parser of Taipo type definitions
|
8
|
+
# @since 1.0.0
|
9
|
+
module Parser
|
10
|
+
|
11
|
+
# Return an array of Taipo::TypeElements based on +str+
|
12
|
+
#
|
13
|
+
# @param str [String] a type definition
|
14
|
+
#
|
15
|
+
# @return [Array<Taipo:TypeElement>] the result
|
16
|
+
#
|
17
|
+
# @raise [::TypeError] if +str+ is not a String
|
18
|
+
# @raise [Taipo::SyntaxError] if +str+ is not a valid type definition
|
19
|
+
#
|
20
|
+
# @since 1.0.0
|
21
|
+
def self.parse(str)
|
22
|
+
Taipo::Parser::Validater.validate str
|
23
|
+
|
24
|
+
content = ''
|
25
|
+
is_fallthrough = false
|
26
|
+
fallthroughs = [ '/', '"' ]
|
27
|
+
closing_symbol = ''
|
28
|
+
stack = Array.new
|
29
|
+
elements = Array.new
|
30
|
+
stack.push elements
|
31
|
+
|
32
|
+
str.each_char do |c|
|
33
|
+
c = '+' + c if is_fallthrough
|
34
|
+
|
35
|
+
case c
|
36
|
+
when '|'
|
37
|
+
next if content.empty? # Previous character must have been '>' or ')'.
|
38
|
+
el = Taipo::TypeElement.new name: content
|
39
|
+
content = ''
|
40
|
+
elements = stack.pop
|
41
|
+
elements.push el
|
42
|
+
stack.push elements
|
43
|
+
when '<'
|
44
|
+
el = Taipo::TypeElement.new name: content
|
45
|
+
content = ''
|
46
|
+
stack.push el
|
47
|
+
child_type = Taipo::TypeElement::ChildType.new
|
48
|
+
stack.push child_type
|
49
|
+
first_component = Array.new
|
50
|
+
stack.push first_component
|
51
|
+
when '>'
|
52
|
+
if content.empty? # Previous character must have been '>' or ')'.
|
53
|
+
last_component = stack.pop
|
54
|
+
else
|
55
|
+
el = Taipo::TypeElement.new name: content.strip
|
56
|
+
content = ''
|
57
|
+
last_component = stack.pop
|
58
|
+
last_component.push el
|
59
|
+
end
|
60
|
+
child_type = stack.pop
|
61
|
+
child_type.push last_component
|
62
|
+
parent_el = stack.pop
|
63
|
+
parent_el.child_type = child_type
|
64
|
+
elements = stack.pop
|
65
|
+
elements.push parent_el
|
66
|
+
stack.push elements
|
67
|
+
when '('
|
68
|
+
if content.empty? # Previous character must have been '>'.
|
69
|
+
elements = stack.pop
|
70
|
+
el = elements.pop
|
71
|
+
stack.push elements
|
72
|
+
else
|
73
|
+
el = Taipo::TypeElement.new name: content
|
74
|
+
content = ''
|
75
|
+
end
|
76
|
+
stack.push el
|
77
|
+
cst_collection = Array.new
|
78
|
+
stack.push cst_collection
|
79
|
+
when '#'
|
80
|
+
if bare_method_constraint? stack
|
81
|
+
content = content + '#'
|
82
|
+
else
|
83
|
+
cst = Taipo::TypeElement::Constraint.new
|
84
|
+
content = ''
|
85
|
+
cst_collection = stack.pop
|
86
|
+
cst_collection.push cst
|
87
|
+
stack.push cst_collection
|
88
|
+
end
|
89
|
+
when ':'
|
90
|
+
cst = Taipo::TypeElement::Constraint.new name: content.strip
|
91
|
+
content = ''
|
92
|
+
cst_collection = stack.pop
|
93
|
+
cst_collection.push cst
|
94
|
+
stack.push cst_collection
|
95
|
+
when ',' # We could be inside a collection or a set of constraints
|
96
|
+
if inside_collection? stack
|
97
|
+
previous_component = stack.pop
|
98
|
+
el = Taipo::TypeElement.new name: content.strip
|
99
|
+
content = ''
|
100
|
+
previous_component.push el
|
101
|
+
child_type = stack.pop
|
102
|
+
child_type.push previous_component
|
103
|
+
stack.push child_type
|
104
|
+
next_component = Array.new
|
105
|
+
stack.push next_component
|
106
|
+
else
|
107
|
+
cst_collection = stack.pop
|
108
|
+
cst = cst_collection.pop
|
109
|
+
cst.value = content.strip
|
110
|
+
content = ''
|
111
|
+
cst_collection.push cst
|
112
|
+
stack.push cst_collection
|
113
|
+
end
|
114
|
+
when ')'
|
115
|
+
cst_collection = stack.pop
|
116
|
+
cst = cst_collection.pop
|
117
|
+
cst.value = content.strip
|
118
|
+
content = ''
|
119
|
+
cst_collection.push cst
|
120
|
+
el = stack.pop
|
121
|
+
el.constraints = cst_collection
|
122
|
+
elements = stack.pop
|
123
|
+
elements.push el
|
124
|
+
stack.push elements
|
125
|
+
else
|
126
|
+
if is_fallthrough
|
127
|
+
c = c[1]
|
128
|
+
is_fallthrough = false if c == closing_symbol
|
129
|
+
elsif fallthroughs.any? { |f| f == c }
|
130
|
+
is_fallthrough = true
|
131
|
+
closing_symbol = c
|
132
|
+
end
|
133
|
+
content = content + c
|
134
|
+
end
|
135
|
+
end
|
136
|
+
|
137
|
+
unless content.empty?
|
138
|
+
el = Taipo::TypeElement.new name: content
|
139
|
+
elements = stack.pop
|
140
|
+
elements.push el
|
141
|
+
stack.push elements
|
142
|
+
end
|
143
|
+
|
144
|
+
stack.pop
|
145
|
+
end
|
146
|
+
|
147
|
+
# Check if the parser is inside a collection
|
148
|
+
#
|
149
|
+
# @param stack [Array] the stack of parsed elements
|
150
|
+
#
|
151
|
+
# @return [Boolean] the result
|
152
|
+
#
|
153
|
+
# @since 1.0.0
|
154
|
+
# @api private
|
155
|
+
def self.inside_collection?(stack)
|
156
|
+
stack[-2]&.class == Taipo::TypeElement::ChildType
|
157
|
+
end
|
158
|
+
|
159
|
+
# Check if this constraint is only a method
|
160
|
+
#
|
161
|
+
# @param stack [Array] the stack of parsed elements
|
162
|
+
#
|
163
|
+
# @return [Boolean] the result
|
164
|
+
#
|
165
|
+
# @since 1.0.0
|
166
|
+
# @api private
|
167
|
+
def self.bare_method_constraint?(stack)
|
168
|
+
stack[-2]&.class != Taipo::TypeElement
|
169
|
+
end
|
170
|
+
end
|
171
|
+
end
|