spec_forge 0.7.1 → 1.0.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 (133) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +75 -1
  3. data/README.md +124 -202
  4. data/bin/spec_forge +1 -1
  5. data/flake.lock +76 -4
  6. data/flake.nix +5 -4
  7. data/lib/spec_forge/attribute/chainable.rb +6 -6
  8. data/lib/spec_forge/attribute/environment.rb +45 -0
  9. data/lib/spec_forge/attribute/factory.rb +26 -17
  10. data/lib/spec_forge/attribute/faker.rb +6 -1
  11. data/lib/spec_forge/attribute/generate.rb +114 -0
  12. data/lib/spec_forge/attribute/literal.rb +1 -14
  13. data/lib/spec_forge/attribute/matcher.rb +6 -2
  14. data/lib/spec_forge/attribute/parameterized.rb +20 -22
  15. data/lib/spec_forge/attribute/resolvable_array.rb +16 -16
  16. data/lib/spec_forge/attribute/resolvable_hash.rb +17 -16
  17. data/lib/spec_forge/attribute/resolvable_struct.rb +67 -0
  18. data/lib/spec_forge/attribute/template.rb +118 -0
  19. data/lib/spec_forge/attribute/transform.rb +14 -19
  20. data/lib/spec_forge/attribute/variable.rb +31 -31
  21. data/lib/spec_forge/attribute.rb +54 -100
  22. data/lib/spec_forge/blueprint.rb +27 -0
  23. data/lib/spec_forge/cli/docs/generate.rb +28 -8
  24. data/lib/spec_forge/cli/docs.rb +5 -2
  25. data/lib/spec_forge/cli/init.rb +4 -4
  26. data/lib/spec_forge/cli/new.rb +78 -27
  27. data/lib/spec_forge/cli/run.rb +84 -52
  28. data/lib/spec_forge/cli/serve.rb +5 -0
  29. data/lib/spec_forge/cli.rb +6 -14
  30. data/lib/spec_forge/configuration.rb +209 -79
  31. data/lib/spec_forge/documentation/{loader → builder}/cache.rb +26 -23
  32. data/lib/spec_forge/documentation/builder/compiler.rb +373 -0
  33. data/lib/spec_forge/documentation/builder/extractor.rb +75 -0
  34. data/lib/spec_forge/documentation/builder.rb +77 -329
  35. data/lib/spec_forge/documentation/document/operation.rb +4 -4
  36. data/lib/spec_forge/documentation/document.rb +0 -6
  37. data/lib/spec_forge/documentation/generator.rb +88 -0
  38. data/lib/spec_forge/documentation/{generators/openapi → openapi/v3_0}/error_formatter.rb +2 -2
  39. data/lib/spec_forge/documentation/openapi/v3_0/example.rb +1 -1
  40. data/lib/spec_forge/documentation/openapi/v3_0/media_type.rb +1 -1
  41. data/lib/spec_forge/documentation/openapi/v3_0/operation.rb +21 -5
  42. data/lib/spec_forge/documentation/openapi/v3_0/response.rb +28 -6
  43. data/lib/spec_forge/documentation/openapi/v3_0/schema.rb +20 -2
  44. data/lib/spec_forge/documentation/openapi/v3_0/tag.rb +1 -1
  45. data/lib/spec_forge/documentation/openapi/v3_0.rb +116 -0
  46. data/lib/spec_forge/documentation/openapi.rb +40 -12
  47. data/lib/spec_forge/documentation.rb +1 -7
  48. data/lib/spec_forge/error.rb +215 -41
  49. data/lib/spec_forge/factory.rb +38 -18
  50. data/lib/spec_forge/forge/action.rb +41 -0
  51. data/lib/spec_forge/forge/actions/call.rb +33 -0
  52. data/lib/spec_forge/forge/actions/debug.rb +47 -0
  53. data/lib/spec_forge/forge/actions/expect.rb +44 -0
  54. data/lib/spec_forge/forge/actions/request.rb +65 -0
  55. data/lib/spec_forge/forge/actions/store.rb +31 -0
  56. data/lib/spec_forge/forge/callbacks.rb +80 -0
  57. data/lib/spec_forge/forge/context.rb +41 -0
  58. data/lib/spec_forge/forge/display.rb +503 -0
  59. data/lib/spec_forge/forge/hooks.rb +131 -0
  60. data/lib/spec_forge/forge/runner/array_io.rb +81 -0
  61. data/lib/spec_forge/forge/runner/content_validator.rb +92 -0
  62. data/lib/spec_forge/forge/runner/header_validator.rb +66 -0
  63. data/lib/spec_forge/forge/runner/reporter.rb +56 -0
  64. data/lib/spec_forge/forge/runner/schema_validator.rb +113 -0
  65. data/lib/spec_forge/forge/runner.rb +118 -0
  66. data/lib/spec_forge/forge/timer.rb +94 -0
  67. data/lib/spec_forge/forge/variables.rb +38 -0
  68. data/lib/spec_forge/forge.rb +207 -133
  69. data/lib/spec_forge/http/backend.rb +49 -146
  70. data/lib/spec_forge/http/client.rb +14 -17
  71. data/lib/spec_forge/http/request.rb +37 -84
  72. data/lib/spec_forge/http/verb.rb +4 -0
  73. data/lib/spec_forge/http.rb +0 -5
  74. data/lib/spec_forge/loader/filter.rb +85 -0
  75. data/lib/spec_forge/loader/step_processor.rb +282 -0
  76. data/lib/spec_forge/loader.rb +105 -220
  77. data/lib/spec_forge/normalizer/default.rb +1 -1
  78. data/lib/spec_forge/normalizer/structure.rb +140 -0
  79. data/lib/spec_forge/normalizer/transformers.rb +168 -0
  80. data/lib/spec_forge/normalizer/validators.rb +50 -8
  81. data/lib/spec_forge/normalizer.rb +76 -119
  82. data/lib/spec_forge/normalizers/callback.yml +38 -0
  83. data/lib/spec_forge/normalizers/configuration.yml +59 -9
  84. data/lib/spec_forge/normalizers/factory.yml +53 -2
  85. data/lib/spec_forge/normalizers/factory_reference.yml +63 -2
  86. data/lib/spec_forge/normalizers/json_schema.yml +79 -0
  87. data/lib/spec_forge/normalizers/step.yml +506 -0
  88. data/lib/spec_forge/step/call.rb +36 -0
  89. data/lib/spec_forge/step/expect.rb +110 -0
  90. data/lib/spec_forge/step/source.rb +22 -0
  91. data/lib/spec_forge/step.rb +129 -0
  92. data/lib/spec_forge/type.rb +115 -66
  93. data/lib/spec_forge/version.rb +1 -1
  94. data/lib/spec_forge.rb +44 -106
  95. data/lib/templates/forge_helper.rb.tt +43 -22
  96. data/lib/templates/new_blueprint.yml.tt +54 -0
  97. metadata +75 -44
  98. data/lib/spec_forge/attribute/global.rb +0 -96
  99. data/lib/spec_forge/attribute/store.rb +0 -65
  100. data/lib/spec_forge/backtrace_formatter.rb +0 -50
  101. data/lib/spec_forge/callbacks.rb +0 -88
  102. data/lib/spec_forge/context/callbacks.rb +0 -91
  103. data/lib/spec_forge/context/global.rb +0 -72
  104. data/lib/spec_forge/context/store.rb +0 -131
  105. data/lib/spec_forge/context/variables.rb +0 -91
  106. data/lib/spec_forge/context.rb +0 -36
  107. data/lib/spec_forge/core_ext/rspec.rb +0 -55
  108. data/lib/spec_forge/core_ext.rb +0 -5
  109. data/lib/spec_forge/documentation/generators/base.rb +0 -81
  110. data/lib/spec_forge/documentation/generators/openapi/base.rb +0 -100
  111. data/lib/spec_forge/documentation/generators/openapi/v3_0.rb +0 -65
  112. data/lib/spec_forge/documentation/generators/openapi.rb +0 -59
  113. data/lib/spec_forge/documentation/generators.rb +0 -17
  114. data/lib/spec_forge/documentation/loader.rb +0 -159
  115. data/lib/spec_forge/documentation/openapi/base.rb +0 -33
  116. data/lib/spec_forge/filter.rb +0 -86
  117. data/lib/spec_forge/normalizer/definition.rb +0 -248
  118. data/lib/spec_forge/normalizers/_shared.yml +0 -76
  119. data/lib/spec_forge/normalizers/constraint.yml +0 -8
  120. data/lib/spec_forge/normalizers/expectation.yml +0 -47
  121. data/lib/spec_forge/normalizers/global_context.yml +0 -28
  122. data/lib/spec_forge/normalizers/spec.yml +0 -50
  123. data/lib/spec_forge/runner/adapter.rb +0 -181
  124. data/lib/spec_forge/runner/callbacks.rb +0 -246
  125. data/lib/spec_forge/runner/debug_proxy.rb +0 -215
  126. data/lib/spec_forge/runner/listener.rb +0 -54
  127. data/lib/spec_forge/runner/metadata.rb +0 -58
  128. data/lib/spec_forge/runner/state.rb +0 -98
  129. data/lib/spec_forge/runner.rb +0 -75
  130. data/lib/spec_forge/spec/expectation/constraint.rb +0 -127
  131. data/lib/spec_forge/spec/expectation.rb +0 -68
  132. data/lib/spec_forge/spec.rb +0 -68
  133. data/lib/templates/new_spec.yml.tt +0 -43
