diff --git a/shard.yml b/shard.yml index 336a01c..36caa1b 100644 --- a/shard.yml +++ b/shard.yml @@ -1,6 +1,6 @@ name: athena-serializer -version: 0.2.6 +version: 0.2.7 crystal: '>= 0.35.0' diff --git a/spec/serialization_context_spec.cr b/spec/serialization_context_spec.cr index c901888..8d2c5ba 100644 --- a/spec/serialization_context_spec.cr +++ b/spec/serialization_context_spec.cr @@ -10,6 +10,31 @@ struct False end describe ASR::SerializationContext do + describe "#init" do + it "that wasn't already inited" do + context = ASR::SerializationContext.new + context.groups = {"group1"} + context.version = "1.0.0" + + context.exclusion_strategy.should be_nil + + context.init + + context.exclusion_strategy.should be_a ASR::ExclusionStrategies::Disjunct + context.exclusion_strategy.try &.as(ASR::ExclusionStrategies::Disjunct).members.size.should eq 2 + end + + it "that was already inited" do + context = ASR::SerializationContext.new + + context.init + + expect_raises ASR::Exceptions::SerializerException, "This context was already initialized, and cannot be re-used." do + context.init + end + end + end + describe "#add_exclusion_strategy" do describe "with no previous strategy" do it "should set it directly" do diff --git a/spec/serializer_spec.cr b/spec/serializer_spec.cr index c9cb549..83c45bc 100644 --- a/spec/serializer_spec.cr +++ b/spec/serializer_spec.cr @@ -76,15 +76,21 @@ describe ASR::Serializer do describe ASR::Serializable do describe NotNilableModel do it "missing" do - expect_raises Exception, "Missing required attribute: 'not_nilable'." do + ex = expect_raises ASR::Exceptions::MissingRequiredProperty, "Missing required property: 'not_nilable'." do ASR.serializer.deserialize NotNilableModel, %({}), :json end + + ex.property_name.should eq "not_nilable" + ex.property_type.should eq "String" end it nil do - expect_raises Exception, "Required property 'not_nilable_not_serializable' cannot be nil." do + ex = expect_raises ASR::Exceptions::NilRequiredProperty, "Required property 'not_nilable_not_serializable' cannot be nil." do ASR.serializer.deserialize NotNilableModel, %({"not_nilable":"FOO","not_nilable_not_serializable":null}), :json end + + ex.property_name.should eq "not_nilable_not_serializable" + ex.property_type.should eq "Unserializable" end end @@ -100,15 +106,19 @@ describe ASR::Serializer do end it "missing discriminator" do - expect_raises(Exception, "Missing discriminator field 'type'.") do + ex = expect_raises ASR::Exceptions::PropertyException, "Missing discriminator field 'type'." do ASR.serializer.deserialize Shape, %({"x":1,"y":2}), :json end + + ex.property_name.should eq "type" end it "unknown discriminator value" do - expect_raises(Exception, "Unknown 'type' discriminator value: 'triangle'.") do + ex = expect_raises(ASR::Exceptions::PropertyException, "Unknown 'type' discriminator value: 'triangle'.") do ASR.serializer.deserialize Shape, %({"x":1,"y":2,"type":"triangle"}), :json end + + ex.property_name.should eq "type" end end @@ -159,7 +169,7 @@ describe ASR::Serializer do describe "primitive" do it nil do - expect_raises Exception, "Could not parse String from 'nil'." do + expect_raises ASR::Exceptions::DeserializationException, "Could not parse String from 'nil'." do ASR.serializer.deserialize String, "null", :json end end diff --git a/spec/visitors/json_deserialization_visitor_spec.cr b/spec/visitors/json_deserialization_visitor_spec.cr index 0c30d62..1693c32 100644 --- a/spec/visitors/json_deserialization_visitor_spec.cr +++ b/spec/visitors/json_deserialization_visitor_spec.cr @@ -33,7 +33,7 @@ describe ASR::Visitors::JSONDeserializationVisitor do assert_deserialized_output ASR::Visitors::JSONDeserializationVisitor, String | Int32, "100000", 100_000 assert_deserialized_output ASR::Visitors::JSONDeserializationVisitor, String | Int32, %("Bar"), "Bar" - expect_raises(Exception, "Couldn't parse (Int32 | String) from 'false'") do + expect_raises(ASR::Exceptions::DeserializationException, "Couldn't parse (Int32 | String) from 'false'") do assert_deserialized_output ASR::Visitors::JSONDeserializationVisitor, String | Int32, "false", false end end @@ -65,7 +65,7 @@ describe ASR::Visitors::JSONDeserializationVisitor do assert_deserialized_output ASR::Visitors::JSONDeserializationVisitor, TestEnum, %("Three"), TestEnum::Three assert_deserialized_output ASR::Visitors::JSONDeserializationVisitor, TestEnum?, "1", TestEnum::One - expect_raises(Exception, "Couldn't parse (TestEnum | Nil) from 'asdf'") do + expect_raises(ASR::Exceptions::DeserializationException, "Couldn't parse (TestEnum | Nil) from 'asdf'") do assert_deserialized_output ASR::Visitors::JSONDeserializationVisitor, TestEnum?, %("asdf"), nil end end diff --git a/spec/visitors/yaml_deserialization_visitor_spec.cr b/spec/visitors/yaml_deserialization_visitor_spec.cr index a249377..da68ff4 100644 --- a/spec/visitors/yaml_deserialization_visitor_spec.cr +++ b/spec/visitors/yaml_deserialization_visitor_spec.cr @@ -33,7 +33,7 @@ describe ASR::Visitors::YAMLDeserializationVisitor do assert_deserialized_output ASR::Visitors::YAMLDeserializationVisitor, String | Int32, "100000", 100_000 assert_deserialized_output ASR::Visitors::YAMLDeserializationVisitor, String | Int32, %("Bar"), "Bar" - expect_raises(Exception, "Couldn't parse (Int32 | String) from 'false'") do + expect_raises(ASR::Exceptions::DeserializationException, "Couldn't parse (Int32 | String) from 'false'") do assert_deserialized_output ASR::Visitors::YAMLDeserializationVisitor, String | Int32, "false", false end end @@ -65,7 +65,7 @@ describe ASR::Visitors::YAMLDeserializationVisitor do assert_deserialized_output ASR::Visitors::YAMLDeserializationVisitor, TestEnum, %("Three"), TestEnum::Three assert_deserialized_output ASR::Visitors::YAMLDeserializationVisitor, TestEnum?, "1", TestEnum::One - expect_raises(Exception, "Couldn't parse (TestEnum | Nil) from 'asdf'") do + expect_raises(ASR::Exceptions::DeserializationException, "Couldn't parse (TestEnum | Nil) from 'asdf'") do assert_deserialized_output ASR::Visitors::YAMLDeserializationVisitor, TestEnum?, %("asdf"), nil end end diff --git a/src/annotations.cr b/src/annotations.cr index 187a88e..d145ac1 100644 --- a/src/annotations.cr +++ b/src/annotations.cr @@ -228,7 +228,8 @@ module Athena::Serializer::Annotations # ASR.serializer.deserialize Example, %({"name":"Jim","password":"password1!"}), :json # => # # ``` # - # NOTE: On deserialization, the excluded property must be nilable, or have a default value. + # !!!warning + # On deserialization, the excluded properties must be nilable, or have a default value. annotation Exclude; end # Defines the default exclusion policy to use on a class. Valid values: `:none`, and `:all`. @@ -259,7 +260,8 @@ module Athena::Serializer::Annotations # ASR.serializer.deserialize Example, %({"name":"Jim","password":"password1!"}), :json # => # # ``` # - # NOTE: On deserialization, the excluded property must be nilable, or have a default value. + # !!!warning + # On deserialization, the excluded properties must be nilable, or have a default value. annotation Expose; end # Defines the group(s) a property belongs to. Properties are automatically added to the `default` group @@ -498,14 +500,16 @@ module Athena::Serializer::Annotations # obj.password # => nil # ``` # - # NOTE: The property must be nilable, or have a default value. + # !!!warning + # The property must be nilable, or have a default value. annotation ReadOnly; end # Represents the first version a property was available. # # See `ASR::ExclusionStrategies::Version`. # - # NOTE: Value must be a `SemanticVersion` version. + # !!!note + # Value must be a `SemanticVersion` version. annotation Since; end # Indicates that a property should not be serialized or deserialized. @@ -558,14 +562,16 @@ module Athena::Serializer::Annotations # ASR.serializer.serialize obj, :json # => {"id":1} # ``` # - # NOTE: Can be used on any type that defines an `#empty?` method. + # !!!tip: + # Can be used on any type that defines an `#empty?` method. annotation SkipWhenEmpty; end # Represents the last version a property was available. # # See `ASR::ExclusionStrategies::Version`. # - # NOTE: Value must be a `SemanticVersion` version. + # !!!note + # Value must be a `SemanticVersion` version. annotation Until; end # Can be applied to a method to make it act like a property. @@ -595,6 +601,7 @@ module Athena::Serializer::Annotations # ASR.serializer.serialize Example.new, :json # => {"foo":"foo","testing":false,"get_val":"VAL"} # ``` # - # NOTE: The return type restriction _MUST_ be defined. + # !!!warning + # The return type restriction _MUST_ be defined. annotation VirtualProperty; end end diff --git a/src/athena-serializer.cr b/src/athena-serializer.cr index a7141d8..7ede64d 100644 --- a/src/athena-serializer.cr +++ b/src/athena-serializer.cr @@ -17,6 +17,7 @@ require "./deserialization_context" require "./serialization_context" require "./construction/*" +require "./exceptions/*" require "./exclusion_strategies/*" require "./navigators/*" require "./visitors/*" @@ -153,7 +154,8 @@ module Athena::Serializer # # Custom strategies can be implemented by via `ExclusionStrategies::ExclusionStrategyInterface`. # - # OPTIMIZE: Once feasible, support compile time exclusion strategies. + # !!!todo + # Once feasible, support compile time exclusion strategies. module Athena::Serializer::ExclusionStrategies; end # Used to denote a type that is (de)serializable. diff --git a/src/context.cr b/src/context.cr index 0a42d21..08429f8 100644 --- a/src/context.cr +++ b/src/context.cr @@ -2,7 +2,8 @@ # # Such as what serialization groups/version to use when serializing. # -# NOTE: Cannot be used for more than one action. +# !!!warning +# Cannot be used for more than one action. abstract class Athena::Serializer::Context # The possible (de)serialization actions. enum Direction @@ -43,7 +44,7 @@ abstract class Athena::Serializer::Context # :nodoc: def init : Nil - raise Exception.new "This context was already initialized, and cannot be re-used." if @initialized + raise ASR::Exceptions::SerializerException.new "This context was already initialized, and cannot be re-used." if @initialized if v = @version add_exclusion_strategy ASR::ExclusionStrategies::Version.new v diff --git a/src/exceptions/deserialization_exception.cr b/src/exceptions/deserialization_exception.cr new file mode 100644 index 0000000..61325a4 --- /dev/null +++ b/src/exceptions/deserialization_exception.cr @@ -0,0 +1,5 @@ +require "./serializer_exception" + +# Represents an error that occurred during deserialization. +class Athena::Serializer::Exceptions::DeserializationException < Athena::Serializer::Exceptions::SerializerException +end diff --git a/src/exceptions/missing_required_property.cr b/src/exceptions/missing_required_property.cr new file mode 100644 index 0000000..b84fe03 --- /dev/null +++ b/src/exceptions/missing_required_property.cr @@ -0,0 +1,12 @@ +require "./property_exception" + +# Represents an error due to a missing required property that was not included in the input data. +# +# Exposes the missing property's name and type. +class Athena::Serializer::Exceptions::MissingRequiredProperty < Athena::Serializer::Exceptions::PropertyException + getter property_type : String + + def initialize(property_name : String, @property_type : String) + super "Missing required property: '#{property_name}'.", property_name + end +end diff --git a/src/exceptions/nil_required_property.cr b/src/exceptions/nil_required_property.cr new file mode 100644 index 0000000..7400fbb --- /dev/null +++ b/src/exceptions/nil_required_property.cr @@ -0,0 +1,12 @@ +require "./property_exception" + +# Represents an error due to a required property that was `nil`. +# +# Exposes the property's name and type. +class Athena::Serializer::Exceptions::NilRequiredProperty < Athena::Serializer::Exceptions::PropertyException + getter property_type : String + + def initialize(property_name : String, @property_type : String) + super "Required property '#{property_name}' cannot be nil.", property_name + end +end diff --git a/src/exceptions/property_exception.cr b/src/exceptions/property_exception.cr new file mode 100644 index 0000000..7019ba6 --- /dev/null +++ b/src/exceptions/property_exception.cr @@ -0,0 +1,12 @@ +require "./serializer_exception" + +# Represents an error due to an invalid property. +# +# Exposes the property's name. +class Athena::Serializer::Exceptions::PropertyException < Athena::Serializer::Exceptions::DeserializationException + getter property_name : String + + def initialize(message : String, @property_name : String) + super message + end +end diff --git a/src/exceptions/serialization_exception.cr b/src/exceptions/serialization_exception.cr new file mode 100644 index 0000000..a4569ed --- /dev/null +++ b/src/exceptions/serialization_exception.cr @@ -0,0 +1,5 @@ +require "./serializer_exception" + +# Represents an error that occurred during serialization. +class Athena::Serializer::Exceptions::SerializationException < Athena::Serializer::Exceptions::SerializerException +end diff --git a/src/exceptions/serializer_exception.cr b/src/exceptions/serializer_exception.cr new file mode 100644 index 0000000..920827c --- /dev/null +++ b/src/exceptions/serializer_exception.cr @@ -0,0 +1,4 @@ +# Base Exception of the `Athena::Serializer` component. +# Can be used to rescue _all_ serializer related exceptions. +class Athena::Serializer::Exceptions::SerializerException < ::Exception +end diff --git a/src/navigators/deserialization_navigator.cr b/src/navigators/deserialization_navigator.cr index b4a5be2..73cbd9b 100644 --- a/src/navigators/deserialization_navigator.cr +++ b/src/navigators/deserialization_navigator.cr @@ -24,10 +24,10 @@ struct Athena::Serializer::Navigators::DeserializationNavigator when {{k.id.stringify}} then {{t}} {% end %} else - raise "Unknown '#{{{ann[:key]}}}' discriminator value: '#{key}'." + raise ASR::Exceptions::PropertyException.new "Unknown '#{{{ann[:key]}}}' discriminator value: '#{key}'.", {{ann[:key].id.stringify}} end else - raise "Missing discriminator field '#{{{ann[:key]}}}'." + raise ASR::Exceptions::PropertyException.new "Missing discriminator field '#{{{ann[:key]}}}'.", {{ann[:key].id.stringify}} end {% end %} diff --git a/src/serializable.cr b/src/serializable.cr index bd22add..4a5a7b8 100644 --- a/src/serializable.cr +++ b/src/serializable.cr @@ -261,12 +261,12 @@ module Athena::Serializer::Serializable @{{ivar.id}} = value else {% if !ivar.type.nilable? && !ivar.has_default_value? %} - raise Exception.new "Required property '{{ivar}}' cannot be nil." + raise ASR::Exceptions::NilRequiredProperty.new {{ivar.name.id.stringify}}, {{ivar.type.id.stringify}} {% end %} end else {% if !ivar.type.nilable? && !ivar.has_default_value? %} - raise Exception.new "Missing required attribute: '{{ivar}}'." + raise ASR::Exceptions::MissingRequiredProperty.new {{ivar.name.id.stringify}}, {{ivar.type.id.stringify}} {% end %} end diff --git a/src/visitors/deserialization_visitor.cr b/src/visitors/deserialization_visitor.cr index f7d5851..7436a6f 100644 --- a/src/visitors/deserialization_visitor.cr +++ b/src/visitors/deserialization_visitor.cr @@ -41,7 +41,7 @@ end def {{type}}.deserialize(visitor : ASR::Visitors::DeserializationVisitorInterface, data : ASR::Any) data{{method.id}} rescue ex : TypeCastError - raise "Could not parse {{type}} from '#{data.inspect}'." + raise ASR::Exceptions::DeserializationException.new "Could not parse {{type}} from '#{data.inspect}'." end {% end %} {% end %} @@ -127,7 +127,7 @@ def NamedTuple.deserialize(visitor : ASR::Visitors::DeserializationVisitorInterf {% for key, type in T %} if %var{key.id}.nil? && !{{type.nilable?}} - raise "Missing required attribute: '{{key}}'" + raise ASR::Exceptions::MissingRequiredProperty.new {{key.id.stringify}}, {{type.id.stringify}} end {% end %} @@ -146,7 +146,7 @@ def Enum.deserialize(visitor : ASR::Visitors::DeserializationVisitorInterface, d elsif val = data.as_s? parse val else - raise "Couldn't parse #{self} from '#{data}'." + raise ASR::Exceptions::DeserializationException.new "Couldn't parse #{self} from '#{data}'." end end @@ -192,5 +192,5 @@ def Union.deserialize(visitor : ASR::Visitors::DeserializationVisitorInterface, {% end %} {% end %} - raise "Couldn't parse #{self} from '#{data}'." + raise ASR::Exceptions::DeserializationException.new "Couldn't parse #{self} from '#{data}'." end