From 68ef60c21421fa020d0c189336897184e48838d9 Mon Sep 17 00:00:00 2001 From: Joel Moss Date: Thu, 4 Apr 2024 11:59:14 +0100 Subject: [PATCH] feat: Added additional Boolean, Union and Nilable types fixes #6 --- .rubocop.yml | 9 ++++++- README.md | 54 +++++++++++++++++++++++++++++++++++++ lib/delivered.rb | 1 + lib/delivered/signature.rb | 2 -- lib/delivered/types.rb | 41 ++++++++++++++++++++++++++++ test/delivered/signature.rb | 8 ++++++ test/delivered/types.rb | 47 ++++++++++++++++++++++++++++++++ 7 files changed, 159 insertions(+), 3 deletions(-) create mode 100644 lib/delivered/types.rb create mode 100644 test/delivered/types.rb diff --git a/.rubocop.yml b/.rubocop.yml index 30a5c66..e2a4f22 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -7,6 +7,14 @@ Style/Documentation: Enabled: false Style/HashAsLastArrayItem: Enabled: false +Style/CaseEquality: + Enabled: false +Style/NilComparison: + Enabled: false + +Naming/MethodName: + Exclude: + - "lib/delivered/types.rb" Layout/LineLength: Max: 100 @@ -23,7 +31,6 @@ Metrics/CyclomaticComplexity: Enabled: false Metrics/PerceivedComplexity: Enabled: false - Metrics/BlockLength: Exclude: - "test/**/*" diff --git a/README.md b/README.md index dd66505..33acb50 100644 --- a/README.md +++ b/README.md @@ -44,3 +44,57 @@ def create(name, age:) "User #{name} created with age #{age}" end ``` + +### Delivered Types + +As well as Ruby's native types (ie. `String`, `Integer`, etc.), _Delivered_ provides a couple of +extra types in `Delivered::Types`. + +You can call these directly with `Delivered::Types.Boolean`, or for brevity, assign +`Delivered::Types` to `T` in your classes: + +```ruby +class User + extend Delivered::Signature + T = Delivered::Types +end +``` + +The following examples all use the `T` alias, and assumes the above. + +#### `Boolean` + +Value **MUST** be `true` or `false`. Does not support "truthy" or "falsy" values. + +```ruby +sig validate: T.Boolean +def create(validate:); end +``` + +#### `Union` + +Value **MUST** be a union of the given list of values, that is, the value must be one of the given list. + +```ruby +sig T.Union(:male, :female) +def create(gender); end +``` + +#### `Nilable` + +When a type is given, the value **MUST** be nil or of the given type. + +```ruby +sig save: T.Nilable(String) +def create(save: nil); end + +sig T.Nilable(String) +def update(name = nil); end +``` + +If no type is given, the value **CAN** be nil. This essentially allows any value, including nil. + +```ruby +sig save: T.Nilable +def create(save: nil); end +``` diff --git a/lib/delivered.rb b/lib/delivered.rb index be2dbb9..9382b61 100644 --- a/lib/delivered.rb +++ b/lib/delivered.rb @@ -2,4 +2,5 @@ module Delivered autoload :Signature, 'delivered/signature' + autoload :Types, 'delivered/types' end diff --git a/lib/delivered/signature.rb b/lib/delivered/signature.rb index ff48b34..eb1edf7 100644 --- a/lib/delivered/signature.rb +++ b/lib/delivered/signature.rb @@ -2,8 +2,6 @@ module Delivered module Signature - NULL = Object.new - def sig(*sig_args, **sig_kwargs, &return_blk) # ap [sig_args, sig_kwargs, return_blk] diff --git a/lib/delivered/types.rb b/lib/delivered/types.rb new file mode 100644 index 0000000..c7c8b3b --- /dev/null +++ b/lib/delivered/types.rb @@ -0,0 +1,41 @@ +# frozen_string_literal: true + +module Delivered + class UnionType + def initialize(*types) + @types = types + end + + def ===(value) + @types.any? { |type| type === value } + end + end + + class NilableType + def initialize(type = nil) + @type = type + end + + def ===(value) + (@type.nil? ? true : nil === value) || @type === value + end + end + + class BooleanType + def initialize + freeze + end + + def ===(value) + [true, false].include?(value) + end + end + + module Types + module_function + + def Nilable(type = nil) = NilableType.new(type) + def Union(*types) = UnionType.new(*types) + def Boolean = BooleanType.new + end +end diff --git a/test/delivered/signature.rb b/test/delivered/signature.rb index 22bbf92..b23151b 100644 --- a/test/delivered/signature.rb +++ b/test/delivered/signature.rb @@ -35,6 +35,9 @@ def to_s = "#{@name}, #{@age}" sig { Integer } def age = @age.to_s + sig(Delivered::Types.Boolean) + def active=(val); end + sig(String, _age: Integer) { Array } def self.where(_name, _age: nil) = [] @@ -106,6 +109,11 @@ def self.find_by_name(name) = User.new(name) end end + it 'raises on incorrect Delivered type' do + user = User.new('Joel') + expect { user.active = 1 }.to raise_exception NoMatchingPatternError + end + it 'raises on missing args' do expect { User.new }.to raise_exception NoMatchingPatternError end diff --git a/test/delivered/types.rb b/test/delivered/types.rb new file mode 100644 index 0000000..6927bf7 --- /dev/null +++ b/test/delivered/types.rb @@ -0,0 +1,47 @@ +# frozen_string_literal: true + +describe Delivered::Types do + T = Delivered::Types + + describe 'Union' do + it 'is within union' do + assert T.Union(:one, :two) === :one + end + + it 'raises when value is not in union' do + expect { :three => ^(T.Union(:one, :two)) }.to raise_exception NoMatchingPatternError + expect { :one => ^(T.Union(:one, :two)) }.not.to raise_exception + end + end + + describe 'Boolean' do + it 'should be true or false' do + assert T.Boolean === true + assert T.Boolean === false + end + + it 'raises when not boolean' do + expect { 0 => ^(T.Boolean) }.to raise_exception NoMatchingPatternError + end + end + + describe 'Nilable' do + it 'can be nil' do + assert T.Nilable === nil + end + + with 'no given type' do + it 'can be any type' do + assert T.Nilable === 'hello' + expect { 'hello' => ^(T.Nilable) }.not.to raise_exception NoMatchingPatternError + end + end + + it 'can receive another type' do + assert T.Nilable(String) === 'hello' + assert T.Nilable(String) === nil + expect { 'hello' => ^(T.Nilable(String)) }.not.to raise_exception + expect { 1 => ^(T.Nilable(String)) }.to raise_exception NoMatchingPatternError + end + end +end