hashmodel 0.3.1 → 0.4.0.beta1

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- hashmodel (0.3.0.beta2)
4
+ hashmodel (0.4.0)
5
5
  file-tail
6
6
  sourcify
7
7
 
@@ -18,27 +18,28 @@ GEM
18
18
  diff-lcs (1.1.2)
19
19
  file-tail (1.0.5)
20
20
  spruz (>= 0.1.0)
21
- gherkin (2.3.3)
21
+ gherkin (2.3.4)
22
22
  json (~> 1.4.6)
23
23
  json (1.4.6)
24
- rspec (2.4.0)
25
- rspec-core (~> 2.4.0)
26
- rspec-expectations (~> 2.4.0)
27
- rspec-mocks (~> 2.4.0)
28
- rspec-core (2.4.0)
29
- rspec-expectations (2.4.0)
24
+ rspec (2.5.0)
25
+ rspec-core (~> 2.5.0)
26
+ rspec-expectations (~> 2.5.0)
27
+ rspec-mocks (~> 2.5.0)
28
+ rspec-core (2.5.1)
29
+ rspec-expectations (2.5.0)
30
30
  diff-lcs (~> 1.1.2)
31
- rspec-mocks (2.4.0)
31
+ rspec-mocks (2.5.0)
32
32
  ruby2ruby (1.2.5)
33
33
  ruby_parser (~> 2.0)
34
34
  sexp_processor (~> 3.0)
35
- ruby_parser (2.0.5)
35
+ ruby_parser (2.0.6)
36
36
  sexp_processor (~> 3.0)
37
37
  sexp_processor (3.0.5)
38
- sourcify (0.4.0)
38
+ sourcify (0.4.2)
39
+ file-tail (>= 1.0.5)
39
40
  ruby2ruby (>= 1.2.5)
40
41
  sexp_processor (>= 3.0.5)
41
- spruz (0.2.2)
42
+ spruz (0.2.5)
42
43
  term-ansicolor (1.0.5)
43
44
 
44
45
  PLATFORMS
@@ -46,7 +47,5 @@ PLATFORMS
46
47
 
47
48
  DEPENDENCIES
48
49
  cucumber
49
- file-tail
50
50
  hashmodel!
51
51
  rspec
52
- sourcify
@@ -6,7 +6,8 @@ It is not meant as a data storage device for managing huge datasets.
6
6
 
7
7
  ## Synopsis
8
8
 
9
- The major usefulness of this class is it allows you to filter and search flattened records based on any field.
9
+ The major usefulness of this class is it allows you to filter and search flattened records based on any field. You can even updated and delete data now.
10
+
10
11
  A field can contain anything, including another hash, a string, an array, or even an Object class like String or Array, not just an instance of an Object class.
11
12
 
12
13
  Searches are very simple and logical. You can search using just using the value of the default index
@@ -29,11 +30,9 @@ Or more powerfully you can search using boolean like logic e.g.
29
30
 
30
31
  ## Status
31
32
 
32
- 2011.03.18 - Production: 0.3.1
33
-
34
- ## Developer Notes
33
+ 2011.03.19 - Beta: 0.4.0.beta01
35
34
 
36
- If you have problems running Autotest on your RSpecs try including the gem file-tail in your app. You **shouldn't** have to since I include it here but I had problems with Autotest and Sourcify and adding that fixed it.
35
+ Lots of changes with this one. Mostly the ability to write to the HashModel and a few bug fixes. See Version History for details.
37
36
 
38
37
  ## Usage
39
38
 
@@ -216,7 +215,61 @@ I've covered most of the major stuff here but to see all of the functionality ta
216
215
  >> {:parameter=>[{:type=>String}, "glorp", {:require=>true}], :switch=>[{:deep1=>{:deep3=>"deepTwo"}}, {:deep2=>"deepTwo"}, "--xtend"], :description=>"Xish stuff"}
217
216
 
218
217
 
