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
@@ -0,0 +1,88 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module SpecForge
|
4
|
+
#
|
5
|
+
# Manages user-defined callbacks for test lifecycle events
|
6
|
+
#
|
7
|
+
# This singleton class stores and executes callback functions that
|
8
|
+
# users can register to run at specific points in the test lifecycle.
|
9
|
+
# Each callback receives a context object containing relevant state
|
10
|
+
# information for that point in execution.
|
11
|
+
#
|
12
|
+
# @example Registering and using a callback
|
13
|
+
# SpecForge::Callbacks.register(:my_callback) do |context|
|
14
|
+
# puts "Running test: #{context.expectation_name}"
|
15
|
+
# end
|
16
|
+
#
|
17
|
+
class Callbacks < Hash
|
18
|
+
include Singleton
|
19
|
+
|
20
|
+
class << self
|
21
|
+
#
|
22
|
+
# Registers a new callback for a specific event
|
23
|
+
#
|
24
|
+
# @param name [String, Symbol] The name of the callback event
|
25
|
+
# @param block [Proc] The callback function to execute
|
26
|
+
#
|
27
|
+
# @raise [ArgumentError] If no block is provided
|
28
|
+
#
|
29
|
+
def register(name, &block)
|
30
|
+
raise ArgumentError, "A block must be provided" unless block.is_a?(Proc)
|
31
|
+
|
32
|
+
if registered?(name)
|
33
|
+
warn("Callback #{name.in_quotes} is already registered. It will be overwritten")
|
34
|
+
end
|
35
|
+
|
36
|
+
instance[name.to_s] = block
|
37
|
+
end
|
38
|
+
|
39
|
+
#
|
40
|
+
# Deregisters a callback
|
41
|
+
#
|
42
|
+
# @param name [String, Symbol] The name of the callback
|
43
|
+
#
|
44
|
+
def deregister(name)
|
45
|
+
instance.delete(name.to_s)
|
46
|
+
end
|
47
|
+
|
48
|
+
#
|
49
|
+
# Checks if a callback is registered for the given event
|
50
|
+
#
|
51
|
+
# @param name [String, Symbol] The name of the callback event
|
52
|
+
#
|
53
|
+
# @return [Boolean] True if the callback exists
|
54
|
+
#
|
55
|
+
def registered?(name)
|
56
|
+
instance.key?(name.to_s)
|
57
|
+
end
|
58
|
+
|
59
|
+
#
|
60
|
+
# Returns all registered callback names
|
61
|
+
#
|
62
|
+
# @return [Array<String>] List of registered callback names
|
63
|
+
#
|
64
|
+
def registered_names
|
65
|
+
instance.keys
|
66
|
+
end
|
67
|
+
|
68
|
+
#
|
69
|
+
# Executes a named callback with the provided context
|
70
|
+
#
|
71
|
+
# @param name [String, Symbol] The name of the callback to run
|
72
|
+
# @param context [Object] Context object containing state data
|
73
|
+
#
|
74
|
+
# @raise [ArgumentError] If the callback is not registered
|
75
|
+
#
|
76
|
+
def run(name, context)
|
77
|
+
callback = instance[name.to_s]
|
78
|
+
raise ArgumentError, "Callback #{name.in_quotes} is not defined" if callback.nil?
|
79
|
+
|
80
|
+
if callback.arity == 0
|
81
|
+
callback.call
|
82
|
+
else
|
83
|
+
callback.call(context)
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
@@ -2,17 +2,44 @@
|
|
2
2
|
|
3
3
|
module SpecForge
|
4
4
|
class CLI
|
5
|
+
#
|
6
|
+
# Provides helper methods for CLI actions such as file generation
|
7
|
+
# and template rendering through Thor::Actions integration.
|
8
|
+
#
|
9
|
+
# @example Using actions in a command
|
10
|
+
# actions.template("my_template.tt", "destination/path.rb")
|
11
|
+
#
|
5
12
|
module Actions
|
13
|
+
#
|
14
|
+
# Internal Ruby hook, called when the module is included in another file
|
15
|
+
#
|
16
|
+
# @param base [Class] The class that included this module
|
17
|
+
#
|
6
18
|
def self.included(base)
|
19
|
+
#
|
20
|
+
# Returns an ActionContext instance for performing file operations
|
21
|
+
#
|
22
|
+
# @return [ActionContext] The action context for this command
|
23
|
+
#
|
7
24
|
base.define_method(:actions) do
|
8
25
|
@actions ||= ActionContext.new
|
9
26
|
end
|
10
27
|
end
|
11
28
|
end
|
12
29
|
|
30
|
+
#
|
31
|
+
# Provides a context for Thor actions that configures paths and options
|
32
|
+
#
|
33
|
+
# @private
|
34
|
+
#
|
13
35
|
class ActionContext < Thor
|
14
36
|
include Thor::Actions
|
15
37
|
|
38
|
+
#
|
39
|
+
# Creates a new action context with SpecForge template paths configured
|
40
|
+
#
|
41
|
+
# @return [ActionContext] A new context for Thor actions
|
42
|
+
#
|
16
43
|
def initialize(...)
|
17
44
|
self.class.source_root(File.expand_path("../../templates", __dir__))
|
18
45
|
self.destination_root = SpecForge.root
|
@@ -2,20 +2,55 @@
|
|
2
2
|
|
3
3
|
module SpecForge
|
4
4
|
class CLI
|
5
|
+
#
|
6
|
+
# Base class for CLI commands that provides common functionality and
|
7
|
+
# defines the DSL for declaring command properties.
|
8
|
+
#
|
9
|
+
# @example Defining a simple command
|
10
|
+
# class MyCommand < Command
|
11
|
+
# command_name "my_command"
|
12
|
+
# syntax "my_command [options]"
|
13
|
+
# summary "Does something awesome"
|
14
|
+
# description "A longer description of what this command does"
|
15
|
+
#
|
16
|
+
# option "-f", "--force", "Force the operation"
|
17
|
+
#
|
18
|
+
# def call
|
19
|
+
# # Command implementation
|
20
|
+
# end
|
21
|
+
# end
|
22
|
+
#
|
5
23
|
class Command
|
6
24
|
include CLI::Actions
|
7
25
|
|
8
26
|
class << self
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
27
|
+
#
|
28
|
+
# Sets the command's name
|
29
|
+
#
|
30
|
+
attr_writer :command_name
|
31
|
+
|
32
|
+
#
|
33
|
+
# Sets the command's syntax string
|
34
|
+
#
|
35
|
+
attr_writer :syntax
|
36
|
+
|
37
|
+
#
|
38
|
+
# Sets the command's detailed description
|
39
|
+
#
|
40
|
+
attr_writer :description
|
41
|
+
|
42
|
+
#
|
43
|
+
# Sets a brief summary of the command
|
44
|
+
#
|
45
|
+
attr_writer :summary
|
46
|
+
|
47
|
+
#
|
48
|
+
# Sets the command's available options
|
49
|
+
#
|
50
|
+
attr_writer :options
|
16
51
|
|
17
52
|
#
|
18
|
-
#
|
53
|
+
# Sets the command's name
|
19
54
|
#
|
20
55
|
# @param name [String] The name of the command
|
21
56
|
#
|
@@ -24,37 +59,37 @@ module SpecForge
|
|
24
59
|
end
|
25
60
|
|
26
61
|
#
|
27
|
-
#
|
62
|
+
# Sets the command's syntax
|
28
63
|
#
|
29
|
-
# @param syntax [String]
|
64
|
+
# @param syntax [String] The command syntax to display in help
|
30
65
|
#
|
31
66
|
def syntax(syntax)
|
32
67
|
self.syntax = syntax
|
33
68
|
end
|
34
69
|
|
35
70
|
#
|
36
|
-
#
|
71
|
+
# Sets the command's description, displayed in detailed help
|
37
72
|
#
|
38
|
-
# @param description [String]
|
73
|
+
# @param description [String] The detailed command description
|
39
74
|
#
|
40
75
|
def description(description)
|
41
76
|
self.description = description
|
42
77
|
end
|
43
78
|
|
44
79
|
#
|
45
|
-
#
|
80
|
+
# Sets the command's summary, displayed in command list
|
46
81
|
#
|
47
|
-
# @param summary [String]
|
82
|
+
# @param summary [String] The short command summary
|
48
83
|
#
|
49
84
|
def summary(summary)
|
50
85
|
self.summary = summary
|
51
86
|
end
|
52
87
|
|
53
88
|
#
|
54
|
-
#
|
89
|
+
# Adds an example of how to use the command
|
55
90
|
#
|
56
|
-
# @param command [String] The example
|
57
|
-
# @param description [String] Description of the example
|
91
|
+
# @param command [String] The example command
|
92
|
+
# @param description [String] Description of what the example does
|
58
93
|
#
|
59
94
|
def example(command, description)
|
60
95
|
@examples ||= []
|
@@ -64,7 +99,10 @@ module SpecForge
|
|
64
99
|
end
|
65
100
|
|
66
101
|
#
|
67
|
-
#
|
102
|
+
# Adds a command line option
|
103
|
+
#
|
104
|
+
# @param args [Array<String>] The option flags (e.g., "-f", "--force")
|
105
|
+
# @yield [value] Block to handle the option value
|
68
106
|
#
|
69
107
|
def option(*args, &block)
|
70
108
|
@options ||= []
|
@@ -73,9 +111,9 @@ module SpecForge
|
|
73
111
|
end
|
74
112
|
|
75
113
|
#
|
76
|
-
#
|
114
|
+
# Adds command aliases
|
77
115
|
#
|
78
|
-
# @param
|
116
|
+
# @param aliases [Array<String>] Alias names for this command
|
79
117
|
#
|
80
118
|
def aliases(*aliases)
|
81
119
|
@aliases ||= []
|
@@ -86,7 +124,7 @@ module SpecForge
|
|
86
124
|
#
|
87
125
|
# Registers the command with Commander
|
88
126
|
#
|
89
|
-
# @param context [Commander::Command]
|
127
|
+
# @param context [Commander::Command] The Commander context
|
90
128
|
#
|
91
129
|
# @private
|
92
130
|
#
|
@@ -112,11 +150,27 @@ module SpecForge
|
|
112
150
|
end
|
113
151
|
end
|
114
152
|
|
115
|
-
|
153
|
+
#
|
154
|
+
# Command arguments passed from the command line
|
155
|
+
#
|
156
|
+
# @return [Array] The positional arguments
|
157
|
+
#
|
158
|
+
attr_reader :arguments
|
159
|
+
|
160
|
+
#
|
161
|
+
# Command options passed from the command line
|
162
|
+
#
|
163
|
+
# @return [Hash] The flag arguments
|
164
|
+
#
|
165
|
+
attr_reader :options
|
116
166
|
|
117
167
|
#
|
118
|
-
#
|
119
|
-
#
|
168
|
+
# Creates a new command instance
|
169
|
+
#
|
170
|
+
# @param arguments [Array] Any positional arguments from the command line
|
171
|
+
# @param options [Hash] Any flag arguments from the command line
|
172
|
+
#
|
173
|
+
# @return [Command] A new command instance
|
120
174
|
#
|
121
175
|
def initialize(arguments, options)
|
122
176
|
@arguments = arguments
|
@@ -0,0 +1,72 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module SpecForge
|
4
|
+
class CLI
|
5
|
+
class Docs < Command
|
6
|
+
#
|
7
|
+
# Shared functionality for generating OpenAPI documentation
|
8
|
+
#
|
9
|
+
# This module contains the core logic for running tests, extracting endpoint
|
10
|
+
# data, and generating OpenAPI specifications. It's used by both the Docs
|
11
|
+
# and Serve commands to avoid duplication.
|
12
|
+
#
|
13
|
+
module Generate
|
14
|
+
#
|
15
|
+
# Generates OpenAPI documentation and writes it to disk
|
16
|
+
#
|
17
|
+
# Runs the documentation generation pipeline: executes tests, extracts
|
18
|
+
# endpoint data, generates OpenAPI spec, validates it, and writes the
|
19
|
+
# output file in the specified format.
|
20
|
+
#
|
21
|
+
# @return [Pathname] The path to the generated documentation file
|
22
|
+
#
|
23
|
+
def generate_documentation
|
24
|
+
generator = Documentation::Generators::OpenAPI["3.0"]
|
25
|
+
output = generator.generate(use_cache: !options.fresh)
|
26
|
+
|
27
|
+
generator.validate!(output) unless options.skip_validation
|
28
|
+
|
29
|
+
# Determine output format and path
|
30
|
+
file_format = determine_file_format
|
31
|
+
file_path = determine_output_path(file_format)
|
32
|
+
|
33
|
+
content =
|
34
|
+
if file_format == "json"
|
35
|
+
JSON.pretty_generate(output)
|
36
|
+
else
|
37
|
+
output.to_yaml(stringify_names: true)
|
38
|
+
end
|
39
|
+
|
40
|
+
::File.write(file_path, content)
|
41
|
+
|
42
|
+
file_path
|
43
|
+
end
|
44
|
+
|
45
|
+
private
|
46
|
+
|
47
|
+
def determine_file_format
|
48
|
+
file_format = options.format&.downcase || "yml"
|
49
|
+
validate_format!(file_format)
|
50
|
+
|
51
|
+
file_format
|
52
|
+
end
|
53
|
+
|
54
|
+
def validate_format!(format)
|
55
|
+
return if VALID_FORMATS.include?(format)
|
56
|
+
|
57
|
+
raise ArgumentError,
|
58
|
+
"Invalid format #{format.in_quotes}. Valid formats: #{VALID_FORMATS.join_map(", ", &:in_quotes)}"
|
59
|
+
end
|
60
|
+
|
61
|
+
def determine_output_path(format)
|
62
|
+
if options.output
|
63
|
+
Pathname.new(options.output)
|
64
|
+
else
|
65
|
+
extension = (format == "json") ? "json" : "yml"
|
66
|
+
SpecForge.openapi_path.join("generated", "openapi.#{extension}")
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
@@ -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
|
data/lib/spec_forge/cli/init.rb
CHANGED
@@ -2,20 +2,62 @@
|
|
2
2
|
|
3
3
|
module SpecForge
|
4
4
|
class CLI
|
5
|
+
#
|
6
|
+
# Command for initializing a new SpecForge project structure
|
7
|
+
#
|
8
|
+
# @example Creating a new SpecForge project
|
9
|
+
# spec_forge init
|
10
|
+
#
|
5
11
|
class Init < Command
|
6
12
|
command_name "init"
|
7
13
|
syntax "init"
|
8
|
-
summary "
|
14
|
+
summary "Set up your SpecForge project (creates folders and config files)"
|
9
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"
|
28
|
+
|
29
|
+
#
|
30
|
+
# Creates the "spec_forge", "spec_forge/factories", and "spec_forge/specs" directories
|
31
|
+
# Also creates the "spec_forge.rb" initialization file
|
32
|
+
#
|
10
33
|
def call
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
34
|
+
initialize_forge
|
35
|
+
initialize_openapi unless options.skip_openapi
|
36
|
+
end
|
37
|
+
|
38
|
+
private
|
39
|
+
|
40
|
+
def initialize_forge
|
41
|
+
base_path = SpecForge.forge_path
|
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
|
58
|
+
|
59
|
+
# openapi/config/openapi.yml
|
60
|
+
actions.template("openapi.yml.tt", config_path.join("openapi.yml"))
|
19
61
|
end
|
20
62
|
end
|
21
63
|
end
|
data/lib/spec_forge/cli/new.rb
CHANGED
@@ -2,9 +2,28 @@
|
|
2
2
|
|
3
3
|
module SpecForge
|
4
4
|
class CLI
|
5
|
+
#
|
6
|
+
# Command for generating new specs or factories
|
7
|
+
#
|
8
|
+
# @example Creating a new spec
|
9
|
+
# spec_forge new spec users
|
10
|
+
#
|
11
|
+
# @example Creating a new factory
|
12
|
+
# spec_forge new factory user
|
13
|
+
#
|
5
14
|
class New < Command
|
6
15
|
command_name "new"
|
7
|
-
summary "Create
|
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
|
8
27
|
|
9
28
|
syntax "new <type> <name>"
|
10
29
|
|
@@ -19,6 +38,9 @@ module SpecForge
|
|
19
38
|
|
20
39
|
aliases :generate, :g
|
21
40
|
|
41
|
+
#
|
42
|
+
# Creates a new spec or factory file in the corresponding directory using templates
|
43
|
+
#
|
22
44
|
def call
|
23
45
|
type = arguments.first.downcase
|
24
46
|
name = arguments.second
|
@@ -39,29 +61,68 @@ module SpecForge
|
|
39
61
|
|
40
62
|
def create_new_spec(name)
|
41
63
|
actions.template(
|
42
|
-
"new_spec.tt",
|
43
|
-
SpecForge.
|
64
|
+
"new_spec.yml.tt",
|
65
|
+
SpecForge.forge_path.join("specs", "#{name}.yml"),
|
44
66
|
context: Proxy.new(name).call
|
45
67
|
)
|
46
68
|
end
|
47
69
|
|
48
70
|
def create_new_factory(name)
|
49
71
|
actions.template(
|
50
|
-
"new_factory.tt",
|
51
|
-
SpecForge.
|
72
|
+
"new_factory.yml.tt",
|
73
|
+
SpecForge.forge_path.join("factories", "#{name}.yml"),
|
52
74
|
context: Proxy.new(name).call
|
53
75
|
)
|
54
76
|
end
|
55
77
|
|
78
|
+
#
|
79
|
+
# Helper class for passing template variables to Thor templates
|
80
|
+
#
|
81
|
+
# @example Creating a proxy with a name
|
82
|
+
# proxy = Proxy.new("user")
|
83
|
+
# proxy.singular_name # => "user"
|
84
|
+
# proxy.plural_name # => "users"
|
85
|
+
#
|
56
86
|
class Proxy
|
57
|
-
|
87
|
+
#
|
88
|
+
# The original name passed to the command
|
89
|
+
#
|
90
|
+
# @return [String]
|
91
|
+
#
|
92
|
+
attr_reader :original_name
|
93
|
+
|
94
|
+
#
|
95
|
+
# The singular form of the name
|
96
|
+
#
|
97
|
+
# @return [String]
|
98
|
+
#
|
99
|
+
attr_reader :singular_name
|
100
|
+
|
101
|
+
#
|
102
|
+
# The plural form of the name
|
103
|
+
#
|
104
|
+
# @return [String]
|
105
|
+
#
|
106
|
+
attr_reader :plural_name
|
58
107
|
|
108
|
+
#
|
109
|
+
# Creates a new Proxy with the specified name
|
110
|
+
#
|
111
|
+
# @param name [String] The resource name to pluralize/singularize
|
112
|
+
#
|
113
|
+
# @return [Proxy] A new proxy instance
|
114
|
+
#
|
59
115
|
def initialize(name)
|
60
116
|
@original_name = name
|
61
117
|
@plural_name = name.pluralize
|
62
118
|
@singular_name = name.singularize
|
63
119
|
end
|
64
120
|
|
121
|
+
#
|
122
|
+
# Returns a binding for use in templates
|
123
|
+
#
|
124
|
+
# @return [Binding] A binding containing template variables
|
125
|
+
#
|
65
126
|
def call
|
66
127
|
binding
|
67
128
|
end
|