# lib/lutaml/model/serialize.rb require_relative "json_adapter/standard" require_relative "json_adapter/multi_json" require_relative "yaml_adapter" require_relative "xml_adapter" require_relative "toml_adapter/toml_rb_adapter" require_relative "toml_adapter/tomlib_adapter" require_relative "config" require_relative "type" require_relative "attribute" require_relative "mapping_rule" require_relative "xml_mapping" require_relative "key_value_mapping" require_relative "json_adapter" module Lutaml module Model module Serialize FORMATS = %i[xml json yaml toml].freeze def self.included(base) base.extend(ClassMethods) end # rubocop:disable Metrics/MethodLength # rubocop:disable Metrics/BlockLength # rubocop:disable Metrics/AbcSize # rubocop:disable Metrics/CyclomaticComplexity # rubocop:disable Metrics/PerceivedComplexity module ClassMethods attr_accessor :attributes, :mappings def attribute(name, type, options = {}) self.attributes ||= {} attr = Attribute.new(name, type, options) attributes[name] = attr define_method(name) do instance_variable_get(:"@#{name}") end define_method(:"#{name}=") do |value| instance_variable_set(:"@#{name}", value) end end FORMATS.each do |format| define_method(format) do |&block| self.mappings ||= {} klass = format == :xml ? XmlMapping : KeyValueMapping self.mappings[format] = klass.new self.mappings[format].instance_eval(&block) end define_method(:"from_#{format}") do |data| adapter = Lutaml::Model::Config.send(:"#{format}_adapter") doc = adapter.parse(data) mapped_attrs = apply_mappings(doc.to_h, format) apply_content_mapping(doc, mapped_attrs) if format == :xml new(mapped_attrs) end end def mappings_for(format) self.mappings[format] || default_mappings(format) end def default_mappings(format) klass = format == :xml ? XmlMapping : KeyValueMapping klass.new.tap do |mapping| attributes&.each do |name, attr| mapping.map_element(name.to_s, to: name, render_nil: attr.render_nil?) end end end def apply_mappings(doc, format) return apply_xml_mapping(doc) if format == :xml mappings = mappings_for(format).mappings mappings.each_with_object({}) do |rule, hash| attr = if rule.delegate attributes[rule.delegate].type.attributes[rule.to] else attributes[rule.to] end raise "Attribute '#{rule.to}' not found in #{self}" unless attr value = if rule.custom_methods[:from] new.send(rule.custom_methods[:from], hash, doc) elsif doc.key?(rule.name) || doc.key?(rule.name.to_sym) doc[rule.name] || doc[rule.name.to_sym] else attr.default end # if attr.collection? # value = (value || []).map do |v| # attr.type <= Serialize ? attr.type.new(v) : v # end # elsif value.is_a?(Hash) && attr.type <= Serialize # value = attr.type.new(value) # else # value = attr.type.cast(value) # end if attr.collection? value = (value || []).map do |v| attr.type <= Serialize ? attr.type.apply_mappings(v, format) : v end elsif value.is_a?(Hash) && attr.type != Lutaml::Model::Type::Hash value = attr.type.apply_mappings(value, format) end if rule.delegate hash[rule.delegate] ||= {} hash[rule.delegate][rule.to] = value else hash[rule.to] = value end end end def apply_xml_mapping(doc) mappings = mappings_for(:xml).mappings mappings.each_with_object({}) do |rule, hash| attr = attributes[rule.to] raise "Attribute '#{rule.to}' not found in #{self}" unless attr value = if rule.name doc[rule.name.to_s] || doc[rule.name.to_sym] else doc["text"] end # if attr.collection? # value = (value || []).map do |v| # attr.type <= Serialize ? attr.type.from_hash(v) : v # end # elsif value.is_a?(Hash) && attr.type <= Serialize # value = attr.type.cast(value) # elsif value.is_a?(Array) # value = attr.type.cast(value.first["text"]&.first) # end if attr.collection? value = (value || []).map do |v| if attr.type <= Serialize attr.type.apply_xml_mapping(v) else v["text"] end end elsif attr.type <= Serialize value = attr.type.apply_xml_mapping(value) if value else if value.is_a?(Hash) && attr.type != Lutaml::Model::Type::Hash value = value["text"] end value = attr.type.cast(value) end hash[rule.to] = value end end def apply_content_mapping(doc, mapped_attrs) content_mapping = mappings_for(:xml).content_mapping return unless content_mapping content = doc.root.children.select(&:text?).map(&:text) mapped_attrs[content_mapping.to] = content end end # rubocop:disable Layout/LineLength def initialize(attrs = {}) return unless self.class.attributes self.class.attributes.each do |name, attr| value = if attrs.key?(name) attrs[name] elsif attrs.key?(name.to_sym) attrs[name.to_sym] elsif attrs.key?(name.to_s) attrs[name.to_s] else attr.default end value = if attr.collection? (value || []).map do |v| if v.is_a?(Hash) attr.type.new(v) else Lutaml::Model::Type.cast( v, attr.type ) end end elsif value.is_a?(Hash) && attr.type != Lutaml::Model::Type::Hash attr.type.new(value) else Lutaml::Model::Type.cast(value, attr.type) end send(:"#{name}=", ensure_utf8(value)) end end # rubocop:enable Layout/LineLength # TODO: Make this work # FORMATS.each do |format| # define_method("to_#{format}") do |options = {}| # adapter = Lutaml::Model::Config.send("#{format}_adapter") # representation = if format == :yaml # self # else # hash_representation(format, options) # end # adapter.new(representation).send("to_#{format}", options) # end # end def to_xml(options = {}) adapter = Lutaml::Model::Config.xml_adapter adapter.new(self).to_xml(options) end def to_json(options = {}) adapter = Lutaml::Model::Config.json_adapter adapter.new(hash_representation(:json, options)).to_json(options) end def to_yaml(options = {}) adapter = Lutaml::Model::Config.yaml_adapter adapter.to_yaml(self, options) end def to_toml(options = {}) adapter = Lutaml::Model::Config.toml_adapter adapter.new(hash_representation(:toml, options)).to_toml end # TODO: END Make this work def hash_representation(format, options = {}) only = options[:only] except = options[:except] mappings = self.class.mappings_for(format).mappings mappings.each_with_object({}) do |rule, hash| name = rule.to next if except&.include?(name) || (only && !only.include?(name)) next handle_delegate(self, rule, hash) if rule.delegate value = if rule.custom_methods[:to] send(rule.custom_methods[:to], self, send(name)) else send(name) end next if value.nil? && !rule.render_nil attribute = self.class.attributes[name] hash[rule.from] = case value when Array value.map do |v| if v.is_a?(Serialize) v.hash_representation(format, options) else attribute.type.serialize(v) end end else if value.is_a?(Serialize) value.hash_representation(format, options) else attribute.type.serialize(value) end end end end private def handle_delegate(_obj, rule, hash) name = rule.to value = send(rule.delegate).send(name) return if value.nil? && !rule.render_nil attribute = send(rule.delegate).class.attributes[name] hash[rule.from] = case value when Array value.map do |v| if v.is_a?(Serialize) v.hash_representation(format, options) else attribute.type.serialize(v) end end else if value.is_a?(Serialize) value.hash_representation(format, options) else attribute.type.serialize(value) end end end 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 { |v| ensure_utf8(v) } else value end end # rubocop:enable Metrics/MethodLength # rubocop:enable Metrics/BlockLength # rubocop:enable Metrics/AbcSize # rubocop:enable Metrics/CyclomaticComplexity # rubocop:enable Metrics/PerceivedComplexity end end end