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,92 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "docs/generate"
4
+
5
+ module SpecForge
6
+ class CLI
7
+ #
8
+ # Command for generating OpenAPI documentation from SpecForge tests
9
+ #
10
+ # Runs tests and extracts endpoint data to create OpenAPI specifications.
11
+ # Uses intelligent caching to avoid unnecessary test re-execution when
12
+ # specs haven't changed.
13
+ #
14
+ # @example Generate documentation
15
+ # spec_forge docs
16
+ #
17
+ # @example Generate with fresh test run
18
+ # spec_forge docs --fresh
19
+ #
20
+ class Docs < Command
21
+ include Docs::Generate
22
+
23
+ #
24
+ # Valid file formats for documentation output
25
+ #
26
+ # Supported formats include YAML variants (yml, yaml) and JSON.
27
+ # Used for validation when users specify the --format option.
28
+ #
29
+ # @api private
30
+ #
31
+ VALID_FORMATS = %w[yml yaml json].freeze
32
+
33
+ command_name "docs"
34
+ syntax "docs"
35
+ summary "Generate OpenAPI documentation from test results"
36
+
37
+ description <<~DESC
38
+ Generate OpenAPI documentation from test results.
39
+
40
+ Uses caching to avoid re-running tests unless specs
41
+ have changed. Output format can be YAML or JSON.
42
+ DESC
43
+
44
+ example "docs",
45
+ "Generates OpenAPI specifications from your tests using smart caching"
46
+
47
+ example "docs --fresh",
48
+ "Forces test re-execution and regenerates OpenAPI specs ignoring cache"
49
+
50
+ example "docs --format json",
51
+ "Generates OpenAPI specifications in JSON format instead of YAML"
52
+
53
+ example "docs --output ./build/api.yml",
54
+ "Generates OpenAPI specs to a custom file path"
55
+
56
+ example "docs --skip-validation",
57
+ "Generates documentation without validating the OpenAPI specification"
58
+
59
+ option "--fresh", "Re-run all tests ignoring cache"
60
+ option "--format=FORMAT", "Output format: yml/yaml or json (default: yml)"
61
+ option "--output=PATH", "Full file path for generated documentation"
62
+ option "--skip-validation", "Skip OpenAPI specification validation during generation"
63
+
64
+ #
65
+ # Generates OpenAPI documentation from tests
66
+ #
67
+ # Runs all SpecForge tests and creates OpenAPI specifications from the
68
+ # successful test results. This is the main entry point for the docs workflow.
69
+ #
70
+ # @return [void]
71
+ #
72
+ def call
73
+ # spec_forge/openapi/generated
74
+ generated_path = SpecForge.openapi_path.join("generated")
75
+ actions.empty_directory(generated_path, verbose: false)
76
+ actions.empty_directory(generated_path.join(".cache"), verbose: false)
77
+
78
+ file_path = generate_documentation
79
+
80
+ puts <<~STRING
81
+
82
+ ========================================
83
+ 🎉 Success!
84
+ ========================================
85
+
86
+ Your OpenAPI specification is valid and ready to use.
87
+ Output written to: #{file_path.relative_path_from(SpecForge.forge_path)}
88
+ STRING
89
+ end
90
+ end
91
+ end
92
+ end
@@ -11,21 +11,53 @@ module SpecForge
11
11
  class Init < Command
12
12
  command_name "init"
13
13
  syntax "init"
14
- summary "Initializes directory structure and configuration files"
14
+ summary "Set up your SpecForge project (creates folders and config files)"
15
+
16
+ description <<~DESC
17
+ Creates the SpecForge project structure.
18
+
19
+ Sets up:
20
+ • spec_forge/specs/ for test files
21
+ • spec_forge/factories/ for test data (optional)
22
+ • spec_forge/openapi/ for documentation config (optional)
23
+ • forge_helper.rb for configuration
24
+ DESC
25
+
26
+ option "--skip-openapi", "Skip generating the \"openapi\" directory"
27
+ option "--skip-factories", "Skip generating the \"factories\" directory"
15
28
 
