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
@@ -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
- if uses_positional_arguments?(matcher_method)
48
- positional = arguments[:positional].resolve.each do |value|
97
+ if (positional = arguments[:positional]) && positional.present?
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
- elsif uses_keyword_arguments?(matcher_method)
54
- matcher_method.call(**arguments[:keyword].resolve.deep_stringify_keys)
103
+ elsif (keyword = arguments[:keyword]) && keyword.present?
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
@@ -2,7 +2,33 @@
2
2
 
3
3
  module SpecForge
4
4
  class Attribute
5
+ #
6
+ # Base class for attributes that support positional and keyword arguments
7
+ #
8
+ # This class provides the foundation for attributes that need to accept
9
+ # arguments, such as Faker, Matcher, and Factory. It handles both positional
10
+ # (array-style) and keyword (hash-style) arguments.
11
+ #
12
+ # @example With keyword arguments in YAML
13
+ # example:
14
+ # keyword:
15
+ # arg1: value1
16
+ # arg2: value2
17
+ #
18
+ # @example With positional arguments in YAML
19
+ # example:
20
+ # keyword:
21
+ # - arg1
22
+ # - arg2
23
+ #
5
24
  class Parameterized < Attribute
25
+ #
26
+ # Creates a new attribute instance from a hash representation
27
+ #
28
+ # @param hash [Hash] A hash containing the attribute name and arguments
29
+ #
30
+ # @return [Parameterized] A new parameterized attribute instance
31
+ #
6
32
  def self.from_hash(hash)
7
33
  metadata = hash.first
8
34
 
@@ -21,22 +47,22 @@ module SpecForge
21
47
  end
22
48
  end
23
49
 
24
- attr_reader :arguments
25
-
26
50
  #
27
- # Represents any attribute that is written in expanded form.
28
- # Expanded form is just a fancy name for a hash.
51
+ # A hash containing both positional and keyword arguments for this attribute
52
+ # The hash has two keys: :positional (Array) and :keyword (Hash)
53
+ #
54
+ # @return [Hash{Symbol => Object}] The arguments hash with structure:
55
+ # {
56
+ # positional: Array - Contains positional arguments in order
57
+ # keyword: Hash - Contains keyword arguments as key-value pairs
58
+ # }
29
59
  #
30
- # keyword:
31
- # <attribute>:
32
- # <keyword_arg>: <value>
60
+ attr_reader :arguments
61
+
33
62
  #
34
- # positional:
35
- # <attribute>:
36
- # - <positional_arg>
37
- # - <positional_arg>
63
+ # Creates a new parameterized attribute with the specified arguments
38
64
  #
39
- # @param input [Hash] The key that contains these arguments
65
+ # @param input [String, Symbol] The key that contains these arguments
40
66
  # @param positional [Array] Any positional arguments
41
67
  # @param keyword [Hash] Any keyword arguments
42
68
  #
@@ -46,6 +72,11 @@ module SpecForge
46
72
  @arguments = {positional:, keyword:}
47
73
  end
48
74
 
75
+ #
76
+ # Binds variables to any nested attributes in the arguments
77
+ #
78
+ # @param variables [Hash] A hash of variable attributes
79
+ #
49
80
  def bind_variables(variables)
50
81
  arguments[:positional].each { |v| Attribute.bind_variables(v, variables) }
51
82
  arguments[:keyword].each_value { |v| Attribute.bind_variables(v, variables) }
@@ -60,17 +91,11 @@ module SpecForge
60
91
  # This is to allow inheriting classes to normalize their arguments before
61
92
  # they are converted to Attributes
62
93
  #
94
+ # @private
95
+ #
63
96
  def prepare_arguments!
64
97
  @arguments = Attribute.from(arguments)
65
98
  end
66
-
67
- def uses_positional_arguments?(method)
68
- method.parameters.any? { |a| [:req, :opt, :rest].include?(a.first) }
69
- end
70
-
71
- def uses_keyword_arguments?(method)
72
- method.parameters.any? { |a| [:keyreq, :key, :keyrest].include?(a.first) }
73
- end
74
99
  end
75
100
  end
76
101
  end
@@ -2,23 +2,62 @@
2
2
 
3
3
  module SpecForge
4
4
  class Attribute
5
+ #
6
+ # Represents a regular expression attribute using Ruby's Regexp class.
7
+ # This class handles the parsing of regex strings from YAML into actual Regexp objects,
8
+ # including support for standard regex flags (m, n, i, x).
9
+ #
10
+ # @example Basic usage in YAML
11
+ # matcher: /pattern/i # Case-insensitive matching
12
+ # email: /@/ # Simple pattern matching
13
+ # slug: /^[a-z0-9-]+$/ # Pattern with start/end anchors
14
+ #
15
+ # @example With flags
16
+ # description: /hello world/i # Case-insensitive match using 'i' flag
17
+ # text_block: /^hello\s+\w+/m # Multi-line match using 'm' flag
18
+ # mixed: /complex pattern/imx # Multiple flags: case-insensitive, multi-line, extended mode
19
+ #
5
20
  class Regex < Attribute
