rmodel 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
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