hashmodel 0.1.1
Sign up to get free protection for your applications and to get access to all the features.
- data/.document +5 -0
- data/.rspec +1 -0
- data/Gemfile +14 -0
- data/Gemfile.lock +41 -0
- data/LICENSE.txt +20 -0
- data/README.markdown +44 -0
- data/Rakefile +49 -0
- data/autotest/discover.rb +3 -0
- data/features/README +9 -0
- data/features/hash_model_flatten.feature +34 -0
- data/features/hash_model_grouping.feature +18 -0
- data/features/hash_model_search.feature +34 -0
- data/features/step_definitions/hash_model_steps.rb +60 -0
- data/features/support/env.rb +14 -0
- data/features/support/helper.rb +18 -0
- data/lib/hash_model.rb +4 -0
- data/lib/hash_model/exceptions.rb +5 -0
- data/lib/hash_model/hash_model.rb +411 -0
- data/lib/hash_model/version.rb +14 -0
- data/spec/hash_model/hash_model_spec.rb +774 -0
- data/spec/spec_helper.rb +23 -0
- metadata +160 -0
data/.document
ADDED
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
|
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,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
|