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.
Files changed (116) hide show
  1. checksums.yaml +4 -4
  2. data/.standard.yml +3 -3
  3. data/CHANGELOG.md +217 -2
  4. data/README.md +162 -25
  5. data/flake.lock +3 -3
  6. data/flake.nix +11 -5
  7. data/lib/spec_forge/attribute/chainable.rb +208 -20
  8. data/lib/spec_forge/attribute/factory.rb +92 -15
  9. data/lib/spec_forge/attribute/faker.rb +62 -13
  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 +186 -11
  13. data/lib/spec_forge/attribute/parameterized.rb +45 -12
  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 +166 -66
  22. data/lib/spec_forge/backtrace_formatter.rb +26 -3
  23. data/lib/spec_forge/callbacks.rb +88 -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/docs/generate.rb +72 -0
  27. data/lib/spec_forge/cli/docs.rb +92 -0
  28. data/lib/spec_forge/cli/init.rb +51 -9
  29. data/lib/spec_forge/cli/new.rb +67 -6
  30. data/lib/spec_forge/cli/run.rb +32 -4
  31. data/lib/spec_forge/cli/serve.rb +155 -0
  32. data/lib/spec_forge/cli.rb +26 -7
  33. data/lib/spec_forge/configuration.rb +96 -24
  34. data/lib/spec_forge/context/callbacks.rb +91 -0
  35. data/lib/spec_forge/context/global.rb +72 -0
  36. data/lib/spec_forge/context/store.rb +131 -0
  37. data/lib/spec_forge/context/variables.rb +91 -0
  38. data/lib/spec_forge/context.rb +36 -0
  39. data/lib/spec_forge/core_ext/array.rb +27 -0
  40. data/lib/spec_forge/core_ext/rspec.rb +22 -4
  41. data/lib/spec_forge/documentation/builder.rb +383 -0
  42. data/lib/spec_forge/documentation/document/operation.rb +47 -0
  43. data/lib/spec_forge/documentation/document/parameter.rb +22 -0
  44. data/lib/spec_forge/documentation/document/request_body.rb +24 -0
  45. data/lib/spec_forge/documentation/document/response.rb +39 -0
  46. data/lib/spec_forge/documentation/document/response_body.rb +27 -0
  47. data/lib/spec_forge/documentation/document.rb +48 -0
  48. data/lib/spec_forge/documentation/generators/base.rb +81 -0
  49. data/lib/spec_forge/documentation/generators/openapi/base.rb +100 -0
  50. data/lib/spec_forge/documentation/generators/openapi/error_formatter.rb +149 -0
  51. data/lib/spec_forge/documentation/generators/openapi/v3_0.rb +65 -0
  52. data/lib/spec_forge/documentation/generators/openapi.rb +59 -0
  53. data/lib/spec_forge/documentation/generators.rb +17 -0
  54. data/lib/spec_forge/documentation/loader/cache.rb +138 -0
  55. data/lib/spec_forge/documentation/loader.rb +159 -0
  56. data/lib/spec_forge/documentation/openapi/base.rb +33 -0
  57. data/lib/spec_forge/documentation/openapi/v3_0/example.rb +44 -0
  58. data/lib/spec_forge/documentation/openapi/v3_0/media_type.rb +42 -0
  59. data/lib/spec_forge/documentation/openapi/v3_0/operation.rb +175 -0
  60. data/lib/spec_forge/documentation/openapi/v3_0/response.rb +65 -0
  61. data/lib/spec_forge/documentation/openapi/v3_0/schema.rb +80 -0
  62. data/lib/spec_forge/documentation/openapi/v3_0/tag.rb +71 -0
  63. data/lib/spec_forge/documentation/openapi.rb +23 -0
  64. data/lib/spec_forge/documentation.rb +27 -0
  65. data/lib/spec_forge/error.rb +284 -113
  66. data/lib/spec_forge/factory.rb +35 -16
  67. data/lib/spec_forge/filter.rb +86 -0
  68. data/lib/spec_forge/forge.rb +171 -0
  69. data/lib/spec_forge/http/backend.rb +101 -29
  70. data/lib/spec_forge/http/client.rb +23 -13
  71. data/lib/spec_forge/http/request.rb +85 -62
  72. data/lib/spec_forge/http/verb.rb +79 -0
  73. data/lib/spec_forge/http.rb +105 -0
  74. data/lib/spec_forge/loader.rb +244 -0
  75. data/lib/spec_forge/matchers.rb +130 -0
  76. data/lib/spec_forge/normalizer/default.rb +51 -0
  77. data/lib/spec_forge/normalizer/definition.rb +248 -0
  78. data/lib/spec_forge/normalizer/validators.rb +99 -0
  79. data/lib/spec_forge/normalizer.rb +486 -115
  80. data/lib/spec_forge/normalizers/_shared.yml +74 -0
  81. data/lib/spec_forge/normalizers/configuration.yml +23 -0
  82. data/lib/spec_forge/normalizers/constraint.yml +8 -0
  83. data/lib/spec_forge/normalizers/expectation.yml +47 -0
  84. data/lib/spec_forge/normalizers/factory.yml +12 -0
  85. data/lib/spec_forge/normalizers/factory_reference.yml +15 -0
  86. data/lib/spec_forge/normalizers/global_context.yml +28 -0
  87. data/lib/spec_forge/normalizers/spec.yml +50 -0
  88. data/lib/spec_forge/runner/adapter.rb +183 -0
  89. data/lib/spec_forge/runner/callbacks.rb +246 -0
  90. data/lib/spec_forge/runner/debug_proxy.rb +213 -0
  91. data/lib/spec_forge/runner/listener.rb +54 -0
  92. data/lib/spec_forge/runner/metadata.rb +58 -0
  93. data/lib/spec_forge/runner/state.rb +98 -0
  94. data/lib/spec_forge/runner.rb +50 -125
  95. data/lib/spec_forge/spec/expectation/constraint.rb +100 -21
  96. data/lib/spec_forge/spec/expectation.rb +47 -51
  97. data/lib/spec_forge/spec.rb +50 -108
  98. data/lib/spec_forge/type.rb +36 -4
  99. data/lib/spec_forge/version.rb +4 -1
  100. data/lib/spec_forge.rb +168 -76
  101. data/lib/templates/openapi.yml.tt +22 -0
  102. data/lib/templates/redoc.html.tt +28 -0
  103. data/lib/templates/swagger.html.tt +59 -0
  104. metadata +109 -16
  105. data/lib/spec_forge/normalizer/configuration.rb +0 -77
  106. data/lib/spec_forge/normalizer/constraint.rb +0 -47
  107. data/lib/spec_forge/normalizer/expectation.rb +0 -86
  108. data/lib/spec_forge/normalizer/factory.rb +0 -65
  109. data/lib/spec_forge/normalizer/factory_reference.rb +0 -71
  110. data/lib/spec_forge/normalizer/spec.rb +0 -74
  111. data/spec_forge/factories/user.yml +0 -4
  112. data/spec_forge/forge_helper.rb +0 -48
  113. data/spec_forge/specs/users.yml +0 -65
  114. /data/lib/templates/{forge_helper.tt → forge_helper.rb.tt} +0 -0
  115. /data/lib/templates/{new_factory.tt → new_factory.yml.tt} +0 -0
  116. /data/lib/templates/{new_spec.tt → new_spec.yml.tt} +0 -0
@@ -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 "Runs specs loaded from spec_forge/specs/"
10
- description "Runs specs loaded from spec_forge/specs/. The optional target argument allows running specific files, specs, or expectations."
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
- # option "-n", "--no-docs", "Do not generate OpenAPI documentation on completion"
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
@@ -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
- # Runs the CLI
23
+ # @return [Array<SpecForge::CLI::Command>] All available commands
17
24
  #
18
- # @private
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, "Write expressive API tests in YAML with the power of RSpec matchers"
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 :run
46
+ default_command :help
28
47
 
29
48
  run!
30
49
  end
31
50
 
32
51
  #
33
- # Registers the commands with Commander
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
- class Configuration < Struct.new(:base_url, :headers, :query, :factories, :specs, :on_debug)
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
- def self.overlay_options(source, overlay)
16
- source.deep_merge(overlay) do |key, source_value, overlay_value|
17
- # If overlay has a populated value, use it
18
- if overlay_value.present? || overlay_value == false
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.default_configuration
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.normalize_configuration!(to_h)
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.except(:specs)
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