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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 4b51ddb57c9b14189952188821a22c36ea9b7c35
4
- data.tar.gz: 1bf6174e4899e88817ea3ec77db991d2cf249cdc
3
+ metadata.gz: 487b8be2050ec50ec28c0129defd800f1f94885b
4
+ data.tar.gz: 6e05b7663023277532c301820b32acc2da5c1580
5
5
  SHA512:
6
- metadata.gz: b098ca14f8ad2789d6dc7e0fc4026452a79d30697f241d31d6b74dfb7397e50b89df681e7648648550e66b66e2f2ec52605dc96359a741ead9c4ebf456645024
7
- data.tar.gz: ee3633820cc0690f469acd67a74c3ff63d6f86d19f08f92b4b09d26296cc448cfb003159295485ab5c4a47532defc8ca7072c302e523ee2cdbfcd7a69db251c8
6
+ metadata.gz: f3808572625a8be8a29d066bd928b766dbfbca794cc63e0c0bb7fd18e17b8e31d02bc201eb1aa49a961d340a62245242797a597751e3eec285e6b0ba36d5f5f9
7
+ data.tar.gz: 9dc671f34a2c6e2e8c5f5ee1103af341429da09c950b37e2f92c037ab06f7ae4247263db0a40ff9bf768dd7a9f72edbbd4f21581ae394fce84efae380e6eae7c
@@ -1,4 +1,8 @@
1
+ bundler_args: --retry=3 --jobs=3
2
+ cache: bundler
1
3
  language: ruby
4
+ sudo: false
2
5
  rvm:
3
- - 2.2.1
4
- before_install: gem install bundler -v 1.10.4
6
+ - 2.2.2
7
+ script:
8
+ - bundle exec rspec
data/README.md CHANGED
@@ -1,76 +1,25 @@
1
- # NiftyServices
1
+ # NiftyServices
2
2
 
