hashme 0.1.2 → 0.2.0

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 2663088632661682c045a62e2674257dd342c568
4
- data.tar.gz: 24af504823c262c52ab5c951ecceb8fa17a8f2ec
3
+ metadata.gz: b1baf318f249321f9486052a01db0881479f461c
4
+ data.tar.gz: f5b623806198098e2d5e4d6fa42c60cb6137498e
5
5
  SHA512:
6
- metadata.gz: ffec5164dd1d0920674d067e1b1c1c354ed6db3333a0c003850f4cf847ef564ef236e0516c698f15d82f36ea8e167ff5f3d4f67c4126e633df080e4f0248df50
7
- data.tar.gz: 99637e30e2c635a4955c3f461eef856bd8d1ace88619eab3f83aa0e3d9705b6495248add82ba9fc7a9e1230cb29cdec76d894e0757dba655c71932bf6eda1678
6
+ metadata.gz: 9bdfc2fea9f19cccd974a2a8d0b8cfad142bb91af0f35d1ba40a91f0fe0049ae085633d48f1abf9e67f211f6bbdb811bb6961e2ebf491f5858bdfcbc4494933b
7
+ data.tar.gz: 9e1b4fb53dde4cafd41b9e5917a7a7e12ea22b036cd330ae3eebf79b749a89897afe3f18bb346ad1e524ceb91a27f0af9e1e3a5be1e65d255ce983a6f7ca26d2
data/.rspec ADDED
@@ -0,0 +1 @@
1
+ --colour
data/README.md CHANGED
@@ -33,16 +33,18 @@ class Cat
33
33
 
34
34
  property :name, String
35
35
  property :description, String
36
+ property :dob, Date
36
37
  end
37
38
 
38
39
  # Do something with it
39
- kitty = Cat.new(:name => "Catso", :description => "Meows a lot")
40
+ kitty = Cat.new(:name => "Catso", :description => "Meows a lot", :dob => '2012-02-03')
40
41
  kitty.name # Catso
41
- kitty.to_hash # {:name => "Catso", :description => "Meows a lot"}
42
- kitty.to_json # "{\"name\":\"Catso\",\"description\":\"Meows a lot\"}"
42
+ kitty.to_hash # {:name => "Catso", :description => "Meows a lot", :dob => "2012-02-03"}
43
+ kitty.to_json # "{\"name\":\"Catso\",\"description\":\"Meows a lot\",\"dob\":\"2012-02-03\"}"
43
44
 
44
45
  kitty2 = Cat.new(kitty.to_hash)
45
46
  kitty2.to_hash == kitty.to_hash # true!
47
+ kitty2.dob.is_a?(Date) # true!
46
48
  ````
47
49
 
48
50
  Models can also be nested, which is probably the most useful part:
@@ -75,6 +77,24 @@ kennel = Kennel.new(store)
75
77
  kennel.cats.length == 2 # true!
76
78
  ````
77
79
 
80
+ Active Model Validation is included out the box:
81
+
82
+ ````ruby
83
+ class User
84
+ include Hashme
85
+
86
+ property :name, String
87
+ property :email, String
88
+
89
+ validates :name, :email, presence: true
90
+ end
91
+
92
+ u = User.new(name: "Sam")
93
+ u.valid? # false !
94
+ u.errors.first # [:email, "can't be blank"]
95
+ ````
96
+
97
+
78
98
  ## Contributing
79
99
 
80
100
  1. Fork it
@@ -89,10 +109,16 @@ kennel.cats.length == 2 # true!
89
109
 
90
110
  ## History
91
111
 
112
+ ### 0.2.0 - 2016-06-02
113
+
114
+ * Added support for advanced type casting, copy stuff from CouchRest Model.
115
+ * Upgrade to latest rspec version.
116
+ * Removed `Property#cast` and switch to just `Property#build`.
117
+
92
118
  ### 0.1.2 - 2014-03-10
93
119
 
94
120
  * Set default property values on object initialization.
95
- * Refactoring to use `class_atrribute` for properties hash for improved inheritance.
121
+ * Refactoring to use `class_attribute` for properties hash for improved inheritance.
96
122
 
97
123
  ### 0.1.1 - 2014-01-21
98
124
 
data/lib/hashme.rb CHANGED
@@ -16,6 +16,7 @@ require "hashme/castable"
16
16
  require "hashme/casted_array"
17
17
  require "hashme/properties"
18
18
  require "hashme/property"
19
+ require "hashme/property_casting"
19
20
 
20
21
  module Hashme
21
22
  extend ActiveSupport::Concern
@@ -20,7 +20,7 @@ module Hashme
20
20
  :encode_json, :as_json, :to_json,
