nifty_services 0.0.3 → 0.0.4

Sign up to get free protection for your applications and to get access to all the features.
data/README.md CHANGED
@@ -1,8 +1,98 @@
1
- # NiftyServices
1
+ # NiftyServices
2
2
 
3
- Welcome to your new gem! In this directory, you'll find the files you need to be able to package up your Ruby library into a gem. Put your Ruby code in the file `lib/nifty_services`. To experiment with that code, run `bin/console` for an interactive prompt.
3
+ ## Introduction
4
4
 
5
- TODO: Delete this and the text above, and describe your gem
5
+ Nifty Services comes to solve your Ruby applications(*including but not limited to* Rails, Grape, Sinatra, and plain Ruby) code mess with **simplicity in mind**!
6
+
7
+ NiftyServices provider a very nifty, simple & clear API to **organize and reuse** your application **domain logic in plain Ruby Services Objects** turning your codebase in a very extensible, standardized and reusable components.
8
+
9
+ **Most important:** You and your team win what I consider the best benefit when using Nifty Services: **Easily and scalable maintained code.**
10
+ Believe me, you'll fall in :heart_eyes: with this small piece of code, keep reading!
11
+
12
+ This gem was designed and conventioned to be used specially with **Web API applications**, but this is just a convention, you can use it's even with [shoes (for desktop apps)](https://github.com/shoes/shoes) applications if you want, for example.
13
+
14
+ #### :book: I know, this README is very huge
15
+
16
+ As you can see, this README needs some time to be full read, but is very difficulty to explain all things, concepts and philosophy of this gem without writing a lot, we can't escape this :(
17
+
18
+ But remember one thing: This is a **tecnical documentation**, not a blog post, I'm pretty sure you can take 1 or 2 hours + :coffee: to better understand all NiftyServices can do for you and your project. Good reading, and if you have some question, [please let me know](/issues/new).
19
+
20
+ ---
21
+
22
+ ## Table of Contents
23
+
24
+ * [Dafuck is this gem](#introduction)
25
+ * [Conventions](#conventions)
26
+ * [Single Responsability](#conventions-single-responsibility)
27
+ * [Method execution](#hammer-common-and-single-run-execution-method)
28
+ * [Rich Service Objects](#package-rich-service-objects)
29
+ * [Security & Access Level Control](#lock-security---access-control-level)
30
+ * [Installation](#installation)
31
+ * [Usage](#usage)
32
+ * [Basic Service Markup](#basic-service-markup)
33
+ * [How a Service must be created](#wrapping-things-up)
34
+ * [Services API](#services-public-api)
35
+ * [Full Public Service API Methods List](#full-public-api-methods-list)
36
+ * [Handling Success & Error Responses](#success--error-responses)
37
+ * [Success response](#white_check_mark-handling-success-zap)
38
+ * [Error response](#red_circle-handling-error-boom)
39
+ * [Custom error response methods](#custom-error-response-methods)
40
+ * [CRUD Services](#crud-services)
41
+ * [**Create** - BaseCreateService](#white_check_mark-crud-create)
42
+ * [I18n Setup](#earth_americas-i18n-setup)
43
+ * [Error - Invalid User](#alien-invalid-user)
44
+ * [Error - Not authorized](#no_entry_sign-not-authorized-to-create)
45
+ * [Error - Invalid record](#boom-record-is-invalid)
46
+ * [**Update** - BaseUpdateService](#white_check_mark-crud-update)
47
+ * [I18n Setup](#earth_asia-i18n-setup)
48
+ * [Error - Invalid User](#update-resource-user-invalid)
49
+ * [Error - Resource don't belongs to user](#update-resource-dont-belongs-to-user)
50
+ * [Error - Resource dont exists](#update-resource-dont-exists)
51
+ * [**Delete** - BaseDeleteService](#white_check_mark-crud-delete)
52
+ * [I18n Setup](#earth_africa-i18n-setup)
53
+ * [Error - Invalid User](#delete-resource-user-invalid)
54
+ * [Error - Resource don't belongs to user](#delete-resource-dont-belongs-to-user)
55
+ * [Error - Resource dont exists](#delete-resource-dont-exists)
56
+ * [I18n Setup](#us-fr-jp-i18n-support-uk-es-de)
57
+ * [Callbacks](#callbacks)
58
+ * [Using custom callbacks](#creating-custom-callbacks)
59
+ * [Configuration](#construction-configuration-construction)
60
+ * [Web Frameworks integration](#web-frameworks-integrations)
61
+ * [Ruby on Rails](#frameworks-rails)
62
+ * [Grape/Sinatra/Rack](#frameworks-rack)
63
+ * [CLI Generators](#cli-generators)
64
+ * [Roadmap](#roadmap)
65
+ * [Development](#computer-development)
66
+ * [Contributing](#thumbsup-contributing)
67
+ * [License - MIT](#memo-license)
68
+
69
+ ---
70
+
71
+ ## Conventions
72
+
73
+ Below, some very importants things about conventions for this cute :gem: :)
74
+
75
+ ### :white_check_mark: Single responsibility <a name="conventions-single-responsibility"></a>
76
+
77
+ Each service class is responsible for perform exactly [one single task](https://en.wikipedia.org/wiki/Single_responsibility_principle), say goodbye for code (most important: logic) duplication in your code.
78
+ Beside this, one of the aim of NiftyServices is to provide a **very standardized** code architecture, allowing developers to quickly develop and implement new features keeping the application codebase organized and stable.
79
+
80
+ ### :hammer: Common and single-run execution method
81
+
82
+ Each service object must respond to `#execute` instance method, which is allowed to be **called just one time** per instance.
83
+ `#execute` method is responsible to perform code validation(parameter validation, access level control), execution(send mail, register users) and fire callbacks so you can execute hooks actions **after/before success or execution fail**.
84
+
85
+ ### :package: Rich Service Objects
86
+
87
+ When dealing with services objects, you will get a very rich objects to work with, forgot about getting only `true or false` return values, one of the main purpose of objects it's to keep your code domain logic accessible and reusable, so your application can really take the best approach when responding to actions.
88
+
89
+ ### :lock: Security - Access Control Level
90
+
91
+ Think and implement security rules from the first minutes of live in your applications! NiftyServices strongly rely on **Access Control Level(ACL)** to perform actions, in other words, you will only **allow authorized users to read, create, update or delete records in your database**!
92
+
93
+ Now you know the basic concepts and philosophy of `NiftyServices`, lets start working with this candy library?
94
+
95
+ ---
6
96
 
7
97
  ## Installation
8
98
 
@@ -14,28 +104,941 @@ gem 'nifty_services'
14
104
 
15
105
  And then execute:
16
106
 
17
- $ bundle
107
+ $ bundle
18
108
 
19
109
  Or install it yourself as:
20
110
 
21
- $ gem install nifty_services
111
+ $ gem install nifty_services
112
+
113
+ ---
22
114
 
23
115
  ## Usage
24
116
 
25
- TODO: Write usage instructions here
117
+ NiftyServices provide a start basic service class for generic code which is `NiftyServices::BaseService`, the very basic service markup is demonstrated below:
118
+
119
+ ### Basic Service Markup
120
+
121
+
122
+ ```ruby
123
+ class SemanticServiceName < NiftyServices::BaseService
124
+
125
+ def execute
126
+ execute_action do
127
+ success_response if do_something_complex
128
+ end
129
+ end
130
+
131
+ def do_something_complex
132
+ # (...) some complex bussiness logic
133
+ return true
134
+ end
135
+
136
+ private
137
+ def can_execute?
138
+ return forbidden_error!('errors.message_key') if some_condition
139
+
140
+ return not_found_error!('errors.message_key') if another_condition
141
+
142
+ return unprocessable_entity_error('errors.message_key') if other_condition
143
+
144
+ # ok, this service can be executed
145
+ return true
146
+ end
147
+ end
148
+
149
+ service = SemanticServiceName.new(options)
150
+ service.execute
151
+ ```
152
+
153
+ ---
154
+
155
+ ### Ok, real world example plizzz
156
+
157
+ Lets work with a real and a little more complex example, an Service responsible to send daily news mail to users.
158
+ 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**!
159
+
160
+ ```ruby
161
+ class DailyNewsMailSendService < NiftyServices::BaseService
162
+
163
+ before_execute do
164
+ log.info('Routine started at: %s' % Time.now)
165
+ end
166
+
167
+ after_execute do
168
+ log.info('Routine ended at: %s' % Time.now)
169
+ end
170
+
171
+ after_initialize do
172
+ user_data = [@user.name, @user.email]
173
+ log.info('Routine Details: Send daily news email to user %s(%s)' % user_data)
174
+ end
175
+
176
+ after_success do
177
+ log.info('Success sent daily news feed email to user')
178
+ end
179
+
180
+ before_error do
181
+ log.warn('Something went wrong')
182
+ end
183
+
184
+ after_error do
185
+ log.error('Error sending email to user. See details below :(')
186
+ log.error(errors)
187
+ end
188
+
189
+ attr_reader :user
190
+
191
+ def initialize(user, options = {})
192
+ @user = user
193
+ super(options)
194
+ end
195
+
196
+ def execute
197
+ execute_action do
198
+ success_response if send_mail_to_user
199
+ end
200
+ end
201
+
202
+ private
203
+ def can_execute?
204
+ unless valid_user?
205
+ # returns false
206
+ return not_found_error!('users.not_found')
207
+ end
208
+
209
+ unless @user.abble_to_receive_daily_news_mail?
210
+ # returns false
211
+ return forbidden_error!('users.yet_received_daily_news_mail')
212
+ end
213
+
214
+ return true
215
+ end
216
+ def send_mail_to_user
217
+ # just to fake, a real implementation could be something like:
218
+ # @user.send_daily_news_mail!
219
+ return true
220
+ end
221
+
222
+ def valid_user?
223
+ # check if object is valid and is a User class type
224
+ valid_object?(@user, User)
225
+ end
226
+ end
227
+
228
+ class User < Struct.new(:name, :email)
229
+ # just to play around with results
230
+ def abble_to_receive_daily_news_mail?
231
+ rand(10) < 5
232
+ end
233
+ end
234
+
235
+ user = User.new('Rafael Fidelis', 'rafa_fidelis@yahoo.com.br')
236
+
237
+ # Default logger is NiftyService.config.logger = Logger.new('/dev/null')
238
+ service = DailyNewsMailSendService.new(user, logger: Logger.new('daily_news.log'))
239
+ service.execute
240
+ ```
241
+
242
+ ### Sample outputs results
243
+
244
+ #### :smile: Success:
245
+
246
+ ```
247
+ I, [2016-07-15T17:13:40.092854 #2480] INFO -- : Routine Details: Send daily news email to user
248
+ Rafael Fidelis(rafa_fidelis@yahoo.com.br)
249
+
250
+ I, [2016-07-15T17:13:40.092987 #2480] INFO -- : Routine started at: 2016-07-15 17:13:40 -0300
251
+
252
+ I, [2016-07-15T17:13:40.093143 #2480] INFO -- : Success sent daily news feed email to user
253
+
254
+ I, [2016-07-15T17:13:40.093242 #2480] INFO -- : Routine ended at: 2016-07-15 17:13:40 -0300
255
+
256
+
257
+ ```
258
+
259
+ #### :weary: Error:
260
+
261
+ ```
262
+ I, [2016-07-15T17:12:10.954792 #756] INFO -- : Routine Details: Send daily news email to user
263
+ Rafael Fidelis(rafa_fidelis@yahoo.com.br)
264
+
265
+ I, [2016-07-15T17:12:10.955025 #756] INFO -- : Routine started at: 2016-07-15 17:12:10 -0300
266
+
267
+ W, [2016-07-15T17:12:10.955186 #756] WARN -- : Something went wrong
268
+
269
+ E, [2016-07-15T17:12:11.019645 #756] ERROR -- : Error sending email to user. See details below :(
270
+
271
+ E, [2016-07-15T17:12:11.019838 #756] ERROR -- : ["User yet received daily news mail today"]
272
+
273
+ I, [2016-07-15T17:12:11.020073 #756] INFO -- : Routine ended at: 2016-07-15 17:12:11 -0300
274
+
275
+ ```
276
+
277
+ <br />
278
+
279
+ ### Wrapping things up
280
+
281
+ 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:
282
+
283
+ * &#9745; All services classes must inherit from `NiftyServices::BaseService`
284
+
285
+ * &#9745; For convention(but not a rule) all services must expose only `execute`(and of course, `initialize`) as public methods.
286
+
287
+ * &#9745; `execute_action(&block)` **MUST** be called to properly setup things in execution context.
288
+
289
+ * &#9745; `can_execute?` must be **ALWAYS** implemented in service classes, **ALWAYS**, this ensure that your code will **safely runned**.
290
+ Note: A `NotImplementedError` exception will be raised if service won't define your own `can_execute?` method.
291
+
292
+ * &#9745; There's a very simple DSL for marking result as success/fail (eg: `unprocessable_entity_error!` or `success_response`).
293
+
294
+ * &#9745; Simple DSL for actions callbacks inside current execution context. (eg: `after_success` or `before_error`)
295
+ 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`
296
+
297
+ 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:
298
+
299
+ ---
300
+
301
+ ## Services Public API
302
+
303
+ Below, a list of most common public accessible methods for any instance of service:
304
+ (Detailed usage and full API list is available below this section)
305
+
306
+ ```ruby
307
+ service.success? # boolean
308
+ service.fail? # boolean
309
+ service.errors # array
310
+ service.response_status # symbol (eg: :ok)
311
+ service.response_status_code # integer (eg: 200)
312
+ ```
313
+
314
+ So, grabbing our `DailyNewsMailSendService` service again, we could do:
315
+
316
+ ```ruby
317
+ service = DailyNewsMailSendService.new(User.new('test', 'test@test.com'))
318
+ service.execute
319
+
320
+ if service.success? # or unless service.fail?
321
+ SentEmails.create(user: service.user, type: 'welcome_email')
322
+ else
323
+ puts 'Error sending email, details below:'
324
+ puts 'Status: %s' % service.response_status
325
+ puts 'Status code: %s' % service.response_status_code
326
+ puts service.errors
327
+ end
328
+
329
+ # trying to re-execute the service will return `nil`
330
+ service.execute
331
+ ```
332
+
333
+ 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.
334
+
335
+ ---
336
+
337
+ ## Success & Error Responses
338
+
339
+ ### :white_check_mark: Handling Success :zap:
340
+
341
+ To mark a service running as successfully, you must call one of this methods (preferencially inside of `execute_action` block):
342
+
343
+ * `success_response # [200, :ok]`
344
+ * `success_created_response [201, :created]`
345
+
346
+ 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`.
347
+
348
+ ---
349
+
350
+
351
+ ### :red_circle: Handling Error :boom:
352
+
353
+ By default, all services comes with following error methods:
354
+ (**Hint**: See all available error methods [`here`](lib/nifty_services/configuration.rb#L10-L16))
355
+
356
+ ```ruby
357
+ bad_request_error(message_key) # set response_status_code to 400
358
+
359
+ not_authorized_error(message_key) # set response_status_code to 401,
360
+
361
+ forbidden_error(message_key) # set response_status_code to 403,
362
+
363
+ not_found_error(message_key) # set response_status_code to 404,
364
+
365
+ unprocessable_entity_error(message_key) # set response_status_code to 422,
366
+
367
+ internal_server_error(message_key) # set response_status_code to 500,
368
+
369
+ not_implemented_error(message_key) # set response_status_code to 501
370
+ ```
371
+
372
+ Beside this methods, you can always use **low level** API to generate errors, just call the `error` method, ex:
373
+
374
+ ```ruby
375
+ # API
376
+ error(status, message_key, options = {})
377
+
378
+ # eg:
379
+ error(409, :conflict_error, reason: 'unkown')
380
+ error!(409, :conflict_error, reason: 'unkown')
381
+
382
+ # suppose you YML locale file have the configuration:
383
+ # nifty_services:
384
+ # errors:
385
+ # conflict_error: 'Conflict! The reason is %{reason}'
386
+ ```
387
+
388
+ #### Custom error response methods
389
+
390
+ But you can always add new convenience errors methods via API, this way you gain more expressivity and sintax sugar:
391
+
392
+ ```ruby
393
+ ## API
394
+ NiftyServices.add_response_error_method(status, status_code)
395
+
396
+ ## eg:
397
+
398
+ NiftyServices.add_response_error_method(:conflict, 409)
399
+
400
+ ## now you gain the methods:
401
+
402
+ ## conflict_error(:conflict_error)
403
+ ## conflit_error!(:conflict_error)
404
+ ```
405
+
406
+ ---
407
+
408
+ ## CRUD Services
409
+
410
+ 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.
411
+
412
+ Above, an example of **Create, Update and Delete** CRUD services for `Post` resource:
413
+
414
+ ## :white_check_mark: CRUD: Create
415
+
416
+ ```ruby
417
+ class PostCreateService < NiftyServices::BaseCreateService
418
+
419
+ # record_type must be a object respond to :build and :save methods
420
+ # is possible to access this record outside of service using
421
+ # `service.record` or `service.post`
422
+ # if you want to create a custom alias name, use:
423
+ # record_type Post, alias_name: :user_post
424
+ # This way, you can access the record using
425
+ # `service.user_post`
426
+
427
+ record_type Post
428
+
429
+ WHITELIST_ATTRIBUTES = [:title, :content]
430
+
431
+ def record_params_whitelist
432
+ WHITELIST_ATTRIBUTES
433
+ end
434
+
435
+ def build_record
436
+ # record_allowed_params auto magically use record_params_whitelist to remove unsafe attributes
437
+ @user.posts.build(record_allowed_params)
438
+ end
439
+
440
+ # this key is used for I18n translations
441
+ def record_error_key
442
+ :posts
443
+ end
444
+
445
+ def user_can_create_record?
446
+ # (here you can do any kind of validation, eg:)
447
+ # check if user is trying to recreate a recent resource
448
+ # this will return false if user yet created a post with
449
+ # this title in the last 30 seconds (usefull to ban bots)
450
+ @user.posts.exists(title: record_allowed_params[:title], created_at: "NOW() - interval(30 seconds)")
451
+ end
452
+ end
453
+
454
+ service = PostCreateService.new(User.first, title: 'Teste', content: 'Post example content')
455
+
456
+ service.execute
457
+
458
+ service.success? # true
459
+ service.response_status_code # 200
460
+ service.response_status # :created
461
+ ```
462
+
463
+ #### :earth_americas: I18n setup
464
+
465
+ You must have the following keys setup up in your locales files:
466
+
467
+ ```yml
468
+ nifty_services:
469
+ users:
470
+ not_found: "Invalid or not found user"
471
+ ip_temporarily_blocked: "This IP is temporarily blocked from creating records"
472
+ # note: posts is the key return in `record_error_key` service method
473
+ posts:
474
+ user_cant_create: "User cant create this record"
475
+ ```
476
+
477
+ #### :alien: Invalid user <a name="create-resource-user-invalid"></a>
478
+
479
+ If you try to create a post for a invalid user, such as:
480
+
481
+ ```ruby
482
+ # PostCreateService.new(user, options)
483
+ service = PostCreateService.new(nil, options)
484
+ service.execute
485
+
486
+ service.success? # false
487
+ service.response_status # :not_found
488
+ service.response_status_code # 404
489
+ service.errors # ["Invalid or not found user"]
490
+ ```
491
+
492
+ #### :no_entry_sign: Not authorized to create
493
+
494
+ Or if user is trying to create a duplicate resource:
495
+
496
+ ```ruby
497
+ # PostCreateService.new(user, options)
498
+ service = PostCreateService.new(User.first, options)
499
+ service.execute
500
+
501
+ service.success? # false
502
+ service.errors # ["User cant create this record"]
503
+ service.response_status # :forbidden_error
504
+ service.response_status_code # 400
505
+ ```
506
+
507
+ #### :boom: Record is invalid
508
+
509
+ Eg: if any validation in Post model won't pass:
510
+
511
+ ```ruby
512
+ # PostCreateService.new(user, options)
513
+ # Post model as the validation:
514
+ # validates_presence_of :title, :content
515
+ service = PostCreateService.new(User.first, title: nil, content: nil)
516
+ service.execute
517
+
518
+ service.success? # false
519
+
520
+ service.errors # => [{ title: 'is empty', content: 'is empty' }]
521
+
522
+ service.response_status # :unprocessable_entity
523
+ service.response_status_code # 422
524
+ ```
525
+ ---
526
+
527
+ ## :white_check_mark: CRUD: Update
528
+
529
+ ```ruby
530
+ class PostUpdateService < NiftyServices::BaseUpdateService
531
+
532
+ # service.post or service.record
533
+ record_type Post
534
+
535
+ WHITELIST_ATTRIBUTES = [:title, :content]
536
+
537
+ def record_allowed_params
538
+ WHITELIST_ATTRIBUTES
539
+ end
540
+
541
+ # by default, internally @record must respond to
542
+ # user_can_update(user)
543
+ # so you can do specific validations per resource
544
+ def user_can_update_record?
545
+ # only system admins and owner can update this record
546
+ @user.admin? || @user.id == @record.id
547
+ end
548
+
549
+ def record_error_key
550
+ :posts
551
+ end
552
+ end
553
+
554
+ # :user_id will be ignored since it's not in whitelisted attributes
555
+ # this can safe yourself from parameter inject attacks, by default
556
+ update_service = PostUpdateService.new(Post.first, User.first, title: 'Changing title', content: 'Updating content', user_id: 2)
557
+
558
+ update_service.execute
559
+
560
+ update_service.success? # true
561
+ update_service.response_status # :ok
562
+ update_service.response_status_code # 200
563
+
564
+ update_service.changed_attributes # [:title, :content]
565
+ update_service.changed? # true
566
+ ```
567
+
568
+ #### :earth_asia: I18n setup
569
+
570
+ Your locale file must have the following keys:
571
+
572
+ ```yml
573
+ posts:
574
+ not_found: "Invalid or not found post"
575
+ user_cant_update: "User can't update this record"
576
+ users:
577
+ not_found: "Invalid or not found user"
578
+ ```
579
+
580
+ #### :alien: User is invalid <a name="update-resource-user-invalid"></a>
581
+
582
+ Response when owner user is not valid:
583
+
584
+ ```ruby
585
+ # PostUpdateService.new(post, user, params)
586
+ update_service = PostUpdateService.new(Post.first, nil, title: 'Changing title', content: 'Updating content')
587
+
588
+ update_service.execute
589
+
590
+ update_service.success? # false
591
+ update_service.response_status # :not_found_error
592
+ update_service.response_status_code # 404
593
+
594
+ update_service.errors # ["Invalid or not found user"]
595
+ ```
596
+
597
+ #### :closed_lock_with_key: Resource (Post) don't belongs to user <a name="update-resource-dont-belongs-to-user"></a>
598
+
599
+ Responses when trying to update to update a resource who don't belongs to owner:
600
+
601
+ ```ruby
602
+ # PostUpdateService.new(post, user, params)
603
+ update_service = PostUpdateService.new(Post.first, User.last, title: 'Changing title', content: 'Updating content')
604
+
605
+ update_service.execute
606
+
607
+ update_service.success? # false
608
+ update_service.response_status # :forbidden
609
+ update_service.response_status_code # 400
610
+
611
+ update_service.changed_attributes # []
612
+ update_service.changed? # false
613
+ update_service.errors # ["User can't update this record"]
614
+ ```
615
+
616
+ #### :santa: Resource don't exists <a name="update-resource-dont-exists"></a>
617
+
618
+ Response when post don't exists:
619
+
620
+ ```ruby
621
+ # PostUpdateService.new(post, user, params)
622
+ update_service = PostUpdateService.new(nil, User.last, title: 'Changing title', content: 'Updating content')
623
+
624
+ update_service.execute
625
+
626
+ update_service.success? # false
627
+ update_service.response_status # :not_found_error
628
+ update_service.response_status_code # 404
629
+
630
+ update_service.errors # ["Invalid or not found post"]
631
+ ```
632
+
633
+ ---
634
+
635
+ ## :white_check_mark: CRUD: Delete
636
+
637
+ ```ruby
638
+ class PostDeleteService < NiftyServices::BaseDeleteService
639
+ # record_type object must respond to :destroy or :delete method
640
+ record_type Post
641
+
642
+ def record_error_key
643
+ :posts
644
+ end
645
+
646
+ # below the code used internally, you can override to
647
+ # create custom delete, but remembers that this method
648
+ # must return a boolean value
649
+ def destroy_record
650
+ @record.try(:destroy) || @record.try(:delete)
651
+ end
652
+
653
+ # by default, internally @record must respond to
654
+ # @record.user_can_delete?(user)
655
+ # so you can do specific validations per resource
656
+ def user_can_delete_record?
657
+ # only system admins and owner can delete this record
658
+ @user.admin? || @user.id == @record.id
659
+ end
660
+ end
661
+ ```
662
+
663
+ #### :earth_africa: I18n setup
664
+
665
+ Your locale file must have the following keys:
666
+
667
+ ```yml
668
+ posts:
669
+ not_found: "Invalid or not found post"
670
+ user_cant_delete: "User can't delete this record"
671
+ users:
672
+ not_found: "Invalid or not found user"
673
+ ```
674
+
675
+ #### :alien: User is invalid <a name="delete-resource-user-invalid"></a>
676
+
677
+ Response when owner user is not valid:
678
+
679
+ ```ruby
680
+ # PostDeleteService.new(post, user, params)
681
+ delete_service = PostDeleteService.new(Post.first, nil)
682
+
683
+ delete_service.execute
684
+
685
+ delete_service.success? # false
686
+ delete_service.response_status # :not_found_error
687
+ delete_service.response_status_code # 404
688
+
689
+ delete_service.errors # ["Invalid or not found user"]
690
+ ```
691
+
692
+ #### :closed_lock_with_key: Resource don't belongs to user <a name="delete-resource-dont-belongs-to-user"></a>
693
+
694
+ Responses when trying to delete a resource who don't belongs to owner:
695
+
696
+ ```ruby
697
+ # PostDeleteService.new(post, user, params)
698
+ delete_service = PostDeleteService.new(Post.first, User.last)
699
+ delete_service.execute
700
+
701
+ delete_service.success? # false
702
+ delete_service.response_status # :forbidden
703
+ delete_service.response_status_code # 400
704
+ delete_service.errors # ["User can't delete this record"]
705
+ ```
706
+
707
+ #### :santa: Resource(Post) don't exists <a name="delete-resource-dont-exists"></a>
708
+
709
+ Response when post don't exists:
710
+
711
+ ```ruby
712
+ # PostDeleteService.new(post, user, params)
713
+ delete_service = PostDeleteService.new(nil, User.last)
714
+
715
+ delete_service.execute
716
+
717
+ delete_service.success? # false
718
+ delete_service.response_status # :not_found_error
719
+ delete_service.response_status_code # 404
720
+
721
+ delete_service.errors # ["Invalid or not found post"]
722
+ ```
723
+
724
+
725
+ ---
726
+
727
+ ## :us: :fr: :jp: I18n Support :uk: :es: :de:
728
+
729
+ 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:
730
+
731
+ ```yml
732
+ # attention: dont use `resource_type`
733
+ # use the key setup up in `record_error_key` methods
734
+ resource_type:
735
+ not_found: "Invalid or not found post"
736
+ user_cant_create: "User can't delete this record"
737
+ user_cant_read: "User can't access this record"
738
+ user_cant_update: "User can't delete this record"
739
+ user_cant_delete: "User can't delete this record"
740
+ users:
741
+ not_found: "Invalid or not found user"
742
+ ```
743
+
744
+ You can configure the default I18n namespace using configuration:
745
+
746
+ ```ruby
747
+ NiftyServies.configure do |config|
748
+ config.i18n_namespace = :my_app
749
+ end
750
+ ```
751
+
752
+ Example config for `Post` and `Comment` resources using `my_app` locale namespace:
753
+
754
+ ```yml
755
+ # default is nifty_services
756
+ my_app:
757
+ errors:
758
+ default_crud: &default_crud
759
+ user_cant_create: "User can't delete this record"
760
+ user_cant_read: "User can't access this record"
761
+ user_cant_update: "User can't delete this record"
762
+ user_cant_delete: "User can't delete this record"
763
+ users:
764
+ not_found: "Invalid or not found user"
765
+ posts:
766
+ <<: *default_crud
767
+ not_found: "Invalid or not found post"
768
+ comments:
769
+ <<: *default_crud
770
+ not_found: "Invalid or not found comment"
771
+ ```
772
+
773
+ ---
774
+
775
+ ## Callbacks
776
+
777
+ Here the most common callbacks list you can use to hook actions in run-time:
778
+ (**Hint**: See all existent callbacks definitions in [`extensions/callbacks_interface.rb`](lib/nifty_services/extensions/callbacks_interface.rb#L8-L24) file)
779
+
780
+ ```
781
+ - before_initialize
782
+ - after_initialize
783
+ - before_execute
784
+ - after_execute
785
+ - before_error
786
+ - after_error
787
+ - before_success
788
+ - after_success
789
+ ```
790
+
791
+ ### Creating custom Callbacks
792
+
793
+ 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:
794
+
795
+ ```ruby
796
+ # Some monkey patch :)
797
+
798
+ NiftyServices::BaseCreateService.class_eval do
799
+ ORIGIN_WHITELIST_ATTRIBUTES = [:provider, :locale, :user_agent, :ip]
800
+
801
+ def origin_params(params = {})
802
+ filter_hash(params.fetch(:origin, {}).to_h, ORIGIN_WHITELIST_ATTRIBUTES)
803
+ end
804
+
805
+ def create_origin(originable, params = {})
806
+ return unless originable.respond_to?(:create_origin)
807
+ return unless create_origin?
808
+
809
+ originable.create_origin(origin_params(params))
810
+ end
811
+
812
+ # for records which we don't need to create origins, just
813
+ # overwrite this method inside service class turning it off with:
814
+ # return false
815
+ def create_origin?
816
+ Application::Config.create_origin_for_records
817
+ end
818
+ end
819
+
820
+ # This register an callback for ALL services who inherit from `NiftyServices::BaseCreateService`
821
+ # In other words: Every and all records created in my application will be tracked
822
+ # I can believe that's is easy like this, I need a beer right now!
823
+ NiftyServices::BaseCreateService.register_callback(:after_success, :create_origin_for_record) do
824
+ create_origin(@record, @options)
825
+ end
826
+
827
+ ```
828
+
829
+ Now, every record created in my application will have an associated `origin` object, really simple and cool!
830
+
831
+ ---
832
+
833
+ ## :construction: Configuration :construction:
834
+
835
+ 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:
836
+
837
+ ```ruby
838
+ NiftyServices.config do |config|
839
+ # [optional - but very recommend! Please, do it]
840
+ # class used to control ACL
841
+ config.user_class = User
842
+
843
+ # [optional]
844
+ # global logger for all services
845
+ # [Default: Logger.new('/dev/null')]
846
+ config.logger = Logger.new('log/services_logger.log')
847
+
848
+ # [optional]
849
+ # Namespace to lookup when using concerns with services
850
+ # [Default: 'NitfyServices::Concerns']
851
+ config.service_concerns_namespace = "Services::V1::Concerns"
852
+
853
+ end
854
+ ```
855
+
856
+ ---
857
+
858
+ ## Web Frameworks Integrations
859
+
860
+ ### Rails <a name="frameworks-rails"></a>
861
+
862
+ You need a very minimal setup to integrate with you existing or new Rails application. I prefer to put my services files inside the `lib/services` folder, cause this allow better namespacing configuration over `app/services`, but this is up to you to decide.
863
+
864
+ First thing to do is add `lib/` folder in `autoload` path, place the following in your `config/application.rb`
865
+
866
+ ```ruby
867
+ # config/application.rb
868
+ config.paths.add(File.join(Rails.root, 'lib'), glob: File.join('**', '*.rb'))
869
+
870
+ config.autoload_paths << Rails.root.join('lib')
871
+ ```
872
+
873
+ Second, create `lib/services` directory:
874
+
875
+ `$ mkdir -p lib/services/v1/users`
876
+
877
+ Next, configure:
878
+ **Note**: See Configurations section below to see all available configs
879
+
880
+ ```ruby
881
+ NiftyServices.configure do |config|
882
+ config.user_class = User
883
+ end
884
+ ```
885
+
886
+ Create your first service:
887
+
888
+ ```
889
+ $ touch lib/services/v1/users/create_service.rb
890
+ ```
891
+
892
+ Use in your controller:
893
+
894
+ ```ruby
895
+ class UsersController < BaseController
896
+ def create
897
+ service = Services::V1::Users::CreateService.new(params).execute
26
898
 
27
- ## Development
899
+ default_response = { status: service.response_status, status_code: service.response_status_code }
900
+
901
+ if service.success?
902
+ response = { user: service.user, subscription: service.subscription }
903
+ else
904
+ response = { error: true, errors: service.errors }
905
+ end
906
+
907
+ render json: default_response.merge(response), status: service.response_status
908
+ end
909
+ end
910
+ ```
911
+
912
+ This can be even better if you move response code to an helper:
913
+
914
+ ```ruby
915
+ # helpers/users_helper.rb
916
+ module UsersHelper
917
+
918
+ def response_for_user_create_service(service)
919
+ success_response = { user: service.user, subscription: service.subscription }
920
+ generic_response_for_service(service, success_response)
921
+ end
922
+
923
+ # THIS IS GREAT, you can use this method to standartize ALL of your
924
+ # endpoints responses, THIS IS SO FUCKING COOL!
925
+ def generic_response_for_service(service, success_response)
926
+ default_response = {
927
+ status: service.response_status,
928
+ status_code: service.response_status_code,
929
+ success: service.success?
930
+ }
931
+
932
+ if service.success?
933
+ response = success_response
934
+ else
935
+ response = {
936
+ error: true,
937
+ errors: service.errors
938
+ }
939
+ end
940
+
941
+ default_response.merge(response)
942
+ end
943
+ end
944
+ ```
945
+
946
+ Changing controller again: (looks so readable now <3)
947
+
948
+ ```ruby
949
+ # controllers/users_controller.rb
950
+ class UsersController < BaseController
951
+ def create
952
+ service = Services::V1::Users::CreateService.new(params).execute
953
+
954
+ render json: response_for_user_create_service(service), status: service.response_status
955
+ end
956
+ end
957
+ ```
958
+
959
+ Well done sir! Did you read the comments in `generic_response_for_service`? Read it and think a little about this and prepare yourself for having orgasms when you realize how fuck awesome this will be for your API's. Need mode? Checkout [Sample Standartized API with NiftyServices Repository](http://github.com/fidelisrafael/nifty_services-api_sample)
960
+
961
+ ---
962
+
963
+ ### Grape/Sinatra/Padrino/Hanami/Rack <a name="frameworks-rack"></a>
964
+
965
+ Well, the integration here don't variate too much from Rails, just follow the steps:
966
+
967
+ **1 -** Decide where you'll put your services
968
+ **2 -** Code that dam amazing services!
969
+ **3 -** Instantiate the service in your framework entry point
970
+ **4 -** Create helpers to handle service response
971
+ **5 -** Be happy and go party!
972
+
973
+ Need examples? Check out one of the following repositories:
974
+
975
+ NiftyServices - Rails Sample
976
+ NiftyServices - Grape Sample
977
+ NiftyServices - Sinatra Sample
978
+
979
+ ---
980
+
981
+ ## Full Public API methods list
982
+
983
+ You can use any of the methods above with your `services instances`:
984
+
985
+ ```ruby
986
+ service.success? # boolean
987
+ service.fail? # boolean
988
+
989
+ service.errors # hash
990
+ service.add_error(error) # array
991
+
992
+ service.response_status # symbol (eg: :ok)
993
+ service.response_status_code # integer (eg: 200)
994
+
995
+ service.changed_attributes # array
996
+ service.changed? # boolean
997
+
998
+ service.callback_fired?(callback_name) # boolean
999
+ service.register_callback(name, method, &block) # nil
1000
+ service.register_callback_action(&block) # nil
1001
+
1002
+ service.option_exists?(option_name) # boolean
1003
+ service.option_enabled?(option_name) # boolean
1004
+ service.option_disabled?(option_name) # boolean
1005
+ ```
1006
+
1007
+ ---
1008
+
1009
+ ## :question: CLI Generators <a name="cli-generators"></a>
1010
+
1011
+ Currently NiftyServices don't have CLI(command line interface) generators, but is in the roadmap, so keep your eyes here!
1012
+
1013
+ ---
1014
+
1015
+ ## :calendar: Roadmap <a name="roadmap"></a>
1016
+
1017
+ - :white_medium_small_square: Remove ActiveSupport dependency
1018
+ - :white_medium_small_square: Create CLI Generators
1019
+ - :white_medium_small_square: Document `BaseActionService`
1020
+ - :white_medium_small_square: Write Sample Applications
1021
+ - :white_medium_small_square: Write better tests for all `Crud Services`
1022
+ - :white_medium_small_square: Write better tests for `BaseActionServices`
1023
+ - :white_medium_small_square: Write tests for Configuration
1024
+ - :white_medium_small_square: Write tests for Callbacks
1025
+
1026
+ ---
1027
+
1028
+ ## :computer: Development
28
1029
 
29
1030
  After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake rspec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
30
1031
 
31
- To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
1032
+ To install this gem(:gem:) onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
32
1033
 
33
- ## Contributing
1034
+ ---
34
1035
 
35
- Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/nifty_services. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [Contributor Covenant](contributor-covenant.org) code of conduct.
1036
+ ## :thumbsup: Contributing
36
1037
 
1038
+ Bug reports and pull requests are welcome on GitHub at http://github.com/fidelisrafael/nifty_services. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [Contributor Covenant](contributor-covenant.org) code of conduct.
37
1039
 
38
- ## License
1040
+ ---
39
1041
 
40
- The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
1042
+ ## :memo: License
41
1043
 
1044
+ The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).