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 +4 -4
- data/.rspec +1 -0
- data/README.md +30 -4
- data/lib/hashme.rb +1 -0
- data/lib/hashme/casted_array.rb +2 -2
- data/lib/hashme/properties.rb +1 -1
- data/lib/hashme/property.rb +10 -33
- data/lib/hashme/property_casting.rb +194 -0
- data/lib/hashme/version.rb +1 -1
- data/spec/hashme/attributes_spec.rb +11 -12
- data/spec/hashme/base_spec.rb +4 -5
- data/spec/hashme/casted_array_spec.rb +20 -17
- data/spec/hashme/properties_spec.rb +21 -21
- data/spec/hashme/property_casting_spec.rb +612 -0
- data/spec/hashme/property_spec.rb +36 -38
- metadata +18 -14
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: b1baf318f249321f9486052a01db0881479f461c
|
4
|
+
data.tar.gz: f5b623806198098e2d5e4d6fa42c60cb6137498e
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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 `
|
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
data/lib/hashme/casted_array.rb
CHANGED
@@ -20,7 +20,7 @@ module Hashme
|
|
20
20
|
:encode_json, :as_json, :to_json,
|
21
21
|
:inspect, :any?
|
22
22
|
|
23
|
-
def initialize(
|
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(
|
57
|
+
casted_by_property.build(self, obj)
|
58
58
|
end
|
59
59
|
|
60
60
|
end
|
data/lib/hashme/properties.rb
CHANGED
data/lib/hashme/property.rb
CHANGED
@@ -1,24 +1,22 @@
|
|
1
1
|
module Hashme
|
2
2
|
class Property
|
3
3
|
|
4
|
-
|
4
|
+
attr_reader :name, :type, :default, :array
|
5
5
|
|
6
6
|
def initialize(name, type, opts = {})
|
7
|
-
|
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
|
-
@
|
12
|
-
|
11
|
+
@array = true
|
12
|
+
@type = type.first
|
13
13
|
else
|
14
|
-
@
|
15
|
-
|
14
|
+
@array = false
|
15
|
+
@type = type
|
16
16
|
end
|
17
17
|
|
18
|
-
self.type = klass
|
19
|
-
|
20
18
|
# Handle options
|
21
|
-
|
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
|
-
|
46
|
-
|
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
|
-
|
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
|
data/lib/hashme/version.rb
CHANGED
@@ -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).
|
26
|
-
@obj.eql
|
27
|
-
@obj.
|
28
|
-
@obj.
|
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].
|
35
|
+
expect(attribs[:akey]).to eql("test")
|
37
36
|
@obj['akey'] = "anothertest"
|
38
|
-
attribs[:akey].
|
39
|
-
attribs['akey'].
|
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].
|
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.
|
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.
|
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.
|
72
|
+
expect(@obj.inspect).to match(/#<.+ key1: "value1", key2: "value2">/)
|
74
73
|
end
|
75
74
|
|
76
75
|
end
|