envato / event_sourcery

A library for building event sourced applications in Ruby
MIT License
84 stars 10 forks source link

Extensible Event Body Serialisation #128

Closed orien closed 7 years ago

orien commented 7 years ago

In an app we've started storing BigDecimal values in our event bodies. These values currently serialise via the BigDecimal#to_s method, which outputs values like "0.22345E2". We'd much prefer these values to look like "22.345". Unfortunately it's quite hard to change the way Event Sourcery serialises values without resorting to monkey patching.

This proposed change makes it easy to define an event body serialiser for a specific class of object. In our case we'd add the following to our Event Sourcery configuration:

EventSourcery.configure do |config|
  config.event_body_serializer
    .add(BigDecimal) { |decimal| decimal.to_s('F') }
end
harukizaemon commented 7 years ago

Almost certainly showing my ignorance here: How does deserialisation work given that the serialisation is on a class basis (rather than named attribute). How does it know how to turn it back in to the correct type going the other way?

orien commented 7 years ago

Great question @harukizaemon. At the minute Event Sourcery doesn't provide help with deserialisation. The various Event Sourcery systems are free to implement this as they see fit. In an app we make use of Dry Types to implement event body attribute readers. For example here is a UsageRecognised event:

    class UsageRecognized < Event

      class Body < EventBody
        attribute :item_id,           Types::Coercible::String
        attribute :customer_id,       Types::Coercible::String
        attribute :author_id,         Types::Coercible::String
        attribute :item_type_points,  Types::Form::Decimal
        attribute :occurred_at,       Types::Form::Time

        validations do
          required(:item_id).filled(:uuid?)
          required(:customer_id).filled(:uuid?)
          required(:author_id).filled(:uuid?)
          required(:item_type_points).filled(:decimal?, gt?: 0)
          required(:occurred_at).filled(:time?)
        end
      end
    end

On contemplation, this is not a very symmetrical solution to serialisation/deserialisation.

stevehodgkiss commented 7 years ago

@harukizaemon this class might be better named event body normaliser, there's no deserialisation. It was initially added to ensure we store Time as iso8601 strings in json vs the default time.to_s.

@orien @harukizaemon @matthewcoxy I'm planning to do a refactoring that moves custom event type classes to the body and leaves the top level event class just Event, so event.body would be an instance of the custom event type, then this kind of attribute specific serialisation could happen there.