rectify 0.2.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +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
|
[](https://codeclimate.com/github/andypike/rectify)
|
4
|
+
[](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
|