spec_forge 0.5.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 +3 -3
  3. data/CHANGELOG.md +106 -1
  4. data/README.md +34 -22
  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 +91 -14
  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 +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 -22
  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 +22 -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 +21 -8
  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 +27 -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 +132 -123
  62. data/lib/spec_forge/spec/expectation/constraint.rb +91 -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 -48
  71. data/spec_forge/specs/users.yml +0 -65
@@ -2,13 +2,54 @@
2
2
 
3
3
  module SpecForge
4
4
  class Attribute
5
+ #
6
+ # Represents an attribute that references a factory to generate test data
7
+ #
8
+ # This class allows SpecForge to integrate with FactoryBot for test data generation.
9
+ # It supports various build strategies like create, build, build_stubbed, etc.
10
+ #
11
+ # @example Basic usage in YAML
12
+ # user: factories.user
13
+ #
14
+ # @example With custom attributes
15
+ # user:
16
+ # factories.user:
17
+ # attributes:
18
+ # name: "Custom Name"
19
+ # email: faker.internet.email
20
+ #
21
+ # @example With build strategy
22
+ # user:
23
+ # factories.user:
24
+ # strategy: build
25
+ # attributes:
26
+ # admin: true
27
+ #
28
+ # @example With an array of 5 user attributes
29
+ # user:
30
+ # factories.user:
31
+ # strategy: attributes_for
32
+ # size: 5
33
+ # attributes:
34
+ # admin: true
35
+ #
5
36
  class Factory < Parameterized
6
37
  include Chainable
7
38
 
39
+ #
40
+ # Regular expression pattern that matches attribute keywords with this prefix
41
+ # Used for identifying this attribute type during parsing
42
+ #
43
+ # @return [Regexp]
44
+ #
8
45
  KEYWORD_REGEX = /^factories\./i
9
46
 
10
- # These are the base strategies that can be provided either with or without size
11
- # stubbed will be transformed into build_stubbed
47
+ #
48
+ # An array of base strategies that can be provided either with or
49
+ # without a size. "stubbed" will automatically be transformed into "build_stubbed"
50
+ #
51
+ # @return [Array<String>]
52
+ #
12
53
  BASE_STRATEGIES = %w[
13
54
  build
14
55
  create
@@ -16,7 +57,7 @@ module SpecForge
16
57
  attributes_for
17
58
  ].freeze
18
59
 
