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
@@ -0,0 +1,233 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Servactory
4
+ module TestKit
5
+ module Rspec
6
+ module Matchers
7
+ module HaveServiceInputMatchers
8
+ class ValidWithMatcher # rubocop:disable Metrics/ClassLength
9
+ attr_reader :missing_option
10
+
11
+ def initialize(described_class, attribute_type, attribute_name, attributes)
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
+ @attributes = attributes
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
+ "valid_with attribute checking"
25
+ end
26
+
27
+ def matches?(subject)
28
+ if attributes.is_a?(FalseClass) || 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
+ :attributes,
44
+ :attribute_data
45
+
46
+ def submatcher_passes?(_subject) # rubocop:disable Metrics/CyclomaticComplexity
47
+ success_passes? &&
48
+ failure_type_passes? &&
49
+ failure_required_passes? &&
50
+ failure_optional_passes? &&
51
+ failure_consists_of_passes? &&
52
+ failure_format_passes? &&
53
+ failure_inclusion_passes? &&
54
+ failure_must_passes?
55
+ end
56
+
57
+ def success_passes?
58
+ expect_success_with!(attributes)
59
+ end
60
+
61
+ def failure_type_passes? # rubocop:disable Metrics/MethodLength, Metrics/AbcSize
62
+ option_types = attribute_data.fetch(:types)
63
+ input_first_type = option_types.first
64
+ input_required = attribute_data.fetch(:required).fetch(:is)
65
+ attribute_consists_of_types = Array(attribute_data.fetch(:consists_of).fetch(:type))
66
+ attribute_consists_of_first_type = attribute_consists_of_types.first
67
+
68
+ prepared_attributes = attributes.dup
69
+ prepared_attributes[attribute_name] = Servactory::TestKit::FakeType.new
70
+
71
+ input_required_message =
72
+ if described_class.config.collection_mode_class_names.include?(input_first_type) &&
73
+ attribute_consists_of_first_type != false
74
+ if input_required
75
+ I18n.t(
76
+ "servactory.#{attribute_type_plural}.validations.required.default_error.for_collection",
77
+ service_class_name: described_class.name,
78
+ "#{attribute_type}_name": attribute_name
79
+ )
80
+ else
81
+ I18n.t(
82
+ "servactory.#{attribute_type_plural}.validations.type.default_error.for_collection.wrong_type",
83
+ service_class_name: described_class.name,
84
+ "#{attribute_type}_name": attribute_name,
85
+ expected_type: option_types.join(", "),
86
+ given_type: Servactory::TestKit::FakeType.new.class.name
87
+ )
88
+ end
89
+ else
90
+ I18n.t(
91
+ "servactory.#{attribute_type_plural}.validations.type.default_error.default",
92
+ service_class_name: described_class.name,
93
+ "#{attribute_type}_name": attribute_name,
94
+ expected_type: option_types.join(", "),
95
+ given_type: Servactory::TestKit::FakeType.new.class.name
96
+ )
97
+ end
98
+
99
+ expect_failure_with!(prepared_attributes, input_required_message)
100
+ end
101
+
102
+ def failure_required_passes? # rubocop:disable Metrics/MethodLength, Metrics/AbcSize
103
+ input_required = attribute_data.fetch(:required).fetch(:is)
104
+
105
+ return true unless input_required
106
+
107
+ prepared_attributes = attributes.dup
108
+ prepared_attributes[attribute_name] = nil
109
+
110
+ input_required_message = attribute_data.fetch(:required).fetch(:message)
111
+
112
+ if input_required_message.nil?
113
+ input_required_message = I18n.t(
114
+ "servactory.#{attribute_type_plural}.validations.required.default_error.default",
115
+ service_class_name: described_class.name,
116
+ "#{attribute_type}_name": attribute_name
117
+ )
118
+ end
119
+
120
+ expect_failure_with!(prepared_attributes, input_required_message)
121
+ end
122
+
123
+ def failure_optional_passes?
124
+ input_required = attribute_data.fetch(:required).fetch(:is)
125
+
126
+ return true if input_required
127
+
128
+ prepared_attributes = attributes.dup
129
+ prepared_attributes[attribute_name] = nil
130
+
131
+ expect_failure_with!(prepared_attributes, nil)
132
+ end
133
+
134
+ def failure_format_passes?
135
+ # NOTE: Checking for negative cases is not implemented for `format`
136
+ true
137
+ end
138
+
139
+ def failure_consists_of_passes? # rubocop:disable Metrics/MethodLength, Metrics/AbcSize
140
+ option_types = attribute_data.fetch(:types)
141
+ input_first_type = option_types.first
142
+
143
+ return true unless described_class.config.collection_mode_class_names.include?(input_first_type)
144
+
145
+ prepared_attributes = attributes.dup
146
+ prepared_attributes[attribute_name] = input_first_type[Servactory::TestKit::FakeType.new]
147
+
148
+ attribute_consists_of_types = Array(attribute_data.fetch(:consists_of).fetch(:type))
149
+ attribute_consists_of_first_type = attribute_consists_of_types.first
150
+
151
+ return true if attribute_consists_of_first_type == false
152
+
153
+ attribute_consists_of_message = attribute_data.fetch(:consists_of).fetch(:message)
154
+
155
+ if attribute_consists_of_message.nil?
156
+ attribute_consists_of_message = I18n.t(
157
+ "servactory.#{attribute_type_plural}.validations.type.default_error.for_collection.wrong_element_type", # rubocop:disable Layout/LineLength
158
+ service_class_name: described_class.name,
159
+ "#{attribute_type}_name": attribute_name,
160
+ expected_type: attribute_consists_of_types.join(", "),
161
+ given_type: prepared_attributes[attribute_name].map { _1.class.name }.join(", ")
162
+ )
163
+ end
164
+
165
+ expect_failure_with!(prepared_attributes, attribute_consists_of_message)
166
+ end
167
+
168
+ def failure_inclusion_passes? # rubocop:disable Metrics/MethodLength, Metrics/AbcSize
169
+ input_inclusion_in = attribute_data.fetch(:inclusion).fetch(:in)
170
+
171
+ return true if input_inclusion_in.blank?
172
+
173
+ wrong_value = Servactory::TestKit::Utils::Faker.fetch_value_for(input_inclusion_in.first.class)
174
+
175
+ prepared_attributes = attributes.dup
176
+ prepared_attributes[attribute_name] = wrong_value
177
+
178
+ input_required_message = attribute_data.fetch(:inclusion).fetch(:message)
179
+
180
+ if input_required_message.nil?
181
+ input_required_message = I18n.t(
182
+ "servactory.#{attribute_type_plural}.validations.inclusion.default_error",
183
+ service_class_name: described_class.name,
184
+ "#{attribute_type}_name": attribute_name,
185
+ "#{attribute_type}_inclusion": input_inclusion_in,
186
+ value: wrong_value
187
+ )
188
+ elsif input_required_message.is_a?(Proc)
189
+ input_work = attribute_data.fetch(:work)
190
+
191
+ input_required_message = input_required_message.call(
192
+ service_class_name: described_class.name,
193
+ input: input_work,
194
+ value: wrong_value
195
+ )
196
+ end
197
+
198
+ expect_failure_with!(prepared_attributes, input_required_message)
199
+ end
200
+
201
+ def failure_must_passes?
202
+ # NOTE: Checking for negative cases is not implemented for `must`
203
+ true
204
+ end
205
+
206
+ def expect_success_with!(prepared_attributes)
207
+ described_class.call!(prepared_attributes).success?
208
+ rescue Servactory::Exceptions::Input
209
+ false
210
+ rescue StandardError
211
+ true
212
+ end
213
+
214
+ def expect_failure_with!(prepared_attributes, expected_message)
215
+ described_class.call!(prepared_attributes).success?
216
+ rescue Servactory::Exceptions::Input => e
217
+ return false if expected_message.blank?
218
+
219
+ expected_message.casecmp(e.message).zero?
220
+ rescue Servactory::Exceptions::Internal, Servactory::Exceptions::Output
221
+ # NOTE: Skips the fall of validations inside the service, which are not important in this place.
222
+ true
223
+ end
224
+
225
+ def build_missing_option
226
+ "should work as expected on the specified attributes based on its options"
227
+ end
228
+ end
229
+ end
230
+ end
231
+ end
232
+ end
233
+ end
@@ -0,0 +1,148 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Servactory
4
+ module TestKit
5
+ module Rspec
6
+ module Matchers
7
+ class HaveServiceInternalMatcher # rubocop:disable Metrics/ClassLength
8
+ attr_reader :described_class, :internal_name, :options
9
+
10
+ def initialize(described_class, internal_name)
11
+ @described_class = described_class
12
+ @internal_name = internal_name
13
+
14
+ @options = {}
15
+ @submatchers = []
16
+
17
+ @missing = ""
18
+ end
19
+
20
+ def supports_block_expectations?
21
+ true
22
+ end
23
+
24
+ def type(type)
25
+ @option_types = Array(type)
26
+ add_submatcher(
27
+ HaveServiceAttributeMatchers::TypesMatcher,
28
+ described_class,
29
+ :internal,
30
+ internal_name,
31
+ @option_types
32
+ )
33
+ self
34
+ end
35
+
36
+ def types(*types)
37
+ @option_types = types
38
+ add_submatcher(
39
+ HaveServiceAttributeMatchers::TypesMatcher,
40
+ described_class,
41
+ :internal,
42
+ internal_name,
43
+ @option_types
44
+ )
45
+ self
46
+ end
47
+
48
+ def consists_of(*types) # rubocop:disable Metrics/MethodLength
49
+ message = block_given? ? yield : nil
50
+
51
+ add_submatcher(
52
+ HaveServiceAttributeMatchers::ConsistsOfMatcher,
53
+ described_class,
54
+ :internal,
55
+ internal_name,
56
+ @option_types,
57
+ Array(types),
58
+ message
59
+ )
60
+ self
61
+ end
62
+
63
+ def inclusion(values)
64
+ add_submatcher(
65
+ HaveServiceAttributeMatchers::InclusionMatcher,
66
+ described_class,
67
+ :internal,
68
+ internal_name,
69
+ Array(values)
70
+ )
71
+ self
72
+ end
73
+
74
+ def must(*must_names)
75
+ add_submatcher(
76
+ HaveServiceAttributeMatchers::MustMatcher,
77
+ described_class,
78
+ :internal,
79
+ internal_name,
80
+ Array(must_names)
81
+ )
82
+ self
83
+ end
84
+
85
+ def description
86
+ "#{internal_name} with #{submatchers.map(&:description).join(', ')}"
87
+ end
88
+
89
+ def failure_message
90
+ "Expected #{expectation}, which #{missing_options}"
91
+ end
92
+
93
+ def failure_message_when_negated
94
+ "Did not expect #{expectation} with specified options"
95
+ end
96
+
97
+ def matches?(subject)
98
+ @subject = subject
99
+
100
+ submatchers_match?
101
+ end
102
+
103
+ protected
104
+
105
+ attr_reader :submatchers, :missing, :subject
106
+
107
+ def add_submatcher(matcher_class, *args)
108
+ remove_submatcher(matcher_class)
109
+ submatchers << matcher_class.new(*args)
110
+ end
111
+
112
+ def remove_submatcher(matcher_class)
113
+ submatchers.delete_if do |submatcher|
114
+ submatcher.is_a?(matcher_class)
115
+ end
116
+ end
117
+
118
+ def expectation
119
+ "#{described_class.name} to have a service internal attribute named #{internal_name}"
120
+ end
121
+
122
+ def missing_options
123
+ missing_options = [missing, missing_options_for_failing_submatchers]
124
+ missing_options.flatten.select(&:present?).join(", ")
125
+ end
126
+
127
+ def failing_submatchers
128
+ @failing_submatchers ||= submatchers.reject do |matcher|
129
+ matcher.matches?(subject)
130
+ end
131
+ end
132
+
133
+ def missing_options_for_failing_submatchers
134
+ if defined?(failing_submatchers)
135
+ failing_submatchers.map(&:missing_option)
136
+ else
137
+ []
138
+ end
139
+ end
140
+
141
+ def submatchers_match?
142
+ failing_submatchers.empty?
143
+ end
144
+ end
145
+ end
146
+ end
147
+ end
148
+ end
@@ -0,0 +1,295 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Servactory
4
+ module TestKit
5
+ module Rspec
6
+ module Matchers # rubocop:disable Metrics/ModuleLength
7
+ def have_service_input(input_name) # rubocop:disable Naming/PredicateName
8
+ HaveServiceInputMatcher.new(described_class, input_name)
9
+ end
10
+
11
+ RSpec::Matchers.alias_matcher :have_input, :have_service_input
12
+
13
+ def have_service_internal(internal_name) # rubocop:disable Naming/PredicateName
14
+ HaveServiceInternalMatcher.new(described_class, internal_name)
15
+ end
16
+
17
+ RSpec::Matchers.alias_matcher :have_internal, :have_service_internal
18
+
19
+ ########################################################################
20
+ ########################################################################
21
+ ########################################################################
22
+
23
+ RSpec::Matchers.define :have_service_output do |output_name| # rubocop:disable Metrics/BlockLength
24
+ description { "service output" }
25
+
26
+ match do |actual|
27
+ match_for(actual, output_name)
28
+ end
29
+
30
+ chain :instance_of do |class_or_name|
31
+ @instance_of = Servactory::Utils.constantize_class(class_or_name)
32
+ end
33
+
34
+ chain :nested do |*values|
35
+ @nested = values
36
+ end
37
+
38
+ chain :with do |value|
39
+ @value = value
40
+ end
41
+
42
+ failure_message do |actual|
43
+ match_for(actual, output_name)
44
+ end
45
+
46
+ # rubocop:disable Metrics/MethodLength, Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
47
+ def match_for(actual, output_name)
48
+ given_value = actual.public_send(output_name)
49
+
50
+ if defined?(@nested) && @nested.present?
51
+ @nested.each do |method_name|
52
+ next unless given_value.respond_to?(method_name)
53
+
54
+ given_value = given_value.public_send(method_name)
55
+ end
56
+ end
57
+
58
+ expect(given_value).to(
59
+ if defined?(@instance_of)
60
+ RSpec::Matchers::BuiltIn::BeAnInstanceOf.new(@instance_of)
61
+ elsif @value.is_a?(Array)
62
+ RSpec::Matchers::BuiltIn::ContainExactly.new(@value)
63
+ elsif @value.is_a?(Hash)
64
+ RSpec::Matchers::BuiltIn::Match.new(@value)
65
+ elsif @value.is_a?(TrueClass) || @value.is_a?(FalseClass)
66
+ RSpec::Matchers::BuiltIn::Equal.new(@value)
67
+ elsif @value.is_a?(NilClass)
68
+ RSpec::Matchers::BuiltIn::BeNil.new(@value)
69
+ else
70
+ RSpec::Matchers::BuiltIn::Eq.new(@value)
71
+ end
72
+ )
73
+ end
74
+ end
75
+ # rubocop:enable Metrics/MethodLength, Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
76
+
77
+ RSpec::Matchers.alias_matcher :have_output, :have_service_output
78
+
79
+ RSpec::Matchers.define :be_success_service do # rubocop:disable Metrics/BlockLength
80
+ description { "service success" }
81
+
82
+ def expected_data
83
+ @expected_data ||= {}
84
+ end
85
+
86
+ match do |actual|
87
+ matched = actual.is_a?(Servactory::Result)
88
+ matched &&= actual.success?
89
+ matched &&= !actual.failure?
90
+
91
+ if defined?(expected_data)
92
+ matched &&= expected_data.all? do |key, value|
93
+ if actual.respond_to?(key)
94
+ actual.public_send(key) == value
95
+ else
96
+ false
97
+ end
98
+ end
99
+ end
100
+
101
+ matched
102
+ end
103
+
104
+ chain :with_output do |key, value|
105
+ expected_data[key] = value
106
+ end
107
+
108
+ chain :with_outputs do |attributes|
109
+ attributes.each do |key, value|
110
+ expected_data[key] = value
111
+ end
112
+ end
113
+
114
+ failure_message do |actual| # rubocop:disable Metrics/BlockLength
115
+ unless actual.instance_of?(Servactory::Result)
116
+ break <<~MESSAGE
117
+ Incorrect service result:
118
+
119
+ expected Servactory::Result
120
+ got #{actual.class.name}
121
+ MESSAGE
122
+ end
123
+
124
+ if actual.failure?
125
+ break <<~MESSAGE
126
+ Incorrect service result:
127
+
128
+ expected success
129
+ got failure
130
+ MESSAGE
131
+ end
132
+
133
+ if defined?(expected_data)
134
+ message = expected_data.each do |key, value|
135
+ unless actual.respond_to?(key)
136
+ break <<~MESSAGE
137
+ Non-existent value key in result:
138
+
139
+ expected #{actual.inspect}
140
+ got #{key}
141
+ MESSAGE
142
+ end
143
+
144
+ expected_value = actual.public_send(key)
145
+ next if actual.public_send(key) == value
146
+
147
+ break <<~MESSAGE
148
+ Incorrect result value for #{key}:
149
+
150
+ expected #{expected_value.inspect}
151
+ got #{value.inspect}
152
+ MESSAGE
153
+ end
154
+ end
155
+
156
+ break message if message.present?
157
+
158
+ <<~MESSAGE
159
+ Unexpected case when using `be_success_service`.
160
+
161
+ Please try to build an example based on the documentation.
162
+ Or report your problem to us:
163
+
164
+ https://github.com/servactory/servactory/issues
165
+ MESSAGE
166
+ end
167
+ end
168
+
169
+ RSpec::Matchers.define :be_failure_service do # rubocop:disable Metrics/BlockLength
170
+ description { "service failure" }
171
+
172
+ match do |actual|
173
+ expected_failure_class =
174
+ defined?(@expected_failure_class) ? @expected_failure_class : Servactory::Exceptions::Failure
175
+
176
+ expected_type = defined?(@expected_type) ? @expected_type : :base
177
+ expected_message = defined?(@expected_message) ? @expected_message : nil
178
+ expected_meta = defined?(@expected_meta) ? @expected_meta : nil
179
+
180
+ matched = actual.is_a?(Servactory::Result)
181
+ matched &&= !actual.success?
182
+ matched &&= actual.failure?
183
+ matched &&= actual.error.is_a?(Servactory::Exceptions::Failure)
184
+ matched &&= actual.error.instance_of?(expected_failure_class)
185
+ matched &&= actual.error.type == expected_type
186
+ matched &&= actual.error.message == expected_message
187
+ matched &&= actual.error.meta == expected_meta
188
+ matched
189
+ end
190
+
191
+ chain :with do |expected_failure_class|
192
+ @expected_failure_class = expected_failure_class
193
+ end
194
+
195
+ chain :type do |expected_type|
196
+ @expected_type = expected_type
197
+ end
198
+
199
+ chain :message do |expected_message|
200
+ @expected_message = expected_message
201
+ end
202
+
203
+ chain :meta do |expected_meta|
204
+ @expected_meta = expected_meta
205
+ end
206
+
207
+ failure_message do |actual| # rubocop:disable Metrics/BlockLength
208
+ unless actual.instance_of?(Servactory::Result)
209
+ break <<~MESSAGE
210
+ Incorrect service result:
211
+
212
+ expected Servactory::Result
213
+ got #{actual.class.name}
214
+ MESSAGE
215
+ end
216
+
217
+ if actual.success?
218
+ break <<~MESSAGE
219
+ Incorrect service result:
220
+
221
+ expected failure
222
+ got success
223
+ MESSAGE
224
+ end
225
+
226
+ unless actual.error.is_a?(Servactory::Exceptions::Failure)
227
+ break <<~MESSAGE
228
+ Incorrect error object:
229
+
230
+ expected Servactory::Exceptions::Failure
231
+ got #{actual.error.class.name}
232
+ MESSAGE
233
+ end
234
+
235
+ if defined?(@expected_failure_class)
236
+ unless actual.error.instance_of?(@expected_failure_class)
237
+ break <<~MESSAGE
238
+ Incorrect instance error:
239
+
240
+ expected #{@expected_failure_class}
241
+ got #{actual.error.class.name}
242
+ MESSAGE
243
+ end
244
+ else
245
+ unless actual.error.instance_of?(Servactory::Exceptions::Failure)
246
+ break <<~MESSAGE
247
+ Incorrect error object:
248
+
249
+ expected Servactory::Exceptions::Failure
250
+ got #{actual.error.class.name}
251
+ MESSAGE
252
+ end
253
+ end
254
+
255
+ if defined?(@expected_type) && actual.error.type != @expected_type
256
+ break <<~MESSAGE
257
+ Incorrect error type:
258
+
259
+ expected #{actual.error.type.inspect}
260
+ got #{@expected_type.inspect}
261
+ MESSAGE
262
+ end
263
+
264
+ if defined?(@expected_message) && actual.error.message != @expected_message
265
+ break <<~MESSAGE
266
+ Incorrect error message:
267
+
268
+ expected #{actual.error.message.inspect}
269
+ got #{@expected_message.inspect}
270
+ MESSAGE
271
+ end
272
+
273
+ if defined?(@expected_meta) && actual.error.meta != @expected_meta
274
+ break <<~MESSAGE
275
+ Incorrect error meta:
276
+
277
+ expected #{actual.error.meta.inspect}
278
+ got #{@expected_meta.inspect}
279
+ MESSAGE
280
+ end
281
+
282
+ <<~MESSAGE
283
+ Unexpected case when using `be_failure_service`.
284
+
285
+ Please try to build an example based on the documentation.
286
+ Or report your problem to us:
287
+
288
+ https://github.com/servactory/servactory/issues
289
+ MESSAGE
290
+ end
291
+ end
292
+ end
293
+ end
294
+ end
295
+ end