module Lutaml::Model::Serialize::ClassMethods
def add_custom_handling_methods_to_model(klass)
def add_custom_handling_methods_to_model(klass) Utils.add_boolean_accessor_if_not_defined(klass, :ordered) Utils.add_boolean_accessor_if_not_defined(klass, :mixed) Utils.add_accessor_if_not_defined(klass, :element_order) Utils.add_accessor_if_not_defined(klass, :encoding) Utils.add_method_if_not_defined(klass, :using_default_for) do |attribute_name| @using_default ||= {} @using_default[attribute_name] = true end Utils.add_method_if_not_defined(klass, :value_set_for) do |attribute_name| @using_default ||= {} @using_default[attribute_name] = false end Utils.add_method_if_not_defined(klass, :using_default?) do |attribute_name| @using_default ||= {} !!@using_default[attribute_name] end end
def add_enum_getter_if_not_defined(klass, enum_name, collection)
def add_enum_getter_if_not_defined(klass, enum_name, collection) Utils.add_method_if_not_defined(klass, enum_name) do i = instance_variable_get(:"@#{enum_name}") || [] if !collection && i.is_a?(Array) i.first else i.uniq end end end
def add_enum_methods_to_model(klass, enum_name, values, collection: false)
def add_enum_methods_to_model(klass, enum_name, values, collection: false) add_enum_getter_if_not_defined(klass, enum_name, collection) add_enum_setter_if_not_defined(klass, enum_name, values, collection) return unless values.all?(::String) values.each do |value| Utils.add_method_if_not_defined(klass, "#{value}?") do curr_value = public_send(:"#{enum_name}") if collection curr_value.include?(value) else curr_value == value end end Utils.add_method_if_not_defined(klass, value.to_s) do public_send(:"#{value}?") end Utils.add_method_if_not_defined(klass, "#{value}=") do |val| value_set_for(enum_name) enum_vals = public_send(:"#{enum_name}") enum_vals = if !!val if collection enum_vals << value else [value] end elsif collection enum_vals.delete(value) enum_vals else [] end instance_variable_set(:"@#{enum_name}", enum_vals) end Utils.add_method_if_not_defined(klass, "#{value}!") do public_send(:"#{value}=", true) end end end
def add_enum_setter_if_not_defined(klass, enum_name, _values, collection)
def add_enum_setter_if_not_defined(klass, enum_name, _values, collection) Utils.add_method_if_not_defined(klass, "#{enum_name}=") do |value| value = [value] unless value.is_a?(Array) value_set_for(enum_name) if collection curr_value = public_send(:"#{enum_name}") instance_variable_set(:"@#{enum_name}", curr_value + value) else instance_variable_set(:"@#{enum_name}", value) end end end
def apply_hash_mapping(doc, instance, format, options = {})
def apply_hash_mapping(doc, instance, format, options = {}) mappings = options[:mappings] || mappings_for(format).mappings mappings.each do |rule| raise "Attribute '#{rule.to}' not found in #{self}" unless valid_rule?(rule) attr = attribute_for_rule(rule) names = rule.multiple_mappings? ? rule.name : [rule.name] value = names.collect do |rule_name| if rule.root_mapping? doc elsif rule.raw_mapping? adapter = Lutaml::Model::Config.public_send(:"#{format}_adapter") adapter.new(doc).public_send(:"to_#{format}") elsif doc.key?(rule_name.to_s) doc[rule_name.to_s] elsif doc.key?(rule_name.to_sym) doc[rule_name.to_sym] else attr&.default end end.compact.first if rule.using_custom_methods? if Utils.present?(value) value = new.send(rule.custom_methods[:from], instance, value) end next end value = translate_mappings(value, rule.hash_mappings, attr, format) value = attr.cast(value, format) unless rule.hash_mappings attr.valid_collection!(value, self) rule.deserialize(instance, value, attributes, self) end instance end
def apply_mappings(doc, format, options = {})
def apply_mappings(doc, format, options = {}) instance = options[:instance] || model.new return instance if Utils.blank?(doc) options[:mappings] = mappings_for(format).mappings return apply_xml_mapping(doc, instance, options) if format == :xml apply_hash_mapping(doc, instance, format, options) end
def apply_xml_mapping(doc, instance, options = {})
def apply_xml_mapping(doc, instance, options = {}) options = Utils.deep_dup(options) instance.encoding = options[:encoding] return instance unless doc if options[:default_namespace].nil? options[:default_namespace] = mappings_for(:xml)&.namespace_uri end mappings = options[:mappings] || mappings_for(:xml).mappings if doc.is_a?(Array) raise Lutaml::Model::CollectionTrueMissingError(self, option[:caller_class]) end if instance.respond_to?(:ordered=) && doc.is_a?(Lutaml::Model::MappingHash) instance.element_order = doc.item_order instance.ordered = mappings_for(:xml).ordered? || options[:ordered] instance.mixed = mappings_for(:xml).mixed_content? || options[:mixed_content] end if doc["attributes"]&.key?("__schema_location") instance.schema_location = Lutaml::Model::SchemaLocation.new( schema_location: doc["attributes"]["__schema_location"][:schema_location], prefix: doc["attributes"]["__schema_location"][:prefix], namespace: doc["attributes"]["__schema_location"][:namespace], ) end defaults_used = [] mappings.each do |rule| raise "Attribute '#{rule.to}' not found in #{self}" unless valid_rule?(rule) attr = attribute_for_rule(rule) value = if rule.raw_mapping? doc.node.inner_xml elsif rule.content_mapping? doc[rule.content_key] elsif val = value_for_rule(doc, rule, options) val else defaults_used << rule.to attr&.default || rule.to_value_for(instance) end value = normalize_xml_value(value, rule, attr, options) rule.deserialize(instance, value, attributes, self) end defaults_used.each do |attribute_name| instance.using_default_for(attribute_name) end instance end
def attribute(name, type, options = {})
def attribute(name, type, options = {}) attr = Attribute.new(name, type, options) attributes[name] = attr if attr.enum? add_enum_methods_to_model( model, name, options[:values], collection: options[:collection], ) else define_method(name) do instance_variable_get(:"@#{name}") end define_method(:"#{name}=") do |value| value_set_for(name) instance_variable_set(:"@#{name}", attr.cast_value(value)) end end end
def attribute_for_child(child_name, format)
def attribute_for_child(child_name, format) mapping_rule = mappings_for(format).find_by_name(child_name) attribute_for_rule(mapping_rule) if mapping_rule end
def attribute_for_rule(rule)
def attribute_for_rule(rule) return attributes[rule.to] unless rule.delegate attributes[rule.delegate].type.attributes[rule.to] end
def cast(value)
def cast(value) value end
def cast_value?(attr, rule)
def cast_value?(attr, rule) attr && !rule.raw_mapping? && !rule.content_mapping? && !rule.custom_methods[:from] end
def default_mappings(format)
def default_mappings(format) klass = format == :xml ? XmlMapping : KeyValueMapping klass.new.tap do |mapping| attributes&.each_key do |name| mapping.map_element( name.to_s, to: name, ) end mapping.root(to_s.split("::").last) if format == :xml end end
def ensure_utf8(value)
def ensure_utf8(value) case value when String value.encode("UTF-8", invalid: :replace, undef: :replace, replace: "") when Array value.map { |v| ensure_utf8(v) } when Hash value.transform_keys do |k| ensure_utf8(k) end.transform_values do |v| ensure_utf8(v) end else value end end
def enums
def enums attributes.select { |_, attr| attr.enum? } end
def generate_hash_from_child_mappings(attr, value, format, child_mappings)
def generate_hash_from_child_mappings(attr, value, format, child_mappings) return value unless child_mappings hash = {} if child_mappings.values == [:key] klass = value.first.class mappings = klass.mappings_for(format) klass.attributes.each_key do |name| next if child_mappings.key?(name.to_sym) || child_mappings.key?(name.to_s) child_mappings[name.to_sym] = mappings.find_by_to(name)&.name.to_s || name.to_s end end value.each do |child_obj| map_key = nil map_value = {} mapping_rules = attr.type.mappings_for(format) child_mappings.each do |attr_name, path| mapping_rule = mapping_rules.find_by_to(attr_name) attr_value = child_obj.send(attr_name) attr_value = if attr_value.is_a?(Lutaml::Model::Serialize) attr_value.to_yaml_hash elsif attr_value.is_a?(Array) && attr_value.first.is_a?(Lutaml::Model::Serialize) attr_value.map(&:to_yaml_hash) else attr_value end next unless mapping_rule&.render?(attr_value) if path == :key map_key = attr_value elsif path == :value map_value = attr_value else path = [path] unless path.is_a?(Array) path[0...-1].inject(map_value) do |acc, k| acc[k.to_s] ||= {} end.public_send(:[]=, path.last.to_s, attr_value) end end map_value = nil if map_value.empty? hash[map_key] = map_value end hash end
def handle_delegate(instance, rule, hash, format)
def handle_delegate(instance, rule, hash, format) name = rule.to value = instance.send(rule.delegate).send(name) return if value.nil? && !rule.render_nil attribute = instance.send(rule.delegate).class.attributes[name] rule_from_name = rule.multiple_mappings? ? rule.from.first.to_s : rule.from.to_s hash[rule_from_name] = attribute.serialize(value, format) end
def hash_representation(instance, format, options = {})
def hash_representation(instance, format, options = {}) only = options[:only] except = options[:except] mappings = mappings_for(format).mappings mappings.each_with_object({}) do |rule, hash| name = rule.to next if except&.include?(name) || (only && !only.include?(name)) next if !rule.custom_methods[:to] && (!rule.render_default? && instance.using_default?(rule.to)) next handle_delegate(instance, rule, hash, format) if rule.delegate if rule.custom_methods[:to] next instance.send(rule.custom_methods[:to], instance, hash) end value = instance.send(name) if rule.raw_mapping? adapter = Lutaml::Model::Config.send(:"#{format}_adapter") return adapter.parse(value, options) end attribute = attributes[name] next hash.merge!(generate_hash_from_child_mappings(attribute, value, format, rule.root_mappings)) if rule.root_mapping? value = if rule.child_mappings generate_hash_from_child_mappings(attribute, value, format, rule.child_mappings) else attribute.serialize(value, format, options) end next unless rule.render?(value) rule_from_name = rule.multiple_mappings? ? rule.from.first.to_s : rule.from.to_s hash[rule_from_name] = value end end
def included(base)
def included(base) base.extend(ClassMethods) base.initialize_attrs(self) end
def inherited(subclass)
def inherited(subclass) super subclass.initialize_attrs(self) end
def initialize_attrs(source_class)
def initialize_attrs(source_class) @mappings = Utils.deep_dup(source_class.instance_variable_get(:@mappings)) || {} @attributes = Utils.deep_dup(source_class.instance_variable_get(:@attributes)) || {} instance_variable_set(:@model, self) end
def key_value(&block)
def key_value(&block) Lutaml::Model::Config::KEY_VALUE_FORMATS.each do |format| mappings[format] ||= KeyValueMapping.new mappings[format].instance_eval(&block) end end
def mappings_for(format)
def mappings_for(format) mappings[format] || default_mappings(format) end
def model(klass = nil)
def model(klass = nil) if klass @model = klass add_custom_handling_methods_to_model(klass) else @model end end
def normalize_xml_value(value, rule, attr, options = {})
def normalize_xml_value(value, rule, attr, options = {}) value = [value].compact if attr&.collection? && !value.is_a?(Array) value = if value.is_a?(Array) value.map do |v| text_hash?(attr, v) ? v.text : v end elsif attr&.raw? && value value.node.children.map(&:to_xml).join elsif text_hash?(attr, value) value.text else value end return value unless cast_value?(attr, rule) options.merge(caller_class: self, mixed_content: rule.mixed_content) attr.cast( value, :xml, options, ) end
def text_hash?(attr, value)
def text_hash?(attr, value) return false unless value.is_a?(Hash) return value.one? && value.text? unless attr !(attr.type <= Serialize) && attr.type != Lutaml::Model::Type::Hash end
def translate_mappings(hash, child_mappings, attr, format)
def translate_mappings(hash, child_mappings, attr, format) return hash unless child_mappings hash.map do |key, value| child_hash = child_mappings.to_h do |attr_name, path| attr_value = if path == :key key elsif path == :value value else path = [path] unless path.is_a?(Array) value.dig(*path.map(&:to_s)) end attr_rule = attr.type.mappings_for(format).find_by_to(attr_name) [attr_rule.from.to_s, attr_value] end if child_mappings.values == [:key] && hash.values.all?(Hash) child_hash.merge!(value) end attr.type.apply_hash_mapping( child_hash, attr.type.model.new, format, { mappings: attr.type.mappings_for(format).mappings }, ) end end
def valid_rule?(rule)
def valid_rule?(rule) attribute = attribute_for_rule(rule) !!attribute || rule.custom_methods[:from] end
def value_for_rule(doc, rule, options)
def value_for_rule(doc, rule, options) rule_names = rule.namespaced_names(options[:default_namespace]) hash = rule.attribute? ? doc["attributes"] : doc["elements"] return unless hash value_key = rule_names.find { |name| hash.key_exist?(name) } hash.fetch(value_key) if value_key end