active_hash 0.6.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,5 @@
1
+ README.rdoc
2
+ lib/**/*.rb
3
+ bin/*
4
+ features/**/*.feature
5
+ LICENSE
@@ -0,0 +1,6 @@
1
+ *.sw?
2
+ .DS_Store
3
+ coverage
4
+ rdoc
5
+ pkg
6
+ .idea
@@ -0,0 +1,13 @@
1
+ 2009-08-19
2
+ - Added custom finders for multiple fields, such as .find_all_by_name_and_age
3
+
4
+ 2009-07-23
5
+ - Added support for auto-defining methods based on hash keys in ActiveHash::Base
6
+ - Changed the :field and :fields API so that they don't overwrite existing methods (useful when ActiveHash auto-defines methods)
7
+ - Fixed a bug where ActiveFile incorrectly set the root_path to be the path in the gem directory, not the current working directory
8
+
9
+ 2009-07-24
10
+ - ActiveFile no longer reloads files by default
11
+ - Added ActiveFile.reload_active_file= so you can cause ActiveFile to reload
12
+ - Setting data to nil correctly causes .all to return an empty array
13
+ - Added reload(force) method, so that you can force a reload from files in ActiveFile, useful for tests
data/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2009 Jeff Dean
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.
@@ -0,0 +1,202 @@
1
+ # ActiveHash
2
+
3
+ ActiveHash is a simple base class that allows you to use a ruby hash as a readonly datasource for an ActiveRecord-like model.
4
+
5
+ ActiveHash assumes that every hash has an :id key, which is what you would probably store in a database. This allows you to seemlessly upgrade from ActiveHash objects to full ActiveRecord objects without having to change any code in your app, or any foreign keys in your database.
6
+
7
+ It also allows you to use #belongs_to in your AR objects.
8
+
9
+ ActiveHash can also be useful to create simple test classes that run without a database - ideal for testing plugins or gems that rely on simple AR behavior, but don't want to deal with databases or migrations for the spec suite.
10
+
11
+ ActiveHash also ships with:
12
+
13
+ * ActiveFile: a base class that will reload data from a flat file every time the flat file is changed
14
+ * ActiveYaml: a base class that will turn YAML into a hash and load the data into an ActiveHash object
15
+
16
+ ## Installation
17
+
18
+ sudo gem install zilkey-active_hash
19
+
20
+ ## Usage
21
+
22
+ To use ActiveHash, you need to:
23
+
24
+ * Inherit from ActiveHash::Base
25
+ * Define your data
26
+ * (optionally) Define your fields and/or default values
27
+
28
+ A quick example would be:
29
+
30
+ class Country < ActiveHash::Base
31
+ self.data = [
32
+ {:id => 1, :name => "US"},
33
+ {:id => 2, :name => "Canada"}
34
+ ]
35
+ end
36
+
37
+ country = Country.new(:name => "Mexico")
38
+ country.name # => "Mexico"
39
+ country.name? # => true
40
+
41
+ ## Auto-Defined fields
42
+
43
+ ActiveHash will auto-define all fields for you when you load the hash. For example, if you have the following class:
44
+
45
+ class CustomField < ActiveYaml::Base
46
+ self.data = [
47
+ {:custom_field_1 => "foo"},
48
+ {:custom_field_2 => "foo"},
49
+ {:custom_field_3 => "foo"}
50
+ ]
51
+ end
52
+
53
+ Once you call CustomField.all it will define methods for :custom_field_1, :custom_field_2 etc...
54
+
55
+ If you need the fields at load time, as opposed to after .all is called, you can also define them manually, like so:
56
+
57
+ class CustomField < ActiveYaml::Base
58
+ fields :custom_field_1, :custom_field_2, :custom_field_3
59
+ end
60
+
61
+ NOTE: auto-defined fields will _not_ override fields you've defined, either on the class or on the instance.
62
+
63
+ ## Defining Fields with default values
64
+
65
+ If some of your hash values contain nil, and you want to provide a default, you can specify defaults with the :field method:
66
+
67
+ class Country < ActiveHash::Base
68
+ field :is_axis_of_evil, :default => false
69
+ end
70
+
71
+ ## Defining Data
72
+
73
+ You can define data inside your class or outside. For example, you might have a class like this:
74
+
75
+ # app/models/country.rb
76
+ class Country < ActiveHash::Base
77
+ end
78
+
79
+ # config/initializers/data.rb
80
+ Country.data = [
81
+ {:id => 1, :name => "US"},
82
+ {:id => 2, :name => "Canada"}
83
+ ]
84
+
85
+ If you prefer to store your data in YAML, see below.
86
+
87
+ ## Class Methods
88
+
89
+ ActiveHash gives you ActiveRecord-esque methods like:
90
+
91
+ Country.all # => returns all Country objects
92
+ Country.count # => returns the length of the .data array
93
+ Country.first # => returns the first country object
94
+ Country.last # => returns the last country object
95
+ Country.find 1 # => returns the first country object with that id
96
+ Country.find [1,2] # => returns all Country objects with ids in the array
97
+ Country.find :all # => same as .all
98
+ Country.find :all, args # => the second argument is totally ignored, but allows it to play nicely with AR
99
+ Country.find_by_id 1 # => find the first object that matches the id
100
+
101
+ It also gives you a few dynamic finder methods. For example, if you defined :name as a field, you'd get:
102
+
103
+ Country.find_by_name "foo" # => returns the first object matching that name
104
+ Country.find_all_by_name "foo" # => returns an array of the objects with matching names
105
+
106
+ ## Instance Methods
107
+
108
+ ActiveHash objects implement enough of the ActiveRecord api to satisfy most common needs. For example:
109
+
110
+ Country#id # => returns the numeric id or nil
111
+ Country#quoted_id # => returns the numeric id
112
+ Country#to_param # => returns the id as a string
113
+ Country#new_record? # => false
114
+ Country#readonly? # => true
115
+ Country#hash # => the hash of the id (or the hash of nil)
116
+ Country#eql? # => compares type and id, returns false if id is nil
117
+
118
+ ActiveHash also gives you methods related to the fields you defined. For example, if you defined :name as a field, you'd get:
119
+
120
+ Country#name # => returns the passed in name
121
+ Country#name? # => returns true if the name is not blank
122
+
123
+ ## Integration with Rails
124
+
125
+ You can create .belongs_to associations from rails objects, like so:
126
+
127
+ class Country < ActiveHash::Base
128
+ end
129
+
130
+ class Person < ActiveRecord::Base
131
+ belongs_to :country
132
+ end
133
+
134
+ You can also use standard rails view helpers, like #collection_select:
135
+
136
+ <%= collection_select :person, :country_id, Country.all, :id, :name %>
137
+
138
+ ## ActiveYaml
139
+
140
+ If you want to store your data in YAML files, just inherit from ActiveYaml and specify your path information:
141
+
142
+ class Country < ActiveYaml::Base
143
+ end
144
+
145
+ By default, this class will look for a yml file named "countries.yml" in the same directory as the file. You can either change the directory it looks in, the filename it looks for, or both:
146
+
147
+ class Country < ActiveYaml::Base
148
+ set_root_path "/u/data"
149
+ set_filename "sample"
150
+ end
151
+
152
+ The above example will look for the file "/u/data/sample.yml".
153
+
154
+ ActiveYaml, as well as ActiveFile, check the mtime of the file you specified, and reloads the data if the mtime has changed. So you can replace the data in the files even if your app is running in production mode in rails.
155
+
156
+ Since ActiveYaml just creates a hash from the YAML file, you will have all fields specified in YAML auto-defined for you once you call all.
157
+
158
+ ## ActiveFile
159
+
160
+ If you store encrypted data, or you'd like to store your flat files as CSV or XML or any other format, you can easily extend ActiveHash to parse and load your file. Just add a custom ::load_file method, and define the extension you want the file to use:
161
+
162
+ class Country < ActiveFile::Base
163
+ set_root_path "/u/data"
164
+ set_filename "sample"
165
+
166
+ class << self
167
+ def extension
168
+ ".super_secret"
169
+ end
170
+
171
+ def load_file
172
+ MyAwesomeDecoder.load_file(full_path)
173
+ end
174
+ end
175
+ end
176
+
177
+ The two methods you need to implement are load_file, which needs to return an array of hashes, and .extension, which returns the file extension you are using. You have full_path available to you if you wish, or you can provide your own path.
178
+
179
+ Setting the default file location in Rails:
180
+
181
+ # config/initializers/active_file.rb
182
+ ActiveFile.set_root_path "config/activefiles"
183
+
184
+ By default, ActiveFile will not reload data from the file when it changes. You can enable this by setting:
185
+
186
+ ActiveFile.reload_active_file = true
187
+
188
+ In Rails, in development mode, it reloads the entire class, which reloads the file, so you don't need to turn this on in development.
189
+
190
+ NOTE: By default, .full_path refers to the current working directory. In a rails app, this will be RAILS_ROOT.
191
+
192
+ ## Authors
193
+
194
+ Written by Jeff Dean, Mike Dalessio and Ben Woosley
195
+
196
+ ## Development
197
+
198
+ The only thing I think I'd really like to add here is support for typecasting the fields.
199
+
200
+ == Copyright
201
+
202
+ Copyright (c) 2009 Jeff Dean. See LICENSE for details.
@@ -0,0 +1,49 @@
1
+ require 'rubygems'
2
+ require 'rake'
3
+
4
+ begin
5
+ require 'jeweler'
6
+ Jeweler::Tasks.new do |gem|
7
+ gem.name = "active_hash"
8
+ gem.summary = %Q{An ActiveRecord-like model that uses a hash as a datasource}
9
+ gem.email = "jeff@zilkey.com"
10
+ gem.homepage = "http://github.com/zilkey/active_hash"
11
+ gem.authors = ["Jeff Dean", "Mike Dalessio", "Corey Innis", "Peter Jaros"]
12
+ gem.add_dependency('activesupport')
13
+ # gem is a Gem::Specification... see http://www.rubygems.org/read/chapter/20 for additional settings
14
+ end
15
+
16
+ rescue LoadError
17
+ puts "Jeweler (or a dependency) not available. Install it with: sudo gem install jeweler"
18
+ end
19
+
20
+ require 'spec/rake/spectask'
21
+ Spec::Rake::SpecTask.new(:spec) do |spec|
22
+ spec.libs << 'lib' << 'spec'
23
+ spec.spec_files = FileList['spec/**/*_spec.rb']
24
+ end
25
+
26
+ Spec::Rake::SpecTask.new(:rcov) do |spec|
27
+ spec.libs << 'lib' << 'spec'
28
+ spec.pattern = 'spec/**/*_spec.rb'
29
+ spec.rcov = true
30
+ end
31
+
32
+
33
+ task :default => :spec
34
+
35
+ require 'rake/rdoctask'
36
+ Rake::RDocTask.new do |rdoc|
37
+ if File.exist?('VERSION.yml')
38
+ config = YAML.load(File.read('VERSION.yml'))
39
+ version = "#{config[:major]}.#{config[:minor]}.#{config[:patch]}"
40
+ else
41
+ version = ""
42
+ end
43
+
44
+ rdoc.rdoc_dir = 'rdoc'
45
+ rdoc.title = "active_hash #{version}"
46
+ rdoc.rdoc_files.include('README*')
47
+ rdoc.rdoc_files.include('lib/**/*.rb')
48
+ end
49
+
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 0.6.1
@@ -0,0 +1,59 @@
1
+ # -*- encoding: utf-8 -*-
2
+
3
+ Gem::Specification.new do |s|
4
+ s.name = %q{active_hash}
5
+ s.version = "0.6.1"
6
+
7
+ s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
8
+ s.authors = ["Jeff Dean", "Mike Dalessio", "Corey Innis", "Peter Jaros"]
9
+ s.date = %q{2009-08-19}
10
+ s.email = %q{jeff@zilkey.com}
11
+ s.extra_rdoc_files = [
12
+ "LICENSE",
13
+ "README.md"
14
+ ]
15
+ s.files = [
16
+ ".document",
17
+ ".gitignore",
18
+ "CHANGELOG",
19
+ "LICENSE",
20
+ "README.md",
21
+ "Rakefile",
22
+ "VERSION",
23
+ "active_hash.gemspec",
24
+ "lib/active_file/base.rb",
25
+ "lib/active_hash.rb",
26
+ "lib/active_hash/base.rb",
27
+ "lib/active_yaml/base.rb",
28
+ "spec/active_file/base_spec.rb",
29
+ "spec/active_hash/base_spec.rb",
30
+ "spec/active_yaml/base_spec.rb",
31
+ "spec/active_yaml/sample.yml",
32
+ "spec/spec_helper.rb"
33
+ ]
34
+ s.has_rdoc = true
35
+ s.homepage = %q{http://github.com/zilkey/active_hash}
36
+ s.rdoc_options = ["--charset=UTF-8"]
37
+ s.require_paths = ["lib"]
38
+ s.rubygems_version = %q{1.3.1}
39
+ s.summary = %q{An ActiveRecord-like model that uses a hash as a datasource}
40
+ s.test_files = [
41
+ "spec/active_file/base_spec.rb",
42
+ "spec/active_hash/base_spec.rb",
43
+ "spec/active_yaml/base_spec.rb",
44
+ "spec/spec_helper.rb"
45
+ ]
46
+
47
+ if s.respond_to? :specification_version then
48
+ current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
49
+ s.specification_version = 2
50
+
51
+ if Gem::Version.new(Gem::RubyGemsVersion) >= Gem::Version.new('1.2.0') then
52
+ s.add_runtime_dependency(%q<activesupport>, [">= 0"])
53
+ else
54
+ s.add_dependency(%q<activesupport>, [">= 0"])
55
+ end
56
+ else
57
+ s.add_dependency(%q<activesupport>, [">= 0"])
58
+ end
59
+ end
@@ -0,0 +1,60 @@
1
+ module ActiveFile
2
+
3
+ class Base < ActiveHash::Base
4
+ class_inheritable_accessor :filename, :root_path, :cached_mtime, :reload_active_file, :data_has_been_set
5
+
6
+ class << self
7
+ def all
8
+ reload
9
+ super
10
+ end
11
+
12
+ def reload(force = false)
13
+ if force || should_reload?
14
+ self.data = load_file
15
+ end
16
+ end
17
+
18
+ def data=(array_of_hashes)
19
+ write_inheritable_attribute :data_has_been_set, true
20
+ super
21
+ end
22
+
23
+ def set_filename(name)
24
+ write_inheritable_attribute :filename, name
25
+ end
26
+
27
+ def set_root_path(path)
28
+ write_inheritable_attribute :root_path, path
29
+ end
30
+
31
+ def load_file
32
+ raise "Override Me"
33
+ end
34
+
35
+ def full_path
36
+ root_path = read_inheritable_attribute(:root_path) || Dir.pwd
37
+ filename = read_inheritable_attribute(:filename) || name.tableize
38
+ File.join(root_path, "#{filename}.#{extension}")
39
+ end
40
+
41
+ def should_reload?
42
+ return false if read_inheritable_attribute(:data_has_been_set) && ! read_inheritable_attribute(:reload_active_file)
43
+ return false if (mtime = File.mtime(full_path)) == read_inheritable_attribute(:cached_mtime)
44
+
45
+ write_inheritable_attribute :cached_mtime, mtime
46
+ true
47
+ end
48
+
49
+ private :should_reload?
50
+
51
+ def extension
52
+ raise "Override Me"
53
+ end
54
+
55
+ protected :extension
56
+
57
+ end
58
+ end
59
+
60
+ end
@@ -0,0 +1,5 @@
1
+ require 'rubygems'
2
+ require 'activesupport'
3
+ require 'active_hash/base'
4
+ require 'active_file/base'
5
+ require 'active_yaml/base'
@@ -0,0 +1,188 @@
1
+ module ActiveHash
2
+ class Base
3
+ class_inheritable_accessor :data
4
+ class << self
5
+ attr_reader :field_names
6
+
7
+ def data=(array_of_hashes)
8
+ @records = nil
9
+ write_inheritable_attribute(:data, array_of_hashes)
10
+ end
11
+
12
+ def all
13
+ unless @records
14
+ records = read_inheritable_attribute(:data) || []
15
+ @records = records.collect {|hash| new(hash)}
16
+ auto_assign_fields( records )
17
+ end
18
+ @records
19
+ end
20
+
21
+ def count
22
+ all.length
23
+ end
24
+
25
+ def find(id, *args)
26
+ case id
27
+ when :all
28
+ all
29
+ when Array
30
+ all.select {|record| id.map(&:to_i).include?(record.id) }
31
+ else
32
+ find_by_id(id)
33
+ end
34
+ end
35
+
36
+ def find_by_id(id)
37
+ all.detect {|record| record.id == id.to_i}
38
+ end
39
+
40
+ delegate :first, :last, :to => :all
41
+
42
+ def fields(*args)
43
+ options = args.extract_options!
44
+ args.each do |field|
45
+ field(field, options)
46
+ end
47
+ end
48
+
49
+ def field(field_name, options = {})
50
+ @field_names ||= []
51
+ @field_names << field_name
52
+
53
+ define_getter_method(field_name, options[:default])
54
+ define_interrogator_method(field_name)
55
+ define_custom_find_method(field_name)
56
+ define_custom_find_all_method(field_name)
57
+ end
58
+
59
+ def respond_to?(method_name)
60
+ super ||
61
+ begin
62
+ config = configuration_for_custom_finder(method_name)
63
+ config && config[:fields].all? { |field| field_names.include?(field.to_sym) }
64
+ end
65
+ end
66
+
67
+ def method_missing(method_name, *args)
68
+ return super unless respond_to? method_name
69
+
70
+ config = configuration_for_custom_finder(method_name)
71
+ attribute_pairs = config[:fields].zip(args)
72
+ matches = all.select { |base| attribute_pairs.all? { |field, value| base.send(field) == value } }
73
+ config[:all?] ? matches : matches.first
74
+ end
75
+
76
+ def configuration_for_custom_finder(finder_name)
77
+ if finder_name.to_s.match(/^find_(all_)?by_(.*)/)
78
+ {
79
+ :all? => !!$1,
80
+ :fields => $2.split('_and_')
81
+ }
82
+ end
83
+ end
84
+
85
+ def define_getter_method(field, default_value)
86
+ unless instance_methods.include?(field.to_s)
87
+ define_method(field) do
88
+ attributes[field] || default_value
89
+ end
90
+ end
91
+ end
92
+
93
+ private :define_getter_method
94
+
95
+ def define_interrogator_method(field)
96
+ method_name = "#{field}?"
97
+ unless instance_methods.include?(method_name)
98
+ define_method(method_name) do
99
+ attributes[field].present?
100
+ end
101
+ end
102
+ end
103
+
104
+ private :define_interrogator_method
105
+
106
+ def define_custom_find_method(field_name)
107
+ method_name = "find_by_#{field_name}"
108
+ unless singleton_methods.include?(method_name)
109
+ metaclass.instance_eval do
110
+ define_method(method_name) do |name|
111
+ all.detect {|record| record.send(field_name) == name }
112
+ end
113
+ end
114
+ end
115
+ end
116
+
117
+ private :define_custom_find_method
118
+
119
+ def define_custom_find_all_method(field_name)
120
+ method_name = "find_all_by_#{field_name}"
121
+ unless singleton_methods.include?(method_name)
122
+ metaclass.instance_eval do
123
+ unless singleton_methods.include?(method_name)
124
+ define_method(method_name) do |name|
125
+ all.select {|record| record.send(field_name) == name }
126
+ end
127
+ end
128
+ end
129
+ end
130
+ end
131
+
132
+ private :define_custom_find_all_method
133
+
134
+ def auto_assign_fields(array_of_hashes)
135
+ array_of_hashes.inject([]) do |array, row|
136
+ row.symbolize_keys!
137
+ row.keys.each do |key|
138
+ unless key.to_s == "id"
139
+ array << key
140
+ end
141
+ end
142
+ array
143
+ end.uniq.each do |key|
144
+ field key
145
+ end
146
+ end
147
+
148
+ private :auto_assign_fields
149
+
150
+ end
151
+
152
+ attr_reader :attributes
153
+
154
+ def initialize(options = {})
155
+ options.symbolize_keys!
156
+ @attributes = options
157
+ end
158
+
159
+ def id
160
+ attributes[:id] ? attributes[:id].to_i : nil
161
+ end
162
+
163
+ alias quoted_id id
164
+
165
+ def new_record?
166
+ false
167
+ end
168
+
169
+ def readonly?
170
+ true
171
+ end
172
+
173
+ def to_param
174
+ id.to_s
175
+ end
176
+
177
+ def eql?(other)
178
+ other.instance_of?(self.class) and not id.nil? and (id == other.id)
179
+ end
180
+
181
+ alias == eql?
182
+
183
+ def hash
184
+ id.hash
185
+ end
186
+
187
+ end
188
+ end