@@ -58,6 +58,8 @@ module SpecForge
58
58
  #
59
59
  # Initializes a new chainable attribute by parsing the input into components
60
60
  #
61
+ # Parses the input string into keyword, header, and invocation chain parts.
62
+ #
61
63
  def initialize(...)
62
64
  super
63
65
 
@@ -125,7 +127,7 @@ module SpecForge
125
127
  def traverse_chain(resolve:)
126
128
  resolution_path = {}
127
129
 
128
- current_path = "#{keyword}.#{header}"
130
+ current_path = keyword.present? ? "#{keyword}.#{header}" : header.to_s
129
131
  current_object = base_object
130
132
 
131
133
  invocation_chain.each do |step|
@@ -160,7 +162,7 @@ module SpecForge
160
162
  def retrieve_value(object, resolve:)
161
163
  return object unless object.is_a?(Attribute)
162
164
 
163
- resolve ? object.resolved : object.value
165
+ resolve ? object.resolve : object.value
164
166
  end
165
167
 
166
168
  #
@@ -174,19 +176,17 @@ module SpecForge
174
176
  #
175
177
  def describe_value(value)
176
178
  case value
177
- when Context::Store::Entry
178
- "Store with attributes: #{value.available_methods.join_map(", ", &:in_quotes)}"
179
179
  when OpenStruct
