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.
Files changed (75) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +1 -0
  3. data/.rubocop.yml +4 -0
  4. data/.travis.yml +4 -3
  5. data/README.md +119 -153
  6. data/Rakefile +1 -2
  7. data/examples/mongo_embedded.rb +27 -21
  8. data/examples/scopes.rb +28 -0
  9. data/examples/sql_repository.rb +14 -26
  10. data/examples/timestamps.rb +7 -10
  11. data/examples/webapp/models/task.rb +9 -0
  12. data/examples/webapp/repositories/task_repository.rb +14 -0
  13. data/examples/webapp/server.rb +25 -0
  14. data/examples/webapp/support/mappers.rb +11 -0
  15. data/examples/webapp/support/repositories.rb +12 -0
  16. data/examples/webapp/support/sources.rb +13 -0
  17. data/examples/webapp/views/index.erb +20 -0
  18. data/lib/rmodel.rb +11 -8
  19. data/lib/rmodel/array_mapper.rb +23 -0
  20. data/lib/rmodel/base_mapper.rb +56 -0
  21. data/lib/rmodel/dummy_mapper.rb +15 -0
  22. data/lib/rmodel/mongo/mapper.rb +11 -0
  23. data/lib/rmodel/mongo/source.rb +50 -0
  24. data/lib/rmodel/repository.rb +36 -0
  25. data/lib/rmodel/repository_ext/scopable.rb +44 -0
  26. data/lib/rmodel/repository_ext/sugarable.rb +35 -0
  27. data/lib/rmodel/repository_ext/timestampable.rb +29 -0
  28. data/lib/rmodel/scope.rb +31 -0
  29. data/lib/rmodel/sequel/mapper.rb +6 -0
  30. data/lib/rmodel/sequel/source.rb +43 -0
  31. data/lib/rmodel/uni_hash.rb +16 -0
  32. data/lib/rmodel/version.rb +1 -1
  33. data/rmodel.gemspec +9 -3
  34. data/spec/rmodel/array_mapper_spec.rb +43 -0
  35. data/spec/rmodel/mongo/mapper_spec.rb +154 -0
  36. data/spec/rmodel/mongo/repository_spec.rb +16 -37
  37. data/spec/rmodel/mongo/source_spec.rb +113 -0
  38. data/spec/rmodel/sequel/mapper_spec.rb +57 -0
  39. data/spec/rmodel/sequel/repository_spec.rb +27 -38
  40. data/spec/rmodel/sequel/source_spec.rb +121 -0
  41. data/spec/shared/base_mapper.rb +39 -0
  42. data/spec/shared/clean_moped.rb +6 -2
  43. data/spec/shared/clean_sequel.rb +1 -1
  44. data/spec/shared/{repository_ext → repository}/crud.rb +20 -14
  45. data/spec/shared/repository/initialization.rb +39 -0
  46. data/spec/shared/repository/scopable.rb +137 -0
  47. data/spec/shared/repository/sugarable.rb +67 -0
  48. data/spec/shared/repository/timestampable.rb +137 -0
  49. data/spec/spec_helper.rb +17 -18
  50. metadata +120 -54
  51. data/examples/advanced_creation_of_repository.rb +0 -15
  52. data/examples/user.rb +0 -48
  53. data/lib/rmodel/base/repository.rb +0 -12
  54. data/lib/rmodel/base/repository_ext/sugarable.rb +0 -17
  55. data/lib/rmodel/base/repository_ext/timestampable.rb +0 -19
  56. data/lib/rmodel/mongo/repository.rb +0 -62
  57. data/lib/rmodel/mongo/repository_ext/query.rb +0 -34
  58. data/lib/rmodel/mongo/repository_ext/queryable.rb +0 -34
  59. data/lib/rmodel/mongo/setup.rb +0 -17
  60. data/lib/rmodel/mongo/simple_factory.rb +0 -78
  61. data/lib/rmodel/sequel/repository.rb +0 -61
  62. data/lib/rmodel/sequel/repository_ext/query.rb +0 -34
  63. data/lib/rmodel/sequel/repository_ext/queryable.rb +0 -30
  64. data/lib/rmodel/sequel/setup.rb +0 -12
  65. data/lib/rmodel/sequel/simple_factory.rb +0 -28
  66. data/lib/rmodel/setup.rb +0 -34
  67. data/spec/rmodel/mongo/repository_ext/queryable_spec.rb +0 -103
  68. data/spec/rmodel/mongo/repository_initialize_spec.rb +0 -174
  69. data/spec/rmodel/mongo/simple_factory_spec.rb +0 -195
  70. data/spec/rmodel/sequel/repository_ext/queryable_spec.rb +0 -114
  71. data/spec/rmodel/sequel/repository_initialize_spec.rb +0 -118
  72. data/spec/rmodel/sequel/simple_factory_spec.rb +0 -55
  73. data/spec/rmodel/setup_spec.rb +0 -54
  74. data/spec/shared/repository_ext/sugarable.rb +0 -44
  75. 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
@@ -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,6 @@
1
+ module Rmodel
2
+ module Sequel
3
+ class Mapper < Rmodel::BaseMapper
4
+ end
5
+ end
6
+ 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
@@ -1,3 +1,3 @@
1
1
  module Rmodel
2
- VERSION = "0.4.0.dev"
2
+ VERSION = '1.0.0'.freeze
3
3
  end
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 = %q{Rmodel is an ORM library, which tends to follow the SOLID principles.}
12
- spec.description = %q{Rmodel is an ORM library, which tends to follow the SOLID principles.}
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