minimodel 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
data/README.txt ADDED
@@ -0,0 +1,27 @@
1
+ A little library for defining little models.
2
+
3
+ Many apps use small "read only" datasets that can easily be held in memory,
4
+ for example: currency data, country data, colour data, advertising banners,
5
+ options for select boxes, payment plans etc. Sometimes it's useful to model
6
+ this data without using your database, and that's what minimodel is for.
7
+
8
+ Quick example:
9
+
10
+ require 'minimodel'
11
+
12
+ class Currency < MiniModel
13
+ indexed_by :code
14
+
15
+ insert code: 'EUR', name: 'Euro'
16
+ insert code: 'GBP', name: 'Pound sterling'
17
+ insert code: 'USD', name: 'United States dollar'
18
+ end
19
+
20
+
21
+ The Currency class will respond to #count (returning the total number of
22
+ currencies), #all (returning an array of currency objects), and #find
23
+ (to lookup a specific currency by its code). Just like ActiveRecord.
24
+
25
+ Take a look at spec/*_spec.rb for more examples.
26
+
27
+ Have fun.
data/Rakefile ADDED
@@ -0,0 +1,5 @@
1
+ require 'rake/testtask'
2
+
3
+ Rake::TestTask.new do |t|
4
+ t.test_files = FileList['spec/*_spec.rb']
5
+ end
@@ -0,0 +1,65 @@
1
+ require 'active_support/inflector'
2
+
3
+ class MiniModel
4
+ class HasManyAssociation
5
+ def initialize(owner, association_name, options = {})
6
+ @owner, @association_name, @options = owner, association_name, options
7
+ end
8
+
9
+ def each(&block)
10
+ collection.each(&block)
11
+ end
12
+
13
+ def size
14
+ arel_model? ? collection.size : collection.length
15
+ end
16
+
17
+ include Enumerable
18
+
19
+ private
20
+
21
+ def collection
22
+ @collection ||= begin
23
+ owner_id = @owner.send(@owner.class.primary_key)
24
+
25
+ if arel_model?
26
+ target_model.where(foreign_key => owner_id)
27
+ else
28
+ target_model.all.select { |object| object.send(foreign_key) == owner_id }
29
+ end
30
+ end
31
+ end
32
+
33
+ def arel_model?
34
+ target_model.respond_to?(:where)
35
+ end
36
+
37
+ def foreign_key
38
+ @foreign_key ||= '%s_%s' % [@owner.class.name.demodulize.underscore, @owner.class.primary_key]
39
+ end
40
+
41
+ def target_model
42
+ @target_model ||= @association_name.to_s.classify.constantize
43
+ end
44
+ end
45
+
46
+ module AssociationClassMethods
47
+ def belongs_to(association_name, options = {})
48
+ define_method(association_name) do
49
+ target_model = association_name.to_s.classify.constantize
50
+
51
+ foreign_key = '%s_%s' % [association_name, target_model.primary_key]
52
+
53
+ target_model.find(self.send(foreign_key))
54
+ end
55
+ end
56
+
57
+ def has_many(association_name, options = {})
58
+ define_method(association_name) do
59
+ HasManyAssociation.new(self, association_name, options)
60
+ end
61
+ end
62
+ end
63
+
64
+ extend AssociationClassMethods
65
+ end
data/lib/minimodel.rb ADDED
@@ -0,0 +1,92 @@
1
+ require 'yaml'
2
+
3
+ class MiniModel
4
+ def initialize(attributes)
5
+ @attributes = attributes.to_hash
6
+ end
7
+
8
+ def hash
9
+ @attributes.hash
10
+ end
11
+
12
+ def eql?(object)
13
+ object.class == self.class && object.hash == self.hash
14
+ end
15
+
16
+ def to_hash
17
+ @attributes
18
+ end
19
+
20
+ def method_missing(symbol, *args, &block)
21
+ if @attributes.has_key?(symbol) && args.empty? && block.nil?
22
+ return @attributes[symbol]
23
+ else
24
+ super symbol, *args, &block
25
+ end
26
+ end
27
+
28
+ class DuplicateKeyError < StandardError
29
+ end
30
+
31
+ module ClassMethods
32
+ def all
33
+ index.values
34
+ end
35
+
36
+ def count
37
+ index.length
38
+ end
39
+
40
+ def primary_key
41
+ @primary_key
42
+ end
43
+
44
+ def indexed_by(primary_key, options = {})
45
+ @primary_key = primary_key
46
+
47
+ @auto_increment = options[:auto_increment] ? 1 : nil
48
+ end
49
+
50
+ def insert(attributes)
51
+ unless @auto_increment.nil?
52
+ attributes[primary_key] = @auto_increment
53
+
54
+ @auto_increment += 1
55
+ end
56
+
57
+ object = new(attributes)
58
+
59
+ pkey = object.send(primary_key)
60
+
61
+ if index.has_key?(pkey)
62
+ raise DuplicateKeyError
63
+ end
64
+
65
+ index[pkey] = object
66
+ end
67
+
68
+ def find(key)
69
+ index[key]
70
+ end
71
+
72
+ def load_from(path)
73
+ YAML.load_file(path).each do |key, attrs|
74
+ insert symbolize_keys(attrs, primary_key => key)
75
+ end
76
+ end
77
+
78
+ private
79
+
80
+ def symbolize_keys(hash, initial_value=nil)
81
+ hash.inject(initial_value) do |tmp, (k, v)|
82
+ tmp.merge(k.to_sym => v)
83
+ end
84
+ end
85
+
86
+ def index
87
+ @index ||= {}
88
+ end
89
+ end
90
+
91
+ extend ClassMethods
92
+ end
data/minimodel.gemspec ADDED
@@ -0,0 +1,14 @@
1
+ Gem::Specification.new do |s|
2
+ s.name = 'minimodel'
3
+ s.version = '0.1.0'
4
+ s.platform = Gem::Platform::RUBY
5
+ s.authors = ['Tim Craft']
6
+ s.email = ['mail@timcraft.com']
7
+ s.homepage = 'http://github.com/timcraft/minimodel'
8
+ s.description = 'A little library for defining little models'
9
+ s.summary = 'See description'
10
+ s.files = Dir.glob('{lib,spec}/**/*') + %w(README.txt Rakefile minimodel.gemspec)
11
+ s.add_development_dependency('activesupport', ['>= 3.0.3'])
12
+ s.add_development_dependency('activerecord', ['>= 3.0.3'])
13
+ s.require_path = 'lib'
14
+ end
@@ -0,0 +1,16 @@
1
+ require 'minitest/autorun'
2
+ require 'minimodel'
3
+
4
+ class Colour < MiniModel
5
+ indexed_by :id, auto_increment: true
6
+
7
+ insert name: 'Blue', hexdigits: '0000FF'
8
+ insert name: 'Red', hexdigits: 'FF0000'
9
+ insert name: 'Green', hexdigits: '00FF00'
10
+ end
11
+
12
+ describe Colour do
13
+ it 'should assign auto incrementing id values' do
14
+ Colour.all.map(&:id).must_equal [1, 2, 3]
15
+ end
16
+ end
@@ -0,0 +1,8 @@
1
+ EUR:
2
+ name: Euro
3
+
4
+ GBP:
5
+ name: Pound sterling
6
+
7
+ USD:
8
+ name: United States dollar
@@ -0,0 +1,89 @@
1
+ require 'minitest/autorun'
2
+ require 'minimodel'
3
+
4
+ class Currency < MiniModel
5
+ indexed_by :code
6
+
7
+ load_from 'spec/currency_data.yml'
8
+
9
+ insert code: 'INR', name: 'Indian rupee'
10
+ insert code: 'JPY', name: 'Japanese yen'
11
+ end
12
+
13
+ describe 'A currency object' do
14
+ before do
15
+ @euro = Currency.new(code: 'EUR', name: 'Euro')
16
+ end
17
+
18
+ it 'should have attribute reader methods' do
19
+ @euro.code.must_equal 'EUR'
20
+ @euro.name.must_equal 'Euro'
21
+ end
22
+
23
+ describe '#eql?' do
24
+ describe 'when passed the same currency object' do
25
+ it 'should return true' do
26
+ @euro.eql?(@euro).must_equal true
27
+ @euro.eql?(Currency.new(code: 'EUR', name: 'Euro')).must_equal true
28
+ end
29
+ end
30
+
31
+ describe 'when passed a different currency object' do
32
+ it 'should return false' do
33
+ @euro.eql?(Currency.new(code: 'GBP', name: 'Pound sterling')).must_equal false
34
+ end
35
+ end
36
+ end
37
+
38
+ describe '#to_hash' do
39
+ it 'should return a hash containing the object attributes' do
40
+ @euro.to_hash.must_be_kind_of Hash
41
+ @euro.to_hash.must_equal code: 'EUR', name: 'Euro'
42
+ end
43
+ end
44
+ end
45
+
46
+ describe Currency do
47
+ describe '#all' do
48
+ it 'should return an array containing currency objects' do
49
+ Currency.all.must_be_kind_of Array
50
+ Currency.all.all? { |object| object.must_be_kind_of Currency }
51
+ end
52
+ end
53
+
54
+ describe '#primary_key' do
55
+ it 'should return the primary key specified using #indexed_by' do
56
+ Currency.primary_key.must_equal :code
57
+ end
58
+ end
59
+
60
+ describe '#count' do
61
+ it 'should return the total number of currencies defined' do
62
+ Currency.count.must_equal 5
63
+ end
64
+ end
65
+
66
+ describe '#find' do
67
+ describe 'when passed a valid currency code' do
68
+ it 'should return the correct currency' do
69
+ currency = Currency.find('EUR')
70
+
71
+ currency.must_be_kind_of Currency
72
+ currency.code.must_equal 'EUR'
73
+ currency.name.must_equal 'Euro'
74
+ end
75
+ end
76
+
77
+ describe 'when passed an invalid currency code' do
78
+ it 'should return nil' do
79
+ Currency.find('FOO').must_be_nil
80
+ end
81
+ end
82
+ end
83
+
84
+ describe '#insert' do
85
+ it 'should raise an error when passed a key that already exists' do
86
+ proc { Currency.insert(code: 'EUR', name: 'Euro') }.must_raise MiniModel::DuplicateKeyError
87
+ end
88
+ end
89
+ end
@@ -0,0 +1,137 @@
1
+ require 'minitest/autorun'
2
+ require 'minimodel'
3
+ require 'minimodel/associations'
4
+ require 'active_record'
5
+
6
+ ActiveRecord::Base.establish_connection(adapter: 'sqlite3', database: ':memory:')
7
+
8
+ ActiveRecord::Schema.define(:version => 1) do
9
+ create_table :courses, :force => true do |t|
10
+ t.string :name, :index => true
11
+ t.string :level_name, :index => true
12
+ end
13
+ end
14
+
15
+ class Level < MiniModel
16
+ indexed_by :name
17
+
18
+ has_many :courses
19
+ has_many :profiles
20
+
21
+ insert name: 'VMBO-T'
22
+ insert name: 'HAVO'
23
+ insert name: 'VWO'
24
+ end
25
+
26
+ class Profile < MiniModel
27
+ indexed_by :id, auto_increment: true
28
+
29
+ belongs_to :level
30
+
31
+ insert level_name: 'VMBO-T', name: 'Techniek'
32
+ insert level_name: 'VMBO-T', name: 'Zorg en Welzijn'
33
+ insert level_name: 'VMBO-T', name: 'Economie'
34
+ insert level_name: 'VMBO-T', name: 'Landbouw'
35
+
36
+ insert level_name: 'HAVO', name: 'Economie en Maatschappij'
37
+ insert level_name: 'HAVO', name: 'Cultuur en Maatschappij'
38
+ insert level_name: 'HAVO', name: 'Natuur en Gezondheid'
39
+ insert level_name: 'HAVO', name: 'Natuur en Techniek'
40
+
41
+ insert level_name: 'VWO', name: 'Economie en Maatschappij'
42
+ insert level_name: 'VWO', name: 'Cultuur en Maatschappij'
43
+ insert level_name: 'VWO', name: 'Natuur en Gezondheid'
44
+ insert level_name: 'VWO', name: 'Natuur en Techniek'
45
+ end
46
+
47
+ class Course < ActiveRecord::Base
48
+ def level
49
+ @level ||= Level.find(level_name)
50
+ end
51
+ end
52
+
53
+ Level.all.each do |level|
54
+ for course_name in %w( Nederlands Engels Duits )
55
+ Course.create!(level_name: level.name, name: "#{level.name}/#{course_name}")
56
+ end
57
+ end
58
+
59
+ describe 'A level object' do
60
+ before do
61
+ @level = Level.find('VMBO-T')
62
+
63
+ @profile_names = ['Economie', 'Landbouw', 'Techniek', 'Zorg en Welzijn']
64
+
65
+ @course_names = %w( VMBO-T/Duits VMBO-T/Engels VMBO-T/Nederlands )
66
+ end
67
+
68
+ describe '#profiles' do
69
+ it 'should be enumerable' do
70
+ @level.profiles.must_respond_to :each
71
+ @level.profiles.class.ancestors.must_include Enumerable
72
+ end
73
+
74
+ describe '#size' do
75
+ it 'should return the number of profiles linked to this level' do
76
+ @level.profiles.size.must_equal 4
77
+ end
78
+ end
79
+
80
+ describe '#to_a' do
81
+ it 'should return an array containing the correct profile objects' do
82
+ profiles = @level.profiles.to_a
83
+
84
+ profiles.must_be_kind_of Array
85
+ profiles.map(&:name).sort.must_equal @profile_names
86
+ end
87
+ end
88
+ end
89
+
90
+ describe '#courses' do
91
+ it 'should be enumerable' do
92
+ @level.courses.must_respond_to :each
93
+ @level.courses.class.ancestors.must_include Enumerable
94
+ end
95
+
96
+ describe '#size' do
97
+ it 'should return the number of courses linked to this level' do
98
+ @level.courses.size.must_equal 3
99
+ end
100
+ end
101
+
102
+ describe '#to_a' do
103
+ it 'should return an array containing the correct course objects' do
104
+ courses = @level.courses.to_a
105
+
106
+ courses.must_be_kind_of Array
107
+ courses.map(&:name).sort.must_equal @course_names
108
+ end
109
+ end
110
+ end
111
+ end
112
+
113
+ describe 'A profile object' do
114
+ before do
115
+ @profile = Profile.find(8)
116
+ end
117
+
118
+ describe '#level' do
119
+ it 'should return the correct level object' do
120
+ @profile.level.must_be_kind_of Level
121
+ @profile.level.name.must_equal 'HAVO'
122
+ end
123
+ end
124
+ end
125
+
126
+ describe 'A course object' do
127
+ before do
128
+ @course = Course.find_by_name('VWO/Engels')
129
+ end
130
+
131
+ describe '#level' do
132
+ it 'should return the correct level object' do
133
+ @course.level.must_be_kind_of Level
134
+ @course.level.name.must_equal 'VWO'
135
+ end
136
+ end
137
+ end
metadata ADDED
@@ -0,0 +1,76 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: minimodel
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Tim Craft
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2011-09-21 00:00:00.000000000Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: activesupport
16
+ requirement: &3990170 !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ! '>='
20
+ - !ruby/object:Gem::Version
21
+ version: 3.0.3
22
+ type: :development
23
+ prerelease: false
24
+ version_requirements: *3990170
25
+ - !ruby/object:Gem::Dependency
26
+ name: activerecord
27
+ requirement: &3989920 !ruby/object:Gem::Requirement
28
+ none: false
29
+ requirements:
30
+ - - ! '>='
31
+ - !ruby/object:Gem::Version
32
+ version: 3.0.3
33
+ type: :development
34
+ prerelease: false
35
+ version_requirements: *3989920
36
+ description: A little library for defining little models
37
+ email:
38
+ - mail@timcraft.com
39
+ executables: []
40
+ extensions: []
41
+ extra_rdoc_files: []
42
+ files:
43
+ - lib/minimodel/associations.rb
44
+ - lib/minimodel.rb
45
+ - spec/colour_spec.rb
46
+ - spec/currency_data.yml
47
+ - spec/currency_spec.rb
48
+ - spec/dutch_education_spec.rb
49
+ - README.txt
50
+ - Rakefile
51
+ - minimodel.gemspec
52
+ homepage: http://github.com/timcraft/minimodel
53
+ licenses: []
54
+ post_install_message:
55
+ rdoc_options: []
56
+ require_paths:
57
+ - lib
58
+ required_ruby_version: !ruby/object:Gem::Requirement
59
+ none: false
60
+ requirements:
61
+ - - ! '>='
62
+ - !ruby/object:Gem::Version
63
+ version: '0'
64
+ required_rubygems_version: !ruby/object:Gem::Requirement
65
+ none: false
66
+ requirements:
67
+ - - ! '>='
68
+ - !ruby/object:Gem::Version
69
+ version: '0'
70
+ requirements: []
71
+ rubyforge_project:
72
+ rubygems_version: 1.8.10
73
+ signing_key:
74
+ specification_version: 3
75
+ summary: See description
76
+ test_files: []