hashme 0.1.2 → 0.2.0

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