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
@@ -3,23 +3,81 @@
3
3
  module SpecForge
4
4
  class Attribute
5
5
  #
6
- # Represents an Array that may contain Attributes
6
+ # Represents an array that may contain attributes that need resolution
7
+ #
8
+ # This delegator wraps an array and provides methods to recursively resolve
9
+ # any attribute objects contained within it. It allows arrays to contain
10
+ # dynamic content like variables and faker values.
11
+ #
12
+ # @example In code
13
+ # array = [1, Attribute::Variable.new("variables.user_id"), 3]
14
+ # resolvable = Attribute::ResolvableArray.new(array)
15
+ # resolvable.resolved # => [1, 42, 3] # assuming user_id resolves to 42
7
16
  #
8
17
  class ResolvableArray < SimpleDelegator
9
18
  include Resolvable
10
19
 
20
+ #
21
+ # Returns the underlying array
22
+ #
23
+ # @return [Array] The delegated array
24
+ #
11
25
  def value
12
26
  __getobj__
13
27
  end
14
28
 
29
+ #
30
+ # Returns a new array with all items fully resolved to their final values.
31
+ # Uses the cached version of each item if available.
32
+ #
33
+ # @return [Array] A new array with all items fully resolved to their final values
34
+ #
35
+ # @example
36
+ # array_attr = Attribute::ResolvableArray.new([Attribute::Faker.new("faker.name.name")])
37
+ # array_attr.resolved # => ["Jane Doe"] (with result cached)
38
+ #
39
+ def resolved
40
+ value.map(&resolved_proc)
41
+ end
42
+
43
+ #
44
+ # Freshly resolves all items in the array.
45
+ # Unlike #resolved, this doesn't use cached values, ensuring fresh resolution.
46
+ #
47
+ # @return [Array] A new array with all items freshly resolved
48
+ #
49
+ # @example
50
+ # array_attr = Attribute::ResolvableArray.new([Attribute::Faker.new("faker.name.name")])
51
+ # array_attr.resolve # => ["John Smith"] (fresh value each time)
52
+ #
15
53
  def resolve
16
- value.map(&resolvable_proc)
54
+ value.map(&resolve_proc)
17
55
  end
18
56
 
19
- def resolve_value
20
- value.map(&resolvable_value_proc)
57
+ #
58
+ # Converts all items in the array to RSpec matchers.
59
+ # First converts each array element to a matcher using resolve_as_matcher_proc,
60
+ # then wraps the entire result in a matcher suitable for array comparison.
61
+ #
62
+ # This ensures all elements in the array are proper matchers,
63
+ # which is essential for compound matchers and proper failure messages.
64
+ #
65
+ # @return [RSpec::Matchers::BuiltIn::BaseMatcher] A matcher for this array
66
+ #
67
+ # @example
68
+ # array = Attribute::ResolvableArray.new(["test", /pattern/, 42])
69
+ # array.resolve_as_matcher # => contain_exactly(eq("test"), match(/pattern/), eq(42))
70
+ #
71
+ def resolve_as_matcher
72
+ result = value.map(&resolve_as_matcher_proc)
73
+ Attribute::Literal.new(result).resolve_as_matcher
21
74
  end
22
75
 
76
+ #
77
+ # Binds variables to any attribute objects in the array
78
+ #
79
+ # @param variables [Hash] The variables to bind
80
+ #
23
81
  def bind_variables(variables)
24
82
  value.each { |v| Attribute.bind_variables(v, variables) }
25
83
  end
@@ -3,23 +3,81 @@
3
3
  module SpecForge
4
4
  class Attribute
5
5
  #
6
- # Represents a hash that may contain Attributes
6
+ # Represents a hash that may contain attributes that need resolution
7
+ #
8
+ # This delegator wraps a hash and provides methods to recursively resolve
9
+ # any attribute objects contained within it. It allows hashes to contain
10
+ # dynamic content like variables and faker values.
11
+ #
12
+ # @example In code
13
+ # hash = {name: Attribute::Faker.new("faker.name.name"), id: 123}
14
+ # resolvable = Attribute::ResolvableHash.new(hash)
15
+ # resolvable.resolved # => {name: "John Smith", id: 123}
7
16
  #