21
21
  :inspect, :any?
22
22
 
23
- def initialize(owner, property, values = [])
23
+ def initialize(property, owner, values = [])
24
24
  @_array = []
25
25
  self.casted_by = owner
26
26
  self.casted_by_property = property
@@ -54,7 +54,7 @@ module Hashme
54
54
  protected
55
55
 
56
56
  def instantiate_and_build(obj)
57
- casted_by_property.build(casted_by, obj)
57
+ casted_by_property.build(self, obj)
58
58
  end
59
59
 
60
60
  end
@@ -16,7 +16,7 @@ module Hashme
16
16
  if property.nil?
17
17
  self[name.to_sym] = value
18
18
  else
19
- self[property.name] = value.present? ? property.cast(self, value) : value
19
+ self[property.name] = property.build(self, value)
20
20
  end
21
21
  end
22
22
 
@@ -1,24 +1,22 @@
1
1
  module Hashme
2
2
  class Property
3
3
 
4
- attr_accessor :name, :type, :default
4
+ attr_reader :name, :type, :default, :array
5
5
 
6
6
  def initialize(name, type, opts = {})
7
- self.name = name.to_sym
7
+ @name = name.to_sym
8
8
 
9
9
  # Always set type to base type
10
10
  if type.is_a?(Array) && !type.first.nil?
11
- @_use_casted_array = true
12
- klass = type.first
11
+ @array = true
12
+ @type = type.first
13
13
  else
14
- @_use_casted_array = false
15
- klass = type
14
+ @array = false
15
+ @type = type
16
16
  end
17
17
 
18
- self.type = klass
19
-
20
18
  # Handle options
21
- self.default = opts[:default]
19
+ @default = opts[:default]
22
20
  end
23
21
 
24
22
  def to_s
@@ -29,34 +27,13 @@ module Hashme
29
27
  name
30
28
  end
31
29
 
32
- # Use cast method when we do not know if we may need to handle a
33
- # casted array of objects.
34
- def cast(owner, value)
35
- if use_casted_array?
36
- CastedArray.new(owner, self, value)
37
- else
38
- build(owner, value)
39
- end
40
- end
41
-
42
30
  # Build a new object of the type defined by the property.
43
- # Will not deal create CastedArrays!
44
31
  def build(owner, value)
45
- obj = nil
46
- if value.is_a?(type)
47
- obj = value
48
- elsif type == Date
49
- obj = type.parse(value)
32
+ if array && value.is_a?(Array)
33
+ CastedArray.new(self, owner, value)
50
34
  else
51
- obj = type.new(value)
35
+ PropertyCasting.cast(self, owner, value)
52
36
  end
53
- obj.casted_by = owner if obj.respond_to?(:casted_by=)
54
- obj.casted_by_property = self if obj.respond_to?(:casted_by_property=)
55
- obj
56
- end
57
-
58
- def use_casted_array?
59
- @_use_casted_array
60
37
  end
61
38
 
62
39
  end
