An ORM born from my frustration with the status quo

Last updated 4 months ago

The idea behind this ORM is it mimics Ember.js's object existance model. That is, when Ember.js expects an object to exist and it doesn't it will provide a skeleton version with the basic needed functionality.

The idea is that you start off with a class that receives the model behavior set. When and if it needs a: serializer, normalizer, sanitizer, validator, or deserializer it can come up with one on the fly if the environment hasn't defined one. We do this by taking the scope (Person) and looking for constants in that scope that match the desired naming scheme: For instance when looking for a Sanitizer we look for Person::Santizer. When looking to valiate the Name field we look for Person::Validator::NameValidator.

I believe this is powerful for two reasons:

  1. The developer can control, to a fine grain, various business logic about his models. For instance, he can allow most of his models to store to one database, but if needed he can define Person::Persistance with an enirely different persistance logic (some cloud database?).

  2. It follows the Principle of Least Work, in that no effort can still return a working solution.

I've taken these cues from both Ember.js and a few blog posts I've read over the years about progressive utility (like Progressive Enhancement, but instead of allowing for more responsive behavior if possible, you allow for more fine-grained utility).

class Person
include ORM::Model
# Take data from the outside world and parse it
# Without definition of any nature this is an empy
# class and will expect native types
class Deserializer
def inititalize(model, attributes)
@model = model
@attributes = attributes
end
def native
Hash.try_convert(@attributes)
end
end
# Fit the outside data structure to match the
# internal data structure. This will only grab
# values defined by the model via the `accessible`
# method if not personally defined.
class Normalizer
def initialize(model, attributes, context = "default")
@model = model
@attributes = attributes
@context = context
end
def normal
@model.accessibles.fetch(@context).inject({}) do |normal, accessible|
normal.update(accessible, @native.fetch(accessible))
end
end
end
# Cleans up the values of the incoming data. If
# given no definition it will simply attempt to
# coerce simplistic values based on the
# Persistance object, like Integer, String, Array,
# etc.
class Sanitizer
def initialize(model, attributes)
@model = model
@attributes = attributes
end
def clean
@attributes.inject({}) do |attributes, attribute|
# WRITE THIS
end
end
end
# Checks the validity of the incoming data. Without
# definition this class will only check for nil
# values being passed to null-not-allowed fields
# on the persistance object
class Validator
def initialize(model, attributes)
@model = model
@attributes = attributes
end
def validate
end
end
# Stores the data
class Storer
def initialize(model, attributes)
@model = model
@attributes = attributes
end
def store
end
end
class Retriever
def initialize(model, attributes)
@model = model
@attributes = attributes
end
def retrieve
end
end
# Turns the record into common formats for output
class Serializer
def initialize(model, attributes)
@model = model
@attributes = attributes
end
def to_json
end
end
end
# Simple example
class Person
include ORM::Model
# Persistance details are the only "required" part of the short way
field :name, index: true
field :age, Integer
field :description, ORM::SQL::Text, unique: true
many :pets, Pet
one :car, Car
reference :company, Company
# Want a generic serialization?
to :json do
attribute :href
attribute :id
attribute :first_name
attribute :last_name
end
# Want a generic validation?
validate :name, uniqueness: true
validate :age, Validator::Age
validate :description do |model, value|
value.truthy?
end
end