rmodel 1.0.0 → 1.1.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 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