spec_forge 0.5.0 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (116) hide show
  1. checksums.yaml +4 -4
  2. data/.standard.yml +3 -3
  3. data/CHANGELOG.md +217 -2
  4. data/README.md +162 -25
  5. data/flake.lock +3 -3
  6. data/flake.nix +11 -5
  7. data/lib/spec_forge/attribute/chainable.rb +208 -20
  8. data/lib/spec_forge/attribute/factory.rb +92 -15
  9. data/lib/spec_forge/attribute/faker.rb +62 -13
  10. data/lib/spec_forge/attribute/global.rb +96 -0
  11. data/lib/spec_forge/attribute/literal.rb +15 -2
  12. data/lib/spec_forge/attribute/matcher.rb +186 -11
  13. data/lib/spec_forge/attribute/parameterized.rb +45 -12
  14. data/lib/spec_forge/attribute/regex.rb +55 -5
  15. data/lib/spec_forge/attribute/resolvable.rb +48 -5
  16. data/lib/spec_forge/attribute/resolvable_array.rb +62 -4
  17. data/lib/spec_forge/attribute/resolvable_hash.rb +62 -4
  18. data/lib/spec_forge/attribute/store.rb +65 -0
  19. data/lib/spec_forge/attribute/transform.rb +33 -5
  20. data/lib/spec_forge/attribute/variable.rb +37 -6
  21. data/lib/spec_forge/attribute.rb +166 -66
  22. data/lib/spec_forge/backtrace_formatter.rb +26 -3
  23. data/lib/spec_forge/callbacks.rb +88 -0
  24. data/lib/spec_forge/cli/actions.rb +27 -0
  25. data/lib/spec_forge/cli/command.rb +78 -24
  26. data/lib/spec_forge/cli/docs/generate.rb +72 -0
  27. data/lib/spec_forge/cli/docs.rb +92 -0
  28. data/lib/spec_forge/cli/init.rb +51 -9
  29. data/lib/spec_forge/cli/new.rb +67 -6
  30. data/lib/spec_forge/cli/run.rb +32 -4
  31. data/lib/spec_forge/cli/serve.rb +155 -0
  32. data/lib/spec_forge/cli.rb +26 -7
  33. data/lib/spec_forge/configuration.rb +96 -24
  34. data/lib/spec_forge/context/callbacks.rb +91 -0
  35. data/lib/spec_forge/context/global.rb +72 -0
  36. data/lib/spec_forge/context/store.rb +131 -0
  37. data/lib/spec_forge/context/variables.rb +91 -0
  38. data/lib/spec_forge/context.rb +36 -0
  39. data/lib/spec_forge/core_ext/array.rb +27 -0
  40. data/lib/spec_forge/core_ext/rspec.rb +22 -4
  41. data/lib/spec_forge/documentation/builder.rb +383 -0
  42. data/lib/spec_forge/documentation/document/operation.rb +47 -0
  43. data/lib/spec_forge/documentation/document/parameter.rb +22 -0
  44. data/lib/spec_forge/documentation/document/request_body.rb +24 -0
  45. data/lib/spec_forge/documentation/document/response.rb +39 -0
  46. data/lib/spec_forge/documentation/document/response_body.rb +27 -0
  47. data/lib/spec_forge/documentation/document.rb +48 -0
  48. data/lib/spec_forge/documentation/generators/base.rb +81 -0
  49. data/lib/spec_forge/documentation/generators/openapi/base.rb +100 -0
  50. data/lib/spec_forge/documentation/generators/openapi/error_formatter.rb +149 -0
  51. data/lib/spec_forge/documentation/generators/openapi/v3_0.rb +65 -0
  52. data/lib/spec_forge/documentation/generators/openapi.rb +59 -0
  53. data/lib/spec_forge/documentation/generators.rb +17 -0
  54. data/lib/spec_forge/documentation/loader/cache.rb +138 -0
  55. data/lib/spec_forge/documentation/loader.rb +159 -0
  56. data/lib/spec_forge/documentation/openapi/base.rb +33 -0
  57. data/lib/spec_forge/documentation/openapi/v3_0/example.rb +44 -0
  58. data/lib/spec_forge/documentation/openapi/v3_0/media_type.rb +42 -0
  59. data/lib/spec_forge/documentation/openapi/v3_0/operation.rb +175 -0
  60. data/lib/spec_forge/documentation/openapi/v3_0/response.rb +65 -0
  61. data/lib/spec_forge/documentation/openapi/v3_0/schema.rb +80 -0
  62. data/lib/spec_forge/documentation/openapi/v3_0/tag.rb +71 -0
  63. data/lib/spec_forge/documentation/openapi.rb +23 -0
  64. data/lib/spec_forge/documentation.rb +27 -0
  65. data/lib/spec_forge/error.rb +284 -113
  66. data/lib/spec_forge/factory.rb +35 -16
  67. data/lib/spec_forge/filter.rb +86 -0
  68. data/lib/spec_forge/forge.rb +171 -0
  69. data/lib/spec_forge/http/backend.rb +101 -29
  70. data/lib/spec_forge/http/client.rb +23 -13
  71. data/lib/spec_forge/http/request.rb +85 -62
  72. data/lib/spec_forge/http/verb.rb +79 -0
  73. data/lib/spec_forge/http.rb +105 -0
  74. data/lib/spec_forge/loader.rb +244 -0
  75. data/lib/spec_forge/matchers.rb +130 -0
  76. data/lib/spec_forge/normalizer/default.rb +51 -0
  77. data/lib/spec_forge/normalizer/definition.rb +248 -0
  78. data/lib/spec_forge/normalizer/validators.rb +99 -0
  79. data/lib/spec_forge/normalizer.rb +486 -115
  80. data/lib/spec_forge/normalizers/_shared.yml +74 -0
  81. data/lib/spec_forge/normalizers/configuration.yml +23 -0
  82. data/lib/spec_forge/normalizers/constraint.yml +8 -0
  83. data/lib/spec_forge/normalizers/expectation.yml +47 -0
  84. data/lib/spec_forge/normalizers/factory.yml +12 -0
  85. data/lib/spec_forge/normalizers/factory_reference.yml +15 -0
  86. data/lib/spec_forge/normalizers/global_context.yml +28 -0
  87. data/lib/spec_forge/normalizers/spec.yml +50 -0
  88. data/lib/spec_forge/runner/adapter.rb +183 -0
  89. data/lib/spec_forge/runner/callbacks.rb +246 -0
  90. data/lib/spec_forge/runner/debug_proxy.rb +213 -0
  91. data/lib/spec_forge/runner/listener.rb +54 -0
  92. data/lib/spec_forge/runner/metadata.rb +58 -0
  93. data/lib/spec_forge/runner/state.rb +98 -0
  94. data/lib/spec_forge/runner.rb +50 -125
  95. data/lib/spec_forge/spec/expectation/constraint.rb +100 -21
  96. data/lib/spec_forge/spec/expectation.rb +47 -51
  97. data/lib/spec_forge/spec.rb +50 -108
  98. data/lib/spec_forge/type.rb +36 -4
  99. data/lib/spec_forge/version.rb +4 -1
  100. data/lib/spec_forge.rb +168 -76
  101. data/lib/templates/openapi.yml.tt +22 -0
  102. data/lib/templates/redoc.html.tt +28 -0
  103. data/lib/templates/swagger.html.tt +59 -0
  104. metadata +109 -16
  105. data/lib/spec_forge/normalizer/configuration.rb +0 -77
  106. data/lib/spec_forge/normalizer/constraint.rb +0 -47
  107. data/lib/spec_forge/normalizer/expectation.rb +0 -86
  108. data/lib/spec_forge/normalizer/factory.rb +0 -65
  109. data/lib/spec_forge/normalizer/factory_reference.rb +0 -71
  110. data/lib/spec_forge/normalizer/spec.rb +0 -74
  111. data/spec_forge/factories/user.yml +0 -4
  112. data/spec_forge/forge_helper.rb +0 -48
  113. data/spec_forge/specs/users.yml +0 -65
  114. /data/lib/templates/{forge_helper.tt → forge_helper.rb.tt} +0 -0
  115. /data/lib/templates/{new_factory.tt → new_factory.yml.tt} +0 -0
  116. /data/lib/templates/{new_spec.tt → new_spec.yml.tt} +0 -0