21
+ #
22
+ # Regular expression pattern that matches attribute keywords with this prefix
23
+ # Used for identifying this attribute type during parsing
24
+ #
25
+ # @return [Regexp]
26
+ #
6
27
  KEYWORD_REGEX = /^\/(?<content>[\s\S]+)\/(?<flags>[mnix\s]*)$/i
7
28
 
29
+ #
30
+ # The parsed Regexp object
31
+ #
32
+ # @return [Regexp]
33
+ #
8
34
  attr_reader :value
9
35
 
36
+ alias_method :resolved, :value
37
+ alias_method :resolve, :value
38
+
39
+ #
40
+ # Creates a new regex attribute by parsing the input string
41
+ #
42
+ # @param input [String] The regular expression pattern as a string
43
+ #
10
44
  def initialize(input)
11
45
  super
12
46
 
13
47
  @value = parse_regex(input)
14
48
  end
15
49
 
16
- def resolve
17
- @value
18
- end
19
-
20
50
  private
21
51
 
52
+ #
53
+ # Parses a regex string into a Regexp object
54
+ #
55
+ # @param input [String] The string representation of the regex (e.g., "/pattern/i")
56
+ #
57
+ # @return [Regexp] The compiled regular expression
58
+ #
59
+ # @private
60
+ #
22
61
  def parse_regex(input)
23
62
  match = input.match(KEYWORD_REGEX)
24
63
  captures = match.named_captures.symbolize_keys
@@ -27,7 +66,18 @@ module SpecForge
27
66
  Regexp.new(captures[:content], flags)
28
67
  end
29
68
 
30
- # I would've used Regexp.new(string, string), but it raises when "n" is provided as a flag
69
+ #
70
+ # Parses regex flags from a string into Regexp option bits
71
+ # Supports i (case insensitive), m (multiline), x (extended), and n (no encoding)
72
+ #
73
+ # @param flags [String] A string containing the flags (e.g., "imx")
74
+ #
75
+ # @return [Integer] The combined flag options as a bitmask
76
+ #
77
+ # @raise [ArgumentError] If an unknown regex flag is provided
78
+ #
79
+ # @private
80
+ #
31
81
  def parse_flags(flags)
32
82
  return 0 if flags.blank?
33
83
 
@@ -3,16 +3,59 @@
3
3
  module SpecForge
4
4
  class Attribute
5
5
  #
6
- # Helpers for ResolvableHash and ResolvableArray
6
+ # Provides helper methods for resolving attributes
7
+ #
8
+ # This module contains shared logic for handling attribute resolution
9
+ # in collection types. It defines procs that can be used with map and
10
+ # transform operations to recursively resolve nested attributes.
7
11
  #
8
12
  module Resolvable
9
- # @private
10
- def resolvable_proc
13
+ #
14
+ # Returns a proc that resolves objects to their cached final values.
15
+ # For objects that respond to #resolved, calls that method.
16
+ # For other objects, simply returns them unchanged.
17
+ #
18
+ # @return [Proc] A proc for resolving objects to their cached final values
19
+ #
20
+ # @example
21
+ # proc = resolved_proc
22
+ # proc.call(Attribute::Faker.new("faker.name.name")) # => "Jane Doe" (cached)
23
+ # proc.call("already resolved") # => "already resolved" (unchanged)
24
+ #
25
+ def resolved_proc
26
+ ->(v) { v.respond_to?(:resolved) ? v.resolved : v }
27
+ end
28
+
29
+ #
30
+ # Returns a proc that freshly resolves objects.
31
+ # For objects that respond to #resolve, calls that method.
32
+ # For other objects, simply returns them unchanged.
33
+ #
34
+ # @return [Proc] A proc for freshly resolving objects
35
+ #
36
+ # @example
37
+ # proc = resolve_proc
38
+ # proc.call(Attribute::Faker.new("faker.name.name")) # => "John Smith" (fresh)
39
+ # proc.call("already resolved") # => "already resolved" (unchanged)
40
+ #
41
+ def resolve_proc
11
42
  ->(v) { v.respond_to?(:resolve) ? v.resolve : v }
12
43
  end
13
44
 
14
- def resolvable_value_proc
15
- ->(v) { v.respond_to?(:resolve_value) ? v.resolve_value : v }
45
+ #
46
+ # Returns a proc that resolves attributes into their matcher form.
47
+ # For objects that respond to #resolve_as_matcher, calls that method.
48
+ # For other objects, simply returns them unchanged.
49
+ #
50
+ # @return [Proc] A proc for resolving attributes to matchers
51
+ #
52
+ # @example
53
+ # proc = resolve_as_matcher_proc
54
+ # proc.call(Attribute::Faker.new("faker.name.name")) # => eq("John Doe")
55
+ # proc.call(Attribute::Regex.new("/hello/")) # => match(/hello/)
56
+ #
57
+ def resolve_as_matcher_proc
58
+ ->(v) { v.respond_to?(:resolve_as_matcher) ? v.resolve_as_matcher : v }
16
59
  end
17
60
  end
18
61
  end