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.
- checksums.yaml +4 -4
- data/.standard.yml +3 -3
- data/CHANGELOG.md +217 -2
- data/README.md +162 -25
- data/flake.lock +3 -3
- data/flake.nix +11 -5
- data/lib/spec_forge/attribute/chainable.rb +208 -20
- data/lib/spec_forge/attribute/factory.rb +92 -15
- data/lib/spec_forge/attribute/faker.rb +62 -13
- data/lib/spec_forge/attribute/global.rb +96 -0
- data/lib/spec_forge/attribute/literal.rb +15 -2
- data/lib/spec_forge/attribute/matcher.rb +186 -11
- data/lib/spec_forge/attribute/parameterized.rb +45 -12
- data/lib/spec_forge/attribute/regex.rb +55 -5
- data/lib/spec_forge/attribute/resolvable.rb +48 -5
- data/lib/spec_forge/attribute/resolvable_array.rb +62 -4
- data/lib/spec_forge/attribute/resolvable_hash.rb +62 -4
- data/lib/spec_forge/attribute/store.rb +65 -0
- data/lib/spec_forge/attribute/transform.rb +33 -5
- data/lib/spec_forge/attribute/variable.rb +37 -6
- data/lib/spec_forge/attribute.rb +166 -66
- data/lib/spec_forge/backtrace_formatter.rb +26 -3
- data/lib/spec_forge/callbacks.rb +88 -0
- data/lib/spec_forge/cli/actions.rb +27 -0
- data/lib/spec_forge/cli/command.rb +78 -24
- data/lib/spec_forge/cli/docs/generate.rb +72 -0
- data/lib/spec_forge/cli/docs.rb +92 -0
- data/lib/spec_forge/cli/init.rb +51 -9
- data/lib/spec_forge/cli/new.rb +67 -6
- data/lib/spec_forge/cli/run.rb +32 -4
- data/lib/spec_forge/cli/serve.rb +155 -0
- data/lib/spec_forge/cli.rb +26 -7
- data/lib/spec_forge/configuration.rb +96 -24
- data/lib/spec_forge/context/callbacks.rb +91 -0
- data/lib/spec_forge/context/global.rb +72 -0
- data/lib/spec_forge/context/store.rb +131 -0
- data/lib/spec_forge/context/variables.rb +91 -0
- data/lib/spec_forge/context.rb +36 -0
- data/lib/spec_forge/core_ext/array.rb +27 -0
- data/lib/spec_forge/core_ext/rspec.rb +22 -4
- data/lib/spec_forge/documentation/builder.rb +383 -0
- data/lib/spec_forge/documentation/document/operation.rb +47 -0
- data/lib/spec_forge/documentation/document/parameter.rb +22 -0
- data/lib/spec_forge/documentation/document/request_body.rb +24 -0
- data/lib/spec_forge/documentation/document/response.rb +39 -0
- data/lib/spec_forge/documentation/document/response_body.rb +27 -0
- data/lib/spec_forge/documentation/document.rb +48 -0
- data/lib/spec_forge/documentation/generators/base.rb +81 -0
- data/lib/spec_forge/documentation/generators/openapi/base.rb +100 -0
- data/lib/spec_forge/documentation/generators/openapi/error_formatter.rb +149 -0
- data/lib/spec_forge/documentation/generators/openapi/v3_0.rb +65 -0
- data/lib/spec_forge/documentation/generators/openapi.rb +59 -0
- data/lib/spec_forge/documentation/generators.rb +17 -0
- data/lib/spec_forge/documentation/loader/cache.rb +138 -0
- data/lib/spec_forge/documentation/loader.rb +159 -0
- data/lib/spec_forge/documentation/openapi/base.rb +33 -0
- data/lib/spec_forge/documentation/openapi/v3_0/example.rb +44 -0
- data/lib/spec_forge/documentation/openapi/v3_0/media_type.rb +42 -0
- data/lib/spec_forge/documentation/openapi/v3_0/operation.rb +175 -0
- data/lib/spec_forge/documentation/openapi/v3_0/response.rb +65 -0
- data/lib/spec_forge/documentation/openapi/v3_0/schema.rb +80 -0
- data/lib/spec_forge/documentation/openapi/v3_0/tag.rb +71 -0
- data/lib/spec_forge/documentation/openapi.rb +23 -0
- data/lib/spec_forge/documentation.rb +27 -0
- data/lib/spec_forge/error.rb +284 -113
- data/lib/spec_forge/factory.rb +35 -16
- data/lib/spec_forge/filter.rb +86 -0
- data/lib/spec_forge/forge.rb +171 -0
- data/lib/spec_forge/http/backend.rb +101 -29
- data/lib/spec_forge/http/client.rb +23 -13
- data/lib/spec_forge/http/request.rb +85 -62
- data/lib/spec_forge/http/verb.rb +79 -0
- data/lib/spec_forge/http.rb +105 -0
- data/lib/spec_forge/loader.rb +244 -0
- data/lib/spec_forge/matchers.rb +130 -0
- data/lib/spec_forge/normalizer/default.rb +51 -0
- data/lib/spec_forge/normalizer/definition.rb +248 -0
- data/lib/spec_forge/normalizer/validators.rb +99 -0
- data/lib/spec_forge/normalizer.rb +486 -115
- data/lib/spec_forge/normalizers/_shared.yml +74 -0
- data/lib/spec_forge/normalizers/configuration.yml +23 -0
- data/lib/spec_forge/normalizers/constraint.yml +8 -0
- data/lib/spec_forge/normalizers/expectation.yml +47 -0
- data/lib/spec_forge/normalizers/factory.yml +12 -0
- data/lib/spec_forge/normalizers/factory_reference.yml +15 -0
- data/lib/spec_forge/normalizers/global_context.yml +28 -0
- data/lib/spec_forge/normalizers/spec.yml +50 -0
- data/lib/spec_forge/runner/adapter.rb +183 -0
- data/lib/spec_forge/runner/callbacks.rb +246 -0
- data/lib/spec_forge/runner/debug_proxy.rb +213 -0
- data/lib/spec_forge/runner/listener.rb +54 -0
- data/lib/spec_forge/runner/metadata.rb +58 -0
- data/lib/spec_forge/runner/state.rb +98 -0
- data/lib/spec_forge/runner.rb +50 -125
- data/lib/spec_forge/spec/expectation/constraint.rb +100 -21
- data/lib/spec_forge/spec/expectation.rb +47 -51
- data/lib/spec_forge/spec.rb +50 -108
- data/lib/spec_forge/type.rb +36 -4
- data/lib/spec_forge/version.rb +4 -1
- data/lib/spec_forge.rb +168 -76
- data/lib/templates/openapi.yml.tt +22 -0
- data/lib/templates/redoc.html.tt +28 -0
- data/lib/templates/swagger.html.tt +59 -0
- metadata +109 -16
- data/lib/spec_forge/normalizer/configuration.rb +0 -77
- data/lib/spec_forge/normalizer/constraint.rb +0 -47
- data/lib/spec_forge/normalizer/expectation.rb +0 -86
- data/lib/spec_forge/normalizer/factory.rb +0 -65
- data/lib/spec_forge/normalizer/factory_reference.rb +0 -71
- data/lib/spec_forge/normalizer/spec.rb +0 -74
- data/spec_forge/factories/user.yml +0 -4
- data/spec_forge/forge_helper.rb +0 -48
- data/spec_forge/specs/users.yml +0 -65
- /data/lib/templates/{forge_helper.tt → forge_helper.rb.tt} +0 -0
- /data/lib/templates/{new_factory.tt → new_factory.yml.tt} +0 -0
- /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
|
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(&
|
54
|
+
value.transform_values(&resolve_proc)
|
17
55
|
end
|
18
56
|
|
19
|
-
|
20
|
-
|
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
|
-
#
|
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].
|
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
|
6
|
+
# Represents an attribute that references a variable
|
7
7
|
#
|
8
|
-
#
|
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
|
-
|
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
|
-
@
|
47
|
+
@variable = variables[variable_name]
|
48
|
+
end
|
24
49
|
|
25
|
-
|
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
|
data/lib/spec_forge/attribute.rb
CHANGED
@@ -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
|
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
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
Regex
|
89
|
-
|
90
|
-
|
91
|
-
|
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
|
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
|
-
#
|
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
|
-
#
|
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
|
-
#
|
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
|
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
|
-
#
|
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
|
-
# @
|
150
|
-
# attr = Attribute::Literal.new("hello")
|
151
|
-
# attr.resolve # => "hello"
|
192
|
+
# @return [Object] The completely resolved value with cached results
|
152
193
|
#
|
153
|
-
# @example
|
154
|
-
#
|
155
|
-
#
|
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
|
158
|
-
@resolved ||=
|
199
|
+
def resolved
|
200
|
+
@resolved ||= resolve
|
159
201
|
end
|
160
202
|
|
161
|
-
|
162
|
-
|
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
|
-
#
|
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
|
-
#
|
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 [
|
240
|
+
# @return [RSpec::Matchers::BuiltIn::BaseMatcher] A matcher representing this attribute
|
171
241
|
#
|
172
|
-
|
173
|
-
|
174
|
-
|
175
|
-
|
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
|
-
|
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
|
-
|
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(
|
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
|
-
#
|
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
|