16
29
  #
17
30
  # Creates the "spec_forge", "spec_forge/factories", and "spec_forge/specs" directories
18
31
  # Also creates the "spec_forge.rb" initialization file
19
32
  #
20
33
  def call
34
+ initialize_forge
35
+ initialize_openapi unless options.skip_openapi
36
+ end
37
+
38
+ private
39
+
40
+ def initialize_forge
21
41
  base_path = SpecForge.forge_path
22
- actions.empty_directory "#{base_path}/factories"
23
- actions.empty_directory "#{base_path}/specs"
42
+ actions.empty_directory(base_path.join("specs"))
43
+ actions.empty_directory(base_path.join("factories")) unless options.skip_factories
44
+ actions.template("forge_helper.rb.tt", base_path.join("forge_helper.rb"))
45
+ end
46
+
47
+ def initialize_openapi
48
+ # spec_forge/openapi
49
+ openapi_path = SpecForge.openapi_path
50
+ actions.empty_directory(openapi_path)
51
+
52
+ # spec_forge/openapi/config
53
+ config_path = openapi_path.join("config")
54
+
55
+ actions.empty_directory(config_path)
56
+ actions.empty_directory(config_path.join("paths")) # openapi/config/paths
57
+ actions.empty_directory(config_path.join("components")) # openapi/config/components
24
58
 
25
- actions.template(
26
- "forge_helper.tt",
27
- SpecForge.root.join(base_path, "forge_helper.rb")
28
- )
59
+ # openapi/config/openapi.yml
60
+ actions.template("openapi.yml.tt", config_path.join("openapi.yml"))
29
61
  end
30
62
  end
31
63
  end
@@ -13,7 +13,17 @@ module SpecForge
13
13
  #
14
14
  class New < Command
15
15
  command_name "new"
16
- summary "Create a new spec or factory"
16
+ summary "Create new test specs or data factories"
17
+
18
+ description <<~DESC
19
+ Generate new files from templates.
20
+
21
+ Types:
22
+ • spec - Creates YAML test files with common patterns
23
+ • factory - Creates FactoryBot factories for test data
24
+
25
+ Files are created in the appropriate spec_forge/ subdirectory.
26
+ DESC
17
27
 
18
28
  syntax "new <type> <name>"
19
29
 
@@ -51,7 +61,7 @@ module SpecForge
51
61
 
52
62
  def create_new_spec(name)
53
63
  actions.template(
54
- "new_spec.tt",
64
+ "new_spec.yml.tt",
55
65
  SpecForge.forge_path.join("specs", "#{name}.yml"),
56
66
  context: Proxy.new(name).call
57
67
  )
@@ -59,7 +69,7 @@ module SpecForge
59
69
 
60
70
  def create_new_factory(name)
61
71
  actions.template(
62
- "new_factory.tt",
72
+ "new_factory.yml.tt",
63
73
  SpecForge.forge_path.join("factories", "#{name}.yml"),
64
74
  context: Proxy.new(name).call
65
75
  )
@@ -21,8 +21,18 @@ module SpecForge
21
21
  command_name "run"
22
22
  syntax "run [target]"
23
23
 
24
- summary "Runs specs loaded from spec_forge/specs/"
25
- 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
26
36
 
27
37
  example "spec_forge run",
28
38
  "Run all specs in spec_forge/specs/"
@@ -39,8 +49,6 @@ module SpecForge
39
49
  example "spec_forge run users:create_user:\"POST /users - Create Admin\"",
40
50
  "Run the specific expectation named \"Create Admin\""
41
51
 
42
- # option "-n", "--no-docs", "Do not generate OpenAPI documentation on completion"
43
-
44
52
  #
45
53
  # Loads and runs all specs, or a subset of specs based on the provided arguments
46
54
  #
