services 0.3.4 → 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 4987f523acb227a70a5fddee85a8fae8aab86f8c
4
- data.tar.gz: 3c4f1ca4a27990a08b9f5fa691392e66329ce76c
3
+ metadata.gz: d822d6579be3909fe382b950cd93d69303fa06a0
4
+ data.tar.gz: 528251f7a5177f619d09402b32aaaf2a84bb9b2c
5
5
  SHA512:
6
- metadata.gz: 4ecc46999894ba5049d92c79c33a7d1745d4ebd6bd2db3b61037ed940ed0cd936800df117926b8b76694868771ab78ba78b99a0da84d80cd40258431e987bbcb
7
- data.tar.gz: 6d9c4a3d5e7360f4e8422395b1b78148bd21f567dc2949610256836316277dc29d86d38057bbb60f5499bb41562b723b0628b178ff020eaa42c389322daeebbb
6
+ metadata.gz: d6a2fc3cefd633a743bb5e86e8ffc2163e2a42c7f4b1406a912b7108f2603ca9f770644f90d6e9b9aa409087b19cf7f4064943ea045bdd425a2f504c458fc331
7
+ data.tar.gz: 23805aad75f6806727fb430eef2bdec3b943ed7c3750ae5e479c97268a76884af1b001d90dc2ebb362cee5d6a1e96c556070856e6f90e8553ba9d4e63d97f87e
data/README.md CHANGED
@@ -5,7 +5,129 @@
5
5
  [![Dependency Status](https://gemnasium.com/krautcomputing/services.png)](https://gemnasium.com/krautcomputing/services)
6
6
  [![Code Climate](https://codeclimate.com/github/krautcomputing/services.png)](https://codeclimate.com/github/krautcomputing/services)
7
7
 
8
- Services is a opinionated set of modules that can be used to implement the use cases (or services, contexts, whatchamacallit) of a Ruby application.
8
+ Services is a collection of modules and base classes that let you implement a nifty service layer in your Rails app.
9
+
10
+ ## Motivation
11
+
12
+ A lot has been written about service layers in Rails apps. There are advantages and disadvantages of course, but after using Services since 2013 in several Rails apps, I must say that in my opinion the advantages far outweigh the disadvantages.
13
+
14
+ **The biggest benefit you get with a service layer is that it makes it much easier to reason about your application, find a bug, or implement new features, when all your business logic is in services, not scattered in models, controllers, helpers etc.**
15
+
16
+ ## Usage
17
+
18
+ For disambiguation, we let's write Services with a uppercase S when we mean this gem, and services with a lowercase s when we mean, well, the plural of service.
19
+
20
+ ### Basic principles
21
+
22
+ Services is based on a couple of basic principles of what a service should be and do in your Rails app:
23
+
24
+ * a service does one thing well (Unix philosophy)
25
+ * a service can be run synchronously (in the foreground) or asynchronously (in the background)
26
+ * a service can be unique, meaning only one instance of it should be run at a time
27
+ * a service logs all the things (start time, end time, caller, exceptions etc.)
28
+ * a service has its own exception class and all exceptions that it may raise must be of that class or a subclass
29
+ * a service can be called with one or multiple objects or one or multiple object IDs
30
+
31
+ Apart from these basic principles, you can implement the actual logic in a service any way you want.
32
+
33
+ ### Conventions
34
+
35
+ Follow these conventions that Services expects/recommends:
36
+
37
+ * services inherit from `Services::Base` (or `Services::BaseFinder`)
38
+ * services are located in `app/services/`
39
+ * services are namespaced with the model they operate on and their names are verbs, e.g. `app/services/users/delete.rb` defines `Services::Users::Delete`. If a service operates on multiple models or no models at all, don't namespace them (`Services::DoLotsOfStuff`) or namespace them by logical groups unrelated to models (`Services::Maintenance::CleanOldUsers`, `Services::Maintenance::SendDailySummary`, etc.)
40
+ * Sometimes services must call other services. Try to not combine multiple calls to other services and business logic in one service. Instead, some services should contain only business logic and other services only a bunch of service calls but no (or little) business logic. This keeps your services nice and modular.
41
+
42
+ ### Dependence
43
+
44
+ To process services in the background, Services uses [Sidekiq](https://github.com/mperham/sidekiq). Sidekiq is not absolutely required to use Services though, if it's not present, a service will raise an exception when you try to enqueue it for background processing. If you're using Sidekiq, make sure to load the Services gem after the Sidekiq gem.
45
+
46
+ The SQL `Services::BaseFinder` (discussed further down) generates is optimized for Postgres. It might work with other databases but it's not guaranteed. If you're not using Postgres, don't use `Services::BaseFinder` or, even better, submit a PR that fixes it to work with your database!
47
+
48
+ ### Examples
49
+
50
+ The following service takes one or more users or user IDs as an argument.
51
+
52
+ ```
53
+ module Services
54
+ module Users
55
+ class Delete < Services::Base
56
+ def call(ids_or_objects)
57
+ users = find_objects(ids_or_objects)
58
+ users.each do |user|
59
+ user.destroy
60
+ Mailer.user_deleted(user).deliver
61
+ end
62
+ users
63
+ end
64
+ end
65
+ end
66
+ end
67
+ ```
68
+
69
+ As you can see, the helper `find_objects` is used to make sure you are dealing with an array of users from that point on, no matter whether `ids_or_objects` is a single user ID or user, or an array of user IDs or users.
70
+
71
+ It's good practice to always return the objects a service has been operating on at the end of the service.
72
+
73
+ Another example, this time using `Services::BaseFinder`:
74
+
75
+ ```
76
+ module Services
77
+ module Users
78
+ class Find < Services::BaseFinder
79
+ private def process(scope, conditions)
80
+ conditions.each do |k, v|
81
+ case k
82
+ when :email, :name
83
+ scope = scope.where(k => v)
84
+ when :product_id
85
+ scope = scope.joins(:products).where("#{Product.table_name}.id" => v)
86
+ when :product_category_id
87
+ scope = scope.joins(:product_categories).where("#{ProductCategory.table_name}.id" => v)
88
+ else
89
+ raise ArgumentError, "Unexpected condition: #{k}"
90
+ end
91
+ end
92
+ scope
93
+ end
94
+ end
95
+ end
96
+ end
97
+ ```
98
+
99
+ Since you will create services to find objects for pretty much every model you have and they all look very similar, i.e. process the find conditions and return a `ActiveRecord::Relation`, you can let those services inherit from `Services::BaseFinder` to remove some of the boilerplate.
100
+
101
+ `Services::BaseFinder` inherits from `Services::Base` and takes an array of IDs and a hash of conditions as parameters. It then extracts some special conditions (:order, :limit, :page, :per_page) that are handled separately and passes a `ActiveRecord::Relation` and the remaining conditions to the `process` method that the inheriting class must define. This method should handle all the conditions, extend the scope and return it.
102
+
103
+ Check out [the source of `Services::BaseFinder`](lib/services/base_finder.rb) to understand what it does in more detail.
104
+
105
+ ### Helpers
106
+
107
+ Your services inherit from `Services::Base` which makes several helper methods available:
108
+
109
+ * `Rails.application.routes.url_helpers` is included so you use all Rails URL helpers.
110
+ * `find_objects` and `find_object` let you automatically find object or a single object from an array of objects or object IDs, or a single object or object ID. The only difference is that `find_object` returns a single object whereas `find_objects` always returns an array.
111
+ * `object_class` tries to figure out the class the service operates on. If you follow the service naming conventions and you have a service `Services::Products::Find`, `object_class` will return `Product`. Don't call it if you have a service like `Services::DoStuff` or it will raise an exception.
112
+ * `controller` creates a `ActionController::Base` instance with an empty request. You can use it to call `render_to_string` to render a view from your service for example.
113
+
114
+ Your services also automatically get a custom `Error` class, so you can `raise Error, 'Uh-oh, something has gone wrong!'` and a `Services::MyService::Error` will be raised.
115
+
116
+ ### Logging
117
+
118
+ to be described...
119
+
120
+ ### Exception wrapping
121
+
122
+ to be described...
123
+
124
+ ### Uniqueness checking
125
+
126
+ to be described...
127
+
128
+ ### Background/asynchronous processing
129
+
130
+ to be described...
9
131
 
10
132
  ## Requirements
11
133
 
@@ -25,10 +147,6 @@ Or install it yourself as:
25
147
 
26
148
  $ gem install services
27
149
 
28
- ## Usage
29
-
30
- Coming soon...
31
-
32
150
  ## Contributing
33
151
 
34
152
  1. Fork it
data/lib/services/base.rb CHANGED
@@ -25,7 +25,7 @@ module Services
25
25
 
26
26
  private
27
27
 
28
- def find_objects(ids_or_objects, klass = service_class)
28
+ def find_objects(ids_or_objects, klass = object_class)
29
29
  ids_or_objects = Array(ids_or_objects)
30
30
  ids, objects = ids_or_objects.grep(Fixnum), ids_or_objects.grep(klass)
31
31
  if ids.size + objects.size < ids_or_objects.size
@@ -52,7 +52,7 @@ module Services
52
52
  end.first
53
53
  end
54
54
 
55
- def service_class
55
+ def object_class
56
56
  self.class.to_s[/Services::([^:]+)/, 1].singularize.constantize
57
57
  rescue
58
58
  raise "Could not determine service class from #{self.class}"
@@ -3,21 +3,21 @@ module Services
3
3
  def call(ids = [], conditions = {})
4
4
  ids, conditions = Array(ids), conditions.symbolize_keys
5
5
  special_conditions = conditions.extract!(:order, :limit, :page, :per_page)
6
- scope = service_class
7
- .select("DISTINCT #{service_class.table_name}.id")
8
- .order("#{service_class.table_name}.id")
6
+ scope = object_class
7
+ .select("DISTINCT #{object_class.table_name}.id")
8
+ .order("#{object_class.table_name}.id")
9
9
  scope = scope.where(id: ids) unless ids.empty?
10
10
 
11
11
  scope = process(scope, conditions)
12
12
 
13
- scope = service_class.where(id: scope)
13
+ scope = object_class.where(id: scope)
14
14
  special_conditions.each do |k, v|
15
15
  case k
16
16
  when :order
17
17
  order = if v == 'random'
18
18
  'RANDOM()'
19
19
  else
20
- "#{service_class.table_name}.#{v}"
20
+ "#{object_class.table_name}.#{v}"
21
21
  end
22
22
  scope = scope.order(order)
23
23
  when :limit
@@ -70,7 +70,7 @@ module Services
70
70
  end
71
71
  when Fixnum, String, TrueClass, FalseClass, NilClass
72
72
  arg
73
- when service_class
73
+ when object_class
74
74
  arg.id
75
75
  else
76
76
  raise "Don't know how to convert arg #{arg.inspect} for rescheduling."
@@ -1,3 +1,3 @@
1
1
  module Services
2
- VERSION = '0.3.4'
2
+ VERSION = '0.4.0'
3
3
  end
data/services.gemspec CHANGED
@@ -11,8 +11,8 @@ Gem::Specification.new do |gem|
11
11
  gem.platform = Gem::Platform::RUBY
12
12
  gem.author = 'Manuel Meurer'
13
13
  gem.email = 'manuel@krautcomputing.com'
14
- gem.summary = ''
15
- gem.description = ''
14
+ gem.summary = 'A nifty service layer for your Rails app'
15
+ gem.description = 'A nifty service layer for your Rails app'
16
16
  gem.homepage = 'http://krautcomputing.github.io/services'
17
17
  gem.license = 'MIT'
18
18
 
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: services
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.4
4
+ version: 0.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Manuel Meurer
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2014-11-11 00:00:00.000000000 Z
11
+ date: 2014-11-12 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rake
@@ -122,7 +122,7 @@ dependencies:
122
122
  - - "~>"
123
123
  - !ruby/object:Gem::Version
124
124
  version: '0.3'
125
- description: ''
125
+ description: A nifty service layer for your Rails app
126
126
  email: manuel@krautcomputing.com
127
127
  executables: []
128
128
  extensions: []
@@ -180,7 +180,7 @@ rubyforge_project:
180
180
  rubygems_version: 2.2.2
181
181
  signing_key:
182
182
  specification_version: 4
183
- summary: ''
183
+ summary: A nifty service layer for your Rails app
184
184
  test_files:
185
185
  - spec/services/base_spec.rb
186
186
  - spec/services/modules/call_logger_spec.rb