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
|
@@ -5,16 +5,12 @@ module SpecForge
|
|
|
5
5
|
#
|
|
6
6
|
# Represents an attribute that transforms other attributes
|
|
7
7
|
#
|
|
8
|
-
# This class provides transformation functions
|
|
9
|
-
#
|
|
10
|
-
#
|
|
8
|
+
# This class provides transformation functions that can be applied to other
|
|
9
|
+
# attributes or values. It allows complex data manipulation without writing
|
|
10
|
+
# Ruby code.
|
|
11
11
|
#
|
|
12
|
-
#
|
|
13
|
-
#
|
|
14
|
-
# transform.join:
|
|
15
|
-
# - variables.first_name
|
|
16
|
-
# - " "
|
|
17
|
-
# - variables.last_name
|
|
12
|
+
# Note: String concatenation is handled via string interpolation ({{ }}) syntax
|
|
13
|
+
# rather than transformation functions.
|
|
18
14
|
#
|
|
19
15
|
class Transform < Parameterized
|
|
20
16
|
#
|
|
@@ -30,24 +26,27 @@ module SpecForge
|
|
|
30
26
|
#
|
|
31
27
|
# @return [Array<String>]
|
|
32
28
|
#
|
|
33
|
-
TRANSFORM_METHODS = %w[
|
|
34
|
-
join
|
|
35
|
-
].freeze
|
|
29
|
+
TRANSFORM_METHODS = %w[].freeze
|
|
36
30
|
|
|
31
|
+
# @return [String] The transformation function name
|
|
37
32
|
attr_reader :function
|
|
38
33
|
|
|
39
34
|
#
|
|
40
35
|
# Creates a new transform attribute with the specified function and arguments
|
|
41
36
|
#
|
|
37
|
+
# @raise [Error::InvalidTransformFunctionError] If the function is not supported
|
|
38
|
+
#
|
|
39
|
+
# @see Parameterized#initialize
|
|
40
|
+
#
|
|
42
41
|
def initialize(...)
|
|
43
42
|
super
|
|
44
43
|
|
|
45
44
|
# Remove prefix
|
|
46
45
|
@function = @input.sub("transform.", "")
|
|
47
46
|
|
|
48
|
-
raise Error::InvalidTransformFunctionError,
|
|
47
|
+
raise Error::InvalidTransformFunctionError.new(input, TRANSFORM_METHODS) unless TRANSFORM_METHODS.include?(function)
|
|
49
48
|
|
|
50
|
-
prepare_arguments
|
|
49
|
+
prepare_arguments
|
|
51
50
|
end
|
|
52
51
|
|
|
53
52
|
#
|
|
@@ -56,11 +55,7 @@ module SpecForge
|
|
|
56
55
|
# @return [Object] The transformed value
|
|
57
56
|
#
|
|
58
57
|
def value
|
|
59
|
-
|
|
60
|
-
when "join"
|
|
61
|
-
# Technically supports any attribute, but I ain't gonna test all them edge cases
|
|
62
|
-
arguments[:positional].resolved.join
|
|
63
|
-
end
|
|
58
|
+
# Noop
|
|
64
59
|
end
|
|
65
60
|
end
|
|
66
61
|
end
|
|
@@ -3,57 +3,57 @@
|
|
|
3
3
|
module SpecForge
|
|
4
4
|
class Attribute
|
|
5
5
|
#
|
|
6
|
-
# Represents
|
|
6
|
+
# Represents a variable reference in a template
|
|
7
7
|
#
|
|
8
|
-
#
|
|
9
|
-
#
|
|
8
|
+
# Variables are resolved at runtime by looking up values stored in the
|
|
9
|
+
# current execution context. Supports dot notation for accessing nested
|
|
10
|
+
# properties (e.g., "response.body.id").
|
|
10
11
|
#
|
|
11
|
-
# @example
|
|
12
|
-
# user_id:
|
|
13
|
-
# company_name: variables.company.name
|
|
12
|
+
# @example Simple variable
|
|
13
|
+
# Variable.new("user_id") # Looks up :user_id in context
|
|
14
14
|
#
|
|
15
|
-
# @example Nested access
|
|
16
|
-
#
|
|
15
|
+
# @example Nested access
|
|
16
|
+
# Variable.new("response.body.token") # response[:body][:token]
|
|
17
17
|
#
|
|
18
18
|
class Variable < Attribute
|
|
19
19
|
include Chainable
|
|
20
20
|
|
|
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
|
-
#
|
|
27
|
-
KEYWORD_REGEX = /^variables\./i
|
|
28
|
-
|
|
29
21
|
alias_method :variable_name, :header
|
|
30
22
|
|
|
31
23
|
#
|
|
32
|
-
#
|
|
33
|
-
#
|
|
34
|
-
# @param variables [Hash] A hash of variables to look up in
|
|
24
|
+
# Creates a new variable attribute
|
|
35
25
|
#
|
|
36
|
-
# @
|
|
37
|
-
# @raise [Error::InvalidTypeError] If variables is not a hash
|
|
26
|
+
# @param input [String] The variable path (e.g., "user_id", "response.body.id")
|
|
38
27
|
#
|
|
39
|
-
def
|
|
40
|
-
|
|
41
|
-
raise Error::InvalidTypeError.new(variables, Hash, for: "'variables'")
|
|
42
|
-
end
|
|
28
|
+
def initialize(...)
|
|
29
|
+
super
|
|
43
30
|
|
|
44
|
-
#
|
|
45
|
-
|
|
31
|
+
# Unset the keyword to keep chainable from displaying it in error messages
|
|
32
|
+
@keyword = nil
|
|
46
33
|
|
|
47
|
-
|
|
34
|
+
sections = @input.split(".")
|
|
35
|
+
@header = sections.first&.to_sym
|
|
36
|
+
@invocation_chain = sections[1..] || []
|
|
48
37
|
end
|
|
49
38
|
|
|
50
39
|
#
|
|
51
|
-
# Returns the base object for
|
|
40
|
+
# Returns the base object for this variable from the current context
|
|
52
41
|
#
|
|
53
|
-
#
|
|
42
|
+
# Looks up the variable name in the current Forge context's variables.
|
|
43
|
+
# Raises an error if the variable is not defined.
|
|
44
|
+
#
|
|
45
|
+
# @return [Object] The value stored under this variable name
|
|
46
|
+
#
|
|
47
|
+
# @raise [Error::MissingVariableError] If the variable is not defined
|
|
54
48
|
#
|
|
55
49
|
def base_object
|
|
56
|
-
@
|
|
50
|
+
variables = @options[:context] || Forge.context&.variables || {}
|
|
51
|
+
|
|
52
|
+
if !variables.key?(variable_name)
|
|
53
|
+
raise Error::MissingVariableError.new(variable_name, available_variables: variables.keys)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
variables[variable_name]
|
|
57
57
|
end
|
|
58
58
|
end
|
|
59
59
|
end
|
data/lib/spec_forge/attribute.rb
CHANGED
|
@@ -1,10 +1,5 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
# Need to be first
|
|
4
|
-
require_relative "attribute/parameterized"
|
|
5
|
-
require_relative "attribute/chainable"
|
|
6
|
-
require_relative "attribute/resolvable"
|
|
7
|
-
|
|
8
3
|
module SpecForge
|
|
9
4
|
#
|
|
10
5
|
# Base class for all attribute types in SpecForge.
|
|
@@ -21,50 +16,37 @@ module SpecForge
|
|
|
21
16
|
# user_id: variables.user.id # A variable reference
|
|
22
17
|
#
|
|
23
18
|
class Attribute
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
#
|
|
27
|
-
# Binds variables to Attribute objects
|
|
28
|
-
#
|
|
29
|
-
# @param input [Array, Hash, Attribute] The input to loop through or bind to
|
|
30
|
-
# @param variables [Hash] Any variables to available to assign
|
|
31
|
-
#
|
|
32
|
-
# @return [Array, Hash, Attribute] The input with bounded variables
|
|
33
|
-
#
|
|
34
|
-
def self.bind_variables(input, variables = {})
|
|
35
|
-
case input
|
|
36
|
-
when ArrayLike
|
|
37
|
-
input.each { |v| v.bind_variables(variables) }
|
|
38
|
-
when HashLike
|
|
39
|
-
input.each_value { |v| v.bind_variables(variables) }
|
|
40
|
-
when Attribute
|
|
41
|
-
input.bind_variables(variables)
|
|
42
|
-
end
|
|
43
|
-
|
|
44
|
-
input
|
|
19
|
+
class << self
|
|
20
|
+
include Resolvable
|
|
45
21
|
end
|
|
46
22
|
|
|
23
|
+
include Resolvable
|
|
24
|
+
|
|
47
25
|
#
|
|
48
26
|
# Creates an Attribute instance based on the input value's type and content.
|
|
49
27
|
# Recursively converts Array and Hash
|
|
50
28
|
#
|
|
51
29
|
# @param value [Object] The input value to convert into an Attribute
|
|
30
|
+
# @param options [Hash] Additional options passed to attribute constructors
|
|
31
|
+
# @option options [Hash] :context Custom variable context for resolution
|
|
52
32
|
#
|
|
53
33
|
# @return [Attribute] A new Attribute instance of the appropriate subclass
|
|
54
34
|
#
|
|
55
|
-
def self.from(value)
|
|
35
|
+
def self.from(value, **options)
|
|
56
36
|
case value
|
|
57
37
|
when String
|
|
58
|
-
from_string(value)
|
|
59
|
-
when
|
|
60
|
-
from_hash(value)
|
|
61
|
-
when Attribute
|
|
38
|
+
from_string(value, **options)
|
|
39
|
+
when Hash
|
|
40
|
+
from_hash(value, **options)
|
|
41
|
+
when Attribute, ResolvableArray, ResolvableHash, ResolvableStruct
|
|
62
42
|
value
|
|
63
|
-
when
|
|
64
|
-
array = value.map { |v| Attribute.from(v) }
|
|
65
|
-
|
|
43
|
+
when Array
|
|
44
|
+
array = value.map { |v| Attribute.from(v, **options) }
|
|
45
|
+
ResolvableArray.new(array)
|
|
46
|
+
when Struct, Data, OpenStruct
|
|
47
|
+
ResolvableStruct.new(value)
|
|
66
48
|
else
|
|
67
|
-
Literal.new(value)
|
|
49
|
+
Literal.new(value, **options)
|
|
68
50
|
end
|
|
69
51
|
end
|
|
70
52
|
|
|
@@ -72,59 +54,63 @@ module SpecForge
|
|
|
72
54
|
# Creates an Attribute instance from a string
|
|
73
55
|
#
|
|
74
56
|
# @param string [String] The input string
|
|
57
|
+
# @param options [Hash] Additional options passed to attribute constructors
|
|
58
|
+
# @option options [Hash] :context Custom variable context for resolution
|
|
75
59
|
#
|
|
76
60
|
# @return [Attribute]
|
|
77
61
|
#
|
|
78
62
|
# @private
|
|
79
63
|
#
|
|
80
|
-
def self.from_string(string)
|
|
64
|
+
def self.from_string(string, **options)
|
|
81
65
|
klass =
|
|
82
66
|
case string
|
|
67
|
+
when Template::REGEX
|
|
68
|
+
Template
|
|
69
|
+
when Environment::KEYWORD_REGEX
|
|
70
|
+
Environment
|
|
83
71
|
when Factory::KEYWORD_REGEX
|
|
84
72
|
Factory
|
|
85
73
|
when Faker::KEYWORD_REGEX
|
|
86
74
|
Faker
|
|
87
|
-
when Global::KEYWORD_REGEX
|
|
88
|
-
Global
|
|
89
75
|
when Matcher::KEYWORD_REGEX
|
|
90
76
|
Matcher
|
|
91
77
|
when Regex::KEYWORD_REGEX
|
|
92
78
|
Regex
|
|
93
|
-
when Store::KEYWORD_REGEX
|
|
94
|
-
Store
|
|
95
|
-
when Variable::KEYWORD_REGEX
|
|
96
|
-
Variable
|
|
97
79
|
else
|
|
98
80
|
Literal
|
|
99
81
|
end
|
|
100
82
|
|
|
101
|
-
klass.new(string)
|
|
83
|
+
klass.new(string, **options)
|
|
102
84
|
end
|
|
103
85
|
|
|
104
86
|
#
|
|
105
87
|
# Creates an Attribute instance from a hash
|
|
106
88
|
#
|
|
107
89
|
# @param hash [Hash] The input hash
|
|
90
|
+
# @param options [Hash] Additional options passed to attribute constructors
|
|
91
|
+
# @option options [Hash] :context Custom variable context for resolution
|
|
108
92
|
#
|
|
109
93
|
# @return [Attribute]
|
|
110
94
|
#
|
|
111
95
|
# @private
|
|
112
96
|
#
|
|
113
|
-
def self.from_hash(hash)
|
|
97
|
+
def self.from_hash(hash, **options)
|
|
114
98
|
# Determine if the hash is an expanded macro call
|
|
115
99
|
has_macro = ->(h, regex) { h.any? { |k, _| k.match?(regex) } }
|
|
116
100
|
|
|
117
101
|
if has_macro.call(hash, Transform::KEYWORD_REGEX)
|
|
118
|
-
Transform.from_hash(hash)
|
|
102
|
+
Transform.from_hash(hash, **options)
|
|
103
|
+
elsif has_macro.call(hash, Generate::KEYWORD_REGEX)
|
|
104
|
+
Generate.from_hash(hash, **options)
|
|
119
105
|
elsif has_macro.call(hash, Faker::KEYWORD_REGEX)
|
|
120
|
-
Faker.from_hash(hash)
|
|
106
|
+
Faker.from_hash(hash, **options)
|
|
121
107
|
elsif has_macro.call(hash, Matcher::KEYWORD_REGEX)
|
|
122
|
-
Matcher.from_hash(hash)
|
|
108
|
+
Matcher.from_hash(hash, **options)
|
|
123
109
|
elsif has_macro.call(hash, Factory::KEYWORD_REGEX)
|
|
124
|
-
Factory.from_hash(hash)
|
|
110
|
+
Factory.from_hash(hash, **options)
|
|
125
111
|
else
|
|
126
|
-
hash = hash.transform_values { |v| Attribute.from(v) }
|
|
127
|
-
|
|
112
|
+
hash = hash.transform_values { |v| Attribute.from(v, **options) }
|
|
113
|
+
ResolvableHash.new(hash)
|
|
128
114
|
end
|
|
129
115
|
end
|
|
130
116
|
|
|
@@ -139,9 +125,13 @@ module SpecForge
|
|
|
139
125
|
# Creates a new attribute
|
|
140
126
|
#
|
|
141
127
|
# @param input [Object] The original input value
|
|
128
|
+
# @param options [Hash] Additional options for attribute behavior
|
|
129
|
+
# @option options [Hash] :context Custom variable context for resolution,
|
|
130
|
+
# bypassing Forge.context lookup
|
|
142
131
|
#
|
|
143
|
-
def initialize(input)
|
|
132
|
+
def initialize(input, **options)
|
|
144
133
|
@input = input
|
|
134
|
+
@options = options
|
|
145
135
|
end
|
|
146
136
|
|
|
147
137
|
#
|
|
@@ -216,56 +206,41 @@ module SpecForge
|
|
|
216
206
|
#
|
|
217
207
|
def resolve
|
|
218
208
|
case value
|
|
219
|
-
when
|
|
220
|
-
value.map(&
|
|
221
|
-
when
|
|
222
|
-
value.transform_values(&
|
|
209
|
+
when Array
|
|
210
|
+
value.map(&resolve_proc)
|
|
211
|
+
when Hash
|
|
212
|
+
value.transform_values(&resolve_proc)
|
|
223
213
|
else
|
|
224
214
|
value
|
|
225
215
|
end
|
|
226
216
|
end
|
|
227
217
|
|
|
228
218
|
#
|
|
229
|
-
# Converts
|
|
230
|
-
# Handles different types of values by creating the right matcher type:
|
|
231
|
-
# - Arrays become contain_exactly matchers
|
|
232
|
-
# - Hashes become include matchers
|
|
233
|
-
# - Regexp become match matchers
|
|
234
|
-
# - Existing matchers are passed through
|
|
235
|
-
# - Other values become eq matchers
|
|
236
|
-
#
|
|
237
|
-
# This method is crucial for nested matcher structures and compound matchers
|
|
238
|
-
# like matcher.and that require all values to be proper matchers.
|
|
239
|
-
#
|
|
240
|
-
# @return [RSpec::Matchers::BuiltIn::BaseMatcher] A matcher representing this attribute
|
|
219
|
+
# Converts the resolved value into an RSpec matcher
|
|
241
220
|
#
|
|
242
|
-
#
|
|
243
|
-
#
|
|
244
|
-
#
|
|
221
|
+
# Transforms the attribute's resolved value into an appropriate RSpec matcher
|
|
222
|
+
# for use in expectations. Handles arrays, hashes, matchers, regexes, and
|
|
223
|
+
# literal values differently to produce the correct matcher type.
|
|
245
224
|
#
|
|
246
|
-
#
|
|
247
|
-
# array_attr.resolve_as_matcher # => contain_exactly(eq(1), eq(2), eq(3))
|
|
248
|
-
#
|
|
249
|
-
# hash_attr = Attribute::ResolvableHash.new({name: "Test"})
|
|
250
|
-
# hash_attr.resolve_as_matcher # => include("name" => eq("Test"))
|
|
225
|
+
# @return [RSpec::Matchers::BuiltIn::BaseMatcher] An RSpec matcher for the value
|
|
251
226
|
#
|
|
252
227
|
def resolve_as_matcher
|
|
253
228
|
methods = Attribute::Matcher::MATCHER_METHODS
|
|
254
229
|
|
|
255
230
|
case resolved
|
|
256
|
-
when Array
|
|
231
|
+
when Array
|
|
257
232
|
resolved_array = resolved.map(&resolve_as_matcher_proc)
|
|
258
233
|
|
|
259
234
|
if resolved_array.size > 0
|
|
260
|
-
|
|
235
|
+
resolved_array
|
|
261
236
|
else
|
|
262
237
|
methods.eq([])
|
|
263
238
|
end
|
|
264
|
-
when Hash
|
|
239
|
+
when Hash
|
|
265
240
|
resolved_hash = resolved.transform_values(&resolve_as_matcher_proc).stringify_keys
|
|
266
241
|
|
|
267
242
|
if resolved_hash.size > 0
|
|
268
|
-
|
|
243
|
+
resolved_hash
|
|
269
244
|
else
|
|
270
245
|
methods.eq({})
|
|
271
246
|
end
|
|
@@ -279,26 +254,5 @@ module SpecForge
|
|
|
279
254
|
methods.eq(resolved)
|
|
280
255
|
end
|
|
281
256
|
end
|
|
282
|
-
|
|
283
|
-
#
|
|
284
|
-
# Used to bind variables to self or any sub attributes
|
|
285
|
-
#
|
|
286
|
-
# @param variables [Hash] A hash of variable attributes
|
|
287
|
-
#
|
|
288
|
-
def bind_variables(variables)
|
|
289
|
-
end
|
|
290
257
|
end
|
|
291
258
|
end
|
|
292
|
-
|
|
293
|
-
# Order doesn't matter
|
|
294
|
-
require_relative "attribute/factory"
|
|
295
|
-
require_relative "attribute/faker"
|
|
296
|
-
require_relative "attribute/global"
|
|
297
|
-
require_relative "attribute/literal"
|
|
298
|
-
require_relative "attribute/matcher"
|
|
299
|
-
require_relative "attribute/regex"
|
|
300
|
-
require_relative "attribute/resolvable_array"
|
|
301
|
-
require_relative "attribute/resolvable_hash"
|
|
302
|
-
require_relative "attribute/store"
|
|
303
|
-
require_relative "attribute/transform"
|
|
304
|
-
require_relative "attribute/variable"
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SpecForge
|
|
4
|
+
#
|
|
5
|
+
# Represents a loaded blueprint containing a sequence of test steps
|
|
6
|
+
#
|
|
7
|
+
# A Blueprint is the runtime representation of a YAML blueprint file.
|
|
8
|
+
# It contains metadata about the file and an ordered list of Step objects
|
|
9
|
+
# ready for execution.
|
|
10
|
+
#
|
|
11
|
+
# @example
|
|
12
|
+
# blueprint = Blueprint.new(
|
|
13
|
+
# file_path: Pathname.new("spec_forge/blueprints/users.yml"),
|
|
14
|
+
# name: "users",
|
|
15
|
+
# steps: [{name: "Create user", request: {...}}]
|
|
16
|
+
# )
|
|
17
|
+
#
|
|
18
|
+
class Blueprint < Data.define(:file_path, :file_name, :hooks, :name, :steps)
|
|
19
|
+
def initialize(file_path:, name:, steps: [], hooks: {})
|
|
20
|
+
file_name = file_path.basename.to_s
|
|
21
|
+
steps = steps.map { |s| Step.new(**s) }
|
|
22
|
+
hooks = Step::Call.wrap_hooks(hooks)
|
|
23
|
+
|
|
24
|
+
super(file_path:, file_name:, hooks:, name:, steps:,)
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
@@ -12,19 +12,26 @@ module SpecForge
|
|
|
12
12
|
#
|
|
13
13
|
module Generate
|
|
14
14
|
#
|
|
15
|
-
# Generates OpenAPI documentation
|
|
15
|
+
# Generates OpenAPI documentation from blueprint test results
|
|
16
16
|
#
|
|
17
|
-
# Runs
|
|
18
|
-
#
|
|
17
|
+
# Runs blueprints with the configured verbosity level, extracts endpoint
|
|
18
|
+
# data, validates the specification (unless skipped), and writes the
|
|
19
19
|
# output file in the specified format.
|
|
20
20
|
#
|
|
21
|
-
# @
|
|
21
|
+
# @param base_path [String, Pathname, nil] Optional base path for blueprints
|
|
22
22
|
#
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
23
|
+
# @return [Pathname] Path to the generated documentation file
|
|
24
|
+
#
|
|
25
|
+
def generate_documentation(base_path: nil)
|
|
26
|
+
document = Documentation::Builder.create_document!(
|
|
27
|
+
base_path:,
|
|
28
|
+
use_cache: !options.fresh,
|
|
29
|
+
verbosity_level: determine_verbosity_level
|
|
30
|
+
)
|
|
31
|
+
generator_class = Documentation::OpenAPI["3.0"]
|
|
32
|
+
output = generator_class.new(document).generate
|
|
26
33
|
|
|
27
|
-
|
|
34
|
+
generator_class.validate!(output) unless options.skip_validation
|
|
28
35
|
|
|
29
36
|
# Determine output format and path
|
|
30
37
|
file_format = determine_file_format
|
|
@@ -66,6 +73,19 @@ module SpecForge
|
|
|
66
73
|
SpecForge.openapi_path.join("generated", "openapi.#{extension}")
|
|
67
74
|
end
|
|
68
75
|
end
|
|
76
|
+
|
|
77
|
+
#
|
|
78
|
+
# Determines verbosity level from command options
|
|
79
|
+
#
|
|
80
|
+
# @return [Integer] Verbosity level (0-3)
|
|
81
|
+
#
|
|
82
|
+
def determine_verbosity_level
|
|
83
|
+
return 3 if options.trace
|
|
84
|
+
return 2 if options.debug
|
|
85
|
+
return 1 if options.verbose
|
|
86
|
+
|
|
87
|
+
0
|
|
88
|
+
end
|
|
69
89
|
end
|
|
70
90
|
end
|
|
71
91
|
end
|
data/lib/spec_forge/cli/docs.rb
CHANGED
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require_relative "docs/generate"
|
|
4
|
-
|
|
5
3
|
module SpecForge
|
|
6
4
|
class CLI
|
|
7
5
|
#
|
|
@@ -60,6 +58,9 @@ module SpecForge
|
|
|
60
58
|
option "--format=FORMAT", "Output format: yml/yaml or json (default: yml)"
|
|
61
59
|
option "--output=PATH", "Full file path for generated documentation"
|
|
62
60
|
option "--skip-validation", "Skip OpenAPI specification validation during generation"
|
|
61
|
+
option "--verbose", "Show detailed step execution (verbosity level 1)"
|
|
62
|
+
option "--debug", "Show full request/response for failures (verbosity level 2)"
|
|
63
|
+
option "--trace", "Show everything for all steps (verbosity level 3)"
|
|
63
64
|
|
|
64
65
|
#
|
|
65
66
|
# Generates OpenAPI documentation from tests
|
|
@@ -86,6 +87,8 @@ module SpecForge
|
|
|
86
87
|
Your OpenAPI specification is valid and ready to use.
|
|
87
88
|
Output written to: #{file_path.relative_path_from(SpecForge.forge_path)}
|
|
88
89
|
STRING
|
|
90
|
+
rescue NoBlueprintsError => e
|
|
91
|
+
puts e.message
|
|
89
92
|
end
|
|
90
93
|
end
|
|
91
94
|
end
|
data/lib/spec_forge/cli/init.rb
CHANGED
|
@@ -17,7 +17,7 @@ module SpecForge
|
|
|
17
17
|
Creates the SpecForge project structure.
|
|
18
18
|
|
|
19
19
|
Sets up:
|
|
20
|
-
• spec_forge/
|
|
20
|
+
• spec_forge/blueprints/ for test files
|
|
21
21
|
• spec_forge/factories/ for test data (optional)
|
|
22
22
|
• spec_forge/openapi/ for documentation config (optional)
|
|
23
23
|
• forge_helper.rb for configuration
|
|
@@ -27,8 +27,8 @@ module SpecForge
|
|
|
27
27
|
option "--skip-factories", "Skip generating the \"factories\" directory"
|
|
28
28
|
|
|
29
29
|
#
|
|
30
|
-
# Creates the "spec_forge", "spec_forge/factories", and "spec_forge/
|
|
31
|
-
# Also creates the "
|
|
30
|
+
# Creates the "spec_forge", "spec_forge/factories", and "spec_forge/blueprints" directories
|
|
31
|
+
# Also creates the "forge_helper.rb" initialization file
|
|
32
32
|
#
|
|
33
33
|
def call
|
|
34
34
|
initialize_forge
|
|
@@ -39,7 +39,7 @@ module SpecForge
|
|
|
39
39
|
|
|
40
40
|
def initialize_forge
|
|
41
41
|
base_path = SpecForge.forge_path
|
|
42
|
-
actions.empty_directory(base_path.join("
|
|
42
|
+
actions.empty_directory(base_path.join("blueprints"))
|
|
43
43
|
actions.empty_directory(base_path.join("factories")) unless options.skip_factories
|
|
44
44
|
actions.template("forge_helper.rb.tt", base_path.join("forge_helper.rb"))
|
|
45
45
|
end
|