spec_forge 0.7.1 → 1.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.
Files changed (133) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +75 -1
  3. data/README.md +124 -202
  4. data/bin/spec_forge +1 -1
  5. data/flake.lock +76 -4
  6. data/flake.nix +5 -4
  7. data/lib/spec_forge/attribute/chainable.rb +6 -6
  8. data/lib/spec_forge/attribute/environment.rb +45 -0
  9. data/lib/spec_forge/attribute/factory.rb +26 -17
  10. data/lib/spec_forge/attribute/faker.rb +6 -1
  11. data/lib/spec_forge/attribute/generate.rb +114 -0
  12. data/lib/spec_forge/attribute/literal.rb +1 -14
  13. data/lib/spec_forge/attribute/matcher.rb +6 -2
  14. data/lib/spec_forge/attribute/parameterized.rb +20 -22
  15. data/lib/spec_forge/attribute/resolvable_array.rb +16 -16
  16. data/lib/spec_forge/attribute/resolvable_hash.rb +17 -16
  17. data/lib/spec_forge/attribute/resolvable_struct.rb +67 -0
  18. data/lib/spec_forge/attribute/template.rb +118 -0
  19. data/lib/spec_forge/attribute/transform.rb +14 -19
  20. data/lib/spec_forge/attribute/variable.rb +31 -31
  21. data/lib/spec_forge/attribute.rb +54 -100
  22. data/lib/spec_forge/blueprint.rb +27 -0
  23. data/lib/spec_forge/cli/docs/generate.rb +28 -8
  24. data/lib/spec_forge/cli/docs.rb +5 -2
  25. data/lib/spec_forge/cli/init.rb +4 -4
  26. data/lib/spec_forge/cli/new.rb +78 -27
  27. data/lib/spec_forge/cli/run.rb +84 -52
  28. data/lib/spec_forge/cli/serve.rb +5 -0
  29. data/lib/spec_forge/cli.rb +6 -14
  30. data/lib/spec_forge/configuration.rb +209 -79
  31. data/lib/spec_forge/documentation/{loader → builder}/cache.rb +26 -23
  32. data/lib/spec_forge/documentation/builder/compiler.rb +373 -0
  33. data/lib/spec_forge/documentation/builder/extractor.rb +75 -0
  34. data/lib/spec_forge/documentation/builder.rb +77 -329
  35. data/lib/spec_forge/documentation/document/operation.rb +4 -4
  36. data/lib/spec_forge/documentation/document.rb +0 -6
  37. data/lib/spec_forge/documentation/generator.rb +88 -0
  38. data/lib/spec_forge/documentation/{generators/openapi → openapi/v3_0}/error_formatter.rb +2 -2
  39. data/lib/spec_forge/documentation/openapi/v3_0/example.rb +1 -1
  40. data/lib/spec_forge/documentation/openapi/v3_0/media_type.rb +1 -1
  41. data/lib/spec_forge/documentation/openapi/v3_0/operation.rb +21 -5
  42. data/lib/spec_forge/documentation/openapi/v3_0/response.rb +28 -6
  43. data/lib/spec_forge/documentation/openapi/v3_0/schema.rb +20 -2
  44. data/lib/spec_forge/documentation/openapi/v3_0/tag.rb +1 -1
  45. data/lib/spec_forge/documentation/openapi/v3_0.rb +116 -0
  46. data/lib/spec_forge/documentation/openapi.rb +40 -12
  47. data/lib/spec_forge/documentation.rb +1 -7
  48. data/lib/spec_forge/error.rb +215 -41
  49. data/lib/spec_forge/factory.rb +38 -18
  50. data/lib/spec_forge/forge/action.rb +41 -0
  51. data/lib/spec_forge/forge/actions/call.rb +33 -0
  52. data/lib/spec_forge/forge/actions/debug.rb +47 -0
  53. data/lib/spec_forge/forge/actions/expect.rb +44 -0
  54. data/lib/spec_forge/forge/actions/request.rb +65 -0
  55. data/lib/spec_forge/forge/actions/store.rb +31 -0
  56. data/lib/spec_forge/forge/callbacks.rb +80 -0
  57. data/lib/spec_forge/forge/context.rb +41 -0
  58. data/lib/spec_forge/forge/display.rb +503 -0
  59. data/lib/spec_forge/forge/hooks.rb +131 -0
  60. data/lib/spec_forge/forge/runner/array_io.rb +81 -0
  61. data/lib/spec_forge/forge/runner/content_validator.rb +92 -0
  62. data/lib/spec_forge/forge/runner/header_validator.rb +66 -0
  63. data/lib/spec_forge/forge/runner/reporter.rb +56 -0
  64. data/lib/spec_forge/forge/runner/schema_validator.rb +113 -0
  65. data/lib/spec_forge/forge/runner.rb +118 -0
  66. data/lib/spec_forge/forge/timer.rb +94 -0
  67. data/lib/spec_forge/forge/variables.rb +38 -0
  68. data/lib/spec_forge/forge.rb +207 -133
  69. data/lib/spec_forge/http/backend.rb +49 -146
  70. data/lib/spec_forge/http/client.rb +14 -17
  71. data/lib/spec_forge/http/request.rb +37 -84
  72. data/lib/spec_forge/http/verb.rb +4 -0
  73. data/lib/spec_forge/http.rb +0 -5
  74. data/lib/spec_forge/loader/filter.rb +85 -0
  75. data/lib/spec_forge/loader/step_processor.rb +282 -0
  76. data/lib/spec_forge/loader.rb +105 -220
  77. data/lib/spec_forge/normalizer/default.rb +1 -1
  78. data/lib/spec_forge/normalizer/structure.rb +140 -0
  79. data/lib/spec_forge/normalizer/transformers.rb +168 -0
  80. data/lib/spec_forge/normalizer/validators.rb +50 -8
  81. data/lib/spec_forge/normalizer.rb +76 -119
  82. data/lib/spec_forge/normalizers/callback.yml +38 -0
  83. data/lib/spec_forge/normalizers/configuration.yml +59 -9
  84. data/lib/spec_forge/normalizers/factory.yml +53 -2
  85. data/lib/spec_forge/normalizers/factory_reference.yml +63 -2
  86. data/lib/spec_forge/normalizers/json_schema.yml +79 -0
  87. data/lib/spec_forge/normalizers/step.yml +506 -0
  88. data/lib/spec_forge/step/call.rb +36 -0
  89. data/lib/spec_forge/step/expect.rb +110 -0
  90. data/lib/spec_forge/step/source.rb +22 -0
  91. data/lib/spec_forge/step.rb +129 -0
  92. data/lib/spec_forge/type.rb +115 -66
  93. data/lib/spec_forge/version.rb +1 -1
  94. data/lib/spec_forge.rb +44 -106
  95. data/lib/templates/forge_helper.rb.tt +43 -22
  96. data/lib/templates/new_blueprint.yml.tt +54 -0
  97. metadata +75 -44
  98. data/lib/spec_forge/attribute/global.rb +0 -96
  99. data/lib/spec_forge/attribute/store.rb +0 -65
  100. data/lib/spec_forge/backtrace_formatter.rb +0 -50
  101. data/lib/spec_forge/callbacks.rb +0 -88
  102. data/lib/spec_forge/context/callbacks.rb +0 -91
  103. data/lib/spec_forge/context/global.rb +0 -72
  104. data/lib/spec_forge/context/store.rb +0 -131
  105. data/lib/spec_forge/context/variables.rb +0 -91
  106. data/lib/spec_forge/context.rb +0 -36
  107. data/lib/spec_forge/core_ext/rspec.rb +0 -55
  108. data/lib/spec_forge/core_ext.rb +0 -5
  109. data/lib/spec_forge/documentation/generators/base.rb +0 -81
  110. data/lib/spec_forge/documentation/generators/openapi/base.rb +0 -100
  111. data/lib/spec_forge/documentation/generators/openapi/v3_0.rb +0 -65
  112. data/lib/spec_forge/documentation/generators/openapi.rb +0 -59
  113. data/lib/spec_forge/documentation/generators.rb +0 -17
  114. data/lib/spec_forge/documentation/loader.rb +0 -159
  115. data/lib/spec_forge/documentation/openapi/base.rb +0 -33
  116. data/lib/spec_forge/filter.rb +0 -86
  117. data/lib/spec_forge/normalizer/definition.rb +0 -248
  118. data/lib/spec_forge/normalizers/_shared.yml +0 -76
  119. data/lib/spec_forge/normalizers/constraint.yml +0 -8
  120. data/lib/spec_forge/normalizers/expectation.yml +0 -47
  121. data/lib/spec_forge/normalizers/global_context.yml +0 -28
  122. data/lib/spec_forge/normalizers/spec.yml +0 -50
  123. data/lib/spec_forge/runner/adapter.rb +0 -181
  124. data/lib/spec_forge/runner/callbacks.rb +0 -246
  125. data/lib/spec_forge/runner/debug_proxy.rb +0 -215
  126. data/lib/spec_forge/runner/listener.rb +0 -54
  127. data/lib/spec_forge/runner/metadata.rb +0 -58
  128. data/lib/spec_forge/runner/state.rb +0 -98
  129. data/lib/spec_forge/runner.rb +0 -75
  130. data/lib/spec_forge/spec/expectation/constraint.rb +0 -127
  131. data/lib/spec_forge/spec/expectation.rb +0 -68
  132. data/lib/spec_forge/spec.rb +0 -68
  133. data/lib/templates/new_spec.yml.tt +0 -43
