hashie-model 1.0.0.alpha

Sign up to get free protection for your applications and to get access to all the features.
data/LICENSE.txt ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2011 Istvan Hoka
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,3 @@
1
+ # Hashie Model
2
+
3
+ © 2011 Doublewide Partners I, LLC
@@ -0,0 +1,106 @@
1
+ # Generated by jeweler
2
+ # DO NOT EDIT THIS FILE DIRECTLY
3
+ # Instead, edit Jeweler::Tasks in Rakefile, and run 'rake gemspec'
4
+ # -*- encoding: utf-8 -*-
5
+
6
+ Gem::Specification.new do |s|
7
+ s.name = %q{hashie-model}
8
+ s.version = "1.0.0.alpha"
9
+
10
+ s.required_rubygems_version = Gem::Requirement.new("> 1.3.1") if s.respond_to? :required_rubygems_version=
11
+ s.authors = [%q{ZenCash.com}]
12
+ s.date = %q{2011-11-01}
13
+ s.description = %q{Hashie + ActiveModel 3, offering declared properties, validations, JSON serialization/deserialization}
14
+ s.email = %q{istvan@zencash.com}
15
+ s.extra_rdoc_files = [
16
+ "LICENSE.txt",
17
+ "README.md"
18
+ ]
19
+ s.files = [
20
+ "LICENSE.txt",
21
+ "README.md",
22
+ "hashie-model.gemspec",
23
+ "lib/hashie-model.rb",
24
+ "lib/hashie_model/array_of.rb",
25
+ "lib/hashie_model/associated_validator.rb",
26
+ "lib/hashie_model/base.rb",
27
+ "lib/hashie_model/money.rb",
28
+ "lib/hashie_model/spec/matchers.rb",
29
+ "lib/hashie_model/version.rb",
30
+ "vendor/hashie/lib/hashie.rb",
31
+ "vendor/hashie/lib/hashie/clash.rb",
32
+ "vendor/hashie/lib/hashie/dash.rb",
33
+ "vendor/hashie/lib/hashie/extensions/coercion.rb",
34
+ "vendor/hashie/lib/hashie/extensions/deep_merge.rb",
35
+ "vendor/hashie/lib/hashie/extensions/indifferent_access.rb",
36
+ "vendor/hashie/lib/hashie/extensions/key_conversion.rb",
37
+ "vendor/hashie/lib/hashie/extensions/merge_initializer.rb",
38
+ "vendor/hashie/lib/hashie/extensions/method_access.rb",
39
+ "vendor/hashie/lib/hashie/extensions/structure.rb",
40
+ "vendor/hashie/lib/hashie/hash.rb",
41
+ "vendor/hashie/lib/hashie/hash_extensions.rb",
42
+ "vendor/hashie/lib/hashie/mash.rb",
43
+ "vendor/hashie/lib/hashie/trash.rb",
44
+ "vendor/hashie/lib/hashie/version.rb"
45
+ ]
46
+ s.homepage = %q{http://github.com/doublewide/hashie-model}
47
+ s.licenses = [%q{MIT}]
48
+ s.require_paths = [%q{lib}]
49
+ s.rubygems_version = %q{1.8.6}
50
+ s.summary = %q{Hashie + ActiveModel 3.x}
51
+
52
+ if s.respond_to? :specification_version then
53
+ s.specification_version = 3
54
+
55
+ if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then
56
+ s.add_runtime_dependency(%q<activesupport>, [">= 3.1.1"])
57
+ s.add_runtime_dependency(%q<activemodel>, [">= 3.1.1"])
58
+ s.add_runtime_dependency(%q<money>, [">= 3.7.1"])
59
+ s.add_development_dependency(%q<rspec>, ["~> 2.3.0"])
60
+ s.add_development_dependency(%q<shoulda-matchers>, ["~> 1.0.0.beta3"])
61
+ s.add_development_dependency(%q<yard>, [">= 0.7.0"])
62
+ s.add_development_dependency(%q<bundler>, ["~> 1.0.0"])
63
+ s.add_development_dependency(%q<jeweler>, ["~> 1.6.4"])
64
+ s.add_development_dependency(%q<rcov>, [">= 0"])
65
+ s.add_development_dependency(%q<rb-fsevent>, [">= 0"])
66
+ s.add_development_dependency(%q<growl>, [">= 0"])
67
+ s.add_development_dependency(%q<guard>, [">= 0"])
68
+ s.add_development_dependency(%q<guard-rspec>, [">= 0"])
69
+ s.add_development_dependency(%q<guard-yard>, [">= 0"])
70
+ s.add_development_dependency(%q<guard-bundler>, [">= 0"])
71
+ else
72
+ s.add_dependency(%q<activesupport>, [">= 3.1.1"])
73
+ s.add_dependency(%q<activemodel>, [">= 3.1.1"])
74
+ s.add_dependency(%q<money>, [">= 3.7.1"])
75
+ s.add_dependency(%q<rspec>, ["~> 2.3.0"])
76
+ s.add_dependency(%q<shoulda-matchers>, ["~> 1.0.0.beta3"])
77
+ s.add_dependency(%q<yard>, [">= 0.7.0"])
78
+ s.add_dependency(%q<bundler>, ["~> 1.0.0"])
79
+ s.add_dependency(%q<jeweler>, ["~> 1.6.4"])
80
+ s.add_dependency(%q<rcov>, [">= 0"])
81
+ s.add_dependency(%q<rb-fsevent>, [">= 0"])
82
+ s.add_dependency(%q<growl>, [">= 0"])
83
+ s.add_dependency(%q<guard>, [">= 0"])
84
+ s.add_dependency(%q<guard-rspec>, [">= 0"])
85
+ s.add_dependency(%q<guard-yard>, [">= 0"])
86
+ s.add_dependency(%q<guard-bundler>, [">= 0"])
87
+ end
88
+ else
89
+ s.add_dependency(%q<activesupport>, [">= 3.1.1"])
90
+ s.add_dependency(%q<activemodel>, [">= 3.1.1"])
91
+ s.add_dependency(%q<money>, [">= 3.7.1"])
92
+ s.add_dependency(%q<rspec>, ["~> 2.3.0"])
93
+ s.add_dependency(%q<shoulda-matchers>, ["~> 1.0.0.beta3"])
94
+ s.add_dependency(%q<yard>, [">= 0.7.0"])
95
+ s.add_dependency(%q<bundler>, ["~> 1.0.0"])
96
+ s.add_dependency(%q<jeweler>, ["~> 1.6.4"])
97
+ s.add_dependency(%q<rcov>, [">= 0"])
98
+ s.add_dependency(%q<rb-fsevent>, [">= 0"])
99
+ s.add_dependency(%q<growl>, [">= 0"])
100
+ s.add_dependency(%q<guard>, [">= 0"])
101
+ s.add_dependency(%q<guard-rspec>, [">= 0"])
102
+ s.add_dependency(%q<guard-yard>, [">= 0"])
103
+ s.add_dependency(%q<guard-bundler>, [">= 0"])
104
+ end
105
+ end
106
+
@@ -0,0 +1,28 @@
1
+ require 'active_model'
2
+
3
+ # Load vendored Hashie unless it is already loaded.
4
+ # We must have version >= 2.0 in order to get the Conversions API.
5
+ if defined?(Hashie)
6
+ raise "Hashie >= 2.0.0 required by HashieModel." unless Hashie::VERSION.split('.').first.to_i >= 2
7
+ else
8
+ $:.unshift File.expand_path(File.join('..', 'vendor', 'hashie', 'lib'), File.dirname(__FILE__))
9
+ end
10
+ require 'hashie'
11
+
12
+ require 'active_support'
13
+ require 'active_support/core_ext'
14
+ require 'active_support/json'
15
+ require 'money'
16
+
17
+ module HashieModel
18
+ extend ActiveSupport::Autoload
19
+
20
+ autoload :ArrayOf
21
+ autoload :AssociatedValidator
22
+ autoload :Base
23
+ autoload :Money
24
+
25
+ autoload :Version
26
+
27
+ include ArrayOf
28
+ end
@@ -0,0 +1,15 @@
1
+ module HashieModel
2
+ module ArrayOf
3
+ extend ActiveSupport::Concern
4
+
5
+ module ClassMethods
6
+ def array_of(klass)
7
+ Class.new(Array) do
8
+ define_method(:initialize) do |values|
9
+ replace values.map { |value| klass.new(value) }
10
+ end
11
+ end
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,8 @@
1
+ module HashieModel
2
+ class AssociatedValidator < ActiveModel::EachValidator
3
+ def validate_each(record, attribute, value)
4
+ return if (value.is_a?(Array) ? value : [value]).collect{ |r| r.nil? || r.valid? }.all?
5
+ record.errors.add(attribute, :invalid, options.merge(:value => value))
6
+ end
7
+ end
8
+ end
@@ -0,0 +1,74 @@
1
+ module HashieModel
2
+ class Base < Hashie::Dash
3
+ extend ActiveModel::Naming
4
+
5
+ include Hashie::Extensions::Coercion
6
+
7
+ include ActiveModel::AttributeMethods
8
+ include ActiveModel::Conversion
9
+ include ActiveModel::Validations
10
+
11
+ attribute_method_suffix '?', '_before_type_cast'
12
+
13
+ class << self
14
+ def prop(name, type = nil, options = {})
15
+ if type.is_a?(Hash)
16
+ options = type
17
+ type = nil
18
+ end
19
+
20
+ property(name, options)
21
+
22
+ if type
23
+ coerce_key(name.to_sym, type)
24
+ coerce_key(name.to_s, type)
25
+ end
26
+
27
+ define_attribute_methods [name]
28
+ alias_method :"#{name}_without_type_cast=", :"#{name}="
29
+
30
+ define_method(:"#{name}=") do |value|
31
+ attributes_before_type_cast[name.to_s] = value
32
+ send(:"#{name}_without_type_cast=", value)
33
+ end
34
+ end
35
+
36
+ def from_json(json)
37
+ new(ActiveSupport::JSON.decode(json))
38
+ end
39
+
40
+ def validates_associated(*attr_names)
41
+ validates_with HashieModel::AssociatedValidator, _merge_attributes(attr_names)
42
+ end
43
+ end
44
+
45
+ def attributes_before_type_cast
46
+ @attributes_before_type_cast ||= {}
47
+ end
48
+
49
+ def attributes
50
+ {}.tap do |attrs|
51
+ self.class.properties.each do |key|
52
+ attrs[key] = self[key]
53
+ end
54
+ end
55
+ end
56
+
57
+ def persisted?
58
+ false
59
+ end
60
+
61
+ protected
62
+ def attribute?(key)
63
+ self[key].present?
64
+ end
65
+
66
+ def reset_attribute(key)
67
+ self[key] = self.class.defaults[key.to_sym]
68
+ end
69
+
70
+ def attribute_before_type_cast(key)
71
+ attributes_before_type_cast[key.to_s]
72
+ end
73
+ end
74
+ end
@@ -0,0 +1,12 @@
1
+ module HashieModel
2
+ class Money < ::Money
3
+ def self.coerce(value)
4
+ cents = value.is_a?(Money) ? value.cents : value.to_money(:USD).cents
5
+ new(cents, :USD)
6
+ end
7
+
8
+ def as_json(options=nil)
9
+ to_s
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,86 @@
1
+ RSpec::Matchers.define :have_property do |*expected|
2
+ options = expected.extract_options!
3
+ property_name = expected.first
4
+ coercion = expected.second
5
+
6
+ description {
7
+ desc = "have"
8
+ desc << " required" if options[:required]
9
+ desc << " property :#{property_name}"
10
+ desc << ", coerced to #{coercion}" if coercion
11
+ desc << ", with default '#{options[:default]}'" if options.has_key?(:default)
12
+ desc
13
+ }
14
+
15
+ match do |actual|
16
+ property_exists = actual.class.properties.include?(property_name)
17
+
18
+ default_matches = if options.has_key?(:default)
19
+ actual.class.defaults[property_name] == options[:default]
20
+ else
21
+ true
22
+ end
23
+
24
+ required_matches = if options[:required]
25
+ actual.class.required_properties.include?(property_name)
26
+ else
27
+ !actual.class.required_properties.include?(property_name)
28
+ end
29
+
30
+ coercion_matches = if coercion
31
+ actual.class.key_coercion(property_name.to_sym) == coercion &&
32
+ actual.class.key_coercion(property_name.to_s) == coercion
33
+ else
34
+ actual.class.key_coercion(property_name.to_sym).nil? &&
35
+ actual.class.key_coercion(property_name.to_s).nil?
36
+ end
37
+
38
+ property_exists && default_matches && required_matches && coercion_matches
39
+ end
40
+ end
41
+
42
+ RSpec::Matchers.define :serialize_to do |json|
43
+ description { "serialize to JSON" }
44
+ expected = ActiveSupport::JSON.decode(json)
45
+
46
+ match do |object|
47
+ actual = ActiveSupport::JSON.decode(object.to_json)
48
+ actual == expected
49
+ end
50
+
51
+ failure_message_for_should do |object|
52
+ actual = ActiveSupport::JSON.decode(object.to_json)
53
+ "expected that #{object} would serialize to #{expected.pretty_inspect}Diff:#{RSpec::Expectations.differ.diff_as_object(actual, expected)}"
54
+ end
55
+ end
56
+
57
+ RSpec::Matchers.define :deserialize_from do |json|
58
+ description { "deserialize from JSON" }
59
+
60
+ match do |object|
61
+ new_object = object.class.from_json(json)
62
+ new_object.is_a?(object.class) && new_object == object
63
+ end
64
+ end
65
+
66
+ RSpec::Matchers.define :act_as_array do
67
+ match do |object|
68
+ klass = object.class
69
+ array_klass = ZCQ::Component.const_get(:"ArrayOf#{klass.name.demodulize}s")
70
+
71
+ actual = [object]
72
+ expected = array_klass.new([object.as_json])
73
+
74
+ actual == expected
75
+ end
76
+ end
77
+
78
+ RSpec::Matchers.define :validate_associated do |attribute|
79
+ match do |component|
80
+ component.stub(attribute => mock("#{component.class}.#{attribute}", :valid? => false))
81
+ is_invalid = !component.valid?
82
+ right_error_count = component.errors[attribute].size == 1
83
+
84
+ is_invalid && right_error_count
85
+ end
86
+ end
@@ -0,0 +1,10 @@
1
+ module HashieModel
2
+ module Version
3
+ MAJOR = 1
4
+ MINOR = 0
5
+ PATCH = 0
6
+ BUILD = 'alpha'
7
+
8
+ STRING = [MAJOR, MINOR, PATCH, BUILD].compact.join('.')
9
+ end
10
+ end
@@ -0,0 +1,23 @@
1
+ module Hashie
2
+ autoload :Clash, 'hashie/clash'
3
+ autoload :Dash, 'hashie/dash'
4
+ autoload :Hash, 'hashie/hash'
5
+ autoload :HashExtensions, 'hashie/hash_extensions'
6
+ autoload :Mash, 'hashie/mash'
7
+ autoload :PrettyInspect, 'hashie/hash_extensions'
8
+ autoload :Trash, 'hashie/trash'
9
+
10
+ module Extensions
11
+ autoload :Coercion, 'hashie/extensions/coercion'
12
+ autoload :DeepMerge, 'hashie/extensions/deep_merge'
13
+ autoload :KeyConversion, 'hashie/extensions/key_conversion'
14
+ autoload :IndifferentAccess, 'hashie/extensions/indifferent_access'
15
+ autoload :MergeInitializer, 'hashie/extensions/merge_initializer'
16
+ autoload :MethodAccess, 'hashie/extensions/method_access'
17
+ autoload :MethodQuery, 'hashie/extensions/method_access'
18
+ autoload :MethodReader, 'hashie/extensions/method_access'
19
+ autoload :MethodWriter, 'hashie/extensions/method_access'
20
+ autoload :StringifyKeys, 'hashie/extensions/key_conversion'
21
+ autoload :SymbolizeKeys, 'hashie/extensions/key_conversion'
22
+ end
23
+ end
@@ -0,0 +1,86 @@
1
+ require 'hashie/hash'
2
+
3
+ module Hashie
4
+ #
5
+ # A Clash is a "Chainable Lazy Hash". Inspired by libraries such as Arel,
6
+ # a Clash allows you to chain together method arguments to build a
7
+ # hash, something that's especially useful if you're doing something
8
+ # like constructing a complex options hash. Here's a basic example:
9
+ #
10
+ # c = Hashie::Clash.new.conditions(:foo => 'bar').order(:created_at)
11
+ # c # => {:conditions => {:foo => 'bar'}, :order => :created_at}
12
+ #
13
+ # Clash provides another way to create sub-hashes by using bang notation.
14
+ # You can dive into a sub-hash by providing a key with a bang and dive
15
+ # back out again with the _end! method. Example:
16
+ #
17
+ # c = Hashie::Clash.new.conditions!.foo('bar').baz(123)._end!.order(:created_at)
18
+ # c # => {:conditions => {:foo => 'bar', :baz => 123}, :order => :created_at}
19
+ #
20
+ # Because the primary functionality of Clash is to build options objects,
21
+ # all keys are converted to symbols since many libraries expect symbols explicitly
22
+ # for keys.
23
+ #
24
+ class Clash < ::Hash
25
+ class ChainError < ::StandardError; end
26
+ # The parent Clash if this Clash was created via chaining.
27
+ attr_reader :_parent
28
+
29
+ # Initialize a new clash by passing in a Hash to
30
+ # convert and, optionally, the parent to which this
31
+ # Clash is chained.
32
+ def initialize(other_hash = {}, parent = nil)
33
+ @_parent = parent
34
+ other_hash.each_pair do |k, v|
35
+ self[k.to_sym] = v
36
+ end
37
+ end
38
+
39
+ # Jump back up a level if you are using bang method
40
+ # chaining. For example:
41
+ #
42
+ # c = Hashie::Clash.new.foo('bar')
43
+ # c.baz!.foo(123) # => c[:baz]
44
+ # c.baz!._end! # => c
45
+ def _end!
46
+ self._parent
47
+ end
48
+
49
+ def id(*args) #:nodoc:
50
+ method_missing(:id, *args)
51
+ end
52
+
53
+ def merge_store(key, *args) #:nodoc:
54
+ case args.length
55
+ when 1
56
+ val = args.first
57
+ val = self[key].merge(val) if self[key].is_a?(::Hash) && val.is_a?(::Hash)
58
+ else
59
+ val = args
60
+ end
61
+
62
+ self[key.to_sym] = val
63
+ self
64
+ end
65
+
66
+ def method_missing(name, *args) #:nodoc:
67
+ name = name.to_s
68
+ if name.match(/!$/) && args.empty?
69
+ key = name[0...-1].to_sym
70
+
71
+ if self[key].nil?
72
+ self[key] = Clash.new({}, self)
73
+ elsif self[key].is_a?(::Hash) && !self[key].is_a?(Clash)
74
+ self[key] = Clash.new(self[key], self)
75
+ else
76
+ raise ChainError, "Tried to chain into a non-hash key."
77
+ end
78
+
79
+ self[key]
80
+ elsif args.any?
81
+ key = name.to_sym
82
+ self.merge_store(key, *args)
83
+ end
84
+ end
85
+ end
86
+ end