delivered 0.1.0 → 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 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