@@ -0,0 +1,194 @@
1
+ module Hashme
2
+
3
+ # Special property casting for reveiving data from sources without Ruby types, such as query
4
+ # parameters from an API or JSON documents.
5
+ #
6
+ # Most of this code is stolen from CouchRest Model typecasting, with a few simplifications.
7
+ module PropertyCasting
8
+ extend self
9
+
10
+ CASTABLE_TYPES = [String, Symbol, TrueClass, Integer, Float, BigDecimal, DateTime, Time, Date, Class]
11
+
12
+ # Automatically typecast the provided value into an instance of the provided type.
13
+ def cast(property, owner, value)
14
+ return nil if value.nil?
15
+ type = property.type
16
+ if value.instance_of?(type) || type == Object
17
+ value
18
+ elsif CASTABLE_TYPES.include?(type)
19
+ send('typecast_to_'+type.to_s.downcase, value)
20
+ else
21
+ # Complex objects we don't know how to cast
22
+ type.new(value).tap do |obj|
23
+ obj.casted_by = owner if obj.respond_to?(:casted_by=)
24
+ obj.casted_by_property = property if obj.respond_to?(:casted_by_property=)
25
+ end
26
+ end
27
+ end
28
+
29
+ protected
30
+
31
+ # Typecast a value to an Integer
32
+ def typecast_to_integer(value)
33
+ typecast_to_numeric(value, :to_i)
34
+ end
35
+
36
+ # Typecast a value to a BigDecimal
37
+ def typecast_to_bigdecimal(value)
38
+ typecast_to_numeric(value, :to_d)
39
+ end
40
+
41
+ # Typecast a value to a Float
42
+ def typecast_to_float(value)
43
+ typecast_to_numeric(value, :to_f)
44
+ end
45
+
46
+ # Convert some kind of object to a number that of the type
47
+ # provided.
48
+ #
49
+ # When a string is provided, It'll attempt to filter out
50
+ # region specific details such as commas instead of points
51
+ # for decimal places, text units, and anything else that is
52
+ # not a number and a human could make out.
53
+ #
54
+ # Esentially, the aim is to provide some kind of sanitary
55
+ # conversion from values in incoming http forms.
56
+ #
57
+ # If what we get makes no sense at all, nil it.
58
+ def typecast_to_numeric(value, method)
59
+ if value.is_a?(String)
60
+ value = value.strip.gsub(/,/, '.').gsub(/[^\d\-\.]/, '').gsub(/\.(?!\d*\Z)/, '')
61
+ value.empty? ? nil : value.send(method)
62
+ elsif value.respond_to?(method)
63
+ value.send(method)
64
+ else
65
+ nil
66
+ end
67
+ end
68
+
69
+ # Typecast a value to a String
70
+ def typecast_to_string(value)
71
+ value.to_s
72
+ end
73
+
74
+ def typecast_to_symbol(value)
75
+ value.is_a?(Symbol) || !value.to_s.empty? ? value.to_sym : nil
76
+ end
77
+
78
+ # Typecast a value to a true or false
79
+ def typecast_to_trueclass(value)
80
+ if value.kind_of?(Integer)
81
+ return true if value == 1
82
+ return false if value == 0
83
+ elsif value.respond_to?(:to_s)
84
+ return true if %w[ true 1 t ].include?(value.to_s.downcase)
85
+ return false if %w[ false 0 f ].include?(value.to_s.downcase)
86
+ end
87
+ nil
88
+ end
89
+
90
+ # Typecasts an arbitrary value to a DateTime.
91
+ # Handles both Hashes and DateTime instances.
92
+ # This is slow!! Use Time instead.
93
+ def typecast_to_datetime(value)
94
+ if value.is_a?(Hash)
95
+ typecast_hash_to_datetime(value)
96
+ else
97
+ DateTime.parse(value.to_s)
98
+ end
99
+ rescue ArgumentError
100
+ nil
101
+ end
102
+
103
+ # Typecasts an arbitrary value to a Date
104
+ # Handles both Hashes and Date instances.
105
+ def typecast_to_date(value)
106
+ if value.is_a?(Hash)
107
+ typecast_hash_to_date(value)
108
+ elsif value.is_a?(Time) # sometimes people think date is time!
109
+ value.to_date
110
+ elsif value.to_s =~ /(\d{4})[\-|\/](\d{2})[\-|\/](\d{2})/
111
+ # Faster than parsing the date
112
+ Date.new($1.to_i, $2.to_i, $3.to_i)
113
+ else
114
+ Date.parse(value)
115
+ end
116
+ rescue ArgumentError
117
+ nil
118
+ end
119
+
120
+ # Typecasts an arbitrary value to a Time
121
+ # Handles both Hashes and Time instances.
122
+ def typecast_to_time(value)
123
+ case value
124
+ when Float # JSON oj already parses Time, FTW.
125
+ Time.at(value).utc
126
+ when Hash
127
+ typecast_hash_to_time(value)
128
+ else
129
+ typecast_iso8601_string_to_time(value.to_s)
130
+ end
131
+ rescue ArgumentError
132
+ nil
133
+ rescue TypeError
134
+ nil
135
+ end
136
+
137
+ def typecast_iso8601_string_to_time(string)
138
+ if (string =~ /(\d{4})[\-\/](\d{2})[\-\/](\d{2})[T\s](\d{2}):(\d{2}):(\d{2}(\.\d+)?)(Z| ?([\+\s\-])?(\d{2}):?(\d{2}))?/)
139
+ # $1 = year
140
+ # $2 = month
141
+ # $3 = day
142
+ # $4 = hours
143
+ # $5 = minutes
144
+ # $6 = seconds (with $7 for fraction)
145
+ # $8 = UTC or Timezone
146
+ # $9 = time zone direction
147
+ # $10 = tz difference hours
148
+ # $11 = tz difference minutes
149
+
150
+ if $8 == 'Z' || $8.to_s.empty?
151
+ Time.utc($1.to_i, $2.to_i, $3.to_i, $4.to_i, $5.to_i, $6.to_r)
152
+ else
153
+ Time.new($1.to_i, $2.to_i, $3.to_i, $4.to_i, $5.to_i, $6.to_r, "#{$9 == '-' ? '-' : '+'}#{$10}:#{$11}")
154
+ end
155
+ else
156
+ Time.parse(string)
157
+ end
158
+ end
159
+
160
+ # Creates a DateTime instance from a Hash with keys :year, :month, :day,
161
+ # :hour, :min, :sec
162
+ def typecast_hash_to_datetime(value)
163
+ DateTime.new(*extract_time(value))
164
+ end
165
+
166
+ # Creates a Date instance from a Hash with keys :year, :month, :day
167
+ def typecast_hash_to_date(value)
168
+ Date.new(*extract_time(value)[0, 3].map(&:to_i))
169
+ end
170
+
171
+ # Creates a Time instance from a Hash with keys :year, :month, :day,
172
+ # :hour, :min, :sec
173
+ def typecast_hash_to_time(value)
174
+ Time.utc(*extract_time(value))
175
+ end
176
+
177
+ # Extracts the given args from the hash. If a value does not exist, it
178
+ # uses the value of Time.now.
179
+ def extract_time(value)
180
+ now = Time.now
181
+ [:year, :month, :day, :hour, :min, :sec].map do |segment|
182
+ typecast_to_numeric(value.fetch(segment, now.send(segment)), :to_i)
183
+ end
184
+ end
185
+
186
+ # Typecast a value to a Class
187
+ def typecast_to_class(value)
188
+ value.to_s.constantize
189
+ rescue NameError
190
+ nil
191
+ end
192
+
193
+ end
194
+ end
@@ -1,3 +1,3 @@
1
1
  module Hashme
