Athena 0.10.0
This released focused on improving some QoL aspects of the framework as well as integrating the new Serializer component into Athena
.
New Annotations
ART::View allows defining some configuration related to how an action result gets serialized. This includes any serialization Groups, if nil
values should be serialized, and/or the HTTP::Status
that should be used for the response.
class ExampleController < ART::Controller
@[ART::Post("/user")]
@[ART::View(status: :created)]
def new_user(request : HTTP::Request) : User
# Logic to create/save a user, ideally would use an `ART::ParamConverterInterface`
end
end
# POST /user body: ... # => 201 Created
Previously the only way to use a custom status code was to use an ART::Response, but that made things a bit too verbose, and didn’t allow tapping into the view layer.
ART::Link and ART::Unlink brings native support for LINK
and UNLINK
endpoints. ART::Route can also be used for arbitrary HTTP
methods.
class ExampleController < ART::Controller
@[ART::Route("/some/path", method: "TRACE")]
def trace_route : Nil
# ...
end
end
Serialization Component
The most exciting feature of this release is the integration of Athena’s Serializer component into the framework. Its integration is included with Athena
itself, but is backwards compatible with JSON::Serializable
. The serializer component is similar to JSON::Serializable
, but with an expanded feature set, all controlled via annotations. Some highlights include:
- ASRA::Name - Supporting different keys when deserializing versus serializing
- ASRA::VirtualProperty - Allow a method to appear as a property upon serialization
- ASRA::IgnoreOnSerialize - Allow a property to be set on deserialization, but should not be serialized (or vice versa)
- ASRA::Expose - Allows for more granular control over which properties should be (de)serialized
The serializer component also introduces a few new concepts.
Exclusion Strategies
ASR::ExclusionStrategies::ExclusionStrategyInterface allow defining runtime logic to determine if a given property should be (de)serialized. By default two strategies are included, the Groups
one mentioned earlier and ASR::ExclusionStrategies::Version. A future iteration of this feature will allow accessing custom annotations defined on the properties. An example use case for this would be to allow for IgnoreOnUpdate
or IgnoreOnCreate
annotations that would skip a property only if the current request is a PUT
and/or POST
request. Another example could be related to ACLs, to only expose information that the current user should see based on their permissions.
struct OddNumberExclusionStrategy
include Athena::Serializer::ExclusionStrategies::ExclusionStrategyInterface
# :inherit:
#
# Skips serializing odd numbered values
def skip_property?(metadata : ASR::PropertyMetadataBase, context : ASR::Context) : Bool
# Don't skip if the value is nil
return false unless value = (metadata.value)
# Only skip on serialization, if the value is an number, and if it's odd.
context.is_a?(ASR::SerializationContext) && value.is_a?(Number) && value.odd?
end
end
serialization_context = ASR::SerializationContext.new
serialization_context.add_exclusion_strategy OddNumberExclusionStrategy.new
record Values, one : Int32 = 1, two : Int32 = 2, three : Int32 = 3 do
include ASR::Serializable
end
ASR.serializer.serialize Values.new, :json, serialization_context # => {"two":2}
Objection Construction
ASR::ObjectConstructorInterface determine how a new object is constructed during deserialization. A good use case for this would be souring an object from a database during a PUT
request. This would allow the updated values to be applied to the existing object as opposed to either needing to create a whole new object from the request data or manually handle applying those changes. For example (taken from the Blog Demo Application):
# Define a custom `ASR::ObjectConstructorInterface` to allow sourcing the model from the database
# as part of `PUT` requests, and if the type is a `Granite::Base`.
#
# Alias our service to `ASR::ObjectConstructorInterface` so ours gets injected instead.
@[ADI::Register(alias: ASR::ObjectConstructorInterface)]
class DBObjectConstructor
include Athena::Serializer::ObjectConstructorInterface
# Inject `ART::RequestStore` in order to have access to the current request.
# Also inject `ASR::InstantiateObjectConstructor` to act as our fallback constructor.
def initialize(@request_store : ART::RequestStore, @fallback_constructor : ASR::InstantiateObjectConstructor); end
# :inherit:
def construct(navigator : ASR::Navigators::DeserializationNavigatorInterface, properties : Array(ASR::PropertyMetadataBase), data : ASR::Any, type)
# Fallback on the default object constructor if the type is not a `Granite` model.
unless type <= Granite::Base
return @fallback_constructor.construct navigator, properties, data, type
end
# Fallback on the default object constructor if the current request is not a `PUT`.
unless @request_store.request.method == "PUT"
return @fallback_constructor.construct navigator, properties, data, type
end
# Lookup the object from the database; assume the object has an `id` property.
object = type.find data["id"].as_i
# Return a `404` error if no record exists with the given ID.
raise ART::Exceptions::NotFound.new "An item with the provided ID could not be found." unless object
# Apply the updated properties to the retrieved record
object.apply navigator, properties, data
# Return the object
object
end
end
# Make the compiler happy when we want to allow any Granite model to be deserializable.
class Granite::Base
include ASR::Model
end
Other Minor Improvements
- Added a
POST
endpoint example to the getting started documentation - Added a startup log message that includes the host and port the server will be listening on
As usual feel free to join me in the Athena Gitter channel if you have any suggestions, questions, or ideas. I’m also available on Discord (Blacksmoke16#0016
) or via Email.