hashmodel 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
data/.document ADDED
@@ -0,0 +1,5 @@
1
+ lib/**/*.rb
2
+ bin/*
3
+ -
4
+ features/**/*.feature
5
+ LICENSE.txt
data/.rspec ADDED
@@ -0,0 +1 @@
1
+ --color
data/Gemfile ADDED
@@ -0,0 +1,14 @@
1
+ source "http://rubygems.org"
2
+ # Add dependencies required to use your gem here.
3
+ # Example:
4
+ # gem "activesupport", ">= 2.3.5"
5
+
6
+ # Add dependencies to develop your gem here.
7
+ # Include everything needed to run rake, tests, features, etc.
8
+ group :development do
9
+ gem "rspec", ">= 2.2.0"
10
+ gem "cucumber", ">= 0.10.0"
11
+ gem "bundler", ">= 1.0.0"
12
+ gem "jeweler", ">= 1.5.0.pre5"
13
+ gem "rcov", ">= 0"
14
+ end
data/Gemfile.lock ADDED
@@ -0,0 +1,41 @@
1
+ GEM
2
+ remote: http://rubygems.org/
3
+ specs:
4
+ builder (3.0.0)
5
+ cucumber (0.10.0)
6
+ builder (>= 2.1.2)
7
+ diff-lcs (~> 1.1.2)
8
+ gherkin (~> 2.3.2)
9
+ json (~> 1.4.6)
10
+ term-ansicolor (~> 1.0.5)
11
+ diff-lcs (1.1.2)
12
+ gherkin (2.3.2)
13
+ json (~> 1.4.6)
14
+ term-ansicolor (~> 1.0.5)
15
+ git (1.2.5)
16
+ jeweler (1.5.1)
17
+ bundler (~> 1.0.0)
18
+ git (>= 1.2.5)
19
+ rake
20
+ json (1.4.6)
21
+ rake (0.8.7)
22
+ rcov (0.9.9)
23
+ rspec (2.2.0)
24
+ rspec-core (~> 2.2)
25
+ rspec-expectations (~> 2.2)
26
+ rspec-mocks (~> 2.2)
27
+ rspec-core (2.2.1)
28
+ rspec-expectations (2.2.0)
29
+ diff-lcs (~> 1.1.2)
30
+ rspec-mocks (2.2.0)
31
+ term-ansicolor (1.0.5)
32
+
33
+ PLATFORMS
34
+ ruby
35
+
36
+ DEPENDENCIES
37
+ bundler (>= 1.0.0)
38
+ cucumber (>= 0.10.0)
39
+ jeweler (>= 1.5.0.pre5)
40
+ rcov
41
+ rspec (>= 2.2.0)
data/LICENSE.txt ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2010 Mike Bethany
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.markdown ADDED
@@ -0,0 +1,44 @@
1
+ # HashModel
2
+
3
+ A simple MVC type model class for storing deeply nested hashes as records.
4
+ It's meant to be used for small, in-memory recordset that you want an easy, flexible way to query.
5
+ It is not meant as a data storage device for managing huge datasets.
6
+
7
+ Note:
8
+ This is more of a programming exercise to learn about Ruby so if you're looking for a good
9
+ model class take a look at ActiveModel, it's probably more of what you're looking for.
10
+
11
+ ## Synopsis
12
+
13
+ The major usefulness of this class is it allows you to filter and search flattened records based on any field.
14
+ A field can contain anything, including another hash, a string, and array, or even an Object class like String or Array, not
15
+ just an instance of an Object class.
16
+
17
+ You can also search using boolean like logic e.g.
18
+
19
+ @hm = HashModel.new(:raw\_data=>@records)
20
+ found = @hm.where {@switch == "-x" && @parameter\_type == String}
21
+
22
+ ## Usage
23
+
24
+ Coming soon...
25
+
26
+ For now take a look at the spec files to see simple examples of how to use the HashModel
27
+
28
+
29
+
30
+ == Contributing to hash\_model
31
+
32
+ * Please feel free to correct any mistakes I make by correcting the code and sending me a pull request. Pull requests are handled ASAP.
33
+ * Check out the latest master to make sure the feature hasn't been implemented or the bug hasn't been fixed yet
34
+ * Check out the issue tracker to make sure someone already hasn't requested it and/or contributed it
35
+ * Fork the project
36
+ * Start a feature/bugfix branch
37
+ * Commit and push until you are happy with your contribution
38
+ * Make sure to add tests for it. This is important so I don't break it in a future version unintentionally.
39
+ * Please try not to mess with the Rakefile, version, or history. 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.
40
+
41
+ == Copyright
42
+
43
+ Copyright (c) 2010 Mike Bethany. See LICENSE.txt for further details.
44
+
data/Rakefile ADDED
@@ -0,0 +1,49 @@
1
+ require 'rubygems'
2
+ require 'bundler'
3
+ begin
4
+ Bundler.setup(:default, :development)
5
+ rescue Bundler::BundlerError => e
6
+ $stderr.puts e.message
7
+ $stderr.puts "Run `bundle install` to install missing gems"
8
+ exit e.status_code
9
+ end
10
+ require 'rake'
11
+ require './lib/hash_model/version'
12
+ version = MikBe::HashModel::VERSION::STRING
13
+
14
+ require 'jeweler'
15
+ Jeweler::Tasks.new do |gem|
16
+ gem.name = "hashmodel"
17
+ gem.version = version
18
+ gem.summary = %Q{Store small amounts of dynamic data and easily search fields (even nested ones)}
19
+ gem.description = %Q{A simple MVC type model class for storing records as an array of hashes. You can store deeply nested hashes and still easily flatten and querying the records using flattened field names.}
20
+ gem.email = "mikbe.tk@gmail.com"
21
+ gem.homepage = "http://github.com/mikbe/hashmodel"
22
+ gem.authors = ["Mike Bethany"]
23
+ end
24
+ Jeweler::RubygemsDotOrgTasks.new
25
+
26
+ require 'rspec/core'
27
+ require 'rspec/core/rake_task'
28
+ RSpec::Core::RakeTask.new(:spec) do |spec|
29
+ spec.pattern = FileList['spec/**/*_spec.rb']
30
+ end
31
+
32
+ RSpec::Core::RakeTask.new(:rcov) do |spec|
33
+ spec.pattern = 'spec/**/*_spec.rb'
34
+ spec.rcov = true
35
+ end
36
+
37
+ require 'cucumber/rake/task'
38
+ Cucumber::Rake::Task.new(:features)
39
+
40
+ task :default => :spec
41
+
42
+ require 'rake/rdoctask'
43
+ Rake::RDocTask.new do |rdoc|
44
+
45
+ rdoc.rdoc_dir = 'rdoc'
46
+ rdoc.title = "hash_model #{version}"
47
+ rdoc.rdoc_files.include('README*')
48
+ rdoc.rdoc_files.include('lib/**/*.rb')
49
+ end
@@ -0,0 +1,3 @@
1
+ Autotest.add_discovery {"rspec2"}
2
+ #Autotest.add_discovery {"cucumber"}
3
+ #AUTOFEATURE=true
data/features/README ADDED
@@ -0,0 +1,9 @@
1
+ These features really only make sense to write when I'm trying to figure out what
2
+ functionality I want to add. When I already know it just seems redundant to do it
3
+ here since this app is such a low level library.
4
+
5
+ The Cucumber methodologies don't seem to be necessary most of the time since I
6
+ RSpec seems to be a more natural fit for a programming library. I'm probably
7
+ wrong but it really does seem to be a lot of redundancy for this application.
8
+
9
+ Oh, plus they suck... I'm still learning.
@@ -0,0 +1,34 @@
1
+ Feature: Flatten the HashModel
2
+ In order to customize looping over the HashModel records
3
+ As a programmer
4
+ I want to be able to flatten the HashModel to one field
5
+ Or flatten all fields
6
+
7
+ Background:
8
+ Given we have a test table
9
+ | switch | description |
10
+ | :switch=>["-x","--xtended"] | :description=>"This is a description" |
11
+ | :switch=>"-y" | :description=>"Why not?" |
12
+ | :switch=>["-z","--zee"] | :description=>"head for zee hills" |
13
+
14
+ Scenario: Create a HashModel
15
+ Given we have a HashModel instance
16
+ When the HashModel is populated with the test table
17
+ Then the flatten index should be :switch
18
+
19
+ Scenario: Change the flatten index
20
+ Given we have a HashModel instance
21
+ When the HashModel is populated with the test table
22
+ And the flatten index is set to :description
23
+ Then the flatten index should be :description
24
+
25
+ Scenario: Flatten input hashes to the default flatten index
26
+ Given we have a HashModel instance
27
+ When the HashModel is populated with the test table
28
+ Then the HashModel recordset should look like
29
+ | id | group_id | switch | description |
30
+ | :hm_id=>0 | :hm_group_id=> 0 | :switch=>"-x" | :description=>"This is a description" |
31
+ | :hm_id=>1 | :hm_group_id=> 0 | :switch=>"--xtended" | :description=>"This is a description" |
32
+ | :hm_id=>2 | :hm_group_id=> 1 | :switch=>"-y" | :description=>"Why not?" |
33
+ | :hm_id=>3 | :hm_group_id=> 2 | :switch=>"-z" | :description=>"head for zee hills" |
34
+ | :hm_id=>4 | :hm_group_id=> 2 | :switch=>"--zee" | :description=>"head for zee hills" |
@@ -0,0 +1,18 @@
1
+ Feature: Return the siblings of a flattened record
2
+ In order to identify all the records created from a single raw record
3
+ As a programmer
4
+ I want to easily retrieve any records within the same group as a flattened record.
5
+
6
+
7
+ Background:
8
+ Given we have a test table
9
+ | switch | description |
10
+ | :switch=>["-x","--xtended"] | :description=>"This is a description" |
11
+ | :switch=>"-y" | :description=>"Why not?" |
12
+ | :switch=>["-z","--zee"] | :description=>"head for zee hills" |
13
+
14
+ Scenario: Get siblings for a record
15
+ Given we have a HashModel instance
16
+ When the HashModel is populated with the test table
17
+ And the siblings are retrieved for a record with parameter "-y"
18
+ Then all the siblings should have the same group id
@@ -0,0 +1,34 @@
1
+ Feature: Search a HashModel using boolean logic
2
+ In order to find records of interest
3
+ As a programmer
4
+ I want to specify an SQL like query string using boolean logic like:
5
+ (something == "something" && something_else == 11) || more_stuff != String || extra_stuff.class == Potato
6
+
7
+ Background:
8
+ Given we have a test table
9
+ | switch | description |
10
+ | :switch=>["-x","--xtended"] | :description=>"This is a description" |
11
+ | :switch=>"-y" | :description=>"Why not?" |
12
+ | :switch=>["-z","--zee"] | :description=>"head for zee hills" |
13
+
14
+ Scenario: Search using a parameter
15
+ Given we have a HashModel instance
16
+ And the HashModel is populated with the test table
17
+ When we search with the single parameter "-x"
18
+ Then the search recordset should look like
19
+ | id | group_id | switch | description |
20
+ | :hm_id=>0 | :hm_group_id=> 0 | :switch=>"-x" | :description=>"This is a description" |
21
+
22
+ @active
23
+ Scenario: Search using a block of boolean logic
24
+ Given we have a HashModel instance
25
+ And the HashModel is populated with the test table
26
+ When we search with the block {@switch == "-x"}
27
+ Then the search recordset should look like
28
+ | id | group_id | switch | description |
29
+ | :hm_id=>0 | :hm_group_id=> 0 | :switch=>"-x" | :description=>"This is a description" |
30
+
31
+
32
+
33
+
34
+
@@ -0,0 +1,60 @@
1
+ Given /^we have a test table$/ do |table|
2
+ @test_table = table
3
+ end
4
+
5
+ Given /^we have a HashModel instance$/ do
6
+ @hm = MikBe::HashModel.new
7
+ end
8
+
9
+ When /^the HashModel is populated with the test table$/ do
10
+ @test_table.rows.each do |record|
11
+ @hm.add(array_to_hash(record))
12
+ end
13
+ end
14
+
15
+ Then /^the flatten index should be :(.*)$/ do |index|
16
+ @hm.flatten_index.should == index.to_sym
17
+ end
18
+
19
+ When /^the flatten index is set to :(.*)$/ do |index|
20
+ @hm.flatten_index = index.to_sym
21
+ end
22
+
23
+ Then /^the flatten index should be nil$/ do
24
+ @hm.flatten_index.should == nil
25
+ end
26
+
27
+ Then /^the HashModel recordset should look like$/ do |example_table|
28
+ formatted_table = table_to_array(example_table)
29
+ @hm.should == formatted_table
30
+ end
31
+
32
+ When /^the siblings are retrieved for a record with parameter "([^"]*)"$/ do |parameter|
33
+ @siblings = @hm.group("#{parameter}")
34
+ end
35
+
36
+ Then /^all the siblings should have the same group id$/ do
37
+ group_ids = []
38
+ @siblings.each {|record| group_ids << record[:hm_group_id]}
39
+ group_ids.uniq.length.should == 1
40
+ group_ids.uniq[0].should_not == nil
41
+ end
42
+
43
+ When /^we search with the single parameter "([^"]*)"$/ do |parameter|
44
+ @hm_search = @hm.where(parameter)
45
+ end
46
+
47
+ When /^we search with the block \{@switch == \"-x\"\}$/ do
48
+ # I know, this is sloppy, I'm just trying to define the basic functionality
49
+ # Real tests are in the RSpecs
50
+ @hm_search = @hm.where{@switch == "-x"}
51
+ end
52
+
53
+
54
+ Then /^the search recordset should look like$/ do |table|
55
+ test_table = table_to_array(table)
56
+ @hm_search.each_with_index do |record, index|
57
+ record.should == test_table[index]
58
+ end
59
+
60
+ end
@@ -0,0 +1,14 @@
1
+ require 'bundler'
2
+ begin
3
+ Bundler.setup(:default, :development)
4
+ rescue Bundler::BundlerError => e
5
+ $stderr.puts e.message
6
+ $stderr.puts "Run `bundle install` to install missing gems"
7
+ exit e.status_code
8
+ end
9
+
10
+ $LOAD_PATH.unshift(File.dirname(__FILE__) + '/../../lib')
11
+ require 'hash_model'
12
+ require 'rspec/expectations'
13
+
14
+ # load helper functions
@@ -0,0 +1,18 @@
1
+ module HashModelHelpers
2
+ # converts ugly arrays from Cucumber::AST::Tables into nice neat hashes
3
+ def array_to_hash(array)
4
+ hash = {}
5
+ array.each { |field| eval("hash.merge!({#{field}})") }
6
+ hash
7
+ end
8
+
9
+ # wrapper for converting Cucumber::AST::Tables into an array of nice neat hashes
10
+ def table_to_array(input_table)
11
+ table_of_hashes = []
12
+ input_table.rows.each do |record|
13
+ table_of_hashes << array_to_hash(record)
14
+ end
15
+ table_of_hashes
16
+ end
17
+ end
18
+ World(HashModelHelpers)
data/lib/hash_model.rb ADDED
@@ -0,0 +1,4 @@
1
+ $: << '.'
2
+ require 'hash_model/hash_model'
3
+ require 'hash_model/exceptions'
4
+ require 'hash_model/version'
@@ -0,0 +1,5 @@
1
+ module MikBe
2
+
3
+ class ReservedNameError < StandardError ; end
4
+
5
+ end
@@ -0,0 +1,411 @@
1
+ # Pickle Pumpers namespace
2
+ module MikBe
3
+
4
+ # A simple MVC type model class for storing hashes as flattenable, searchable records
5
+ class HashModel
6
+ include Enumerable
7
+
8
+ def initialize(parameters={})
9
+ # Initialize variables
10
+ clear
11
+
12
+ # Map Array methods
13
+ mimic_methods
14
+
15
+ # Set values given as hashes
16
+ parameters.each { |key,value| instance_variable_set("@#{key}", value) }
17
+
18
+ check_field_names(@raw_data) if !@raw_data.empty?
19
+
20
+ # Setup the flat data
21
+ flatten
22
+
23
+ end
24
+
25
+ ## Properties
26
+
27
+ attr_accessor :flatten_index, :raw_data
28
+
29
+ # Sets field name used to flatten the recordset
30
+ def flatten_index=(value)
31
+ @flatten_index = value
32
+ flatten
33
+ end
34
+
35
+ # Are the records being filtered?
36
+ def filtered?
37
+ !!@filter
38
+ end
39
+
40
+ # Trap changes to raw data so we can re-flatten the data
41
+ def raw_data=(value)
42
+ value = [] if value.nil?
43
+ raise SyntaxError, "Raw data may only be an array of hashes" if value.class != Array
44
+ check_field_names(value)
45
+ @raw_data = value.clone
46
+ end
47
+
48
+
49
+ ## Public Methods
50
+
51
+ # Freeze the raw data
52
+ def freeze
53
+ @raw_data.freeze
54
+ @modified_data.freeze
55
+ @flatten_index.freeze
56
+ @filter.freeze
57
+ super
58
+ end
59
+
60
+ # Remove the where filter
61
+ def clear_filter
62
+ @filter = nil
63
+ flatten
64
+ end
65
+ alias :clear_where :clear_filter # in case this makes more sense to people
66
+
67
+ # Reset the HashModel
68
+ def clear
69
+ @raw_data = []
70
+ @modified_data = []
71
+ @flatten_index = nil
72
+ @filter = nil
73
+ end
74
+
75
+
76
+ ## Operators
77
+
78
+ # Overload Array#<< function so we can create the flatten index as the first record is added
79
+ # and allows us to send back this instance of the HashModel instead of an array.
80
+ def <<(value)
81
+ case value
82
+ when HashModel
83
+ @raw_data.concat(value.raw_data)
84
+ when Hash
85
+ check_field_names(value)
86
+ @raw_data << value
87
+ when Array
88
+ # It goes crazy if you don't clone the array before recursing
89
+ value.clone.each{ |member| self << member }
90
+ else
91
+ raise SyntaxError, "You may only add a hash, another HashModel, or an array of either"
92
+ end
93
+ flatten
94
+ end
95
+ # I like the method name "add" for adding to recordsets seems more natural
96
+ alias :add :<<
97
+ alias :concat :<<
98
+ alias :push :<<
99
+
100
+ # remap... no loops... you know the deal
101
+ alias :_equals_ :==
102
+
103
+ # Compare values with the HashModel based on the type of values given
104
+ def ==(value)
105
+ flatten
106
+ case value
107
+ when HashModel
108
+ @raw_data == value.raw_data &&
109
+ @flatten_index == value.flatten_index &&
110
+ @modified_data == value
111
+ when Array
112
+ # test for something other than hashes, a flattened recordset, or raw data
113
+ if !value.empty? && value[0].class == Hash && value[0].has_key?(:hm_group_id)
114
+ @modified_data == value
115
+ else
116
+ @raw_data == value
117
+ end
118
+ else
119
+ false
120
+ end
121
+ end
122
+ alias :eql? :==
123
+
124
+
125
+ # Remap spaceship to stop infinite loops
126
+ alias :_spaceship_ :<=>
127
+ private :_spaceship_
128
+
129
+ # Spaceship - Don't probe me bro'!
130
+ def <=>(value)
131
+ case value
132
+ when HashModel
133
+ _spaceship_(value)
134
+ when Array
135
+ # test for a flattened recordset or raw data
136
+ if !value.empty? && value[0].has_key?(:hm_group_id)
137
+ @modified_data <=> value
138
+ else
139
+ @raw_data <=> value
140
+ end
141
+ else
142
+ nil
143
+ end
144
+ end
145
+
146
+
147
+ ## Searching
148
+
149
+ # Tests flat or raw data depending of if you use flat or raw data
150
+ def include?(value)
151
+ return false if value.class != Hash
152
+ @modified_data.include?(value) || @raw_data.include?(value)
153
+ end
154
+
155
+ # Search creating a new instance of HashModel based on this one
156
+ def where(value=nil, &search)
157
+ self.clone.where!(value, &search)
158
+ end
159
+
160
+ # Search the flattened records using a
161
+ def where!(value=nil, &search)
162
+ # Parameter checks
163
+ raise SyntaxError, "You may only provide a parameter or a block but not both" if value && !search.nil?
164
+
165
+ # Allow clearing the filter and returning the entire recordset if nothing is given
166
+ if !value && search.nil?
167
+ @filter = nil
168
+ return flatten
169
+ end
170
+
171
+ # If given a parameter make our own search based on the flatten index
172
+ if !value.nil?
173
+ # Make sure the field name is available to the proc
174
+ flatten_index = @flatten_index
175
+ search = proc do
176
+ instance_variable_get("@#{flatten_index}") == value
177
+ end # search
178
+ end # !value.nil?
179
+
180
+ # Set and process the filter
181
+ @filter = search
182
+ flatten
183
+ end
184
+
185
+ # Return the other records created from the same raw data record as the one(s) searched for
186
+ def group(value=nil, &search)
187
+ if !value.nil? || !search.nil?
188
+ sibling = where(value, &search)
189
+ else
190
+ sibling = where &@filter
191
+ end
192
+
193
+ # Get all the unique group id's
194
+ group_ids = sibling.collect {|hash| hash[:hm_group_id]}.uniq
195
+
196
+ # Find any records with matching group ids
197
+ where {group_ids.include? @hm_group_id}
198
+ end
199
+
200
+ # Group the records in place based on the existing filter
201
+ # This is basically a short hand for filtering based on
202
+ # group ids of filtered records
203
+ def group!(value=nil, &search)
204
+
205
+ if !value.nil? || !search.nil?
206
+ where!(value, &search)
207
+ end
208
+
209
+ # Get all the unique group id's
210
+ group_ids = @modified_data.collect {|hash| hash[:hm_group_id]}.uniq
211
+
212
+ # Find any records with matching group ids
213
+ where! {group_ids.include? @hm_group_id}
214
+ end
215
+
216
+ # Find the raw data record for a given flat record
217
+ def parent(flat_record)
218
+ flatten
219
+ @raw_data[flat_record[:hm_group_id]]
220
+ end
221
+
222
+ # Set the array value for self to the flattened hashes based on the flatten_index
223
+ def flatten
224
+ # Don't flatten the data if we don't need to
225
+ return self if !dirty?
226
+
227
+ id = -1
228
+ group_id = -1
229
+ @modified_data.clear
230
+ # set the flatten index if this is the first time the function is called
231
+ @flatten_index = @raw_data[0].keys[0] if @raw_data != [] && @flatten_index.nil?
232
+ flatten_index = @flatten_index.to_s
233
+
234
+ # Flatten and filter the raw data
235
+ @raw_data.each do |record|
236
+ new_records, duplicate_data = flatten_hash(record, flatten_index)
237
+ # catch raw data records that don't have the flatten index
238
+ new_records << {@flatten_index.to_sym=>nil} if new_records.empty?
239
+ group_id += 1
240
+ new_records.collect! do |new_record|
241
+ # Double bangs aren't needed but are they more efficient?
242
+ new_record.merge!( duplicate_data.merge!( { :hm_id=>(id+=1), :hm_group_id=>group_id } ) )
243
+ end
244
+
245
+ # Add the records to modified data if they pass the filter
246
+ new_records.each do |new_record|
247
+ @modified_data << new_record if @filter.nil? ? true : (create_object_from_flat_hash(new_record).instance_eval &@filter)
248
+ end
249
+ end # raw_data.each
250
+ set_dirty_hash
251
+ self
252
+ end # flatten
253
+
254
+ # If the hash_model has been changed but not flattened
255
+ def dirty?
256
+ get_current_dirty_hash != @dirty_hash
257
+ end
258
+
259
+ # Return a string consisting of the flattened data
260
+ def to_s
261
+ @modified_data.to_s
262
+ end
263
+
264
+ # Return an array of the flattened data
265
+ def to_ary
266
+ @modified_data.to_ary
267
+ end
268
+
269
+ # Iterate over the flattened records
270
+ def each
271
+ @modified_data.each do |record|
272
+ # change or manipulate the values in your value array inside this block
273
+ yield record
274
+ end
275
+ end
276
+
277
+ private
278
+
279
+ # Checks hash keys for reserved field names
280
+ def check_field_names(input)
281
+ case input
282
+ when Hash
283
+ input.each do |key, value|
284
+ raise ReservedNameError, "use of reserved name :#{key} as a field name." if [:hm_id, :hm_group_id].include?(key)
285
+ check_field_names(value)
286
+ end
287
+ when Array
288
+ input.clone.each { |record| check_field_names(record) }
289
+ end
290
+ end
291
+
292
+ # Save a hash for later evaluation
293
+ def set_dirty_hash
294
+ @dirty_hash = get_current_dirty_hash
295
+ end
296
+
297
+ # Create a hash based on internal values
298
+ def get_current_dirty_hash
299
+ # self.hash won't work
300
+ [@raw_data.hash, @filter.hash, @flatten_index.hash].hash
301
+ end
302
+
303
+ # Recursively convert a single record into an array of new
304
+ # records that are flattened based on the given flattened hash key
305
+ # e.g. {:x=>{:x1=>1}, :y=>{:y1=>{:y2=>2,:y3=>4}, y4:=>5}, :z=>6}
306
+ # if you wanted to flatten to :x1 you would set flatten_index to :x_x1
307
+ # To flatten to :y2 you would set flatten_index to :y_y1_y2
308
+ def flatten_hash(input, flatten_index, recordset=[], duplicate_data={}, parent_key=nil)
309
+ case input
310
+ when Hash
311
+ # Check to see if the found key is on this level - We need to add duplicate data differently if so
312
+ found_key = (input.select { |key, value| flatten_index == "#{parent_key}#{"_" if !parent_key.nil?}#{key}"} != {})
313
+
314
+ # Add records for matching flatten fields and save duplicate record data for later addition to each record.
315
+ input.each do |key, value|
316
+ flat_key = "#{parent_key}#{"_" if !parent_key.nil?}#{key}"
317
+ flat_key_starts_with_flatten_index = flat_key.start_with?(flatten_index)
318
+ flatten_index_starts_with_flat_key = flatten_index.start_with?(flat_key)
319
+ # figure out what we need to do based on where we're at in the record's value tree and man does it look ugly
320
+ if flat_key == flatten_index
321
+ # go deeper
322
+ recordset, duplicate_data = flatten_hash(value, flatten_index, recordset, duplicate_data, flat_key)
323
+ elsif flat_key_starts_with_flatten_index && !flatten_index_starts_with_flat_key
324
+ # new record
325
+ recordset << {parent_key.to_sym=>{key=>value}}
326
+ elsif !flat_key_starts_with_flatten_index && flatten_index_starts_with_flat_key
327
+ # go deeper
328
+ recordset, duplicate_data = flatten_hash(value, flatten_index, recordset, duplicate_data, flat_key)
329
+ elsif found_key
330
+ # add to dup data for same level as flatten index
331
+ duplicate_data.merge!(flat_key.to_sym=>value)
332
+ else
333
+ # add to dupe data
334
+ duplicate_data.merge!(key=>value)
335
+ end
336
+ end # input.each
337
+ when Array
338
+ input.each do |value|
339
+ recordset, duplicate_data = flatten_hash(value, flatten_index, recordset, duplicate_data, parent_key)
340
+ end
341
+ else
342
+ recordset << {parent_key.to_sym=>input}
343
+ end # case
344
+ return recordset, duplicate_data
345
+ end # flatten_hash
346
+
347
+ # Creates an object with instance variables for each field at every level
348
+ # This allows using a block like {:field1==true && :field2_subfield21="potato"}
349
+ def create_object_from_flat_hash(record, hash_record=Class.new.new, parent_key=nil)
350
+
351
+ # Iterate through the record creating the object recursively
352
+ case record
353
+ when Hash
354
+ record.each do |key, value|
355
+ flat_key = "#{parent_key}#{"_" if !parent_key.nil?}#{key}"
356
+ hash_record.instance_variable_set("@#{flat_key}", value)
357
+ hash_record = create_object_from_flat_hash(value, hash_record, flat_key)
358
+ end
359
+ when Array
360
+ record.each do |value|
361
+ hash_record = create_object_from_flat_hash(value, hash_record, parent_key)
362
+ end
363
+ else
364
+ hash_record.instance_variable_set("@#{parent_key}", record)
365
+ end # case
366
+
367
+ hash_record
368
+ end # create_object_from_flat_hash
369
+
370
+ # Deal with the array methods allowing multiple functions to use the same code
371
+ # You couldn't do this with alias because you can't tell what alias is used.
372
+ #
373
+ # My rule for using this vs a seperate method is if I can use the
374
+ # same code for more than one method it goes in here, if the method
375
+ # only works for one method then it gets its own method.
376
+ def wrapper_method(method, *args, &block)
377
+ # grab the raw data if it's a hashmodel
378
+ case method
379
+ when :[], :each_index, :uniq, :last, :collect, :length, :at, :map, :combination, :count, :cycle, :empty?, :fetch, :index, :first, :permutation, :size, :values_at
380
+ flatten
381
+ @modified_data.send(method, *args, &block)
382
+ when :+, :*
383
+ case args[0]
384
+ when HashModel
385
+ args = [args[0].raw_data]
386
+ when Hash
387
+ args = [args]
388
+ end
389
+ clone = self.clone
390
+ clone.raw_data = clone.raw_data.send(method, *args, &block)
391
+ clone.flatten
392
+ else
393
+ raise NoMethodError, "undefined method `#{method}' for #{self}"
394
+ end
395
+ end
396
+
397
+ # create methods like the given object so we can trap them
398
+ def mimic_methods
399
+ Array.new.public_methods(false).each do |method|
400
+ # Don't mimic methods we specifically declare or methods that don't make sense for the class
401
+ if !self.respond_to?(method)
402
+ self.class.class_eval do
403
+ define_method(method) { |*args, &block| wrapper_method(method, *args, &block) }
404
+ end
405
+ end
406
+ end
407
+ end
408
+
409
+ end # HashModel
410
+
411
+ end # MikBe