spec_forge 0.4.0 → 0.6.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 (71) hide show
  1. checksums.yaml +4 -4
  2. data/.standard.yml +4 -0
  3. data/CHANGELOG.md +145 -1
  4. data/README.md +49 -638
  5. data/flake.lock +3 -3
  6. data/flake.nix +8 -2
  7. data/lib/spec_forge/attribute/chainable.rb +208 -20
  8. data/lib/spec_forge/attribute/factory.rb +141 -12
  9. data/lib/spec_forge/attribute/faker.rb +64 -15
  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 +188 -13
  13. data/lib/spec_forge/attribute/parameterized.rb +45 -20
  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 +168 -66
  22. data/lib/spec_forge/backtrace_formatter.rb +26 -3
  23. data/lib/spec_forge/callbacks.rb +79 -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/init.rb +11 -1
  27. data/lib/spec_forge/cli/new.rb +54 -3
  28. data/lib/spec_forge/cli/run.rb +20 -0
  29. data/lib/spec_forge/cli.rb +16 -5
  30. data/lib/spec_forge/configuration.rb +94 -25
  31. data/lib/spec_forge/context/callbacks.rb +91 -0
  32. data/lib/spec_forge/context/global.rb +72 -0
  33. data/lib/spec_forge/context/store.rb +148 -0
  34. data/lib/spec_forge/context/variables.rb +91 -0
  35. data/lib/spec_forge/context.rb +36 -0
  36. data/lib/spec_forge/core_ext/rspec.rb +24 -4
  37. data/lib/spec_forge/error.rb +267 -113
  38. data/lib/spec_forge/factory.rb +33 -14
  39. data/lib/spec_forge/filter.rb +87 -0
  40. data/lib/spec_forge/forge.rb +170 -0
  41. data/lib/spec_forge/http/backend.rb +99 -29
  42. data/lib/spec_forge/http/client.rb +23 -13
  43. data/lib/spec_forge/http/request.rb +74 -62
  44. data/lib/spec_forge/http/verb.rb +79 -0
  45. data/lib/spec_forge/http.rb +105 -0
  46. data/lib/spec_forge/loader.rb +254 -0
  47. data/lib/spec_forge/matchers.rb +130 -0
  48. data/lib/spec_forge/normalizer/configuration.rb +24 -11
  49. data/lib/spec_forge/normalizer/constraint.rb +22 -9
  50. data/lib/spec_forge/normalizer/expectation.rb +31 -12
  51. data/lib/spec_forge/normalizer/factory.rb +24 -11
  52. data/lib/spec_forge/normalizer/factory_reference.rb +32 -13
  53. data/lib/spec_forge/normalizer/global_context.rb +88 -0
  54. data/lib/spec_forge/normalizer/spec.rb +39 -16
  55. data/lib/spec_forge/normalizer.rb +255 -41
  56. data/lib/spec_forge/runner/callbacks.rb +246 -0
  57. data/lib/spec_forge/runner/debug_proxy.rb +213 -0
  58. data/lib/spec_forge/runner/listener.rb +54 -0
  59. data/lib/spec_forge/runner/metadata.rb +58 -0
  60. data/lib/spec_forge/runner/state.rb +99 -0
  61. data/lib/spec_forge/runner.rb +133 -119
  62. data/lib/spec_forge/spec/expectation/constraint.rb +95 -20
  63. data/lib/spec_forge/spec/expectation.rb +43 -51
  64. data/lib/spec_forge/spec.rb +83 -96
  65. data/lib/spec_forge/type.rb +36 -4
  66. data/lib/spec_forge/version.rb +4 -1
  67. data/lib/spec_forge.rb +161 -76
  68. metadata +20 -5
  69. data/spec_forge/factories/user.yml +0 -4
  70. data/spec_forge/forge_helper.rb +0 -37
  71. data/spec_forge/specs/users.yml +0 -65
data/lib/spec_forge.rb CHANGED
@@ -16,92 +16,177 @@ require "singleton"
16
16
  require "thor"
