app/lib/fields/fields.rb
# frozen_string_literal: true
module Fields::Fields
extend ActiveSupport::Concern
included do
validate :optional_fields_valid?, :if => :validate_fields?
# Used to manualy set source in special cases
attr_writer :fields_definitions_source
@optional_fields = []
@required_fields = []
@default_fields = []
@internal_fields = []
after_save :clear_fields_cache
include Fields::ExtraFields
prepend Fields::Fields::ActiverecordOverrides
FieldsDefinition.push_target self
end
class NoFieldDefinitionsSourceObjectDefined < StandardError; end
class NoFieldsDefinitionsSource < StandardError
include Bugsnag::MetaData
def initialize(object)
self.bugsnag_meta_data = {
object: object.as_json(root: false),
source: object.class.try(:fields_source_object)
}
end
end
module ClassMethods
# sets the model's required db-fields, it accepts params in several ways
# required_fields_are :org_name, :org_legaladdress
# required_fields_are %w{org_name org_legaladdress}
def required_fields_are(*fields)
@required_fields = fields.flatten.map(&:to_s)
end
# sets the model's optional db-fields, it accepts params in several ways
# optional_fields_are :org_name, :org_legaladdress
# optional_fields_are %w{org_name org_legaladdress}
def optional_fields_are(*fields)
@optional_fields = fields.flatten.map(&:to_s)
end
# sets the model's default db-fields, it accepts params in several ways
# default_fields_are :org_name, :org_legaladdress
# default_fields_are %w{org_name org_legaladdress}
def default_fields_are(*fields)
@default_fields = fields.flatten.map(&:to_s)
end
# sets the model's internal db-fields, it accepts params in several ways
# These fields won't be editable by providers
# internal_fields_are :org_name, :org_legaladdress
# internal_fields_are %w{org_name org_legaladdress}
def internal_fields_are(*fields)
@internal_fields = fields.flatten.map(&:to_s)
end
def set_fields_source(method)
@fields_source_object = method
end
alias set_fields_account_source set_fields_source
def required_fields
@required_fields
end
def optional_fields
@optional_fields
end
def default_fields
@default_fields
end
def internal_fields
@internal_fields
end
def fields_source_object
@fields_source_object
end
def builtin_fields
required_fields | optional_fields | internal_fields
end
def has_fields?
true
end
# these fields are here to be able to recognize fields that are not in the
# db but still can be updated and such. This is a need given the flatten
# params on user-management-api, if that gets removed this won't be needed
# e.g. password and password_confirmation in User
def special_fields
[]
end
end # ClassMethods
module ActiverecordOverrides
## fields definitions
#
# Fields definitions source has to be set before everything else
# because fields needs source to validate attributes and handle dynamic ones when assigning
def initialize(attributes = nil, *args)
@fields_definitions_source = attributes&.delete(:fields_definitions_source)
super
end
# This overrides Rails method
# it takes out all extra fields and assigns them separately
# otherwise it raises exception on unknown (extra) fields
# check http://api.rubyonrails.org/classes/ActiveRecord/AttributeAssignment.html#method-i-assign_attributes
def assign_attributes(extra_attributes, options = {})
# dup the fields because we are mutating them (params hash)
attributes = extra_attributes.present? ? extra_attributes.dup : {}
# when creating first provider there is no fields definitions source
# maybe because of factories, maybe because of sun eruptions
if fields_definitions_source_root
fields_attributes = attributes.extract!(*defined_extra_fields_names)
self.extra_fields = fields_attributes
end
super(attributes, options)
end
alias attributes= assign_attributes
def reload(*)
super
ensure
@fields_validations = nil
@defined_fields_hash = nil
@defined_fields = nil
@fields_definitions_source = nil
end
end
def required_fields
self.class.required_fields
end
def optional_fields
self.class.optional_fields
end
def builtin_fields
self.class.builtin_fields
end
def internal_fields
self.class.internal_fields
end
def special_fields
self.class.special_fields
end
# TODO: implementation of field_definitions_object, fields_definitions_source_root_object, validate_fields?
# is 3scale specific and should be extracted to separate module
# and here should be just non implemented methods
def fields_definitions_object
raise NoFieldDefinitionsSourceObjectDefined unless self.class.fields_source_object
source = self.class.fields_source_object
object = if source == :self
self
else
send(source)
end
object
end
# This methods is 3scale specific and overrides generic one
# Traverses fields definitions source to find proper account for fields definitions
#
# buyers take fields definitions from provider
# providers get their fields_definitions from master
# master always takes them from itself
#
def fields_definitions_source_root(source = fields_definitions_source)
account = source
return unless account
# traverse accounts tree to find first saved provider or master
begin
next if account.new_record?
valid = if source.buyer?
account.provider?
elsif source.provider?
account.master?
else
account.master? or account.provider?
end
return account if valid
end while account = account&.provider_account
end
def fields_definitions_source_root!
fields_definitions_source_root(fields_definitions_source!) or raise NoFieldsDefinitionsSource, self
end
# This methods is 3scale specific and overrides generic one
# fields should be validated if account is buyer
#
def validate_fields?
(fields_definitions_object || fields_definitions_set_source)&.buyer? and fields_validations?
end
# Returns previously set source via instance variable or tries to find by method set in model
# the order is intentional - we dont want to call association methods
# because they get cached before object has all attributes
#
# but when fields source is self, we can use it safely
def fields_definitions_source
if self.class.fields_source_object == :self
fields_definitions_object
else
fields_definitions_set_source or fields_definitions_object
end
end
def fields_definitions_source!
fields_definitions_source or raise NoFieldsDefinitionsSource, self
end
# Returns set fields definitions source stored in instance variablr
def fields_definitions_set_source
@fields_definitions_source
end
def defined_fields
@defined_fields ||= fields_definitions_source_root!.fields_definitions
.by_target(self.class.to_s.underscore)
end
def visible_defined_fields_for(user)
defined_fields.select { |field| field.visible_for?(user) }
end
def editable_defined_fields_for(user)
defined_fields.select { |field| field.editable_by?(user) }
end
def defined_builtin_fields
defined_fields.reject { |f| extra_field?(f.name) }
end
def defined_extra_fields
defined_fields - defined_builtin_fields
end
def defined_extra_fields_names
defined_extra_fields.map(&:name)
end
def defined_fields_hash
@defined_fields_hash ||= Hash[defined_fields.map { |f| [f.name.to_sym, f]}]
end
def clear_fields_cache
@defined_fields_hash = nil
end
## validations
#this makes optional_fields and extra_fields validations be active
def validate_fields!
@fields_validations = true
end
def fields_validations?
!!@fields_validations
end
def optional_fields_valid?
optional_fields.each do |field_name|
field = field(field_name)
next unless field&.required?
if field_value(field.name).blank?
errors.add(field_name, "can't be blank")
elsif field.choices.present? &&
field.choices.exclude?(field_value(field.name))
errors.add(field_name, "illegal value")
end
end
end
## fields accessing methods
def builtin_field?(name)
field(name) && builtin_fields.include?(name.to_s)
end
def extra_field?(name)
field(name) && builtin_fields.exclude?(name.to_s) && internal_fields.exclude?(name.to_s)
end
def internal_field?(name)
field(name) && internal_fields.include?(name.to_s)
end
def special_field?(key)
special_fields.include?(key.to_sym)
end
def field(name)
defined_fields_hash[name.to_sym]
end
def field_label(name)
field(name).try(:label) || self.class.human_attribute_name(name)
end
def field_value(name)
if extra_field?(name)
extra_fields && extra_fields[name]
elsif respond_to?(name)
value = public_send(name)
if value.is_a? ActiveRecord::Base
value.name # this should check for multiple methods like label, etc.
# or define some to_field_value method in each model
else
value
end
end
end
def fields_to_xml(xml)
defined_builtin_fields.each do |field|
next if field_value(field.name).blank?
value = field_value(field.name)
if value.respond_to?(:to_xml)
value.to_xml(builder: xml, root: field.name)
else
xml.__send__(:method_missing, field.name, value.to_s.strip)
end
end
end
end