@@ -0,0 +1,156 @@
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
+ #
22
+ # Valid file formats for documentation output
23
+ #
24
+ # Supported formats include YAML variants (yml, yaml) and JSON.
25
+ # Used for validation when users specify the --format option.
26
+ #
27
+ # @api private
28
+ #
29
+ VALID_FORMATS = %w[yml yaml json].freeze
30
+
31
+ command_name "serve"
32
+ syntax "serve"
33
+ summary "Start a local server to preview your API documentation"
34
+ description <<~DESC
35
+ Generate documentation and start a local preview server.
36
+
37
+ Combines docs generation with a web interface. Choose between
38
+ Swagger UI or Redoc for viewing the documentation.
39
+ DESC
40
+
41
+ example "serve",
42
+ "Generates docs (if needed) and starts documentation server at localhost:8080"
43
+
44
+ example "serve --fresh",
45
+ "Re-runs tests, regenerates docs, and starts the documentation server"
46
+
47
+ example "serve --ui redoc",
48
+ "Starts server with Redoc interface instead of Swagger UI"
49
+
50
+ example "serve --port 3001",
51
+ "Starts documentation server on port 3001"
52
+
53
+ example "serve --fresh --ui redoc --port 3001",
54
+ "Re-runs tests and serves fresh docs with Redoc on custom port"
55
+
56
+ # Generation options
57
+ option "--fresh", "Re-run all tests before starting server"
58
+ option "--format=FORMAT", "Output format: yml/yaml or json (default: yml)"
59
+ option "--skip-validation", "Skip OpenAPI specification validation during generation"
60
+
61
+ # Server options
62
+ option "--ui=UI", "Documentation interface: swagger or redoc (default: swagger)"
63
+ option "--port=PORT", "Port to serve documentation on (default: 8080)"
64
+
65
+ aliases :s
66
+
67
+ #
68
+ # Generates documentation and starts a local web server
69
+ #
70
+ # Creates OpenAPI documentation from tests and serves it through a local
71
+ # HTTP server with either Swagger UI or Redoc interface for easy viewing.
72
+ #
73
+ # @return [void]
74
+ #
75
+ def call
76
+ server_path = SpecForge.openapi_path.join("server")
77
+ actions.empty_directory(server_path, verbose: false) # spec_forge/openapi/server
78
+
79
+ # Generate and copy the OpenAPI spec file
80
+ file_name = generate_and_copy_openapi_spec
81
+
82
+ # Determine which template file to use
83
+ template_name =
84
+ if options.ui == "redoc"
85
+ "redoc.html.tt"
86
+ else
87
+ "swagger.html.tt"
88
+ end
89
+
90
+ # Remove the index if it exists
91
+ index_path = server_path.join("index.html")
92
+ index_path.delete if index_path.exist?
93
+
94
+ # Generate index.html
95
+ actions.template(
96
+ template_name,
97
+ index_path,
98
+ context: Proxy.new(spec_url: file_name).call,
99
+ verbose: false
100
+ )
101
+
102
+ # And serve it!
103
+ port = options.port || 8080
104
+ server = WEBrick::HTTPServer.new(
105
+ Port: port,
106
+ DocumentRoot: server_path
107
+ )
108
+
109
+ puts <<~STRING
110
+ ========================================
111
+ 🚀 SpecForge Documentation Server
112
+ ========================================
113
+ Server running at: http://localhost:#{port}
114
+ Press Ctrl+C to stop
115
+ ========================================
116
+ STRING
117
+
118
+ trap("INT") { server.shutdown }
119
+ server.start
120
+ end
121
+
122
+ private
123
+
124
+ def generate_and_copy_openapi_spec
125
+ server_path = SpecForge.openapi_path.join("server")
126
+
127
+ file_path = generate_documentation
128
+
129
+ file_name = file_path.basename
130
+ path = server_path.join(file_name)
131
+
132
+ # If the file already exists, delete it
133
+ # This ensures we always have the latest spec
134
+ path.delete if path.exist?
135
+
136
+ actions.copy_file(file_path, path, verbose: false)
137
+
138
+ file_name
139
+ end
140
+
141
+ #
142
+ # Helper class for passing template variables to Thor templates
143
+ #
144
+ class Proxy < Struct.new(:spec_url)
145
+ #
146
+ # Returns a binding for use in templates
147
+ #
148
+ # @return [Binding] A binding containing template variables
149
+ #
150
+ def call
151
+ binding
152
+ end
153
+ end
154
+ end
155
+ end
156
+ end
@@ -2,18 +2,17 @@
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
10
12
  #
