spec_forge 0.6.0 → 0.7.1

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 (84) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +174 -8
  3. data/README.md +135 -10
  4. data/flake.lock +3 -3
  5. data/flake.nix +3 -3
  6. data/lib/spec_forge/attribute/factory.rb +1 -1
  7. data/lib/spec_forge/attribute/transform.rb +1 -1
  8. data/lib/spec_forge/callbacks.rb +9 -0
  9. data/lib/spec_forge/cli/docs/generate.rb +72 -0
  10. data/lib/spec_forge/cli/docs.rb +92 -0
  11. data/lib/spec_forge/cli/init.rb +39 -7
  12. data/lib/spec_forge/cli/new.rb +13 -3
  13. data/lib/spec_forge/cli/run.rb +12 -4
  14. data/lib/spec_forge/cli/serve.rb +156 -0
  15. data/lib/spec_forge/cli.rb +14 -6
  16. data/lib/spec_forge/configuration.rb +13 -9
  17. data/lib/spec_forge/context/store.rb +23 -40
  18. data/lib/spec_forge/core_ext/array.rb +27 -0
  19. data/lib/spec_forge/documentation/builder.rb +383 -0
  20. data/lib/spec_forge/documentation/document/operation.rb +47 -0
  21. data/lib/spec_forge/documentation/document/parameter.rb +22 -0
  22. data/lib/spec_forge/documentation/document/request_body.rb +24 -0
  23. data/lib/spec_forge/documentation/document/response.rb +39 -0
  24. data/lib/spec_forge/documentation/document/response_body.rb +27 -0
  25. data/lib/spec_forge/documentation/document.rb +48 -0
  26. data/lib/spec_forge/documentation/generators/base.rb +81 -0
  27. data/lib/spec_forge/documentation/generators/openapi/base.rb +100 -0
  28. data/lib/spec_forge/documentation/generators/openapi/error_formatter.rb +149 -0
  29. data/lib/spec_forge/documentation/generators/openapi/v3_0.rb +65 -0
  30. data/lib/spec_forge/documentation/generators/openapi.rb +59 -0
  31. data/lib/spec_forge/documentation/generators.rb +17 -0
  32. data/lib/spec_forge/documentation/loader/cache.rb +138 -0
  33. data/lib/spec_forge/documentation/loader.rb +159 -0
  34. data/lib/spec_forge/documentation/openapi/base.rb +33 -0
  35. data/lib/spec_forge/documentation/openapi/v3_0/example.rb +44 -0
  36. data/lib/spec_forge/documentation/openapi/v3_0/media_type.rb +42 -0
  37. data/lib/spec_forge/documentation/openapi/v3_0/operation.rb +175 -0
  38. data/lib/spec_forge/documentation/openapi/v3_0/response.rb +65 -0
  39. data/lib/spec_forge/documentation/openapi/v3_0/schema.rb +80 -0
  40. data/lib/spec_forge/documentation/openapi/v3_0/tag.rb +71 -0
  41. data/lib/spec_forge/documentation/openapi.rb +23 -0
  42. data/lib/spec_forge/documentation.rb +27 -0
  43. data/lib/spec_forge/error.rb +17 -0
  44. data/lib/spec_forge/factory.rb +2 -2
  45. data/lib/spec_forge/filter.rb +3 -4
  46. data/lib/spec_forge/forge.rb +5 -4
  47. data/lib/spec_forge/http/backend.rb +5 -0
  48. data/lib/spec_forge/http/request.rb +14 -3
  49. data/lib/spec_forge/loader.rb +14 -24
  50. data/lib/spec_forge/normalizer/default.rb +51 -0
  51. data/lib/spec_forge/normalizer/definition.rb +248 -0
  52. data/lib/spec_forge/normalizer/validators.rb +99 -0
  53. data/lib/spec_forge/normalizer.rb +356 -199
  54. data/lib/spec_forge/normalizers/_shared.yml +76 -0
  55. data/lib/spec_forge/normalizers/configuration.yml +23 -0
  56. data/lib/spec_forge/normalizers/constraint.yml +8 -0
  57. data/lib/spec_forge/normalizers/expectation.yml +47 -0
  58. data/lib/spec_forge/normalizers/factory.yml +12 -0
  59. data/lib/spec_forge/normalizers/factory_reference.yml +15 -0
  60. data/lib/spec_forge/normalizers/global_context.yml +28 -0
  61. data/lib/spec_forge/normalizers/spec.yml +50 -0
  62. data/lib/spec_forge/runner/adapter.rb +181 -0
  63. data/lib/spec_forge/runner/debug_proxy.rb +44 -42
  64. data/lib/spec_forge/runner/state.rb +4 -5
  65. data/lib/spec_forge/runner.rb +40 -124
  66. data/lib/spec_forge/spec/expectation/constraint.rb +13 -5
  67. data/lib/spec_forge/spec/expectation.rb +7 -3
  68. data/lib/spec_forge/spec.rb +13 -58
  69. data/lib/spec_forge/version.rb +1 -1
  70. data/lib/spec_forge.rb +30 -23
  71. data/lib/templates/openapi.yml.tt +22 -0
  72. data/lib/templates/redoc.html.tt +28 -0
  73. data/lib/templates/swagger.html.tt +59 -0
  74. metadata +92 -14
  75. data/lib/spec_forge/normalizer/configuration.rb +0 -90
  76. data/lib/spec_forge/normalizer/constraint.rb +0 -60
  77. data/lib/spec_forge/normalizer/expectation.rb +0 -105
  78. data/lib/spec_forge/normalizer/factory.rb +0 -78
  79. data/lib/spec_forge/normalizer/factory_reference.rb +0 -85
  80. data/lib/spec_forge/normalizer/global_context.rb +0 -88
  81. data/lib/spec_forge/normalizer/spec.rb +0 -97
  82. /data/lib/templates/{forge_helper.tt → forge_helper.rb.tt} +0 -0
  83. /data/lib/templates/{new_factory.tt → new_factory.yml.tt} +0 -0
  84. /data/lib/templates/{new_spec.tt → new_spec.yml.tt} +0 -0
