servactory 2.5.0.rc2 → 2.5.0.rc3

Sign up to get free protection for your applications and to get access to all the features.
Files changed (26) hide show
  1. checksums.yaml +4 -4
  2. data/lib/generators/servactory/install_generator.rb +21 -0
  3. data/lib/generators/servactory/rspec_generator.rb +88 -0
  4. data/lib/generators/servactory/service_generator.rb +49 -0
  5. data/lib/generators/servactory/templates/services/application_service/base.rb +60 -0
  6. data/lib/generators/servactory/templates/services/application_service/exceptions.rb +11 -0
  7. data/lib/generators/servactory/templates/services/application_service/result.rb +5 -0
  8. data/lib/servactory/context/workspace/internals.rb +1 -1
  9. data/lib/servactory/info/dsl.rb +56 -4
  10. data/lib/servactory/result.rb +1 -1
  11. data/lib/servactory/test_kit/rspec/helpers.rb +95 -0
  12. data/lib/servactory/test_kit/rspec/matchers/have_service_attribute_matchers/consists_of_matcher.rb +121 -0
  13. data/lib/servactory/test_kit/rspec/matchers/have_service_attribute_matchers/inclusion_matcher.rb +70 -0
  14. data/lib/servactory/test_kit/rspec/matchers/have_service_attribute_matchers/must_matcher.rb +61 -0
  15. data/lib/servactory/test_kit/rspec/matchers/have_service_attribute_matchers/types_matcher.rb +72 -0
  16. data/lib/servactory/test_kit/rspec/matchers/have_service_input_matcher.rb +203 -0
  17. data/lib/servactory/test_kit/rspec/matchers/have_service_input_matchers/default_matcher.rb +67 -0
  18. data/lib/servactory/test_kit/rspec/matchers/have_service_input_matchers/optional_matcher.rb +63 -0
  19. data/lib/servactory/test_kit/rspec/matchers/have_service_input_matchers/required_matcher.rb +78 -0
  20. data/lib/servactory/test_kit/rspec/matchers/have_service_input_matchers/valid_with_matcher.rb +233 -0
  21. data/lib/servactory/test_kit/rspec/matchers/have_service_internal_matcher.rb +148 -0
  22. data/lib/servactory/test_kit/rspec/matchers.rb +295 -0
  23. data/lib/servactory/test_kit/utils/faker.rb +78 -0
  24. data/lib/servactory/version.rb +1 -1
  25. data/lib/servactory.rb +1 -0
  26. metadata +21 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ce8eb77c5347f231539b115081d7706f6546830ccda8269e414ba05d097d3d85
4
- data.tar.gz: 3393084486439b10177a5a13eb4826f4b2ab19c21aefbe92a23aaa7c5ce0096c
3
+ metadata.gz: 1e7661bf7b65a1dd0c2214be8364897c91522116fb208de82ac7869f4ec81534
4
+ data.tar.gz: f87706b5056118c9c772a08b9cd13047c79d121eb8dbdc21447e21677c4d1504
5
5
  SHA512:
