data_bindings 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
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