spec_forge 0.4.0 → 0.6.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 (71) hide show
  1. checksums.yaml +4 -4
  2. data/.standard.yml +4 -0
  3. data/CHANGELOG.md +145 -1
  4. data/README.md +49 -638
  5. data/flake.lock +3 -3
  6. data/flake.nix +8 -2
  7. data/lib/spec_forge/attribute/chainable.rb +208 -20
  8. data/lib/spec_forge/attribute/factory.rb +141 -12
  9. data/lib/spec_forge/attribute/faker.rb +64 -15
  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 +188 -13
  13. data/lib/spec_forge/attribute/parameterized.rb +45 -20
  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 +168 -66
  22. data/lib/spec_forge/backtrace_formatter.rb +26 -3
  23. data/lib/spec_forge/callbacks.rb +79 -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/init.rb +11 -1
  27. data/lib/spec_forge/cli/new.rb +54 -3
  28. data/lib/spec_forge/cli/run.rb +20 -0
  29. data/lib/spec_forge/cli.rb +16 -5
  30. data/lib/spec_forge/configuration.rb +94 -25
  31. data/lib/spec_forge/context/callbacks.rb +91 -0
  32. data/lib/spec_forge/context/global.rb +72 -0
  33. data/lib/spec_forge/context/store.rb +148 -0
  34. data/lib/spec_forge/context/variables.rb +91 -0
  35. data/lib/spec_forge/context.rb +36 -0
  36. data/lib/spec_forge/core_ext/rspec.rb +24 -4
  37. data/lib/spec_forge/error.rb +267 -113
  38. data/lib/spec_forge/factory.rb +33 -14
  39. data/lib/spec_forge/filter.rb +87 -0
  40. data/lib/spec_forge/forge.rb +170 -0
  41. data/lib/spec_forge/http/backend.rb +99 -29
  42. data/lib/spec_forge/http/client.rb +23 -13
  43. data/lib/spec_forge/http/request.rb +74 -62
  44. data/lib/spec_forge/http/verb.rb +79 -0
  45. data/lib/spec_forge/http.rb +105 -0
  46. data/lib/spec_forge/loader.rb +254 -0
  47. data/lib/spec_forge/matchers.rb +130 -0
  48. data/lib/spec_forge/normalizer/configuration.rb +24 -11
  49. data/lib/spec_forge/normalizer/constraint.rb +22 -9
  50. data/lib/spec_forge/normalizer/expectation.rb +31 -12
  51. data/lib/spec_forge/normalizer/factory.rb +24 -11
  52. data/lib/spec_forge/normalizer/factory_reference.rb +32 -13
  53. data/lib/spec_forge/normalizer/global_context.rb +88 -0
  54. data/lib/spec_forge/normalizer/spec.rb +39 -16
  55. data/lib/spec_forge/normalizer.rb +255 -41
  56. data/lib/spec_forge/runner/callbacks.rb +246 -0
  57. data/lib/spec_forge/runner/debug_proxy.rb +213 -0
  58. data/lib/spec_forge/runner/listener.rb +54 -0
  59. data/lib/spec_forge/runner/metadata.rb +58 -0
  60. data/lib/spec_forge/runner/state.rb +99 -0
  61. data/lib/spec_forge/runner.rb +133 -119
  62. data/lib/spec_forge/spec/expectation/constraint.rb +95 -20
  63. data/lib/spec_forge/spec/expectation.rb +43 -51
  64. data/lib/spec_forge/spec.rb +83 -96
  65. data/lib/spec_forge/type.rb +36 -4
  66. data/lib/spec_forge/version.rb +4 -1
  67. data/lib/spec_forge.rb +161 -76
  68. metadata +20 -5
  69. data/spec_forge/factories/user.yml +0 -4
  70. data/spec_forge/forge_helper.rb +0 -37
  71. data/spec_forge/specs/users.yml +0 -65
@@ -2,18 +2,41 @@
2
2
 
3
3
  module SpecForge
4
4
  #
5
- # Used internally by RSpec
6
- # This class handles formatting backtraces, hence the name ;)
5
+ # Used internally by RSpec to format backtraces for test failures
6
+ # Customizes error output to make it more readable and useful for SpecForge
7
7
  #
8
8
  module BacktraceFormatter
