đŸĻ Sparrow Entity

English | įŽ€äŊ“中文 | æ—ĨæœŦčĒž

Gem Version License: MIT

📖 Table of Contents


🌟 Introduction

Sparrow Entity is a lightweight Ruby gem designed to provide ORM-like entity modeling for non-database-backed data.

In modern architectures, especially microservices, applications often consume vast amounts of data from external APIs, message queues, or gRPC services rather than local databases. In these scenarios, ActiveRecord::Base cannot be used since there is no underlying database table. Sparrow perfectly fills this gap by allowing you to define structured, strongly-typed Entity objects (Plain Old Ruby Objects on steroids) with all the familiar ActiveModel conveniences.

✨ Features

  • 🛠 ORM-like Experience: Define attributes and types just like you would in ActiveRecord.
  • 🗂 Automatic Type Casting: Automatically converts assigned values to defined types (e.g., String, Integer, Date, Sparrow::Boolean).
  • âš™ī¸ Default Values: Set default fallback values for attributes when initialization is blank or invalid.
  • 🛤 ActiveModel Integration: Supports callbacks, validations, dirty tracking, and translations out of the box.
  • 🚀 Rails Generator: Instantly generate entity classes using the built-in Rails generator CLI.
  • đŸ“Ļ Serialization: Built-in support for activemodel_object_info to easily serialize entities into Hashes/JSON.

đŸ“Ļ Installation

Add this line to your application's Gemfile:

gem 'sparrow-entity', require: 'sparrow'

Then execute:

$ bundle install

Or install it globally via:

$ gem install sparrow-entity

đŸ’ģ Usage Guide

1. Basic Entity Definition

To create your own entity, simply inherit from Sparrow::Base. You can define fields with their expected Ruby types.

require 'sparrow' # Not required in Rails

#
# Represents a User entity fetched from an external User Service
#
# [Changelog]
# - 1.0.0: Initial release
#
class UserEntity < Sparrow::Base
  field :id, Integer
  field :family_name, String
  field :given_name, String
  field :birthday, Date
  field :nationality, String

  #
  # Format and return the full name of the user
  #
  # @since 1.0.0
  # @param [Hash] options formatting options
  # @option options [Symbol] :last whether to place family name last or first
  # @option options [String] :separator string used to join names
  # @return [String] the formatted full name
  #
  def full_name(options = {})
    last = options[:last] || :given
    separator = options[:separator] || ' '
    
    val = [family_name, given_name]
    val.reverse! if last == :family
    val.join(separator)
  end
end

user = UserEntity.new(id: 1, family_name: 'Doe', given_name: 'John')
puts user.full_name(last: :family) # => "John Doe"

2. Automatic Type Casting

Sparrow will attempt to coerce assigned values into the defined types automatically.

user = UserEntity.new
user.id = "42"          # Assigned as a String
user.birthday = "2023-10-01" # Assigned as a String

puts user.id.class       # => Integer (42)
puts user.birthday.class # => Date (<Date: 2023-10-01 ...>)

3. Boolean Fields

For boolean logic, Sparrow provides Sparrow::Boolean. It handles truthy and falsey values gracefully based on .present? checks or explicit boolean equivalents.

class SettingsEntity < Sparrow::Base
  field :notifications_enabled, Sparrow::Boolean
end

settings = SettingsEntity.new
puts settings.notifications_enabled.inspect # => nil

settings.notifications_enabled = 'yes'
puts settings.notifications_enabled         # => true

settings.notifications_enabled = ''
puts settings.notifications_enabled         # => false

4. Default Values

You can provide a default: option. If the assigned value is empty or invalid, it will fallback to this default value.

class TaskEntity < Sparrow::Base
  field :status, String, default: 'pending'
  field :retry_count, Integer, default: 0
end

task = TaskEntity.new
puts task.status      # => "pending"
puts task.retry_count # => 0

task.status = 'in_progress'
puts task.status      # => "in_progress"

5. ActiveModel Integrations (Validations & Callbacks)

Because Sparrow includes ActiveModel modules, you can use validations and callbacks (before_validation, after_initialize, etc.) out of the box.

class OrderEntity < Sparrow::Base
  field :order_no, String
  field :amount, Float
  field :state, String, default: 'created'

  validates :order_no, presence: true
  validates :amount, numericality: { greater_than: 0 }

  before_validation :generate_order_no, if: -> { order_no.blank? }

  private

  #
  # Generate a random order number if not present
  #
  # @since 1.0.0
  # @return [String] the generated order number
  #
  def generate_order_no
    self.order_no = "ORD-#{Time.now.to_i}"
  end
end

order = OrderEntity.new(amount: 150.50)
order.valid? # => true
puts order.order_no # => "ORD-1698765432"

6. Serialization

Sparrow leverages activemodel_object_info (v0.4.2+) to easily dump entities into Hashes or JSON formats, offering powerful contextual formatting, whitelisting/blacklisting, and custom lambdas.

class ProductEntity < Sparrow::Base
  field :sku, String
  field :price, Float
  field :stock, Integer, default: 0

  # Define default output configuration
  INSTANCE_INFO = {
    only: [:sku, :price],
    attributes: [
      :sku,
      { name: :price, type: :abstract, filter: ->(v) { "$#{format('%.2f', v)}" } }
    ]
  }.freeze

  # Define contextual output configuration
  INSTANCE_INFO_DETAIL = {
    only: [:sku, :price, :stock]
  }.freeze
end

product = ProductEntity.new(sku: 'MAC-M2-001', price: 1299.9)

# Standard attributes hash
puts product.attributes 
# => { sku: "MAC-M2-001", price: 1299.9, stock: 0 }

# 1. Uses default INSTANCE_INFO constant
puts product.instance_info
# => { sku: "MAC-M2-001", price: "$1299.90" }

# 2. Contextual output: Pass `context` to use INSTANCE_INFO_DETAIL
puts product.instance_info(context: :detail)
# => { sku: "MAC-M2-001", price: 1299.9, stock: 0 }

# 3. Override at runtime
puts product.instance_info(only: [:sku])
# => { sku: "MAC-M2-001" }

# 4. Exclude specific fields (except)
puts product.instance_info(context: :detail, except: [:price])
# => { sku: "MAC-M2-001", stock: 0 }

🚀 Rails Integration & Generator

If you are using Ruby on Rails, sparrow-entity files will be automatically required.

You can use the built-in generator to quickly scaffold entity classes:

$ rails g sparrow:entity Post::Reply

This will generate app/entities/post/reply.rb:

module Post
  class Reply < Sparrow::Base
  end
end

📝 License

The gem is available as open source under the terms of the MIT License.