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.
- checksums.yaml +4 -4
- data/.standard.yml +4 -0
- data/CHANGELOG.md +145 -1
- data/README.md +49 -638
- data/flake.lock +3 -3
- data/flake.nix +8 -2
- data/lib/spec_forge/attribute/chainable.rb +208 -20
- data/lib/spec_forge/attribute/factory.rb +141 -12
- data/lib/spec_forge/attribute/faker.rb +64 -15
- 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 +188 -13
- data/lib/spec_forge/attribute/parameterized.rb +45 -20
- 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 +168 -66
- data/lib/spec_forge/backtrace_formatter.rb +26 -3
- data/lib/spec_forge/callbacks.rb +79 -0
- data/lib/spec_forge/cli/actions.rb +27 -0
- data/lib/spec_forge/cli/command.rb +78 -24
- data/lib/spec_forge/cli/init.rb +11 -1
- data/lib/spec_forge/cli/new.rb +54 -3
- data/lib/spec_forge/cli/run.rb +20 -0
- data/lib/spec_forge/cli.rb +16 -5
- data/lib/spec_forge/configuration.rb +94 -25
- 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 +148 -0
- data/lib/spec_forge/context/variables.rb +91 -0
- data/lib/spec_forge/context.rb +36 -0
- data/lib/spec_forge/core_ext/rspec.rb +24 -4
- data/lib/spec_forge/error.rb +267 -113
- data/lib/spec_forge/factory.rb +33 -14
- data/lib/spec_forge/filter.rb +87 -0
- data/lib/spec_forge/forge.rb +170 -0
- data/lib/spec_forge/http/backend.rb +99 -29
- data/lib/spec_forge/http/client.rb +23 -13
- data/lib/spec_forge/http/request.rb +74 -62
- data/lib/spec_forge/http/verb.rb +79 -0
- data/lib/spec_forge/http.rb +105 -0
- data/lib/spec_forge/loader.rb +254 -0
- data/lib/spec_forge/matchers.rb +130 -0
- data/lib/spec_forge/normalizer/configuration.rb +24 -11
- data/lib/spec_forge/normalizer/constraint.rb +22 -9
- data/lib/spec_forge/normalizer/expectation.rb +31 -12
- data/lib/spec_forge/normalizer/factory.rb +24 -11
- data/lib/spec_forge/normalizer/factory_reference.rb +32 -13
- data/lib/spec_forge/normalizer/global_context.rb +88 -0
- data/lib/spec_forge/normalizer/spec.rb +39 -16
- data/lib/spec_forge/normalizer.rb +255 -41
- 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 +99 -0
- data/lib/spec_forge/runner.rb +133 -119
- data/lib/spec_forge/spec/expectation/constraint.rb +95 -20
- data/lib/spec_forge/spec/expectation.rb +43 -51
- data/lib/spec_forge/spec.rb +83 -96
- data/lib/spec_forge/type.rb +36 -4
- data/lib/spec_forge/version.rb +4 -1
- data/lib/spec_forge.rb +161 -76
- metadata +20 -5
- data/spec_forge/factories/user.yml +0 -4
- data/spec_forge/forge_helper.rb +0 -37
- 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
|
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(&
|
54
|
+
value.map(&resolve_proc)
|
17
55
|
end
|
18
56
|
|
19
|
-
|
20
|
-
|
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
|
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,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
|
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
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
Regex
|
87
|
-
|
88
|
-
|
89
|
-
|
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
|
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
|
-
#
|
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
|
-
#
|
148
|
+
# Compares this attributes input to other
|
130
149
|
#
|
131
|
-
#
|
132
|
-
#
|
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
|
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
|
-
#
|
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
|
-
# @
|
148
|
-
# attr = Attribute::Literal.new("hello")
|
149
|
-
# attr.resolve # => "hello"
|
192
|
+
# @return [Object] The completely resolved value with cached results
|
150
193
|
#
|
151
|
-
# @example
|
152
|
-
#
|
153
|
-
#
|
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
|
156
|
-
@resolved ||=
|
199
|
+
def resolved
|
200
|
+
@resolved ||= resolve
|
157
201
|
end
|
158
202
|
|
159
|
-
|
160
|
-
|
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
|
-
#
|
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
|
-
#
|
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 [
|
240
|
+
# @return [RSpec::Matchers::BuiltIn::BaseMatcher] A matcher representing this attribute
|
169
241
|
#
|
170
|
-
|
171
|
-
|
172
|
-
|
173
|
-
|
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
|
-
|
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
|
-
|
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(
|
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"
|