ActiveType

repository·main·Indexed 22 days ago

https://github.com/makandra/active_type

A Rails library for creating presenter or form models that behave like ActiveRecord models with validations, callbacks, and typecasting. It supports tableless models via ActiveType::Object, virtual attributes for ActiveRecord models via ActiveType::Record, and nested attributes using nests_one and nests_many. It also provides utility methods for casting ActiveRecord objects or relations into specific classes while preserving state.

Tokens
4.3K
Snippets
17
Records
20
Agent score
78%

What's inside active_type

  1. Configure attribute defaults with procs

    main

    Attributes can have default values defined via a proc. These procs are evaluated lazily on the first read and are executed in the instance context of the object, allowing them to access other attributes.

    class SignIn < ActiveType::Object
      attribute :email, :string
      attribute :nickname, :string, default: proc { email.split('@').first }
    end
    
    SignIn.new(email: "tobias@example.org").nickname # => "tobias"
  2. Ensure virtual attributes are preserved during ActiveRecord marshalling

    main

    When using ActiveType with ActiveRecord, virtual attributes (stored in @virtual_attributes) might be lost during serialization if Rails is using its 7.1+ marshalling implementation. ActiveType automatically patches ActiveRecord::Marshalling to ensure these attributes are included in the marshal_dump and marshal_load cycle.

    This compatibility depends on ActiveRecord::Marshalling.format_version:

    • Version 6.1: Uses default Ruby implementation (via _dump/_load).
    • Version 7.1: Uses a custom Rails implementation that only dumps standard attributes. ActiveType overrides this to include @virtual_attributes.

    If you are manually managing ActiveRecord::Marshalling.format_version, ensure it matches your Rails version to allow ActiveType to apply the correct method overrides.

  3. Configure default values for virtual attributes

    main

    When defining a virtual attribute using at_attribute, you can provide a :default option.

    Important Note on Mutability: To avoid side effects where mutating an attribute changes the default for all instances of the class, you should either:

    1. Pass a frozen object.
    2. Pass a Proc (which is evaluated in the context of the instance).

    If you pass a non-frozen, non-proc object, ActiveType will emit a deprecation warning.

    # Recommended: Use a Proc for dynamic defaults
    at_attribute :preferences, :hash, default: -> { { theme: 'dark' } }
    
    # Recommended: Use a frozen object for static defaults
    at_attribute :role, :string, default: 'guest'.freeze
  4. Use dirty tracking with virtual attributes

    main

    Virtual attributes in ActiveType implement methods compatible with the ActiveModel::Dirty API. This allows you to track changes to virtual attributes just as you would with database-backed columns.

    Supported methods include:

    • #{name}_changed?: Returns true if the attribute has changed from its original value.
    • #{name}_was: Returns the original value of the attribute before it was changed.
    • #{name}_will_change!: A no-op method provided for compatibility with gems expecting this API.
    user = User.new(full_name: 'John Doe')
    user.full_name = 'Jane Doe'
    
    user.full_name_changed? # => true
    user.full_name_was     # => 'John Doe'
  5. Use ActiveType::Record[BaseClass] to extend ActiveRecord models

    main

    Inherit from ActiveType::Record[BaseClass] (where BaseClass is an ActiveRecord model) to create a specialized version of that model. This allows you to add methods, validations, callbacks, and virtual attributes that are specific to a certain context (like a sign-up flow) without polluting the main model class.

    To access the original class from the extended class, use extended_record_base_class.

    class User < ActiveRecord::Base
    end
    
    class SignUp < ActiveType::Record[User]
      validates :password, confirmation: true
      attribute :password, :string
    end
    
    SignUp.extended_record_base_class # => "User (...)"
  6. Override associations with change_association

    main

    Use the change_association macro within an ActiveType::Record to ensure that when an association is loaded, it returns instances of a specific ActiveType class instead of the standard ActiveRecord class.

    class Credential < ActiveRecord::Base
    end
    
    class User < ActiveRecord::Base
      has_many :credentials
    end
    
    class SignUpCredential < ActiveType::Record[Credential]
    end
    
    class SignUp < ActiveType::Record[User]
      change_association :credentials, class_name: 'SignUpCredential'
    end
    
    # Now, user.credentials will return SignUpCredential objects
  7. Override attribute accessors with super

    main

    You can customize how attributes are read or written by overriding the getter or setter methods and calling super to interact with the underlying ActiveType storage.

    class SignIn < ActiveType::Object
      attribute :email, :string
    
      def email
        super.downcase
      end
    end
  8. Use nested attributes with nests_one and nests_many

    main

    ActiveType provides nests_one and nests_many to support nested attributes, similar to ActiveRecord's accepts_nested_attributes_for.

    nests_many options:

    • build_scope: Proc to build new records.
    • find_scope: Proc to find existing records.
    • scope: Sets both build_scope and find_scope (defaults to the association name).
    • allow_destroy: Allows destroying records if attributes contain _destroy => '1'.
    • reject_if: A proc, symbol, or :all_blank to determine if attributes should be rejected.
    • index_errors: Boolean to get indexed errors on related records.
    • default: Initializes the association on first access via a proc.
    class Holiday < ActiveRecord::Base
      validates :date, presence: true
    end
    
    class HolidaysForm < ActiveType::Object
      nests_many :holidays, reject_if: :all_blank, default: proc { Holiday.all }
    end
    
    # Usage in controller
    form = HolidaysForm.new(params[:holidays_form])
    form.save # Validates and saves nested holidays
  9. Use ActiveType::Object for tableless models

    main

    Inherit from ActiveType::Object to create models that behave like ActiveRecord (supporting validations, callbacks, and mass-assignment) but are not backed by a database table. This is ideal for 'form models' or 'presenter models' like sign-in forms.

    Key behaviors:

    • Attributes: Define them using attribute :name, :type. Supported types include :string, :integer, :float, :decimal, :datetime, :time, :date, and :boolean.
    • Callbacks: Use before_save and after_save. Avoid before_create or before_update as there is no real database persistence.
    • Transactions: Since there is no database, #save does not open a real transaction. Wrap manual database changes in an explicit transaction if rollback capability is needed.
    • Associations: Supports belongs_to if you define the foreign key as an attribute (e.g., attribute :child_id, :integer).
    class SignIn < ActiveType::Object
      attribute :email, :string
      attribute :date_of_birth, :date
      attribute :accepted_terms, :boolean
    
      validates :email, presence: true
    end
    
    sign_in = SignIn.new(email: "user@example.com", accepted_terms: "1")
    sign_in.accepted_terms? # => true
  10. Cast ActiveRecord instances to ActiveType::Record using ActiveType.cast

    main

    Use ActiveType.cast(record, Class) to transform an existing ActiveRecord instance (or a relation) into its extended ActiveType::Record variant. This is similar to becomes but more consistent for ActiveType.

    Warning: cast is destructive. The original record and the returned record share internal state. To prevent accidental corruption, the original record will raise an error if you attempt to change or persist it after casting. Use force: true to bypass this protection if necessary.

    You can also implement an after_cast(original_record) method in your ActiveType class to run logic immediately after the casting occurs.

    user = User.find(1)
    sign_up = ActiveType.cast(user, SignUp)
    sign_up.is_a?(SignUp) # => true
    
    # Casting a relation
    users = User.where(active: true)
    sign_up_users = ActiveType.cast(users, SignUp)
  11. Use ActiveType::Record to add virtual attributes to ActiveRecord models

    main

    Inherit from ActiveType::Record when you have an existing ActiveRecord::Base model but want to declare virtual attributes that are not persisted to the database.

    class User < ActiveRecord::Base
    end
    
    class UserPresenter < ActiveType::Record[User]
      attribute :terms_accepted, :boolean
    end