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.
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