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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +75 -1
- data/README.md +124 -202
- data/bin/spec_forge +1 -1
- data/flake.lock +76 -4
- data/flake.nix +5 -4
- data/lib/spec_forge/attribute/chainable.rb +6 -6
- data/lib/spec_forge/attribute/environment.rb +45 -0
- data/lib/spec_forge/attribute/factory.rb +26 -17
- data/lib/spec_forge/attribute/faker.rb +6 -1
- data/lib/spec_forge/attribute/generate.rb +114 -0
- data/lib/spec_forge/attribute/literal.rb +1 -14
- data/lib/spec_forge/attribute/matcher.rb +6 -2
- data/lib/spec_forge/attribute/parameterized.rb +20 -22
- data/lib/spec_forge/attribute/resolvable_array.rb +16 -16
- data/lib/spec_forge/attribute/resolvable_hash.rb +17 -16
- data/lib/spec_forge/attribute/resolvable_struct.rb +67 -0
- data/lib/spec_forge/attribute/template.rb +118 -0
- data/lib/spec_forge/attribute/transform.rb +14 -19
- data/lib/spec_forge/attribute/variable.rb +31 -31
- data/lib/spec_forge/attribute.rb +54 -100
- data/lib/spec_forge/blueprint.rb +27 -0
- data/lib/spec_forge/cli/docs/generate.rb +28 -8
- data/lib/spec_forge/cli/docs.rb +5 -2
- data/lib/spec_forge/cli/init.rb +4 -4
- data/lib/spec_forge/cli/new.rb +78 -27
- data/lib/spec_forge/cli/run.rb +84 -52
- data/lib/spec_forge/cli/serve.rb +5 -0
- data/lib/spec_forge/cli.rb +6 -14
- data/lib/spec_forge/configuration.rb +209 -79
- data/lib/spec_forge/documentation/{loader → builder}/cache.rb +26 -23
- data/lib/spec_forge/documentation/builder/compiler.rb +373 -0
- data/lib/spec_forge/documentation/builder/extractor.rb +75 -0
- data/lib/spec_forge/documentation/builder.rb +77 -329
- data/lib/spec_forge/documentation/document/operation.rb +4 -4
- data/lib/spec_forge/documentation/document.rb +0 -6
- data/lib/spec_forge/documentation/generator.rb +88 -0
- data/lib/spec_forge/documentation/{generators/openapi → openapi/v3_0}/error_formatter.rb +2 -2
- data/lib/spec_forge/documentation/openapi/v3_0/example.rb +1 -1
- data/lib/spec_forge/documentation/openapi/v3_0/media_type.rb +1 -1
- data/lib/spec_forge/documentation/openapi/v3_0/operation.rb +21 -5
- data/lib/spec_forge/documentation/openapi/v3_0/response.rb +28 -6
- data/lib/spec_forge/documentation/openapi/v3_0/schema.rb +20 -2
- data/lib/spec_forge/documentation/openapi/v3_0/tag.rb +1 -1
- data/lib/spec_forge/documentation/openapi/v3_0.rb +116 -0
- data/lib/spec_forge/documentation/openapi.rb +40 -12
- data/lib/spec_forge/documentation.rb +1 -7
- data/lib/spec_forge/error.rb +215 -41
- data/lib/spec_forge/factory.rb +38 -18
- data/lib/spec_forge/forge/action.rb +41 -0
- data/lib/spec_forge/forge/actions/call.rb +33 -0
- data/lib/spec_forge/forge/actions/debug.rb +47 -0
- data/lib/spec_forge/forge/actions/expect.rb +44 -0
- data/lib/spec_forge/forge/actions/request.rb +65 -0
- data/lib/spec_forge/forge/actions/store.rb +31 -0
- data/lib/spec_forge/forge/callbacks.rb +80 -0
- data/lib/spec_forge/forge/context.rb +41 -0
- data/lib/spec_forge/forge/display.rb +503 -0
- data/lib/spec_forge/forge/hooks.rb +131 -0
- data/lib/spec_forge/forge/runner/array_io.rb +81 -0
- data/lib/spec_forge/forge/runner/content_validator.rb +92 -0
- data/lib/spec_forge/forge/runner/header_validator.rb +66 -0
- data/lib/spec_forge/forge/runner/reporter.rb +56 -0
- data/lib/spec_forge/forge/runner/schema_validator.rb +113 -0
- data/lib/spec_forge/forge/runner.rb +118 -0
- data/lib/spec_forge/forge/timer.rb +94 -0
- data/lib/spec_forge/forge/variables.rb +38 -0
- data/lib/spec_forge/forge.rb +207 -133
- data/lib/spec_forge/http/backend.rb +49 -146
- data/lib/spec_forge/http/client.rb +14 -17
- data/lib/spec_forge/http/request.rb +37 -84
- data/lib/spec_forge/http/verb.rb +4 -0
- data/lib/spec_forge/http.rb +0 -5
- data/lib/spec_forge/loader/filter.rb +85 -0
- data/lib/spec_forge/loader/step_processor.rb +282 -0
- data/lib/spec_forge/loader.rb +105 -220
- data/lib/spec_forge/normalizer/default.rb +1 -1
- data/lib/spec_forge/normalizer/structure.rb +140 -0
- data/lib/spec_forge/normalizer/transformers.rb +168 -0
- data/lib/spec_forge/normalizer/validators.rb +50 -8
- data/lib/spec_forge/normalizer.rb +76 -119
- data/lib/spec_forge/normalizers/callback.yml +38 -0
- data/lib/spec_forge/normalizers/configuration.yml +59 -9
- data/lib/spec_forge/normalizers/factory.yml +53 -2
- data/lib/spec_forge/normalizers/factory_reference.yml +63 -2
- data/lib/spec_forge/normalizers/json_schema.yml +79 -0
- data/lib/spec_forge/normalizers/step.yml +506 -0
- data/lib/spec_forge/step/call.rb +36 -0
- data/lib/spec_forge/step/expect.rb +110 -0
- data/lib/spec_forge/step/source.rb +22 -0
- data/lib/spec_forge/step.rb +129 -0
- data/lib/spec_forge/type.rb +115 -66
- data/lib/spec_forge/version.rb +1 -1
- data/lib/spec_forge.rb +44 -106
- data/lib/templates/forge_helper.rb.tt +43 -22
- data/lib/templates/new_blueprint.yml.tt +54 -0
- metadata +75 -44
- data/lib/spec_forge/attribute/global.rb +0 -96
- data/lib/spec_forge/attribute/store.rb +0 -65
- data/lib/spec_forge/backtrace_formatter.rb +0 -50
- data/lib/spec_forge/callbacks.rb +0 -88
- data/lib/spec_forge/context/callbacks.rb +0 -91
- data/lib/spec_forge/context/global.rb +0 -72
- data/lib/spec_forge/context/store.rb +0 -131
- data/lib/spec_forge/context/variables.rb +0 -91
- data/lib/spec_forge/context.rb +0 -36
- data/lib/spec_forge/core_ext/rspec.rb +0 -55
- data/lib/spec_forge/core_ext.rb +0 -5
- data/lib/spec_forge/documentation/generators/base.rb +0 -81
- data/lib/spec_forge/documentation/generators/openapi/base.rb +0 -100
- data/lib/spec_forge/documentation/generators/openapi/v3_0.rb +0 -65
- data/lib/spec_forge/documentation/generators/openapi.rb +0 -59
- data/lib/spec_forge/documentation/generators.rb +0 -17
- data/lib/spec_forge/documentation/loader.rb +0 -159
- data/lib/spec_forge/documentation/openapi/base.rb +0 -33
- data/lib/spec_forge/filter.rb +0 -86
- data/lib/spec_forge/normalizer/definition.rb +0 -248
- data/lib/spec_forge/normalizers/_shared.yml +0 -76
- data/lib/spec_forge/normalizers/constraint.yml +0 -8
- data/lib/spec_forge/normalizers/expectation.yml +0 -47
- data/lib/spec_forge/normalizers/global_context.yml +0 -28
- data/lib/spec_forge/normalizers/spec.yml +0 -50
- data/lib/spec_forge/runner/adapter.rb +0 -181
- data/lib/spec_forge/runner/callbacks.rb +0 -246
- data/lib/spec_forge/runner/debug_proxy.rb +0 -215
- data/lib/spec_forge/runner/listener.rb +0 -54
- data/lib/spec_forge/runner/metadata.rb +0 -58
- data/lib/spec_forge/runner/state.rb +0 -98
- data/lib/spec_forge/runner.rb +0 -75
- data/lib/spec_forge/spec/expectation/constraint.rb +0 -127
- data/lib/spec_forge/spec/expectation.rb +0 -68
- data/lib/spec_forge/spec.rb +0 -68
- 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.
|
|
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
|
|
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
|
|
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
|
|
120
|
-
value.map(&
|
|
121
|
-
when
|
|
122
|
-
value.transform_values(&
|
|
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
|
-
#
|
|
137
|
-
|
|
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
|
-
#
|
|
145
|
-
# FactoryBot
|
|
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
|
-
|
|
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
|
-
|
|
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]) && !
|
|
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
|
|
40
|
-
new(input, arguments)
|
|
41
|
-
when
|
|
42
|
-
|
|
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
|
|
67
|
-
# @
|
|
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(
|
|
70
|
-
super
|
|
70
|
+
def initialize(...)
|
|
71
|
+
super
|
|
71
72
|
|
|
72
|
-
@
|
|
73
|
-
end
|
|
73
|
+
@input = @input.to_s.downcase
|
|
74
74
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
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
|
|
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 <
|
|
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
|
|
32
|
+
# @return [Array] The array itself
|
|
24
33
|
#
|
|
25
34
|
def value
|
|
26
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
|
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 <
|
|
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
|
|
33
|
+
# @return [Hash] The hash itself
|
|
24
34
|
#
|
|
25
35
|
def value
|
|
26
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|