nifty_services 0.0.5 → 0.0.6
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/.travis.yml +6 -2
- data/README.md +64 -1195
- data/docs/api.md +142 -0
- data/docs/callbacks.md +67 -0
- data/docs/cli.md +13 -0
- data/docs/configuration.md +62 -0
- data/docs/crud_services.md +534 -0
- data/docs/i18n.md +55 -0
- data/docs/services_markup.md +271 -0
- data/docs/usage.md +204 -0
- data/docs/webframeworks_integration.md +145 -0
- data/lib/nifty_services.rb +5 -8
- data/lib/nifty_services/base_action_service.rb +7 -10
- data/lib/nifty_services/base_create_service.rb +35 -45
- data/lib/nifty_services/base_crud_service.rb +18 -43
- data/lib/nifty_services/base_delete_service.rb +22 -21
- data/lib/nifty_services/base_service.rb +52 -42
- data/lib/nifty_services/base_update_service.rb +19 -20
- data/lib/nifty_services/configuration.rb +16 -17
- data/lib/nifty_services/errors.rb +2 -2
- data/lib/nifty_services/extensions/callbacks.rb +173 -0
- data/lib/nifty_services/support/hash.rb +14 -0
- data/lib/nifty_services/support/string.rb +21 -0
- data/lib/nifty_services/util.rb +4 -4
- data/lib/nifty_services/version.rb +1 -1
- data/nifty_services.gemspec +1 -1
- metadata +21 -10
- data/lib/nifty_services/extensions/callbacks_interface.rb +0 -171
data/docs/api.md
ADDED
@@ -0,0 +1,142 @@
|
|
1
|
+
# NiftyServices documentation
|
2
|
+
|
3
|
+
---
|
4
|
+
|
5
|
+
## Services Public API
|
6
|
+
|
7
|
+
Below, a list of most common public accessible methods for any instance of service:
|
8
|
+
(Detailed usage and full API list is available below this section)
|
9
|
+
|
10
|
+
```ruby
|
11
|
+
service.success? # boolean
|
12
|
+
service.fail? # boolean
|
13
|
+
service.errors # array
|
14
|
+
service.response_status # symbol (eg: :ok)
|
15
|
+
service.response_status_code # integer (eg: 200)
|
16
|
+
```
|
17
|
+
|
18
|
+
So, grabbing our `DailyNewsMailSendService` service again, we could do:
|
19
|
+
|
20
|
+
```ruby
|
21
|
+
service = DailyNewsMailSendService.new(User.new('test', 'test@test.com'))
|
22
|
+
service.execute
|
23
|
+
|
24
|
+
if service.success? # or unless service.fail?
|
25
|
+
SentEmails.create(user: service.user, type: 'welcome_email')
|
26
|
+
else
|
27
|
+
puts 'Error sending email, details below:'
|
28
|
+
puts 'Status: %s' % service.response_status
|
29
|
+
puts 'Status code: %s' % service.response_status_code
|
30
|
+
puts service.errors
|
31
|
+
end
|
32
|
+
|
33
|
+
# trying to re-execute the service will return `nil`
|
34
|
+
service.execute
|
35
|
+
```
|
36
|
+
|
37
|
+
This is really great and nifty, no? But we already started, there's some really cool stuff when dealing with **Restful** API's actions, before entering this subject let's see how to handle error and success response.
|
38
|
+
|
39
|
+
---
|
40
|
+
|
41
|
+
## Success & Error Responses
|
42
|
+
|
43
|
+
### :white_check_mark: Handling Success :zap:
|
44
|
+
|
45
|
+
To mark a service running as successfully, you must call one of this methods (preferencially inside of `execute_action` block):
|
46
|
+
|
47
|
+
* `success_response # [200, :ok]`
|
48
|
+
* `success_created_response [201, :created]`
|
49
|
+
|
50
|
+
The first value in comments above is the value which will be defined to `service.response_status_code` and the last is the value set to `service.response_status`.
|
51
|
+
|
52
|
+
---
|
53
|
+
|
54
|
+
|
55
|
+
### :red_circle: Handling Error :boom:
|
56
|
+
|
57
|
+
By default, all services comes with following error methods:
|
58
|
+
(**Hint**: See all available error methods [`here`](lib/nifty_services/configuration.rb#L10-L16))
|
59
|
+
|
60
|
+
```ruby
|
61
|
+
bad_request_error(message_key) # set response_status_code to 400
|
62
|
+
|
63
|
+
not_authorized_error(message_key) # set response_status_code to 401,
|
64
|
+
|
65
|
+
forbidden_error(message_key) # set response_status_code to 403,
|
66
|
+
|
67
|
+
not_found_error(message_key) # set response_status_code to 404,
|
68
|
+
|
69
|
+
unprocessable_entity_error(message_key) # set response_status_code to 422,
|
70
|
+
|
71
|
+
internal_server_error(message_key) # set response_status_code to 500,
|
72
|
+
|
73
|
+
not_implemented_error(message_key) # set response_status_code to 501
|
74
|
+
```
|
75
|
+
|
76
|
+
Beside this methods, you can always use **low level** API to generate errors, just call the `error` method, ex:
|
77
|
+
|
78
|
+
```ruby
|
79
|
+
# API
|
80
|
+
error(status, message_key, options = {})
|
81
|
+
|
82
|
+
# eg:
|
83
|
+
error(409, :conflict_error, reason: 'unkown')
|
84
|
+
error!(409, :conflict_error, reason: 'unkown')
|
85
|
+
|
86
|
+
# suppose you YML locale file have the configuration:
|
87
|
+
# nifty_services:
|
88
|
+
# errors:
|
89
|
+
# conflict_error: 'Conflict! The reason is %{reason}'
|
90
|
+
```
|
91
|
+
|
92
|
+
#### Custom error response methods
|
93
|
+
|
94
|
+
But you can always add new convenience errors methods via API, this way you will have more expressivity and sintax sugar:
|
95
|
+
|
96
|
+
```ruby
|
97
|
+
## API
|
98
|
+
NiftyServices.add_response_error_method(status, status_code)
|
99
|
+
|
100
|
+
## eg:
|
101
|
+
|
102
|
+
NiftyServices.add_response_error_method(:conflict, 409)
|
103
|
+
|
104
|
+
## now you have the methods:
|
105
|
+
|
106
|
+
## conflict_error(:conflict_error)
|
107
|
+
## conflit_error!(:conflict_error)
|
108
|
+
```
|
109
|
+
|
110
|
+
---
|
111
|
+
|
112
|
+
## Full Public API methods list
|
113
|
+
|
114
|
+
You can use any of the methods above with your `services instances`:
|
115
|
+
|
116
|
+
```ruby
|
117
|
+
service.success? # boolean
|
118
|
+
service.fail? # boolean
|
119
|
+
|
120
|
+
service.errors # hash
|
121
|
+
service.add_error(error) # array
|
122
|
+
|
123
|
+
service.response_status # symbol (eg: :ok)
|
124
|
+
service.response_status_code # integer (eg: 200)
|
125
|
+
|
126
|
+
service.changed_attributes # array
|
127
|
+
service.changed? # boolean
|
128
|
+
|
129
|
+
service.callback_fired?(callback_name) # boolean
|
130
|
+
service.register_callback(name, method, &block) # nil
|
131
|
+
service.register_callback_action(&block) # nil
|
132
|
+
|
133
|
+
service.option_exists?(option_name) # boolean
|
134
|
+
service.option_enabled?(option_name) # boolean
|
135
|
+
service.option_disabled?(option_name) # boolean
|
136
|
+
```
|
137
|
+
|
138
|
+
---
|
139
|
+
|
140
|
+
### Next
|
141
|
+
|
142
|
+
See [Crud Services](./crud_services.md)
|
data/docs/callbacks.md
ADDED
@@ -0,0 +1,67 @@
|
|
1
|
+
# NiftyServices documentation
|
2
|
+
|
3
|
+
---
|
4
|
+
|
5
|
+
## Callbacks
|
6
|
+
|
7
|
+
Here the most common callbacks list you can use to hook actions in run-time:
|
8
|
+
(**Hint**: See all existent callbacks definitions in [`extensions/callbacks.rb`](../lib/nifty_services/extensions/callbacks.rb#L7-L22) file)
|
9
|
+
|
10
|
+
```
|
11
|
+
- before_initialize
|
12
|
+
- after_initialize
|
13
|
+
- before_execute
|
14
|
+
- after_execute
|
15
|
+
- before_error
|
16
|
+
- after_error
|
17
|
+
- before_success
|
18
|
+
- after_success
|
19
|
+
```
|
20
|
+
|
21
|
+
|
22
|
+
|
23
|
+
### Creating custom Callbacks
|
24
|
+
|
25
|
+
Well, probably you will need to add custom callbacks to your services, in my case I need to save in database an object which tracks information about the environment used to create **ALL RECORDS** in my application, I was able to do it with just a few lines of code, see for yourself:
|
26
|
+
|
27
|
+
```ruby
|
28
|
+
# Some monkey patch :)
|
29
|
+
|
30
|
+
NiftyServices::BaseCreateService.class_eval do
|
31
|
+
ORIGIN_WHITELIST_ATTRIBUTES = [:provider, :locale, :user_agent, :ip]
|
32
|
+
|
33
|
+
def origin_params(params = {})
|
34
|
+
filter_hash(params.fetch(:origin, {}).to_h, ORIGIN_WHITELIST_ATTRIBUTES)
|
35
|
+
end
|
36
|
+
|
37
|
+
def create_origin(originable, params = {})
|
38
|
+
return unless originable.respond_to?(:create_origin)
|
39
|
+
return unless create_origin?
|
40
|
+
|
41
|
+
originable.create_origin(origin_params(params))
|
42
|
+
end
|
43
|
+
|
44
|
+
# for records which we don't need to create origins, just
|
45
|
+
# overwrite this method inside service class turning it off with:
|
46
|
+
# return false
|
47
|
+
def create_origin?
|
48
|
+
Application::Config.create_origin_for_records
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
# This register a callback for ALL services who inherit from `NiftyServices::BaseCreateService`
|
53
|
+
# In other words: Every and all records created in my application will be tracked
|
54
|
+
# I can believe that is easy like this, I need a beer right now!
|
55
|
+
NiftyServices::BaseCreateService.register_callback(:after_success, :create_origin_for_record) do
|
56
|
+
create_origin(@record, @options)
|
57
|
+
end
|
58
|
+
|
59
|
+
```
|
60
|
+
|
61
|
+
Now, every record created in my application will have an associated `origin` object, really simple and cool!
|
62
|
+
|
63
|
+
---
|
64
|
+
|
65
|
+
### Next
|
66
|
+
|
67
|
+
See [Configuration](./configuration.md)
|
data/docs/cli.md
ADDED
@@ -0,0 +1,13 @@
|
|
1
|
+
# NiftyServices documentation
|
2
|
+
|
3
|
+
---
|
4
|
+
|
5
|
+
## :question: CLI Generators <a name="cli-generators"></a>
|
6
|
+
|
7
|
+
Currently NiftyServices don't have CLI(command line interface) generators, but is in the roadmap, so keep your eyes here!
|
8
|
+
|
9
|
+
---
|
10
|
+
|
11
|
+
### Next
|
12
|
+
|
13
|
+
See [Install on your Ruby Application](../README.md#installation)
|
@@ -0,0 +1,62 @@
|
|
1
|
+
# NiftyServices documentation
|
2
|
+
|
3
|
+
---
|
4
|
+
|
5
|
+
## :construction: Configuration :construction:
|
6
|
+
|
7
|
+
There are only a few things you must want and have to configure for your services work properly, below you can see all needed configuration:
|
8
|
+
|
9
|
+
```ruby
|
10
|
+
NiftyServices.config do |config|
|
11
|
+
# [optional]
|
12
|
+
# global logger for all services
|
13
|
+
# [Default: Logger.new('/dev/null')]
|
14
|
+
config.logger = Logger.new('log/services_logger.log')
|
15
|
+
|
16
|
+
# [Optional - Default: `'nifty_services'`]
|
17
|
+
# Set a custom I18n lookup namespace for error messages
|
18
|
+
config.i18n_namespace = 'my_app'
|
19
|
+
|
20
|
+
# [Optional - Default: `save`]
|
21
|
+
# Set the method called when saving a record using `BaseCreateService`
|
22
|
+
# The default value is `save`, this will call:`record.save`
|
23
|
+
# Eg: If you want to use `#persist`(`record.persist`), use:
|
24
|
+
config.save_record_method = :persist
|
25
|
+
|
26
|
+
# But you can pass any object that responds to `#call` method, like a Proc:
|
27
|
+
# This way, NiftyServices will call the method sending the record as argument.
|
28
|
+
config.save_record_method = ->(record) {
|
29
|
+
record.save_in_database!
|
30
|
+
}
|
31
|
+
|
32
|
+
# [Optional - Default: `delete`]
|
33
|
+
# Set the method called when deleting a record using BaseDeleteService
|
34
|
+
# The default value is `delete`, this will call:`record.delete`
|
35
|
+
# Eg: If you want to use `#destroy`(`record.destroy`), use:
|
36
|
+
config.delete_record_method = :destroy
|
37
|
+
|
38
|
+
# But you can pass any object that responds to `#call` method, like a Proc:
|
39
|
+
# This way, NiftyServices will call the method sending the record as argument.
|
40
|
+
config.delete_record_method = ->(record) {
|
41
|
+
record.remove
|
42
|
+
}
|
43
|
+
|
44
|
+
# [Optional - Default: `update`]
|
45
|
+
# Set the method called when updating a record using BaseUpdateService
|
46
|
+
# The default value is `update`, this will call:`record.update(attributes)`
|
47
|
+
# Eg: If you want to use `sync`(`record.sync(attributes)`), use:
|
48
|
+
config.update_record_method = :sync
|
49
|
+
|
50
|
+
# But you can pass any object that responds to `#call` method, like a Proc:
|
51
|
+
# This way, NiftyServices will call this block sending the record and attributes as arguments.
|
52
|
+
config.update_record_method = ->(record, attributes) {
|
53
|
+
record.update_attributes(attributes)
|
54
|
+
}
|
55
|
+
end
|
56
|
+
```
|
57
|
+
|
58
|
+
---
|
59
|
+
|
60
|
+
### Next
|
61
|
+
|
62
|
+
See [Web Frameworks Integration](./webframeworks_integration.md)
|
@@ -0,0 +1,534 @@
|
|
1
|
+
# NiftyServices documentation
|
2
|
+
|
3
|
+
---
|
4
|
+
|
5
|
+
## CRUD Services
|
6
|
+
|
7
|
+
So, until now we saw how to use `NiftyServices::BaseService` to create generic services to couple specific domain logic for actions, this is very usefull, but things get a lot better when you're working with **CRUD** actions for your api.
|
8
|
+
|
9
|
+
Follow an example of **Create, Update and Delete** CRUD services for `Post` resource:
|
10
|
+
|
11
|
+
## :white_check_mark: CRUD: Create
|
12
|
+
|
13
|
+
By default, NiftyServices expect that record responds to `new` class method. (eg: `Post.new`).
|
14
|
+
|
15
|
+
NiftyServices will expect that record respond to `#save` instance method when trying to save the record after creating it, you can override this behavior using the method `save_record` or using global configuration:
|
16
|
+
|
17
|
+
```ruby
|
18
|
+
NiftyServices.config do |config|
|
19
|
+
# The default value is `save`, this will call:`record.save`
|
20
|
+
# Eg: If you want to use `#persist`(`record.persist`), use:
|
21
|
+
config.save_record_method = :persist
|
22
|
+
|
23
|
+
# But you can pass any object that responds to `#call` method, like a Proc:
|
24
|
+
# This way, NiftyServices will call the method sending the record as argument.
|
25
|
+
config.save_record_method = ->(record) {
|
26
|
+
record.save_in_database!
|
27
|
+
}
|
28
|
+
end
|
29
|
+
```
|
30
|
+
|
31
|
+
|
32
|
+
```ruby
|
33
|
+
class PostCreateService < NiftyServices::BaseCreateService
|
34
|
+
|
35
|
+
attr_reader :user
|
36
|
+
|
37
|
+
# You can freely override initialize method to receive more arguments
|
38
|
+
def initialize(user, options = {})
|
39
|
+
@user = user
|
40
|
+
super(options)
|
41
|
+
end
|
42
|
+
|
43
|
+
# record_type must be a object respond to :build and :save methods
|
44
|
+
# is possible to access this record outside of service using
|
45
|
+
# `service.record` or `service.post`
|
46
|
+
# if you want to create a custom alias name, use:
|
47
|
+
# record_type Post, alias_name: :user_post
|
48
|
+
# This way, you can access the record using
|
49
|
+
# `service.user_post`
|
50
|
+
|
51
|
+
record_type Post
|
52
|
+
|
53
|
+
WHITELIST_ATTRIBUTES = [:title, :content]
|
54
|
+
|
55
|
+
whitelist_attributes WHITELIST_ATTRIBUTES
|
56
|
+
|
57
|
+
|
58
|
+
# use custom scope to create the record
|
59
|
+
# scope returned below must respond_to :build method
|
60
|
+
def build_record_scope
|
61
|
+
@user.posts
|
62
|
+
end
|
63
|
+
|
64
|
+
# this key is used for I18n translations, you don't need to override or implement
|
65
|
+
# NiftyService will try to inflect this using `record_type.to_s.underscore + 's'`
|
66
|
+
# So, if your record type is `Post`, `record_error_key` will be `posts`
|
67
|
+
def record_error_key
|
68
|
+
:posts
|
69
|
+
end
|
70
|
+
|
71
|
+
# This method is strict required by NiftyServices, each service must implement
|
72
|
+
def can_create_record?
|
73
|
+
# Checking user before trying to create a post for this same user
|
74
|
+
unless valid_user?
|
75
|
+
return not_found_error!('users.not_found')
|
76
|
+
end
|
77
|
+
|
78
|
+
return !duplicated?
|
79
|
+
end
|
80
|
+
|
81
|
+
def duplicated?
|
82
|
+
# (here you can do any kind of validation, eg:)
|
83
|
+
# check if user is trying to recreate a recent resource
|
84
|
+
# this will return false if user has already created a post with
|
85
|
+
# this title in the last 30 seconds (usefull to ban bots)
|
86
|
+
@user.posts.exists(title: record_allowed_attributes[:title], created_at: "NOW() - interval(30 seconds)")
|
87
|
+
end
|
88
|
+
|
89
|
+
# This is a custom method of this class, not NiftyService stuff
|
90
|
+
def valid_user?
|
91
|
+
# `valid_object?` signature: `valid_object?(object, expected_class)`
|
92
|
+
valid_object?(@user, User)
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
service = PostCreateService.new(User.first, title: 'Teste', content: 'Post example content')
|
97
|
+
|
98
|
+
service.execute
|
99
|
+
|
100
|
+
service.success? # true
|
101
|
+
service.response_status_code # 201
|
102
|
+
service.response_status # :created
|
103
|
+
```
|
104
|
+
|
105
|
+
|
106
|
+
|
107
|
+
#### :earth_americas: I18n setup
|
108
|
+
|
109
|
+
You must have the following keys setup up in your locales files:
|
110
|
+
|
111
|
+
```yml
|
112
|
+
nifty_services:
|
113
|
+
users:
|
114
|
+
not_found: "Invalid or not found user"
|
115
|
+
ip_temporarily_blocked: "This IP is temporarily blocked from creating records"
|
116
|
+
# note: posts is the key return in `record_error_key` service method
|
117
|
+
posts:
|
118
|
+
cant_create: "Can't create this record"
|
119
|
+
```
|
120
|
+
|
121
|
+
#### :alien: Invalid user <a name="create-resource-user-invalid"></a>
|
122
|
+
|
123
|
+
If you try to create a post for a invalid user, such as:
|
124
|
+
|
125
|
+
```ruby
|
126
|
+
# PostCreateService.new(user, options)
|
127
|
+
service = PostCreateService.new(nil, options)
|
128
|
+
service.execute
|
129
|
+
|
130
|
+
service.success? # false
|
131
|
+
service.response_status # :not_found
|
132
|
+
service.response_status_code # 404
|
133
|
+
service.errors # ["Invalid or not found user"]
|
134
|
+
```
|
135
|
+
|
136
|
+
#### :no_entry_sign: Not authorized to create
|
137
|
+
|
138
|
+
Or if user is trying to create a duplicate resource:
|
139
|
+
|
140
|
+
```ruby
|
141
|
+
# PostCreateService.new(user, options)
|
142
|
+
service = PostCreateService.new(User.first, options)
|
143
|
+
service.execute
|
144
|
+
|
145
|
+
service.success? # false
|
146
|
+
service.errors # ["User cant create this record"]
|
147
|
+
service.response_status # :forbidden_error
|
148
|
+
service.response_status_code # 400
|
149
|
+
```
|
150
|
+
|
151
|
+
#### :boom: Record is invalid
|
152
|
+
|
153
|
+
Eg: if any validation in Post model won't pass:
|
154
|
+
|
155
|
+
```ruby
|
156
|
+
# PostCreateService.new(user, options)
|
157
|
+
# Post model as the validation:
|
158
|
+
# validates_presence_of :title, :content
|
159
|
+
service = PostCreateService.new(User.first, title: nil, content: nil)
|
160
|
+
service.execute
|
161
|
+
|
162
|
+
service.success? # false
|
163
|
+
|
164
|
+
service.errors # => [{ title: 'is empty', content: 'is empty' }]
|
165
|
+
|
166
|
+
service.response_status # :unprocessable_entity
|
167
|
+
service.response_status_code # 422
|
168
|
+
```
|
169
|
+
---
|
170
|
+
|
171
|
+
## :white_check_mark: CRUD: Update
|
172
|
+
|
173
|
+
By default, NiftyServices expect that record responds to `update` class method. (eg: `Post.update(id, data)`)
|
174
|
+
|
175
|
+
You can override this behavior using `update_record` method in your service class, or using global configuration:
|
176
|
+
|
177
|
+
```ruby
|
178
|
+
NiftyServices.config do |config|
|
179
|
+
# Set the method called when updating a record using BaseUpdateService
|
180
|
+
# Eg: If you want to use `sync`(`record.sync(attributes)`), use:
|
181
|
+
config.update_record_method = :sync
|
182
|
+
|
183
|
+
# But you can pass any object that responds to `#call` method, like a Proc:
|
184
|
+
# This way, NiftyServices will call the method sending the record and attributes.
|
185
|
+
config.update_record_method = ->(record, attributes) {
|
186
|
+
record.update_attributes(attributes)
|
187
|
+
}
|
188
|
+
end
|
189
|
+
```
|
190
|
+
|
191
|
+
For validation, NiftyServices expect that record respond to `#valid?` and `#errors` instance methods.
|
192
|
+
|
193
|
+
You can override this behavior using `success_updated?` and `update_errors` methods.
|
194
|
+
|
195
|
+
|
196
|
+
```ruby
|
197
|
+
class PostUpdateService < NiftyServices::BaseUpdateService
|
198
|
+
|
199
|
+
attr_reader :user
|
200
|
+
|
201
|
+
# service.post or service.record
|
202
|
+
record_type Post
|
203
|
+
|
204
|
+
WHITELIST_ATTRIBUTES = [:title, :content]
|
205
|
+
|
206
|
+
whitelist_attributes WHITELIST_ATTRIBUTES
|
207
|
+
|
208
|
+
# You can freely override initialize method to receive more arguments
|
209
|
+
def initialize(record, user, options = {})
|
210
|
+
@user = user
|
211
|
+
super(record, options)
|
212
|
+
end
|
213
|
+
|
214
|
+
# This method is strict required by NiftyServices, each service must implement
|
215
|
+
def can_update_record?
|
216
|
+
unless valid_user?
|
217
|
+
return not_found_error!('users.not_found')
|
218
|
+
end
|
219
|
+
|
220
|
+
return user_has_permission?
|
221
|
+
end
|
222
|
+
|
223
|
+
def user_has_permission?
|
224
|
+
# only system admins and owner can update this record
|
225
|
+
# or you can transfer the logic below to something like:
|
226
|
+
# @record.user_can_update_record?(@user)
|
227
|
+
@user.admin? || @user.id == @record.id
|
228
|
+
end
|
229
|
+
|
230
|
+
# this key is used for I18n translations, you don't need to override or implement
|
231
|
+
def record_error_key
|
232
|
+
:posts
|
233
|
+
end
|
234
|
+
|
235
|
+
# This is a custom method of this class, not NiftyService stuff
|
236
|
+
def valid_user?
|
237
|
+
valid_object?(@user, User)
|
238
|
+
end
|
239
|
+
end
|
240
|
+
|
241
|
+
# :user_id will be ignored since it's not in whitelisted attributes
|
242
|
+
# this can safe yourself from parameter inject attacks, by default
|
243
|
+
update_service = PostUpdateService.new(Post.first, User.first, title: 'Changing title', content: 'Updating content', user_id: 2)
|
244
|
+
|
245
|
+
update_service.execute
|
246
|
+
|
247
|
+
update_service.success? # true
|
248
|
+
update_service.response_status # :ok
|
249
|
+
update_service.response_status_code # 200
|
250
|
+
|
251
|
+
update_service.changed_attributes # [:title, :content]
|
252
|
+
update_service.changed? # true
|
253
|
+
```
|
254
|
+
|
255
|
+
#### :earth_asia: I18n setup
|
256
|
+
|
257
|
+
Your locale file must have the following keys:
|
258
|
+
|
259
|
+
```yml
|
260
|
+
posts:
|
261
|
+
not_found: "Invalid or not found post"
|
262
|
+
cant_update: "Can't update this record"
|
263
|
+
users:
|
264
|
+
not_found: "Invalid or not found user"
|
265
|
+
```
|
266
|
+
|
267
|
+
#### :alien: User is invalid <a name="update-resource-user-invalid"></a>
|
268
|
+
|
269
|
+
Response when owner user is not valid:
|
270
|
+
|
271
|
+
```ruby
|
272
|
+
# PostUpdateService.new(post, user, params)
|
273
|
+
update_service = PostUpdateService.new(Post.first, nil, title: 'Changing title', content: 'Updating content')
|
274
|
+
|
275
|
+
update_service.execute
|
276
|
+
|
277
|
+
update_service.success? # false
|
278
|
+
update_service.response_status # :not_found_error
|
279
|
+
update_service.response_status_code # 404
|
280
|
+
|
281
|
+
update_service.errors # ["Invalid or not found user"]
|
282
|
+
```
|
283
|
+
|
284
|
+
#### :closed_lock_with_key: Resource (Post) don't belongs to user <a name="update-resource-dont-belongs-to-user"></a>
|
285
|
+
|
286
|
+
Responses when trying to update to update a resource who don't belongs to owner:
|
287
|
+
|
288
|
+
```ruby
|
289
|
+
# PostUpdateService.new(post, user, params)
|
290
|
+
update_service = PostUpdateService.new(Post.first, User.last, title: 'Changing title', content: 'Updating content')
|
291
|
+
|
292
|
+
update_service.execute
|
293
|
+
|
294
|
+
update_service.success? # false
|
295
|
+
update_service.response_status # :forbidden
|
296
|
+
update_service.response_status_code # 400
|
297
|
+
|
298
|
+
update_service.changed_attributes # []
|
299
|
+
update_service.changed? # false
|
300
|
+
update_service.errors # ["User can't update this record"]
|
301
|
+
```
|
302
|
+
|
303
|
+
#### :santa: Resource don't exists <a name="update-resource-dont-exists"></a>
|
304
|
+
|
305
|
+
Response when post don't exists:
|
306
|
+
|
307
|
+
```ruby
|
308
|
+
# PostUpdateService.new(post, user, params)
|
309
|
+
update_service = PostUpdateService.new(nil, User.last, title: 'Changing title', content: 'Updating content')
|
310
|
+
|
311
|
+
update_service.execute
|
312
|
+
|
313
|
+
update_service.success? # false
|
314
|
+
update_service.response_status # :not_found_error
|
315
|
+
update_service.response_status_code # 404
|
316
|
+
|
317
|
+
update_service.errors # ["Invalid or not found post"]
|
318
|
+
```
|
319
|
+
|
320
|
+
---
|
321
|
+
|
322
|
+
## :white_check_mark: CRUD: Delete
|
323
|
+
|
324
|
+
|
325
|
+
By default, NiftyServices expect that record responds to `delete` instance method. (eg: `post.delete`)
|
326
|
+
|
327
|
+
You can override this behavior using `delete_record` method in your Service class, or using global configuration:
|
328
|
+
|
329
|
+
```ruby
|
330
|
+
NiftyServices.config do |config|
|
331
|
+
# The default value is `delete`, this will call:`record.delete`
|
332
|
+
# Eg: If you want to use `#destroy`(`record.destroy`), use:
|
333
|
+
config.delete_record_method = :destroy
|
334
|
+
|
335
|
+
# But you can pass any object that responds to `#call` method, like a Proc:
|
336
|
+
# This way, NiftyServices will call the method sending the record
|
337
|
+
config.delete_record_method = ->(record) {
|
338
|
+
record.remove
|
339
|
+
}
|
340
|
+
end
|
341
|
+
```
|
342
|
+
|
343
|
+
|
344
|
+
```ruby
|
345
|
+
class PostDeleteService < NiftyServices::BaseDeleteService
|
346
|
+
|
347
|
+
attr_reader :user
|
348
|
+
|
349
|
+
# record_type object must respond to :delete method
|
350
|
+
# But you can override `delete_method` method to do whatever you want
|
351
|
+
record_type Post
|
352
|
+
|
353
|
+
# You can freely override initialize method to receive more arguments
|
354
|
+
def initialize(record, user, options = {})
|
355
|
+
@user = user
|
356
|
+
super(record, options)
|
357
|
+
end
|
358
|
+
|
359
|
+
# this key is used for I18n translations, you don't need to override or implement
|
360
|
+
def record_error_key
|
361
|
+
:posts
|
362
|
+
end
|
363
|
+
|
364
|
+
# below the code used internally, you can override to
|
365
|
+
# create custom delete, but remembers that this method
|
366
|
+
# must return a boolean value
|
367
|
+
def delete_record
|
368
|
+
@record.delete
|
369
|
+
end
|
370
|
+
|
371
|
+
# This method is strict required by NiftyServices, each service must implement
|
372
|
+
def can_delete_record?
|
373
|
+
# Checking user before trying to create a post for this same user
|
374
|
+
unless valid_user?
|
375
|
+
return not_found_error!('users.not_found')
|
376
|
+
end
|
377
|
+
|
378
|
+
return user_has_permission?
|
379
|
+
end
|
380
|
+
|
381
|
+
def user_has_permission?
|
382
|
+
# only system admins and owner can delete this record
|
383
|
+
# or you can transfer the logic below something like:
|
384
|
+
# @record.user_can_delete_record?(@user)
|
385
|
+
@user.admin? || @user.id == @record.id
|
386
|
+
end
|
387
|
+
|
388
|
+
# This is a custom method of this class, not NiftyService stuff
|
389
|
+
def valid_user?
|
390
|
+
valid_object?(@user, User)
|
391
|
+
end
|
392
|
+
end
|
393
|
+
```
|
394
|
+
|
395
|
+
#### :earth_africa: I18n setup
|
396
|
+
|
397
|
+
Your locale file must have the following keys:
|
398
|
+
|
399
|
+
```yml
|
400
|
+
posts:
|
401
|
+
not_found: "Invalid or not found post"
|
402
|
+
cant_delete: "Can't delete this record"
|
403
|
+
users:
|
404
|
+
not_found: "Invalid or not found user"
|
405
|
+
```
|
406
|
+
|
407
|
+
#### :alien: User is invalid <a name="delete-resource-user-invalid"></a>
|
408
|
+
|
409
|
+
Response when owner user is not valid:
|
410
|
+
|
411
|
+
```ruby
|
412
|
+
# PostDeleteService.new(post, user, params)
|
413
|
+
delete_service = PostDeleteService.new(Post.first, nil)
|
414
|
+
|
415
|
+
delete_service.execute
|
416
|
+
|
417
|
+
delete_service.success? # false
|
418
|
+
delete_service.response_status # :not_found_error
|
419
|
+
delete_service.response_status_code # 404
|
420
|
+
|
421
|
+
delete_service.errors # ["Invalid or not found user"]
|
422
|
+
```
|
423
|
+
|
424
|
+
#### :closed_lock_with_key: Resource don't belongs to user <a name="delete-resource-dont-belongs-to-user"></a>
|
425
|
+
|
426
|
+
Responses when trying to delete a resource who don't belongs to owner:
|
427
|
+
|
428
|
+
```ruby
|
429
|
+
# PostDeleteService.new(post, user, params)
|
430
|
+
delete_service = PostDeleteService.new(Post.first, User.last)
|
431
|
+
delete_service.execute
|
432
|
+
|
433
|
+
delete_service.success? # false
|
434
|
+
delete_service.response_status # :forbidden
|
435
|
+
delete_service.response_status_code # 400
|
436
|
+
delete_service.errors # ["User can't delete this record"]
|
437
|
+
```
|
438
|
+
|
439
|
+
#### :santa: Resource(Post) don't exists <a name="delete-resource-dont-exists"></a>
|
440
|
+
|
441
|
+
Response when post don't exists:
|
442
|
+
|
443
|
+
```ruby
|
444
|
+
# PostDeleteService.new(post, user, params)
|
445
|
+
delete_service = PostDeleteService.new(nil, User.last)
|
446
|
+
|
447
|
+
delete_service.execute
|
448
|
+
|
449
|
+
delete_service.success? # false
|
450
|
+
delete_service.response_status # :not_found_error
|
451
|
+
delete_service.response_status_code # 404
|
452
|
+
|
453
|
+
delete_service.errors # ["Invalid or not found post"]
|
454
|
+
```
|
455
|
+
|
456
|
+
---
|
457
|
+
|
458
|
+
#### Tip
|
459
|
+
|
460
|
+
You can DRY the examples above using `concerns`, if you take a look you will see
|
461
|
+
that in `PostUpdateService` and `PostDeleteService` same validation
|
462
|
+
methods are repeated, so lets improve this, first thing is create a Ruby plain module:
|
463
|
+
|
464
|
+
```ruby
|
465
|
+
module PostCrudExtensions
|
466
|
+
# You can freely override initialize method to receive more arguments
|
467
|
+
def initialize(record, user, options = {})
|
468
|
+
@user = user
|
469
|
+
super(record, options)
|
470
|
+
end
|
471
|
+
|
472
|
+
def record_allowed_attributes
|
473
|
+
WHITELIST_ATTRIBUTES
|
474
|
+
end
|
475
|
+
|
476
|
+
def self.whitelist_attributes
|
477
|
+
WHITELIST_ATTRIBUTES
|
478
|
+
end
|
479
|
+
|
480
|
+
def user_has_permission?
|
481
|
+
# only system admins and owner can update this record
|
482
|
+
# or you can transfer the logic below to something like:
|
483
|
+
# @record.user_can_update_record?(@user)
|
484
|
+
@user.admin? || @user.id == @record.id
|
485
|
+
end
|
486
|
+
|
487
|
+
# this key is used for I18n translations, you don't need to override or implement
|
488
|
+
def record_error_key
|
489
|
+
:posts
|
490
|
+
end
|
491
|
+
|
492
|
+
def valid_user?
|
493
|
+
valid_object?(@user, User)
|
494
|
+
end
|
495
|
+
end
|
496
|
+
```
|
497
|
+
|
498
|
+
The second step, is call class method `concern` in Service class, lets use `PostDeleteService`
|
499
|
+
|
500
|
+
```ruby
|
501
|
+
class PostDeleteService < NiftyServices::BaseDeleteService
|
502
|
+
|
503
|
+
# Include shared CRUD methods to Post's CRUD Services
|
504
|
+
concern PostCrudExtensions
|
505
|
+
|
506
|
+
# record_type object must respond to :delete method
|
507
|
+
# But you can override `delete_method` method to do whatever you want
|
508
|
+
|
509
|
+
# below the code used internally, you can override to
|
510
|
+
# create custom delete, but remembers that this method
|
511
|
+
# must return a boolean value
|
512
|
+
def delete_record
|
513
|
+
@record.delete
|
514
|
+
end
|
515
|
+
|
516
|
+
# This method is strict required by NiftyServices, each service must implement
|
517
|
+
def can_delete_record?
|
518
|
+
# Checking user before trying to create a post for this same user
|
519
|
+
unless valid_user?
|
520
|
+
return not_found_error!('users.not_found')
|
521
|
+
end
|
522
|
+
|
523
|
+
return user_has_permission?
|
524
|
+
end
|
525
|
+
end
|
526
|
+
```
|
527
|
+
|
528
|
+
The code is much more readable and DRY now.
|
529
|
+
|
530
|
+
Now, you can share `PostCrudExtensions` will all others Post related CRUD services.
|
531
|
+
|
532
|
+
### Next
|
533
|
+
|
534
|
+
See [I18n Configuration](./i18n.md)
|