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:
Michael Kohl 2020-11-10 09:09:35 +07:00 committed by GitHub
parent 5664cd3ca8
commit 824e9a13cb
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 239 additions and 0 deletions

View file

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

View file

@ -20,6 +20,7 @@ items:
- scheduled-jobs.md
- metrics.md
- tracking.md
- service-objects.md
---
# Backend Guide

View 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
```

View 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

View 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

View 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 -%>

View file

@ -0,0 +1,5 @@
require "rails_helper"
RSpec.describe <%= class_name %>, type: :service do
pending "add some examples to (or delete) #{__FILE__}"
end

View file

@ -0,0 +1,5 @@
require "rails_helper"
RSpec.describe "Services", type: :generator do
pending "add some scenarios (or delete) #{__FILE__}"
end