17
17
  require "yaml"
18
18
 
19
+ #
20
+ # SpecForge: Write expressive API tests in YAML with the power of RSpec matchers
21
+ #
22
+ # SpecForge is a testing framework that allows writing API tests in a YAML format
23
+ # that reads like documentation. It combines the readability of YAML with the
24
+ # power of RSpec matchers, Faker data generation, and FactoryBot test objects.
25
+ #
26
+ # @example Basic spec in YAML
27
+ # get_user:
28
+ # path: /users/1
29
+ # expectations:
30
+ # - expect:
31
+ # status: 200
32
+ # json:
33
+ # name: kind_of.string
34
+ # email: /@/
35
+ #
36
+ # @example Running specs
37
+ # # Run all specs
38
+ # SpecForge.run
39
+ #
40
+ # # Run specific file
41
+ # SpecForge.run(file_name: "users")
42
+ #
43
+ # # Run specific spec
44
+ # SpecForge.run(file_name: "users", spec_name: "create_user")
45
+ #
46
+ module SpecForge
47
+ class << self
48
+ #
49
+ # Loads all factories and specs and runs the tests with optional filtering
50
+ #
51
+ # This is the main entry point for running SpecForge tests. It loads the
52
+ # forge_helper.rb file if it exists, configures the environment, loads
53
+ # factories and specs, and runs the tests through RSpec.
54
+ #
55
+ # @param file_name [String, nil] Optional name of spec file to run
56
+ # @param spec_name [String, nil] Optional name of spec to run
57
+ # @param expectation_name [String, nil] Optional name of expectation to run
58
+ #
59
+ def run(file_name: nil, spec_name: nil, expectation_name: nil)
60
+ # Load spec_helper.rb
61
+ forge_helper = SpecForge.forge_path.join("forge_helper.rb")
62
+ require_relative forge_helper if File.exist?(forge_helper)
63
+
64
+ # Validate in case anything was changed
65
+ configuration.validate
66
+
67
+ # Load factories
68
+ Factory.load_and_register
69
+
70
+ # Load the specs from their files and create forges from them
71
+ forges = Loader.load_from_files.map { |f| Forge.new(*f) }
72
+
73
+ # Filter out the specs and expectations
74
+ forges = Filter.apply(forges, file_name:, spec_name:, expectation_name:)
75
+
76
+ # Tell the user that we filtered if we did
77
+ Filter.announce(forges, file_name:, spec_name:, expectation_name:)
78
+
79
+ # Define and run everything
80
+ Runner.define(forges)
81
+ Runner.run
82
+ end
83
+
84
+ #
85
+ # Returns the directory root for the working directory
86
+ #
87
+ # @return [Pathname] The root directory path
88
+ #
89
+ def root
90
+ @root ||= Pathname.pwd
91
+ end
92
+
93
+ #
94
+ # Returns SpecForge's working directory
95
+ #
96
+ # @return [Pathname] The spec_forge directory path
97
+ #
98
+ def forge_path
99
+ @forge_path ||= root.join("spec_forge")
100
+ end
101
+
102
+ #
103
+ # Returns SpecForge's configuration
104
+ #
105
+ # @return [Configuration] The current configuration
106
+ #
107
+ def configuration
108
+ @configuration ||= Configuration.new
109
+ end
110
+
111
+ #
112
+ # Yields SpecForge's configuration to a block for modification
113
+ #
114
+ # @yield [config] Block that receives the configuration object
115
+ # @yieldparam config [Configuration] The configuration to modify
116
+ #
117
+ # @return [Configuration] The updated configuration
118
+ #
119
+ def configure(&block)
120
+ block&.call(configuration)
121
+ configuration
122
+ end
123
+
124
+ #
125
+ # Returns a backtrace cleaner configured for SpecForge
126
+ #
127
+ # Creates and configures an ActiveSupport::BacktraceCleaner to improve
128
+ # error messages by removing unnecessary lines and root paths.
129
+ #
130
+ # @return [ActiveSupport::BacktraceCleaner] The configured backtrace cleaner
131
+ #
132
+ def backtrace_cleaner
133
+ @backtrace_cleaner ||= begin
134
+ root = "#{SpecForge.root}/"
135
+
136
+ cleaner = ActiveSupport::BacktraceCleaner.new
137
+ cleaner.add_filter { |line| line.delete_prefix(root) }
138
+ cleaner.add_silencer { |line| /rubygems|backtrace_cleaner/.match?(line) }
139
+ cleaner
140
+ end
141
+ end
142
+
143
+ #
144
+ # Returns the current execution context
145
+ #
146
+ # @return [Context] The current context object
147
+ #
148
+ def context
149
+ @context ||= Context.new
150
+ end
151
+
152
+ #
153
+ # Registers a callback for a specific test lifecycle event
154
+ # Allows custom code execution at specific points during test execution
155
+ #
156
+ # @param name [Symbol, String] A unique identifier for this callback
157
+ # @yield A block to execute when the callback is triggered
158
+ # @yieldparam context [Object] An object containing context-specific state data, depending
159
+ # on which hook the callback is triggered from.
160
+ #
161
+ # @return [Proc] The registered callback
162
+ #
163
+ # @example Registering a custom debug handler
164
+ # SpecForge.register_callback(:clean_database) do |context|
165
+ # DatabaseCleaner.clean
166
+ # end
167
+ #
168
+ def register_callback(name, &)
169
+ Callbacks.register(name, &)
170
+ end
171
+ end
172
+ end
173
+
19
174
  require_relative "spec_forge/attribute"
