graphiti 1.0.alpha.5 → 1.0.alpha.6
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/lib/generators/graphiti/api_test_generator.rb +71 -0
- data/lib/generators/graphiti/generator_mixin.rb +45 -0
- data/lib/generators/graphiti/resource_generator.rb +99 -0
- data/lib/generators/graphiti/resource_test_generator.rb +57 -0
- data/lib/generators/{jsonapi → graphiti}/templates/application_resource.rb.erb +0 -0
- data/lib/generators/{jsonapi → graphiti}/templates/controller.rb.erb +0 -0
- data/lib/generators/{jsonapi → graphiti}/templates/create_request_spec.rb.erb +3 -3
- data/lib/generators/{jsonapi → graphiti}/templates/destroy_request_spec.rb.erb +5 -5
- data/lib/generators/graphiti/templates/index_request_spec.rb.erb +22 -0
- data/lib/generators/{jsonapi → graphiti}/templates/resource.rb.erb +0 -0
- data/lib/generators/{jsonapi → graphiti}/templates/resource_reads_spec.rb.erb +12 -12
- data/lib/generators/{jsonapi → graphiti}/templates/resource_writes_spec.rb.erb +12 -12
- data/lib/generators/graphiti/templates/show_request_spec.rb.erb +21 -0
- data/lib/generators/{jsonapi → graphiti}/templates/update_request_spec.rb.erb +5 -5
- data/lib/graphiti.rb +0 -6
- data/lib/graphiti/adapters/abstract.rb +0 -1
- data/lib/graphiti/adapters/active_record/inferrence.rb +8 -1
- data/lib/graphiti/base.rb +1 -1
- data/lib/graphiti/errors.rb +70 -5
- data/lib/graphiti/jsonapi_serializable_ext.rb +18 -6
- data/lib/graphiti/query.rb +28 -17
- data/lib/graphiti/resource.rb +14 -0
- data/lib/graphiti/resource/configuration.rb +22 -3
- data/lib/graphiti/resource/dsl.rb +17 -2
- data/lib/graphiti/resource/interface.rb +10 -8
- data/lib/graphiti/resource/links.rb +6 -3
- data/lib/graphiti/runner.rb +2 -1
- data/lib/graphiti/schema.rb +33 -5
- data/lib/graphiti/schema_diff.rb +71 -1
- data/lib/graphiti/scope.rb +4 -9
- data/lib/graphiti/scoping/base.rb +2 -2
- data/lib/graphiti/scoping/filter.rb +5 -0
- data/lib/graphiti/scoping/filterable.rb +21 -6
- data/lib/graphiti/scoping/paginate.rb +4 -4
- data/lib/graphiti/scoping/sort.rb +26 -5
- data/lib/graphiti/sideload.rb +13 -22
- data/lib/graphiti/sideload/belongs_to.rb +11 -2
- data/lib/graphiti/sideload/has_many.rb +1 -1
- data/lib/graphiti/sideload/polymorphic_belongs_to.rb +2 -3
- data/lib/graphiti/types.rb +1 -1
- data/lib/graphiti/util/class.rb +5 -2
- data/lib/graphiti/util/persistence.rb +1 -1
- data/lib/graphiti/version.rb +1 -1
- metadata +16 -13
- data/lib/generators/jsonapi/resource_generator.rb +0 -169
- data/lib/generators/jsonapi/templates/index_request_spec.rb.erb +0 -22
- data/lib/generators/jsonapi/templates/show_request_spec.rb.erb +0 -21
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: d7c1746bfb06d696a267ff57810838ea0f5da577
|
4
|
+
data.tar.gz: 76133042dda4240b704a4a3f2903f1f5297ccc86
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: a2e690b8375aef1392a9d59a6d1e51be0eb177a5ffa8ffd82950baaf7103ba4315bd96a5530155bf214577a853ee250eddf1ec326cb7f8d7bd521e6818d594f3
|
7
|
+
data.tar.gz: 08f1879eb8ae822005635f386b4c6b89287d657d771adacfb7b8cc364a2240b36d8e18edd4db68913017b75dcbeac80c0747286c773536b0c00f29ac0e66d05c
|
@@ -0,0 +1,71 @@
|
|
1
|
+
$:.unshift File.dirname(__FILE__)
|
2
|
+
require 'generator_mixin'
|
3
|
+
|
4
|
+
module Graphiti
|
5
|
+
class ApiTestGenerator < ::Rails::Generators::Base
|
6
|
+
include GeneratorMixin
|
7
|
+
|
8
|
+
source_root File.expand_path('../templates', __FILE__)
|
9
|
+
|
10
|
+
argument :resource, type: :string
|
11
|
+
class_option :'actions',
|
12
|
+
type: :array,
|
13
|
+
default: nil,
|
14
|
+
aliases: ['--actions', '-a'],
|
15
|
+
desc: 'Array of controller actions, e.g. "index show destroy"'
|
16
|
+
|
17
|
+
desc 'Generates rspec request specs at spec/api'
|
18
|
+
def generate
|
19
|
+
generate_api_specs
|
20
|
+
end
|
21
|
+
|
22
|
+
private
|
23
|
+
|
24
|
+
def var
|
25
|
+
dir.singularize
|
26
|
+
end
|
27
|
+
|
28
|
+
def dir
|
29
|
+
@resource.gsub('Resource', '').underscore.pluralize
|
30
|
+
end
|
31
|
+
|
32
|
+
def generate_api_specs
|
33
|
+
if actions?('index')
|
34
|
+
to = "spec/api/v1/#{dir}/index_spec.rb"
|
35
|
+
template('index_request_spec.rb.erb', to)
|
36
|
+
end
|
37
|
+
|
38
|
+
if actions?('show')
|
39
|
+
to = "spec/api/v1/#{dir}/show_spec.rb"
|
40
|
+
template('show_request_spec.rb.erb', to)
|
41
|
+
end
|
42
|
+
|
43
|
+
if actions?('create')
|
44
|
+
to = "spec/api/v1/#{dir}/create_spec.rb"
|
45
|
+
template('create_request_spec.rb.erb', to)
|
46
|
+
end
|
47
|
+
|
48
|
+
if actions?('update')
|
49
|
+
to = "spec/api/v1/#{dir}/update_spec.rb"
|
50
|
+
template('update_request_spec.rb.erb', to)
|
51
|
+
end
|
52
|
+
|
53
|
+
if actions?('destroy')
|
54
|
+
to = "spec/api/v1/#{dir}/destroy_spec.rb"
|
55
|
+
template('destroy_request_spec.rb.erb', to)
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
def resource_class
|
60
|
+
@resource.constantize
|
61
|
+
end
|
62
|
+
|
63
|
+
def type
|
64
|
+
resource_class.type
|
65
|
+
end
|
66
|
+
|
67
|
+
def model_class
|
68
|
+
resource_class.model
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
module Graphiti
|
2
|
+
module GeneratorMixin
|
3
|
+
def prompt(header: nil, description: nil, default: nil)
|
4
|
+
say(set_color("\n#{header}", :magenta, :bold)) if header
|
5
|
+
say("\n#{description}") if description
|
6
|
+
answer = ask(set_color("\n(default: #{default}):", :magenta, :bold))
|
7
|
+
answer = default if answer.blank? && default != 'nil'
|
8
|
+
say(set_color("\nGot it!\n", :white, :bold))
|
9
|
+
answer
|
10
|
+
end
|
11
|
+
|
12
|
+
def api_namespace
|
13
|
+
@api_namespace ||= begin
|
14
|
+
ns = graphiti_config['namespace']
|
15
|
+
|
16
|
+
if ns.blank?
|
17
|
+
ns = prompt \
|
18
|
+
header: "What is your API namespace?",
|
19
|
+
description: "This will be used as a route prefix, e.g. if you want the route '/books_api/v1/authors' your namespace would be 'books_api'",
|
20
|
+
default: 'api'
|
21
|
+
update_config!('namespace' => ns)
|
22
|
+
end
|
23
|
+
|
24
|
+
ns
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def actions
|
29
|
+
@options['actions'] || %w(index show create update destroy)
|
30
|
+
end
|
31
|
+
|
32
|
+
def actions?(*methods)
|
33
|
+
methods.any? { |m| actions.include?(m) }
|
34
|
+
end
|
35
|
+
|
36
|
+
def graphiti_config
|
37
|
+
File.exists?('.graphiticfg.yml') ? YAML.load_file('.graphiticfg.yml') : {}
|
38
|
+
end
|
39
|
+
|
40
|
+
def update_config!(attrs)
|
41
|
+
config = graphiti_config.merge(attrs)
|
42
|
+
File.open('.graphiticfg.yml', 'w') { |f| f.write(config.to_yaml) }
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
@@ -0,0 +1,99 @@
|
|
1
|
+
$:.unshift File.dirname(__FILE__)
|
2
|
+
require 'generator_mixin'
|
3
|
+
|
4
|
+
module Graphiti
|
5
|
+
class ResourceGenerator < ::Rails::Generators::NamedBase
|
6
|
+
include GeneratorMixin
|
7
|
+
|
8
|
+
source_root File.expand_path('../templates', __FILE__)
|
9
|
+
|
10
|
+
argument :attributes, type: :array, default: [], banner: "field[:type][:index] field[:type][:index]"
|
11
|
+
|
12
|
+
class_option :'omit-comments',
|
13
|
+
type: :boolean,
|
14
|
+
default: false,
|
15
|
+
aliases: ['--omit-comments', '-c'],
|
16
|
+
desc: 'Generate without documentation comments'
|
17
|
+
class_option :'actions',
|
18
|
+
type: :array,
|
19
|
+
default: nil,
|
20
|
+
aliases: ['--actions', '-a'],
|
21
|
+
desc: 'Array of controller actions to support, e.g. "index show destroy"'
|
22
|
+
|
23
|
+
desc "This generator creates a resource file at app/resources, as well as corresponding controller/specs/route/etc"
|
24
|
+
def generate_all
|
25
|
+
unless model_klass
|
26
|
+
raise "You must define a #{class_name} model before generating the corresponding resource."
|
27
|
+
end
|
28
|
+
|
29
|
+
generate_controller
|
30
|
+
generate_application_resource unless application_resource_defined?
|
31
|
+
generate_route
|
32
|
+
generate_resource
|
33
|
+
generate_resource_specs
|
34
|
+
generate_api_specs
|
35
|
+
end
|
36
|
+
|
37
|
+
private
|
38
|
+
|
39
|
+
def omit_comments?
|
40
|
+
@options['omit-comments']
|
41
|
+
end
|
42
|
+
|
43
|
+
def responders?
|
44
|
+
defined?(Responders)
|
45
|
+
end
|
46
|
+
|
47
|
+
def generate_controller
|
48
|
+
to = File.join('app/controllers', class_path, "#{file_name.pluralize}_controller.rb")
|
49
|
+
template('controller.rb.erb', to)
|
50
|
+
end
|
51
|
+
|
52
|
+
def generate_application_resource
|
53
|
+
to = File.join('app/resources', class_path, "application_resource.rb")
|
54
|
+
template('application_resource.rb.erb', to)
|
55
|
+
end
|
56
|
+
|
57
|
+
def application_resource_defined?
|
58
|
+
'ApplicationResource'.safe_constantize.present?
|
59
|
+
end
|
60
|
+
|
61
|
+
def generate_route
|
62
|
+
code = " resources :#{type}"
|
63
|
+
code << ", only: [#{actions.map { |a| ":#{a}" }.join(', ')}]" if actions.length < 5
|
64
|
+
code << "\n"
|
65
|
+
inject_into_file 'config/routes.rb', after: "scope path: '/v1' do\n" do
|
66
|
+
code
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
def generate_resource_specs
|
71
|
+
opts = {}
|
72
|
+
opts[:actions] = @options[:actions] if @options[:actions]
|
73
|
+
invoke 'graphiti:resource_test', [resource_klass], opts
|
74
|
+
end
|
75
|
+
|
76
|
+
def generate_api_specs
|
77
|
+
opts = {}
|
78
|
+
opts[:actions] = @options[:actions] if @options[:actions]
|
79
|
+
invoke 'graphiti:api_test', [resource_klass], opts
|
80
|
+
end
|
81
|
+
|
82
|
+
def generate_resource
|
83
|
+
to = File.join('app/resources', class_path, "#{file_name}_resource.rb")
|
84
|
+
template('resource.rb.erb', to)
|
85
|
+
end
|
86
|
+
|
87
|
+
def model_klass
|
88
|
+
class_name.safe_constantize
|
89
|
+
end
|
90
|
+
|
91
|
+
def resource_klass
|
92
|
+
"#{model_klass}Resource"
|
93
|
+
end
|
94
|
+
|
95
|
+
def type
|
96
|
+
model_klass.name.underscore.pluralize
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
@@ -0,0 +1,57 @@
|
|
1
|
+
$:.unshift File.dirname(__FILE__)
|
2
|
+
require 'generator_mixin'
|
3
|
+
|
4
|
+
module Graphiti
|
5
|
+
class ResourceTestGenerator < ::Rails::Generators::Base
|
6
|
+
include GeneratorMixin
|
7
|
+
|
8
|
+
source_root File.expand_path('../templates', __FILE__)
|
9
|
+
|
10
|
+
argument :resource, type: :string
|
11
|
+
argument :attributes, type: :array, default: [], banner: "field[:type][:index] field[:type][:index]"
|
12
|
+
class_option :'actions',
|
13
|
+
type: :array,
|
14
|
+
default: nil,
|
15
|
+
aliases: ['--actions', '-a'],
|
16
|
+
desc: 'Array of controller actions, e.g. "index show destroy"'
|
17
|
+
|
18
|
+
desc 'Generates rspec request specs at spec/api'
|
19
|
+
def generate
|
20
|
+
generate_resource_specs
|
21
|
+
end
|
22
|
+
|
23
|
+
private
|
24
|
+
|
25
|
+
def var
|
26
|
+
dir.singularize
|
27
|
+
end
|
28
|
+
|
29
|
+
def dir
|
30
|
+
@resource.gsub('Resource', '').underscore.pluralize
|
31
|
+
end
|
32
|
+
|
33
|
+
def generate_resource_specs
|
34
|
+
if actions?('create', 'update', 'destroy')
|
35
|
+
to = "spec/resources/#{var}/writes_spec.rb.rb"
|
36
|
+
template('resource_writes_spec.rb.erb', to)
|
37
|
+
end
|
38
|
+
|
39
|
+
if actions?('index', 'show')
|
40
|
+
to = "spec/resources/#{var}/reads_spec.rb.rb"
|
41
|
+
template('resource_reads_spec.rb.erb', to)
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
def resource_class
|
46
|
+
@resource.constantize
|
47
|
+
end
|
48
|
+
|
49
|
+
def type
|
50
|
+
resource_class.type
|
51
|
+
end
|
52
|
+
|
53
|
+
def model_class
|
54
|
+
resource_class.model
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
File without changes
|
File without changes
|
@@ -18,11 +18,11 @@ RSpec.describe "<%= type %>#create", type: :request do
|
|
18
18
|
end
|
19
19
|
|
20
20
|
it 'works' do
|
21
|
-
expect(<%=
|
21
|
+
expect(<%= resource_class %>).to receive(:build).and_call_original
|
22
22
|
expect {
|
23
23
|
make_request
|
24
|
-
}.to change { <%=
|
25
|
-
<%=
|
24
|
+
}.to change { <%= model_class %>.count }.by(1)
|
25
|
+
<%= var %> = <%= model_class %>.last
|
26
26
|
# assert on properties
|
27
27
|
expect(response.status).to eq(201)
|
28
28
|
end
|
@@ -2,16 +2,16 @@ require 'rails_helper'
|
|
2
2
|
|
3
3
|
RSpec.describe "<%= type %>#destroy", type: :request do
|
4
4
|
subject(:make_request) do
|
5
|
-
jsonapi_delete "/<%= api_namespace %>/v1/<%= type %>/#{<%=
|
5
|
+
jsonapi_delete "/<%= api_namespace %>/v1/<%= type %>/#{<%= var %>.id}"
|
6
6
|
end
|
7
7
|
|
8
8
|
describe 'basic destroy' do
|
9
|
-
let!(:<%=
|
9
|
+
let!(:<%= var %>) { create(:<%= var %>) }
|
10
10
|
|
11
11
|
it 'updates the resource' do
|
12
|
-
expect(<%=
|
13
|
-
expect { make_request }.to change { <%=
|
14
|
-
expect { <%=
|
12
|
+
expect(<%= resource_class %>).to receive(:find).and_call_original
|
13
|
+
expect { make_request }.to change { <%= model_class %>.count }.by(-1)
|
14
|
+
expect { <%= var %>.reload }
|
15
15
|
.to raise_error(ActiveRecord::RecordNotFound)
|
16
16
|
expect(response.status).to eq(200)
|
17
17
|
expect(json).to eq('meta' => {})
|
@@ -0,0 +1,22 @@
|
|
1
|
+
require 'rails_helper'
|
2
|
+
|
3
|
+
RSpec.describe "<%= type %>#index", type: :request do
|
4
|
+
let(:params) { {} }
|
5
|
+
|
6
|
+
subject(:make_request) do
|
7
|
+
jsonapi_get "/<%= api_namespace %>/v1/<%= type %>", params: params
|
8
|
+
end
|
9
|
+
|
10
|
+
describe 'basic fetch' do
|
11
|
+
let!(:<%= var %>1) { create(:<%= var %>) }
|
12
|
+
let!(:<%= var %>2) { create(:<%= var %>) }
|
13
|
+
|
14
|
+
it 'works' do
|
15
|
+
expect(<%= resource_class %>).to receive(:all).and_call_original
|
16
|
+
make_request
|
17
|
+
expect(response.status).to eq(200)
|
18
|
+
expect(d.map(&:jsonapi_type).uniq).to eq(['<%= type %>'])
|
19
|
+
expect(d.map(&:id)).to match_array([<%= var %>1.id, <%= var %>2.id])
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
File without changes
|
@@ -1,14 +1,14 @@
|
|
1
1
|
require 'rails_helper'
|
2
2
|
|
3
|
-
RSpec.describe <%=
|
3
|
+
RSpec.describe <%= resource_class %>, type: :resource do
|
4
4
|
describe 'serialization' do
|
5
|
-
let!(:<%=
|
5
|
+
let!(:<%= var %>) { create(:<%= var %>) }
|
6
6
|
|
7
7
|
it 'works' do
|
8
8
|
render
|
9
9
|
data = jsonapi_data[0]
|
10
|
-
expect(data.id).to eq(<%=
|
11
|
-
expect(data.jsonapi_type).to eq('<%=
|
10
|
+
expect(data.id).to eq(<%= var %>.id)
|
11
|
+
expect(data.jsonapi_type).to eq('<%= type %>')
|
12
12
|
<%- attributes.each do |a| -%>
|
13
13
|
<%- if [:created_at, :updated_at].include?(a.name.to_sym) -%>
|
14
14
|
expect(data.<%= a.name %>).to eq(datetime(<%= file_name %>.<%= a.name %>))
|
@@ -21,25 +21,25 @@ RSpec.describe <%= resource_klass %>, type: :resource do
|
|
21
21
|
<%- if actions?('index') -%>
|
22
22
|
|
23
23
|
describe 'filtering' do
|
24
|
-
let!(:<%=
|
25
|
-
let!(:<%=
|
24
|
+
let!(:<%= var %>1) { create(:<%= var %>) }
|
25
|
+
let!(:<%= var %>2) { create(:<%= var %>) }
|
26
26
|
|
27
27
|
context 'by id' do
|
28
28
|
before do
|
29
|
-
params[:filter] = { id: { eq: <%=
|
29
|
+
params[:filter] = { id: { eq: <%= var %>2.id } }
|
30
30
|
end
|
31
31
|
|
32
32
|
it 'works' do
|
33
33
|
render
|
34
|
-
expect(d.map(&:id)).to eq([<%=
|
34
|
+
expect(d.map(&:id)).to eq([<%= var %>2.id])
|
35
35
|
end
|
36
36
|
end
|
37
37
|
end
|
38
38
|
|
39
39
|
describe 'sorting' do
|
40
40
|
context 'by id descending' do
|
41
|
-
let!(:<%=
|
42
|
-
let!(:<%=
|
41
|
+
let!(:<%= var %>1) { create(:<%= var %>) }
|
42
|
+
let!(:<%= var %>2) { create(:<%= var %>) }
|
43
43
|
|
44
44
|
before do
|
45
45
|
params[:sort] = '-id'
|
@@ -48,8 +48,8 @@ RSpec.describe <%= resource_klass %>, type: :resource do
|
|
48
48
|
it 'works' do
|
49
49
|
render
|
50
50
|
expect(d.map(&:id)).to eq([
|
51
|
-
<%=
|
52
|
-
<%=
|
51
|
+
<%= var %>2.id,
|
52
|
+
<%= var %>1.id
|
53
53
|
])
|
54
54
|
end
|
55
55
|
end
|
@@ -1,63 +1,63 @@
|
|
1
1
|
require 'rails_helper'
|
2
2
|
|
3
|
-
RSpec.describe <%=
|
3
|
+
RSpec.describe <%= resource_class %>, type: :resource do
|
4
4
|
describe 'creating' do
|
5
5
|
let(:payload) do
|
6
6
|
{
|
7
7
|
data: {
|
8
|
-
type: '<%=
|
8
|
+
type: '<%= type %>',
|
9
9
|
attributes: { }
|
10
10
|
}
|
11
11
|
}
|
12
12
|
end
|
13
13
|
|
14
14
|
let(:instance) do
|
15
|
-
<%=
|
15
|
+
<%= resource_class %>.build(payload)
|
16
16
|
end
|
17
17
|
|
18
18
|
it 'works' do
|
19
19
|
expect {
|
20
20
|
expect(instance.save).to eq(true)
|
21
|
-
}.to change { <%=
|
21
|
+
}.to change { <%= model_class %>.count }.by(1)
|
22
22
|
end
|
23
23
|
end
|
24
24
|
|
25
25
|
describe 'updating' do
|
26
|
-
let!(:<%=
|
26
|
+
let!(:<%= var %>) { create(:<%= var %>) }
|
27
27
|
|
28
28
|
let(:payload) do
|
29
29
|
{
|
30
30
|
data: {
|
31
|
-
id: <%=
|
32
|
-
type: '<%=
|
31
|
+
id: <%= var %>.id.to_s,
|
32
|
+
type: '<%= type %>',
|
33
33
|
attributes: { } # Todo!
|
34
34
|
}
|
35
35
|
}
|
36
36
|
end
|
37
37
|
|
38
38
|
let(:instance) do
|
39
|
-
<%=
|
39
|
+
<%= resource_class %>.find(payload)
|
40
40
|
end
|
41
41
|
|
42
42
|
xit 'works (add some attributes and enable this spec)' do
|
43
43
|
expect {
|
44
44
|
expect(instance.update_attributes).to eq(true)
|
45
|
-
}.to change { <%=
|
45
|
+
}.to change { <%= var %>.reload.updated_at }
|
46
46
|
# and change { instance.foo }.to('bar') <- example
|
47
47
|
end
|
48
48
|
end
|
49
49
|
|
50
50
|
describe 'destroying' do
|
51
|
-
let!(:<%=
|
51
|
+
let!(:<%= var %>) { create(:<%= var %>) }
|
52
52
|
|
53
53
|
let(:instance) do
|
54
|
-
<%=
|
54
|
+
<%= resource_class %>.find(id: <%= var %>.id)
|
55
55
|
end
|
56
56
|
|
57
57
|
it 'works' do
|
58
58
|
expect {
|
59
59
|
expect(instance.destroy).to eq(true)
|
60
|
-
}.to change { <%=
|
60
|
+
}.to change { <%= model_class %>.count }.by(-1)
|
61
61
|
end
|
62
62
|
end
|
63
63
|
end
|