6
- metadata.gz: 4ae8873bb459d8bedac4d64cee6ca8f4049e57fd87f62e3842b47e5f20415196cc6470704e3d9fd1d2c380e75ae2ffdcd5f93ad5dfc87e677b259ef215d8974b
7
- data.tar.gz: c0a37302c68b09fa238656f88bcdd536bb81d1a5f47db60ac8de52d7455d534b3622cbcd5568474e7d303b86bb7a6f5219f1f7e5fa0c7a5a0833d955194406d9
6
+ metadata.gz: 62ff3f88d0df55e1a02761990cc0b624bb151ef0f7d5fe2f5e5f13f22934d88e50c036e97faa854b27b7a34bfb05fcef4d839e3cc4c0aed1b282613b93a37da5
7
+ data.tar.gz: 1f78163c75c54c7f6eaac73b9ceaa8b433964bfae3ba0d45fb0bc743a5a4d586f19453ccb1d4e67d67e8dbd3157d32d8b5c047405550775b6aab6976c0944e98
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rails/generators/base"
4
+
5
+ module Servactory
6
+ module Generators
7
+ class InstallGenerator < Rails::Generators::Base
8
+ source_root File.expand_path("templates", __dir__)
9
+
10
+ def copy_services
11
+ directory "services/application_service", "app/services/application_service"
12
+ end
13
+
14
+ def copy_locales
15
+ %i[en ru].each do |locale|
16
+ copy_file "../../../../config/locales/#{locale}.yml", "config/locales/servactory.#{locale}.yml"
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,88 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rails/generators/named_base"
4
+
5
+ module Servactory
6
+ module Generators
7
+ class RspecGenerator < Rails::Generators::NamedBase
8
+ source_root File.expand_path("templates", __dir__)
9
+
10
+ argument :attributes, type: :array, default: [], banner: "input_name"
11
+
12
+ def create_service
13
+ create_file "app/services/#{file_path}_spec.rb" do
14
+ <<~RUBY
15
+ # frozen_string_literal: true
16
+
17
+ RSpec.describe #{class_name}, type: :service do
18
+ pending "add some examples to (or delete) \#{__FILE__}"
19
+
20
+ # let(:attributes) do
21
+ # {
22
+ #{input_attribute_draw}
23
+ # }
24
+ # end
25
+ #
26
+ #{input_let_draw}
27
+ #
28
+ # describe "validation" do
29
+ # describe "inputs" do
30
+ #{input_validation_draw}
31
+ # end
32
+ #
33
+ # describe "internals" do
34
+ # it { expect { perform }.to have_internal(:some_data).type(String) }
35
+ # end
36
+ # end
37
+ #
38
+ # describe ".call!" do
39
+ # subject(:perform) { described_class.call!(**attributes) }
40
+ #
41
+ # describe "and the data required for work is also valid" do
42
+ # it { expect(perform).to be_success_service }
43
+ #
44
+ # # ...
45
+ # end
46
+ #
47
+ # describe "but the data required for work is invalid" do
48
+ # # Provide a reason why the data is invalid and then use this:
49
+ # it { expect(perform).to be_failure_service }
50
+ #
51
+ # # ...
52
+ # end
53
+ # end
54
+ end
55
+ RUBY
56
+ end
57
+ end
58
+
59
+ def input_attribute_draw
60
+ input_names.map do |input_name|
61
+ <<~RUBY.strip
62
+ # #{input_name}: #{input_name}
63
+ RUBY
64
+ end.join(",\n ")
65
+ end
66
+
67
+ def input_let_draw
68
+ input_names.map do |input_name|
69
+ <<~RUBY.strip
70
+ # let(:#{input_name}) { "Some value" }
71
+ RUBY
72
+ end.join("\n ")
73
+ end
74
+
75
+ def input_validation_draw
76
+ input_names.map do |input_name|
77
+ <<~RUBY.strip
78
+ # it { expect { perform }.to have_input(:#{input_name}).valid_with(attributes).type(String).required }
79
+ RUBY
80
+ end.join("\n ")
81
+ end
82
+
83
+ def input_names
84
+ @input_names ||= attributes_names
85
+ end
86
+ end
87
+ end
88
+ end
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rails/generators/named_base"
4
+
5
+ module Servactory
6
+ module Generators
7
+ class ServiceGenerator < Rails::Generators::NamedBase
8
+ source_root File.expand_path("templates", __dir__)
9
+
10
+ argument :attributes, type: :array, default: [], banner: "input_name"
11
+
12
+ def create_service # rubocop:disable Metrics/MethodLength
13
+ create_file "app/services/#{file_path}.rb" do
14
+ <<~RUBY
15
+ # frozen_string_literal: true
16
+
17
+ class #{class_name} < ApplicationService::Base
18
+ #{input_draw}
19
+
20
+ output :result, type: Symbol
21
+
22
+ make :something
23
+
24
+ private
25
+
26
+ def something
27
+ # Write your code here
28
+
29
+ outputs.result = :done
30
+ end
31
+ end
32
+ RUBY
33
+ end
34
+ end
35
+
36
+ def input_draw
37
+ input_names.map do |input_name|
38
+ <<~RUBY.squish
39
+ input :#{input_name}, type: String
40
+ RUBY
41
+ end.join("\n ")
42
+ end
43
+
44
+ def input_names
45
+ @input_names ||= attributes_names
46
+ end
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,60 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ApplicationService
4
+ class Base
5
+ include Servactory::DSL
6
+
7
+ # More information: https://servactory.com/guide/extensions
8
+ # include Servactory::DSL.with_extensions(
9
+ # ApplicationService::Extensions::YourExtension::DSL
10
+ # )
11
+
12
+ fail_on! ActiveRecord::RecordInvalid
13
+
14
+ # More information: https://servactory.com/guide/configuration
15
+ configuration do
16
+ input_exception_class ApplicationService::Exceptions::Input
17
+ internal_exception_class ApplicationService::Exceptions::Internal
18
+ output_exception_class ApplicationService::Exceptions::Output
19
+
20
+ failure_class ApplicationService::Exceptions::Failure
21
+
22
+ result_class ApplicationService::Result
23
+
24
+ # input_option_helpers(
25
+ # [
26
+ # Servactory::ToolKit::DynamicOptions::Format.use,
27
+ # Servactory::ToolKit::DynamicOptions::Min.use,
28
+ # Servactory::ToolKit::DynamicOptions::Max.use,
29
+ # ApplicationService::DynamicOptions::CustomEq.use
30
+ # ]
31
+ # )
32
+
33
+ # internal_option_helpers(
34
+ # [
35
+ # Servactory::ToolKit::DynamicOptions::Format.use,
36
+ # Servactory::ToolKit::DynamicOptions::Min.use,
37
+ # Servactory::ToolKit::DynamicOptions::Max.use,
38
+ # ApplicationService::DynamicOptions::CustomEq.use
39
+ # ]
40
+ # )
41
+
42
+ # output_option_helpers(
43
+ # [
44
+ # Servactory::ToolKit::DynamicOptions::Format.use,
45
+ # Servactory::ToolKit::DynamicOptions::Min.use,
46
+ # Servactory::ToolKit::DynamicOptions::Max.use,
47
+ # ApplicationService::DynamicOptions::CustomEq.use
48
+ # ]
49
+ # )
50
+
51
+ # collection_mode_class_names [ActiveRecord::Relation]
52
+
53
+ # hash_mode_class_names [CustomHash]
54
+
55
+ # action_shortcuts %i[assign build create save]
56
+
57
+ # action_aliases %i[do_it!]
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ApplicationService
4
+ module Exceptions
5
+ class Input < Servactory::Exceptions::Input; end
6
+ class Output < Servactory::Exceptions::Output; end
7
+ class Internal < Servactory::Exceptions::Internal; end
8
+
9
+ class Failure < Servactory::Exceptions::Failure; end
10
+ end
11
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ApplicationService
4
+ class Result < Servactory::Result; end
5
+ end
@@ -75,7 +75,7 @@ module Servactory
75
75
  internal_name: name