20
175
  require_relative "spec_forge/backtrace_formatter"
176
+ require_relative "spec_forge/callbacks"
21
177
  require_relative "spec_forge/cli"
22
178
  require_relative "spec_forge/configuration"
179
+ require_relative "spec_forge/context"
23
180
  require_relative "spec_forge/core_ext"
24
181
  require_relative "spec_forge/error"
25
182
  require_relative "spec_forge/factory"
183
+ require_relative "spec_forge/filter"
184
+ require_relative "spec_forge/forge"
26
185
  require_relative "spec_forge/http"
186
+ require_relative "spec_forge/loader"
187
+ require_relative "spec_forge/matchers"
27
188
  require_relative "spec_forge/normalizer"
28
189
  require_relative "spec_forge/runner"
29
190
  require_relative "spec_forge/spec"
30
191
  require_relative "spec_forge/type"
31
192
  require_relative "spec_forge/version"
32
-
33
- module SpecForge
34
- #
35
- # Loads all factories and specs located in "path", then runs all of the specs
36
- #
37
- # @param path [String] The file path that contains factories and specs
38
- #
39
- def self.run(file_name: nil, spec_name: nil, expectation_name: nil)
40
- path = SpecForge.forge
41
-
42
- # Initialize
43
- forge_helper = path.join("forge_helper.rb")
44
- require_relative forge_helper if File.exist?(forge_helper)
45
-
46
- # Validate
47
- configuration.validate
48
-
49
- # Prepare
50
- Factory.load_and_register
51
- Spec.load_and_define(file_name:, spec_name:, expectation_name:)
52
-
53
- # Run
54
- Runner.run
55
- end
56
-
57
- #
58
- # Returns the directory root for the working directory
59
- #
60
- # @return [Pathname]
61
- #
62
- def self.root
63
- @root ||= Pathname.pwd
64
- end
65
-
66
- #
67
- # Returns SpecForge's working directory
68
- #
69
- # @return [Pathname]
70
- #
71
- def self.forge
72
- @forge ||= root.join("spec_forge")
73
- end
74
-
75
- #
76
- # Returns SpecForge's configuration
77
- #
78
- # @return [Config]
79
- #
80
- def self.configuration
81
- @configuration ||= Configuration.new
82
- end
83
-
84
- #
85
- # Yields SpecForge's configuration to a block
86
- #
87
- def self.configure(&block)
88
- block&.call(configuration)
89
- configuration
90
- end
91
-
92
- #
93
- # Returns a backtrace cleaner configured for SpecForge
94
- #
95
- # @return [ActiveSupport::BacktraceCleaner]
96
- #
97
- def self.backtrace_cleaner
98
- @backtrace_cleaner ||= begin
99
- root = "#{SpecForge.root}/"
100
-
101
- cleaner = ActiveSupport::BacktraceCleaner.new
102
- cleaner.add_filter { |line| line.delete_prefix(root) }
103
- cleaner.add_silencer { |line| /rubygems|backtrace_cleaner/.match?(line) }
104
- cleaner
105
- end
106
- end
107
- end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: spec_forge
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.4.0
4
+ version: 0.6.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Bryan
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2025-02-23 00:00:00.000000000 Z
11
+ date: 2025-03-24 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activesupport
@@ -163,6 +163,7 @@ files:
163
163
  - lib/spec_forge/attribute/chainable.rb
