# 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.

# Introduction

Working with data serialization formats directly can be painfull. This is especially true for XML. Let's consider this simple example of adding an address to a person using Nokogiri:

require 'nokogiri' doc = Nokogiri::XML(<<~XML) <person></person> XML address = Nokogiri::XML::Node.new('address', doc) street = Nokogiri::XML::Node.new('street', doc) street.content = 'Oxford Street' address.add_child(street) city = Nokogiri::XML::Node.new('city', doc) city.content = 'London' address.add_child(city) doc.root.add_child(address) puts doc.to_xml

That's a lot of code for very simple use case. Anything more complex and code complexity increases exponentially leading to a maintanace problems and errors.

With Shale you can use Ruby objects to work with data converting it to/from JSON, YAML, TOML, CSV or XML.

Let's convert the same example to Shale:

require 'shale' class Address < Shale::Mapper attribute :street, Shale::Type::String attribute :city, Shale::Type::String end class Person < Shale::Mapper attribute :address, Address end person = Person.from_xml('<person></person>') person.address = Address.new(street: 'Oxford Street', city: 'London') puts person.to_xml

That's much simpler and it stays simple when the code complexity increases.

# Prerequisites

If you want to work with XML you will need one of:

If you want to work with TOML you will need one of:

Shale doesn't have external dependencies. It uses standard library's JSON, YAML and CSV parsers by default.

If you need more customizations you can use custom libraries. Out of the box, Shale provides adapters for REXML, Nokogiri, Ox and toml-rb, but you can provide your own adapters - see how to.

# Features

# Installation

Add this line to your application's Gemfile:

gem 'shale'

And then execute:

$ bundle install

Or install it yourself as:

$ gem install shale

# Convert data to Ruby

JSON, YAML and CSV are supported by default, but to work with XML you need to install XML parser and setup adapter. For example to setup REXML as an adapter use:

require 'shale/adapter/rexml' Shale.xml_adapter = Shale::Adapter::REXML

If you want to work with TOML you will need to install Tomlib parser and setup it as an adapter:

require 'tomlib' Shale.toml_adapter = Tomlib

Converting data to Ruby is as simple as defining model classes and calling from_<format> method on this class. e.g. Person.from_json(json_doc)

NOTE

CSV represents a flat data structure, so you can't map properties to complex types directly, but you can use attribute delegation or methods to map properties to complex types (see using methods to extract and generate data section).

WARNING

.from_csv method allways returns an array of records.

# Convert Ruby to data

To convert Ruby to data just define model class, initialize object and call to_<format> method on it. e.g. Person.new(name: 'John Doe').to_json

# Convert Collections

Shale allows converting collections for formats that support it (JSON, YAML and CSV).

To convert data to Ruby array:

To convert Ruby array to data:

# Custom mappings

When you define a class and add attributes, underneath Shale creates an implicit mapping of keys (for JSON/YAML/TOML), columns (for CSV) or elements (for XML) to attributes. That is nice for setting up your data model quickly, but usually your data format doesn't match your data model so cleanly.

That's why you can explicitly map keys, element and attributes from your data format to attributes on your Ruby model.

WARNING

Declaring custom mapping removes default mapping for given format!

NOTE

For CSV the order of mapping matters. The first argument in the map method is only used as a label in header row. So, in the example below the first column will be mapped to :first_name attribute and the second column to :last_name.

XML is more complex format.

You can use cdata: true option on map_element and map_content to handle CDATA nodes:

# Using XML namespaces

To Work with XML namespaces you need to use adapter that supports XML namespaces. Use one of Shale::Adapter::REXML or Shale::Adapter::Nokogiri. Shale::Adapter::Ox doesn't support namespaces.

To map namespaced elements and attributes use namespace and prefix properties on map_element and map_attribute

To define default namespace for all elements use namespace declaration (this will define namespace only on elements, if you want to define namespace on an attribute, explicitly declare it on map_attribute).

# Rendering nil values

For JSON, YAML, TOML and XML by default elements with nil value are not rendered. You can change this behavior by using render_nil: true on a mapping.

For CSV, the default is to render nil elements.

If you want to change how nil values are rendered for all mappings you can use render_nil method:

WARNING

The default affects only the mappings declared after setting the default value e.g.

class Person < Base attribute :first_name, Shale::Type::String attribute :last_name, Shale::Type::String json do render_nil false # render_nil will be false for this mapping map 'first_name', to: :first_name render_nil true # render_nil will be true for this mapping map 'last_name', to: :last_name end end

