Data bindings

There are many ways to represent data. For instance, XML, JSON and YAML are all very similar while having different representations. Data bindings attempts to unify these various representations by allowing the creation of representation-free schemas which can be used to valiate a document. As well, it provides adapters to normalize access across these various types.

Data bindings has four central concepts. Adapters provide normal access independent of representation. Readers allow you to define adapter-independent ways of reading data. Writers allows you define adapter-independent ways of writing data.Validations allow you to define a schema for your document.

5 minute demo

Start by loading from a JSON object

a = DataBindings.from_json('{"name":"Proust","books":[{"published":1913,"title":"Swan\'s Way"},{"published":1923,"title":"The Prisoner"}]}')

(You could also load from YAML, XML BSON, etc by using #from_yaml, #from_xml, #from_bson and so forth)

We can go ahead and access that like we nomrally would

a[:name]
# "proust"
a[:name][0][:title]
# "Swan's Way"

Great, now let's get a validated copy of that object

b = a.bind {
  property :name, String
  property :books, [] {
    property :published, Integer
    property :title, String
  }
}

Is it okay?

b.valid?
# => true

How about we represent it in YAML!

b.convert_to_yaml
# => "---\nname: Proust\nbooks:\n- published: 1913\n  title: Swan's Way\n- published: 1923\n  title: The Prisoner\n" 

Or, right out to a YAML file

b.convert_to_yaml_file("/tmp/proust.yaml")

And load it back

from_file = DataBindings.from_yaml_file("/tmp/proust.yaml")
from_file.bind(a) # Use the binding from above

We can also define the types independently so that we can associate them with Ruby constructors later.

DataBindings.type(:book) {
  property :published, Integer
  property :title, String
}

DataBindings.type(:person) {
  property :name
  property :books, [:book]
}

proust = DataBindings.from_yaml_file('/tmp/proust.yaml').bind(:person)
p proust[:name]
# => "Proust" 
p proust[:books][1]
# => {"published"=>1923, "title"=>"The Prisoner"}

Maybe we also want to create a Ruby object out of person, let's do that.

class Person
  attr_reader :name, :books

  def initialize(name, books)
    @name = name
    @books = books
  end

  def proust?
    name.downcase == 'proust'
  end
end

class Book
  attr_reader :published, :title

  def initialize(published, title)
    @published, @title = published, title
  end

  def published_before?(year)
    published < year
  end
end

DataBindings.for_native(:person) { |attrs| Person.new(attrs[:name], attrs[:books]) }
DataBindings.for_native(:book) { |attrs| Book.new(attrs[:published], attrs[:title]) }

proust = DataBindings.from_yaml_file('/tmp/proust.yaml').bind!(:person).to_native
proust.proust?
# => true
proust.books[0].published_before?(2011)
# => true
proust.books[0].published_before?(1800)
# => false

Adapters

Adapters have a simple contract. They must be a module. They must define a method #from_* where * is a type. For example, the JSONAdapter provides #from_json. They must also provide a singleton method #construct that can serialize an object into it's target representation. They may provide other methods to your base generator; they are included into it and thus can access any of it's internals. They are typically expected to return a ruby hash or array. For instance:

a = DataBindings.from_json('{"Hello":"World"}')
# => {"Hello"=>"World"} 
a.class
# => DataBindings::Adapters::Ruby::RubyObjectAdapter 

Binding

Bindings provide a mechanism to validate certain properties of a Hash.

To create a type, define it from your generator. For example:

DataBindings.type(:person) do
  property :name, String
  property :age, Integer
end

Would define a type for :person. This object would have two properties name and age. The types available are String, Integer, Float, DataBindings::Boolean. As well, you can refer to any of the types you've defined previously. You can refer to an implicit array of values by putting the type in []. For example, you could have

DataBindings.type(:person) do
  property :name, String
  property :age, Integer
  property :lottery_numbers, [Integer]
end

Readers

Readers provide an adapter-indepedent way of reading data from other sources. By default, we are also dealing with a String representation of the data. For instance:

DataBindings.from_json('{"Hello":"World"}')

would create a JSON representation. You could provide file access by adding a file reader.

DataBindings.reader(:file) { |f| File.read(f) }

Now, we could load the above JSON from disk by using

DataBindings.from_json_file('/tmp/file.json')

The #from_json_file method is synthesized into your generator by adding a :file reader. By default, there are readers for files, io, and http.

Writers

Writers provide an adapter-indepedent way of writing data to other sources. By default, we emit our representation of the data as a String. For instance:

DataBindings.from_ruby({"Hello" => "World"}).convert_to_yaml

would create a YAML representation. You could provide file writing by adding a file writer.

DataBindings.reader(:file) { |obj, f| File.open(f, 'w') { |h| h << obj } }

Now, if you wanted to write the above JSON to disk as YAML, you could do the following:

DataBindings.from_ruby({"Hello" => "World"}).convert_to_file(:yaml, "/tmp/out.yaml")

The #convert_to_file method that would be synthesized into your generator. By default, there are writers for files, io, and http.