11
13
  # Command-line interface for SpecForge that provides the overall command structure
12
14
  # and entry point for the CLI functionality.
13
15
  #
14
- # @example Running the default command
15
- # SpecForge::CLI.new.run
16
- #
17
16
  # @example Running a specific command
18
17
  # # From command line: spec_forge init
19
18
  #
@@ -23,7 +22,7 @@ module SpecForge
23
22
  #
24
23
  # @return [Array<SpecForge::CLI::Command>] All available commands
25
24
  #
26
- COMMANDS = [Init, New, Run].freeze
25
+ COMMANDS = [Docs, Init, New, Run, Serve].freeze
27
26
 
28
27
  #
29
28
  # Runs the CLI application, setting up program information and registering commands
@@ -31,11 +30,20 @@ module SpecForge
31
30
  def run
32
31
  program :name, "SpecForge"
33
32
  program :version, SpecForge::VERSION
34
- 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
35
43
 
36
44
  register_commands
37
45
 
38
- default_command :run
46
+ default_command :help
39
47
 
40
48
  run!
41
49
  end
@@ -5,7 +5,7 @@ module SpecForge
5
5
  # Configuration container for SpecForge settings
6
6
  # Defines default values and validation for all configuration options
7
7
  #
8
- class Configuration < Struct.new(:base_url, :headers, :query, :factories, :on_debug)
8
+ class Configuration < Struct.new(:base_url, :headers, :query, :factories, :on_debug_proc)
9
9
  #
10
10
  # Manages factory configuration settings
11
11
  # Controls auto-discovery behavior and custom factory paths
@@ -40,11 +40,11 @@ module SpecForge
40
40
  # @return [Configuration] A new configuration instance with defaults
41
41
  #
42
42
  def initialize
43
- config = Normalizer.default_configuration
43
+ config = Normalizer.default(:configuration)
44
44
 
45
45
  config[:base_url] = "http://localhost:3000"
46
46
  config[:factories] = Factories.new
47
- config[:on_debug] = Runner::DebugProxy.default
47
+ config[:on_debug_proc] = Runner::DebugProxy.default
48
48
 
49
49
  super(**config)
50
50
  end
@@ -58,7 +58,7 @@ module SpecForge
58
58
  # @api private
59
59
  #
60
60
  def validate
61
- output = Normalizer.normalize_configuration!(to_h)
61
+ output = Normalizer.normalize!(to_h, using: :configuration)
62
62
 
63
63
  # In case any value was set to `nil`
64
64
  self.base_url = output[:base_url] if base_url.blank?
@@ -68,6 +68,15 @@ module SpecForge
68
68
  self
69
69
  end
70
70
 
71
+ def on_debug(&block)
72
+ self.on_debug_proc = block
73
+ end
74
+
75
+ def on_debug=(block)
76
+ warn("SpecForge::Configuration#on_debug= is deprecated. Use #on_debug instead")
77
+ self.on_debug_proc = block
78
+ end
79
+
71
80
  #
72
81
  # Recursively converts the configuration to a hash representation
73
82
  #
@@ -108,11 +117,6 @@ module SpecForge
108
117
  #
109
118
  # @return [Proc] The registered callback
110
119
  #
111
- # @example Registering a custom debug handler
112
- # SpecForge.configure do |config|
113
- # config.register_callback(:on_debug) { binding.pry }
114
- # end
115
- #
116
120
  # @example Cleaning database after each test
117
121
  # SpecForge.configure do |config|
