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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 3ab93c9434d5e06930f9fa814dfef95849cda9c6b76c611ed54b7c51d1e4856d
4
- data.tar.gz: 868e6e469da0d2a07e752f7c35725414a99aa06ab898f13c8b177cf6c2d892f5
3
+ metadata.gz: 23b613f6dd8b19b06d0ca874c1a4f7408708f18b88e05d53b683693b27a20a04
4
+ data.tar.gz: 1380e56b87e1fa5b534b5bd6645dc1d965dec1c81021e042c4fc7f031aafdae2
5
5
  SHA512:
6
- metadata.gz: 1d94413829107f3b7cd6c60d4a28fc9c98abc5649b5dca0987ba88499729798d090563c882630a3be7495d22c99c32011a219c8d819d11429f7b23afb15c1db6
7
- data.tar.gz: ac6388a9494a423891d5d9260c42ca7c9e4c90f0d85be89f36e54732aaa0cac0c0797c1947f8d983aa544e2967000e8187925ca60087d3062fe05d10d5fbf072
6
+ metadata.gz: ef984b3f51256ace952efaac631986a500c121408a42f544f6f9c4c296d2d88e2cc909452232bbdb498b4177ead3b910139205105c4137180c56dfcca2297f28
7
+ data.tar.gz: 17d2eaeb8cc23bfe69dd3f76b7b33d09d967f0f87d81221209c4b09c6cbd3777ed8ea45d4fd62ad119155a7bd1cd1aba8476677d022a6b24afb23032ff9dd423
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- delivered (0.1.0)
4
+ delivered (0.2.0)
5
5
 
6
6
  GEM
7
7
  remote: https://rubygems.org/
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. 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
+ #### `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.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,88 @@
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}` 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
- 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
74
+ sig_check.call(self, false, name, *args, **kwargs, &block)
75
+ end
76
+ end
21
77
 
22
- result = if block
23
- send(:"__#{name}", *args, **kwargs, &block)
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
- result => ^returns if returns != NULL
81
+ meta.send :remove_method, :singleton_method_added
82
+ meta.send :remove_method, :method_added
29
83
 
30
- result
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Delivered
4
- VERSION = '0.1.0'
4
+ VERSION = '0.2.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.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-03-27 00:00:00.000000000 Z
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.2'
45
+ version: '3.1'
42
46
  required_rubygems_version: !ruby/object:Gem::Requirement
43
47
  requirements:
44
48
  - - ">="