spec_forge 0.6.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 (83) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +112 -2
  3. data/README.md +133 -8
  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/callbacks.rb +9 -0
  8. data/lib/spec_forge/cli/docs/generate.rb +72 -0
  9. data/lib/spec_forge/cli/docs.rb +92 -0
  10. data/lib/spec_forge/cli/init.rb +39 -7
  11. data/lib/spec_forge/cli/new.rb +13 -3
  12. data/lib/spec_forge/cli/run.rb +12 -4
  13. data/lib/spec_forge/cli/serve.rb +155 -0
  14. data/lib/spec_forge/cli.rb +14 -6
  15. data/lib/spec_forge/configuration.rb +2 -2
  16. data/lib/spec_forge/context/store.rb +23 -40
  17. data/lib/spec_forge/core_ext/array.rb +27 -0
  18. data/lib/spec_forge/documentation/builder.rb +383 -0
  19. data/lib/spec_forge/documentation/document/operation.rb +47 -0
  20. data/lib/spec_forge/documentation/document/parameter.rb +22 -0
  21. data/lib/spec_forge/documentation/document/request_body.rb +24 -0
  22. data/lib/spec_forge/documentation/document/response.rb +39 -0
  23. data/lib/spec_forge/documentation/document/response_body.rb +27 -0
  24. data/lib/spec_forge/documentation/document.rb +48 -0
  25. data/lib/spec_forge/documentation/generators/base.rb +81 -0
  26. data/lib/spec_forge/documentation/generators/openapi/base.rb +100 -0
  27. data/lib/spec_forge/documentation/generators/openapi/error_formatter.rb +149 -0
  28. data/lib/spec_forge/documentation/generators/openapi/v3_0.rb +65 -0
  29. data/lib/spec_forge/documentation/generators/openapi.rb +59 -0
  30. data/lib/spec_forge/documentation/generators.rb +17 -0
  31. data/lib/spec_forge/documentation/loader/cache.rb +138 -0
  32. data/lib/spec_forge/documentation/loader.rb +159 -0
  33. data/lib/spec_forge/documentation/openapi/base.rb +33 -0
  34. data/lib/spec_forge/documentation/openapi/v3_0/example.rb +44 -0
  35. data/lib/spec_forge/documentation/openapi/v3_0/media_type.rb +42 -0
  36. data/lib/spec_forge/documentation/openapi/v3_0/operation.rb +175 -0
  37. data/lib/spec_forge/documentation/openapi/v3_0/response.rb +65 -0
  38. data/lib/spec_forge/documentation/openapi/v3_0/schema.rb +80 -0
  39. data/lib/spec_forge/documentation/openapi/v3_0/tag.rb +71 -0
  40. data/lib/spec_forge/documentation/openapi.rb +23 -0
  41. data/lib/spec_forge/documentation.rb +27 -0
  42. data/lib/spec_forge/error.rb +17 -0
  43. data/lib/spec_forge/factory.rb +2 -2
  44. data/lib/spec_forge/filter.rb +3 -4
  45. data/lib/spec_forge/forge.rb +5 -4
  46. data/lib/spec_forge/http/backend.rb +2 -0
  47. data/lib/spec_forge/http/request.rb +14 -3
  48. data/lib/spec_forge/loader.rb +14 -24
  49. data/lib/spec_forge/normalizer/default.rb +51 -0
  50. data/lib/spec_forge/normalizer/definition.rb +248 -0
  51. data/lib/spec_forge/normalizer/validators.rb +99 -0
  52. data/lib/spec_forge/normalizer.rb +356 -199
  53. data/lib/spec_forge/normalizers/_shared.yml +74 -0
  54. data/lib/spec_forge/normalizers/configuration.yml +23 -0
  55. data/lib/spec_forge/normalizers/constraint.yml +8 -0
  56. data/lib/spec_forge/normalizers/expectation.yml +47 -0
  57. data/lib/spec_forge/normalizers/factory.yml +12 -0
  58. data/lib/spec_forge/normalizers/factory_reference.yml +15 -0
  59. data/lib/spec_forge/normalizers/global_context.yml +28 -0
  60. data/lib/spec_forge/normalizers/spec.yml +50 -0
  61. data/lib/spec_forge/runner/adapter.rb +183 -0
  62. data/lib/spec_forge/runner/debug_proxy.rb +3 -3
  63. data/lib/spec_forge/runner/state.rb +4 -5
  64. data/lib/spec_forge/runner.rb +40 -124
  65. data/lib/spec_forge/spec/expectation/constraint.rb +13 -5
  66. data/lib/spec_forge/spec/expectation.rb +7 -3
  67. data/lib/spec_forge/spec.rb +13 -58
  68. data/lib/spec_forge/version.rb +1 -1
  69. data/lib/spec_forge.rb +30 -23
  70. data/lib/templates/openapi.yml.tt +22 -0
  71. data/lib/templates/redoc.html.tt +28 -0
  72. data/lib/templates/swagger.html.tt +59 -0
  73. metadata +92 -14
  74. data/lib/spec_forge/normalizer/configuration.rb +0 -90
  75. data/lib/spec_forge/normalizer/constraint.rb +0 -60
  76. data/lib/spec_forge/normalizer/expectation.rb +0 -105
  77. data/lib/spec_forge/normalizer/factory.rb +0 -78
  78. data/lib/spec_forge/normalizer/factory_reference.rb +0 -85
  79. data/lib/spec_forge/normalizer/global_context.rb +0 -88
  80. data/lib/spec_forge/normalizer/spec.rb +0 -97
  81. /data/lib/templates/{forge_helper.tt → forge_helper.rb.tt} +0 -0
  82. /data/lib/templates/{new_factory.tt → new_factory.yml.tt} +0 -0
  83. /data/lib/templates/{new_spec.tt → new_spec.yml.tt} +0 -0
@@ -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,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
@@ -40,7 +40,7 @@ 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
@@ -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?
@@ -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