Skip to content

Object Constructors

DB#

The Serializer component also has the concept of Object Constructors that determine how a new object is constructed during deserialization. A use case could be retrieving the object from the database as part of a PUT request in order to apply the deserialized data onto it. This would allow it to retain the PK, any timestamps, or ASRA::ReadOnly values.

Note

This example uses the Granite ORM, but should work with others.

# 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.
    entity = type.find data["id"].as_i64

    # 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 entity

    # Apply the updated properties to the retrieved record
    entity.apply navigator, properties, data

    # Return the entity
    entity
  end
end

This object constructor could then be used with the RequestBody param converter, assuming the type of the configuration value is Granite::Base.class. For example:

@[ADI::Register]
struct RequestBody < ART::ParamConverterInterface
  # Define a customer configuration for this converter.
  # This allows us to provide a `entity` field within the annotation
  # in order to define _what_ entity should be queried for.
  configuration entity : Granite::Base.class

  # ...
end

# Make the compiler happy when we want to allow any
# Granite entity to be deserializable via the Serializer component.
class Granite::Base
  include ASR::Model
end

class Article < Granite::Base
  connection "default"
  table "articles"

  column id : Int64, primary: true
  column title : String
end

@[ARTA::Prefix("article")]
class ExampleController < ART::Controller
  @[ARTA::Post(path: "")]
  @[ARTA::View(status: :created)]
  @[ARTA::ParamConverter("article", converter: RequestBody, entity: Article)]
  def new_article(article : Article) : Article
    # Since we have an actual `Article` instance with the updates
    # from the JSON payload already applied,
    # we can simply save and return the article.
    article.save
    article
  end
end