@@ -0,0 +1,76 @@
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
+ aliases:
12
+ - base_path
13
+
14
+ url:
15
+ type: string
16
+ default: null
17
+ required: false
18
+ aliases:
19
+ - path
20
+
21
+ http_verb:
22
+ type: string
23
+ default: null # Do not default this to "GET". Leave it null. Seriously.
24
+ required: false
25
+ aliases:
26
+ - method
27
+ - http_method
28
+ validator: http_verb
29
+
30
+ headers:
31
+ type: hash
32
+ default: {}
33
+ required: false
34
+
35
+ query:
36
+ type:
37
+ - hash
38
+ - string
39
+ aliases:
40
+ - params
41
+ default: {}
42
+ required: false
43
+
44
+ body:
45
+ type:
46
+ - hash
47
+ - string
48
+ aliases:
49
+ - data
50
+ default: {}
51
+ required: false
52
+
53
+ variables:
54
+ type:
55
+ - hash
56
+ - string
57
+ default: {}
58
+ required: false
59
+
60
+ debug:
61
+ type: boolean
62
+ aliases:
63
+ - pry
64
+ - breakpoint
65
+ default: false
66
+ required: false
67
+
68
+ callback:
69
+ type: string
70
+ required: false
71
+ validator: callback
72
+
73
+ documentation:
74
+ type: boolean
75
+ required: false
76
+ 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_proc:
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,181 @@
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
+ case match_json
156
+ when Hash
157
+ match_json.each do |key, matcher|
158
+ expect(response.body).to include(key)
159
+
160
+ begin
161
+ expect(response.body[key]).to matcher
162
+ rescue RSpec::Expectations::ExpectationNotMetError => e
163
+ # Add the key that failed to the front of the error message
164
+ e.message.insert(0, "Key: #{key.in_quotes}\n")
165
+ raise e
166
+ end
167
+ end
168
+ else
169
+ expect(response.body).to match_json
170
+ end
171
+ end
172
+ end
173
+ end
174
+ end
175
+ end
176
+ end
177
+ end
178
+ end
179
+ end
180
+ end
181
+ end
@@ -30,7 +30,46 @@ module SpecForge
30
30
  # @return [Proc] The default debugging handler that outputs JSON state information
31
31
  #
32
32
  def self.default
33
- -> { puts inspect }
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
34
73
  end
35
74
 
36
75
  # @return [RSpec::Forge] The current Forge that is being tested
@@ -67,7 +106,7 @@ module SpecForge
67
106
  # @return [SpecForge::Runner::DebugProxy]
68
107
  #
69
108
  def initialize(forge, spec, expectation, example_group)
70
- @callback = SpecForge.configuration.on_debug
109
+ @callback = SpecForge.configuration.on_debug_proc
71
110
 
72
111
  @forge = forge
73
112
  @spec = spec
@@ -90,43 +129,6 @@ module SpecForge
90
129
  # @return [void]
91
130
  #
92
131
  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
132
  instance_exec(&@callback)
131
133
  end
132
134
 
@@ -179,14 +181,14 @@ module SpecForge
179
181
  expectation_hash[:expect][:json] = matchers_to_description(expectation_hash[:expect][:json])
180
182
 
181
183
  {
184
+ global:,
185
+ variables:,
186
+ request: request.to_h,
182
187
  response: {
183
188
  status: response.status,
184
189
  body: response.body,
185
190
  headers: response.headers
186
191
  },
187
- global:,
188
- variables:,
189
- request: request.to_h,
190
192
  expectation: expectation_hash,
191
193
  spec: spec_hash
192
194
  }
@@ -87,11 +87,10 @@ module SpecForge
87
87
  scope:,
88
88
  request: request&.to_h,
89
89
  variables: SpecForge.context.variables.deep_dup,
90
- response: {
91
- headers: response&.headers,
92
- status: response&.status,
93
- body: response&.body
94
- }
90
+ response:,
91
+ headers: response&.headers,
92
+ status: response&.status,
93
+ body: response&.body
95
94
  )
96
95
  end
97
96
  end