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
@@ -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
- attr_writer(*%i[
10
- command_name
11
- syntax
12
- description
13
- summary
14
- options
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
- # The command's name
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
- # The command's syntax
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
- # The command's description, long form
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
- # The command's summary, short form
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
- # Defines an example on how to use the command
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
- # Defines a command flag (-f, --force)
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
- # Defines any aliases for this command
114
+ # Adds command aliases
77
115
  #
78
- # @param *aliases [Array<String>]
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
- attr_reader :arguments, :options
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
- # @param arguments [Array] Any positional arguments
119
- # @param options [Hash] Any flag arguments
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
@@ -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 "Initializes directory structure and configuration files"
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
- base_path = SpecForge.forge
12
- actions.empty_directory "#{base_path}/factories"
13
- actions.empty_directory "#{base_path}/specs"
14
-
15
- actions.template(
16
- "forge_helper.tt",
17
- SpecForge.root.join(base_path, "forge_helper.rb")
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
@@ -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 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
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.forge.join("specs", "#{name}.yml"),
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.forge.join("factories", "#{name}.yml"),
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
- attr_reader :original_name, :singular_name, :plural_name
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