From f1b16fd6b079a3bb733cb87a08647e899f2a4f42 Mon Sep 17 00:00:00 2001 From: Max VelDink Date: Fri, 8 Mar 2024 05:39:26 -0500 Subject: [PATCH 1/4] refactor!: Made Coercer an abstract class instead of module --- lib/typed/coercion.rb | 8 ++++---- lib/typed/coercion/coercer.rb | 2 +- lib/typed/coercion/float_coercer.rb | 8 +++----- lib/typed/coercion/integer_coercer.rb | 8 +++----- lib/typed/coercion/string_coercer.rb | 8 +++----- lib/typed/coercion/struct_coercer.rb | 8 +++----- test/typed/coercion/float_coercer_test.rb | 8 ++++---- test/typed/coercion/integer_coercer_test.rb | 8 ++++---- test/typed/coercion/string_coercer_test.rb | 4 ++-- test/typed/coercion/struct_coercer_test.rb | 6 +++--- 10 files changed, 30 insertions(+), 38 deletions(-) diff --git a/lib/typed/coercion.rb b/lib/typed/coercion.rb index e75e8e2..58b67e2 100644 --- a/lib/typed/coercion.rb +++ b/lib/typed/coercion.rb @@ -11,13 +11,13 @@ module Coercion sig { type_parameters(:U).params(field: Field, value: Value).returns(Result[Value, CoercionError]) } def self.coerce(field:, value:) if field.type < T::Struct - StructCoercer.coerce(field: field, value: value) + StructCoercer.new.coerce(field: field, value: value) elsif field.type == String - StringCoercer.coerce(field: field, value: value) + StringCoercer.new.coerce(field: field, value: value) elsif field.type == Integer - IntegerCoercer.coerce(field: field, value: value) + IntegerCoercer.new.coerce(field: field, value: value) elsif field.type == Float - FloatCoercer.coerce(field: field, value: value) + FloatCoercer.new.coerce(field: field, value: value) else Failure.new(CoercionNotSupportedError.new) end diff --git a/lib/typed/coercion/coercer.rb b/lib/typed/coercion/coercer.rb index e343b4d..a83e1f3 100644 --- a/lib/typed/coercion/coercer.rb +++ b/lib/typed/coercion/coercer.rb @@ -2,7 +2,7 @@ module Typed module Coercion - module Coercer + class Coercer extend T::Sig extend T::Generic diff --git a/lib/typed/coercion/float_coercer.rb b/lib/typed/coercion/float_coercer.rb index 6385410..0c68475 100644 --- a/lib/typed/coercion/float_coercer.rb +++ b/lib/typed/coercion/float_coercer.rb @@ -2,16 +2,14 @@ module Typed module Coercion - class FloatCoercer + class FloatCoercer < Coercer extend T::Sig extend T::Generic - extend Coercer - - Target = type_template { {fixed: Float} } + Target = type_member { {fixed: Float} } sig { override.params(field: Field, value: Value).returns(Result[Target, CoercionError]) } - def self.coerce(field:, value:) + def coerce(field:, value:) Success.new(Float(value)) rescue ArgumentError, TypeError Failure.new(CoercionError.new("'#{value}' cannot be coerced into Float.")) diff --git a/lib/typed/coercion/integer_coercer.rb b/lib/typed/coercion/integer_coercer.rb index 9c3abf0..c17c647 100644 --- a/lib/typed/coercion/integer_coercer.rb +++ b/lib/typed/coercion/integer_coercer.rb @@ -2,16 +2,14 @@ module Typed module Coercion - class IntegerCoercer + class IntegerCoercer < Coercer extend T::Sig extend T::Generic - extend Coercer - - Target = type_template { {fixed: Integer} } + Target = type_member { {fixed: Integer} } sig { override.params(field: Field, value: Value).returns(Result[Target, CoercionError]) } - def self.coerce(field:, value:) + def coerce(field:, value:) Success.new(Integer(value)) rescue ArgumentError, TypeError Failure.new(CoercionError.new("'#{value}' cannot be coerced into Integer.")) diff --git a/lib/typed/coercion/string_coercer.rb b/lib/typed/coercion/string_coercer.rb index 7f6e22d..3a3d122 100644 --- a/lib/typed/coercion/string_coercer.rb +++ b/lib/typed/coercion/string_coercer.rb @@ -2,16 +2,14 @@ module Typed module Coercion - class StringCoercer + class StringCoercer < Coercer extend T::Sig extend T::Generic - extend Coercer - - Target = type_template { {fixed: String} } + Target = type_member { {fixed: String} } sig { override.params(field: Field, value: Value).returns(Result[Target, CoercionError]) } - def self.coerce(field:, value:) + def coerce(field:, value:) Success.new(String(value)) end end diff --git a/lib/typed/coercion/struct_coercer.rb b/lib/typed/coercion/struct_coercer.rb index 6650922..ce71c12 100644 --- a/lib/typed/coercion/struct_coercer.rb +++ b/lib/typed/coercion/struct_coercer.rb @@ -2,16 +2,14 @@ module Typed module Coercion - class StructCoercer + class StructCoercer < Coercer extend T::Sig extend T::Generic - extend Coercer - - Target = type_template { {fixed: T::Struct} } + Target = type_member { {fixed: T::Struct} } sig { override.params(field: Field, value: Value).returns(Result[Target, CoercionError]) } - def self.coerce(field:, value:) + def coerce(field:, value:) type = field.type return Failure.new(CoercionError.new("Field type must inherit from T::Struct for Struct coercion.")) unless type < T::Struct diff --git a/test/typed/coercion/float_coercer_test.rb b/test/typed/coercion/float_coercer_test.rb index 8283613..deda525 100644 --- a/test/typed/coercion/float_coercer_test.rb +++ b/test/typed/coercion/float_coercer_test.rb @@ -6,12 +6,12 @@ def setup end def test_when_coercable_returns_success - assert_payload(1.1, Typed::Coercion::FloatCoercer.coerce(field: @field, value: "1.1")) - assert_payload(1.0, Typed::Coercion::FloatCoercer.coerce(field: @field, value: 1)) + assert_payload(1.1, Typed::Coercion::FloatCoercer.new.coerce(field: @field, value: "1.1")) + assert_payload(1.0, Typed::Coercion::FloatCoercer.new.coerce(field: @field, value: 1)) end def test_when_not_coercable_returns_failure - assert_error(Typed::Coercion::CoercionError.new("'a' cannot be coerced into Float."), Typed::Coercion::FloatCoercer.coerce(field: @field, value: "a")) - assert_error(Typed::Coercion::CoercionError.new("'true' cannot be coerced into Float."), Typed::Coercion::FloatCoercer.coerce(field: @field, value: true)) + assert_error(Typed::Coercion::CoercionError.new("'a' cannot be coerced into Float."), Typed::Coercion::FloatCoercer.new.coerce(field: @field, value: "a")) + assert_error(Typed::Coercion::CoercionError.new("'true' cannot be coerced into Float."), Typed::Coercion::FloatCoercer.new.coerce(field: @field, value: true)) end end diff --git a/test/typed/coercion/integer_coercer_test.rb b/test/typed/coercion/integer_coercer_test.rb index bddb213..162eecf 100644 --- a/test/typed/coercion/integer_coercer_test.rb +++ b/test/typed/coercion/integer_coercer_test.rb @@ -6,12 +6,12 @@ def setup end def test_when_coercable_returns_success - assert_payload(1, Typed::Coercion::IntegerCoercer.coerce(field: @field, value: "1")) - assert_payload(1, Typed::Coercion::IntegerCoercer.coerce(field: @field, value: 1.1)) + assert_payload(1, Typed::Coercion::IntegerCoercer.new.coerce(field: @field, value: "1")) + assert_payload(1, Typed::Coercion::IntegerCoercer.new.coerce(field: @field, value: 1.1)) end def test_when_not_coercable_returns_failure - assert_error(Typed::Coercion::CoercionError.new("'a' cannot be coerced into Integer."), Typed::Coercion::IntegerCoercer.coerce(field: @field, value: "a")) - assert_error(Typed::Coercion::CoercionError.new("'true' cannot be coerced into Integer."), Typed::Coercion::IntegerCoercer.coerce(field: @field, value: true)) + assert_error(Typed::Coercion::CoercionError.new("'a' cannot be coerced into Integer."), Typed::Coercion::IntegerCoercer.new.coerce(field: @field, value: "a")) + assert_error(Typed::Coercion::CoercionError.new("'true' cannot be coerced into Integer."), Typed::Coercion::IntegerCoercer.new.coerce(field: @field, value: true)) end end diff --git a/test/typed/coercion/string_coercer_test.rb b/test/typed/coercion/string_coercer_test.rb index 428283f..ab2f000 100644 --- a/test/typed/coercion/string_coercer_test.rb +++ b/test/typed/coercion/string_coercer_test.rb @@ -4,7 +4,7 @@ class StringCoercerTest < Minitest::Test def test_returns_success field = Typed::Field.new(name: :testing, type: String) - assert_payload("1", Typed::Coercion::StringCoercer.coerce(field: field, value: 1)) - assert_payload("[1, 2]", Typed::Coercion::StringCoercer.coerce(field: field, value: [1, 2])) + assert_payload("1", Typed::Coercion::StringCoercer.new.coerce(field: field, value: 1)) + assert_payload("[1, 2]", Typed::Coercion::StringCoercer.new.coerce(field: field, value: [1, 2])) end end diff --git a/test/typed/coercion/struct_coercer_test.rb b/test/typed/coercion/struct_coercer_test.rb index 44195ed..925abf6 100644 --- a/test/typed/coercion/struct_coercer_test.rb +++ b/test/typed/coercion/struct_coercer_test.rb @@ -2,21 +2,21 @@ class StructCoercerTest < Minitest::Test def test_when_non_struct_field_given_returns_failure - result = Typed::Coercion::StructCoercer.coerce(field: Typed::Field.new(name: :testing, type: Integer), value: "testing") + result = Typed::Coercion::StructCoercer.new.coerce(field: Typed::Field.new(name: :testing, type: Integer), value: "testing") assert_failure(result) assert_error(Typed::Coercion::CoercionError.new("Field type must inherit from T::Struct for Struct coercion."), result) end def test_when_struct_can_be_coerced_returns_success - result = Typed::Coercion::StructCoercer.coerce(field: Typed::Field.new(name: :job, type: Job), value: {"title" => "Software Developer", "salary" => 90_000_00}) + result = Typed::Coercion::StructCoercer.new.coerce(field: Typed::Field.new(name: :job, type: Job), value: {"title" => "Software Developer", "salary" => 90_000_00}) assert_success(result) assert_payload(Job.new(title: "Software Developer", salary: 90_000_00), result) end def test_when_struct_cannot_be_coerced_returns_failure - result = Typed::Coercion::StructCoercer.coerce(field: Typed::Field.new(name: :job, type: Job), value: "bad") + result = Typed::Coercion::StructCoercer.new.coerce(field: Typed::Field.new(name: :job, type: Job), value: "bad") assert_failure(result) assert_error(Typed::Coercion::CoercionError.new("Value must be a Hash for Struct coercion."), result) From 21d15bb3684cba74695d80669eff7737ca0a11a0 Mon Sep 17 00:00:00 2001 From: Max VelDink Date: Fri, 8 Mar 2024 06:06:15 -0500 Subject: [PATCH 2/4] feat: Add `used_for_type?` to Coercer interface --- lib/typed/coercion/coercer.rb | 4 ++++ lib/typed/coercion/float_coercer.rb | 5 +++++ lib/typed/coercion/integer_coercer.rb | 5 +++++ lib/typed/coercion/string_coercer.rb | 5 +++++ lib/typed/coercion/struct_coercer.rb | 5 +++++ test/typed/coercion/float_coercer_test.rb | 14 ++++++++++---- test/typed/coercion/integer_coercer_test.rb | 14 ++++++++++---- test/typed/coercion/string_coercer_test.rb | 16 ++++++++++++---- test/typed/coercion/struct_coercer_test.rb | 16 +++++++++++++--- 9 files changed, 69 insertions(+), 15 deletions(-) diff --git a/lib/typed/coercion/coercer.rb b/lib/typed/coercion/coercer.rb index a83e1f3..0d50345 100644 --- a/lib/typed/coercion/coercer.rb +++ b/lib/typed/coercion/coercer.rb @@ -10,6 +10,10 @@ class Coercer Target = type_member(:out) + sig { abstract.params(type: T::Class[T.anything]).returns(T::Boolean) } + def used_for_type?(type) + end + sig { abstract.params(field: Field, value: Value).returns(Result[Target, CoercionError]) } def coerce(field:, value:) end diff --git a/lib/typed/coercion/float_coercer.rb b/lib/typed/coercion/float_coercer.rb index 0c68475..21b7168 100644 --- a/lib/typed/coercion/float_coercer.rb +++ b/lib/typed/coercion/float_coercer.rb @@ -8,6 +8,11 @@ class FloatCoercer < Coercer Target = type_member { {fixed: Float} } + sig { override.params(type: T::Class[T.anything]).returns(T::Boolean) } + def used_for_type?(type) + type == Float + end + sig { override.params(field: Field, value: Value).returns(Result[Target, CoercionError]) } def coerce(field:, value:) Success.new(Float(value)) diff --git a/lib/typed/coercion/integer_coercer.rb b/lib/typed/coercion/integer_coercer.rb index c17c647..87bdd3a 100644 --- a/lib/typed/coercion/integer_coercer.rb +++ b/lib/typed/coercion/integer_coercer.rb @@ -8,6 +8,11 @@ class IntegerCoercer < Coercer Target = type_member { {fixed: Integer} } + sig { override.params(type: T::Class[T.anything]).returns(T::Boolean) } + def used_for_type?(type) + type == Integer + end + sig { override.params(field: Field, value: Value).returns(Result[Target, CoercionError]) } def coerce(field:, value:) Success.new(Integer(value)) diff --git a/lib/typed/coercion/string_coercer.rb b/lib/typed/coercion/string_coercer.rb index 3a3d122..2b89506 100644 --- a/lib/typed/coercion/string_coercer.rb +++ b/lib/typed/coercion/string_coercer.rb @@ -8,6 +8,11 @@ class StringCoercer < Coercer Target = type_member { {fixed: String} } + sig { override.params(type: T::Class[T.anything]).returns(T::Boolean) } + def used_for_type?(type) + type == String + end + sig { override.params(field: Field, value: Value).returns(Result[Target, CoercionError]) } def coerce(field:, value:) Success.new(String(value)) diff --git a/lib/typed/coercion/struct_coercer.rb b/lib/typed/coercion/struct_coercer.rb index ce71c12..6aa246d 100644 --- a/lib/typed/coercion/struct_coercer.rb +++ b/lib/typed/coercion/struct_coercer.rb @@ -8,6 +8,11 @@ class StructCoercer < Coercer Target = type_member { {fixed: T::Struct} } + sig { override.params(type: T::Class[T.anything]).returns(T::Boolean) } + def used_for_type?(type) + !!(type < T::Struct) + end + sig { override.params(field: Field, value: Value).returns(Result[Target, CoercionError]) } def coerce(field:, value:) type = field.type diff --git a/test/typed/coercion/float_coercer_test.rb b/test/typed/coercion/float_coercer_test.rb index deda525..f263d57 100644 --- a/test/typed/coercion/float_coercer_test.rb +++ b/test/typed/coercion/float_coercer_test.rb @@ -2,16 +2,22 @@ class FloatCoercerTest < Minitest::Test def setup + @coercer = Typed::Coercion::FloatCoercer.new @field = Typed::Field.new(name: :testing, type: Float) end + def test_used_for_type_works + assert(@coercer.used_for_type?(Float)) + refute(@coercer.used_for_type?(Integer)) + end + def test_when_coercable_returns_success - assert_payload(1.1, Typed::Coercion::FloatCoercer.new.coerce(field: @field, value: "1.1")) - assert_payload(1.0, Typed::Coercion::FloatCoercer.new.coerce(field: @field, value: 1)) + assert_payload(1.1, @coercer.coerce(field: @field, value: "1.1")) + assert_payload(1.0, @coercer.coerce(field: @field, value: 1)) end def test_when_not_coercable_returns_failure - assert_error(Typed::Coercion::CoercionError.new("'a' cannot be coerced into Float."), Typed::Coercion::FloatCoercer.new.coerce(field: @field, value: "a")) - assert_error(Typed::Coercion::CoercionError.new("'true' cannot be coerced into Float."), Typed::Coercion::FloatCoercer.new.coerce(field: @field, value: true)) + assert_error(Typed::Coercion::CoercionError.new("'a' cannot be coerced into Float."), @coercer.coerce(field: @field, value: "a")) + assert_error(Typed::Coercion::CoercionError.new("'true' cannot be coerced into Float."), @coercer.coerce(field: @field, value: true)) end end diff --git a/test/typed/coercion/integer_coercer_test.rb b/test/typed/coercion/integer_coercer_test.rb index 162eecf..b2add99 100644 --- a/test/typed/coercion/integer_coercer_test.rb +++ b/test/typed/coercion/integer_coercer_test.rb @@ -2,16 +2,22 @@ class IntegerCoercerTest < Minitest::Test def setup + @coercer = Typed::Coercion::IntegerCoercer.new @field = Typed::Field.new(name: :testing, type: Integer) end + def test_used_for_type_works + assert(@coercer.used_for_type?(Integer)) + refute(@coercer.used_for_type?(Float)) + end + def test_when_coercable_returns_success - assert_payload(1, Typed::Coercion::IntegerCoercer.new.coerce(field: @field, value: "1")) - assert_payload(1, Typed::Coercion::IntegerCoercer.new.coerce(field: @field, value: 1.1)) + assert_payload(1, @coercer.coerce(field: @field, value: "1")) + assert_payload(1, @coercer.coerce(field: @field, value: 1.1)) end def test_when_not_coercable_returns_failure - assert_error(Typed::Coercion::CoercionError.new("'a' cannot be coerced into Integer."), Typed::Coercion::IntegerCoercer.new.coerce(field: @field, value: "a")) - assert_error(Typed::Coercion::CoercionError.new("'true' cannot be coerced into Integer."), Typed::Coercion::IntegerCoercer.new.coerce(field: @field, value: true)) + assert_error(Typed::Coercion::CoercionError.new("'a' cannot be coerced into Integer."), @coercer.coerce(field: @field, value: "a")) + assert_error(Typed::Coercion::CoercionError.new("'true' cannot be coerced into Integer."), @coercer.coerce(field: @field, value: true)) end end diff --git a/test/typed/coercion/string_coercer_test.rb b/test/typed/coercion/string_coercer_test.rb index ab2f000..da6e680 100644 --- a/test/typed/coercion/string_coercer_test.rb +++ b/test/typed/coercion/string_coercer_test.rb @@ -1,10 +1,18 @@ # typed: true class StringCoercerTest < Minitest::Test - def test_returns_success - field = Typed::Field.new(name: :testing, type: String) + def setup + @coercer = Typed::Coercion::StringCoercer.new + @field = Typed::Field.new(name: :testing, type: String) + end - assert_payload("1", Typed::Coercion::StringCoercer.new.coerce(field: field, value: 1)) - assert_payload("[1, 2]", Typed::Coercion::StringCoercer.new.coerce(field: field, value: [1, 2])) + def test_used_for_type_works + assert(@coercer.used_for_type?(String)) + refute(@coercer.used_for_type?(Integer)) + end + + def test_returns_success + assert_payload("1", @coercer.coerce(field: @field, value: 1)) + assert_payload("[1, 2]", @coercer.coerce(field: @field, value: [1, 2])) end end diff --git a/test/typed/coercion/struct_coercer_test.rb b/test/typed/coercion/struct_coercer_test.rb index 925abf6..828dd04 100644 --- a/test/typed/coercion/struct_coercer_test.rb +++ b/test/typed/coercion/struct_coercer_test.rb @@ -1,22 +1,32 @@ # typed: true class StructCoercerTest < Minitest::Test + def setup + @coercer = Typed::Coercion::StructCoercer.new + end + + def test_used_for_type_works + assert(@coercer.used_for_type?(Job)) + refute(@coercer.used_for_type?(T::Struct)) + refute(@coercer.used_for_type?(Integer)) + end + def test_when_non_struct_field_given_returns_failure - result = Typed::Coercion::StructCoercer.new.coerce(field: Typed::Field.new(name: :testing, type: Integer), value: "testing") + result = @coercer.coerce(field: Typed::Field.new(name: :testing, type: Integer), value: "testing") assert_failure(result) assert_error(Typed::Coercion::CoercionError.new("Field type must inherit from T::Struct for Struct coercion."), result) end def test_when_struct_can_be_coerced_returns_success - result = Typed::Coercion::StructCoercer.new.coerce(field: Typed::Field.new(name: :job, type: Job), value: {"title" => "Software Developer", "salary" => 90_000_00}) + result = @coercer.coerce(field: Typed::Field.new(name: :job, type: Job), value: {"title" => "Software Developer", "salary" => 90_000_00}) assert_success(result) assert_payload(Job.new(title: "Software Developer", salary: 90_000_00), result) end def test_when_struct_cannot_be_coerced_returns_failure - result = Typed::Coercion::StructCoercer.new.coerce(field: Typed::Field.new(name: :job, type: Job), value: "bad") + result = @coercer.coerce(field: Typed::Field.new(name: :job, type: Job), value: "bad") assert_failure(result) assert_error(Typed::Coercion::CoercionError.new("Value must be a Hash for Struct coercion."), result) From 11b95f17cfd0c311f8779675494a422b0d9ac3ff Mon Sep 17 00:00:00 2001 From: Max VelDink Date: Fri, 8 Mar 2024 06:24:57 -0500 Subject: [PATCH 3/4] feat: Add CoercerRegistry --- lib/typed/coercion/coercer_registry.rb | 28 ++++++++++++++++++++ lib/typed/coercion/float_coercer.rb | 1 - lib/typed/coercion/integer_coercer.rb | 1 - lib/typed/coercion/string_coercer.rb | 1 - lib/typed/coercion/struct_coercer.rb | 1 - test/support/simple_string_coercer.rb | 17 ++++++++++++ test/test_helper.rb | 2 +- test/typed/coercion/coercer_registry_test.rb | 13 +++++++++ 8 files changed, 59 insertions(+), 5 deletions(-) create mode 100644 lib/typed/coercion/coercer_registry.rb create mode 100644 test/support/simple_string_coercer.rb create mode 100644 test/typed/coercion/coercer_registry_test.rb diff --git a/lib/typed/coercion/coercer_registry.rb b/lib/typed/coercion/coercer_registry.rb new file mode 100644 index 0000000..88f89e0 --- /dev/null +++ b/lib/typed/coercion/coercer_registry.rb @@ -0,0 +1,28 @@ +# typed: strict + +require "singleton" + +module Typed + module Coercion + class CoercerRegistry + extend T::Sig + + include Singleton + + sig { void } + def initialize + @available = T.let([StringCoercer, IntegerCoercer, FloatCoercer, StructCoercer], T::Array[T.class_of(Coercer)]) + end + + sig { params(coercer: T.class_of(Coercer)).void } + def register(coercer) + @available.prepend(coercer) + end + + sig { params(type: T::Class[T.anything]).returns(T.nilable(T.class_of(Coercer))) } + def select_coercer_by(type:) + @available.find { |coercer| coercer.new.used_for_type?(type) } + end + end + end +end diff --git a/lib/typed/coercion/float_coercer.rb b/lib/typed/coercion/float_coercer.rb index 21b7168..526637f 100644 --- a/lib/typed/coercion/float_coercer.rb +++ b/lib/typed/coercion/float_coercer.rb @@ -3,7 +3,6 @@ module Typed module Coercion class FloatCoercer < Coercer - extend T::Sig extend T::Generic Target = type_member { {fixed: Float} } diff --git a/lib/typed/coercion/integer_coercer.rb b/lib/typed/coercion/integer_coercer.rb index 87bdd3a..6a24396 100644 --- a/lib/typed/coercion/integer_coercer.rb +++ b/lib/typed/coercion/integer_coercer.rb @@ -3,7 +3,6 @@ module Typed module Coercion class IntegerCoercer < Coercer - extend T::Sig extend T::Generic Target = type_member { {fixed: Integer} } diff --git a/lib/typed/coercion/string_coercer.rb b/lib/typed/coercion/string_coercer.rb index 2b89506..29e8b41 100644 --- a/lib/typed/coercion/string_coercer.rb +++ b/lib/typed/coercion/string_coercer.rb @@ -3,7 +3,6 @@ module Typed module Coercion class StringCoercer < Coercer - extend T::Sig extend T::Generic Target = type_member { {fixed: String} } diff --git a/lib/typed/coercion/struct_coercer.rb b/lib/typed/coercion/struct_coercer.rb index 6aa246d..9b14d9f 100644 --- a/lib/typed/coercion/struct_coercer.rb +++ b/lib/typed/coercion/struct_coercer.rb @@ -3,7 +3,6 @@ module Typed module Coercion class StructCoercer < Coercer - extend T::Sig extend T::Generic Target = type_member { {fixed: T::Struct} } diff --git a/test/support/simple_string_coercer.rb b/test/support/simple_string_coercer.rb new file mode 100644 index 0000000..8a49917 --- /dev/null +++ b/test/support/simple_string_coercer.rb @@ -0,0 +1,17 @@ +# typed: true + +class SimpleStringCoercer < Typed::Coercion::Coercer + extend T::Generic + + Target = type_member { {fixed: String} } + + sig { override.params(type: T::Class[T.anything]).returns(T::Boolean) } + def used_for_type?(type) + type == String + end + + sig { override.params(field: Typed::Field, value: Typed::Value).returns(Typed::Result[Target, Typed::Coercion::CoercionError]) } + def coerce(field:, value:) + Typed::Success.new("always this value") + end +end diff --git a/test/test_helper.rb b/test/test_helper.rb index 5be6d95..f099999 100644 --- a/test/test_helper.rb +++ b/test/test_helper.rb @@ -12,4 +12,4 @@ require "sorbet-schema" require "sorbet-schema/struct_ext" -Dir["test/support/structs/*.rb"].each { |file| require file } +Dir["test/support/**/*.rb"].each { |file| require file } diff --git a/test/typed/coercion/coercer_registry_test.rb b/test/typed/coercion/coercer_registry_test.rb new file mode 100644 index 0000000..fb98802 --- /dev/null +++ b/test/typed/coercion/coercer_registry_test.rb @@ -0,0 +1,13 @@ +# typed: true + +class CoercerRegistryTest < Minitest::Test + def test_register_prepends_coercer_so_it_overrides_built_in_ones + Typed::Coercion::CoercerRegistry.instance.register(SimpleStringCoercer) + + assert_equal(SimpleStringCoercer, Typed::Coercion::CoercerRegistry.instance.select_coercer_by(type: String)) + end + + def test_when_type_doesnt_match_coercer_returns_nil + assert_nil(Typed::Coercion::CoercerRegistry.instance.select_coercer_by(type: Array)) + end +end From 15e2ff00db241b0fbd1fba748027e5bc612474f9 Mon Sep 17 00:00:00 2001 From: Max VelDink Date: Fri, 8 Mar 2024 08:21:32 -0500 Subject: [PATCH 4/4] feat: Expose CoercerRegistry on Coercion module for consumers --- lib/typed/coercion.rb | 25 +++++++---------- lib/typed/coercion/coercer_registry.rb | 11 +++++++- test/typed/coercion/coercer_registry_test.rb | 4 +++ test/typed/coercion_test.rb | 29 ++++++-------------- 4 files changed, 33 insertions(+), 36 deletions(-) diff --git a/lib/typed/coercion.rb b/lib/typed/coercion.rb index 58b67e2..88e2209 100644 --- a/lib/typed/coercion.rb +++ b/lib/typed/coercion.rb @@ -4,23 +4,18 @@ module Typed module Coercion extend T::Sig - # TODO: We can definitely improve how we select which coercer to use - # Related issues: - # * https://github.com/maxveldink/sorbet-schema/issues/9 - # * https://github.com/maxveldink/sorbet-schema/issues/10 + sig { params(coercer: T.class_of(Coercer)).void } + def self.register_coercer(coercer) + CoercerRegistry.instance.register(coercer) + end + sig { type_parameters(:U).params(field: Field, value: Value).returns(Result[Value, CoercionError]) } def self.coerce(field:, value:) - if field.type < T::Struct - StructCoercer.new.coerce(field: field, value: value) - elsif field.type == String - StringCoercer.new.coerce(field: field, value: value) - elsif field.type == Integer - IntegerCoercer.new.coerce(field: field, value: value) - elsif field.type == Float - FloatCoercer.new.coerce(field: field, value: value) - else - Failure.new(CoercionNotSupportedError.new) - end + coercer = CoercerRegistry.instance.select_coercer_by(type: field.type) + + return Failure.new(CoercionNotSupportedError.new) unless coercer + + coercer.new.coerce(field: field, value: value) end end end diff --git a/lib/typed/coercion/coercer_registry.rb b/lib/typed/coercion/coercer_registry.rb index 88f89e0..b4a027f 100644 --- a/lib/typed/coercion/coercer_registry.rb +++ b/lib/typed/coercion/coercer_registry.rb @@ -9,9 +9,13 @@ class CoercerRegistry include Singleton + Registry = T.type_alias { T::Array[T.class_of(Coercer)] } + + DEFAULT_COERCERS = T.let([StringCoercer, IntegerCoercer, FloatCoercer, StructCoercer], Registry) + sig { void } def initialize - @available = T.let([StringCoercer, IntegerCoercer, FloatCoercer, StructCoercer], T::Array[T.class_of(Coercer)]) + @available = T.let(DEFAULT_COERCERS.clone, Registry) end sig { params(coercer: T.class_of(Coercer)).void } @@ -19,6 +23,11 @@ def register(coercer) @available.prepend(coercer) end + sig { void } + def reset! + @available = DEFAULT_COERCERS.clone + end + sig { params(type: T::Class[T.anything]).returns(T.nilable(T.class_of(Coercer))) } def select_coercer_by(type:) @available.find { |coercer| coercer.new.used_for_type?(type) } diff --git a/test/typed/coercion/coercer_registry_test.rb b/test/typed/coercion/coercer_registry_test.rb index fb98802..4a96adf 100644 --- a/test/typed/coercion/coercer_registry_test.rb +++ b/test/typed/coercion/coercer_registry_test.rb @@ -1,6 +1,10 @@ # typed: true class CoercerRegistryTest < Minitest::Test + def teardown + Typed::Coercion::CoercerRegistry.instance.reset! + end + def test_register_prepends_coercer_so_it_overrides_built_in_ones Typed::Coercion::CoercerRegistry.instance.register(SimpleStringCoercer) diff --git a/test/typed/coercion_test.rb b/test/typed/coercion_test.rb index ce3d9c3..2c7a363 100644 --- a/test/typed/coercion_test.rb +++ b/test/typed/coercion_test.rb @@ -3,35 +3,24 @@ require "date" class CoercionTest < Minitest::Test - def test_coercion_coerces_structs - result = Typed::Coercion.coerce(field: Typed::Field.new(name: :job, type: Job), value: {"title" => "Software Developer", "salary" => 90_000_00}) - - assert_success(result) - assert_payload(Job.new(title: "Software Developer", salary: 90_000_00), result) + def teardown + Typed::Coercion::CoercerRegistry.instance.reset! end - def test_coercion_coerces_strings - result = Typed::Coercion.coerce(field: Typed::Field.new(name: :name, type: String), value: 1) + def test_new_coercers_can_be_registered + Typed::Coercion.register_coercer(SimpleStringCoercer) - assert_success(result) - assert_payload("1", result) + assert_equal(SimpleStringCoercer, Typed::Coercion::CoercerRegistry.instance.select_coercer_by(type: String)) end - def test_coercion_coerces_integers - result = Typed::Coercion.coerce(field: Typed::Field.new(name: :name, type: Integer), value: "1") - - assert_success(result) - assert_payload(1, result) - end - - def test_coercion_coerces_floats - result = Typed::Coercion.coerce(field: Typed::Field.new(name: :name, type: Float), value: "1.1") + def test_when_coercer_is_matched_coerce_coerces + result = Typed::Coercion.coerce(field: Typed::Field.new(name: :name, type: String), value: 1) assert_success(result) - assert_payload(1.1, result) + assert_payload("1", result) end - def test_when_coercer_isnt_matched_returns_failure + def test_when_coercer_isnt_matched_coerce_returns_failure result = Typed::Coercion.coerce(field: Typed::Field.new(name: :testing, type: Date), value: "testing") assert_failure(result)