require "spec_helper" require "lutaml/model" require "lutaml/model/xml_adapter/nokogiri_adapter" require "lutaml/model/xml_adapter/ox_adapter" module CDATA class Beta < Lutaml::Model::Serializable attribute :element1, :string xml do root "beta" map_content to: :element1, cdata: true end end class Alpha < Lutaml::Model::Serializable attribute :element1, :string attribute :element2, :string attribute :element3, :string attribute :beta, Beta xml do root "alpha" map_element "element1", to: :element1, cdata: false map_element "element2", to: :element2, cdata: true map_element "element3", to: :element3, cdata: false map_element "beta", to: :beta end end class Address < Lutaml::Model::Serializable attribute :street, :string attribute :city, :string attribute :house, :string attribute :address, Address xml do root "address" map_element "street", to: :street map_element "city", with: { from: :city_from_xml, to: :city_to_xml }, cdata: true map_element "house", with: { from: :house_from_xml, to: :house_to_xml }, cdata: false map_element "address", to: :address end def house_from_xml(model, node) model.house = node end def house_to_xml(model, _parent, doc) doc.create_and_add_element("house") do |element| element.add_text(element, model.house, cdata: false) end end def city_from_xml(model, node) model.city = node end def city_to_xml(model, _parent, doc) doc.create_and_add_element("city") do |element| element.add_text(element, model.city, cdata: true) end end end class CustomModelChild attr_accessor :street, :city end class CustomModelParent attr_accessor :first_name, :middle_name, :last_name, :child_mapper def name "#{first_name} #{last_name}" end end class CustomModelChildMapper < Lutaml::Model::Serializable model CustomModelChild attribute :street, Lutaml::Model::Type::String attribute :city, Lutaml::Model::Type::String xml do map_element :street, to: :street, cdata: true map_element :city, to: :city, cdata: true end end class CustomModelParentMapper < Lutaml::Model::Serializable model CustomModelParent attribute :first_name, Lutaml::Model::Type::String attribute :middle_name, Lutaml::Model::Type::String attribute :last_name, Lutaml::Model::Type::String attribute :child_mapper, CustomModelChildMapper xml do root "CustomModelParent" map_element :first_name, to: :first_name, cdata: true map_element :middle_name, to: :middle_name, cdata: true map_element :last_name, to: :last_name, cdata: false map_element :CustomModelChild, with: { to: :child_to_xml, from: :child_from_xml }, cdata: true end def child_to_xml(model, _parent, doc) doc.create_and_add_element("CustomModelChild") do |child_el| child_el.create_and_add_element("street") do |street_el| street_el.add_text(street_el, model.child_mapper.street, cdata: true) end child_el.create_and_add_element("city") do |city_el| city_el.add_text(city_el, model.child_mapper.city, cdata: true) end end end def child_from_xml(model, value) model.child_mapper ||= CustomModelChild.new model.child_mapper.street = value["street"].text model.child_mapper.city = value["city"].text end end class RootMixedContent < Lutaml::Model::Serializable attribute :id, :string attribute :bold, :string, collection: true attribute :italic, :string, collection: true attribute :underline, :string attribute :content, :string xml do root "RootMixedContent", mixed: true map_attribute :id, to: :id map_element :bold, to: :bold, cdata: true map_element :italic, to: :italic, cdata: true map_element :underline, to: :underline, cdata: true map_content to: :content, cdata: true end end class RootMixedContentNested < Lutaml::Model::Serializable attribute :id, :string attribute :data, :string attribute :content, RootMixedContent attribute :sup, :string, collection: true attribute :sub, :string, collection: true xml do root "RootMixedContentNested", mixed: true map_content to: :data, cdata: true map_attribute :id, to: :id map_element :sup, to: :sup, cdata: true map_element :sub, to: :sub, cdata: false map_element "MixedContent", to: :content end end class DefaultValue < Lutaml::Model::Serializable attribute :name, :string, default: -> { "Default Value" } attribute :temperature, :integer, default: -> { 1050 } attribute :opacity, :string, default: -> { "Opaque" } attribute :content, :string, default: -> { " " } xml do root "DefaultValue" map_element "name", to: :name, render_default: true, cdata: true map_element "temperature", to: :temperature, render_default: true, cdata: true map_element "opacity", to: :opacity, cdata: false, render_default: true map_content to: :content, cdata: true, render_default: true end end end RSpec.describe "CDATA" do let(:parent_mapper) { CDATA::CustomModelParentMapper } let(:child_mapper) { CDATA::CustomModelChildMapper } let(:parent_model) { CDATA::CustomModelParent } let(:child_model) { CDATA::CustomModelChild } shared_examples "cdata behavior" do |adapter_class| around do |example| old_adapter = Lutaml::Model::Config.xml_adapter Lutaml::Model::Config.xml_adapter = adapter_class example.run ensure Lutaml::Model::Config.xml_adapter = old_adapter end context "with CDATA option" do let(:xml) do <<~XML.strip <alpha> <element1><![CDATA[foo]]></element1> <element2><![CDATA[one]]></element2> <element2><![CDATA[two]]></element2> <element2><![CDATA[three]]></element2> <element3>bar</element3> <beta><![CDATA[child]]></beta> </alpha> XML end let(:expected_xml) do <<~XML.strip <alpha> <element1>foo</element1> <element2> <![CDATA[one]]> </element2> <element2> <![CDATA[two]]> </element2> <element2> <![CDATA[three]]> </element2> <element3>bar</element3> <beta> <![CDATA[child]]> </beta> </alpha> XML end it "maps xml to object" do instance = CDATA::Alpha.from_xml(xml) expect(instance.element1).to eq("foo") expect(instance.element2).to eq(%w[one two three]) expect(instance.element3).to eq("bar") expect(instance.beta.element1).to eq("child") end it "converts objects to xml" do instance = CDATA::Alpha.new( element1: "foo", element2: %w[one two three], element3: "bar", beta: CDATA::Beta.new(element1: "child"), ) expect(instance.to_xml).to be_equivalent_to(expected_xml) end end context "with custom methods" do let(:xml) do <<~XML <address> <street>A</street> <city><![CDATA[B]]></city> <house><![CDATA[H]]></house> <address> <street>C</street> <city><![CDATA[D]]></city> <house><![CDATA[G]]></house> </address> </address> XML end let(:expected_xml) do <<~XML <address> <street>A</street> <city> <![CDATA[B]]> </city> <house>H</house> <address> <street>C</street> <city> <![CDATA[D]]> </city> <house>G</house> </address> </address> XML end it "round-trips XML" do model = CDATA::Address.from_xml(xml) expect(model.to_xml).to be_equivalent_to(expected_xml) end end context "with custom models" do let(:input_xml) do <<~XML <CustomModelParent> <first_name><![CDATA[John]]></first_name> <last_name><![CDATA[Doe]]></last_name> <CustomModelChild> <street><![CDATA[Oxford Street]]></street> <city><![CDATA[London]]></city> </CustomModelChild> </CustomModelParent> XML end let(:expected_nokogiri_xml) do <<~XML <CustomModelParent> <first_name><![CDATA[John]]></first_name> <last_name>Doe</last_name> <CustomModelChild> <street><![CDATA[Oxford Street]]></street> <city><![CDATA[London]]></city> </CustomModelChild> </CustomModelParent> XML end let(:expected_ox_xml) do <<~XML <CustomModelParent> <first_name> <![CDATA[John]]> </first_name> <last_name>Doe</last_name> <CustomModelChild> <street> <![CDATA[Oxford Street]]> </street> <city> <![CDATA[London]]> </city> </CustomModelChild> </CustomModelParent> XML end describe ".from_xml" do it "maps XML content to custom model using custom methods" do instance = parent_mapper.from_xml(input_xml) expect(instance.class).to eq(parent_model) expect(instance.first_name).to eq("John") expect(instance.last_name).to eq("Doe") expect(instance.name).to eq("John Doe") expect(instance.child_mapper.class).to eq(child_model) expect(instance.child_mapper.street).to eq("Oxford Street") expect(instance.child_mapper.city).to eq("London") end end describe ".to_xml" do it "with correct model converts objects to xml using custom methods" do instance = parent_mapper.from_xml(input_xml) result_xml = parent_mapper.to_xml(instance) expected_output = adapter_class == Lutaml::Model::XmlAdapter::OxAdapter ? expected_ox_xml : expected_nokogiri_xml expect(result_xml.strip).to eq(expected_output.strip) end end end context "when mixed: true is set for nested content" do let(:xml) do <<~XML <RootMixedContentNested id="outer123"> <![CDATA[The following text is about the Moon.]]> <MixedContent id="inner456"> <![CDATA[The Earth's Moon rings like a ]]> <bold><![CDATA[bell]]></bold> <![CDATA[ when struck by meteroids. Distanced from the Earth by ]]> <italic><![CDATA[384,400 km]]></italic>, <![CDATA[ ,its surface is covered in ]]> <underline><![CDATA[craters]]></underline>. <![CDATA[ .Ain't that ]]> <bold><![CDATA[cool]]></bold> <![CDATA[ ? ]]> </MixedContent> <sup><![CDATA[1]]></sup>: <![CDATA[The Moon is not a planet.]]> <sup><![CDATA[2]]></sup>: <![CDATA[The Moon's atmosphere is mainly composed of helium in the form of He]]><sub><![CDATA[2]]></sub>. </RootMixedContentNested> XML end expected_xml = "<RootMixedContentNested id=\"outer123\"><![CDATA[The following text is about the Moon.]]><MixedContent id=\"inner456\"><![CDATA[The Earth's Moon rings like a ]]><bold><![CDATA[bell]]></bold><![CDATA[ when struck by meteroids. Distanced from the Earth by ]]><italic><![CDATA[384,400 km]]></italic><![CDATA[ ,its surface is covered in ]]><underline><![CDATA[craters]]></underline><![CDATA[ .Ain't that ]]><bold><![CDATA[cool]]></bold><![CDATA[ ? ]]></MixedContent><sup><![CDATA[1]]></sup><![CDATA[The Moon is not a planet.]]><sup><![CDATA[2]]></sup><![CDATA[The Moon's atmosphere is mainly composed of helium in the form of He]]><sub>2</sub></RootMixedContentNested>" expected_ox_xml = <<~XML <RootMixedContentNested id="outer123"> <![CDATA[The following text is about the Moon.]]> <MixedContent id="inner456"> <![CDATA[The Earth's Moon rings like a ]]> <bold> <![CDATA[bell]]> </bold> <![CDATA[ when struck by meteroids. Distanced from the Earth by ]]> <italic> <![CDATA[384,400 km]]> </italic> <![CDATA[ ,its surface is covered in ]]> <underline> <![CDATA[craters]]> </underline> <![CDATA[ .Ain't that ]]> <bold> <![CDATA[cool]]> </bold> <![CDATA[ ? ]]> </MixedContent> <sup> <![CDATA[1]]> </sup> <![CDATA[The Moon is not a planet.]]> <sup> <![CDATA[2]]> </sup> <![CDATA[The Moon's atmosphere is mainly composed of helium in the form of He]]> <sub>2</sub> </RootMixedContentNested> XML it "deserializes and serializes mixed content correctly" do parsed = CDATA::RootMixedContentNested.from_xml(xml) expected_content = [ "The Earth's Moon rings like a ", " when struck by meteroids. Distanced from the Earth by ", " ,its surface is covered in ", " .Ain't that ", " ? ", ] expect(parsed.id).to eq("outer123") expect(parsed.sup).to eq(["1", "2"]) expect(parsed.sub).to eq(["2"]) expect(parsed.content.id).to eq("inner456") expect(parsed.content.bold).to eq(["bell", "cool"]) expect(parsed.content.italic).to eq(["384,400 km"]) expect(parsed.content.underline).to eq("craters") parsed.content.content.each_with_index do |content, index| expected_output = expected_content[index] # due to the difference in capturing # newlines in ox and nokogiri adapters if adapter_class == Lutaml::Model::XmlAdapter::OxAdapter expected_xml = expected_ox_xml end expect(content).to eq(expected_output) end serialized = parsed.to_xml expect(serialized).to eq(expected_xml) end end context "when defualt: true is set for attributes default values" do let(:xml) do <<~XML <DefaultValue> <![CDATA[The following text is about the Moon]]> <temperature> <![CDATA[500]]> </temperature> <![CDATA[The Moon's atmosphere is mainly composed of helium in the form]]> </DefaultValue> XML end expected_xml = "<DefaultValue><name><![CDATA[Default Value]]></name><temperature><![CDATA[500]]></temperature><opacity>Opaque</opacity><![CDATA[The following text is about the MoonThe Moon's atmosphere is mainly composed of helium in the form]]></DefaultValue>" expected_ox_xml = <<~XML <DefaultValue> <name> <![CDATA[Default Value]]> </name> <temperature> <![CDATA[500]]> </temperature> <opacity>Opaque</opacity> <![CDATA[The following text is about the MoonThe Moon's atmosphere is mainly composed of helium in the form]]> </DefaultValue> XML it "deserializes and serializes mixed content correctly" do parsed = CDATA::DefaultValue.from_xml(xml) expected_content = [ "The following text is about the Moon", "The Moon's atmosphere is mainly composed of helium in the form", ] expect(parsed.name).to eq("Default Value") expect(parsed.opacity).to eq("Opaque") expect(parsed.temperature).to eq(500) parsed.content.each_with_index do |content, index| expected_output = expected_content[index] # due to the difference in capturing # newlines in ox and nokogiri adapters if adapter_class == Lutaml::Model::XmlAdapter::OxAdapter expected_xml = expected_ox_xml end expect(content).to eq(expected_output) end serialized = parsed.to_xml expect(serialized).to eq(expected_xml) end end end describe Lutaml::Model::XmlAdapter::NokogiriAdapter do it_behaves_like "cdata behavior", described_class end describe Lutaml::Model::XmlAdapter::OxAdapter do it_behaves_like "cdata behavior", described_class end end