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
data/lib/spec_forge/factory.rb
CHANGED
|
@@ -25,6 +25,8 @@ module SpecForge
|
|
|
25
25
|
# Loads factory definitions from YAML files
|
|
26
26
|
# Creates Factory instances but doesn't register them with FactoryBot
|
|
27
27
|
#
|
|
28
|
+
# Factory names are derived from the filename (e.g., user.yml -> :user)
|
|
29
|
+
#
|
|
28
30
|
# @return [Array<Factory>] Array of loaded factory instances
|
|
29
31
|
#
|
|
30
32
|
def self.load_from_files
|
|
@@ -35,11 +37,11 @@ module SpecForge
|
|
|
35
37
|
Dir[path].each do |file_path|
|
|
36
38
|
hash = YAML.load_file(file_path, symbolize_names: true)
|
|
37
39
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
+
# Extract factory name from filename (e.g., "user.yml" -> :user)
|
|
41
|
+
factory_name = File.basename(file_path, ".yml").to_sym
|
|
42
|
+
hash[:name] = factory_name
|
|
40
43
|
|
|
41
|
-
|
|
42
|
-
end
|
|
44
|
+
factories << new(**hash)
|
|
43
45
|
end
|
|
44
46
|
|
|
45
47
|
factories
|
|
@@ -62,6 +64,9 @@ module SpecForge
|
|
|
62
64
|
# @return [Hash<Symbol, Attribute>] The attributes that define this factory
|
|
63
65
|
attr_reader :attributes
|
|
64
66
|
|
|
67
|
+
# @return [Hash<Symbol, Hash<Symbol, Attribute>>] Traits defined for this factory
|
|
68
|
+
attr_reader :traits
|
|
69
|
+
|
|
65
70
|
#
|
|
66
71
|
# Creates a new Factory instance
|
|
67
72
|
#
|
|
@@ -77,8 +82,9 @@ module SpecForge
|
|
|
77
82
|
@input = input
|
|
78
83
|
@model_class = input[:model_class]
|
|
79
84
|
|
|
80
|
-
@variables =
|
|
81
|
-
@attributes =
|
|
85
|
+
@variables = Attribute.from(input[:variables])
|
|
86
|
+
@attributes = Attribute.from(input[:attributes], context: @variables)
|
|
87
|
+
@traits = extract_traits(input[:traits], context: @variables)
|
|
82
88
|
end
|
|
83
89
|
|
|
84
90
|
#
|
|
@@ -91,13 +97,23 @@ module SpecForge
|
|
|
91
97
|
dsl = FactoryBot::Syntax::Default::DSL.new
|
|
92
98
|
|
|
93
99
|
options = {}
|
|
94
|
-
options[:class] = model_class if model_class
|
|
100
|
+
options[:class] = model_class if model_class.present?
|
|
95
101
|
|
|
96
102
|
# This creates the factory in FactoryBot
|
|
97
103
|
factory_forge = self
|
|
98
104
|
dsl.factory(name, options) do
|
|
99
|
-
|
|
100
|
-
|
|
105
|
+
# Register base attributes
|
|
106
|
+
factory_forge.attributes.each do |attr_name, attribute|
|
|
107
|
+
add_attribute(attr_name) { attribute.resolve }
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
# Register traits
|
|
111
|
+
factory_forge.traits.each do |trait_name, trait_attributes|
|
|
112
|
+
trait(trait_name) do
|
|
113
|
+
trait_attributes.each do |attr_name, attribute|
|
|
114
|
+
add_attribute(attr_name) { attribute.resolve }
|
|
115
|
+
end
|
|
116
|
+
end
|
|
101
117
|
end
|
|
102
118
|
end
|
|
103
119
|
|
|
@@ -106,16 +122,20 @@ module SpecForge
|
|
|
106
122
|
|
|
107
123
|
private
|
|
108
124
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
125
|
+
#
|
|
126
|
+
# Extracts and processes trait definitions from the input hash
|
|
127
|
+
# Trait definitions are flat hashes of attributes (no nested "attributes:" key)
|
|
128
|
+
#
|
|
129
|
+
# @param traits_hash [Hash] Raw trait definitions from YAML
|
|
130
|
+
# @param context [Hash] Variable context for attribute resolution
|
|
131
|
+
# @return [Hash<Symbol, Hash<Symbol, Attribute>>] Processed traits with Attribute values
|
|
132
|
+
#
|
|
133
|
+
def extract_traits(traits_hash, context: nil)
|
|
134
|
+
return {} if traits_hash.blank?
|
|
115
135
|
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
136
|
+
traits_hash.transform_values do |trait_attributes|
|
|
137
|
+
Attribute.from(trait_attributes || {}, context:)
|
|
138
|
+
end
|
|
119
139
|
end
|
|
120
140
|
end
|
|
121
141
|
end
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SpecForge
|
|
4
|
+
class Forge
|
|
5
|
+
#
|
|
6
|
+
# Base class for step actions
|
|
7
|
+
#
|
|
8
|
+
# Actions are the executable units within a step. Each action type
|
|
9
|
+
# (Call, Debug, Expect, Request, Store) inherits from this base class
|
|
10
|
+
# and implements the run method to perform its specific behavior.
|
|
11
|
+
#
|
|
12
|
+
class Action
|
|
13
|
+
# @return [Step] The step being executed
|
|
14
|
+
attr_reader :step
|
|
15
|
+
|
|
16
|
+
#
|
|
17
|
+
# Creates a new action for the given step
|
|
18
|
+
#
|
|
19
|
+
# @param step [Step] The step this action belongs to
|
|
20
|
+
#
|
|
21
|
+
# @return [Action] A new action instance
|
|
22
|
+
#
|
|
23
|
+
def initialize(step)
|
|
24
|
+
@step = step
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
#
|
|
28
|
+
# Executes the action
|
|
29
|
+
#
|
|
30
|
+
# @param _forge [Forge] The forge instance executing this action
|
|
31
|
+
#
|
|
32
|
+
# @return [void]
|
|
33
|
+
#
|
|
34
|
+
# @raise [RuntimeError] If not implemented by subclass
|
|
35
|
+
#
|
|
36
|
+
def run(_forge)
|
|
37
|
+
raise "not implemented"
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SpecForge
|
|
4
|
+
class Forge
|
|
5
|
+
#
|
|
6
|
+
# Action for the `call:` step attribute
|
|
7
|
+
#
|
|
8
|
+
# Executes registered callbacks defined in forge_helper.rb during step processing.
|
|
9
|
+
# Callbacks can receive the current context and optional arguments.
|
|
10
|
+
#
|
|
11
|
+
class Call < Action
|
|
12
|
+
#
|
|
13
|
+
# Executes all callbacks defined in the step's call: attribute
|
|
14
|
+
#
|
|
15
|
+
# @param forge [Forge] The forge instance for accessing callbacks and display
|
|
16
|
+
#
|
|
17
|
+
# @return [void]
|
|
18
|
+
#
|
|
19
|
+
def run(forge)
|
|
20
|
+
context = SpecForge::Forge.context
|
|
21
|
+
|
|
22
|
+
step.calls.each do |call|
|
|
23
|
+
callback_name = call.callback_name
|
|
24
|
+
arguments = call.arguments
|
|
25
|
+
|
|
26
|
+
forge.display.action("Call #{callback_name}", symbol: :checkmark, symbol_styles: :yellow)
|
|
27
|
+
|
|
28
|
+
forge.callbacks.run(callback_name, context, arguments)
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SpecForge
|
|
4
|
+
class Forge
|
|
5
|
+
#
|
|
6
|
+
# Action for the `debug:` step attribute
|
|
7
|
+
#
|
|
8
|
+
# Triggers a debug breakpoint when a step has debug: true, invoking the
|
|
9
|
+
# configured debug handler (e.g., binding.pry) to allow interactive debugging.
|
|
10
|
+
#
|
|
11
|
+
class Debug < Action
|
|
12
|
+
#
|
|
13
|
+
# Triggers the debug breakpoint
|
|
14
|
+
#
|
|
15
|
+
# @param forge [Forge] The forge instance
|
|
16
|
+
#
|
|
17
|
+
# @return [void]
|
|
18
|
+
#
|
|
19
|
+
# @raise [Error] If no debug handler is configured
|
|
20
|
+
#
|
|
21
|
+
def run(forge, blueprint)
|
|
22
|
+
forge.display.action("Debug breakpoint triggered", symbol: :flag, symbol_styles: :yellow)
|
|
23
|
+
|
|
24
|
+
callback = SpecForge.configuration.on_debug_proc
|
|
25
|
+
if callback.nil?
|
|
26
|
+
raise Error, <<~STRING
|
|
27
|
+
Debug breakpoint triggered but no debug handler is configured.
|
|
28
|
+
|
|
29
|
+
Add a debug handler in your forge_helper.rb:
|
|
30
|
+
|
|
31
|
+
SpecForge.configure do |config|
|
|
32
|
+
config.on_debug { binding.pry } # or byebug, debug, etc.
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
STRING
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
if callback.arity == 1
|
|
39
|
+
context = SpecForge::Forge.context.with(forge:, blueprint:, step:)
|
|
40
|
+
callback.call(context)
|
|
41
|
+
else
|
|
42
|
+
callback.call
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SpecForge
|
|
4
|
+
class Forge
|
|
5
|
+
#
|
|
6
|
+
# Action for the `expect:` step attribute
|
|
7
|
+
#
|
|
8
|
+
# Runs expectations against the current response, validating status codes,
|
|
9
|
+
# headers, and JSON body against expected values using RSpec matchers.
|
|
10
|
+
#
|
|
11
|
+
class Expect < Action
|
|
12
|
+
#
|
|
13
|
+
# Runs all expectations for the step against the current response
|
|
14
|
+
#
|
|
15
|
+
# @param forge [Forge] The forge instance containing response data
|
|
16
|
+
#
|
|
17
|
+
# @return [void]
|
|
18
|
+
#
|
|
19
|
+
# @raise [Error::ExpectationFailure] If any expectations fail
|
|
20
|
+
#
|
|
21
|
+
def run(forge)
|
|
22
|
+
show_expectation_count = step.expects.size > 1
|
|
23
|
+
|
|
24
|
+
failed_examples =
|
|
25
|
+
step.expects.flat_map.with_index do |expectation, index|
|
|
26
|
+
failed = forge.runner.run(forge, step, expectation)
|
|
27
|
+
|
|
28
|
+
forge.display.expectation_finished(
|
|
29
|
+
failed_examples: failed,
|
|
30
|
+
total_count: expectation.size,
|
|
31
|
+
index: index + 1,
|
|
32
|
+
show_index: show_expectation_count
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
failed
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
return if failed_examples.empty?
|
|
39
|
+
|
|
40
|
+
raise Error::ExpectationFailure.new(failed_examples)
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SpecForge
|
|
4
|
+
class Forge
|
|
5
|
+
#
|
|
6
|
+
# Action for the `request:` step attribute
|
|
7
|
+
#
|
|
8
|
+
# Builds and executes an HTTP request based on the step's configuration,
|
|
9
|
+
# then stores both the request and response in the forge's variables for
|
|
10
|
+
# use by subsequent actions.
|
|
11
|
+
#
|
|
12
|
+
class Request < Action
|
|
13
|
+
#
|
|
14
|
+
# Executes the HTTP request and stores the response
|
|
15
|
+
#
|
|
16
|
+
# @param forge [Forge] The forge instance
|
|
17
|
+
#
|
|
18
|
+
# @return [void]
|
|
19
|
+
#
|
|
20
|
+
def run(forge)
|
|
21
|
+
sendable_request, resolved_request = build_requests
|
|
22
|
+
|
|
23
|
+
forge.display.action(
|
|
24
|
+
"#{sendable_request.http_verb} #{sendable_request.url}",
|
|
25
|
+
symbol: :right_arrow, symbol_styles: :yellow
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
response = forge.http_client.perform(sendable_request)
|
|
29
|
+
response = parse_response(response)
|
|
30
|
+
|
|
31
|
+
# Only store the original resolved request before we modify it
|
|
32
|
+
forge.variables[:request] = resolved_request
|
|
33
|
+
forge.variables[:response] = response
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
private
|
|
37
|
+
|
|
38
|
+
def build_requests
|
|
39
|
+
resolved_request = step.request.to_h.transform_values { |v| v.respond_to?(:resolved) ? v.resolved : v }
|
|
40
|
+
resolved_request[:base_url] = SpecForge.configuration.base_url if resolved_request[:base_url].blank?
|
|
41
|
+
|
|
42
|
+
request = resolved_request.deep_dup
|
|
43
|
+
request[:body] =
|
|
44
|
+
if step.request.json?
|
|
45
|
+
request[:body].to_json
|
|
46
|
+
else
|
|
47
|
+
request[:body].to_s
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
[HTTP::Request.new(**request), resolved_request]
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def parse_response(response)
|
|
54
|
+
response.to_hash.tap do |response|
|
|
55
|
+
response[:headers] = response.delete(:response_headers)
|
|
56
|
+
|
|
57
|
+
case response[:headers]["content-type"]
|
|
58
|
+
when "application/json"
|
|
59
|
+
response[:body] = response[:body].to_h
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SpecForge
|
|
4
|
+
class Forge
|
|
5
|
+
#
|
|
6
|
+
# Action for the `store:` step attribute
|
|
7
|
+
#
|
|
8
|
+
# Resolves attribute values and stores them as variables for use by
|
|
9
|
+
# subsequent steps. Can store literal values or extract values from
|
|
10
|
+
# the response using template syntax.
|
|
11
|
+
#
|
|
12
|
+
class Store < Action
|
|
13
|
+
#
|
|
14
|
+
# Stores all configured values in the forge's variables
|
|
15
|
+
#
|
|
16
|
+
# @param forge [Forge] The forge instance
|
|
17
|
+
#
|
|
18
|
+
# @return [void]
|
|
19
|
+
#
|
|
20
|
+
def run(forge)
|
|
21
|
+
step.store.each do |name, value|
|
|
22
|
+
event = forge.variables.key?(name) ? "Update" : "Store"
|
|
23
|
+
|
|
24
|
+
forge.display.action("#{event} #{name.in_quotes}", symbol: :flag, symbol_styles: :bright_cyan)
|
|
25
|
+
|
|
26
|
+
forge.variables[name] = value.resolved
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SpecForge
|
|
4
|
+
class Forge
|
|
5
|
+
#
|
|
6
|
+
# Manages registered callbacks that can be invoked from blueprints
|
|
7
|
+
#
|
|
8
|
+
# Callbacks are Ruby blocks registered in forge_helper.rb that can be
|
|
9
|
+
# called during step execution using the call: attribute.
|
|
10
|
+
#
|
|
11
|
+
class Callbacks
|
|
12
|
+
#
|
|
13
|
+
# Creates a new empty callback registry
|
|
14
|
+
#
|
|
15
|
+
# @return [Callbacks] A new callbacks instance
|
|
16
|
+
#
|
|
17
|
+
def initialize
|
|
18
|
+
@callbacks = {}
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
#
|
|
22
|
+
# Registers a callback with the given name
|
|
23
|
+
#
|
|
24
|
+
# @param name [String, Symbol] The name to register the callback under
|
|
25
|
+
#
|
|
26
|
+
# @yield The block to execute when the callback is invoked
|
|
27
|
+
#
|
|
28
|
+
# @raise [ArgumentError] If no block is provided
|
|
29
|
+
#
|
|
30
|
+
def register(name, &block)
|
|
31
|
+
raise ArgumentError, "A block must be provided" unless block.is_a?(Proc)
|
|
32
|
+
|
|
33
|
+
if registered?(name)
|
|
34
|
+
warn("Callback #{name.in_quotes} is already registered. It will be overwritten")
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
@callbacks[name.to_sym] = block
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
#
|
|
41
|
+
# Checks if a callback with the given name has been registered
|
|
42
|
+
#
|
|
43
|
+
# @param name [String, Symbol] The callback name to check
|
|
44
|
+
#
|
|
45
|
+
# @return [Boolean] Whether the callback is registered
|
|
46
|
+
#
|
|
47
|
+
def registered?(name)
|
|
48
|
+
@callbacks.key?(name.to_sym)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
#
|
|
52
|
+
# Executes a registered callback by name
|
|
53
|
+
#
|
|
54
|
+
# @param name [String, Symbol] The callback name to execute
|
|
55
|
+
# @param context [Forge::Context, nil] The current execution context
|
|
56
|
+
# @param arguments [Array, Hash] Arguments to pass to the callback
|
|
57
|
+
#
|
|
58
|
+
# @return [Object] The return value of the callback
|
|
59
|
+
#
|
|
60
|
+
# @raise [Error::UndefinedCallbackError] If the callback is not registered
|
|
61
|
+
#
|
|
62
|
+
def run(name, context = nil, arguments = [])
|
|
63
|
+
raise Error::UndefinedCallbackError.new(name, @callbacks.keys) unless registered?(name)
|
|
64
|
+
|
|
65
|
+
callback = @callbacks[name.to_sym]
|
|
66
|
+
|
|
67
|
+
# No arguments? Just call
|
|
68
|
+
return callback.call if callback.arity == 0
|
|
69
|
+
return callback.call(context) if callback.arity == 1
|
|
70
|
+
|
|
71
|
+
case arguments
|
|
72
|
+
when Array
|
|
73
|
+
callback.call(context, *arguments)
|
|
74
|
+
when Hash
|
|
75
|
+
callback.call(context, **arguments)
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SpecForge
|
|
4
|
+
class Forge
|
|
5
|
+
#
|
|
6
|
+
# Immutable execution context passed to callbacks and hooks
|
|
7
|
+
#
|
|
8
|
+
# Contains the current state during forge execution including
|
|
9
|
+
# variables, the current blueprint and step, and any error that occurred.
|
|
10
|
+
#
|
|
11
|
+
class Context < Data.define(:variables, :forge, :blueprint, :step, :error)
|
|
12
|
+
def initialize(**context)
|
|
13
|
+
context[:variables] ||= nil
|
|
14
|
+
context[:forge] ||= nil
|
|
15
|
+
context[:blueprint] ||= nil
|
|
16
|
+
context[:step] ||= nil
|
|
17
|
+
context[:error] ||= nil
|
|
18
|
+
|
|
19
|
+
super(context)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
#
|
|
23
|
+
# Returns whether the context represents a successful state
|
|
24
|
+
#
|
|
25
|
+
# @return [Boolean] True if no error is present
|
|
26
|
+
#
|
|
27
|
+
def success?
|
|
28
|
+
error.nil?
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
#
|
|
32
|
+
# Returns whether the context represents a failed state
|
|
33
|
+
#
|
|
34
|
+
# @return [Boolean] True if an error is present
|
|
35
|
+
#
|
|
36
|
+
def failure?
|
|
37
|
+
!success?
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|