taipo 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
@@ -0,0 +1,12 @@
1
+ /.bundle
2
+ /.ruby-version
3
+ /.yardoc
4
+ /Gemfile.lock
5
+ /_yardoc/
6
+ /coverage/
7
+ /doc/
8
+ /pkg/
9
+ /spec/reports/
10
+ /vendor/
11
+ /tmp/
12
+ *.swp
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in taipo.gemspec
4
+ gemspec
@@ -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/>
@@ -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.
@@ -0,0 +1,9 @@
1
+ require "bundler/gem_tasks"
2
+ require "rake/testtask"
3
+
4
+ Rake::TestTask.new do |task|
5
+ task.libs << %w(test)
6
+ task.pattern = "test/test_*.rb"
7
+ end
8
+
9
+ task :default => :test
@@ -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
@@ -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
@@ -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
@@ -0,0 +1,6 @@
1
+ require 'taipo/exceptions/name_error'
2
+ require 'taipo/exceptions/syntax_error'
3
+ require 'taipo/exceptions/type_error'
4
+
5
+ module Taipo
6
+ end
@@ -0,0 +1,8 @@
1
+ module Taipo
2
+
3
+ # An exception raised when an argument with a given name does not exist
4
+ #
5
+ # @since 1.0.0
6
+ class NameError < ::NameError
7
+ end
8
+ end
@@ -0,0 +1,9 @@
1
+ module Taipo
2
+
3
+ # An exception raised when there is an error in the syntax of a type
4
+ # definition
5
+ #
6
+ # @since 1.0.0
7
+ class SyntaxError < ::StandardError
8
+ end
9
+ end
@@ -0,0 +1,9 @@
1
+ module Taipo
2
+
3
+ # An exception raised when the type of a given argument does not match the
4
+ # given type definition
5
+ #
6
+ # @since 1.0.0
7
+ class TypeError < TypeError
8
+ end
9
+ end
@@ -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