delivered 0.1.0 → 0.2.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: 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
  - - ">="