test_fish0 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/MIT-LICENSE +20 -0
- data/README.md +174 -0
- data/Rakefile +22 -0
- data/lib/fish0/collection.rb +21 -0
- data/lib/fish0/concerns/base.rb +90 -0
- data/lib/fish0/concerns/cacheable.rb +37 -0
- data/lib/fish0/concerns/equalable.rb +10 -0
- data/lib/fish0/concerns/paginatable.rb +25 -0
- data/lib/fish0/concerns/view_model.rb +20 -0
- data/lib/fish0/configuration.rb +30 -0
- data/lib/fish0/engine.rb +12 -0
- data/lib/fish0/exceptions.rb +3 -0
- data/lib/fish0/model.rb +10 -0
- data/lib/fish0/paginator.rb +55 -0
- data/lib/fish0/repository.rb +125 -0
- data/lib/fish0/version.rb +3 -0
- data/lib/fish0.rb +33 -0
- data/spec/dummy/README.rdoc +28 -0
- data/spec/dummy/Rakefile +6 -0
- data/spec/dummy/app/assets/javascripts/application.js +13 -0
- data/spec/dummy/app/assets/stylesheets/application.css +15 -0
- data/spec/dummy/app/controllers/application_controller.rb +5 -0
- data/spec/dummy/app/helpers/application_helper.rb +2 -0
- data/spec/dummy/app/models/article.rb +5 -0
- data/spec/dummy/app/models/article_id.rb +7 -0
- data/spec/dummy/app/views/layouts/application.html.erb +13 -0
- data/spec/dummy/bin/bundle +3 -0
- data/spec/dummy/bin/rails +4 -0
- data/spec/dummy/bin/rake +4 -0
- data/spec/dummy/bin/setup +29 -0
- data/spec/dummy/config/application.rb +29 -0
- data/spec/dummy/config/boot.rb +5 -0
- data/spec/dummy/config/environment.rb +5 -0
- data/spec/dummy/config/environments/development.rb +24 -0
- data/spec/dummy/config/environments/production.rb +76 -0
- data/spec/dummy/config/environments/test.rb +42 -0
- data/spec/dummy/config/initializers/backtrace_silencers.rb +7 -0
- data/spec/dummy/config/initializers/cookies_serializer.rb +3 -0
- data/spec/dummy/config/initializers/filter_parameter_logging.rb +4 -0
- data/spec/dummy/config/initializers/inflections.rb +16 -0
- data/spec/dummy/config/initializers/mime_types.rb +4 -0
- data/spec/dummy/config/initializers/session_store.rb +3 -0
- data/spec/dummy/config/initializers/wrap_parameters.rb +9 -0
- data/spec/dummy/config/locales/en.yml +23 -0
- data/spec/dummy/config/mongo.yml +7 -0
- data/spec/dummy/config/routes.rb +56 -0
- data/spec/dummy/config/secrets.yml +22 -0
- data/spec/dummy/config.ru +4 -0
- data/spec/dummy/public/404.html +67 -0
- data/spec/dummy/public/422.html +67 -0
- data/spec/dummy/public/500.html +66 -0
- data/spec/dummy/public/favicon.ico +0 -0
- data/spec/factories/article.rb +6 -0
- data/spec/lib/repository_spec.rb +24 -0
- data/spec/models/model_spec.rb +13 -0
- data/spec/spec_helper.rb +19 -0
- data/spec/support/factory_girl.rb +3 -0
- data/spec/support/mongo_cleaner.rb +5 -0
- data/spec/support/update_behaviour.rb +7 -0
- metadata +287 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 3dd996ed9f7ffef651e15e2e1ccd9c308aabd0e05b488c5c3e706616b4c2656a
|
4
|
+
data.tar.gz: 799b77b1d84372b044b5cd6f338babc3a0e0140e5fb8252e855084a9a18c8db1
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 9a97c20d84d27cb4192397852df15fe60d73fdba2727314e5a99e3a008c316467cec5b2b24e94d72b5338e1e94665ed8c61f430302234dbf660c8aacf3ec45cb
|
7
|
+
data.tar.gz: 607dc072a84459d44a8c7e4acae0294856e5dfc4ce7174ca7ad5757d673a3e58456c022c750427fe402edf5ddada91a4d9c05bc07c61ec56ec4fde107affec23
|
data/MIT-LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright 2016 Dmitry Zuev
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,174 @@
|
|
1
|
+
# Fish0
|
2
|
+
|
3
|
+
[![Build Status](https://api.travis-ci.org/rambler-digital-solutions/fish0.svg)](https://travis-ci.org/rambler-digital-solutions/fish0)
|
4
|
+
[![Code Climate](https://codeclimate.com/github/rambler-digital-solutions/fish0/badges/gpa.svg)](https://codeclimate.com/github/rambler-digital-solutions/fish0)
|
5
|
+
[![Gem Version](https://badge.fury.io/rb/fish0.svg)](https://badge.fury.io/rb/fish0)
|
6
|
+
|
7
|
+
> The fish doesn't think because the fish knows everything.
|
8
|
+
|
9
|
+
Fish0 is the plugin for read-only content websites with MongoDB storage. Works perfect with Rambler&Co CQRS architecture.
|
10
|
+
|
11
|
+
## Installation
|
12
|
+
|
13
|
+
Simply add gem to your `Gemfile`
|
14
|
+
|
15
|
+
````
|
16
|
+
gem 'fish0'
|
17
|
+
````
|
18
|
+
|
19
|
+
## Configuration
|
20
|
+
|
21
|
+
```ruby
|
22
|
+
# config/initializers/fish0.rb
|
23
|
+
|
24
|
+
Fish0::Configuration.configure do |config|
|
25
|
+
config.mongo_uri = 'mongodb://user:password@host_1:27017,replica_host_2:27017/project_db?auth_source=admin'
|
26
|
+
config.mongo_params = { read: { mode: :secondary } }
|
27
|
+
end
|
28
|
+
```
|
29
|
+
|
30
|
+
## Models
|
31
|
+
|
32
|
+
Inherit your model class from `Fish0::Model` and feel the power of the Fish!
|
33
|
+
|
34
|
+
With `attribute` define your attributes and with `primary_key` set your main primary key, e.g. `id`, `slug`, etc.
|
35
|
+
|
36
|
+
```ruby
|
37
|
+
# app/models/article.rb
|
38
|
+
class Article < Fish0::Model
|
39
|
+
# Define some attributes
|
40
|
+
attribute :headline, String
|
41
|
+
attribute :slug, String
|
42
|
+
attribute :content, Array[Hash]
|
43
|
+
attribute :published_at, DateTime
|
44
|
+
|
45
|
+
primary_key :slug
|
46
|
+
|
47
|
+
# ...
|
48
|
+
end
|
49
|
+
|
50
|
+
# app/controllers/articles_controller.rb
|
51
|
+
class ArticlesController < ApplicationController
|
52
|
+
# ...
|
53
|
+
|
54
|
+
def show
|
55
|
+
@article = Article.where(slug: params[:slug]).first
|
56
|
+
end
|
57
|
+
|
58
|
+
# ...
|
59
|
+
end
|
60
|
+
```
|
61
|
+
|
62
|
+
## Repository
|
63
|
+
|
64
|
+
### Basic repository usage
|
65
|
+
|
66
|
+
This code will get first article with `slug: 'content123'` from `articles` MongoDB collection, and return content with class `Article`.
|
67
|
+
|
68
|
+
```ruby
|
69
|
+
Fish0::Repository.new(:articles)
|
70
|
+
.where(slug: 'content123')
|
71
|
+
.first!
|
72
|
+
```
|
73
|
+
|
74
|
+
By default Fish0::Repository will coerce `:entity_class` from `:collection`, so you can skip this parameter.
|
75
|
+
|
76
|
+
### Writing your own repository
|
77
|
+
|
78
|
+
```ruby
|
79
|
+
# app/services/article_repository.rb
|
80
|
+
class ArticleRepository < Fish0::Repository
|
81
|
+
def initialize
|
82
|
+
super(:articles)
|
83
|
+
end
|
84
|
+
|
85
|
+
def published
|
86
|
+
where(visible: true, published_at: { '$lt': DateTime.now })
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
# app/controllers/articles_controller.rb
|
91
|
+
class ArticlesController < ApplicationController
|
92
|
+
# ...
|
93
|
+
|
94
|
+
def show
|
95
|
+
@article = Article.where(slug: params[:slug]).published.first!
|
96
|
+
end
|
97
|
+
|
98
|
+
# ...
|
99
|
+
end
|
100
|
+
```
|
101
|
+
|
102
|
+
## Pagination
|
103
|
+
|
104
|
+
```ruby
|
105
|
+
# app/controllers/articles_controller.rb
|
106
|
+
class ArticlesController < ApplicationController
|
107
|
+
include Fish0::Concerns::Paginatable
|
108
|
+
|
109
|
+
def index
|
110
|
+
@articles = paginate(Article.published)
|
111
|
+
end
|
112
|
+
|
113
|
+
# ...
|
114
|
+
|
115
|
+
protected
|
116
|
+
|
117
|
+
def per_page
|
118
|
+
31
|
119
|
+
end
|
120
|
+
|
121
|
+
# ...
|
122
|
+
end
|
123
|
+
```
|
124
|
+
|
125
|
+
## ViewModel
|
126
|
+
|
127
|
+
ViewModel concern wraps Virtus around your models. It also adds `#to_partial_path` and `#type` methods. Method `#to_partial_path` helps render your models via `render` helper.
|
128
|
+
|
129
|
+
|
130
|
+
```ruby
|
131
|
+
# app/models/article.rb
|
132
|
+
class Article
|
133
|
+
include Fish0::Concerns::ViewModel
|
134
|
+
|
135
|
+
attribute :headline, String
|
136
|
+
attribute :slug, String
|
137
|
+
attribute :content, Array[Hash]
|
138
|
+
attribute :published_at, DateTime
|
139
|
+
|
140
|
+
# ...
|
141
|
+
end
|
142
|
+
```
|
143
|
+
|
144
|
+
## Cacheable
|
145
|
+
|
146
|
+
If you want your models to support `#cache_key` method and use Rails caching, you should include Fish0::Concerns::Cacheable to such models.
|
147
|
+
|
148
|
+
Your model should respond to `:updated_at` with DateTime object.
|
149
|
+
|
150
|
+
```ruby
|
151
|
+
# app/models/article.rb
|
152
|
+
class Article
|
153
|
+
# ...
|
154
|
+
cacheable
|
155
|
+
|
156
|
+
# ...
|
157
|
+
end
|
158
|
+
|
159
|
+
# app/controllers/articles_controller.rb
|
160
|
+
class ArticlesController < ApplicationController
|
161
|
+
# ...
|
162
|
+
|
163
|
+
def show
|
164
|
+
@article = Article.where(slug: params[:slug]).first!
|
165
|
+
if stale?(@article)
|
166
|
+
respond_to do |format|
|
167
|
+
format.html
|
168
|
+
end
|
169
|
+
end
|
170
|
+
end
|
171
|
+
|
172
|
+
# ...
|
173
|
+
end
|
174
|
+
```
|
data/Rakefile
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
#!/usr/bin/env rake
|
2
|
+
|
3
|
+
begin
|
4
|
+
require 'bundler/setup'
|
5
|
+
rescue LoadError
|
6
|
+
puts 'You must `gem install bundler` and `bundle install` to run rake tasks'
|
7
|
+
end
|
8
|
+
|
9
|
+
APP_RAKEFILE = File.expand_path('../spec/dummy/Rakefile', __FILE__)
|
10
|
+
load 'rails/tasks/engine.rake'
|
11
|
+
|
12
|
+
Bundler::GemHelper.install_tasks
|
13
|
+
|
14
|
+
Dir[File.join(File.dirname(__FILE__), 'tasks/**/*.rake')].each { |f| load f }
|
15
|
+
|
16
|
+
require 'rspec/core'
|
17
|
+
require 'rspec/core/rake_task'
|
18
|
+
|
19
|
+
desc 'Run all specs in spec directory (excluding plugin specs)'
|
20
|
+
RSpec::Core::RakeTask.new(:spec)
|
21
|
+
|
22
|
+
task default: :spec
|
@@ -0,0 +1,21 @@
|
|
1
|
+
module Fish0
|
2
|
+
class Collection < Array
|
3
|
+
def cache_key
|
4
|
+
most_recent = select(&:updated_at).sort_by(&:updated_at).last
|
5
|
+
|
6
|
+
timestamp = time_to_string(most_recent ? most_recent.updated_at : Time.zone.now)
|
7
|
+
|
8
|
+
"#{objects_key}-#{timestamp}"
|
9
|
+
end
|
10
|
+
|
11
|
+
protected
|
12
|
+
|
13
|
+
def objects_key
|
14
|
+
Digest::MD5.hexdigest(map(&:primary_key_value).join)
|
15
|
+
end
|
16
|
+
|
17
|
+
def time_to_string(timestamp)
|
18
|
+
timestamp.strftime('%Y%m%d%H%M%S%9N')
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,90 @@
|
|
1
|
+
module Fish0
|
2
|
+
module Concerns
|
3
|
+
module Base
|
4
|
+
extend ActiveSupport::Concern
|
5
|
+
|
6
|
+
def primary_key
|
7
|
+
self.class.primary_key
|
8
|
+
end
|
9
|
+
|
10
|
+
def primary_key_value
|
11
|
+
send(primary_key)
|
12
|
+
end
|
13
|
+
|
14
|
+
included do
|
15
|
+
class << self
|
16
|
+
def primary_key(val = @primary_key)
|
17
|
+
@primary_key = val
|
18
|
+
return default_primary_key unless @primary_key
|
19
|
+
@primary_key
|
20
|
+
end
|
21
|
+
|
22
|
+
def cacheable
|
23
|
+
include Concerns::Cacheable
|
24
|
+
end
|
25
|
+
|
26
|
+
def disable_coercion
|
27
|
+
include Virtus.model(coerce: false)
|
28
|
+
end
|
29
|
+
alias_method :skip_coercion, :disable_coercion # DEPRECATED
|
30
|
+
|
31
|
+
def enable_coercion
|
32
|
+
include Virtus.model(coerce: true)
|
33
|
+
end
|
34
|
+
|
35
|
+
def scope(name, body)
|
36
|
+
scopes << [name, body]
|
37
|
+
end
|
38
|
+
|
39
|
+
def scopes
|
40
|
+
@scopes ||= []
|
41
|
+
end
|
42
|
+
|
43
|
+
# rubocop:disable Style/TrivialAccessors
|
44
|
+
def default_scope(body)
|
45
|
+
@default_scope = body
|
46
|
+
end
|
47
|
+
# rubocop:enable Style/TrivialAccessors
|
48
|
+
|
49
|
+
def method_missing(method_name, *arguments, &block)
|
50
|
+
if repository.respond_to?(method_name)
|
51
|
+
repository.send(method_name, *arguments, &block)
|
52
|
+
else
|
53
|
+
super
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
def respond_to_missing?(method_name, include_private = false)
|
58
|
+
repository.respond_to?(method_name) || super
|
59
|
+
end
|
60
|
+
|
61
|
+
protected
|
62
|
+
|
63
|
+
def default_primary_key
|
64
|
+
:slug
|
65
|
+
end
|
66
|
+
|
67
|
+
def entity
|
68
|
+
self
|
69
|
+
end
|
70
|
+
|
71
|
+
def collection
|
72
|
+
model_name.collection
|
73
|
+
end
|
74
|
+
|
75
|
+
def repository
|
76
|
+
rep = repository_class.new(collection: collection, entity_class: entity)
|
77
|
+
rep.instance_exec(&@default_scope) if @default_scope
|
78
|
+
scopes.each { |s| rep.scope(*s) }
|
79
|
+
rep
|
80
|
+
end
|
81
|
+
|
82
|
+
def repository_class
|
83
|
+
return "#{entity}Repository".constantize if "#{entity}Repository".safe_constantize
|
84
|
+
Fish0::Repository
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
module Fish0
|
2
|
+
module Concerns
|
3
|
+
module Cacheable
|
4
|
+
extend ActiveSupport::Concern
|
5
|
+
|
6
|
+
included do
|
7
|
+
def cache_key(*timestamp_names)
|
8
|
+
if timestamp_names.any?
|
9
|
+
cache_key_string(max_updated_column_timestamp(timestamp_names))
|
10
|
+
elsif (timestamp = max_updated_column_timestamp)
|
11
|
+
cache_key_string(timestamp)
|
12
|
+
else
|
13
|
+
"#{self.class.to_s.tableize}/#{primary_key_value}"
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
private
|
18
|
+
|
19
|
+
def timestamp_attributes_for_update
|
20
|
+
[:updated_at]
|
21
|
+
end
|
22
|
+
|
23
|
+
def max_updated_column_timestamp(timestamp_names = timestamp_attributes_for_update)
|
24
|
+
timestamp_names
|
25
|
+
.map { |attr| self[attr] }
|
26
|
+
.compact
|
27
|
+
.map(&:to_time)
|
28
|
+
.max
|
29
|
+
end
|
30
|
+
|
31
|
+
def cache_key_string(timestamp)
|
32
|
+
"#{self.class.to_s.tableize}/#{primary_key_value}-#{timestamp.utc.to_s(:nsec)}"
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
module Fish0
|
2
|
+
module Concerns
|
3
|
+
module Paginatable
|
4
|
+
extend ActiveSupport::Concern
|
5
|
+
|
6
|
+
included do
|
7
|
+
helper_method :page
|
8
|
+
|
9
|
+
protected
|
10
|
+
|
11
|
+
def page
|
12
|
+
@page ||= (params[:page].to_i || 1)
|
13
|
+
end
|
14
|
+
|
15
|
+
def paginate(collection)
|
16
|
+
Fish0::Paginator.new(collection, page_number: page, per_page: per_page).to_collection
|
17
|
+
end
|
18
|
+
|
19
|
+
def per_page
|
20
|
+
22
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
module Fish0
|
2
|
+
module Concerns
|
3
|
+
module ViewModel
|
4
|
+
extend ActiveSupport::Concern
|
5
|
+
|
6
|
+
included do
|
7
|
+
include Virtus.model
|
8
|
+
attribute :type, String
|
9
|
+
|
10
|
+
def type
|
11
|
+
(super || '').demodulize.underscore
|
12
|
+
end
|
13
|
+
|
14
|
+
def to_partial_path
|
15
|
+
type.to_s
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
module Fish0
|
2
|
+
##
|
3
|
+
# Fish0::Configuration
|
4
|
+
#
|
5
|
+
# Usage:
|
6
|
+
# # config/initializers/fish0.rb
|
7
|
+
# Fish0::Configuration.configure do |config|
|
8
|
+
# config.mongo_uri = 'mongodb://user:password@host_1:27017,replica_host_2:27017/'\
|
9
|
+
# 'fish0_development?auth_source=admin'
|
10
|
+
# config.mongo_params = { read: { mode: :secondary } }
|
11
|
+
# end
|
12
|
+
#
|
13
|
+
|
14
|
+
class Configuration
|
15
|
+
include ActiveSupport::Configurable
|
16
|
+
|
17
|
+
# Usage:
|
18
|
+
# Enter your full mongo_uri (can include username, password, multiple hosts, ports, db name,
|
19
|
+
# and additional options as get-parameters
|
20
|
+
config_accessor :mongo_uri do
|
21
|
+
'mongodb://localhost:27017/fish0_development'
|
22
|
+
end
|
23
|
+
|
24
|
+
# Usage:
|
25
|
+
# Enter your as second argument to MongoClient::New
|
26
|
+
config_accessor :mongo_params do
|
27
|
+
{ read: { mode: :secondary } }
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
data/lib/fish0/engine.rb
ADDED
@@ -0,0 +1,12 @@
|
|
1
|
+
module Fish0
|
2
|
+
class Engine < ::Rails::Engine
|
3
|
+
isolate_namespace Fish0
|
4
|
+
|
5
|
+
config.generators do |g|
|
6
|
+
g.test_framework :rspec, fixture: false
|
7
|
+
g.fixture_replacement :factory_girl, dir: 'spec/factories'
|
8
|
+
g.assets false
|
9
|
+
g.helper false
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
data/lib/fish0/model.rb
ADDED
@@ -0,0 +1,55 @@
|
|
1
|
+
module Fish0
|
2
|
+
class Paginator
|
3
|
+
include Enumerable
|
4
|
+
delegate :each, :to_collection, :skip, :limit, :padding, :fetch, to: :collection
|
5
|
+
|
6
|
+
attr_reader :collection
|
7
|
+
|
8
|
+
def initialize(collection, page_number: 1, per_page: 22, padding: 0)
|
9
|
+
raise ArgumentError,
|
10
|
+
'you can paginate only Fish0::Repository' unless collection.is_a?(Fish0::Repository)
|
11
|
+
@collection = collection
|
12
|
+
@per = per_page
|
13
|
+
@page = page_number
|
14
|
+
@padding = padding
|
15
|
+
per(per_page)
|
16
|
+
page(page_number)
|
17
|
+
end
|
18
|
+
|
19
|
+
def page(value = @page)
|
20
|
+
@page = value
|
21
|
+
skip((@page - 1) * @per + @padding)
|
22
|
+
self
|
23
|
+
end
|
24
|
+
|
25
|
+
def per(value = @per)
|
26
|
+
@per = value
|
27
|
+
skip((@page - 1) * @per + @padding)
|
28
|
+
limit(@per)
|
29
|
+
self
|
30
|
+
end
|
31
|
+
|
32
|
+
def padding(value)
|
33
|
+
@padding = value
|
34
|
+
page
|
35
|
+
skip
|
36
|
+
self
|
37
|
+
end
|
38
|
+
|
39
|
+
def total
|
40
|
+
collection.count
|
41
|
+
end
|
42
|
+
|
43
|
+
def current_page
|
44
|
+
@page
|
45
|
+
end
|
46
|
+
|
47
|
+
def pages
|
48
|
+
(total.to_f / @per).ceil
|
49
|
+
end
|
50
|
+
|
51
|
+
def last_page?
|
52
|
+
current_page >= pages
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
@@ -0,0 +1,125 @@
|
|
1
|
+
module Fish0
|
2
|
+
class Repository
|
3
|
+
attr_reader :source,
|
4
|
+
:collection,
|
5
|
+
:conditions,
|
6
|
+
:order,
|
7
|
+
:skip_quantity,
|
8
|
+
:limit_quantity,
|
9
|
+
:entity_class
|
10
|
+
|
11
|
+
include Enumerable
|
12
|
+
|
13
|
+
delegate :aggregate, to: :source
|
14
|
+
delegate :each, to: :to_collection
|
15
|
+
|
16
|
+
def initialize(collection:, entity_class: nil, source: nil)
|
17
|
+
raise ArgumentError, 'you should provide collection name' unless collection
|
18
|
+
@collection = collection
|
19
|
+
@source = source ? source[collection] : Fish0.mongo_reader[collection]
|
20
|
+
@conditions = default_conditions
|
21
|
+
@order = {}
|
22
|
+
@limit_quantity = 0
|
23
|
+
@skip_quantity = 0
|
24
|
+
@entity_class = entity_class || String(collection).singularize.camelize.constantize
|
25
|
+
end
|
26
|
+
|
27
|
+
def find_one(query)
|
28
|
+
where(query).first
|
29
|
+
end
|
30
|
+
|
31
|
+
def find_one!(query)
|
32
|
+
find_one(query) || raise(RecordNotFound, "can't find in #{collection} with #{conditions}")
|
33
|
+
end
|
34
|
+
|
35
|
+
def to_collection
|
36
|
+
Fish0::Collection.new(fetch.map(&to_entity))
|
37
|
+
end
|
38
|
+
|
39
|
+
def find(filter = nil, options = {})
|
40
|
+
@source.find filter.dup, options
|
41
|
+
end
|
42
|
+
|
43
|
+
def all
|
44
|
+
self
|
45
|
+
end
|
46
|
+
|
47
|
+
def projection(values)
|
48
|
+
@projection = values
|
49
|
+
self
|
50
|
+
end
|
51
|
+
|
52
|
+
def distinct(field)
|
53
|
+
@source.distinct field, @conditions
|
54
|
+
end
|
55
|
+
|
56
|
+
def first
|
57
|
+
element = fetch.limit(1).first
|
58
|
+
to_entity.call(element) if element
|
59
|
+
end
|
60
|
+
|
61
|
+
def first!
|
62
|
+
first || raise(RecordNotFound, "can't find in #{collection} with #{conditions}")
|
63
|
+
end
|
64
|
+
|
65
|
+
def where(query)
|
66
|
+
conditions.merge!(query)
|
67
|
+
self
|
68
|
+
end
|
69
|
+
|
70
|
+
def search(string)
|
71
|
+
where('$text' => { '$search' => string })
|
72
|
+
self
|
73
|
+
end
|
74
|
+
|
75
|
+
def order_by(query)
|
76
|
+
order.merge!(query)
|
77
|
+
self
|
78
|
+
end
|
79
|
+
|
80
|
+
def limit(value)
|
81
|
+
@limit_quantity = value
|
82
|
+
self
|
83
|
+
end
|
84
|
+
|
85
|
+
def skip(value)
|
86
|
+
@skip_quantity = value
|
87
|
+
self
|
88
|
+
end
|
89
|
+
|
90
|
+
def scope(name, body)
|
91
|
+
return if respond_to?(name)
|
92
|
+
|
93
|
+
unless body.respond_to?(:call)
|
94
|
+
raise ArgumentError, 'The scope body needs to be callable.'
|
95
|
+
end
|
96
|
+
|
97
|
+
define_singleton_method(name) do |*args|
|
98
|
+
instance_exec(*args, &body)
|
99
|
+
self
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
def fetch
|
104
|
+
scoped = find(conditions, sort: order)
|
105
|
+
scoped = scoped.projection(@projection) if @projection
|
106
|
+
scoped = scoped.skip(skip_quantity) if skip_quantity.positive?
|
107
|
+
scoped = scoped.limit(limit_quantity) if limit_quantity.positive?
|
108
|
+
scoped
|
109
|
+
end
|
110
|
+
|
111
|
+
def count
|
112
|
+
find(conditions).count
|
113
|
+
end
|
114
|
+
|
115
|
+
protected
|
116
|
+
|
117
|
+
def default_conditions
|
118
|
+
{}
|
119
|
+
end
|
120
|
+
|
121
|
+
def to_entity
|
122
|
+
-> (attrs) { entity_class.new(attrs) }
|
123
|
+
end
|
124
|
+
end
|
125
|
+
end
|