delivered 0.1.0 → 0.2.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/Gemfile.lock +1 -1
- data/README.md +126 -4
- data/delivered.gemspec +5 -1
- data/lib/delivered/signature.rb +72 -16
- data/lib/delivered/types.rb +41 -0
- data/lib/delivered/version.rb +1 -1
- data/lib/delivered.rb +4 -0
- metadata +7 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 23b613f6dd8b19b06d0ca874c1a4f7408708f18b88e05d53b683693b27a20a04
|
4
|
+
data.tar.gz: 1380e56b87e1fa5b534b5bd6645dc1d965dec1c81021e042c4fc7f031aafdae2
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: ef984b3f51256ace952efaac631986a500c121408a42f544f6f9c4c296d2d88e2cc909452232bbdb498b4177ead3b910139205105c4137180c56dfcca2297f28
|
7
|
+
data.tar.gz: 17d2eaeb8cc23bfe69dd3f76b7b33d09d967f0f87d81221209c4b09c6cbd3777ed8ea45d4fd62ad119155a7bd1cd1aba8476677d022a6b24afb23032ff9dd423
|
data/Gemfile.lock
CHANGED
data/README.md
CHANGED
@@ -1,18 +1,140 @@
|
|
1
1
|
# Delivered: Simple runtime type checking for Ruby method signatures
|
2
2
|
|
3
|
+
> Signed, Sealed, Delivered 🎹
|
4
|
+
|
3
5
|
Delivered gives you the ability to define method signatures in Ruby, and have them checked at
|
4
6
|
runtime. This is useful for ensuring that your methods are being called with the correct arguments,
|
5
|
-
and for providing better error messages when they are not.
|
7
|
+
and for providing better error messages when they are not. It also serves as a nice way of
|
8
|
+
documenting your methods using actual code instead of comments.
|
9
|
+
|
10
|
+
## Usage
|
6
11
|
|
7
12
|
Simply define a method signature using the `sig` method directly before the method to be checked,
|
8
|
-
and Delivered will check that the method is being called with the correct arguments and types.
|
9
|
-
can alos chreck the return value of the method.
|
13
|
+
and Delivered will check that the method is being called with the correct arguments and types.
|
10
14
|
|
11
15
|
```ruby
|
12
16
|
class User
|
13
|
-
|
17
|
+
extend Delivered::Signature
|
18
|
+
|
19
|
+
sig String, age: Integer
|
14
20
|
def create(name, age:)
|
15
21
|
"User #{name} created with age #{age}"
|
16
22
|
end
|
17
23
|
end
|
18
24
|
```
|
25
|
+
|
26
|
+
If an invalid argument is given to `User#create`, for example, if `age` is a `String` instead of
|
27
|
+
the required `Integer`, a `Delivered::ArgumentError` exception will be raised.
|
28
|
+
|
29
|
+
### Return Types
|
30
|
+
|
31
|
+
You can also check the return value of the method by passing a Hash with an Array as the key, and
|
32
|
+
the value as the return type to check.
|
33
|
+
|
34
|
+
```ruby
|
35
|
+
sig [String, age: Integer] => String
|
36
|
+
def create(name, age:)
|
37
|
+
"User #{name} created with age #{age}"
|
38
|
+
end
|
39
|
+
```
|
40
|
+
|
41
|
+
Or by placing the return type in a block to `sig`.
|
42
|
+
|
43
|
+
```ruby
|
44
|
+
sig(String, age: Integer) { String }
|
45
|
+
def create(name, age:)
|
46
|
+
"User #{name} created with age #{age}"
|
47
|
+
end
|
48
|
+
```
|
49
|
+
|
50
|
+
### Delivered Types
|
51
|
+
|
52
|
+
As well as Ruby's native types (ie. `String`, `Integer`, etc.), _Delivered_ provides a couple of
|
53
|
+
extra types in `Delivered::Types`.
|
54
|
+
|
55
|
+
You can call these directly with `Delivered::Types.Boolean`, or for brevity, assign
|
56
|
+
`Delivered::Types` to `T` in your classes:
|
57
|
+
|
58
|
+
```ruby
|
59
|
+
class User
|
60
|
+
extend Delivered::Signature
|
61
|
+
T = Delivered::Types
|
62
|
+
end
|
63
|
+
```
|
64
|
+
|
65
|
+
The following examples all use the `T` alias, and assumes the above.
|
66
|
+
|
67
|
+
#### `Boolean`
|
68
|
+
|
69
|
+
Value **MUST** be `true` or `false`. Does not support "truthy" or "falsy" values.
|
70
|
+
|
71
|
+
```ruby
|
72
|
+
sig validate: T.Boolean
|
73
|
+
def create(validate:); end
|
74
|
+
```
|
75
|
+
|
76
|
+
#### `Any`
|
77
|
+
|
78
|
+
Value **MUST** be any of the given list of values, that is, the value must be one of the given list.
|
79
|
+
|
80
|
+
```ruby
|
81
|
+
sig T.Any(:male, :female)
|
82
|
+
def create(gender); end
|
83
|
+
```
|
84
|
+
|
85
|
+
If no type is given, the value **CAN** be any type or value.
|
86
|
+
|
87
|
+
```ruby
|
88
|
+
sig save: T.Any
|
89
|
+
def create(save: nil); end
|
90
|
+
```
|
91
|
+
|
92
|
+
You can also pass `nil` to allow a nil value alongside any other types you provide.
|
93
|
+
|
94
|
+
```ruby
|
95
|
+
sig T.Any(String, nil)
|
96
|
+
def create(save = nil); end
|
97
|
+
```
|
98
|
+
|
99
|
+
#### `Nilable`
|
100
|
+
|
101
|
+
When a type is given, the value **MUST** be nil **OR** of the given type.
|
102
|
+
|
103
|
+
```ruby
|
104
|
+
sig save: T.Nilable(String)
|
105
|
+
def create(save: nil); end
|
106
|
+
|
107
|
+
sig T.Nilable(String)
|
108
|
+
def update(name = nil); end
|
109
|
+
```
|
110
|
+
|
111
|
+
If no type is given, the value **CAN** be nil. This essentially allows any value, including nil.
|
112
|
+
|
113
|
+
```ruby
|
114
|
+
sig save: T.Nilable
|
115
|
+
def create(save: nil); end
|
116
|
+
```
|
117
|
+
|
118
|
+
You may notice that `Nilable` is interchangeable with `Any`. The following are equivilent:
|
119
|
+
|
120
|
+
```ruby
|
121
|
+
sig save: T.Nilable
|
122
|
+
def create(save: nil); end
|
123
|
+
```
|
124
|
+
|
125
|
+
```ruby
|
126
|
+
sig save: T.Any
|
127
|
+
def create(save: nil); end
|
128
|
+
```
|
129
|
+
|
130
|
+
As are these:
|
131
|
+
|
132
|
+
```ruby
|
133
|
+
sig T.Nilable(String)
|
134
|
+
def update(name = nil); end
|
135
|
+
```
|
136
|
+
|
137
|
+
```ruby
|
138
|
+
sig T.Any(String, nil)
|
139
|
+
def update(name = nil); end
|
140
|
+
```
|
data/delivered.gemspec
CHANGED
@@ -11,7 +11,11 @@ Gem::Specification.new do |s|
|
|
11
11
|
s.homepage = 'https://github.com/joelmoss/delivered'
|
12
12
|
s.licenses = ['MIT']
|
13
13
|
s.summary = 'Simple runtime type checking for Ruby method signatures'
|
14
|
-
s.required_ruby_version = '>= 3.
|
14
|
+
s.required_ruby_version = '>= 3.1'
|
15
|
+
|
16
|
+
s.metadata['homepage_uri'] = s.homepage
|
17
|
+
s.metadata['source_code_uri'] = s.homepage
|
18
|
+
s.metadata['changelog_uri'] = "#{s.homepage}/releases"
|
15
19
|
|
16
20
|
s.files = Dir.glob('{bin/*,lib/**/*,[A-Z]*}')
|
17
21
|
s.platform = Gem::Platform::RUBY
|
data/lib/delivered/signature.rb
CHANGED
@@ -2,32 +2,88 @@
|
|
2
2
|
|
3
3
|
module Delivered
|
4
4
|
module Signature
|
5
|
-
|
5
|
+
def sig(*sig_args, **sig_kwargs, &return_blk)
|
6
|
+
# ap [sig_args, sig_kwargs, return_blk]
|
7
|
+
|
8
|
+
# Block return
|
9
|
+
returns = return_blk&.call
|
10
|
+
|
11
|
+
# Hashrocket return
|
12
|
+
if sig_kwargs.keys[0].is_a?(Array)
|
13
|
+
unless returns.nil?
|
14
|
+
raise Delivered::ArgumentError,
|
15
|
+
'Cannot mix block and hash for return type. Use one or the other.', caller
|
16
|
+
end
|
17
|
+
|
18
|
+
returns = sig_kwargs.values[0]
|
19
|
+
sig_args = sig_kwargs.keys[0]
|
20
|
+
sig_kwargs = sig_args.pop if sig_args.last.is_a?(Hash)
|
21
|
+
end
|
22
|
+
|
23
|
+
# ap [sig_args, sig_kwargs, returns]
|
6
24
|
|
7
|
-
def sig(*sig_args, returns: NULL, **sig_kwargs)
|
8
25
|
meta = class << self; self; end
|
26
|
+
sig_check = lambda do |klass, class_method, name, *args, **kwargs, &block| # rubocop:disable Metrics/BlockLength
|
27
|
+
cname = if class_method
|
28
|
+
"#{klass.name}.#{name}"
|
29
|
+
else
|
30
|
+
"#{klass.class.name}##{name}"
|
31
|
+
end
|
32
|
+
|
33
|
+
sig_args.each.with_index do |arg, i|
|
34
|
+
args[i] => ^arg
|
35
|
+
rescue NoMatchingPatternError => e
|
36
|
+
raise Delivered::ArgumentError,
|
37
|
+
"`#{cname}` expected `#{arg}` as positional arg #{i}, but received " \
|
38
|
+
"`#{args[i].inspect}`",
|
39
|
+
caller, cause: e
|
40
|
+
end
|
41
|
+
|
42
|
+
kwargs.each do |key, value|
|
43
|
+
value => ^(sig_kwargs[key])
|
44
|
+
rescue NoMatchingPatternError => e
|
45
|
+
raise Delivered::ArgumentError,
|
46
|
+
"`#{cname}` expected `#{sig_kwargs[key]}` as keyword arg :#{key}, but received " \
|
47
|
+
"`#{value.inspect}`",
|
48
|
+
caller, cause: e
|
49
|
+
end
|
50
|
+
|
51
|
+
result = if block
|
52
|
+
klass.send(:"__#{name}", *args, **kwargs, &block)
|
53
|
+
else
|
54
|
+
klass.send(:"__#{name}", *args, **kwargs)
|
55
|
+
end
|
56
|
+
|
57
|
+
begin
|
58
|
+
result => ^returns unless returns.nil?
|
59
|
+
rescue NoMatchingPatternError => e
|
60
|
+
raise Delivered::ArgumentError,
|
61
|
+
"`#{cname}` expected to return `#{returns}`, but returned `#{result.inspect}`",
|
62
|
+
caller, cause: e
|
63
|
+
end
|
64
|
+
|
65
|
+
result
|
66
|
+
end
|
67
|
+
|
9
68
|
meta.send :define_method, :method_added do |name|
|
10
69
|
meta.send :remove_method, :method_added
|
70
|
+
meta.send :remove_method, :singleton_method_added
|
11
71
|
|
12
72
|
alias_method :"__#{name}", name
|
13
73
|
define_method name do |*args, **kwargs, &block|
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
kwargs.each do |key, value|
|
19
|
-
value => ^(sig_kwargs[key])
|
20
|
-
end
|
74
|
+
sig_check.call(self, false, name, *args, **kwargs, &block)
|
75
|
+
end
|
76
|
+
end
|
21
77
|
|
22
|
-
|
23
|
-
|
24
|
-
else
|
25
|
-
send(:"__#{name}", *args, **kwargs)
|
26
|
-
end
|
78
|
+
meta.send :define_method, :singleton_method_added do |name|
|
79
|
+
next if name == :singleton_method_added
|
27
80
|
|
28
|
-
|
81
|
+
meta.send :remove_method, :singleton_method_added
|
82
|
+
meta.send :remove_method, :method_added
|
29
83
|
|
30
|
-
|
84
|
+
meta.alias_method :"__#{name}", name
|
85
|
+
define_singleton_method name do |*args, **kwargs, &block|
|
86
|
+
sig_check.call(self, true, name, *args, **kwargs, &block)
|
31
87
|
end
|
32
88
|
end
|
33
89
|
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Delivered
|
4
|
+
class AnyType
|
5
|
+
def initialize(*types)
|
6
|
+
@types = types
|
7
|
+
end
|
8
|
+
|
9
|
+
def ===(value)
|
10
|
+
@types.empty? ? true : @types.any? { |type| type === value }
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
class NilableType
|
15
|
+
def initialize(type = nil)
|
16
|
+
@type = type
|
17
|
+
end
|
18
|
+
|
19
|
+
def ===(value)
|
20
|
+
(@type.nil? ? true : nil === value) || @type === value
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
class BooleanType
|
25
|
+
def initialize
|
26
|
+
freeze
|
27
|
+
end
|
28
|
+
|
29
|
+
def ===(value)
|
30
|
+
[true, false].include?(value)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
module Types
|
35
|
+
module_function
|
36
|
+
|
37
|
+
def Nilable(type = nil) = NilableType.new(type)
|
38
|
+
def Any(*types) = AnyType.new(*types)
|
39
|
+
def Boolean = BooleanType.new
|
40
|
+
end
|
41
|
+
end
|
data/lib/delivered/version.rb
CHANGED
data/lib/delivered.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: delivered
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.2.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Joel Moss
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2024-
|
11
|
+
date: 2024-04-04 00:00:00.000000000 Z
|
12
12
|
dependencies: []
|
13
13
|
description:
|
14
14
|
email:
|
@@ -24,11 +24,15 @@ files:
|
|
24
24
|
- delivered.gemspec
|
25
25
|
- lib/delivered.rb
|
26
26
|
- lib/delivered/signature.rb
|
27
|
+
- lib/delivered/types.rb
|
27
28
|
- lib/delivered/version.rb
|
28
29
|
homepage: https://github.com/joelmoss/delivered
|
29
30
|
licenses:
|
30
31
|
- MIT
|
31
32
|
metadata:
|
33
|
+
homepage_uri: https://github.com/joelmoss/delivered
|
34
|
+
source_code_uri: https://github.com/joelmoss/delivered
|
35
|
+
changelog_uri: https://github.com/joelmoss/delivered/releases
|
32
36
|
rubygems_mfa_required: 'true'
|
33
37
|
post_install_message:
|
34
38
|
rdoc_options: []
|
@@ -38,7 +42,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
38
42
|
requirements:
|
39
43
|
- - ">="
|
40
44
|
- !ruby/object:Gem::Version
|
41
|
-
version: '3.
|
45
|
+
version: '3.1'
|
42
46
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
43
47
|
requirements:
|
44
48
|
- - ">="
|