8
17
  class ResolvableHash < SimpleDelegator
9
18
  include Resolvable
10
19
 
20
+ #
21
+ # Returns the underlying hash
22
+ #
23
+ # @return [Hash] The delegated hash
24
+ #
11
25
  def value
12
26
  __getobj__
13
27
  end
14
28
 
29
+ #
30
+ # Returns a new hash with all values fully resolved to their final values.
31
+ # Uses the cached version of each value if available.
32
+ #
33
+ # @return [Hash] A new hash with all values fully resolved to their final values
34
+ #
35
+ # @example
36
+ # hash_attr = Attribute::ResolvableHash.new({name: Attribute::Faker.new("faker.name.name")})
37
+ # hash_attr.resolved # => {name: "Jane Doe"} (with result cached)
38
+ #
39
+ def resolved
40
+ value.transform_values(&resolved_proc)
41
+ end
42
+
43
+ #
44
+ # Freshly resolves all values in the hash.
45
+ # Unlike #resolved, this doesn't use cached values, ensuring fresh resolution.
46
+ #
47
+ # @return [Hash] A new hash with all values freshly resolved
48
+ #
49
+ # @example
50
+ # hash_attr = Attribute::ResolvableHash.new({name: Attribute::Faker.new("faker.name.name")})
51
+ # hash_attr.resolve # => {name: "John Smith"} (fresh value each time)
52
+ #
15
53
  def resolve
16
- value.transform_values(&resolvable_proc)
54
+ value.transform_values(&resolve_proc)
17
55
  end
18
56
 
19
- def resolve_value
20
- value.transform_values(&resolvable_value_proc)
57
+ #
58
+ # Converts all values in the hash to RSpec matchers.
59
+ # Transforms each hash value to a matcher using resolve_as_matcher_proc,
60
+ # then wraps the entire result in a matcher suitable for hash comparison.
61
+ #
62
+ # This ensures proper nesting of matchers in hash structures,
63
+ # which is vital for readable failure messages in complex expectations.
64
+ #
65
+ # @return [RSpec::Matchers::BuiltIn::BaseMatcher] A matcher for this hash
66
+ #
67
+ # @example
68
+ # hash = Attribute::ResolvableHash.new({name: "Test", age: 42})
69
+ # hash.resolve_as_matcher # => include("name" => eq("Test"), "age" => eq(42))
70
+ #
71
+ def resolve_as_matcher
72
+ result = value.transform_values(&resolve_as_matcher_proc)
73
+ Attribute::Literal.new(result).resolve_as_matcher
21
74
  end
22
75
 
76
+ #
77
+ # Binds variables to any attribute objects in the hash values
78
+ #
79
+ # @param variables [Hash] The variables to bind
80
+ #
23
81
  def bind_variables(variables)
24
82
  value.each_value { |v| Attribute.bind_variables(v, variables) }
25
83
  end
@@ -0,0 +1,65 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SpecForge
4
+ class Attribute
5
+ #
6
+ # Represents an attribute that references values from stored test results
7
+ #
8
+ # This class allows accessing data from previous test executions that were
9
+ # saved using the `store_as` directive. It provides access to response data
10
+ # including status, headers, and body from previously run expectations.
11
+ #
12
+ # @example Basic usage in YAML
13
+ # create_user:
14
+ # path: /users
15
+ # method: post
16
+ # expectations:
17
+ # - store_as: new_user
18
+ # body:
19
+ # name: faker.name.name
20
+ # expect:
21
+ # status: 201
22
+ #
23
+ # get_user:
24
+ # path: /users/{id}
25
+ # expectations:
26
+ # - query:
27
+ # id: store.new_user.body.id
28
+ # expect:
29
+ # status: 200
30
+ #
31
+ # @example Accessing specific response components
32
+ # check_status:
33
+ # path: /health
34
+ # expectations:
35
+ # - variables:
36
+ # expected_status: store.new_user.status
37
+ # auth_token: store.new_user.headers.authorization
38
+ # user_name: store.new_user.body.user.name
39
+ # expect:
40
+ # status: 200
41
+ #
42
+ class Store < Attribute
43
+ include Chainable
44
+
45
+ #
46
+ # Regular expression pattern that matches attribute keywords with this prefix
47
+ # Used for identifying this attribute type during parsing
48
+ #
49
+ # @return [Regexp]
50
+ #
51
+ KEYWORD_REGEX = /^store\./i
52
+
53
+ alias_method :stored_id, :header
54
+
55
+ #
56
+ # Returns the base object for the variable chain
57
+ #
58
+ # @return [Context::Store::Entry, nil] The stored entry or nil if not found
59
+ #
60
+ def base_object
61
+ @base_object ||= SpecForge.context.store[stored_id.to_s]
62
+ end
63
+ end
64
+ end
65
+ end
@@ -2,9 +2,34 @@
2
2
 
