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.
Files changed (71) hide show
  1. checksums.yaml +4 -4
  2. data/.standard.yml +4 -0
  3. data/CHANGELOG.md +145 -1
  4. data/README.md +49 -638
  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 +141 -12
  9. data/lib/spec_forge/attribute/faker.rb +64 -15
  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 +188 -13
  13. data/lib/spec_forge/attribute/parameterized.rb +45 -20
  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 +168 -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 -25
  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 +24 -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 +22 -9
  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 +32 -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 +133 -119
  62. data/lib/spec_forge/spec/expectation/constraint.rb +95 -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 -37
  71. data/spec_forge/specs/users.yml +0 -65
data/flake.lock CHANGED
@@ -20,11 +20,11 @@
20
20
  },
21
21
  "nixpkgs": {
22
22
  "locked": {
23
- "lastModified": 1737469691,
24
- "narHash": "sha256-nmKOgAU48S41dTPIXAq0AHZSehWUn6ZPrUKijHAMmIk=",
23
+ "lastModified": 1742422364,
24
+ "narHash": "sha256-mNqIplmEohk5jRkqYqG19GA8MbQ/D4gQSK0Mu4LvfRQ=",
25
25
  "owner": "NixOS",
26
26
  "repo": "nixpkgs",
27
- "rev": "9e4d5190a9482a1fb9d18adf0bdb83c6e506eaab",
27
+ "rev": "a84ebe20c6bc2ecbcfb000a50776219f48d134cc",
28
28
  "type": "github"
29
29
  },
30
30
  "original": {
data/flake.nix CHANGED
@@ -6,8 +6,14 @@
6
6
  flake-utils.url = "github:numtide/flake-utils";
7
7
  };
8
8
 
9
- outputs = { self, nixpkgs, flake-utils }:
10
- flake-utils.lib.eachDefaultSystem (system:
9
+ outputs =
10
+ {
11
+ self,
12
+ nixpkgs,
13
+ flake-utils,
14
+ }:
15
+ flake-utils.lib.eachDefaultSystem (
16
+ system:
11
17
  let
12
18
  pkgs = nixpkgs.legacyPackages.${system};
13
19
  in
@@ -2,84 +2,272 @@
2
2
 
3
3
  module SpecForge
4
4
  class Attribute
5
+ #
6
+ # Adds support for an attribute to accept n-number of chained calls. It supports chaining
7
+ # methods, hash keys, and array indexes. It also works well alongside Parameterized attributes
8
+ #
9
+ # This module requires being included into a class first before it can be used
10
+ #
11
+ # @example Basic usage in YAML
12
+ # my_variable: variable.users.first
13
+ #
14
+ # @example Advanced usage in YAML
15
+ # my_variable: variable.users.0.posts.second.author.name
16
+ #
17
+ # @example Basic usage in code
18
+ # faker = SpecForge::Attribute.from("faker.name.name.upcase")
19
+ # faker.resolved #=> BENDING UNIT 22
20
+ #
5
21
  module Chainable
22
+ #
23
+ # Regular expression that matches pure numeric strings
24
+ # Used for detecting potential array index operations
25
+ #
26
+ # @return [Regexp] A case-insensitive regex matching strings containing only digits
27
+ #
6
28
  NUMBER_REGEX = /^\d+$/i
7
29
 
8
- attr_reader :header, :invocation_chain, :base_object
30
+ #
31
+ # The first part of the chained attribute
32
+ #
33
+ # @return [Symbol] The first component of the chained attribute
34
+ #
35
+ attr_reader :keyword
36
+
37
+ #
38
+ # The second part of the chained attribute
39
+ #
40
+ # @return [Symbol] The second component of the chained attribute
41
+ #
42
+ attr_reader :header
43
+
44
+ #
45
+ # The remaining parts of the attribute chain after the header
46
+ #
47
+ # @return [Array<Symbol>] The remaining method/key invocations in the chain
48
+ #
49
+ attr_reader :invocation_chain
9
50
 
10
51
  #
11
- # Represents any attribute that is a series of chained invocations:
52
+ # The initial object from which the chain will start traversing
53
+ #
54
+ # @return [Object] The base object that starts the method/attribute chain
12
55
  #
13
- # <keyword>.<header>.<segment(hash_key | method | index)>...
56
+ attr_reader :base_object
57
+
14
58
  #
15
- # This module is not used as is, but is included in another class.
16
- # Note: There can be any n number of segments.
59
+ # Initializes a new chainable attribute by parsing the input into components
17
60
  #
18
61
  def initialize(...)
19
62
  super
20
63
 
21
- # Drop the keyword
22
- sections = input.split(".")[1..]
64
+ sections = input.split(".")
23
65
 
24
- @header = sections.first&.to_sym
25
- @invocation_chain = sections[1..] || []
66
+ @keyword = sections.first.to_sym
67
+ @header = sections.second&.to_sym
68
+ @invocation_chain = sections[2..] || []
26
69
  end
27
70
 
71
+ #
72
+ # Returns the value of this attribute by resolving the chain
73
+ # Will return a new value on each call for dynamic attributes like Faker
74
+ #
75
+ # @return [Object] The result of invoking the chain on the base object
76
+ #
28
77
  def value
29
78
  invoke_chain
30
79
  end
31
80
 
32
- def resolve
81
+ #
82
+ # Resolves the chain and stores the result
83
+ # The result is memoized, so subsequent calls return the same value
84
+ # even for dynamic attributes like Faker and Factory
85
+ #
86
+ # @return [Object] The fully resolved and memoized value
87
+ #
88
+ def resolved
33
89
  @resolved ||= resolve_chain
34
90
  end
35
91
 
36
92
  private
37
93
 
94
+ #
95
+ # Invokes the chain by calling #value on each object
96
+ #
97
+ # @return [Object] The result of invoking the chain
98
+ #
99
+ # @private
100
+ #
38
101
  def invoke_chain
39
102
  traverse_chain(resolve: false)
40
103
  end
41
104
 
105
+ #
106
+ # Resolves the chain by calling #resolve on each object
107
+ #
108
+ # @return [Object] The fully resolved result
109
+ #
110
+ # @private
111
+ #
42
112
  def resolve_chain
43
- __resolve(traverse_chain(resolve: true))
113
+ traverse_chain(resolve: true)
44
114
  end
45
115
 
116
+ #
117
+ # Traverses the chain of invocations step by step
118
+ #
119
+ # @param resolve [Boolean] Whether to use resolve during traversal
120
+ #
121
+ # @return [Object] The result of the traversal
122
+ #
123
+ # @private
124
+ #
46
125
  def traverse_chain(resolve:)
47
- result = invocation_chain.reduce(base_object) do |current_value, step|
48
- next_value = retrieve_value(current_value, resolve:)
126
+ resolution_path = {}
127
+
128
+ current_path = "#{keyword}.#{header}"
129
+ current_object = base_object
130
+
131
+ invocation_chain.each do |step|
132
+ next_value = retrieve_value(current_object, resolve:)
133
+
134
+ # Store this step's resolution for error reporting
135
+ resolution_path[current_path] = describe_value(next_value)
136
+ current_path += ".#{step}"
137
+
138
+ # Try to invoke the next step
139
+ current_object = invoke(step, next_value)
140
+ rescue Error::InvalidInvocationError => e
141
+ resolution_path[current_path] = "Error: #{e.message}"
49
142
 
50
- invoke(step, next_value)
143
+ raise e.with_resolution_path(resolution_path)
51
144
  end
52
145
 
53
- retrieve_value(result, resolve:)
146
+ # Return final result
147
+ retrieve_value(current_object, resolve:)
54
148
  end
55
149
 
150
+ #
151
+ # Retrieves the value from an object, resolving it if needed
152
+ #
153
+ # @param object [Object] The object to retrieve a value from
154
+ # @param resolve [Boolean] Whether to resolve the object's value
155
+ #
156
+ # @return [Object] The retrieved value
157
+ #
158
+ # @private
159
+ #
56
160
  def retrieve_value(object, resolve:)
57
161
  return object unless object.is_a?(Attribute)
58
162
 
59
- resolve ? object.resolve : object.value
163
+ resolve ? object.resolved : object.value
164
+ end
165
+
166
+ #
167
+ # Creates a description of a value for error messages
168
+ #
169
+ # @param value [Object] The value to describe
170
+ #
171
+ # @return [String] A description
172
+ #
173
+ # @private
174
+ #
175
+ def describe_value(value)
176
+ case value
177
+ when Context::Store::Entry
178
+ "Store with attributes: #{value.available_methods.join_map(", ", &:in_quotes)}"
179
+ when OpenStruct
180
+ "Object with attributes: #{value.table.keys.join_map(", ", &:in_quotes)}"
181
+ when Struct, Data
182
+ "Object with attributes: #{value.members.join_map(", ", &:in_quotes)}"
183
+ when ArrayLike
184
+ # Preview the first 5 value's classes
185
+ preview = value.take(5).map(&:class)
186
+ preview << "..." if value.size > 5
187
+
188
+ "Array with #{value.size} #{"element".pluralize(value.size)}: #{preview}"
189
+ when HashLike
190
+ # Preview the first 5 keys
191
+ keys = value.keys.take(5)
192
+
193
+ preview = keys.join_map(", ") { |key| "\"#{key}\"" }
194
+ preview += ", ..." if value.keys.size > 5
195
+
196
+ "Hash with #{"key".pluralize(keys.size)}: #{preview}"
197
+ when String
198
+ "\"#{value.truncate(50)}\""
199
+ when NilClass
200
+ "nil"
201
+ when Proc
202
+ "Proc defined at #{value.source_location.join(":")}"
203
+ else
204
+ "#{value.class}: #{value.inspect[0..50]}"
205
+ end
60
206
  end
61
207
 
208
+ #
209
+ # Invokes an operation on an object based on the step type (hash key, array index, or method)
210
+ #
211
+ # @param step [String] The step to invoke
212
+ # @param object [Object] The object to invoke the step on
213
+ #
214
+ # @return [Object] The result of the invocation
215
+ #
216
+ # @raise [Error::InvalidInvocationError] If the step cannot be invoked on the object
217
+ #
218
+ # @private
219
+ #
62
220
  def invoke(step, object)
63
221
  if hash_key?(object, step)
64
- object[step.to_sym]
222
+ object[step.to_s] || object[step.to_sym]
65
223
  elsif index?(object, step)
66
224
  object[step.to_i]
67
225
  elsif method?(object, step)
68
226
  object.public_send(step)
69
227
  else
70
- raise InvalidInvocationError.new(step, object)
228
+ raise Error::InvalidInvocationError.new(step, object)
71
229
  end
72
230
  end
73
231
 
232
+ #
233
+ # Checks if the object can be accessed with the given key
234
+ #
235
+ # @param object [Object] The object to check
236
+ # @param key [String] The key to check
237
+ #
238
+ # @return [Boolean] Whether the object supports hash-like access with the key
239
+ #
240
+ # @private
241
+ #
74
242
  def hash_key?(object, key)
75
- # This is to support the silly delegator
76
- method?(object, :key?) && object.key?(key.to_sym)
243
+ # This is to support the silly delegator and both symbol/string
244
+ method?(object, :key?) && (object.key?(key.to_s) || object.key?(key.to_sym))
77
245
  end
78
246
 
247
+ #
248
+ # Checks if the object responds to the given method
249
+ #
250
+ # @param object [Object] The object to check
251
+ # @param method_name [String, Symbol] The method name to check
252
+ #
253
+ # @return [Boolean] Whether the object responds to the method
254
+ #
255
+ # @private
256
+ #
79
257
  def method?(object, method_name)
80
258
  object.respond_to?(method_name)
81
259
  end
82
260
 
261
+ #
262
+ # Checks if the object supports array-like access with the given index
263
+ #
264
+ # @param object [Object] The object to check
265
+ # @param step [String] The potential index
266
+ #
267
+ # @return [Boolean] Whether the object supports array-like access with the step
268
+ #
269
+ # @private
270
+ #
83
271
  def index?(object, step)
84
272
  # This is to support the silly delegator
85
273
  method?(object, :index) && step.match?(NUMBER_REGEX)
@@ -2,24 +2,79 @@
2
2
 
3
3
  module SpecForge
4
4
  class Attribute
5
+ #
6
+ # Represents an attribute that references a factory to generate test data
7
+ #
8
+ # This class allows SpecForge to integrate with FactoryBot for test data generation.
9
+ # It supports various build strategies like create, build, build_stubbed, etc.
10
+ #
11
+ # @example Basic usage in YAML
12
+ # user: factories.user
13
+ #
14
+ # @example With custom attributes
15
+ # user:
16
+ # factories.user:
17
+ # attributes:
18
+ # name: "Custom Name"
19
+ # email: faker.internet.email
20
+ #
21
+ # @example With build strategy
22
+ # user:
23
+ # factories.user:
24
+ # strategy: build
25
+ # attributes:
26
+ # admin: true
27
+ #
28
+ # @example With an array of 5 user attributes
29
+ # user:
30
+ # factories.user:
31
+ # strategy: attributes_for
32
+ # size: 5
33
+ # attributes:
34
+ # admin: true
35
+ #
5
36
  class Factory < Parameterized
6
37
  include Chainable
7
38
 
39
+ #
40
+ # Regular expression pattern that matches attribute keywords with this prefix
41
+ # Used for identifying this attribute type during parsing
42
+ #
43
+ # @return [Regexp]
44
+ #
8
45
  KEYWORD_REGEX = /^factories\./i
9
46
 
10
- BUILD_STRATEGIES = %w[
47
+ #
48
+ # An array of base strategies that can be provided either with or
49
+ # without a size. "stubbed" will automatically be transformed into "build_stubbed"
50
+ #
51
+ # @return [Array<String>]
52
+ #
53
+ BASE_STRATEGIES = %w[
11
54
  build
12
55
  create
56
+ build_stubbed
57
+ attributes_for
58
+ ].freeze
59
+
60
+ # @return [Array<String>] All available build strategies
61
+ BUILD_STRATEGIES = %w[
13
62
  attributes_for
63
+ attributes_for_list
64
+ build
65
+ build_list
66
+ build_pair
14
67
  build_stubbed
68
+ build_stubbed_list
69
+ create
70
+ create_list
71
+ create_pair
15
72
  ].freeze
16
73
 
17
74
  alias_method :factory_name, :header
18
75
 
19
76
  #
20
- # Represents any attribute that is a factory reference
21
- #
22
- # factories.<factory_name>
77
+ # Creates a new factory attribute with the specified name and arguments
23
78
  #
24
79
  def initialize(...)
25
80
  super
@@ -30,21 +85,95 @@ module SpecForge
30
85
  prepare_arguments!
31
86
  end
32
87
 
33
- private
34
-
88
+ #
89
+ # Returns the base object for the variable chain
90
+ #
91
+ # @return [Object] The result of the FactoryBot call
92
+ #
35
93
  def base_object
36
94
  attributes = arguments[:keyword]
95
+
96
+ # Default functionality is to create ("factory.user")
37
97
  return FactoryBot.create(factory_name) if attributes.blank?
38
98
 
39
- # Determine build strat
40
- build_strategy = attributes[:build_strategy].resolve_value
99
+ build_arguments = construct_factory_parameters(attributes)
100
+ FactoryBot.public_send(*build_arguments)
101
+ end
102
+
103
+ #
104
+ # Similar to #resolved but doesn't cache the result, allowing for re-resolution.
105
+ # Recursively calls #resolve on all nested attributes without storing results.
106
+ #
107
+ # Use this when you need to ensure fresh values each time, particularly with
108
+ # factories or other attributes that should generate new values on each call.
109
+ #
110
+ # @return [Object] The completely resolved value without caching
111
+ #
112
+ # @example
113
+ # factory_attr = Attribute::Factory.new("factories.user")
114
+ # factory_attr.resolve # => User#1 (a new user)
115
+ # factory_attr.resolve # => User#2 (another new user)
116
+ #
117
+ def resolve
118
+ case value
119
+ when ArrayLike
120
+ value.map(&resolved_proc)
121
+ when HashLike
122
+ value.transform_values(&resolved_proc)
123
+ else
124
+ value
125
+ end
126
+ end
127
+
128
+ private
129
+
130
+ #
131
+ # @private
132
+ #
133
+ def construct_factory_parameters(attributes)
134
+ build_strategy, list_size = determine_build_strategy(attributes)
135
+
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
+ ]
143
+
144
+ # Insert the list size after the strategy
145
+ # FactoryBot.<build_strategy>_list(factory_name, list_size, **attributes)
146
+ if build_strategy.end_with?("_list")
147
+ build_arguments.insert(2, list_size)
148
+ end
149
+
150
+ build_arguments
151
+ end
152
+
153
+ #
154
+ # @private
155
+ #
156
+ def determine_build_strategy(attributes)
157
+ # Determine build strat, and unfreeze
158
+ build_strategy = +attributes[:build_strategy].resolve
159
+ list_size = attributes[:size].resolve
41
160
 
42
161
  # stubbed => build_stubbed
43
- build_strategy.prepend("build_") if build_strategy == "stubbed"
44
- raise InvalidBuildStrategy, build_strategy unless BUILD_STRATEGIES.include?(build_strategy)
162
+ build_strategy.prepend("build_") if build_strategy.start_with?("stubbed")
163
+
164
+ # create + size => create_list
165
+ # build + size => build_list
166
+ # build_stubbed + size => build_stubbed_list
167
+ # attributes_for + size => attributes_for_list
168
+ if list_size.positive? && BASE_STRATEGIES.include?(build_strategy)
169
+ build_strategy += "_list"
170
+ end
171
+
172
+ if !BUILD_STRATEGIES.include?(build_strategy)
173
+ raise Error::InvalidBuildStrategy, build_strategy
174
+ end
45
175
 
46
- attributes = attributes[:attributes].resolve_value
47
- FactoryBot.public_send(build_strategy, factory_name, **attributes)
176
+ [build_strategy, list_size]
48
177
  end
49
178
  end
50
179
  end
@@ -2,17 +2,44 @@
2
2
 
3
3
  module SpecForge
4
4
  class Attribute
5
+ #
6
+ # Represents an attribute that generates fake data using the Faker gem
7
+ #
8
+ # This class allows SpecForge to integrate with the Faker library to generate realistic
9
+ # test data like names, emails, addresses, etc.
10
+ #
11
+ # @example Basic usage in YAML
12
+ # name: faker.name.name
13
+ # email: faker.internet.email
14
+ #
15
+ # @example With method arguments
16
+ # age:
17
+ # faker.number.between:
18
+ # from: 18
19
+ # to: 65
20
+ #
21
+ # @example Handles nested faker classes
22
+ # character: faker.games.zelda.character
23
+ #
5
24
  class Faker < Parameterized
6
25
  include Chainable
7
26
 
27
+ #
28
+ # Regular expression pattern that matches attribute keywords with this prefix
29
+ # Used for identifying this attribute type during parsing
30
+ #
31
+ # @return [Regexp]
32
+ #
8
33
  KEYWORD_REGEX = /^faker\./i
9
34
 
10
- attr_reader :faker_class, :faker_method
35
+ # @return [Class] The Faker class
36
+ attr_reader :faker_class
37
+
38
+ # @return [Method] The Faker class method
39
+ attr_reader :faker_method
11
40
 
12
41
  #
13
- # Represents any attribute that is a faker call
14
- #
15
- # faker.<faker_class>.<faker_method>
42
+ # Creates a new faker attribute with the specified name and arguments
16
43
  #
17
44
  def initialize(...)
18
45
  super
@@ -22,18 +49,37 @@ module SpecForge
22
49
  prepare_arguments!
23
50
  end
24
51
 
25
- private
26
-
52
+ #
53
+ # Returns the base object for the variable chain
54
+ #
55
+ # @return [Object] The result of the Faker call
56
+ #
27
57
  def base_object
28
- if uses_positional_arguments?(faker_method)
29
- faker_method.call(*arguments[:positional].resolve)
30
- elsif uses_keyword_arguments?(faker_method)
31
- faker_method.call(**arguments[:keyword].resolve)
58
+ if (positional = arguments[:positional]) && positional.present?
59
+ faker_method.call(*positional.resolved)
60
+ elsif (keyword = arguments[:keyword]) && keyword.present?
61
+ faker_method.call(**keyword.resolved)
32
62
  else
33
63
  faker_method.call
34
64
  end
35
65
  end
36
66
 
67
+ private
68
+
69
+ #
70
+ # Extracts the Faker class and method from the input string
71
+ # Handles both simple cases like "faker.name.first_name" and complex
72
+ # nested namespaces like "faker.games.zelda.game"
73
+ #
74
+ # @return [Array<Class, Method>] A two-element array containing:
75
+ # 1. The resolved Faker class (e.g., Faker::Name)
76
+ # 2. The method object to call on that class (e.g., #first_name)
77
+ #
78
+ # @raise [Error::InvalidFakerClassError] If the specified Faker class doesn't exist
79
+ # @raise [Error::InvalidFakerMethodError] If the specified method doesn't exist on the class
80
+ #
81
+ # @private
82
+ #
37
83
  def extract_faker_call
38
84
  class_name = header.downcase.to_s
39
85
 
@@ -42,10 +88,10 @@ module SpecForge
42
88
  return resolve_faker_class_and_method(class_name, invocation_chain.shift)
43
89
  end
44
90
 
45
- # Try each part of the chain as a potential class name
46
- # Example: faker.games.zelda.game.underscore
47
91
  namespace = []
48
92
 
93
+ # Try each part of the chain as a potential class name
94
+ # Example: faker.games.zelda.game.underscore
49
95
  while invocation_chain.any?
50
96
  part = invocation_chain.first.downcase
51
97
  test_class_name = ([class_name] + namespace + [part]).map(&:camelize).join("::")
@@ -65,22 +111,25 @@ module SpecForge
65
111
 
66
112
  # If we get here, we consumed all parts as classes but found no method
67
113
  class_name = ([class_name] + namespace).map(&:camelize).join("::")
68
- raise InvalidFakerMethodError.new(nil, "::#{class_name}".constantize)
114
+ raise Error::InvalidFakerMethodError.new(nil, "::#{class_name}".constantize)
69
115
  end
70
116
 
117
+ #
118
+ # @private
119
+ #
71
120
  def resolve_faker_class_and_method(class_name, method_name)
72
121
  # Load the class
73
122
  faker_class = begin
74
123
  "::Faker::#{class_name.camelize}".constantize
75
124
  rescue NameError
76
- raise InvalidFakerClassError, class_name
125
+ raise Error::InvalidFakerClassError, class_name
77
126
  end
78
127
 
79
128
  # Load the method
80
129
  faker_method = begin
81
130
  faker_class.method(method_name)
82
131
  rescue NameError
83
- raise InvalidFakerMethodError.new(method_name, faker_class)
132
+ raise Error::InvalidFakerMethodError.new(method_name, faker_class)
84
133
  end
85
134
 
86
135
  [faker_class, faker_method]