From f8f718ed696e0275b70533cfdf3c08efc1ad428b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?T=C3=ABma=20Bolshakov?= Date: Tue, 2 Apr 2024 22:22:34 +0200 Subject: [PATCH] Extract Dry::Types integration into a separate gem (#173) --- README.md | 74 +---------- lib/dry/types/fear.rb | 8 -- lib/dry/types/fear/option.rb | 125 ------------------ lib/fear.rb | 1 - .../dry/types/fear/option/constrained_spec.rb | 22 --- spec/dry/types/fear/option/core_spec.rb | 77 ----------- spec/dry/types/fear/option/default_spec.rb | 21 --- spec/dry/types/fear/option/hash_spec.rb | 58 -------- spec/dry/types/fear/option/option_spec.rb | 97 -------------- spec/support/.keep | 0 spec/support/dry_types.rb | 6 - 11 files changed, 4 insertions(+), 485 deletions(-) delete mode 100644 lib/dry/types/fear.rb delete mode 100644 lib/dry/types/fear/option.rb delete mode 100644 spec/dry/types/fear/option/constrained_spec.rb delete mode 100644 spec/dry/types/fear/option/core_spec.rb delete mode 100644 spec/dry/types/fear/option/default_spec.rb delete mode 100644 spec/dry/types/fear/option/hash_spec.rb delete mode 100644 spec/dry/types/fear/option/option_spec.rb create mode 100644 spec/support/.keep delete mode 100644 spec/support/dry_types.rb diff --git a/README.md b/README.md index 7eb7b9c..cbda7f8 100644 --- a/README.md +++ b/README.md @@ -1193,76 +1193,7 @@ end ### Dry-Types integration -#### Option - - NOTE: Requires the dry-tyes gem to be loaded. - -Load the `:fear_option` extension in your application. - -```ruby -require 'dry-types' -require 'dry/types/fear' - -Dry::Types.load_extensions(:fear_option) - -module Types - include Dry.Types() -end -``` - -Append .option to a Type to return a `Fear::Option` object: - -```ruby -Types::Option::Strict::Integer[nil] -#=> Fear.none -Types::Option::Coercible::String[nil] -#=> Fear.none -Types::Option::Strict::Integer[123] -#=> Fear.some(123) -Types::Option::Strict::String[123] -#=> Fear.some(123) -Types::Option::Coercible::Float['12.3'] -#=> Fear.some(12.3) -``` - -'Option' types can also accessed by calling '.option' on a regular type: - -```ruby -Types::Strict::Integer.option # equivalent to Types::Option::Strict::Integer -``` - - -You can define your own optional types: - -```ruby -option_string = Types::Strict::String.option -option_string[nil] -# => Fear.none -option_string[nil].map(&:upcase) -# => Fear.none -option_string['something'] -# => Fear.some('something') -option_string['something'].map(&:upcase) -# => Fear.some('SOMETHING') -option_string['something'].map(&:upcase).get_or_else { 'NOTHING' } -# => "SOMETHING" -``` - -You can use it with dry-struct as well: - -```ruby -class User < Dry::Struct - attribute :name, Types::Coercible::String - attribute :age, Types::Coercible::Integer.option -end - -user = User.new(name: 'Bob', age: nil) -user.name #=> "Bob" -user.age #=> Fear.none - -user = User.new(name: 'Bob', age: 42) -user.age #=> Fear.some(42) -``` +To use `Fear::Option` as optional type for `Dry::Types` use the [dry-types-fear] gem. ## Testing @@ -1286,3 +1217,6 @@ provides a bunch of rspec matchers. * [maybe](https://github.com/bhb/maybe) * [ruby-possibly](https://github.com/rap1ds/ruby-possibly) * [rumonade](https://github.com/ms-ati/rumonade) + + +[dry-types-fear]: https://github.com/bolshakov/dry-types-fear diff --git a/lib/dry/types/fear.rb b/lib/dry/types/fear.rb deleted file mode 100644 index 6a0a437..0000000 --- a/lib/dry/types/fear.rb +++ /dev/null @@ -1,8 +0,0 @@ -# frozen_string_literal: true - -require "dry/types" -require "fear" - -Dry::Types.register_extension(:fear_option) do - require "dry/types/fear/option" -end diff --git a/lib/dry/types/fear/option.rb b/lib/dry/types/fear/option.rb deleted file mode 100644 index 9a9de34..0000000 --- a/lib/dry/types/fear/option.rb +++ /dev/null @@ -1,125 +0,0 @@ -# frozen_string_literal: true - -module Dry - module Types - class Option - include Type - include ::Dry::Equalizer(:type, :options, inspect: false, immutable: true) - include Decorator - include Builder - include Printable - - # @param [Fear::Option, Object] input - # - # @return [Fear::Option] - # - # @api private - def call_unsafe(input = Undefined) - case input - when ::Fear::Option - input - when Undefined - Fear.none - else - Fear.option(type.call_unsafe(input)) - end - end - - # @param [Fear::Option, Object] input - # - # @return [Fear::Option] - # - # @api private - def call_safe(input = Undefined) - case input - when ::Fear::Option - input - when Undefined - Fear.none - else - Fear.option(type.call_safe(input) { |output = input| return yield(output) }) - end - end - - # @param [Object] input - # - # @return [Result::Success] - # - # @api public - def try(input = Undefined) - result = type.try(input) - - if result.success? - Result::Success.new(Fear.option(result.input)) - else - result - end - end - - # @return [true] - # - # @api public - def default? - true - end - - # @param [Object] value - # - # @see Dry::Types::Builder#default - # - # @raise [ArgumentError] if nil provided as default value - # - # @api public - def default(value) - if value.nil? - raise ArgumentError, "nil cannot be used as a default of a maybe type" - else - super - end - end - end - - module Builder - # Turn a type into a maybe type - # - # @return [Option] - # - # @api public - def option - Option.new(Types["nil"] | self) - end - end - - # @api private - class Schema - class Key - # @api private - def option - __new__(type.option) - end - end - end - - # @api private - class Printer - MAPPING[Option] = :visit_option - - # @api private - def visit_option(maybe) - visit(maybe.type) do |type| - yield "Fear::Option<#{type}>" - end - end - end - - # Register non-coercible maybe types - NON_NIL.each_key do |name| - register("option.strict.#{name}", self[name.to_s].option) - end - - # Register coercible maybe types - COERCIBLE.each_key do |name| - register("option.coercible.#{name}", self["coercible.#{name}"].option) - end - end -end diff --git a/lib/fear.rb b/lib/fear.rb index f6dea83..9d5a2c0 100644 --- a/lib/fear.rb +++ b/lib/fear.rb @@ -2,7 +2,6 @@ require "zeitwerk" loader = Zeitwerk::Loader.for_gem -loader.ignore("#{__dir__}/dry") loader.setup module Fear diff --git a/spec/dry/types/fear/option/constrained_spec.rb b/spec/dry/types/fear/option/constrained_spec.rb deleted file mode 100644 index 4c6a2a4..0000000 --- a/spec/dry/types/fear/option/constrained_spec.rb +++ /dev/null @@ -1,22 +0,0 @@ -# frozen_string_literal: true - -require "support/dry_types" - -RSpec.describe Dry::Types::Constrained, :option do - context "with a option type" do - subject(:type) do - Dry::Types["nominal.string"].constrained(size: 4).option - end - - it_behaves_like "Dry::Types::Nominal without primitive" - - it "passes when constraints are not violated" do - expect(type[nil]).to be_none - expect(type["hell"]).to be_some_of("hell") - end - - it "raises when a given constraint is violated" do - expect { type["hel"] }.to raise_error(Dry::Types::ConstraintError, /hel/) - end - end -end diff --git a/spec/dry/types/fear/option/core_spec.rb b/spec/dry/types/fear/option/core_spec.rb deleted file mode 100644 index 743ec69..0000000 --- a/spec/dry/types/fear/option/core_spec.rb +++ /dev/null @@ -1,77 +0,0 @@ -# frozen_string_literal: true - -require "support/dry_types" - -RSpec.describe Dry::Types::Nominal, :option do - describe "with opt-in option types" do - context "with strict string" do - let(:string) { Dry::Types["option.strict.string"] } - - it_behaves_like "Dry::Types::Nominal without primitive" do - let(:type) { string } - end - - it "accepts nil" do - expect(string[nil]).to be_none - end - - it "accepts a string" do - expect(string["something"]).to be_some_of("something") - end - end - - context "with coercible string" do - let(:string) { Dry::Types["option.coercible.string"] } - - it_behaves_like "Dry::Types::Nominal without primitive" do - let(:type) { string } - end - - it "accepts nil" do - expect(string[nil]).to be_none - end - - it "accepts a string" do - expect(string[:something]).to be_some_of("something") - end - end - end - - describe "defining coercible Option String" do - let(:option_string) { Dry::Types["coercible.string"].option } - - it_behaves_like "Dry::Types::Nominal without primitive" do - let(:type) { option_string } - end - - it "accepts nil" do - expect(option_string[nil]).to be_none - end - - it "accepts an object coercible to a string" do - expect(option_string[123]).to be_some_of("123") - end - end - - describe "defining Option String" do - let(:option_string) { Dry::Types["strict.string"].option } - - it_behaves_like "Dry::Types::Nominal without primitive" do - let(:type) { option_string } - end - - it "accepts nil and returns None instance" do - value = option_string[nil] - - expect(value).to be_none - expect(value.map(&:downcase).map(&:upcase)).to be_none - end - - it "accepts a string and returns Some instance" do - value = option_string["SomeThing"] - - expect(value).to be_some_of("SomeThing") - expect(value.map(&:downcase).map(&:upcase)).to be_some_of("SOMETHING") - end - end -end diff --git a/spec/dry/types/fear/option/default_spec.rb b/spec/dry/types/fear/option/default_spec.rb deleted file mode 100644 index 32b1ed1..0000000 --- a/spec/dry/types/fear/option/default_spec.rb +++ /dev/null @@ -1,21 +0,0 @@ -# frozen_string_literal: true - -require "support/dry_types" - -RSpec.describe Dry::Types::Nominal, "#default", :option do - context "with a maybe" do - subject(:type) { Dry::Types["strict.integer"].option } - - it_behaves_like "Dry::Types::Nominal without primitive" do - let(:type) { Dry::Types["strict.integer"].option.default(0) } - end - - it "does not allow nil" do - expect { type.default(nil) }.to raise_error(ArgumentError, /nil/) - end - - it "accepts a non-nil value" do - expect(type.default(0)[0]).to be_some_of(0) - end - end -end diff --git a/spec/dry/types/fear/option/hash_spec.rb b/spec/dry/types/fear/option/hash_spec.rb deleted file mode 100644 index 3b20fec..0000000 --- a/spec/dry/types/fear/option/hash_spec.rb +++ /dev/null @@ -1,58 +0,0 @@ -# frozen_string_literal: true - -require "support/dry_types" - -RSpec.describe Dry::Types::Hash, :option do - let(:email) { Dry::Types["option.strict.string"] } - - context "Symbolized constructor" do - subject(:hash) do - Dry::Types["nominal.hash"].schema( - name: "string", - email: email, - ).with_key_transform(&:to_sym) - end - - describe "#[]" do - it "sets None as a default value for option" do - result = hash["name" => "Jane"] - - expect(result[:email]).to be_none - end - end - end - - context "Schema constructor" do - subject(:hash) do - Dry::Types["nominal.hash"].schema( - name: "string", - email: email, - ) - end - - describe "#[]" do - it "sets None as a default value for option types" do - result = hash[name: "Jane"] - - expect(result[:email]).to be_none - end - end - end - - context "Strict with defaults" do - subject(:hash) do - Dry::Types["nominal.hash"].schema( - name: "string", - email: email, - ) - end - - describe "#[]" do - it "sets None as a default value for option types" do - result = hash[name: "Jane"] - - expect(result[:email]).to be_none - end - end - end -end diff --git a/spec/dry/types/fear/option/option_spec.rb b/spec/dry/types/fear/option/option_spec.rb deleted file mode 100644 index 367012c..0000000 --- a/spec/dry/types/fear/option/option_spec.rb +++ /dev/null @@ -1,97 +0,0 @@ -# frozen_string_literal: true - -require "support/dry_types" - -RSpec.describe Dry::Types::Nominal, "#option", :option do - context "with a nominal" do - subject(:type) { Dry::Types["nominal.string"].option } - - it_behaves_like "Dry::Types::Nominal without primitive" - - it "returns None when value is nil" do - expect(type[nil]).to be_none - end - - it "returns Some when value exists" do - expect(type["hello"]).to be_some_of("hello") - end - - it "returns original if input is already a option" do - expect(type[Fear.some("hello")]).to be_some_of("hello") - end - - it "aliases #[] as #call" do - expect(type.("hello")).to be_some_of("hello") - end - - it "does not have primitive" do - expect(type).to_not respond_to(:primitive) - end - end - - context "with a strict type" do - subject(:type) { Dry::Types["strict.integer"].option } - - it_behaves_like "Dry::Types::Nominal without primitive" - - it "returns None when value is nil" do - expect(type[nil]).to be_none - end - - it "returns Some when value exists" do - expect(type[231]).to be_some_of(231) - end - end - - context "with a sum" do - subject(:type) { Dry::Types["nominal.bool"].option } - - it_behaves_like "Dry::Types::Nominal without primitive" - - it "returns None when value is nil" do - expect(type[nil]).to be_none - end - - it "returns Some when value exists" do - expect(type[true]).to be_some_of(true) - expect(type[false]).to be_some_of(false) - end - - it "does not have primitive" do - expect(type).to_not respond_to(:primitive) - end - end - - context "with keys" do - subject(:type) do - Dry::Types["hash"].schema(foo: Dry::Types["integer"]).key(:foo) - end - - it "gets wrapped by key type" do - expect(type.option).to be_a(Dry::Types::Schema::Key) - expect(type.option[nil]).to be_none - expect(type.option[1]).to be_some_of(1) - end - end - - describe "#try" do - subject(:type) { Dry::Types["coercible.integer"].option } - - it "maps successful result" do - expect(type.try("1")).to eq(Dry::Types::Result::Success.new(Fear.some(1))) - expect(type.try(nil)).to eq(Dry::Types::Result::Success.new(Fear.none)) - expect(type.try("a")).to be_a(Dry::Types::Result::Failure) - end - end - - describe "#call" do - describe "safe calls" do - subject(:type) { Dry::Types["coercible.integer"].option } - - specify do - expect(type.("a") { :fallback }).to be(:fallback) - expect(type.(Fear.some(1)) { :fallback }).to eq(Fear.some(1)) - end - end - end -end diff --git a/spec/support/.keep b/spec/support/.keep new file mode 100644 index 0000000..e69de29 diff --git a/spec/support/dry_types.rb b/spec/support/dry_types.rb deleted file mode 100644 index 7540fc1..0000000 --- a/spec/support/dry_types.rb +++ /dev/null @@ -1,6 +0,0 @@ -# frozen_string_literal: true - -require "dry/types/fear" -require "dry/types/spec/types" - -Dry::Types.load_extensions(:fear_option)