spec_forge 0.5.0 → 0.7.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.
Files changed (116) hide show
  1. checksums.yaml +4 -4
  2. data/.standard.yml +3 -3
  3. data/CHANGELOG.md +217 -2
  4. data/README.md +162 -25
  5. data/flake.lock +3 -3
  6. data/flake.nix +11 -5
  7. data/lib/spec_forge/attribute/chainable.rb +208 -20
  8. data/lib/spec_forge/attribute/factory.rb +92 -15
  9. data/lib/spec_forge/attribute/faker.rb +62 -13
  10. data/lib/spec_forge/attribute/global.rb +96 -0
  11. data/lib/spec_forge/attribute/literal.rb +15 -2
  12. data/lib/spec_forge/attribute/matcher.rb +186 -11
  13. data/lib/spec_forge/attribute/parameterized.rb +45 -12
  14. data/lib/spec_forge/attribute/regex.rb +55 -5
  15. data/lib/spec_forge/attribute/resolvable.rb +48 -5
  16. data/lib/spec_forge/attribute/resolvable_array.rb +62 -4
  17. data/lib/spec_forge/attribute/resolvable_hash.rb +62 -4
  18. data/lib/spec_forge/attribute/store.rb +65 -0
  19. data/lib/spec_forge/attribute/transform.rb +33 -5
  20. data/lib/spec_forge/attribute/variable.rb +37 -6
  21. data/lib/spec_forge/attribute.rb +166 -66
  22. data/lib/spec_forge/backtrace_formatter.rb +26 -3
  23. data/lib/spec_forge/callbacks.rb +88 -0
  24. data/lib/spec_forge/cli/actions.rb +27 -0
  25. data/lib/spec_forge/cli/command.rb +78 -24
  26. data/lib/spec_forge/cli/docs/generate.rb +72 -0
  27. data/lib/spec_forge/cli/docs.rb +92 -0
  28. data/lib/spec_forge/cli/init.rb +51 -9
  29. data/lib/spec_forge/cli/new.rb +67 -6
  30. data/lib/spec_forge/cli/run.rb +32 -4
  31. data/lib/spec_forge/cli/serve.rb +155 -0
  32. data/lib/spec_forge/cli.rb +26 -7
  33. data/lib/spec_forge/configuration.rb +96 -24
  34. data/lib/spec_forge/context/callbacks.rb +91 -0
  35. data/lib/spec_forge/context/global.rb +72 -0
  36. data/lib/spec_forge/context/store.rb +131 -0
  37. data/lib/spec_forge/context/variables.rb +91 -0
  38. data/lib/spec_forge/context.rb +36 -0
  39. data/lib/spec_forge/core_ext/array.rb +27 -0
  40. data/lib/spec_forge/core_ext/rspec.rb +22 -4
  41. data/lib/spec_forge/documentation/builder.rb +383 -0
  42. data/lib/spec_forge/documentation/document/operation.rb +47 -0
  43. data/lib/spec_forge/documentation/document/parameter.rb +22 -0
  44. data/lib/spec_forge/documentation/document/request_body.rb +24 -0
  45. data/lib/spec_forge/documentation/document/response.rb +39 -0
  46. data/lib/spec_forge/documentation/document/response_body.rb +27 -0
  47. data/lib/spec_forge/documentation/document.rb +48 -0
  48. data/lib/spec_forge/documentation/generators/base.rb +81 -0
  49. data/lib/spec_forge/documentation/generators/openapi/base.rb +100 -0
  50. data/lib/spec_forge/documentation/generators/openapi/error_formatter.rb +149 -0
  51. data/lib/spec_forge/documentation/generators/openapi/v3_0.rb +65 -0
  52. data/lib/spec_forge/documentation/generators/openapi.rb +59 -0
  53. data/lib/spec_forge/documentation/generators.rb +17 -0
  54. data/lib/spec_forge/documentation/loader/cache.rb +138 -0
  55. data/lib/spec_forge/documentation/loader.rb +159 -0
  56. data/lib/spec_forge/documentation/openapi/base.rb +33 -0
  57. data/lib/spec_forge/documentation/openapi/v3_0/example.rb +44 -0
  58. data/lib/spec_forge/documentation/openapi/v3_0/media_type.rb +42 -0
  59. data/lib/spec_forge/documentation/openapi/v3_0/operation.rb +175 -0
  60. data/lib/spec_forge/documentation/openapi/v3_0/response.rb +65 -0
  61. data/lib/spec_forge/documentation/openapi/v3_0/schema.rb +80 -0
  62. data/lib/spec_forge/documentation/openapi/v3_0/tag.rb +71 -0
  63. data/lib/spec_forge/documentation/openapi.rb +23 -0
  64. data/lib/spec_forge/documentation.rb +27 -0
  65. data/lib/spec_forge/error.rb +284 -113
  66. data/lib/spec_forge/factory.rb +35 -16
  67. data/lib/spec_forge/filter.rb +86 -0
  68. data/lib/spec_forge/forge.rb +171 -0
  69. data/lib/spec_forge/http/backend.rb +101 -29
  70. data/lib/spec_forge/http/client.rb +23 -13
  71. data/lib/spec_forge/http/request.rb +85 -62
  72. data/lib/spec_forge/http/verb.rb +79 -0
  73. data/lib/spec_forge/http.rb +105 -0
  74. data/lib/spec_forge/loader.rb +244 -0
  75. data/lib/spec_forge/matchers.rb +130 -0
  76. data/lib/spec_forge/normalizer/default.rb +51 -0
  77. data/lib/spec_forge/normalizer/definition.rb +248 -0
  78. data/lib/spec_forge/normalizer/validators.rb +99 -0
  79. data/lib/spec_forge/normalizer.rb +486 -115
  80. data/lib/spec_forge/normalizers/_shared.yml +74 -0
  81. data/lib/spec_forge/normalizers/configuration.yml +23 -0
  82. data/lib/spec_forge/normalizers/constraint.yml +8 -0
  83. data/lib/spec_forge/normalizers/expectation.yml +47 -0
  84. data/lib/spec_forge/normalizers/factory.yml +12 -0
  85. data/lib/spec_forge/normalizers/factory_reference.yml +15 -0
  86. data/lib/spec_forge/normalizers/global_context.yml +28 -0
  87. data/lib/spec_forge/normalizers/spec.yml +50 -0
  88. data/lib/spec_forge/runner/adapter.rb +183 -0
  89. data/lib/spec_forge/runner/callbacks.rb +246 -0
  90. data/lib/spec_forge/runner/debug_proxy.rb +213 -0
  91. data/lib/spec_forge/runner/listener.rb +54 -0
  92. data/lib/spec_forge/runner/metadata.rb +58 -0
  93. data/lib/spec_forge/runner/state.rb +98 -0
  94. data/lib/spec_forge/runner.rb +50 -125
  95. data/lib/spec_forge/spec/expectation/constraint.rb +100 -21
  96. data/lib/spec_forge/spec/expectation.rb +47 -51
  97. data/lib/spec_forge/spec.rb +50 -108
  98. data/lib/spec_forge/type.rb +36 -4
  99. data/lib/spec_forge/version.rb +4 -1
  100. data/lib/spec_forge.rb +168 -76
  101. data/lib/templates/openapi.yml.tt +22 -0
  102. data/lib/templates/redoc.html.tt +28 -0
  103. data/lib/templates/swagger.html.tt +59 -0
  104. metadata +109 -16
  105. data/lib/spec_forge/normalizer/configuration.rb +0 -77
  106. data/lib/spec_forge/normalizer/constraint.rb +0 -47
  107. data/lib/spec_forge/normalizer/expectation.rb +0 -86
  108. data/lib/spec_forge/normalizer/factory.rb +0 -65
  109. data/lib/spec_forge/normalizer/factory_reference.rb +0 -71
  110. data/lib/spec_forge/normalizer/spec.rb +0 -74
  111. data/spec_forge/factories/user.yml +0 -4
  112. data/spec_forge/forge_helper.rb +0 -48
  113. data/spec_forge/specs/users.yml +0 -65
  114. /data/lib/templates/{forge_helper.tt → forge_helper.rb.tt} +0 -0
  115. /data/lib/templates/{new_factory.tt → new_factory.yml.tt} +0 -0
  116. /data/lib/templates/{new_spec.tt → new_spec.yml.tt} +0 -0
