rectify 0.0.1 → 0.0.2

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 60744b875fac9607cce596dab61c9a5cedcbe5d7
4
- data.tar.gz: 32e93aa6eb8bb655b72e729cae000c5488eb4424
3
+ metadata.gz: b0c393c5fdb2019cf7d46a8f71723f53dc5bac79
4
+ data.tar.gz: cfe199b4e00480e6b0f208d9a03004fd86455569
5
5
  SHA512:
6
- metadata.gz: 5350eb7555707bf27d979e8031fb444d041d859e1485eaa241b1e2f05015676a48d9e31c89a42e966398cc55c6794130591c518218f2579aa59f72157ecd55ec
7
- data.tar.gz: a930de694298dff402f41fd3a6e91856a0b8a50fde8a7d870e8fede605bda68e42509b5f2a8b2b694a4eb73530c61aaee46039910b8685a1c1ec850c634e8f2c
6
+ metadata.gz: 6b482e2f24f15a60441017fe4783b78ba604ff496284bcd380cf00528c4585f305d01c6829e47cfc76f3ef88204af1637c8de4d5eb87e8bde53822dacb705f8e
7
+ data.tar.gz: 7192fdc7913ba5142f382150ca45fe4cc7cc560e215440a1e110d4e1438c9632aed0ce87075a19da987abc634c465155d054d9ce7e625171849325aae1086ece
@@ -0,0 +1,38 @@
1
+ module Rectify
2
+ class Command
3
+ include Wisper::Publisher
4
+
5
+ def self.call(*args, &block)
6
+ command = new(*args)
7
+ command.evaluate(&block) if block_given?
8
+ command.call
9
+ end
10
+
11
+ def evaluate(&block)
12
+ @caller = eval("self", block.binding)
13
+ instance_eval(&block)
14
+ end
15
+
16
+ def expose(instance_variables)
17
+ instance_variables.each do |name, value|
18
+ @caller.instance_variable_set("@#{name}", value)
19
+ end
20
+ end
21
+
22
+ def transaction(&block)
23
+ ActiveRecord::Base.transaction(&block) if block_given?
24
+ end
25
+
26
+ def method_missing(method_name, *args, &block)
27
+ if @caller.respond_to?(method_name)
28
+ @caller.send(method_name, *args, &block)
29
+ else
30
+ super
31
+ end
32
+ end
33
+
34
+ def respond_to_missing?(method_name, include_private = false)
35
+ @caller.respond_to?(method_name, include_private)
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,80 @@
1
+ module Rectify
2
+ class Form
3
+ include Virtus.model
4
+ include ActiveModel::Validations
5
+
6
+ attribute :id, Integer
7
+
8
+ def self.from_params(params, additional_params = {})
9
+ params = params.with_indifferent_access
10
+ attributes = params.fetch(mimicked_model_name, {}).merge(additional_params)
11
+
12
+ new(attributes).tap do |f|
13
+ f.id = params[:id]
14
+ end
15
+ end
16
+
17
+ def self.from_model(model)
18
+ new(model.attributes)
19
+ end
20
+
21
+ def self.mimic(model_name)
22
+ @model_name = model_name.to_s.underscore.to_sym
23
+ end
24
+
25
+ def self.mimicked_model_name
26
+ @model_name || infer_model_name
27
+ end
28
+
29
+ def self.infer_model_name
30
+ class_name = name.split("::").last
31
+ return :form if class_name == "Form"
32
+
33
+ class_name.chomp("Form").underscore.to_sym
34
+ end
35
+
36
+ def self.model_name
37
+ ActiveModel::Name.new(self, nil, mimicked_model_name.to_s.camelize)
38
+ end
39
+
40
+ def persisted?
41
+ id.present? && id.to_i > 0
42
+ end
43
+
44
+ def valid?
45
+ [super, form_attributes_valid?, arrays_attributes_valid?].all?
46
+ end
47
+
48
+ def to_key
49
+ [id]
50
+ end
51
+
52
+ def to_model
53
+ self
54
+ end
55
+
56
+ def attributes
57
+ super.except(:id)
58
+ end
59
+
60
+ private
61
+
62
+ def form_attributes_valid?
63
+ attributes
64
+ .each_value
65
+ .select { |f| f.respond_to?(:valid?) }
66
+ .map(&:valid?)
67
+ .all?
68
+ end
69
+
70
+ def arrays_attributes_valid?
71
+ attributes
72
+ .each_value
73
+ .select { |a| a.is_a?(Array) }
74
+ .flatten
75
+ .select { |f| f.respond_to?(:valid?) }
76
+ .map(&:valid?)
77
+ .all?
78
+ end
79
+ end
80
+ end
@@ -0,0 +1,21 @@
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
@@ -1,3 +1,3 @@
1
1
  module Rectify
