hashing 0.0.1 → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,15 @@
1
1
  ---
2
- SHA1:
3
- metadata.gz: 58fbbbe8dddf5ac8299796af0123035abba17b68
4
- data.tar.gz: 142748d477848bc270bdd7fc4c74be5bcd6335ab
2
+ !binary "U0hBMQ==":
3
+ metadata.gz: !binary |-
4
+ YWJiN2EwYTk5Y2MyNDQzZjQ1MGQxYzcyMjE0YTE1MjRlMmRlZDIyMQ==
5
+ data.tar.gz: !binary |-
6
+ M2NkMjcxOThkMGI2ZWVkY2JhZjY4YTMwYjEzMGY0YTliYjVhOWQ2OA==
5
7
  SHA512:
6
- metadata.gz: a5488323cd2dc9d7d53b2f2c53eeb091b7283b7f3d37be183d2311e67fd2cc5484ce7c02a070051d40ae7e30db87c718ee97f276dd332f9034321ae9f3600dbb
7
- data.tar.gz: bbfa91cdfcacc250f95b9a9a1ff66986cb281db6007b8f95dac0198641f446c3fd10a7ad8681a07c455e4f3299952aadea8332cf2b06eb30e4878715dec8e7ab
8
+ metadata.gz: !binary |-
9
+ M2I3ODUxM2UxZWRkZWQxNmM2NjcxMDc5NDQwNTU5ZmM3YzViZWRjMDQyMGY5
10
+ NDZlNDYyMTdmNTU3YmYwYWI0NGNhYjgyMGExZDcxZDNhZjA0NTljNGMxMmJm
11
+ MzkyM2FkNGYyZjJlN2FlMGJiZjZlNDJlMmFlNWM2OTQ0Mjk2YmY=
12
+ data.tar.gz: !binary |-
13
+ MWQ2ZjMyYjQxMDMyNmYwMWUzOTRkZDJhMjVhMDQ0YmUzNTY0NmZiMTA1MzZi
14
+ ZGQxM2U0NWQ5NjQyYmExYzAyOTlhY2IyYTBlOTYxZDQ0ZDNiODM5ZWVmNDZj
15
+ ZGZkMzJlZjRlYmI2NDZlYzRkZDdhOWNmZmJlOWM1NTA5ZDI2ZTM=
@@ -0,0 +1,13 @@
1
+ # v0.1.0 2014-05-23
2
+
3
+ ## better api naming and conventions
4
+
5
+ * removes the Hash as last parameters, introducing a well established api
6
+
7
+ # v0.0.1 2014-05-02
8
+
9
+ ## first public release :heartpulse:
10
+
11
+ * module `Hashing` and class methods `hasherize` and `loading`
12
+ * class (module) `Hasherize` to sugar the `ivars` declaration
13
+ * nested `Hashing` for collections
data/README.md CHANGED
@@ -6,9 +6,9 @@ method. Also gives you a `YourClass::from_hash` to reconstruct the instances.
6
6
 
7
7
  ## Status
