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

Inheritance in format (e.g. `xml do`) specific blocks #7

Open ronaldtse opened 2 months ago

ronaldtse commented 2 months ago

Shale provides format specific blocks such as:

class X < Shale::Mapper
  attribute :a, Shale::Type::String
  attribute :b, Shale::Type::String

  xml do
    root 'foo'
    map_attribute 'A', to: :a
    map_attribute 'B', to: :b
  end
end

When a class inherits from it, the xml block mapping is completely wiped:

class Y < X
  xml do
    root 'bar'
    # map_attribute 'A', to: :a # This is gone!
    map_attribute 'B', to: :b
  end
end

It is very inconvenient to have to re-specify all format-specific mappings.

This situation is terrible for versioned namespaces because let's say there is UML version 2013 vs version 2016, the objects are slightly different, but the entire format block really needs to get rewritten.

Here's an example of the ideal situation:

class Uml2013 < Shale::Mapper
  attribute :a, Uml2013String
  attribute :b, Uml2013Note

  xml do
    root 'uml'
    map_attribute 'A', to: :a
    map_attribute 'B', to: :b
  end
end

class Uml2016 < Uml2013
  attribute :b, Uml2016Note
  xml do
    map_attribute 'B', to: :b
  end
end

The correct situation is to have format specific mappings retained unless overridden. There could also be a command that removes all mappings should that be necessary, such as xml! do.

andrew2net commented 2 months ago

@ronaldtse In some cases we can use modules to share code:


module BibliographicItem
  def self.included(base)
    base.class_eval do
      attribute :schema_version, Shale::Type::String
      ...

      xml do
        map_attribute "schema-version", to: :schema_version
        ...
      end
    end
  end
end

class Bibitem < Shale::Mapper
  include BibliographicItem

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

class Bibdata < Shale::Mapper
  include BibliographicItem

  @xml_mapping.instance_eval do
    root "bibdata"
    ...
  end
end
opoudjis commented 2 months ago

This is a pain for me as well, and I have already been yelling at the clown set up of this software. Repeating all accessors every time we inherit a class defeats the entire point of using classes to define accessors to XML and YAML.

ronaldtse commented 2 months ago

For @opoudjis it’s even more painful because he needs to override only one element in a tree of Mapper classes, and it forces him to inherit the entire tree of classes but only override one.

ronaldtse commented 2 months ago

This is a pain for me as well, and I have already been yelling at the clown set up of this software. Repeating all accessors every time we inherit a class defeats the entire point of using classes to define accessors to XML and YAML.

But can you design a syntax that address this? We can then find a way to implement it.

opoudjis commented 2 months ago

The most obvious thing to do would be to treat the do block like a class method (even if it isn't one), and introduce super.

So:

class Uml2013 < Shale::Mapper
  attribute :a, Uml2013String
  attribute :b, Uml2013Note

  xml do
    root 'uml'
    map_attribute 'A', to: :a
    map_attribute 'B', to: :b
  end
end

class Uml2016 < Uml2013
  attribute :b, Uml2016Note
  attribute :c, Uml2013Note
  xml do
    super
    map_attribute 'C', to: :c
  end
end

The other thing to do would be to modularise mappings in the XML block into class methods, which could then be overridden:

class Uml2013 < Shale::Mapper
  attribute :a, Uml2013String
  attribute :b, Uml2013Note

  xml do
    root 'uml'
    mapA
    mapB
  end

  def mapA
    map_attribute 'A', to: :a
  end

  def mapB
    map_attribute 'B', to: :b
  end
end

class Uml2016 < Uml2013
  attribute :b, Uml2016Note
  attribute :c, Uml2013Note

  def mapB
    map_attribute 'B', to: :b
    map_attribute 'C', to: :c
  end

end