trailblazer 2.1.0.beta4 → 2.1.0.beta5
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/.rubocop_todo.yml +194 -502
- data/CHANGES.md +4 -0
- data/CONTRIBUTING.md +170 -0
- data/Gemfile +4 -1
- data/README.md +183 -40
- data/Rakefile +6 -2
- data/lib/trailblazer/version.rb +1 -1
- data/lib/trailblazer.rb +3 -12
- data/test/{operation/dsl → dsl}/contract_test.rb +2 -2
- data/trailblazer.gemspec +3 -3
- metadata +17 -63
- data/lib/trailblazer/operation/contract.rb +0 -82
- data/lib/trailblazer/operation/guard.rb +0 -18
- data/lib/trailblazer/operation/model.rb +0 -65
- data/lib/trailblazer/operation/nested.rb +0 -91
- data/lib/trailblazer/operation/persist.rb +0 -14
- data/lib/trailblazer/operation/policy.rb +0 -44
- data/lib/trailblazer/operation/pundit.rb +0 -38
- data/lib/trailblazer/operation/representer.rb +0 -36
- data/lib/trailblazer/operation/rescue.rb +0 -24
- data/lib/trailblazer/operation/validate.rb +0 -74
- data/lib/trailblazer/operation/wrap.rb +0 -64
- data/test/docs/contract_test.rb +0 -545
- data/test/docs/dry_test.rb +0 -31
- data/test/docs/guard_test.rb +0 -162
- data/test/docs/macro_test.rb +0 -36
- data/test/docs/model_test.rb +0 -75
- data/test/docs/nested_test.rb +0 -300
- data/test/docs/policy_test.rb +0 -2
- data/test/docs/pundit_test.rb +0 -133
- data/test/docs/representer_test.rb +0 -268
- data/test/docs/rescue_test.rb +0 -154
- data/test/docs/wrap_test.rb +0 -219
- data/test/nested_test.rb +0 -293
- data/test/operation/contract_test.rb +0 -290
- data/test/operation/dsl/representer_test.rb +0 -169
- data/test/operation/model_test.rb +0 -54
- data/test/operation/persist_test.rb +0 -51
- data/test/operation/pundit_test.rb +0 -106
- data/test/operation/representer_test.rb +0 -254
data/CHANGES.md
CHANGED
@@ -30,6 +30,10 @@ document Task API and define step API
|
|
30
30
|
deprecate step->(options) ?
|
31
31
|
injectable, per-operation step arguments strategy?
|
32
32
|
|
33
|
+
# 2.1.0.beta5
|
34
|
+
|
35
|
+
* All macros are now cleanly extracted to `trailblazer-macro` and `trailblazer-macro-contract`.
|
36
|
+
|
33
37
|
# 2.1.0.beta4
|
34
38
|
|
35
39
|
* Simple maintenance release to establish `activity-0.5.0`.
|
data/CONTRIBUTING.md
ADDED
@@ -0,0 +1,170 @@
|
|
1
|
+
# Contributing to Trailblazer
|
2
|
+
Trailblazer is an open source project and we would love you to help us make it better.
|
3
|
+
|
4
|
+
## Questions/Help
|
5
|
+
If our [guides][guides] nor [docs][api-docs] can help you figure things out, and you're stuck, refrain from posting questions on github issues, and please just find us on the [trailblazer gitter chat][chat] and drop us a line.
|
6
|
+
|
7
|
+
Keep in mind when asking questions though, an example will get you help faster than anything else you do.
|
8
|
+
|
9
|
+
If you file an issue with a question, it will be closed. We're not trying to be mean, don't get us wrong, we're just trying to stay sane.
|
10
|
+
|
11
|
+
## Reporting Issues
|
12
|
+
A well formatted issue is appreciated, and goes a long way in helping us help you.
|
13
|
+
|
14
|
+
* Make sure you have a [GitHub account](https://github.com/signup/free)
|
15
|
+
* Submit a [Github issue][issues-link] by:
|
16
|
+
* Clearly describing the issue
|
17
|
+
* Provide a descriptive summary
|
18
|
+
* Provide sample code where possible (preferably in the form of a test, in a [Gist][gist] for bonus points)
|
19
|
+
* Explain the expected behavior
|
20
|
+
* Explain the actual behavior
|
21
|
+
* Provide steps to reproduce the actual behavior
|
22
|
+
* Provide your application's complete `Gemfile.lock` as text (in a [Gist][gist] for bonus points)
|
23
|
+
* Any relevant stack traces
|
24
|
+
|
25
|
+
If you provide code, make sure it is formatted with the triple backticks (\`).
|
26
|
+
|
27
|
+
At this point, we'd love to tell you how long it will take for us to respond, but we just don't know.
|
28
|
+
|
29
|
+
## Pull requests
|
30
|
+
We accept pull requests to Trailblazer for:
|
31
|
+
|
32
|
+
* Adding documentation
|
33
|
+
* Fixing bugs
|
34
|
+
* Adding new features
|
35
|
+
|
36
|
+
Not all features proposed will be added but we are open to having a conversation about a feature you are championing.
|
37
|
+
|
38
|
+
###Here's a quick guide:
|
39
|
+
#### Fork the Project
|
40
|
+
Fork the [project repository][project-repo-link] on Github and check out your copy.
|
41
|
+
|
42
|
+
```
|
43
|
+
git clone https://github.com/YOUR_HANDLE/trailblazer.git
|
44
|
+
cd trailblazer
|
45
|
+
git remote add upstream https://github.com/trailblazer/trailblazer.git
|
46
|
+
```
|
47
|
+
|
48
|
+
#### Create a Topic Branch
|
49
|
+
Make sure your fork is up-to-date and create a topic branch for your feature or bug fix.
|
50
|
+
```
|
51
|
+
git checkout master
|
52
|
+
git pull upstream master
|
53
|
+
git checkout -b my-feature-branch
|
54
|
+
```
|
55
|
+
|
56
|
+
#### Bundle and Test
|
57
|
+
Run bundle install/update to gather any and all dependencies. Run the tests. This is to make sure your starting point works.
|
58
|
+
|
59
|
+
```
|
60
|
+
bundle install
|
61
|
+
bundle exec rake
|
62
|
+
```
|
63
|
+
|
64
|
+
#### Write Tests
|
65
|
+
Try to write a test that reproduces the problem you're trying to fix or describes a feature that you want to build. Add to [test][test-link].
|
66
|
+
|
67
|
+
We definitely appreciate pull requests that highlight or reproduce a problem, even without a fix.
|
68
|
+
|
69
|
+
#### Write Code
|
70
|
+
Implement your feature or bug fix.
|
71
|
+
|
72
|
+
Ruby style is enforced with [RuboCop](https://github.com/bbatsov/rubocop), run `bundle exec rubocop` and fix any style issues highlighted.
|
73
|
+
|
74
|
+
Make sure that `bundle exec rake` completes without errors.
|
75
|
+
|
76
|
+
#### Write Documentation
|
77
|
+
Document any external behavior in the [README](README.md).
|
78
|
+
|
79
|
+
#### Commit Changes
|
80
|
+
Make sure git knows your name and email address:
|
81
|
+
|
82
|
+
```
|
83
|
+
git config --global user.name "Your Name"
|
84
|
+
git config --global user.email "contributor@example.com"
|
85
|
+
```
|
86
|
+
|
87
|
+
Writing good commit logs is important. A commit log should describe what has changed and why, but be brief.
|
88
|
+
|
89
|
+
```
|
90
|
+
git add your_filename.rb (File names to add content from, or fileglobs e.g. *.rb)
|
91
|
+
git commit
|
92
|
+
```
|
93
|
+
|
94
|
+
#### Push
|
95
|
+
```
|
96
|
+
git push origin my-feature-branch
|
97
|
+
```
|
98
|
+
|
99
|
+
#### Make a Pull Request
|
100
|
+
Go to https://github.com/YOUR_GH_HANDLE/trailblazer and select your feature branch. Click the 'Pull Request' button and fill out the form. Pull requests are usually reviewed within a few days, but no need to rush us if it takes longer.
|
101
|
+
|
102
|
+
#### Rebase
|
103
|
+
If you've been working on a change for a while, rebase with upstream/master.
|
104
|
+
|
105
|
+
```
|
106
|
+
git fetch upstream
|
107
|
+
git rebase upstream/master
|
108
|
+
git push origin my-feature-branch -f
|
109
|
+
```
|
110
|
+
|
111
|
+
#### Update CHANGELOG Again
|
112
|
+
Update the [CHANGELOG](CHANGELOG.md) with the pull request number. A typical entry looks as follows.
|
113
|
+
|
114
|
+
```
|
115
|
+
* [#123](https://github.com/trailblazer/trailblazer/pull/123): Your brief description - [@your_gh_handle](https://github.com/your_gh_handle).
|
116
|
+
```
|
117
|
+
|
118
|
+
Amend your previous commit and force push the changes.
|
119
|
+
|
120
|
+
```
|
121
|
+
git commit --amend
|
122
|
+
git push origin my-feature-branch -f
|
123
|
+
```
|
124
|
+
|
125
|
+
#### Check on Your Pull Request
|
126
|
+
Go back to your pull request after a few minutes and see whether it passed muster with Travis-CI. Everything should look green, otherwise fix issues and amend your commit as described above.
|
127
|
+
|
128
|
+
## Releasing
|
129
|
+
|
130
|
+
When you have release rights, please follow these rules.
|
131
|
+
|
132
|
+
* When tagging a commit for a release, use the format `vX.X.X` for the tag, e.g. `git tag v2.1.0`.
|
133
|
+
* The tagged commit **must contain the line** "Releasing vX.X.X" so it can be quickly spotted later in the commit list.
|
134
|
+
|
135
|
+
#### What now?
|
136
|
+
At this point you're waiting on us. Expect a conversation regarding your pull request; Questions, clarifications, and so on.
|
137
|
+
|
138
|
+
Some things that will increase the chance that your pull request is accepted:
|
139
|
+
* Use Trailblazer idioms and follow the Trailblazer idealogy
|
140
|
+
* Include tests that fail without your code, and pass with it
|
141
|
+
* Update the documentation, guides, etc.
|
142
|
+
|
143
|
+
## What do we need help with?
|
144
|
+
### Helping others!
|
145
|
+
There are a lot of questions from people as they get started using Trailblazer. If you could **please do the following things**, that would really help:
|
146
|
+
|
147
|
+
- Hang out on [the chat][chat]
|
148
|
+
- Watch the [trailblazer repositories][repositories] for issues or requests that you could help with
|
149
|
+
|
150
|
+
### Contributing to community
|
151
|
+
- Create Macros!
|
152
|
+
- Write blog posts!
|
153
|
+
- Record screencasts
|
154
|
+
- Write examples.
|
155
|
+
|
156
|
+
### Contributing to the core
|
157
|
+
- Tests are always helpful!
|
158
|
+
- Any of the issues in GitHub, let us know if you have some time to fix one.
|
159
|
+
|
160
|
+
## Thank You
|
161
|
+
Please do know that we really appreciate and value your time and work.
|
162
|
+
|
163
|
+
[gist]: https://gist.github.com
|
164
|
+
[guides]: https://www.trailblazer.to
|
165
|
+
[api-docs]: https://www.trailblazer.to/api-docs
|
166
|
+
[chat]: https://gitter.im/trailblazer/chat
|
167
|
+
[repositories]: https://github.com/trailblazer
|
168
|
+
[test-link]: https://github.com/trailblazer/trailblazer/tree/master/test
|
169
|
+
[project-repo-link]: https://github.com/trailblazer/trailblazer
|
170
|
+
[issues-link]: https://www.github.com/trailblazer/trailblazer/issues
|
data/Gemfile
CHANGED
@@ -14,13 +14,16 @@ gem "dry-auto_inject"
|
|
14
14
|
gem "dry-matcher"
|
15
15
|
gem "dry-validation"
|
16
16
|
|
17
|
+
# gem "trailblazer-operation", path: "../operation"
|
18
|
+
gem "trailblazer-macro", github: "trailblazer/trailblazer-macro"
|
19
|
+
gem "trailblazer-macro-contract", github: "trailblazer/trailblazer-macro-contract"
|
17
20
|
gem "trailblazer-operation", path: "../operation"
|
18
21
|
# gem "trailblazer-operation", github: "trailblazer/trailblazer-operation"
|
19
22
|
|
20
23
|
gem "minitest-line"
|
21
24
|
|
22
25
|
gem "rubocop", require: false
|
23
|
-
|
26
|
+
gem "trailblazer-activity", path: "../trailblazer-circuit"
|
24
27
|
# gem "trailblazer-activity", github: "trailblazer/trailblazer-activity"
|
25
28
|
# gem "trailblazer-activity", path: "../trailblazer-circuit"
|
26
29
|
# gem "trailblazer-activity", github: "trailblazer/trailblazer-activity"
|
data/README.md
CHANGED
@@ -42,7 +42,7 @@ Trailblazer offers you a new, more intuitive file layout in applications.
|
|
42
42
|
```
|
43
43
|
app
|
44
44
|
├── concepts
|
45
|
-
│ ├──
|
45
|
+
│ ├── song
|
46
46
|
│ │ ├── operation
|
47
47
|
│ │ │ ├── create.rb
|
48
48
|
│ │ │ ├── update.rb
|
@@ -55,7 +55,7 @@ app
|
|
55
55
|
│ │ ├── view
|
56
56
|
│ │ │ ├── show.haml
|
57
57
|
│ │ │ ├── index.rb
|
58
|
-
│ │ │ ├──
|
58
|
+
│ │ │ ├── song.css.sass
|
59
59
|
```
|
60
60
|
|
61
61
|
Instead of grouping by technology, classes and views are structured by *concept*, and then by technology. A concept can relate to a model, or can be a completely abstract concern such as `invoicing`.
|
@@ -81,7 +81,7 @@ Trailblazer uses Rails routing to map URLs to controllers, because it works.
|
|
81
81
|
|
82
82
|
```ruby
|
83
83
|
Rails.application.routes.draw do
|
84
|
-
resources :
|
84
|
+
resources :songs
|
85
85
|
end
|
86
86
|
```
|
87
87
|
|
@@ -90,9 +90,9 @@ end
|
|
90
90
|
Controllers are lean endpoints for HTTP. They do not contain any business logic. Actions immediately dispatch to an operation.
|
91
91
|
|
92
92
|
```ruby
|
93
|
-
class
|
93
|
+
class SongsController < ApplicationController
|
94
94
|
def create
|
95
|
-
run
|
95
|
+
run Song::Create # Song::Create is an operation class.
|
96
96
|
end
|
97
97
|
end
|
98
98
|
```
|
@@ -100,10 +100,10 @@ end
|
|
100
100
|
The `#run` method invokes the operation. It allows you to run a conditional block of logic if the operation was successful.
|
101
101
|
|
102
102
|
```ruby
|
103
|
-
class
|
103
|
+
class SongsController < ApplicationController
|
104
104
|
def create
|
105
|
-
run
|
106
|
-
return redirect_to(
|
105
|
+
run Song::Create do |op|
|
106
|
+
return redirect_to(song_path op.model) # success!
|
107
107
|
end
|
108
108
|
|
109
109
|
render :new # invalid. re-render form.
|
@@ -126,7 +126,7 @@ Operations don't know about HTTP or the environment. You could use an operation
|
|
126
126
|
An operation is not just a monolithic replacement for your business code. It's a simple orchestrator between the form objects, models, your business code and all other layers needed to get the job done.
|
127
127
|
|
128
128
|
```ruby
|
129
|
-
class
|
129
|
+
class Song::Create < Trailblazer::Operation
|
130
130
|
step :process!
|
131
131
|
|
132
132
|
def process!(options)
|
@@ -140,44 +140,188 @@ Operations only need to define and implement steps, like the `#process!` steps.
|
|
140
140
|
You cannot instantiate them per design. The only way to invoke them is `call`.
|
141
141
|
|
142
142
|
```ruby
|
143
|
-
|
143
|
+
Song::Create.call(whatever: "goes", in: "here")
|
144
144
|
# same as
|
145
|
-
|
145
|
+
Song::Create.(whatever: "goes", in: "here")
|
146
146
|
```
|
147
147
|
|
148
148
|
Their high degree of encapsulation makes them a [replacement for test factories](#test), too.
|
149
149
|
|
150
150
|
[Learn more.](http://trailblazer.to/gems/operation)
|
151
151
|
|
152
|
-
|
152
|
+
### Contract
|
153
|
+
The Contract Macro, covers the contracts for Trailblazer, they are basically Reform objects that you can define and validate inside an operation. Reform is a fantastic tool for deserializing and validating deeply nested hashes, and then, when valid, writing those to the database using your persistence layer such as ActiveRecord.
|
153
154
|
|
154
|
-
|
155
|
+
```ruby
|
156
|
+
# app/concepts/song/contract/create.rb
|
157
|
+
module Song::Contract
|
158
|
+
class Create < Reform::Form
|
159
|
+
property :title
|
160
|
+
property :length
|
161
|
+
|
162
|
+
validates :title, length: 2..33
|
163
|
+
validates :length, numericality: true
|
164
|
+
end
|
165
|
+
end
|
166
|
+
```
|
155
167
|
|
156
|
-
|
168
|
+
The Contract then gets hooked into the operation. using this Macro.
|
169
|
+
```ruby
|
170
|
+
# app/concepts/song/operation/create.rb
|
171
|
+
class Song::Create < Trailblazer::Operation
|
172
|
+
step Model( Song, :new )
|
173
|
+
step Contract::Build( constant: Song::Contract::Create )
|
174
|
+
step Contract::Validate()
|
175
|
+
step Contract::Persist()
|
176
|
+
end
|
177
|
+
```
|
178
|
+
As you can see, using contracts consists of five steps.
|
157
179
|
|
158
|
-
|
180
|
+
Define the contract class (or multiple of them) for the operation.
|
181
|
+
Plug the contract creation into the operation’s pipe using Contract::Build.
|
182
|
+
Run the contract’s validation for the params using Contract::Validate.
|
183
|
+
If successful, write the sane data to the model(s). This will usually happen in the Contract::Persist macro.
|
184
|
+
After the operation has been run, interpret the result. For instance, a controller calling an operation will render a erroring form for invalid input.
|
159
185
|
|
186
|
+
Here’s what the result would look like after running the Create operation with invalid data.
|
160
187
|
```ruby
|
161
|
-
|
162
|
-
|
188
|
+
result = Song::Create.( title: "A" )
|
189
|
+
result.success? #=> false
|
190
|
+
result["contract.default"].errors.messages
|
191
|
+
#=> {:title=>["is too short (minimum is 2 characters)"], :length=>["is not a number"]}
|
192
|
+
```
|
163
193
|
|
164
|
-
|
165
|
-
|
166
|
-
|
167
|
-
|
194
|
+
#### Build
|
195
|
+
The Contract::Build macro helps you to instantiate the contract. It is both helpful for a complete workflow, or to create the contract, only, without validating it, e.g. when presenting the form.
|
196
|
+
```ruby
|
197
|
+
class Song::New < Trailblazer::Operation
|
198
|
+
step Model( Song, :new )
|
199
|
+
step Contract::Build( constant: Song::Contract::Create )
|
200
|
+
end
|
201
|
+
```
|
202
|
+
|
203
|
+
This macro will grab the model from options["model"] and pass it into the contract’s constructor. The contract is then saved in options["contract.default"].
|
204
|
+
```ruby
|
205
|
+
result = Song::New.()
|
206
|
+
result["model"] #=> #<struct Song title=nil, length=nil>
|
207
|
+
result["contract.default"]
|
208
|
+
#=> #<Song::Contract::Create model=#<struct Song title=nil, length=nil>>
|
209
|
+
```
|
210
|
+
The Build macro accepts the :name option to change the name from default.
|
168
211
|
|
169
|
-
|
170
|
-
|
171
|
-
|
212
|
+
#### Validation
|
213
|
+
The Contract::Validate macro is responsible for validating the incoming params against its contract. That means you have to use Contract::Build beforehand, or create the contract yourself. The macro will then grab the params and throw then into the contract’s validate (or call) method.
|
214
|
+
|
215
|
+
```ruby
|
216
|
+
class Song::ValidateOnly < Trailblazer::Operation
|
217
|
+
step Model( Song, :new )
|
218
|
+
step Contract::Build( constant: Song::Contract::Create )
|
219
|
+
step Contract::Validate()
|
220
|
+
end
|
221
|
+
```
|
222
|
+
Depending on the outcome of the validation, it either stays on the right track, or deviates to left, skipping the remaining steps.
|
223
|
+
```ruby
|
224
|
+
result = Song::ValidateOnly.({}) # empty params
|
225
|
+
result.success? #=> false
|
226
|
+
```
|
227
|
+
|
228
|
+
Note that Validate really only validates the contract, nothing is written to the model, yet. You need to push data to the model manually, e.g. with Contract::Persist.
|
229
|
+
```ruby
|
230
|
+
result = Song::ValidateOnly.({ title: "Rising Force", length: 13 })
|
231
|
+
|
232
|
+
result.success? #=> true
|
233
|
+
result["model"] #=> #<struct Song title=nil, length=nil>
|
234
|
+
result["contract.default"].title #=> "Rising Force"
|
235
|
+
```
|
236
|
+
|
237
|
+
Validate will use options["params"] as the input. You can change the nesting with the :key option.
|
238
|
+
|
239
|
+
Internally, this macro will simply call Form#validate on the Reform object.
|
240
|
+
|
241
|
+
Note: Reform comes with sophisticated deserialization semantics for nested forms, it might be worth reading a bit about Reform to fully understand what you can do in the Validate step.
|
242
|
+
|
243
|
+
##### Key
|
244
|
+
Per default, Contract::Validate will use options["params"] as the data to be validated. Use the key: option if you want to validate a nested hash from the original params structure.
|
245
|
+
```ruby
|
246
|
+
class Song::Create < Trailblazer::Operation
|
247
|
+
step Model( Song, :new )
|
248
|
+
step Contract::Build( constant: Song::Contract::Create )
|
249
|
+
step Contract::Validate( key: "song" )
|
172
250
|
step Contract::Persist( )
|
173
251
|
end
|
174
252
|
```
|
175
253
|
|
176
|
-
|
254
|
+
This automatically extracts the nested "song" hash.
|
255
|
+
```ruby
|
256
|
+
result = Song::Create.({ "song" => { title: "Rising Force", length: 13 } })
|
257
|
+
result.success? #=> true
|
258
|
+
```
|
259
|
+
|
260
|
+
If that key isn’t present in the params hash, the operation fails before the actual validation.
|
261
|
+
```ruby
|
262
|
+
result = Song::Create.({ title: "Rising Force", length: 13 })
|
263
|
+
result.success? #=> false
|
264
|
+
```
|
265
|
+
|
266
|
+
Note: String vs. symbol do matter here since the operation will simply do a hash lookup using the key you provided.
|
267
|
+
|
268
|
+
#### Persist
|
269
|
+
To push validated data from the contract to the model(s), use Persist. Like Validate, this requires a contract to be set up beforehand.
|
270
|
+
```ruby
|
271
|
+
class Song::Create < Trailblazer::Operation
|
272
|
+
step Model( Song, :new )
|
273
|
+
step Contract::Build( constant: Song::Contract::Create )
|
274
|
+
step Contract::Validate()
|
275
|
+
step Contract::Persist()
|
276
|
+
end
|
277
|
+
```
|
278
|
+
|
279
|
+
After the step, the contract’s attribute values are written to the model, and the contract will call save on the model.
|
280
|
+
```ruby
|
281
|
+
result = Song::Create.( title: "Rising Force", length: 13 )
|
282
|
+
result.success? #=> true
|
283
|
+
result["model"] #=> #<Song title="Rising Force", length=13>
|
284
|
+
```
|
285
|
+
|
286
|
+
You can also configure the Persist step to call sync instead of Reform’s save.
|
287
|
+
```ruby
|
288
|
+
step Persist( method: :sync )
|
289
|
+
```
|
290
|
+
This will only write the contract’s data to the model without calling save on it.
|
291
|
+
|
292
|
+
##### Name
|
293
|
+
Explicit naming for the contract is possible, too.
|
294
|
+
```ruby
|
295
|
+
|
296
|
+
class Song::Create < Trailblazer::Operation
|
297
|
+
step Model( Song, :new )
|
298
|
+
step Contract::Build( name: "form", constant: Song::Contract::Create )
|
299
|
+
step Contract::Validate( name: "form" )
|
300
|
+
step Contract::Persist( name: "form" )
|
301
|
+
end
|
302
|
+
```
|
177
303
|
|
178
|
-
|
304
|
+
You have to use the name: option to tell each step what contract to use. The contract and its result will now use your name instead of default.
|
305
|
+
```ruby
|
306
|
+
result = Song::Create.({ title: "A" })
|
307
|
+
result["contract.form"].errors.messages #=> {:title=>["is too short (minimum is 2 ch...
|
308
|
+
```
|
179
309
|
|
180
|
-
|
310
|
+
Use this if your operation has multiple contracts.
|
311
|
+
|
312
|
+
#### Result Object
|
313
|
+
The operation will store the validation result for every contract in its own result object.
|
314
|
+
|
315
|
+
The path is result.contract.#{name}.
|
316
|
+
```ruby
|
317
|
+
result = Create.({ length: "A" })
|
318
|
+
|
319
|
+
result["result.contract.default"].success? #=> false
|
320
|
+
result["result.contract.default"].errors #=> Errors object
|
321
|
+
result["result.contract.default"].errors.messages #=> {:length=>["is not a number"]}
|
322
|
+
```
|
323
|
+
|
324
|
+
Each result object responds to success?, failure?, and errors, which is an Errors object.
|
181
325
|
|
182
326
|
## Models
|
183
327
|
|
@@ -186,7 +330,7 @@ Models for persistence can be implemented using any ORM you fancy, for instance
|
|
186
330
|
In Trailblazer, models are completely empty. They solely contain associations and finders. No business logic is allowed in models.
|
187
331
|
|
188
332
|
```ruby
|
189
|
-
class
|
333
|
+
class Song < ActiveRecord::Base
|
190
334
|
belongs_to :thing
|
191
335
|
|
192
336
|
scope :latest, lambda { all.limit(9).order("id DESC") }
|
@@ -200,9 +344,9 @@ Only operations and views/cells can access models directly.
|
|
200
344
|
You can abort running an operation using a policy. "[Pundit](https://github.com/elabs/pundit)-style" policy classes define the rules.
|
201
345
|
|
202
346
|
```ruby
|
203
|
-
class
|
204
|
-
def initialize(user,
|
205
|
-
@user, @
|
347
|
+
class Song::Policy
|
348
|
+
def initialize(user, song)
|
349
|
+
@user, @song = user, song
|
206
350
|
end
|
207
351
|
|
208
352
|
def create?
|
@@ -214,8 +358,8 @@ end
|
|
214
358
|
The rule is enabled via the `::policy` call.
|
215
359
|
|
216
360
|
```ruby
|
217
|
-
class
|
218
|
-
step Policy(
|
361
|
+
class Song::Create < Trailblazer::Operation
|
362
|
+
step Policy( Song::Policy, :create? )
|
219
363
|
end
|
220
364
|
```
|
221
365
|
|
@@ -233,9 +377,9 @@ More complex UI logic happens in _View Models_ as found in [Cells](https://githu
|
|
233
377
|
The operation's form object can be rendered in views, too.
|
234
378
|
|
235
379
|
```ruby
|
236
|
-
class
|
380
|
+
class SongsController < ApplicationController
|
237
381
|
def new
|
238
|
-
form
|
382
|
+
form Song::Create # will assign the form object to @form.
|
239
383
|
end
|
240
384
|
end
|
241
385
|
```
|
@@ -255,12 +399,12 @@ Operations can use representers from [Roar](https://github.com/apotonick/roar) t
|
|
255
399
|
Representers can be inferred automatically from your contract, then may be refined, e.g. with hypermedia or a format like `JSON-API`.
|
256
400
|
|
257
401
|
```ruby
|
258
|
-
class
|
402
|
+
class Song::Create < Trailblazer::Operation
|
259
403
|
representer do
|
260
404
|
# inherited :body
|
261
405
|
include Roar::JSON::HAL
|
262
406
|
|
263
|
-
link(:self) {
|
407
|
+
link(:self) { song_path(represented.id) }
|
264
408
|
end
|
265
409
|
end
|
266
410
|
```
|
@@ -276,8 +420,8 @@ In Trailblazer, you only have operation unit tests and integration smoke tests t
|
|
276
420
|
Operations completely replace the need for leaky factories.
|
277
421
|
|
278
422
|
```ruby
|
279
|
-
describe
|
280
|
-
let(:
|
423
|
+
describe Song::Update do
|
424
|
+
let(:song) { Song::Create.(song: {body: "[That](http://trailblazer.to)!"}) }
|
281
425
|
end
|
282
426
|
```
|
283
427
|
|
@@ -302,4 +446,3 @@ gem "trailblazer-cells"
|
|
302
446
|
```
|
303
447
|
|
304
448
|
Cells is _not_ required per default! Add it if you use it, which is highly recommended.
|
305
|
-
|
data/Rakefile
CHANGED
@@ -2,7 +2,7 @@ require "bundler/gem_tasks"
|
|
2
2
|
require "rake/testtask"
|
3
3
|
require "rubocop/rake_task"
|
4
4
|
|
5
|
-
task :default => [
|
5
|
+
task :default => %i[test rubocop]
|
6
6
|
|
7
7
|
Rake::TestTask.new(:test) do |test|
|
8
8
|
test.libs << 'test'
|
@@ -16,4 +16,8 @@ Rake::TestTask.new(:testdep) do |test|
|
|
16
16
|
test.verbose = true
|
17
17
|
end
|
18
18
|
|
19
|
-
RuboCop::RakeTask.new
|
19
|
+
RuboCop::RakeTask.new(:rubocop) do |task|
|
20
|
+
task.patterns = ['lib/**/*.rb', 'test/**/*.rb']
|
21
|
+
task.options << "--display-cop-names"
|
22
|
+
task.fail_on_error = false
|
23
|
+
end
|
data/lib/trailblazer/version.rb
CHANGED
data/lib/trailblazer.rb
CHANGED
@@ -6,17 +6,8 @@ require "trailblazer/dsl"
|
|
6
6
|
require "trailblazer/task"
|
7
7
|
|
8
8
|
require "trailblazer/operation/deprecations"
|
9
|
-
|
10
|
-
require "trailblazer/operation/model"
|
11
|
-
require "trailblazer/operation/contract"
|
12
|
-
require "trailblazer/operation/validate"
|
13
|
-
require "trailblazer/operation/representer"
|
14
|
-
require "trailblazer/operation/policy"
|
15
|
-
require "trailblazer/operation/pundit"
|
16
|
-
require "trailblazer/operation/guard"
|
17
|
-
require "trailblazer/operation/persist"
|
18
|
-
require "trailblazer/operation/nested"
|
19
|
-
require "trailblazer/operation/wrap"
|
20
|
-
require "trailblazer/operation/rescue"
|
21
9
|
require "trailblazer/operation/inject"
|
22
10
|
require "trailblazer/operation/input_output"
|
11
|
+
|
12
|
+
require "trailblazer/macro"
|
13
|
+
require "trailblazer/macro/contract"
|
@@ -38,7 +38,7 @@ class DslContractTest < MiniTest::Spec
|
|
38
38
|
end
|
39
39
|
|
40
40
|
# no inheritance with setter.
|
41
|
-
it { CreateOrFind["contract.default.class"].
|
41
|
+
it { CreateOrFind["contract.default.class"].must_equal nil }
|
42
42
|
|
43
43
|
# ---
|
44
44
|
# Op::contract Constant
|
@@ -197,7 +197,7 @@ class DslContractTest < MiniTest::Spec
|
|
197
197
|
form.sync
|
198
198
|
|
199
199
|
song.genre.must_equal "Punkrock"
|
200
|
-
song.band.
|
200
|
+
song.band.must_equal nil
|
201
201
|
end
|
202
202
|
end
|
203
203
|
|
data/trailblazer.gemspec
CHANGED
@@ -17,10 +17,10 @@ Gem::Specification.new do |spec|
|
|
17
17
|
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
18
18
|
spec.require_paths = ["lib"]
|
19
19
|
|
20
|
-
spec.add_dependency "trailblazer-operation", ">= 0.2.
|
21
|
-
spec.add_dependency "trailblazer-
|
20
|
+
spec.add_dependency "trailblazer-operation", ">= 0.2.4", "< 0.3.0"
|
21
|
+
spec.add_dependency "trailblazer-macro", ">= 2.1.0.beta2", "< 2.2.0"
|
22
|
+
spec.add_dependency "trailblazer-macro-contract", ">= 2.1.0.beta2", "< 2.2.0"
|
22
23
|
|
23
|
-
spec.add_dependency "reform", ">= 2.2.0", "< 3.0.0"
|
24
24
|
spec.add_dependency "declarative"
|
25
25
|
|
26
26
|
spec.add_development_dependency "activemodel" # for Reform::AM::V
|