lib/mongoff/model.rb
module Mongoff
class Model
include Setup::InstanceModelParser
include MetadataAccess
include PrettyErrors
include ThreadAware
EMPTY_SCHEMA = {}.freeze
attr_reader :name
attr_reader :parent
def to_s
parent ? "#{parent}::#{name}" : name
end
def schema_name
to_s
end
def data_type_id
case @data_type_id
when Setup::DataType
@data_type_id.id
when Setup::BuildInDataType
@data_type_id.db_data_type.id
else
@data_type_id
end
end
def data_type
if @data_type_id.is_a?(Setup::DataType) || @data_type_id.is_a?(Setup::BuildInDataType)
@data_type_id
else
Setup::DataType.where(id: @data_type_id).first
end
end
def new(attributes = {})
record_class.new(self, attributes)
end
def record_class
Record
end
def observable?
@observable.nil? ? reflectable? : @observable && true
end
def reflectable?
persistable?
end
def type_polymorphic?
reflectable?
end
def persistable?
@persistable
end
def modelable?
if @modelable.nil?
@modelable = model_schema?(schema)
else
@modelable
end
end
def schema
unless @schema
if model_schema?(@schema = proto_schema)
@schema = @schema.deep_reverse_merge(Model[:base_schema] || {})
end
end
@schema
end
def model_schema?(schema)
schema = schema['items'] if schema.is_a?(Hash) && schema['type'] == 'array' && schema['items']
schema = data_type.merge_schema(schema)
schema.is_a?(Hash) && schema['type'] == 'object' && !schema['properties'].nil?
end
def property_model?(property)
property = property.to_s
schema['type'] == 'object' && schema['properties'] && (property_schema = schema['properties'][property]) && model_schema?(property_schema)
end
def properties_models
@properties_models ||= {}
end
def property_model(property)
property = property.to_s
model = nil
if schema.is_a?(Hash) && schema['type'] == 'object' && schema['properties'].is_a?(Hash) && (property_schema = schema['properties'][property])
if properties_models.key?(property)
model = properties_models[property]
else
ref, property_dt = check_referenced_schema(property_schema)
model =
if ref
if property_dt
data_type_records_model(property_dt)
else
fail "Data type reference not found: #{ref}"
end
else
property_schema = data_type.merge_schema(property_schema)
records_schema =
if property_schema['type'] == 'array' && property_schema.has_key?('items')
property_schema['items']
else
property_schema
end
Model.for(data_type: data_type, name: property.camelize, parent: self, schema: records_schema)
end
schema['properties'][property] = property_schema
properties_models[property] = model
end
end
model
end
def stored_properties_on(record)
properties = Set.new
record.document.each_key do |field|
if property?(field)
properties << field.to_s
elsif (property = property_for_attribute(field.to_s))
properties << property.to_s
elsif data_type.additional_properties?
properties << field.to_s
end
end
record.fields.each_key { |field| properties << field.to_s }
properties
end
def associations
unless @associations
@associations = {}.with_indifferent_access
properties_schemas.each do |property, property_schema|
if model.model_schema?(property_schema)
macro =
if property_schema['type'] == 'array'
property_schema['referenced'] ? :has_and_belongs_to_many : :embeds_many
else
property_schema['referenced'] ? :belongs_to : :embeds_one
end
@associations[property.to_sym] = Mongoff::Association.new(self, property, macro)
end
end
end
@associations
end
def get_associations
associations
end
def reflect_on_all_associations(*macros)
associations.values.select { |a| macros.include?(a.macro) }
end
def reflect_on_association(name)
relations[name.to_sym]
end
def for_each_association(&block)
properties_schemas.each do |property, schema|
block.yield(name: property, embedded: !schema['referenced'], many: schema['type'] == 'array') if property_model?(property)
end
end
def excluded_relation?(_relation_name)
false
end
def hereditary?
data_type.subtype?
end
def all_collections_names
persistable? ? data_type.all_data_type_collections_names : [:empty_collection]
end
def collection_name
persistable? ? data_type.data_type_collection_name.to_sym : :empty_collection
end
def count
persistable? ? collection.find.count : 0
end
def delete_all
all_collections_names.each { |name| mongo_client[name.to_sym].drop }
end
def collection
mongo_client[collection_name]
end
def mongo_client
Mongoid.default_client
end
def storage_size(scale = 1)
subtype_count = data_type.subtype? && data_type.count
data_type.all_data_type_storage_collections_names.inject(0) do |size, name|
s =
begin
stats = mongo_client.command(collstats: name.to_s, scale: scale).first
if subtype_count
subtype_count + stats['avgObjSize']
else
stats['size']
end
rescue
0
end
size + s
end
end
def unscoped
all
end
def method_missing(symbol, *args, &block)
@criteria ||= Mongoff::Criteria.new(self)
if @criteria.respond_to?(symbol)
@criteria.send(symbol, *args, &block)
else
super
end
ensure
@criteria = nil
end
def respond_to?(*args)
super || ((@criteria = Mongoff::Criteria.new(self)) && @criteria.respond_to?(args[0]))
end
def eql?(obj)
if obj.is_a?(Mongoff::Model)
to_s == obj.to_s
else
super
end
end
def submodel_of?(model)
return true if eql?(model) || (@base_model && @base_model.submodel_of?(model))
base_model =
if (base_data_type = data_type.find_data_type(data_type.schema['extends']))
Model.for(data_type: base_data_type, cache: caching?)
else
nil
end
if base_model
@base_model = base_model if caching?
base_model.submodel_of?(model)
else
false
end
end
def <=(model)
submodel_of?(model)
end
def attribute_key(field, field_metadata = {})
field_metadata[:model] ||= property_model(field)
model = field_metadata[:model]
if model&.persistable? && (schema = (field_metadata[:schema] ||= property_schema(field)))['referenced']
((schema['type'] == 'array') ? field.to_s.singularize + '_ids' : "#{field}_id").to_sym
else
field.to_s == 'id' ? :_id : field.to_sym
end
end
def property_for_attribute(name)
if property?(name)
name
else
match = name.to_s.match(/\A(.+)(_id(s)?)\Z/)
name = match && "#{match[1]}#{match[3]}"
if property?(name)
name
else
nil
end
end
end
def to_string(value)
case value
when Hash, Array
value.to_json
else
value.to_s
end
end
CONVERSION = {
BSON::ObjectId => ->(value) do
if (id = value.try(:id)).is_a?(BSON::ObjectId)
id
else
value = value.to_s
if (match = value.match(/\A\$oid#(.*\Z)/))
value = match[1]
end
BSON::ObjectId.from_string(value)
end
end,
BSON::Binary => ->(value) { BSON::Binary.new(value.to_s) },
Mongoid::Boolean => ->(value) { value.to_s.to_b },
String => ->(value) do
case value
when Array, Hash
value.to_json
else
value.to_s
end
end,
Integer => ->(value) { value.to_s.to_i },
Float => ->(value) { value.to_s.to_f },
Date => ->(value) { DateTime.parse(value.to_s) },
DateTime => ->(value) { DateTime.parse(value.to_s) },
Time => ->(value) { Time.parse(value.to_s) },
Hash => ->(value) do
unless value.is_a?(Hash) || (value = JSON.parse(value.to_s)).is_a?(Hash)
fail 'Array JSON not a Hash'
end
value
end,
Array => ->(value) do
unless value.is_a?(Array) || (value = JSON.parse(value.to_s)).is_a?(Array)
fail 'Hash JSON not an Array'
end
value
end,
NilClass => ->(value) do
if Cenit::Utility.json_object?(value)
value
else
fail 'Not a JSON value'
end
end
}
def mongo_value(value, field, schema = nil, &success_block)
field = '_id' if field.to_s == 'id'
types =
if !caching? || schema
mongo_type_for(field, schema)
else
@mongo_types[field] ||= mongo_type_for(field, schema)
end
success_value = nil
success_type = nil
conversion_value = nil
conversion_type = nil
types.each do |type|
break if success_type
if value.is_a?(type)
success_value = value
success_type = type
elsif !conversion_type
begin
conversion_value = CONVERSION[type].call(value)
conversion_type = type
rescue Exception
end
end
end
if !success_type && conversion_type
success_value = conversion_value
success_type = conversion_type
end
if success_type && success_block
args =
case success_block.arity
when 0
[]
when 1
[success_value]
else
[success_value, success_type]
end
success_block.call(*args)
end
success_value
end
class << self
def options
@options ||=
{
before_save: ->(_) {},
after_save: ->(_) {}
}
end
def [](option)
options[option]
end
def []=(option, value)
validate_option!(option, value)
options[option] = value
end
def validate_option!(option, value)
unless case option
when :before_save, :after_save
value.is_a?(Proc)
else
true
end
raise Exception.new("Invalid value #{value} for option #{option}")
end
end
def config(&block)
class_eval(&block) if block
end
def method_missing(symbol, *args)
if !symbol.to_s.end_with?('=') && ((args.length.zero? && block_given?) || args.length == 1 && !block_given?)
self[symbol] = block_given? ? yield : args[0]
elsif args.length.zero? && !block_given?
self[symbol]
else
super
end
end
def for(options)
model_name = options[:name]
cache_model = (cache_models = current_thread_cache)[model_name]
unless (data_type = (options[:data_type] || (cache_model && cache_model.data_type)))
raise Exception.new('name or data type required') unless model_name
unless (data_type = Setup::DataType.for_name(model_name.split('::').first))
raise Exception.new("unknown data type for #{model_name}")
end
end
options[:cache] = true if options[:cache].nil?
return new(data_type, options) unless options[:cache]
unless cache_model
cache_model = new(data_type, options)
cache_models[cache_model.to_s] = cache_model
end
cache_model
end
end
def labeled?
schema.key?('label')
end
def label_template
if @label_template.nil? && (template = schema['label'])
begin
@label_template = Liquid::Template.parse(template)
rescue Exception => ex
return ex.message
end
end
@label_template
end
def label(context = nil)
if parent
schema['title'] || to_s.split('::').last
else
case context
when nil
data_type.title
when :breadcrumb
data_type.custom_title('/')
else
data_type.custom_title
end
end
end
def for(options)
self.class.for(options)
end
def model_name
@model_name ||= ActiveModel::Name.new(nil, nil, name)
end
def human_attribute_name(attribute, _ = {})
attribute.to_s.titleize
end
def method_defined?(*args)
property?(args[0])
end
def relations
associations
end
protected
def initialize(data_type, options = {})
@data_type_id =
if data_type.is_a?(Setup::BuildInDataType) || options[:cache] || !data_type.persisted?
data_type
else
data_type.id.to_s
end
@name = options[:name] || data_type.data_type_name
@parent = options[:parent]
unless (@persistable = (@schema = options[:schema]).nil?)
@schema = data_type.merge_schema(@schema, root_schema: options[:root_schema])
end
@modelable = options[:modelable]
unless options[:observable].nil?
@observable = options[:observable]
end
@mongo_types = {}
end
def caching?
!@data_type_id.is_a?(String) #TODO Check this, not a BSON::Id ?
end
def proto_schema
sch = data_type.merged_schema || {}
if (properties = sch['properties'])
sch['properties'] = data_type.merge_schema(properties)
end
sch
rescue Exception => ex
{
title: 'Error',
description: "The schema data type of this model has errors: #{ex.message}"
}.stringify_keys
end
def data_type_records_model(data_type)
data_type.records_model
end
def check_referenced_schema(schema, check_for_array = true)
if schema.is_a?(Hash) && (schema = schema.reject { |key, _| %w(types contextual_params data filter group xml unique title description edi format example enum readOnly default visible referenced_by maxProperties minProperties auto export_embedded exclusive).include?(key) })
property_dt = nil
ns = data_type.namespace
if (ref = schema['$ref']).is_a?(Array)
ref = nil
elsif ref.is_a?(Hash)
(ns = ref['namespace'].to_s)
ref = ref['name']
end
((ref.is_a?(String) && (schema.size == 1 || (schema.size == 2 && schema.has_key?('referenced')))) ||
(schema['type'] == 'array' && (items = schema['items']) &&
(schema.size == 2 || (schema.size == 3 && schema.has_key?('referenced'))) &&
(items = items.reject { |key, _| %w(title description edi referenced_by).include?(key) }) &&
items.size == 1 &&
((ref = items['$ref']).is_a?(String) ||
(ref.is_a?(Hash) && (ns = ref['namespace'].to_s) && (ref = ref['name']).is_a?(String))))) &&
(property_dt = data_type.find_data_type(ref, ns))
[ref, property_dt]
else
[nil, nil]
end
end
end
end