3
+ [![Build Status](https://travis-ci.org/fidelisrafael/nifty_services.svg)](https://travis-ci.org/fidelisrafael/nifty_services)
3
4
  ## Introduction
4
5
 
5
6
  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
 
7
8
  NiftyServices provides 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
 
9
- **Most important:** You and your team win what I consider the best benefit when using Nifty Services: **Easily and scalable maintained code.**
10
+ **Most important:** You and your team win what I consider the best benefit when using Nifty Services: **Easily and scalable maintained code.**
10
11
  Believe me, you'll fall in :heart_eyes: with this small piece of code, keep reading!
11
12
 
12
13
  This gem was designed and conventioned to be used specially with **Web API applications**, but this is just a convention, you can use it even with [shoes (for desktop apps)](https://github.com/shoes/shoes) applications if you want, for example.
13
14
 
14
15
  #### :book: I know, this README is very huge
15
16
 
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
+ 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
 
18
- But remember one thing: This is a **tecnical documentation**, not a blog post, I'm pretty sure you can take about 30 minutes + some cups of :coffee: to better understand all NiftyServices can
19
+ But remember one thing: This is a **tecnical documentation**, not a blog post, I'm pretty sure you can take about 30 minutes + some cups of :coffee: to better understand all NiftyServices can
19
20
  do for you and your project. Good reading, and if you have some question, [please let me know](issues/new).
20
21
 
21
- ---
22
-
23
- ## Table of Contents
24
-
25
- * [Dafuck is this gem](#introduction)
26
- * [Conventions](#conventions)
27
- * [Single Responsability](#conventions-single-responsibility)
28
- * [Method execution](#hammer-common-and-single-run-execution-method)
29
- * [Rich Service Objects](#package-rich-service-objects)
30
- * [Security & Access Level Control](#lock-security---access-control-level)
31
- * [Installation](#installation)
32
- * [Usage](#usage)
33
- * [Basic Service Markup](#basic-service-markup)
34
- * [How a Service must be created](#wrapping-things-up)
35
- * [Services API](#services-public-api)
36
- * [Full Public Service API Methods List](#full-public-api-methods-list)
37
- * [Handling Success & Error Responses](#success--error-responses)
38
- * [Success response](#white_check_mark-handling-success-zap)
39
- * [Error response](#red_circle-handling-error-boom)
40
- * [Custom error response methods](#custom-error-response-methods)
41
- * [CRUD Services](#crud-services)
42
- * [**Create** - BaseCreateService](#white_check_mark-crud-create)
43
- * [I18n Setup](#earth_americas-i18n-setup)
44
- * [Error - Invalid User](#alien-invalid-user)
45
- * [Error - Not authorized](#no_entry_sign-not-authorized-to-create)
46
- * [Error - Invalid record](#boom-record-is-invalid)
47
- * [**Update** - BaseUpdateService](#white_check_mark-crud-update)
48
- * [I18n Setup](#earth_asia-i18n-setup)
49
- * [Error - Invalid User](#update-resource-user-invalid)
50
- * [Error - Resource don't belongs to user](#update-resource-dont-belongs-to-user)
51
- * [Error - Resource dont exists](#update-resource-dont-exists)
52
- * [**Delete** - BaseDeleteService](#white_check_mark-crud-delete)
53
- * [I18n Setup](#earth_africa-i18n-setup)
54
- * [Error - Invalid User](#delete-resource-user-invalid)
55
- * [Error - Resource don't belongs to user](#delete-resource-dont-belongs-to-user)
56
- * [Error - Resource dont exists](#delete-resource-dont-exists)
57
- * [I18n Setup](#us-fr-jp-i18n-support-uk-es-de)
58
- * [Callbacks](#callbacks)
59
- * [Using custom callbacks](#creating-custom-callbacks)
60
- * [Configuration](#construction-configuration-construction)
61
- * [Web Frameworks integration](#web-frameworks-integrations)
62
- * [Ruby on Rails](#frameworks-rails)
63
- * [Grape/Sinatra/Rack](#frameworks-rack)
64
- * [ Basic Services class Markups](#pray-basic-service-markups-raised_hands)
65
- * [BaseCreateService Basic Markup](#basecreateservice-basic-markup)
66
- * [BaseUpdateService Basic Markup](#baseupdateservice-basic-markup)
67
- * [BaseDeleteService Basic Markup](#basedeleteservice-basic-markup)
68
- * [BaseActionService Basic Markup](#baseactionservice-basic-markup)
69
- * [CLI Generators](#cli-generators)
70
- * [Roadmap](#roadmap)
71
- * [Development](#computer-development)
72
- * [Contributing](#thumbsup-contributing)
73
- * [License - MIT](#memo-license)
22
+ Update: Now the documentation was separated in Wiki format. So it's a lot easier for reading.
74
23
 
75
24
  ---
76
25
 
@@ -98,6 +47,62 @@ Think and implement security rules from the first minutes of live in your applic
98
47
 
99
48
  Now you know the basic concepts and philosophy of `NiftyServices`, lets start working with this candy library?
100
49
 
50
+ ---
51
+
52
+ ## Table of Contents
53
+
54
+ * [Dafuck is this gem](#introduction)
55
+ * [Conventions](#conventions)
56
+ * [Single Responsability](#conventions-single-responsibility)
57
+ * [Method execution](#hammer-common-and-single-run-execution-method)
58
+ * [Rich Service Objects](#package-rich-service-objects)
59
+ * [Security & Access Level Control](#lock-security---access-control-level)
60
+ * [Installation](#installation)
61
+ * [Usage](./docs/usage.md#usage)
62
+ * [Basic Service Markup](./docs/usage.md#basic-service-markup)
63
+ * [How a Service must be created](./docs/usage.md#wrapping-things-up)
64
+ * [Services Objects API](./docs/api.md#services-public-api)
65
+ * [Handling Success & Error Responses](./docs/api.md#success--error-responses)
66
+ * [Success response](./docs/api.md#white_check_mark-handling-success-zap)
67
+ * [Error response](./docs/api.md#red_circle-handling-error-boom)
68
+ * [Custom error response methods](./docs/api.md#custom-error-response-methods)
69
+ * [Full Public Service API Methods List](./docs/api.md#full-public-api-methods-list)
70
+ * [CRUD Services](./docs/crud_services.md#crud-services)
71
+ * [**Create** - BaseCreateService](./docs/crud_services.md#white_check_mark-crud-create)
72
+ * [I18n Setup](./docs/crud_services.md#earth_americas-i18n-setup)
73
+ * [Error - Invalid User](./docs/crud_services.md#alien-invalid-user)
74
+ * [Error - Not authorized](./docs/crud_services.md#no_entry_sign-not-authorized-to-create)
75
+ * [Error - Invalid record](./docs/crud_services.md#boom-record-is-invalid)
76
+ * [**Update** - BaseUpdateService](./docs/crud_services.md#white_check_mark-crud-update)
77
+ * [I18n Setup](./docs/crud_services.md#earth_asia-i18n-setup)
78
+ * [Error - Invalid User](./docs/crud_services.md#update-resource-user-invalid)
79
+ * [Error - Resource don't belongs to user](./docs/crud_services.md#update-resource-dont-belongs-to-user)
80
+ * [Error - Resource dont exists](./docs/crud_services.md#update-resource-dont-exists)
81
+ * [**Delete** - BaseDeleteService](./docs/crud_services.md#white_check_mark-crud-delete)
82
+ * [I18n Setup](./docs/crud_services.md#earth_africa-i18n-setup)
83
+ * [Error - Invalid User](./docs/crud_services.md#delete-resource-user-invalid)
84
+ * [Error - Resource don't belongs to user](./docs/crud_services.md#delete-resource-dont-belongs-to-user)
85
+ * [Error - Resource dont exists](./docs/crud_services.md#delete-resource-dont-exists)
86
+ * [I18n Setup](./docs/i18n.md)
87
+ * [Callbacks](./docs/callbacks.md)
88
+ * [Using custom callbacks](./docs/callbacks.md#creating-custom-callbacks)
89
+ * [Configuration](./docs/configuration.md)
90
+ * [Web Frameworks integration](./docs/webframeworks_integration.md)
91
+ * [Ruby on Rails](./docs/webframeworks_integration.md#frameworks-rails)
92
+ * [Grape/Sinatra/Rack](./docs/webframeworks_integration.md#frameworks-rack)
93
+ * [Sample Integrations](./docs/webframeworks_integration.md#integration-examples)
94
+ * [Base Services Class Markups](./docs/services_markup.md)
95
+ * [BaseCreateService Base Markup](./docs/services_markup.md#basecreateservice-basic-markup)
96
+ * [BaseUpdateService Base Markup](./docs/services_markup.md#baseupdateservice-basic-markup)
97
+ * [BaseDeleteService Base Markup](./docs/services_markup.md#basedeleteservice-basic-markup)
98
+ * [BaseActionService Base Markup](./docs/services_markup.md#baseactionservice-basic-markup)
99
+ * [CLI Generators](./docs/cli.md)
100
+ * [Roadmap](#roadmap)
101
+ * [Development](#computer-development)
102
+ * [Contributing](#thumbsup-contributing)
103
+ * [License - MIT](#memo-license)
104
+
105
+
101
106
  ---
102
107
 
103
108
  ## Installation
@@ -118,1144 +123,8 @@ Or install it yourself as:
118
123
 
119
124
  ---
120
125
 
121
- ## Usage
122
-
123
- NiftyServices provide a start basic service class for generic code which is `NiftyServices::BaseService`, the very basic service markup is demonstrated below:
124
-
125
- ### Basic Service Markup
126
-
127
-
128
- ```ruby
129
- class SemanticServiceName < NiftyServices::BaseService
130
-
131
- def execute
132
- execute_action do
133
- success_response if do_something_complex
134
- end
135
- end
136
-
137
- def do_something_complex
138
- # (...) some complex bussiness logic
139
- return true
140
- end
141
-
142
- private
143
- def can_execute?
144
- return forbidden_error!('errors.message_key') if some_condition
145
-
146
- return not_found_error!('errors.message_key') if another_condition
147
-
148
- return unprocessable_entity_error!('errors.message_key') if other_condition
149
-
150
- # ok, this service can be executed
151
- return true
152
- end
153
- end
154
-
155
- service = SemanticServiceName.new(options)
156
- service.execute
157
- ```
158
-
159
- ---
160
-
161
- ### Ok, real world example plizzz
162
-
163
- Lets work with a real and a little more complex example, an Service responsible to send daily news mail to users.
164
- 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**!
165
-
166
- ```ruby
167
- class DailyNewsMailSendService < NiftyServices::BaseService
168
-
169
- before_execute do
170
- log.info('Routine started at: %s' % Time.now)
171
- end
172
-
173
- after_execute do
174
- log.info('Routine ended at: %s' % Time.now)
175
- end
176
-
177
- after_initialize do
178
- user_data = [@user.name, @user.email]
179
- log.info('Routine Details: Send daily news email to user %s(%s)' % user_data)
180
- end
181
-
182
- after_success do
183
- log.info('Success sent daily news feed email to user')
184
- end
185
-
186
- before_error do
187
- log.warn('Something went wrong')
188
- end
189
-
190
- after_error do
191
- log.error('Error sending email to user. See details below :(')
192
- log.error(errors)
193
- end
194
-
195
- attr_reader :user
196
-
197
- def initialize(user, options = {})
198
- @user = user
199
- super(options)
200
- end
201
-
202
- def execute
203
- execute_action do
204
- success_response if send_mail_to_user
205
- end
206
- end
207
-
208
- private
209
- def can_execute?
210
- unless valid_user?
211
- # returns false
212
- return not_found_error!('users.not_found')
213
- end
214
-
215
- unless @user.abble_to_receive_daily_news_mail?
216
- # returns false
217
- return forbidden_error!('users.already_received_daily_news_mail')
218
- end
219
-
220
- return true
221
- end
222
-
223
- def send_mail_to_user
224
- # just to fake, a real implementation could be something like:
225
- # @user.send_daily_news_mail!
226
- return true
227
- end
228
-
229
- def valid_user?
230
- # check if object is valid and is a User class type
231
- valid_object?(@user, User)
232
- end
233
-
234
- # you can use `default_options` method to add default { keys => values } to @options
235
- # so you can use the option_enabled?(key) to verify if option is enabled
236
- # or option_disabled?(key) to verify is option is disabled
237
- # This default values can be override when creating new instance of Service, eg:
238
- # DailyNewsMailSendService.new(User.last, validate_api_key: false)
239
- def default_options
240
- { validate_api_key: true }
241
- end
242
- end
243
-
244
- class User < Struct.new(:name, :email)
245
- # just to play around with results
246
- def abble_to_receive_daily_news_mail?
247
- rand(10) < 5
248
- end
249
- end
250
-
251
- user = User.new('Rafael Fidelis', 'rafa_fidelis@yahoo.com.br')
252
-
253
- # Default logger is NiftyService.config.logger = Logger.new('/dev/null')
254
- service = DailyNewsMailSendService.new(user, logger: Logger.new('daily_news.log'))
255
- service.execute
256
- ```
257
-
258
- ### Sample outputs results
259
-
260
- #### :smile: Success:
261
-
262
- ```
263
- I, [2016-07-15T17:13:40.092854 #2480] INFO -- : Routine Details: Send daily news email to user
264
- Rafael Fidelis(rafa_fidelis@yahoo.com.br)
265
-
266
- I, [2016-07-15T17:13:40.092987 #2480] INFO -- : Routine started at: 2016-07-15 17:13:40 -0300
267
-
268
- I, [2016-07-15T17:13:40.093143 #2480] INFO -- : Success sent daily news feed email to user
269
-
270
- I, [2016-07-15T17:13:40.093242 #2480] INFO -- : Routine ended at: 2016-07-15 17:13:40 -0300
271
-
272
-
273
- ```
274
-
275
- #### :weary: Error:
276
-
277
- ```
278
- I, [2016-07-15T17:12:10.954792 #756] INFO -- : Routine Details: Send daily news email to user
279
- Rafael Fidelis(rafa_fidelis@yahoo.com.br)
280
-
281
- I, [2016-07-15T17:12:10.955025 #756] INFO -- : Routine started at: 2016-07-15 17:12:10 -0300
282
-
283
- W, [2016-07-15T17:12:10.955186 #756] WARN -- : Something went wrong
284
-
285
- E, [2016-07-15T17:12:11.019645 #756] ERROR -- : Error sending email to user. See details below :(
286
-
287
- E, [2016-07-15T17:12:11.019838 #756] ERROR -- : ["User has already received daily news mail today"]
288
-
289
- I, [2016-07-15T17:12:11.020073 #756] INFO -- : Routine ended at: 2016-07-15 17:12:11 -0300
290
-
291
- ```
292
-
293
- <br />
294
-
295
- ### Wrapping things up
296
-
297
- 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:
298
-
299
- * &#9745; All services classes must inherit from `NiftyServices::BaseService`
300
-
301
- * &#9745; For convention(but not a rule) all services must expose only `execute`(and of course, `initialize`) as public methods.
302
-
303
- * &#9745; `execute_action(&block)` **MUST** be called to properly setup things in execution context.
304
-
305
- * &#9745; `can_execute?` must be **ALWAYS** implemented in service classes, **ALWAYS**, this ensure that your code will **safely runned**.
306
- Note: A `NotImplementedError` exception will be raised if service won't define your own `can_execute?` method.
307
-
308
- * &#9745; There's a very simple DSL for marking result as success/fail (eg: `unprocessable_entity_error!` or `success_response`).
309
-
310
- * &#9745; Simple DSL for actions callbacks inside current execution context. (eg: `after_success` or `before_error`)
311
- 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`
312
-
313
- 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:
314
-
315
- ---
316
-
317
- ## Services Public API
318
-
319
- Below, a list of most common public accessible methods for any instance of service:
320
- (Detailed usage and full API list is available below this section)
321
-
322
- ```ruby
323
- service.success? # boolean
324
- service.fail? # boolean
325
- service.errors # array
326
- service.response_status # symbol (eg: :ok)
327
- service.response_status_code # integer (eg: 200)
328
- ```
329
-
330
- So, grabbing our `DailyNewsMailSendService` service again, we could do:
331
-
332
- ```ruby
333
- service = DailyNewsMailSendService.new(User.new('test', 'test@test.com'))
334
- service.execute
335
-
336
- if service.success? # or unless service.fail?
337
- SentEmails.create(user: service.user, type: 'welcome_email')
338
- else
339
- puts 'Error sending email, details below:'
340
- puts 'Status: %s' % service.response_status
341
- puts 'Status code: %s' % service.response_status_code
342
- puts service.errors
343
- end
344
-
345
- # trying to re-execute the service will return `nil`
346
- service.execute
347
- ```
348
-
349
- 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.
350
-
351
- ---
352
-
353
- ## Success & Error Responses
354
-
355
- ### :white_check_mark: Handling Success :zap:
356
-
357
- To mark a service running as successfully, you must call one of this methods (preferencially inside of `execute_action` block):
358
-
359
- * `success_response # [200, :ok]`
360
- * `success_created_response [201, :created]`
361
-
362
- 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`.
363
-
364
- ---
365
-
366
-
367
- ### :red_circle: Handling Error :boom:
368
-
369
- By default, all services comes with following error methods:
370
- (**Hint**: See all available error methods [`here`](lib/nifty_services/configuration.rb#L10-L16))
371
-
372
- ```ruby
373
- bad_request_error(message_key) # set response_status_code to 400
374
-
375
- not_authorized_error(message_key) # set response_status_code to 401,
376
-
377
- forbidden_error(message_key) # set response_status_code to 403,
378
-
379
- not_found_error(message_key) # set response_status_code to 404,
380
-
381
- unprocessable_entity_error(message_key) # set response_status_code to 422,
382
-
383
- internal_server_error(message_key) # set response_status_code to 500,
384
-
385
- not_implemented_error(message_key) # set response_status_code to 501
386
- ```
387
-
388
- Beside this methods, you can always use **low level** API to generate errors, just call the `error` method, ex:
389
-
390
- ```ruby
391
- # API
392
- error(status, message_key, options = {})
393
-
394
- # eg:
395
- error(409, :conflict_error, reason: 'unkown')
396
- error!(409, :conflict_error, reason: 'unkown')
397
-
398
- # suppose you YML locale file have the configuration:
399
- # nifty_services:
400
- # errors:
401
- # conflict_error: 'Conflict! The reason is %{reason}'
402
- ```
403
-
404
- #### Custom error response methods
405
-
406
- But you can always add new convenience errors methods via API, this way you will have more expressivity and sintax sugar:
407
-
408
- ```ruby
409
- ## API
410
- NiftyServices.add_response_error_method(status, status_code)
411
-
412
- ## eg:
413
-
414
- NiftyServices.add_response_error_method(:conflict, 409)
415
-
416
- ## now you have the methods:
417
-
418
- ## conflict_error(:conflict_error)
419
- ## conflit_error!(:conflict_error)
420
- ```
421
-
422
- ---
423
-
424
- ## CRUD Services
425
-
426
- 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.
427
-
428
- Follow an example of **Create, Update and Delete** CRUD services for `Post` resource:
429
-
430
- ## :white_check_mark: CRUD: Create
431
-
432
- ```ruby
433
- class PostCreateService < NiftyServices::BaseCreateService
434
-
435
- # record_type must be a object respond to :build and :save methods
436
- # is possible to access this record outside of service using
437
- # `service.record` or `service.post`
438
- # if you want to create a custom alias name, use:
439
- # record_type Post, alias_name: :user_post
440
- # This way, you can access the record using
441
- # `service.user_post`
442
-
443
- record_type Post
444
-
445
- WHITELIST_ATTRIBUTES = [:title, :content]
446
-
447
- def record_attributes_whitelist
448
- WHITELIST_ATTRIBUTES
449
- end
450
-
451
- # use custom scope to create the record
452
- # scope returned below must respond_to :build instance method
453
- def build_record_scope
454
- @user.posts
455
- end
456
-
457
- # this key is used for I18n translations
458
- def record_error_key
459
- :posts
460
- end
461
-
462
- def user_can_create_record?
463
- # (here you can do any kind of validation, eg:)
464
- # check if user is trying to recreate a recent resource
465
- # this will return false if user has already created a post with
466
- # this title in the last 30 seconds (usefull to ban bots)
467
- @user.posts.exists(title: record_allowed_attributes[:title], created_at: "NOW() - interval(30 seconds)")
468
- end
469
- end
470
-
471
- service = PostCreateService.new(User.first, title: 'Teste', content: 'Post example content')
472
-
473
- service.execute
474
-
475
- service.success? # true
476
- service.response_status_code # 200
477
- service.response_status # :created
478
- ```
479
-
480
- #### :earth_americas: I18n setup
481
-
482
- You must have the following keys setup up in your locales files:
483
-
484
- ```yml
485
- nifty_services:
486
- users:
487
- not_found: "Invalid or not found user"
488
- ip_temporarily_blocked: "This IP is temporarily blocked from creating records"
489
- # note: posts is the key return in `record_error_key` service method
490
- posts:
491
- user_cant_create: "User cant create this record"
492
- ```
493
-
494
- #### :alien: Invalid user <a name="create-resource-user-invalid"></a>
495
-
496
- If you try to create a post for a invalid user, such as:
497
-
498
- ```ruby
499
- # PostCreateService.new(user, options)
500
- service = PostCreateService.new(nil, options)
501
- service.execute
502
-
503
- service.success? # false
504
- service.response_status # :not_found
505
- service.response_status_code # 404
506
- service.errors # ["Invalid or not found user"]
507
- ```
508
-
509
- #### :no_entry_sign: Not authorized to create
510
-
511
- Or if user is trying to create a duplicate resource:
512
-
513
- ```ruby
514
- # PostCreateService.new(user, options)
515
- service = PostCreateService.new(User.first, options)
516
- service.execute
517
-
518
- service.success? # false
519
- service.errors # ["User cant create this record"]
520
- service.response_status # :forbidden_error
521
- service.response_status_code # 400
522
- ```
523
-
524
- #### :boom: Record is invalid
525
-
526
- Eg: if any validation in Post model won't pass:
527
-
528
- ```ruby
529
- # PostCreateService.new(user, options)
530
- # Post model as the validation:
531
- # validates_presence_of :title, :content
532
- service = PostCreateService.new(User.first, title: nil, content: nil)
533
- service.execute
534
-
535
- service.success? # false
536
-
537
- service.errors # => [{ title: 'is empty', content: 'is empty' }]
538
-
539
- service.response_status # :unprocessable_entity
540
- service.response_status_code # 422
541
- ```
542
- ---
543
-
544
- ## :white_check_mark: CRUD: Update
545
-
546
- ```ruby
547
- class PostUpdateService < NiftyServices::BaseUpdateService
548
-
549
- # service.post or service.record
550
- record_type Post
551
-
552
- WHITELIST_ATTRIBUTES = [:title, :content]
553
-
554
- def record_allowed_attributes
555
- WHITELIST_ATTRIBUTES
556
- end
557
-
558
- # by default, internally @record must respond to
559
- # user_can_update(user)
560
- # so you can do specific validations per resource
561
- def user_can_update_record?
562
- # only system admins and owner can update this record
563
- @user.admin? || @user.id == @record.id
564
- end
565
-
566
- def record_error_key
567
- :posts
568
- end
569
- end
570
-
571
- # :user_id will be ignored since it's not in whitelisted attributes
572
- # this can safe yourself from parameter inject attacks, by default
573
- update_service = PostUpdateService.new(Post.first, User.first, title: 'Changing title', content: 'Updating content', user_id: 2)
574
-
575
- update_service.execute
576
-
577
- update_service.success? # true
578
- update_service.response_status # :ok
579
- update_service.response_status_code # 200
580
-
581
- update_service.changed_attributes # [:title, :content]
582
- update_service.changed? # true
583
- ```
584
-
585
- #### :earth_asia: I18n setup
586
-
587
- Your locale file must have the following keys:
588
-
589
- ```yml
590
- posts:
591
- not_found: "Invalid or not found post"
592
- user_cant_update: "User can't update this record"
593
- users:
594
- not_found: "Invalid or not found user"
595
- ```
596
-
597
- #### :alien: User is invalid <a name="update-resource-user-invalid"></a>
598
-
599
- Response when owner user is not valid:
600
-
601
- ```ruby
602
- # PostUpdateService.new(post, user, params)
603
- update_service = PostUpdateService.new(Post.first, nil, title: 'Changing title', content: 'Updating content')
604
-
605
- update_service.execute
606
-
607
- update_service.success? # false
608
- update_service.response_status # :not_found_error
609
- update_service.response_status_code # 404
610
-
611
- update_service.errors # ["Invalid or not found user"]
612
- ```
613
-
614
- #### :closed_lock_with_key: Resource (Post) don't belongs to user <a name="update-resource-dont-belongs-to-user"></a>
615
-
616
- Responses when trying to update to update a resource who don't belongs to owner:
617
-
618
- ```ruby
619
- # PostUpdateService.new(post, user, params)
620
- update_service = PostUpdateService.new(Post.first, User.last, title: 'Changing title', content: 'Updating content')
621
-
622
- update_service.execute
623
-
624
- update_service.success? # false
625
- update_service.response_status # :forbidden
626
- update_service.response_status_code # 400
627
-
628
- update_service.changed_attributes # []
629
- update_service.changed? # false
630
- update_service.errors # ["User can't update this record"]
631
- ```
632
-
633
- #### :santa: Resource don't exists <a name="update-resource-dont-exists"></a>
634
-
635
- Response when post don't exists:
636
-
637
- ```ruby
638
- # PostUpdateService.new(post, user, params)
639
- update_service = PostUpdateService.new(nil, User.last, title: 'Changing title', content: 'Updating content')
640
-
641
- update_service.execute
642
-
643
- update_service.success? # false
644
- update_service.response_status # :not_found_error
645
- update_service.response_status_code # 404
646
-
647
- update_service.errors # ["Invalid or not found post"]
648
- ```
649
-
650
- ---
651
-
652
- ## :white_check_mark: CRUD: Delete
653
-
654
- ```ruby
655
- class PostDeleteService < NiftyServices::BaseDeleteService
656
- # record_type object must respond to :destroy or :delete method
657
- record_type Post
658
-
659
- def record_error_key
660
- :posts
661
- end
662
-
663
- # below the code used internally, you can override to
664
- # create custom delete, but remembers that this method
665
- # must return a boolean value
666
- def destroy_record
667
- @record.try(:destroy) || @record.try(:delete)
668
- end
669
-
670
- # by default, internally @record must respond to
671
- # @record.user_can_delete?(user)
672
- # so you can do specific validations per resource
673
- def user_can_delete_record?
674
- # only system admins and owner can delete this record
675
- @user.admin? || @user.id == @record.id
676
- end
677
- end
678
- ```
679
-
680
- #### :earth_africa: I18n setup
681
-
682
- Your locale file must have the following keys:
683
-
684
- ```yml
685
- posts:
686
- not_found: "Invalid or not found post"
687
- user_cant_delete: "User can't delete this record"
688
- users:
689
- not_found: "Invalid or not found user"
690
- ```
691
-
692
- #### :alien: User is invalid <a name="delete-resource-user-invalid"></a>
693
-
694
- Response when owner user is not valid:
695
-
696
- ```ruby
697
- # PostDeleteService.new(post, user, params)
698
- delete_service = PostDeleteService.new(Post.first, nil)
699
-
700
- delete_service.execute
701
-
702
- delete_service.success? # false
703
- delete_service.response_status # :not_found_error
704
- delete_service.response_status_code # 404
705
-
706
- delete_service.errors # ["Invalid or not found user"]
707
- ```
708
-
709
- #### :closed_lock_with_key: Resource don't belongs to user <a name="delete-resource-dont-belongs-to-user"></a>
710
-
711
- Responses when trying to delete a resource who don't belongs to owner:
712
-
713
- ```ruby
714
- # PostDeleteService.new(post, user, params)
715
- delete_service = PostDeleteService.new(Post.first, User.last)
716
- delete_service.execute
717
-
718
- delete_service.success? # false
719
- delete_service.response_status # :forbidden
720
- delete_service.response_status_code # 400
721
- delete_service.errors # ["User can't delete this record"]
722
- ```
723
-
724
- #### :santa: Resource(Post) don't exists <a name="delete-resource-dont-exists"></a>
725
-
726
- Response when post don't exists:
727
-
728
- ```ruby
729
- # PostDeleteService.new(post, user, params)
730
- delete_service = PostDeleteService.new(nil, User.last)
731
-
732
- delete_service.execute
733
-
734
- delete_service.success? # false
735
- delete_service.response_status # :not_found_error
736
- delete_service.response_status_code # 404
737
-
738
- delete_service.errors # ["Invalid or not found post"]
739
- ```
740
-
741
-
742
- ---
743
-
744
- ## :us: :fr: :jp: I18n Support :uk: :es: :de:
745
-
746
- 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:
747
-
748
- ```yml
749
- # attention: dont use `resource_type`
750
- # use the key setup up in `record_error_key` methods
751
- resource_type:
752
- not_found: "Invalid or not found post"
753
- user_cant_create: "User can't delete this record"
754
- user_cant_read: "User can't access this record"
755
- user_cant_update: "User can't delete this record"
756
- user_cant_delete: "User can't delete this record"
757
- users:
758
- not_found: "Invalid or not found user"
759
- ```
760
-
761
- You can configure the default I18n namespace using configuration:
762
-
763
- ```ruby
764
- NiftyServies.configure do |config|
765
- config.i18n_namespace = :my_app
766
- end
767
- ```
768
-
769
- Example config for `Post` and `Comment` resources using `my_app` locale namespace:
770
-
771
- ```yml
772
- # default is nifty_services
773
- my_app:
774
- errors:
775
- default_crud: &default_crud
776
- user_cant_create: "User can't delete this record"
777
- user_cant_read: "User can't access this record"
778
- user_cant_update: "User can't delete this record"
779
- user_cant_delete: "User can't delete this record"
780
- users:
781
- not_found: "Invalid or not found user"
782
- posts:
783
- <<: *default_crud
784
- not_found: "Invalid or not found post"
785
- comments:
786
- <<: *default_crud
787
- not_found: "Invalid or not found comment"
788
- ```
789
-
790
- ---
791
-
792
- ## Callbacks
793
-
794
- Here the most common callbacks list you can use to hook actions in run-time:
795
- (**Hint**: See all existent callbacks definitions in [`extensions/callbacks_interface.rb`](lib/nifty_services/extensions/callbacks_interface.rb#L8-L24) file)
796
-
797
- ```
798
- - before_initialize
799
- - after_initialize
800
- - before_execute
801
- - after_execute
802
- - before_error
803
- - after_error
804
- - before_success
805
- - after_success
806
- ```
807
-
808
- ### Creating custom Callbacks
809
-
810
- 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:
811
-
812
- ```ruby
813
- # Some monkey patch :)
814
-
815
- NiftyServices::BaseCreateService.class_eval do
816
- ORIGIN_WHITELIST_ATTRIBUTES = [:provider, :locale, :user_agent, :ip]
817
-
818
- def origin_params(params = {})
819
- filter_hash(params.fetch(:origin, {}).to_h, ORIGIN_WHITELIST_ATTRIBUTES)
820
- end
821
-
822
- def create_origin(originable, params = {})
823
- return unless originable.respond_to?(:create_origin)
824
- return unless create_origin?
825
-
826
- originable.create_origin(origin_params(params))
827
- end
828
-
829
- # for records which we don't need to create origins, just
830
- # overwrite this method inside service class turning it off with:
831
- # return false
832
- def create_origin?
833
- Application::Config.create_origin_for_records
834
- end
835
- end
836
-
837
- # This register a callback for ALL services who inherit from `NiftyServices::BaseCreateService`
838
- # In other words: Every and all records created in my application will be tracked
839
- # I can believe that is easy like this, I need a beer right now!
840
- NiftyServices::BaseCreateService.register_callback(:after_success, :create_origin_for_record) do
841
- create_origin(@record, @options)
842
- end
843
-
844
- ```
845
-
846
- Now, every record created in my application will have an associated `origin` object, really simple and cool!
847
-
848
- ---
849
-
850
- ## :construction: Configuration :construction:
851
-
852
- 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:
853
-
854
- ```ruby
855
- NiftyServices.config do |config|
856
- # [optional - but very recommend! Please, do it]
857
- # class used to control ACL
858
- config.user_class = User
859
-
860
- # [optional]
861
- # global logger for all services
862
- # [Default: Logger.new('/dev/null')]
863
- config.logger = Logger.new('log/services_logger.log')
864
-
865
- # [optional]
866
- # Namespace to lookup when using concerns with services
867
- # [Default: 'NitfyServices::Concerns']
868
- config.service_concerns_namespace = "Services::V1::Concerns"
869
-
870
- end
871
- ```
872
-
873
- ---
874
-
875
- ## Web Frameworks Integrations
876
-
877
- ### Rails <a name="frameworks-rails"></a>
878
-
879
- You need a very minimal setup to integrate with your 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.
880
-
881
- First thing to do is add `lib/` folder in `autoload` path, place the following in your `config/application.rb`
882
-
883
- ```ruby
884
- # config/application.rb
885
- config.paths.add(File.join(Rails.root, 'lib'), glob: File.join('**', '*.rb'))
886
-
887
- config.autoload_paths << Rails.root.join('lib')
888
- ```
889
-
890
- Second, create `lib/services` directory:
891
-
892
- `$ mkdir -p lib/services/v1/users`
893
-
894
- Next, configure:
895
-
896
- ```ruby
897
- NiftyServices.configure do |config|
898
- config.user_class = User
899
- end
900
- ```
901
- **Note**: See [Configurations](#construction-configuration-construction) section to see all available configs
902
-
903
- Create your first service:
904
-
905
- ```
906
- $ touch lib/services/v1/users/create_service.rb
907
- ```
908
-
909
- Use in your controller:
910
-
911
- ```ruby
912
- class UsersController < BaseController
913
- def create
914
- service = Services::V1::Users::CreateService.new(params).execute
915
-
916
- default_response = { status: service.response_status, status_code: service.response_status_code }
917
-
918
- if service.success?
919
- response = { user: service.user, subscription: service.subscription }
920
- else
921
- response = { error: true, errors: service.errors }
922
- end
923
-
924
- render json: default_response.merge(response), status: service.response_status
925
- end
926
- end
927
- ```
928
-
929
- This can be even better if you move response code to a helper:
930
-
931
- ```ruby
932
- # helpers/users_helper.rb
933
- module UsersHelper
934
-
935
- def response_for_user_create_service(service)
936
- success_response = { user: service.user, subscription: service.subscription }
937
- generic_response_for_service(service, success_response)
938
- end
939
-
940
- # THIS IS GREAT, you can use this method to standardize ALL of your
941
- # endpoints responses, THIS IS SO FUCKING COOL!
942
- def generic_response_for_service(service, success_response)
943
- default_response = {
944
- status: service.response_status,
945
- status_code: service.response_status_code,
946
- success: service.success?
947
- }
948
-
949
- if service.success?
950
- response = success_response
951
- else
952
- response = {
953
- error: true,
954
- errors: service.errors
955
- }
956
- end
957
-
958
- default_response.merge(response)
959
- end
960
- end
961
- ```
962
-
963
- Changing controller again: (looks so readable now <3)
964
-
965
- ```ruby
966
- # controllers/users_controller.rb
967
- class UsersController < BaseController
968
- def create
969
- service = Services::V1::Users::CreateService.new(params).execute
970
-
971
- render json: response_for_user_create_service(service), status: service.response_status
972
- end
973
- end
974
- ```
975
-
976
- 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)
977
-
978
- ---
979
-
980
- ### Grape/Sinatra/Padrino/Hanami/Rack <a name="frameworks-rack"></a>
981
-
982
- Well, the integration here don't variate too much from Rails, just follow the steps:
983
-
984
- **1 -** Decide where you'll put your services
985
- **2 -** Code that dam amazing services!
986
- **3 -** Instantiate the service in your framework entry point
987
- **4 -** Create helpers to handle service response
988
- **5 -** Be happy and go party!
989
-
990
- Need examples? Check out one of the following repositories:
991
-
992
- NiftyServices - Rails Sample
993
- NiftyServices - Grape Sample
994
- NiftyServices - Sinatra Sample
995
-
996
- ---
997
-
998
- ## :pray: Basic Service Markups :raised_hands:
999
-
1000
- Here, for your convenience and sanity all basic service structures for reference when you start a brand new Service.
1001
- Most of time, the best way is to copy all content from each service described below and change according to your needs.
1002
-
1003
- ### BaseCreateService Basic Markup
1004
-
1005
- ```ruby
1006
- class SomeCreateService < NiftyServices::BaseCreateService
1007
-
1008
- # [Required]
1009
- # remember that inside the Service you always can use
1010
- # @record variable to access current record
1011
- # and from outside (service instance):
1012
- # service.record or service.record_type
1013
- # eg:
1014
- # record_type BlogPost
1015
- # service.record # BlogPost.new(...)
1016
- # service.blog_post # BlogPost.new(...)
1017
- # service.record == service.blog_post # true
1018
- # alias_name can be used to create a custom alias name
1019
- # eg:
1020
- # record_type BlogPost, alias_name: :post
1021
- # service.record # BlogPost.new(...)
1022
- # service.post # BlogPost.new(...)
1023
- # service.record == service.post # true
1024
-
1025
- record_type RecordType, alias_name: :my_custom_alias_name
1026
-
1027
- private
1028
- # [Required]
1029
- # Always validate if @user can create the current record_type
1030
- # If this method is not implemented a NotImplementedError exception will be raised
1031
- def user_can_create_record?
1032
- return forbidden_error!('errors.some_error') if (some_validation)
1033
-
1034
- return bad_request_error!('errors.some_other_error') if (another_validation)
1035
-
1036
- # remember to return true after all validations
1037
- # if you don't return true Service will not be able to create the record
1038
- return true
1039
- end
1040
-
1041
- # [Optional]
1042
- # method called when save_error method call raises an exception
1043
- # this ocurr for example with ActiveRecord objects
1044
- # default: unprocessable_entity_error!(error)
1045
- def on_save_record_error(error)
1046
- logger.error(error)
1047
- if error.is_a?(ActiveRecord::RecordNotUnique)
1048
- return unprocessable_entity_error!(%s(posts.duplicate_record))
1049
- end
1050
- end
1051
-
1052
- # [Optional]
1053
- # determine wheter user will be validate as valid object before
1054
- # record creation
1055
- # (default: true)
1056
- def validate_user?
1057
- return true
1058
- end
1059
-
1060
- # [Optional]
1061
- # custom scope for record, eg: @user.posts
1062
- # default is nil
1063
- def build_record_scope
1064
- end
1065
- end
1066
- ```
1067
-
1068
- ### BaseUpdateService Basic Markup
1069
-
1070
- ```ruby
1071
- class SomeUpdateService < NiftyServices::BaseUpdateService
1072
-
1073
- # [Required]
1074
- record_type RecordType, alias_name: :custom_alias_name
1075
-
1076
- WHITELIST_ATTRIBUTES = [
1077
- :safe_attribute_1,
1078
- :safe_attribute_2,
1079
- ]
1080
-
1081
- private
1082
- # [Required]
1083
- # When a new instance of Service is created, the @options variables receive some
1084
- # values, eg: { user: { email: "...", name: "...."} }
1085
- # use record_attributes_hash to tell the Service from where to pull theses values
1086
- # eg: @options.fetch(:user, {})
1087
- # If this method is not implemented a NotImplementedError exception will be raised
1088
- def record_attributes_hash
1089
- @options.fetch(options_key, {})
1090
- end
1091
-
1092
- # [Required]
1093
- # whitelisted attributes (must be an Array) which can be updated by this Service
1094
- # If this method is not implemented a NotImplementedError exception will be raised
1095
- def record_attributes_whitelist
1096
- WHITELIST_ATTRIBUTES
1097
- end
1098
-
1099
- # [required]
1100
- # This is a VERY IMPORTANT point of attention
1101
- # always verify if @user has permissions to update the current @record object
1102
- # Hint: if @record respond_to `user_can_update?(user)` you can remove this
1103
- # method and do the validation inside `user_can_update(user)` method in @record
1104
- # If this method is not implemented a NotImplementedError exception will be raised
1105
- def user_can_update_record?
1106
- @record.user_id == @user.id
1107
- end
1108
-
1109
-
1110
- # [Optional]
1111
- # This is the default implementation of update record, you may overwrite it
1112
- # to to custom updates (MOST OF TIME YOU DONT NEED TO DO THIS)
1113
- # only change this if you know what you are really doing
1114
- def update_record
1115
- @record.class.send(:update, @record.id, record_allowed_attributes)
1116
- end
1117
-
1118
-
1119
- # [optional]
1120
- # Any callback is optional, this is just a example
1121
- def after_success
1122
- if changed?
1123
- logger.info 'Successfully update record ID %s' % @record.id
1124
- logger.info 'Changed attributes are %s' % changed_attributes
1125
- end
1126
- end
1127
- end
1128
- ```
1129
-
1130
- ---
1131
-
1132
- ### BaseDeleteService Basic Markup
1133
-
1134
- ```ruby
1135
- class SomeDeleteService < NiftyServices::BaseDeleteService
1136
-
1137
- # [Required]
1138
- record_type RecordType, alias_name: :custom_alias_name
1139
-
1140
- private
1141
-
1142
- # [Required]
1143
- # This is a VERY IMPORTANT point of attention
1144
- # always verify if @user has permissions to delete the current @record object
1145
- # Hint: if @record respond_to `user_can_delete?(user)` you can remove this
1146
- # method and do the validation inside `user_can_delete(user)` method in @record
1147
- # If this method is not implemented a NotImplementedError exception will be raised
1148
-
1149
- def user_can_delete_record?
1150
- @record.user_id == @user.id
1151
- end
1152
-
1153
- # [optional]
1154
- # Any callback is optional, this is just a example
1155
- def after_success
1156
- logger.info('Successfully Deleted resource ID %s' % @record.id)
1157
- end
1158
-
1159
- # [Optional]
1160
- # This is the default implementation of delete record, you may overwrite it
1161
- # to do custom delete (MOST OF TIME YOU DONT NEED TO DO THIS)
1162
- # only change this if you know what you are really doing
1163
- def destroy_record
1164
- @record.try(:destroy) || @record.try(:delete)
1165
- end
1166
-
1167
- end
1168
-
1169
- ```
1170
-
1171
- ### BaseActionService Basic Markup
1172
-
1173
- ```ruby
1174
- class SomeCustomActionService < NiftyServices::BaseActionService
1175
-
1176
- # [required]
1177
- # this is the action identifier used internally
1178
- # and to generate error messages
1179
- # see: invalid_action_error_key method
1180
- action_name :custom_action_name
1181
-
1182
- private
1183
- # [Required]
1184
- # Always validate if Service can execute the action
1185
- # This method MUST return a boolean value indicating if Service can or not
1186
- # run the method `execute_service_action`
1187
- # If this method is not implemented a NotImplementedError exception will be raised
1188
- def user_can_execute_action?
1189
- # do some specific validation here, you can return errors such:
1190
- # return not_found_error!(%(users.invalid_user)) # returns false and avoid execution
1191
- return true
1192
- end
1193
-
1194
- # [Required]
1195
- # The core function of BaseActionServices
1196
- # This method is called when all validations passes, so here you can put
1197
- # all logic for Service (eg: send mails, clear logs, any kind of action you want)
1198
- # If this method is not implemented a NotImplementedError exception will be raised
1199
- def execute_service_action
1200
- # (do some complex stuff)
1201
- end
1202
-
1203
- # You dont need to overwrite this method, just `record_error_key`
1204
- # But it's important you know how final message key will be created
1205
- # using the pattern below
1206
- def invalid_action_error_key
1207
- "#{record_error_key}.cant_execute_#{action_name}"
1208
- end
1209
-
1210
- # [Required]
1211
- # Key used to created the error messages for this Service
1212
- # If this method is not implemented a NotImplementedError exception will be raised
1213
- def record_error_key
1214
- :users
1215
- end
1216
- end
1217
- ```
1218
-
1219
-
1220
- ---
1221
-
1222
- ## Full Public API methods list
1223
-
1224
- You can use any of the methods above with your `services instances`:
1225
-
1226
- ```ruby
1227
- service.success? # boolean
1228
- service.fail? # boolean
1229
-
1230
- service.errors # hash
1231
- service.add_error(error) # array
1232
-
1233
- service.response_status # symbol (eg: :ok)
1234
- service.response_status_code # integer (eg: 200)
1235
-
1236
- service.changed_attributes # array
1237
- service.changed? # boolean
1238
-
1239
- service.callback_fired?(callback_name) # boolean
1240
- service.register_callback(name, method, &block) # nil
1241
- service.register_callback_action(&block) # nil
1242
-
1243
- service.option_exists?(option_name) # boolean
1244
- service.option_enabled?(option_name) # boolean
1245
- service.option_disabled?(option_name) # boolean
1246
- ```
1247
-
1248
- ---
1249
-
1250
- ## :question: CLI Generators <a name="cli-generators"></a>
1251
-
1252
- Currently NiftyServices don't have CLI(command line interface) generators, but is in the roadmap, so keep your eyes here!
1253
-
1254
- ---
1255
-
1256
126
  ## :calendar: Roadmap <a name="roadmap"></a>
1257
127
 
1258
- - :white_medium_small_square: Remove ActiveSupport dependency
1259
128
  - :white_medium_small_square: Create CLI Generators
1260
129
  - :white_medium_small_square: Beter documentation for `BaseActionService`
1261
130
  - :white_medium_small_square: Write Sample Applications
@@ -1263,7 +132,7 @@ Currently NiftyServices don't have CLI(command line interface) generators, but i
1263
132
  - :white_medium_small_square: Write better tests for `BaseActionServices`
1264
133
  - :white_medium_small_square: Write tests for Configuration
1265
134
  - :white_medium_small_square: Write tests for Callbacks
1266
-
135
+
1267
136
  ---
1268
137
 
1269
138
  ## :computer: Development
@@ -1282,4 +151,4 @@ Bug reports and pull requests are welcome on GitHub at http://github.com/fidelis
1282
151
 
1283
152
  ## :memo: License
1284
153
 
1285
- The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
154
+ The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).