groovestack-base 0.1.5

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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 21487aa70317a98995fd2f11993f6679972cfbedfb0727e78abe72c8db9b352c
4
+ data.tar.gz: e673c4e3a53e23f6c16cc4337c7bb38ff3684a74d87b8c6aab3b2b70170039e5
5
+ SHA512:
6
+ metadata.gz: f7d41d9614e4dba925d3cdee07c0978230538219e0e8caef9ffeb0bfc297f938a9d64f7afb00106c299a7d835a591b724fbd15ddfd3624c225bd2a4feac7053c
7
+ data.tar.gz: c843c0f90a34b5bd64460fb6e872684d46d1bee0daf0faef9045fe4a791cbfda48c97225bbf815b43fef3d520544c69a630efb15598bcf2b8491e21d1e36537a
data/.rspec ADDED
@@ -0,0 +1 @@
1
+ --require spec_helper
data/.rubocop.yml ADDED
@@ -0,0 +1,44 @@
1
+ require:
2
+ - rubocop-graphql
3
+ - rubocop-rails
4
+
5
+ AllCops:
6
+ NewCops: enable
7
+ TargetRubyVersion: 3.1.3
8
+
9
+ GraphQL/FieldDescription:
10
+ Enabled: false
11
+
12
+ GraphQL/ObjectDescription:
13
+ Enabled: false
14
+
15
+ Metrics/BlockLength:
16
+ Exclude:
17
+ - spec/**/*
18
+ - '*.gemspec'
19
+
20
+ Metrics/MethodLength:
21
+ Max: 20
22
+
23
+ # TODO: reenable and replace puts / logs with global, configurable logger
24
+ Rails/Output:
25
+ Enabled: false
26
+
27
+ Style/Documentation:
28
+ Enabled: false
29
+
30
+ Style/RedundantConstantBase:
31
+ Enabled: false
32
+
33
+
34
+
35
+ # Style/StringLiterals:
36
+ # Enabled: true
37
+ # EnforcedStyle: double_quotes
38
+
39
+ # Style/StringLiteralsInInterpolation:
40
+ # Enabled: true
41
+ # EnforcedStyle: double_quotes
42
+
43
+ # Layout/LineLength:
44
+ # Max: 120
data/CHANGELOG.md ADDED
@@ -0,0 +1,4 @@
1
+ ## [0.1.5] - 2025-05-06
2
+
3
+ - Add CHANGELOG
4
+ - Update gemspec to new `groovestack` namespace
data/Gemfile ADDED
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ source 'https://rubygems.org'
4
+
5
+ # Specify your gem's dependencies in groovestack-base.gemspec
6
+ gemspec
7
+
8
+ gem 'minitest', '~> 5.0'
9
+ gem 'rake', '~> 12.0'
10
+ gem 'rspec'
11
+ gem 'rubocop', require: false
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ module GraphQL
4
+ class Schema
5
+ class Member
6
+ module BaseDSLMethods
7
+ def default_graphql_name
8
+ @default_graphql_name ||= begin
9
+ if name.nil?
10
+ raise ::GraphQL::RequiredImplementationMissingError,
11
+ 'Anonymous class should declare a `graphql_name`'
12
+ end
13
+
14
+ # ex name: "GraphQL::User::Type"
15
+ # ex name: "GraphQL::Admin::User::Type"
16
+
17
+ graphql_name = name.split('GraphQL').last.split('Types::').last.split('::Type').first.gsub('::', '')
18
+ # TODO: refactor
19
+ # assumption here that graphql schema has an admin namespace.
20
+ graphql_name.gsub('Admin', '')
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
data/config.ru ADDED
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rubygems'
4
+ require 'bundler'
5
+
6
+ Bundler.require :default, :development
7
+
8
+ # Combustion.schema_format = :sql
9
+ # Combustion.initialize! :active_record
10
+
11
+ # # Combustion.initialize! :all
12
+ # run Combustion::Application
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'lib/groovestack/base/version'
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = 'groovestack-base'
7
+ spec.version = Groovestack::Base::VERSION
8
+ spec.authors = ['Darren Rush']
9
+ spec.email = ['dlrush@gmail.com']
10
+
11
+ spec.summary = 'Shared extensions for CORE modules'
12
+ spec.description = 'Groovestack::Base defines reusable extensions for the CORE Platform.'
13
+ spec.post_install_message = 'Groovestack::Base installed'
14
+
15
+ spec.homepage = 'https://github.com/talysto/groovestack-core/'
16
+ spec.required_ruby_version = '>= 3.1.0'
17
+
18
+ spec.metadata['allowed_push_host'] = "https://rubygems.org"
19
+
20
+ spec.metadata['homepage_uri'] = spec.homepage
21
+ spec.metadata['source_code_uri'] = 'https://github.com/talysto/groovestack-core/'
22
+ spec.metadata['changelog_uri'] = 'https://github.com/talysto/groovestack-core/'
23
+
24
+ # Specify which files should be added to the gem when it is released.
25
+ # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
26
+ spec.files = Dir.chdir(File.expand_path(__dir__)) do
27
+ `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
28
+ end
29
+ spec.bindir = 'exe'
30
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
31
+ spec.require_paths = ['lib']
32
+
33
+ spec.add_dependency 'activerecord', '~> 7.0'
34
+ spec.add_dependency 'dry-configurable', '~> 1.0'
35
+ spec.add_dependency 'graphql', '~> 2.5'
36
+ spec.add_dependency 'pg', '~> 1.0'
37
+ spec.add_dependency 'pg_lock', '~> 1.0'
38
+ spec.add_dependency 'puma', '~> 5.0'
39
+
40
+ spec.metadata['rubygems_mfa_required'] = 'true'
41
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Groovestack
4
+ module Base
5
+ module ActiveRecord
6
+ module Authorization
7
+ module FieldsForSerialization
8
+ extend ActiveSupport::Concern
9
+
10
+ # required for memoization during graphql serialization
11
+ def authorized_fields_for_serialization(user)
12
+ @authorized_fields_for_serialization ||= Pundit.policy!(user, self).permitted_attributes_for_show
13
+ @authorized_fields_for_serialization
14
+ end
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Groovestack
4
+ module Base
5
+ module GraphQL
6
+ class BaseInputObject < ::GraphQL::Schema::InputObject
7
+ argument_class ::Groovestack::Base::GraphQL::Types::BaseArgument
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Groovestack
4
+ module Base
5
+ module GraphQL
6
+ class BaseMutation < ::GraphQL::Schema::Mutation
7
+ argument_class ::Groovestack::Base::GraphQL::Types::BaseArgument
8
+
9
+ def resolve(**args)
10
+ perform(**args)
11
+ rescue StandardError => e
12
+ Groovestack::Base.notify_error(self.class, e)
13
+
14
+ raise e
15
+ end
16
+
17
+ def perform(**args)
18
+ raise NotImplementedError
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Groovestack
4
+ module Base
5
+ module GraphQL
6
+ class BaseSubscription < ::GraphQL::Schema::Subscription
7
+ argument_class ::Groovestack::Base::GraphQL::Types::BaseArgument
8
+ field_class ::Groovestack::Base::GraphQL::Types::BaseField
9
+ object_class ::Groovestack::Base::GraphQL::Types::BaseObject
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'ostruct'
4
+
5
+ module Groovestack
6
+ module Base
7
+ module GraphQL
8
+ module Documentation
9
+ # rubocop:disable Style/OpenStructUse
10
+ Arguments = OpenStruct.new({
11
+ id: 'a unique record identifier',
12
+ page: 'page number of the paginated results',
13
+ per_page: 'number of records per page of the paginated results',
14
+ sort_field: 'field to sort the results by (i.e. created_at)',
15
+ sort_order: 'order with which to sort the results (i.e. ASC, DESC)',
16
+ filter: 'hash of parameters by which to filter the results (i.e. { ids: [1,2,3] })'
17
+ })
18
+ Fields = OpenStruct.new({
19
+ created_at: 'time of record creation',
20
+ id: 'a unique record identifier',
21
+ relation_count: 'total number of records in a given scope',
22
+ updated_at: 'time of last record update'
23
+ })
24
+
25
+ # rubocop:enable Style/OpenStructUse
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,152 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Groovestack
4
+ module Base
5
+ module GraphQL
6
+ module Helpers
7
+ module Types
8
+ module StatusEventVirtualAttributes
9
+ def all_events
10
+ object.class.aasm.events.map { |event| event.name.to_s }
11
+ end
12
+
13
+ def permitted_events
14
+ object.aasm.events(permitted: true).map { |event| event.name.to_s }
15
+ end
16
+
17
+ def status_events
18
+ all = all_events
19
+ permitted = permitted_events
20
+
21
+ all.map do |event|
22
+ { name: event.titleize, key: event, enabled: permitted.include?(event) }
23
+ end
24
+ end
25
+ end
26
+
27
+ module Typified
28
+ extend ActiveSupport::Concern
29
+
30
+ included do
31
+ field :type, String, null: true, resolver_method: :object_type
32
+
33
+ def object_type
34
+ object.class.to_s if object.respond_to?(:class)
35
+ end
36
+ end
37
+ end
38
+ end
39
+
40
+ module Mutations
41
+ module StatusEvents
42
+ def trigger_status_event!(obj:, attrs:, event:, args: nil, authorization_policy: nil) # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
43
+ # NOTE: args spread requires keys to be symbols, but by default
44
+ # they are passed as strings. This is why we use `symbolize_keys` here
45
+ raise ::GraphQL::ExecutionError, 'event not present' if event.blank?
46
+
47
+ event = event.gsub('!', '').to_sym
48
+
49
+ if attrs.present?
50
+ raise ::GraphQL::ExecutionError,
51
+ 'Cannot update multiple attributes when firing instance methods'
52
+ end
53
+ unless obj.aasm.events.map(&:name).include?(event)
54
+ raise ::GraphQL::ExecutionError,
55
+ "#{event.capitalize} unavailable"
56
+ end
57
+ unless authorization_policy.nil? || authorization_policy.permitted_aasm_events.include?(event)
58
+ raise ::GraphQL::ExecutionError,
59
+ "Unauthorized not allowed to #{event} this #{obj.class}"
60
+ end
61
+
62
+ event = :"#{event}!"
63
+
64
+ if args.present? && args.is_a?(Array)
65
+ obj.send(event.to_s, *args)
66
+ elsif args.present? && args.is_a?(Hash)
67
+ obj.send(event.to_s, **args.symbolize_keys!)
68
+ else
69
+ obj.send(event.to_s)
70
+ end
71
+ end
72
+ end
73
+
74
+ module InstanceMethods
75
+ def trigger_instance_method!(obj:, attrs:, instance_method:, args: nil, authorization_policy: nil) # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
76
+ raise GraphQL::ExecutionError, 'instance_method not present' if instance_method.blank?
77
+
78
+ instance_method = instance_method.to_sym
79
+
80
+ raise ::GraphQL::ExecutionError, 'instance_method undefined' unless obj.respond_to?(instance_method)
81
+
82
+ if attrs.present?
83
+ raise ::GraphQL::ExecutionError,
84
+ 'Cannot update multiple attributes when triggering instance method'
85
+ end
86
+ unless authorization_policy.nil? || authorization_policy.permitted_instance_methods.include?(instance_method) # rubocop:disable Layout/LineLength
87
+ raise ::GraphQL::ExecutionError,
88
+ "Unauthorized not allowed to trigger #{instance_method} for this #{obj.class}"
89
+ end
90
+
91
+ if args.present? && args.is_a?(Array)
92
+ obj.send(instance_method.to_s, *args)
93
+ elsif args.present? && args.is_a?(Hash)
94
+ obj.send(instance_method.to_s, **args.symbolize_keys!)
95
+ else
96
+ obj.send(instance_method.to_s)
97
+ end
98
+ end
99
+ end
100
+ end
101
+
102
+ module Controller
103
+ extend ActiveSupport::Concern
104
+
105
+ included do # rubocop:disable Metrics/BlockLength
106
+ private
107
+
108
+ def operation_name
109
+ params[:operationName]
110
+ end
111
+
112
+ def query
113
+ params[:query]
114
+ end
115
+
116
+ def variables
117
+ prepare_variables(params[:variables])
118
+ end
119
+
120
+ # Handle variables in form data, JSON body, or a blank value
121
+ def prepare_variables(variables_param)
122
+ case variables_param
123
+ when String
124
+ if variables_param.present?
125
+ JSON.parse(variables_param) || {}
126
+ else
127
+ {}
128
+ end
129
+ when Hash
130
+ variables_param
131
+ when ActionController::Parameters
132
+ variables_param.to_unsafe_hash # GraphQL-Ruby will validate name and type of incoming variables.
133
+ when nil
134
+ {}
135
+ else
136
+ raise ArgumentError, "Unexpected parameter: #{variables_param}"
137
+ end
138
+ end
139
+
140
+ def handle_error_in_development(err)
141
+ logger.error err.message
142
+ logger.error err.backtrace.join('\/n')
143
+
144
+ render json: { errors: [{ message: err.message, backtrace: err.backtrace }], data: {} },
145
+ status: :internal_server_error
146
+ end
147
+ end
148
+ end
149
+ end
150
+ end
151
+ end
152
+ end
@@ -0,0 +1,146 @@
1
+ # frozen_string_literal: true
2
+
3
+ # TODO: refactor according to rubocop suggestions
4
+
5
+ module Groovestack
6
+ module Base
7
+ module GraphQL
8
+ module Providers
9
+ module ReactAdmin
10
+ module Resource # rubocop:disable Metrics/ModuleLength
11
+ extend ActiveSupport::Concern
12
+
13
+ class_methods do # rubocop:disable Metrics/BlockLength
14
+ # TODO: make authorize default to true
15
+
16
+ # rubocop:disable Metrics/ParameterLists, Metrics/MethodLength, Metrics/AbcSize, Metrics/PerceivedComplexity, Metrics/CyclomaticComplexity
17
+ def react_admin_resource(entity,
18
+ class_name: nil,
19
+ graphql_path: nil,
20
+ graphql_type: nil,
21
+ graphql_filter: nil,
22
+ authorize: false,
23
+ visibility_permission: nil,
24
+ policy: nil,
25
+ camelize: ::Groovestack::Base.config.graphql.camelize,
26
+ **args)
27
+ # NOTE: class_name is only required if a custom _base_scope is not defined
28
+ # NOTE graphql_path is only required to override the default graphql path
29
+
30
+ graphql_namespace = graphql_path&.dup&.concat('::')
31
+ entity_model_name = entity.to_s.classify
32
+ entity_class = class_name || entity_model_name
33
+ entity_type = (graphql_type || "#{graphql_namespace}#{entity_model_name}::Type").constantize
34
+ entity_filter_type = (graphql_filter || "#{graphql_namespace}#{entity_model_name}::Filter").constantize
35
+ except = args.delete(:except) || []
36
+ policy ||= "#{entity_class}Policy"
37
+ base_scope = begin
38
+ entity_class.constantize.unscoped
39
+ rescue StandardError
40
+ nil
41
+ end
42
+
43
+ # resolver_method for Record
44
+
45
+ unless except.include?(:find)
46
+ define_method entity_model_name.to_sym do |id:|
47
+ scope = if authorize
48
+ policy.constantize::ShowScope.new(context[:current_user],
49
+ base_scope).resolve
50
+ else
51
+ base_scope
52
+ end
53
+ scope.find id
54
+ end
55
+ end
56
+
57
+ # resolver_method for Collection
58
+
59
+ unless except.include?(:collection)
60
+ define_method entity do |page: nil, per_page: nil, **attrs|
61
+ scope = if authorize
62
+ policy.constantize::IndexScope.new(context[:current_user],
63
+ base_scope).resolve
64
+ else
65
+ base_scope
66
+ end
67
+ scope = send("#{entity}_scope", **attrs, base_scope: scope)
68
+ scope = scope.offset(page * per_page).limit(per_page) if page.present? && scope.respond_to?(:offset)
69
+ scope
70
+ end
71
+ end
72
+
73
+ # resolver_method for Collection meta
74
+
75
+ unless except.include?(:collection_meta)
76
+ define_method :"#{entity}_meta" do |page: nil, per_page: nil, **attrs| # rubocop:disable Lint/UnusedBlockArgument
77
+ scope = if authorize
78
+ policy.constantize::IndexScope.new(context[:current_user],
79
+ base_scope).resolve
80
+ else
81
+ base_scope
82
+ end
83
+ { count: send("#{entity}_scope", **attrs, base_scope: scope).size }
84
+ end
85
+ end
86
+
87
+ # Record
88
+
89
+ unless except.include?(:find)
90
+ field entity_model_name.to_sym, entity_type,
91
+ null: true,
92
+ visibility_permission: visibility_permission,
93
+ resolver_method: entity_model_name.to_sym,
94
+ description: "Find #{entity_class}." do
95
+ argument :id, ::GraphQL::Types::ID, required: true, description: Documentation::Arguments.id
96
+ end
97
+ end
98
+
99
+ # Collection
100
+
101
+ unless except.include?(:collection)
102
+ field :"all_#{entity.to_s.underscore}",
103
+ type: [entity_type],
104
+ null: false,
105
+ camelize: camelize,
106
+ visibility_permission: visibility_permission,
107
+ resolver_method: entity do
108
+ argument :page, ::GraphQL::Types::Int, required: false, description: Documentation::Arguments.page
109
+ argument :per_page, ::GraphQL::Types::Int, required: false,
110
+ description: Documentation::Arguments.per_page
111
+ argument :sort_field, ::GraphQL::Types::String, required: false,
112
+ description: Documentation::Arguments.sort_field
113
+ argument :sort_order, ::GraphQL::Types::String, required: false,
114
+ description: Documentation::Arguments.sort_order
115
+ argument :filter, entity_filter_type, required: false, description: Documentation::Arguments.filter
116
+ end
117
+ end
118
+
119
+ # Collection meta
120
+
121
+ return if except.include?(:collection_meta)
122
+
123
+ field :"_all_#{entity.to_s.underscore}_meta",
124
+ type: ::Groovestack::Base::GraphQL::Providers::ReactAdmin::Types::RAListMetadata,
125
+ camelize: camelize,
126
+ null: true,
127
+ visibility_permission: visibility_permission,
128
+ resolver_method: :"#{entity}_meta" do
129
+ argument :page, ::GraphQL::Types::Int, required: false, description: Documentation::Arguments.page
130
+ argument :per_page, ::GraphQL::Types::Int, required: false,
131
+ description: Documentation::Arguments.per_page
132
+ argument :sort_field, ::GraphQL::Types::String, required: false,
133
+ description: Documentation::Arguments.sort_field
134
+ argument :sort_order, ::GraphQL::Types::String, required: false,
135
+ description: Documentation::Arguments.sort_order
136
+ argument :filter, entity_filter_type, required: false, description: Documentation::Arguments.filter
137
+ end
138
+ end
139
+ # rubocop:enable Metrics/ParameterLists, Metrics/MethodLength, Metrics/AbcSize, Metrics/PerceivedComplexity, Metrics/CyclomaticComplexity
140
+ end
141
+ end
142
+ end
143
+ end
144
+ end
145
+ end
146
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Groovestack
4
+ module Base
5
+ module GraphQL
6
+ module Providers
7
+ module ReactAdmin
8
+ module Types
9
+ class RAListMetadata < ::GraphQL::Schema::Object
10
+ field :count, Int, null: false, description: Documentation::Fields.relation_count
11
+ end
12
+ end
13
+ end
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Groovestack
4
+ module Base
5
+ module GraphQL
6
+ module Subscriptions
7
+ class EventHandler
8
+ def self.trigger(subscription, args, event, kwargs)
9
+ triggered = false
10
+
11
+ ::GraphQL::Schema.descendants.each do |schema|
12
+ subscription_type = schema.types['Subscription'] || schema.types['SubscriptionType']
13
+ next if subscription_type.blank?
14
+ next unless schema.get_fields(subscription_type).keys.include?(subscription.to_s)
15
+
16
+ triggered = true
17
+ schema.subscriptions.trigger(subscription, args, event, **kwargs)
18
+ end
19
+
20
+ puts 'app schema not defined' unless triggered
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,63 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Groovestack
4
+ module Base
5
+ module GraphQL
6
+ module Tracers
7
+ # The AtmoicMultiplexTransaction module provides a mechanism to execute GraphQL multiplex queries
8
+ # within a database transaction. If any of the queries in the multiplex is a mutation, the entire
9
+ # multiplex is executed within a transaction. If an error occurs during the execution of a mutation,
10
+ # the transaction is rolled back, and the errors are returned with null data.
11
+ #
12
+ # Methods:
13
+ # - execute_multiplex(multiplex:): Executes the provided multiplex within a transaction if any of the
14
+ # queries is a mutation. If an error occurs during the execution of a mutation, the transaction is
15
+ # rolled back, and the errors are returned with null data.
16
+ #
17
+
18
+ # NOTE: This module assumes that the multiplex object responds to `queries` and each query
19
+ # responds to `mutation?`.
20
+
21
+ module AtomicMultiplexTransaction
22
+ # rubocop:disable Metrics/AbcSize, Metrics/MethodLength, Metrics/PerceivedComplexity, Metrics/CyclomaticComplexity
23
+ def execute_multiplex(multiplex:)
24
+ is_mutation = multiplex.queries.any?(&:mutation?)
25
+
26
+ return yield unless is_mutation
27
+
28
+ results = nil
29
+ rollback = false
30
+
31
+ begin
32
+ ::ActiveRecord::Base.transaction do
33
+ results = yield
34
+
35
+ rollback = results.any? do |result|
36
+ result.is_a?(::GraphQL::Query::Result) && result.to_h['errors'].present?
37
+ end
38
+
39
+ raise ::ActiveRecord::Rollback if rollback
40
+
41
+ results
42
+ end
43
+ rescue ::ActiveRecord::Rollback
44
+ rollback = true
45
+ end
46
+
47
+ return results unless rollback
48
+
49
+ # IF rollback, extract errors and return null for data
50
+
51
+ results.map do |result|
52
+ ::GraphQL::Query::Result.new(
53
+ query: result.query,
54
+ values: result.to_h.merge('data' => nil)
55
+ )
56
+ end
57
+ end
58
+ # rubocop:enable Metrics/AbcSize, Metrics/MethodLength, Metrics/PerceivedComplexity, Metrics/CyclomaticComplexity
59
+ end
60
+ end
61
+ end
62
+ end
63
+ end