@@ -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,18 +5,21 @@ 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
21
24
  include Resolvable
22
25
 
@@ -66,7 +69,7 @@ module SpecForge
66
69
  end
67
70
 
68
71
  #
69
- # Creates an Attribute instance from a string, handling any macros
72
+ # Creates an Attribute instance from a string
70
73
  #
71
74
  # @param string [String] The input string
72
75
  #
@@ -75,24 +78,31 @@ module SpecForge
75
78
  # @private
76
79
  #
77
80
  def self.from_string(string)
78
- case string
79
- when Faker::KEYWORD_REGEX
80
- Faker.new(string)
81
- when Variable::KEYWORD_REGEX
82
- Variable.new(string)
83
- when Matcher::KEYWORD_REGEX
84
- Matcher.new(string)
85
- when Factory::KEYWORD_REGEX
86
- Factory.new(string)
87
- when Regex::KEYWORD_REGEX
88
- Regex.new(string)
89
- else
90
- Literal.new(string)
91
- 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)
92
102
  end
93
103
 
94
104
  #
95
- # Creates an Attribute instance from a hash, handling any macros
105
+ # Creates an Attribute instance from a hash
96
106
  #
97
107
  # @param hash [Hash] The input hash
98
108
  #
@@ -118,66 +128,156 @@ module SpecForge
118
128
  end
