delivered 0.1.0 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 3ab93c9434d5e06930f9fa814dfef95849cda9c6b76c611ed54b7c51d1e4856d
4
- data.tar.gz: 868e6e469da0d2a07e752f7c35725414a99aa06ab898f13c8b177cf6c2d892f5
3
+ metadata.gz: c2cc2a244302acba122bf449caa221233453955568ca1a7c42f9a4c66e75093a
4
+ data.tar.gz: 2c6033b4adee7756b8fb7bbb9ff0cac83bbe7f42e35220eb9a24857c17e2aa7e
5
5
  SHA512:
6
- metadata.gz: 1d94413829107f3b7cd6c60d4a28fc9c98abc5649b5dca0987ba88499729798d090563c882630a3be7495d22c99c32011a219c8d819d11429f7b23afb15c1db6
7
- data.tar.gz: ac6388a9494a423891d5d9260c42ca7c9e4c90f0d85be89f36e54732aaa0cac0c0797c1947f8d983aa544e2967000e8187925ca60087d3062fe05d10d5fbf072
6
+ metadata.gz: 3adf4d4bb048a39a00e331e598814998fffe28673702d08bba181cd574f521b3f2d164725c5fe61d349c85c960c32a59d98bb8855ff0f26db6ad5bd74123ae4e
7
+ data.tar.gz: 735bde97671a2e2d69238b5363de35971ecb1a7ab4003e2352c560231bd461b712a3e652bd01c9b02cf067d5d8b2a856baf6b8709621c5f8e34b0eb94c692e9f
data/Gemfile.lock CHANGED
@@ -1,24 +1,24 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- delivered (0.1.0)
4
+ delivered (0.3.0)
5
5
 
6
6
  GEM
7
7
  remote: https://rubygems.org/
8
8
  specs:
9
9
  amazing_print (1.6.0)
10
10
  ast (2.4.2)
11
- debug (1.9.1)
11
+ debug (1.9.2)
12
12
  irb (~> 1.10)
13
13
  reline (>= 0.3.8)
14
14
  io-console (0.7.2)
15
- irb (1.12.0)
16
- rdoc
15
+ irb (1.13.1)
16
+ rdoc (>= 4.0.0)
17
17
  reline (>= 0.4.2)
18
- json (2.7.1)
18
+ json (2.7.2)
19
19
  language_server-protocol (3.17.0.3)
20
20
  parallel (1.24.0)
21
- parser (3.3.0.5)
21
+ parser (3.3.1.0)
22
22
  ast (~> 2.4.1)
23
23
  racc
24
24
  psych (5.1.2)
@@ -27,11 +27,12 @@ GEM
27
27
  rainbow (3.1.1)
28
28
  rdoc (6.6.3.1)
29
29
  psych (>= 4.0.0)
30
- regexp_parser (2.9.0)
31
- reline (0.5.0)
30
+ regexp_parser (2.9.2)
31
+ reline (0.5.7)
32
32
  io-console (~> 0.5)
33
- rexml (3.2.6)
34
- rubocop (1.62.1)
33
+ rexml (3.2.8)
34
+ strscan (>= 3.0.9)
35
+ rubocop (1.63.5)
35
36
  json (~> 2.3)
36
37
  language_server-protocol (>= 3.17.0)
37
38
  parallel (~> 1.10)
@@ -42,11 +43,12 @@ GEM
42
43
  rubocop-ast (>= 1.31.1, < 2.0)
43
44
  ruby-progressbar (~> 1.7)
44
45
  unicode-display_width (>= 2.4.0, < 3.0)
45
- rubocop-ast (1.31.2)
46
- parser (>= 3.3.0.4)
46
+ rubocop-ast (1.31.3)
47
+ parser (>= 3.3.1.0)
47
48
  ruby-progressbar (1.13.0)
48
49
  stringio (3.1.0)
49
- sus (0.24.6)
50
+ strscan (3.1.0)
51
+ sus (0.25.0)
50
52
  unicode-display_width (2.5.0)
51
53
 
52
54
  PLATFORMS
data/README.md CHANGED
@@ -1,18 +1,149 @@
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. it
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
- sig String, age: Integer, returns: String
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
+ #### `RespondTo`
77
+
78
+ Value **MUST** be respond to the given method(s).
79
+
80
+ ```ruby
81
+ sig name: T.RespondTo(:to_s)
82
+ def create(name:); end
83
+ ```
84
+
85
+ #### `Any`
86
+
87
+ Value **MUST** be any of the given list of values, that is, the value must be one of the given list.
88
+
89
+ ```ruby
90
+ sig T.Any(:male, :female)
91
+ def create(gender); end
92
+ ```
93
+
94
+ If no type is given, the value **CAN** be any type or value.
95
+
96
+ ```ruby
97
+ sig save: T.Any
98
+ def create(save: nil); end
99
+ ```
100
+
101
+ You can also pass `nil` to allow a nil value alongside any other types you provide.
102
+
103
+ ```ruby
104
+ sig T.Any(String, nil)
105
+ def create(save = nil); end
106
+ ```
107
+
108
+ #### `Nilable`
109
+
110
+ When a type is given, the value **MUST** be nil **OR** of the given type.
111
+
112
+ ```ruby
113
+ sig save: T.Nilable(String)
114
+ def create(save: nil); end
115
+
116
+ sig T.Nilable(String)
117
+ def update(name = nil); end
118
+ ```
119
+
120
+ If no type is given, the value **CAN** be nil. This essentially allows any value, including nil.
121
+
122
+ ```ruby
123
+ sig save: T.Nilable
124
+ def create(save: nil); end
125
+ ```
126
+
127
+ You may notice that `Nilable` is interchangeable with `Any`. The following are equivilent:
128
+
129
+ ```ruby
130
+ sig save: T.Nilable
131
+ def create(save: nil); end
132
+ ```
133
+
134
+ ```ruby
135
+ sig save: T.Any
136
+ def create(save: nil); end
137
+ ```
138
+
139
+ As are these:
140
+
141
+ ```ruby
142
+ sig T.Nilable(String)
143
+ def update(name = nil); end
144
+ ```
145
+
146
+ ```ruby
147
+ sig T.Any(String, nil)
148
+ def update(name = nil); end
149
+ ```
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.2'
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
@@ -2,32 +2,89 @@
2
2
 
