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.
@@ -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)