rectify 0.2.0 → 0.3.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/lib/rectify/controller_helpers.rb +1 -1
- data/lib/rectify/errors.rb +11 -0
- data/lib/rectify/null_query.rb +11 -0
- data/lib/rectify/presenter.rb +2 -1
- data/lib/rectify/query.rb +65 -0
- data/lib/rectify/rspec/database_reporter/display.rb +62 -0
- data/lib/rectify/rspec/database_reporter/query_info.rb +46 -0
- data/lib/rectify/rspec/database_reporter/query_stats.rb +51 -0
- data/lib/rectify/rspec/database_reporter/reporter.rb +59 -0
- data/lib/rectify/rspec/helpers.rb +10 -0
- data/lib/rectify/rspec/matchers.rb +30 -0
- data/lib/rectify/rspec/stub_form.rb +25 -0
- data/lib/rectify/rspec/stub_query.rb +13 -0
- data/lib/rectify/rspec.rb +9 -0
- data/lib/rectify/sql_query.rb +7 -0
- data/lib/rectify/version.rb +1 -1
- data/lib/rectify.rb +6 -1
- data/readme.md +319 -27
- metadata +43 -3
- data/lib/rectify/save_command.rb +0 -21
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: e805f930c1178fd2d4b9b8672208711752d5c2ef
|
4
|
+
data.tar.gz: 1d53aeaaa143f2014c377d83db35b8c82f6adf0e
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: b74e8c3a073953f597603a1f248e7c2b7c17e82b917475c2de124f3a86f7ba7038cf8325198384e18be39e5186866b6e4bb36e8e06b914aa54820ca644abccf0
|
7
|
+
data.tar.gz: e27148d4f4c7122ae23032d9f868b39ea4f012980511856305d46d0f7244dafac27ec2b45a84828387f94eb8adb72b39a8c8e17bf7b5f6f4c3544c5e714b61dd
|
@@ -0,0 +1,11 @@
|
|
1
|
+
module Rectify
|
2
|
+
class UnableToComposeQueries < StandardError
|
3
|
+
def initialize(query, other)
|
4
|
+
super(
|
5
|
+
"Unable to composite queries #{query.class.name} and " \
|
6
|
+
"#{other.class.name}. You cannot compose queries where #query " \
|
7
|
+
"returns an ActiveRecord::Relation in one and an array in the other."
|
8
|
+
)
|
9
|
+
end
|
10
|
+
end
|
11
|
+
end
|
data/lib/rectify/presenter.rb
CHANGED
@@ -0,0 +1,65 @@
|
|
1
|
+
module Rectify
|
2
|
+
class Query
|
3
|
+
def self.merge(*queries)
|
4
|
+
queries.reduce(NullQuery.new) { |a, e| a.merge(e) }
|
5
|
+
end
|
6
|
+
|
7
|
+
def initialize(scope = ActiveRecord::NullRelation)
|
8
|
+
@scope = scope
|
9
|
+
end
|
10
|
+
|
11
|
+
def query
|
12
|
+
@scope
|
13
|
+
end
|
14
|
+
|
15
|
+
def |(other)
|
16
|
+
if relation? && other.relation?
|
17
|
+
Rectify::Query.new(cached_query.merge(other.cached_query))
|
18
|
+
elsif eager? && other.eager?
|
19
|
+
Rectify::Query.new(cached_query | other.cached_query)
|
20
|
+
else
|
21
|
+
fail UnableToComposeQueries.new(self, other)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
alias_method :merge, :|
|
26
|
+
|
27
|
+
def count
|
28
|
+
cached_query.count
|
29
|
+
end
|
30
|
+
|
31
|
+
def first
|
32
|
+
cached_query.first
|
33
|
+
end
|
34
|
+
|
35
|
+
def each(&block)
|
36
|
+
cached_query.each(&block)
|
37
|
+
end
|
38
|
+
|
39
|
+
def exists?
|
40
|
+
return cached_query.exists? if relation?
|
41
|
+
|
42
|
+
cached_query.present?
|
43
|
+
end
|
44
|
+
|
45
|
+
def none?
|
46
|
+
!exists?
|
47
|
+
end
|
48
|
+
|
49
|
+
def to_a
|
50
|
+
cached_query.to_a
|
51
|
+
end
|
52
|
+
|
53
|
+
def relation?
|
54
|
+
cached_query.is_a?(ActiveRecord::Relation)
|
55
|
+
end
|
56
|
+
|
57
|
+
def eager?
|
58
|
+
cached_query.is_a?(Array)
|
59
|
+
end
|
60
|
+
|
61
|
+
def cached_query
|
62
|
+
@cached_query ||= query
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
@@ -0,0 +1,62 @@
|
|
1
|
+
module Rectify
|
2
|
+
module RSpec
|
3
|
+
class DatabaseReporter
|
4
|
+
class Display
|
5
|
+
def initialize(query_stats)
|
6
|
+
@query_stats = query_stats
|
7
|
+
end
|
8
|
+
|
9
|
+
def render
|
10
|
+
return if query_stats.empty?
|
11
|
+
|
12
|
+
header
|
13
|
+
rows
|
14
|
+
summary
|
15
|
+
end
|
16
|
+
|
17
|
+
private
|
18
|
+
|
19
|
+
attr_reader :query_stats
|
20
|
+
|
21
|
+
def target_length
|
22
|
+
query_stats.longest_target
|
23
|
+
end
|
24
|
+
|
25
|
+
def header
|
26
|
+
puts ""
|
27
|
+
puts headers
|
28
|
+
puts "-" * headers.length
|
29
|
+
end
|
30
|
+
|
31
|
+
def headers
|
32
|
+
target_header = "Target".ljust(target_length)
|
33
|
+
type_header = "Type".ljust(10)
|
34
|
+
queries_header = "Queries".rjust(7)
|
35
|
+
time_header = "Time (s)".rjust(7)
|
36
|
+
|
37
|
+
"#{target_header} | " \
|
38
|
+
"#{type_header} | " \
|
39
|
+
"#{queries_header} | " \
|
40
|
+
"#{time_header}"
|
41
|
+
end
|
42
|
+
|
43
|
+
def rows
|
44
|
+
query_stats.each do |target, type, count, time|
|
45
|
+
puts(
|
46
|
+
"#{target.ljust(target_length)} | " \
|
47
|
+
"#{type.to_s.ljust(10)} | " \
|
48
|
+
"#{count.to_s.rjust(7)} | " \
|
49
|
+
"#{time.to_s.rjust(7)}"
|
50
|
+
)
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
def summary
|
55
|
+
puts ""
|
56
|
+
puts "Database Queries: #{query_stats.total_queries} "\
|
57
|
+
"in #{query_stats.total_time}s"
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
module Rectify
|
2
|
+
module RSpec
|
3
|
+
class DatabaseReporter
|
4
|
+
class QueryInfo
|
5
|
+
def initialize(example, start, finish, query)
|
6
|
+
@example = example
|
7
|
+
@start = start
|
8
|
+
@finish = finish
|
9
|
+
@query = query
|
10
|
+
end
|
11
|
+
|
12
|
+
def target
|
13
|
+
return described_class.name if described_class
|
14
|
+
|
15
|
+
root_example_group_description
|
16
|
+
end
|
17
|
+
|
18
|
+
def time
|
19
|
+
finish.to_f - start.to_f
|
20
|
+
end
|
21
|
+
|
22
|
+
def type
|
23
|
+
return example.metadata[:type] unless described_class
|
24
|
+
|
25
|
+
described_class <= Rectify::Query ? :query : :unit
|
26
|
+
end
|
27
|
+
|
28
|
+
def ignore?
|
29
|
+
SQL_TO_IGNORE.match(query[:sql]) || example.blank?
|
30
|
+
end
|
31
|
+
|
32
|
+
private
|
33
|
+
|
34
|
+
attr_reader :example, :start, :finish, :query
|
35
|
+
|
36
|
+
def described_class
|
37
|
+
example.metadata[:described_class]
|
38
|
+
end
|
39
|
+
|
40
|
+
def root_example_group_description
|
41
|
+
example.example_group.parent_groups.last.description
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
module Rectify
|
2
|
+
module RSpec
|
3
|
+
class DatabaseReporter
|
4
|
+
class QueryStats
|
5
|
+
def initialize
|
6
|
+
@stats = Hash.new { |h, k| h[k] = [] }
|
7
|
+
end
|
8
|
+
|
9
|
+
def add(example, start, finish, query)
|
10
|
+
info = QueryInfo.new(example, start, finish, query)
|
11
|
+
return if info.ignore?
|
12
|
+
|
13
|
+
stats[info.target] << info
|
14
|
+
end
|
15
|
+
|
16
|
+
def each
|
17
|
+
stats.sort.each do |target, infos|
|
18
|
+
yield(
|
19
|
+
target,
|
20
|
+
infos.first.type,
|
21
|
+
infos.count,
|
22
|
+
infos.sum(&:time).round(5)
|
23
|
+
)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def total_queries
|
28
|
+
stats.values.flatten.count
|
29
|
+
end
|
30
|
+
|
31
|
+
def total_time
|
32
|
+
stats.values.flatten.sum(&:time).round(5)
|
33
|
+
end
|
34
|
+
|
35
|
+
def longest_target
|
36
|
+
return 0 if stats.empty?
|
37
|
+
|
38
|
+
stats.keys.max_by(&:length).length
|
39
|
+
end
|
40
|
+
|
41
|
+
def empty?
|
42
|
+
stats.empty?
|
43
|
+
end
|
44
|
+
|
45
|
+
private
|
46
|
+
|
47
|
+
attr_reader :stats
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
@@ -0,0 +1,59 @@
|
|
1
|
+
module Rectify
|
2
|
+
module RSpec
|
3
|
+
class DatabaseReporter
|
4
|
+
SQL_TO_IGNORE = /
|
5
|
+
pg_table|
|
6
|
+
pg_attribute|
|
7
|
+
pg_namespace|
|
8
|
+
current_database|
|
9
|
+
information_schema|
|
10
|
+
sqlite_master|
|
11
|
+
^TRUNCATE TABLE|
|
12
|
+
^ALTER TABLE|
|
13
|
+
^BEGIN|
|
14
|
+
^COMMIT|
|
15
|
+
^ROLLBACK|
|
16
|
+
^RELEASE|
|
17
|
+
^SAVEPOINT|
|
18
|
+
^SHOW|
|
19
|
+
^PRAGMA
|
20
|
+
/xi
|
21
|
+
|
22
|
+
def self.enable
|
23
|
+
::RSpec.configure do |config|
|
24
|
+
config.reporter.register_listener(
|
25
|
+
Reporter.new,
|
26
|
+
:start,
|
27
|
+
:example_started,
|
28
|
+
:start_dump
|
29
|
+
)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
class Reporter
|
34
|
+
def initialize
|
35
|
+
@query_stats = QueryStats.new
|
36
|
+
end
|
37
|
+
|
38
|
+
def start(_)
|
39
|
+
ActiveSupport::Notifications
|
40
|
+
.subscribe("sql.active_record") do |_, start, finish, _, query|
|
41
|
+
query_stats.add(current_example, start, finish, query)
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
def example_started(notification)
|
46
|
+
@current_example = notification.example
|
47
|
+
end
|
48
|
+
|
49
|
+
def start_dump(_)
|
50
|
+
Display.new(query_stats).render
|
51
|
+
end
|
52
|
+
|
53
|
+
private
|
54
|
+
|
55
|
+
attr_reader :query_stats, :current_example
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
require "rspec/expectations"
|
2
|
+
|
3
|
+
RSpec::Matchers.define :make_database_queries_of do |expected|
|
4
|
+
supports_block_expectations
|
5
|
+
|
6
|
+
queries = []
|
7
|
+
|
8
|
+
match do |proc|
|
9
|
+
ActiveSupport::Notifications
|
10
|
+
.subscribe("sql.active_record") do |_, _, _, _, query|
|
11
|
+
sql = query[:sql]
|
12
|
+
|
13
|
+
unless Rectify::RSpec::DatabaseReporter::SQL_TO_IGNORE.match(sql)
|
14
|
+
queries << sql
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
proc.call
|
19
|
+
|
20
|
+
queries.size == expected
|
21
|
+
end
|
22
|
+
|
23
|
+
failure_message do |_|
|
24
|
+
all_queries = queries.join("\n")
|
25
|
+
|
26
|
+
"expected the number of queries to be #{expected} " \
|
27
|
+
"but there were #{queries.size}.\n\n" \
|
28
|
+
"Here are the queries that were made:\n\n#{all_queries}"
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
module Rectify
|
2
|
+
class StubForm
|
3
|
+
attr_accessor :attributes
|
4
|
+
|
5
|
+
def initialize(attributes)
|
6
|
+
@attributes = attributes
|
7
|
+
end
|
8
|
+
|
9
|
+
def invalid?
|
10
|
+
!valid?
|
11
|
+
end
|
12
|
+
|
13
|
+
def method_missing(method_name, *args, &block)
|
14
|
+
if attributes.key?(method_name)
|
15
|
+
attributes[method_name]
|
16
|
+
else
|
17
|
+
super
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
def respond_to_missing?(method_name, _include_private = false)
|
22
|
+
attributes.key?(method_name)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,9 @@
|
|
1
|
+
require "rectify/rspec/stub_query"
|
2
|
+
require "rectify/rspec/stub_form"
|
3
|
+
require "rectify/rspec/helpers"
|
4
|
+
require "rectify/rspec/matchers"
|
5
|
+
|
6
|
+
require "rectify/rspec/database_reporter/display"
|
7
|
+
require "rectify/rspec/database_reporter/query_info"
|
8
|
+
require "rectify/rspec/database_reporter/query_stats"
|
9
|
+
require "rectify/rspec/database_reporter/reporter"
|
data/lib/rectify/version.rb
CHANGED
data/lib/rectify.rb
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
require "bundler/setup"
|
2
|
+
|
1
3
|
require "virtus"
|
2
4
|
require "wisper"
|
3
5
|
require "active_support/core_ext/hash"
|
@@ -7,6 +9,9 @@ require "active_record"
|
|
7
9
|
require "rectify/version"
|
8
10
|
require "rectify/form"
|
9
11
|
require "rectify/command"
|
10
|
-
require "rectify/save_command"
|
11
12
|
require "rectify/presenter"
|
13
|
+
require "rectify/query"
|
14
|
+
require "rectify/null_query"
|
15
|
+
require "rectify/sql_query"
|
12
16
|
require "rectify/controller_helpers"
|
17
|
+
require "rectify/errors"
|
data/readme.md
CHANGED
@@ -1,6 +1,7 @@
|
|
1
1
|
# Rectify
|
2
2
|
|
3
3
|
[![Code Climate](https://codeclimate.com/github/andypike/rectify/badges/gpa.svg)](https://codeclimate.com/github/andypike/rectify)
|
4
|
+
[![Build Status](https://travis-ci.org/andypike/rectify.svg?branch=master)](https://travis-ci.org/andypike/rectify)
|
4
5
|
|
5
6
|
Rectify is a gem that provides some lightweight classes that will make it easier
|
6
7
|
to build Rails applications in a more maintainable way. It's built on top of
|
@@ -28,6 +29,7 @@ Currently, Rectify consists of the following concepts:
|
|
28
29
|
* [Form Objects](#form-objects)
|
29
30
|
* [Commands](#commands)
|
30
31
|
* [Presenters](#presenters)
|
32
|
+
* [Query Objects](#query-objects)
|
31
33
|
|
32
34
|
You can use these separately or together to improve the structure of your Rails
|
33
35
|
applications.
|
@@ -38,15 +40,21 @@ views are filled with too much logic. The opinion of Rectify is that these
|
|
38
40
|
places are incorrect and that your models in particular are doing too much.
|
39
41
|
|
40
42
|
Rectify's opinion is that controllers should just be concerned with HTTP related
|
41
|
-
things and models should just be concerned with data
|
42
|
-
becomes, how and where do you place validations and other business
|
43
|
+
things and models should just be concerned with data relationships. The problem
|
44
|
+
then becomes, how and where do you place validations, queries and other business
|
45
|
+
logic?
|
43
46
|
|
44
|
-
Using Rectify,
|
47
|
+
Using Rectify, Form Objects contain validations and represent the data input
|
45
48
|
of your system. Commands then take a Form Object (as well as other data) and
|
46
|
-
perform a single action which is invoked by a controller.
|
47
|
-
|
49
|
+
perform a single action which is invoked by a controller. Query objects
|
50
|
+
encapsulate a single database query (and any logic it needs). Presenters contain
|
51
|
+
the presentation logic in a way that is easily testable and keeps your views as
|
48
52
|
clean as possible.
|
49
53
|
|
54
|
+
Rectify is designed to be very lightweight and allows you to use some or all of
|
55
|
+
it's components. We also advise to use these components where they make sense
|
56
|
+
not just blindly everywhere. More on that later.
|
57
|
+
|
50
58
|
Here's an example controller that shows details about a user and also allows a
|
51
59
|
user to register an account. This creates a user, sends some emails, does some
|
52
60
|
special auditing and integrates with a third party system:
|
@@ -81,11 +89,12 @@ business logic of registering a new account. The controller is clean and
|
|
81
89
|
business logic now has a natural home:
|
82
90
|
|
83
91
|
```
|
84
|
-
HTTP
|
85
|
-
Data Input
|
86
|
-
Business Logic
|
87
|
-
Data
|
88
|
-
|
92
|
+
HTTP => Controller (redirecting, rendering, etc)
|
93
|
+
Data Input => Form Object (validation, acceptable input)
|
94
|
+
Business Logic => Command (logic for a specific use case)
|
95
|
+
Data Persistence => Model (relationships between models)
|
96
|
+
Data Access => Query Object (database queries)
|
97
|
+
View Logic => Presenter (formatting data)
|
89
98
|
```
|
90
99
|
|
91
100
|
The next sections will give further details about using Form Objects, Commands
|
@@ -503,7 +512,7 @@ provide a more object oriented approach to the problem.
|
|
503
512
|
To create a Presenter just derive off of `Rectify::Presenter`, add attributes as
|
504
513
|
you do for Form Objects using [Virtus](https://github.com/solnic/virtus)
|
505
514
|
`attribute` declaration. Inside a Presenter you have access to all view helper
|
506
|
-
methods so it's easy to move the
|
515
|
+
methods so it's easy to move the presentation logic here:
|
507
516
|
|
508
517
|
```ruby
|
509
518
|
class UserDetailsPresenter < Rectify::Presenter
|
@@ -526,12 +535,12 @@ class UsersController < ApplicationController
|
|
526
535
|
def show
|
527
536
|
user = User.find(params[:id])
|
528
537
|
|
529
|
-
@presenter = UserDetailsPresenter.new(:user => user).
|
538
|
+
@presenter = UserDetailsPresenter.new(:user => user).attach_controller(self)
|
530
539
|
end
|
531
540
|
end
|
532
541
|
```
|
533
542
|
|
534
|
-
You need to call `#
|
543
|
+
You need to call `#attach_controller` and pass it a controller instance which will
|
535
544
|
allow it access to the view helpers. You can then use the Presenter in your
|
536
545
|
views as you would expect:
|
537
546
|
|
@@ -602,7 +611,7 @@ class UsersController < ApplicationController
|
|
602
611
|
def other_action
|
603
612
|
user = User.find(params[:id])
|
604
613
|
|
605
|
-
@presenter = UserDetailsPresenter.new(:user => user).
|
614
|
+
@presenter = UserDetailsPresenter.new(:user => user).attach_controller(self)
|
606
615
|
@presenter.user = User.first
|
607
616
|
end
|
608
617
|
end
|
@@ -669,13 +678,267 @@ user.each do |u|
|
|
669
678
|
end
|
670
679
|
```
|
671
680
|
|
681
|
+
## Query Objects
|
682
|
+
|
683
|
+
The final main component to Rectify is the Query Object. It's role is to
|
684
|
+
encapsulate a single database query and any logic that it query needs to
|
685
|
+
operate. It still uses ActiveRecord but adds some very light sugar on the top to
|
686
|
+
make this style of architecture easier. This helps to keep your model classes
|
687
|
+
lean and gives a natural home to this code.
|
688
|
+
|
689
|
+
To create a query object, you create a new class and derive off of
|
690
|
+
`Rectify::Query`. The only thing you need to do is to implement the
|
691
|
+
`#query` method and return an `ActiveRecord::Relation` object from it:
|
692
|
+
|
693
|
+
```ruby
|
694
|
+
class ActiveUsers < Rectify::Query
|
695
|
+
def query
|
696
|
+
User.where(:active => true)
|
697
|
+
end
|
698
|
+
end
|
699
|
+
```
|
700
|
+
|
701
|
+
To use this object, you just instantiate it and then use one of the following
|
702
|
+
methods to make use of it:
|
703
|
+
|
704
|
+
```ruby
|
705
|
+
ActiveUsers.new.count # => Returns the number of records
|
706
|
+
ActiveUsers.new.first # => Returns the first record
|
707
|
+
ActiveUsers.new.exists? # => Returns true if there are any records, else false
|
708
|
+
ActiveUsers.new.none? # => Returns true if there are no records, else false
|
709
|
+
ActiveUsers.new.to_a # => Execute the query and returns the resulting objects
|
710
|
+
ActiveUsers.new.each do |user| # => Iterates over each result
|
711
|
+
puts user.name
|
712
|
+
end
|
713
|
+
```
|
714
|
+
|
715
|
+
### Passing data to query objects
|
716
|
+
|
717
|
+
Passing data that your queries need to operate is best done via the constructor:
|
718
|
+
|
719
|
+
```ruby
|
720
|
+
class UsersOlderThan < Rectify::Query
|
721
|
+
def initialize(age)
|
722
|
+
@age = age
|
723
|
+
end
|
724
|
+
|
725
|
+
def query
|
726
|
+
User.where("age > ?", @age)
|
727
|
+
end
|
728
|
+
end
|
729
|
+
|
730
|
+
UsersOlderThan.new(25).count # => Returns the number of users over 25 years old
|
731
|
+
```
|
732
|
+
|
733
|
+
Sometimes your queries will need to do a little work with the provided data
|
734
|
+
before they can use it. Having your query encapsulated in an object makes this
|
735
|
+
easy and maintainable (here's a trivial example):
|
736
|
+
|
737
|
+
```ruby
|
738
|
+
class UsersWithBlacklistedEmail < Rectify::Query
|
739
|
+
def initialize(blacklist)
|
740
|
+
@blacklist = blacklist
|
741
|
+
end
|
742
|
+
|
743
|
+
def query
|
744
|
+
User.where(:email => blacklisted_emails)
|
745
|
+
end
|
746
|
+
|
747
|
+
private
|
748
|
+
|
749
|
+
def blacklisted_emails
|
750
|
+
@blacklist.map { |b| b.email.strip.downcase }
|
751
|
+
end
|
752
|
+
end
|
753
|
+
```
|
754
|
+
|
755
|
+
### Composition
|
756
|
+
|
757
|
+
One of this great features of ActiveRecord is the ability to easily compose
|
758
|
+
queries together in a simple way which helps reusability. Rectify Query Objects
|
759
|
+
can also be combined to created composed queries using the `|` operator as we
|
760
|
+
use in Ruby for Set Union. Here's how it looks:
|
761
|
+
|
762
|
+
```ruby
|
763
|
+
active_users_over_20 = ActiveUsers.new | UsersOlderThan.new(20)
|
764
|
+
|
765
|
+
active_users_over_20.count # => Returns number of active users over 20 years old
|
766
|
+
```
|
767
|
+
|
768
|
+
You can union many queries in this manner which will result in another
|
769
|
+
`Rectify::Query` object that you can use just like any other. This results in a
|
770
|
+
single database query.
|
771
|
+
|
772
|
+
As an alternative you can also use the `#merge` method which is simply an alias
|
773
|
+
of the `|` operator:
|
774
|
+
|
775
|
+
```ruby
|
776
|
+
active_users_over_20 = ActiveUsers.new.merge(UsersOlderThan.new(20))
|
777
|
+
|
778
|
+
active_users_over_20.count # => Returns number of active users over 20 years old
|
779
|
+
```
|
780
|
+
|
781
|
+
If you have a long list of queries to compose, Rectify also comes with the class
|
782
|
+
method `.merge` which takes multiple queries and combines them for you as if you
|
783
|
+
used `|` (or `#merge`) on them all:
|
784
|
+
|
785
|
+
```ruby
|
786
|
+
active_users_over_20 = Rectify::Query.merge(
|
787
|
+
ActiveUsers.new,
|
788
|
+
UsersOlderThan.new(20)
|
789
|
+
)
|
790
|
+
|
791
|
+
active_users_over_20.count # => Returns number of active users over 20 years old
|
792
|
+
```
|
793
|
+
|
794
|
+
If you don't those options for composing queries then the final option would be
|
795
|
+
to pass in a query object to another in it's constructor and use it as the base
|
796
|
+
scope:
|
797
|
+
|
798
|
+
```ruby
|
799
|
+
class UsersOlderThan < Rectify::Query
|
800
|
+
def initialize(age, scope = AllUsers.new)
|
801
|
+
@age = age
|
802
|
+
@scope = scope
|
803
|
+
end
|
804
|
+
|
805
|
+
def query
|
806
|
+
@scope.query.where("age > ?", @age)
|
807
|
+
end
|
808
|
+
end
|
809
|
+
|
810
|
+
UsersOlderThan.new(20, ActiveUsers.new).count
|
811
|
+
```
|
812
|
+
|
813
|
+
### Leveraging your database
|
814
|
+
|
815
|
+
Using `ActiveRecord::Relation` is a great way to construct your database queries
|
816
|
+
but sometimes you need to to use features of your database that aren't supported
|
817
|
+
by ActiveRecord directly. These are usually database specific and can greatly
|
818
|
+
improve your query efficiency. When that happens, you will need to write some
|
819
|
+
raw SQL. Rectify Query Objects allow for this. In addition to your `#query`
|
820
|
+
method returning an `ActiveRecord::Relation` you can also return an array of
|
821
|
+
objects. This means you can run raw SQL using
|
822
|
+
`ActiveRecord::Querying#find_by_sql`:
|
823
|
+
|
824
|
+
```ruby
|
825
|
+
class UsersOverUsingSql < Rectify::Query
|
826
|
+
def initialize(age)
|
827
|
+
@age = age
|
828
|
+
end
|
829
|
+
|
830
|
+
def query
|
831
|
+
User.find_by_sql([
|
832
|
+
"SELECT * FROM users WHERE age > :age ORDER BY age ASC", { :age => @age }
|
833
|
+
])
|
834
|
+
end
|
835
|
+
end
|
836
|
+
```
|
837
|
+
|
838
|
+
When you do this, the normal `Rectify::Query` methods are available but they
|
839
|
+
operate on the returned array rather than on the `ActiveRecord::Relation`. This
|
840
|
+
includes composition using the `|` operator but you can't compose an
|
841
|
+
`ActiveRecord::Relation` query object with one that returns an array of objects
|
842
|
+
from its `#query` method. You can compose two queries where both return arrays
|
843
|
+
but be aware that this will query the database for each query object and then
|
844
|
+
perform a Ruby array set union on the results. This might not be the most
|
845
|
+
efficient way to get the results so only use this when you are sure it's the
|
846
|
+
right thing to do.
|
847
|
+
|
848
|
+
The above example is fine for short SQL statements but if you are using raw SQL,
|
849
|
+
they will probably be much longer than a single line. Rectify provides a small
|
850
|
+
module that you can include to makes your query objects cleaner:
|
851
|
+
|
852
|
+
```ruby
|
853
|
+
class UsersOverUsingSql < Rectify::Query
|
854
|
+
include Rectify::SqlQuery
|
855
|
+
|
856
|
+
def initialize(age)
|
857
|
+
@age = age
|
858
|
+
end
|
859
|
+
|
860
|
+
def model
|
861
|
+
User
|
862
|
+
end
|
863
|
+
|
864
|
+
def sql
|
865
|
+
<<-SQL.strip_heredoc
|
866
|
+
SELECT *
|
867
|
+
FROM users
|
868
|
+
WHERE age > :age
|
869
|
+
ORDER BY age ASC
|
870
|
+
SQL
|
871
|
+
end
|
872
|
+
|
873
|
+
def params
|
874
|
+
{ :age => @age }
|
875
|
+
end
|
876
|
+
end
|
877
|
+
```
|
878
|
+
|
879
|
+
Just include `Rectify::SqlQuery` in your query object and then supply the a
|
880
|
+
`model` method that returns the model of the returned objects. A
|
881
|
+
`params` method that returns a hash containing named parameters that the SQL
|
882
|
+
statement requires. Lastly, you must supply a `sql` method that returns the raw
|
883
|
+
SQL. We recommend using a heredoc which makes the SQL much cleaner and easier
|
884
|
+
to read. Parameters use the ActiveRecord standard symbol notation as shown above
|
885
|
+
with the `:age` parameter.
|
886
|
+
|
887
|
+
### Stubbing Query Objects in tests
|
888
|
+
|
889
|
+
Now that you have your queries nicely encapsulated, it's now easier with a clear
|
890
|
+
division of responsibility to improve how you use the database within your
|
891
|
+
tests. You should unit test your Query Objects to ensure they return the correct
|
892
|
+
data from a know database state.
|
893
|
+
|
894
|
+
What you can now do it stub out these database calls when you use them in other
|
895
|
+
classes. This improves your test code in a couple of ways:
|
896
|
+
|
897
|
+
1. You need less database setup code within your tests. Normally you might use
|
898
|
+
something like factory_girl to create records in your database and then when
|
899
|
+
your tests run they query this set of data. Stubbing the queries within your
|
900
|
+
tests can reduce this complexity.
|
901
|
+
2. Fewer database queries running and less factory usage means that your tests
|
902
|
+
3. are doing less work and therefore will run a bit faster.
|
903
|
+
|
904
|
+
In Rectify, we provide the RSpec helper method `stub_query` that will make
|
905
|
+
stubbing Query Objects easy:
|
906
|
+
|
907
|
+
```ruby
|
908
|
+
# inside spec/rails_helper.rb
|
909
|
+
|
910
|
+
require "rectify/rspec"
|
911
|
+
|
912
|
+
RSpec.configure do |config|
|
913
|
+
# snip ...
|
914
|
+
|
915
|
+
config.include Rectify::RSpec::Helpers
|
916
|
+
end
|
917
|
+
|
918
|
+
# within a spec:
|
919
|
+
|
920
|
+
it "returns the number of users" do
|
921
|
+
stub_query(UsersOlderThan, :results => [User.new, User.new])
|
922
|
+
|
923
|
+
expect(subject.awesome_method).to eq(2)
|
924
|
+
end
|
925
|
+
```
|
926
|
+
|
927
|
+
As a convenience `:results` accepts either an array of objects or a single
|
928
|
+
instance:
|
929
|
+
|
930
|
+
```ruby
|
931
|
+
stub_query(UsersOlderThan, :results => [User.new, User.new])
|
932
|
+
stub_query(UsersOlderThan, :results => User.new)
|
933
|
+
```
|
934
|
+
|
672
935
|
## Where do I put my files?
|
673
936
|
|
674
|
-
The next inevitable question is "Where do I put my Forms, Commands and
|
675
|
-
Presenters?". You could create `forms`, `commands` and `presenters`
|
676
|
-
follow the Rails Way. Rectify suggests grouping your classes by
|
677
|
-
than by pattern. For example, create a folder called `core` (this
|
678
|
-
anything) and within that, create a folder for each broad feature of your
|
937
|
+
The next inevitable question is "Where do I put my Forms, Commands, Queries and
|
938
|
+
Presenters?". You could create `forms`, `commands`, `queries` and `presenters`
|
939
|
+
folders and follow the Rails Way. Rectify suggests grouping your classes by
|
940
|
+
feature rather than by pattern. For example, create a folder called `core` (this
|
941
|
+
can be anything) and within that, create a folder for each broad feature of your
|
679
942
|
application. Something like the following:
|
680
943
|
|
681
944
|
```
|
@@ -693,7 +956,8 @@ application. Something like the following:
|
|
693
956
|
```
|
694
957
|
|
695
958
|
Then you would place your classes in the appropriate feature folder. If you
|
696
|
-
follow this pattern remember to namespace your classes with a matching module
|
959
|
+
follow this pattern remember to namespace your classes with a matching module
|
960
|
+
which will allow Rails to load them:
|
697
961
|
|
698
962
|
```ruby
|
699
963
|
# in app/core/billing/send_invoice.rb
|
@@ -708,6 +972,25 @@ end
|
|
708
972
|
You don't need to alter your load path as everything in the `app` folder is
|
709
973
|
loaded automatically.
|
710
974
|
|
975
|
+
As stated above, if you prefer not to use this method of organizing your code
|
976
|
+
then that is totally fine. Just create folders under `app` for the things in
|
977
|
+
Rectify that you use:
|
978
|
+
|
979
|
+
```
|
980
|
+
.
|
981
|
+
└── app
|
982
|
+
├── commands
|
983
|
+
├── controllers
|
984
|
+
├── forms
|
985
|
+
├── models
|
986
|
+
├── presenters
|
987
|
+
├── queries
|
988
|
+
└── views
|
989
|
+
```
|
990
|
+
|
991
|
+
You don't need to make any configuration changes for your preferred folder
|
992
|
+
structure, just use whichever you feel most comfortable with.
|
993
|
+
|
711
994
|
## Trade offs
|
712
995
|
|
713
996
|
This style of Rails architecture is not a silver bullet for all projects. If
|
@@ -721,12 +1004,21 @@ whole system in your head. Personally I would prefer that as maintaining it will
|
|
721
1004
|
be easier as all code around a specific user task is on one place.
|
722
1005
|
|
723
1006
|
Before you use these methods in your project, consider the trade off and use
|
724
|
-
these strategies where they make sense for you and your project.
|
1007
|
+
these strategies where they make sense for you and your project. It maybe most
|
1008
|
+
pragmatic to use a mixture of the classic Rails Way and the Rectify approach
|
1009
|
+
depending on the complexity of different areas of your application.
|
1010
|
+
|
1011
|
+
## Developing Rectify
|
725
1012
|
|
726
|
-
|
1013
|
+
Some tests (specifically for Query objects) we need access to a database that
|
1014
|
+
ActiveRecord can connect to. We use SQLite for this at present. When you run the
|
1015
|
+
specs with `bundle exec rspec`, the database will be created for you.
|
727
1016
|
|
728
|
-
|
729
|
-
|
730
|
-
|
731
|
-
|
732
|
-
|
1017
|
+
There are some Rake tasks to help with the management of this test database
|
1018
|
+
using normal(ish) commands from Rails:
|
1019
|
+
|
1020
|
+
```
|
1021
|
+
rake db:migrate # => Migrates the test database
|
1022
|
+
rake db:schema # => Dumps database schema
|
1023
|
+
rake g:migration # => Create a new migration file
|
1024
|
+
```
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: rectify
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.3.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Andy Pike
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2016-
|
11
|
+
date: 2016-04-15 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: virtus
|
@@ -206,6 +206,34 @@ dependencies:
|
|
206
206
|
- - ">="
|
207
207
|
- !ruby/object:Gem::Version
|
208
208
|
version: 1.1.2
|
209
|
+
- !ruby/object:Gem::Dependency
|
210
|
+
name: sqlite3
|
211
|
+
requirement: !ruby/object:Gem::Requirement
|
212
|
+
requirements:
|
213
|
+
- - ">="
|
214
|
+
- !ruby/object:Gem::Version
|
215
|
+
version: '0'
|
216
|
+
type: :development
|
217
|
+
prerelease: false
|
218
|
+
version_requirements: !ruby/object:Gem::Requirement
|
219
|
+
requirements:
|
220
|
+
- - ">="
|
221
|
+
- !ruby/object:Gem::Version
|
222
|
+
version: '0'
|
223
|
+
- !ruby/object:Gem::Dependency
|
224
|
+
name: rake
|
225
|
+
requirement: !ruby/object:Gem::Requirement
|
226
|
+
requirements:
|
227
|
+
- - ">="
|
228
|
+
- !ruby/object:Gem::Version
|
229
|
+
version: '0'
|
230
|
+
type: :development
|
231
|
+
prerelease: false
|
232
|
+
version_requirements: !ruby/object:Gem::Requirement
|
233
|
+
requirements:
|
234
|
+
- - ">="
|
235
|
+
- !ruby/object:Gem::Version
|
236
|
+
version: '0'
|
209
237
|
description: Build Rails apps in a more maintainable way
|
210
238
|
email: andy@andypike.com
|
211
239
|
executables: []
|
@@ -216,9 +244,21 @@ files:
|
|
216
244
|
- lib/rectify.rb
|
217
245
|
- lib/rectify/command.rb
|
218
246
|
- lib/rectify/controller_helpers.rb
|
247
|
+
- lib/rectify/errors.rb
|
219
248
|
- lib/rectify/form.rb
|
249
|
+
- lib/rectify/null_query.rb
|
220
250
|
- lib/rectify/presenter.rb
|
221
|
-
- lib/rectify/
|
251
|
+
- lib/rectify/query.rb
|
252
|
+
- lib/rectify/rspec.rb
|
253
|
+
- lib/rectify/rspec/database_reporter/display.rb
|
254
|
+
- lib/rectify/rspec/database_reporter/query_info.rb
|
255
|
+
- lib/rectify/rspec/database_reporter/query_stats.rb
|
256
|
+
- lib/rectify/rspec/database_reporter/reporter.rb
|
257
|
+
- lib/rectify/rspec/helpers.rb
|
258
|
+
- lib/rectify/rspec/matchers.rb
|
259
|
+
- lib/rectify/rspec/stub_form.rb
|
260
|
+
- lib/rectify/rspec/stub_query.rb
|
261
|
+
- lib/rectify/sql_query.rb
|
222
262
|
- lib/rectify/version.rb
|
223
263
|
- readme.md
|
224
264
|
homepage: https://github.com/andypike/rectify
|
data/lib/rectify/save_command.rb
DELETED
@@ -1,21 +0,0 @@
|
|
1
|
-
module Rectify
|
2
|
-
class SaveCommand < Command
|
3
|
-
def initialize(form, model)
|
4
|
-
@form = form
|
5
|
-
@model = model
|
6
|
-
end
|
7
|
-
|
8
|
-
def call
|
9
|
-
return broadcast(:invalid) unless form.valid?
|
10
|
-
|
11
|
-
model.attributes = form.attributes
|
12
|
-
model.save!
|
13
|
-
|
14
|
-
broadcast(:ok)
|
15
|
-
end
|
16
|
-
|
17
|
-
private
|
18
|
-
|
19
|
-
attr_reader :form, :model
|
20
|
-
end
|
21
|
-
end
|