118
122
  # config.register_callback(:after_expectation) do
@@ -19,63 +19,46 @@ module SpecForge
19
19
  #
20
20
  class Store
21
21
  #
22
- # Represents a single stored entry with request, variables, and response data
22
+ # Represents a stored entry containing arbitrary data from test execution
23
23
  #
24
- # Entries are immutable once created and contain a deep-frozen
25
- # snapshot of the test state at the time of storage.
24
+ # Entries are created during test execution to store custom data that can be
25
+ # accessed in subsequent tests. Unlike the original rigid Data structure, this
26
+ # OpenStruct-based approach allows storing any key-value pairs, making it perfect
27
+ # for complex test scenarios that need custom configuration, metadata, or
28
+ # computed values.
26
29
  #
27
- # @example Accessing stored entry data
28
- # entry = store["user_creation"]
29
- # entry.status # => 201
30
- # entry.body.id # => 42
30
+ # @example Storing custom configuration data
31
+ # SpecForge.context.store.set(
32
+ # "app_config",
33
+ # api_version: "v2.1",
34
+ # feature_flags: { advanced_search: true }
35
+ # )
31
36
  #
32
- class Entry < Data.define(:scope, :request, :variables, :response)
37
+ # @example Accessing stored data in tests
38
+ # headers:
39
+ # X-API-Version: store.app_config.api_version
40
+ # query:
41
+ # search_enabled: store.app_config.feature_flags.advanced_search
42
+ #
43
+ class Entry < OpenStruct
33
44
  #
34
- # Creates a new immutable store entry
45
+ # Creates a new store entry
35
46
  #
36
- # @param request [Hash] The HTTP request that was executed
37
- # @param variables [Hash] Variables from the test context
38
- # @param response [Hash] The HTTP response received
39
47
  # @param scope [Symbol] Scope of this entry, either :file or :spec
40
48
  #
41
- # @return [Entry] A new immutable entry instance
49
+ # @return [Entry] A new entry instance
42
50
  #
43
- def initialize(request:, variables:, response:, scope: :file)
44
- request = request.deep_freeze
45
- variables = variables.deep_freeze
46
- response = response.deep_freeze
47
-
51
+ def initialize(scope: :file, **)
48
52
  super
49
53
  end
50
54
 
51
- #
52
- # Shorthand accessor for the HTTP status code
53
- #
54
- # @return [Integer] The response status code
55
- #
56
- def status = response[:status]
57
-
58
- #
59
- # Shorthand accessor for the response body
60
- #
61
- # @return [Hash, Array, String] The parsed response body
62
- #
63
- def body = response[:body]
64
-
65
- #
66
- # Shorthand accessor for the response headers
67
- #
68
- # @return [Hash] The response headers
69
- #
70
- def headers = response[:headers]
71
-
72
55
  #
73
56
  # Returns all available methods that can be called
74
57
  #
75
58
  # @return [Array] The method names
76
59
  #
77
60
  def available_methods
78
- members + [:status, :body, :headers]
61
+ @table.keys
79
62
  end
80
63
  end
81
64
 
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ #
4
+ # Extensions to Ruby's Array class for SpecForge functionality
5
+ #
6
+ # Adds utility methods used throughout SpecForge for array manipulation
7
+ # and data processing.
8
+ #
9
+ class Array
10
+ #
11
+ # Merges an array of hashes into a single hash
12
+ #
13
+ # Performs a deep merge on each hash in the array, combining them
14
+ # into a single hash with all keys and values.
15
+ #
16
+ # @return [Hash] A hash containing the merged contents of all hashes in the array
17
+ #
18
+ # @example Merging an array of hashes
19
+ # [{a: 1}, {b: 2}, {a: 3}].to_merged_h
20
+ # # => {a: 3, b: 2}
21
+ #
22
+ def to_merged_h
23
+ each_with_object({}) do |hash, output|
24
+ output.deep_merge!(hash)
25
+ end
26
+ end
27
+ end