spec_forge 0.4.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.standard.yml +4 -0
- data/CHANGELOG.md +145 -1
- data/README.md +49 -638
- data/flake.lock +3 -3
- data/flake.nix +8 -2
- data/lib/spec_forge/attribute/chainable.rb +208 -20
- data/lib/spec_forge/attribute/factory.rb +141 -12
- data/lib/spec_forge/attribute/faker.rb +64 -15
- data/lib/spec_forge/attribute/global.rb +96 -0
- data/lib/spec_forge/attribute/literal.rb +15 -2
- data/lib/spec_forge/attribute/matcher.rb +188 -13
- data/lib/spec_forge/attribute/parameterized.rb +45 -20
- data/lib/spec_forge/attribute/regex.rb +55 -5
- data/lib/spec_forge/attribute/resolvable.rb +48 -5
- data/lib/spec_forge/attribute/resolvable_array.rb +62 -4
- data/lib/spec_forge/attribute/resolvable_hash.rb +62 -4
- data/lib/spec_forge/attribute/store.rb +65 -0
- data/lib/spec_forge/attribute/transform.rb +33 -5
- data/lib/spec_forge/attribute/variable.rb +37 -6
- data/lib/spec_forge/attribute.rb +168 -66
- data/lib/spec_forge/backtrace_formatter.rb +26 -3
- data/lib/spec_forge/callbacks.rb +79 -0
- data/lib/spec_forge/cli/actions.rb +27 -0
- data/lib/spec_forge/cli/command.rb +78 -24
- data/lib/spec_forge/cli/init.rb +11 -1
- data/lib/spec_forge/cli/new.rb +54 -3
- data/lib/spec_forge/cli/run.rb +20 -0
- data/lib/spec_forge/cli.rb +16 -5
- data/lib/spec_forge/configuration.rb +94 -25
- data/lib/spec_forge/context/callbacks.rb +91 -0
- data/lib/spec_forge/context/global.rb +72 -0
- data/lib/spec_forge/context/store.rb +148 -0
- data/lib/spec_forge/context/variables.rb +91 -0
- data/lib/spec_forge/context.rb +36 -0
- data/lib/spec_forge/core_ext/rspec.rb +24 -4
- data/lib/spec_forge/error.rb +267 -113
- data/lib/spec_forge/factory.rb +33 -14
- data/lib/spec_forge/filter.rb +87 -0
- data/lib/spec_forge/forge.rb +170 -0
- data/lib/spec_forge/http/backend.rb +99 -29
- data/lib/spec_forge/http/client.rb +23 -13
- data/lib/spec_forge/http/request.rb +74 -62
- data/lib/spec_forge/http/verb.rb +79 -0
- data/lib/spec_forge/http.rb +105 -0
- data/lib/spec_forge/loader.rb +254 -0
- data/lib/spec_forge/matchers.rb +130 -0
- data/lib/spec_forge/normalizer/configuration.rb +24 -11
- data/lib/spec_forge/normalizer/constraint.rb +22 -9
- data/lib/spec_forge/normalizer/expectation.rb +31 -12
- data/lib/spec_forge/normalizer/factory.rb +24 -11
- data/lib/spec_forge/normalizer/factory_reference.rb +32 -13
- data/lib/spec_forge/normalizer/global_context.rb +88 -0
- data/lib/spec_forge/normalizer/spec.rb +39 -16
- data/lib/spec_forge/normalizer.rb +255 -41
- data/lib/spec_forge/runner/callbacks.rb +246 -0
- data/lib/spec_forge/runner/debug_proxy.rb +213 -0
- data/lib/spec_forge/runner/listener.rb +54 -0
- data/lib/spec_forge/runner/metadata.rb +58 -0
- data/lib/spec_forge/runner/state.rb +99 -0
- data/lib/spec_forge/runner.rb +133 -119
- data/lib/spec_forge/spec/expectation/constraint.rb +95 -20
- data/lib/spec_forge/spec/expectation.rb +43 -51
- data/lib/spec_forge/spec.rb +83 -96
- data/lib/spec_forge/type.rb +36 -4
- data/lib/spec_forge/version.rb +4 -1
- data/lib/spec_forge.rb +161 -76
- metadata +20 -5
- data/spec_forge/factories/user.yml +0 -4
- data/spec_forge/forge_helper.rb +0 -37
- data/spec_forge/specs/users.yml +0 -65
@@ -0,0 +1,96 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module SpecForge
|
4
|
+
class Attribute
|
5
|
+
#
|
6
|
+
# Represents an attribute that references values from the global context
|
7
|
+
#
|
8
|
+
# This class allows accessing shared data defined at the global level through
|
9
|
+
# namespaced references. It provides access to global variables that are shared
|
10
|
+
# across all specs in a file, enabling consistent test data without repetition.
|
11
|
+
#
|
12
|
+
# Currently supports the "variables" namespace.
|
13
|
+
#
|
14
|
+
# @example Basic usage in YAML
|
15
|
+
# # Reference a global variable in a spec
|
16
|
+
# session_token: global.variables.session_token
|
17
|
+
#
|
18
|
+
# # Using within a request body
|
19
|
+
# body:
|
20
|
+
# api_version: global.variables.api_version
|
21
|
+
# auth_token: global.variables.auth_token
|
22
|
+
#
|
23
|
+
class Global < Attribute
|
24
|
+
#
|
25
|
+
# Regular expression pattern that matches attribute keywords with this prefix
|
26
|
+
# Used for identifying this attribute type during parsing
|
27
|
+
#
|
28
|
+
# @return [Regexp]
|
29
|
+
#
|
30
|
+
KEYWORD_REGEX = /^global\./i
|
31
|
+
|
32
|
+
#
|
33
|
+
# An array of valid namespaces that can be access on global
|
34
|
+
#
|
35
|
+
# @return [Array<String>]
|
36
|
+
#
|
37
|
+
VALID_NAMESPACES = %w[
|
38
|
+
variables
|
39
|
+
].freeze
|
40
|
+
|
41
|
+
#
|
42
|
+
# Creates a new global attribute from the input string
|
43
|
+
#
|
44
|
+
# Parses the input string to extract the namespace to validate it
|
45
|
+
# Conversion happens when `#value` is called
|
46
|
+
#
|
47
|
+
# @raise [Error::InvalidGlobalNamespaceError] If an unsupported namespace is referenced
|
48
|
+
#
|
49
|
+
def initialize(...)
|
50
|
+
super
|
51
|
+
|
52
|
+
# Check to make sure the namespace is valid
|
53
|
+
namespace = input.split(".").second
|
54
|
+
|
55
|
+
if !VALID_NAMESPACES.include?(namespace)
|
56
|
+
raise Error::InvalidGlobalNamespaceError, namespace
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
#
|
61
|
+
# Converts the global reference into an underlying attribute
|
62
|
+
#
|
63
|
+
# Parses the input and returns the corresponding attribute based on the namespace.
|
64
|
+
# Currently supports extracting variables from the global context.
|
65
|
+
#
|
66
|
+
# @return [Attribute] An attribute representing the referenced global value
|
67
|
+
#
|
68
|
+
def value
|
69
|
+
# Skip the "global" prefix
|
70
|
+
components = input.split(".")[1..]
|
71
|
+
namespace = components.first
|
72
|
+
|
73
|
+
global_context = SpecForge.context.global
|
74
|
+
|
75
|
+
case namespace
|
76
|
+
when "variables"
|
77
|
+
variable_input = components.join(".")
|
78
|
+
variable = Attribute::Variable.new(variable_input)
|
79
|
+
variable.bind_variables(global_context.variables.to_h)
|
80
|
+
variable
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
#
|
85
|
+
# Resolves the global reference to its actual value
|
86
|
+
#
|
87
|
+
# Delegates resolution to the underlying attribute and caches the result
|
88
|
+
#
|
89
|
+
# @return [Object] The fully resolved value from the global context
|
90
|
+
#
|
91
|
+
def resolved
|
92
|
+
@resolved ||= value.resolved
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
@@ -2,12 +2,25 @@
|
|
2
2
|
|
3
3
|
module SpecForge
|
4
4
|
class Attribute
|
5
|
+
#
|
6
|
+
# Represents an attribute that is a literal value
|
7
|
+
#
|
8
|
+
# This is the simplest form of attribute, storing values like strings, numbers,
|
9
|
+
# and booleans without any processing.
|
10
|
+
#
|
11
|
+
# @example Basic usage in YAML
|
12
|
+
# name: "John Doe"
|
13
|
+
# age: 42
|
14
|
+
# active: true
|
15
|
+
#
|
5
16
|
class Literal < Attribute
|
17
|
+
# @return [Object] The literal value
|
6
18
|
attr_reader :value
|
7
19
|
|
8
20
|
#
|
9
|
-
#
|
10
|
-
#
|
21
|
+
# Creates a new literal attribute with the specified value
|
22
|
+
#
|
23
|
+
# @param input [Object] The value to store
|
11
24
|
#
|
12
25
|
def initialize(input)
|
13
26
|
super
|
@@ -2,28 +2,65 @@
|
|
2
2
|
|
3
3
|
module SpecForge
|
4
4
|
class Attribute
|
5
|
+
#
|
6
|
+
# Represents an attribute that uses RSpec matchers for response validation
|
7
|
+
#
|
8
|
+
# This class allows SpecForge to integrate with RSpec's powerful matchers
|
9
|
+
# for flexible response validation. It supports most of the built-in RSpec matchers
|
10
|
+
# and most custom matchers, assuming they do not require more Ruby code
|
11
|
+
#
|
12
|
+
# @example Basic matchers in YAML
|
13
|
+
# be_true: be.true
|
14
|
+
# include_admin:
|
15
|
+
# matcher.include:
|
16
|
+
# - admin
|
17
|
+
#
|
18
|
+
# @example Comparison matchers
|
19
|
+
# count:
|
20
|
+
# be.greater_than: 5
|
21
|
+
#
|
22
|
+
# @example Type checking
|
23
|
+
# name: kind_of.string
|
24
|
+
# id: kind_of.integer
|
25
|
+
#
|
5
26
|
class Matcher < Parameterized
|
6
|
-
|
27
|
+
#
|
28
|
+
# Helper class to access RSpec matcher methods
|
29
|
+
#
|
30
|
+
class RSpecMatchers
|
7
31
|
include RSpec::Matchers
|
8
32
|
end
|
9
33
|
|
10
|
-
|
11
|
-
|
34
|
+
#
|
35
|
+
# Regular expression pattern that matches attribute keywords with this prefix
|
36
|
+
# Used for identifying this attribute type during parsing
|
37
|
+
#
|
38
|
+
# @return [Regexp]
|
39
|
+
#
|
40
|
+
KEYWORD_REGEX = /^matchers?\.|^be\.|^kind_of\./i
|
41
|
+
|
42
|
+
#
|
43
|
+
# Instance of Methods providing access to all RSpec matchers
|
44
|
+
#
|
45
|
+
MATCHER_METHODS = RSpecMatchers.new.freeze
|
12
46
|
|
47
|
+
#
|
48
|
+
# Mapping of literal string values to their Ruby equivalents
|
49
|
+
# Used for be.nil, be.true, and be.false matchers
|
50
|
+
#
|
13
51
|
LITERAL_MAPPINGS = {
|
14
52
|
"nil" => nil,
|
15
53
|
"true" => true,
|
16
54
|
"false" => false
|
17
55
|
}.freeze
|
18
56
|
|
57
|
+
#
|
58
|
+
# The resolved RSpec matcher method to call
|
59
|
+
#
|
19
60
|
attr_reader :matcher_method
|
20
61
|
|
21
62
|
#
|
22
|
-
#
|
23
|
-
#
|
24
|
-
# matcher.<method>
|
25
|
-
# be.<method>
|
26
|
-
# kind_of.<method>
|
63
|
+
# Creates a new matcher attribute with the specified matcher and arguments
|
27
64
|
#
|
28
65
|
def initialize(...)
|
29
66
|
super
|
@@ -37,28 +74,87 @@ module SpecForge
|
|
37
74
|
when "kind_of"
|
38
75
|
resolve_kind_of_matcher(method)
|
39
76
|
else
|
40
|
-
|
77
|
+
resolve_base_matcher(method)
|
41
78
|
end
|
42
79
|
|
43
80
|
prepare_arguments!
|
81
|
+
|
82
|
+
# An argument can be an expanded version of something (such as matcher.include)
|
83
|
+
# Move it to where it belongs
|
84
|
+
if (keyword = arguments[:keyword]) && !Type.hash?(keyword)
|
85
|
+
arguments[:positional] << keyword
|
86
|
+
arguments[:keyword] = {}
|
87
|
+
end
|
44
88
|
end
|
45
89
|
|
90
|
+
#
|
91
|
+
# Returns the result of applying the matcher with the given arguments
|
92
|
+
# Creates an RSpec matcher that can be used in expectations
|
93
|
+
#
|
94
|
+
# @return [RSpec::Matchers::BuiltIn::BaseMatcher] The configured matcher
|
95
|
+
#
|
46
96
|
def value
|
47
|
-
if
|
48
|
-
positional =
|
97
|
+
if (positional = arguments[:positional]) && positional.present?
|
98
|
+
positional = positional.resolved.each do |value|
|
49
99
|
value.deep_stringify_keys! if value.respond_to?(:deep_stringify_keys!)
|
50
100
|
end
|
51
101
|
|
52
102
|
matcher_method.call(*positional)
|
53
|
-
elsif
|
54
|
-
matcher_method.call(**
|
103
|
+
elsif (keyword = arguments[:keyword]) && keyword.present?
|
104
|
+
matcher_method.call(**keyword.resolved.deep_stringify_keys)
|
55
105
|
else
|
56
106
|
matcher_method.call
|
57
107
|
end
|
58
108
|
end
|
59
109
|
|
110
|
+
#
|
111
|
+
# Ensures proper conversion of nested matcher arguments based on context
|
112
|
+
#
|
113
|
+
# This method overrides handles a special case of matchers that take arguments
|
114
|
+
# which themselves might need to be converted to matchers. It skips conversion
|
115
|
+
# for string arguments that should remain strings
|
116
|
+
# (like with include, start_with, and end_with) while correctly handling nested
|
117
|
+
# matchers and other argument types.
|
118
|
+
#
|
119
|
+
# @example Problem case handled
|
120
|
+
# # In YAML:
|
121
|
+
# matcher.all:
|
122
|
+
# matcher.include:
|
123
|
+
# - /@/ # Should become match(/@/) when used with include
|
124
|
+
#
|
125
|
+
# @example Edge case handled
|
126
|
+
# # In YAML:
|
127
|
+
# matcher.include: "." # Should remain a string, not eq(".")
|
128
|
+
#
|
129
|
+
# @return [RSpec::Matchers::BuiltIn::BaseMatcher] The properly configured matcher
|
130
|
+
# with all arguments correctly converted based on context
|
131
|
+
#
|
132
|
+
def resolve_as_matcher
|
133
|
+
# Argument conversion only matters for the base matchers
|
134
|
+
if input.start_with?("matcher")
|
135
|
+
block = lambda do |argument|
|
136
|
+
next argument unless convert_argument?(argument)
|
137
|
+
|
138
|
+
argument.resolve_as_matcher
|
139
|
+
end
|
140
|
+
|
141
|
+
arguments[:positional].map!(&block)
|
142
|
+
arguments[:keyword].transform_values!(&block)
|
143
|
+
end
|
144
|
+
|
145
|
+
super
|
146
|
+
end
|
147
|
+
|
60
148
|
private
|
61
149
|
|
150
|
+
#
|
151
|
+
# Extracts the namespace and method name from the input string
|
152
|
+
# For example, "be.empty" would return ["be", "empty"]
|
153
|
+
#
|
154
|
+
# @return [Array<String, String>] The namespace and method name
|
155
|
+
#
|
156
|
+
# @private
|
157
|
+
#
|
62
158
|
def extract_namespace_and_method
|
63
159
|
sections = input.split(".", 2)
|
64
160
|
|
@@ -69,10 +165,51 @@ module SpecForge
|
|
69
165
|
end
|
70
166
|
end
|
71
167
|
|
168
|
+
#
|
169
|
+
# Resolves a matcher with the "matcher" prefix
|
170
|
+
#
|
171
|
+
# @param method [String] The method part after "matcher."
|
172
|
+
#
|
173
|
+
# @return [Method] The resolved matcher method
|
174
|
+
#
|
175
|
+
# @private
|
176
|
+
#
|
177
|
+
def resolve_base_matcher(method)
|
178
|
+
if method == "and"
|
179
|
+
resolve_matcher("forge_and")
|
180
|
+
else
|
181
|
+
resolve_matcher(method)
|
182
|
+
end
|
183
|
+
end
|
184
|
+
|
185
|
+
#
|
186
|
+
# Resolves a matcher method by name from the given namespace
|
187
|
+
#
|
188
|
+
# @param method_name [String, Symbol] The matcher method name
|
189
|
+
# @param namespace [Object] The object to resolve the method from
|
190
|
+
#
|
191
|
+
# @return [Method] The resolved matcher method
|
192
|
+
#
|
193
|
+
# @private
|
194
|
+
#
|
72
195
|
def resolve_matcher(method_name, namespace: MATCHER_METHODS)
|
196
|
+
if !namespace.respond_to?(method_name)
|
197
|
+
raise Error::UndefinedMatcherError, method_name
|
198
|
+
end
|
199
|
+
|
73
200
|
namespace.public_method(method_name)
|
74
201
|
end
|
75
202
|
|
203
|
+
#
|
204
|
+
# Resolves a matcher with the "be" prefix
|
205
|
+
# Handles special cases like be.true, be.nil, comparison operators, etc.
|
206
|
+
#
|
207
|
+
# @param method [String] The method part after "be."
|
208
|
+
#
|
209
|
+
# @return [Method] The resolved matcher method
|
210
|
+
#
|
211
|
+
# @private
|
212
|
+
#
|
76
213
|
def resolve_be_matcher(method)
|
77
214
|
# Resolve any custom matchers
|
78
215
|
resolved_matcher =
|
@@ -107,12 +244,50 @@ module SpecForge
|
|
107
244
|
resolve_matcher(:"be_#{method}")
|
108
245
|
end
|
109
246
|
|
247
|
+
#
|
248
|
+
# Resolves a kind_of matcher for the given type
|
249
|
+
# For example, kind_of.string would check if an object is a String
|
250
|
+
#
|
251
|
+
# @param method [String] The type name to check for
|
252
|
+
#
|
253
|
+
# @return [Method] The resolved matcher method
|
254
|
+
#
|
255
|
+
# @private
|
256
|
+
#
|
110
257
|
def resolve_kind_of_matcher(method)
|
111
258
|
type_class = Object.const_get(method.capitalize)
|
112
259
|
arguments[:positional].insert(0, type_class)
|
113
260
|
|
114
261
|
resolve_matcher(:be_kind_of)
|
115
262
|
end
|
263
|
+
|
264
|
+
#
|
265
|
+
# Determines whether an argument should skip conversion to a matcher
|
266
|
+
#
|
267
|
+
# This helper method handles the case where string arguments to certain matchers
|
268
|
+
# (include, start_with, end_with) should remain as strings rather than being
|
269
|
+
# converted to eq() matchers.
|
270
|
+
#
|
271
|
+
# @param argument [Object] The argument to analyze
|
272
|
+
#
|
273
|
+
# @return [Boolean] true if the argument should skip conversion, false otherwise
|
274
|
+
#
|
275
|
+
# @example Skip conversion
|
276
|
+
# skip_argument_conversion?(Attribute::Literal.new(".")) #=> true
|
277
|
+
# # When used with include, start_with, or end_with
|
278
|
+
#
|
279
|
+
# @example Apply conversion
|
280
|
+
# skip_argument_conversion?(Attribute::Regex.new("/@/")) #=> false
|
281
|
+
# # Regex should be converted to match(/@/)
|
282
|
+
#
|
283
|
+
def convert_argument?(argument)
|
284
|
+
return true if argument.is_a?(Attribute::Matcher) || argument.is_a?(Attribute::Regex)
|
285
|
+
|
286
|
+
return true unless [:include, :start_with, :end_with].include?(matcher_method.name)
|
287
|
+
|
288
|
+
resolved = argument.resolved
|
289
|
+
resolved.is_a?(Array) || resolved.is_a?(Hash)
|
290
|
+
end
|
116
291
|
end
|
117
292
|
end
|
118
293
|
end
|
@@ -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)
|
53
|
+
#
|
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
|
+
# }
|
29
59
|
#
|
30
|
-
|
31
|
-
|
32
|
-
# <keyword_arg>: <value>
|
60
|
+
attr_reader :arguments
|
61
|
+
|
33
62
|
#
|
34
|
-
#
|
35
|
-
# <attribute>:
|
36
|
-
# - <positional_arg>
|
37
|
-
# - <positional_arg>
|
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,17 +91,11 @@ 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
|
66
|
-
|
67
|
-
def uses_positional_arguments?(method)
|
68
|
-
method.parameters.any? { |a| [:req, :opt, :rest].include?(a.first) }
|
69
|
-
end
|
70
|
-
|
71
|
-
def uses_keyword_arguments?(method)
|
72
|
-
method.parameters.any? { |a| [:keyreq, :key, :keyrest].include?(a.first) }
|
73
|
-
end
|
74
99
|
end
|
75
100
|
end
|
76
101
|
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
|