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

Unable to model dynamic key as content structures (e.g. JSON, YAML) #13

Closed ronaldtse closed 2 months ago

ronaldtse commented 2 months ago

In JSON there are often structures like this:

{
  "content": {
    "example.css": "text/css",
    "example.js": "text/javascript",
    "index.html": "text/html"
  }
}

Or

{
  "content": {
    "example.css": { "mime": "text/css" },
    "example.js": { "mime": "text/javascript" },
    "index.html": { "mime": "text/html" }
  }
}

However, Shale only supports collections in JSON arrays:

{
  "content": [
    { "file": "index.html", "mime": "text/html" },
    { "file": "example.css", "mime": "text/css" },
    { "file": "example.js", "mime": "text/javascript" }
  ]
}

The first type differs from the second type:

  1. A much better shorthand than the second type. "index.html": "text/html" is much easier to write than { "file": "index.html", "mime": "text/html" },.
  2. It is a mapping instead of an array, which means:
    1. There is no order in this mapping, distinct from an array which retains order.
    2. There is guaranteed uniqueness in the mapping, distinct from an array which does not enforce key uniqueness.
    3. Not to mention it is much faster in processing.

I have encountered this problem in Capsium and also the ISO 10303 schema.yaml parsing.

Right now it requires this sort of override:

class Config < Shale::Mapper
  attribute :schemas, Schema, collection: true

  yaml do
    map 'schemas', using: { from: :schemas_from_yaml, to: :schemas_to_yaml }
  end

  def schemas_from_yaml(model, value)
    model.schemas = value.map do |k, v|
      Schema.new(id: k, path: v['path'])
    end
  end

  def schemas_to_yaml(model, doc)
    doc['schemas'] = model.schemas.sort_by do |schema|
      schema.id
    end.to_h do |schema|
      [schema.id, { 'path' => schema.path }]
    end
  end
end

Which is rather annoying for every class of this type, and you need to repeat all these methods for JSON and YAML.

Instead, this can easily be done with:

class Config < Shale::Mapper
  attribute :schemas, Schema, collection: true

  yaml do
    # maps serialization 'schemas[].id' to 'Schema.new.id'., 'schemas[].value' to 'Schema.new.value'
    map 'schemas', to: schemas, map: { key: :id, value: :path }

    # maps serialization 'schemas[].id' to 'Schema.new.id'., 'schemas[].*' to 'Schema.new.*'
    map 'schemas', to: schemas, map: { key: :id, value: :* }
  end
end