@@ -0,0 +1,74 @@
1
+ id: string
2
+
3
+ name: string
4
+
5
+ line_number: integer
6
+
7
+ base_url:
8
+ type: string
9
+ default: null
10
+ required: false
11
+
12
+ url:
13
+ type: string
14
+ default: null
15
+ required: false
16
+ aliases:
17
+ - path
18
+
19
+ http_verb:
20
+ type: string
21
+ default: null # Do not default this to "GET". Leave it null. Seriously.
22
+ required: false
23
+ aliases:
24
+ - method
25
+ - http_method
26
+ validator: http_verb
27
+
28
+ headers:
29
+ type: hash
30
+ default: {}
31
+ required: false
32
+
33
+ query:
34
+ type:
35
+ - hash
36
+ - string
37
+ aliases:
38
+ - params
39
+ default: {}
40
+ required: false
41
+
42
+ body:
43
+ type:
44
+ - hash
45
+ - string
46
+ aliases:
47
+ - data
48
+ default: {}
49
+ required: false
50
+
51
+ variables:
52
+ type:
53
+ - hash
54
+ - string
55
+ default: {}
56
+ required: false
57
+
58
+ debug:
59
+ type: boolean
60
+ aliases:
61
+ - pry
62
+ - breakpoint
63
+ default: false
64
+ required: false
65
+
66
+ callback:
67
+ type: string
68
+ required: false
69
+ validator: callback
70
+
71
+ documentation:
72
+ type: boolean
73
+ required: false
74
+ default: true
@@ -0,0 +1,23 @@
1
+ base_url: string
2
+
3
+ headers:
4
+ reference: headers
5
+
6
+ query:
7
+ reference: query
8
+
9
+ factories:
10
+ type: hash
11
+ default: {}
12
+ structure:
13
+ ###########################################
14
+ auto_discover:
15
+ type: boolean
16
+ default: true
17
+
18
+ paths:
19
+ type: array
20
+ default: []
21
+
22
+ on_debug:
23
+ type: proc
@@ -0,0 +1,8 @@
1
+ status:
2
+ - integer
3
+ - string
4
+ json:
5
+ type:
6
+ - hash
7
+ - array
8
+ default: {}
@@ -0,0 +1,47 @@
1
+ # Internal
2
+ id:
3
+ reference: id
4
+
5
+ line_number:
6
+ reference: line_number
7
+
8
+ # User defined
9
+ name:
10
+ reference: name
11
+
12
+ base_url:
13
+ reference: base_url
14
+
15
+ url:
16
+ reference: url
17
+
18
+ http_verb:
19
+ reference: http_verb
20
+
21
+ headers:
22
+ reference: headers
23
+
24
+ query:
25
+ reference: query
26
+
27
+ body:
28
+ reference: body
29
+
30
+ variables:
31
+ reference: variables
32
+
33
+ debug:
34
+ reference: debug
35
+
36
+ store_as:
37
+ type: string
38
+ default: ""
39
+
40
+ documentation:
41
+ reference: documentation
42
+
43
+ expect:
44
+ type: hash
45
+ structure:
46
+ ###########################################
47
+ reference: constraint
@@ -0,0 +1,12 @@
1
+ model_class:
2
+ type: string
3
+ default: ""
4
+ aliases:
5
+ - class
6
+
7
+ variables:
8
+ reference: variables
9
+
10
+ attributes:
11
+ type: hash
12
+ default: {}
@@ -0,0 +1,15 @@
1
+ attributes:
2
+ type: hash
3
+ default: {}
4
+
5
+ build_strategy:
6
+ type: string
7
+ default: create
8
+ aliases:
9
+ - strategy
10
+
11
+ size:
12
+ type: integer
13
+ default: 0
14
+ aliases:
15
+ - count
@@ -0,0 +1,28 @@
1
+ variables:
2
+ reference: variables
3
+
4
+ callbacks:
5
+ type: array
6
+ default: []
7
+ structure:
8
+ ###########################################
9
+ type: hash
10
+ default: {}
11
+ structure:
12
+ ###########################################
13
+ before_file:
14
+ reference: callback
15
+ before_spec:
16
+ reference: callback
17
+ before_each:
18
+ reference: callback
19
+ aliases:
20
+ - before
21
+ after_each:
22
+ reference: callback
23
+ aliases:
24
+ - after
25
+ after_spec:
26
+ reference: callback
27
+ after_file:
28
+ reference: callback
@@ -0,0 +1,50 @@
1
+ # Internal
2
+ id:
3
+ reference: id
4
+
5
+ name:
6
+ reference: name
7
+
8
+ file_name: string
9
+
10
+ file_path: string
11
+
12
+ line_number:
13
+ reference: line_number
14
+
15
+ # User defined
16
+ base_url:
17
+ reference: base_url
18
+
19
+ url:
20
+ reference: url
21
+
22
+ http_verb:
23
+ reference: http_verb
24
+
25
+ headers:
26
+ reference: headers
27
+
28
+ query:
29
+ reference: query
30
+
31
+ body:
32
+ reference: body
33
+
34
+ variables:
35
+ reference: variables
36
+
37
+ debug:
38
+ reference: debug
39
+
40
+ documentation:
41
+ reference: documentation
42
+
43
+ expectations:
44
+ type: array
45
+ structure:
46
+ ###########################################
47
+ type: hash
48
+ structure:
49
+ ###########################################
50
+ reference: expectation
@@ -0,0 +1,183 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SpecForge
4
+ class Runner
5
+ #
6
+ # Bridges SpecForge specs with RSpec execution
7
+ #
8
+ # Converts SpecForge forge objects into RSpec test structures
9
+ # and manages the test execution lifecycle.
10
+ #
11
+ class Adapter
12
+ include Singleton
13
+
14
+ #
15
+ # Configures RSpec with forge definitions
16
+ #
17
+ # Sets up RSpec and prepares everything for running tests
18
+ #
19
+ # @param forges [Array<Forge>] The forges to set up for testing
20
+ #
21
+ def self.setup(forges)
22
+ # Defines the forges with RSpec
23
+ forges.each { |forge| instance.describe(forge) }
24
+
25
+ # Disable autorun because RSpec does it
26
+ RSpec::Core::Runner.disable_autorun!
27
+
28
+ # Allows modifying the error backtrace reporting within rspec
29
+ RSpec.configuration.instance_variable_set(:@backtrace_formatter, BacktraceFormatter)
30
+
31
+ # Listen for passed/failed events to trigger the "after_each" callback
32
+ RSpec.configuration.reporter.register_listener(
33
+ Listener.instance,
34
+ :example_passed, :example_failed
35
+ )
36
+ end
37
+
38
+ #
39
+ # Executes the configured RSpec tests
40
+ #
41
+ # Runs all configured tests through RSpec with optional exit behavior.
42
+ #
43
+ # @param exit_on_finish [Boolean] Whether to exit the process when done
44
+ # @param exit_on_failure [Boolean] Whether to exit the process if any test fails
45
+ #
46
+ # @return [Integer, nil] Exit status if exit_on_finish is false
47
+ #
48
+ def self.run(exit_on_finish: false, exit_on_failure: false)
49
+ status = RSpec::Core::Runner.run([]).to_i
50
+
51
+ exit(status) if exit_on_finish || (exit_on_failure && status != 0)
52
+
53
+ status
54
+ end
55
+
56
+ ##########################################################################
57
+
58
+ #
59
+ # Defines RSpec examples for a specific forge
60
+ # Creates the test structure for a single forge file
61
+ #
62
+ # @param forge [Forge] The forge to define
63
+ #
64
+ def describe(forge)
65
+ # This is just like writing a normal RSpec test, except with loops ;)
66
+ RSpec.describe(forge.name) do
67
+ # Callback for the file
68
+ before(:context) { Callbacks.before_file(forge) }
69
+ after(:context) { Callbacks.after_file(forge) }
70
+
71
+ # Specs
72
+ forge.specs.each do |spec|
73
+ # Describe the spec
74
+ describe(spec.name) do
75
+ # Request data is for the spec and contains the base and overlays
76
+ let!(:request_data) { forge.request[spec.id] }
77
+
78
+ # The HTTP client for the spec
79
+ let!(:http_client) { HTTP::Client.new(**request_data[:base]) }
80
+
81
+ # Callback for the spec
82
+ before(:context) { Callbacks.before_spec(forge, spec) }
83
+ after(:context) { Callbacks.after_spec(forge, spec) }
84
+
85
+ # Expectations
86
+ spec.expectations.each do |expectation|
87
+ # Onto the actual expectation itself
88
+ describe(expectation.name) do
89
+ # Set metadata for the example group for error reporting
90
+ Metadata.set_for_group(spec, expectation, self)
91
+
92
+ # Lazily load the constraints
93
+ let(:constraints) { expectation.constraints.as_matchers }
94
+
95
+ let(:match_status) { constraints[:status] }
96
+ let(:match_json) { constraints[:json] }
97
+ let(:match_json_class) { be_kind_of(match_json.class) }
98
+ let(:match_headers) { constraints[:headers] }
99
+
100
+ # The request for the test itself. Overlays the expectation's data if it exists
101
+ let(:request) do
102
+ request = request_data[:base]
103
+
104
+ if (overlay = request_data[:overlay][expectation.id])
105
+ request = request.deep_merge(overlay)
106
+ end
107
+
108
+ HTTP::Request.new(**request)
109
+ end
110
+
111
+ # The Faraday response
112
+ subject(:response) { http_client.call(request) }
113
+
114
+ # Callbacks for the expectation
115
+ before :each do
116
+ Callbacks.before_expectation(
117
+ forge, spec, expectation, self, RSpec.current_example
118
+ )
119
+ end
120
+
121
+ # The 'after_expectation' callback is handled by Listener due to RSpec not
122
+ # reporting the example's status until after the describe block has finished.
123
+ after :each do
124
+ # However, the downside about having the callback triggered later is that RSpec
125
+ # will have reset the memoized let variables back to nil.
126
+ # This causes an issue when an expectation goes to store the state, it will end
127
+ # up re-calling the various variables and triggering another HTTP request.
128
+ # Since the variables are still memoized in this hook, it is the perfect
129
+ # time to store the referenced to them.
130
+ State.set(response:)
131
+ end
132
+
133
+ # The test itself
134
+ it(expectation.constraints.description) do
135
+ # Debugging
136
+ if spec.debug? || expectation.debug?
137
+ Callbacks.on_debug(forge, spec, expectation, self)
138
+ end
139
+
140
+ ############################################################
141
+ # Status check
142
+ expect(response.status).to match_status
143
+
144
+ ############################################################
145
+ # Headers check
146
+ if match_headers.present?
147
+ match_headers.each do |key, matcher|
148
+ expect(response.headers).to include(key.downcase => matcher)
149
+ end
150
+ end
151
+
152
+ ############################################################
153
+ # JSON check
154
+ if match_json.present?
155
+ expect(response.body).to match_json_class
156
+
157
+ case match_json
158
+ when Hash
159
+ match_json.each do |key, matcher|
160
+ expect(response.body).to include(key)
161
+
162
+ begin
163
+ expect(response.body[key]).to matcher
164
+ rescue RSpec::Expectations::ExpectationNotMetError => e
165
+ # Add the key that failed to the front of the error message
166
+ e.message.insert(0, "Key: #{key.in_quotes}\n")
167
+ raise e
168
+ end
169
+ end
170
+ else
171
+ expect(response.body).to match_json
172
+ end
173
+ end
174
+ end
175
+ end
176
+ end
177
+ end
178
+ end
179
+ end
180
+ end
181
+ end
182
+ end
183
+ end
@@ -0,0 +1,246 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SpecForge
4
+ class Runner
5
+ #
6
+ # Manages lifecycle hooks for test execution
7
+ #
8
+ # This class provides callback methods that run at specific points during test execution
9
+ # to prepare the test environment, manage state, and perform cleanup operations. These
10
+ # callbacks integrate with RSpec's test lifecycle and maintain the SpecForge context.
11
+ #
12
+ # @example Running before file callback
13
+ # Callbacks.before_file(forge)
14
+ #
15
+ class Callbacks
16
+ class << self
17
+ #
18
+ # Callback executed before a file's specs are run
19
+ #
20
+ # Initializes global context and sets up any file-level state needed
21
+ # for all specs in the file.
22
+ #
23
+ # @param forge [SpecForge::Forge] The forge representing the current file
24
+ #
25
+ def before_file(forge)
26
+ # Set the global variables
27
+ SpecForge.context.global.set(**forge.global)
28
+
29
+ # Clear the store for this file
30
+ SpecForge.context.store.clear
31
+
32
+ # Start fresh
33
+ State.clear
34
+
35
+ # Run the user's before_file callbacks
36
+ run_user_callbacks(:before_file, file_context(forge))
37
+ end
38
+
39
+ #
40
+ # Callback executed before each spec is run
41
+ #
42
+ # Prepares the context for a specific spec, including loading
43
+ # spec-level variables and configuration.
44
+ #
45
+ # @param forge [SpecForge::Forge] The forge being tested
46
+ # @param spec [SpecForge::Spec] The spec about to be executed
47
+ #
48
+ def before_spec(forge, spec)
49
+ # Prepare the variables for this spec
50
+ SpecForge.context.variables.set(**forge.variables_for_spec(spec))
51
+
52
+ # Clear any "spec" level stored data
53
+ SpecForge.context.store.clear_specs
54
+
55
+ # Run the user's before_spec callbacks
56
+ run_user_callbacks(:before_spec, spec_context(forge, spec))
57
+ end
58
+
59
+ #
60
+ # Callback executed before each expectation is run
61
+ #
62
+ # Prepares variables for the specific expectation and sets up
63
+ # example metadata for error reporting.
64
+ #
65
+ # @param forge [SpecForge::Forge] The forge being tested
66
+ # @param spec [SpecForge::Spec] The spec being tested
67
+ # @param expectation [SpecForge::Spec::Expectation] The expectation about to be evaluated
68
+ # @param example_group [RSpec::Core::ExampleGroup] The current running example group
69
+ # @param example [RSpec::Core::Example] The current example
70
+ #
71
+ def before_expectation(forge, spec, expectation, example_group, example)
72
+ # Store metadata to failure/error messages display the correct information
73
+ Metadata.set_for_example(spec, expectation)
74
+
75
+ # Store state data for callbacks and persisting data into the store
76
+ State.set(
77
+ forge:, spec:, expectation:, example_group:, example:,
78
+ request: example_group.request
79
+ )
80
+
81
+ # Load the variable overlay for this expectation (if one exists)
82
+ SpecForge.context.variables.use_overlay(expectation.id)
83
+
84
+ # Run the user's before_each callbacks
85
+ run_user_callbacks(:before_each, expectation_context(forge, spec, expectation, example))
86
+ end
87
+
88
+ #
89
+ # Handles debug mode for an expectation
90
+ #
91
+ # When debugging is enabled for a spec or expectation, this method
92
+ # creates a debugging environment for inspecting test state.
93
+ #
94
+ # @param forge [SpecForge::Forge] The forge being tested
95
+ # @param spec [SpecForge::Spec] The spec being tested
96
+ # @param expectation [SpecForge::Spec::Expectation] The expectation being evaluated
97
+ # @param example_group [RSpec::Core::ExampleGroup] The current running example group
98
+ #
99
+ def on_debug(forge, spec, expectation, example_group)
100
+ DebugProxy.new(forge, spec, expectation, example_group).call
101
+ end
102
+
103
+ #
104
+ # Callback executed after each expectation is run
105
+ #
106
+ # Performs cleanup and stores results if needed for future reference.
107
+ #
108
+ # @param forge [SpecForge::Forge] The forge being tested
109
+ # @param spec [SpecForge::Spec] The spec being tested
110
+ # @param expectation [SpecForge::Spec::Expectation] The expectation that was evaluated
111
+ # @param example_group [RSpec::Core::ExampleGroup] The current running example group
112
+ # @param example [RSpec::Core::Example] The current example
113
+ #
114
+ def after_expectation(forge, spec, expectation, example_group, example)
115
+ # Note: Let variables on `example_group` have been reset by RSpec at this point.
116
+ # Calling them will result in a new value being returned and memoized.
117
+ # In other words, do not call `example_group.response` in here unless you
118
+ # like potentially duplicating data ;)
119
+ State.persist
120
+
121
+ # Run the user's after_each callbacks
122
+ run_user_callbacks(:after_each, expectation_context(forge, spec, expectation, example))
123
+
124
+ # Clear the state for the next expectation
125
+ State.clear
126
+ end
127
+
128
+ #
129
+ # Callback executed after each spec is ran
130
+ #
131
+ # @param forge [SpecForge::Forge] The forge being tested
132
+ # @param spec [SpecForge::Spec] The spec that was executed
133
+ #
134
+ def after_spec(forge, spec)
135
+ # Run the user's after_spec callbacks
136
+ run_user_callbacks(:after_spec, spec_context(forge, spec))
137
+ end
138
+
139
+ #
140
+ # Callback executed after a file's specs have been ran
141
+ #
142
+ # @param forge [SpecForge::Forge] The forge representing the current file
143
+ #
144
+ def after_file(forge)
145
+ # Run the user's after_file callbacks
146
+ run_user_callbacks(:after_file, file_context(forge))
147
+ end
148
+
149
+ private
150
+
151
+ #
152
+ # Executes user-defined callbacks for a specific lifecycle point
153
+ #
154
+ # Processes the callback_type to extract timing and scope information,
155
+ # adds this metadata to the context, and then triggers all registered
156
+ # callbacks for that type.
157
+ #
158
+ # @param callback_type [Symbol, String] The type of callback to run
159
+ # (:before_file, :after_spec, etc.)
160
+ # @param context [Hash] Context data containing state information for the callback
161
+ #
162
+ # @private
163
+ #
164
+ def run_user_callbacks(callback_type, context)
165
+ callback_timing, callback_scope = callback_type.to_s.split("_")
166
+
167
+ # Adds "before_each", "before", and "each" into the context so callbacks
168
+ # can build logic off of them
169
+ context.merge!(
170
+ callback_type: callback_type.to_s,
171
+ callback_timing:, callback_scope:
172
+ )
173
+
174
+ # Run the callbacks for this type
175
+ SpecForge.context.global.callbacks.run(callback_type, context)
176
+ end
177
+
178
+ #
179
+ # Builds the base context for file-level callbacks
180
+ #
181
+ # @param forge [SpecForge::Forge] The forge representing the file
182
+ #
183
+ # @return [Hash] Basic file context
184
+ #
185
+ # @private
186
+ #
187
+ def file_context(forge)
188
+ {
189
+ forge: forge,
190
+ file_path: forge.metadata[:file_path],
191
+ file_name: forge.metadata[:file_name]
192
+ }
193
+ end
194
+
195
+ #
196
+ # Builds context for spec-level callbacks
197
+ # Includes file context plus spec information
198
+ #
199
+ # @param forge [SpecForge::Forge] The forge representing the file
200
+ # @param spec [SpecForge::Spec] The spec being executed
201
+ #
202
+ # @return [Hash] Context with file and spec information
203
+ #
204
+ # @private
205
+ #
206
+ def spec_context(forge, spec)
207
+ file_context(forge).merge(
208
+ spec: spec,
209
+ spec_name: spec.name,
210
+ variables: SpecForge.context.variables
211
+ )
212
+ end
213
+
214
+ #
215
+ # Builds context for expectation-level callbacks
216
+ # Includes spec context plus expectation information
217
+ #
218
+ # @param forge [SpecForge::Forge] The forge being tested
219
+ # @param spec [SpecForge::Spec] The spec being tested
220
+ # @param expectation [SpecForge::Spec::Expectation] The expectation being evaluated
221
+ # @param example [RSpec::Core::Example] The current example
222
+ #
223
+ # @return [Hash] Context with file, spec and expectation information
224
+ #
225
+ # @private
226
+ #
227
+ def expectation_context(forge, spec, expectation, example)
228
+ example_group = State.current.example_group
229
+
230
+ # Pull this data from the State instead of example group to avoid creating a new value
231
+ request = State.current.request
232
+ response = State.current.response
233
+
234
+ spec_context(forge, spec).merge(
235
+ expectation:,
236
+ expectation_name: expectation.name,
237
+ request:,
238
+ response:,
239
+ example_group:,
240
+ example:
241
+ )
242
+ end
243
+ end
244
+ end
245
+ end
246
+ end