light-services 2.2.1 → 3.0.0
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 +4 -4
- data/.github/config/rubocop_linter_action.yml +4 -4
- data/.github/workflows/ci.yml +12 -12
- data/.gitignore +1 -0
- data/.rubocop.yml +77 -7
- data/CHANGELOG.md +23 -0
- data/CLAUDE.md +139 -0
- data/Gemfile +16 -11
- data/Gemfile.lock +53 -27
- data/README.md +76 -13
- data/docs/arguments.md +267 -0
- data/docs/best-practices.md +153 -0
- data/docs/callbacks.md +476 -0
- data/docs/concepts.md +80 -0
- data/docs/configuration.md +168 -0
- data/docs/context.md +128 -0
- data/docs/crud.md +525 -0
- data/docs/errors.md +250 -0
- data/docs/generators.md +250 -0
- data/docs/outputs.md +135 -0
- data/docs/pundit-authorization.md +320 -0
- data/docs/quickstart.md +134 -0
- data/docs/readme.md +100 -0
- data/docs/recipes.md +14 -0
- data/docs/service-rendering.md +222 -0
- data/docs/steps.md +337 -0
- data/docs/summary.md +19 -0
- data/docs/testing.md +549 -0
- data/lib/generators/light_services/install/USAGE +15 -0
- data/lib/generators/light_services/install/install_generator.rb +41 -0
- data/lib/generators/light_services/install/templates/application_service.rb.tt +8 -0
- data/lib/generators/light_services/install/templates/application_service_spec.rb.tt +7 -0
- data/lib/generators/light_services/install/templates/initializer.rb.tt +30 -0
- data/lib/generators/light_services/service/USAGE +21 -0
- data/lib/generators/light_services/service/service_generator.rb +68 -0
- data/lib/generators/light_services/service/templates/service.rb.tt +48 -0
- data/lib/generators/light_services/service/templates/service_spec.rb.tt +40 -0
- data/lib/light/services/base.rb +23 -113
- data/lib/light/services/callbacks.rb +103 -0
- data/lib/light/services/collection.rb +97 -0
- data/lib/light/services/concerns/execution.rb +76 -0
- data/lib/light/services/concerns/parent_service.rb +34 -0
- data/lib/light/services/concerns/state_management.rb +30 -0
- data/lib/light/services/config.rb +4 -18
- data/lib/light/services/constants.rb +97 -0
- data/lib/light/services/dsl/arguments_dsl.rb +84 -0
- data/lib/light/services/dsl/outputs_dsl.rb +80 -0
- data/lib/light/services/dsl/steps_dsl.rb +205 -0
- data/lib/light/services/dsl/validation.rb +132 -0
- data/lib/light/services/exceptions.rb +7 -2
- data/lib/light/services/messages.rb +19 -31
- data/lib/light/services/rspec/matchers/define_argument.rb +174 -0
- data/lib/light/services/rspec/matchers/define_output.rb +147 -0
- data/lib/light/services/rspec/matchers/define_step.rb +225 -0
- data/lib/light/services/rspec/matchers/execute_step.rb +230 -0
- data/lib/light/services/rspec/matchers/have_error_on.rb +148 -0
- data/lib/light/services/rspec/matchers/have_warning_on.rb +148 -0
- data/lib/light/services/rspec/matchers/trigger_callback.rb +138 -0
- data/lib/light/services/rspec.rb +15 -0
- data/lib/light/services/settings/field.rb +86 -0
- data/lib/light/services/settings/step.rb +31 -16
- data/lib/light/services/utils.rb +38 -0
- data/lib/light/services/version.rb +1 -1
- data/lib/light/services.rb +2 -0
- data/light-services.gemspec +6 -8
- metadata +54 -26
- data/lib/light/services/class_based_collection/base.rb +0 -86
- data/lib/light/services/class_based_collection/mount.rb +0 -33
- data/lib/light/services/collection/arguments.rb +0 -34
- data/lib/light/services/collection/base.rb +0 -59
- data/lib/light/services/collection/outputs.rb +0 -16
- data/lib/light/services/settings/argument.rb +0 -68
- data/lib/light/services/settings/output.rb +0 -34
|
@@ -4,20 +4,32 @@
|
|
|
4
4
|
module Light
|
|
5
5
|
module Services
|
|
6
6
|
class Messages
|
|
7
|
+
extend Forwardable
|
|
8
|
+
|
|
9
|
+
def_delegators :@messages, :[], :any?, :empty?, :size, :keys, :values, :each, :each_with_index, :key?
|
|
10
|
+
alias has_key? key?
|
|
11
|
+
|
|
7
12
|
def initialize(config)
|
|
8
13
|
@break = false
|
|
9
14
|
@config = config
|
|
10
15
|
@messages = {}
|
|
11
16
|
end
|
|
12
17
|
|
|
18
|
+
# Returns total count of all messages across all keys
|
|
19
|
+
def count
|
|
20
|
+
@messages.values.sum(&:size)
|
|
21
|
+
end
|
|
22
|
+
|
|
13
23
|
def add(key, texts, opts = {})
|
|
14
|
-
raise Light::Services::Error, "Error
|
|
24
|
+
raise Light::Services::Error, "Error must be a non-empty string" unless texts
|
|
15
25
|
|
|
16
26
|
message = nil
|
|
17
27
|
|
|
18
28
|
[*texts].each do |text|
|
|
19
29
|
message = text.is_a?(Message) ? text : Message.new(key, text, opts)
|
|
20
30
|
|
|
31
|
+
raise Light::Services::Error, "Error must be a non-empty string" unless valid_error_text?(message.text)
|
|
32
|
+
|
|
21
33
|
@messages[key] ||= []
|
|
22
34
|
@messages[key] << message
|
|
23
35
|
end
|
|
@@ -46,43 +58,19 @@ module Light
|
|
|
46
58
|
raise Light::Services::Error, "Don't know how to import errors from #{entity}"
|
|
47
59
|
end
|
|
48
60
|
end
|
|
49
|
-
|
|
50
|
-
def copy_to(entity)
|
|
51
|
-
if (defined?(ActiveRecord::Base) && entity.is_a?(ActiveRecord::Base)) || entity.is_a?(Light::Services::Base)
|
|
52
|
-
each do |key, messages|
|
|
53
|
-
messages.each do |message|
|
|
54
|
-
entity.errors.add(key, message.to_s)
|
|
55
|
-
end
|
|
56
|
-
end
|
|
57
|
-
elsif entity.is_a?(Hash)
|
|
58
|
-
each do |key, messages|
|
|
59
|
-
entity[key] ||= []
|
|
60
|
-
entity[key] += messages.map(&:to_s)
|
|
61
|
-
end
|
|
62
|
-
else
|
|
63
|
-
raise Light::Services::Error, "Don't know how to export errors to #{entity}"
|
|
64
|
-
end
|
|
65
|
-
|
|
66
|
-
entity
|
|
67
|
-
end
|
|
61
|
+
alias from_record copy_from
|
|
68
62
|
|
|
69
63
|
def to_h
|
|
70
64
|
@messages.to_h.transform_values { |value| value.map(&:to_s) }
|
|
71
65
|
end
|
|
72
66
|
|
|
73
|
-
|
|
74
|
-
if @messages.respond_to?(method)
|
|
75
|
-
@messages.public_send(method, *args, &block)
|
|
76
|
-
else
|
|
77
|
-
super
|
|
78
|
-
end
|
|
79
|
-
end
|
|
67
|
+
private
|
|
80
68
|
|
|
81
|
-
def
|
|
82
|
-
|
|
83
|
-
end
|
|
69
|
+
def valid_error_text?(text)
|
|
70
|
+
return false unless text.is_a?(String)
|
|
84
71
|
|
|
85
|
-
|
|
72
|
+
!text.strip.empty?
|
|
73
|
+
end
|
|
86
74
|
|
|
87
75
|
def break!(break_execution)
|
|
88
76
|
return unless break_execution.nil? ? @config[:break_on_add] : break_execution
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Light
|
|
4
|
+
module Services
|
|
5
|
+
module RSpec
|
|
6
|
+
module Matchers
|
|
7
|
+
# Matcher for testing argument definitions on a service class
|
|
8
|
+
#
|
|
9
|
+
# @example Basic usage
|
|
10
|
+
# expect(MyService).to define_argument(:name)
|
|
11
|
+
#
|
|
12
|
+
# @example With type constraint
|
|
13
|
+
# expect(MyService).to define_argument(:name).with_type(String)
|
|
14
|
+
#
|
|
15
|
+
# @example With optional flag
|
|
16
|
+
# expect(MyService).to define_argument(:email).optional
|
|
17
|
+
#
|
|
18
|
+
# @example With default value
|
|
19
|
+
# expect(MyService).to define_argument(:status).with_default("pending")
|
|
20
|
+
#
|
|
21
|
+
# @example With context flag
|
|
22
|
+
# expect(MyService).to define_argument(:current_user).with_context
|
|
23
|
+
#
|
|
24
|
+
# @example Combined
|
|
25
|
+
# expect(MyService).to define_argument(:count).with_type(Integer).optional.with_default(0)
|
|
26
|
+
def define_argument(name)
|
|
27
|
+
DefineArgumentMatcher.new(name)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
class DefineArgumentMatcher
|
|
31
|
+
def initialize(name)
|
|
32
|
+
@name = name
|
|
33
|
+
@expected_type = nil
|
|
34
|
+
@expected_optional = nil
|
|
35
|
+
@expected_default = nil
|
|
36
|
+
@check_default = false
|
|
37
|
+
@expected_context = nil
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def with_type(type)
|
|
41
|
+
@expected_type = type
|
|
42
|
+
self
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def optional(value = true)
|
|
46
|
+
@expected_optional = value
|
|
47
|
+
self
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def required
|
|
51
|
+
@expected_optional = false
|
|
52
|
+
self
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def with_default(default)
|
|
56
|
+
@check_default = true
|
|
57
|
+
@expected_default = default
|
|
58
|
+
self
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def with_context(value = true)
|
|
62
|
+
@expected_context = value
|
|
63
|
+
self
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def matches?(service_class)
|
|
67
|
+
@service_class = service_class
|
|
68
|
+
@actual_class = service_class.is_a?(Class) ? service_class : service_class.class
|
|
69
|
+
|
|
70
|
+
return false unless argument_defined?
|
|
71
|
+
return false unless type_matches?
|
|
72
|
+
return false unless optional_matches?
|
|
73
|
+
return false unless default_matches?
|
|
74
|
+
return false unless context_matches?
|
|
75
|
+
|
|
76
|
+
true
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def failure_message
|
|
80
|
+
return "expected #{@actual_class} to define argument :#{@name}" unless argument_defined?
|
|
81
|
+
return type_failure_message unless type_matches?
|
|
82
|
+
return optional_failure_message unless optional_matches?
|
|
83
|
+
return default_failure_message unless default_matches?
|
|
84
|
+
return context_failure_message unless context_matches?
|
|
85
|
+
|
|
86
|
+
""
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def failure_message_when_negated
|
|
90
|
+
"expected #{@actual_class} not to define argument :#{@name}"
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def description
|
|
94
|
+
desc = "define argument :#{@name}"
|
|
95
|
+
desc += " with type #{@expected_type}" if @expected_type
|
|
96
|
+
desc += " as optional" if @expected_optional == true
|
|
97
|
+
desc += " as required" if @expected_optional == false
|
|
98
|
+
desc += " with default #{@expected_default.inspect}" if @check_default
|
|
99
|
+
desc += " with context" if @expected_context
|
|
100
|
+
desc
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
private
|
|
104
|
+
|
|
105
|
+
def argument_defined?
|
|
106
|
+
@actual_class.respond_to?(:arguments) && @actual_class.arguments.key?(@name)
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def argument
|
|
110
|
+
@argument ||= @actual_class.arguments[@name]
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def type_matches?
|
|
114
|
+
return true if @expected_type.nil?
|
|
115
|
+
|
|
116
|
+
# Access the type via instance variable since there's no public getter
|
|
117
|
+
actual_type = argument.instance_variable_get(:@type)
|
|
118
|
+
actual_type == @expected_type
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def optional_matches?
|
|
122
|
+
return true if @expected_optional.nil?
|
|
123
|
+
|
|
124
|
+
argument.optional == @expected_optional
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def default_matches?
|
|
128
|
+
return true unless @check_default
|
|
129
|
+
|
|
130
|
+
argument.default_exists && argument.default == @expected_default
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def context_matches?
|
|
134
|
+
return true if @expected_context.nil?
|
|
135
|
+
|
|
136
|
+
argument.context == @expected_context
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
def type_failure_message
|
|
140
|
+
actual_type = argument.instance_variable_get(:@type)
|
|
141
|
+
"expected #{@actual_class} argument :#{@name} to have type #{@expected_type}, " \
|
|
142
|
+
"but it has type #{actual_type.inspect}"
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
def optional_failure_message
|
|
146
|
+
if @expected_optional
|
|
147
|
+
"expected #{@actual_class} argument :#{@name} to be optional, but it is required"
|
|
148
|
+
else
|
|
149
|
+
"expected #{@actual_class} argument :#{@name} to be required, but it is optional"
|
|
150
|
+
end
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
def default_failure_message
|
|
154
|
+
if argument.default_exists
|
|
155
|
+
"expected #{@actual_class} argument :#{@name} to have default #{@expected_default.inspect}, " \
|
|
156
|
+
"but it has default #{argument.default.inspect}"
|
|
157
|
+
else
|
|
158
|
+
"expected #{@actual_class} argument :#{@name} to have default #{@expected_default.inspect}, " \
|
|
159
|
+
"but no default is defined"
|
|
160
|
+
end
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
def context_failure_message
|
|
164
|
+
if @expected_context
|
|
165
|
+
"expected #{@actual_class} argument :#{@name} to have context flag, but it doesn't"
|
|
166
|
+
else
|
|
167
|
+
"expected #{@actual_class} argument :#{@name} not to have context flag, but it does"
|
|
168
|
+
end
|
|
169
|
+
end
|
|
170
|
+
end
|
|
171
|
+
end
|
|
172
|
+
end
|
|
173
|
+
end
|
|
174
|
+
end
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Light
|
|
4
|
+
module Services
|
|
5
|
+
module RSpec
|
|
6
|
+
module Matchers
|
|
7
|
+
# Matcher for testing output definitions on a service class
|
|
8
|
+
#
|
|
9
|
+
# @example Basic usage
|
|
10
|
+
# expect(MyService).to define_output(:result)
|
|
11
|
+
#
|
|
12
|
+
# @example With type constraint
|
|
13
|
+
# expect(MyService).to define_output(:product).with_type(Product)
|
|
14
|
+
#
|
|
15
|
+
# @example With optional flag
|
|
16
|
+
# expect(MyService).to define_output(:message).optional
|
|
17
|
+
#
|
|
18
|
+
# @example With default value
|
|
19
|
+
# expect(MyService).to define_output(:count).with_default(0)
|
|
20
|
+
#
|
|
21
|
+
# @example Combined
|
|
22
|
+
# expect(MyService).to define_output(:data).with_type(Hash).optional.with_default({})
|
|
23
|
+
def define_output(name)
|
|
24
|
+
DefineOutputMatcher.new(name)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
class DefineOutputMatcher
|
|
28
|
+
def initialize(name)
|
|
29
|
+
@name = name
|
|
30
|
+
@expected_type = nil
|
|
31
|
+
@expected_optional = nil
|
|
32
|
+
@expected_default = nil
|
|
33
|
+
@check_default = false
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def with_type(type)
|
|
37
|
+
@expected_type = type
|
|
38
|
+
self
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def optional(value = true)
|
|
42
|
+
@expected_optional = value
|
|
43
|
+
self
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def required
|
|
47
|
+
@expected_optional = false
|
|
48
|
+
self
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def with_default(default)
|
|
52
|
+
@check_default = true
|
|
53
|
+
@expected_default = default
|
|
54
|
+
self
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def matches?(service_class)
|
|
58
|
+
@service_class = service_class
|
|
59
|
+
@actual_class = service_class.is_a?(Class) ? service_class : service_class.class
|
|
60
|
+
|
|
61
|
+
return false unless output_defined?
|
|
62
|
+
return false unless type_matches?
|
|
63
|
+
return false unless optional_matches?
|
|
64
|
+
return false unless default_matches?
|
|
65
|
+
|
|
66
|
+
true
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def failure_message
|
|
70
|
+
return "expected #{@actual_class} to define output :#{@name}" unless output_defined?
|
|
71
|
+
return type_failure_message unless type_matches?
|
|
72
|
+
return optional_failure_message unless optional_matches?
|
|
73
|
+
return default_failure_message unless default_matches?
|
|
74
|
+
|
|
75
|
+
""
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def failure_message_when_negated
|
|
79
|
+
"expected #{@actual_class} not to define output :#{@name}"
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def description
|
|
83
|
+
desc = "define output :#{@name}"
|
|
84
|
+
desc += " with type #{@expected_type}" if @expected_type
|
|
85
|
+
desc += " as optional" if @expected_optional == true
|
|
86
|
+
desc += " as required" if @expected_optional == false
|
|
87
|
+
desc += " with default #{@expected_default.inspect}" if @check_default
|
|
88
|
+
desc
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
private
|
|
92
|
+
|
|
93
|
+
def output_defined?
|
|
94
|
+
@actual_class.respond_to?(:outputs) && @actual_class.outputs.key?(@name)
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def output
|
|
98
|
+
@output ||= @actual_class.outputs[@name]
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def type_matches?
|
|
102
|
+
return true if @expected_type.nil?
|
|
103
|
+
|
|
104
|
+
actual_type = output.instance_variable_get(:@type)
|
|
105
|
+
actual_type == @expected_type
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def optional_matches?
|
|
109
|
+
return true if @expected_optional.nil?
|
|
110
|
+
|
|
111
|
+
output.optional == @expected_optional
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def default_matches?
|
|
115
|
+
return true unless @check_default
|
|
116
|
+
|
|
117
|
+
output.default_exists && output.default == @expected_default
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def type_failure_message
|
|
121
|
+
actual_type = output.instance_variable_get(:@type)
|
|
122
|
+
"expected #{@actual_class} output :#{@name} to have type #{@expected_type}, " \
|
|
123
|
+
"but it has type #{actual_type.inspect}"
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def optional_failure_message
|
|
127
|
+
if @expected_optional
|
|
128
|
+
"expected #{@actual_class} output :#{@name} to be optional, but it is required"
|
|
129
|
+
else
|
|
130
|
+
"expected #{@actual_class} output :#{@name} to be required, but it is optional"
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def default_failure_message
|
|
135
|
+
if output.default_exists
|
|
136
|
+
"expected #{@actual_class} output :#{@name} to have default #{@expected_default.inspect}, " \
|
|
137
|
+
"but it has default #{output.default.inspect}"
|
|
138
|
+
else
|
|
139
|
+
"expected #{@actual_class} output :#{@name} to have default #{@expected_default.inspect}, " \
|
|
140
|
+
"but no default is defined"
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
end
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Light
|
|
4
|
+
module Services
|
|
5
|
+
module RSpec
|
|
6
|
+
module Matchers
|
|
7
|
+
# Matcher for testing step definitions on a service class
|
|
8
|
+
#
|
|
9
|
+
# @example Basic usage
|
|
10
|
+
# expect(MyService).to define_step(:validate)
|
|
11
|
+
#
|
|
12
|
+
# @example With always flag
|
|
13
|
+
# expect(MyService).to define_step(:cleanup).with_always(true)
|
|
14
|
+
#
|
|
15
|
+
# @example With if condition
|
|
16
|
+
# expect(MyService).to define_step(:notify).with_if(:should_notify?)
|
|
17
|
+
#
|
|
18
|
+
# @example With unless condition
|
|
19
|
+
# expect(MyService).to define_step(:skip_validation).with_unless(:production?)
|
|
20
|
+
#
|
|
21
|
+
# @example Check multiple steps
|
|
22
|
+
# expect(MyService).to define_steps(:validate, :process, :save)
|
|
23
|
+
#
|
|
24
|
+
# @example Check step order
|
|
25
|
+
# expect(MyService).to define_steps_in_order(:validate, :process, :save)
|
|
26
|
+
def define_step(name)
|
|
27
|
+
DefineStepMatcher.new(name)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def define_steps(*names)
|
|
31
|
+
DefineStepsMatcher.new(names, ordered: false)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def define_steps_in_order(*names)
|
|
35
|
+
DefineStepsMatcher.new(names, ordered: true)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
class DefineStepMatcher
|
|
39
|
+
def initialize(name)
|
|
40
|
+
@name = name
|
|
41
|
+
@expected_always = nil
|
|
42
|
+
@expected_if = nil
|
|
43
|
+
@expected_unless = nil
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def with_always(value = true)
|
|
47
|
+
@expected_always = value
|
|
48
|
+
self
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def with_if(condition)
|
|
52
|
+
@expected_if = condition
|
|
53
|
+
self
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def with_unless(condition)
|
|
57
|
+
@expected_unless = condition
|
|
58
|
+
self
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def matches?(service_class)
|
|
62
|
+
@service_class = service_class
|
|
63
|
+
@actual_class = service_class.is_a?(Class) ? service_class : service_class.class
|
|
64
|
+
|
|
65
|
+
return false unless step_defined?
|
|
66
|
+
return false unless always_matches?
|
|
67
|
+
return false unless if_matches?
|
|
68
|
+
return false unless unless_matches?
|
|
69
|
+
|
|
70
|
+
true
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def failure_message
|
|
74
|
+
return "expected #{@actual_class} to define step :#{@name}" unless step_defined?
|
|
75
|
+
return always_failure_message unless always_matches?
|
|
76
|
+
return if_failure_message unless if_matches?
|
|
77
|
+
return unless_failure_message unless unless_matches?
|
|
78
|
+
|
|
79
|
+
""
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def failure_message_when_negated
|
|
83
|
+
"expected #{@actual_class} not to define step :#{@name}"
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def description
|
|
87
|
+
desc = "define step :#{@name}"
|
|
88
|
+
desc += " with always: #{@expected_always}" unless @expected_always.nil?
|
|
89
|
+
desc += " with if: #{@expected_if.inspect}" if @expected_if
|
|
90
|
+
desc += " with unless: #{@expected_unless.inspect}" if @expected_unless
|
|
91
|
+
desc
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
private
|
|
95
|
+
|
|
96
|
+
def step_defined?
|
|
97
|
+
@actual_class.respond_to?(:steps) && @actual_class.steps.key?(@name)
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def step
|
|
101
|
+
@step ||= @actual_class.steps[@name]
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def always_matches?
|
|
105
|
+
return true if @expected_always.nil?
|
|
106
|
+
|
|
107
|
+
step.always == @expected_always
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def if_matches?
|
|
111
|
+
return true if @expected_if.nil?
|
|
112
|
+
|
|
113
|
+
actual_if = step.instance_variable_get(:@if)
|
|
114
|
+
actual_if == @expected_if
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def unless_matches?
|
|
118
|
+
return true if @expected_unless.nil?
|
|
119
|
+
|
|
120
|
+
actual_unless = step.instance_variable_get(:@unless)
|
|
121
|
+
actual_unless == @expected_unless
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def always_failure_message
|
|
125
|
+
"expected #{@actual_class} step :#{@name} to have always: #{@expected_always}, " \
|
|
126
|
+
"but it has always: #{step.always.inspect}"
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def if_failure_message
|
|
130
|
+
actual_if = step.instance_variable_get(:@if)
|
|
131
|
+
"expected #{@actual_class} step :#{@name} to have if: #{@expected_if.inspect}, " \
|
|
132
|
+
"but it has if: #{actual_if.inspect}"
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def unless_failure_message
|
|
136
|
+
actual_unless = step.instance_variable_get(:@unless)
|
|
137
|
+
"expected #{@actual_class} step :#{@name} to have unless: #{@expected_unless.inspect}, " \
|
|
138
|
+
"but it has unless: #{actual_unless.inspect}"
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
class DefineStepsMatcher
|
|
143
|
+
def initialize(names, ordered:)
|
|
144
|
+
@names = names
|
|
145
|
+
@ordered = ordered
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
def matches?(service_class)
|
|
149
|
+
@service_class = service_class
|
|
150
|
+
@actual_class = service_class.is_a?(Class) ? service_class : service_class.class
|
|
151
|
+
@missing_steps = []
|
|
152
|
+
@actual_order = []
|
|
153
|
+
|
|
154
|
+
return false unless all_steps_defined?
|
|
155
|
+
return false unless order_matches?
|
|
156
|
+
|
|
157
|
+
true
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
def failure_message
|
|
161
|
+
return missing_steps_failure_message unless all_steps_defined?
|
|
162
|
+
return order_failure_message unless order_matches?
|
|
163
|
+
|
|
164
|
+
""
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
def failure_message_when_negated
|
|
168
|
+
if @ordered
|
|
169
|
+
"expected #{@actual_class} not to define steps #{@names.inspect} in that order"
|
|
170
|
+
else
|
|
171
|
+
"expected #{@actual_class} not to define steps #{@names.inspect}"
|
|
172
|
+
end
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
def description
|
|
176
|
+
if @ordered
|
|
177
|
+
"define steps #{@names.inspect} in order"
|
|
178
|
+
else
|
|
179
|
+
"define steps #{@names.inspect}"
|
|
180
|
+
end
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
private
|
|
184
|
+
|
|
185
|
+
def all_steps_defined?
|
|
186
|
+
return false unless @actual_class.respond_to?(:steps)
|
|
187
|
+
|
|
188
|
+
actual_step_names = @actual_class.steps.keys
|
|
189
|
+
@missing_steps = @names - actual_step_names
|
|
190
|
+
@missing_steps.empty?
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
def order_matches?
|
|
194
|
+
return true unless @ordered
|
|
195
|
+
|
|
196
|
+
actual_step_names = @actual_class.steps.keys
|
|
197
|
+
@actual_order = @names.select { |name| actual_step_names.include?(name) }
|
|
198
|
+
|
|
199
|
+
# Check if the expected steps appear in the same order in actual steps
|
|
200
|
+
last_index = -1
|
|
201
|
+
@names.all? do |name|
|
|
202
|
+
current_index = actual_step_names.index(name)
|
|
203
|
+
return false unless current_index
|
|
204
|
+
return false unless current_index > last_index
|
|
205
|
+
|
|
206
|
+
last_index = current_index
|
|
207
|
+
true
|
|
208
|
+
end
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
def missing_steps_failure_message
|
|
212
|
+
"expected #{@actual_class} to define steps #{@names.inspect}, " \
|
|
213
|
+
"but missing: #{@missing_steps.inspect}"
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
def order_failure_message
|
|
217
|
+
actual_step_names = @actual_class.steps.keys
|
|
218
|
+
"expected #{@actual_class} to define steps #{@names.inspect} in that order, " \
|
|
219
|
+
"but actual order is: #{actual_step_names.inspect}"
|
|
220
|
+
end
|
|
221
|
+
end
|
|
222
|
+
end
|
|
223
|
+
end
|
|
224
|
+
end
|
|
225
|
+
end
|