Last active
June 25, 2018 10:46
-
-
Save inopinatus/f454241886866562f7b39b9b41267a4c to your computer and use it in GitHub Desktop.
json models embedded in your activerecord models
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
class Invoice < ApplicationRecord | |
include JsonModel::Attribute | |
json_attribute_model :payment, Payment | |
validates_associated :payment | |
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
module JsonModel | |
module Inspectable | |
def attribute_for_inspect(attr_name) | |
value = send(attr_name) | |
if value.is_a?(String) && value.length > 50 | |
"#{value[0, 50]}...".inspect | |
elsif value.is_a?(Date) || value.is_a?(Time) | |
%("#{value.to_s(:iso8601)}") | |
else | |
value.inspect | |
end | |
end | |
def inspect | |
inspection = if defined?(@attributes) && @attributes | |
@attributes.keys.map do |name| | |
"#{name}: #{attribute_for_inspect(name)}" if attribute_method?(name) | |
end.compact.join(", ") | |
else | |
"not initialized" | |
end | |
"#<#{self.class} #{inspection}>" | |
end | |
end | |
module Predicates | |
extend ActiveSupport::Concern | |
class_methods do | |
def json_predicate(optype, attr, key, comparator, value) | |
opclass = "JsonModel::Predicates::JsonOperation::#{optype.to_s.camelize}".constantize | |
operation = opclass.new(arel_table[attr], Arel::Nodes.build_quoted(key)) | |
operation.send(comparator, operation.cast_value(value)) | |
end | |
private | |
module JsonOperation | |
class Object < Arel::Nodes::InfixOperation | |
def initialize(left, right) | |
super(:"->", left, right) | |
end | |
def cast_value(value) | |
Arel::Nodes::NamedFunction.new("CAST", [ | |
Arel::Nodes::As.new( | |
Arel::Nodes::Quoted.new(ActiveSupport::JSON.encode(value)), | |
Arel::Nodes::SqlLiteral.new("jsonb") | |
) | |
]) | |
end | |
end | |
class Text < Arel::Nodes::InfixOperation | |
def initialize(left, right) | |
super(:"->>", left, right) | |
end | |
def cast_value(value) | |
Arel::Nodes::Quoted.new(value) | |
end | |
end | |
end | |
end | |
end | |
class Type < ActiveModel::Type::Value | |
attr_reader :klass | |
def initialize(klass) | |
@klass = klass | |
end | |
def cast(attributes) | |
klass === attributes ? attributes : new_with_type_injection(attributes) | |
end | |
def deserialize(json) | |
new_with_type_injection(json_to_hash(json)) | |
end | |
def serialize(object) | |
ActiveSupport::JSON.encode(object_to_hash(object)) | |
end | |
def json_to_hash(json) | |
ActiveSupport::JSON.decode(json || '{}') | |
end | |
def object_to_hash(object) | |
object.respond_to?(:serializable_hash) ? object.serializable_hash : Hash(object) | |
end | |
def changed_in_place?(raw_old_json, new_object) | |
json_to_hash(raw_old_json) != json_to_hash(serialize(new_object)) | |
end | |
private | |
def new_with_type_injection(attributes) | |
klass.new_with_json_model_type(attributes: attributes, json_model_attribute_type: self) | |
end | |
end | |
module Model | |
extend ActiveSupport::Concern | |
include ActiveModel::Model | |
include ActiveModel::Attributes | |
include ActiveModel::Serializers::JSON | |
include Inspectable | |
included do | |
attr_accessor :_json_model_attribute_type | |
end | |
class_methods do | |
def new_with_json_model_type(attributes:, json_model_attribute_type:) | |
new(attributes).tap { |o| o._json_model_attribute_type = json_model_attribute_type } | |
end | |
end | |
end | |
module Attribute | |
extend ActiveSupport::Concern | |
include Predicates | |
included do | |
scope :wherein, ->(attr, key, value) { where json_predicate(:object, attr, key, :eq, value) } | |
scope :wherein_not, ->(attr, key, value) { where json_predicate(:object, attr, key, :not_eq, value) } | |
scope :wherein_null, ->(attr, key) { where json_predicate(:text, attr, key, :eq, nil) } | |
scope :wherein_not_null, ->(attr, key) { where json_predicate(:text, attr, key, :not_eq, nil) } | |
end | |
class_methods do | |
def json_attribute_model(attr, klass) | |
if (column_type = columns_hash[attr.to_s].sql_type) != "jsonb" | |
raise StandardError, | |
"Unsupported json_attribute_model type '#{column_type}' for column '#{attr}'. Only jsonb is supported." | |
end | |
attribute attr, Type.new(klass) | |
define_method :"#{attr}_attributes=" do |attrs| | |
send(attr).assign_attributes(attrs) | |
end | |
end | |
end | |
end | |
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
class Payment | |
include JsonModel::Model | |
attribute :amount, :integer | |
attribute :fees, :integer | |
attribute :amount_refunded, :integer, default: 0 | |
attribute :comment, :string | |
attribute :when, :datetime, default: -> { Time.current } | |
validates :amount, :fees, :amount_refunded, numericality: { | |
only_integer: true, | |
greater_than_or_equal_to: 0 | |
} | |
end |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Migration like
usage like