minimodel 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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: []