data_bindings 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/.gitignore ADDED
@@ -0,0 +1,5 @@
1
+ Gemfile.lock
2
+ .yardoc
3
+ doc
4
+ Gemfile.lock
5
+ .DS_Store
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source :rubygems
2
+
3
+ gemspec
4
+ gem 'hashie', :git => 'https://github.com/intridea/hashie.git'
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