Composing Rails models, in four steps

This demo is a bulletin board and a small marketplace built with ECS Rails. Every model is an identity row composed from standard components, and every table came from one install migration. Follow the steps; each opens a live page. You are acting as Ada Lovelace, and can switch at the top.

  1. 1 · Compose

    A person is a list of parts

    No table was written for User. Each line is a component from the catalogue, stored in a table it shares with every other entity. A prefix adds a second copy under a named slot.

    class User < ApplicationEntity
      component Name
      component Email
      component Address,     prefix: :shipping
      component Address,     prefix: :billing
      component Geolocation, prefix: :shipping
      marker :moderator
      has_many :orders, via: :customer
    end

    Open Ada Lovelace's profile and fill in an address. Step 4 picks it up.

  2. 2 · Use

    Shop with the same parts

    A product reuses Text, Counter and the rest under its own slots. The market's filters and sorting query by those components.

    class Product < ApplicationEntity
      component Text,       prefix: :title
      component Money,      prefix: :price
      component Counter,    prefix: :stock
      component Rating
      relates_to :seller, Company
    end

    Open the market, add something to your basket and check out. The card is simulated: use 4242 4242 4242 4242.

  3. 3 · Inspect

    An invoice keeps copies, not links

    Checkout copies the billing address and totals onto the invoice. Its Address is the same component a person has, so a later change to the person leaves the invoice as it was.

    class Invoice < ApplicationEntity
      component Identifier, prefix: :invoice_number
      component Money,      prefix: :total
      component Address,    prefix: :billing
      relates_to :order, Order, unique: true
    end

    Open invoice INV-000001.

  4. 4 · Extend

    One system for every kind of entity

    The geocoder reads the addresses table, so it handles a seller's address and a customer's shipping address in one pass without naming either class. Declaring the pair is all an entity does to take part. No migration was needed.

    # in Demo::Geocoder: every address, whatever owns it
    Address.where.not(current.arel.exists)
    
    # in Company, beside its address
    component Geolocation

    Run the geocoder, then filter its map by entity type.

And the rest

The bulletin board has posts, comments and likes; groups use a join entity for membership; people carry moderator and admin markers. How it works explains each idea, and the source is on GitHub.