180
180
  "Object with attributes: #{value.table.keys.join_map(", ", &:in_quotes)}"
181
181
  when Struct, Data
182
182
  "Object with attributes: #{value.members.join_map(", ", &:in_quotes)}"
183
- when ArrayLike
183
+ when Array
184
184
  # Preview the first 5 value's classes
185
185
  preview = value.take(5).map(&:class)
186
186
  preview << "..." if value.size > 5
187
187
 
188
188
  "Array with #{value.size} #{"element".pluralize(value.size)}: #{preview}"
189
- when HashLike
189
+ when Hash
190
190
  # Preview the first 5 keys
191
191
  keys = value.keys.take(5)
192
192
 
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SpecForge
4
+ class Attribute
5
+ #
6
+ # Represents an attribute that retrieves its value from an environment variable.
7
+ # This allows specs to reference environment variables dynamically.
8
+ #
9
+ # @example Basic usage in YAML
10
+ # api_key: "{{ env.API_KEY }}"
11
+ # database_url: "{{ env.DATABASE_URL }}"
12
+ # secret: "{{ env.MY_SECRET_TOKEN }}"
13
+ #
14
+ class Environment < Attribute
15
+ #
16
+ # Regular expression pattern that matches attribute keywords with this prefix.
17
+ # Used for identifying this attribute type during parsing.
18
+ # Matches case-insensitively (env., ENV., Env., etc.)
19
+ #
20
+ # @return [Regexp]
21
+ #
22
+ KEYWORD_REGEX = /^env\./i
23
+
24
+ #
25
+ # Creates a new environment attribute by extracting the variable name
26
+ #
27
+ # @param input [String] The environment variable reference (e.g., "env.API_KEY")
28
+ #
29
+ def initialize(...)
30
+ super
31
+
32
+ @variable_name = input.sub(KEYWORD_REGEX, "")
33
+ end
34
+
35
+ #
36
+ # Returns the value of the referenced environment variable
37
+ #
38
+ # @return [String, nil] The environment variable value, or nil if not set
39
+ #
40
+ def value
41
+ ENV[@variable_name]
42
+ end
43
+ end
44
+ end
45
+ end
@@ -76,13 +76,15 @@ module SpecForge
76
76
  #
