hashie-model 1.0.0.alpha

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/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