feat(orm): introduce Prism and Orm
This commit is contained in:
1 parent
1e3485dc40
commit
7532dc62a9
1 file changed
+111
@@ -0,0 +1,111 @@
|
|||||||
|
module Prism
|
||||||
|
module Orm
|
||||||
|
# Mixed into Relation/HabtmCollection so `json(Post.all)`/`render json:
|
||||||
|
# post.comments` works the same way as a single record: each element is
|
||||||
|
# serialized through Model#as_json (so hide_from_json still applies to
|
||||||
|
# every record in the collection, not just a lone one).
|
||||||
|
module CollectionSerialization
|
||||||
|
def as_json(options = {})
|
||||||
|
to_a.map { |record| record.as_json(options) }
|
||||||
|
end
|
||||||
|
|
||||||
|
def to_json(*args)
|
||||||
|
as_json.to_json(*args)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# ActiveModel::Serialization-lite: turns a record into a plain Hash
|
||||||
|
# (#serializable_hash) or a JSON string (#as_json/#to_json), Rails-style
|
||||||
|
# only:/except:/methods:/include:.
|
||||||
|
#
|
||||||
|
# Before this existed, `render json: record`/`json(record)` fell through
|
||||||
|
# to the stdlib json library's default Object#to_json (String#to_json of
|
||||||
|
# #to_s, e.g. "#<Post:0x...>") and hand-built hashes in controllers had
|
||||||
|
# no protection against leaking a raw column - notably SecurePassword's
|
||||||
|
# digest column, since Model#to_h dumps every attribute unfiltered.
|
||||||
|
# `hide_from_json` closes that: a class can mark a column excluded by
|
||||||
|
# default, and an explicit `only:` naming it is the one way to opt back
|
||||||
|
# in (a deliberate ask overrides the safety net, it does not disable it).
|
||||||
|
module Serialization
|
||||||
|
def self.included(base)
|
||||||
|
base.extend(ClassMethods)
|
||||||
|
end
|
||||||
|
|
||||||
|
module ClassMethods
|
||||||
|
# Same superclass-plus-own fallback as Callbacks#callbacks_for /
|
||||||
|
# Controller#before_actions - a plain `@ivar ||= []` per class would
|
||||||
|
# silently drop a base class's hidden columns in every subclass.
|
||||||
|
def json_hidden_attributes
|
||||||
|
inherited = superclass.respond_to?(:json_hidden_attributes) ? superclass.json_hidden_attributes : []
|
||||||
|
inherited + own_json_hidden_attributes
|
||||||
|
end
|
||||||
|
|
||||||
|
def hide_from_json(*names)
|
||||||
|
own_json_hidden_attributes.concat(names.map(&:to_s))
|
||||||
|
end
|
||||||
|
|
||||||
|
def own_json_hidden_attributes
|
||||||
|
@own_json_hidden_attributes ||= []
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def serializable_hash(only: nil, except: nil, methods: nil, include: nil)
|
||||||
|
only_names = only && Array(only).map(&:to_s)
|
||||||
|
except_names = Array(except).map(&:to_s)
|
||||||
|
hidden = self.class.json_hidden_attributes
|
||||||
|
|
||||||
|
hash = to_h.each_with_object({}) do |(key, value), acc|
|
||||||
|
allowed = only_names ? only_names.include?(key) : !hidden.include?(key) && !except_names.include?(key)
|
||||||
|
acc[key] = value if allowed
|
||||||
|
end
|
||||||
|
|
||||||
|
Array(methods).each { |name| hash[name.to_s] = send(name) }
|
||||||
|
merge_included_associations(hash, include) if include
|
||||||
|
hash
|
||||||
|
end
|
||||||
|
|
||||||
|
def as_json(options = {})
|
||||||
|
jsonify(serializable_hash(**options))
|
||||||
|
end
|
||||||
|
|
||||||
|
def to_json(*args)
|
||||||
|
as_json.to_json(*args)
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def jsonify(value)
|
||||||
|
case value
|
||||||
|
when Hash then value.transform_values { |nested| jsonify(nested) }
|
||||||
|
when Array then value.map { |nested| jsonify(nested) }
|
||||||
|
when Time, Date, DateTime then value.iso8601
|
||||||
|
else value
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def merge_included_associations(hash, include)
|
||||||
|
normalize_include(include).each do |name, nested_options|
|
||||||
|
associated = send(name)
|
||||||
|
hash[name.to_s] = serialize_association(associated, nested_options || {})
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def serialize_association(associated, nested_options)
|
||||||
|
if associated.is_a?(Model)
|
||||||
|
associated.serializable_hash(**nested_options)
|
||||||
|
elsif associated.respond_to?(:map)
|
||||||
|
associated.map { |record| record.serializable_hash(**nested_options) }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def normalize_include(include)
|
||||||
|
case include
|
||||||
|
when Symbol, String then { include.to_sym => nil }
|
||||||
|
when Array then include.each_with_object({}) { |name, acc| acc[name.to_sym] = nil }
|
||||||
|
when Hash then include
|
||||||
|
else {}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
Reference in new issue
Block a user