spec_forge 0.5.0 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.standard.yml +3 -3
- data/CHANGELOG.md +217 -2
- data/README.md +162 -25
- data/flake.lock +3 -3
- data/flake.nix +11 -5
- data/lib/spec_forge/attribute/chainable.rb +208 -20
- data/lib/spec_forge/attribute/factory.rb +92 -15
- data/lib/spec_forge/attribute/faker.rb +62 -13
- data/lib/spec_forge/attribute/global.rb +96 -0
- data/lib/spec_forge/attribute/literal.rb +15 -2
- data/lib/spec_forge/attribute/matcher.rb +186 -11
- data/lib/spec_forge/attribute/parameterized.rb +45 -12
- data/lib/spec_forge/attribute/regex.rb +55 -5
- data/lib/spec_forge/attribute/resolvable.rb +48 -5
- data/lib/spec_forge/attribute/resolvable_array.rb +62 -4
- data/lib/spec_forge/attribute/resolvable_hash.rb +62 -4
- data/lib/spec_forge/attribute/store.rb +65 -0
- data/lib/spec_forge/attribute/transform.rb +33 -5
- data/lib/spec_forge/attribute/variable.rb +37 -6
- data/lib/spec_forge/attribute.rb +166 -66
- data/lib/spec_forge/backtrace_formatter.rb +26 -3
- data/lib/spec_forge/callbacks.rb +88 -0
- data/lib/spec_forge/cli/actions.rb +27 -0
- data/lib/spec_forge/cli/command.rb +78 -24
- data/lib/spec_forge/cli/docs/generate.rb +72 -0
- data/lib/spec_forge/cli/docs.rb +92 -0
- data/lib/spec_forge/cli/init.rb +51 -9
- data/lib/spec_forge/cli/new.rb +67 -6
- data/lib/spec_forge/cli/run.rb +32 -4
- data/lib/spec_forge/cli/serve.rb +155 -0
- data/lib/spec_forge/cli.rb +26 -7
- data/lib/spec_forge/configuration.rb +96 -24
- data/lib/spec_forge/context/callbacks.rb +91 -0
- data/lib/spec_forge/context/global.rb +72 -0
- data/lib/spec_forge/context/store.rb +131 -0
- data/lib/spec_forge/context/variables.rb +91 -0
- data/lib/spec_forge/context.rb +36 -0
- data/lib/spec_forge/core_ext/array.rb +27 -0
- data/lib/spec_forge/core_ext/rspec.rb +22 -4
- data/lib/spec_forge/documentation/builder.rb +383 -0
- data/lib/spec_forge/documentation/document/operation.rb +47 -0
- data/lib/spec_forge/documentation/document/parameter.rb +22 -0
- data/lib/spec_forge/documentation/document/request_body.rb +24 -0
- data/lib/spec_forge/documentation/document/response.rb +39 -0
- data/lib/spec_forge/documentation/document/response_body.rb +27 -0
- data/lib/spec_forge/documentation/document.rb +48 -0
- data/lib/spec_forge/documentation/generators/base.rb +81 -0
- data/lib/spec_forge/documentation/generators/openapi/base.rb +100 -0
- data/lib/spec_forge/documentation/generators/openapi/error_formatter.rb +149 -0
- data/lib/spec_forge/documentation/generators/openapi/v3_0.rb +65 -0
- data/lib/spec_forge/documentation/generators/openapi.rb +59 -0
- data/lib/spec_forge/documentation/generators.rb +17 -0
- data/lib/spec_forge/documentation/loader/cache.rb +138 -0
- data/lib/spec_forge/documentation/loader.rb +159 -0
- data/lib/spec_forge/documentation/openapi/base.rb +33 -0
- data/lib/spec_forge/documentation/openapi/v3_0/example.rb +44 -0
- data/lib/spec_forge/documentation/openapi/v3_0/media_type.rb +42 -0
- data/lib/spec_forge/documentation/openapi/v3_0/operation.rb +175 -0
- data/lib/spec_forge/documentation/openapi/v3_0/response.rb +65 -0
- data/lib/spec_forge/documentation/openapi/v3_0/schema.rb +80 -0
- data/lib/spec_forge/documentation/openapi/v3_0/tag.rb +71 -0
- data/lib/spec_forge/documentation/openapi.rb +23 -0
- data/lib/spec_forge/documentation.rb +27 -0
- data/lib/spec_forge/error.rb +284 -113
- data/lib/spec_forge/factory.rb +35 -16
- data/lib/spec_forge/filter.rb +86 -0
- data/lib/spec_forge/forge.rb +171 -0
- data/lib/spec_forge/http/backend.rb +101 -29
- data/lib/spec_forge/http/client.rb +23 -13
- data/lib/spec_forge/http/request.rb +85 -62
- data/lib/spec_forge/http/verb.rb +79 -0
- data/lib/spec_forge/http.rb +105 -0
- data/lib/spec_forge/loader.rb +244 -0
- data/lib/spec_forge/matchers.rb +130 -0
- data/lib/spec_forge/normalizer/default.rb +51 -0
- data/lib/spec_forge/normalizer/definition.rb +248 -0
- data/lib/spec_forge/normalizer/validators.rb +99 -0
- data/lib/spec_forge/normalizer.rb +486 -115
- data/lib/spec_forge/normalizers/_shared.yml +74 -0
- data/lib/spec_forge/normalizers/configuration.yml +23 -0
- data/lib/spec_forge/normalizers/constraint.yml +8 -0
- data/lib/spec_forge/normalizers/expectation.yml +47 -0
- data/lib/spec_forge/normalizers/factory.yml +12 -0
- data/lib/spec_forge/normalizers/factory_reference.yml +15 -0
- data/lib/spec_forge/normalizers/global_context.yml +28 -0
- data/lib/spec_forge/normalizers/spec.yml +50 -0
- data/lib/spec_forge/runner/adapter.rb +183 -0
- data/lib/spec_forge/runner/callbacks.rb +246 -0
- data/lib/spec_forge/runner/debug_proxy.rb +213 -0
- data/lib/spec_forge/runner/listener.rb +54 -0
- data/lib/spec_forge/runner/metadata.rb +58 -0
- data/lib/spec_forge/runner/state.rb +98 -0
- data/lib/spec_forge/runner.rb +50 -125
- data/lib/spec_forge/spec/expectation/constraint.rb +100 -21
- data/lib/spec_forge/spec/expectation.rb +47 -51
- data/lib/spec_forge/spec.rb +50 -108
- data/lib/spec_forge/type.rb +36 -4
- data/lib/spec_forge/version.rb +4 -1
- data/lib/spec_forge.rb +168 -76
- data/lib/templates/openapi.yml.tt +22 -0
- data/lib/templates/redoc.html.tt +28 -0
- data/lib/templates/swagger.html.tt +59 -0
- metadata +109 -16
- data/lib/spec_forge/normalizer/configuration.rb +0 -77
- data/lib/spec_forge/normalizer/constraint.rb +0 -47
- data/lib/spec_forge/normalizer/expectation.rb +0 -86
- data/lib/spec_forge/normalizer/factory.rb +0 -65
- data/lib/spec_forge/normalizer/factory_reference.rb +0 -71
- data/lib/spec_forge/normalizer/spec.rb +0 -74
- data/spec_forge/factories/user.yml +0 -4
- data/spec_forge/forge_helper.rb +0 -48
- data/spec_forge/specs/users.yml +0 -65
- /data/lib/templates/{forge_helper.tt → forge_helper.rb.tt} +0 -0
- /data/lib/templates/{new_factory.tt → new_factory.yml.tt} +0 -0
- /data/lib/templates/{new_spec.tt → new_spec.yml.tt} +0 -0
data/lib/spec_forge/cli/run.rb
CHANGED
@@ -2,12 +2,37 @@
|
|
2
2
|
|
3
3
|
module SpecForge
|
4
4
|
class CLI
|
5
|
+
#
|
6
|
+
# Command for running SpecForge tests with filtering options
|
7
|
+
#
|
8
|
+
# @example Running all specs
|
9
|
+
# spec_forge run
|
10
|
+
#
|
11
|
+
# @example Running specific file
|
12
|
+
# spec_forge run users
|
13
|
+
#
|
14
|
+
# @example Running specific spec
|
15
|
+
# spec_forge run users:create_user
|
16
|
+
#
|
17
|
+
# @example Running specific expectation
|
18
|
+
# spec_forge run users:create_user:"POST /users"
|
19
|
+
#
|
5
20
|
class Run < Command
|
6
21
|
command_name "run"
|
7
22
|
syntax "run [target]"
|
8
23
|
|
9
|
-
summary "
|
10
|
-
|
24
|
+
summary "Execute your API tests with smart filtering options"
|
25
|
+
|
26
|
+
description <<~DESC
|
27
|
+
Execute API tests with filtering options.
|
28
|
+
|
29
|
+
Target formats:
|
30
|
+
• file_name - Run all specs in a file
|
31
|
+
• file:spec - Run specific spec
|
32
|
+
• file:spec:"expectation" - Run individual expectation
|
33
|
+
|
34
|
+
Uses RSpec for execution with detailed error reporting.
|
35
|
+
DESC
|
11
36
|
|
12
37
|
example "spec_forge run",
|
13
38
|
"Run all specs in spec_forge/specs/"
|
@@ -24,8 +49,9 @@ module SpecForge
|
|
24
49
|
example "spec_forge run users:create_user:\"POST /users - Create Admin\"",
|
25
50
|
"Run the specific expectation named \"Create Admin\""
|
26
51
|
|
27
|
-
#
|
28
|
-
|
52
|
+
#
|
53
|
+
# Loads and runs all specs, or a subset of specs based on the provided arguments
|
54
|
+
#
|
29
55
|
def call
|
30
56
|
return SpecForge.run if arguments.blank?
|
31
57
|
|
@@ -53,6 +79,8 @@ module SpecForge
|
|
53
79
|
# Example with name:
|
54
80
|
# "users:show_user:'GET /users/:id - Returns 404 due to missing user'"
|
55
81
|
#
|
82
|
+
# @private
|
83
|
+
#
|
56
84
|
def extract_filter(input)
|
57
85
|
# Note: Only split 3 because the expectation name can have colons in them.
|
58
86
|
file_name, spec_name, expectation_name = input.split(":", 3).map(&:strip)
|
@@ -0,0 +1,155 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module SpecForge
|
4
|
+
class CLI
|
5
|
+
#
|
6
|
+
# Command for generating and serving API documentation
|
7
|
+
#
|
8
|
+
# Combines documentation generation with a local web server to provide
|
9
|
+
# an easy way to view and interact with generated API documentation.
|
10
|
+
# Supports both Swagger UI and Redoc interfaces.
|
11
|
+
#
|
12
|
+
# @example Start documentation server
|
13
|
+
# spec_forge serve
|
14
|
+
#
|
15
|
+
# @example Serve with Redoc UI
|
16
|
+
# spec_forge serve --ui redoc
|
17
|
+
#
|
18
|
+
class Serve < Command
|
19
|
+
include Docs::Generate
|
20
|
+
#
|
21
|
+
# Valid file formats for documentation output
|
22
|
+
#
|
23
|
+
# Supported formats include YAML variants (yml, yaml) and JSON.
|
24
|
+
# Used for validation when users specify the --format option.
|
25
|
+
#
|
26
|
+
# @api private
|
27
|
+
#
|
28
|
+
VALID_FORMATS = %w[yml yaml json].freeze
|
29
|
+
|
30
|
+
command_name "serve"
|
31
|
+
syntax "serve"
|
32
|
+
summary "Start a local server to preview your API documentation"
|
33
|
+
description <<~DESC
|
34
|
+
Generate documentation and start a local preview server.
|
35
|
+
|
36
|
+
Combines docs generation with a web interface. Choose between
|
37
|
+
Swagger UI or Redoc for viewing the documentation.
|
38
|
+
DESC
|
39
|
+
|
40
|
+
example "serve",
|
41
|
+
"Generates docs (if needed) and starts documentation server at localhost:8080"
|
42
|
+
|
43
|
+
example "serve --fresh",
|
44
|
+
"Re-runs tests, regenerates docs, and starts the documentation server"
|
45
|
+
|
46
|
+
example "serve --ui redoc",
|
47
|
+
"Starts server with Redoc interface instead of Swagger UI"
|
48
|
+
|
49
|
+
example "serve --port 3001",
|
50
|
+
"Starts documentation server on port 3001"
|
51
|
+
|
52
|
+
example "serve --fresh --ui redoc --port 3001",
|
53
|
+
"Re-runs tests and serves fresh docs with Redoc on custom port"
|
54
|
+
|
55
|
+
# Generation options
|
56
|
+
option "--fresh", "Re-run all tests before starting server"
|
57
|
+
option "--format=FORMAT", "Output format: yml/yaml or json (default: yml)"
|
58
|
+
option "--skip-validation", "Skip OpenAPI specification validation during generation"
|
59
|
+
|
60
|
+
# Server options
|
61
|
+
option "--ui=UI", "Documentation interface: swagger or redoc (default: swagger)"
|
62
|
+
option "--port=PORT", "Port to serve documentation on (default: 8080)"
|
63
|
+
|
64
|
+
aliases :s
|
65
|
+
|
66
|
+
#
|
67
|
+
# Generates documentation and starts a local web server
|
68
|
+
#
|
69
|
+
# Creates OpenAPI documentation from tests and serves it through a local
|
70
|
+
# HTTP server with either Swagger UI or Redoc interface for easy viewing.
|
71
|
+
#
|
72
|
+
# @return [void]
|
73
|
+
#
|
74
|
+
def call
|
75
|
+
server_path = SpecForge.openapi_path.join("server")
|
76
|
+
actions.empty_directory(server_path, verbose: false) # spec_forge/openapi/server
|
77
|
+
|
78
|
+
# Generate and copy the OpenAPI spec file
|
79
|
+
file_name = generate_and_copy_openapi_spec
|
80
|
+
|
81
|
+
# Determine which template file to use
|
82
|
+
template_name =
|
83
|
+
if options.ui == "redoc"
|
84
|
+
"redoc.html.tt"
|
85
|
+
else
|
86
|
+
"swagger.html.tt"
|
87
|
+
end
|
88
|
+
|
89
|
+
# Remove the index if it exists
|
90
|
+
index_path = server_path.join("index.html")
|
91
|
+
index_path.delete if index_path.exist?
|
92
|
+
|
93
|
+
# Generate index.html
|
94
|
+
actions.template(
|
95
|
+
template_name,
|
96
|
+
index_path,
|
97
|
+
context: Proxy.new(spec_url: file_name).call,
|
98
|
+
verbose: false
|
99
|
+
)
|
100
|
+
|
101
|
+
# And serve it!
|
102
|
+
port = options.port || 8080
|
103
|
+
server = WEBrick::HTTPServer.new(
|
104
|
+
Port: port,
|
105
|
+
DocumentRoot: server_path
|
106
|
+
)
|
107
|
+
|
108
|
+
puts <<~STRING
|
109
|
+
========================================
|
110
|
+
🚀 SpecForge Documentation Server
|
111
|
+
========================================
|
112
|
+
Server running at: http://localhost:#{port}
|
113
|
+
Press Ctrl+C to stop
|
114
|
+
========================================
|
115
|
+
STRING
|
116
|
+
|
117
|
+
trap("INT") { server.shutdown }
|
118
|
+
server.start
|
119
|
+
end
|
120
|
+
|
121
|
+
private
|
122
|
+
|
123
|
+
def generate_and_copy_openapi_spec
|
124
|
+
server_path = SpecForge.openapi_path.join("server")
|
125
|
+
|
126
|
+
file_path = generate_documentation
|
127
|
+
|
128
|
+
file_name = file_path.basename
|
129
|
+
path = server_path.join(file_name)
|
130
|
+
|
131
|
+
# If the file already exists, delete it
|
132
|
+
# This ensures we always have the latest spec
|
133
|
+
path.delete if path.exist?
|
134
|
+
|
135
|
+
actions.copy_file(file_path, path, verbose: false)
|
136
|
+
|
137
|
+
file_name
|
138
|
+
end
|
139
|
+
|
140
|
+
#
|
141
|
+
# Helper class for passing template variables to Thor templates
|
142
|
+
#
|
143
|
+
class Proxy < Struct.new(:spec_url)
|
144
|
+
#
|
145
|
+
# Returns a binding for use in templates
|
146
|
+
#
|
147
|
+
# @return [Binding] A binding containing template variables
|
148
|
+
#
|
149
|
+
def call
|
150
|
+
binding
|
151
|
+
end
|
152
|
+
end
|
153
|
+
end
|
154
|
+
end
|
155
|
+
end
|
data/lib/spec_forge/cli.rb
CHANGED
@@ -2,35 +2,54 @@
|
|
2
2
|
|
3
3
|
require_relative "cli/actions"
|
4
4
|
require_relative "cli/command"
|
5
|
+
require_relative "cli/docs"
|
5
6
|
require_relative "cli/init"
|
6
7
|
require_relative "cli/new"
|
7
8
|
require_relative "cli/run"
|
9
|
+
require_relative "cli/serve"
|
8
10
|
|
9
11
|
module SpecForge
|
12
|
+
#
|
13
|
+
# Command-line interface for SpecForge that provides the overall command structure
|
14
|
+
# and entry point for the CLI functionality.
|
15
|
+
#
|
16
|
+
# @example Running a specific command
|
17
|
+
# # From command line: spec_forge init
|
18
|
+
#
|
10
19
|
class CLI
|
11
20
|
include Commander::Methods
|
12
21
|
|
13
|
-
COMMANDS = [Init, New, Run]
|
14
|
-
|
15
22
|
#
|
16
|
-
#
|
23
|
+
# @return [Array<SpecForge::CLI::Command>] All available commands
|
17
24
|
#
|
18
|
-
|
25
|
+
COMMANDS = [Docs, Init, New, Run, Serve].freeze
|
26
|
+
|
27
|
+
#
|
28
|
+
# Runs the CLI application, setting up program information and registering commands
|
19
29
|
#
|
20
30
|
def run
|
21
31
|
program :name, "SpecForge"
|
22
32
|
program :version, SpecForge::VERSION
|
23
|
-
program :description,
|
33
|
+
program :description, <<~DESC.strip
|
34
|
+
Write expressive API tests in YAML with the power of RSpec matchers.
|
35
|
+
|
36
|
+
Quick Start:
|
37
|
+
spec_forge init # Set up your project
|
38
|
+
spec_forge new spec users # Create your first test
|
39
|
+
spec_forge run # Execute tests
|
40
|
+
spec_forge docs # Generate API docs
|
41
|
+
spec_forge serve # Serve API docs locally
|
42
|
+
DESC
|
24
43
|
|
25
44
|
register_commands
|
26
45
|
|
27
|
-
default_command :
|
46
|
+
default_command :help
|
28
47
|
|
29
48
|
run!
|
30
49
|
end
|
31
50
|
|
32
51
|
#
|
33
|
-
# Registers the
|
52
|
+
# Registers the command classes with Commander
|
34
53
|
#
|
35
54
|
# @private
|
36
55
|
#
|
@@ -1,45 +1,64 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
module SpecForge
|
4
|
-
|
5
|
-
|
6
|
-
|
4
|
+
#
|
5
|
+
# Configuration container for SpecForge settings
|
6
|
+
# Defines default values and validation for all configuration options
|
7
|
+
#
|
8
|
+
class Configuration < Struct.new(:base_url, :headers, :query, :factories, :on_debug)
|
9
|
+
#
|
10
|
+
# Manages factory configuration settings
|
11
|
+
# Controls auto-discovery behavior and custom factory paths
|
12
|
+
#
|
13
|
+
# @example
|
14
|
+
# config.factories.auto_discover = false
|
15
|
+
# config.factories.paths += ["lib/factories"]
|
16
|
+
#
|
7
17
|
class Factories < Struct.new(:auto_discover, :paths)
|
18
|
+
#
|
19
|
+
# Creates reader methods that return boolean values
|
20
|
+
# Allows for checking configuration with predicate methods
|
21
|
+
#
|
8
22
|
attr_predicate :auto_discover, :paths
|
9
23
|
|
24
|
+
#
|
25
|
+
# Initializes a new Factories configuration
|
26
|
+
# Sets default values for auto-discovery and paths
|
27
|
+
#
|
28
|
+
# @param auto_discover [Boolean] Whether to auto-discover factories (default: true)
|
29
|
+
# @param paths [Array<String>] Additional paths to look for factories (default: [])
|
30
|
+
#
|
31
|
+
# @return [Factories] A new factories configuration instance
|
32
|
+
#
|
10
33
|
def initialize(auto_discover: true, paths: []) = super
|
11
34
|
end
|
12
35
|
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
overlay_value
|
20
|
-
# If source is nil and overlay exists (but wasn't "present"), use overlay
|
21
|
-
elsif source_value.nil? && !overlay_value.nil?
|
22
|
-
overlay_value
|
23
|
-
# Otherwise keep source value
|
24
|
-
else
|
25
|
-
source_value
|
26
|
-
end
|
27
|
-
end
|
28
|
-
end
|
29
|
-
|
36
|
+
#
|
37
|
+
# Initializes a new Configuration with default values
|
38
|
+
# Sets up the configuration structure including factory settings and debug proxy
|
39
|
+
#
|
40
|
+
# @return [Configuration] A new configuration instance with defaults
|
41
|
+
#
|
30
42
|
def initialize
|
31
|
-
config = Normalizer.
|
43
|
+
config = Normalizer.default(:configuration)
|
32
44
|
|
33
45
|
config[:base_url] = "http://localhost:3000"
|
34
46
|
config[:factories] = Factories.new
|
35
|
-
config[:specs] = RSpec.configuration
|
36
47
|
config[:on_debug] = Runner::DebugProxy.default
|
37
48
|
|
38
49
|
super(**config)
|
39
50
|
end
|
40
51
|
|
52
|
+
#
|
53
|
+
# Validates the configuration and applies normalization
|
54
|
+
# Ensures all required fields have values and applies defaults when needed
|
55
|
+
#
|
56
|
+
# @return [self] Returns self for method chaining
|
57
|
+
#
|
58
|
+
# @api private
|
59
|
+
#
|
41
60
|
def validate
|
42
|
-
output = Normalizer.
|
61
|
+
output = Normalizer.normalize!(to_h, using: :configuration)
|
43
62
|
|
44
63
|
# In case any value was set to `nil`
|
45
64
|
self.base_url = output[:base_url] if base_url.blank?
|
@@ -49,10 +68,63 @@ module SpecForge
|
|
49
68
|
self
|
50
69
|
end
|
51
70
|
|
71
|
+
#
|
72
|
+
# Recursively converts the configuration to a hash representation
|
73
|
+
#
|
74
|
+
# @return [Hash] Hash representation of the configuration
|
75
|
+
#
|
52
76
|
def to_h
|
53
|
-
hash = super
|
77
|
+
hash = super
|
54
78
|
hash[:factories] = hash[:factories].to_h
|
55
79
|
hash
|
56
80
|
end
|
81
|
+
|
82
|
+
#
|
83
|
+
# Returns the RSpec configuration object
|
84
|
+
# Provides access to RSpec's internal configuration for test customization
|
85
|
+
#
|
86
|
+
# @return [RSpec::Core::Configuration] RSpec's configuration object
|
87
|
+
#
|
88
|
+
# @example Setting formatter options
|
89
|
+
# SpecForge.configure do |config|
|
90
|
+
# config.specs.formatter = :documentation
|
91
|
+
# end
|
92
|
+
#
|
93
|
+
def specs
|
94
|
+
RSpec.configuration
|
95
|
+
end
|
96
|
+
|
97
|
+
alias_method :rspec, :specs
|
98
|
+
|
99
|
+
#
|
100
|
+
# Registers a callback for a specific test lifecycle event
|
101
|
+
# Allows custom code execution at specific points during test execution
|
102
|
+
#
|
103
|
+
# @param name [Symbol, String] The callback point to register for
|
104
|
+
# (:before_file, :after_expectation, etc.)
|
105
|
+
# @yield A block to execute when the callback is triggered
|
106
|
+
# @yieldparam context [Object] An object containing context-specific state data, depending
|
107
|
+
# on which hook the callback is triggered from.
|
108
|
+
#
|
109
|
+
# @return [Proc] The registered callback
|
110
|
+
#
|
111
|
+
# @example Registering a custom debug handler
|
112
|
+
# SpecForge.configure do |config|
|
113
|
+
# config.register_callback(:on_debug) { binding.pry }
|
114
|
+
# end
|
115
|
+
#
|
116
|
+
# @example Cleaning database after each test
|
117
|
+
# SpecForge.configure do |config|
|
118
|
+
# config.register_callback(:after_expectation) do
|
119
|
+
# DatabaseCleaner.clean
|
120
|
+
# end
|
121
|
+
# end
|
122
|
+
#
|
123
|
+
def register_callback(name, &)
|
124
|
+
Callbacks.register(name, &)
|
125
|
+
end
|
126
|
+
|
127
|
+
alias_method :define_callback, :register_callback
|
128
|
+
alias_method :callback, :register_callback
|
57
129
|
end
|
58
130
|
end
|
@@ -0,0 +1,91 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module SpecForge
|
4
|
+
class Context
|
5
|
+
#
|
6
|
+
# Manages user-defined callbacks grouped by lifecycle hook
|
7
|
+
#
|
8
|
+
# This class collects and organizes callbacks by their hook type
|
9
|
+
# (before_file, after_each, etc.) to support the test lifecycle.
|
10
|
+
# It ensures callbacks are properly categorized for execution.
|
11
|
+
#
|
12
|
+
# @example Creating callback groups
|
13
|
+
# callbacks = Context::Callbacks.new([
|
14
|
+
# {before_file: "setup_environment"},
|
15
|
+
# {after_each: "log_test_result"}
|
16
|
+
# ])
|
17
|
+
#
|
18
|
+
class Callbacks
|
19
|
+
#
|
20
|
+
# Creates a new callbacks collection
|
21
|
+
#
|
22
|
+
# @param callback_array [Array] Optional initial callbacks to register
|
23
|
+
#
|
24
|
+
# @return [Callbacks] A new callbacks collection
|
25
|
+
#
|
26
|
+
def initialize(callback_array = [])
|
27
|
+
set(callback_array)
|
28
|
+
end
|
29
|
+
|
30
|
+
#
|
31
|
+
# Updates the callbacks collection
|
32
|
+
#
|
33
|
+
# @param callback_array [Array] New callbacks to register
|
34
|
+
#
|
35
|
+
# @return [self]
|
36
|
+
#
|
37
|
+
def set(callback_array)
|
38
|
+
@inner = organize_callbacks_by_hook(callback_array)
|
39
|
+
self
|
40
|
+
end
|
41
|
+
|
42
|
+
#
|
43
|
+
# Returns the hash representation of callbacks
|
44
|
+
#
|
45
|
+
# @return [Hash] Callbacks organized by hook type
|
46
|
+
#
|
47
|
+
def to_h
|
48
|
+
@inner
|
49
|
+
end
|
50
|
+
|
51
|
+
#
|
52
|
+
# Executes all registered callbacks for a specific lifecycle hook
|
53
|
+
#
|
54
|
+
# @param hook_name [String, Symbol] The lifecycle hook (before_file, after_each, etc.)
|
55
|
+
# @param context [Hash] State data that will be converted to a structured object
|
56
|
+
# and passed to callbacks
|
57
|
+
#
|
58
|
+
def run(hook_name, context = {})
|
59
|
+
context = context.to_struct
|
60
|
+
|
61
|
+
@inner[hook_name].each do |callback_name|
|
62
|
+
SpecForge::Callbacks.run(callback_name, context)
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
private
|
67
|
+
|
68
|
+
#
|
69
|
+
# Organizes callbacks from an array to hash structure by hook type
|
70
|
+
# Groups callbacks like before_file, after_each, etc. for easier lookup
|
71
|
+
#
|
72
|
+
# @param callback_array [Array] The array of callbacks
|
73
|
+
#
|
74
|
+
# @return [Hash] Callbacks indexed by hook type
|
75
|
+
#
|
76
|
+
# @private
|
77
|
+
#
|
78
|
+
def organize_callbacks_by_hook(callback_array)
|
79
|
+
groups = Hash.new { |h, k| h[k] = Set.new }
|
80
|
+
|
81
|
+
callback_array.each_with_object(groups) do |callbacks, groups|
|
82
|
+
callbacks.each do |hook, name|
|
83
|
+
next if name.blank?
|
84
|
+
|
85
|
+
groups[hook].add(name)
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
@@ -0,0 +1,72 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module SpecForge
|
4
|
+
class Context
|
5
|
+
#
|
6
|
+
# Manages global state and variables at the spec file level.
|
7
|
+
#
|
8
|
+
# The Global class provides access to variables that are defined at the global level
|
9
|
+
# in a spec file and are accessible across all specs and expectations in a file.
|
10
|
+
# Unlike regular variables, global variables do not support overlaying - they maintain
|
11
|
+
# consistent values throughout test execution.
|
12
|
+
#
|
13
|
+
# @example Basic usage
|
14
|
+
# global = Global.new(variables: {api_version: "v2", environment: "test"})
|
15
|
+
#
|
16
|
+
# global.variables[:api_version] #=> "v2"
|
17
|
+
# global.variables[:environment] #=> "test"
|
18
|
+
#
|
19
|
+
# # Update global variables
|
20
|
+
# global.set(variables: {environment: "staging"})
|
21
|
+
# global.variables[:environment] #=> "staging"
|
22
|
+
# global.variables[:api_version] #=> nil
|
23
|
+
#
|
24
|
+
class Global
|
25
|
+
# @return [Context::Variables] The container for global variables
|
26
|
+
attr_reader :variables
|
27
|
+
|
28
|
+
# @return [Context::Callbacks] The container for callbacks
|
29
|
+
attr_reader :callbacks
|
30
|
+
|
31
|
+
#
|
32
|
+
# Creates a new Global context instance
|
33
|
+
#
|
34
|
+
# @param variables [Hash<Symbol, Object>] A hash of variable names and values
|
35
|
+
# @param callbacks [Array<Hash<Symbol, String>>] An array of callback hooks
|
36
|
+
#
|
37
|
+
# @return [Global] The new Global instance
|
38
|
+
#
|
39
|
+
def initialize(variables: {}, callbacks: [])
|
40
|
+
@variables = Variables.new(base: variables)
|
41
|
+
@callbacks = Callbacks.new(callbacks)
|
42
|
+
end
|
43
|
+
|
44
|
+
#
|
45
|
+
# Sets the global variables
|
46
|
+
#
|
47
|
+
# @param variables [Hash<Symbol, Object>] A hash of variable names and values
|
48
|
+
# @param callbacks [Array<Hash<Symbol, String>>] An array of callback hooks
|
49
|
+
#
|
50
|
+
# @return [self]
|
51
|
+
#
|
52
|
+
def set(variables: {}, callbacks: [])
|
53
|
+
@variables.set(base: variables)
|
54
|
+
@callbacks.set(callbacks)
|
55
|
+
|
56
|
+
self
|
57
|
+
end
|
58
|
+
|
59
|
+
#
|
60
|
+
# Returns a hash representation of the global context
|
61
|
+
#
|
62
|
+
# @return [Hash]
|
63
|
+
#
|
64
|
+
def to_h
|
65
|
+
{
|
66
|
+
variables: variables.to_h,
|
67
|
+
callbacks: callbacks.to_h
|
68
|
+
}
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|