9
+ #
10
+ # Returns the RSpec backtrace formatter instance
11
+ # Lazily initializes the formatter on first access
12
+ #
13
+ # @return [RSpec::Core::BacktraceFormatter] The backtrace formatter
14
+ #
9
15
  def self.formatter
10
16
  @formatter ||= RSpec::Core::BacktraceFormatter.new
11
17
  end
12
18
 
19
+ #
20
+ # Formats a single backtrace line
21
+ # Delegates to the RSpec formatter
22
+ #
23
+ # @param line [String] The backtrace line to format
24
+ #
25
+ # @return [String] The formatted backtrace line
26
+ #
13
27
  def self.backtrace_line(line)
14
28
  formatter.backtrace_line(line)
15
29
  end
16
30
 
31
+ #
32
+ # Formats a complete backtrace for an example
33
+ # Adds the YAML location to the front of the backtrace for better context
34
+ #
35
+ # @param backtrace [Array<String>] The raw backtrace lines
36
+ # @param example_metadata [Hash] Metadata about the failing example
37
+ #
38
+ # @return [Array<String>] The formatted backtrace with YAML location first
39
+ #
17
40
  def self.format_backtrace(backtrace, example_metadata)
18
41
  backtrace = SpecForge.backtrace_cleaner.clean(backtrace)
19
42
 
@@ -21,7 +44,7 @@ module SpecForge
21
44
  line_number = example_metadata[:example_group][:line_number]
22
45
 
23
46
  # Add the yaml location to the front so it's the first thing people see
24
- ["#{location}:#{line_number}"] + backtrace
47
+ ["#{location}:#{line_number}"] + backtrace[0..50]
25
48
  end
26
49
  end
27
50
  end
@@ -0,0 +1,79 @@
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
+ # Checks if a callback is registered for the given event
41
+ #
42
+ # @param name [String, Symbol] The name of the callback event
43
+ #
44
+ # @return [Boolean] True if the callback exists
45
+ #
46
+ def registered?(name)
47
+ instance.key?(name.to_s)
48
+ end
49
+
50
+ #
51
+ # Returns all registered callback names
52
+ #
53
+ # @return [Array<String>] List of registered callback names
54
+ #
55
+ def registered_names
56
+ instance.keys
57
+ end
58
+
59
+ #
60
+ # Executes a named callback with the provided context
61
+ #
62
+ # @param name [String, Symbol] The name of the callback to run
63
+ # @param context [Object] Context object containing state data
64
+ #
65
+ # @raise [ArgumentError] If the callback is not registered
66
+ #
67
+ def run(name, context)
68
+ callback = instance[name.to_s]
69
+ raise ArgumentError, "Callback #{name.in_quotes} is not defined" if callback.nil?
70
+
71
+ if callback.arity == 0
72
+ callback.call
73
+ else
74
+ callback.call(context)
75
+ end
76
+ end
77
+ end
78
+ end
79
+ 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
@@ -2,13 +2,23 @@
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
14
  summary "Initializes directory structure and configuration files"
9
15
 
16
+ #
17
+ # Creates the "spec_forge", "spec_forge/factories", and "spec_forge/specs" directories
18
+ # Also creates the "spec_forge.rb" initialization file
19
+ #
10
20
  def call
11
- base_path = SpecForge.forge
21
+ base_path = SpecForge.forge_path
12
22
  actions.empty_directory "#{base_path}/factories"
13
23
  actions.empty_directory "#{base_path}/specs"
14
24
 
@@ -2,6 +2,15 @@
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
16
  summary "Create a new spec or factory"
@@ -19,6 +28,9 @@ module SpecForge
19
28
 
20
29
  aliases :generate, :g
21
30
 
31
+ #
32
+ # Creates a new spec or factory file in the corresponding directory using templates
33
+ #
22
34
  def call
23
35
  type = arguments.first.downcase
24
36
  name = arguments.second
@@ -40,7 +52,7 @@ module SpecForge
40
52
  def create_new_spec(name)
41
53
  actions.template(
42
54
  "new_spec.tt",
43
- SpecForge.forge.join("specs", "#{name}.yml"),
55
+ SpecForge.forge_path.join("specs", "#{name}.yml"),
44
56
  context: Proxy.new(name).call
45
57
  )
46
58
  end
@@ -48,20 +60,59 @@ module SpecForge
48
60
  def create_new_factory(name)
