spec_forge 0.5.0 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.standard.yml +3 -3
- data/CHANGELOG.md +217 -2
- data/README.md +162 -25
- data/flake.lock +3 -3
- data/flake.nix +11 -5
- data/lib/spec_forge/attribute/chainable.rb +208 -20
- data/lib/spec_forge/attribute/factory.rb +92 -15
- data/lib/spec_forge/attribute/faker.rb +62 -13
- data/lib/spec_forge/attribute/global.rb +96 -0
- data/lib/spec_forge/attribute/literal.rb +15 -2
- data/lib/spec_forge/attribute/matcher.rb +186 -11
- data/lib/spec_forge/attribute/parameterized.rb +45 -12
- data/lib/spec_forge/attribute/regex.rb +55 -5
- data/lib/spec_forge/attribute/resolvable.rb +48 -5
- data/lib/spec_forge/attribute/resolvable_array.rb +62 -4
- data/lib/spec_forge/attribute/resolvable_hash.rb +62 -4
- data/lib/spec_forge/attribute/store.rb +65 -0
- data/lib/spec_forge/attribute/transform.rb +33 -5
- data/lib/spec_forge/attribute/variable.rb +37 -6
- data/lib/spec_forge/attribute.rb +166 -66
- data/lib/spec_forge/backtrace_formatter.rb +26 -3
- data/lib/spec_forge/callbacks.rb +88 -0
- data/lib/spec_forge/cli/actions.rb +27 -0
- data/lib/spec_forge/cli/command.rb +78 -24
- data/lib/spec_forge/cli/docs/generate.rb +72 -0
- data/lib/spec_forge/cli/docs.rb +92 -0
- data/lib/spec_forge/cli/init.rb +51 -9
- data/lib/spec_forge/cli/new.rb +67 -6
- data/lib/spec_forge/cli/run.rb +32 -4
- data/lib/spec_forge/cli/serve.rb +155 -0
- data/lib/spec_forge/cli.rb +26 -7
- data/lib/spec_forge/configuration.rb +96 -24
- data/lib/spec_forge/context/callbacks.rb +91 -0
- data/lib/spec_forge/context/global.rb +72 -0
- data/lib/spec_forge/context/store.rb +131 -0
- data/lib/spec_forge/context/variables.rb +91 -0
- data/lib/spec_forge/context.rb +36 -0
- data/lib/spec_forge/core_ext/array.rb +27 -0
- data/lib/spec_forge/core_ext/rspec.rb +22 -4
- data/lib/spec_forge/documentation/builder.rb +383 -0
- data/lib/spec_forge/documentation/document/operation.rb +47 -0
- data/lib/spec_forge/documentation/document/parameter.rb +22 -0
- data/lib/spec_forge/documentation/document/request_body.rb +24 -0
- data/lib/spec_forge/documentation/document/response.rb +39 -0
- data/lib/spec_forge/documentation/document/response_body.rb +27 -0
- data/lib/spec_forge/documentation/document.rb +48 -0
- data/lib/spec_forge/documentation/generators/base.rb +81 -0
- data/lib/spec_forge/documentation/generators/openapi/base.rb +100 -0
- data/lib/spec_forge/documentation/generators/openapi/error_formatter.rb +149 -0
- data/lib/spec_forge/documentation/generators/openapi/v3_0.rb +65 -0
- data/lib/spec_forge/documentation/generators/openapi.rb +59 -0
- data/lib/spec_forge/documentation/generators.rb +17 -0
- data/lib/spec_forge/documentation/loader/cache.rb +138 -0
- data/lib/spec_forge/documentation/loader.rb +159 -0
- data/lib/spec_forge/documentation/openapi/base.rb +33 -0
- data/lib/spec_forge/documentation/openapi/v3_0/example.rb +44 -0
- data/lib/spec_forge/documentation/openapi/v3_0/media_type.rb +42 -0
- data/lib/spec_forge/documentation/openapi/v3_0/operation.rb +175 -0
- data/lib/spec_forge/documentation/openapi/v3_0/response.rb +65 -0
- data/lib/spec_forge/documentation/openapi/v3_0/schema.rb +80 -0
- data/lib/spec_forge/documentation/openapi/v3_0/tag.rb +71 -0
- data/lib/spec_forge/documentation/openapi.rb +23 -0
- data/lib/spec_forge/documentation.rb +27 -0
- data/lib/spec_forge/error.rb +284 -113
- data/lib/spec_forge/factory.rb +35 -16
- data/lib/spec_forge/filter.rb +86 -0
- data/lib/spec_forge/forge.rb +171 -0
- data/lib/spec_forge/http/backend.rb +101 -29
- data/lib/spec_forge/http/client.rb +23 -13
- data/lib/spec_forge/http/request.rb +85 -62
- data/lib/spec_forge/http/verb.rb +79 -0
- data/lib/spec_forge/http.rb +105 -0
- data/lib/spec_forge/loader.rb +244 -0
- data/lib/spec_forge/matchers.rb +130 -0
- data/lib/spec_forge/normalizer/default.rb +51 -0
- data/lib/spec_forge/normalizer/definition.rb +248 -0
- data/lib/spec_forge/normalizer/validators.rb +99 -0
- data/lib/spec_forge/normalizer.rb +486 -115
- data/lib/spec_forge/normalizers/_shared.yml +74 -0
- data/lib/spec_forge/normalizers/configuration.yml +23 -0
- data/lib/spec_forge/normalizers/constraint.yml +8 -0
- data/lib/spec_forge/normalizers/expectation.yml +47 -0
- data/lib/spec_forge/normalizers/factory.yml +12 -0
- data/lib/spec_forge/normalizers/factory_reference.yml +15 -0
- data/lib/spec_forge/normalizers/global_context.yml +28 -0
- data/lib/spec_forge/normalizers/spec.yml +50 -0
- data/lib/spec_forge/runner/adapter.rb +183 -0
- data/lib/spec_forge/runner/callbacks.rb +246 -0
- data/lib/spec_forge/runner/debug_proxy.rb +213 -0
- data/lib/spec_forge/runner/listener.rb +54 -0
- data/lib/spec_forge/runner/metadata.rb +58 -0
- data/lib/spec_forge/runner/state.rb +98 -0
- data/lib/spec_forge/runner.rb +50 -125
- data/lib/spec_forge/spec/expectation/constraint.rb +100 -21
- data/lib/spec_forge/spec/expectation.rb +47 -51
- data/lib/spec_forge/spec.rb +50 -108
- data/lib/spec_forge/type.rb +36 -4
- data/lib/spec_forge/version.rb +4 -1
- data/lib/spec_forge.rb +168 -76
- data/lib/templates/openapi.yml.tt +22 -0
- data/lib/templates/redoc.html.tt +28 -0
- data/lib/templates/swagger.html.tt +59 -0
- metadata +109 -16
- data/lib/spec_forge/normalizer/configuration.rb +0 -77
- data/lib/spec_forge/normalizer/constraint.rb +0 -47
- data/lib/spec_forge/normalizer/expectation.rb +0 -86
- data/lib/spec_forge/normalizer/factory.rb +0 -65
- data/lib/spec_forge/normalizer/factory_reference.rb +0 -71
- data/lib/spec_forge/normalizer/spec.rb +0 -74
- data/spec_forge/factories/user.yml +0 -4
- data/spec_forge/forge_helper.rb +0 -48
- data/spec_forge/specs/users.yml +0 -65
- /data/lib/templates/{forge_helper.tt → forge_helper.rb.tt} +0 -0
- /data/lib/templates/{new_factory.tt → new_factory.yml.tt} +0 -0
- /data/lib/templates/{new_spec.tt → new_spec.yml.tt} +0 -0
@@ -0,0 +1,80 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module SpecForge
|
4
|
+
module Documentation
|
5
|
+
module OpenAPI
|
6
|
+
module V3_0 # standard:disable Naming/ClassAndModuleCamelCase
|
7
|
+
#
|
8
|
+
# Represents an OpenAPI 3.0 Schema object
|
9
|
+
#
|
10
|
+
# Handles schema definitions for data types, converting internal type
|
11
|
+
# representations to OpenAPI-compliant schema objects.
|
12
|
+
#
|
13
|
+
# @see https://spec.openapis.org/oas/v3.0.4.html#schema-object
|
14
|
+
#
|
15
|
+
class Schema
|
16
|
+
#
|
17
|
+
# The schema type (string, integer, object, etc.)
|
18
|
+
#
|
19
|
+
# @return [String, nil] The OpenAPI schema type
|
20
|
+
#
|
21
|
+
attr_reader :type
|
22
|
+
|
23
|
+
#
|
24
|
+
# The schema format (date-time, int64, etc.)
|
25
|
+
#
|
26
|
+
# @return [String, nil] The OpenAPI schema format
|
27
|
+
#
|
28
|
+
attr_reader :format
|
29
|
+
|
30
|
+
#
|
31
|
+
# Creates a new OpenAPI schema object
|
32
|
+
#
|
33
|
+
# @param options [Hash] Schema configuration options
|
34
|
+
# @option options [String] :type The data type to convert to OpenAPI format
|
35
|
+
#
|
36
|
+
# @return [Schema] A new schema instance
|
37
|
+
#
|
38
|
+
def initialize(options = {})
|
39
|
+
@type, @format = transform_type(options[:type])
|
40
|
+
end
|
41
|
+
|
42
|
+
#
|
43
|
+
# Converts the schema to an OpenAPI-compliant hash
|
44
|
+
#
|
45
|
+
# @return [Hash] OpenAPI-formatted schema object
|
46
|
+
#
|
47
|
+
def to_h
|
48
|
+
{
|
49
|
+
type:,
|
50
|
+
format:
|
51
|
+
}.compact_blank!
|
52
|
+
end
|
53
|
+
|
54
|
+
private
|
55
|
+
|
56
|
+
def transform_type(format)
|
57
|
+
case format
|
58
|
+
when "datetime", "time"
|
59
|
+
["string", "date-time"]
|
60
|
+
when "int64", "i64"
|
61
|
+
["integer", "int64"]
|
62
|
+
when "int32", "i32"
|
63
|
+
["integer", "int32"]
|
64
|
+
when "double", "float"
|
65
|
+
["number", format]
|
66
|
+
when "object"
|
67
|
+
["object"]
|
68
|
+
when "array"
|
69
|
+
["array"]
|
70
|
+
when "boolean", "number", "integer", "string"
|
71
|
+
[format]
|
72
|
+
else
|
73
|
+
["string", format]
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
@@ -0,0 +1,71 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module SpecForge
|
4
|
+
module Documentation
|
5
|
+
module OpenAPI
|
6
|
+
module V3_0 # standard:disable Naming/ClassAndModuleCamelCase
|
7
|
+
#
|
8
|
+
# Represents an OpenAPI 3.0 Tag object
|
9
|
+
#
|
10
|
+
# Handles tag definitions for categorizing and organizing API operations
|
11
|
+
# with optional descriptions and external documentation links.
|
12
|
+
#
|
13
|
+
# @see https://spec.openapis.org/oas/v3.0.4.html#tag-object
|
14
|
+
#
|
15
|
+
class Tag < Data.define(:name, :description, :external_docs)
|
16
|
+
#
|
17
|
+
# Creates a tag object from name and data
|
18
|
+
#
|
19
|
+
# Handles both string descriptions and hash configurations with
|
20
|
+
# external documentation references.
|
21
|
+
#
|
22
|
+
# @param name [String, Symbol] The tag name
|
23
|
+
# @param data [String, Hash] Either a description string or full config hash
|
24
|
+
#
|
25
|
+
# @return [Tag] A new tag instance
|
26
|
+
#
|
27
|
+
def self.parse(name, data)
|
28
|
+
name = name.to_s
|
29
|
+
|
30
|
+
case data
|
31
|
+
when String
|
32
|
+
description = data
|
33
|
+
when Hash
|
34
|
+
description = data[:description]
|
35
|
+
external_docs = data[:external_docs]
|
36
|
+
end
|
37
|
+
|
38
|
+
new(name:, description:, external_docs:)
|
39
|
+
end
|
40
|
+
|
41
|
+
#
|
42
|
+
# Creates a new OpenAPI tag object
|
43
|
+
#
|
44
|
+
# @param name [String] The tag name
|
45
|
+
# @param description [String, nil] Optional tag description
|
46
|
+
# @param external_docs [Hash, nil] Optional external documentation reference
|
47
|
+
#
|
48
|
+
# @return [Tag] A new tag instance
|
49
|
+
#
|
50
|
+
def initialize(name:, description: nil, external_docs: nil)
|
51
|
+
super
|
52
|
+
end
|
53
|
+
|
54
|
+
#
|
55
|
+
# Converts the tag to an OpenAPI-compliant hash
|
56
|
+
#
|
57
|
+
# Transforms internal attribute names to match OpenAPI specification
|
58
|
+
# and removes any blank values for clean output.
|
59
|
+
#
|
60
|
+
# @return [Hash] OpenAPI-formatted tag object
|
61
|
+
#
|
62
|
+
def to_h
|
63
|
+
super
|
64
|
+
.rename_key_unordered!(:external_docs, :externalDocs)
|
65
|
+
.compact_blank!
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module SpecForge
|
4
|
+
module Documentation
|
5
|
+
#
|
6
|
+
# OpenAPI documentation generation functionality
|
7
|
+
#
|
8
|
+
# Contains classes and modules for generating OpenAPI specifications
|
9
|
+
# from SpecForge test data. Supports multiple OpenAPI versions.
|
10
|
+
#
|
11
|
+
module OpenAPI
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
require_relative "openapi/base"
|
17
|
+
|
18
|
+
require_relative "openapi/v3_0/example"
|
19
|
+
require_relative "openapi/v3_0/media_type"
|
20
|
+
require_relative "openapi/v3_0/operation"
|
21
|
+
require_relative "openapi/v3_0/response"
|
22
|
+
require_relative "openapi/v3_0/schema"
|
23
|
+
require_relative "openapi/v3_0/tag"
|
@@ -0,0 +1,27 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module SpecForge
|
4
|
+
#
|
5
|
+
# API documentation generation functionality
|
6
|
+
#
|
7
|
+
# This module provides tools for extracting API documentation from SpecForge
|
8
|
+
# test files and generating various output formats like OpenAPI specifications.
|
9
|
+
# It handles the complete pipeline from test execution to documentation rendering.
|
10
|
+
#
|
11
|
+
# @example Generating OpenAPI documentation
|
12
|
+
# # From CLI
|
13
|
+
# spec_forge docs generate
|
14
|
+
#
|
15
|
+
# # Programmatically
|
16
|
+
# document = Documentation::Loader.load_document
|
17
|
+
# spec = Documentation::Generators::OpenAPI["3.0"].new(document).generate
|
18
|
+
#
|
19
|
+
module Documentation
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
require_relative "documentation/builder"
|
24
|
+
require_relative "documentation/document"
|
25
|
+
require_relative "documentation/loader"
|
26
|
+
require_relative "documentation/openapi"
|
27
|
+
require_relative "documentation/generators"
|
data/lib/spec_forge/error.rb
CHANGED
@@ -1,150 +1,321 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
module SpecForge
|
4
|
-
# Pass into to_sentence
|
5
|
-
OR_CONNECTOR = {
|
6
|
-
last_word_connector: ", or ",
|
7
|
-
two_words_connector: " or ",
|
8
|
-
# This is a minor performance improvement to avoid locales being loaded
|
9
|
-
# This will need to be removed if locales are added
|
10
|
-
locale: false
|
11
|
-
}.freeze
|
12
|
-
|
13
|
-
private_constant :OR_CONNECTOR
|
14
|
-
|
15
|
-
class Error < StandardError; end
|
16
|
-
|
17
4
|
#
|
18
|
-
#
|
5
|
+
# Base error class for all SpecForge-specific exceptions
|
19
6
|
#
|
20
|
-
class
|
21
|
-
|
22
|
-
|
23
|
-
|
7
|
+
class Error < StandardError
|
8
|
+
# Pass into to_sentence
|
9
|
+
OR_CONNECTOR = {
|
10
|
+
last_word_connector: ", or ",
|
11
|
+
two_words_connector: " or ",
|
12
|
+
# This is a minor performance improvement to avoid locales being loaded
|
13
|
+
# This will need to be removed if locales are added
|
14
|
+
locale: false
|
15
|
+
}.freeze
|
24
16
|
|
25
|
-
|
26
|
-
corrections = CLASS_CHECKER.correct(input)
|
17
|
+
private_constant :OR_CONNECTOR
|
27
18
|
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
19
|
+
#
|
20
|
+
# Raised when a provided Faker class name doesn't exist
|
21
|
+
# Provides helpful suggestions for similar class names
|
22
|
+
#
|
23
|
+
# @example
|
24
|
+
# Attribute::Faker.new("faker.invalid.method")
|
25
|
+
# # => InvalidFakerClassError: Undefined Faker class "invalid". Did you mean? name, games, ...
|
26
|
+
#
|
27
|
+
class InvalidFakerClassError < Error
|
28
|
+
#
|
29
|
+
# A spell checker for Faker classes
|
30
|
+
#
|
31
|
+
# @return [DidYouMean::SpellChecker]
|
32
|
+
#
|
33
|
+
CLASS_CHECKER = DidYouMean::SpellChecker.new(
|
34
|
+
dictionary: Faker::Base.descendants.map { |c| c.to_s.downcase.gsub!("::", ".") }
|
33
35
|
)
|
36
|
+
|
37
|
+
def initialize(input)
|
38
|
+
corrections = CLASS_CHECKER.correct(input)
|
39
|
+
|
40
|
+
super(<<~STRING.chomp
|
41
|
+
Undefined Faker class "#{input}". #{DidYouMean::Formatter.message_for(corrections)}
|
42
|
+
|
43
|
+
For available classes, please check https://github.com/faker-ruby/faker#generators.
|
44
|
+
STRING
|
45
|
+
)
|
46
|
+
end
|
34
47
|
end
|
35
|
-
end
|
36
48
|
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
49
|
+
#
|
50
|
+
# Raised when a provided method for a Faker class doesn't exist
|
51
|
+
# Provides helpful suggestions for similar method names
|
52
|
+
#
|
53
|
+
# @example
|
54
|
+
# Attribute::Faker.new("faker.name.invlaid")
|
55
|
+
# # => InvalidFakerMethodError: Undefined Faker method "invlaid" for "Faker::Name".
|
56
|
+
# Did you mean? first_name, last_name, ...
|
57
|
+
#
|
58
|
+
class InvalidFakerMethodError < Error
|
59
|
+
def initialize(input, klass)
|
60
|
+
spell_checker = DidYouMean::SpellChecker.new(dictionary: klass.public_methods)
|
61
|
+
corrections = spell_checker.correct(input)
|
44
62
|
|
45
|
-
|
46
|
-
|
63
|
+
super(<<~STRING.chomp
|
64
|
+
Undefined Faker method "#{input}" for "#{klass}". #{DidYouMean::Formatter.message_for(corrections)}
|
47
65
|
|
48
|
-
|
49
|
-
|
50
|
-
|
66
|
+
For available methods for this class, please check https://github.com/faker-ruby/faker#generators.
|
67
|
+
STRING
|
68
|
+
)
|
69
|
+
end
|
51
70
|
end
|
52
|
-
end
|
53
71
|
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
72
|
+
#
|
73
|
+
# Raised when an unknown transform function is referenced
|
74
|
+
# Indicates when a transform name isn't supported
|
75
|
+
#
|
76
|
+
class InvalidTransformFunctionError < Error
|
77
|
+
def initialize(input)
|
78
|
+
# TODO: Update link to docs
|
79
|
+
super(<<~STRING.chomp
|
80
|
+
Undefined transform function "#{input}".
|
81
|
+
|
82
|
+
For available functions, please check https://github.com/itsthedevman/spec_forge.
|
83
|
+
STRING
|
84
|
+
)
|
85
|
+
end
|
66
86
|
end
|
67
|
-
end
|
68
87
|
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
88
|
+
#
|
89
|
+
# Raised when a step in an invocation chain is invalid
|
90
|
+
# Provides detailed information about where in the chain the error occurred
|
91
|
+
#
|
92
|
+
# @example
|
93
|
+
# variable_attr = Attribute::Variable.new("variables.user.invalid_method")
|
94
|
+
# variable_attr.resolved
|
95
|
+
# # => InvalidInvocationError: Cannot invoke "invalid_method" on User
|
96
|
+
#
|
97
|
+
class InvalidInvocationError < Error
|
98
|
+
def initialize(step, object, resolution_path = {})
|
99
|
+
@step = step
|
100
|
+
@object = object
|
101
|
+
@resolution_path = resolution_path
|
102
|
+
|
103
|
+
object_class =
|
104
|
+
case object
|
105
|
+
when Data
|
106
|
+
object.class.name || "Data"
|
107
|
+
when Struct
|
108
|
+
object.class.name || "Struct"
|
109
|
+
else
|
110
|
+
object.class
|
111
|
+
end
|
112
|
+
|
113
|
+
super(<<~STRING.chomp
|
114
|
+
Cannot invoke "#{step}" on #{object_class}
|
115
|
+
#{resolution_path_message}
|
116
|
+
STRING
|
117
|
+
)
|
118
|
+
end
|
119
|
+
|
120
|
+
#
|
121
|
+
# Creates a new InvalidInvocationError with a new resolution path
|
122
|
+
#
|
123
|
+
# @param path [Hash] The steps taken up until this point
|
124
|
+
#
|
125
|
+
def with_resolution_path(path)
|
126
|
+
self.class.new(@step, @object, path)
|
127
|
+
end
|
128
|
+
|
129
|
+
private
|
130
|
+
|
131
|
+
def resolution_path_message
|
132
|
+
return "" if @resolution_path.empty?
|
133
|
+
|
134
|
+
message =
|
135
|
+
@resolution_path.map.with_index do |(path, description), index|
|
136
|
+
"#{index + 1}. #{path} --> #{description}"
|
137
|
+
end.join("\n")
|
138
|
+
|
139
|
+
"\nResolution path:\n#{message}"
|
140
|
+
end
|
141
|
+
end
|
142
|
+
|
143
|
+
#
|
144
|
+
# An extended version of TypeError with better error messages
|
145
|
+
# Makes it easier to understand type mismatches in the codebase
|
146
|
+
#
|
147
|
+
# @example
|
148
|
+
# raise Error::InvalidTypeError.new(123, String, for: "name parameter")
|
149
|
+
# # => Expected String, got Integer for name parameter
|
150
|
+
#
|
151
|
+
class InvalidTypeError < Error
|
152
|
+
def initialize(object, expected_type, **opts)
|
153
|
+
if expected_type.instance_of?(Array)
|
154
|
+
expected_type = expected_type.to_sentence(**OR_CONNECTOR)
|
82
155
|
end
|
83
156
|
|
84
|
-
|
85
|
-
|
157
|
+
message = "Expected #{expected_type}, got #{object.class}"
|
158
|
+
message += " for #{opts[:for]}" if opts[:for].present?
|
86
159
|
|
87
|
-
|
88
|
-
|
89
|
-
)
|
160
|
+
super(message)
|
161
|
+
end
|
90
162
|
end
|
91
|
-
end
|
92
163
|
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
164
|
+
#
|
165
|
+
# Raised when a variable reference cannot be resolved
|
166
|
+
# Indicates when a spec or expectation references an undefined variable
|
167
|
+
#
|
168
|
+
class MissingVariableError < Error
|
169
|
+
def initialize(variable_name)
|
170
|
+
super("Undefined variable \"#{variable_name}\" referenced in expectation")
|
100
171
|
end
|
172
|
+
end
|
101
173
|
|
102
|
-
|
103
|
-
|
174
|
+
#
|
175
|
+
# Raised when a YAML structure doesn't match expectations
|
176
|
+
# Acts as a container for multiple validation errors
|
177
|
+
#
|
178
|
+
class InvalidStructureError < Error
|
179
|
+
def initialize(errors)
|
180
|
+
message = errors.to_a.join_map("\n") do |error|
|
181
|
+
next error if error.is_a?(SpecForge::Error)
|
182
|
+
|
183
|
+
# Normal errors, let's get verbose
|
184
|
+
backtrace = SpecForge.backtrace_cleaner.clean(error.backtrace)
|
185
|
+
"#{error.inspect}\n # ./#{backtrace.join("\n # ./")}\n"
|
186
|
+
end
|
104
187
|
|
105
|
-
|
188
|
+
super(message)
|
189
|
+
end
|
106
190
|
end
|
107
|
-
end
|
108
191
|
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
192
|
+
#
|
193
|
+
# Raised when an unknown factory build strategy is provided
|
194
|
+
# Indicates when a strategy string doesn't match supported options
|
195
|
+
#
|
196
|
+
class InvalidBuildStrategy < Error
|
197
|
+
def initialize(build_strategy)
|
198
|
+
valid_strategies = Attribute::Factory::BUILD_STRATEGIES.to_sentence(**OR_CONNECTOR)
|
199
|
+
|
200
|
+
super(<<~STRING.chomp
|
201
|
+
Unknown build strategy "#{build_strategy}" referenced in spec.
|
202
|
+
|
203
|
+
Valid strategies include: #{valid_strategies}
|
204
|
+
STRING
|
205
|
+
)
|
206
|
+
end
|
115
207
|
end
|
116
|
-
end
|
117
208
|
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
209
|
+
#
|
210
|
+
# Raised when a spec file cannot be loaded
|
211
|
+
# Provides detailed information about the cause of the loading error
|
212
|
+
#
|
213
|
+
class SpecLoadError < Error
|
214
|
+
def initialize(error, file_path, spec: nil)
|
215
|
+
message =
|
216
|
+
if spec
|
217
|
+
"Error loading spec #{spec[:name].in_quotes} in file #{file_path.in_quotes} (line #{spec[:line_number]})"
|
218
|
+
else
|
219
|
+
"Error loading spec file #{file_path.in_quotes}"
|
220
|
+
end
|
221
|
+
|
222
|
+
causes = error.message.split("\n").map(&:strip).reject(&:empty?)
|
223
|
+
|
224
|
+
message +=
|
225
|
+
if causes.size > 1
|
226
|
+
"\nCauses:\n - #{causes.join_map("\n - ")}"
|
227
|
+
else
|
228
|
+
"\nCause: #{error}"
|
229
|
+
end
|
230
|
+
|
231
|
+
super(message)
|
129
232
|
end
|
233
|
+
end
|
130
234
|
|
131
|
-
|
235
|
+
#
|
236
|
+
# Raised when the provided namespace is not defined on the global context
|
237
|
+
#
|
238
|
+
class InvalidGlobalNamespaceError < Error
|
239
|
+
def initialize(provided_namespace)
|
240
|
+
super("Invalid global namespace #{provided_namespace.in_quotes}. Currently supported namespaces are: \"variables\"")
|
241
|
+
end
|
132
242
|
end
|
133
|
-
end
|
134
243
|
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
244
|
+
#
|
245
|
+
# Raised when the provided matcher name does not defined with RSpec
|
246
|
+
#
|
247
|
+
class UndefinedMatcherError < Error
|
248
|
+
def initialize(matcher_name)
|
249
|
+
matcher_categories = {
|
250
|
+
Equality: ["matcher.eq", "matcher.eql", "matcher.equal"],
|
251
|
+
Types: ["kind_of.string", "kind_of.integer", "kind_of.array", "kind_of.hash"],
|
252
|
+
Truthiness: ["be.true", "be.false", "be.nil"],
|
253
|
+
Comparison: ["be.within", "be.between", "be.greater_than", "be.less_than"],
|
254
|
+
Collections: ["matcher.include", "matcher.contain_exactly", "matcher.all"],
|
255
|
+
Strings: ["/regex/", "matcher.start_with", "matcher.end_with"]
|
256
|
+
}
|
141
257
|
|
142
|
-
|
143
|
-
|
258
|
+
formatted_categories =
|
259
|
+
matcher_categories.join_map("\n") do |category, matchers|
|
260
|
+
" #{category}: #{matchers.join(", ")}"
|
261
|
+
end
|
144
262
|
|
145
|
-
|
146
|
-
|
147
|
-
|
263
|
+
super(<<~STRING.chomp
|
264
|
+
Undefined matcher method "#{matcher_name}" is not available in RSpec matchers.
|
265
|
+
|
266
|
+
Common matchers you can use:
|
267
|
+
#{formatted_categories}
|
268
|
+
|
269
|
+
For the complete list of available matchers, check the RSpec documentation:
|
270
|
+
https://rspec.info/documentation/3.12/rspec-expectations/RSpec/Matchers.html
|
271
|
+
STRING
|
272
|
+
)
|
273
|
+
end
|
274
|
+
end
|
275
|
+
|
276
|
+
#
|
277
|
+
# Raised when a callback is referenced in config but hasn't been defined
|
278
|
+
#
|
279
|
+
class UndefinedCallbackError < Error
|
280
|
+
def initialize(callback_name, available_callbacks = [])
|
281
|
+
message = "The callback #{callback_name.in_quotes} was referenced but hasn't been defined."
|
282
|
+
|
283
|
+
message +=
|
284
|
+
if available_callbacks.any?
|
285
|
+
<<~STR.chomp
|
286
|
+
|
287
|
+
Available callbacks are: #{available_callbacks.join_map(", ", &:in_quotes)}
|
288
|
+
STR
|
289
|
+
else
|
290
|
+
<<~STR.chomp
|
291
|
+
|
292
|
+
No callbacks have been defined yet. Register callbacks with:
|
293
|
+
|
294
|
+
SpecForge.register_callback(:#{callback_name}) do |context|
|
295
|
+
# Your callback code
|
296
|
+
end
|
297
|
+
STR
|
298
|
+
end
|
299
|
+
|
300
|
+
super(message)
|
301
|
+
end
|
302
|
+
end
|
303
|
+
|
304
|
+
#
|
305
|
+
# Raised when an OpenAPI specification fails validation
|
306
|
+
#
|
307
|
+
# This error indicates that the generated OpenAPI document contains
|
308
|
+
# validation errors according to the OpenAPI specification standards.
|
309
|
+
# It's typically raised after attempting to validate a generated specification.
|
310
|
+
#
|
311
|
+
# @example
|
312
|
+
# begin
|
313
|
+
# generator.validate!(openapi_spec)
|
314
|
+
# rescue SpecForge::Error::InvalidOASDocument
|
315
|
+
# puts "Generated specification has validation errors"
|
316
|
+
# end
|
317
|
+
#
|
318
|
+
class InvalidOASDocument < Error
|
148
319
|
end
|
149
320
|
end
|
150
321
|
end
|