2
- VERSION = "0.1.2"
2
+ VERSION = "0.2.0"
3
3
  end
@@ -22,21 +22,20 @@ describe Hashme::Attributes do
22
22
  end
23
23
 
24
24
  it "should assign some of the basic hash methods" do
25
- (@obj == @hash).should be_true
26
- @obj.eql?(@hash).should be_true
27
- @obj.keys.should eql(@hash.keys)
28
- @obj.values.should eql(@hash.values)
29
- @obj.to_hash.should eql(@hash)
25
+ expect(@obj == @hash).to be_truthy
26
+ expect(@obj.keys).to eql(@hash.keys)
27
+ expect(@obj.values).to eql(@hash.values)
28
+ expect(@obj.to_hash).to eql(@hash)
30
29
  end
31
30
  end
32
31
 
33
32
  describe "#[]=" do
34
33
  it "should assign values to attributes hash" do
35
34
  @obj[:akey] = "test"
36
- attribs[:akey].should eql("test")
35
+ expect(attribs[:akey]).to eql("test")
37
36
  @obj['akey'] = "anothertest"
38
- attribs[:akey].should eql("anothertest")
39
- attribs['akey'].should be_nil
37
+ expect(attribs[:akey]).to eql("anothertest")
38
+ expect(attribs['akey']).to be_nil
40
39
  end
41
40
  end
42
41
 
@@ -44,7 +43,7 @@ describe Hashme::Attributes do
44
43
  it "should remove attribtue entry" do
45
44
  @obj[:key] = 'value'
46
45
  @obj.delete(:key)
47
- @obj[:key].should be_nil
46
+ expect(@obj[:key]).to be_nil
48
47
  end
49
48
  end
50
49
 
@@ -52,7 +51,7 @@ describe Hashme::Attributes do
52
51
  it "should duplicate attributes" do
53
52
  @obj[:key] = 'value'
54
53
  @obj2 = @obj.dup
55
- @obj2.send(:_attributes).object_id.should_not eql(@obj.send(:_attributes).object_id)
54
+ expect(@obj2.send(:_attributes).object_id).to_not eql(@obj.send(:_attributes).object_id)
56
55
  end
57
56
  end
58
57
 
@@ -60,7 +59,7 @@ describe Hashme::Attributes do
60
59
  it "should clone attributes" do
61
60
  @obj[:key] = 'value'
62
61
  @obj2 = @obj.clone
63
- @obj2.send(:_attributes).object_id.should_not eql(@obj.send(:_attributes).object_id)
62
+ expect(@obj2.send(:_attributes).object_id).to_not eql(@obj.send(:_attributes).object_id)
64
63
  end
65
64
  end
66
65
 
@@ -70,7 +69,7 @@ describe Hashme::Attributes do
70
69
  it "should provide something useful" do
71
70
  @obj[:key1] = 'value1'
72
71
  @obj[:key2] = 'value2'
73
- @obj.inspect.should match(/#<.+ key1: "value1", key2: "value2">/)
72
+ expect(@obj.inspect).to match(/#<.+ key1: "value1", key2: "value2">/)
74
73
  end
75
74
 
76
75
  end