rmodel 0.4.0.dev → 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.gitignore +1 -0
- data/.rubocop.yml +4 -0
- data/.travis.yml +4 -3
- data/README.md +119 -153
- data/Rakefile +1 -2
- data/examples/mongo_embedded.rb +27 -21
- data/examples/scopes.rb +28 -0
- data/examples/sql_repository.rb +14 -26
- data/examples/timestamps.rb +7 -10
- data/examples/webapp/models/task.rb +9 -0
- data/examples/webapp/repositories/task_repository.rb +14 -0
- data/examples/webapp/server.rb +25 -0
- data/examples/webapp/support/mappers.rb +11 -0
- data/examples/webapp/support/repositories.rb +12 -0
- data/examples/webapp/support/sources.rb +13 -0
- data/examples/webapp/views/index.erb +20 -0
- data/lib/rmodel.rb +11 -8
- data/lib/rmodel/array_mapper.rb +23 -0
- data/lib/rmodel/base_mapper.rb +56 -0
- data/lib/rmodel/dummy_mapper.rb +15 -0
- data/lib/rmodel/mongo/mapper.rb +11 -0
- data/lib/rmodel/mongo/source.rb +50 -0
- data/lib/rmodel/repository.rb +36 -0
- data/lib/rmodel/repository_ext/scopable.rb +44 -0
- data/lib/rmodel/repository_ext/sugarable.rb +35 -0
- data/lib/rmodel/repository_ext/timestampable.rb +29 -0
- data/lib/rmodel/scope.rb +31 -0
- data/lib/rmodel/sequel/mapper.rb +6 -0
- data/lib/rmodel/sequel/source.rb +43 -0
- data/lib/rmodel/uni_hash.rb +16 -0
- data/lib/rmodel/version.rb +1 -1
- data/rmodel.gemspec +9 -3
- data/spec/rmodel/array_mapper_spec.rb +43 -0
- data/spec/rmodel/mongo/mapper_spec.rb +154 -0
- data/spec/rmodel/mongo/repository_spec.rb +16 -37
- data/spec/rmodel/mongo/source_spec.rb +113 -0
- data/spec/rmodel/sequel/mapper_spec.rb +57 -0
- data/spec/rmodel/sequel/repository_spec.rb +27 -38
- data/spec/rmodel/sequel/source_spec.rb +121 -0
- data/spec/shared/base_mapper.rb +39 -0
- data/spec/shared/clean_moped.rb +6 -2
- data/spec/shared/clean_sequel.rb +1 -1
- data/spec/shared/{repository_ext → repository}/crud.rb +20 -14
- data/spec/shared/repository/initialization.rb +39 -0
- data/spec/shared/repository/scopable.rb +137 -0
- data/spec/shared/repository/sugarable.rb +67 -0
- data/spec/shared/repository/timestampable.rb +137 -0
- data/spec/spec_helper.rb +17 -18
- metadata +120 -54
- data/examples/advanced_creation_of_repository.rb +0 -15
- data/examples/user.rb +0 -48
- data/lib/rmodel/base/repository.rb +0 -12
- data/lib/rmodel/base/repository_ext/sugarable.rb +0 -17
- data/lib/rmodel/base/repository_ext/timestampable.rb +0 -19
- data/lib/rmodel/mongo/repository.rb +0 -62
- data/lib/rmodel/mongo/repository_ext/query.rb +0 -34
- data/lib/rmodel/mongo/repository_ext/queryable.rb +0 -34
- data/lib/rmodel/mongo/setup.rb +0 -17
- data/lib/rmodel/mongo/simple_factory.rb +0 -78
- data/lib/rmodel/sequel/repository.rb +0 -61
- data/lib/rmodel/sequel/repository_ext/query.rb +0 -34
- data/lib/rmodel/sequel/repository_ext/queryable.rb +0 -30
- data/lib/rmodel/sequel/setup.rb +0 -12
- data/lib/rmodel/sequel/simple_factory.rb +0 -28
- data/lib/rmodel/setup.rb +0 -34
- data/spec/rmodel/mongo/repository_ext/queryable_spec.rb +0 -103
- data/spec/rmodel/mongo/repository_initialize_spec.rb +0 -174
- data/spec/rmodel/mongo/simple_factory_spec.rb +0 -195
- data/spec/rmodel/sequel/repository_ext/queryable_spec.rb +0 -114
- data/spec/rmodel/sequel/repository_initialize_spec.rb +0 -118
- data/spec/rmodel/sequel/simple_factory_spec.rb +0 -55
- data/spec/rmodel/setup_spec.rb +0 -54
- data/spec/shared/repository_ext/sugarable.rb +0 -44
- data/spec/shared/repository_ext/timestampable.rb +0 -67
@@ -0,0 +1,36 @@
|
|
1
|
+
require 'rmodel/repository_ext/sugarable'
|
2
|
+
require 'rmodel/repository_ext/timestampable'
|
3
|
+
require 'rmodel/repository_ext/scopable'
|
4
|
+
|
5
|
+
module Rmodel
|
6
|
+
class Repository
|
7
|
+
include RepositoryExt::Sugarable
|
8
|
+
include RepositoryExt::Scopable
|
9
|
+
prepend RepositoryExt::Timestampable
|
10
|
+
|
11
|
+
def initialize(source, mapper)
|
12
|
+
@source = source or raise ArgumentError, 'Source is not set up'
|
13
|
+
@mapper = mapper or raise ArgumentError, 'Mapper is not set up'
|
14
|
+
end
|
15
|
+
|
16
|
+
def find(id)
|
17
|
+
record = @source.find(id)
|
18
|
+
@mapper.deserialize(record)
|
19
|
+
end
|
20
|
+
|
21
|
+
def insert_one(object)
|
22
|
+
record = @mapper.serialize(object, true)
|
23
|
+
id = @source.insert(record)
|
24
|
+
object.id ||= id
|
25
|
+
end
|
26
|
+
|
27
|
+
def update(object)
|
28
|
+
record = @mapper.serialize(object, false)
|
29
|
+
@source.update(object.id, record)
|
30
|
+
end
|
31
|
+
|
32
|
+
def destroy(object)
|
33
|
+
@source.delete(object.id)
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
module Rmodel
|
2
|
+
module RepositoryExt
|
3
|
+
module Scopable
|
4
|
+
def self.included(base)
|
5
|
+
base.extend ClassMethods
|
6
|
+
end
|
7
|
+
|
8
|
+
def fetch
|
9
|
+
self.class.scope_class.new(self, @source.build_query)
|
10
|
+
end
|
11
|
+
|
12
|
+
def find_all(scope = nil)
|
13
|
+
raw_query = (scope || fetch).raw_query
|
14
|
+
|
15
|
+
@source.exec_query(raw_query).map do |hash|
|
16
|
+
@mapper.deserialize(hash)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def delete_all(scope = nil)
|
21
|
+
raw_query = (scope || fetch).raw_query
|
22
|
+
@source.delete_by_query(raw_query)
|
23
|
+
end
|
24
|
+
|
25
|
+
def destroy_all(scope = nil)
|
26
|
+
find_all(scope).each { |object| destroy(object) }
|
27
|
+
end
|
28
|
+
|
29
|
+
def all
|
30
|
+
fetch.to_a
|
31
|
+
end
|
32
|
+
|
33
|
+
module ClassMethods
|
34
|
+
def scope_class
|
35
|
+
@scope_class ||= Class.new(Rmodel::Scope)
|
36
|
+
end
|
37
|
+
|
38
|
+
def scope(name, &block)
|
39
|
+
scope_class.define_scope(name, &block)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
module Rmodel
|
2
|
+
module RepositoryExt
|
3
|
+
module Sugarable
|
4
|
+
def find!(id)
|
5
|
+
find(id) or raise(Rmodel::NotFound.new(self, id: id))
|
6
|
+
end
|
7
|
+
|
8
|
+
def insert(*args)
|
9
|
+
if args.length == 1
|
10
|
+
if args.first.is_a?(Array)
|
11
|
+
insert_array(args.first)
|
12
|
+
else
|
13
|
+
insert_one(args.first)
|
14
|
+
end
|
15
|
+
else
|
16
|
+
insert_array(args)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def save(object)
|
21
|
+
if object.id.nil?
|
22
|
+
insert_one(object)
|
23
|
+
else
|
24
|
+
update(object)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
private
|
29
|
+
|
30
|
+
def insert_array(array)
|
31
|
+
array.each { |object| insert_one(object) }
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
module Rmodel
|
2
|
+
module RepositoryExt
|
3
|
+
module Timestampable
|
4
|
+
def insert_one(object)
|
5
|
+
object.created_at = now if able_to_set_created_at?(object)
|
6
|
+
super
|
7
|
+
end
|
8
|
+
|
9
|
+
def update(object)
|
10
|
+
object.updated_at = now if able_to_set_updated_at?(object)
|
11
|
+
super
|
12
|
+
end
|
13
|
+
|
14
|
+
private
|
15
|
+
|
16
|
+
def able_to_set_created_at?(object)
|
17
|
+
object.respond_to?(:created_at=) && object.created_at.nil?
|
18
|
+
end
|
19
|
+
|
20
|
+
def able_to_set_updated_at?(object)
|
21
|
+
object.respond_to?(:updated_at=)
|
22
|
+
end
|
23
|
+
|
24
|
+
def now
|
25
|
+
Time.try(:current) || Time.now
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
data/lib/rmodel/scope.rb
ADDED
@@ -0,0 +1,31 @@
|
|
1
|
+
module Rmodel
|
2
|
+
class Scope
|
3
|
+
include Enumerable
|
4
|
+
|
5
|
+
attr_reader :raw_query
|
6
|
+
|
7
|
+
def initialize(repo, raw_query)
|
8
|
+
@repo = repo
|
9
|
+
@raw_query = raw_query
|
10
|
+
end
|
11
|
+
|
12
|
+
def each(&block)
|
13
|
+
@repo.find_all(self).each(&block)
|
14
|
+
end
|
15
|
+
|
16
|
+
def delete_all
|
17
|
+
@repo.delete_all(self)
|
18
|
+
end
|
19
|
+
|
20
|
+
def destroy_all
|
21
|
+
@repo.destroy_all(self)
|
22
|
+
end
|
23
|
+
|
24
|
+
def self.define_scope(name, &block)
|
25
|
+
define_method name do |*args|
|
26
|
+
new_raw_query = @raw_query.instance_exec(*args, &block)
|
27
|
+
self.class.new(@repo, new_raw_query)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
require 'sequel'
|
2
|
+
|
3
|
+
module Rmodel
|
4
|
+
module Sequel
|
5
|
+
class Source
|
6
|
+
def initialize(connection, table)
|
7
|
+
@connection = connection
|
8
|
+
raise ArgumentError, 'Connection is not setup' unless @connection
|
9
|
+
|
10
|
+
@table = table
|
11
|
+
raise ArgumentError, 'Table can not be guessed' unless @table
|
12
|
+
end
|
13
|
+
|
14
|
+
def find(id)
|
15
|
+
@connection[@table].where(id: id).first
|
16
|
+
end
|
17
|
+
|
18
|
+
def insert(tuple)
|
19
|
+
@connection[@table].insert(tuple)
|
20
|
+
end
|
21
|
+
|
22
|
+
def update(id, tuple)
|
23
|
+
@connection[@table].where(id: id).update(tuple)
|
24
|
+
end
|
25
|
+
|
26
|
+
def delete(id)
|
27
|
+
@connection[@table].where(id: id).delete
|
28
|
+
end
|
29
|
+
|
30
|
+
def build_query
|
31
|
+
@connection[@table]
|
32
|
+
end
|
33
|
+
|
34
|
+
def exec_query(query)
|
35
|
+
query
|
36
|
+
end
|
37
|
+
|
38
|
+
def delete_by_query(query)
|
39
|
+
exec_query(query).delete
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
module Rmodel
|
2
|
+
class UniHash < SimpleDelegator
|
3
|
+
def initialize(hash, key_op)
|
4
|
+
super(hash)
|
5
|
+
@key_op = key_op
|
6
|
+
end
|
7
|
+
|
8
|
+
def [](key)
|
9
|
+
super(key.public_send(@key_op))
|
10
|
+
end
|
11
|
+
|
12
|
+
def []=(key, value)
|
13
|
+
super(key.public_send(@key_op), value)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
data/lib/rmodel/version.rb
CHANGED
data/rmodel.gemspec
CHANGED
@@ -8,8 +8,10 @@ Gem::Specification.new do |spec|
|
|
8
8
|
spec.version = Rmodel::VERSION
|
9
9
|
spec.authors = ['Alexei']
|
10
10
|
spec.email = ['alexei.lexx@gmail.com']
|
11
|
-
spec.summary =
|
12
|
-
|
11
|
+
spec.summary = 'Rmodel is an ORM library, which tends to follow the
|
12
|
+
SOLID principles.'
|
13
|
+
spec.description = 'Rmodel is an ORM library, which tends to follow the
|
14
|
+
SOLID principles.'
|
13
15
|
spec.homepage = 'https://github.com/alexei-lexx/rmodel'
|
14
16
|
spec.license = 'MIT'
|
15
17
|
|
@@ -19,7 +21,6 @@ Gem::Specification.new do |spec|
|
|
19
21
|
spec.require_paths = ['lib']
|
20
22
|
|
21
23
|
spec.add_dependency 'mongo', '~> 2.1'
|
22
|
-
spec.add_dependency 'activesupport'
|
23
24
|
spec.add_dependency 'origin'
|
24
25
|
spec.add_dependency 'sequel'
|
25
26
|
|
@@ -27,6 +28,11 @@ Gem::Specification.new do |spec|
|
|
27
28
|
spec.add_development_dependency 'rake'
|
28
29
|
spec.add_development_dependency 'rspec'
|
29
30
|
spec.add_development_dependency 'sqlite3'
|
31
|
+
spec.add_development_dependency 'rubocop'
|
32
|
+
spec.add_development_dependency 'activesupport'
|
33
|
+
spec.add_development_dependency 'sinatra'
|
34
|
+
spec.add_development_dependency 'activemodel'
|
35
|
+
spec.add_development_dependency 'lazy_injector'
|
30
36
|
|
31
37
|
spec.required_ruby_version = '>= 2.0.0'
|
32
38
|
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
RSpec.describe Rmodel::ArrayMapper do
|
2
|
+
before { stub_const 'Thing', Struct.new(:id, :name) }
|
3
|
+
|
4
|
+
let(:mapper) { Rmodel::Mongo::Mapper.new(Thing).define_attributes(:name) }
|
5
|
+
|
6
|
+
subject { described_class.new(mapper) }
|
7
|
+
|
8
|
+
describe '#deserialize(array)' do
|
9
|
+
it 'returns an array of instances of the appropriate class' do
|
10
|
+
objects = subject.deserialize([{}, {}])
|
11
|
+
|
12
|
+
expect(objects.length).to eq 2
|
13
|
+
objects.each do |object|
|
14
|
+
expect(object).to be_an_instance_of Thing
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
context 'when nil is given' do
|
19
|
+
it 'returns nil' do
|
20
|
+
expect(subject.deserialize(nil)).to be_nil
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
describe '#serialize(objects, id_included)' do
|
26
|
+
it 'returns an instance of Array' do
|
27
|
+
things = [Thing.new(1, 'chair'), Thing.new(2, 'table')]
|
28
|
+
array = subject.serialize(things, true)
|
29
|
+
|
30
|
+
expect(array.length).to eq 2
|
31
|
+
array.each do |entry|
|
32
|
+
expect(entry['_id']).not_to be_nil
|
33
|
+
expect(entry['name']).not_to be_nil
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
context 'when nil is given' do
|
38
|
+
it 'returns nil' do
|
39
|
+
expect(subject.serialize(nil, true)).to be_nil
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,154 @@
|
|
1
|
+
RSpec.describe Rmodel::Mongo::Mapper do
|
2
|
+
before do
|
3
|
+
stub_const 'User', Struct.new(:id, :name, :age, :address, :phones)
|
4
|
+
stub_const 'Address', Struct.new(:id, :city, :street)
|
5
|
+
stub_const 'Phone', Struct.new(:id, :number)
|
6
|
+
end
|
7
|
+
|
8
|
+
let(:address_mapper) do
|
9
|
+
described_class.new(Address).define_attributes(:city, :street)
|
10
|
+
end
|
11
|
+
|
12
|
+
let(:phone_mapper) do
|
13
|
+
described_class.new(Phone).define_attributes(:number)
|
14
|
+
end
|
15
|
+
|
16
|
+
subject do
|
17
|
+
phones_mapper = Rmodel::ArrayMapper.new(phone_mapper)
|
18
|
+
described_class.new(User)
|
19
|
+
.define_attributes(:name, :age)
|
20
|
+
.define_attribute(:address, address_mapper)
|
21
|
+
.define_attribute(:phones, phones_mapper)
|
22
|
+
end
|
23
|
+
|
24
|
+
describe '#deserialize(hash)' do
|
25
|
+
it 'returns an instance of the appropriate class' do
|
26
|
+
expect(subject.deserialize({})).to be_an_instance_of User
|
27
|
+
end
|
28
|
+
|
29
|
+
it 'sets the attributes correctly' do
|
30
|
+
object = subject.deserialize('name' => 'John', 'age' => 20)
|
31
|
+
|
32
|
+
expect(object.name).to eq 'John'
|
33
|
+
expect(object.age).to eq 20
|
34
|
+
end
|
35
|
+
|
36
|
+
it 'leaves not specified attributes out' do
|
37
|
+
object = subject.deserialize('name' => 'John')
|
38
|
+
expect(object.age).to be_nil
|
39
|
+
end
|
40
|
+
|
41
|
+
context 'when _id is given' do
|
42
|
+
it 'sets the #id correctly' do
|
43
|
+
object = subject.deserialize('_id' => 1)
|
44
|
+
expect(object.id).to eq 1
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
context 'when an embedded hash is given' do
|
49
|
+
let(:hash) do
|
50
|
+
{
|
51
|
+
'address' => {
|
52
|
+
'_id' => 10,
|
53
|
+
'city' => 'NY',
|
54
|
+
'street' => '1st Avenue'
|
55
|
+
}
|
56
|
+
}
|
57
|
+
end
|
58
|
+
let(:object) { subject.deserialize(hash) }
|
59
|
+
|
60
|
+
it 'creates the embedded object of the appropriate type' do
|
61
|
+
expect(object.address).to be_an_instance_of Address
|
62
|
+
end
|
63
|
+
|
64
|
+
it 'sets the attributes of the embedded object correctly' do
|
65
|
+
expect(object.address.id).to eq 10
|
66
|
+
expect(object.address.city).to eq 'NY'
|
67
|
+
expect(object.address.street).to eq '1st Avenue'
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
context 'when an embedded array is given' do
|
72
|
+
let(:hash) do
|
73
|
+
{
|
74
|
+
'phones' => [
|
75
|
+
{ '_id' => 100, 'number' => '+1111' },
|
76
|
+
{ '_id' => 101, 'number' => '+2222' }
|
77
|
+
]
|
78
|
+
}
|
79
|
+
end
|
80
|
+
let(:object) { subject.deserialize(hash) }
|
81
|
+
|
82
|
+
it 'creates the embedded array of objects of the appropriate type' do
|
83
|
+
expect(object.phones).to be_an_instance_of Array
|
84
|
+
expect(object.phones.length).to eq 2
|
85
|
+
end
|
86
|
+
|
87
|
+
it 'sets the attributes of the embedded array correctly' do
|
88
|
+
expect(object.phones[0].id).to eq 100
|
89
|
+
expect(object.phones[0].number).to eq '+1111'
|
90
|
+
|
91
|
+
expect(object.phones[1].id).to eq 101
|
92
|
+
expect(object.phones[1].number).to eq '+2222'
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
describe '#serialize(object, id_included)' do
|
98
|
+
it 'returns an instance of Hash' do
|
99
|
+
hash = subject.serialize(User.new(1, 'John', 20), true)
|
100
|
+
expect(hash).to be_an_instance_of Hash
|
101
|
+
end
|
102
|
+
|
103
|
+
it 'sets the keys correctly' do
|
104
|
+
hash = subject.serialize(User.new(1, 'John', 20), true)
|
105
|
+
|
106
|
+
expect(hash['name']).to eq 'John'
|
107
|
+
expect(hash['age']).to eq 20
|
108
|
+
end
|
109
|
+
|
110
|
+
context 'when id_included = true' do
|
111
|
+
it 'sets the _id' do
|
112
|
+
hash = subject.serialize(User.new(1), true)
|
113
|
+
expect(hash['_id']).to eq 1
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
|
+
context 'when id_included = false' do
|
118
|
+
it 'doesnt set the _id' do
|
119
|
+
hash = subject.serialize(User.new(1), false)
|
120
|
+
expect(hash.key?('_id')).to be false
|
121
|
+
end
|
122
|
+
end
|
123
|
+
|
124
|
+
context 'when an embedded object is given' do
|
125
|
+
let(:object) do
|
126
|
+
user = User.new(1, 'John', 20)
|
127
|
+
user.address = Address.new(10, 'NY', '1st Avenue')
|
128
|
+
user
|
129
|
+
end
|
130
|
+
let(:hash) { subject.serialize(object, true) }
|
131
|
+
|
132
|
+
it 'creates the embedded hash correctly' do
|
133
|
+
expect(hash['address']['_id']).to eq 10
|
134
|
+
expect(hash['address']['city']).to eq 'NY'
|
135
|
+
expect(hash['address']['street']).to eq '1st Avenue'
|
136
|
+
end
|
137
|
+
end
|
138
|
+
|
139
|
+
context 'when an embedded array of objects is given' do
|
140
|
+
let(:object) do
|
141
|
+
user = User.new
|
142
|
+
user.phones = [Phone.new(100, '+1111'), Phone.new(101, '+2222')]
|
143
|
+
user
|
144
|
+
end
|
145
|
+
let(:hash) { subject.serialize(object, true) }
|
146
|
+
|
147
|
+
it 'creates the embedded array correctly' do
|
148
|
+
expect(hash['phones'].length).to eq 2
|
149
|
+
end
|
150
|
+
end
|
151
|
+
end
|
152
|
+
|
153
|
+
it_behaves_like 'base mapper'
|
154
|
+
end
|