3
3
  module Delivered
4
4
  module Signature
5
- NULL = Object.new
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.inspect} as argument #{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].inspect} as keyword argument :#{key}, " \
47
+ "but received `#{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.inspect}, " \
62
+ "but returned `#{result.inspect}`",
63
+ caller, cause: e
64
+ end
65
+
66
+ result
67
+ end
68
+
9
69
  meta.send :define_method, :method_added do |name|
10
70
  meta.send :remove_method, :method_added
71
+ meta.send :remove_method, :singleton_method_added
11
72
 
12
73
  alias_method :"__#{name}", name
13
74
  define_method name do |*args, **kwargs, &block|
14
- sig_args.each.with_index do |arg, i|
15
- args[i] => ^arg
16
- end
17
-
18
- kwargs.each do |key, value|
19
- value => ^(sig_kwargs[key])
20
- end
75
+ sig_check.call(self, false, name, *args, **kwargs, &block)
76
+ end
77
+ end
21
78
 
22
- result = if block
23
- send(:"__#{name}", *args, **kwargs, &block)
24
- else
25
- send(:"__#{name}", *args, **kwargs)
26
- end
79
+ meta.send :define_method, :singleton_method_added do |name|
80
+ next if name == :singleton_method_added
27
81
 
28
- result => ^returns if returns != NULL
82
+ meta.send :remove_method, :singleton_method_added
83
+ meta.send :remove_method, :method_added
29
84
 
30
- result
85
+ meta.alias_method :"__#{name}", name
86
+ define_singleton_method name do |*args, **kwargs, &block|
87
+ sig_check.call(self, true, name, *args, **kwargs, &block)
31
88
  end
32
89
  end
33
90
  end
@@ -0,0 +1,60 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Delivered
4
+ class AnyType
5
+ def initialize(*types)
6
+ @types = types
7
+ end
8
+
9
+ def inspect = "Any(#{@types.map(&:inspect).join(', ')})"
10
+
11
+ def ===(value)
12
+ @types.empty? ? true : @types.any? { |type| type === value }
13
+ end
14
+ end
15
+
16
+ class RespondToType
17
+ def initialize(*methods)
18
+ @methods = methods
19
+ end
20
+
21
+ def inspect = "RespondTo(#{@methods.map(&:inspect).join(', ')})"
22
+
23
+ def ===(value)
24
+ @methods.all? { |m| value.respond_to?(m) }
25
+ end
26
+ end
27
+
28
+ class NilableType
29
+ def initialize(type = nil)
30
+ @type = type
31
+ end
32
+
33
+ def inspect = "Nilable(#{@type&.inspect || 'Any'})"
34
+
35
+ def ===(value)
36
+ (@type.nil? ? true : nil === value) || @type === value
37
+ end
38
+ end
39
+
40
+ class BooleanType
41
+ def initialize
42
+ freeze
43
+ end
44
+
45
+ def inspect = 'Boolean'
46
+
47
+ def ===(value)
48
+ [true, false].include?(value)
49
+ end
50
+ end
51
+
52
+ module Types
53
+ module_function
54
+
55
+ def Nilable(type = nil) = NilableType.new(type)
56
+ def RespondTo(*methods) = RespondToType.new(*methods)
57
+ def Any(*types) = AnyType.new(*types)
58
+ def Boolean = BooleanType.new
59
+ end
60
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Delivered
4
- VERSION = '0.1.0'
4
+ VERSION = '0.3.0'
5
5
  end
data/lib/delivered.rb CHANGED
@@ -1,5 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Delivered
4
+ class ArgumentError < ArgumentError
5
+ end
6
+
4
7
  autoload :Signature, 'delivered/signature'
8
+ autoload :Types, 'delivered/types'
5
9
  end
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.1.0
4
+ version: 0.3.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-03-27 00:00:00.000000000 Z
11
+ date: 2024-05-17 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,14 +42,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
38
42
  requirements:
39
43
  - - ">="
40
44
  - !ruby/object:Gem::Version
41
- version: '3.2'
45
+ version: '3.1'
42
46
  required_rubygems_version: !ruby/object:Gem::Requirement
43
47
  requirements:
44
48
  - - ">="
45
49
  - !ruby/object:Gem::Version
46
50
  version: '0'
47
51
  requirements: []
48
- rubygems_version: 3.5.7
52
+ rubygems_version: 3.5.9
49
53
  signing_key:
50
54
  specification_version: 4
51
55
  summary: Simple runtime type checking for Ruby method signatures