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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +112 -2
- data/README.md +133 -8
- data/flake.lock +3 -3
- data/flake.nix +3 -3
- data/lib/spec_forge/attribute/factory.rb +1 -1
- data/lib/spec_forge/callbacks.rb +9 -0
- 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 +39 -7
- data/lib/spec_forge/cli/new.rb +13 -3
- data/lib/spec_forge/cli/run.rb +12 -4
- data/lib/spec_forge/cli/serve.rb +155 -0
- data/lib/spec_forge/cli.rb +14 -6
- data/lib/spec_forge/configuration.rb +2 -2
- data/lib/spec_forge/context/store.rb +23 -40
- data/lib/spec_forge/core_ext/array.rb +27 -0
- 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 +17 -0
- data/lib/spec_forge/factory.rb +2 -2
- data/lib/spec_forge/filter.rb +3 -4
- data/lib/spec_forge/forge.rb +5 -4
- data/lib/spec_forge/http/backend.rb +2 -0
- data/lib/spec_forge/http/request.rb +14 -3
- data/lib/spec_forge/loader.rb +14 -24
- 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 +356 -199
- 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/debug_proxy.rb +3 -3
- data/lib/spec_forge/runner/state.rb +4 -5
- data/lib/spec_forge/runner.rb +40 -124
- data/lib/spec_forge/spec/expectation/constraint.rb +13 -5
- data/lib/spec_forge/spec/expectation.rb +7 -3
- data/lib/spec_forge/spec.rb +13 -58
- data/lib/spec_forge/version.rb +1 -1
- data/lib/spec_forge.rb +30 -23
- 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 +92 -14
- data/lib/spec_forge/normalizer/configuration.rb +0 -90
- data/lib/spec_forge/normalizer/constraint.rb +0 -60
- data/lib/spec_forge/normalizer/expectation.rb +0 -105
- data/lib/spec_forge/normalizer/factory.rb +0 -78
- data/lib/spec_forge/normalizer/factory_reference.rb +0 -85
- data/lib/spec_forge/normalizer/global_context.rb +0 -88
- data/lib/spec_forge/normalizer/spec.rb +0 -97
- /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
@@ -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,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,
|
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 :
|
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.
|
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.
|
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
|
22
|
+
# Represents a stored entry containing arbitrary data from test execution
|
23
23
|
#
|
24
|
-
# Entries are
|
25
|
-
#
|
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
|
28
|
-
#
|
29
|
-
#
|
30
|
-
#
|
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
|
-
|
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
|
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
|
49
|
+
# @return [Entry] A new entry instance
|
42
50
|
#
|
43
|
-
def initialize(
|
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
|
-
|
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
|