49
61
  actions.template(
50
62
  "new_factory.tt",
51
- SpecForge.forge.join("factories", "#{name}.yml"),
63
+ SpecForge.forge_path.join("factories", "#{name}.yml"),
52
64
  context: Proxy.new(name).call
53
65
  )
54
66
  end
55
67
 
68
+ #
69
+ # Helper class for passing template variables to Thor templates
70
+ #
71
+ # @example Creating a proxy with a name
72
+ # proxy = Proxy.new("user")
73
+ # proxy.singular_name # => "user"
74
+ # proxy.plural_name # => "users"
75
+ #
56
76
  class Proxy
57
- attr_reader :original_name, :singular_name, :plural_name
77
+ #
78
+ # The original name passed to the command
79
+ #
80
+ # @return [String]
81
+ #
82
+ attr_reader :original_name
58
83
 
84
+ #
85
+ # The singular form of the name
86
+ #
87
+ # @return [String]
88
+ #
89
+ attr_reader :singular_name
90
+
91
+ #
92
+ # The plural form of the name
93
+ #
94
+ # @return [String]
95
+ #
96
+ attr_reader :plural_name
97
+
98
+ #
99
+ # Creates a new Proxy with the specified name
100
+ #
101
+ # @param name [String] The resource name to pluralize/singularize
102
+ #
103
+ # @return [Proxy] A new proxy instance
104
+ #
59
105
  def initialize(name)
60
106
  @original_name = name
61
107
  @plural_name = name.pluralize
62
108
  @singular_name = name.singularize
63
109
  end
64
110
 
111
+ #
112
+ # Returns a binding for use in templates
113
+ #
114
+ # @return [Binding] A binding containing template variables
115
+ #
65
116
  def call
66
117
  binding
67
118
  end
@@ -2,6 +2,21 @@
2
2
 
3
3
  module SpecForge
4
4
  class CLI
5
+ #
6
+ # Command for running SpecForge tests with filtering options
7
+ #
8
+ # @example Running all specs
9
+ # spec_forge run
10
+ #
11
+ # @example Running specific file
12
+ # spec_forge run users
13
+ #
14
+ # @example Running specific spec
15
+ # spec_forge run users:create_user
16
+ #
17
+ # @example Running specific expectation
18
+ # spec_forge run users:create_user:"POST /users"
19
+ #
5
20
  class Run < Command
6
21
  command_name "run"
7
22
  syntax "run [target]"
@@ -26,6 +41,9 @@ module SpecForge
26
41
 
27
42
  # option "-n", "--no-docs", "Do not generate OpenAPI documentation on completion"
28
43
 
44
+ #
45
+ # Loads and runs all specs, or a subset of specs based on the provided arguments
46
+ #
29
47
  def call
30
48
  return SpecForge.run if arguments.blank?
31
49
 
@@ -53,6 +71,8 @@ module SpecForge
53
71
  # Example with name:
54
72
  # "users:show_user:'GET /users/:id - Returns 404 due to missing user'"
55
73
  #
74
+ # @private
75
+ #
56
76
  def extract_filter(input)
57
77
  # Note: Only split 3 because the expectation name can have colons in them.
58
78
  file_name, spec_name, expectation_name = input.split(":", 3).map(&:strip)
@@ -7,15 +7,26 @@ require_relative "cli/new"
7
7
  require_relative "cli/run"
8
8
 
9
9
  module SpecForge
10
+ #
11
+ # Command-line interface for SpecForge that provides the overall command structure
12
+ # and entry point for the CLI functionality.
13
+ #
14
+ # @example Running the default command
15
+ # SpecForge::CLI.new.run
16
+ #
17
+ # @example Running a specific command
18
+ # # From command line: spec_forge init
19
+ #
10
20
  class CLI
11
21
  include Commander::Methods
12
22
 
13
- COMMANDS = [Init, New, Run]
14
-
15
23
  #
16
- # Runs the CLI
24
+ # @return [Array<SpecForge::CLI::Command>] All available commands
17
25
  #
18
- # @private
26
+ COMMANDS = [Init, New, Run].freeze
27
+
28
+ #
29
+ # Runs the CLI application, setting up program information and registering commands
19
30
  #
20
31
  def run
21
32
  program :name, "SpecForge"
@@ -30,7 +41,7 @@ module SpecForge
30
41
  end
31
42
 
32
43
  #
33
- # Registers the commands with Commander
44
+ # Registers the command classes with Commander
34
45
  #
35
46
  # @private
36
47
  #