77
77
  # Creates a new factory attribute with the specified name and arguments
78
78
  #
79
+ # @see Parameterized#initialize
80
+ #
79
81
  def initialize(...)
80
82
  super
81
83
 
82
84
  # Check the arguments before preparing them
83
85
  arguments[:keyword] = Normalizer.normalize!(arguments[:keyword], using: :factory_reference)
84
86
 
85
- prepare_arguments!
87
+ prepare_arguments
86
88
  end
87
89
 
88
90
  #
@@ -116,10 +118,10 @@ module SpecForge
116
118
  #
117
119
  def resolve
118
120
  case value
119
- when ArrayLike
120
- value.map(&resolved_proc)
121
- when HashLike
122
- value.transform_values(&resolved_proc)
121
+ when Array
122
+ value.map(&resolve_proc)
123
+ when Hash
124
+ value.transform_values(&resolve_proc)
123
125
  else
124
126
  value
125
127
  end
@@ -133,21 +135,28 @@ module SpecForge
133
135
  def construct_factory_parameters(attributes)
134
136
  build_strategy, list_size = determine_build_strategy(attributes)
135
137
 
136
- # This is set up for the base strategies + _pair
137
- # FactoryBot.<build_strategy>(factory_name, **attributes)
138
- build_arguments = [
139
- build_strategy,
140
- factory_name,
141
- **attributes[:attributes].resolve
142
- ]
138
+ # Extract and resolve traits
139
+ traits = (attributes[:traits].resolve || []).map(&:to_sym)
143
140
 
144
- # Insert the list size after the strategy
145
- # FactoryBot.<build_strategy>_list(factory_name, list_size, **attributes)
141
+ # Build arguments depend on whether it's a list strategy
142
+ # For list strategies: FactoryBot.build_list(factory_name, count, *traits, **attributes)
143
+ # For other strategies: FactoryBot.build(factory_name, *traits, **attributes)
146
144
  if build_strategy.end_with?("_list")
147
- build_arguments.insert(2, list_size)
145
+ [
146
+ build_strategy,
147
+ factory_name,
148
+ list_size,
149
+ *traits,
150
+ **attributes[:attributes].resolve
151
+ ]
152
+ else
153
+ [
154
+ build_strategy,
155
+ factory_name,
156
+ *traits,
157
+ **attributes[:attributes].resolve
158
+ ]
148
159
  end
149
-
150
- build_arguments
151
160
  end
152
161
 
153
162
  #
@@ -41,12 +41,17 @@ module SpecForge
41
41
  #
42
42
  # Creates a new faker attribute with the specified name and arguments
43
43
  #
44
+ # @raise [Error::InvalidFakerClassError] If the faker class doesn't exist
45
+ # @raise [Error::InvalidFakerMethodError] If the faker method doesn't exist
46
+ #
47
+ # @see Parameterized#initialize
48
+ #
44
49
  def initialize(...)
45
50
  super
46
51
 
47
52
  @faker_class, @faker_method = extract_faker_call
48
53
 
49
- prepare_arguments!
54
+ prepare_arguments
50
55
  end
51
56
 
52
57
  #
@@ -0,0 +1,114 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SpecForge
4
+ class Attribute
5
+ #
6
+ # Represents an attribute that generates data structures dynamically.
7
+ #
8
+ # This class provides generation functions like `array` that can create
9
+ # collections of arbitrary size with evaluated values. It's useful for
10
+ # testing batch endpoints or generating large payloads.
11
+ #
12
+ # @example Generate an array of static values
13
+ # generate.array:
14
+ # size: 5
15
+ # value: "test"
16
+ #
17
+ # @example Generate an array with faker values
18
+ # generate.array:
19
+ # size: 100
20
+ # value: "{{ faker.string.alphanumeric }}"
21
+ #
22
+ # @example Generate an array with sequential indices
23
+ # generate.array:
24
+ # size: 3
25
+ # value: "user_{{ index }}"
26
+ # # Produces: ["user_0", "user_1", "user_2"]
27
+ #
28
+ # @example Combine faker with index
29
+ # generate.array:
30
+ # size: 10
31
+ # value: "{{ faker.internet.username }}_{{ index }}"
32
+ #
33
+ class Generate < Parameterized
34
+ #
35
+ # Regular expression pattern that matches attribute keywords with this prefix.
36
+ # Used for identifying this attribute type during parsing.
37
+ # Matches case-insensitively (generate., GENERATE., Generate., etc.)
38
+ #
39
+ # @return [Regexp]
40
+ #
41
+ KEYWORD_REGEX = /^generate\./i
42
+
43
+ #
44
+ # The available generation methods
45
+ #
46
+ # @return [Array<String>]
47
+ #
48
+ METHODS = %w[
49
+ array
50
+ ].freeze
51
+
52
+ #
53
+ # The generation function name (e.g., "array")
54
+ #
55
+ # @return [String]
56
+ #
57
+ attr_reader :function
58
+
59
+ #
60
+ # Creates a new generate attribute with the specified function and arguments
61
+ #
62
+ # @raise [Error::InvalidGenerateFunctionError] If the function is not supported
63
+ #
64
+ # @see Parameterized#initialize
65
+ #
66
+ def initialize(...)
67
+ super
68
+
69
+ @function = @input.sub(KEYWORD_REGEX, "")
70
+ raise Error::InvalidGenerateFunctionError.new(input, METHODS) unless METHODS.include?(function)
71
+
72
+ prepare_arguments
73
+ end
74
+
75
+ #
76
+ # Returns the result of applying the generation function
77
+ #
78
+ # @return [Object] The generated value
79
+ #
80
+ def value
81
+ case function
82
+ when "array"
83
+ generate_array
84
+ end
85
+ end
86
+
87
+ private
88
+
89
+ #
90
+ # Generates an array of the specified size with evaluated values
91
+ #
92
+ # The special variable `index` (0-based) is available within `value` expressions
93
+ # and will shadow any existing variable with the same name during generation.
94
+ #
95
+ # @return [Array] The generated array
96
+ #
97
+ # @private
98
+ #
99
+ def generate_array
100
+ args = @arguments[:keyword]
101
+ size = args[:size].resolve
102
+ value_template = args[:value]
103
+ variables = SpecForge::Forge.context.variables
104
+
105
+ Array.new(size) do |index|
106
+ variables[:index] = index
107
+ value_template.value
108
+ ensure
109
+ variables.delete(:index)
110
+ end
111
+ end
112
+ end
113
+ end
114
+ end
@@ -14,20 +14,7 @@ module SpecForge
14
14
  # active: true
15
15
  #
16
16
  class Literal < Attribute
17
- # @return [Object] The literal value
18
- attr_reader :value
19
-
20
- #
21
- # Creates a new literal attribute with the specified value
22
- #
23
- # @param input [Object] The value to store
24
- #
25
- def initialize(input)
26
- super
27
-
28
- @value = input
29
- end
30
-
17
+ alias_method :value, :input
31
18
  alias_method :resolve, :value
32
19
  end
33
20
  end
@@ -62,6 +62,10 @@ module SpecForge
62
62
  #
63
63
  # Creates a new matcher attribute with the specified matcher and arguments
64
64
  #
65
+ # @raise [Error::UndefinedMatcherError] If the matcher is not available
66
+ #
67
+ # @see Parameterized#initialize
68
+ #
65
69
  def initialize(...)
66
70
  super
67
71
 
@@ -77,11 +81,11 @@ module SpecForge
77
81
  resolve_base_matcher(method)
78
82
  end
79
83
 
80
- prepare_arguments!
84
+ prepare_arguments
81
85
 
82
86
  # An argument can be an expanded version of something (such as matcher.include)
83
87
  # Move it to where it belongs
84
- if (keyword = arguments[:keyword]) && !Type.hash?(keyword)
88
+ if (keyword = arguments[:keyword]) && !keyword.is_a?(Hash)
85
89
  arguments[:positional] << keyword
86
90
  arguments[:keyword] = {}
87
91
  end
@@ -26,24 +26,24 @@ module SpecForge
26
26
  # Creates a new attribute instance from a hash representation
27
27
  #
28
28
  # @param hash [Hash] A hash containing the attribute name and arguments
29
+ # @param options [Hash] Additional options to pass to the attribute (e.g., context)
29
30
  #
30
31
  # @return [Parameterized] A new parameterized attribute instance
31
32
  #
32
- def self.from_hash(hash)
33
+ def self.from_hash(hash, **options)
33
34
  metadata = hash.first
34
35
 
35
36
  input = metadata.first
36
37
  arguments = metadata.second
37
38
 
38
39
  case arguments
39
- when ArrayLike
40
- new(input, arguments)
41
- when HashLike
42
- # Offset for positional arguments. No support for both at this time
43
- new(input, [], arguments)
40
+ when Array
41
+ new(input, positional: arguments, **options)
42
+ when Hash
43
+ new(input, keyword: arguments, **options)
44
44
  else
45
45
  # Single value
46
- new(input, [arguments])
46
+ new(input, positional: [arguments], **options)
47
47
  end
48
48
  end
49
49
 
@@ -63,23 +63,21 @@ module SpecForge
63
63
  # Creates a new parameterized attribute with the specified arguments
64
64
  #
65
65
  # @param input [String, Symbol] The key that contains these arguments
66
- # @param positional [Array] Any positional arguments
67
- # @param keyword [Hash] Any keyword arguments
66
+ # @param options [Hash] Options including positional and keyword arguments
67
+ # @option options [Array] :positional Positional arguments
68
+ # @option options [Hash] :keyword Keyword arguments
68
69
  #
69
- def initialize(input, positional = [], keyword = {})
70
- super(input.to_s.downcase)
70
+ def initialize(...)
71
+ super
71
72
 
72
- @arguments = {positional:, keyword:}
73
- end
73
+ @input = @input.to_s.downcase
74
74
 
75
- #
76
- # Binds variables to any nested attributes in the arguments
77
- #
78
- # @param variables [Hash] A hash of variable attributes
79
- #
80
- def bind_variables(variables)
81
- arguments[:positional].each { |v| Attribute.bind_variables(v, variables) }
82
- arguments[:keyword].each_value { |v| Attribute.bind_variables(v, variables) }
75
+ @arguments = {
76
+ positional: @options[:positional] || [],
77
+ keyword: @options[:keyword] || {}
78
+ }
79
+
80
+ @options.clear # No need to store a duplicate
83
81
  end
84
82
 
85
83
  protected
@@ -93,7 +91,7 @@ module SpecForge
93
91
  #
94
92
  # @private
95
93
  #
96
- def prepare_arguments!
94
+ def prepare_arguments
97
95
  @arguments = Attribute.from(arguments)
98
96
  end
99
97
  end
@@ -5,7 +5,7 @@ module SpecForge
5
5
  #
6
6
  # Represents an array that may contain attributes that need resolution
7
7
  #
8
- # This delegator wraps an array and provides methods to recursively resolve
8
+ # This class extends Array and provides methods to recursively resolve
9
9
  # any attribute objects contained within it. It allows arrays to contain
10
10
  # dynamic content like variables and faker values.
11
11
  #
@@ -14,16 +14,25 @@ module SpecForge
14
14
  # resolvable = Attribute::ResolvableArray.new(array)
15
15
  # resolvable.resolved # => [1, 42, 3] # assuming user_id resolves to 42
16
16
  #
17
- class ResolvableArray < SimpleDelegator
17
+ class ResolvableArray < Array
18
18
  include Resolvable
19
19
 
20
+ #
21
+ # Creates a new ResolvableArray from the given array
22
+ #
23
+ # @param array [Array] The array to wrap
24
+ #
25
+ def initialize(array = [])
26
+ super
27
+ end
28
+
20
29
  #
21
30
  # Returns the underlying array
22
31
  #
23
- # @return [Array] The delegated array
32
+ # @return [Array] The array itself
24
33
  #
25
34
  def value
26
- __getobj__
35
+ self
27
36
  end
28
37
 
29
38
  #
@@ -37,7 +46,7 @@ module SpecForge
37
46
  # array_attr.resolved # => ["Jane Doe"] (with result cached)
38
47
  #
39
48
  def resolved
40
- value.map(&resolved_proc)
49
+ map(&resolved_proc)
41
50
  end
42
51
 
43
52
  #
@@ -51,7 +60,7 @@ module SpecForge
51
60
  # array_attr.resolve # => ["John Smith"] (fresh value each time)
52
61
  #
53
62
  def resolve
54
- value.map(&resolve_proc)
63
+ map(&resolve_proc)
55
64
  end
56
65
 
57
66
  #
@@ -69,18 +78,9 @@ module SpecForge
69
78
  # array.resolve_as_matcher # => contain_exactly(eq("test"), match(/pattern/), eq(42))
70
79
  #
71
80
  def resolve_as_matcher
72
- result = value.map(&resolve_as_matcher_proc)
81
+ result = map(&resolve_as_matcher_proc)
73
82
  Attribute::Literal.new(result).resolve_as_matcher
74
83
  end
75
-
76
- #
77
- # Binds variables to any attribute objects in the array
78
- #
79
- # @param variables [Hash] The variables to bind
80
- #
81
- def bind_variables(variables)
82
- value.each { |v| Attribute.bind_variables(v, variables) }
83
- end
84
84
  end
85
85
  end
86
86
  end
@@ -5,7 +5,7 @@ module SpecForge
5
5
  #
6
6
  # Represents a hash that may contain attributes that need resolution
7
7
  #
8
- # This delegator wraps a hash and provides methods to recursively resolve
8
+ # This class extends Hash and provides methods to recursively resolve
9
9
  # any attribute objects contained within it. It allows hashes to contain
10
10
  # dynamic content like variables and faker values.
11
11
  #
@@ -14,16 +14,26 @@ module SpecForge
14
14
  # resolvable = Attribute::ResolvableHash.new(hash)
15
15
  # resolvable.resolved # => {name: "John Smith", id: 123}
16
16
  #
17
- class ResolvableHash < SimpleDelegator
17
+ class ResolvableHash < Hash
18
18
  include Resolvable
19
19
 
20
+ #
21
+ # Creates a new ResolvableHash from the given hash
22
+ #
23
+ # @param hash [Hash] The hash to wrap
24
+ #
25
+ def initialize(hash = {})
26
+ super()
27
+ merge!(hash)
28
+ end
29
+
20
30
  #
21
31
  # Returns the underlying hash
22
32
  #
23
- # @return [Hash] The delegated hash
33
+ # @return [Hash] The hash itself
24
34
  #
25
35
  def value
26
- __getobj__
36
+ self
27
37
  end
28
38
 
29
39
  #
@@ -37,7 +47,7 @@ module SpecForge
37
47
  # hash_attr.resolved # => {name: "Jane Doe"} (with result cached)
38
48
  #
39
49
  def resolved
40
- value.transform_values(&resolved_proc)
50
+ transform_values(&resolved_proc)
41
51
  end
42
52
 
43
53
  #
@@ -51,7 +61,7 @@ module SpecForge
51
61
  # hash_attr.resolve # => {name: "John Smith"} (fresh value each time)
52
62
  #
53
63
  def resolve
54
- value.transform_values(&resolve_proc)
64
+ transform_values(&resolve_proc)
55
65
  end
56
66
 
57
67
  #
@@ -69,18 +79,9 @@ module SpecForge
69
79
  # hash.resolve_as_matcher # => include("name" => eq("Test"), "age" => eq(42))
70
80
  #
71
81
  def resolve_as_matcher
72
- result = value.transform_values(&resolve_as_matcher_proc)
82
+ result = transform_values(&resolve_as_matcher_proc)
73
83
  Attribute::Literal.new(result).resolve_as_matcher
74
84
  end
75
-
76
- #
77
- # Binds variables to any attribute objects in the hash values
78
- #
79
- # @param variables [Hash] The variables to bind
80
- #
81
- def bind_variables(variables)
82
- value.each_value { |v| Attribute.bind_variables(v, variables) }
83
- end
84
85
  end
85
86
  end
86
87
  end
@@ -0,0 +1,67 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SpecForge
4
+ class Attribute
5
+ #
6
+ # Wraps struct-like objects (Struct, Data, OpenStruct) to make them resolvable
7
+ #
8
+ # Provides resolution capabilities for struct-like objects, allowing their
9
+ # values to be resolved recursively while maintaining the original struct type.
10
+ #
11
+ class ResolvableStruct < SimpleDelegator
12
+ include Resolvable
13
+
14
+ #
15
+ # Returns the wrapped struct object
16
+ #
17
+ # @return [Struct, Data, OpenStruct] The underlying struct-like object
18
+ #
19
+ def value
20
+ __getobj__
21
+ end
22
+
23
+ #
24
+ # Returns the struct with all values fully resolved and cached
25
+ #
26
+ # @return [Struct, Data, OpenStruct] A new struct-like object with resolved values
27
+ #
28
+ def resolved
29
+ hash = value.to_h.transform_values(&resolved_proc)
30
+ to_structlike(hash)
31
+ end
32
+
33
+ #
34
+ # Returns the struct with all values resolved (not cached)
35
+ #
36
+ # @return [Struct, Data, OpenStruct] A new struct-like object with resolved values
37
+ #
38
+ def resolve
39
+ hash = value.to_h.transform_values(&resolve_proc)
40
+ to_structlike(hash)
41
+ end
42
+
43
+ #
44
+ # Converts the struct's values into RSpec matchers
45
+ #
46
+ # @return [RSpec::Matchers::BuiltIn::BaseMatcher] An RSpec matcher for the struct
47
+ #
48
+ def resolve_as_matcher
49
+ result = value.to_h.transform_values(&resolve_as_matcher_proc)
50
+ Attribute::Literal.new(result).resolve_as_matcher
51
+ end
52
+
53
+ private
54
+
55
+ def to_structlike(hash)
56
+ case value
57
+ when OpenStruct
58
+ hash.to_ostruct
59
+ when Data
60
+ hash.to_istruct
61
+ else
62
+ hash.to_struct
63
+ end
64
+ end
65
+ end
66
+ end
67
+ end