76
76
  )
77
77
 
78
- raise @context.class.config.input_exception_class.new(message: message_text)
78
+ raise @context.class.config.internal_exception_class.new(message: message_text)
79
79
  end
80
80
  end
81
81
  end
@@ -8,11 +8,63 @@ module Servactory
8
8
  end
9
9
 
10
10
  module ClassMethods
11
- def info
11
+ def info # rubocop:disable Metrics/MethodLength, Metrics/AbcSize
12
12
  Servactory::Info::Result.new(
13
- inputs: collection_of_inputs.names,
14
- internals: collection_of_internals.names,
15
- outputs: collection_of_outputs.names
13
+ inputs: collection_of_inputs.to_h do |input|
14
+ work = input.class::Work.new(input)
15
+ consists_of = input.collection_of_options.find_by(name: :consists_of)
16
+ inclusion = input.collection_of_options.find_by(name: :inclusion)
17
+ must = input.collection_of_options.find_by(name: :must)
18
+
19
+ [
20
+ input.name,
21
+ {
22
+ work: work,
23
+ types: input.types,
24
+ required: input.required,
25
+ default: input.default,
26
+ consists_of: consists_of.body,
27
+ inclusion: inclusion.body,
28
+ must: must.body
29
+ }
30
+ ]
31
+ end,
32
+
33
+ internals: collection_of_internals.to_h do |internal|
34
+ work = internal.class::Work.new(internal)
35
+ consists_of = internal.collection_of_options.find_by(name: :consists_of)
36
+ inclusion = internal.collection_of_options.find_by(name: :inclusion)
37
+ must = internal.collection_of_options.find_by(name: :must)
38
+
39
+ [
40
+ internal.name,
41
+ {
42
+ work: work,
43
+ types: internal.types,
44
+ consists_of: consists_of.body,
45
+ inclusion: inclusion.body,
46
+ must: must.body
47
+ }
48
+ ]
49
+ end,
50
+
51
+ outputs: collection_of_outputs.to_h do |output|
52
+ work = output.class::Work.new(output)
53
+ consists_of = output.collection_of_options.find_by(name: :consists_of)
54
+ inclusion = output.collection_of_options.find_by(name: :inclusion)
55
+ must = output.collection_of_options.find_by(name: :must)
56
+
57
+ [
58
+ output.name,
59
+ {
60
+ work: work,
61
+ types: output.types,
62
+ consists_of: consists_of.body,
63
+ inclusion: inclusion.body,
64
+ must: must.body
65
+ }
66
+ ]
67
+ end
16
68
  )