3
3
  module SpecForge
4
4
  class Attribute
5
+ #
6
+ # Represents an attribute that transforms other attributes
7
+ #
8
+ # This class provides transformation functions like join that can be applied
9
+ # to other attributes or values. It allows complex data manipulation without
10
+ # writing Ruby code.
11
+ #
12
+ # @example Join transformation in YAML
13
+ # full_name:
14
+ # transform.join:
15
+ # - variables.first_name
16
+ # - " "
17
+ # - variables.last_name
18
+ #
5
19
  class Transform < Parameterized
20
+ #
21
+ # Regular expression pattern that matches attribute keywords with this prefix
22
+ # Used for identifying this attribute type during parsing
23
+ #
24
+ # @return [Regexp]
25
+ #
6
26
  KEYWORD_REGEX = /^transform\./i
7
27
 
28
+ #
29
+ # The available transformation methods
30
+ #
31
+ # @return [Array<String>]
32
+ #
8
33
  TRANSFORM_METHODS = %w[
9
34
  join
10
35
  ].freeze
@@ -12,9 +37,7 @@ module SpecForge
12
37
  attr_reader :function
13
38
 
14
39
  #
15
- # Represents any attribute that is a transform call
16
- #
17
- # transform.<function>
40
+ # Creates a new transform attribute with the specified function and arguments
18
41
  #
19
42
  def initialize(...)
20
43
  super
@@ -22,16 +45,21 @@ module SpecForge
22
45
  # Remove prefix
23
46
  @function = @input.sub("transform.", "")
24
47
 
25
- raise InvalidTransformFunctionError, input unless TRANSFORM_METHODS.include?(function)
48
+ raise Error::InvalidTransformFunctionError, input unless TRANSFORM_METHODS.include?(function)
26
49
 
27
50
  prepare_arguments!
28
51
  end
29
52
 
53
+ #
54
+ # Returns the result of applying the transformation function
55
+ #
56
+ # @return [Object] The transformed value
57
+ #
30
58
  def value
31
59
  case function
32
60
  when "join"
33
61
  # Technically supports any attribute, but I ain't gonna test all them edge cases
34
- arguments[:positional].resolve.join
62
+ arguments[:positional].resolved.join
35
63
  end
36
64
  end
37
65
  end
@@ -3,26 +3,57 @@
3
3
  module SpecForge
4
4
  class Attribute
5
5
  #
6
- # Represents any attribute that is a variable reference
6
+ # Represents an attribute that references a variable
7
7
  #
8
- # variables.<variable_name>
8
+ # This class allows referencing variables defined in the test context.
9
+ # It supports chained access to methods and properties of variable values.
10
+ #
11
+ # @example Basic usage in YAML
12
+ # user_id: variables.user.id
13
+ # company_name: variables.company.name
14
+ #
15
+ # @example Nested access in YAML
16
+ # post_author: variables.post.comments.first.author.name
9
17
  #
10
18
  class Variable < Attribute
11
19
  include Chainable
12
20
 
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
+ #
13
27
  KEYWORD_REGEX = /^variables\./i
14
28
 
15
29
  alias_method :variable_name, :header
16
30
 
