lutaml / lutaml-model

LutaML Model is the Ruby data modeler part of the LutaML data modeling suite. It supports creating serialization object models (XML, YAML, JSON, TOML) and mappings to and from them.
Other
2 stars 2 forks source link

`using` needed in custom serialisations #88

Open opoudjis opened 2 months ago

opoudjis commented 2 months ago

Modspec under Metanorma is following the Metanorma modelling of requirements: component is an element for general, otherwise unspecified components of requirements.

Modspec under modspec-ruby, created by @ronaldtse without talking to me, differentiates guidance, purpose, and method.

Under Metanorma, these are component[@type = 'guidance'], component[@type = 'purpose'], and component[@type = 'test-method'].

@ronaldtse wants these deserialised to Metanorma XML under lutaml-model.

I see from the lutaml-model readme that:

There is an implementation difference between Lutaml::Model and Shale for custom serialization methods.

Custom serialization methods in Lutaml::Model map to individual attributes.

Custom serialization methods in Shale do not map to specific attributes, but allow the user to specify where the data goes.

I understand that makes life a lot easier for lutaml-model. But that means that lutaml-model forces the XML serialisation (and any serialisation) of a model to have a 1:1 model of its model attributes to its serialisation. I cannot choose to specify where the data goes; and that means I cannot decide that all three of guidance, purpose, and method go to component, and that there is logic to serialise multiple instances of component to one of guidance, purpose, or method.

Which means I cannot use lutaml-model, unless I change Metanorma's model to align to modspec-ruby's. Which I'm not going to do, and which is completely unreasonable for a serialisation tool to be forcing me to do.

As it stands,

 map_element "component", to: :guidance,
                                     with: { to: :component_to_xml, from: :component_from_xml }
map_element "component", to: :purpose,
                                     with: { to: :component_to_xml, from: :component_from_xml }
map_element "component", to: :method,
                                     with: { to: :component_to_xml, from: :component_from_xml }

      def component_to_xml(model, parent, doc)
            %i(guidance purpose method).each do |i|
              c = model.send(i) or next
              el = doc.create_element("component")
              el["type"] = i
              el << c
              doc.add_element(parent, el)
            end
          end

which is my effort to make this happen, simply doesn't work: it is triggered for guidance, doesn't see guidance in my test model, and ignores that purpose is in my test model.

If lutaml-model is to be usable under the range of serialisation I need in Metanorma, it must be prepared to deal with mismatches in source and target information model. It must give me the freedom I had under Shale, to have complete control over how to serialise the model in custom methods. If it does not do that, then I will need to postprocess its output, to move the tags to where I need them to be.

@ronaldtse You need to decide which of the two to do: make lutaml-model more complicated and more permissive, or be less purist about how I use lutaml-model downstream, and accept that I will postprocess it to get what I want. But as it stands, lutaml-model is not usable for Modspec under Metanorma.

And again, I will absolutely not change my XML model of requirements, just because you made a different modelling choice to me. The serialisation tools do not get to dictate the target model. (And don't assume that this will not keep coming up. Target models can and will mismatch the input model.)

ronaldtse commented 1 month ago

What @opoudjis needs is:

The sample XML is this:

<recommendation model="ogc" id="_">
  <identifier>/ogc/recommendation/wfs/2</identifier>
  <inherit>/ss/584/2015/level/1</inherit>
  <subject>user</subject>
  <description>
    <p id="_">I recommend <em>1</em>.</p>
    <classification>
      <tag>scope</tag>
      <value>random</value>
    </classification>
    <classification>
      <tag>widgets</tag>
      <value>randomer</value>
    </classification>
  </description>
  <component class="test-purpose" id="A1"><p>TEST PURPOSE</p></component>
  <description><p id="_">I recommend <em>2</em>.</p></description>
  <component class="guidance" id="A7"><p>GUIDANCE #1</p></component>
  <description><p id="_">I recommend <em>2a</em>.</p></description>
  <component class="conditions" id="A2"><p>CONDITIONS</p></component>
  <description><p id="_">I recommend <em>3</em>.</p></description>
  <component class="part" id="A3"><p>FIRST PART</p></component>
  <description><p id="_">I recommend <em>4</em>.</p></description>
  <component class="part" id="A4"><p>SECOND PART</p></component>
  <description><p id="_">I recommend <em>5</em>.</p></description>
  <component class="test-method" id="A5"><p>TEST METHOD</p></component>
  <description><p id="_">I recommend <em>6</em>.</p></description>
  <component class="part" id="A6"><p>THIRD PART</p></component>
  <description><p id="_">I recommend <em>7</em>.</p></description>
  <component class="guidance" id="A8"><p>GUIDANCE #2</p></component>
  <description><p id="_">I recommend <em>7a</em>.</p></description>
  <component class="panda GHz express" id="A7"><p>PANDA PART</p></component>
  <description><p id="_">I recommend <em>8</em>.</p></description>
</recommendation>

The pseudocode that parses this could look like this:

class Recommendation < Lutaml::Model::Serializable
  attribute :identifier, :string
  attribute :inherit, :string
  attribute :subject, :string
  attribute :description, :string, raw: true, collection: true
  attribute :test_purpose, TestPurpose
  attribute :test_method, TestMethod
  attribute :conditions, Condition, collection: true
  attribute :guidance, Guidance, collection: true
  attribute :part, Part, collection: true

  # 1. Notice that the component / description elements are POSITIONAL.

  xml do
    map_element "identifier", :identifier
    map_element "inherit", :inherit
    map_element "subject", :subject
    map_element "description", :description
    map_element "component", with: {
      to: :xml_component_to_specialized,
      from: :specialized_to_xml_component
    }, attributes: [
      :test_purpose,
      :test_method,
      :conditions,
      :guidance,
      :part
    ]
  end

  def xml_component_to_test_purpose(model, value)
    case value['class']
    when 'test_purpose'
      model.test_purpose << TestPurpose.new(id: value['id'], description: value['text'])
    when 'test_method'
      model.test_method << TestMethod.new(id: value['id'], description: value['text'])
    end
  end

  def test_purpose_to_xml_component(model, parent, doc)
    doc.add_element(
      name: 'component',
      attribs: { id: model.id, class: model.class.name.underscore },
      text: model.description
    )
  end
end

class TestPurpose < Lutaml::Model::Serializable
  attribute :id, :string
  attribute :description, :string, raw: true

  xml do
    map_attribute "id", :id
    map_content :description
  end
end

The point is we need to facilitate this use case.

@HassanAkbar can you please help work this into viable code and make this work in lutaml-model? Thanks.