17
69
  end
18
70
  end
@@ -110,7 +110,7 @@ module Servactory
110
110
  ########################################################################
111
111
 
112
112
  def rescue_no_method_error_with(exception:) # rubocop:disable Metrics/MethodLength
113
- raise exception if @context.blank?
113
+ raise exception if @context.blank? || @context.instance_of?(Servactory::TestKit::Result)
114
114
 
115
115
  raise @context.class.config.failure_class.new(
116
116
  type: :base,
@@ -0,0 +1,95 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Servactory
4
+ module TestKit
5
+ module Rspec
6
+ module Helpers
7
+ def allow_service_as_success!(service_class_name, &block)
8
+ allow_service!(service_class_name, :as_success, &block)
9
+ end
10
+
11
+ def allow_service_as_success(service_class_name, &block)
12
+ allow_service(service_class_name, :as_success, &block)
13
+ end
14
+
15
+ def allow_service_as_failure!(service_class_name, &block)
16
+ allow_service!(service_class_name, :as_failure, &block)
17
+ end
18
+
19
+ def allow_service_as_failure(service_class_name, &block)
20
+ allow_service(service_class_name, :as_failure, &block)
21
+ end
22
+
23
+ ########################################################################
24
+
25
+ def allow_service!(service_class_name, result_type, &block)
26
+ allow_servactory(service_class_name, :call!, result_type, &block)
27
+ end
28
+
29
+ def allow_service(service_class_name, result_type, &block)
30
+ allow_servactory(service_class_name, :call, result_type, &block)
31
+ end
32
+
33
+ ########################################################################
34
+
35
+ # rubocop:disable Metrics/MethodLength, Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
36
+ def allow_servactory(service_class_name, method_call, result_type)
37
+ method_call = method_call.to_sym
38
+ result_type = result_type.to_sym
39
+
40
+ unless %i[call! call].include?(method_call)
41
+ raise ArgumentError, "Invalid value for `method_call`. Must be `:call!` or `:call`."
42
+ end
43
+
44
+ unless %i[as_success as_failure].include?(result_type)
45
+ raise ArgumentError, "Invalid value for `result_type`. Must be `:as_success` or `:as_failure`."
46
+ end
47
+
48
+ as_success = result_type == :as_success
49
+ with_bang = method_call == :call!
50
+
51
+ if block_given? && !yield.is_a?(Hash) && as_success
52
+ raise ArgumentError, "Invalid value for block. Must be a Hash with attributes."
53
+ end
54
+
55
+ and_return_or_raise = with_bang && !as_success ? :and_raise : :and_return
56
+
57
+ result = if block_given?
58
+ if yield.is_a?(Hash)
59
+ yield
60
+ else
61
+ { with_bang ? :exception : :error => yield }
62
+ end
63
+ else
64
+ {}
65
+ end
66
+
67
+ # puts
68
+ # puts <<~RUBY
69
+ # allow(#{service_class_name}).to(
70
+ # receive(#{method_call.inspect})
71
+ # .public_send(
72
+ # #{and_return_or_raise.inspect},
73
+ # Servactory::TestKit::Result.public_send(#{result_type.inspect}, #{result})
74
+ # )
75
+ # )
76
+ # RUBY
77
+ # puts
78
+
79
+ allow(service_class_name).to(
80
+ receive(method_call)
81
+ .public_send(
82
+ and_return_or_raise,
83
+ if as_success
84
+ Servactory::TestKit::Result.public_send(result_type, **result)
85
+ else
86
+ Servactory::TestKit::Result.public_send(result_type, result)
87
+ end
88
+ )
89
+ )
90
+ end
91
+ # rubocop:enable Metrics/MethodLength, Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
92
+ end
93
+ end
94
+ end
95
+ end
@@ -0,0 +1,121 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Servactory
4
+ module TestKit
5
+ module Rspec
6
+ module Matchers
7
+ module HaveServiceAttributeMatchers
8
+ class ConsistsOfMatcher
9
+ attr_reader :missing_option
10
+
11
+ def initialize(described_class, attribute_type, attribute_name, option_types, consists_of_types,
12
+ custom_message)
13
+ @described_class = described_class
14
+ @attribute_type = attribute_type
15
+ @attribute_type_plural = attribute_type.to_s.pluralize.to_sym
16
+ @attribute_name = attribute_name
17
+ @option_types = option_types
18
+ @consists_of_types = consists_of_types
19
+ @custom_message = custom_message
20
+
21
+ @attribute_data = described_class.info.public_send(attribute_type_plural).fetch(attribute_name)
22
+
23
+ @missing_option = ""
24
+ end
25
+
26
+ def description
27
+ result = "consists_of: "
28
+ result + consists_of_types.join(", ")
29
+ end
30
+
31
+ def matches?(subject)
32
+ if submatcher_passes?(subject)
33
+ true
34
+ else
35
+ @missing_option = build_missing_option
36
+
37
+ false
38
+ end
39
+ end
40
+
41
+ private
42
+
43
+ attr_reader :described_class,
44
+ :attribute_type,
45
+ :attribute_type_plural,
46
+ :attribute_name,
47
+ :option_types,
48
+ :consists_of_types,
49
+ :custom_message,
50
+ :attribute_data
51
+
52
+ def submatcher_passes?(_subject)
53
+ attribute_consists_of = Array(attribute_data.fetch(:consists_of).fetch(:type) || [])
54
+
55
+ matched = attribute_consists_of.difference(consists_of_types).empty?
56
+
57
+ matched &&= attribute_consists_of_message.casecmp(custom_message).zero? if custom_message.present?
58
+
59
+ matched
60
+ end
61
+
62
+ def attribute_consists_of_message # rubocop:disable Metrics/MethodLength, Metrics/AbcSize
63
+ attribute_consists_of_message = attribute_data.fetch(:consists_of).fetch(:message)
64
+
65
+ if attribute_consists_of_message.nil?
66
+ I18n.t(
67
+ "servactory.#{attribute_type_plural}.validations.required.default_error.for_collection",
68
+ service_class_name: described_class.name,
69
+ "#{attribute_type}_name": attribute_name
70
+ )
71
+ elsif attribute_consists_of_message.is_a?(Proc)
72
+ input_work = attribute_data.fetch(:work)
73
+
74
+ attribute_consists_of_message.call(
75
+ input: input_work,
76
+ expected_type: String,
77
+ given_type: Servactory::TestKit::FakeType.new.class.name
78
+ )
79
+ else
80
+ attribute_consists_of_message
81
+ end
82
+ end
83
+
84
+ def build_missing_option # rubocop:disable Metrics/MethodLength, Metrics/AbcSize
85
+ attribute_consists_of = Array(attribute_data.fetch(:consists_of).fetch(:type) || [])
86
+
87
+ unless attribute_consists_of.difference(consists_of_types).empty?
88
+ text_about_types = option_types.size > 1 ? "the following types" : "type"
89
+
90
+ return <<~MESSAGE
91
+ should be a collection consisting of #{text_about_types}
92
+
93
+ expected #{consists_of_types.inspect}
94
+ got #{attribute_consists_of.inspect}
95
+ MESSAGE
96
+ end
97
+
98
+ if custom_message.present? && !attribute_consists_of_message.casecmp(custom_message).zero?
99
+ return <<~MESSAGE
100
+ should be a collection with a message
101
+
102
+ expected #{custom_message.inspect}
103
+ got #{attribute_consists_of_message.inspect}
104
+ MESSAGE
105
+ end
106
+
107
+ <<~MESSAGE
108
+ got an unexpected case when using `consists_of`
109
+
110
+ Please try to build an example based on the documentation.
111
+ Or report your problem to us:
112
+
113
+ https://github.com/servactory/servactory/issues
114
+ MESSAGE
115
+ end
116
+ end
117
+ end
118
+ end
119
+ end
120
+ end
121
+ end
@@ -0,0 +1,70 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Servactory
4
+ module TestKit
5
+ module Rspec
6
+ module Matchers
7
+ module HaveServiceAttributeMatchers
8
+ class InclusionMatcher
9
+ attr_reader :missing_option
10
+
11
+ def initialize(described_class, attribute_type, attribute_name, values)
12
+ @described_class = described_class
13
+ @attribute_type = attribute_type
14
+ @attribute_type_plural = attribute_type.to_s.pluralize.to_sym
15
+ @attribute_name = attribute_name
16
+ @values = values
17
+
18
+ @attribute_data = described_class.info.public_send(attribute_type_plural).fetch(attribute_name)
19
+
20
+ @missing_option = ""
21
+ end
22
+
23
+ def description
24
+ "inclusion: #{values.join(', ')}"
25
+ end
26
+
27
+ def matches?(subject)
28
+ if submatcher_passes?(subject)
29
+ true
30
+ else
31
+ @missing_option = build_missing_option
32
+
33
+ false
34
+ end
35
+ end
36
+
37
+ private
38
+
39
+ attr_reader :described_class,
40
+ :attribute_type,
41
+ :attribute_type_plural,
42
+ :attribute_name,
43
+ :values,
44
+ :attribute_data
45
+
46
+ def submatcher_passes?(_subject)
47
+ attribute_inclusion = attribute_data.fetch(:inclusion)
48
+ attribute_inclusion_in = attribute_inclusion.fetch(:in)
49
+
50
+ attribute_inclusion_in.difference(values).empty? &&
51
+ values.difference(attribute_inclusion_in).empty?
52
+ end
53
+
54
+ def build_missing_option
55
+ attribute_inclusion = attribute_data.fetch(:inclusion)
56
+ attribute_inclusion_in = attribute_inclusion.fetch(:in)
57
+
58
+ <<~MESSAGE
59
+ should include the expected values
60
+
61
+ expected #{values.inspect}
62
+ got #{attribute_inclusion_in.inspect}
63
+ MESSAGE
64
+ end
65
+ end
66
+ end
67
+ end
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,61 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Servactory
4
+ module TestKit
5
+ module Rspec
6
+ module Matchers
7
+ module HaveServiceAttributeMatchers
8
+ class MustMatcher
9
+ attr_reader :missing_option
10
+
11
+ def initialize(described_class, attribute_type, attribute_name, must_names)
12
+ @described_class = described_class
13
+ @attribute_type = attribute_type
14
+ @attribute_type_plural = attribute_type.to_s.pluralize.to_sym
15
+ @attribute_name = attribute_name
16
+ @must_names = must_names
17
+
18
+ @attribute_data = described_class.info.public_send(attribute_type_plural).fetch(attribute_name)
19
+
20
+ @missing_option = ""
21
+ end
22
+
23
+ def description
24
+ "must: #{must_names.join(', ')}"
25
+ end
26
+
27
+ def matches?(subject)
28
+ if submatcher_passes?(subject)
29
+ true
30
+ else
31
+ @missing_option = build_missing_option
32
+
33
+ false
34
+ end
35
+ end
36
+
37
+ private
38
+
39
+ attr_reader :described_class,
40
+ :attribute_type,
41
+ :attribute_type_plural,
42
+ :attribute_name,
43
+ :must_names,
44
+ :attribute_data
45
+
46
+ def submatcher_passes?(_subject)
47
+ attribute_must = attribute_data.fetch(:must)
48
+
49
+ attribute_must.keys.difference(must_names).empty? &&
50
+ must_names.difference(attribute_must.keys).empty?
51
+ end
52
+
53
+ def build_missing_option
54
+ "should #{must_names.join(', ')}"
55
+ end
56
+ end
57
+ end
58
+ end
59
+ end
60
+ end
61
+ end