Skip to content

Commit

Permalink
Introduce more specialized exception types (#16)
Browse files Browse the repository at this point in the history
* Introduce more specialized exception types
* Better leverage admonition markdown extension
  • Loading branch information
Blacksmoke16 authored Apr 9, 2021
1 parent 5195221 commit 8938ce3
Show file tree
Hide file tree
Showing 17 changed files with 123 additions and 28 deletions.
2 changes: 1 addition & 1 deletion shard.yml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
name: athena-serializer

version: 0.2.6
version: 0.2.7

crystal: '>= 0.35.0'

Expand Down
25 changes: 25 additions & 0 deletions spec/serialization_context_spec.cr
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
20 changes: 15 additions & 5 deletions spec/serializer_spec.cr
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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

Expand Down Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions spec/visitors/json_deserialization_visitor_spec.cr
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions spec/visitors/yaml_deserialization_visitor_spec.cr
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
21 changes: 14 additions & 7 deletions src/annotations.cr
Original file line number Diff line number Diff line change
Expand Up @@ -228,7 +228,8 @@ module Athena::Serializer::Annotations
# ASR.serializer.deserialize Example, %({"name":"Jim","password":"password1!"}), :json # => #<Example:0x7f6eec4b6a60 @name="Jim", @password="monkey">
# ```
#
# 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`.
Expand Down Expand Up @@ -259,7 +260,8 @@ module Athena::Serializer::Annotations
# ASR.serializer.deserialize Example, %({"name":"Jim","password":"password1!"}), :json # => #<Example:0x7f6eec4b6a60 @name="Jim", @password="monkey">
# ```
#
# 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
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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
4 changes: 3 additions & 1 deletion src/athena-serializer.cr
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ require "./deserialization_context"
require "./serialization_context"

require "./construction/*"
require "./exceptions/*"
require "./exclusion_strategies/*"
require "./navigators/*"
require "./visitors/*"
Expand Down Expand Up @@ -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.
Expand Down
5 changes: 3 additions & 2 deletions src/context.cr
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
5 changes: 5 additions & 0 deletions src/exceptions/deserialization_exception.cr
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
require "./serializer_exception"

# Represents an error that occurred during deserialization.
class Athena::Serializer::Exceptions::DeserializationException < Athena::Serializer::Exceptions::SerializerException
end
12 changes: 12 additions & 0 deletions src/exceptions/missing_required_property.cr
Original file line number Diff line number Diff line change
@@ -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
12 changes: 12 additions & 0 deletions src/exceptions/nil_required_property.cr
Original file line number Diff line number Diff line change
@@ -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
12 changes: 12 additions & 0 deletions src/exceptions/property_exception.cr
Original file line number Diff line number Diff line change
@@ -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
5 changes: 5 additions & 0 deletions src/exceptions/serialization_exception.cr
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
require "./serializer_exception"

# Represents an error that occurred during serialization.
class Athena::Serializer::Exceptions::SerializationException < Athena::Serializer::Exceptions::SerializerException
end
4 changes: 4 additions & 0 deletions src/exceptions/serializer_exception.cr
Original file line number Diff line number Diff line change
@@ -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
4 changes: 2 additions & 2 deletions src/navigators/deserialization_navigator.cr
Original file line number Diff line number Diff line change
Expand Up @@ -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 %}

Expand Down
4 changes: 2 additions & 2 deletions src/serializable.cr
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
8 changes: 4 additions & 4 deletions src/visitors/deserialization_visitor.cr
Original file line number Diff line number Diff line change
Expand Up @@ -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 %}
Expand Down Expand Up @@ -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 %}

Expand All @@ -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

Expand Down Expand Up @@ -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

0 comments on commit 8938ce3

Please sign in to comment.