219
- ## Version History
218
+ ## Version History ##
219
+
220
+ 0.4.0.beta01 - 2011.03-19
221
+
222
+ Lots of updates and code fixes for this release. After using it for a little while I've broken down and added the write functionality I was avoiding previously.
223
+
224
+ **Additions/Changes**
225
+
226
+ #### Methods: `update` and `update!` methods ####
227
+ These methods use a `where` like search that is slightly different. The methods look like this `update(default_index_search, field_new_value_hash, boolean_search_block)`. So if you search using a single value, a default index search, then you put the update hashes at the end. If you want to search using a boolean search then you put the update hashes at the beginning.
228
+
229
+ For instance:
230
+
231
+ # Default index search
232
+ my_hash_model.update("-x", :parameter__type=>Fixnum)
233
+
234
+ # Boolean search
235
+ my_hash_model.update(:parameter__type=>Fixnum) {:switch == "-x"}
236
+
237
+ You don't have to put in any search criteria at all though, you can just put in the field you want updated and it will update all records that are in the current filter set.
238
+
239
+
240
+ #### Methods: `update` and `update!` methods ####
241
+ Just like `update` and `update!` but will also add a hash if it doesn't exist already.
242
+
243
+ For instance if your HashModel has a record like `{:a=>"a"}` and you do `my_hash_model.update(:b=>"b")` it won't change that record, but it you do `my_hash_model.update_and_add(:b=>"b")` then your record will be `{:a=>'a', :b=>"b"}`.
244
+
245
+
246
+ #### Methods: `delete` and `delete!` ####
247
+ These use a the standard `where` type search used everywhere else. This function removes data from the raw data so if you have other flattened records that are based on that raw data they will no longer exist since the data that generated them is gone.
248
+
249
+ Just like an array these methods return the records they deleted.
250
+
251
+ #### Method: `parent` ####
252
+ Again this uses a `where` search and returns all raw parent records for flattened records matching the search criteria. (This method was there before but hadn't actually be coded, it was just a copy of some code I started but shouldn't have been in a release version).
253
+
254
+ #### Changed: `filter` ####
255
+ Filter has been changed from a property to a method. It is exactly like a `where` search but it's in-place and non-destructive. Since `where!` was changed to be truly destructive this change was needed. It also makes all the search functions identical in their usage.
256
+
257
+ #### Changed: `where!` ####
258
+ Changed to be truly destructive. If you run a where! on the class you're losing records that don't match it. The non-destructive version `where` is changed in that it does not contain the raw data of any records that don't match the `where` clause but the original HashModel remains untouched.
259
+
260
+ #### Removed: `group!` ####
261
+ Since bangs (!) are all now destructive, to bring the class inline with Ruby standards, it doesn't make logical sense to have a a group method that deletes all data except the search data then tries to find siblings; they were just deleted with the destructive call. Instead just use `group` and it will return the sibling records without touching the data in the HashModel.
262
+
263
+ #### Other changes ####
264
+ Because of the new destructive methods all input values will be cloned. You don't have to worry about cloning input objects yourself. If it's clonable HashModel will clone it.
265
+
266
+ I've reorganized the code into multiple files based on functionality to make it easier to debug when adding new features. This is in anticipation of a major cleanup and refactoring.
267
+
268
+ Cleaned up RSpecs a little along the lines of reorganization.
269
+
270
+ #### Bug fixes ####
271
+ * Threw error if you searched an empty HashModel (can't build a flatten index on nothing)
272
+ * Couldn't change the flatten index in some rare cases.
220
273
 
221
274
  0.3.1 - 2011.03.18
222
275
 
@@ -253,7 +306,7 @@ e.g. hash_model.where{:x == "x" && :y == "y"} instead of the less natural hash_m
253
306
  * Released on wrong RubyGems account (yanked)
254
307
 
255
308
 
256
- ##Contributing to HashModel
309
+ ## Contributing to HashModel ##
257
310
 
258
311
  * Pull requests are handled ASAP.
259
312
  * Check out the latest master to make sure the feature hasn't been implemented or the bug hasn't been fixed yet
@@ -261,10 +314,10 @@ e.g. hash_model.where{:x == "x" && :y == "y"} instead of the less natural hash_m
261
314
  * Fork the project
262
315
  * Start a feature/bugfix branch
263
316
  * Commit and push until you are happy with your contribution
264
- * Make sure to add RSpecs in a separate file so I can easily tell what changed (changes without specs will not be pulled) for it.
317
+ * Make sure to add RSpecs in a separate file so I can easily tell what changed (changes without specs will not be pulled).
265
318
  * Changes to the configuration files, version numbers, or branches will not be pulled. If you want to have your own version, or is otherwise necessary, that is fine, but please isolate to its own commit so I can cherry-pick around it.
266
319
 
267
- ##Copyright
320
+ ## Copyright ##
268
321
 
269
- Copyright (c) 2010 Mike Bethany. See LICENSE.txt for further details.
322
+ Copyrighted free software - Copyright (c) 2011 Mike Bethany. See LICENSE.txt for further details.
270
323
 
@@ -15,6 +15,12 @@ Gem::Specification.new do |s|
15
15
  s.add_dependency "sourcify"
16
16
  s.add_dependency "file-tail"
17
17
 
18
+ # After hassling with version dependency it makes more sense NOT to
19
+ # declare version numbers and let it break if it is actually going
20
+ # to break instead of having it break every single time even if it
21
+ # wouldn't have broken with the new versions. Or maybe people need
22
+ # to be looser with their versioning requiremetns. My 2 cents.
23
+ #
18
24
  s.add_development_dependency "rspec"
19
25
  s.add_development_dependency "cucumber"
20
26
 
@@ -1,4 +1,11 @@
1
1
  require 'sourcify'
2
+ require 'hash_model/hash_model_delete'
3
+ require 'hash_model/hash_model_group'
4
+ require 'hash_model/hash_model_filter'
5
+ require 'hash_model/hash_model_update'
6
+ require 'hash_model/hash_model_where'
7
+
8
+
2
9
  # A simple MVC type model class for storing hashes as flattenable, searchable records
3
10
  class HashModel
4
11
  include Enumerable
@@ -11,12 +18,13 @@ class HashModel
11
18
  mimic_methods
12
19
 
13
20
  # Set values given as hashes
14
- parameters.each { |key,value| instance_variable_set("@#{key}", value) }
21
+ parameters.each { |key,value|
22
+ instance_variable_set("@#{key}", smart_clone(value)) }
15
23
 
16
24
  # Allow a single hash to be added with :raw_data
17
25
  @raw_data = [@raw_data] if @raw_data.class == Hash
18
26
 
19
- check_field_names(@raw_data) if !@raw_data.empty?
27
+ check_field_names(@raw_data) unless @raw_data.empty?
20
28
 
21
29
  # Setup the flat data
22
30
  flatten
@@ -25,12 +33,14 @@ class HashModel
25
33
 
26
34
  ## Properties
27
35
 
28
- attr_accessor :flatten_index, :raw_data, :filter
29
-
36
+ attr_accessor :flatten_index, :raw_data, :filter_proc
37
+
30
38
  # Sets field name used to flatten the recordset
31
39
  def flatten_index=(value)
32
40
  old_flatten = @flatten_index
41
+ old_filter = @filter_proc
33
42
  @flatten_index = value
43
+ @filter_proc = nil
34
44
  flatten
35
45
 
36
46
  # Verify the flatten index is a valid index
@@ -41,6 +51,7 @@ class HashModel
41
51
 
42
52
  unless flatten_found
43
53
  @flatten_index = old_flatten
54
+ @filter_proc = old_filter
44
55
  flatten
45
56
  raise ArgumentError, "Flatten index could not be created: #{value}"
46
57
  end
@@ -49,12 +60,7 @@ class HashModel
49
60
 
50
61
  # Are the records being filtered?
51
62
  def filtered?
52
- !!@filter
53
- end
54
-
55
- def filter=(filter)
56
- @filter = filter
57
- flatten
63
+ !!@filter_proc
58
64
  end
59
65
 
60
66
  # Trap changes to raw data so we can re-flatten the data
@@ -62,11 +68,10 @@ class HashModel
62
68
  value = [] if value.nil?
63
69
  raise SyntaxError, "Raw data may only be an array of hashes" if value.class != Array
64
70
  check_field_names(value)
65
- @raw_data = value.clone
71
+ @raw_data = smart_clone(value)
66
72
  flatten
67
73
  end
68
74
 
69
-
70
75
  ## Public Methods
71
76
 
72
77
  # Freeze all the data properties
@@ -79,7 +84,7 @@ class HashModel
79
84
 
80
85
  # Remove the in-place where filter
81
86
  def clear_filter
82
- @filter = nil
87
+ @filter_proc = nil
83
88
  flatten
84
89
  end
85
90
  alias :clear_where :clear_filter # in case this makes more sense to people
@@ -90,23 +95,25 @@ class HashModel
90
95
  @modified_data = []
91
96
  @unflatten_data = []
92
97
  @flatten_index = nil
93
- @filter = nil
98
+ @filter_proc = nil
94
99
  end
95
100
 
96
- # Force internal arrays to be cloned
101
+ # Force internal arrays and variables to be cloned
97
102
  def clone
103
+ return self if @raw_data.empty?
98
104
  flatten
99
105
  hm = HashModel.new(:raw_data=>@raw_data.clone)
100
106
  hm.flatten_index = @flatten_index
101
- hm.filter = @filter
107
+ hm.filter_proc = @filter_proc
102
108
  hm
103
109
  end
104
110
 
105
111
  ## Operators
106
112
 
107
- # Overload Array#<< function so we can create the flatten index as the first record is added
108
- # and allows us to send back this instance of the HashModel instead of an array.
113
+ # Overload Array#<< function so we can create the flatten index as the first record is added.
114
+ # This also allows us to send back this instance of the HashModel instead of an array.
109
115
  def <<(value)
116
+ value = smart_clone(value)
110
117
  case value
111
118
  when HashModel
112
119
  @raw_data.concat(value.raw_data)
@@ -116,8 +123,7 @@ class HashModel
116
123
  check_field_names(value)
117
124
  @raw_data << value
118
125
  when Array
119
- # It goes crazy if you don't clone the array before recursing
120
- value.clone.each{ |member| self << member }
126
+ value.each{ |member| self << member }
121
127
  else
122
128
  raise SyntaxError, "You may only add a hash, another HashModel, or an array of either"
123
129
  end
@@ -152,7 +158,6 @@ class HashModel
152
158
  end
153
159
  alias :eql? :==
154
160
 
155
-
156
161
  # Remap spaceship to stop infinite loops
157
162
  alias :_spaceship_ :<=>
158
163
  private :_spaceship_
@@ -174,7 +179,6 @@ class HashModel
174
179
  end
175
180
  end
176
181
 
177
-
178
182
  ## Searching
179
183
 
180
184
  # Tests flat or raw data depending of if you use flat or raw data
@@ -183,78 +187,17 @@ class HashModel
183
187
  @modified_data.include?(value) || @raw_data.include?(value)
184
188
  end
185
189
 
186
- # Search creating a new instance of HashModel based on this one
187
- def where(value=nil, &search)
188
- self.clone.where!(value, &search)
189
- end
190
-
191
- # Search the flattened records using the default flatten index or a boolean block
192
- def where!(value=nil, &search)
193
- # Parameter checks
194
- raise SyntaxError, "You may only provide a parameter or a block but not both" if value && !search.nil?
195
-
196
- # Allow clearing the filter and returning the entire recordset if nothing is given
197
- if !value && search.nil?
198
- @filter = nil
199
- return flatten
200
- end
201
-
202
- # If given a parameter make our own search based on the flatten index
203
- unless value.nil?
204
- # Make sure the field name is available to the proc
205
- case value
206
- when String
207
- string_search = ":#{@flatten_index} == \"#{value}\"".to_s
208
- when Symbol
209
- string_search = ":#{@flatten_index} == :#{value}".to_s
210
- else
211
- string_search = ":#{@flatten_index} == #{value}".to_s
212
- end
213
- else
214
- # Convert the proc to a string so it can be viewed
215
- # and later have :'s turned into @'s
216
-
217
- # Sourcify can create single or multi-line procs so we have to make sure we deal with them accordingly
218
- source = search.to_source
219
- unless (match = source.match(/^proc do\n(.*)\nend$/))
220
- match = source.match(/^proc { (.*) }$/)
221
- end
222
- string_search = match[1]
223
- end # !value.nil?
224
-
225
- # Set and process the filter
226
- @filter = string_search
227
- flatten
228
- end
229
-
230
- # Return the other records created from the same raw data record as the one(s) searched for
231
- def group(value=nil, &search)
232
- self.clone.group!(value, &search)
233
- end
234
-
235
- # Filter in place based on the parent record
236
- def group!(value=nil, &search)
237
- # Filter the recordset if applicable
238
- if !value.nil? || !search.nil?
239
- where!(value, &search)
240
- end
241
- # Get all the unique group id's
242
- group_ids = @modified_data.collect {|hash| hash[:_group_id]}.uniq
243
- self.filter = "#{group_ids.to_s}.include? :_group_id"
244
- flatten
245
- end
246
-
247
- # Find the raw data record for a given flat record
248
- def parent(flat_record)
249
- flatten
250
- @raw_data[flat_record[:_group_id]]
190
+ # Find the raw data record based on the search criteria
191
+ def parents(index_search=nil, &block_search)
192
+ flat_records = where(index_search, &block_search)
193
+ flat_records.collect{|flat| @raw_data[flat[:_group_id]]}.uniq
251
194
  end
252
195
 
253
196
  # Set the array value for self to the flattened hashes based on the flatten_index
254
197
  def flatten
255
198
  # Don't flatten the data if we don't need to
256
- return self if !dirty?
257
-
199
+ return self unless dirty?
200
+
258
201
  id = -1
259
202
  group_id = -1
260
203
  @modified_data.clear
@@ -262,6 +205,17 @@ class HashModel
262
205
  @flatten_index = @raw_data[0].keys[0] if @raw_data != [] && @flatten_index.nil?
263
206
  flatten_index = @flatten_index.to_s
264
207
 
208
+
209
+ # Change the filter so it looks for variables instead of symbols
210
+ unless @filter_proc.nil?
211
+ proc_filter = @filter_proc.clone
212
+ proc_filter.scan(/(:\S+) ==/).each {|match| proc_filter.sub!(match[0], match[0].sub(":","@"))}
213
+ proc_filter.sub!(":_group_id", "@_group_id")
214
+ proc_filter = "proc { #{proc_filter} }.call"
215
+ end
216
+ #dp "newfilter: #{proc_filter}"
217
+
218
+
265
219
  # Flatten and filter the raw data
266
220
  @raw_data.each do |record|
267
221
  new_records, duplicate_data = flatten_hash(record, flatten_index)
@@ -272,18 +226,11 @@ class HashModel
272
226
  # Double bangs aren't needed but are they more efficient?
273
227
  new_record.merge!( duplicate_data.merge!( { :_id=>(id+=1), :_group_id=>group_id } ) )
274
228
  end
275
-
276
- # Change the filter so it looks for variables instead of symbols
277
- unless @filter.nil?
278
- proc_filter = @filter.clone
279
- proc_filter.scan(/(:\S+) ==/).each {|match| proc_filter.sub!(match[0], match[0].sub(":","@"))}
280
- proc_filter.sub!(":_group_id", "@_group_id")
281
- proc_filter = "proc { #{proc_filter} }.call"
282
- end
283
-
229
+
284
230
  # Add the records to modified data if they pass the filter
285
231
  new_records.each do |new_record|
286
- unless @filter.nil?
232
+ #dp "rec: #{new_record}"
233
+ unless @filter_proc.nil?
287
234
  flat = create_object_from_flat_hash(new_record)
288
235
  @modified_data << new_record if flat.instance_eval proc_filter
289
236
  else
@@ -310,42 +257,41 @@ class HashModel
310
257
  # Return an array of the flattened data
311
258
  def to_ary
312
259
  flatten
313
- @modified_data.to_ary
260
+ @modified_data.clone.to_ary
314
261
  end
315
262
 
263
+ # Outputs the flattened data
316
264
  def to_a
317
265
  flatten
318
- @modified_data.to_a
266
+ @modified_data.clone.to_a
319
267
  end
320
268
 
321
269
  # Iterate over the flattened records
322
270
  def each
323
271
  @modified_data.each do |record|
324
- # change or manipulate the values in your value array inside this block
325
- yield record
272
+ yield record
326
273
  end
327
274
  end
328
275
 
329
276
  # Convert a flat record into an unflattened record
330
- def unflatten(input)
331
- HashModel.unflatten(input)
277
+ def unflatten(flat_record)
278
+ HashModel.unflatten(flat_record)
332
279
  end
333
280
 
334
281
  # Convert a flat record into an unflattened record
335
- def self.unflatten(input)
282
+ def self.unflatten(flat_record)
336
283
  # Seriously in need of a refactor, just looking at this hurts my brain
337
284
  # There's a lot of redundancy here.
338
- case input
285
+ case flat_record
339
286
  when Hash
340
287
  new_record = {}
341
- input.each do |key, value|
288
+ flat_record.each do |key, value|
342
289
  # recursively look for flattened keys
343
290
  keys = key.to_s.split("__", 2)
344
291
  if keys[1]
345
292
  key = keys[0].to_sym
346
293
  value = unflatten({keys[1].to_sym => value})
347
294
  end
348
-
349
295
  # Don't overwrite existing value
350
296
  if (existing = new_record[key])
351
297
  # convert to array and search for subkeys if appropriate
@@ -382,11 +328,19 @@ class HashModel
382
328
  new_record
383
329
  when Array
384
330
  # recurse into array
385
- input.collect! {|item| unflatten(item) }
331
+ flat_record.collect! {|item| unflatten(item) }
386
332
  else
387
- input
333
+ flat_record
388
334
  end
389
335
  end
336
+
337
+ protected
338
+
339
+ # Allows access to the internal filter, needed to make sure clones filter properly
340
+ def filter_proc=(filter)
341
+ @filter_proc = filter
342
+ flatten
343
+ end
390
344
 
391
345
  private
392
346
 
@@ -419,15 +373,15 @@ class HashModel
419
373
  end
420
374
 
421
375
  # Checks hash keys for reserved field names
422
- def check_field_names(input)
423
- case input
376
+ def check_field_names(argument_list)
377
+ case argument_list
424
378
  when Hash
425
- input.each do |key, value|
379
+ argument_list.each do |key, value|
426
380
  raise ReservedNameError, "use of reserved name :#{key} as a field name." if [:_id, :_group_id].include?(key)
427
381
  check_field_names(value)
428
382
  end
429
383
  when Array
430
- input.clone.each { |record| check_field_names(record) }
384
+ argument_list.clone.each { |record| check_field_names(record) }
431
385
  end
432
386
  end
433
387
 
@@ -439,7 +393,7 @@ class HashModel
439
393
  # Create a hash based on internal values
440
394
  def get_current_dirty_hash
441
395
  # self.hash won't work
442
- [@raw_data.hash, @filter.hash, @flatten_index.hash].hash
396
+ [@raw_data.hash, @filter_proc.hash, @flatten_index.hash].hash
443
397
  end
444
398
 
445
399
  # Recursively convert a single record into an array of new
@@ -563,4 +517,20 @@ class HashModel
563
517
  end
564
518
  end
565
519
 
520
+ # It's annoying to raise an error if an object can't
521
+ # be cloned, like in the case of symbols, It is much
522
+ # more friendly, and less surprising too, just to
523
+ # return the same object so you can go about your work.
524
+ # The only reason I clone is to protect the values, if
525
+ # the values don't need to be protected I don't want an
526
+ # annoying error message hosing up my whole day. </rant>
527
+ def smart_clone(object)
528
+ # Stupid error trapping
529
+ begin
530
+ object.clone
531
+ rescue
532
+ object
533
+ end
534
+ end
535
+
566
536
  end # HashModel