lutaml / shale

Shale is a Ruby object mapper and serializer for JSON, YAML, TOML, CSV and XML. It allows you to parse JSON, YAML, TOML, CSV and XML data and convert it into Ruby data structures, as well as serialize data structures into JSON, YAML, TOML, CSV or XML.
https://shalerb.org/
MIT License
0 stars 1 forks source link

Bundles of elements #18

Open andrew2net opened 1 month ago

andrew2net commented 1 month ago

In grammar, which describes the Relaton data model, we have groups of elements included in other elements. For example:

  ...
  <define name="sup">
    <element name="sup">
      <zeroOrMore>
        <ref name="PureTextElement"/>
      </zeroOrMore>
    </element>
  </define>
  ...
  <define name="PureTextElement">
    <choice>
      <text/>
      <ref name="em"/>
      <ref name="strong"/>
      <ref name="sub"/>
      <ref name="sup"/>
      <ref name="tt"/>
      <ref name="underline"/>
      <ref name="strike"/>
      <ref name="smallcap"/>
      <ref name="br"/>
    </choice>
  </define>
  ...

With the ShaleI want to be able to use a pattern like:

class PureTextElement < Shale::Mapper
  attribute :text, Shale::Type::String
  attribute :em, Em
  attribute :strong, Strong
  ...
  xml do
    map_content to: :text
    map_element 'em', to: :em
    map_element 'strong', to: :strong
    ...
  end
end

class Sup < Shale::Mapper
  attribute :content, PureTextElement, collection: true

  xml do
    root 'sup'
    map_content to: :content
  end
end

We need to have any number of bundled elements (PureTextElements in the example) as children in any sequence. We need Shale to parse that sequence and render it in the same order.

HassanAkbar commented 1 month ago

@andrew2net I've tried the following and this is currently working in lutaml/shale

I've created the following classes

class PureTextElement < Shale::Mapper
  attribute :text, Shale::Type::String
  attribute :em, Shale::Type::String # Em
  attribute :strong, Shale::Type::String # Strong
  attribute :underline, Shale::Type::String # Underline

  xml do
    preserve_element_order true

    map_content to: :text

    map_element 'em', to: :em
    map_element 'strong', to: :strong
    map_element 'underline', to: :underline
  end
end

class Sup < Shale::Mapper
  attribute :pure_text_element, PureTextElement, collection: true

  xml do
    root 'sup'

    map_element 'PureTextElement', to: :pure_text_element
  end
end

require 'shale/adapter/nokogiri'
Shale.xml_adapter = Shale::Adapter::Nokogiri

then I tried to convert the following XML from and to xml through shale

xml = <<~XML
  <sup>
    <PureTextElement>
      Some text <strong>Bold text</strong><em>important text</em>
    </PureTextElement>
    <PureTextElement>
      Some text <em>Bold text</em><strong>important text</strong>
    </PureTextElement>
    <PureTextElement>
      Some text <strong>Bold text</strong><em>important text</em>
    </PureTextElement>
  </sup>
XML

obj = Sup.from_xml(x)
obj.to_xml(pretty: true)

and it generates the same output as input.

preserve_element_order true is our own extension attribute to shale to preserve the order of the elements so it outputs them in the same order as they are received.

Can you explain what else is missing from shale?

andrew2net commented 1 month ago

@HassanAkbar it is almost good but the PureTextElement is the definition name, not an element name. XML should look like:

  <sup>
    Some text <strong>Bold text</strong><em>important text</em>
    Some text <em>Bold text</em><strong>important text</strong>
    Some text <strong>Bold text</strong><em>important text</em>
  </sup>

I've tried to map the PureTextElement as content but Shale only recognizes text, it ignores other elements.

andrew2net commented 1 month ago

@HassanAkbar I found workaround but it looks not so elegant:

module Relaton
  module Model
    class PureTextElement
      def initialize(element)
        @element = element
      end

      def self.cast(value)
        value
      end

      def self.of_xml(node) # rubocop:disable Metrics/CyclomaticComplexity,Metrics/AbcSize,Metrics/MethodLength
        case node.name
        when "text"
          text = node.text.strip
          text.empty? ? nil : new(text)
        when "em" then new Em.of_xml(node)
        when "strong" then new Strong.of_xml(node)
        when "sub" then new Sub.of_xml(node)
        when "sup" then new Sup.of_xml(node)
        when "tt" then new Tt.of_xml(node)
        when "underline" then new Underline.of_xml(node)
        when "strike" then new Strike.of_xml(node)
        when "smallcap" then new Smallcap.of_xml(node)
        when "br" then new Br.of_xml(node)
        end
      end

      def add_to_xml(parent, doc)
        if @element.is_a? String
          doc.add_text(parent, @element)
        else
          parent << @element.to_xml
        end
      end

      module Mapper
        def self.included(base)
          base.class_eval do
            attribute :content, PureTextElement, collection: true

            xml do
              map_content to: :content, using: { from: :content_from_xml, to: :content_to_xml }
            end
          end
        end

        def content_from_xml(model, node)
          (node.instance_variable_get(:@node) || node).children.each do |n|
            next if n.text? && n.text.strip.empty?

            model.content << PureTextElement.of_xml(n)
          end
        end

        def content_to_xml(model, parent, doc)
          model.content.each do |e|
            e.add_to_xml parent, doc
          end
        end
      end
    end
  end
end

module Relaton
  module Model
    class Sup < Shale::Mapper
      include PureTextElement::Mapper

      @xml_mapping.instance_eval do
        root "sup"
      end
    end
  end
end

Is it possible to implement this functionality within Shale? Also in some cases we need to add elements to a bundle:

  <define name="strike">
    <element name="strike">
      <zeroOrMore>
        <choice>
          <ref name="PureTextElement"/>
          <ref name="index"/>
          <ref name="index-xref"/>
        </choice>
      </zeroOrMore>
    </element>
  </define>

To implement this I use the code:

module Relaton
  module Model
    class Strike < Shale::Mapper
      class Content
        def initialize(elements = [])
          @elements = elements
        end

        def self.cast(value)
          value
        end

        def self.of_xml(node)
          elms = node.children.map do |n|
            case n.name
            when "index" then Index.of_xml n
            when "index-xref" then IndexXref.of_xml n
            else PureTextElement.of_xml n
            end
          end
          new elms
        end

        def add_to_xml(parent, doc)
          @elements.each { |e| e.add_to_xml parent, doc }
        end
      end

      attribute :content, Content, collection: true

      xml do
        root "strike"
        map_content to: :content, using: { from: :content_from_xml, to: :content_to_xml }
      end

      def content_from_xml(model, node)
        model.content << Content.of_xml(node.instance_variable_get(:@node) || node)
      end

      def content_to_xml(model, parent, doc)
        model.content.each { |e| e.add_to_xml parent, doc }
      end
    end
  end
end
HassanAkbar commented 1 month ago

@andrew2net I've been looking into the code and @ronaldtse Suggestion in this ticket -> https://github.com/metanorma/sts-ruby/issues/12.

Currently, I'm working on adding map_all_content and map_content_element methods to xml so the classes will look something like below.

class TextWithTags < Shale::Mapper
  attribute :content, Shale::Type::String
  attribute :strong, Shale::Type::String, collection: true
  attribute :em, Shale::Type::String, collection: true

  xml do
    root "text-with-tags"

    map_all_content to: :content
    map_content_element 'strong', to: :strong
    map_content_element 'em', to: :em
  end
end
andrew2net commented 1 month ago

@HassanAkbar in the example the content, strong, and em elements are in predefined order. This issue is about having any number of the elements in any order. Will it be possible to achieve it with the update?