feat(orm): add Prism and Orm
This commit is contained in:
1 parent
83345a6a4a
commit
4f2807a1f6
2 files changed
+138
No files matched your search
@@ -0,0 +1,73 @@
|
||||
module Prism
|
||||
module Orm
|
||||
# ActiveModel::Attributes-lite: `attribute :featured, :boolean` casts
|
||||
# every read AND write of that column through TypeCasting, and
|
||||
# optionally seeds a default before mass-assignment runs.
|
||||
#
|
||||
# Deliberately opt-in per attribute rather than inferred from the
|
||||
# migration's declared column type (`t.boolean :featured`) - Store's
|
||||
# schema has never tracked column types past table creation
|
||||
# (TableDefinition#column discards its `type` argument), and making
|
||||
# every column on every model suddenly cast automatically would be a
|
||||
# behavior change no existing model/test asked for. Declaring it
|
||||
# explicitly is the same shape as SecurePassword's `password=` or
|
||||
# SecureToken's `has_secure_token`: a plain writer method already
|
||||
# defined before Model.load_schema! runs, so load_schema! leaves it
|
||||
# alone instead of overwriting it with a raw passthrough (see its
|
||||
# `next if method_defined?(...)` guard).
|
||||
module Attributes
|
||||
NO_DEFAULT = Object.new.freeze
|
||||
|
||||
def self.included(base)
|
||||
base.extend(ClassMethods)
|
||||
end
|
||||
|
||||
module ClassMethods
|
||||
def own_attribute_types
|
||||
@own_attribute_types ||= {}
|
||||
end
|
||||
|
||||
def own_attribute_defaults
|
||||
@own_attribute_defaults ||= {}
|
||||
end
|
||||
|
||||
# Same superclass-plus-own fallback as Callbacks#callbacks_for/
|
||||
# Controller#before_actions/Serialization#json_hidden_attributes -
|
||||
# a plain `@ivar ||= {}` per class would silently drop whatever a
|
||||
# base model class declared once a subclass declared its own.
|
||||
def attribute_types
|
||||
inherited = superclass.respond_to?(:attribute_types) ? superclass.attribute_types : {}
|
||||
inherited.merge(own_attribute_types)
|
||||
end
|
||||
|
||||
def attribute_defaults
|
||||
inherited = superclass.respond_to?(:attribute_defaults) ? superclass.attribute_defaults : {}
|
||||
inherited.merge(own_attribute_defaults)
|
||||
end
|
||||
|
||||
def attribute(name, type, default: NO_DEFAULT)
|
||||
name = name.to_s
|
||||
own_attribute_types[name] = type
|
||||
own_attribute_defaults[name] = default unless default.equal?(NO_DEFAULT)
|
||||
|
||||
define_method(name) { TypeCasting.cast(type, self[name]) }
|
||||
define_method("#{name}=") { |value| self[name] = TypeCasting.cast(type, value) }
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
# Runs before mass-assignment in Model#initialize so an explicit
|
||||
# value passed to .new/.create always wins over the default, and so
|
||||
# a default is visible to validations/before_save callbacks even
|
||||
# when the caller never mentioned the attribute at all.
|
||||
def apply_attribute_defaults
|
||||
self.class.attribute_defaults.each do |name, default|
|
||||
next unless self[name].nil?
|
||||
raw = default.is_a?(Proc) ? instance_exec(&default) : default
|
||||
self[name] = TypeCasting.cast(self.class.attribute_types[name], raw)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,65 @@
|
||||
module Prism
|
||||
module Orm
|
||||
# ActiveRecord-style lifecycle hooks: before/after_validation, _save,
|
||||
# _create, _update, _destroy. A hook is either a method name (Symbol) or
|
||||
# a block, run via `send`/`instance_exec` in registration order.
|
||||
#
|
||||
# `throw :abort` from a `before_*` hook halts the chain and makes the
|
||||
# caller (valid?/save/destroy) bail out, matching Rails 5+ semantics -
|
||||
# returning `false` from a hook does NOT halt anything, only the throw does.
|
||||
module Callbacks
|
||||
KINDS = %i[
|
||||
before_validation after_validation
|
||||
before_save after_save
|
||||
before_create after_create
|
||||
before_update after_update
|
||||
before_destroy after_destroy
|
||||
before_restore after_restore
|
||||
after_commit after_rollback
|
||||
after_touch
|
||||
].freeze
|
||||
|
||||
def self.included(base)
|
||||
base.extend(ClassMethods)
|
||||
end
|
||||
|
||||
module ClassMethods
|
||||
# `on:` restricts an after_commit/after_rollback hook to one
|
||||
# operation (:create/:update/:destroy), same idea as Rails'
|
||||
# `after_commit on: :create` - ignored (always matches) for every
|
||||
# other kind, since those already run inside a single known phase.
|
||||
KINDS.each do |kind|
|
||||
define_method(kind) do |method_name = nil, on: nil, &block|
|
||||
own_callbacks[kind] << { hook: (method_name || block), on: on }
|
||||
end
|
||||
end
|
||||
|
||||
def own_callbacks
|
||||
@own_callbacks ||= Hash.new { |hash, key| hash[key] = [] }
|
||||
end
|
||||
|
||||
# Superclass hooks run before the subclass's own, same fallback
|
||||
# pattern as Controller#before_actions - a plain `@ivar ||= []` per
|
||||
# class would silently drop everything a base model class declared.
|
||||
def callbacks_for(kind)
|
||||
inherited = superclass.respond_to?(:callbacks_for) ? superclass.callbacks_for(kind) : []
|
||||
inherited + own_callbacks[kind]
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def run_callbacks(kind, operation = nil)
|
||||
catch(:abort) {
|
||||
self.class.callbacks_for(kind).each do |entry|
|
||||
next if entry[:on] && entry[:on] != operation
|
||||
hook = entry[:hook]
|
||||
hook.is_a?(Proc) ? instance_exec(&hook) : send(hook)
|
||||
end
|
||||
return true
|
||||
}
|
||||
false
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
Reference in new issue
Block a user