data_bindings 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +5 -0
- data/Gemfile +4 -0
- data/README.md +167 -0
- data/Rakefile +13 -0
- data/data_bindings.gemspec +35 -0
- data/lib/data_bindings/adapters/bson.rb +32 -0
- data/lib/data_bindings/adapters/json.rb +32 -0
- data/lib/data_bindings/adapters/native.rb +50 -0
- data/lib/data_bindings/adapters/params.rb +91 -0
- data/lib/data_bindings/adapters/ruby.rb +44 -0
- data/lib/data_bindings/adapters/tnetstring.rb +32 -0
- data/lib/data_bindings/adapters/xml.rb +74 -0
- data/lib/data_bindings/adapters/yaml.rb +26 -0
- data/lib/data_bindings/adapters.rb +12 -0
- data/lib/data_bindings/bound.rb +331 -0
- data/lib/data_bindings/converters.rb +83 -0
- data/lib/data_bindings/generator.rb +140 -0
- data/lib/data_bindings/unbound.rb +76 -0
- data/lib/data_bindings/util.rb +78 -0
- data/lib/data_bindings/version.rb +3 -0
- data/lib/data_bindings.rb +52 -0
- data/test/array_test.rb +55 -0
- data/test/bson_test.rb +23 -0
- data/test/converter_test.rb +36 -0
- data/test/data_bindings_test.rb +67 -0
- data/test/fixtures/1.json +1 -0
- data/test/json_test.rb +40 -0
- data/test/native_test.rb +64 -0
- data/test/params_test.rb +60 -0
- data/test/test_helper.rb +17 -0
- data/test/tnetstring_test.rb +23 -0
- data/test/validation_test.rb +217 -0
- data/test/xml_test.rb +20 -0
- data/test/yaml_test.rb +19 -0
- metadata +282 -0
data/.gitignore
ADDED
data/Gemfile
ADDED
data/README.md
ADDED
@@ -0,0 +1,167 @@
|
|
1
|
+
# Data bindings
|
2
|
+
|
3
|
+
There are many ways to represent data. For instance, XML, JSON and YAML are all very similar while having different representations.
|
4
|
+
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,
|
5
|
+
it provides adapters to normalize access across these various types.
|
6
|
+
|
7
|
+
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.
|
8
|
+
|
9
|
+
## 5 minute demo
|
10
|
+
|
11
|
+
Start by loading from a JSON object
|
12
|
+
|
13
|
+
a = DataBindings.from_json('{"name":"Proust","books":[{"published":1913,"title":"Swan\'s Way"},{"published":1923,"title":"The Prisoner"}]}')
|
14
|
+
|
15
|
+
(You could also load from YAML, XML BSON, etc by using `#from_yaml`, `#from_xml`, `#from_bson` and so forth)
|
16
|
+
|
17
|
+
We can go ahead and access that like we nomrally would
|
18
|
+
|
19
|
+
a[:name]
|
20
|
+
# "proust"
|
21
|
+
a[:name][0][:title]
|
22
|
+
# "Swan's Way"
|
23
|
+
|
24
|
+
Great, now let's get a validated copy of that object
|
25
|
+
|
26
|
+
b = a.bind {
|
27
|
+
property :name, String
|
28
|
+
property :books, [] {
|
29
|
+
property :published, Integer
|
30
|
+
property :title, String
|
31
|
+
}
|
32
|
+
}
|
33
|
+
|
34
|
+
Is it okay?
|
35
|
+
|
36
|
+
b.valid?
|
37
|
+
# => true
|
38
|
+
|
39
|
+
How about we represent it in YAML!
|
40
|
+
|
41
|
+
b.convert_to_yaml
|
42
|
+
# => "---\nname: Proust\nbooks:\n- published: 1913\n title: Swan's Way\n- published: 1923\n title: The Prisoner\n"
|
43
|
+
|
44
|
+
Or, right out to a YAML file
|
45
|
+
|
46
|
+
b.convert_to_yaml_file("/tmp/proust.yaml")
|
47
|
+
|
48
|
+
And load it back
|
49
|
+
|
50
|
+
from_file = DataBindings.from_yaml_file("/tmp/proust.yaml")
|
51
|
+
from_file.bind(a) # Use the binding from above
|
52
|
+
|
53
|
+
We can also define the types independently so that we can associate them with Ruby constructors later.
|
54
|
+
|
55
|
+
DataBindings.type(:book) {
|
56
|
+
property :published, Integer
|
57
|
+
property :title, String
|
58
|
+
}
|
59
|
+
|
60
|
+
DataBindings.type(:person) {
|
61
|
+
property :name
|
62
|
+
property :books, [:book]
|
63
|
+
}
|
64
|
+
|
65
|
+
proust = DataBindings.from_yaml_file('/tmp/proust.yaml').bind(:person)
|
66
|
+
p proust[:name]
|
67
|
+
# => "Proust"
|
68
|
+
p proust[:books][1]
|
69
|
+
# => {"published"=>1923, "title"=>"The Prisoner"}
|
70
|
+
|
71
|
+
Maybe we also want to create a Ruby object out of person, let's do that.
|
72
|
+
|
73
|
+
class Person
|
74
|
+
attr_reader :name, :books
|
75
|
+
|
76
|
+
def initialize(name, books)
|
77
|
+
@name = name
|
78
|
+
@books = books
|
79
|
+
end
|
80
|
+
|
81
|
+
def proust?
|
82
|
+
name.downcase == 'proust'
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
class Book
|
87
|
+
attr_reader :published, :title
|
88
|
+
|
89
|
+
def initialize(published, title)
|
90
|
+
@published, @title = published, title
|
91
|
+
end
|
92
|
+
|
93
|
+
def published_before?(year)
|
94
|
+
published < year
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
DataBindings.for_native(:person) { |attrs| Person.new(attrs[:name], attrs[:books]) }
|
99
|
+
DataBindings.for_native(:book) { |attrs| Book.new(attrs[:published], attrs[:title]) }
|
100
|
+
|
101
|
+
proust = DataBindings.from_yaml_file('/tmp/proust.yaml').bind!(:person).to_native
|
102
|
+
proust.proust?
|
103
|
+
# => true
|
104
|
+
proust.books[0].published_before?(2011)
|
105
|
+
# => true
|
106
|
+
proust.books[0].published_before?(1800)
|
107
|
+
# => false
|
108
|
+
|
109
|
+
## Adapters
|
110
|
+
|
111
|
+
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:
|
112
|
+
|
113
|
+
a = DataBindings.from_json('{"Hello":"World"}')
|
114
|
+
# => {"Hello"=>"World"}
|
115
|
+
a.class
|
116
|
+
# => DataBindings::Adapters::Ruby::RubyObjectAdapter
|
117
|
+
|
118
|
+
## Binding
|
119
|
+
|
120
|
+
Bindings provide a mechanism to validate certain properties of a Hash.
|
121
|
+
|
122
|
+
To create a type, define it from your generator. For example:
|
123
|
+
|
124
|
+
DataBindings.type(:person) do
|
125
|
+
property :name, String
|
126
|
+
property :age, Integer
|
127
|
+
end
|
128
|
+
|
129
|
+
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
|
130
|
+
|
131
|
+
DataBindings.type(:person) do
|
132
|
+
property :name, String
|
133
|
+
property :age, Integer
|
134
|
+
property :lottery_numbers, [Integer]
|
135
|
+
end
|
136
|
+
|
137
|
+
## Readers
|
138
|
+
|
139
|
+
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:
|
140
|
+
|
141
|
+
DataBindings.from_json('{"Hello":"World"}')
|
142
|
+
|
143
|
+
would create a JSON representation. You could provide file access by adding a `file` reader.
|
144
|
+
|
145
|
+
DataBindings.reader(:file) { |f| File.read(f) }
|
146
|
+
|
147
|
+
Now, we could load the above JSON from disk by using
|
148
|
+
|
149
|
+
DataBindings.from_json_file('/tmp/file.json')
|
150
|
+
|
151
|
+
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.
|
152
|
+
|
153
|
+
## Writers
|
154
|
+
|
155
|
+
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:
|
156
|
+
|
157
|
+
DataBindings.from_ruby({"Hello" => "World"}).convert_to_yaml
|
158
|
+
|
159
|
+
would create a YAML representation. You could provide file writing by adding a `file` writer.
|
160
|
+
|
161
|
+
DataBindings.reader(:file) { |obj, f| File.open(f, 'w') { |h| h << obj } }
|
162
|
+
|
163
|
+
Now, if you wanted to write the above JSON to disk as YAML, you could do the following:
|
164
|
+
|
165
|
+
DataBindings.from_ruby({"Hello" => "World"}).convert_to_file(:yaml, "/tmp/out.yaml")
|
166
|
+
|
167
|
+
The `#convert_to_file` method that would be synthesized into your generator. By default, there are writers for files, io, and http.
|
data/Rakefile
ADDED
@@ -0,0 +1,13 @@
|
|
1
|
+
require "bundler/gem_tasks"
|
2
|
+
require 'rake/testtask'
|
3
|
+
require 'yard'
|
4
|
+
|
5
|
+
task :test do
|
6
|
+
Rake::TestTask.new do |t|
|
7
|
+
Dir['test/**/*_test.rb'].each{|f| require File.expand_path(f)}
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
YARD::Rake::YardocTask.new do |t|
|
12
|
+
t.files = ['lib/**/*.rb'] # optional
|
13
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
$:.push File.expand_path("../lib", __FILE__)
|
3
|
+
require "data_bindings/version"
|
4
|
+
|
5
|
+
Gem::Specification.new do |s|
|
6
|
+
s.name = "data_bindings"
|
7
|
+
s.version = DataBindings::VERSION
|
8
|
+
s.authors = ["Joshual Hull"]
|
9
|
+
s.email = ["joshbuddy@gmail.com"]
|
10
|
+
s.homepage = "http://github.com/joshbuddy/data_bindings"
|
11
|
+
s.summary = %q{Bind data to and from things}
|
12
|
+
s.description = %q{Bind data to and from things.}
|
13
|
+
|
14
|
+
s.rubyforge_project = "data_bindings"
|
15
|
+
|
16
|
+
s.files = `git ls-files`.split("\n")
|
17
|
+
s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
18
|
+
s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
19
|
+
s.require_paths = ["lib"]
|
20
|
+
|
21
|
+
s.add_runtime_dependency 'hashie', '= 2.0.0.beta'
|
22
|
+
|
23
|
+
# specify any dependencies here; for example:
|
24
|
+
s.add_development_dependency 'bson'
|
25
|
+
s.add_development_dependency 'multi_json'
|
26
|
+
s.add_development_dependency 'nokogiri'
|
27
|
+
s.add_development_dependency 'builder'
|
28
|
+
s.add_development_dependency 'httparty'
|
29
|
+
s.add_development_dependency "minitest"
|
30
|
+
s.add_development_dependency "rake"
|
31
|
+
s.add_development_dependency "fakeweb"
|
32
|
+
s.add_development_dependency "yard"
|
33
|
+
s.add_development_dependency "redcarpet"
|
34
|
+
s.add_development_dependency "tnetstring"
|
35
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
module DataBindings
|
2
|
+
module Adapters
|
3
|
+
module BSON
|
4
|
+
include Ruby
|
5
|
+
include DataBindings::GemRequirement
|
6
|
+
|
7
|
+
# Constructs a wrapped object from a JSON string
|
8
|
+
# @param [String] str The JSON object
|
9
|
+
# @return [RubyObjectAdapter, RubyArrayAdapter] The wrapped object
|
10
|
+
def from_bson(str)
|
11
|
+
from_ruby(::BSON.deserialize(str.unpack("C*")))
|
12
|
+
end
|
13
|
+
gentle_require_gem :from_bson, 'bson'
|
14
|
+
|
15
|
+
module Convert
|
16
|
+
include ConverterHelper
|
17
|
+
include DataBindings::GemRequirement
|
18
|
+
|
19
|
+
# Creates a String repsentation of a Ruby Hash or Array.
|
20
|
+
# @param [Generator] generator The generator that invokes this constructor
|
21
|
+
# @param [Symbol] name The name of the binding used on this object
|
22
|
+
# @param [Array, Hash] obj The object to be represented in JSON
|
23
|
+
# @return [String] The JSON representation of this object
|
24
|
+
def force_convert_to_bson
|
25
|
+
::BSON.serialize(self).to_s
|
26
|
+
end
|
27
|
+
gentle_require_gem :force_convert_to_bson, 'bson'
|
28
|
+
standard_converter :convert_to_bson
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
module DataBindings
|
2
|
+
module Adapters
|
3
|
+
module JSON
|
4
|
+
include Ruby
|
5
|
+
include DataBindings::GemRequirement
|
6
|
+
|
7
|
+
# Constructs a wrapped object from a JSON string
|
8
|
+
# @param [String] str The JSON object
|
9
|
+
# @return [RubyObjectAdapter, RubyArrayAdapter] The wrapped object
|
10
|
+
def from_json(str)
|
11
|
+
from_ruby(MultiJson.decode(str))
|
12
|
+
end
|
13
|
+
gentle_require_gem :from_json, 'multi_json'
|
14
|
+
|
15
|
+
module Convert
|
16
|
+
include ConverterHelper
|
17
|
+
include DataBindings::GemRequirement
|
18
|
+
|
19
|
+
# Creates a String repsentation of a Ruby Hash or Array.
|
20
|
+
# @param [Generator] generator The generator that invokes this constructor
|
21
|
+
# @param [Symbol] name The name of the binding used on this object
|
22
|
+
# @param [Array, Hash] obj The object to be represented in JSON
|
23
|
+
# @return [String] The JSON representation of this object
|
24
|
+
def force_convert_to_json
|
25
|
+
MultiJson.encode(self)
|
26
|
+
end
|
27
|
+
gentle_require_gem :force_convert_to_json, 'multi_json'
|
28
|
+
standard_converter :convert_to_json
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
module DataBindings
|
2
|
+
module Adapters
|
3
|
+
module Native
|
4
|
+
# Constructs a wrapped object from a native Ruby object. This object is expected
|
5
|
+
# to respond to calls similar to those defined by #attr_accessor
|
6
|
+
# @param [Object] obj The object to be wrapped
|
7
|
+
# @return [NativeArrayAdapter, NativeObjectAdapter] The wrapped object
|
8
|
+
def from_native(obj)
|
9
|
+
binding_class(NativeAdapter).new(self, obj)
|
10
|
+
end
|
11
|
+
|
12
|
+
class NativeAdapter
|
13
|
+
include Unbound
|
14
|
+
|
15
|
+
def initialize(generator, object)
|
16
|
+
@generator, @object = generator, object
|
17
|
+
end
|
18
|
+
|
19
|
+
def pre_convert
|
20
|
+
raise DataBindings::UnboundError unless @name
|
21
|
+
end
|
22
|
+
|
23
|
+
def type
|
24
|
+
@object.is_a?(Array) ? :array : :hash
|
25
|
+
end
|
26
|
+
|
27
|
+
def [](idx)
|
28
|
+
val = @object.respond_to?(:[]) ? @object[idx] : @object.send(idx)
|
29
|
+
if DataBindings.primitive_value?(val)
|
30
|
+
val
|
31
|
+
else
|
32
|
+
binding_class(NativeAdapter).new(@generator, val)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
def []=(idx, value)
|
37
|
+
@object.respond_to?(:[]=) ? @object[idx] = value : @object.send("#{idx}=", value)
|
38
|
+
end
|
39
|
+
|
40
|
+
def key?(name)
|
41
|
+
@object.respond_to?(name)
|
42
|
+
end
|
43
|
+
|
44
|
+
def to_hash
|
45
|
+
raise UnboundError
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
@@ -0,0 +1,91 @@
|
|
1
|
+
require 'cgi'
|
2
|
+
|
3
|
+
module DataBindings
|
4
|
+
module Adapters
|
5
|
+
module Params
|
6
|
+
include Ruby
|
7
|
+
|
8
|
+
def from_params(str)
|
9
|
+
from_ruby( parse_nested_query(str) )
|
10
|
+
end
|
11
|
+
|
12
|
+
|
13
|
+
def parse_nested_query(qs, d = nil)
|
14
|
+
params = {}
|
15
|
+
|
16
|
+
(qs || '').split(d ? /[#{d}] */n : /[&;] */n).each do |p|
|
17
|
+
k, v = p.split('=', 2).map { |s| CGI::unescape(s) }
|
18
|
+
normalize_params(params, k, v)
|
19
|
+
end
|
20
|
+
|
21
|
+
return params
|
22
|
+
end
|
23
|
+
|
24
|
+
private
|
25
|
+
def normalize_params(params, name, v = nil)
|
26
|
+
name =~ %r(\A[\[\]]*([^\[\]]+)\]*)
|
27
|
+
k = $1 || ''
|
28
|
+
after = $' || ''
|
29
|
+
|
30
|
+
return if k.empty?
|
31
|
+
|
32
|
+
if after == ""
|
33
|
+
params[k] = v
|
34
|
+
elsif after == "[]"
|
35
|
+
params[k] ||= []
|
36
|
+
raise TypeError, "expected Array (got #{params[k].class.name}) for param `#{k}'" unless params[k].is_a?(Array)
|
37
|
+
params[k] << v
|
38
|
+
elsif after =~ %r(^\[\]\[([^\[\]]+)\]$) || after =~ %r(^\[\](.+)$)
|
39
|
+
child_key = $1
|
40
|
+
params[k] ||= []
|
41
|
+
raise TypeError, "expected Array (got #{params[k].class.name}) for param `#{k}'" unless params[k].is_a?(Array)
|
42
|
+
if params[k].last.is_a?(Hash) && !params[k].last.key?(child_key)
|
43
|
+
normalize_params(params[k].last, child_key, v)
|
44
|
+
else
|
45
|
+
params[k] << normalize_params({}, child_key, v)
|
46
|
+
end
|
47
|
+
else
|
48
|
+
params[k] ||= {}
|
49
|
+
raise TypeError, "expected Hash (got #{params[k].class.name}) for param `#{k}'" unless params[k].is_a?(Hash)
|
50
|
+
params[k] = normalize_params(params[k], after, v)
|
51
|
+
end
|
52
|
+
|
53
|
+
return params
|
54
|
+
end
|
55
|
+
|
56
|
+
module Convert
|
57
|
+
include ConverterHelper
|
58
|
+
|
59
|
+
# Creates a String repsentation of a Ruby Hash or Array.
|
60
|
+
# @param [Generator] generator The generator that invokes this constructor
|
61
|
+
# @param [Symbol] name The name of the binding used on this object
|
62
|
+
# @param [Array, Hash] obj The object to be represented in JSON
|
63
|
+
# @return [String] The JSON representation of this object
|
64
|
+
def force_convert_to_params
|
65
|
+
build_nested_query(to_hash)
|
66
|
+
end
|
67
|
+
standard_converter :convert_to_params
|
68
|
+
|
69
|
+
private
|
70
|
+
|
71
|
+
def build_nested_query(value, prefix = nil)
|
72
|
+
case value
|
73
|
+
when Array
|
74
|
+
index = 0
|
75
|
+
value.map { |v|
|
76
|
+
query_string = build_nested_query(v, prefix ? "#{prefix}[#{index}]" : index)
|
77
|
+
index += 1
|
78
|
+
query_string
|
79
|
+
}.join("&")
|
80
|
+
when Hash
|
81
|
+
value.map { |k, v|
|
82
|
+
build_nested_query(v, prefix ? "#{prefix}[#{CGI::escape(k)}]" : CGI::escape(k))
|
83
|
+
}.join("&")
|
84
|
+
else
|
85
|
+
"#{prefix}=#{CGI::escape(value.to_s)}"
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
module DataBindings
|
2
|
+
module Adapters
|
3
|
+
module Ruby
|
4
|
+
|
5
|
+
# Constructs a wrapped object from an Array or Hash
|
6
|
+
# @param [Array, Hash] obj The Ruby array or hash
|
7
|
+
# @return [RubyObjectAdapter, RubyArrayAdapter] The wrapped object
|
8
|
+
def from_ruby(obj)
|
9
|
+
case obj
|
10
|
+
when Array then from_ruby_array(obj)
|
11
|
+
when Hash then from_ruby_hash(obj)
|
12
|
+
else obj
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
def from_ruby_hash(h)
|
17
|
+
binding_class(RubyObjectAdapter).new(self, h)
|
18
|
+
end
|
19
|
+
alias_method :from_ruby_object, :from_ruby_hash
|
20
|
+
|
21
|
+
def from_ruby_array(a)
|
22
|
+
binding_class(RubyArrayAdapter).new(self, a)
|
23
|
+
end
|
24
|
+
|
25
|
+
class RubyArrayAdapter < Array
|
26
|
+
include Unbound
|
27
|
+
|
28
|
+
def initialize(generator, o)
|
29
|
+
@generator = generator
|
30
|
+
super o
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
class RubyObjectAdapter < IndifferentHash
|
35
|
+
include Unbound
|
36
|
+
|
37
|
+
def initialize(generator, o)
|
38
|
+
@generator = generator
|
39
|
+
replace o
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|