minimodel 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/README.txt +27 -0
- data/Rakefile +5 -0
- data/lib/minimodel/associations.rb +65 -0
- data/lib/minimodel.rb +92 -0
- data/minimodel.gemspec +14 -0
- data/spec/colour_spec.rb +16 -0
- data/spec/currency_data.yml +8 -0
- data/spec/currency_spec.rb +89 -0
- data/spec/dutch_education_spec.rb +137 -0
- metadata +76 -0
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,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
|
data/spec/colour_spec.rb
ADDED
@@ -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,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: []
|