rmodel 0.4.0.dev → 1.0.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.
- 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
|