19
- # All available build strategies that are accepted
60
+ # @return [Array<String>] All available build strategies
20
61
  BUILD_STRATEGIES = %w[
21
62
  attributes_for
22
63
  attributes_for_list
@@ -33,9 +74,7 @@ module SpecForge
33
74
  alias_method :factory_name, :header
34
75
 
35
76
  #
36
- # Represents any attribute that is a factory reference
37
- #
38
- # factories.<factory_name>
77
+ # Creates a new factory attribute with the specified name and arguments
39
78
  #
40
79
  def initialize(...)
41
80
  super
@@ -46,8 +85,11 @@ module SpecForge
46
85
  prepare_arguments!
47
86
  end
48
87
 
49
- private
50
-
88
+ #
89
+ # Returns the base object for the variable chain
90
+ #
91
+ # @return [Object] The result of the FactoryBot call
92
+ #
51
93
  def base_object
52
94
  attributes = arguments[:keyword]
53
95
 
@@ -58,19 +100,49 @@ module SpecForge
58
100
  FactoryBot.public_send(*build_arguments)
59
101
  end
60
102
 
103
+ #
104
+ # Similar to #resolved but doesn't cache the result, allowing for re-resolution.
105
+ # Recursively calls #resolve on all nested attributes without storing results.
106
+ #
107
+ # Use this when you need to ensure fresh values each time, particularly with
108
+ # factories or other attributes that should generate new values on each call.
109
+ #
110
+ # @return [Object] The completely resolved value without caching
111
+ #
112
+ # @example
113
+ # factory_attr = Attribute::Factory.new("factories.user")
114
+ # factory_attr.resolve # => User#1 (a new user)
115
+ # factory_attr.resolve # => User#2 (another new user)
116
+ #
117
+ def resolve
118
+ case value
119
+ when ArrayLike
120
+ value.map(&resolved_proc)
121
+ when HashLike
122
+ value.transform_values(&resolved_proc)
123
+ else
124
+ value
125
+ end
126
+ end
127
+
128
+ private
129
+
130
+ #
131
+ # @private
132
+ #
61
133
  def construct_factory_parameters(attributes)
62
134
  build_strategy, list_size = determine_build_strategy(attributes)
63
135
 
64
136
  # This is set up for the base strategies + _pair
65
- # FactoryBot.create(factory_name, **attributes)
137
+ # FactoryBot.<build_strategy>(factory_name, **attributes)
66
138
  build_arguments = [
67
139
  build_strategy,
68
140
  factory_name,
69
- **attributes[:attributes].resolve_value
141
+ **attributes[:attributes].resolve
70
142
  ]
71
143
 
72
144
  # Insert the list size after the strategy
73
- # FactoryBot.create_list(factory_name, list_size, **attributes)
145
+ # FactoryBot.<build_strategy>_list(factory_name, list_size, **attributes)
74
146
  if build_strategy.end_with?("_list")
75
147
  build_arguments.insert(2, list_size)
76
148
  end
@@ -78,10 +150,13 @@ module SpecForge
78
150
  build_arguments
79
151
  end
80
152
 
153
+ #
154
+ # @private
155
+ #
81
156
  def determine_build_strategy(attributes)
82
157
  # Determine build strat, and unfreeze
83
- build_strategy = +attributes[:build_strategy].resolve_value
84
- list_size = attributes[:size].resolve_value
158
+ build_strategy = +attributes[:build_strategy].resolve
159
+ list_size = attributes[:size].resolve
85
160
 
86
161
  # stubbed => build_stubbed
87
162
  build_strategy.prepend("build_") if build_strategy.start_with?("stubbed")
@@ -94,7 +169,9 @@ module SpecForge
94
169
  build_strategy += "_list"
95
170
  end
96
171
 
97
- raise InvalidBuildStrategy, build_strategy unless BUILD_STRATEGIES.include?(build_strategy)
172
+ if !BUILD_STRATEGIES.include?(build_strategy)
173
+ raise Error::InvalidBuildStrategy, build_strategy
174
+ end
98
175
 
99
176
  [build_strategy, list_size]
100
177
  end
@@ -2,17 +2,44 @@
2
2
 
3
3
  module SpecForge
4
4
  class Attribute
5
+ #
6
+ # Represents an attribute that generates fake data using the Faker gem
7
+ #
8
+ # This class allows SpecForge to integrate with the Faker library to generate realistic
9
+ # test data like names, emails, addresses, etc.
10
+ #
11
+ # @example Basic usage in YAML
12
+ # name: faker.name.name
13
+ # email: faker.internet.email
14
+ #
15
+ # @example With method arguments
16
+ # age:
17
+ # faker.number.between:
18
+ # from: 18
19
+ # to: 65
20
+ #
21
+ # @example Handles nested faker classes
22
+ # character: faker.games.zelda.character
23
+ #
5
24
  class Faker < Parameterized
6
25
  include Chainable
7
26
 
27
+ #
28
+ # Regular expression pattern that matches attribute keywords with this prefix
29
+ # Used for identifying this attribute type during parsing
30
+ #
31
+ # @return [Regexp]
32
+ #
8
33
  KEYWORD_REGEX = /^faker\./i
9
34
 
10
- attr_reader :faker_class, :faker_method
35
+ # @return [Class] The Faker class
36
+ attr_reader :faker_class
37
+
38
+ # @return [Method] The Faker class method
39
+ attr_reader :faker_method
11
40
 
12
41
  #
13
- # Represents any attribute that is a faker call
14
- #
15
- # faker.<faker_class>.<faker_method>
42
+ # Creates a new faker attribute with the specified name and arguments
16
43
  #
17
44
  def initialize(...)
18
45
  super
@@ -22,18 +49,37 @@ module SpecForge
22
49
  prepare_arguments!
23
50
  end
24
51
 
25
- private
26
-
52
+ #
53
+ # Returns the base object for the variable chain
54
+ #
55
+ # @return [Object] The result of the Faker call
56
+ #
27
57
  def base_object
28
58
  if (positional = arguments[:positional]) && positional.present?
29
- faker_method.call(*positional.resolve)
59
+ faker_method.call(*positional.resolved)
30
60
  elsif (keyword = arguments[:keyword]) && keyword.present?
31
- faker_method.call(**keyword.resolve)
61
+ faker_method.call(**keyword.resolved)
32
62
  else
33
63
  faker_method.call
34
64
  end
35
65
  end
36
66
 
67
+ private
68
+
69
+ #
70
+ # Extracts the Faker class and method from the input string
71
+ # Handles both simple cases like "faker.name.first_name" and complex
72
+ # nested namespaces like "faker.games.zelda.game"
73
+ #
74
+ # @return [Array<Class, Method>] A two-element array containing:
75
+ # 1. The resolved Faker class (e.g., Faker::Name)
76
+ # 2. The method object to call on that class (e.g., #first_name)
77
+ #
78
+ # @raise [Error::InvalidFakerClassError] If the specified Faker class doesn't exist
79
+ # @raise [Error::InvalidFakerMethodError] If the specified method doesn't exist on the class
80
+ #
81
+ # @private
82
+ #
37
83
  def extract_faker_call
38
84
  class_name = header.downcase.to_s
39
85
 
@@ -42,10 +88,10 @@ module SpecForge
42
88
  return resolve_faker_class_and_method(class_name, invocation_chain.shift)
43
89
  end
44
90
 
45
- # Try each part of the chain as a potential class name
46
- # Example: faker.games.zelda.game.underscore
47
91
  namespace = []
48
92
 
93
+ # Try each part of the chain as a potential class name
94
+ # Example: faker.games.zelda.game.underscore
49
95
  while invocation_chain.any?
50
96
  part = invocation_chain.first.downcase
51
97
  test_class_name = ([class_name] + namespace + [part]).map(&:camelize).join("::")
@@ -65,22 +111,25 @@ module SpecForge
65
111
 
66
112
  # If we get here, we consumed all parts as classes but found no method
67
113
  class_name = ([class_name] + namespace).map(&:camelize).join("::")
68
- raise InvalidFakerMethodError.new(nil, "::#{class_name}".constantize)
114
+ raise Error::InvalidFakerMethodError.new(nil, "::#{class_name}".constantize)
69
115
  end
70
116
 
117
+ #
118
+ # @private
119
+ #
71
120
  def resolve_faker_class_and_method(class_name, method_name)
72
121
  # Load the class
73
122
  faker_class = begin
74
123
  "::Faker::#{class_name.camelize}".constantize
75
124
  rescue NameError
76
- raise InvalidFakerClassError, class_name
125
+ raise Error::InvalidFakerClassError, class_name
77
126
  end
78
127
 
79
128
  # Load the method
80
129
  faker_method = begin
81
130
  faker_class.method(method_name)
82
131
  rescue NameError
83
- raise InvalidFakerMethodError.new(method_name, faker_class)
132
+ raise Error::InvalidFakerMethodError.new(method_name, faker_class)
84
133
  end
85
134
 
86
135
  [faker_class, faker_method]
@@ -0,0 +1,96 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SpecForge
4
+ class Attribute
5
+ #
6
+ # Represents an attribute that references values from the global context
7
+ #
8
+ # This class allows accessing shared data defined at the global level through
9
+ # namespaced references. It provides access to global variables that are shared
10
+ # across all specs in a file, enabling consistent test data without repetition.
11
+ #
12
+ # Currently supports the "variables" namespace.
13
+ #
14
+ # @example Basic usage in YAML
15
+ # # Reference a global variable in a spec
16
+ # session_token: global.variables.session_token
17
+ #
18
+ # # Using within a request body
19
+ # body:
20
+ # api_version: global.variables.api_version
21
+ # auth_token: global.variables.auth_token
22
+ #
23
+ class Global < Attribute
24
+ #
25
+ # Regular expression pattern that matches attribute keywords with this prefix
26
+ # Used for identifying this attribute type during parsing
27
+ #
28
+ # @return [Regexp]
29
+ #
30
+ KEYWORD_REGEX = /^global\./i
31
+
32
+ #
33
+ # An array of valid namespaces that can be access on global
34
+ #
35
+ # @return [Array<String>]
36
+ #
37
+ VALID_NAMESPACES = %w[
38
+ variables
39
+ ].freeze
40
+
41
+ #
42
+ # Creates a new global attribute from the input string
43
+ #
44
+ # Parses the input string to extract the namespace to validate it
45
+ # Conversion happens when `#value` is called
46
+ #
47
+ # @raise [Error::InvalidGlobalNamespaceError] If an unsupported namespace is referenced
48
+ #
49
+ def initialize(...)
50
+ super
51
+
52
+ # Check to make sure the namespace is valid
53
+ namespace = input.split(".").second
54
+
55
+ if !VALID_NAMESPACES.include?(namespace)
56
+ raise Error::InvalidGlobalNamespaceError, namespace
57
+ end
58
+ end
59
+
60
+ #
61
+ # Converts the global reference into an underlying attribute
62
+ #
63
+ # Parses the input and returns the corresponding attribute based on the namespace.
64
+ # Currently supports extracting variables from the global context.
65
+ #
66
+ # @return [Attribute] An attribute representing the referenced global value
67
+ #
68
+ def value
69
+ # Skip the "global" prefix
70
+ components = input.split(".")[1..]
71
+ namespace = components.first
72
+
73
+ global_context = SpecForge.context.global
74
+
75
+ case namespace
76
+ when "variables"
77
+ variable_input = components.join(".")
78
+ variable = Attribute::Variable.new(variable_input)
79
+ variable.bind_variables(global_context.variables.to_h)
80
+ variable
81
+ end
82
+ end
83
+
84
+ #
85
+ # Resolves the global reference to its actual value
86
+ #
87
+ # Delegates resolution to the underlying attribute and caches the result
88
+ #
89
+ # @return [Object] The fully resolved value from the global context
90
+ #
91
+ def resolved
92
+ @resolved ||= value.resolved
93
+ end
94
+ end
95
+ end
96
+ end
@@ -2,12 +2,25 @@
2
2
 
3
3
  module SpecForge
4
4
  class Attribute
5
+ #
6
+ # Represents an attribute that is a literal value
7
+ #
8
+ # This is the simplest form of attribute, storing values like strings, numbers,
9
+ # and booleans without any processing.
10
+ #
11
+ # @example Basic usage in YAML
12
+ # name: "John Doe"
13
+ # age: 42
14
+ # active: true
15
+ #
5
16
  class Literal < Attribute
17
+ # @return [Object] The literal value
6
18
  attr_reader :value
7
19
 
8
20
  #
9
- # Represents any attribute that is a literal value.
10
- # A literal value can be any value YAML value, except Array and Hash
21
+ # Creates a new literal attribute with the specified value
22
+ #
23
+ # @param input [Object] The value to store
11
24
  #
12
25
  def initialize(input)
13
26
  super
@@ -2,28 +2,65 @@
2
2
 
3
3
  module SpecForge
4
4
  class Attribute
5
+ #
6
+ # Represents an attribute that uses RSpec matchers for response validation
7
+ #
8
+ # This class allows SpecForge to integrate with RSpec's powerful matchers
9
+ # for flexible response validation. It supports most of the built-in RSpec matchers
10
+ # and most custom matchers, assuming they do not require more Ruby code
11
+ #
12
+ # @example Basic matchers in YAML
13
+ # be_true: be.true
14
+ # include_admin:
15
+ # matcher.include:
16
+ # - admin
17
+ #
18
+ # @example Comparison matchers
19
+ # count:
20
+ # be.greater_than: 5
21
+ #
22
+ # @example Type checking
23
+ # name: kind_of.string
24
+ # id: kind_of.integer
25
+ #
5
26
  class Matcher < Parameterized
6
- class Methods
27
+ #
28
+ # Helper class to access RSpec matcher methods
29
+ #
30
+ class RSpecMatchers
7
31
  include RSpec::Matchers
8
32
  end
9
33
 
10
- MATCHER_METHODS = Methods.new.freeze
11
- KEYWORD_REGEX = /^matcher\.|^be\.|^kind_of\./i
34
+ #
35
+ # Regular expression pattern that matches attribute keywords with this prefix
36
+ # Used for identifying this attribute type during parsing
37
+ #
38
+ # @return [Regexp]
39
+ #
40
+ KEYWORD_REGEX = /^matchers?\.|^be\.|^kind_of\./i
41
+
42
+ #
43
+ # Instance of Methods providing access to all RSpec matchers
44
+ #
45
+ MATCHER_METHODS = RSpecMatchers.new.freeze
12
46
 
47
+ #
48
+ # Mapping of literal string values to their Ruby equivalents
49
+ # Used for be.nil, be.true, and be.false matchers
50
+ #
13
51
  LITERAL_MAPPINGS = {
14
52
  "nil" => nil,
15
53
  "true" => true,
16
54
  "false" => false
17
55
  }.freeze
18
56
 
57
+ #
58
+ # The resolved RSpec matcher method to call
59
+ #
19
60
  attr_reader :matcher_method
20
61
 
21
62
  #
22
- # Represents any attribute that is a matcher call.
23
- #
24
- # matcher.<method>
25
- # be.<method>
26
- # kind_of.<method>
63
+ # Creates a new matcher attribute with the specified matcher and arguments
27
64
  #
28
65
  def initialize(...)
29
66
  super
@@ -37,28 +74,87 @@ module SpecForge
37
74
  when "kind_of"
38
75
  resolve_kind_of_matcher(method)
39
76
  else
40
- resolve_matcher(method)
77
+ resolve_base_matcher(method)
41
78
  end
42
79
 
43
80
  prepare_arguments!
81
+
82
+ # An argument can be an expanded version of something (such as matcher.include)
83
+ # Move it to where it belongs
84
+ if (keyword = arguments[:keyword]) && !Type.hash?(keyword)
85
+ arguments[:positional] << keyword
86
+ arguments[:keyword] = {}
87
+ end
44
88
  end
45
89
 
90
+ #
91
+ # Returns the result of applying the matcher with the given arguments
92
+ # Creates an RSpec matcher that can be used in expectations
93
+ #
94
+ # @return [RSpec::Matchers::BuiltIn::BaseMatcher] The configured matcher
95
+ #
46
96
  def value
47
97
  if (positional = arguments[:positional]) && positional.present?
48
- positional = positional.resolve.each do |value|
98
+ positional = positional.resolved.each do |value|
49
99
  value.deep_stringify_keys! if value.respond_to?(:deep_stringify_keys!)
50
100
  end
51
101
 
52
102
  matcher_method.call(*positional)
53
103
  elsif (keyword = arguments[:keyword]) && keyword.present?
54
- matcher_method.call(**keyword.resolve.deep_stringify_keys)
104
+ matcher_method.call(**keyword.resolved.deep_stringify_keys)
55
105
  else
56
106
  matcher_method.call
57
107
  end
58
108
  end
59
109
 
110
+ #
111
+ # Ensures proper conversion of nested matcher arguments based on context
112
+ #
113
+ # This method overrides handles a special case of matchers that take arguments
114
+ # which themselves might need to be converted to matchers. It skips conversion
115
+ # for string arguments that should remain strings
116
+ # (like with include, start_with, and end_with) while correctly handling nested
117
+ # matchers and other argument types.
118
+ #
119
+ # @example Problem case handled
120
+ # # In YAML:
121
+ # matcher.all:
122
+ # matcher.include:
123
+ # - /@/ # Should become match(/@/) when used with include
124
+ #
125
+ # @example Edge case handled
126
+ # # In YAML:
127
+ # matcher.include: "." # Should remain a string, not eq(".")
128
+ #
129
+ # @return [RSpec::Matchers::BuiltIn::BaseMatcher] The properly configured matcher
130
+ # with all arguments correctly converted based on context
131
+ #
132
+ def resolve_as_matcher
133
+ # Argument conversion only matters for the base matchers
134
+ if input.start_with?("matcher")
135
+ block = lambda do |argument|
136
+ next argument unless convert_argument?(argument)
137
+
138
+ argument.resolve_as_matcher
139
+ end
140
+
141
+ arguments[:positional].map!(&block)
142
+ arguments[:keyword].transform_values!(&block)
143
+ end
144
+
145
+ super
146
+ end
147
+
60
148
  private
61
149
 
150
+ #
151
+ # Extracts the namespace and method name from the input string
152
+ # For example, "be.empty" would return ["be", "empty"]
153
+ #
154
+ # @return [Array<String, String>] The namespace and method name
155
+ #
156
+ # @private
157
+ #
62
158
  def extract_namespace_and_method
63
159
  sections = input.split(".", 2)
64
160
 
@@ -69,10 +165,51 @@ module SpecForge
69
165
  end
70
166
  end
71
167
 
168
+ #
169
+ # Resolves a matcher with the "matcher" prefix
170
+ #
171
+ # @param method [String] The method part after "matcher."
172
+ #
173
+ # @return [Method] The resolved matcher method
174
+ #
175
+ # @private
176
+ #
177
+ def resolve_base_matcher(method)
178
+ if method == "and"
179
+ resolve_matcher("forge_and")
180
+ else
181
+ resolve_matcher(method)
182
+ end
183
+ end
184
+
185
+ #
186
+ # Resolves a matcher method by name from the given namespace
187
+ #
188
+ # @param method_name [String, Symbol] The matcher method name
189
+ # @param namespace [Object] The object to resolve the method from
190
+ #
191
+ # @return [Method] The resolved matcher method
192
+ #
193
+ # @private
194
+ #
72
195
  def resolve_matcher(method_name, namespace: MATCHER_METHODS)
196
+ if !namespace.respond_to?(method_name)
197
+ raise Error::UndefinedMatcherError, method_name
198
+ end
199
+
73
200
  namespace.public_method(method_name)
74
201
  end
75
202
 
203
+ #
204
+ # Resolves a matcher with the "be" prefix
205
+ # Handles special cases like be.true, be.nil, comparison operators, etc.
206
+ #
207
+ # @param method [String] The method part after "be."
208
+ #
209
+ # @return [Method] The resolved matcher method
210
+ #
211
+ # @private
212
+ #
76
213
  def resolve_be_matcher(method)
77
214
  # Resolve any custom matchers
78
215
  resolved_matcher =
@@ -107,12 +244,50 @@ module SpecForge
107
244
  resolve_matcher(:"be_#{method}")
108
245
  end
109
246
 
247
+ #
248
+ # Resolves a kind_of matcher for the given type
249
+ # For example, kind_of.string would check if an object is a String
250
+ #
251
+ # @param method [String] The type name to check for
252
+ #
253
+ # @return [Method] The resolved matcher method
254
+ #
255
+ # @private
256
+ #
110
257
  def resolve_kind_of_matcher(method)
111
258
  type_class = Object.const_get(method.capitalize)
112
259
  arguments[:positional].insert(0, type_class)
113
260
 
114
261
  resolve_matcher(:be_kind_of)
115
262
  end
263
+
264
+ #
265
+ # Determines whether an argument should skip conversion to a matcher
266
+ #
267
+ # This helper method handles the case where string arguments to certain matchers
268
+ # (include, start_with, end_with) should remain as strings rather than being
269
+ # converted to eq() matchers.
270
+ #
271
+ # @param argument [Object] The argument to analyze
272
+ #
273
+ # @return [Boolean] true if the argument should skip conversion, false otherwise
274
+ #
275
+ # @example Skip conversion
276
+ # skip_argument_conversion?(Attribute::Literal.new(".")) #=> true
277
+ # # When used with include, start_with, or end_with
278
+ #
279
+ # @example Apply conversion
280
+ # skip_argument_conversion?(Attribute::Regex.new("/@/")) #=> false
281
+ # # Regex should be converted to match(/@/)
282
+ #
283
+ def convert_argument?(argument)
284
+ return true if argument.is_a?(Attribute::Matcher) || argument.is_a?(Attribute::Regex)
285
+
286
+ return true unless [:include, :start_with, :end_with].include?(matcher_method.name)
287
+
288
+ resolved = argument.resolved
289
+ resolved.is_a?(Array) || resolved.is_a?(Hash)
290
+ end
116
291
  end
117
292
  end
118
293
  end