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.
@@ -0,0 +1,83 @@
1
+ module DataBindings
2
+
3
+ # Exception raised by invalid #*_http calls.
4
+ class HttpError < RuntimeError
5
+ # The HTTParty::Response object underlying this exception
6
+ attr_reader :response
7
+
8
+ def initialize(m, response)
9
+ super m
10
+ @response = response
11
+ end
12
+ end
13
+
14
+ # This defines the default readers used.
15
+ module Readers
16
+ include GemRequirement
17
+
18
+ # Takes an IO object and reads it's contents.
19
+ # @param [IO] i The IO object to read from
20
+ # @return The contents of the IO object
21
+ def io(i)
22
+ i.rewind
23
+ i.read
24
+ end
25
+
26
+ # Takes a file path and returns it's contents.
27
+ # @param [String] path The file path
28
+ # @return The contents of the file
29
+ def file(path)
30
+ File.read(path)
31
+ end
32
+
33
+ # Takes a URL and returns it's contents. Uses HTTPParty underlyingly.
34
+ # @param [String] url The URL to request from
35
+ # @param [Hash] opts The options to pass in to HTTPParty
36
+ # @return The body of the response from the URL as a String
37
+ # @see https://github.com/jnunemaker/httparty
38
+ def http(url, opts = {})
39
+ method = opts[:method] || :get
40
+ response = HTTParty.send(method, url, opts)
41
+ if (200..299).include?(response.code)
42
+ response.body
43
+ else
44
+ raise HttpError.new("Bad response: #{response.code} #{response.body}", response)
45
+ end
46
+ end
47
+ gentle_require_gem :http, 'httparty'
48
+ end
49
+
50
+ # This defines the default writers used.
51
+ module Writers
52
+ include GemRequirement
53
+
54
+ # Takes data and an IO object and writes it's contents to it.
55
+ # @param [String] data The data to be written
56
+ # @param [IO] i The IO object to write to
57
+ def io(data, io)
58
+ io.write(obj)
59
+ end
60
+
61
+ # Takes data and a file path and writes it's contents to it.
62
+ # @param [String] data The data to be written
63
+ # @param [String] path The IO object to write to
64
+ def file(data, path)
65
+ File.open(path, 'w') { |f| f << data }
66
+ end
67
+
68
+ # Takes a URL and posts the contents of your data to it. Uses HTTPParty underlyingly.
69
+ # @param [String] data The data to send to
70
+ # @param [String] url The URL to send your request to
71
+ # @param [Hash] opts The options to pass in to HTTPParty
72
+ # @see https://github.com/jnunemaker/httparty
73
+ def http(data, url, opts = {})
74
+ method = opts[:method] || :post
75
+ opts[:data] = data
76
+ response = HTTParty.send(method, url, opts)
77
+ unless (200..299).include?(response.code)
78
+ raise HttpError.new("Bad response: #{response.code} #{response.body}", response)
79
+ end
80
+ end
81
+ gentle_require_gem :http, 'httparty'
82
+ end
83
+ end
@@ -0,0 +1,140 @@
1
+ module DataBindings
2
+ # This is the class the handles registering readers, writers, adapters and types.
3
+ class Generator
4
+ # Enable/disable strict mode
5
+ attr_accessor :strict
6
+ alias_method :strict?, :strict
7
+
8
+ def initialize
9
+ reset!
10
+ end
11
+
12
+ # Defines an object type
13
+ # @param [Symbol] name The name of the type
14
+ # @see https://github.com/joshbuddy/data_bindings/wiki/Types
15
+ def type(name, &blk)
16
+ @types[name] = blk
17
+ end
18
+
19
+ # Retrieves an object type
20
+ # @param [Symbol] name The name of the type
21
+ # @return [Proc] The body of the type
22
+ def get_type(name)
23
+ @types[name]
24
+ end
25
+
26
+ def binding_class(cls)
27
+ mod = @writer_module
28
+ @binding_classes[cls] ||= begin
29
+ Class.new(cls) do
30
+ include mod
31
+ end
32
+ end
33
+ end
34
+
35
+ # Retrieves an adapter
36
+ # @param [Symbol] name The name of the adapter
37
+ # @return [Object] The adapter
38
+ def get_adapter(name)
39
+ @adapter_classes[name] or raise UnknownAdapterError, "Could not find adapter #{name.inspect}"
40
+ end
41
+
42
+ # Defines a reader
43
+ # @param [Symbol] name The name of the reader
44
+ # @yield [*Object] All arguments passed to the method used to invoke this reader
45
+ def reader(name, &blk)
46
+ @reader_module.define_singleton_method(name, &blk)
47
+ @build = false
48
+ end
49
+
50
+ # Defines a writer
51
+ # @param [Symbol] name The name of the writer
52
+ # @yield [*Object] All arguments passed to the method used to invoke this writer
53
+ def writer(name, &blk)
54
+ @writer_module.define_singleton_method(name, &blk)
55
+ end
56
+
57
+ # Passes off writing of an object through a specific writer.
58
+ # @param [Symbol] method_name The method name to be invoked on the writer
59
+ # @param [String] data The data to be written
60
+ def write(method_name, obj, *args, &blk)
61
+ @writer_module.send(method_name, obj, *args, &blk)
62
+ end
63
+
64
+ # Tests if a specific type of writer is supported
65
+ # @param [Symbol] name The name of the writer to test
66
+ # @return [Boolean]
67
+ def write_targets(name)
68
+ target, format = name.to_s.split(/_/, 2)
69
+ end
70
+
71
+ # Registers an adapter
72
+ # @param [Symbol] name The name of the adapter
73
+ # @param [Object] The adapter
74
+ def register(name, cls)
75
+ @adapters[name] = cls
76
+ @build = false
77
+ end
78
+
79
+ # Resets the generator to a blank state
80
+ def reset!
81
+ @reader_module = Module.new { extend Readers }
82
+ @writer_module = Module.new { extend Writers; include WritingInterceptor }
83
+ @strict = false
84
+ @types = {}
85
+ @adapters = {}
86
+ @adapter_classes = {}
87
+ @binding_classes = {}
88
+ end
89
+
90
+ # Defines a native constructor
91
+ # @param [Symbol] name The name of the type to create a constructor for
92
+ def for_native(name, &blk)
93
+ native_constructors[name] = blk
94
+ end
95
+
96
+ def native_constructors
97
+ @native_constructors ||= {}
98
+ end
99
+
100
+ private
101
+ # @api private
102
+ def build!
103
+ return if @built
104
+ @adapters.each do |name, cls|
105
+ unless @adapter_classes[name]
106
+ @adapter_classes[name] = cls.to_s.split('::').inject(Object) {|const, n| const.const_get(n)}
107
+ extend @adapter_classes[name]
108
+ @writer_module.send(:include, @adapter_classes[name]::Convert) if @adapter_classes[name].const_defined?(:Convert)
109
+ end
110
+ converter_methods = @reader_module.methods - Module.methods
111
+ converter_methods.each do |m|
112
+ method_name = :"from_#{name}_#{m}"
113
+ unless singleton_methods.include?(method_name)
114
+ define_singleton_method method_name do |*args|
115
+ out = @reader_module.send(m, *args)
116
+ send(:"from_#{name}", out)
117
+ end
118
+ end
119
+ end
120
+ end
121
+ @build = true
122
+ end
123
+ end
124
+
125
+ class DefaultGenerator < Generator
126
+ # Resets the generator to a blank state and installs the json, yaml, ruby and native
127
+ # adpaters
128
+ def reset!
129
+ super
130
+ register(:json, 'DataBindings::Adapters::JSON')
131
+ register(:yaml, 'DataBindings::Adapters::YAML')
132
+ register(:ruby, 'DataBindings::Adapters::Ruby')
133
+ register(:native, 'DataBindings::Adapters::Native')
134
+ register(:bson, 'DataBindings::Adapters::BSON')
135
+ register(:params, 'DataBindings::Adapters::Params')
136
+ register(:xml, 'DataBindings::Adapters::XML')
137
+ register(:tnetstring, 'DataBindings::Adapters::TNetstring')
138
+ end
139
+ end
140
+ end
@@ -0,0 +1,76 @@
1
+ module DataBindings
2
+ # Module that handles unbound objects
3
+ module Unbound
4
+
5
+ attr_reader :binding, :binding_name
6
+
7
+ def bind!(name = nil, &blk)
8
+ bind(name, &blk).valid!
9
+ end
10
+
11
+ def convert_target
12
+ bind { copy_source }
13
+ end
14
+
15
+ def bind(name = nil, opts = nil, &blk)
16
+ if name.is_a?(Unbound)
17
+ update_binding(name.binding_name, &name.binding)
18
+ else
19
+ name, opts = nil, name if name.is_a?(Hash) && opts.nil?
20
+ raise if name.nil? && blk.nil?
21
+ update_binding(name, &blk)
22
+ end
23
+ binding_class.new(@generator, @array, self, name, opts, &@binding)
24
+ end
25
+
26
+ def bind_array(type = nil, opts = nil, &blk)
27
+ type, opts = nil, type if type.is_a?(Hash) && opts.nil?
28
+ update_binding([], &blk)
29
+ binding_class.new(@generator, @array, self, nil, opts, &blk)
30
+ end
31
+
32
+ def type
33
+ if self.is_a?(Array)
34
+ :array
35
+ elsif self.is_a?(Hash)
36
+ :hash
37
+ else
38
+ raise
39
+ end
40
+ end
41
+
42
+ def hash?
43
+ type == :hash
44
+ end
45
+
46
+ def array?
47
+ type == :array
48
+ end
49
+
50
+ def to_native
51
+ array? ?
52
+ map{ |m| m.respond_to?(:to_native) ? m.to_native : m } :
53
+ OpenStruct.new(inject({}) {|h, (k, v)| v = @generator.from_ruby(v); h[k] = (v.respond_to?(:to_native) ? v.to_native : v); h})
54
+ end
55
+
56
+ def update_binding(name, &blk)
57
+ if name.is_a?(Array)
58
+ n = name.at(0)
59
+ @array = true
60
+ blk = proc { all_elements n }
61
+ name = nil
62
+ else
63
+ @array = false
64
+ end
65
+ @binding = @generator.get_type(name) || blk || raise(UnknownBindingError, "Unknown binding #{name.inspect}")
66
+ @name = name
67
+ end
68
+
69
+ def binding_class
70
+ case type
71
+ when :array then @generator.binding_class(Bound::BoundArray)
72
+ when :hash then @generator.binding_class(Bound::BoundObject)
73
+ end
74
+ end
75
+ end
76
+ end
@@ -0,0 +1,78 @@
1
+ module DataBindings
2
+ class FailedValidation < RuntimeError
3
+ attr_reader :errors, :original
4
+ def initialize(message, errors, original)
5
+ @errors, @original = errors, original
6
+ super message
7
+ end
8
+ end
9
+
10
+ UnboundError = Class.new(RuntimeError)
11
+ UnknownAdapterError = Class.new(RuntimeError)
12
+ UnknownBindingError = Class.new(RuntimeError)
13
+ BindingMismatch = Class.new(RuntimeError)
14
+
15
+ class IndifferentHash < Hash
16
+ include Hashie::Extensions::IndifferentAccess
17
+ end
18
+
19
+ module ConverterHelper
20
+ def self.included(m)
21
+ m.class_eval <<-EOT, __FILE__, __LINE__ +1
22
+ def self.standard_converter(m)
23
+ define_method(m) do
24
+ pre_convert if respond_to?(:pre_convert)
25
+ send(:"force_\#{m}")
26
+ end
27
+ end
28
+ EOT
29
+ end
30
+ end
31
+
32
+ module WritingInterceptor
33
+ def method_missing(m, *args, &blk)
34
+ if match = m.to_s.match(/^((?:force_)?convert_to_(?:[^_]+))_(.*)/)
35
+ self.class.class_eval <<-EOT, __FILE__, __LINE__ + 1
36
+ def #{m}(*args, &blk)
37
+ @generator.write(#{match[2].inspect}, send(#{match[1].inspect}, *args, &blk), *args, &blk)
38
+ end
39
+ EOT
40
+ send(m, *args, &blk)
41
+ else
42
+ super
43
+ end
44
+ end
45
+ end
46
+
47
+ module GemRequirement
48
+ def self.included(o)
49
+ o.extend ClassMethods
50
+ end
51
+
52
+ module ClassMethods
53
+ def gentle_require_gem(method, gem)
54
+ class_eval <<-EOT, __FILE__, __LINE__ + 1
55
+ alias_method :#{method}_without_gem, :#{method}
56
+ def #{method}(*args, &blk)
57
+ DataBindings::GemRequirement.gentle_require_gem #{gem.to_s.inspect}
58
+ class << self
59
+ self
60
+ end.instance_eval do
61
+ alias_method :#{method}, :#{method}_without_gem
62
+ end
63
+ #{method}(*args, &blk)
64
+ end
65
+ EOT
66
+ end
67
+ end
68
+
69
+ def self.gentle_require_gem(gem)
70
+ begin
71
+ require gem
72
+ rescue LoadError
73
+ warn "The `#{gem}' gem must be loadable"
74
+ exit 1
75
+ end
76
+ end
77
+ end
78
+ end
@@ -0,0 +1,3 @@
1
+ module DataBindings
2
+ VERSION = "0.0.1"
3
+ end
@@ -0,0 +1,52 @@
1
+ require 'hashie'
2
+
3
+ require 'data_bindings/util'
4
+ require 'data_bindings/generator'
5
+ require 'data_bindings/version'
6
+ require 'data_bindings/converters'
7
+ require 'data_bindings/bound'
8
+ require 'data_bindings/unbound'
9
+ require 'data_bindings/adapters'
10
+
11
+ # From https://github.com/marcandre/backports
12
+ module Kernel
13
+ # Standard in ruby 1.9. See official documentation[http://ruby-doc.org/core-1.9/classes/Object.html]
14
+ def define_singleton_method(*args, &block)
15
+ class << self
16
+ self
17
+ end.send(:define_method, *args, &block)
18
+ end unless method_defined? :define_singleton_method
19
+ end
20
+
21
+ # Top-level constant for DataBindings
22
+ module DataBindings
23
+
24
+ class << self
25
+ # Sends all methods calls to DefaultGenerator
26
+ def method_missing(m, *args, &blk)
27
+ DefaultGeneratorInstance.send(:build!)
28
+ DefaultGeneratorInstance.send(m, *args, &blk)
29
+ end
30
+
31
+ def type(name, &blk)
32
+ DefaultGeneratorInstance.type(name, &blk)
33
+ end
34
+
35
+ def true_boolean?(el)
36
+ el == true or el == 'true' or el == 1 or el == '1' or el == 'yes'
37
+ end
38
+
39
+ def primitive_value?(val)
40
+ case val
41
+ when Integer, Float, true, false, String, Symbol, nil
42
+ true
43
+ else
44
+ false
45
+ end
46
+ end
47
+ end
48
+
49
+ # Generator instance used by default when you make a call to DataBindings. This can act as a singleton, so, if you want your own
50
+ # generator, create an instance of it
51
+ DefaultGeneratorInstance = DefaultGenerator.new
52
+ end
@@ -0,0 +1,55 @@
1
+ require File.expand_path("../test_helper", __FILE__)
2
+
3
+ describe "Data Bindings array" do
4
+ before do
5
+ DataBindings.reset!
6
+ end
7
+
8
+ describe "from bind" do
9
+ it "should validate a list of integers" do
10
+ a = DataBindings.from_json("[1,2,3]").bind([Integer])
11
+ assert a.valid?
12
+ assert a.errors.empty?
13
+ assert_equal [1, 2, 3], [a[0], a[1], a[2]]
14
+ a.unshift 'asd'
15
+ refute a.valid?
16
+ refute a.errors.empty?
17
+ end
18
+
19
+ it "should bind a list of complex things" do
20
+ DataBindings.type(:person) { property :name, String }
21
+ a = DataBindings.from_json('[{"name":"a"},{"name":"b"},{"name":"c"}]').bind([:person])
22
+ assert a.valid?
23
+ assert a.errors.empty?
24
+ assert_equal 3, a.size
25
+ assert_equal 'c', a[2][:name]
26
+ a.unshift 'asd'
27
+ refute a.valid?
28
+ refute a.errors.empty?
29
+ end
30
+
31
+ it "should validate a list" do
32
+ assert DataBindings.from_json("[1,2,3]").bind([]).valid?
33
+ refute DataBindings.from_json("[1,2,3]").bind([], :length => 2).valid?
34
+ assert_raises(DataBindings::BindingMismatch) { DataBindings.from_json("{}").bind([]) }
35
+ end
36
+ end
37
+
38
+ describe "from within a bind" do
39
+ it "should validate a list of integers" do
40
+ a = DataBindings.from_json('{"a":[1,2,3]}').bind { property :a, [Integer] }
41
+ assert a.valid?
42
+ assert a.errors.empty?
43
+ assert_equal [1, 2, 3], [a[:a][0], a[:a][1], a[:a][2]]
44
+ a[:a].unshift 'asd'
45
+ refute a.valid?
46
+ refute a.errors.empty?
47
+ end
48
+
49
+ it "should validate a list" do
50
+ assert DataBindings.from_json('{"a":[1,2,3]}').bind { property :a, [Integer], :length => 3 }.valid?
51
+ refute DataBindings.from_json('{"a":[1,2,3,4]}').bind { property :a, [Integer], :length => 3 }.valid?
52
+ end
53
+
54
+ end
55
+ end