164
164
  - lib/spec_forge/attribute/factory.rb
165
165
  - lib/spec_forge/attribute/faker.rb
166
+ - lib/spec_forge/attribute/global.rb
166
167
  - lib/spec_forge/attribute/literal.rb
167
168
  - lib/spec_forge/attribute/matcher.rb
168
169
  - lib/spec_forge/attribute/parameterized.rb
@@ -170,9 +171,11 @@ files:
170
171
  - lib/spec_forge/attribute/resolvable.rb
171
172
  - lib/spec_forge/attribute/resolvable_array.rb
172
173
  - lib/spec_forge/attribute/resolvable_hash.rb
174
+ - lib/spec_forge/attribute/store.rb
173
175
  - lib/spec_forge/attribute/transform.rb
174
176
  - lib/spec_forge/attribute/variable.rb
175
177
  - lib/spec_forge/backtrace_formatter.rb
178
+ - lib/spec_forge/callbacks.rb
176
179
  - lib/spec_forge/cli.rb
177
180
  - lib/spec_forge/cli/actions.rb
178
181
  - lib/spec_forge/cli/command.rb
@@ -180,23 +183,38 @@ files:
180
183
  - lib/spec_forge/cli/new.rb
181
184
  - lib/spec_forge/cli/run.rb
182
185
  - lib/spec_forge/configuration.rb
186
+ - lib/spec_forge/context.rb
187
+ - lib/spec_forge/context/callbacks.rb
188
+ - lib/spec_forge/context/global.rb
189
+ - lib/spec_forge/context/store.rb
190
+ - lib/spec_forge/context/variables.rb
183
191
  - lib/spec_forge/core_ext.rb
184
192
  - lib/spec_forge/core_ext/rspec.rb
185
193
  - lib/spec_forge/error.rb
186
194
  - lib/spec_forge/factory.rb
195
+ - lib/spec_forge/filter.rb
196
+ - lib/spec_forge/forge.rb
187
197
  - lib/spec_forge/http.rb
188
198
  - lib/spec_forge/http/backend.rb
189
199
  - lib/spec_forge/http/client.rb
190
200
  - lib/spec_forge/http/request.rb
191
201
  - lib/spec_forge/http/verb.rb
202
+ - lib/spec_forge/loader.rb
203
+ - lib/spec_forge/matchers.rb
192
204
  - lib/spec_forge/normalizer.rb
193
205
  - lib/spec_forge/normalizer/configuration.rb
194
206
  - lib/spec_forge/normalizer/constraint.rb
195
207
  - lib/spec_forge/normalizer/expectation.rb
196
208
  - lib/spec_forge/normalizer/factory.rb
197
209
  - lib/spec_forge/normalizer/factory_reference.rb
210
+ - lib/spec_forge/normalizer/global_context.rb
198
211
  - lib/spec_forge/normalizer/spec.rb
199
212
  - lib/spec_forge/runner.rb