# Using methods to extract and generate data

If you need full controll over extracting and generating data you can use methods to do so.

You can also pass a context object that will be available in extractor/generator methods:

If you want to work on multiple elements at a time you can group them with group block:

Example: JSON XML

# Attribute delegation

To delegate fields to child complex types you can use receiver: :child declaration on a mapping.

# Additional options

You can control which attributes to render and parse by using only: [] and except: [] parameters.

By default generated JSON and XML are compacted. If you need human readable format use pretty: true parameter on #to_json and #to_xml

person.to_json(pretty: true) # => # # { # "name": "John Doe", # "address": { # "city": "London" # } # }

You can also add an XML declaration by passing declaration: true and encoding: true to #to_xml. You can use specific version by using string argument e.g. #to_xml(declaration: '1.1', encoding: 'ASCII')

person.to_xml(pretty: true, declaration: true, encoding: true) # => # # <?xml version="1.0" encoding="UTF-8"?> # <Person> # <Address city="London"/> # </Person>

For CSV you can pass headers: true to indicate that the first row contains column names and shouldn't be included in the returned collection. It also accepts all the options that CSV parser accepts.

class Person attribute :first_name, Shale::Type::String attribute :last_name, Shale::Type::String end people = Person.from_csv(<<~DATA, headers: true, col_sep: '|') first_name|last_name John|Doe James|Sixpack DATA # => # # [ # #<Person:0x0000000113d7a488 @first_name="John", @last_name="Doe">, # #<Person:0x0000000113d7a488 @first_name="James", @last_name="Sixpack"> # ]

# Using custom models

By default Shale combines mapper and model into one class. If you want to use your own classes as models you can do it by using model directive on the mapper:

# Generating JSON and XML Schema

WARNING

Shale only supports Draft 2020-12 JSON Schema

To generate JSON or XML Schema from you Shale data model use:

Example: JSON XML

You can also use a command line tool to do it:

$ shaleb -i data_model.rb -r Person -p -f json

or XML Schema:

$ shaleb -i data_model.rb -r Person -p -f xml

If you want to convert your own types to Schema types use:

Example: JSON XML

# Compiling JSON and XML Schema

To compile JSON or XML Schema and generate Ruby data model use Shale::Schema.from_json or Shale::Schema.from_xml. You can pass namespace_mapping: {} to map JSON schemas or XML namespaces to Ruby modules. For JSON schema, you can also pass root_name: 'Foobar' to change the name of the root type.

Example: JSON XML

You can also use a command line tool to do it (JSON Schema):

$ shaleb -c -i schema.json -r Person -m http://bar.com=Api::Bar,=Api::Foo

and XML Schema:

$ shaleb -c -f xml -i schema.xml -m http://bar.com=Api::Bar,=Api::Foo

# Supported types

Shale supports these types out of the box:

To add your own type define a class and extend it from Shale::Type::Value and implement .cast class method.

require 'shale/type/value' class MyIntegerType < Shale::Type::Value def self.cast(value) value.to_i end end

# Adapters

Shale uses adapters for parsing and generating documents. By default Ruby's standard JSON, YAML and CSV parsers are used for handling JSON, YAML and CSV documents.

You can change it by providing your own adapter. For JSON, TOML, YAML and CSV, adapter must implement .load and .dump class methods.

require 'shale' require 'multi_json' Shale.json_adapter = MultiJson Shale.yaml_adapter = MyYamlAdapter

To handle TOML documents you have to explicitly set TOML adapter. Out of the box Tomlib is supported. Shale also provides adapter for toml-rb parser:

require 'shale' # if you want to use Tomlib require 'tomlib' Shale.toml_adapter = Tomlib # if you want to use toml-rb require 'shale/adapter/toml_rb' Shale.toml_adapter = Shale::Adapter::TomlRB

To handle XML documents you have to explicitly set XML adapter. Shale provides adapters for most popular Ruby XML parsers:

WARNING

Ox parser doesn't support XML namespaces

require 'shale' # if you want to use REXML: require 'shale/adapter/rexml' Shale.xml_adapter = Shale::Adapter::REXML # if you want to use Nokogiri: require 'shale/adapter/nokogiri' Shale.xml_adapter = Shale::Adapter::Nokogiri # or if you want to use Ox: require 'shale/adapter/ox' Shale.xml_adapter = Shale::Adapter::Ox