spec_forge 0.7.0 → 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 +139 -9
  3. data/README.md +125 -203
  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 +6 -0
  29. data/lib/spec_forge/cli.rb +6 -14
  30. data/lib/spec_forge/configuration.rb +212 -78
  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 +22 -6
  42. data/lib/spec_forge/documentation/openapi/v3_0/response.rb +29 -7
  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 -143
  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 -74
  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 -183
  124. data/lib/spec_forge/runner/callbacks.rb +0 -246
  125. data/lib/spec_forge/runner/debug_proxy.rb +0 -213
  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,213 +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
- -> { puts inspect }
34
- end
35
-
36
- # @return [RSpec::Forge] The current Forge that is being tested
37
- attr_reader :forge
38
-
39
- # @return [SpecForge::Spec] The current Spec that is being tested
40
- attr_reader :spec
41
-
42
- # @return [SpecForge::Spec::Expectation] The current expectation that is being tested
43
- attr_reader :expectation
44
-
45
- # @return [RSpec::ExampleGroup] The current RSpec example group
46
- attr_reader :example_group
47
-
48
- # @return [RSpec::Example] The current RSpec example that is running
49
- attr_reader :example
50
-
51
- # @return [Integer] The expected HTTP status code
52
- attr_reader :expected_status
53
-
54
- # @return [Object] The expected response body structure
55
- attr_reader :expected_json
56
-
57
- delegate_missing_to :@example_group
58
-
59
- #
60
- # Creates a new DebugProxy instance
61
- #
62
- # @param forge [SpecForge::Forge] The forge being tested
63
- # @param spec [SpecForge::Spec] The spec being tested
64
- # @param expectation [SpecForge::Spec::Expectation] The expectation being tested
65
- # @param example_group [RSpec::Core::ExampleGroup] The current example group
66
- #
67
- # @return [SpecForge::Runner::DebugProxy]
68
- #
69
- def initialize(forge, spec, expectation, example_group)
70
- @callback = SpecForge.configuration.on_debug
71
-
72
- @forge = forge
73
- @spec = spec
74
- @expectation = expectation
75
- @example_group = example_group
76
- @example = RSpec.current_example
77
-
78
- constraints = expectation.constraints
79
-
80
- @expected_status = constraints.status.resolved
81
- @expected_json = constraints.json.resolved
82
- end
83
-
84
- #
85
- # Triggers the debugging environment
86
- #
87
- # Displays available debugging contexts and executes the configured debug callback.
88
- # The callback runs in the context of this proxy, giving it access to all helper methods.
89
- #
90
- # @return [void]
91
- #
92
- def call
93
- puts <<~STRING
94
-
95
- Debug triggered for:
96
- > #{example.metadata[:rerun_file_path]} on line #{expectation.line_number}
97
-
98
- Available debugging contexts:
99
- - spec: Current spec details
100
- - expectation: Current expectation being tested
101
- - variables: Variables defined for this test
102
- - global: Global context shared across tests
103
- - store: Stored data from expectations
104
-
105
- Request & Response:
106
- - request: HTTP request details (method, url, headers, body)
107
- - response: HTTP response with headers, status and body
108
-
109
- Expectations:
110
- - expected_status: Expected HTTP status code
111
- - expected_json: Expected response body structure
112
-
113
- Matchers:
114
- - match_status: Matcher used to test status
115
- - match_json: Matcher used to test response body
116
-
117
- Helper objects:
118
- - http_client: The HTTP client used for the request
119
- - request_data: Raw request configuration data
120
- - example_group: Current RSpec example group
121
- - example: Current RSpec example
122
- - forge: Current file being tested
123
-
124
- 💡 Pro tips:
125
- - Type 'self' or 'inspect' for a pretty-printed JSON overview
126
- - Use 'to_h' for the hash representation
127
- - Access the shared context with 'SpecForge.context'
128
- STRING
129
-
130
- instance_exec(&@callback)
131
- end
132
-
133
- ##########################################################################
134
-
135
- #
136
- # Returns a hash representation of the global context
137
- #
138
- # @return [Hash] The global context with resolved variables
139
- #
140
- def global
141
- @global ||= SpecForge.context.global.to_h
142
- end
143
-
144
- #
145
- # Returns a hash representation of the variables in the current context
146
- #
147
- # Includes both spec-level and expectation-level variables combined
148
- # with values fully resolved.
149
- #
150
- # @return [Hash]
151
- #
152
- def variables
153
- @variables ||= SpecForge.context.variables
154
- end
155
-
156
- #
157
- # Returns a hash representation of the store context
158
- #
159
- # @return [Hash] The store context
160
- #
161
- def store
162
- @store ||= SpecForge.context.store.to_h
163
- end
164
-
165
- ##########################################################################
166
-
167
- #
168
- # Returns a hash representation of the test state
169
- #
170
- # Includes the spec, expectation, request, response, variables and global context.
171
- # RSpec matchers are converted to human-readable descriptions.
172
- #
173
- # @return [Hash]
174
- #
175
- def to_h
176
- spec_hash = spec.to_h.except(:expectations)
177
-
178
- expectation_hash = expectation.to_h
179
- expectation_hash[:expect][:json] = matchers_to_description(expectation_hash[:expect][:json])
180
-
181
- {
182
- global:,
183
- variables:,
184
- request: request.to_h,
185
- response: {
186
- status: response.status,
187
- body: response.body,
188
- headers: response.headers
189
- },
190
- expectation: expectation_hash,
191
- spec: spec_hash
192
- }
193
- end
194
-
195
- #
196
- # Returns a formatted JSON representation of the test state
197
- #
198
- # @return [String] Pretty-printed JSON of the test state
199
- #
200
- def inspect
201
- JSON.pretty_generate(to_h)
202
- end
203
-
204
- private
205
-
206
- def matchers_to_description(value)
207
- return value unless value.is_a?(RSpec::Matchers::BuiltIn::BaseMatcher)
208
-
209
- value.description
210
- end
211
- end
212
- end
213
- 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"