31
+ #
32
+ # Binds the referenced variable to this attribute
33
+ #
34
+ # @param variables [Hash] A hash of variables to look up in
35
+ #
36
+ # @raise [Error::MissingVariableError] If the variable is not found
37
+ # @raise [Error::InvalidTypeError] If variables is not a hash
38
+ #
17
39
  def bind_variables(variables)
18
- raise InvalidTypeError.new(variables, Hash, for: "'variables'") unless Type.hash?(variables)
40
+ if !Type.hash?(variables)
41
+ raise Error::InvalidTypeError.new(variables, Hash, for: "'variables'")
42
+ end
19
43
 
20
44
  # Don't nil check here.
21
- raise MissingVariableError, variable_name unless variables.key?(variable_name)
45
+ raise Error::MissingVariableError, variable_name unless variables.key?(variable_name)
22
46
 
23
- @base_object = variables[variable_name]
47
+ @variable = variables[variable_name]
48
+ end
24
49
 
25
- self
50
+ #
51
+ # Returns the base object for the variable chain
52
+ #
53
+ # @return [Object] The variable value
54
+ #
55
+ def base_object
56
+ @variable || bind_variables(SpecForge.context.variables)
26
57
  end
27
58
  end
28
59
  end
@@ -5,19 +5,24 @@ require_relative "attribute/parameterized"
5
5
  require_relative "attribute/chainable"
6
6
  require_relative "attribute/resolvable"
7
7
 
8
- # Doesn't matter
9
- require_relative "attribute/factory"
10
- require_relative "attribute/faker"
11
- require_relative "attribute/literal"
12
- require_relative "attribute/matcher"
13
- require_relative "attribute/regex"
14
- require_relative "attribute/resolvable_array"
15
- require_relative "attribute/resolvable_hash"
16
- require_relative "attribute/transform"
17
- require_relative "attribute/variable"
18
-
19
8
  module SpecForge
9
+ #
10
+ # Base class for all attribute types in SpecForge.
11
+ # Attributes represent values that can be transformed, resolved, or have special meaning
12
+ # in the context of specs and expectations.
13
+ #
14
+ # The Attribute system handles dynamic data generation, variable references,
15
+ # matchers, transformations and other special values in YAML specs.
16
+ #
17
+ # @example Basic usage in YAML
18
+ # username: faker.internet.username # A dynamic faker attribute
19
+ # email: /\w+@\w+\.\w+/ # A regex attribute
20
+ # status: kind_of.integer # A matcher attribute
21
+ # user_id: variables.user.id # A variable reference
22
+ #
20
23
  class Attribute
24
+ include Resolvable
25
+
21
26
  #
22
27
  # Binds variables to Attribute objects
23
28
  #
@@ -64,7 +69,7 @@ module SpecForge
64
69
  end
65
70
 
66
71
  #
67
- # Creates an Attribute instance from a string, handling any macros
72
+ # Creates an Attribute instance from a string
68
73
  #
69
74
  # @param string [String] The input string
70
75
  #
@@ -73,24 +78,31 @@ module SpecForge
73
78
  # @private
74
79
  #
75
80
  def self.from_string(string)
76
- case string
77
- when Faker::KEYWORD_REGEX
78
- Faker.new(string)
79
- when Variable::KEYWORD_REGEX
80
- Variable.new(string)
81
- when Matcher::KEYWORD_REGEX
82
- Matcher.new(string)
83
- when Factory::KEYWORD_REGEX
84
- Factory.new(string)
85
- when Regex::KEYWORD_REGEX
86
- Regex.new(string)
87
- else
88
- Literal.new(string)
89
- end
81
+ klass =
82
+ case string
83
+ when Factory::KEYWORD_REGEX
84
+ Factory
85
+ when Faker::KEYWORD_REGEX
86
+ Faker
87
+ when Global::KEYWORD_REGEX
88
+ Global
89
+ when Matcher::KEYWORD_REGEX
90
+ Matcher
91
+ when Regex::KEYWORD_REGEX
92
+ Regex
93
+ when Store::KEYWORD_REGEX
94
+ Store
95
+ when Variable::KEYWORD_REGEX
96
+ Variable
97
+ else
98
+ Literal
99
+ end
100
+
101
+ klass.new(string)
90
102
  end
