spec_forge 0.7.0 → 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 +139 -9
- data/README.md +125 -203
- 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 +6 -0
- data/lib/spec_forge/cli.rb +6 -14
- data/lib/spec_forge/configuration.rb +212 -78
- 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 +22 -6
- data/lib/spec_forge/documentation/openapi/v3_0/response.rb +29 -7
- 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 -143
- 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 -74
- 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 -183
- data/lib/spec_forge/runner/callbacks.rb +0 -246
- data/lib/spec_forge/runner/debug_proxy.rb +0 -213
- 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
|
@@ -76,23 +76,65 @@ module SpecForge
|
|
|
76
76
|
end
|
|
77
77
|
|
|
78
78
|
#
|
|
79
|
-
# Validates that
|
|
79
|
+
# Validates that shape and schema are not both defined
|
|
80
80
|
#
|
|
81
|
-
# Ensures
|
|
82
|
-
#
|
|
81
|
+
# Ensures only one of shape or schema is used for JSON validation,
|
|
82
|
+
# as they represent different validation approaches that cannot be combined.
|
|
83
83
|
#
|
|
84
|
-
# @param value [
|
|
84
|
+
# @param value [Hash] The expectation hash containing shape and/or schema keys
|
|
85
85
|
#
|
|
86
|
-
# @raise [Error
|
|
86
|
+
# @raise [Error] If both shape and schema are defined
|
|
87
87
|
#
|
|
88
88
|
# @example Using the validator in a structure
|
|
89
|
-
#
|
|
89
|
+
# json: {type: Hash, validator: :json_expectation}
|
|
90
|
+
#
|
|
91
|
+
def json_expectation(value)
|
|
92
|
+
# Both shape and schema cannot be defined at the same time
|
|
93
|
+
return if value[:shape].blank? || value[:schema].blank?
|
|
94
|
+
|
|
95
|
+
raise Error, "Cannot define both \"shape\" and \"schema\". Use \"shape\" for simple validation or \"schema\" for explicit control."
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
#
|
|
99
|
+
# Validates a JSON schema structure recursively
|
|
100
|
+
#
|
|
101
|
+
# Ensures the schema definition follows the expected format,
|
|
102
|
+
# validating nested structures and patterns.
|
|
103
|
+
#
|
|
104
|
+
# @param value [Hash] The schema definition to validate
|
|
105
|
+
#
|
|
106
|
+
# @raise [Error::InvalidStructureError] If the schema is invalid
|
|
107
|
+
#
|
|
108
|
+
def json_schema(value)
|
|
109
|
+
Normalizer.validate!(value, using: :json_schema)
|
|
110
|
+
json_schema(value[:pattern]) if value[:pattern]
|
|
111
|
+
|
|
112
|
+
case value[:structure]
|
|
113
|
+
when Array
|
|
114
|
+
value[:structure].each { |v| json_schema(v) }
|
|
115
|
+
when Hash
|
|
116
|
+
value[:structure].each_value { |v| json_schema(v) }
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
#
|
|
121
|
+
# Validates a callback definition
|
|
122
|
+
#
|
|
123
|
+
# Ensures the callback has a valid structure with a required name
|
|
124
|
+
# and optional arguments. Handles both single callbacks and arrays
|
|
125
|
+
# of callbacks.
|
|
126
|
+
#
|
|
127
|
+
# @param value [Array<Hash>] The callback definition(s) to validate
|
|
128
|
+
#
|
|
129
|
+
# @raise [Error::InvalidStructureError] If the callback structure is invalid
|
|
130
|
+
#
|
|
131
|
+
# @example Using the validator in a structure
|
|
132
|
+
# call: {type: Hash, validator: :callback}
|
|
90
133
|
#
|
|
91
134
|
def callback(value)
|
|
92
135
|
return if value.blank?
|
|
93
|
-
return if SpecForge::Callbacks.registered?(value)
|
|
94
136
|
|
|
95
|
-
|
|
137
|
+
value.each { |v| Normalizer.validate!(v, using: :callback) }
|
|
96
138
|
end
|
|
97
139
|
end
|
|
98
140
|
end
|
|
@@ -1,93 +1,24 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require_relative "normalizer/default"
|
|
4
|
-
require_relative "normalizer/definition"
|
|
5
|
-
require_relative "normalizer/validators"
|
|
6
|
-
|
|
7
3
|
module SpecForge
|
|
8
4
|
#
|
|
9
|
-
#
|
|
10
|
-
# according to defined structures. It handles type checking, default values,
|
|
11
|
-
# references between structures, and custom validation logic.
|
|
12
|
-
#
|
|
13
|
-
# == Structure Definitions
|
|
14
|
-
#
|
|
15
|
-
# Structures define validation rules as YAML files in the lib/spec_forge/normalizers directory:
|
|
16
|
-
#
|
|
17
|
-
# # Example structure (users.yml)
|
|
18
|
-
# name:
|
|
19
|
-
# type: String
|
|
20
|
-
# required: true
|
|
21
|
-
#
|
|
22
|
-
# age:
|
|
23
|
-
# type: Integer
|
|
24
|
-
# default: 0
|
|
25
|
-
#
|
|
26
|
-
# settings:
|
|
27
|
-
# type: Hash
|
|
28
|
-
# structure:
|
|
29
|
-
# notifications:
|
|
30
|
-
# type: Boolean
|
|
31
|
-
# default: true
|
|
32
|
-
#
|
|
33
|
-
# == Core Attribute Behaviors
|
|
34
|
-
#
|
|
35
|
-
# 1. With 'default:' - Always included in output, using default if nil
|
|
36
|
-
# 2. With 'required: false' - Omitted from output if nil
|
|
37
|
-
# 3. Default behavior - Required, errors if missing/nil
|
|
38
|
-
#
|
|
39
|
-
# == Available Options
|
|
40
|
-
#
|
|
41
|
-
# * type: - Required. Class name or array of class names (string, integer, hash, etc.)
|
|
42
|
-
# * default: - Optional. Default value if attribute is nil
|
|
43
|
-
# * required: - Optional. Set to false to make attribute optional
|
|
44
|
-
# * aliases: - Optional. Alternative keys to check for value
|
|
45
|
-
# * structure: - Optional. Sub-structure for nested objects
|
|
46
|
-
# * validator: - Optional. Custom validation method (see Validators)
|
|
47
|
-
# * reference: - Optional. Reference another structure definition (e.g., reference: headers)
|
|
48
|
-
#
|
|
49
|
-
# == Structure References
|
|
50
|
-
#
|
|
51
|
-
# References allow reusing common structures:
|
|
52
|
-
#
|
|
53
|
-
# # In your YAML definition:
|
|
54
|
-
# user_id:
|
|
55
|
-
# reference: id # Will inherit all properties from the 'id' structure
|
|
56
|
-
# required: false # Can override specific properties
|
|
57
|
-
#
|
|
58
|
-
# # Nested structure references:
|
|
59
|
-
# settings:
|
|
60
|
-
# type: Hash
|
|
61
|
-
# structure:
|
|
62
|
-
# email_prefs:
|
|
63
|
-
# reference: email_preferences # References another complete structure
|
|
64
|
-
#
|
|
65
|
-
# == Common Usage Patterns
|
|
66
|
-
#
|
|
67
|
-
# Basic Normalization:
|
|
68
|
-
# result = SpecForge::Normalizer.normalize!({name: "Test"}, using: :user)
|
|
69
|
-
#
|
|
70
|
-
# Using Custom Structure:
|
|
71
|
-
# structure = {count: {type: Integer, default: 0}}
|
|
72
|
-
# result = SpecForge::Normalizer.normalize!({}, using: structure, label: "counter")
|
|
73
|
-
#
|
|
74
|
-
# Getting Default Values:
|
|
75
|
-
# defaults = SpecForge::Normalizer.default(:user)
|
|
76
|
-
#
|
|
77
|
-
# == Error Handling
|
|
5
|
+
# Validates and transforms input data against structure definitions
|
|
78
6
|
#
|
|
79
|
-
#
|
|
80
|
-
#
|
|
81
|
-
#
|
|
82
|
-
#
|
|
83
|
-
# == Creating Custom Structures
|
|
84
|
-
#
|
|
85
|
-
# Add YAML files to lib/spec_forge/normalizers/ directory:
|
|
86
|
-
# - Use '_shared.yml' for common structures that can be referenced
|
|
87
|
-
# - Create custom validators in Normalizer::Validators class
|
|
88
|
-
# - Specify labels for error messages with default_label method
|
|
7
|
+
# The Normalizer system ensures that YAML input conforms to expected
|
|
8
|
+
# structures, applying defaults, type checking, and custom validations.
|
|
9
|
+
# Structure definitions are loaded from YAML files in the normalizers/ directory.
|
|
89
10
|
#
|
|
90
11
|
class Normalizer
|
|
12
|
+
#
|
|
13
|
+
# Mapping of structure names to their human-readable labels
|
|
14
|
+
#
|
|
15
|
+
# @return [Hash<Symbol, String>]
|
|
16
|
+
#
|
|
17
|
+
LABELS = {
|
|
18
|
+
factory_reference: "factory reference",
|
|
19
|
+
global_context: "global context"
|
|
20
|
+
}.freeze
|
|
21
|
+
|
|
91
22
|
class << self
|
|
92
23
|
#
|
|
93
24
|
# Collection of structure definitions used for validation
|
|
@@ -128,6 +59,8 @@ module SpecForge
|
|
|
128
59
|
raise_errors! { normalize(input, using:, label:) }
|
|
129
60
|
end
|
|
130
61
|
|
|
62
|
+
alias_method :validate!, :normalize!
|
|
63
|
+
|
|
131
64
|
#
|
|
132
65
|
# Normalizes input data against a structure without raising errors
|
|
133
66
|
#
|
|
@@ -154,11 +87,10 @@ module SpecForge
|
|
|
154
87
|
raise ArgumentError, "A label must be provided when using a custom structure"
|
|
155
88
|
end
|
|
156
89
|
else
|
|
157
|
-
|
|
90
|
+
structure = @structures[using.to_sym]
|
|
158
91
|
|
|
159
92
|
# We have a predefined structure and structures all have labels
|
|
160
|
-
label ||=
|
|
161
|
-
structure = data[:structure]
|
|
93
|
+
label ||= structure.label
|
|
162
94
|
end
|
|
163
95
|
|
|
164
96
|
# Ensure we have a structure
|
|
@@ -171,7 +103,7 @@ module SpecForge
|
|
|
171
103
|
|
|
172
104
|
# This is checked down here because it felt like it belonged...
|
|
173
105
|
# and because of that pesky label
|
|
174
|
-
raise Error::InvalidTypeError.new(input, Hash, for: label)
|
|
106
|
+
raise Error::InvalidTypeError.new(input, Hash, for: label) unless input.is_a?(Hash)
|
|
175
107
|
|
|
176
108
|
new(label, input, structure:).normalize
|
|
177
109
|
end
|
|
@@ -198,10 +130,17 @@ module SpecForge
|
|
|
198
130
|
# # => {name: "Unnamed"}
|
|
199
131
|
#
|
|
200
132
|
def default(name = nil, structure: nil, include_optional: false)
|
|
201
|
-
structure ||= @structures
|
|
133
|
+
structure ||= @structures[name.to_sym]
|
|
202
134
|
|
|
203
135
|
if !structure.is_a?(Hash)
|
|
204
|
-
|
|
136
|
+
message =
|
|
137
|
+
if name.present?
|
|
138
|
+
"No normalizer structure exists with name #{name.in_quotes}"
|
|
139
|
+
else
|
|
140
|
+
"The provided normalizer structure must be a Hash. Got #{structure.inspect}"
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
raise ArgumentError, message
|
|
205
144
|
end
|
|
206
145
|
|
|
207
146
|
default_from_structure(structure, include_optional:)
|
|
@@ -218,7 +157,21 @@ module SpecForge
|
|
|
218
157
|
# @api private
|
|
219
158
|
#
|
|
220
159
|
def load_from_files
|
|
221
|
-
|
|
160
|
+
base_path = Pathname.new(File.expand_path("normalizers", __dir__))
|
|
161
|
+
paths = Dir[base_path.join("**/*.yml")].sort
|
|
162
|
+
|
|
163
|
+
@structures =
|
|
164
|
+
paths.each_with_object({}) do |path, hash|
|
|
165
|
+
path = Pathname.new(path)
|
|
166
|
+
|
|
167
|
+
# Include the directory name in the path to include normalizers in directories
|
|
168
|
+
name = path.relative_path_from(base_path).to_s.delete_suffix(".yml").to_sym
|
|
169
|
+
|
|
170
|
+
input = YAML.safe_load_file(path, symbolize_names: true, aliases: true)
|
|
171
|
+
raise Error, "Normalizer defined at #{path.to_s.in_quotes} is empty" if input.blank?
|
|
172
|
+
|
|
173
|
+
hash[name] = Structure.new(input, label: LABELS[name] || name.to_s.humanize.downcase)
|
|
174
|
+
end
|
|
222
175
|
end
|
|
223
176
|
|
|
224
177
|
#
|
|
@@ -252,15 +205,6 @@ module SpecForge
|
|
|
252
205
|
include Default
|
|
253
206
|
end
|
|
254
207
|
|
|
255
|
-
# @return [String] A label that describes the data itself
|
|
256
|
-
attr_reader :label
|
|
257
|
-
|
|
258
|
-
# @return [Hash] The data to normalize
|
|
259
|
-
attr_reader :input
|
|
260
|
-
|
|
261
|
-
# @return [Hash] The structure to normalize the data to
|
|
262
|
-
attr_reader :structure
|
|
263
|
-
|
|
264
208
|
#
|
|
265
209
|
# Creates a normalizer for normalizing Hash data based on a structure
|
|
266
210
|
#
|
|
@@ -282,7 +226,7 @@ module SpecForge
|
|
|
282
226
|
# @return [Array<Hash, Set>] The normalized data and any errors
|
|
283
227
|
#
|
|
284
228
|
def normalize
|
|
285
|
-
case input
|
|
229
|
+
case @input
|
|
286
230
|
when Hash
|
|
287
231
|
normalize_hash
|
|
288
232
|
when Array
|
|
@@ -290,7 +234,7 @@ module SpecForge
|
|
|
290
234
|
end
|
|
291
235
|
end
|
|
292
236
|
|
|
293
|
-
|
|
237
|
+
private
|
|
294
238
|
|
|
295
239
|
#
|
|
296
240
|
# Extracts a value from a hash checking multiple keys
|
|
@@ -348,7 +292,8 @@ module SpecForge
|
|
|
348
292
|
error_label += " (aliases #{aliases})"
|
|
349
293
|
end
|
|
350
294
|
|
|
351
|
-
error_label
|
|
295
|
+
error_label += " in #{@label}" if @label.present?
|
|
296
|
+
error_label
|
|
352
297
|
end
|
|
353
298
|
|
|
354
299
|
#
|
|
@@ -372,7 +317,7 @@ module SpecForge
|
|
|
372
317
|
def normalize_hash
|
|
373
318
|
output, errors = {}, Set.new
|
|
374
319
|
|
|
375
|
-
structure.each do |key, definition|
|
|
320
|
+
@structure.each do |key, definition|
|
|
376
321
|
# Skip the wildcard key if it exists, handled below
|
|
377
322
|
next if key == :* || key == "*"
|
|
378
323
|
|
|
@@ -385,17 +330,17 @@ module SpecForge
|
|
|
385
330
|
end
|
|
386
331
|
|
|
387
332
|
# A wildcard will normalize the rest of the keys in the input
|
|
388
|
-
wildcard_structure = structure[:*] || structure["*"]
|
|
333
|
+
wildcard_structure = @structure[:*] || @structure["*"]
|
|
389
334
|
|
|
390
335
|
if wildcard_structure.present?
|
|
391
336
|
# We need to determine which keys we need to check
|
|
392
|
-
structure_keys = (structure.keys + structure.values.key_map(:aliases))
|
|
337
|
+
structure_keys = (@structure.keys + @structure.values.key_map(:aliases))
|
|
393
338
|
.compact
|
|
394
339
|
.flatten
|
|
395
340
|
.map(&:to_sym)
|
|
396
341
|
|
|
397
342
|
# Once we have which keys the structure used, we can get the remaining keys
|
|
398
|
-
keys_to_normalize = (input.keys - structure_keys)
|
|
343
|
+
keys_to_normalize = (@input.keys - structure_keys)
|
|
399
344
|
|
|
400
345
|
# They are checked against the wildcard's structure
|
|
401
346
|
keys_to_normalize.each do |key|
|
|
@@ -439,10 +384,10 @@ module SpecForge
|
|
|
439
384
|
type_class = definition[:type]
|
|
440
385
|
aliases = definition[:aliases] || []
|
|
441
386
|
default = definition[:default]
|
|
442
|
-
required = definition[:required]
|
|
387
|
+
required = definition[:required] == true
|
|
443
388
|
|
|
444
389
|
# Get the value
|
|
445
|
-
value = value_from_keys(input, [key.to_s] + aliases)
|
|
390
|
+
value = value_from_keys(@input, [key.to_s] + aliases)
|
|
446
391
|
|
|
447
392
|
# Drop the key if needed
|
|
448
393
|
return [false] if value.nil? && !has_default && !required
|
|
@@ -452,13 +397,20 @@ module SpecForge
|
|
|
452
397
|
|
|
453
398
|
error_label = generate_error_label(key, aliases)
|
|
454
399
|
|
|
455
|
-
# Type + existence check
|
|
456
400
|
if !valid_class?(value, type_class, nilable: has_default)
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
401
|
+
raise Error::InvalidTypeError.new(
|
|
402
|
+
value,
|
|
403
|
+
type_class,
|
|
404
|
+
for: error_label,
|
|
405
|
+
attribute_name: key.to_s,
|
|
406
|
+
description: definition[:description],
|
|
407
|
+
examples: definition[:examples]
|
|
408
|
+
)
|
|
409
|
+
end
|
|
460
410
|
|
|
461
|
-
|
|
411
|
+
# Call the transformer if it has one
|
|
412
|
+
if (name = definition[:transformer]) && name.present?
|
|
413
|
+
value = Transformers.call(name, value)
|
|
462
414
|
end
|
|
463
415
|
|
|
464
416
|
# Call the validator if it has one
|
|
@@ -495,7 +447,7 @@ module SpecForge
|
|
|
495
447
|
#
|
|
496
448
|
def normalize_substructure(new_label, value, substructure, errors)
|
|
497
449
|
if substructure.is_a?(Proc)
|
|
498
|
-
return substructure.call(value, errors:, label:)
|
|
450
|
+
return substructure.call(value, errors:, label: @label)
|
|
499
451
|
end
|
|
500
452
|
|
|
501
453
|
return value unless value.is_a?(Hash) || value.is_a?(Array)
|
|
@@ -526,20 +478,25 @@ module SpecForge
|
|
|
526
478
|
def normalize_array
|
|
527
479
|
output, errors = [], Set.new
|
|
528
480
|
|
|
529
|
-
input.each_with_index do |value, index|
|
|
530
|
-
type_class = structure[:type]
|
|
531
|
-
error_label = "index #{index} of #{label}"
|
|
481
|
+
@input.each_with_index do |value, index|
|
|
482
|
+
type_class = @structure[:type]
|
|
483
|
+
error_label = "index #{index} of #{@label}"
|
|
532
484
|
|
|
533
485
|
if !valid_class?(value, type_class)
|
|
534
486
|
raise Error::InvalidTypeError.new(value, type_class, for: error_label)
|
|
535
487
|
end
|
|
536
488
|
|
|
489
|
+
# Call the transformer if it has one
|
|
490
|
+
if (name = @structure[:transformer]) && name.present?
|
|
491
|
+
value = Transformers.call(name, value)
|
|
492
|
+
end
|
|
493
|
+
|
|
537
494
|
# Call the validator if it has one
|
|
538
|
-
if (name = structure[:validator]) && name.present?
|
|
495
|
+
if (name = @structure[:validator]) && name.present?
|
|
539
496
|
Validators.call(name, value, label: error_label)
|
|
540
497
|
end
|
|
541
498
|
|
|
542
|
-
if (substructure = structure[:structure])
|
|
499
|
+
if (substructure = @structure[:structure])
|
|
543
500
|
value = normalize_substructure(error_label, value, substructure, errors)
|
|
544
501
|
end
|
|
545
502
|
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# =============================================================================
|
|
2
|
+
# Callback Structure Definition
|
|
3
|
+
# =============================================================================
|
|
4
|
+
# Defines the structure for callback invocations in SpecForge blueprints.
|
|
5
|
+
# Callbacks execute registered Ruby code during test execution for tasks
|
|
6
|
+
# like database seeding, cleanup, or custom operations.
|
|
7
|
+
#
|
|
8
|
+
# Callbacks are registered in forge_helper.rb:
|
|
9
|
+
# SpecForge.configure do |config|
|
|
10
|
+
# config.register_callback(:seed_users) do |context, count:|
|
|
11
|
+
# count.times { User.create!(name: Faker::Name.name) }
|
|
12
|
+
# end
|
|
13
|
+
# end
|
|
14
|
+
#
|
|
15
|
+
# The context parameter provides access to variables, step, blueprint, etc.
|
|
16
|
+
# =============================================================================
|
|
17
|
+
|
|
18
|
+
name:
|
|
19
|
+
type: string
|
|
20
|
+
required: true
|
|
21
|
+
description: The name of the registered callback to execute.
|
|
22
|
+
examples:
|
|
23
|
+
- "seed_database"
|
|
24
|
+
- "cleanup"
|
|
25
|
+
- "create_users"
|
|
26
|
+
|
|
27
|
+
arguments:
|
|
28
|
+
type: [array, hash]
|
|
29
|
+
description: |-
|
|
30
|
+
Arguments to pass to the callback. Use a hash for keyword arguments
|
|
31
|
+
or an array for positional arguments.
|
|
32
|
+
examples:
|
|
33
|
+
- |-
|
|
34
|
+
arguments:
|
|
35
|
+
count: 10
|
|
36
|
+
role: "admin"
|
|
37
|
+
- |-
|
|
38
|
+
arguments: [1, 2, 3]
|
|
@@ -1,23 +1,73 @@
|
|
|
1
|
-
|
|
1
|
+
# =============================================================================
|
|
2
|
+
# Configuration Structure Definition
|
|
3
|
+
# =============================================================================
|
|
4
|
+
# Defines the structure for SpecForge global configuration.
|
|
5
|
+
# Configuration is set in forge_helper.rb via SpecForge.configure block.
|
|
6
|
+
#
|
|
7
|
+
# Example usage in forge_helper.rb:
|
|
8
|
+
# SpecForge.configure do |config|
|
|
9
|
+
# config.base_url = "https://api.example.com"
|
|
10
|
+
# config.global_variables = {
|
|
11
|
+
# api_version: "v1",
|
|
12
|
+
# admin_email: "admin@test.com"
|
|
13
|
+
# }
|
|
14
|
+
# end
|
|
15
|
+
# =============================================================================
|
|
2
16
|
|
|
3
|
-
|
|
4
|
-
|
|
17
|
+
base_url:
|
|
18
|
+
type: string
|
|
19
|
+
required: true
|
|
20
|
+
description: |-
|
|
21
|
+
Base URL prepended to all request paths. Can be overridden per-step
|
|
22
|
+
using request.base_url.
|
|
23
|
+
examples:
|
|
24
|
+
- "https://api.example.com"
|
|
25
|
+
- "http://localhost:3000"
|
|
5
26
|
|
|
6
|
-
|
|
7
|
-
|
|
27
|
+
global_variables:
|
|
28
|
+
type: hash
|
|
29
|
+
default: {}
|
|
30
|
+
description: |-
|
|
31
|
+
Variables available to all blueprints via {{ variable_name }} syntax.
|
|
32
|
+
Local variables (defined with store:) can shadow global variables
|
|
33
|
+
within a blueprint. Supports nested hashes for grouped configuration.
|
|
34
|
+
examples:
|
|
35
|
+
- |-
|
|
36
|
+
global_variables:
|
|
37
|
+
api_version: "v1"
|
|
38
|
+
admin_email: "admin@test.com"
|
|
39
|
+
- |-
|
|
40
|
+
global_variables:
|
|
41
|
+
admin_credentials:
|
|
42
|
+
email: "admin@test.com"
|
|
43
|
+
password: "admin123"
|
|
8
44
|
|
|
9
45
|
factories:
|
|
10
46
|
type: hash
|
|
11
47
|
default: {}
|
|
48
|
+
description: |-
|
|
49
|
+
Configuration for FactoryBot integration. Controls how factories
|
|
50
|
+
are discovered and loaded for use in blueprints via the
|
|
51
|
+
factories. namespace.
|
|
12
52
|
structure:
|
|
13
|
-
###########################################
|
|
14
53
|
auto_discover:
|
|
15
54
|
type: boolean
|
|
16
55
|
default: true
|
|
56
|
+
description: |-
|
|
57
|
+
When true, automatically discovers and loads factory definitions
|
|
58
|
+
from standard paths. Set to false to manually specify factory paths.
|
|
59
|
+
examples:
|
|
60
|
+
- true
|
|
61
|
+
- false
|
|
17
62
|
|
|
18
63
|
paths:
|
|
19
64
|
type: array
|
|
20
65
|
default: []
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
66
|
+
structure:
|
|
67
|
+
type: [string, pathname]
|
|
68
|
+
description: |-
|
|
69
|
+
Additional paths to search for factory definitions. These are
|
|
70
|
+
searched in addition to (not instead of) auto-discovered paths
|
|
71
|
+
when auto_discover is true.
|
|
72
|
+
examples:
|
|
73
|
+
- '["lib/factories", "spec/support/factories"]'
|
|
@@ -1,12 +1,63 @@
|
|
|
1
|
+
# =============================================================================
|
|
2
|
+
# Factory Structure Definition
|
|
3
|
+
# =============================================================================
|
|
4
|
+
# Defines the structure for YAML factory definitions in spec_forge/factories/.
|
|
5
|
+
# The filename determines the factory name (e.g., user.yml creates a user factory).
|
|
6
|
+
#
|
|
7
|
+
# Factories are referenced in blueprints using the factories. namespace:
|
|
8
|
+
# {{ factories.user.id }}
|
|
9
|
+
# {{ factories.post.title }}
|
|
10
|
+
# =============================================================================
|
|
11
|
+
|
|
1
12
|
model_class:
|
|
2
13
|
type: string
|
|
3
14
|
default: ""
|
|
4
15
|
aliases:
|
|
5
|
-
|
|
16
|
+
- class
|
|
17
|
+
description: |-
|
|
18
|
+
Override the inferred model class when the factory name differs from the model.
|
|
19
|
+
Usually inferred automatically from the factory name.
|
|
20
|
+
examples:
|
|
21
|
+
- "User"
|
|
22
|
+
- "Admin::Account"
|
|
6
23
|
|
|
7
24
|
variables:
|
|
8
|
-
|
|
25
|
+
type: hash
|
|
26
|
+
default: {}
|
|
27
|
+
description: |-
|
|
28
|
+
Computed values for use in attributes. Use when you need the same
|
|
29
|
+
generated value in multiple attributes.
|
|
30
|
+
examples:
|
|
31
|
+
- |-
|
|
32
|
+
variables:
|
|
33
|
+
hire_date: "{{ faker.date.backward(days: 365) }}"
|
|
34
|
+
|
|
35
|
+
traits:
|
|
36
|
+
type: hash
|
|
37
|
+
default: {}
|
|
38
|
+
description: |-
|
|
39
|
+
Named variations that modify or extend the base factory definition.
|
|
40
|
+
Each trait defines attribute overrides that are applied when the trait is used.
|
|
41
|
+
examples:
|
|
42
|
+
- |-
|
|
43
|
+
traits:
|
|
44
|
+
admin:
|
|
45
|
+
role: "admin"
|
|
46
|
+
inactive:
|
|
47
|
+
active: false
|
|
48
|
+
with_department:
|
|
49
|
+
department: "{{ faker.company.department }}"
|
|
9
50
|
|
|
10
51
|
attributes:
|
|
11
52
|
type: hash
|
|
12
53
|
default: {}
|
|
54
|
+
description: |-
|
|
55
|
+
Core record data for the factory. Supports variable interpolation
|
|
56
|
+
and faker data generation.
|
|
57
|
+
examples:
|
|
58
|
+
- |-
|
|
59
|
+
attributes:
|
|
60
|
+
name: "{{ faker.name.name }}"
|
|
61
|
+
email: "{{ faker.internet.email }}"
|
|
62
|
+
role: "user"
|
|
63
|
+
active: true
|
|
@@ -1,15 +1,76 @@
|
|
|
1
|
+
# =============================================================================
|
|
2
|
+
# Factory Reference Structure Definition
|
|
3
|
+
# =============================================================================
|
|
4
|
+
# Defines the structure for referencing factories within blueprints.
|
|
5
|
+
# Factory references allow inline factory usage with custom attributes
|
|
6
|
+
# and build strategies.
|
|
7
|
+
#
|
|
8
|
+
# Example usage in blueprints:
|
|
9
|
+
# store:
|
|
10
|
+
# user:
|
|
11
|
+
# factories.user:
|
|
12
|
+
# strategy: build
|
|
13
|
+
# attributes:
|
|
14
|
+
# role: "admin"
|
|
15
|
+
# =============================================================================
|
|
16
|
+
|
|
17
|
+
traits:
|
|
18
|
+
type: array
|
|
19
|
+
default: []
|
|
20
|
+
aliases:
|
|
21
|
+
- trait
|
|
22
|
+
description: |-
|
|
23
|
+
FactoryBot traits to apply to this factory invocation.
|
|
24
|
+
Traits are defined in the factory file and modify the built object.
|
|
25
|
+
Multiple traits can be combined and are applied in order.
|
|
26
|
+
examples:
|
|
27
|
+
- "traits: [admin]"
|
|
28
|
+
- "traits: [admin, inactive]"
|
|
29
|
+
|
|
1
30
|
attributes:
|
|
2
31
|
type: hash
|
|
3
32
|
default: {}
|
|
33
|
+
description: |-
|
|
34
|
+
Attribute overrides for this specific factory invocation.
|
|
35
|
+
Merged with the factory's default attributes. Supports variable
|
|
36
|
+
interpolation and faker data generation.
|
|
37
|
+
examples:
|
|
38
|
+
- |-
|
|
39
|
+
attributes:
|
|
40
|
+
name: "Custom Name"
|
|
41
|
+
email: "custom@test.com"
|
|
42
|
+
- |-
|
|
43
|
+
attributes:
|
|
44
|
+
department: "Engineering"
|
|
4
45
|
|
|
5
46
|
build_strategy:
|
|
6
47
|
type: string
|
|
7
48
|
default: create
|
|
8
49
|
aliases:
|
|
9
|
-
|
|
50
|
+
- strategy
|
|
51
|
+
description: |-
|
|
52
|
+
The FactoryBot build strategy to use. Determines whether records
|
|
53
|
+
are persisted to the database or just built in memory.
|
|
54
|
+
- create: Persists to database (default)
|
|
55
|
+
- build: Instantiates without saving
|
|
56
|
+
- build_stubbed / stubbed: Creates a stubbed instance
|
|
57
|
+
- attributes_for: Returns a hash of attributes
|
|
58
|
+
examples:
|
|
59
|
+
- "create"
|
|
60
|
+
- "build"
|
|
61
|
+
- "build_stubbed"
|
|
10
62
|
|
|
11
63
|
size:
|
|
12
64
|
type: integer
|
|
13
65
|
default: 0
|
|
14
66
|
aliases:
|
|
15
|
-
|
|
67
|
+
- count
|
|
68
|
+
transformer: abs
|
|
69
|
+
description: |-
|
|
70
|
+
Number of records to create. When 0 (default), creates a single
|
|
71
|
+
record. When > 0, creates that many records and returns an array.
|
|
72
|
+
Useful for testing list endpoints or batch operations.
|
|
73
|
+
examples:
|
|
74
|
+
- 0
|
|
75
|
+
- 5
|
|
76
|
+
- 100
|