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