rmodel 1.0.0 → 1.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: f1cabda8d90f129c91be844839240e35c667e866
4
- data.tar.gz: 63d948217cbee488f8e93be4ef575ff838d9cabf
3
+ metadata.gz: dc6dc338effa3435151dfc878fb4c211f3e7a389
4
+ data.tar.gz: 6c794461936c774923c7ebdcf08c762a3ddf8876
5
5
  SHA512:
6
- metadata.gz: 1eb0276bbb918397139567f1a3c081fa3de431eff3cbffb406f2aa0e524c4d862082d5d5155bad31ce2188d5cb66ea300272a3657edc4d8f1e3debbc54f03628
7
- data.tar.gz: 85fa52294b5ee0e0cbed4fb23a443f995ef442114e9ec67738aabbf21b8d437506af32834b26de7fa64f9ff217096577ca4dea48972fb8afd9f61a9bbf691dcb
6
+ metadata.gz: 363d01b511dfa3a9db13a896701fc624a04ee9a83a64fe4fd33ae6859fa7178cb6a20255d665e938709843cbcc1efa9c781b4d165724d6665119f1860e89f041
7
+ data.tar.gz: 0aa72c78a0b9f7c110488b6518d1d5a0b706a14ef77a269e5b7f69b0f3061a0d622e43d74e524c47e361355792aa2610a2baac5ea2ac15d12e9834659b2349ed
data/README.md CHANGED
@@ -1,4 +1,5 @@
1
1
  [![Build Status](https://travis-ci.org/alexei-lexx/rmodel.svg)](https://travis-ci.org/alexei-lexx/rmodel)
2
+ [![Code Climate](https://codeclimate.com/github/alexei-lexx/rmodel/badges/gpa.svg)](https://codeclimate.com/github/alexei-lexx/rmodel)
2
3
 
3
4
  # Rmodel
4
5
 
@@ -8,9 +9,9 @@
8
9
  * [Scopes](#scopes)
9
10
  * [Timestamps](#timestamps)
10
11
  * [Sugar methods](#sugar-methods)
11
- * [Advanced creation of repository](#advanced-creation-of-repository)
12
12
  * [SQL repository](#sql-repository)
13
13
  * [Embedded documents in MongoDB](#embedded-documents-in-mongodb)
14
+ * [Entity class as a repository](#entity-class-as-a-repository)
14
15
 
15
16
  Rmodel is an ORM library, which tends to follow the SOLID principles.
16
17
 
@@ -165,15 +166,15 @@ a query builder for mongo.
165
166
  Also it's possible to use scopes to run the multi-row operations.
166
167
 
167
168
  ```ruby
169
+ repo.fetch.have_email.find(1) # return an object with a given id or nil
168
170
  repo.fetch.have_email.delete_all # simply run the operation against the database
169
171
  repo.fetch.have_email.destroy_all # extract users and run repo.destroy for the each one
170
- p repo.fetch.count # 0
172
+ repo.fetch.count # 0
171
173
  ```
172
174
 
173
175
  If you have no scopes then just call
174
176
 
175
177
  ```ruby
176
- repo.all
177
178
  repo.delete_all
178
179
  repo.destroy_all
179
180
  ```
@@ -356,6 +357,67 @@ repo.insert(flat)
356
357
  p repo.find(flat.id)
357
358
  ```
358
359
 
360
+ ### Entity class as a repository
361
+
362
+ Ruby on Rails lovers would like to use the entity's class to fetch, create,
363
+ update and delete objects. For example `User.find(1)`, `User.create(john)`
364
+ instead of addressing to the repository object `user_repository.find(1)`.
365
+
366
+ Rmodel has a special feature to resemble the Active Record style - the
367
+ repository injector. Look at the example below.
368
+
369
+ ```ruby
370
+ require 'rmodel'
371
+
372
+ DB = Mongo::Client.new(['localhost'], database: 'test')
373
+
374
+ class UserRepository < Rmodel::Repository
375
+ scope :start_with do |s|
376
+ where(name: { '$regex' => "^#{s}", '$options' => 'i' })
377
+ end
378
+
379
+ def initialize
380
+ source = Rmodel::Mongo::Source.new(DB, :users)
381
+ mapper = Rmodel::Mongo::Mapper.new(User).define_attributes(:name, :email)
382
+
383
+ super(source, mapper)
384
+ end
385
+
386
+ def fetch_johns
387
+ fetch.start_with('john')
388
+ end
389
+ end
390
+
391
+ User = Struct.new(:id, :name, :email) do
392
+ include UserRepository.injector
393
+ end
394
+
395
+ User.delete_all
396
+
397
+ john = User.new(nil, 'John', 'john@example.com')
398
+ User.insert(john)
399
+
400
+ john.name = 'John Smith'
401
+ User.update(john)
402
+
403
+ User.insert(User.new)
404
+
405
+ p User.fetch.start_with('J').count
406
+ p User.fetch_johns.count
407
+ ```
408
+
409
+ Here is the explanation:
410
+
411
+ * `UserRepository.injector` is a module that delegates method calls to the
412
+ underlying repository. It can be included to the class (like User) or extend the
413
+ object.
414
+ * UserRepository must have a constructor with an empty list of arguments.
415
+ * UserRepository is defined as usually. It can have scopes and custom instance
416
+ methods. All of them become available as methods of User.
417
+ * Remember `user_repo.find(1)`. Now it's available as `User.find(1)`.
418
+ * The same `user.fetch.start_with('a')` becomes `User.fetch.start_with('a')`.
419
+ * And the custom method is available as `User.fetch_johns`.
420
+
359
421
  ## Contributing
360
422
 
361
423
  1. Fork it ( https://github.com/alexei-lexx/rmodel/fork )
@@ -0,0 +1,37 @@
1
+ require 'rmodel'
2
+
3
+ DB = Mongo::Client.new(['localhost'], database: 'test')
4
+
5
+ class UserRepository < Rmodel::Repository
6
+ scope :start_with do |s|
7
+ where(name: { '$regex' => "^#{s}", '$options' => 'i' })
8
+ end
9
+
10
+ def initialize
11
+ source = Rmodel::Mongo::Source.new(DB, :users)
12
+ mapper = Rmodel::Mongo::Mapper.new(User).define_attributes(:name, :email)
13
+
14
+ super(source, mapper)
15
+ end
16
+
17
+ def fetch_johns
18
+ fetch.start_with('john')
19
+ end
20
+ end
21
+
22
+ User = Struct.new(:id, :name, :email) do
23
+ include UserRepository.injector
24
+ end
25
+
26
+ User.delete_all
27
+
28
+ john = User.new(nil, 'John', 'john@example.com')
29
+ User.insert(john)
30
+
31
+ john.name = 'John Smith'
32
+ User.update(john)
33
+
34
+ User.insert(User.new)
35
+
36
+ p User.fetch.start_with('J').count
37
+ p User.fetch_johns.count
data/lib/rmodel.rb CHANGED
@@ -2,13 +2,16 @@ require 'active_support/inflector'
2
2
  require 'rmodel/version'
3
3
  require 'rmodel/errors'
4
4
  require 'rmodel/scope'
5
+ require 'rmodel/injector'
5
6
  require 'rmodel/repository'
6
7
  require 'rmodel/uni_hash'
7
8
  require 'rmodel/base_mapper'
8
9
  require 'rmodel/array_mapper'
9
10
  require 'rmodel/dummy_mapper'
11
+ require 'rmodel/mongo/query'
10
12
  require 'rmodel/mongo/source'
11
13
  require 'rmodel/mongo/mapper'
14
+ require 'rmodel/sequel/query'
12
15
  require 'rmodel/sequel/source'
13
16
  require 'rmodel/sequel/mapper'
14
17
 
@@ -0,0 +1,42 @@
1
+ require 'lazy_object'
2
+
3
+ module Rmodel
4
+ class Injector < Module
5
+ def initialize(repository_class)
6
+ @repository = LazyObject.new { repository_class.new }
7
+ end
8
+
9
+ def included(base)
10
+ add_method_missing(base)
11
+ end
12
+
13
+ def extended(base)
14
+ included(base)
15
+ end
16
+
17
+ private
18
+
19
+ def add_method_missing(base)
20
+ repository = @repository
21
+ injector = self
22
+
23
+ base.define_singleton_method :method_missing do |name, *args|
24
+ if repository.respond_to?(name)
25
+ injector.send(:delegate_to_repository, self, name, *args)
26
+ else
27
+ super(name, *args)
28
+ end
29
+ end
30
+ end
31
+
32
+ def delegate_to_repository(base, name, *delegated_args)
33
+ repository = @repository
34
+
35
+ base.define_singleton_method name do |*args|
36
+ repository.public_send(name, *args)
37
+ end
38
+
39
+ base.public_send(name, *delegated_args)
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,13 @@
1
+ require 'origin'
2
+
3
+ module Rmodel
4
+ module Mongo
5
+ class Query
6
+ include Origin::Queryable
7
+
8
+ def find_by_id(id)
9
+ where('_id' => id)
10
+ end
11
+ end
12
+ end
13
+ end
@@ -1,13 +1,8 @@
1
- require 'origin'
2
1
  require 'mongo'
3
2
 
4
3
  module Rmodel
5
4
  module Mongo
6
5
  class Source
7
- class Query
8
- include Origin::Queryable
9
- end
10
-
11
6
  def initialize(connection, collection)
12
7
  @connection = connection
13
8
  raise ArgumentError, 'Connection is not setup' unless @connection
@@ -1,12 +1,14 @@
1
1
  require 'rmodel/repository_ext/sugarable'
2
2
  require 'rmodel/repository_ext/timestampable'
3
3
  require 'rmodel/repository_ext/scopable'
4
+ require 'rmodel/repository_ext/injectable'
4
5
 
5
6
  module Rmodel
6
7
  class Repository
7
8
  include RepositoryExt::Sugarable
8
9
  include RepositoryExt::Scopable
9
10
  prepend RepositoryExt::Timestampable
11
+ extend RepositoryExt::Injectable
10
12
 
11
13
  def initialize(source, mapper)
12
14
  @source = source or raise ArgumentError, 'Source is not set up'
@@ -0,0 +1,9 @@
1
+ module Rmodel
2
+ module RepositoryExt
3
+ module Injectable
4
+ def injector
5
+ @injector ||= Injector.new(self)
6
+ end
7
+ end
8
+ end
9
+ end
@@ -26,10 +26,6 @@ module Rmodel
26
26
  find_all(scope).each { |object| destroy(object) }
27
27
  end
28
28
 
29
- def all
30
- fetch.to_a
31
- end
32
-
33
29
  module ClassMethods
34
30
  def scope_class
35
31
  @scope_class ||= Class.new(Rmodel::Scope)
@@ -2,7 +2,9 @@ module Rmodel
2
2
  module RepositoryExt
3
3
  module Timestampable
4
4
  def insert_one(object)
5
- object.created_at = now if able_to_set_created_at?(object)
5
+ time = now
6
+ object.created_at = time if able_to_set_created_at?(object)
7
+ object.updated_at = time if able_to_set_updated_at?(object)
6
8
  super
7
9
  end
8
10
 
data/lib/rmodel/scope.rb CHANGED
@@ -21,6 +21,11 @@ module Rmodel
21
21
  @repo.destroy_all(self)
22
22
  end
23
23
 
24
+ def find(id)
25
+ new_raw_query = @raw_query.find_by_id(id)
26
+ self.class.new(@repo, new_raw_query).first
27
+ end
28
+
24
29
  def self.define_scope(name, &block)
25
30
  define_method name do |*args|
26
31
  new_raw_query = @raw_query.instance_exec(*args, &block)
@@ -0,0 +1,11 @@
1
+ require 'origin'
2
+
3
+ module Rmodel
4
+ module Sequel
5
+ class Query < SimpleDelegator
6
+ def find_by_id(id)
7
+ where(id: id)
8
+ end
9
+ end
10
+ end
11
+ end
@@ -28,7 +28,7 @@ module Rmodel
28
28
  end
29
29
 
30
30
  def build_query
31
- @connection[@table]
31
+ Query.new(@connection[@table])
32
32
  end
33
33
 
34
34
  def exec_query(query)
@@ -1,3 +1,3 @@
1
1
  module Rmodel
2
- VERSION = '1.0.0'.freeze
2
+ VERSION = '1.1.0'.freeze
3
3
  end
data/rmodel.gemspec CHANGED
@@ -23,6 +23,7 @@ Gem::Specification.new do |spec|
23
23
  spec.add_dependency 'mongo', '~> 2.1'
24
24
  spec.add_dependency 'origin'
25
25
  spec.add_dependency 'sequel'
26
+ spec.add_dependency 'lazy_object'
26
27
 
27
28
  spec.add_development_dependency 'bundler', '~> 1.6'
28
29
  spec.add_development_dependency 'rake'
@@ -26,4 +26,8 @@ RSpec.describe 'Repository with MongoDB' do
26
26
  it_behaves_like 'scopable repository' do
27
27
  include_context 'definitions'
28
28
  end
29
+
30
+ it_behaves_like 'injectable repository' do
31
+ include_context 'definitions'
32
+ end
29
33
  end
@@ -24,7 +24,7 @@ RSpec.describe 'Repository with Sequel' do
24
24
  end
25
25
 
26
26
  it_behaves_like 'initialization' do
27
- before { create_database(true) }
27
+ before { create_database }
28
28
  include_context 'definitions'
29
29
  end
30
30
 
@@ -50,4 +50,9 @@ RSpec.describe 'Repository with Sequel' do
50
50
  end
51
51
  end
52
52
  end
53
+
54
+ it_behaves_like 'injectable repository' do
55
+ before { create_database }
56
+ include_context 'definitions'
57
+ end
53
58
  end
@@ -85,7 +85,8 @@ RSpec.describe Rmodel::Sequel::Source do
85
85
 
86
86
  describe 'build_query' do
87
87
  it 'returns the instance of Sequel::Dataset' do
88
- expect(subject.build_query).to be_a_kind_of Sequel::Dataset
88
+ expect(subject.build_query).to respond_to :select
89
+ expect(subject.build_query).to respond_to :order
89
90
  end
90
91
  end
91
92
 
@@ -0,0 +1,91 @@
1
+ RSpec.shared_examples 'injectable repository' do
2
+ describe '.injector' do
3
+ it 'is a module' do
4
+ expect(Rmodel::Repository.injector).to be_a Module
5
+ end
6
+
7
+ it 'returns the same instance each time' do
8
+ expected_injector = Rmodel::Repository.injector
9
+ expect(Rmodel::Repository.injector).to equal expected_injector
10
+ end
11
+ end
12
+
13
+ before do
14
+ stub_const 'Thing', Struct.new(:id, :name)
15
+ stub_const 'MyRepository', Class.new(Rmodel::Repository)
16
+
17
+ a_source = source
18
+ mapper = mapper_klass.new(Thing).define_attribute(:name)
19
+
20
+ MyRepository.class_eval do
21
+ define_method :initialize do
22
+ super(a_source, mapper)
23
+ end
24
+
25
+ def custom_method; end
26
+ end
27
+ end
28
+
29
+ let(:repository) do
30
+ MyRepository.injector.instance_variable_get('@repository').__target_object__
31
+ end
32
+
33
+ context "when it's included in a class" do
34
+ before do
35
+ stub_const 'Host', Class.new
36
+
37
+ class Host
38
+ include MyRepository.injector
39
+ end
40
+ end
41
+
42
+ context 'when no method is called' do
43
+ before { allow(MyRepository).to receive(:new) }
44
+
45
+ it "doesn't instantiate the repository" do
46
+ expect(MyRepository).not_to have_received(:new)
47
+ end
48
+ end
49
+
50
+ describe '.find' do
51
+ it 'is delegated to the repository' do
52
+ expect(repository).to receive(:find).with(1)
53
+ Host.find(1)
54
+ end
55
+ end
56
+
57
+ describe '.fetch' do
58
+ it 'is delegated to the repository' do
59
+ expect(repository).to receive(:fetch)
60
+ Host.fetch
61
+ end
62
+ end
63
+
64
+ describe '.custom_method' do
65
+ it 'is delegated to the repository' do
66
+ expect(repository).to receive(:custom_method)
67
+ Host.custom_method
68
+ end
69
+ end
70
+ end
71
+
72
+ context 'when it extends an object' do
73
+ let(:tester) { Object.new }
74
+
75
+ before { tester.extend MyRepository.injector }
76
+
77
+ describe '.find' do
78
+ it 'is delegated to the repository' do
79
+ expect(repository).to receive(:find).with(1)
80
+ tester.find(1)
81
+ end
82
+ end
83
+
84
+ describe '.custom_method' do
85
+ it 'is delegated to the repository' do
86
+ expect(repository).to receive(:custom_method)
87
+ tester.custom_method
88
+ end
89
+ end
90
+ end
91
+ end
@@ -20,13 +20,14 @@ RSpec.shared_examples 'scopable repository' do
20
20
  end
21
21
 
22
22
  let(:mapper) { mapper_klass.new(Thing).define_attributes(:a, :b) }
23
+ let(:one_thing) { Thing.new(nil, 2, 3) }
23
24
 
24
25
  subject { ThingRepository.new(source, mapper) }
25
26
 
26
27
  before do
27
28
  create_database if defined?(create_database)
28
29
 
29
- subject.insert(Thing.new(nil, 2, 3))
30
+ subject.insert(one_thing)
30
31
  subject.insert(Thing.new(nil, 2, 4))
31
32
  subject.insert(Thing.new(nil, 5, 6))
32
33
  end
@@ -103,12 +104,21 @@ RSpec.shared_examples 'scopable repository' do
103
104
  end
104
105
  end
105
106
  end
106
- end
107
107
 
108
- describe '#all' do
109
- it 'gets all objects' do
110
- objects = subject.all
111
- expect(objects.length).to eq 3
108
+ describe '#find' do
109
+ context 'when an existent id is given' do
110
+ it 'returns a proper object' do
111
+ found = subject.fetch.find(one_thing.id)
112
+ expect(found.id).to eq one_thing.id
113
+ end
114
+ end
115
+
116
+ context 'when a non-existent id is given' do
117
+ it 'returns nil' do
118
+ found = subject.fetch.find('wrong-id')
119
+ expect(found).to be_nil
120
+ end
121
+ end
112
122
  end
113
123
  end
114
124
 
@@ -29,6 +29,10 @@ RSpec.shared_examples 'timestampable repository' do
29
29
  expect(thing.created_at).not_to be_nil
30
30
  end
31
31
 
32
+ it 'sets the same value to updated_at' do
33
+ expect(thing.updated_at).to eq thing.created_at
34
+ end
35
+
32
36
  it 'saves created_at in the database' do
33
37
  expect(subject.find(thing.id).created_at).not_to be_nil
34
38
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rmodel
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.0
4
+ version: 1.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Alexei
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2016-04-24 00:00:00.000000000 Z
11
+ date: 2016-05-11 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: mongo
@@ -52,6 +52,20 @@ dependencies:
52
52
  - - ">="
53
53
  - !ruby/object:Gem::Version
54
54
  version: '0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: lazy_object
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ type: :runtime
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
55
69
  - !ruby/object:Gem::Dependency
56
70
  name: bundler
57
71
  requirement: !ruby/object:Gem::Requirement
@@ -195,6 +209,7 @@ files:
195
209
  - LICENSE.txt
196
210
  - README.md
197
211
  - Rakefile
212
+ - examples/inject_repo.rb
198
213
  - examples/mongo_embedded.rb
199
214
  - examples/scopes.rb
200
215
  - examples/sql_repository.rb
@@ -211,14 +226,18 @@ files:
211
226
  - lib/rmodel/base_mapper.rb
212
227
  - lib/rmodel/dummy_mapper.rb
213
228
  - lib/rmodel/errors.rb
229
+ - lib/rmodel/injector.rb
214
230
  - lib/rmodel/mongo/mapper.rb
231
+ - lib/rmodel/mongo/query.rb
215
232
  - lib/rmodel/mongo/source.rb
216
233
  - lib/rmodel/repository.rb
234
+ - lib/rmodel/repository_ext/injectable.rb
217
235
  - lib/rmodel/repository_ext/scopable.rb
218
236
  - lib/rmodel/repository_ext/sugarable.rb
219
237
  - lib/rmodel/repository_ext/timestampable.rb
220
238
  - lib/rmodel/scope.rb
221
239
  - lib/rmodel/sequel/mapper.rb
240
+ - lib/rmodel/sequel/query.rb
222
241
  - lib/rmodel/sequel/source.rb
223
242
  - lib/rmodel/uni_hash.rb
224
243
  - lib/rmodel/version.rb
@@ -235,6 +254,7 @@ files:
235
254
  - spec/shared/clean_sequel.rb
236
255
  - spec/shared/repository/crud.rb
237
256
  - spec/shared/repository/initialization.rb
257
+ - spec/shared/repository/injectable.rb
238
258
  - spec/shared/repository/scopable.rb
239
259
  - spec/shared/repository/sugarable.rb
240
260
  - spec/shared/repository/timestampable.rb
@@ -276,6 +296,7 @@ test_files:
276
296
  - spec/shared/clean_sequel.rb
277
297
  - spec/shared/repository/crud.rb
278
298
  - spec/shared/repository/initialization.rb
299
+ - spec/shared/repository/injectable.rb
279
300
  - spec/shared/repository/scopable.rb
280
301
  - spec/shared/repository/sugarable.rb
281
302
  - spec/shared/repository/timestampable.rb