91
103
 
92
104
  #
93
- # Creates an Attribute instance from a hash, handling any macros
105
+ # Creates an Attribute instance from a hash
94
106
  #
95
107
  # @param hash [Hash] The input hash
96
108
  #
@@ -116,66 +128,156 @@ module SpecForge
116
128
  end
117
129
  end
118
130
 
131
+ #
132
+ # The original input value
133
+ #
134
+ # @return [Object]
135
+ #
119
136
  attr_reader :input
120
137
 
121
138
  #
122
- # @param input [Object] Anything
139
+ # Creates a new attribute
140
+ #
141
+ # @param input [Object] The original input value
123
142
  #
124
143
  def initialize(input)
125
144
  @input = input
126
145
  end
127
146
 
128
147
  #
129
- # Returns the processed value of the input
148
+ # Compares this attributes input to other
130
149
  #
131
- # For literals, this is the input itself.
132
- # For generated values (Faker, Transform), this is the result of their operations.
150
+ # @param other [Object, Attribute] If another Attribute, the input will be compared
151
+ #
152
+ # @return [Boolean]
153
+ #
154
+ def ==(other)
155
+ other =
156
+ if other.is_a?(Attribute)
157
+ other.input
158
+ else
159
+ other
160
+ end
161
+
162
+ input == other
163
+ end
164
+
165
+ #
166
+ # Returns the processed value of this attribute.
167
+ # Recursively calls #value on underlying attributes, but does NOT resolve
168
+ # all nested structures completely.
169
+ #
170
+ # This returns an intermediate representation - for fully resolved values, use #resolve instead.
133
171
  #
134
172
  # @return [Object] The processed value of this attribute
135
173
  #
136
174
  # @raise [RuntimeError] if not implemented by subclass
137
175
  #
176
+ # @example
177
+ # variable_attr = Attribute::Variable.new("variables.user")
178
+ # variable_attr.value # => User instance, but any attributes of User remain
179
+ # as Attribute objects
180
+ #
138
181
  def value
139
182
  raise "not implemented"
140
183
  end
141
184
 
142
185
  #
143
- # Returns the fully evaluated result, recursively resolving any nested attributes
186
+ # Returns the fully evaluated result with complete recursive resolution.
187
+ # Calls #value internally and then resolves all nested attributes, caching the result.
144
188
  #
145
- # @return [Object] The resolved value
189
+ # Use this when you need the final, fully-resolved value with all nested attributes
190
+ # fully evaluated to their primitive values.
146
191
  #
147
- # @example Simple literal
148
- # attr = Attribute::Literal.new("hello")
149
- # attr.resolve # => "hello"
192
+ # @return [Object] The completely resolved value with cached results
150
193
  #
151
- # @example Nested array with faker
152
- # attr = Attribute::Literal.new(["faker.number.positive", ["faker.name.first_name"]])
153
- # attr.resolve # => [42, ["Jane"]]
194
+ # @example
195
+ # faker_attr = Attribute::Faker.new("faker.name.first_name")
196
+ # faker_attr.resolved # => "Jane" (result is cached in @resolved)
197
+ # faker_attr.resolved # => "Jane" (returns same cached value)
154
198
  #
155
- def resolve
156
- @resolved ||= resolve_value
199
+ def resolved
200
+ @resolved ||= resolve
157
201
  end
158
202
 
159
- def resolve_value
160
- __resolve(value)
203
+ #
204
+ # Performs recursive resolution of the attribute's value.
205
+ # Handles nested arrays and hashes by recursively resolving their elements.
206
+ #
207
+ # Unlike #resolved, this method doesn't cache results and can be used
208
+ # when fresh resolution is needed each time.
209
+ #
210
+ # @return [Object] The recursively resolved value without caching
211
+ #
212
+ # @example
213
+ # hash_attr = Attribute::ResolvableHash.new({name: Attribute::Faker.new("faker.name.name")})
214
+ # hash_attr.resolve # => {name: "John Smith"}
215
+ # hash_attr.resolve # => {name: "Jane Doe"} (different value on each call)
216
+ #
217
+ def resolve
218
+ case value
219
+ when ArrayLike
220
+ value.map(&resolved_proc)
221
+ when HashLike
222
+ value.transform_values(&resolved_proc)
223
+ else
224
+ value
225
+ end
161
226
  end