2
- VERSION = "0.0.1"
2
+ VERSION = "0.0.2"
3
3
  end
data/lib/rectify.rb CHANGED
@@ -1,5 +1,10 @@
1
- class Rectify
2
- def self.hi
3
- puts "Hello world!"
4
- end
5
- end
1
+ require "virtus"
2
+ require "wisper"
3
+ require "active_support/core_ext/hash"
4
+ require "active_model"
5
+ require "active_record"
6
+
7
+ require "rectify/version"
8
+ require "rectify/form"
9
+ require "rectify/command"
10
+ require "rectify/save_command"
data/readme.md CHANGED
@@ -1 +1,265 @@
1
1
  # Rectify
2
+
3
+ [![Code Climate](https://codeclimate.com/github/andypike/rectify/badges/gpa.svg)](https://codeclimate.com/github/andypike/rectify)
4
+
5
+ Rectify is a gem that provides some lightweight classes that will make it easier
6
+ to build Rails applications in a more maintainable way. It's built on top of
7
+ several other gems and adds improved APIs to make things easier.
8
+
9
+ Rectify is an extraction from a number of projects that I've worked on and used
10
+ these techniques.
11
+
12
+ To install, add it to your `Gemfile`:
13
+
14
+ ```
15
+ gem "rectify"
16
+ ```
17
+
18
+ Then use Bundler to install it:
19
+
20
+ ```
21
+ bundle install
22
+ ```
23
+
24
+ ## Overview
25
+
26
+ Currently, Rectify consists of two main concepts: Form Objects and Commands. You
27
+ can use these separately or together to improve the structure of your Rails
28
+ applications.
29
+
30
+ The main problem that Rectify tries to solve is where your logic should go. Commonly,
31
+ business logic is either placed in the controller or the model. The opinion of Rectify
32
+ is that both of these places are incorrect and that your models in particular are
33
+ doing too much.
34
+
35
+ Rectify's opinion is that controllers should just be concerned with HTTP related
36
+ things and models should just be concerned with data access. The problem then
37
+ becomes, how and where do you place validations and other business logic.
38
+
39
+ Using Rectify, the Form Objects contain validations and represent the data input
40
+ of your system. Commands then take a Form Object (as well as other data) and
41
+ perform a single action which is invoked by a controller.
42
+
43
+ Here's an example when a user registers an account. This creates a user, sends
44
+ some emails, does some special auditing and integrates with a third party system:
45
+
46
+ ```ruby
47
+ class UserController < ApplicationController
48
+ def new
49
+ @form = RegistrationForm.new
50
+ end
51
+
52
+ def create
53
+ @form = RegistrationForm.from_params(params)
54
+
55
+ RegisterAccount.call(@form) do
56
+ on(:ok) { redirect_to dashboard_path }
57
+ on(:invalid) { render :new }
58
+ on(:already_registered) { redirect_to login_path }
59
+ end
60
+ end
61
+ end
62
+ ```
63
+
64
+ The `RegistrationForm` Form Object encapsulates the relevant data that is required for the
65
+ action and the `RegisterAccount` Command encapsulates the business logic of registering
66
+ a new account. The controller is clean and business logic now has a natural home:
67
+
68
+ ```
69
+ HTTP => Controller (redirecting, rendering, etc)
70
+ Data Input => Form Object (validation, acceptable input)
71
+ Business Logic => Command (logic for a specific use case)
72
+ Data Access => Model (relationships, queries)
73
+ ```
74
+
75
+ The next sections will give further details about using Form Objects and Commands.
76
+
77
+ ## Form Objects
78
+
79
+ Form objects in Rectify are based on [Virtus](https://github.com/solnic/virtus)
80
+ and make them compatible with Rails form builders, add ActiveModel validations
81
+ and all allow you to specify a model to mimic.
82
+
83
+ Here is how you define a form object:
84
+
85
+ ```ruby
86
+ class UserForm < Rectify::Form
87
+ attribute :first_name, String
88
+ attribute :last_name, String
89
+
90
+ validates :first_name, :last_name, :presence => true
91
+ end
92
+ ```
93
+
94
+ You can then set that up in your controller instead of a normal ActiveRecord model:
95
+
96
+ ```ruby
97
+ class UsersController < ApplicationController
98
+ def new
99
+ @form = UserForm.new
100
+ end
101
+
102
+ def create
103
+ @form = UserForm.from_params(params)
104
+
105
+ if @form.valid?
106
+ # Do something interesting
107
+ end
108
+ end
109
+ end
110
+ ```
111
+
112
+ You can use the form object with form builders such as
113
+ [simple_form](https://github.com/plataformatec/simple_form) like this:
114
+
115
+ ```ruby
116
+ = simple_form_for @form do |f|
117
+ = f.input :first_name
118
+ = f.input :last_name
119
+ = f.submit
120
+ ```
121
+
122
+ ### Mimicking models
123
+
124
+ When the form is generated it uses the name of the form class to infer what "model"
125
+ it should mimic. In the example above, it will mimic the `User` model as it removes
126
+ the `Form` suffix from the form class name by default.
127
+
128
+ The model being mimicked affects two things about the form:
129
+
130
+ 1. The route path helpers to use as the url to post to, for example: `users_path`.
131
+ 2. The parent key in the params hash that the controller receives, for example
132
+ `user` in this case:
133
+
134
+ ```ruby
135
+ params = {
136
+ "id" => "1",
137
+ "user" => {
138
+ "first_name" => "Andy",
139
+ "last_name" => "Pike"
140
+ }
141
+ }
142
+ ```
143
+
144
+ You might want to mimic something different and use a form object that is not
145
+ named in a way where the correct model can be mimicked. For example:
146
+
147
+ ```ruby
148
+ class UserForm < Rectify::Form
149
+ mimic :teacher
150
+
151
+ attribute :first_name, String
152
+ attribute :last_name, String
153
+
154
+ validates :first_name, :last_name, :presence => true
155
+ end
156
+ ```
157
+
158
+ In this example we are using the same `UserForm` class but am mimicking a
159
+ `Teacher` model. The above form will then use the route path helpers
160
+ `teachers_path` and the params key will be `teacher` rather than `users_path`
161
+ and `user` respectively.
162
+
163
+ ### Attributes
164
+
165
+ You define your attributes for your form object just like you do in
166
+ [Virtus](https://github.com/solnic/virtus).
167
+
168
+ By default, Rectify forms include an `id` attribute for you so you don't need to
169
+ add that. We use this `id` attribute to fulfil some of the requirements of ActiveModel
170
+ so your forms will work with form builders. For example, your form object has a
171
+ `#persisted?` method. Your form object is never persisted so technically this
172
+ should always return `false`.
173
+
174
+ However, you are normally representing something that is persistable. So we use
175
+ the value of `id` to workout if what this should return. If `id` is a number
176
+ greater than zero then we assume it is persisted otherwise we assume it isn't. This
177
+ is important as it affects where your form is posted (to the `#create` or
178
+ `#update` action in your controller).
179
+
180
+ #### Populating attributes
181
+
182
+ There are a number of ways to populate attributes of a form object.
183
+
184
+ **Constructor**
185
+
186
+ You can use the constructor and pass it a hash of values:
187
+
188
+ ```ruby
189
+ form = UserForm.new(:first_name => "Andy", :last_name => "Pike")
190
+ ```
191
+
192
+ **Prams hash**
193
+
194
+ You can use the params hash that a Rails controller provides that contains all
195
+ the data in the request:
196
+
197
+ ```ruby
198
+ form = User.from_params(params)
199
+ ```
200
+
201
+ When populating from params we will populate the built in `id` attribute from the
202
+ root of the params hash and populate the rest of the form attributes from within
203
+ the parent key. For example:
204
+
205
+ ```ruby
206
+ params = {
207
+ "id" => "1",
208
+ "user" => {
209
+ "first_name" => "Andy",
210
+ "last_name" => "Pike"
211
+ }
212
+ }
213
+
214
+ form = User.from_params(params)
215
+
216
+ form.id # => 1
217
+ form.first_name # => "Andy"
218
+ form.last_name # => "Pike"
219
+ ```
220
+
221
+ The other thing to notice is that (thanks to Virtus), attribute values are cast
222
+ to the correct type. The params hash is actually all string based but when you
223
+ get values from the form, they are returned as the correct type (see `id` above).
224
+
225
+ **Model**
226
+
227
+ The final way is to pass an ActiveModel to the form to populate it's attribute
228
+ values. This is useful when editing a model:
229
+
230
+ ```ruby
231
+ user = User.create(:first_name => "Andy", :last_name => "Pike")
232
+
233
+ form = UserForm.from_model(user)
234
+
235
+ form.id # => 1
236
+ form.first_name # => "Andy"
237
+ form.last_name # => "Pike"
238
+ ```
239
+
240
+ One important thing that is different about Rectify forms is that they are not
241
+ bound by a model. You can use a model to populate the forms attributes but that
242
+ is all it will do. It does not keep a reference to the model or interact with it.
243
+ Rectify forms are designed to be lightweight representations of the data you want
244
+ to collect or show in your forms, not something that is linked to a model. This
245
+ allows you to create any form that you like which doesn't need to match the
246
+ representation of the data in the database.
247
+
248
+ ### Validations
249
+
250
+ Rectify includes `ActiveModel::Validations` for you so you can use all of the
251
+ Rails validations that you are used to within your models.
252
+
253
+ Your Form Object has a `#valid?` method that will validate the attributes of your
254
+ form as well as any nested form objects.
255
+
256
+ ### Strong Parameters
257
+
258
+ Did you notice in the example above that there was no mention of strong
259
+ parameters. That's because with Form Objects you do not need strong parameters.
260
+ You only specify attributes in your form that are allowed to be accepted. All
261
+ other data in your params hash is ignored.
262
+
263
+ ## Commands
264
+
265
+ Docs coming soon...
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rectify
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.1
4
+ version: 0.0.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Andy Pike
@@ -9,7 +9,183 @@ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
11
  date: 2015-12-08 00:00:00.000000000 Z
12
- dependencies: []
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: virtus
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '1.0'
20
+ - - ">="
21
+ - !ruby/object:Gem::Version
22
+ version: 1.0.5
23
+ type: :runtime
24
+ prerelease: false
25
+ version_requirements: !ruby/object:Gem::Requirement
26
+ requirements:
27
+ - - "~>"
28
+ - !ruby/object:Gem::Version
29
+ version: '1.0'
30
+ - - ">="
31
+ - !ruby/object:Gem::Version
32
+ version: 1.0.5
33
+ - !ruby/object:Gem::Dependency
34
+ name: wisper
35
+ requirement: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - "~>"
38
+ - !ruby/object:Gem::Version
39
+ version: '1.6'
40
+ - - ">="
41
+ - !ruby/object:Gem::Version
42
+ version: 1.6.1
43
+ type: :runtime
44
+ prerelease: false
45
+ version_requirements: !ruby/object:Gem::Requirement
46
+ requirements:
47
+ - - "~>"
48
+ - !ruby/object:Gem::Version
49
+ version: '1.6'
50
+ - - ">="
51
+ - !ruby/object:Gem::Version
52
+ version: 1.6.1
53
+ - !ruby/object:Gem::Dependency
54
+ name: activesupport
55
+ requirement: !ruby/object:Gem::Requirement
56
+ requirements:
57
+ - - "~>"
58
+ - !ruby/object:Gem::Version
59
+ version: '4.2'
60
+ - - ">="
61
+ - !ruby/object:Gem::Version
62
+ version: 4.2.0
63
+ type: :runtime
64
+ prerelease: false
65
+ version_requirements: !ruby/object:Gem::Requirement
66
+ requirements:
67
+ - - "~>"
68
+ - !ruby/object:Gem::Version
69
+ version: '4.2'
70
+ - - ">="
71
+ - !ruby/object:Gem::Version
72
+ version: 4.2.0
73
+ - !ruby/object:Gem::Dependency
74
+ name: activemodel
75
+ requirement: !ruby/object:Gem::Requirement
76
+ requirements:
77
+ - - "~>"
78
+ - !ruby/object:Gem::Version
79
+ version: '4.2'
80
+ - - ">="
81
+ - !ruby/object:Gem::Version
82
+ version: 4.2.0
83
+ type: :runtime
84
+ prerelease: false
85
+ version_requirements: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - "~>"
88
+ - !ruby/object:Gem::Version
89
+ version: '4.2'
90
+ - - ">="
91
+ - !ruby/object:Gem::Version
92
+ version: 4.2.0
93
+ - !ruby/object:Gem::Dependency
94
+ name: activerecord
95
+ requirement: !ruby/object:Gem::Requirement
96
+ requirements:
97
+ - - "~>"
98
+ - !ruby/object:Gem::Version
99
+ version: '4.2'
100
+ - - ">="
101
+ - !ruby/object:Gem::Version
102
+ version: 4.2.0
103
+ type: :runtime
104
+ prerelease: false
105
+ version_requirements: !ruby/object:Gem::Requirement
106
+ requirements:
107
+ - - "~>"
108
+ - !ruby/object:Gem::Version
109
+ version: '4.2'
110
+ - - ">="
111
+ - !ruby/object:Gem::Version
112
+ version: 4.2.0
113
+ - !ruby/object:Gem::Dependency
114
+ name: awesome_print
115
+ requirement: !ruby/object:Gem::Requirement
116
+ requirements:
117
+ - - "~>"
118
+ - !ruby/object:Gem::Version
119
+ version: '1.6'
120
+ type: :development
121
+ prerelease: false
122
+ version_requirements: !ruby/object:Gem::Requirement
123
+ requirements:
124
+ - - "~>"
125
+ - !ruby/object:Gem::Version
126
+ version: '1.6'
127
+ - !ruby/object:Gem::Dependency
128
+ name: pry
129
+ requirement: !ruby/object:Gem::Requirement
130
+ requirements:
131
+ - - "~>"
132
+ - !ruby/object:Gem::Version
133
+ version: 0.10.3
134
+ type: :development
135
+ prerelease: false
136
+ version_requirements: !ruby/object:Gem::Requirement
137
+ requirements:
138
+ - - "~>"
139
+ - !ruby/object:Gem::Version
140
+ version: 0.10.3
141
+ - !ruby/object:Gem::Dependency
142
+ name: wisper-rspec
143
+ requirement: !ruby/object:Gem::Requirement
144
+ requirements:
145
+ - - "~>"
146
+ - !ruby/object:Gem::Version
147
+ version: 0.0.2
148
+ type: :development
149
+ prerelease: false
150
+ version_requirements: !ruby/object:Gem::Requirement
151
+ requirements:
152
+ - - "~>"
153
+ - !ruby/object:Gem::Version
154
+ version: 0.0.2
155
+ - !ruby/object:Gem::Dependency
156
+ name: rspec
157
+ requirement: !ruby/object:Gem::Requirement
158
+ requirements:
159
+ - - "~>"
160
+ - !ruby/object:Gem::Version
161
+ version: '3.4'
162
+ type: :development
163
+ prerelease: false
164
+ version_requirements: !ruby/object:Gem::Requirement
165
+ requirements:
166
+ - - "~>"
167
+ - !ruby/object:Gem::Version
168
+ version: '3.4'
169
+ - !ruby/object:Gem::Dependency
170
+ name: rspec-collection_matchers
171
+ requirement: !ruby/object:Gem::Requirement
172
+ requirements:
173
+ - - "~>"
174
+ - !ruby/object:Gem::Version
175
+ version: '1.1'
176
+ - - ">="
177
+ - !ruby/object:Gem::Version
178
+ version: 1.1.2
179
+ type: :development
180
+ prerelease: false
181
+ version_requirements: !ruby/object:Gem::Requirement
182
+ requirements:
183
+ - - "~>"
184
+ - !ruby/object:Gem::Version
185
+ version: '1.1'
186
+ - - ">="
187
+ - !ruby/object:Gem::Version
188
+ version: 1.1.2
13
189
  description: Build Rails apps in a more maintainable way
14
190
  email: andy@andypike.com
15
191
  executables: []
@@ -17,9 +193,12 @@ extensions: []
17
193
  extra_rdoc_files: []
18
194
  files:
19
195
  - LICENSE.txt
20
- - readme.md
21
- - lib/rectify/version.rb
22
196
  - lib/rectify.rb
197
+ - lib/rectify/command.rb
198
+ - lib/rectify/form.rb
199
+ - lib/rectify/save_command.rb
200
+ - lib/rectify/version.rb
201
+ - readme.md
23
202
  homepage: https://github.com/andypike/rectify
24
203
  licenses:
25
204
  - MIT
@@ -30,18 +209,19 @@ require_paths:
30
209
  - lib
31
210
  required_ruby_version: !ruby/object:Gem::Requirement
32
211
  requirements:
33
- - - '>='
212
+ - - ">="
34
213
  - !ruby/object:Gem::Version
35
214
  version: '0'
36
215
  required_rubygems_version: !ruby/object:Gem::Requirement
37
216
  requirements:
38
- - - '>='
217
+ - - ">="
39
218
  - !ruby/object:Gem::Version
40
219
  version: '0'
41
220
  requirements: []
42
221
  rubyforge_project:
43
- rubygems_version: 2.0.14
222
+ rubygems_version: 2.4.7
44
223
  signing_key:
45
224
  specification_version: 4
46
225
  summary: Improvements for building Rails apps
47
226
  test_files: []
227
+ has_rdoc: