metacosm

Code Climate GPA

Description

Metacosm is an awesome microframework for building reactive systems.

The idea is to enable quick prototyping of command-query separated or event-sourced systems.

One core concept is that we use commands to update "write-only" models, which trigger events that update "read-only" view models that are used by queries.

Models only transform their state in response to commands, so their state can be reconstructed by replaying the stream of commands.

Features

  • One interesting feature here is a sort of mock in-memory AR component called Registrable that is used for internal tests (note: this has been extracted to PassiveRecord)

Examples

A Fizzbuzz implementation contrived enough to show off many of the features of the framework.

  require 'metacosm'
  include Metacosm

  class Counter < Model
    def initialize
      @counter = 0
      super
    end

    def fizz!
      emit fizz
    end

    def buzz!
      emit buzz
    end

    def increment!(inc)
      @counter += inc
      emit(counter_incremented)
    end

    protected
    def fizz
      FizzEvent.create
    end

    def buzz
      BuzzEvent.create
    end

    def counter_incremented
      CounterIncrementedEvent.create(
        value: @counter,
        counter_id: @id
      )
    end
  end

  class CounterView < View
    attr_accessor :value, :counter_id
    def update_value(new_value)
      @value = new_value
      self
    end
  end

  class IncrementCounterCommand < Command
    attr_accessor :increment, :counter_id
  end

  class IncrementCounterCommandHandler
    def handle(increment:,counter_id:)
      counter = Counter.find(counter_id)
      counter.increment!(increment)
    end
  end

  class CounterIncrementedEvent < Event
    attr_accessor :value, :counter_id
  end

  class CounterIncrementedEventListener < EventListener
    def receive(value:,counter_id:)
      update_counter_view(counter_id, value)

      fizz_buzz!(counter_id, value)
      puts(value) unless fizz?(value) || buzz?(value)
    end

    def update_counter_view(counter_id, value)
      counter_view = CounterView.where(counter_id: counter_id).first_or_create
      counter_view.update value: value
    end

    private
    def fizz_buzz!(counter_id, n)
      fire(FizzCommand.create(counter_id: counter_id)) if fizz?(n)
      fire(BuzzCommand.create(counter_id: counter_id)) if buzz?(n)
    end

    def fizz?(n); n % 3 == 0 end
    def buzz?(n); n % 5 == 0 end
  end

  class FizzCommand < Command
    attr_accessor :counter_id
  end

  class FizzCommandHandler
    def handle(counter_id:)
      counter = Counter.find(counter_id)
      counter.fizz!
    end
  end

  class BuzzCommand < Command
    attr_accessor :counter_id
  end

  class BuzzCommandHandler
    def handle(counter_id:)
      counter = Counter.find(counter_id)
      counter.buzz!
    end
  end

  class FizzEvent < Event
  end

  class FizzEventListener < EventListener
    def receive
      puts "fizz"
    end
  end

  class BuzzEvent < Event
  end

  class BuzzEventListener < EventListener
    def receive
      puts "buzz"
    end
  end

Given all this prelude we can run a fizzbuzz "simulation":

  sim = Simulation.current
  counter_model = Counter.create
  counter_view = CounterView.find_by(counter_id: counter_model.id)

  counter_view.value # => 0

  increment_counter_command = IncrementCounterCommand.create(
    increment: 1, counter_id: counter_model.id
  )

  sim.apply(increment_counter_command)

  counter_view.value # => 1

  100.times { sim.apply(increment_counter_command) }

  sim.events.take(10)
  # => [CounterCreatedEvent (id: 1, counter_id: 1),
  #  CounterIncrementedEvent (id: 1, value: 1, counter_id: 1),
  #  CounterIncrementedEvent (id: 2, value: 2, counter_id: 1),
  #  CounterIncrementedEvent (id: 3, value: 3, counter_id: 1),
  #  FizzEvent (id: 1),
  #  CounterIncrementedEvent (id: 4, value: 4, counter_id: 1),
  #  CounterIncrementedEvent (id: 5, value: 5, counter_id: 1),
  #  BuzzEvent (id: 1),
  #  CounterIncrementedEvent (id: 6, value: 6, counter_id: 1)]

Requirements

Install

$ gem install metacosm

Synopsis

$ metacosm

Copyright (c) 2016 Joseph Weissman

See LICENSE for details.