213
+ - lib/spec_forge/runner/callbacks.rb
214
+ - lib/spec_forge/runner/debug_proxy.rb
215
+ - lib/spec_forge/runner/listener.rb
216
+ - lib/spec_forge/runner/metadata.rb
217
+ - lib/spec_forge/runner/state.rb
200
218
  - lib/spec_forge/spec.rb
201
219
  - lib/spec_forge/spec/expectation.rb
202
220
  - lib/spec_forge/spec/expectation/constraint.rb
@@ -205,9 +223,6 @@ files:
205
223
  - lib/templates/forge_helper.tt
206
224
  - lib/templates/new_factory.tt
207
225
  - lib/templates/new_spec.tt
208
- - spec_forge/factories/user.yml
209
- - spec_forge/forge_helper.rb
210
- - spec_forge/specs/users.yml
211
226
  homepage: https://github.com/itsthedevman/spec_forge
212
227
  licenses:
213
228
  - MIT
@@ -1,4 +0,0 @@
1
- user:
2
- class: User
3
- attributes:
4
- attribute: value
@@ -1,37 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- ## Using Rails? Uncomment to load your app
4
- # ENV["RAILS_ENV"] ||= "test"
5
- # require_relative "../config/environment"
6
-
7
- ## Not using Rails? Load anything you need here
8
- # Dir[SpecForge.root.join("lib", "my_api", "models", "**/*.rb")].sort.each { |path| require path }
9
-
10
- ## Using RSpec? Uncomment to use your existing configurations
11
- # require_relative "../spec/spec_helper"
12
-
13
- SpecForge.configure do |config|
14
- ## Base URL prefix for all API requests. All test paths will be appended to this URL
15
- config.base_url = "http://localhost:3000"
16
-
17
- ## Default request headers - commonly used for authentication and content negotiation
18
- api_token = ENV.fetch("API_TOKEN", "")
19
- config.headers = {
20
- "Authorization" => "Bearer #{api_token}"
21
- }
22
-
23
- ## Default query parameters - useful for API keys or additional request context
24
- # config.query = {api_token:}
25
-
26
- ## Factory configuration options
27
- ##
28
- ## Enable/disable automatic factory discovery. When enabled, SpecForge will automatically
29
- ## load factories from FactoryBot's default paths. Note: Factories defined in
30
- ## "spec_forge/factories" are always loaded regardless of this setting.
31
- # config.factories.auto_discover = false # Default: true
32
-
33
- ##
34
- ## Additional paths, relative to the project folder, for discovering FactoryBot factories
35
- ## By default, FactoryBot looks in "spec/factories" and "test/factories"
36
- # config.factories.paths += ["custom/factories/path"]
37
- end
@@ -1,65 +0,0 @@
1
- index_users:
2
- url: /users
3
- expectations:
4
- - expect:
5
- status: 200
6
-
7
- show_user:
8
- url: /users/{id}
9
- expectations:
10
- - query:
11
- id: -1
12
- expect:
13
- status: 404
14
- - query:
15
- id: 1
16
- expect:
17
- status: 200
18
- json:
19
- name: kind_of.string
20
- email: /\w+@example\.com/i
21
-
22
- create_user:
23
- url: /users
24
- method: post
25
- expectations:
26
- - expect:
27
- status: 400
28
- - expect:
29
- status: 200
30
- json:
31
- name: variables.name
32
- role: variables.role
33
- variables:
34
- name: faker.name.name
35
- role: user
36
- body:
37
- name: variables.name
38
-
39
- update_user:
40
- url: /users/{id}
41
- method: patch
42
- query:
43
- id: 1
44
- variables:
45
- number:
46
- faker.number.between:
47
- from: 100000
48
- to: 999999
49
- expectations:
50
- - expect:
51
- status: 200
52
- json:
53
- name: kind_of.string
54
- number: kind_of.integer
55
- body:
56
- number: variables.number
57
-
58
- destroy_user:
59
- url: /users/{id}
60
- method: delete
61
- query:
62
- id: 1
63
- expectations:
64
- - expect:
65
- status: 200