8
8
  [![Gem Version](https://badge.fury.io/rb/hashing.svg)](http://badge.fury.io/rb/hashing)
9
- [![Build Status](https://travis-ci.org/ricardovaleriano/hashing.png?branch=master)](http://travis-ci.org/ricardovaleriano/hashing?branch=master)
9
+ [![Build Status](https://travis-ci.org/ricardovaleriano/hashing.svg?branch=master)](http://travis-ci.org/ricardovaleriano/hashing?branch=master)
10
10
  [![Code Climate](https://codeclimate.com/github/ricardovaleriano/hashing.png)](https://codeclimate.com/github/ricardovaleriano/hashing)
11
- [![Inline docs](http://inch-pages.github.io/github/ricardovaleriano/hashing.png)](http://inch-pages.github.io/github/ricardovaleriano/hashing)
11
+ [![Inline docs](http://inch-pages.github.io/github/ricardovaleriano/hashing.svg)](http://inch-pages.github.io/github/ricardovaleriano/hashing)
12
12
 
13
13
  ## Installation
14
14
 
@@ -24,6 +24,12 @@ Or install it yourself as:
24
24
 
25
25
  $ gem install hashing
26
26
 
27
+ ## Contact
28
+
29
+ * API Doc: http://rdoc.info/gems/hashing
30
+ * Bugs, issues and feature requests: https://github.com/ricardovaleriano/hashing/issues
31
+ * Support: http://stackoverflow.com/questions/tagged/hashing-ruby
32
+
27
33
  ## Usage
28
34
 
29
35
  Given a `File` class like this:
@@ -82,11 +88,8 @@ valid `Hash` like the one created by a `#to_h` call:
82
88
  ```ruby
83
89
  class File
84
90
  include Hashing
85
- hasherize :path, :commit, :content
86
-
87
- loading ->(hash) {
88
- new hash[:path], hash[:commit], hash[:content]
89
- }
91
+ hasherize(:path, :commit, :content).
92
+ loading ->(hash) { new hash[:path], hash[:commit], hash[:content] }
90
93
 
91
94
  # ...
92
95
  end
@@ -101,36 +104,58 @@ should call them whatever you need in your programs. But the `::from_hash`
101
104
  method will be called by `Hashing` when building your instances from hashes (more
102
105
  about this: [nested hasherizing](#nested-hasherized-objects)).
103
106
 
104
- #### Hasherize
107
+ ### Custom "hasherizing" and loading strategies
105
108
 
106
- `Hashing` comes with an alternative way to indicate what fields should be used to
107
- represent your objects as a `Hash`. This can be done by the `Hasherize` class.
108
- The previous example can be writen like this:
109
+ Many times you will need apply some computation over the contents of your
110
+ `ivars` transform them in a primitive that can be stored as a `Hash`.
111
+
112
+ Following the `File` class example, maybe you want to store the `@content` as a
113
+ `Base64` enconded string.
114
+
115
+ `Hashing` allows you to specify the strategy of serialization and loading when
116
+ indicating which `ivars` should be part of the final `Hash`:
109
117
 
110
118
  ```ruby
119
+ require 'base64'
120
+
111
121
  class File
112
- include Hasherize.new :path, :commit, :content
122
+ include Hashing
123
+
124
+ hasherize :path, :commit
113
125
 
114
- loading ->(hash) {
115
- new hash[:path], hash[:commit], hash[:content]
116
- }
126
+ hasherize(:content).
127
+ to(->(content) { Base64.encode64 content }).
128
+ from(->(content_string) { Base64.decode64 content_string }).
129
+ loading ->(hash) { new hash[:path], hash[:commit], hash[:content] }
117
130
 
118
131
  # ...
119
132
  end
120
133
  ```
121
134
 
122
- It's just a matter of taste. Use the one that is more appealing to you.
135
+ But I will recomend this approach only if your strategy for serialization is
136
+ more complex than just call a method in an object passing the raw value. If your
137
+ need is exactly like this, you can just indicate the object and the methods that
138
+ should be called in what moment:
123
139
 
124
- ### Custom "hasherizing" and loading strategies
140
+ ```ruby
141
+ require 'base64'
125
142
 
126
- Many times you will need apply some computation over the contents of your
127
- `ivars` transform them in a primitive that can be stored as a `Hash`.
143
+ class File
144
+ include Hashing
128
145
 
129
- Following the `File` class example, maybe you want to store the `@content` as a
130
- `Base64` enconded string.
146
+ hasherize(:path, :commit).
147
+ loading ->(hash) { new hash[:path], hash[:commit], hash[:content] }
131
148
 
132
- `Hashing` allows you to specify the strategy of serialization and loading when
133
- indicating which `ivars` should be part of the final `Hash`:
149
+ hasherize(:content).using(Base64).to(:encode64).from(:decode64)
150
+
151
+ # ...
152
+ end
153
+ ```
154
+
155
+ And finally, if the your serialization logic is worth a method on it's own, you
156
+ can indicate this by passing the method names via symbol to the `:to` and
157
+ `:from` options. Since those methods don't necessarily make sense as part of
158
+ your public api, so you can even make then private:
134
159
 
135
160
  ```ruby
136
161
  require 'base64'
@@ -138,15 +163,21 @@ require 'base64'
138
163
  class File
139
164
  include Hashing
140
165
 
141
- hasherize :path, :commit
166
+ hasherize(:path, :commit).
167
+ loading ->(hash) { new hash[:path], hash[:commit], hash[:content] }
168
+
169
+ hasherize(:content).to(:encode).from(:decode)
142
170
 
143
- hasherize :content,
144
- to_hash: ->(content) { Base64.encode64 content },
145
- from_hash: ->(content_string) { Base64.decode64 content_string }
171
+ # ...
146
172
 
147
- loading ->(hash) {
148
- new hash[:path], hash[:commit], hash[:content]
149
- }
173
+ private
174
+ def encode(content)
175
+ Base64.encode64 content
176
+ end
177
+
178
+ def decode(content)
179
+ Base64.decode64 content
180
+ end
150
181
 
151
182
  # ...
152
183
  end
@@ -161,12 +192,16 @@ multiple `ivars` if it makes sense to your program:
161
192
  class File
162
193
  include Hashing
163
194
 
164
- hasherize :path, :commit,
165
- to_hash: ->(value) { value.downcase },
166
- from_hash: ->(value) { value.downcase }
195
+ hasherize(:path, :commit).
196
+ to(->(value) { value.downcase }).
197
+ from(->(value) { value.upcase })
167
198
  end
168
199
  ```
169
200
 
201
+ This will guarantees that the final `Hash` has the `path` and the `commit`
202
+ values "downcased" when your object is serialized, and "upcased" when the
203
+ instance is reconstructed.
204
+
170
205
  #### Nested hasherized objects
171
206
 
172
207
  But if your transformations are a little more complicated than a simple `Base64`
@@ -206,13 +241,14 @@ end
206
241
 
207
242
  So in this case, if you wants a file to be `hasherized®` with it's internall
208
243
  `@annotations` preserved, you just indicate this in the `File` class. The
209
- example now can be rewriten as:
244
+ example now can be rewritten as:
210
245
 
211
246
  ```ruby
212
247
  class File
213
248
  include Hashing
214
249
 
215
- hasherize :path, :commit, :annotations
250
+ hasherize :path, :commit
251
+ hasherize(:annotations).collection Annotation
216
252
 
217
253
  # ...
218
254
  end
@@ -221,9 +257,34 @@ end
221
257
  Since the `Annotation` class has it's own notion of `#to_h` and `::from_hash`,
222
258
  this is all that `Hashing` needs to build a `File` instances from a valid `Hash`.
223
259
 
260
+ #### Defining `attr_reader` within the `.hasherize` invocation
261
+
262
+ If you want to define `readers` for the `ivars` passed to `.hasherize`, you can
263
+ do this with the option `attr: true` (defaults to `false`).
264
+
265
+ So, the following example:
266
+
267
+ ```ruby
268
+ class File
269
+ include Hashing
270
+ hasherize :path, :commit, :content
271
+
272
+ attr_reader :path, :commit, :content
273
+ end
274
+ ```
275
+
276
+ Can be written as:
277
+
278
+ ```ruby
279
+ class File
280
+ include Hashing
281
+ hasherize(:path, :commit, :content).reader true
282
+ end
283
+ ```
284
+
224
285
  ## Contributing
225
286
 
226
- This was a rapid "scratch your own itch" kind of project. It will make me real
287
+ This is a rapid "scratch your own itch" kind of project. It will make me really
227
288
  happy if it can be used used in your software anyhow. If you need something
228
289
  different than what is in it, or can solve us some bugs or add documentation, it
229
290
  will be very well received!
@@ -8,8 +8,8 @@ Gem::Specification.new do |spec|
8
8
  spec.version = Hashing::VERSION
9
9
  spec.authors = ["Ricardo Valeriano"]
10
10
  spec.email = ["ricardo.valeriano@gmail.com"]
11
- spec.summary = %q{Serialize your objects as Hashes}
12
- spec.description = %q{Gives you an easy way to specify which instances vars of your objects should be used as `key => value` to serialize it into a hash returned by the `#to_h` method. Also gives you a `YourClass::from_hash` to reconstruct the instances.}
11
+ spec.summary = %q{Serialize your objects into Hashes}
12
+ spec.description = %q{Provides an easy way to specify which instances vars of your objects should be used as `key` in a Hash returned by the `#to_h` method. Also gives you a `YourClass::from_hash` to reconstruct the instances.}
13
13
  spec.homepage = "http://github.com/ricardovaleriano/hashing"
14
14
  spec.license = "MIT"
15
15
 
@@ -1,49 +1,40 @@
1
- require 'hashing/version'
2
- require 'hashing/ivar'
3
- require 'hasherize'
4
-
1
+ require 'hashing/macros'
2
+
3
+ # Inject the public api into the host class, and it's instances.
4
+ # By "public api" we understand the class methods
5
+ #
6
+ # - {Hashing::Macros#hasherize}
7
+ # - {Hashing::Macros#from_hash}
8
+ # - {Hashing::Macros#__hasher}
9
+ #
10
+ # And the instance method:
11
+ #
12
+ # - {Hashing#to_h}
13
+ #
14
+ # @since 0.0.1
15
+ #
16
+ # @example including Hashing
17
+ # require 'hashing'
18
+ #
19
+ # class File
20
+ # include Hashing
21
+ # hasherize :path, :commit
22
+ #
23
+ # def initialize(path, commit)
24
+ # @path, @commit = path, commit
25
+ # end
26
+ # end
27
+ #
28
+ # When `Hashing` is included, the host class will gain the `.from_hash({})`
29
+ # method and the `#to_h` instance method.
30
+ # Another method that will be added is the private class method `.hasherize`
31
+ # will be added so you can indicate what ivars do you want in your sarialized
32
+ # objects.
5
33
  module Hashing
6
- # Inform the user about an attempt to create an instance, using a `Hash` with
7
- # keys that does not correspond to the mape made using `.hasherize`
8
- class UnconfiguredIvar < StandardError
9
- def initialize(ivar_name, class_name)
10
- super "The Hash has a :#{ivar_name} key, "+
11
- "but no @#{ivar_name} was configured in #{class_name}"
12
- end
13
- end
14
-
15
- # Inject the public api into the client class.
16
- #
17
- # @since 0.0.1
18
- #
19
- # @example including Hashing
20
- # require 'hashing'
21
- #
22
- # class File
23
- # include Hashing
24
- # hasherize :path, :commit
25
- #
26
- # def initialize(path, commit)
27
- # @path, @commit = path, commit
28
- # end
29
- # end
30
- #
31
- # When `Hashing` is included, the host class will gain the `.from_hash({})`
32
- # method and the `#to_h` instance method.
33
- # Another method that will be added is the private class method `.hasherize`
34
- # will be added so you can indicate what ivars do you want in your sarialized
35
- # objects.
36
34
  def self.included(client_class)
37
- client_class.extend Hasherizer
35
+ client_class.extend Macros
38
36
  end
39
37
 
40
- def meta_data(name, value)
41
- @_hashing_meta_data ||= { __hashing__: { types: {} } }
42
- @_hashing_meta_data[:__hashing__][:types][name] = value
43
- @_hashing_meta_data
44
- end
45
-
46
- #
47
38
  # The `Hash` returned by `#to_h` will be formed by keys based on the ivars
48
39
  # names passed to `hasherize` method.
49
40
  #
@@ -53,118 +44,6 @@ module Hashing
53
44
  # file.to_h
54
45
  # # => { path: 'README.md', commit: 'cfe9aacbc02528b' }
55
46
  def to_h
56
- hash_pairs = self.class.ivars.map { |ivar|
57
- value = instance_variable_get "@#{ivar}"
58
- if value.respond_to? :map
59
- meta_data ivar.to_sym, value.first.class
60
- end
61
- [ivar.to_sym, ivar.to_h(value)]
62
- }
63
- Hash[hash_pairs].merge(@_hashing_meta_data || {})
64
- end
65
-
66
- # Define the class methods that should be available in a 'hasherized ®' class
67
- # (a class that include `Hashing`).
68
- module Hasherizer
69
- # Configures which instance variables will be used to compose the `Hash`
70
- # generated by `#to_h`
71
- #
72
- # @api
73
- # @param ivars_and_options [*arguments]
74
- def hasherize(*ivars_and_options)
75
- @ivars ||= []
76
- ivars = extract_ivars ivars_and_options
77
- to_strategy = strategy_for_key :to_hash, ivars_and_options
78
- from_strategy = strategy_for_key :from_hash, ivars_and_options
79
- @ivars += ivars.map { |ivar| Ivar.new ivar, to_strategy, from_strategy }
80
- end
81
-
82
- # Configures the strategy to (re)create an instance of the 'hasherized ®'
83
- # class based on a `Hash` instance. This strategy will be used by the
84
- # `.from_hash({})` method.
85
- #
86
- # This configuration is optional, if it's not called, then the strategy will
87
- # be just repassing the `Hash` to the initializer.
88
- #
89
- # @param strategy [#call]
90
- # @return void
91
- def loading(strategy)
92
- @strategy = strategy
93
- end
94
-
95
- # Provides the default strategy for recreate objects from hashes (which is
96
- # just call .new passing the `Hash` as is.
97
- #
98
- # @return the result of calling the strategy
99
- def strategy
100
- @strategy || ->(h) { new h }
101
- end
102
-
103
- # those methods are private but part of the class api (macros).
104
- # #TODO: there is a way to document the 'macros' for a class in YARD?
105
- private :hasherize, :loading, :strategy
106
-
107
- # provides access to the current configuration on what `ivars` should be
108
- # used to generate a `Hash` representation of instances of the client class.
109
- #
110
- # @return [Array] ivars that should be included in the final Hash
111
- def ivars
112
- @ivars ||= []
113
- end
114
-
115
- # Receives a `Hash` and uses the strategy configured by `.loading` to
116
- # (re)create an instance of the 'hasherized ®' class.
117
- #
118
- # @param pairs [Hash] in a valid form defined by `.hasherize`
119
- # @return new object
120
- def from_hash(pairs)
121
- metadata = pairs.delete(:__hashing__) || {}
122
- hash_to_load = pairs.map do |ivar_name, value|
123
- ivar = ivar_by_name ivar_name.to_sym
124
- [ivar.to_sym, ivar.from_hash(value, metadata)]
125
- end
126
- strategy.call Hash[hash_to_load]
127
- end
128
-
129
- private
130
- # Cleanup the arguments received by `.hasherize` so only the `ivar` names
131
- # are returned. This is necessarey since the `.hasherize` can receive a
132
- # `Hash` with strategies `:from_hash` and `:to_hash` as the last argument.
133
- #
134
- # @param ivars_and_options [Array] arguments received by `.serialize`
135
- # @return [Array[:Symbol]] ivar names that should be used in the `Hash` serialization
136
- def extract_ivars(ivars_and_options)
137
- ivars = ivars_and_options.dup
138
- if ivars.last.is_a? Hash
139
- ivars.pop
140
- end
141
- ivars
142
- end
143
-
144
- # Fetches the strategy to serialize or deserialize (defined by the first
145
- # param `strategy`) the `ivars` passed as second parameter
146
- #
147
- # @param hash_key [Symbol] (:to_hash || :from_hash) type of strategy to fetch
148
- # @param ivars_and_options [*args | *args, Hash]
149
- # @return [#call] strategy to be used
150
- def strategy_for_key(strategy, ivars_and_options)
151
- default_strategy_just_returns = ->(ivar_value) { ivar_value }
152
- strategies = { strategy => default_strategy_just_returns }
153
- strategies = ivars_and_options.last if ivars_and_options.last.is_a? Hash
154
- strategies[strategy]
155
- end
156
-
157
- # Search an `ivar` by it's name in the class ivars collection
158
- #
159
- # #TODO: Can be enhanced since now the ivars doesn't have a sense of
160
- # equality.
161
- #
162
- # @param ivar_name [Symbol] `ivar` name
163
- # @return [Ivar]
164
- def ivar_by_name(ivar_name)
165
- ivar = ivars.select { |ivar| ivar.to_sym == ivar_name }.first
166
- raise UnconfiguredIvar.new ivar_name, name unless ivar
167
- ivar
168
- end
47
+ self.class.__hasher.to_h self
169
48
  end
170
49
  end
@@ -0,0 +1,184 @@
1
+ require "hashing/ivar"
2
+ require "hashing/unconfigured_ivar_error"
3
+
4
+ module Hashing
5
+ # This class contains the interface "exported" by a call to the `.hasherize`
6
+ # method in any class that includes the module [Hashing].
7
+ class Hasher
8
+ attr_reader :ivars
9
+
10
+ # @param host_class [Class] the class whose ivars will be used to serialize
11
+ # @param ivars [Array<Symbol>] the ivars that will be serialized
12
+ def initialize(host_class, ivars = nil)
13
+ @host_class = host_class
14
+ add ivars
15
+ end
16
+
17
+ # --- api {{{
18
+
19
+ # Provides the api to configure the strategy to serialize an instance of a
20
+ # class that includes {Hashing} into a hash object.
21
+ #
22
+ # @since 0.1.0
23
+ #
24
+ # @param strategy [#call] the logic to convert some value to a [Hash]
25
+ # @return [Hasher]
26
+ def to(strategy)
27
+ logic_for :to_h, strategy
28
+ end
29
+
30
+ # Provides the api to configure the logic to serialize an instance of a
31
+ # class that includes {Hashing} into a hash object.
32
+ #
33
+ # @since 0.1.0
34
+ #
35
+ # @param strategy [#call] the logic to convert some value to a [Hash]
36
+ # @return [Hasher]
37
+ def from(strategy)
38
+ logic_for :from_hash, strategy
39
+ end
40
+
41
+ # Provides the api to create attr_readers in the host class for the current
42
+ # configured ivars (those passed to {#add}).
43
+ #
44
+ # @since 0.1.0
45
+ #
46
+ # @example: creating attr_reader from :path and :commit
47
+ # hasherize(:path, :commit).reader true
48
+ #
49
+ # The example above will create accessors for :path and :commit
50
+ #
51
+ # @return [Hasher]
52
+ def reader(should_create_attr_reader = true)
53
+ if should_create_attr_reader
54
+ @current_ivars.each { |ivar| @host_class.send :attr_reader, ivar.to_sym }
55
+ end
56
+ self
57
+ end
58
+
59
+ # Provides the api to indicate an object with the serialization and
60
+ # unserialization logic.
61
+ #
62
+ # @example: using Base64 to serialize and unserialize some ivar:
63
+ # hahserize(:content).unsing(Base64).to(:encode64).from(:decode64)
64
+ #
65
+ # Note: any object can be passe to {#using}, and the methods in these object
66
+ # passed as arguments to {#to} and {#from} need to be public.
67
+ #
68
+ # @since 0.1.0
69
+ #
70
+ # @return [Hasher]
71
+ def using(serializator)
72
+ @serializator = serializator
73
+ self
74
+ end
75
+
76
+ # Provides the api to say if an ivar is a collection (#map) of instances of
77
+ # a class that includes {Hashing}.
78
+ # The idea here is just to be able to restore nested {Hashing} objects.
79
+ #
80
+ # @since 0.1.0
81
+ #
82
+ # @return [Hasher]
83
+ def collection(type)
84
+ # replace current ivar for it's collection version...
85
+ collections = @current_ivars.map { |ivar| IvarCollection.new ivar, type }
86
+ @current_ivars.each { |ivar| @ivars.delete ivar }
87
+ @ivars += collections
88
+ self
89
+ end
90
+
91
+ # Configures the strategy to (re)create an instance of the 'hasherized ®'
92
+ # class based on a `Hash` instance. This strategy will be used by the
93
+ # `.from_hash({})` method.
94
+ #
95
+ # This configuration is optional, if it's not called, then the strategy will
96
+ # be just repassing the `Hash` to the initializer.
97
+ #
98
+ # @param strategy [#call]
99
+ # @return [Hasher] (fluent interface)
100
+ def loading(strategy)
101
+ @loading = strategy
102
+ self
103
+ end
104
+
105
+ # }}} api
106
+
107
+ # This method can be called to define which ivars will be used as keys in
108
+ # the final hash.
109
+ # Ivar names passed as arguments to this method will be used to create
110
+ # instances of [Ivar]. Those have the real logic of serialization and
111
+ # unserialization of an [Ivar].
112
+ # This method also keeps a reference to the last ivars passed as parameter.
113
+ # This is necessary to allow us to do the following call:
114
+ #
115
+ # hasherize(:ivar1, :ivar2).to(->(value){})
116
+ #
117
+ # The previous call will configure the `:to` strategy for `:ivar1` and
118
+ # `:ivar2`.
119
+ #
120
+ # @param ivar_names [Array<Symbol>] ivars to be used to hasherize the instance
121
+ # @return [Hasher]
122
+ def add(ivar_names)
123
+ ivar_names = Array(ivar_names)
124
+ @current_ivars = ivar_names.map { |ivar_name| Ivar.new ivar_name }
125
+ @ivars ||= []
126
+ @ivars += @current_ivars
127
+ self
128
+ end
129
+
130
+ # Provides the logic to transform an instance of a {Hashing} class into an
131
+ # hash object.
132
+ #
133
+ # @return [Hash] a new hash in which keys are the ivar names and values the string value for those ivars.
134
+ def to_h(instance)
135
+ pairs = @ivars.map { |ivar|
136
+ ivar_value = instance.instance_variable_get :"@#{ivar.to_sym}"
137
+ [ivar.to_sym, ivar.to_h(ivar_value)]
138
+ }
139
+ Hash[pairs]
140
+ end
141
+
142
+ # This method will be called to reconstruct an instance of the type which
143
+ # includes {Hashing}.
144
+ # The `loader` in which `#call` will be called here is the one passed to the
145
+ # {Hashing::Hasher#loading}. If none was passed, this method will just call
146
+ # `.new` in the host class passing the [Hash] as argument.
147
+ #
148
+ # @param [Hash] hash serialized by a call to {Hashing::Hasher#to_h}
149
+ def load(hash)
150
+ check_for_unconfigured_keys hash
151
+ loader = @loading || ->(serialized) { @host_class.new serialized }
152
+ loader.call process_hash_values hash
153
+ end
154
+
155
+ private
156
+
157
+ # @since 0.1.0
158
+ #
159
+ # @param way [Symbol] :from_hash or :to_h strategy
160
+ # @param strategy [#call] the strategy to convert some value to or from a [Hash]
161
+ # @return [Hasher]
162
+ def logic_for(way, strategy)
163
+ if @serializator
164
+ strategy = @serializator.method strategy
165
+ end
166
+ @current_ivars.each { |ivar| ivar.send :"#{way}=", strategy }
167
+ self
168
+ end
169
+
170
+ def check_for_unconfigured_keys(hash)
171
+ unrecognized_keys = hash.keys - @ivars.map(&:to_sym)
172
+ if unrecognized_keys.count > 0
173
+ raise Hashing::UnconfiguredIvarError.new unrecognized_keys, @host_class
174
+ end
175
+ end
176
+
177
+ def process_hash_values(hash)
178
+ transformed_hash = @ivars.map { |ivar|
179
+ [ivar.to_sym, ivar.from_hash(hash[ivar.to_sym])]
180
+ }
181
+ Hash[transformed_hash]
182
+ end
183
+ end
184
+ end
@@ -1,8 +1,11 @@
1
+ require_relative 'ivar_collection'
2
+
1
3
  module Hashing
2
4
  # Represents each one of the instance variables in a class that should be used
3
5
  # to represent an object in a `Hash` form (serialization).
4
6
  class Ivar
5
7
  attr_reader :name
8
+ attr_writer :to_h, :from_hash
6
9
 
7
10
  # Configure the name of an `ivar` and the 'callable' objects thath will be
8
11
  # used to prepare the `ivar` value for serialization, and to load the object
@@ -28,10 +31,6 @@ module Hashing
28
31
  # @return the value that will be stored in the `Hash`
29
32
  def to_h(value)
30
33
  return value unless @to_h
31
-
32
- if value.respond_to? :map
33
- value = hasherize value
34
- end
35
34
  @to_h.call value
36
35
  end
37
36
 
@@ -56,39 +55,5 @@ module Hashing
56
55
  def to_s
57
56
  @name.to_s
58
57
  end
59
-
60
- private
61
- # Is an object descendent of {Hashing}?
62
- #
63
- # @param value [Object]
64
- def hashing?(value)
65
- value.class.ancestors.include? Hashing
66
- end
67
-
68
- # Hasherize a value when it has {Hashing} in it's method lookup or return
69
- # the value. Util when a collection of {Hashing} objects is given and need
70
- # to be "hasherized"
71
- #
72
- # @param value [#map] the value to be verified as a {Hashing} (or not)
73
- # @return [#map] collection of hashes
74
- def hasherize(collection)
75
- collection.map { |item| hashing?(item) ? item.to_h : item }
76
- end
77
-
78
- # If a collection of {Hashing} objects is given, we have to reconstruct all
79
- # collections members before while reconstructing the collection itself.
80
- # This method provides that
81
- #
82
- # TODO: (need?) recursion to reconstruct collections of collections
83
- #
84
- # @param value [#map] the collection of {Hashing} objects
85
- # @param metadata [Hash] containing serialized data about the original object
86
- # @return [#map] collection of {Hashing} instances
87
- def normalize(collection, metadata)
88
- elements_class = metadata.fetch(:types, {}).fetch(@name, nil)
89
- return collection unless elements_class.respond_to? :from_hash
90
-
91
- collection.map { |element| elements_class.from_hash element }
92
- end
93
58
  end
94
59
  end
@@ -0,0 +1,23 @@
1
+ module Hashing
2
+ # Represents an ivar in a class that includes {Hashing} which contains a
3
+ # collection of other {Hashing} instances.
4
+ class IvarCollection
5
+ extend Forwardable
6
+ def_delegators :@holder, :to_sym, :to_s, :name, :to_h=, :from_hash=
7
+
8
+ def initialize(collection_holder_ivar, type)
9
+ @holder = collection_holder_ivar
10
+ @type = type
11
+ end
12
+
13
+ def to_h(value)
14
+ @holder.to_h value.map { |item|
15
+ item.respond_to?(:to_h) ? item.to_h : item
16
+ }
17
+ end
18
+
19
+ def from_hash(value)
20
+ @holder.from_hash value.map { |item| @type.from_hash item }
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,38 @@
1
+ require "hashing/hasher"
2
+
3
+ module Hashing
4
+ # Define the class methods that should be available in a 'hasherized ®' class
5
+ # (a class that include {Hashing}).
6
+ module Macros
7
+ # Configures which instance variables will be used to compose the `Hash`
8
+ # generated by `#to_h`
9
+ #
10
+ # @api
11
+ # @param ivars [Array<Symbol>]
12
+ def hasherize(*ivars)
13
+ __hasher.add ivars
14
+ end
15
+
16
+ # those methods are private but part of the class api (macros).
17
+ # #TODO: there is a way to document the 'macros' for a class in YARD?
18
+ private :hasherize
19
+
20
+ # Receives a `Hash` and uses the strategy configured by `.loading` to
21
+ # (re)create an instance of the 'hasherized ®' class.
22
+ #
23
+ # @param hash [Hash] in a valid form defined by `.hasherize`
24
+ # @return new object
25
+ def from_hash(hash)
26
+ __hasher.load hash
27
+ end
28
+
29
+ # Provides the entry point to the object that has the actual logic of
30
+ # serialization/unserialization for {Hashing} instances.
31
+ # The ideia here is to not polute the host class with a bunch of methods
32
+ # included by the {Hashing}. Instead, we just inject the api method
33
+ # {#hasherize}, {#from_hash} and the internally used {#__hasher} method.
34
+ def __hasher
35
+ @__hasher ||= Hasher.new self
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,13 @@
1
+ module Hashing
2
+ # Inform the user about an attempt to create an instance, using a `Hash` with
3
+ # keys that does not correspond to the mape made using `.hasherize`
4
+ class UnconfiguredIvarError < StandardError
5
+ def initialize(ivar_names, class_name)
6
+ super [
7
+ "The hash passed to #{class_name}.from_hash has the following ",
8
+ "keys that aren't configured by the .hasherize method: ",
9
+ "#{ivar_names.join ","}."
10
+ ].join
11
+ end
12
+ end
13
+ end
@@ -1,3 +1,3 @@
1
1
  module Hashing
2
- VERSION = "0.0.1"
2
+ VERSION = "0.1.0"
3
3
  end
@@ -0,0 +1,35 @@
1
+ describe Hashing::Hasher do
2
+ before do
3
+ class WowSuchClass
4
+ include Hashing
5
+ def initialize(so_ivar = nil)
6
+ @so_ivar = so_ivar
7
+ end
8
+ end
9
+ end
10
+
11
+ let(:hasher) { Hashing::Hasher.new WowSuchClass }
12
+
13
+ describe "#attr" do
14
+ it "with true, provides attr_readers for the current ivars" do
15
+ hasher.add(:very_ivar).reader true
16
+ WowSuchClass.new.respond_to?(:very_ivar).must_be :==, true
17
+ end
18
+ end
19
+
20
+ describe "#using" do
21
+ require 'base64'
22
+
23
+ it "stores an object with methods to transform the current ivar" do
24
+ class WowSuchClass
25
+ hasherize(:so_ivar).reader(true).
26
+ using(Base64).to(:encode64).from(:decode64).
27
+ loading ->(hash) { new hash[:so_ivar] }
28
+ end
29
+
30
+ wow = WowSuchClass.new "borba"
31
+ wow.to_h.must_be :==, {so_ivar: Base64.encode64("borba")}
32
+ WowSuchClass.from_hash({so_ivar: Base64.encode64("borba")}).so_ivar.must_be :==, 'borba'
33
+ end
34
+ end
35
+ end
@@ -1,29 +1,31 @@
1
1
  describe Hashing do
2
2
  describe 'when one of the ivars is an `Array` of hasherized objects' do
3
- class HashingCollectionOwner
4
- attr_reader :file, :commit, :annotations
5
- include Hasherize.new :file, :commit, :annotations
6
-
7
- loading ->(hash) { new hash[:file], hash[:commit], hash[:annotations] }
8
-
9
- def initialize(file, commit, annotations)
10
- @file, @commit, @annotations = file, commit, annotations
11
- end
12
- end
13
-
14
3
  class HashingCollectionMember
15
4
  attr_reader :annotation
16
- include Hasherize.new :annotation,
17
- to_hash: ->(value) { "--#{value}" },
18
- from_hash: ->(value) { "#{value}--" }
19
-
20
- loading ->(hash) { new hash[:annotation] }
5
+ include Hashing
6
+ hasherize(:annotation).
7
+ to(->(value) { "--#{value}" }).
8
+ from(->(value) { "#{value}--" }).
9
+ loading(->(hash) { new hash[:annotation] })
21
10
 
22
11
  def initialize(annotation)
23
12
  @annotation = annotation
24
13
  end
25
14
  end
26
15
 
16
+ class HashingCollectionOwner
17
+ attr_reader :file, :commit, :annotations
18
+ include Hashing
19
+ hasherize :file, :commit
20
+ hasherize(:annotations).
21
+ collection(HashingCollectionMember).
22
+ loading ->(hash) { new hash[:file], hash[:commit], hash[:annotations] }
23
+
24
+ def initialize(file, commit, annotations)
25
+ @file, @commit, @annotations = file, commit, annotations
26
+ end
27
+ end
28
+
27
29
  describe '#to_h' do
28
30
  it 'calls #to_h for each array item when hashifying the object' do
29
31
  owner = HashingCollectionOwner.new 'README.md', 'cfe9aacbc02528b', [
@@ -36,17 +38,24 @@ describe Hashing do
36
38
  annotations: [
37
39
  { annotation: '--first' },
38
40
  { annotation: '--second' },
39
- ],
40
- __hashing__: {
41
- types: {
42
- annotations: HashingCollectionMember
43
- }
44
- }
41
+ ]
45
42
  }
46
- p owner.to_h
47
43
  end
48
44
 
49
- it "don't call the #to_h on inner object that don't include `Hashing`"
45
+ it "don't call the #to_h on inner object that don't include `Hashing`" do
46
+ owner = HashingCollectionOwner.new 'README.md', 'cfe9aacbc02528b', [
47
+ HashingCollectionMember.new('first'),
48
+ "xpto",
49
+ ]
50
+ owner.to_h.must_be :==, {
51
+ file: 'README.md',
52
+ commit: 'cfe9aacbc02528b',
53
+ annotations: [
54
+ { annotation: '--first' },
55
+ 'xpto',
56
+ ]
57
+ }
58
+ end
50
59
  end
51
60
 
52
61
  describe '#from_h' do
@@ -57,16 +66,11 @@ describe Hashing do
57
66
  annotations: [
58
67
  {annotation: "first"},
59
68
  {annotation: "second"}
60
- ],
61
- __hashing__: {
62
- types: {
63
- annotations: HashingCollectionMember
64
- }
65
- }
69
+ ]
66
70
  }
67
71
  end
68
72
 
69
- it 'calls #from_h for each on an yaml array that contains hasherized objects' do
73
+ it 'calls #from_hash for each element on an yaml array that contains hasherized objects' do
70
74
  owner = HashingCollectionOwner.from_hash hash_values
71
75
  owner.annotations.first.annotation.must_be :==, 'first--'
72
76
  owner.annotations.last.annotation.must_be :==, 'second--'
@@ -3,14 +3,9 @@ require "base64"
3
3
  describe Hashing do
4
4
  describe 'interface' do
5
5
  let(:hasherized) do
6
- # if in doubt about the absense of assertions in this test, please
7
- # refer to:
8
- # - http://blog.zenspider.com/blog/2012/01/assert_nothing_tested.html
9
- # and https://github.com/seattlerb/minitest/issues/159
10
6
  Class.new do
11
7
  include Hashing
12
- hasherize :ivar
13
- loading ->() {}
8
+ hasherize(:ivar)
14
9
  end
15
10
  end
16
11
 
@@ -24,48 +19,61 @@ describe Hashing do
24
19
  end# interface
25
20
 
26
21
  describe 'Recreating a `hasherized` class instance' do
27
- let(:hasherized) do
28
- Class.new do
29
- attr_reader :h
22
+ describe '.loading' do
30
23
 
31
- include Hashing
32
- hasherize :h
24
+ before do
25
+ @original_stdout, $stdout = $stdout, StringIO.new
26
+ end
33
27
 
34
- def initialize(h)
35
- @h = h
36
- end
28
+ after do
29
+ $stdout = @original_stdout
37
30
  end
38
- end
39
31
 
40
- describe '.loading' do
41
32
  it 'uses (`#call`) the strategy defined by `.loading`' do
42
- called = false
43
- my_strategy = ->(h) { called = true }
44
- hasherized.send :loading, my_strategy
45
- hasherized.from_hash Hash.new
46
- called.must_be :==, true
33
+ Class.new do
34
+ include Hashing
35
+ hasherize(:omg).loading ->(hash) { $stdout.write hash }
36
+ end.from_hash omg: 'lol'
37
+ $stdout.string.must_be :==, '{:omg=>"lol"}'
47
38
  end
48
39
  end
49
40
 
50
41
  describe '#from_hash' do
51
- it 'just calls .new if none strategy was defined by .loading' do
52
- new_object = hasherized.from_hash h: 'hasherizing'
53
- new_object.h.must_be :==, { h: 'hasherizing' }
42
+ it 'default strategy is just call `.new` passing the hash' do
43
+ new_object = Class.new do
44
+ attr_reader :h
45
+
46
+ include Hashing
47
+ hasherize :omg
48
+
49
+ def initialize(h)
50
+ @h = h
51
+ end
52
+ end.from_hash omg: 'lol'
53
+
54
+ new_object.h.must_be :==, { omg: 'lol' }
54
55
  end
55
56
 
56
57
  it 'give an informative message in case the Hash is malformed' do
57
- OmgLolBBQ = hasherized
58
+ OmgLolBBQ = Class.new do
59
+ include Hashing
60
+ hasherize(:h).loading ->(hash) { $stdout.write hash }
61
+ end
62
+
58
63
  message = nil
59
- proc {
64
+
65
+ -> {
60
66
  begin
61
67
  OmgLolBBQ.from_hash xpto: 'JUST NO!'
62
68
  rescue => e
63
69
  message = e.message
64
70
  raise e
65
71
  end
66
- }.must_raise Hashing::UnconfiguredIvar
67
- message.must_be :==, 'The Hash has a :xpto key, but no @xpto '+
68
- 'was configured in OmgLolBBQ'
72
+ }.must_raise Hashing::UnconfiguredIvarError
73
+
74
+ message.must_be :==, 'The hash passed to OmgLolBBQ.from_hash has the '+
75
+ 'following keys that aren\'t configured by the .hasherize method: '+
76
+ 'xpto.'
69
77
  end
70
78
  end
71
79
  end
@@ -78,10 +86,10 @@ describe Hashing do
78
86
  attr_reader :content
79
87
 
80
88
  hasherize :file, :commit
81
- hasherize :content,
82
- to_hash: ->(content) { Base64.encode64(content) },
83
- from_hash: ->(hash_string) { Base64.decode64(hash_string) }
84
- loading ->(hash) { new hash[:file], hash[:commit], hash[:content] }
89
+ hasherize(:content).
90
+ to(->(content) { Base64.encode64(content) }).
91
+ from(->(hash_string) { Base64.decode64(hash_string) }).
92
+ loading(->(hash) { new hash[:file], hash[:commit], hash[:content] })
85
93
 
86
94
  def initialize(file, commit, content)
87
95
  @file, @commit, @content = file, commit, content
@@ -106,8 +114,8 @@ describe Hashing do
106
114
  let(:hasherized) do
107
115
  Class.new do
108
116
  include Hashing
109
- hasherize :file, :commit
110
- loading ->(hash) { new hash[:file], hash[:commit] }
117
+ hasherize(:file, :commit).
118
+ loading ->(hash) { new hash[:file], hash[:commit] }
111
119
 
112
120
  def initialize(file, commit)
113
121
  @file, @commit = file, commit
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: hashing
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.1
4
+ version: 0.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ricardo Valeriano
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2014-05-02 00:00:00.000000000 Z
11
+ date: 2014-05-23 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -28,14 +28,14 @@ dependencies:
28
28
  name: rake
29
29
  requirement: !ruby/object:Gem::Requirement
30
30
  requirements:
31
- - - '>='
31
+ - - ! '>='
32
32
  - !ruby/object:Gem::Version
33
33
  version: '0'
34
34
  type: :development
35
35
  prerelease: false
36
36
  version_requirements: !ruby/object:Gem::Requirement
37
37
  requirements:
38
- - - '>='
38
+ - - ! '>='
39
39
  - !ruby/object:Gem::Version
40
40
  version: '0'
41
41
  - !ruby/object:Gem::Dependency
@@ -52,9 +52,9 @@ dependencies:
52
52
  - - ~>
53
53
  - !ruby/object:Gem::Version
54
54
  version: '1.0'
55
- description: Gives you an easy way to specify which instances vars of your objects
56
- should be used as `key => value` to serialize it into a hash returned by the `#to_h`
57
- method. Also gives you a `YourClass::from_hash` to reconstruct the instances.
55
+ description: Provides an easy way to specify which instances vars of your objects
56
+ should be used as `key` in a Hash returned by the `#to_h` method. Also gives you
57
+ a `YourClass::from_hash` to reconstruct the instances.
58
58
  email:
59
59
  - ricardo.valeriano@gmail.com
60
60
  executables: []
@@ -63,17 +63,21 @@ extra_rdoc_files: []
63
63
  files:
64
64
  - .gitignore
65
65
  - .travis.yml
66
+ - CHANGELOG.md
66
67
  - Gemfile
67
68
  - LICENSE.txt
68
69
  - README.md
69
70
  - Rakefile
70
71
  - hashing.gemspec
71
- - lib/hasherize.rb
72
72
  - lib/hashing.rb
73
+ - lib/hashing/hasher.rb
73
74
  - lib/hashing/ivar.rb
75
+ - lib/hashing/ivar_collection.rb
76
+ - lib/hashing/macros.rb
77
+ - lib/hashing/unconfigured_ivar_error.rb
74
78
  - lib/hashing/version.rb
75
79
  - tasks/test.rake
76
- - test/hasherize_test.rb
80
+ - test/hashing/hasher_test.rb
77
81
  - test/hashing/ivar_test.rb
78
82
  - test/hashing/nested_test.rb
79
83
  - test/hashing_test.rb
@@ -88,23 +92,24 @@ require_paths:
88
92
  - lib
89
93
  required_ruby_version: !ruby/object:Gem::Requirement
90
94
  requirements:
91
- - - '>='
95
+ - - ! '>='
92
96
  - !ruby/object:Gem::Version
93
97
  version: '0'
94
98
  required_rubygems_version: !ruby/object:Gem::Requirement
95
99
  requirements:
96
- - - '>='
100
+ - - ! '>='
97
101
  - !ruby/object:Gem::Version
98
102
  version: '0'
99
103
  requirements: []
100
104
  rubyforge_project:
101
- rubygems_version: 2.1.11
105
+ rubygems_version: 2.1.4
102
106
  signing_key:
103
107
  specification_version: 4
104
- summary: Serialize your objects as Hashes
108
+ summary: Serialize your objects into Hashes
105
109
  test_files:
106
- - test/hasherize_test.rb
110
+ - test/hashing/hasher_test.rb
107
111
  - test/hashing/ivar_test.rb
108
112
  - test/hashing/nested_test.rb
109
113
  - test/hashing_test.rb
110
114
  - test/test_helper.rb
115
+ has_rdoc:
@@ -1,40 +0,0 @@
1
- require 'hashing'
2
-
3
- # Provides some sugar syntax to declare which `ivars` should be used to
4
- # represent an object as a `Hash`.
5
- #
6
- # It respects all the behavior you will get by including {Hashing}. In fact,
7
- # using this constructor is a shortcut to `include Hashing`, and call
8
- # `.hasherize`
9
- #
10
- # @since 0.0.1
11
- #
12
- # @example shortcut to `include Hashing` and call `.hasherize`
13
- # class File
14
- # include Hasherize.new :path, :commit
15
- # end
16
- #
17
- # @example configuring :to_hash and :from_hash strategies
18
- # class File
19
- # include Hasherize.new :content,
20
- # to_hash: ->(content) { Base64.encode64 content },
21
- # from_hash: ->(content_string) { Base64.decode64 content_string }
22
- # end
23
- class Hasherize < Module
24
- # Stores the ivars and options to be repassed to `Hashing.serialize` by the
25
- # hook {#included}
26
- #
27
- # @param ivars_and_options [*args] ivar names and options (`:to_hash` and `:from_hash`)
28
- def initialize(*ivars_and_options)
29
- @ivars = ivars_and_options
30
- end
31
-
32
- # Includes the `Hashing` module and calls {Hashing.hasherize}, repassing the
33
- # ivar names an the options received in the constructor
34
- def included(serializable_class)
35
- serializable_class.module_eval do
36
- include Hashing
37
- end
38
- serializable_class.send :hasherize, *@ivars
39
- end
40
- end
@@ -1,32 +0,0 @@
1
- describe Hasherize do
2
- let(:hasherized) do
3
- Class.new do
4
- attr_reader :file, :commit
5
-
6
- include Hasherize.new :file, :commit,
7
- to_hash: ->(value) { "X-#{value}" },
8
- from_hash: ->(value) { "#{value}-X" }
9
-
10
- loading ->(params) { new params[:file], params[:commit] }
11
-
12
- def initialize(file, commit)
13
- @file, @commit = file, commit
14
- end
15
- end
16
- end
17
-
18
- it "just sugar to `include Hashing` and call `hasherize`" do
19
- hasherized.ancestors.include?(Hashing).must_be :==, true
20
- end
21
-
22
- it "configures the ivars correctly (so I can recreate instances by Hash)" do
23
- object = hasherized.from_hash file: 'README.md', commit: 'omglolbbq123'
24
- object.file.must_be :==, 'README.md-X'
25
- object.commit.must_be :==, 'omglolbbq123-X'
26
- end
27
-
28
- it "configure `:to_hash` e `:from_hash` serialization strategies" do
29
- object = hasherized.new 'README.md', 'omglolbbq123'
30
- object.to_h.must_be :==, { file: 'X-README.md', commit: 'X-omglolbbq123' }
31
- end
32
- end