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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: d7db41f0a655c68a1cc60f253b0bca46e049be1e
4
- data.tar.gz: 18cff71a5e52be32ec14c25df243b3aa1bdb4338
3
+ metadata.gz: e805f930c1178fd2d4b9b8672208711752d5c2ef
4
+ data.tar.gz: 1d53aeaaa143f2014c377d83db35b8c82f6adf0e
5
5
  SHA512:
6
- metadata.gz: c014786d12349c161f38d17d5249156fc206e21fa43da1061d4237f72778a17ca2190353eb99d6a004ae39a2a5abbe9a9bbede7c0aaff187aa8fd62c2f3f3f4d
7
- data.tar.gz: 16590d5c8313f1570612c1fc7d0308a0e3b848ed8cc8bb20c5afcb9ce073434abec5a68383c70f2e5896b51c77d5d242e62dcbfc583157655b23e79d03dfb1d5
6
+ metadata.gz: b74e8c3a073953f597603a1f248e7c2b7c17e82b917475c2de124f3a86f7ba7038cf8325198384e18be39e5186866b6e4bb36e8e06b914aa54820ca644abccf0
7
+ data.tar.gz: e27148d4f4c7122ae23032d9f868b39ea4f012980511856305d46d0f7244dafac27ec2b45a84828387f94eb8adb72b39a8c8e17bf7b5f6f4c3544c5e714b61dd
@@ -7,7 +7,7 @@ module Rectify
7
7
  def present(presenter, options = {})
8
8
  presenter_type = options.fetch(:for) { :template }
9
9
 
10
- presenter.for_controller(self)
10
+ presenter.attach_controller(self)
11
11
  rectify_presenters[presenter_type] = presenter
12
12
  end
13
13
 
@@ -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
@@ -0,0 +1,11 @@
1
+ module Rectify
2
+ class NullQuery < Query
3
+ def merge(query)
4
+ query
5
+ end
6
+
7
+ def query
8
+ []
9
+ end
10
+ end
11
+ end
@@ -2,8 +2,9 @@ module Rectify
2
2
  class Presenter
3
3
  include Virtus.model
4
4
 
5
- def for_controller(controller)
5
+ def attach_controller(controller)
6
6
  @controller = controller
7
+ self
7
8
  end
8
9
 
9
10
  def method_missing(method_name, *args, &block)
@@ -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,10 @@
1
+ module Rectify
2
+ module RSpec
3
+ module Helpers
4
+ def stub_query(query_class, options = {})
5
+ results = options.fetch(:results, [])
6
+ allow(query_class).to receive(:new) { StubQuery.new(results) }
7
+ end
8
+ end
9
+ end
10
+ 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,13 @@
1
+ module Rectify
2
+ module RSpec
3
+ class StubQuery < Query
4
+ def initialize(results)
5
+ @results = Array(results)
6
+ end
7
+
8
+ def query
9
+ @results
10
+ end
11
+ end
12
+ end
13
+ 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"
@@ -0,0 +1,7 @@
1
+ module Rectify
2
+ module SqlQuery
3
+ def query
4
+ model.find_by_sql([sql, params])
5
+ end
6
+ end
7
+ end
@@ -1,3 +1,3 @@
1
1
  module Rectify
2
- VERSION = "0.2.0"
2
+ VERSION = "0.3.0"
3
3
  end
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 access. The problem then
42
- becomes, how and where do you place validations and other business logic.
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, the Form Objects contain validations and represent the data input
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. Presenters contain the
47
- presentation logic in a way that is easily testable and keeps your views as
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 => Controller (redirecting, rendering, etc)
85
- Data Input => Form Object (validation, acceptable input)
86
- Business Logic => Command (logic for a specific use case)
87
- Data Access => Model (relationships, queries)
88
- View Logic => Presenter (formatting data)
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 presetation logic here:
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).for_controller(self)
538
+ @presenter = UserDetailsPresenter.new(:user => user).attach_controller(self)
530
539
  end
531
540
  end
532
541
  ```
533
542
 
534
- You need to call `#for_controller` and pass it a controller instance which will
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).for_controller(self)
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` folders and
676
- follow the Rails Way. Rectify suggests grouping your classes by feature rather
677
- than by pattern. For example, create a folder called `core` (this can be
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
- ## What's next?
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
- We stated above that the models should be responsible for data access. We
729
- may introduce a nice way to keep using the power of ActiveRecord but in a way
730
- where your models don't end up as a big ball of queries. We're thinking about
731
- Query Objects and a nice way to do this and we're also thinking about a nicer
732
- way to use raw SQL.
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.2.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-02-20 00:00:00.000000000 Z
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/save_command.rb
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
@@ -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