@@ -1,215 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module SpecForge
4
- class Runner
5
- #
6
- # Creates a debugging environment during test execution.
7
- # When a breakpoint is triggered, this provides an interface to inspect
8
- # the current test state including the request, response, variables, and expectations.
9
- #
10
- # By default, this outputs a JSON representation of the current testing context,
11
- # but it can be customized by configuring SpecForge.configuration.on_debug
12
- # to use any Ruby debugger (like pry or debug).
13
- #
14
- # @example Basic usage in a spec with `debug: true`
15
- # # In your YAML test:
16
- # get_users:
17
- # debug: true
18
- # path: /users
19
- # expectations:
20
- # - expect:
21
- # status: 200
22
- #
23
- # @example Custom debug handler in forge_helper.rb
24
- # SpecForge.configure do |config|
25
- # config.on_debug { binding.pry } # Requires 'pry' gem
26
- # end
27
- #
28
- class DebugProxy
29
- #
30
- # @return [Proc] The default debugging handler that outputs JSON state information
31
- #
32
- def self.default
33
- lambda do
34
- puts <<~STRING
35
-
36
- Debug triggered for:
37
- > #{example.metadata[:rerun_file_path]} on line #{expectation.line_number}
38
-
39
- Available debugging contexts:
40
- - spec: Current spec details
41
- - expectation: Current expectation being tested
42
- - variables: Variables defined for this test
43
- - global: Global context shared across tests
44
- - store: Stored data from expectations
45
-
46
- Request & Response:
47
- - request: HTTP request details (method, url, headers, body)
48
- - response: HTTP response with headers, status and body
49
-
50
- Expectations:
51
- - expected_status: Expected HTTP status code
52
- - expected_json: Expected response body structure
53
-
54
- Matchers:
55
- - match_status: Matcher used to test status
56
- - match_json: Matcher used to test response body
57
-
58
- Helper objects:
59
- - http_client: The HTTP client used for the request
60
- - request_data: Raw request configuration data
61
- - example_group: Current RSpec example group
62
- - example: Current RSpec example
63
- - forge: Current file being tested
64
-
65
- 💡 Pro tips:
66
- - Type 'self' or 'inspect' for a pretty-printed JSON overview
67
- - Use 'to_h' for the hash representation
68
- - Access the shared context with 'SpecForge.context'
69
- STRING
70
-
71
- puts inspect
72
- end
73
- end
74
-
75
- # @return [RSpec::Forge] The current Forge that is being tested
76
- attr_reader :forge
77
-
78
- # @return [SpecForge::Spec] The current Spec that is being tested
79
- attr_reader :spec
80
-
81
- # @return [SpecForge::Spec::Expectation] The current expectation that is being tested
82
- attr_reader :expectation
83
-
84
- # @return [RSpec::ExampleGroup] The current RSpec example group
85
- attr_reader :example_group
86
-
87
- # @return [RSpec::Example] The current RSpec example that is running
88
- attr_reader :example
89
-
90
- # @return [Integer] The expected HTTP status code
91
- attr_reader :expected_status
92
-
93
- # @return [Object] The expected response body structure
94
- attr_reader :expected_json
95
-
96
- delegate_missing_to :@example_group
97
-
98
- #
99
- # Creates a new DebugProxy instance
100
- #
101
- # @param forge [SpecForge::Forge] The forge being tested
102
- # @param spec [SpecForge::Spec] The spec being tested
103
- # @param expectation [SpecForge::Spec::Expectation] The expectation being tested
104
- # @param example_group [RSpec::Core::ExampleGroup] The current example group
105
- #
106
- # @return [SpecForge::Runner::DebugProxy]
107
- #
108
- def initialize(forge, spec, expectation, example_group)
109
- @callback = SpecForge.configuration.on_debug_proc
110
-
111
- @forge = forge
112
- @spec = spec
113
- @expectation = expectation
114
- @example_group = example_group
115
- @example = RSpec.current_example
116
-
117
- constraints = expectation.constraints
118
-
119
- @expected_status = constraints.status.resolved
120
- @expected_json = constraints.json.resolved
121
- end
122
-
123
- #
124
- # Triggers the debugging environment
125
- #
126
- # Displays available debugging contexts and executes the configured debug callback.
127
- # The callback runs in the context of this proxy, giving it access to all helper methods.
128
- #
129
- # @return [void]
130
- #
131
- def call
132
- instance_exec(&@callback)
133
- end
134
-
135
- ##########################################################################
136
-
137
- #
138
- # Returns a hash representation of the global context
139
- #
140
- # @return [Hash] The global context with resolved variables
141
- #
142
- def global
143
- @global ||= SpecForge.context.global.to_h
144
- end
145
-
146
- #
147
- # Returns a hash representation of the variables in the current context
148
- #
149
- # Includes both spec-level and expectation-level variables combined
150
- # with values fully resolved.
151
- #
152
- # @return [Hash]
153
- #
154
- def variables
155
- @variables ||= SpecForge.context.variables
156
- end
157
-
158
- #
159
- # Returns a hash representation of the store context
160
- #
161
- # @return [Hash] The store context
162
- #
163
- def store
164
- @store ||= SpecForge.context.store.to_h
165
- end
166
-
167
- ##########################################################################
168
-
169
- #
170
- # Returns a hash representation of the test state
171
- #
172
- # Includes the spec, expectation, request, response, variables and global context.
173
- # RSpec matchers are converted to human-readable descriptions.
174
- #
175
- # @return [Hash]
176
- #
177
- def to_h
178
- spec_hash = spec.to_h.except(:expectations)
179
-
180
- expectation_hash = expectation.to_h
181
- expectation_hash[:expect][:json] = matchers_to_description(expectation_hash[:expect][:json])
182
-
183
- {
184
- global:,
185
- variables:,
186
- request: request.to_h,
187
- response: {
188
- status: response.status,
189
- body: response.body,
190
- headers: response.headers
191
- },
192
- expectation: expectation_hash,
193
- spec: spec_hash
194
- }
195
- end
196
-
197
- #
198
- # Returns a formatted JSON representation of the test state
199
- #
200
- # @return [String] Pretty-printed JSON of the test state
201
- #
202
- def inspect
203
- JSON.pretty_generate(to_h)
204
- end
205
-
206
- private
207
-
208
- def matchers_to_description(value)
209
- return value unless value.is_a?(RSpec::Matchers::BuiltIn::BaseMatcher)
210
-
211
- value.description
212
- end
213
- end
214
- end
215
- end
@@ -1,54 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module SpecForge
4
- class Runner
5
- #
6
- # Listens for RSpec test result notifications and triggers the appropriate callbacks
7
- #
8
- # This singleton class receives notifications from RSpec when examples pass or fail,
9
- # retrieves the current test context, and triggers the appropriate SpecForge callbacks.
10
- # It acts as a bridge between RSpec's notification system and SpecForge's callback system.
11
- #
12
- class Listener
13
- include Singleton
14
-
15
- #
16
- # Handles RSpec notifications for passing examples
17
- #
18
- # @param notification [RSpec::Core::Notifications::ExampleNotification]
19
- # The notification object
20
- #
21
- def example_passed(notification)
22
- trigger_callback
23
- end
24
-
25
- #
26
- # Handles RSpec notifications for failing examples
27
- #
28
- # @param notification [RSpec::Core::Notifications::FailedExampleNotification]
29
- # The notification object
30
- #
31
- def example_failed(notification)
32
- trigger_callback
33
- end
34
-
35
- private
36
-
37
- #
38
- # Triggers the appropriate SpecForge callback with the complete context
39
- #
40
- # Retrieves the current example context stored during the RSpec execution, and passes
41
- # everything to the appropriate callback.
42
- #
43
- # @private
44
- #
45
- def trigger_callback
46
- context = Runner::State.current.to_h.slice(
47
- :forge, :spec, :expectation, :example_group, :example
48
- )
49
-
50
- Runner::Callbacks.after_expectation(*context.values)
51
- end
52
- end
53
- end
54
- end
@@ -1,58 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module SpecForge
4
- class Runner
5
- #
6
- # Manages metadata for RSpec example groups and examples
7
- #
8
- # This class provides methods for setting up correct metadata on RSpec groups
9
- # and examples, which enables proper error reporting and command-line rerun
10
- # instructions when tests fail.
11
- #
12
- # @example Setting metadata on an example group
13
- # Metadata.set_for_group(spec, expectation, example_group)
14
- #
15
- class Metadata
16
- class << self
17
- #
18
- # Updates the example group metadata for error reporting
19
- #
20
- # Sets the file path, line number, and location information in the
21
- # example group's metadata. This ensures that RSpec can generate the
22
- # proper command to rerun the failing tests.
23
- #
24
- # @param spec [SpecForge::Spec] The spec being tested
25
- # @param expectation [SpecForge::Spec::Expectation] The expectation being evaluated
26
- # @param example_group [RSpec::Core::ExampleGroup] The example group to update
27
- #
28
- def set_for_group(spec, expectation, example_group)
29
- metadata = {
30
- file_path: spec.file_path,
31
- absolute_file_path: spec.file_path,
32
- line_number: expectation.line_number,
33
- location: spec.file_path,
34
- rerun_file_path: "#{spec.file_name}:#{spec.name}:\"#{expectation.name}\""
35
- }
36
-
37
- example_group.metadata.merge!(metadata)
38
- end
39
-
40
- #
41
- # Updates the current example's metadata for error reporting
42
- #
43
- # Sets location information on the currently running example.
44
- # This helps RSpec generate more accurate error messages when
45
- # an exception occurs during test execution.
46
- #
47
- # @param spec [SpecForge::Spec] The spec being tested
48
- # @param expectation [SpecForge::Spec::Expectation] The expectation being evaluated
49
- #
50
- def set_for_example(spec, expectation)
51
- metadata = {location: "#{spec.file_path}:#{expectation.line_number}"}
52
-
53
- RSpec.current_example.metadata.merge!(metadata)
54
- end
55
- end
56
- end
57
- end
58
- end
@@ -1,98 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module SpecForge
4
- class Runner
5
- #
6
- # Maintains test execution state to prevent duplicate HTTP requests
7
- #
8
- # This singleton class captures and preserves references to the current test context,
9
- # including request and response objects that would otherwise be re-evaluated
10
- # when accessed after RSpec clears its memoized variables. It solves a specific
11
- # issue where accessing response data in after_expectation callbacks would
12
- # trigger duplicate HTTP requests.
13
- #
14
- class State < Struct.new(
15
- :forge, :spec, :expectation, :example_group, :example, :response, :request
16
- )
17
- include Singleton
18
-
19
- #
20
- # Returns the singleton instance representing the current test state
21
- #
22
- # @return [State] The current state instance
23
- #
24
- def self.current
25
- instance
26
- end
27
-
28
- #
29
- # Updates multiple attributes of the state at once
30
- #
31
- # @param attributes [Hash] A hash mapping attribute names to values
32
- #
33
- def self.set(attributes)
34
- attributes.each do |key, value|
35
- instance[key] = value
36
- end
37
- end
38
-
39
- #
40
- # Persists the current state to the context store if needed
41
- #
42
- # Only runs if the current expectation has a store_as directive
43
- #
44
- def self.persist
45
- return unless instance.expectation.store_as?
46
-
47
- instance.persist_to_store
48
- end
49
-
50
- #
51
- # Clears all state attributes
52
- #
53
- def self.clear
54
- instance.clear
55
- end
56
-
57
- ##########################################################################
58
-
59
- #
60
- # Clears all attributes in the state
61
- #
62
- def clear
63
- members.each { |key| self[key] = nil }
64
- end
65
-
66
- #
67
- # Persists the current test execution data to the context store
68
- #
69
- # Handles scope determination and stores request/response data
70
- # for later access via the store attribute
71
- #
72
- def persist_to_store
73
- id = expectation.store_as
74
- scope = :file
75
-
76
- # Remove the file prefix if it was explicitly provided
77
- id = id.delete_prefix("file.") if id.start_with?("file.")
78
-
79
- # Change scope to spec if desired
80
- if id.start_with?("spec.")
81
- id = id.delete_prefix("spec.")
82
- scope = :spec
83
- end
84
-
85
- SpecForge.context.store.set(
86
- id,
87
- scope:,
88
- request: request&.to_h,
89
- variables: SpecForge.context.variables.deep_dup,
90
- response:,
91
- headers: response&.headers,
92
- status: response&.status,
93
- body: response&.body
94
- )
95
- end
96
- end
97
- end
98
- end
@@ -1,75 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module SpecForge
4
- #
5
- # Handles the execution of specs through RSpec
6
- # Converts SpecForge specs into RSpec examples and runs them
7
- #
8
- class Runner
9
- class << self
10
- #
11
- # Prepares forge objects for test execution
12
- #
13
- # Loads the forge helper, registers factories, loads specs from files,
14
- # applies filtering, and returns ready-to-run forge objects.
15
- #
16
- # @param file_name [String, nil] Optional file name filter
17
- # @param spec_name [String, nil] Optional spec name filter
18
- # @param expectation_name [String, nil] Optional expectation name filter
19
- #
20
- # @return [Array<Forge>] Array of prepared forge objects
21
- #
22
- def prepare(file_name: nil, spec_name: nil, expectation_name: nil)
23
- load_forge_helper
24
-
25
- # Load factories
26
- Factory.load_and_register
27
-
28
- # Load the specs from their files and create forges from them
29
- forges = Loader.load_from_files.map { |f| Forge.new(*f) }
30
-
31
- # Filter out the specs and expectations
32
- forges = Filter.apply(forges, file_name:, spec_name:, expectation_name:)
33
-
34
- # Tell the user that we filtered if we did
35
- Filter.announce(forges, file_name:, spec_name:, expectation_name:)
36
-
37
- forges
38
- end
39
-
40
- #
41
- # Runs the prepared forges through RSpec
42
- #
43
- # Sets up the RSpec adapter and executes all tests, with optional
44
- # exit behavior for CLI usage.
45
- #
46
- # @param forges [Array<Forge>] The forge objects to run
47
- # @param exit_on_finish [Boolean] Whether to exit the process when complete
48
- # @param exit_on_failure [Boolean] Whether to exit the process if any test fails
49
- #
50
- # @return [Integer, nil] Exit status if exit_on_finish is false
51
- #
52
- def run(forges, **)
53
- Adapter.setup(forges)
54
- Adapter.run(**)
55
- end
56
-
57
- private
58
-
59
- def load_forge_helper
60
- forge_helper = SpecForge.forge_path.join("forge_helper.rb")
61
- require_relative forge_helper if File.exist?(forge_helper)
62
-
63
- # Validate in case anything was changed
64
- SpecForge.configuration.validate
65
- end
66
- end
67
- end
68
- end
69
-
70
- require_relative "runner/adapter"
71
- require_relative "runner/callbacks"
72
- require_relative "runner/debug_proxy"
73
- require_relative "runner/listener"
74
- require_relative "runner/metadata"
75
- require_relative "runner/state"
@@ -1,127 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module SpecForge
4
- class Spec
5
- class Expectation
6
- #
7
- # Represents the expected response constraints for an expectation
8
- #
9
- # A Constraint defines what the API response should look like,
10
- # including status code and body content with support for matchers.
11
- #
12
- # @example In code
13
- # constraint = Constraint.new(
14
- # status: 200,
15
- # headers: {response_header: "kind_of.string"},
16
- # json: {name: {"matcher.eq" => "John"}}
17
- # )
18
- #
19
- class Constraint < Data.define(:status, :headers, :json) # :xml, :html
20
- #
21
- # Creates a new constraint
22
- #
23
- # @param status [Integer, String] The expected HTTP status code, or reference to one
24
- # @param headers [Hash] The expected headers with matchers
25
- # @param json [Hash, Array] The expected JSON with matchers
26
- #
27
- # @return [Constraint] A new constraint instance
28
- #
29
- def initialize(status:, headers: {}, json: {})
30
- super(
31
- status: Attribute.from(status),
32
- headers: Attribute.from(headers),
33
- json: Attribute.from(json)
34
- )
35
- end
36
-
37
- #
38
- # Converts the constraint to a hash with resolved values
39
- #
40
- # @return [Hash] Hash representation with resolved values
41
- #
42
- def to_h
43
- super.transform_values(&:resolve)
44
- end
45
-
46
- #
47
- # Converts constraints to RSpec matchers for validation
48
- #
49
- # Transforms the defined constraints (status and JSON expectations) into
50
- # appropriate RSpec matchers that can be used in test expectations.
51
- # This method resolves all values and applies the appropriate matcher
52
- # conversions to create a complete expectation structure.
53
- #
54
- # @return [Hash] A hash containing resolved matchers
55
- #
56
- # @example
57
- # constraint = Constraint.new(status: 200, json: {name: "John"})
58
- # matchers = constraint.as_matchers
59
- # # => {status: eq(200), json: include("name" => eq("John"))}
60
- #
61
- def as_matchers
62
- {
63
- status: status.resolve_as_matcher,
64
- json: resolve_json_matcher,
65
- headers: resolve_hash_matcher(headers)
66
- }
67
- end
68
-
69
- #
70
- # Generates a human-readable description of what this constraint expects in the response
71
- #
72
- # Creates a description string for RSpec examples that clearly explains the expected
73
- # status code and JSON structure. This makes test output more informative and helps
74
- # developers understand what's being tested at a glance.
75
- #
76
- # @return [String] A human-readable description of the constraint expectations
77
- #
78
- # @example Status code with JSON object
79
- # constraint.description
80
- # # => "is expected to respond with \"200 OK\" and a JSON object that contains keys: \"id\", \"name\""
81
- #
82
- # @example Status code with JSON array
83
- # constraint.description
84
- # # => "is expected to respond with \"201 Created\" and a JSON array that contains 3 items"
85
- #
86
- def description
87
- description = "is expected to respond with"
88
-
89
- description += if status.is_a?(Attribute::Literal)
90
- " #{HTTP.status_code_to_description(status.input).in_quotes}"
91
- else
92
- " the expected status code"
93
- end
94
-
95
- size = json.size
96
-
97
- if Type.array?(json)
98
- description +=
99
- " and a JSON array that contains #{size} #{"item".pluralize(size)}"
100
- elsif Type.hash?(json) && size > 0
101
- keys = json.keys.join_map(", ", &:in_quotes)
102
-
103
- description +=
104
- " and a JSON object that contains #{"key".pluralize(size)}: #{keys}"
105
- end
106
-
107
- description
108
- end
109
-
110
- private
111
-
112
- def resolve_json_matcher
113
- case json
114
- when HashLike
115
- resolve_hash_matcher(json)
116
- else
117
- json.resolve_as_matcher
118
- end
119
- end
120
-
121
- def resolve_hash_matcher(hash)
122
- hash.transform_values(&:resolve_as_matcher).stringify_keys
123
- end
124
- end
125
- end
126
- end
127
- end
@@ -1,68 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module SpecForge
4
- class Spec
5
- #
6
- # Represents a single test expectation within a spec
7
- #
8
- # An Expectation defines what should be tested for a specific API request,
9
- # including the expected status code and response structure.
10
- #
11
- # @example YAML representation
12
- # - name: "Get user successfully"
13
- # expect:
14
- # status: 200
15
- # json:
16
- # name: kind_of.string
17
- #
18
- class Expectation < Data.define(
19
- :id, :name, :line_number,
20
- :debug, :store_as, :documentation, :constraints
21
- )
22
- #
23
- # @return [Boolean] True if debugging is enabled
24
- #
25
- attr_predicate :debug
26
-
27
- #
28
- # @return [Boolean] True if store_as is set
29
- #
30
- attr_predicate :store_as
31
-
32
- #
33
- # Creates a new expectation with constraints
34
- #
35
- # @param id [String] Unique identifier
36
- # @param name [String] Human-readable name
37
- # @param line_number [Integer] Line number in source
38
- # @param debug [Boolean] Whether to enable debugging
39
- # @param store_as [String] Unique Context::Store identifier
40
- # @param documentation [Boolean] Whether to include in documentation generation
41
- # @param expect [Hash] Expected constraints
42
- #
43
- # @return [Expectation] A new expectation instance
44
- #
45
- def initialize(id:, name:, line_number:, debug:, store_as:, expect:, documentation:)
46
- constraints = Constraint.new(**expect)
47
-
48
- super(id:, name:, line_number:, debug:, store_as:, documentation:, constraints:)
49
- end
50
-
51
- #
52
- # Converts the expectation to a hash representation
53
- #
54
- # @return [Hash] Hash representation
55
- #
56
- def to_h
57
- {
58
- name:,
59
- line_number:,
60
- debug:,
61
- expect: constraints.to_h
62
- }
63
- end
64
- end
65
- end
66
- end
67
-
68
- require_relative "expectation/constraint"