typerb 0.1.6 → 0.3.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.
- checksums.yaml +4 -4
- data/.gitignore +3 -0
- data/.rubocop.yml +1 -0
- data/.ruby-version +1 -1
- data/.travis.yml +2 -1
- data/Gemfile.lock +50 -19
- data/Guardfile +36 -0
- data/README.md +32 -9
- data/lib/typerb.rb +29 -1
- data/lib/typerb/exceptional.rb +8 -0
- data/lib/typerb/variable_name.rb +2 -2
- data/lib/typerb/version.rb +1 -1
- data/typerb.gemspec +2 -0
- metadata +32 -4
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 0d36da771caf7376c9ade13b380b77ae20cd50ac03172c86e6d54f3c54af5414
|
4
|
+
data.tar.gz: 1bb04d2199a1384049168302010c8a8d0e5114301e26108167d8d54810f4c79a
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 79fa49f81f729c9e760afe1cd4abf5e2d259b1ed48a711ff64732a1411219c2d6c6fd2154dc5386e3b12d4f7cd607d4f612e06908938802ae6890c2cfd0029be
|
7
|
+
data.tar.gz: 5f405c7242a53f27925df6981944cc31f57b7bad60077aa36c9f7c08fd6a6a428998caca93ddf3f124829c5b40254f840bcbdf183d20ce19b41caf7b2438ebfa
|
data/.gitignore
CHANGED
data/.rubocop.yml
CHANGED
data/.ruby-version
CHANGED
@@ -1 +1 @@
|
|
1
|
-
2.6.
|
1
|
+
2.6.6
|
data/.travis.yml
CHANGED
data/Gemfile.lock
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
PATH
|
2
2
|
remote: .
|
3
3
|
specs:
|
4
|
-
typerb (0.
|
4
|
+
typerb (0.3.0)
|
5
5
|
|
6
6
|
GEM
|
7
7
|
remote: https://rubygems.org/
|
@@ -10,48 +10,79 @@ GEM
|
|
10
10
|
awesome_print (1.8.0)
|
11
11
|
coderay (1.1.2)
|
12
12
|
diff-lcs (1.3)
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
13
|
+
ffi (1.11.1)
|
14
|
+
formatador (0.2.5)
|
15
|
+
guard (2.15.0)
|
16
|
+
formatador (>= 0.2.4)
|
17
|
+
listen (>= 2.7, < 4.0)
|
18
|
+
lumberjack (>= 1.0.12, < 2.0)
|
19
|
+
nenv (~> 0.1)
|
20
|
+
notiffany (~> 0.0)
|
21
|
+
pry (>= 0.9.12)
|
22
|
+
shellany (~> 0.0)
|
23
|
+
thor (>= 0.18.1)
|
24
|
+
guard-compat (1.2.1)
|
25
|
+
guard-rspec (4.7.3)
|
26
|
+
guard (~> 2.1)
|
27
|
+
guard-compat (~> 1.1)
|
28
|
+
rspec (>= 2.99.0, < 4.0)
|
29
|
+
jaro_winkler (1.5.3)
|
30
|
+
listen (3.1.5)
|
31
|
+
rb-fsevent (~> 0.9, >= 0.9.4)
|
32
|
+
rb-inotify (~> 0.9, >= 0.9.7)
|
33
|
+
ruby_dep (~> 1.2)
|
34
|
+
lumberjack (1.0.13)
|
35
|
+
method_source (0.9.2)
|
36
|
+
nenv (0.3.0)
|
37
|
+
notiffany (0.1.1)
|
38
|
+
nenv (~> 0.1)
|
39
|
+
shellany (~> 0.0)
|
40
|
+
parallel (1.17.0)
|
41
|
+
parser (2.6.3.0)
|
17
42
|
ast (~> 2.4.0)
|
18
|
-
|
19
|
-
pry (0.12.0)
|
43
|
+
pry (0.12.2)
|
20
44
|
coderay (~> 1.1.0)
|
21
45
|
method_source (~> 0.9.0)
|
22
46
|
rainbow (3.0.0)
|
23
|
-
rake (
|
47
|
+
rake (12.3.2)
|
48
|
+
rb-fsevent (0.10.3)
|
49
|
+
rb-inotify (0.10.0)
|
50
|
+
ffi (~> 1.0)
|
24
51
|
rspec (3.8.0)
|
25
52
|
rspec-core (~> 3.8.0)
|
26
53
|
rspec-expectations (~> 3.8.0)
|
27
54
|
rspec-mocks (~> 3.8.0)
|
28
|
-
rspec-core (3.8.
|
55
|
+
rspec-core (3.8.1)
|
29
56
|
rspec-support (~> 3.8.0)
|
30
|
-
rspec-expectations (3.8.
|
57
|
+
rspec-expectations (3.8.4)
|
31
58
|
diff-lcs (>= 1.2.0, < 2.0)
|
32
59
|
rspec-support (~> 3.8.0)
|
33
|
-
rspec-mocks (3.8.
|
60
|
+
rspec-mocks (3.8.1)
|
34
61
|
diff-lcs (>= 1.2.0, < 2.0)
|
35
62
|
rspec-support (~> 3.8.0)
|
36
|
-
rspec-support (3.8.
|
37
|
-
rubocop (0.
|
63
|
+
rspec-support (3.8.2)
|
64
|
+
rubocop (0.72.0)
|
38
65
|
jaro_winkler (~> 1.5.1)
|
39
66
|
parallel (~> 1.10)
|
40
|
-
parser (>= 2.
|
41
|
-
powerpack (~> 0.1)
|
67
|
+
parser (>= 2.6)
|
42
68
|
rainbow (>= 2.2.2, < 4.0)
|
43
69
|
ruby-progressbar (~> 1.7)
|
44
|
-
unicode-display_width (
|
45
|
-
ruby-progressbar (1.10.
|
70
|
+
unicode-display_width (>= 1.4.0, < 1.7)
|
71
|
+
ruby-progressbar (1.10.1)
|
72
|
+
ruby_dep (1.5.0)
|
73
|
+
shellany (0.0.1)
|
46
74
|
super_awesome_print (0.2.5)
|
47
75
|
awesome_print
|
48
|
-
|
76
|
+
thor (0.20.3)
|
77
|
+
unicode-display_width (1.6.0)
|
49
78
|
|
50
79
|
PLATFORMS
|
51
80
|
ruby
|
52
81
|
|
53
82
|
DEPENDENCIES
|
54
83
|
bundler (>= 1.17)
|
84
|
+
guard
|
85
|
+
guard-rspec
|
55
86
|
pry
|
56
87
|
rake (>= 10.0)
|
57
88
|
rspec (>= 3.0)
|
@@ -60,4 +91,4 @@ DEPENDENCIES
|
|
60
91
|
typerb!
|
61
92
|
|
62
93
|
BUNDLED WITH
|
63
|
-
1.17.
|
94
|
+
1.17.2
|
data/Guardfile
ADDED
@@ -0,0 +1,36 @@
|
|
1
|
+
# A sample Guardfile
|
2
|
+
# More info at https://github.com/guard/guard#readme
|
3
|
+
|
4
|
+
## Uncomment and set this to only include directories you want to watch
|
5
|
+
# directories %w(app lib config test spec features) \
|
6
|
+
# .select{|d| Dir.exists?(d) ? d : UI.warning("Directory #{d} does not exist")}
|
7
|
+
|
8
|
+
## Note: if you are using the `directories` clause above and you are not
|
9
|
+
## watching the project directory ('.'), then you will want to move
|
10
|
+
## the Guardfile to a watched dir and symlink it back, e.g.
|
11
|
+
#
|
12
|
+
# $ mkdir config
|
13
|
+
# $ mv Guardfile config/
|
14
|
+
# $ ln -s config/Guardfile .
|
15
|
+
#
|
16
|
+
# and, you'll have to watch "config/Guardfile" instead of "Guardfile"
|
17
|
+
guard :rspec, cmd: "bundle exec rspec" do
|
18
|
+
require "guard/rspec/dsl"
|
19
|
+
dsl = Guard::RSpec::Dsl.new(self)
|
20
|
+
|
21
|
+
# Feel free to open issues for suggestions and improvements
|
22
|
+
|
23
|
+
# RSpec files
|
24
|
+
rspec = dsl.rspec
|
25
|
+
watch(rspec.spec_helper) { rspec.spec_dir }
|
26
|
+
watch(rspec.spec_support) { rspec.spec_dir }
|
27
|
+
watch(rspec.spec_files)
|
28
|
+
|
29
|
+
# Ruby files
|
30
|
+
ruby = dsl.ruby
|
31
|
+
dsl.watch_spec_files_for(ruby.lib_files)
|
32
|
+
|
33
|
+
|
34
|
+
# watch(rails.routes) { "#{rspec.spec_dir}/routing" }
|
35
|
+
# watch(rails.app_controller) { "#{rspec.spec_dir}/controllers" }
|
36
|
+
end
|
data/README.md
CHANGED
@@ -3,7 +3,7 @@
|
|
3
3
|
|
4
4
|
# Typerb
|
5
5
|
|
6
|
-
Proof of concept type-checking library for Ruby 2.6.
|
6
|
+
Proof of concept type-checking library for Ruby 2.6. Works with previous versions too with some limitation (see below).
|
7
7
|
|
8
8
|
```ruby
|
9
9
|
class A
|
@@ -12,9 +12,19 @@ class A
|
|
12
12
|
def call(some_arg)
|
13
13
|
some_arg.type!(String, Symbol)
|
14
14
|
end
|
15
|
+
|
16
|
+
def call_with_respond_checks(some_arg)
|
17
|
+
some_arg.respond_to!(:strip)
|
18
|
+
end
|
19
|
+
|
20
|
+
def call_with_enum(arg)
|
21
|
+
arg.enum!(:one, :two)
|
22
|
+
end
|
15
23
|
end
|
16
24
|
|
17
25
|
A.new.call(1) #=> TypeError: `some_arg` should be String or Symbol, not Integer
|
26
|
+
A.new.call_with_respond_checks(1) #=> TypeError: 'Integer should respond to all methods: strip'
|
27
|
+
A.new.call_with_enum(:three) #=> TypeError: 'Symbol (`arg`) should be one of: [one, two], not three'
|
18
28
|
```
|
19
29
|
|
20
30
|
This is equivalent to:
|
@@ -23,6 +33,10 @@ class A
|
|
23
33
|
def call(some_arg)
|
24
34
|
raise TypeError, "`some_arg` should be String or Symbol, not #{some_arg.class}" unless [String, Symbol].include?(some_arg.class)
|
25
35
|
end
|
36
|
+
|
37
|
+
def call_with_respond_checks(some_arg)
|
38
|
+
raise TypeError, "#{some_arg.class} should respond to all methods: strip" unless [:strip].all{|meth| some_arg.respond_to?(meth)}
|
39
|
+
end
|
26
40
|
end
|
27
41
|
```
|
28
42
|
|
@@ -42,6 +56,11 @@ end
|
|
42
56
|
A.new.call(nil) #=> TypeError: expected not nil, but got nil
|
43
57
|
```
|
44
58
|
|
59
|
+
## Why?
|
60
|
+
|
61
|
+
1. Catch error as early as possible (especially nils);
|
62
|
+
2. Additional documentation: you're telling other people more about interfaces.
|
63
|
+
|
45
64
|
## Installation
|
46
65
|
|
47
66
|
Add this line to your application's Gemfile:
|
@@ -90,13 +109,13 @@ end
|
|
90
109
|
|
91
110
|
If you're unfamiliar with `using` keyword - this is refinement - a relatively new feature in Ruby (since 2.0). It's kind of monkey-patch, but with strict scope. Learn more about [refinements](https://ruby-doc.org/core-2.5.3/doc/syntax/refinements_rdoc.html).
|
92
111
|
|
93
|
-
This refinement adds `type!()`
|
112
|
+
This refinement adds `type!()` and `not_nil!` methods to `Object` class so you can call it on almost much any object (except those inherited from `BasicObject`, but these are rare).
|
94
113
|
|
95
114
|
The method will raise an exception if `self` is not an instance of one of the classes passed as arguments. The tricky part, however, is to get the variable name on which it's called. You need this to get a nice error message telling you exactly which variable has wrong type, not just an abstract `TypeError`. That's why we need Ruby 2.6 with its new `RubyVM::AST` (https://ruby-doc.org/core-2.6.0.preview3/RubyVM/AST.html).
|
96
115
|
|
97
116
|
## Limitations
|
98
117
|
|
99
|
-
|
118
|
+
Full functionality Ruby 2.6.0-preview3. Relies on `RubyVM::AST` which may change in release version. So, expect breaking changes in Ruby. Previous versions also supported, but without variable name in exception message.
|
100
119
|
|
101
120
|
Known limitations:
|
102
121
|
|
@@ -107,8 +126,9 @@ class A
|
|
107
126
|
|
108
127
|
def call(some_arg)
|
109
128
|
some_arg.
|
110
|
-
type!(String)
|
111
|
-
|
129
|
+
type!(String)
|
130
|
+
# this won't work. type!() call must be on the same line with the variable it's called on - raise error message without variable name
|
131
|
+
# some_arg. type!(String) is ok though
|
112
132
|
end
|
113
133
|
end
|
114
134
|
```
|
@@ -119,10 +139,11 @@ end
|
|
119
139
|
[1] pry(main)* using Typerb
|
120
140
|
[1] pry(main)* def call(a)
|
121
141
|
[1] pry(main)* a.type!(Hash)
|
122
|
-
[1] pry(main)* end
|
123
|
-
[1] pry(main)* end
|
142
|
+
[1] pry(main)* end
|
143
|
+
[1] pry(main)* end
|
124
144
|
[2] pry(main)> A.new.call(1)
|
125
|
-
TypeError: expected Hash, got Integer
|
145
|
+
TypeError: expected Hash, got Integer
|
146
|
+
# here we cannot get the source code for a line containing "a.type!(Hash)", so cannot see the variable name
|
126
147
|
```
|
127
148
|
|
128
149
|
3. Multiple arguments on the same line:
|
@@ -131,7 +152,9 @@ class A
|
|
131
152
|
using Typerb
|
132
153
|
|
133
154
|
def initialize(arg1, arg2)
|
134
|
-
arg1.type!(Integer); arg2.type!(String)
|
155
|
+
arg1.type!(Integer); arg2.type!(String)
|
156
|
+
# no way to tell the variable - raise error message without variable name
|
157
|
+
# same error will be raised on Ruby < 2.6.0 because there is no RubyVM::AST
|
135
158
|
end
|
136
159
|
end
|
137
160
|
```
|
data/lib/typerb.rb
CHANGED
@@ -5,7 +5,7 @@ require 'typerb/variable_name'
|
|
5
5
|
require 'typerb/exceptional'
|
6
6
|
|
7
7
|
module Typerb
|
8
|
-
refine
|
8
|
+
refine BasicObject do
|
9
9
|
def type!(*klasses)
|
10
10
|
raise ArgumentError, 'provide at least one class' if klasses.empty?
|
11
11
|
return self if klasses.any? { |kls| is_a?(kls) }
|
@@ -31,5 +31,33 @@ module Typerb
|
|
31
31
|
|
32
32
|
Typerb::Exceptional.new.raise_with(caller, exception_text)
|
33
33
|
end
|
34
|
+
|
35
|
+
def respond_to!(*methods)
|
36
|
+
raise ArgumentError, 'provide at least one method' if methods.empty?
|
37
|
+
return self if methods.all? { |meth| respond_to?(meth) }
|
38
|
+
|
39
|
+
methods_text = Typerb::Exceptional.methods_text(methods)
|
40
|
+
exception_text = if (var_name = Typerb::VariableName.new(caller_locations(1, 1)).get)
|
41
|
+
"#{self.class} (`#{var_name}`) should respond to all methods: #{methods_text}"
|
42
|
+
else
|
43
|
+
"#{self.class} should respond to all methods: #{methods_text}"
|
44
|
+
end
|
45
|
+
|
46
|
+
Typerb::Exceptional.new.raise_with(caller, exception_text)
|
47
|
+
end
|
48
|
+
|
49
|
+
def enum!(*elements)
|
50
|
+
raise ArgumentError, 'provide at least one enum element' if elements.empty?
|
51
|
+
return self if elements.include?(self)
|
52
|
+
|
53
|
+
elements_text = Typerb::Exceptional.elements_text(elements)
|
54
|
+
exception_text = if (var_name = Typerb::VariableName.new(caller_locations(1, 1)).get)
|
55
|
+
"#{self.class} (`#{var_name}`) should be one of: #{elements_text}, not #{self}"
|
56
|
+
else
|
57
|
+
"#{self.class} expected one of: #{elements_text}, got #{self}"
|
58
|
+
end
|
59
|
+
|
60
|
+
Typerb::Exceptional.new.raise_with(caller, exception_text)
|
61
|
+
end
|
34
62
|
end
|
35
63
|
end
|
data/lib/typerb/exceptional.rb
CHANGED
@@ -6,6 +6,14 @@ module Typerb
|
|
6
6
|
def klasses_text(klasses)
|
7
7
|
klasses.size > 1 ? klasses.map(&:name).join(' or ') : klasses.first.name
|
8
8
|
end
|
9
|
+
|
10
|
+
def methods_text(methods)
|
11
|
+
methods.join(', ')
|
12
|
+
end
|
13
|
+
|
14
|
+
def elements_text(elements)
|
15
|
+
'[' + elements.join(', ') + ']'
|
16
|
+
end
|
9
17
|
end
|
10
18
|
|
11
19
|
def raise_with(backtrace, exception_text)
|
data/lib/typerb/variable_name.rb
CHANGED
@@ -10,7 +10,7 @@ module Typerb
|
|
10
10
|
end
|
11
11
|
|
12
12
|
def get
|
13
|
-
return
|
13
|
+
return unless defined?(RubyVM::AbstractSyntaxTree)
|
14
14
|
return unless File.exist?(file)
|
15
15
|
|
16
16
|
caller_method = caller_locations(1, 1)[0].label.to_sym
|
@@ -21,7 +21,7 @@ module Typerb
|
|
21
21
|
|
22
22
|
def from_ast(caller_method) # rubocop: disable Metrics/AbcSize not worth fixing
|
23
23
|
code = File.read(file).lines[line - 1].strip
|
24
|
-
node = RubyVM::
|
24
|
+
node = RubyVM::AbstractSyntaxTree.parse(code)
|
25
25
|
if node.children.last.children.size == 3 && node.children.last.children[1] == caller_method # rubocop: disable Style/IfUnlessModifier, Style/GuardClause
|
26
26
|
node.children.last.children.first.children.first
|
27
27
|
end
|
data/lib/typerb/version.rb
CHANGED
data/typerb.gemspec
CHANGED
@@ -25,6 +25,8 @@ Gem::Specification.new do |spec|
|
|
25
25
|
spec.require_paths = ['lib']
|
26
26
|
|
27
27
|
spec.add_development_dependency 'bundler', '>= 1.17'
|
28
|
+
spec.add_development_dependency 'guard'
|
29
|
+
spec.add_development_dependency 'guard-rspec'
|
28
30
|
spec.add_development_dependency 'pry'
|
29
31
|
spec.add_development_dependency 'rake', '>= 10.0'
|
30
32
|
spec.add_development_dependency 'rspec', '>= 3.0'
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: typerb
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.3.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Oleg Antonyan
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2021-06-10 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: bundler
|
@@ -24,6 +24,34 @@ dependencies:
|
|
24
24
|
- - ">="
|
25
25
|
- !ruby/object:Gem::Version
|
26
26
|
version: '1.17'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: guard
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ">="
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '0'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - ">="
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: guard-rspec
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - ">="
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '0'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - ">="
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0'
|
27
55
|
- !ruby/object:Gem::Dependency
|
28
56
|
name: pry
|
29
57
|
requirement: !ruby/object:Gem::Requirement
|
@@ -109,6 +137,7 @@ files:
|
|
109
137
|
- CODE_OF_CONDUCT.md
|
110
138
|
- Gemfile
|
111
139
|
- Gemfile.lock
|
140
|
+
- Guardfile
|
112
141
|
- LICENSE.txt
|
113
142
|
- README.md
|
114
143
|
- Rakefile
|
@@ -138,8 +167,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
138
167
|
- !ruby/object:Gem::Version
|
139
168
|
version: '0'
|
140
169
|
requirements: []
|
141
|
-
|
142
|
-
rubygems_version: 3.0.0.beta2
|
170
|
+
rubygems_version: 3.0.3
|
143
171
|
signing_key:
|
144
172
|
specification_version: 4
|
145
173
|
summary: Typecheck sugar for Ruby.
|