rectify 0.0.1 → 0.0.2

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: 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: