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.
Files changed (71) hide show
  1. checksums.yaml +4 -4
  2. data/.standard.yml +3 -3
  3. data/CHANGELOG.md +106 -1
  4. data/README.md +34 -22
  5. data/flake.lock +3 -3
  6. data/flake.nix +8 -2
  7. data/lib/spec_forge/attribute/chainable.rb +208 -20
  8. data/lib/spec_forge/attribute/factory.rb +91 -14
  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 +79 -0
  24. data/lib/spec_forge/cli/actions.rb +27 -0
  25. data/lib/spec_forge/cli/command.rb +78 -24
  26. data/lib/spec_forge/cli/init.rb +11 -1
  27. data/lib/spec_forge/cli/new.rb +54 -3
  28. data/lib/spec_forge/cli/run.rb +20 -0
  29. data/lib/spec_forge/cli.rb +16 -5
  30. data/lib/spec_forge/configuration.rb +94 -22
  31. data/lib/spec_forge/context/callbacks.rb +91 -0
  32. data/lib/spec_forge/context/global.rb +72 -0
  33. data/lib/spec_forge/context/store.rb +148 -0
  34. data/lib/spec_forge/context/variables.rb +91 -0
  35. data/lib/spec_forge/context.rb +36 -0
  36. data/lib/spec_forge/core_ext/rspec.rb +22 -4
  37. data/lib/spec_forge/error.rb +267 -113
  38. data/lib/spec_forge/factory.rb +33 -14
  39. data/lib/spec_forge/filter.rb +87 -0
  40. data/lib/spec_forge/forge.rb +170 -0
  41. data/lib/spec_forge/http/backend.rb +99 -29
  42. data/lib/spec_forge/http/client.rb +23 -13
  43. data/lib/spec_forge/http/request.rb +74 -62
  44. data/lib/spec_forge/http/verb.rb +79 -0
  45. data/lib/spec_forge/http.rb +105 -0
  46. data/lib/spec_forge/loader.rb +254 -0
  47. data/lib/spec_forge/matchers.rb +130 -0
  48. data/lib/spec_forge/normalizer/configuration.rb +24 -11
  49. data/lib/spec_forge/normalizer/constraint.rb +21 -8
  50. data/lib/spec_forge/normalizer/expectation.rb +31 -12
  51. data/lib/spec_forge/normalizer/factory.rb +24 -11
  52. data/lib/spec_forge/normalizer/factory_reference.rb +27 -13
  53. data/lib/spec_forge/normalizer/global_context.rb +88 -0
  54. data/lib/spec_forge/normalizer/spec.rb +39 -16
  55. data/lib/spec_forge/normalizer.rb +255 -41
  56. data/lib/spec_forge/runner/callbacks.rb +246 -0
  57. data/lib/spec_forge/runner/debug_proxy.rb +213 -0
  58. data/lib/spec_forge/runner/listener.rb +54 -0
  59. data/lib/spec_forge/runner/metadata.rb +58 -0
  60. data/lib/spec_forge/runner/state.rb +99 -0
  61. data/lib/spec_forge/runner.rb +132 -123
  62. data/lib/spec_forge/spec/expectation/constraint.rb +91 -20
  63. data/lib/spec_forge/spec/expectation.rb +43 -51
  64. data/lib/spec_forge/spec.rb +83 -96
  65. data/lib/spec_forge/type.rb +36 -4
  66. data/lib/spec_forge/version.rb +4 -1
  67. data/lib/spec_forge.rb +161 -76
  68. metadata +20 -5
  69. data/spec_forge/factories/user.yml +0 -4
  70. data/spec_forge/forge_helper.rb +0 -48
  71. 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
- # Represents any attribute that is written in expanded form.
28
- # Expanded form is just a fancy name for a hash.
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
- # keyword:
31
- # <attribute>:
32
- # <keyword_arg>: <value>
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
- # positional:
35
- # <attribute>:
36
- # - <positional_arg>
37
- # - <positional_arg>
60
+ attr_reader :arguments
61
+
62
+ #
63
+ # Creates a new parameterized attribute with the specified arguments
38
64
  #
39
- # @param input [Hash] The key that contains these arguments
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
- # I would've used Regexp.new(string, string), but it raises when "n" is provided as a flag
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
- # Helpers for ResolvableHash and ResolvableArray
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
- # @private
10
- def resolvable_proc
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
- def resolvable_value_proc
15
- ->(v) { v.respond_to?(:resolve_value) ? v.resolve_value : v }
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 Array that may contain Attributes
6
+ # Represents an array that may contain attributes that need resolution
7
+ #
8
+ # This delegator wraps an array and provides methods to recursively resolve
9
+ # any attribute objects contained within it. It allows arrays to contain
10
+ # dynamic content like variables and faker values.
11
+ #
12
+ # @example In code
13
+ # array = [1, Attribute::Variable.new("variables.user_id"), 3]
14
+ # resolvable = Attribute::ResolvableArray.new(array)
15
+ # resolvable.resolved # => [1, 42, 3] # assuming user_id resolves to 42
7
16
  #
8
17
  class ResolvableArray < SimpleDelegator
9
18
  include Resolvable
10
19
 
20
+ #
21
+ # Returns the underlying array
22
+ #
23
+ # @return [Array] The delegated array
24
+ #
11
25
  def value
12
26
  __getobj__
13
27
  end
14
28
 
29
+ #
30
+ # Returns a new array with all items fully resolved to their final values.
31
+ # Uses the cached version of each item if available.
32
+ #
33
+ # @return [Array] A new array with all items fully resolved to their final values
34
+ #
35
+ # @example
36
+ # array_attr = Attribute::ResolvableArray.new([Attribute::Faker.new("faker.name.name")])
37
+ # array_attr.resolved # => ["Jane Doe"] (with result cached)
38
+ #
39
+ def resolved
40
+ value.map(&resolved_proc)
41
+ end
42
+
43
+ #
44
+ # Freshly resolves all items in the array.
45
+ # Unlike #resolved, this doesn't use cached values, ensuring fresh resolution.
46
+ #
47
+ # @return [Array] A new array with all items freshly resolved
48
+ #
49
+ # @example
50
+ # array_attr = Attribute::ResolvableArray.new([Attribute::Faker.new("faker.name.name")])
51
+ # array_attr.resolve # => ["John Smith"] (fresh value each time)
52
+ #
15
53
  def resolve
16
- value.map(&resolvable_proc)
54
+ value.map(&resolve_proc)
17
55
  end
18
56
 
19
- def resolve_value
20
- value.map(&resolvable_value_proc)
57
+ #
58
+ # Converts all items in the array to RSpec matchers.
59
+ # First converts each array element to a matcher using resolve_as_matcher_proc,
60
+ # then wraps the entire result in a matcher suitable for array comparison.
61
+ #
62
+ # This ensures all elements in the array are proper matchers,
63
+ # which is essential for compound matchers and proper failure messages.
64
+ #
65
+ # @return [RSpec::Matchers::BuiltIn::BaseMatcher] A matcher for this array
66
+ #
67
+ # @example
68
+ # array = Attribute::ResolvableArray.new(["test", /pattern/, 42])
69
+ # array.resolve_as_matcher # => contain_exactly(eq("test"), match(/pattern/), eq(42))
70
+ #
71
+ def resolve_as_matcher
72
+ result = value.map(&resolve_as_matcher_proc)
73
+ Attribute::Literal.new(result).resolve_as_matcher
21
74
  end
22
75
 
76
+ #
77
+ # Binds variables to any attribute objects in the array
78
+ #
79
+ # @param variables [Hash] The variables to bind
80
+ #
23
81
  def bind_variables(variables)
24
82
  value.each { |v| Attribute.bind_variables(v, variables) }
25
83
  end
@@ -3,23 +3,81 @@
3
3
  module SpecForge
4
4
  class Attribute
5
5
  #
6
- # Represents a hash that may contain Attributes
6
+ # Represents a hash that may contain attributes that need resolution
7
+ #
8
+ # This delegator wraps a hash and provides methods to recursively resolve
9
+ # any attribute objects contained within it. It allows hashes to contain
10
+ # dynamic content like variables and faker values.
11
+ #
12
+ # @example In code
13
+ # hash = {name: Attribute::Faker.new("faker.name.name"), id: 123}
14
+ # resolvable = Attribute::ResolvableHash.new(hash)
15
+ # resolvable.resolved # => {name: "John Smith", id: 123}
7
16
  #
8
17
  class ResolvableHash < SimpleDelegator
9
18
  include Resolvable
10
19
 
20
+ #
21
+ # Returns the underlying hash
22
+ #
23
+ # @return [Hash] The delegated hash
24
+ #
11
25
  def value
12
26
  __getobj__
13
27
  end
14
28
 
29
+ #
30
+ # Returns a new hash with all values fully resolved to their final values.
31
+ # Uses the cached version of each value if available.
32
+ #
33
+ # @return [Hash] A new hash with all values fully resolved to their final values
34
+ #
35
+ # @example
36
+ # hash_attr = Attribute::ResolvableHash.new({name: Attribute::Faker.new("faker.name.name")})
37
+ # hash_attr.resolved # => {name: "Jane Doe"} (with result cached)
38
+ #
39
+ def resolved
40
+ value.transform_values(&resolved_proc)
41
+ end
42
+
43
+ #
44
+ # Freshly resolves all values in the hash.
45
+ # Unlike #resolved, this doesn't use cached values, ensuring fresh resolution.
46
+ #
47
+ # @return [Hash] A new hash with all values freshly resolved
48
+ #
49
+ # @example
50
+ # hash_attr = Attribute::ResolvableHash.new({name: Attribute::Faker.new("faker.name.name")})
51
+ # hash_attr.resolve # => {name: "John Smith"} (fresh value each time)
52
+ #
15
53
  def resolve
16
- value.transform_values(&resolvable_proc)
54
+ value.transform_values(&resolve_proc)
17
55
  end
18
56
 
19
- def resolve_value
20
- value.transform_values(&resolvable_value_proc)
57
+ #
58
+ # Converts all values in the hash to RSpec matchers.
59
+ # Transforms each hash value to a matcher using resolve_as_matcher_proc,
60
+ # then wraps the entire result in a matcher suitable for hash comparison.
61
+ #
62
+ # This ensures proper nesting of matchers in hash structures,
63
+ # which is vital for readable failure messages in complex expectations.
64
+ #
65
+ # @return [RSpec::Matchers::BuiltIn::BaseMatcher] A matcher for this hash
66
+ #
67
+ # @example
68
+ # hash = Attribute::ResolvableHash.new({name: "Test", age: 42})
69
+ # hash.resolve_as_matcher # => include("name" => eq("Test"), "age" => eq(42))
70
+ #
71
+ def resolve_as_matcher
72
+ result = value.transform_values(&resolve_as_matcher_proc)
73
+ Attribute::Literal.new(result).resolve_as_matcher
21
74
  end
22
75
 
76
+ #
77
+ # Binds variables to any attribute objects in the hash values
78
+ #
79
+ # @param variables [Hash] The variables to bind
80
+ #
23
81
  def bind_variables(variables)
24
82
  value.each_value { |v| Attribute.bind_variables(v, variables) }
25
83
  end
@@ -0,0 +1,65 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SpecForge
4
+ class Attribute
5
+ #
6
+ # Represents an attribute that references values from stored test results
7
+ #
8
+ # This class allows accessing data from previous test executions that were
9
+ # saved using the `store_as` directive. It provides access to response data
10
+ # including status, headers, and body from previously run expectations.
11
+ #
12
+ # @example Basic usage in YAML
13
+ # create_user:
14
+ # path: /users
15
+ # method: post
16
+ # expectations:
17
+ # - store_as: new_user
18
+ # body:
19
+ # name: faker.name.name
20
+ # expect:
21
+ # status: 201
22
+ #
23
+ # get_user:
24
+ # path: /users/{id}
25
+ # expectations:
26
+ # - query:
27
+ # id: store.new_user.body.id
28
+ # expect:
29
+ # status: 200
30
+ #
31
+ # @example Accessing specific response components
32
+ # check_status:
33
+ # path: /health
34
+ # expectations:
35
+ # - variables:
36
+ # expected_status: store.new_user.status
37
+ # auth_token: store.new_user.headers.authorization
38
+ # user_name: store.new_user.body.user.name
39
+ # expect:
40
+ # status: 200
41
+ #
42
+ class Store < Attribute
43
+ include Chainable
44
+
45
+ #
46
+ # Regular expression pattern that matches attribute keywords with this prefix
47
+ # Used for identifying this attribute type during parsing
48
+ #
49
+ # @return [Regexp]
50
+ #
51
+ KEYWORD_REGEX = /^store\./i
52
+
53
+ alias_method :stored_id, :header
54
+
55
+ #
56
+ # Returns the base object for the variable chain
57
+ #
58
+ # @return [Context::Store::Entry, nil] The stored entry or nil if not found
59
+ #
60
+ def base_object
61
+ @base_object ||= SpecForge.context.store[stored_id.to_s]
62
+ end
63
+ end
64
+ end
65
+ end
@@ -2,9 +2,34 @@
2
2
 
3
3
  module SpecForge
4
4
  class Attribute
