rmodel 0.0.1

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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: deb9462cdda8354cf1ae328254df31b82ddd3a74
4
+ data.tar.gz: 98e72a0ff775b44114b7c90383046806f15948db
5
+ SHA512:
6
+ metadata.gz: dd4006a1d057a592bac459e31f39039c496a89d9c379c7953bea129bc8e3acbd92f13b7cd6d2c32a5ad8ee8a71caebdd8b72a9cd328de1ed41f697ab033b8be3
7
+ data.tar.gz: d3e1d56effdceeb17d3978b8242f35aaf9689b2eddf5b71fffc30e6665aea25972672bddcf1d1d3f4053559ffe59cc7d74362cacb5d7877befcaf10cf5c33279
data/.gitignore ADDED
@@ -0,0 +1,22 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
18
+ *.bundle
19
+ *.so
20
+ *.o
21
+ *.a
22
+ mkmf.log
data/.rspec ADDED
@@ -0,0 +1,3 @@
1
+ --format doc
2
+ --color
3
+ --require spec_helper
data/.travis.yml ADDED
@@ -0,0 +1,9 @@
1
+ language: ruby
2
+ script: rspec
3
+ rvm:
4
+ - 1.9.3
5
+ - 2.0.0
6
+ - 2.1.7
7
+ - 2.2.3
8
+ services:
9
+ - mongodb
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in rmodel.gemspec
4
+ gemspec
data/LICENSE.txt ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2015 Alexei
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,165 @@
1
+ [![Build Status](https://travis-ci.org/alexei-lexx/rmodel.svg)](https://travis-ci.org/alexei-lexx/rmodel)
2
+
3
+ # Rmodel
4
+
5
+ Rmodel is an ORM library, which tends to follow the SOLID principles.
6
+
7
+ The main thoughts behind it are:
8
+
9
+ * let you models be simple and independent of the persistent layer,
10
+ * be able to switch the persistent layer at any moment,
11
+ * keep the simplicity of the Active Record pattern by default,
12
+ * be able to implement any type of persistence: SQL, NoSQL, files, HTTP etc.
13
+
14
+ It consists of 3 major components:
15
+
16
+ 1. **Entities**; ex.: User, Order etc.
17
+ 2. **Repositories**, which are used to fetch, save and delete entities; ex.: UserRepository, OrderRepository
18
+ 3. **Factories**, which play the role of mappers.
19
+
20
+ Basic implemented features:
21
+
22
+ 1. CRUD operations: `find`, `insert`, `update`, `remove`;
23
+ 2. Scopes: `userRepository.query.recent.sorted`
24
+ 3. Based on query operations: `userRepository.query.recent.remove`
25
+
26
+ ## Installation
27
+
28
+ Add this line to your application's Gemfile:
29
+
30
+ gem 'rmodel'
31
+
32
+ And then execute:
33
+
34
+ $ bundle
35
+
36
+ Or install it yourself as:
37
+
38
+ $ gem install rmodel
39
+
40
+ ## Usage
41
+
42
+ Let's define an entity
43
+
44
+ ```ruby
45
+ class User
46
+ attr_accessor :id, :name, :email
47
+ end
48
+ ```
49
+
50
+ As you see it's a plain ruby class with attributes. It must have either the zero-argument `#initialize` method or no `#initialize` at all.
51
+
52
+ Of course we need a repository to save users.
53
+
54
+ ```ruby
55
+ require 'rmodel' # dont forget to require the gem
56
+
57
+ class User
58
+ attr_accessor :id, :name, :email
59
+ end
60
+
61
+ class UserRepository < Rmodel::Mongo::Repository
62
+ end
63
+
64
+ userRepository = UserRepository.new
65
+ ```
66
+ The code above raises the exception *Client driver is not setup (ArgumentError)*. UserRepository derives from Rmodel::Mongo::Repository, which uses the ruby mongo driver to access the database. We must provide the appropriate connection options. To do this we use the following code:
67
+
68
+ ```ruby
69
+ require 'rmodel'
70
+
71
+ Rmodel.setup do
72
+ client :default, { hosts: [ 'localhost' ], database: 'test' }
73
+ end
74
+ ```
75
+
76
+ The `:default` client is used by every repository that doesn't specify it's client explicitly.
77
+
78
+ Run the code again and get another error *Factory can not be guessed (ArgumentError)*. The factory is used to convert the array of database tuples (hashes) to the array of User objects.
79
+
80
+ ```ruby
81
+ class UserRepository < Rmodel::Mongo::Repository
82
+ simple_factory User, :name, :email
83
+ end
84
+ ```
85
+
86
+ The `simple_factory` class macro says that every database tuple will be straightforwardly converted to an instance of User with attributes :id, :name and :email. There is no need to specify :id, because it's required.
87
+
88
+ ### CRUD
89
+
90
+ Let's create and insert several users.
91
+
92
+ ```ruby
93
+ john = User.new('John', 'john@example.com')
94
+ bill = User.new('Bill', 'bill@example.com')
95
+ bob = User.new('Bob', 'bob@example.com')
96
+
97
+ userRepository.insert(john)
98
+ userRepository.insert(bill)
99
+ userRepository.insert(bob)
100
+ ```
101
+
102
+ Now you can check you `test` database. There are 3 new users there. Print the `john`. As you can see it's got the `@id`.
103
+
104
+ ```ruby
105
+ p john
106
+ #<User:0x00... @name="John", @email="john@example.com", @id=BSON::ObjectId('562a...')>
107
+ ```
108
+
109
+ Let's update John and remove Bob.
110
+
111
+ ```ruby
112
+ john.name = 'John Smith'
113
+ userRepository.update(john)
114
+
115
+ userRepository.remove(bob)
116
+
117
+ p userRepository.find(john.id) # #<User:0x000000037237d0 @name="John Smith" ... >
118
+ p userRepository.find(bob.id) # nil
119
+ p userRepository.find!(bob.id) # nil
120
+ ```
121
+
122
+ ### Scopes
123
+
124
+ Scopes are defined inside the repository.
125
+
126
+ ```ruby
127
+ class UserRepository < Rmodel::Mongo::Repository
128
+ simple_factory User, :name, :email
129
+
130
+ scope :have_email do
131
+ where(email: { '$exists' => true })
132
+ end
133
+
134
+ scope :start_with do |letter|
135
+ where(name: { '$regex' => "^#{letter}", '$options' => 'i' })
136
+ end
137
+ end
138
+
139
+ userRepository.query.start_with('b').to_a
140
+ ```
141
+
142
+ Of course you can chain scopes.
143
+
144
+ ```ruby
145
+ userRepository.query.start_with('b').have_email
146
+ ```
147
+
148
+ The result of the scope is Enumerable, so you can apply the #each method and others (map, select etc).
149
+
150
+ Inside the scopes you can use any methods supported by the driver (database client). In our case we use Origin (https://github.com/mongoid/origin) as a query builder for mongo.
151
+
152
+ Also it's possible to use scopes to run the multi-row operations.
153
+
154
+ ```ruby
155
+ userRepository.query.have_email.remove
156
+ p userRepository.query.count # 0
157
+ ```
158
+
159
+ ## Contributing
160
+
161
+ 1. Fork it ( https://github.com/alexei-lexx/rmodel/fork )
162
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
163
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
164
+ 4. Push to the branch (`git push origin my-new-feature`)
165
+ 5. Create a new Pull Request
data/Rakefile ADDED
@@ -0,0 +1,2 @@
1
+ require "bundler/gem_tasks"
2
+
data/examples/user.rb ADDED
@@ -0,0 +1,48 @@
1
+ require 'rmodel'
2
+
3
+ Rmodel.setup do
4
+ client :default, { hosts: [ 'localhost' ], database: 'test' }
5
+ end
6
+
7
+ class User
8
+ attr_accessor :id, :name, :email
9
+
10
+ def initialize(name = nil, email = nil)
11
+ self.name = name
12
+ self.email = email
13
+ end
14
+ end
15
+
16
+ class UserRepository < Rmodel::Mongo::Repository
17
+ simple_factory User, :name, :email
18
+
19
+ scope :have_email do
20
+ where(email: { '$exists' => true })
21
+ end
22
+
23
+ scope :start_with do |letter|
24
+ where(name: { '$regex' => "^#{letter}", '$options' => 'i' })
25
+ end
26
+ end
27
+
28
+ userRepository = UserRepository.new
29
+ userRepository.query.remove
30
+
31
+ john = User.new('John', 'john@example.com')
32
+ bill = User.new('Bill', 'bill@example.com')
33
+ bob = User.new('Bob', 'bob@example.com')
34
+
35
+ userRepository.insert(john)
36
+ userRepository.insert(bill)
37
+ userRepository.insert(bob)
38
+
39
+ john.name = 'John Smith'
40
+ userRepository.update(john)
41
+
42
+ userRepository.remove(bob)
43
+
44
+ p userRepository.find(john.id)
45
+ p userRepository.find(bob.id)
46
+
47
+ userRepository.query.have_email.remove
48
+ p userRepository.query.count
@@ -0,0 +1,7 @@
1
+ module Rmodel
2
+ class NotFound < StandardError
3
+ def initialize(repo_klass, criteria)
4
+ super("#{repo_klass.class.name} can't find an object by #{criteria}")
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,65 @@
1
+ require 'mongo'
2
+ require 'active_support/inflector'
3
+ require 'rmodel/mongo/repository_ext/queryable'
4
+
5
+ module Rmodel::Mongo
6
+ class Repository
7
+ include RepositoryExt::Queryable
8
+
9
+ def initialize
10
+ @client = Rmodel.setup.establish_mongo_client(self.class.client_name || :default) or
11
+ raise ArgumentError.new('Client driver is not setup')
12
+
13
+ @collection = self.class.setting_collection ||
14
+ self.class.collection_by_convention or
15
+ raise ArgumentError.new('Collection can not be guessed')
16
+
17
+ @factory = self.class.setting_factory or
18
+ raise ArgumentError.new('Factory can not be guessed')
19
+ end
20
+
21
+ def find(id)
22
+ result = @client[@collection].find(_id: id).first
23
+ result && @factory.fromHash(result)
24
+ end
25
+
26
+ def find!(id)
27
+ find(id) or raise Rmodel::NotFound.new(self, { id: id })
28
+ end
29
+
30
+ def insert(object)
31
+ object.id ||= BSON::ObjectId.new
32
+ @client[@collection].insert_one(@factory.toHash(object, true))
33
+ end
34
+
35
+ def update(object)
36
+ @client[@collection].find(_id: object.id).update_one(@factory.toHash(object, false))
37
+ end
38
+
39
+ def remove(object)
40
+ @client[@collection].find(_id: object.id).delete_one
41
+ end
42
+
43
+ class << self
44
+ attr_reader :client_name, :setting_collection, :setting_factory
45
+
46
+ def client(name)
47
+ @client_name = name
48
+ end
49
+
50
+ def collection(name)
51
+ @setting_collection = name
52
+ end
53
+
54
+ def collection_by_convention
55
+ if name =~ /(.*)Repository$/
56
+ ActiveSupport::Inflector.tableize($1).to_sym
57
+ end
58
+ end
59
+
60
+ def simple_factory(klass, *attributes)
61
+ @setting_factory = SimpleFactory.new(klass, *attributes)
62
+ end
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,34 @@
1
+ require 'origin'
2
+
3
+ module Rmodel::Mongo
4
+ module RepositoryExt
5
+ class Query
6
+ include Enumerable
7
+
8
+ class Queryable
9
+ include Origin::Queryable
10
+ end
11
+
12
+ def initialize(repo, queryable = nil)
13
+ @repo = repo
14
+ @queryable = queryable || Queryable.new
15
+ end
16
+
17
+ def each(&block)
18
+ @repo.find_by_query(@queryable.selector, @queryable.options).each(&block)
19
+ self
20
+ end
21
+
22
+ def remove
23
+ @repo.execute_query(@queryable.selector, @queryable.options).delete_many
24
+ end
25
+
26
+ def self.define_scope(name, &block)
27
+ define_method name do |*args|
28
+ new_queryable = @queryable.instance_exec(*args, &block)
29
+ self.class.new(@repo, new_queryable)
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,34 @@
1
+ require 'rmodel/mongo/repository_ext/query'
2
+
3
+ module Rmodel::Mongo
4
+ module RepositoryExt
5
+ module Queryable
6
+ def self.included(base)
7
+ base.extend ClassMethods
8
+ end
9
+
10
+ def query
11
+ (self.class.query_klass ||= Class.new(Query)).new(self)
12
+ end
13
+
14
+ def find_by_query(selector, options)
15
+ execute_query(selector, options).map do |hash|
16
+ @factory.fromHash(hash)
17
+ end
18
+ end
19
+
20
+ def execute_query(selector, options)
21
+ @client[@collection].find(selector, options)
22
+ end
23
+
24
+ module ClassMethods
25
+ attr_accessor :query_klass
26
+
27
+ def scope(name, &block)
28
+ self.query_klass ||= Class.new(Query)
29
+ self.query_klass.define_scope(name, &block)
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,17 @@
1
+ module Rmodel::Mongo
2
+ module Setup
3
+ def establish_mongo_client(name)
4
+ config = @clients_config[name]
5
+ if config
6
+ options = config.dup
7
+ options.delete :hosts
8
+
9
+ establish_client(name) { Mongo::Client.new(config[:hosts], options) }
10
+ end
11
+ end
12
+ end
13
+ end
14
+
15
+ class Rmodel::Setup
16
+ include Rmodel::Mongo::Setup
17
+ end
@@ -0,0 +1,28 @@
1
+ module Rmodel::Mongo
2
+ class SimpleFactory
3
+ def initialize(klass, *attributes)
4
+ @klass = klass
5
+ @attributes = attributes
6
+ end
7
+
8
+ def fromHash(hash)
9
+ object = @klass.new
10
+ object.id = hash['_id']
11
+ @attributes.each do |attribute|
12
+ object.public_send "#{attribute}=", hash[attribute.to_s]
13
+ end
14
+ object
15
+ end
16
+
17
+ def toHash(object, id_included)
18
+ hash = {}
19
+ @attributes.each do |attribute|
20
+ hash[attribute.to_s] = object.public_send(attribute)
21
+ end
22
+ if id_included
23
+ hash['_id'] = object.id
24
+ end
25
+ hash
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,33 @@
1
+ require 'singleton'
2
+
3
+ module Rmodel
4
+ class Setup
5
+ include Singleton
6
+
7
+ def initialize
8
+ @clients_config = {}
9
+ @established_clients = {}
10
+ end
11
+
12
+ def client(name, config)
13
+ @clients_config[name] = config
14
+ end
15
+
16
+ def clear
17
+ @clients_config.clear
18
+ end
19
+
20
+ private
21
+
22
+ def establish_client(name)
23
+ @established_clients[name] ||= yield
24
+ end
25
+ end
26
+
27
+ def self.setup(&block)
28
+ if block
29
+ Setup.instance.instance_eval &block
30
+ end
31
+ Setup.instance
32
+ end
33
+ end
@@ -0,0 +1,3 @@
1
+ module Rmodel
2
+ VERSION = "0.0.1"
3
+ end
data/lib/rmodel.rb ADDED
@@ -0,0 +1,10 @@
1
+ require 'rmodel/version'
2
+ require 'rmodel/setup'
3
+ require 'rmodel/errors'
4
+ require 'rmodel/mongo/setup'
5
+ require 'rmodel/mongo/simple_factory'
6
+ require 'rmodel/mongo/repository'
7
+
8
+ module Rmodel
9
+
10
+ end
data/rmodel.gemspec ADDED
@@ -0,0 +1,28 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'rmodel/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = 'rmodel'
8
+ spec.version = Rmodel::VERSION
9
+ spec.authors = ['Alexei']
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.}
13
+ spec.homepage = 'https://github.com/alexei-lexx/rmodel'
14
+ spec.license = 'MIT'
15
+
16
+ spec.files = `git ls-files -z`.split("\x0")
17
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
18
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
19
+ spec.require_paths = ['lib']
20
+
21
+ spec.add_dependency 'mongo', '~> 2.1'
22
+ spec.add_dependency 'activesupport'
23
+ spec.add_dependency 'origin'
24
+
25
+ spec.add_development_dependency 'bundler', '~> 1.6'
26
+ spec.add_development_dependency 'rake'
27
+ spec.add_development_dependency 'rspec'
28
+ end
@@ -0,0 +1,115 @@
1
+ RSpec.describe Rmodel::Mongo::Repository do
2
+ include_context 'clean Mongo database'
3
+
4
+ before do
5
+ stub_const('User', Struct.new(:id, :name, :email))
6
+ stub_const('UserRepository', Class.new(Rmodel::Mongo::Repository) {
7
+ simple_factory User, :name, :email
8
+ })
9
+ Rmodel.setup do
10
+ client :default, hosts: [ 'localhost' ], database: 'rmodel_test'
11
+ end
12
+ end
13
+
14
+ let(:factory) { Rmodel::Mongo::SimpleFactory.new(User, :name, :email) }
15
+ subject(:repo) { UserRepository.new }
16
+
17
+ describe '#find' do
18
+ context 'when an existent id is given' do
19
+ before do
20
+ mongo_session[:users].insert_one(_id: 1, name: 'John', email: 'john@example.com')
21
+ end
22
+
23
+ it 'returns the instance of correct type' do
24
+ expect(repo.find(1)).to be_an_instance_of User
25
+ end
26
+ end
27
+
28
+ context 'when a non-existent id is given' do
29
+ it 'returns nil' do
30
+ expect(repo.find(1)).to be_nil
31
+ end
32
+ end
33
+ end
34
+
35
+ describe '#find!' do
36
+ context 'when an existent id is given' do
37
+ before do
38
+ mongo_session[:users].insert_one(_id: 1, name: 'John', email: 'john@example.com')
39
+ end
40
+
41
+ it 'returns the right instance' do
42
+ expect(repo.find!(1)).not_to be_nil
43
+ end
44
+ end
45
+
46
+ context 'when a non-existent id is given' do
47
+ it 'raises the NotFound error' do
48
+ expect {
49
+ repo.find!(1)
50
+ }.to raise_error Rmodel::NotFound
51
+ end
52
+ end
53
+ end
54
+
55
+ describe '#insert' do
56
+ context 'when the id is not provided' do
57
+ let(:user) { User.new(nil, 'John', 'john@example.com') }
58
+
59
+ it 'sets the id before insert' do
60
+ repo.insert(user)
61
+ expect(user.id).not_to be_nil
62
+ end
63
+
64
+ it 'persists the object' do
65
+ repo.insert(user)
66
+ found = mongo_session[:users].find(name: 'John', email: 'john@example.com').count
67
+ expect(found).to eq 1
68
+ end
69
+ end
70
+
71
+ context 'when the id is provided' do
72
+ let(:user) { User.new(1, 'John', 'john@example.com') }
73
+
74
+ it 'uses the existent id' do
75
+ repo.insert(user)
76
+ expect(user.id).to eq 1
77
+ end
78
+ end
79
+
80
+ context 'when the given id already exists' do
81
+ let(:user) { User.new(nil, 'John', 'john@example.com') }
82
+ before { repo.insert(user) }
83
+
84
+ it 'raises the error' do
85
+ expect { repo.insert(user) }.to raise_error Mongo::Error::OperationFailure
86
+ end
87
+ end
88
+ end
89
+
90
+ describe '#update' do
91
+ let(:user) { User.new(nil, 'John', 'john@example.com') }
92
+
93
+ before do
94
+ repo.insert(user)
95
+ user.name = 'John Smith'
96
+ end
97
+
98
+ it 'updates the record' do
99
+ repo.update(user)
100
+ found = mongo_session[:users].find(name: 'John Smith').count
101
+ expect(found).to eq 1
102
+ end
103
+ end
104
+
105
+ describe '#remove' do
106
+ let(:user) { User.new(nil, 'John', 'john@example.com') }
107
+ before { repo.insert(user) }
108
+
109
+ it 'removes the record' do
110
+ repo.remove(user)
111
+ found = mongo_session[:users].find(name: 'John').count
112
+ expect(found).to eq 0
113
+ end
114
+ end
115
+ end
@@ -0,0 +1,140 @@
1
+ RSpec.describe Rmodel::Mongo::Repository do
2
+ before do
3
+ Rmodel.setup.clear
4
+ stub_const('User', Struct.new(:id, :name, :email))
5
+ end
6
+
7
+ subject { UserRepository.new }
8
+
9
+ describe '.client(name)' do
10
+ before do
11
+ Rmodel::Setup.send :public, :client
12
+ end
13
+ context 'when it is called with an existent name' do
14
+ before do
15
+ Rmodel.setup do
16
+ client :mongo, { hosts: [ 'localhost' ] }
17
+ end
18
+
19
+ stub_const('UserRepository', Class.new(Rmodel::Mongo::Repository) {
20
+ client :mongo
21
+ simple_factory User, :name, :email
22
+ attr_reader :client
23
+ })
24
+ end
25
+
26
+ it 'sets the appropriate #client' do
27
+ expect(subject.client).to be_an_instance_of Mongo::Client
28
+ end
29
+ end
30
+
31
+ context 'when it is called with a non-existent name' do
32
+ before do
33
+ stub_const('UserRepository', Class.new(Rmodel::Mongo::Repository) {
34
+ client :mongo
35
+ simple_factory User, :name, :email
36
+ attr_reader :client
37
+ })
38
+ end
39
+
40
+ it 'makes #client raise the ArgumentError' do
41
+ expect { subject.client }.to raise_error ArgumentError
42
+ end
43
+ end
44
+
45
+ context 'when it is not called' do
46
+ before do
47
+ stub_const('UserRepository', Class.new(Rmodel::Mongo::Repository) {
48
+ simple_factory User, :name, :email
49
+ attr_reader :client
50
+ })
51
+ end
52
+
53
+ context 'when the :default client is set' do
54
+ before do
55
+ Rmodel.setup do
56
+ client :default, { hosts: [ 'localhost' ] }
57
+ end
58
+ end
59
+
60
+ it 'sets #client to be default' do
61
+ expect(subject.client).to be_an_instance_of Mongo::Client
62
+ end
63
+ end
64
+
65
+ context 'when the :default client is not set' do
66
+ it 'makes #client raise the ArgumentError' do
67
+ expect { subject.client }.to raise_error ArgumentError
68
+ end
69
+ end
70
+ end
71
+ end
72
+
73
+ describe '.collection(name)' do
74
+ before do
75
+ Rmodel.setup do
76
+ client :default, { hosts: [ 'localhost' ] }
77
+ end
78
+ end
79
+
80
+ context 'when the :people collection is given' do
81
+ before do
82
+ stub_const('UserRepository', Class.new(Rmodel::Mongo::Repository) {
83
+ collection :people
84
+ simple_factory User, :name, :email
85
+ attr_reader :collection
86
+ })
87
+ end
88
+
89
+ it 'uses the :people' do
90
+ expect(subject.collection).to eq :people
91
+ end
92
+ end
93
+
94
+ context 'when no collection is given' do
95
+ before do
96
+ stub_const('UserRepository', Class.new(Rmodel::Mongo::Repository) {
97
+ simple_factory User, :name, :email
98
+ attr_reader :collection
99
+ })
100
+ end
101
+
102
+ it 'gets the right name by convention' do
103
+ expect(subject.collection).to eq :users
104
+ end
105
+ end
106
+ end
107
+
108
+ describe '.simple_factory(klass, attribute1, attribute2, ...)' do
109
+ before do
110
+ Rmodel.setup do
111
+ client :default, { hosts: [ 'localhost' ] }
112
+ end
113
+ end
114
+
115
+ context 'when it is called' do
116
+ before do
117
+ stub_const('UserRepository', Class.new(Rmodel::Mongo::Repository) {
118
+ simple_factory User, :name, :email
119
+ attr_reader :factory
120
+ })
121
+ end
122
+
123
+ it 'sets the appropriate #factory' do
124
+ expect(subject.factory).to be_an_instance_of Rmodel::Mongo::SimpleFactory
125
+ end
126
+ end
127
+
128
+ context 'when it is not called' do
129
+ before do
130
+ stub_const('UserRepository', Class.new(Rmodel::Mongo::Repository))
131
+ end
132
+
133
+ it 'make #initialize raise an error' do
134
+ expect {
135
+ UserRepository.new
136
+ }.to raise_error ArgumentError
137
+ end
138
+ end
139
+ end
140
+ end
@@ -0,0 +1,107 @@
1
+ RSpec.describe Rmodel::Mongo::Repository do
2
+ include_context 'clean Mongo database'
3
+
4
+ before do
5
+ Rmodel.setup do
6
+ client :default, hosts: [ 'localhost' ], database: 'rmodel_test'
7
+ end
8
+ stub_const('Thing', Struct.new(:id, :a, :b))
9
+ stub_const('ThingRepository', Class.new(Rmodel::Mongo::Repository) {
10
+ simple_factory Thing, :a, :b
11
+ })
12
+ end
13
+
14
+ let(:repo) { ThingRepository.new }
15
+
16
+ before do
17
+ repo.insert(Thing.new(nil, 2, 3))
18
+ repo.insert(Thing.new(nil, 2, 4))
19
+ repo.insert(Thing.new(nil, 5, 6))
20
+ end
21
+
22
+ describe '.scope' do
23
+ context 'when a scope w/o arguments is defined' do
24
+ before do
25
+ ThingRepository.class_eval do
26
+ scope :a_equals_2 do
27
+ where(a: 2)
28
+ end
29
+ end
30
+ end
31
+
32
+ it 'works!' do
33
+ expect(repo.query.a_equals_2.count).to eq 2
34
+ end
35
+
36
+ it 'returns an array of instances of the appropriate class' do
37
+ expect(repo.query.a_equals_2.first).to be_an_instance_of Thing
38
+ end
39
+ end
40
+
41
+ context 'when a scope w/ arguments is defined' do
42
+ before do
43
+ ThingRepository.class_eval do
44
+ scope :a_equals do |n|
45
+ where(a: n)
46
+ end
47
+ end
48
+ end
49
+
50
+ it 'works!' do
51
+ expect(repo.query.a_equals(2).count).to eq 2
52
+ end
53
+ end
54
+
55
+ context 'when two scopes are defined and chained' do
56
+ before do
57
+ ThingRepository.class_eval do
58
+ scope :a_equals do |n|
59
+ where(a: n)
60
+ end
61
+
62
+ scope :b_equals do |n|
63
+ where(b: n)
64
+ end
65
+ end
66
+ end
67
+
68
+ it 'works!' do
69
+ expect(repo.query.a_equals(2).b_equals(4).count).to eq 1
70
+ end
71
+ end
72
+
73
+ context 'when an unknown scope is used' do
74
+ it 'raises the NoMethodError' do
75
+ expect {
76
+ repo.query.something
77
+ }.to raise_error NoMethodError
78
+ end
79
+ end
80
+ end
81
+
82
+ describe '.query' do
83
+ describe '#remove' do
84
+ context 'when no scope is given' do
85
+ it 'removes all objects' do
86
+ repo.query.remove
87
+ expect(repo.query.count).to eq 0
88
+ end
89
+ end
90
+
91
+ context 'when the scope filters 2 objects from 3' do
92
+ before do
93
+ ThingRepository.class_eval do
94
+ scope :a_equals_2 do
95
+ where(a: 2)
96
+ end
97
+ end
98
+ end
99
+
100
+ it 'removes 2 objects' do
101
+ repo.query.a_equals_2.remove
102
+ expect(repo.query.count).to eq 1
103
+ end
104
+ end
105
+ end
106
+ end
107
+ end
@@ -0,0 +1,55 @@
1
+ RSpec.describe Rmodel::Mongo::SimpleFactory do
2
+ context 'when the User(id, name, email) class is defined' do
3
+ before { stub_const('User', Struct.new(:id, :name, :email)) }
4
+
5
+ subject(:factory) { Rmodel::Mongo::SimpleFactory.new(User, :name, :email) }
6
+
7
+ describe '#fromHash' do
8
+ context 'when the hash with _id, name and email is given' do
9
+ let(:hash) { { '_id' => 1, 'name' => 'John', 'email' => 'john@example.com' } }
10
+ let(:result) { factory.fromHash(hash) }
11
+
12
+ it 'returns an instance of User' do
13
+ expect(result).to be_an_instance_of User
14
+ end
15
+
16
+ it 'sets the attributes correctly' do
17
+ expect(result.name).to eq 'John'
18
+ expect(result.email).to eq 'john@example.com'
19
+ end
20
+
21
+ it 'sets the User#id correctly' do
22
+ expect(result.id).to eq 1
23
+ end
24
+ end
25
+ end
26
+
27
+ describe '#toHash' do
28
+ let(:user) { User.new(1, 'John', 'john@example.com') }
29
+ context 'when id_included is false' do
30
+ let(:result) { factory.toHash(user, false) }
31
+
32
+ it 'returns an instance of Hash' do
33
+ expect(result).to be_an_instance_of Hash
34
+ end
35
+
36
+ it 'sets the keys correctly' do
37
+ expect(result['name']).to eq 'John'
38
+ expect(result['email']).to eq 'john@example.com'
39
+ end
40
+
41
+ it 'has no the "_id" key' do
42
+ expect(result.has_key?('_id')).to be false
43
+ end
44
+ end
45
+
46
+ context 'when id_included is true' do
47
+ let(:result) { factory.toHash(user, true) }
48
+
49
+ it 'sets the "_id" key' do
50
+ expect(result['_id']).to eq 1
51
+ end
52
+ end
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,54 @@
1
+ RSpec.describe Rmodel do
2
+ describe '.setup' do
3
+ before { Rmodel::Setup.instance.clear }
4
+ context 'when no block is passed' do
5
+ it 'returns Rmodel::Setup.instance' do
6
+ expect(Rmodel.setup).to equal Rmodel::Setup.instance
7
+ end
8
+ end
9
+
10
+ context 'when the block is passed' do
11
+ let(:clients_config) { Rmodel::Setup.instance.instance_variable_get('@clients_config') }
12
+
13
+ it 'returns Rmodel::Setup.instance' do
14
+ expect(Rmodel.setup).to equal Rmodel::Setup.instance
15
+ end
16
+
17
+ it 'runs setup methods within the block' do
18
+ Rmodel.setup do
19
+ client :default, {}
20
+ end
21
+ expect(clients_config[:default]).to eq({})
22
+ end
23
+ end
24
+ end
25
+
26
+ describe Rmodel::Setup do
27
+ subject { Rmodel::Setup.instance }
28
+ let(:clients_config) { subject.instance_variable_get('@clients_config') }
29
+
30
+ describe '#new' do
31
+ it 'raises the NoMethodError' do
32
+ expect { Rmodel::Setup.new }.to raise_error NoMethodError
33
+ end
34
+ end
35
+
36
+ describe '#client(name, config)' do
37
+ it 'makes config available via #clients[name]' do
38
+ subject.client :default, { host: 'localhost' }
39
+ expect(clients_config[:default]).to eq( host: 'localhost' )
40
+ end
41
+ end
42
+
43
+ describe '#clear' do
44
+ context 'when one client is set' do
45
+ before { subject.client :default, { host: 'localhost' } }
46
+
47
+ it 'removes all clients' do
48
+ subject.clear
49
+ expect(clients_config).to be_empty
50
+ end
51
+ end
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,10 @@
1
+ RSpec.shared_context 'clean Mongo database' do
2
+ let(:mongo_session) { Mongo::Client.new([ '127.0.0.1:27017' ], database: 'rmodel_test') }
3
+
4
+ before(:all) do
5
+ Mongo::Logger.logger.level = Logger::ERROR
6
+ mongo_session = Mongo::Client.new([ '127.0.0.1:27017' ], database: 'rmodel_test')
7
+ mongo_session.database.drop
8
+ end
9
+ after { mongo_session.database.drop }
10
+ end
@@ -0,0 +1,100 @@
1
+ require 'rmodel'
2
+
3
+ Dir[File.dirname(__FILE__) + '/shared/**/*.rb'].each { |f| require f }
4
+
5
+ # This file was generated by the `rspec --init` command. Conventionally, all
6
+ # specs live under a `spec` directory, which RSpec adds to the `$LOAD_PATH`.
7
+ # The generated `.rspec` file contains `--require spec_helper` which will cause
8
+ # this file to always be loaded, without a need to explicitly require it in any
9
+ # files.
10
+ #
11
+ # Given that it is always loaded, you are encouraged to keep this file as
12
+ # light-weight as possible. Requiring heavyweight dependencies from this file
13
+ # will add to the boot time of your test suite on EVERY test run, even for an
14
+ # individual file that may not need all of that loaded. Instead, consider making
15
+ # a separate helper file that requires the additional dependencies and performs
16
+ # the additional setup, and require it from the spec files that actually need
17
+ # it.
18
+ #
19
+ # The `.rspec` file also contains a few flags that are not defaults but that
20
+ # users commonly want.
21
+ #
22
+ # See http://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration
23
+ RSpec.configure do |config|
24
+ # rspec-expectations config goes here. You can use an alternate
25
+ # assertion/expectation library such as wrong or the stdlib/minitest
26
+ # assertions if you prefer.
27
+ config.expect_with :rspec do |expectations|
28
+ # This option will default to `true` in RSpec 4. It makes the `description`
29
+ # and `failure_message` of custom matchers include text for helper methods
30
+ # defined using `chain`, e.g.:
31
+ # be_bigger_than(2).and_smaller_than(4).description
32
+ # # => "be bigger than 2 and smaller than 4"
33
+ # ...rather than:
34
+ # # => "be bigger than 2"
35
+ expectations.include_chain_clauses_in_custom_matcher_descriptions = true
36
+ end
37
+
38
+ # rspec-mocks config goes here. You can use an alternate test double
39
+ # library (such as bogus or mocha) by changing the `mock_with` option here.
40
+ config.mock_with :rspec do |mocks|
41
+ # Prevents you from mocking or stubbing a method that does not exist on
42
+ # a real object. This is generally recommended, and will default to
43
+ # `true` in RSpec 4.
44
+ mocks.verify_partial_doubles = true
45
+ end
46
+
47
+ # The settings below are suggested to provide a good initial experience
48
+ # with RSpec, but feel free to customize to your heart's content.
49
+ =begin
50
+ # These two settings work together to allow you to limit a spec run
51
+ # to individual examples or groups you care about by tagging them with
52
+ # `:focus` metadata. When nothing is tagged with `:focus`, all examples
53
+ # get run.
54
+ config.filter_run :focus
55
+ config.run_all_when_everything_filtered = true
56
+
57
+ # Allows RSpec to persist some state between runs in order to support
58
+ # the `--only-failures` and `--next-failure` CLI options. We recommend
59
+ # you configure your source control system to ignore this file.
60
+ config.example_status_persistence_file_path = "spec/examples.txt"
61
+
62
+ # Limits the available syntax to the non-monkey patched syntax that is
63
+ # recommended. For more details, see:
64
+ # - http://myronmars.to/n/dev-blog/2012/06/rspecs-new-expectation-syntax
65
+ # - http://www.teaisaweso.me/blog/2013/05/27/rspecs-new-message-expectation-syntax/
66
+ # - http://myronmars.to/n/dev-blog/2014/05/notable-changes-in-rspec-3#new__config_option_to_disable_rspeccore_monkey_patching
67
+ config.disable_monkey_patching!
68
+
69
+ # This setting enables warnings. It's recommended, but in some cases may
70
+ # be too noisy due to issues in dependencies.
71
+ config.warnings = true
72
+
73
+ # Many RSpec users commonly either run the entire suite or an individual
74
+ # file, and it's useful to allow more verbose output when running an
75
+ # individual spec file.
76
+ if config.files_to_run.one?
77
+ # Use the documentation formatter for detailed output,
78
+ # unless a formatter has already been configured
79
+ # (e.g. via a command-line flag).
80
+ config.default_formatter = 'doc'
81
+ end
82
+
83
+ # Print the 10 slowest examples and example groups at the
84
+ # end of the spec run, to help surface which specs are running
85
+ # particularly slow.
86
+ config.profile_examples = 10
87
+
88
+ # Run specs in random order to surface order dependencies. If you find an
89
+ # order dependency and want to debug it, you can fix the order by providing
90
+ # the seed, which is printed after each run.
91
+ # --seed 1234
92
+ config.order = :random
93
+
94
+ # Seed global randomization in this process using the `--seed` CLI option.
95
+ # Setting this allows you to use `--seed` to deterministically reproduce
96
+ # test failures related to randomization by passing the same `--seed` value
97
+ # as the one that triggered the failure.
98
+ Kernel.srand config.seed
99
+ =end
100
+ end
metadata ADDED
@@ -0,0 +1,160 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: rmodel
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - Alexei
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2015-10-24 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: mongo
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '2.1'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '2.1'
27
+ - !ruby/object:Gem::Dependency
28
+ name: activesupport
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: origin
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: bundler
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '1.6'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '1.6'
69
+ - !ruby/object:Gem::Dependency
70
+ name: rake
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ">="
74
+ - !ruby/object:Gem::Version
75
+ version: '0'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ">="
81
+ - !ruby/object:Gem::Version
82
+ version: '0'
83
+ - !ruby/object:Gem::Dependency
84
+ name: rspec
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - ">="
88
+ - !ruby/object:Gem::Version
89
+ version: '0'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - ">="
95
+ - !ruby/object:Gem::Version
96
+ version: '0'
97
+ description: Rmodel is an ORM library, which tends to follow the SOLID principles.
98
+ email:
99
+ - alexei.lexx@gmail.com
100
+ executables: []
101
+ extensions: []
102
+ extra_rdoc_files: []
103
+ files:
104
+ - ".gitignore"
105
+ - ".rspec"
106
+ - ".travis.yml"
107
+ - Gemfile
108
+ - LICENSE.txt
109
+ - README.md
110
+ - Rakefile
111
+ - examples/user.rb
112
+ - lib/rmodel.rb
113
+ - lib/rmodel/errors.rb
114
+ - lib/rmodel/mongo/repository.rb
115
+ - lib/rmodel/mongo/repository_ext/query.rb
116
+ - lib/rmodel/mongo/repository_ext/queryable.rb
117
+ - lib/rmodel/mongo/setup.rb
118
+ - lib/rmodel/mongo/simple_factory.rb
119
+ - lib/rmodel/setup.rb
120
+ - lib/rmodel/version.rb
121
+ - rmodel.gemspec
122
+ - spec/rmodel/mongo/repository_crud_spec.rb
123
+ - spec/rmodel/mongo/repository_initialize_spec.rb
124
+ - spec/rmodel/mongo/repository_queryable_spec.rb
125
+ - spec/rmodel/mongo/simple_factory_spec.rb
126
+ - spec/rmodel/setup_spec.rb
127
+ - spec/shared/clean_moped.rb
128
+ - spec/spec_helper.rb
129
+ homepage: https://github.com/alexei-lexx/rmodel
130
+ licenses:
131
+ - MIT
132
+ metadata: {}
133
+ post_install_message:
134
+ rdoc_options: []
135
+ require_paths:
136
+ - lib
137
+ required_ruby_version: !ruby/object:Gem::Requirement
138
+ requirements:
139
+ - - ">="
140
+ - !ruby/object:Gem::Version
141
+ version: '0'
142
+ required_rubygems_version: !ruby/object:Gem::Requirement
143
+ requirements:
144
+ - - ">="
145
+ - !ruby/object:Gem::Version
146
+ version: '0'
147
+ requirements: []
148
+ rubyforge_project:
149
+ rubygems_version: 2.4.5
150
+ signing_key:
151
+ specification_version: 4
152
+ summary: Rmodel is an ORM library, which tends to follow the SOLID principles.
153
+ test_files:
154
+ - spec/rmodel/mongo/repository_crud_spec.rb
155
+ - spec/rmodel/mongo/repository_initialize_spec.rb
156
+ - spec/rmodel/mongo/repository_queryable_spec.rb
157
+ - spec/rmodel/mongo/simple_factory_spec.rb
158
+ - spec/rmodel/setup_spec.rb
159
+ - spec/shared/clean_moped.rb
160
+ - spec/spec_helper.rb