module SerializeableSpec class TestModel attr_accessor :name, :age def initialize(name: nil, age: nil) @name = name @age = age end end class TestModelMapper < Lutaml::Model::Serializable model TestModel attribute :name, Lutaml::Model::Type::String attribute :age, Lutaml::Model::Type::String end class TestMapper < Lutaml::Model::Serializable attribute :name, Lutaml::Model::Type::String attribute :age, Lutaml::Model::Type::String yaml do map :na, to: :name map :ag, to: :age end end class KeyValueMapper < Lutaml::Model::Serializable attribute :first_name, :string attribute :last_name, :string attribute :age, :integer key_value do map :first_name, to: :first_name map :last_name, to: :last_name map :age, to: :age end end ### XML root mapping class RecordDate < Lutaml::Model::Serializable attribute :content, :string xml do root "recordDate" map_content to: :content end end class OriginInfo < Lutaml::Model::Serializable attribute :date_issued, RecordDate, collection: true xml do root "originInfo" map_element "dateIssued", to: :date_issued end end ### Enumeration class Ceramic < Lutaml::Model::Serializable attribute :type, :string attribute :firing_temperature, :integer end class CeramicCollection < Lutaml::Model::Serializable attribute :featured_piece, Ceramic, values: [ Ceramic.new(type: "Porcelain", firing_temperature: 1300), Ceramic.new(type: "Stoneware", firing_temperature: 1200), Ceramic.new(type: "Earthenware", firing_temperature: 1000), ] end class GlazeTechnique < Lutaml::Model::Serializable attribute :name, :string, values: ["Celadon", "Raku", "Majolica"] end end RSpec.describe Lutaml::Model::Serializable do describe ".model" do it "sets the model for the class" do expect do described_class.model(SerializeableSpec::TestModel) end.to change( described_class, :model ) .from(nil) .to(SerializeableSpec::TestModel) end end describe ".attribute" do subject(:mapper) { described_class.new } it "adds the attribute and getter setter for that attribute" do expect { described_class.attribute("foo", Lutaml::Model::Type::String) } .to change { described_class.attributes.keys }.from([]).to(["foo"]) .and change { mapper.respond_to?(:foo) }.from(false).to(true) .and change { mapper.respond_to?(:foo=) }.from(false).to(true) end end describe ".hash_representation" do context "when model is separate" do let(:instance) do SerializeableSpec::TestModel.new(name: "John", age: 18) end let(:expected_hash) do { "name" => "John", "age" => "18", } end it "return hash representation" do generate_hash = SerializeableSpec::TestModelMapper.hash_representation( instance, :yaml ) expect(generate_hash).to eq(expected_hash) end end context "when model is self" do let(:instance) do SerializeableSpec::TestMapper.new(name: "John", age: 18) end let(:expected_hash) do { na: "John", ag: "18", } end it "return hash representation" do generate_hash = SerializeableSpec::TestMapper.hash_representation( instance, :yaml ) expect(generate_hash).to eq(expected_hash) end end end describe ".mappings_for" do context "when mapping is defined" do it "returns the defined mapping" do actual_mappings = SerializeableSpec::TestMapper.mappings_for(:yaml).mappings expect(actual_mappings[0].name).to eq(:na) expect(actual_mappings[0].to).to eq(:name) expect(actual_mappings[1].name).to eq(:ag) expect(actual_mappings[1].to).to eq(:age) end end context "when mapping is not defined" do it "maps attributes to mappings" do allow(SerializeableSpec::TestMapper.mappings).to receive(:[]).with(:yaml).and_return(nil) actual_mappings = SerializeableSpec::TestMapper.mappings_for(:yaml).mappings expect(actual_mappings[0].name).to eq("name") expect(actual_mappings[0].to).to eq(:name) expect(actual_mappings[1].name).to eq("age") expect(actual_mappings[1].to).to eq(:age) end end end describe ".apply_child_mappings" do let(:child_mappings) do { id: :key, path: %i[path link], name: %i[path name], } end let(:hash) do { "foo" => { "path" => { "link" => "link one", "name" => "one", }, }, "abc" => { "path" => { "link" => "link two", "name" => "two", }, }, "hello" => { "path" => { "link" => "link three", "name" => "three", }, }, } end let(:expected_value) do [ { id: "foo", path: "link one", name: "one" }, { id: "abc", path: "link two", name: "two" }, { id: "hello", path: "link three", name: "three" }, ] end it "generates hash based on child_mappings" do expect(described_class.apply_child_mappings(hash, child_mappings)).to eq(expected_value) end end describe "#key_value" do let(:model) { SerializeableSpec::KeyValueMapper } Lutaml::Model::Config::KEY_VALUE_FORMATS.each do |format| it "defines 3 mappings for #{format}" do expect(model.mappings_for(format).mappings.count).to eq(3) end it "defines mappings correctly for #{format}" do defined_mappings = model.mappings_for(format).mappings.map(&:name) expect(defined_mappings).to eq(%i[first_name last_name age]) end end end describe "XML root name override" do it "uses root name defined at the component class" do record_date = SerializeableSpec::RecordDate.new(content: "2021-01-01") expected_xml = "2021-01-01" expect(record_date.to_xml).to eq(expected_xml) end it "uses mapped element name at the aggregating class, overriding root name" do origin_info = SerializeableSpec::OriginInfo.new(date_issued: [SerializeableSpec::RecordDate.new(content: "2021-01-01")]) expected_xml = <<~XML 2021-01-01 XML expect(origin_info.to_xml).to be_equivalent_to(expected_xml) end end describe "String enumeration" do context "when assigning an invalid value" do it "raises an error after creation after validate" do glaze = SerializeableSpec::GlazeTechnique.new(name: "Celadon") glaze.name = "Tenmoku" expect do glaze.validate! end.to raise_error(Lutaml::Model::ValidationError) do |error| expect(error).to include(Lutaml::Model::InvalidValueError) expect(error.error_messages).to include("name is `Tenmoku`, must be one of the following [Celadon, Raku, Majolica]") end end end context "when assigning a valid value" do it "changes the value after creation" do glaze = SerializeableSpec::GlazeTechnique.new(name: "Celadon") glaze.name = "Raku" expect(glaze.name).to eq("Raku") end it "assigns the value during creation" do glaze = SerializeableSpec::GlazeTechnique.new(name: "Majolica") expect(glaze.name).to eq("Majolica") end end end describe "Serializable object enumeration" do context "when assigning an invalid value" do it "raises ValidationError containing InvalidValueError after creation" do glaze = SerializeableSpec::GlazeTechnique.new(name: "Celadon") glaze.name = "Tenmoku" expect do glaze.validate! end.to raise_error(Lutaml::Model::ValidationError) do |error| expect(error).to include(Lutaml::Model::InvalidValueError) expect(error.error_messages).to include(a_string_matching(/name is `Tenmoku`, must be one of the following/)) end end it "raises ValidationError containing InvalidValueError during creation" do expect do SerializeableSpec::GlazeTechnique.new(name: "Crystalline").validate! end.to raise_error(Lutaml::Model::ValidationError) do |error| expect(error).to include(Lutaml::Model::InvalidValueError) expect(error.error_messages).to include(a_string_matching(/name is `Crystalline`, must be one of the following/)) end end end context "when assigning a valid value" do it "changes the value after creation" do collection = SerializeableSpec::CeramicCollection.new( featured_piece: SerializeableSpec::Ceramic.new(type: "Porcelain", firing_temperature: 1300), ) collection.featured_piece = SerializeableSpec::Ceramic.new(type: "Stoneware", firing_temperature: 1200) expect(collection.featured_piece.type).to eq("Stoneware") end it "assigns the value during creation" do collection = SerializeableSpec::CeramicCollection.new( featured_piece: SerializeableSpec::Ceramic.new(type: "Earthenware", firing_temperature: 1000), ) expect(collection.featured_piece.type).to eq("Earthenware") end end end end