5
+ #
6
+ # Represents an attribute that transforms other attributes
7
+ #
8
+ # This class provides transformation functions like join that can be applied
9
+ # to other attributes or values. It allows complex data manipulation without
10
+ # writing Ruby code.
11
+ #
12
+ # @example Join transformation in YAML
13
+ # full_name:
14
+ # transform.join:
15
+ # - variables.first_name
16
+ # - " "
17
+ # - variables.last_name
18
+ #
5
19
  class Transform < Parameterized
20
+ #
21
+ # Regular expression pattern that matches attribute keywords with this prefix
22
+ # Used for identifying this attribute type during parsing
23
+ #
24
+ # @return [Regexp]
25
+ #
6
26
  KEYWORD_REGEX = /^transform\./i
7
27
 
28
+ #
29
+ # The available transformation methods
30
+ #
31
+ # @return [Array<String>]
32
+ #
8
33
  TRANSFORM_METHODS = %w[
9
34
  join
10
35
  ].freeze
@@ -12,9 +37,7 @@ module SpecForge
12
37
  attr_reader :function
13
38
 
14
39
  #
15
- # Represents any attribute that is a transform call
16
- #
17
- # transform.<function>
40
+ # Creates a new transform attribute with the specified function and arguments
18
41
  #
19
42
  def initialize(...)
20
43
  super
@@ -22,16 +45,21 @@ module SpecForge
22
45
  # Remove prefix
23
46
  @function = @input.sub("transform.", "")
24
47
 
25
- raise InvalidTransformFunctionError, input unless TRANSFORM_METHODS.include?(function)
48
+ raise Error::InvalidTransformFunctionError, input unless TRANSFORM_METHODS.include?(function)
26
49
 
27
50
  prepare_arguments!
28
51
  end
29
52
 
53
+ #
54
+ # Returns the result of applying the transformation function
55
+ #
56
+ # @return [Object] The transformed value
57
+ #
30
58
  def value
31
59
  case function
32
60
  when "join"
33
61
  # Technically supports any attribute, but I ain't gonna test all them edge cases
34
- arguments[:positional].resolve.join
62
+ arguments[:positional].resolved.join
35
63
  end
36
64
  end
37
65
  end
@@ -3,26 +3,57 @@
3
3
  module SpecForge
4
4
  class Attribute
5
5
  #
6
- # Represents any attribute that is a variable reference
6
+ # Represents an attribute that references a variable
7
7
  #
8
- # variables.<variable_name>
8
+ # This class allows referencing variables defined in the test context.
9
+ # It supports chained access to methods and properties of variable values.
10
+ #
11
+ # @example Basic usage in YAML
12
+ # user_id: variables.user.id
13
+ # company_name: variables.company.name
14
+ #
15
+ # @example Nested access in YAML
16
+ # post_author: variables.post.comments.first.author.name
9
17
  #
10
18
  class Variable < Attribute
11
19
  include Chainable
12
20
 
21
+ #
22
+ # Regular expression pattern that matches attribute keywords with this prefix
23
+ # Used for identifying this attribute type during parsing
24
+ #
25
+ # @return [Regexp]
26
+ #
13
27
  KEYWORD_REGEX = /^variables\./i
14
28
 
15
29
  alias_method :variable_name, :header
16
30
 
31
+ #
32
+ # Binds the referenced variable to this attribute
33
+ #
34
+ # @param variables [Hash] A hash of variables to look up in
35
+ #
36
+ # @raise [Error::MissingVariableError] If the variable is not found
37
+ # @raise [Error::InvalidTypeError] If variables is not a hash
38
+ #
17
39
  def bind_variables(variables)
18
- raise InvalidTypeError.new(variables, Hash, for: "'variables'") unless Type.hash?(variables)
40
+ if !Type.hash?(variables)
41
+ raise Error::InvalidTypeError.new(variables, Hash, for: "'variables'")
42
+ end
19
43
 
20
44
  # Don't nil check here.
21
- raise MissingVariableError, variable_name unless variables.key?(variable_name)
45
+ raise Error::MissingVariableError, variable_name unless variables.key?(variable_name)
22
46
 
23
- @base_object = variables[variable_name]
47
+ @variable = variables[variable_name]
48
+ end
24
49
 
25
- self
50
+ #
51
+ # Returns the base object for the variable chain
52
+ #
53
+ # @return [Object] The variable value
54
+ #
55
+ def base_object
56
+ @variable || bind_variables(SpecForge.context.variables)
26
57
  end
27
58
  end
28
59
  end