119
129
  end
120
130
 
131
+ #
132
+ # The original input value
133
+ #
134
+ # @return [Object]
135
+ #
121
136
  attr_reader :input
122
137
 
123
138
  #
124
- # @param input [Object] Anything
139
+ # Creates a new attribute
140
+ #
141
+ # @param input [Object] The original input value
125
142
  #
126
143
  def initialize(input)
127
144
  @input = input
128
145
  end
129
146
 
130
147
  #
131
- # Returns the processed value of the input
148
+ # Compares this attributes input to other
149
+ #
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.
132
169
  #
133
- # For literals, this is the input itself.
134
- # For generated values (Faker, Transform), this is the result of their operations.
170
+ # This returns an intermediate representation - for fully resolved values, use #resolve instead.
135
171
  #
136
172
  # @return [Object] The processed value of this attribute
137
173
  #
138
174
  # @raise [RuntimeError] if not implemented by subclass
139
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
+ #
140
181
  def value
141
182
  raise "not implemented"
142
183
  end
143
184
 
144
185
  #
145
- # 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.
146
188
  #
147
- # @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.
148
191
  #
149
- # @example Simple literal
150
- # attr = Attribute::Literal.new("hello")
151
- # attr.resolve # => "hello"
192
+ # @return [Object] The completely resolved value with cached results
152
193
  #
153
- # @example Nested array with faker
154
- # attr = Attribute::Literal.new(["faker.number.positive", ["faker.name.first_name"]])
155
- # 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)
156
198
  #
157
- def resolve
158
- @resolved ||= resolve_value
199
+ def resolved
200
+ @resolved ||= resolve
159
201
  end
160
202
 
161
- def resolve_value
162
- __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
163
226
  end
164
227
 
165
228
  #
166
- # 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
167
236
  #
168
- # @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.
169
239
  #
170
- # @return [Boolean]
240
+ # @return [RSpec::Matchers::BuiltIn::BaseMatcher] A matcher representing this attribute
171
241
  #
172
- def ==(other)
173
- other =
174
- if other.is_a?(Attribute)
175
- 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)
176
261
  else
177
- other
262
+ methods.eq([])
178
263
  end
264
+ when Hash, HashLike
265
+ resolved_hash = resolved.transform_values(&resolve_as_matcher_proc).stringify_keys
179
266
 
180
- 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
181
281
  end
182
282
 
183
283
  #
@@ -185,20 +285,20 @@ module SpecForge
185
285
  #
186
286
  # @param variables [Hash] A hash of variable attributes
187
287
  #
188
- def bind_variables(_variables)
189
- end
190
-
191
- protected
192
-
193
- def __resolve(value)
194
- case value
195
- when ArrayLike
196
- value.map(&resolvable_proc)
197
- when HashLike
198
- value.transform_values(&resolvable_proc)
199
- else
200
- value
201
- end
288
+ def bind_variables(variables)
202
289
  end
203
290
  end
204
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"
@@ -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