162
227
 
163
228
  #
164
- # Compares this attributes input to other
229
+ # Converts this attribute to an appropriate RSpec matcher.
230
+ # Handles different types of values by creating the right matcher type:
231
+ # - Arrays become contain_exactly matchers
232
+ # - Hashes become include matchers
233
+ # - Regexp become match matchers
234
+ # - Existing matchers are passed through
235
+ # - Other values become eq matchers
165
236
  #
166
- # @param other [Object, Attribute] If another Attribute, the input will be compared
237
+ # This method is crucial for nested matcher structures and compound matchers
238
+ # like matcher.and that require all values to be proper matchers.
167
239
  #
168
- # @return [Boolean]
240
+ # @return [RSpec::Matchers::BuiltIn::BaseMatcher] A matcher representing this attribute
169
241
  #
170
- def ==(other)
171
- other =
172
- if other.is_a?(Attribute)
173
- other.input
242
+ # @example Converting different values to matchers
243
+ # literal_attr = Attribute::Literal.new("hello")
244
+ # literal_attr.resolve_as_matcher # => eq("hello")
245
+ #
246
+ # array_attr = Attribute::ResolvableArray.new([1, 2, 3])
247
+ # array_attr.resolve_as_matcher # => contain_exactly(eq(1), eq(2), eq(3))
248
+ #
249
+ # hash_attr = Attribute::ResolvableHash.new({name: "Test"})
250
+ # hash_attr.resolve_as_matcher # => include("name" => eq("Test"))
251
+ #
252
+ def resolve_as_matcher
253
+ methods = Attribute::Matcher::MATCHER_METHODS
254
+
255
+ case resolved
256
+ when Array, ArrayLike
257
+ resolved_array = resolved.map(&resolve_as_matcher_proc)
258
+
259
+ if resolved_array.size > 0
260
+ methods.contain_exactly(*resolved_array)
174
261
  else
175
- other
262
+ methods.eq([])
176
263
  end
264
+ when Hash, HashLike
265
+ resolved_hash = resolved.transform_values(&resolve_as_matcher_proc).stringify_keys
177
266
 
178
- input == other
267
+ if resolved_hash.size > 0
268
+ methods.include(**resolved_hash)
269
+ else
270
+ methods.eq({})
271
+ end
272
+ when Attribute::Matcher, Regexp
273
+ methods.match(resolved)
274
+ when RSpec::Matchers::BuiltIn::BaseMatcher,
275
+ RSpec::Matchers::DSL::Matcher,
276
+ Class
277
+ resolved # Pass through
278
+ else
279
+ methods.eq(resolved)
280
+ end
179
281
  end
180
282
 
181
283
  #
@@ -183,20 +285,20 @@ module SpecForge
183
285
  #
184
286
  # @param variables [Hash] A hash of variable attributes
185
287
  #
186
- def bind_variables(_variables)
187
- end
188
-
189
- protected
190
-
191
- def __resolve(value)
192
- case value
193
- when ArrayLike
194
- value.map(&:resolve)
195
- when HashLike
196
- value.transform_values(&:resolve)
197
- else
198
- value
199
- end
288
+ def bind_variables(variables)
200
289
  end
201
290
  end
202
291
  end
292
+
293
+ # Order doesn't matter
294
+ require_relative "attribute/factory"
295
+ require_relative "attribute/faker"
296
+ require_relative "attribute/global"
297
+ require_relative "attribute/literal"
298
+ require_relative "attribute/matcher"
299
+ require_relative "attribute/regex"
300
+ require_relative "attribute/resolvable_array"
301
+ require_relative "attribute/resolvable_hash"
302
+ require_relative "attribute/store"
303
+ require_relative "attribute/transform"
304
+ require_relative "attribute/variable"