nifty_services 0.0.5 → 0.0.6

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,55 @@
1
+ # NiftyServices documentation
2
+
3
+ ---
4
+
5
+ ## :us: :fr: :jp: I18n Support :uk: :es: :de:
6
+
7
+ As you see in the above examples, with `NiftyServices` you can respond in multiples languages for the same service error messages, by default your locales config file must be configured as:
8
+
9
+ ```yml
10
+ # attention: dont use `resource_type`
11
+ # use the key setup up in `record_error_key` methods
12
+ resource_type:
13
+ not_found: "Invalid or not found post"
14
+ cant_create: "Can't delete this record"
15
+ cant_read: "Can't access this record"
16
+ cant_update: "Can't delete this record"
17
+ cant_delete: "Can't delete this record"
18
+ users:
19
+ not_found: "Invalid or not found user"
20
+ ```
21
+
22
+ You can configure the default I18n namespace using configuration:
23
+
24
+ ```ruby
25
+ NiftyServies.configure do |config|
26
+ config.i18n_namespace = :my_app
27
+ end
28
+ ```
29
+
30
+ Example config for `Post` and `Comment` resources using `my_app` locale namespace:
31
+
32
+ ```yml
33
+ # default is nifty_services
34
+ my_app:
35
+ errors:
36
+ default_crud: &default_crud
37
+ cant_create: "Can't delete this record"
38
+ cant_read: "Can't access this record"
39
+ cant_update: "Can't delete this record"
40
+ cant_delete: "Can't delete this record"
41
+ users:
42
+ not_found: "Invalid or not found user"
43
+ posts:
44
+ <<: *default_crud
45
+ not_found: "Invalid or not found post"
46
+ comments:
47
+ <<: *default_crud
48
+ not_found: "Invalid or not found comment"
49
+ ```
50
+
51
+ ---
52
+
53
+ ### Next
54
+
55
+ See [Callbacks](./callbacks.md)
@@ -0,0 +1,271 @@
1
+ # NiftyServices documentation
2
+
3
+ ---
4
+
5
+ ## :pray: Basic Service Markups :raised_hands:
6
+
7
+ Here, for your convenience and sanity all basic service structures for reference when you start a brand new Service.
8
+ Most of time, the best way is to copy all content from each service described below and change according to your needs.
9
+
10
+ ### BaseCreateService Basic Markup
11
+
12
+ ```ruby
13
+ class SomeCreateService < NiftyServices::BaseCreateService
14
+ # [Required]
15
+ # remember that inside the Service you always can use
16
+ # @record variable to access current record
17
+ # and from outside (service instance):
18
+ # service.record or service.record_type
19
+ # eg:
20
+ # record_type BlogPost
21
+ # service.record # BlogPost.new(...)
22
+ # service.blog_post # BlogPost.new(...)
23
+ # service.record == service.blog_post # true
24
+ # alias_name can be used to create a custom alias name
25
+ # eg:
26
+ # record_type BlogPost, alias_name: :post
27
+ # service.record # BlogPost.new(...)
28
+ # service.post # BlogPost.new(...)
29
+ # service.record == service.post # true
30
+
31
+ record_type RecordType, alias_name: :my_custom_alias_name
32
+
33
+ ## Its a good convention to create a constant and not returning the plain
34
+ ## array in `record_attributes_whitelist` since this can be accessed from others
35
+ ## places, as: `SomeCreateService::WHITELIST_ATTRIBUTES`
36
+ WHITELIST_ATTRIBUTES = [
37
+ :safe_attribute_1,
38
+ :safe_attribute_2,
39
+ ]
40
+
41
+ # [Required]
42
+ # You must setup whitelist attributes or define the method `record_attributes_whitelist` method
43
+ whitelist_attributes WHITELIST_ATTRIBUTES
44
+
45
+ # You can freely override initialize method to receive more arguments
46
+ def initialize(record, user, options = {})
47
+ @user = user
48
+ super(record, options)
49
+ end
50
+
51
+ private
52
+ # [Optional]
53
+ # this key is used for I18n translations, you don't need to override or implement
54
+ # NiftyService will try to inflect this using `record_type.to_s.underscore + 's'`
55
+
56
+ # So, if your record type is `Post`, `record_error_key` will be `posts`
57
+ def record_error_key
58
+ :posts
59
+ end
60
+
61
+ # [Required]
62
+ # Always validate if service can be executed based on your rules and ACL
63
+ # If this method is not implemented a NotImplementedError exception will be raised
64
+ def can_create_record?
65
+ unless valid_user?
66
+ return not_found_error!('users.not_found')
67
+ end
68
+
69
+ return forbidden_error!('errors.some_error') if (some_validation)
70
+
71
+ return bad_request_error!('errors.some_other_error') if (another_validation)
72
+
73
+ # remember to return true after all validations
74
+ # if you don't return true Service will not be able to create the record
75
+ return true
76
+ end
77
+
78
+ # [Optional]
79
+ # method called when save_error method call raises an exception
80
+ # this ocurr for example with ActiveRecord objects
81
+ # default: unprocessable_entity_error!(error)
82
+ def on_save_record_error(error)
83
+ logger.error(error)
84
+
85
+ if error.is_a?(ActiveRecord::RecordNotUnique)
86
+ return unprocessable_entity_error!(%s(posts.duplicate_record))
87
+ end
88
+ end
89
+
90
+ # [Optional]
91
+ # custom scope for record, eg: @user.posts
92
+ # default is nil
93
+ def build_record_scope
94
+ nil
95
+ end
96
+
97
+ def valid_user?
98
+ valid_object?(@user, User)
99
+ end
100
+ end
101
+ ```
102
+
103
+ ### BaseUpdateService Basic Markup
104
+
105
+ ```ruby
106
+ class SomeUpdateService < NiftyServices::BaseUpdateService
107
+
108
+ attr_reader :user
109
+
110
+ # [Required]
111
+ record_type RecordType, alias_name: :custom_alias_name
112
+
113
+ # You can freely override initialize method to receive more arguments
114
+ def initialize(record, user, options = {})
115
+ @user = user
116
+ super(record, options)
117
+ end
118
+
119
+ ## Its a good convention to create a constant and not returning the plain
120
+ ## array in `record_attributes_whitelist` since this can be accessed from others
121
+ ## places, as: `SomeCreateService::WHITELIST_ATTRIBUTES`
122
+ WHITELIST_ATTRIBUTES = [
123
+ :safe_attribute_1,
124
+ :safe_attribute_2,
125
+ ]
126
+
127
+ # [Required]
128
+ # You must setup whitelist attributes or define the method `record_attributes_whitelist` method
129
+ whitelist_attributes WHITELIST_ATTRIBUTES
130
+
131
+ private
132
+
133
+ # [Required]
134
+ # When a new instance of Service is created, the @options variables receive some
135
+ # values, eg: { user: { email: "...", name: "...."} }
136
+ # use record_attributes_hash to tell the Service from where to pull theses values
137
+ # eg: @options.fetch(:user, {})
138
+ # If this method is not implemented a NotImplementedError exception will be raised
139
+ def record_attributes_hash
140
+ @options.fetch(:data, {})
141
+ end
142
+
143
+ # [required]
144
+ # This is a VERY IMPORTANT point of attention
145
+ # always verify if @user has permissions to update the current @record object
146
+ # If this method is not implemented a NotImplementedError exception will be raised
147
+ def can_update_record?
148
+ @record.user_id == @user.id
149
+ end
150
+
151
+ # [Optional]
152
+ # This is the default implementation of update record, you may overwrite it
153
+ # to to custom updates (MOST OF TIME YOU DONT NEED TO DO THIS)
154
+ # only change this if you know what you are really doing
155
+ def update_record
156
+ @record.class.send(:update, @record.id, record_allowed_attributes)
157
+ end
158
+
159
+ # [optional]
160
+ # Any callback is optional, this is just a example
161
+ def after_success
162
+ if changed?
163
+ logger.info 'Successfully update record ID %s' % @record.id
164
+ logger.info 'Changed attributes are %s' % changed_attributes
165
+ end
166
+ end
167
+ end
168
+ ```
169
+
170
+ ---
171
+
172
+ ### BaseDeleteService Basic Markup
173
+
174
+ ```ruby
175
+ class SomeDeleteService < NiftyServices::BaseDeleteService
176
+
177
+ # You can freely override initialize method to receive more arguments
178
+ def initialize(record, user, options = {})
179
+ @user = user
180
+ super(record, options)
181
+ end
182
+
183
+ # [Required]
184
+ # record_type object must respond to :delete method
185
+ # But you can override `delete_method` method to do whatever you want
186
+ record_type RecordType, alias_name: :custom_alias_name
187
+
188
+ private
189
+
190
+ # [Required]
191
+ # This is a VERY IMPORTANT point of attention
192
+ # always verify if @user has permissions to delete the current @record object
193
+ # Hint: if @record respond_to `user_can_delete?(user)` you can remove this
194
+ # method and do the validation inside `user_can_delete(user)` method in @record
195
+ # If this method is not implemented a NotImplementedError exception will be raised
196
+ def can_delete_record?
197
+ @record.user_id == @user.id
198
+ end
199
+
200
+ # [optional]
201
+ # Any callback is optional, this is just a example
202
+ def after_success
203
+ logger.info('Successfully Deleted resource ID %s' % @record.id)
204
+ end
205
+
206
+ # [Optional]
207
+ # This is the default implementation of delete record, you may overwrite it
208
+ # to do custom delete (MOST OF TIME YOU DONT NEED TO DO THIS)
209
+ # only change this if you know what you are really doing
210
+ def delete_record
211
+ @record.delete
212
+ end
213
+
214
+ end
215
+
216
+ ```
217
+
218
+ ### BaseActionService Basic Markup
219
+
220
+ ```ruby
221
+ class SomeCustomActionService < NiftyServices::BaseActionService
222
+
223
+ # [required]
224
+ # this is the action identifier used internally
225
+ # and to generate error messages
226
+ # see: invalid_action_error_key method
227
+ action_name :custom_action_name
228
+
229
+ private
230
+ # [Required]
231
+ # Always validate if Service can execute the action
232
+ # This method MUST return a boolean value indicating if Service can or not
233
+ # run the method `execute_service_action`
234
+ # If this method is not implemented a NotImplementedError exception will be raised
235
+ def can_execute_action?
236
+ # do some specific validation here, you can return errors such:
237
+ # return not_found_error!(%(users.invalid_user)) # returns false and avoid execution
238
+ return true
239
+ end
240
+
241
+ # [Required]
242
+ # The core function of BaseActionServices
243
+ # This method is called when all validations passes, so here you can put
244
+ # all logic for Service (eg: send mails, clear logs, any kind of action you want)
245
+ # If this method is not implemented a NotImplementedError exception will be raised
246
+ def execute_service_action
247
+ # (do some complex stuff)
248
+ end
249
+
250
+ # [Optional]
251
+ # You dont need to overwrite this method, just `record_error_key`
252
+ # But it's important you know how final message key will be created
253
+ # using the pattern below
254
+ def invalid_action_error_key
255
+ "#{record_error_key}.cant_execute_#{action_name}"
256
+ end
257
+
258
+ # [Required]
259
+ # Key used to created the error messages for this Service
260
+ # If this method is not implemented a NotImplementedError exception will be raised
261
+ def record_error_key
262
+ :users
263
+ end
264
+ end
265
+ ```
266
+
267
+ ---
268
+
269
+ ### Next
270
+
271
+ See [CLI Generators](./cli.md)
@@ -0,0 +1,204 @@
1
+ # NiftyServices documentation
2
+
3
+ ---
4
+
5
+ ## Usage
6
+
7
+ NiftyServices provide a start basic service class for generic code which is `NiftyServices::BaseService`, the very basic service markup is demonstrated below:
8
+
9
+ ### Basic Service Markup
10
+
11
+
12
+ ```ruby
13
+ class SemanticServiceName < NiftyServices::BaseService
14
+
15
+ def execute
16
+ execute_action do
17
+ success_response if do_something_complex
18
+ end
19
+ end
20
+
21
+ def do_something_complex
22
+ # (...) some complex bussiness logic
23
+ return true
24
+ end
25
+
26
+ private
27
+ def can_execute?
28
+ return forbidden_error!('errors.message_key') if some_condition
29
+
30
+ return not_found_error!('errors.message_key') if another_condition
31
+
32
+ return unprocessable_entity_error!('errors.message_key') if other_condition
33
+
34
+ # ok, this service can be executed
35
+ return true
36
+ end
37
+ end
38
+
39
+ service = SemanticServiceName.new(options)
40
+ service.execute
41
+ ```
42
+
43
+ ---
44
+
45
+ ### Ok, real world example plizzz
46
+
47
+ Lets work with a real and a little more complex example, an Service responsible to send daily news mail to users.
48
+ The code below shows basically everything you need to know about services structure, such: entry point, callbacks, authorization, error and success response handling, so after understanding this little piece of code, you will be **ready to code your own services**!
49
+
50
+ ```ruby
51
+ class DailyNewsMailSendService < NiftyServices::BaseService
52
+
53
+ before_execute do
54
+ log.info('Routine started at: %s' % Time.now)
55
+ end
56
+
57
+ after_execute do
58
+ log.info('Routine ended at: %s' % Time.now)
59
+ end
60
+
61
+ after_initialize do
62
+ user_data = [@user.name, @user.email]
63
+ log.info('Routine Details: Send daily news email to user %s(%s)' % user_data)
64
+ end
65
+
66
+ after_success do
67
+ log.info('Success sent daily news feed email to user')
68
+ end
69
+
70
+ before_error do
71
+ log.warn('Something went wrong')
72
+ end
73
+
74
+ after_error do
75
+ log.error('Error sending email to user. See details below :(')
76
+ log.error(errors)
77
+ end
78
+
79
+ attr_reader :user
80
+
81
+ def initialize(user, options = {})
82
+ @user = user
83
+ super(options)
84
+ end
85
+
86
+ def execute
87
+ execute_action do
88
+ success_response if send_mail_to_user
89
+ end
90
+ end
91
+
92
+ private
93
+ def can_execute?
94
+ unless valid_user?
95
+ # returns false
96
+ return not_found_error!('users.not_found')
97
+ end
98
+
99
+ unless @user.abble_to_receive_daily_news_mail?
100
+ # returns false
101
+ return forbidden_error!('users.already_received_daily_news_mail')
102
+ end
103
+
104
+ return true
105
+ end
106
+
107
+ def send_mail_to_user
108
+ # just to fake, a real implementation could be something like:
109
+ # @user.send_daily_news_mail!
110
+ return true
111
+ end
112
+
113
+ def valid_user?
114
+ # check if object is valid and is a User class type
115
+ valid_object?(@user, User)
116
+ end
117
+
118
+ # you can use `default_options` method to add default { keys => values } to @options
119
+ # so you can use the option_enabled?(key) to verify if option is enabled
120
+ # or option_disabled?(key) to verify is option is disabled
121
+ # This default values can be override when creating new instance of Service, eg:
122
+ # DailyNewsMailSendService.new(User.last, validate_api_key: false)
123
+ def default_options
124
+ { validate_api_key: true }
125
+ end
126
+ end
127
+
128
+ class User < Struct.new(:name, :email)
129
+ # just to play around with results
130
+ def abble_to_receive_daily_news_mail?
131
+ rand(10) < 5
132
+ end
133
+ end
134
+
135
+ user = User.new('Rafael Fidelis', 'rafa_fidelis@yahoo.com.br')
136
+
137
+ # Default logger is NiftyService.config.logger = Logger.new('/dev/null')
138
+ service = DailyNewsMailSendService.new(user, logger: Logger.new('daily_news.log'))
139
+ service.execute
140
+ ```
141
+
142
+ ### Sample outputs results
143
+
144
+ #### :smile: Success:
145
+
146
+ ```
147
+ I, [2016-07-15T17:13:40.092854 #2480] INFO -- : Routine Details: Send daily news email to user
148
+ Rafael Fidelis(rafa_fidelis@yahoo.com.br)
149
+
150
+ I, [2016-07-15T17:13:40.092987 #2480] INFO -- : Routine started at: 2016-07-15 17:13:40 -0300
151
+
152
+ I, [2016-07-15T17:13:40.093143 #2480] INFO -- : Success sent daily news feed email to user
153
+
154
+ I, [2016-07-15T17:13:40.093242 #2480] INFO -- : Routine ended at: 2016-07-15 17:13:40 -0300
155
+
156
+
157
+ ```
158
+
159
+ #### :weary: Error:
160
+
161
+ ```
162
+ I, [2016-07-15T17:12:10.954792 #756] INFO -- : Routine Details: Send daily news email to user
163
+ Rafael Fidelis(rafa_fidelis@yahoo.com.br)
164
+
165
+ I, [2016-07-15T17:12:10.955025 #756] INFO -- : Routine started at: 2016-07-15 17:12:10 -0300
166
+
167
+ W, [2016-07-15T17:12:10.955186 #756] WARN -- : Something went wrong
168
+
169
+ E, [2016-07-15T17:12:11.019645 #756] ERROR -- : Error sending email to user. See details below :(
170
+
171
+ E, [2016-07-15T17:12:11.019838 #756] ERROR -- : ["User has already received daily news mail today"]
172
+
173
+ I, [2016-07-15T17:12:11.020073 #756] INFO -- : Routine ended at: 2016-07-15 17:12:11 -0300
174
+
175
+ ```
176
+
177
+ <br />
178
+
179
+ ### Wrapping things up
180
+
181
+ The code above demonstrate a very basic example of **how dead easy** is to work with Services, let me clarify some things to your better understanding:
182
+
183
+ * &#9745; All services classes must inherit from `NiftyServices::BaseService`
184
+
185
+ * &#9745; For convention(but not a rule) all services must expose only `execute`(and of course, `initialize`) as public methods.
186
+
187
+ * &#9745; `execute_action(&block)` **MUST** be called to properly setup things in execution context.
188
+
189
+ * &#9745; `can_execute?` must be **ALWAYS** implemented in service classes, **ALWAYS**, this ensure that your code will **safely runned**.
190
+ Note: A `NotImplementedError` exception will be raised if service won't define your own `can_execute?` method.
191
+
192
+ * &#9745; There's a very simple helper functions for marking result as success/fail (eg: `unprocessable_entity_error!` or `success_response`).
193
+
194
+ * &#9745; Simple DSL for actions callbacks inside current execution context. (eg: `after_success` or `before_error`)
195
+ Note: You don't need to use the DSL if you don't want, you can simply define the methods(such as: `private def after_success; do_something; end`
196
+
197
+ This is the very basic concept of creating and executing a service object, now we need to know how to work with responses to get the most of our services, for this, let's digg in the mainly public API methods of `NiftyService::BaseService` class:
198
+
199
+
200
+ ---
201
+
202
+ ### Next
203
+
204
+ See [Crud Objects API Interface](./api.md)