Add service generator (#11265)
* Add service generator * Finalize service generator * Add documentation * Explicitly require rails/generators * Update Zeitwerk initializer * Remove explicit require * Update docs/backend/service-objects.md Co-authored-by: Ridhwana <Ridhwana.Khan16@gmail.com> Co-authored-by: Ridhwana <Ridhwana.Khan16@gmail.com>
This commit is contained in:
parent
5664cd3ca8
commit
824e9a13cb
8 changed files with 239 additions and 0 deletions
|
|
@ -13,3 +13,4 @@ end
|
|||
# Ignoring folders that don't adhere to the new naming conventions
|
||||
Rails.autoloaders.main.ignore(Rails.root.join("lib/data_update_scripts"))
|
||||
Rails.autoloaders.main.ignore(Rails.root.join("lib/generators/data_update"))
|
||||
Rails.autoloaders.main.ignore(Rails.root.join("lib/generators/service"))
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ items:
|
|||
- scheduled-jobs.md
|
||||
- metrics.md
|
||||
- tracking.md
|
||||
- service-objects.md
|
||||
---
|
||||
|
||||
# Backend Guide
|
||||
|
|
|
|||
114
docs/backend/service-objects.md
Normal file
114
docs/backend/service-objects.md
Normal file
|
|
@ -0,0 +1,114 @@
|
|||
---
|
||||
title: Service Objects
|
||||
---
|
||||
|
||||
## What are Service Objects
|
||||
|
||||
Service objects are Plain Old Ruby Objects (POROs) which encapsulate a whole
|
||||
business process/user interaction.
|
||||
|
||||
Our services are located in `app/services` with the corresponding specs in
|
||||
`spec/services`. Their main interface is a class level method named `call`, as
|
||||
in the following example:
|
||||
|
||||
```ruby
|
||||
class ImportUsers
|
||||
def self.call(arg1)
|
||||
new(arg1).call
|
||||
end
|
||||
|
||||
def initialize(arg1)
|
||||
@arg1 = arg1
|
||||
end
|
||||
|
||||
def call
|
||||
# import code goes here
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
To distinguish services from models we often give them verb names vs noun
|
||||
names, e.g. `ImportUsers` instead of `UserImporter`.
|
||||
|
||||
## Generating Service Objects
|
||||
|
||||
To make our services more consistent we use a custom Rails generator. Some usage
|
||||
examples:
|
||||
|
||||
**Generate a non-namespaced service without arguments**
|
||||
|
||||
`$ rails generate service DoTheThing`
|
||||
|
||||
```ruby
|
||||
# app/services/do_the_thing.rb
|
||||
class DoTheThing
|
||||
def self.call
|
||||
new.call
|
||||
end
|
||||
|
||||
def call
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
```ruby
|
||||
# spec/services/do_the_thing_spec.rb
|
||||
require "rails_helper"
|
||||
|
||||
RSpec.describe DoTheThing, type: :service do
|
||||
pending "add some examples to (or delete) #{__FILE__}"
|
||||
end
|
||||
```
|
||||
|
||||
**Generate a non-namespaced service with arguments:**
|
||||
|
||||
`$ rails generate service DoTheThing arg1 arg2`
|
||||
|
||||
```ruby
|
||||
# app/services/do_the_thing.rb
|
||||
class DoTheThing
|
||||
def self.call(arg1, arg2)
|
||||
new(arg1, arg2).call
|
||||
end
|
||||
|
||||
def initialize(arg1, arg2)
|
||||
@arg1 = arg1
|
||||
@arg2 = arg2
|
||||
end
|
||||
|
||||
def call
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
The generated spec is the same as above.
|
||||
|
||||
**Generate a namespaced service with arguments**
|
||||
|
||||
`$ rails generate service things/dothem arg1 arg2`
|
||||
|
||||
```ruby
|
||||
# app/services/things/do_them.rb
|
||||
class Things::DoThem
|
||||
def self.call(arg1, arg2)
|
||||
new(arg1, arg2).call
|
||||
end
|
||||
|
||||
def initialize(arg1, arg2)
|
||||
@arg1 = arg1
|
||||
@arg2 = arg2
|
||||
end
|
||||
|
||||
def call
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
```ruby
|
||||
# spec/services/things/do_them_spec.rb
|
||||
require "rails_helper"
|
||||
|
||||
RSpec.describe Things::DoThem, type: :service do
|
||||
pending "add some examples to (or delete) #{__FILE__}"
|
||||
end
|
||||
```
|
||||
71
lib/generators/service/USAGE
Normal file
71
lib/generators/service/USAGE
Normal file
|
|
@ -0,0 +1,71 @@
|
|||
Description:
|
||||
Generates a service object.
|
||||
|
||||
Examples:
|
||||
* Generating a service with no arguments:
|
||||
|
||||
$ rails generate service ImportUsers
|
||||
|
||||
# app/services/import_users.rb
|
||||
class ImportUsers
|
||||
def self.call
|
||||
new.call
|
||||
end
|
||||
|
||||
def call
|
||||
end
|
||||
end
|
||||
|
||||
# spec/services/import_users_spec.rb
|
||||
require "rails_helper"
|
||||
|
||||
RSpec.describe ImportUsers, type: :service do
|
||||
pending "add some examples to (or delete) #{__FILE__}"
|
||||
end
|
||||
|
||||
* Generating a service with arguments:
|
||||
|
||||
$ rails generate service ImportUsers users
|
||||
|
||||
# app/services/import_users.rb
|
||||
class ImportUsers
|
||||
def self.call(user)
|
||||
new(user).call
|
||||
end
|
||||
|
||||
def initialize(user)
|
||||
@user = user
|
||||
end
|
||||
|
||||
def call
|
||||
end
|
||||
end
|
||||
|
||||
# spec/servics/import_users_spec.rb
|
||||
# Same as above
|
||||
|
||||
* Generating a namespaced service:
|
||||
|
||||
$ rails generate service users/import users
|
||||
|
||||
# app/services/users/import.rb
|
||||
class Users::Import
|
||||
def self.call(users)
|
||||
new(users).call
|
||||
end
|
||||
|
||||
def initialize(users)
|
||||
@users = users
|
||||
end
|
||||
|
||||
def call
|
||||
end
|
||||
end
|
||||
|
||||
# spec/services/users/import_spec.rb
|
||||
require "rails_helper"
|
||||
|
||||
RSpec.describe Users::Import, type: :service do
|
||||
pending "add some examples to (or delete) #{__FILE__}"
|
||||
end
|
||||
|
||||
24
lib/generators/service/service_generator.rb
Normal file
24
lib/generators/service/service_generator.rb
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
class ServiceGenerator < Rails::Generators::NamedBase
|
||||
source_root File.expand_path("templates", __dir__)
|
||||
|
||||
argument :arguments, type: :array, default: [], banner: "arguments for .call"
|
||||
class_option :spec, type: :boolean, default: true
|
||||
|
||||
def create_service_file
|
||||
template(
|
||||
"service.erb",
|
||||
File.join("app/services", class_path, "#{file_name}.rb"),
|
||||
)
|
||||
|
||||
return unless options["spec"]
|
||||
|
||||
template(
|
||||
"service_spec.erb",
|
||||
File.join("spec/services", class_path, "#{file_name}_spec.rb"),
|
||||
)
|
||||
end
|
||||
|
||||
def signature
|
||||
@signature ||= arguments.present? ? "(#{arguments.join(', ')})" : nil
|
||||
end
|
||||
end
|
||||
18
lib/generators/service/templates/service.erb
Normal file
18
lib/generators/service/templates/service.erb
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
<% module_namespacing do -%>
|
||||
class <%= class_name %>
|
||||
def self.call<%= signature %>
|
||||
new<%= signature %>.call
|
||||
end
|
||||
|
||||
<%- if arguments.present? -%>
|
||||
def initialize<%= signature %>
|
||||
<% arguments.each do |argument| -%>
|
||||
<%= "@#{argument}" %> = <%= argument %>
|
||||
<% end -%>
|
||||
end
|
||||
|
||||
<% end -%>
|
||||
def call
|
||||
end
|
||||
end
|
||||
<% end -%>
|
||||
5
lib/generators/service/templates/service_spec.erb
Normal file
5
lib/generators/service/templates/service_spec.erb
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
require "rails_helper"
|
||||
|
||||
RSpec.describe <%= class_name %>, type: :service do
|
||||
pending "add some examples to (or delete) #{__FILE__}"
|
||||
end
|
||||
5
spec/generator/services_generator_spec.rb
Normal file
5
spec/generator/services_generator_spec.rb
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
require "rails_helper"
|
||||
|
||||
RSpec.describe "Services", type: :generator do
|
||||
pending "add some scenarios (or delete) #{__FILE__}"
|
||||
end
|
||||
Loading…
Add table
Reference in a new issue