spec_forge 0.7.1 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +75 -1
- data/README.md +124 -202
- data/bin/spec_forge +1 -1
- data/flake.lock +76 -4
- data/flake.nix +5 -4
- data/lib/spec_forge/attribute/chainable.rb +6 -6
- data/lib/spec_forge/attribute/environment.rb +45 -0
- data/lib/spec_forge/attribute/factory.rb +26 -17
- data/lib/spec_forge/attribute/faker.rb +6 -1
- data/lib/spec_forge/attribute/generate.rb +114 -0
- data/lib/spec_forge/attribute/literal.rb +1 -14
- data/lib/spec_forge/attribute/matcher.rb +6 -2
- data/lib/spec_forge/attribute/parameterized.rb +20 -22
- data/lib/spec_forge/attribute/resolvable_array.rb +16 -16
- data/lib/spec_forge/attribute/resolvable_hash.rb +17 -16
- data/lib/spec_forge/attribute/resolvable_struct.rb +67 -0
- data/lib/spec_forge/attribute/template.rb +118 -0
- data/lib/spec_forge/attribute/transform.rb +14 -19
- data/lib/spec_forge/attribute/variable.rb +31 -31
- data/lib/spec_forge/attribute.rb +54 -100
- data/lib/spec_forge/blueprint.rb +27 -0
- data/lib/spec_forge/cli/docs/generate.rb +28 -8
- data/lib/spec_forge/cli/docs.rb +5 -2
- data/lib/spec_forge/cli/init.rb +4 -4
- data/lib/spec_forge/cli/new.rb +78 -27
- data/lib/spec_forge/cli/run.rb +84 -52
- data/lib/spec_forge/cli/serve.rb +5 -0
- data/lib/spec_forge/cli.rb +6 -14
- data/lib/spec_forge/configuration.rb +209 -79
- data/lib/spec_forge/documentation/{loader → builder}/cache.rb +26 -23
- data/lib/spec_forge/documentation/builder/compiler.rb +373 -0
- data/lib/spec_forge/documentation/builder/extractor.rb +75 -0
- data/lib/spec_forge/documentation/builder.rb +77 -329
- data/lib/spec_forge/documentation/document/operation.rb +4 -4
- data/lib/spec_forge/documentation/document.rb +0 -6
- data/lib/spec_forge/documentation/generator.rb +88 -0
- data/lib/spec_forge/documentation/{generators/openapi → openapi/v3_0}/error_formatter.rb +2 -2
- data/lib/spec_forge/documentation/openapi/v3_0/example.rb +1 -1
- data/lib/spec_forge/documentation/openapi/v3_0/media_type.rb +1 -1
- data/lib/spec_forge/documentation/openapi/v3_0/operation.rb +21 -5
- data/lib/spec_forge/documentation/openapi/v3_0/response.rb +28 -6
- data/lib/spec_forge/documentation/openapi/v3_0/schema.rb +20 -2
- data/lib/spec_forge/documentation/openapi/v3_0/tag.rb +1 -1
- data/lib/spec_forge/documentation/openapi/v3_0.rb +116 -0
- data/lib/spec_forge/documentation/openapi.rb +40 -12
- data/lib/spec_forge/documentation.rb +1 -7
- data/lib/spec_forge/error.rb +215 -41
- data/lib/spec_forge/factory.rb +38 -18
- data/lib/spec_forge/forge/action.rb +41 -0
- data/lib/spec_forge/forge/actions/call.rb +33 -0
- data/lib/spec_forge/forge/actions/debug.rb +47 -0
- data/lib/spec_forge/forge/actions/expect.rb +44 -0
- data/lib/spec_forge/forge/actions/request.rb +65 -0
- data/lib/spec_forge/forge/actions/store.rb +31 -0
- data/lib/spec_forge/forge/callbacks.rb +80 -0
- data/lib/spec_forge/forge/context.rb +41 -0
- data/lib/spec_forge/forge/display.rb +503 -0
- data/lib/spec_forge/forge/hooks.rb +131 -0
- data/lib/spec_forge/forge/runner/array_io.rb +81 -0
- data/lib/spec_forge/forge/runner/content_validator.rb +92 -0
- data/lib/spec_forge/forge/runner/header_validator.rb +66 -0
- data/lib/spec_forge/forge/runner/reporter.rb +56 -0
- data/lib/spec_forge/forge/runner/schema_validator.rb +113 -0
- data/lib/spec_forge/forge/runner.rb +118 -0
- data/lib/spec_forge/forge/timer.rb +94 -0
- data/lib/spec_forge/forge/variables.rb +38 -0
- data/lib/spec_forge/forge.rb +207 -133
- data/lib/spec_forge/http/backend.rb +49 -146
- data/lib/spec_forge/http/client.rb +14 -17
- data/lib/spec_forge/http/request.rb +37 -84
- data/lib/spec_forge/http/verb.rb +4 -0
- data/lib/spec_forge/http.rb +0 -5
- data/lib/spec_forge/loader/filter.rb +85 -0
- data/lib/spec_forge/loader/step_processor.rb +282 -0
- data/lib/spec_forge/loader.rb +105 -220
- data/lib/spec_forge/normalizer/default.rb +1 -1
- data/lib/spec_forge/normalizer/structure.rb +140 -0
- data/lib/spec_forge/normalizer/transformers.rb +168 -0
- data/lib/spec_forge/normalizer/validators.rb +50 -8
- data/lib/spec_forge/normalizer.rb +76 -119
- data/lib/spec_forge/normalizers/callback.yml +38 -0
- data/lib/spec_forge/normalizers/configuration.yml +59 -9
- data/lib/spec_forge/normalizers/factory.yml +53 -2
- data/lib/spec_forge/normalizers/factory_reference.yml +63 -2
- data/lib/spec_forge/normalizers/json_schema.yml +79 -0
- data/lib/spec_forge/normalizers/step.yml +506 -0
- data/lib/spec_forge/step/call.rb +36 -0
- data/lib/spec_forge/step/expect.rb +110 -0
- data/lib/spec_forge/step/source.rb +22 -0
- data/lib/spec_forge/step.rb +129 -0
- data/lib/spec_forge/type.rb +115 -66
- data/lib/spec_forge/version.rb +1 -1
- data/lib/spec_forge.rb +44 -106
- data/lib/templates/forge_helper.rb.tt +43 -22
- data/lib/templates/new_blueprint.yml.tt +54 -0
- metadata +75 -44
- data/lib/spec_forge/attribute/global.rb +0 -96
- data/lib/spec_forge/attribute/store.rb +0 -65
- data/lib/spec_forge/backtrace_formatter.rb +0 -50
- data/lib/spec_forge/callbacks.rb +0 -88
- data/lib/spec_forge/context/callbacks.rb +0 -91
- data/lib/spec_forge/context/global.rb +0 -72
- data/lib/spec_forge/context/store.rb +0 -131
- data/lib/spec_forge/context/variables.rb +0 -91
- data/lib/spec_forge/context.rb +0 -36
- data/lib/spec_forge/core_ext/rspec.rb +0 -55
- data/lib/spec_forge/core_ext.rb +0 -5
- data/lib/spec_forge/documentation/generators/base.rb +0 -81
- data/lib/spec_forge/documentation/generators/openapi/base.rb +0 -100
- data/lib/spec_forge/documentation/generators/openapi/v3_0.rb +0 -65
- data/lib/spec_forge/documentation/generators/openapi.rb +0 -59
- data/lib/spec_forge/documentation/generators.rb +0 -17
- data/lib/spec_forge/documentation/loader.rb +0 -159
- data/lib/spec_forge/documentation/openapi/base.rb +0 -33
- data/lib/spec_forge/filter.rb +0 -86
- data/lib/spec_forge/normalizer/definition.rb +0 -248
- data/lib/spec_forge/normalizers/_shared.yml +0 -76
- data/lib/spec_forge/normalizers/constraint.yml +0 -8
- data/lib/spec_forge/normalizers/expectation.yml +0 -47
- data/lib/spec_forge/normalizers/global_context.yml +0 -28
- data/lib/spec_forge/normalizers/spec.yml +0 -50
- data/lib/spec_forge/runner/adapter.rb +0 -181
- data/lib/spec_forge/runner/callbacks.rb +0 -246
- data/lib/spec_forge/runner/debug_proxy.rb +0 -215
- data/lib/spec_forge/runner/listener.rb +0 -54
- data/lib/spec_forge/runner/metadata.rb +0 -58
- data/lib/spec_forge/runner/state.rb +0 -98
- data/lib/spec_forge/runner.rb +0 -75
- data/lib/spec_forge/spec/expectation/constraint.rb +0 -127
- data/lib/spec_forge/spec/expectation.rb +0 -68
- data/lib/spec_forge/spec.rb +0 -68
- data/lib/templates/new_spec.yml.tt +0 -43
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
module SpecForge
|
|
4
4
|
module Documentation
|
|
5
5
|
module OpenAPI
|
|
6
|
-
|
|
6
|
+
class V30
|
|
7
7
|
#
|
|
8
8
|
# Represents an OpenAPI 3.0 Response object
|
|
9
9
|
#
|
|
@@ -12,7 +12,23 @@ module SpecForge
|
|
|
12
12
|
#
|
|
13
13
|
# @see https://spec.openapis.org/oas/v3.0.4.html#response-object
|
|
14
14
|
#
|
|
15
|
-
class Response
|
|
15
|
+
class Response
|
|
16
|
+
#
|
|
17
|
+
# The document object containing structured API data
|
|
18
|
+
#
|
|
19
|
+
# @return [Object] The document with endpoint information
|
|
20
|
+
#
|
|
21
|
+
attr_reader :document
|
|
22
|
+
|
|
23
|
+
#
|
|
24
|
+
# Creates a new Response from a document
|
|
25
|
+
#
|
|
26
|
+
# @param document [Object] The document containing response data
|
|
27
|
+
#
|
|
28
|
+
def initialize(document)
|
|
29
|
+
@document = document
|
|
30
|
+
end
|
|
31
|
+
|
|
16
32
|
#
|
|
17
33
|
# Converts the response to an OpenAPI-compliant hash
|
|
18
34
|
#
|
|
@@ -38,10 +54,12 @@ module SpecForge
|
|
|
38
54
|
# Creates media type objects with schemas and merges with any
|
|
39
55
|
# documentation-provided content definitions.
|
|
40
56
|
#
|
|
41
|
-
# @return [Hash] Content definitions by media type
|
|
57
|
+
# @return [Hash, nil] Content definitions by media type
|
|
42
58
|
#
|
|
43
59
|
def content
|
|
44
|
-
|
|
60
|
+
return nil if document.content_type.blank?
|
|
61
|
+
|
|
62
|
+
schema = Schema.new(type: document.body.type, content: document.body.content).to_h
|
|
45
63
|
|
|
46
64
|
{
|
|
47
65
|
document.content_type => MediaType.new(schema:).to_h
|
|
@@ -51,12 +69,16 @@ module SpecForge
|
|
|
51
69
|
#
|
|
52
70
|
# Returns header definitions for the response
|
|
53
71
|
#
|
|
54
|
-
#
|
|
72
|
+
# Transforms document headers into OpenAPI format with schema wrappers.
|
|
55
73
|
#
|
|
56
74
|
# @return [Hash, nil] Header definitions
|
|
57
75
|
#
|
|
58
76
|
def headers
|
|
59
|
-
document.headers.
|
|
77
|
+
return nil if document.headers.blank?
|
|
78
|
+
|
|
79
|
+
document.headers.transform_values do |header|
|
|
80
|
+
{schema: header}
|
|
81
|
+
end
|
|
60
82
|
end
|
|
61
83
|
end
|
|
62
84
|
end
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
module SpecForge
|
|
4
4
|
module Documentation
|
|
5
5
|
module OpenAPI
|
|
6
|
-
|
|
6
|
+
class V30
|
|
7
7
|
#
|
|
8
8
|
# Represents an OpenAPI 3.0 Schema object
|
|
9
9
|
#
|
|
@@ -27,16 +27,25 @@ module SpecForge
|
|
|
27
27
|
#
|
|
28
28
|
attr_reader :format
|
|
29
29
|
|
|
30
|
+
#
|
|
31
|
+
# The schema content (for arrays/objects)
|
|
32
|
+
#
|
|
33
|
+
# @return [Object, nil] The schema content
|
|
34
|
+
#
|
|
35
|
+
attr_reader :content
|
|
36
|
+
|
|
30
37
|
#
|
|
31
38
|
# Creates a new OpenAPI schema object
|
|
32
39
|
#
|
|
33
40
|
# @param options [Hash] Schema configuration options
|
|
34
41
|
# @option options [String] :type The data type to convert to OpenAPI format
|
|
42
|
+
# @option options [Object] :content The content/items for arrays or properties for objects
|
|
35
43
|
#
|
|
36
44
|
# @return [Schema] A new schema instance
|
|
37
45
|
#
|
|
38
46
|
def initialize(options = {})
|
|
39
47
|
@type, @format = transform_type(options[:type])
|
|
48
|
+
@content = options[:content]
|
|
40
49
|
end
|
|
41
50
|
|
|
42
51
|
#
|
|
@@ -45,10 +54,19 @@ module SpecForge
|
|
|
45
54
|
# @return [Hash] OpenAPI-formatted schema object
|
|
46
55
|
#
|
|
47
56
|
def to_h
|
|
48
|
-
{
|
|
57
|
+
base = {
|
|
49
58
|
type:,
|
|
50
59
|
format:
|
|
51
60
|
}.compact_blank!
|
|
61
|
+
|
|
62
|
+
# Add items for arrays
|
|
63
|
+
if type == "array" && content.present?
|
|
64
|
+
# Content is an array like [{type: "string"}], take first element as items schema
|
|
65
|
+
items_type = content.first&.dig(:type) || "object"
|
|
66
|
+
base[:items] = {type: items_type}
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
base
|
|
52
70
|
end
|
|
53
71
|
|
|
54
72
|
private
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SpecForge
|
|
4
|
+
module Documentation
|
|
5
|
+
module OpenAPI
|
|
6
|
+
# https://spec.openapis.org/oas/v3.0.4.html
|
|
7
|
+
class V30 < Generator
|
|
8
|
+
#
|
|
9
|
+
# Current OpenAPI 3.0 version supported by this generator
|
|
10
|
+
#
|
|
11
|
+
# @api private
|
|
12
|
+
#
|
|
13
|
+
CURRENT_VERSION = "3.0.4"
|
|
14
|
+
|
|
15
|
+
#
|
|
16
|
+
# Validates an OpenAPI specification against the standard
|
|
17
|
+
#
|
|
18
|
+
# Uses the openapi3_parser gem to validate the generated specification
|
|
19
|
+
# and provides detailed error reporting if validation fails.
|
|
20
|
+
#
|
|
21
|
+
# @param output [Hash] The OpenAPI specification to validate
|
|
22
|
+
#
|
|
23
|
+
# @return [void]
|
|
24
|
+
#
|
|
25
|
+
# @raise [Error::InvalidOASDocument] If the specification is invalid
|
|
26
|
+
#
|
|
27
|
+
def self.validate!(output)
|
|
28
|
+
document = Openapi3Parser.load(output)
|
|
29
|
+
if document.valid?
|
|
30
|
+
puts "✅ No validation errors found!"
|
|
31
|
+
return
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
puts ErrorFormatter.format(document.errors.errors)
|
|
35
|
+
raise Error::InvalidOASDocument
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
#
|
|
39
|
+
# Generates an OpenAPI 3.0 specification from the input document
|
|
40
|
+
#
|
|
41
|
+
# Creates a complete OpenAPI specification by combining the document's
|
|
42
|
+
# endpoint data with configuration files and ensuring compliance with
|
|
43
|
+
# OpenAPI 3.0.4 standards.
|
|
44
|
+
#
|
|
45
|
+
# @return [Hash] Complete OpenAPI 3.0 specification
|
|
46
|
+
#
|
|
47
|
+
def generate
|
|
48
|
+
output = {
|
|
49
|
+
openapi: CURRENT_VERSION,
|
|
50
|
+
paths:
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
output.deep_stringify_keys!
|
|
54
|
+
output.deep_merge!(config)
|
|
55
|
+
|
|
56
|
+
output
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
#
|
|
60
|
+
# Transforms document endpoints into OpenAPI paths structure
|
|
61
|
+
#
|
|
62
|
+
# Converts the internal endpoint representation into the OpenAPI paths
|
|
63
|
+
# format, with each path containing operations organized by HTTP method.
|
|
64
|
+
#
|
|
65
|
+
# @return [Hash] OpenAPI paths object with operations
|
|
66
|
+
#
|
|
67
|
+
def paths
|
|
68
|
+
paths = input.endpoints.deep_dup
|
|
69
|
+
|
|
70
|
+
paths.each do |path, operations|
|
|
71
|
+
operations.transform_values! do |document|
|
|
72
|
+
Operation.new(document).to_h
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
protected
|
|
78
|
+
|
|
79
|
+
#
|
|
80
|
+
# Loads OpenAPI configuration from YAML
|
|
81
|
+
#
|
|
82
|
+
# @return [Hash] The normalized OpenAPI configuration
|
|
83
|
+
#
|
|
84
|
+
# @api private
|
|
85
|
+
#
|
|
86
|
+
def config
|
|
87
|
+
@config ||= begin
|
|
88
|
+
file_extension_glob = "*.{yml,yaml}"
|
|
89
|
+
base_path = SpecForge.openapi_path.join("config")
|
|
90
|
+
|
|
91
|
+
root_paths = base_path.join(file_extension_glob)
|
|
92
|
+
path_paths = base_path.join("paths", "**", file_extension_glob)
|
|
93
|
+
component_paths = base_path.join("components", "**", file_extension_glob)
|
|
94
|
+
|
|
95
|
+
config = load_yml_from_paths(root_paths).to_merged_h
|
|
96
|
+
paths_config = load_yml_from_paths(path_paths).to_merged_h
|
|
97
|
+
component_config = load_yml_from_paths(component_paths).to_merged_h
|
|
98
|
+
|
|
99
|
+
(config["paths"] ||= {}).deep_merge!(paths_config)
|
|
100
|
+
(config["components"] ||= {}).deep_merge!(component_config)
|
|
101
|
+
|
|
102
|
+
config
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
private
|
|
107
|
+
|
|
108
|
+
def load_yml_from_paths(paths)
|
|
109
|
+
Dir[paths].map do |path|
|
|
110
|
+
YAML.safe_load_file(path)
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
end
|
|
@@ -3,21 +3,49 @@
|
|
|
3
3
|
module SpecForge
|
|
4
4
|
module Documentation
|
|
5
5
|
#
|
|
6
|
-
# OpenAPI
|
|
6
|
+
# OpenAPI specification generation and version management
|
|
7
7
|
#
|
|
8
|
-
#
|
|
9
|
-
#
|
|
8
|
+
# Provides version-aware access to OpenAPI generators and validators.
|
|
9
|
+
# Supports multiple OpenAPI versions through semantic versioning matching.
|
|
10
10
|
#
|
|
11
11
|
module OpenAPI
|
|
12
|
+
#
|
|
13
|
+
# Current OpenAPI version used as default
|
|
14
|
+
#
|
|
15
|
+
# Points to the latest supported OpenAPI version for new specifications.
|
|
16
|
+
#
|
|
17
|
+
# @api private
|
|
18
|
+
#
|
|
19
|
+
CURRENT_VERSION = V30::CURRENT_VERSION
|
|
20
|
+
|
|
21
|
+
#
|
|
22
|
+
# Mapping of semantic versions to their generator classes
|
|
23
|
+
#
|
|
24
|
+
# @return [Hash{SemVersion => Class}]
|
|
25
|
+
#
|
|
26
|
+
VERSIONS = {
|
|
27
|
+
V30.to_sem_version => V30
|
|
28
|
+
}.freeze
|
|
29
|
+
|
|
30
|
+
#
|
|
31
|
+
# Returns the generator class for the specified OpenAPI version
|
|
32
|
+
#
|
|
33
|
+
# @param version [String, SemVersion] The OpenAPI version (e.g., "3.0")
|
|
34
|
+
#
|
|
35
|
+
# @return [Class] The generator class for the requested version
|
|
36
|
+
#
|
|
37
|
+
# @raise [ArgumentError] If the version is not supported
|
|
38
|
+
#
|
|
39
|
+
def self.[](version)
|
|
40
|
+
version = SemVersion.from_loose_version(version)
|
|
41
|
+
generator = VERSIONS.value_where { |k, _v| k.satisfies?("~> #{version}") }
|
|
42
|
+
|
|
43
|
+
if generator.nil?
|
|
44
|
+
raise ArgumentError, "Invalid OpenAPI version provided: #{version.to_s.in_quotes}"
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
generator
|
|
48
|
+
end
|
|
12
49
|
end
|
|
13
50
|
end
|
|
14
51
|
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"
|
|
@@ -14,14 +14,8 @@ module SpecForge
|
|
|
14
14
|
#
|
|
15
15
|
# # Programmatically
|
|
16
16
|
# document = Documentation::Loader.load_document
|
|
17
|
-
# spec = Documentation::
|
|
17
|
+
# spec = Documentation::OpenAPI["3.0"].new(document).generate
|
|
18
18
|
#
|
|
19
19
|
module Documentation
|
|
20
20
|
end
|
|
21
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
|
@@ -5,17 +5,6 @@ module SpecForge
|
|
|
5
5
|
# Base error class for all SpecForge-specific exceptions
|
|
6
6
|
#
|
|
7
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
|
|
16
|
-
|
|
17
|
-
private_constant :OR_CONNECTOR
|
|
18
|
-
|
|
19
8
|
#
|
|
20
9
|
# Raised when a provided Faker class name doesn't exist
|
|
21
10
|
# Provides helpful suggestions for similar class names
|
|
@@ -40,7 +29,7 @@ module SpecForge
|
|
|
40
29
|
super(<<~STRING.chomp
|
|
41
30
|
Undefined Faker class "#{input}". #{DidYouMean::Formatter.message_for(corrections)}
|
|
42
31
|
|
|
43
|
-
For available classes, please check https://github.com/faker-ruby/faker
|
|
32
|
+
For available classes, please check https://github.com/faker-ruby/faker/blob/main/GENERATORS.md.
|
|
44
33
|
STRING
|
|
45
34
|
)
|
|
46
35
|
end
|
|
@@ -74,12 +63,30 @@ module SpecForge
|
|
|
74
63
|
# Indicates when a transform name isn't supported
|
|
75
64
|
#
|
|
76
65
|
class InvalidTransformFunctionError < Error
|
|
77
|
-
def initialize(input)
|
|
78
|
-
|
|
66
|
+
def initialize(input, valid_functions)
|
|
67
|
+
formatted_functions = valid_functions.join_map(", ") { |f| "transform.#{f}".in_quotes }
|
|
68
|
+
|
|
79
69
|
super(<<~STRING.chomp
|
|
80
70
|
Undefined transform function "#{input}".
|
|
81
71
|
|
|
82
|
-
|
|
72
|
+
Valid functions: #{formatted_functions}
|
|
73
|
+
STRING
|
|
74
|
+
)
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
#
|
|
79
|
+
# Raised when an unknown generation function is referenced
|
|
80
|
+
# Indicates when a generation name isn't supported
|
|
81
|
+
#
|
|
82
|
+
class InvalidGenerateFunctionError < Error
|
|
83
|
+
def initialize(input, valid_functions)
|
|
84
|
+
formatted_functions = valid_functions.join_map(", ") { |f| "generate.#{f}".in_quotes }
|
|
85
|
+
|
|
86
|
+
super(<<~STRING.chomp
|
|
87
|
+
Undefined generate function "#{input}".
|
|
88
|
+
|
|
89
|
+
Valid functions: #{formatted_functions}
|
|
83
90
|
STRING
|
|
84
91
|
)
|
|
85
92
|
end
|
|
@@ -151,23 +158,48 @@ module SpecForge
|
|
|
151
158
|
class InvalidTypeError < Error
|
|
152
159
|
def initialize(object, expected_type, **opts)
|
|
153
160
|
if expected_type.instance_of?(Array)
|
|
154
|
-
expected_type = expected_type.
|
|
161
|
+
expected_type = expected_type.to_or_sentence
|
|
155
162
|
end
|
|
156
163
|
|
|
157
164
|
message = "Expected #{expected_type}, got #{object.class}"
|
|
158
165
|
message += " for #{opts[:for]}" if opts[:for].present?
|
|
159
166
|
|
|
167
|
+
if opts[:description] || opts[:examples]
|
|
168
|
+
message += "\n"
|
|
169
|
+
|
|
170
|
+
if opts[:description]
|
|
171
|
+
message += "\nAbout #{opts[:attribute_name].in_quotes}:"
|
|
172
|
+
message += "\n #{opts[:description].gsub("\n", "\n ")}"
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
if opts[:examples].present?
|
|
176
|
+
message += "\n\nExamples:\n #{opts[:examples].join("\n\n").gsub("\n", "\n ")}"
|
|
177
|
+
end
|
|
178
|
+
end
|
|
179
|
+
|
|
160
180
|
super(message)
|
|
161
181
|
end
|
|
162
182
|
end
|
|
163
183
|
|
|
164
184
|
#
|
|
165
|
-
# Raised when a variable
|
|
166
|
-
#
|
|
185
|
+
# Raised when a referenced variable is not defined in the current context
|
|
186
|
+
#
|
|
187
|
+
# Provides helpful suggestions for similar variable names using spell checking.
|
|
167
188
|
#
|
|
168
189
|
class MissingVariableError < Error
|
|
169
|
-
def initialize(variable_name)
|
|
170
|
-
|
|
190
|
+
def initialize(variable_name, available_variables: [])
|
|
191
|
+
message = "Undefined variable \"#{variable_name}\""
|
|
192
|
+
|
|
193
|
+
checker = DidYouMean::SpellChecker.new(dictionary: available_variables)
|
|
194
|
+
suggestions = checker.correct(variable_name.to_s)
|
|
195
|
+
|
|
196
|
+
message += ". #{DidYouMean::Formatter.message_for(suggestions)}" if suggestions.size > 0
|
|
197
|
+
|
|
198
|
+
if available_variables.size > 0 && available_variables.size <= 5
|
|
199
|
+
message += ".\nAvailable: #{available_variables.join_map(", ", &:in_quotes)}"
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
super(message)
|
|
171
203
|
end
|
|
172
204
|
end
|
|
173
205
|
|
|
@@ -195,7 +227,7 @@ module SpecForge
|
|
|
195
227
|
#
|
|
196
228
|
class InvalidBuildStrategy < Error
|
|
197
229
|
def initialize(build_strategy)
|
|
198
|
-
valid_strategies = Attribute::Factory::BUILD_STRATEGIES.
|
|
230
|
+
valid_strategies = Attribute::Factory::BUILD_STRATEGIES.to_or_sentence
|
|
199
231
|
|
|
200
232
|
super(<<~STRING.chomp
|
|
201
233
|
Unknown build strategy "#{build_strategy}" referenced in spec.
|
|
@@ -207,37 +239,53 @@ module SpecForge
|
|
|
207
239
|
end
|
|
208
240
|
|
|
209
241
|
#
|
|
210
|
-
# Raised when a
|
|
211
|
-
# Provides detailed information about the cause of the loading error
|
|
242
|
+
# Raised when a step has an invalid configuration
|
|
212
243
|
#
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
causes = error.message.split("\n").map(&:strip).reject(&:empty?)
|
|
244
|
+
# Common cases:
|
|
245
|
+
# - Action attributes (request, expect, call, debug, store) combined with steps
|
|
246
|
+
# - Expects defined without a corresponding request
|
|
247
|
+
#
|
|
248
|
+
class InvalidStepError < Error
|
|
249
|
+
def initialize(message, step = nil)
|
|
250
|
+
if step
|
|
251
|
+
step_name = step[:name].presence || "(unnamed)"
|
|
223
252
|
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
"\nCauses:\n - #{causes.join_map("\n - ")}"
|
|
253
|
+
line_info = if (source = step[:source])
|
|
254
|
+
"#{source[:file_name]}:#{source[:line_number]}"
|
|
227
255
|
else
|
|
228
|
-
"
|
|
256
|
+
"unknown location"
|
|
229
257
|
end
|
|
230
258
|
|
|
259
|
+
message = "Step #{step_name.in_quotes} [#{line_info}]: #{message}"
|
|
260
|
+
end
|
|
261
|
+
|
|
231
262
|
super(message)
|
|
232
263
|
end
|
|
233
264
|
end
|
|
234
265
|
|
|
235
266
|
#
|
|
236
|
-
# Raised when
|
|
267
|
+
# Raised when an error occurs while loading a step during blueprint processing
|
|
268
|
+
#
|
|
269
|
+
# Wraps the original error with step context information to help identify
|
|
270
|
+
# which step caused the problem.
|
|
237
271
|
#
|
|
238
|
-
class
|
|
239
|
-
def initialize(
|
|
240
|
-
|
|
272
|
+
class LoadStepError < Error
|
|
273
|
+
def initialize(error, step, depth = 0)
|
|
274
|
+
step_name = step[:name].presence || "(unnamed)"
|
|
275
|
+
|
|
276
|
+
line_info = if (source = step[:source])
|
|
277
|
+
"#{source[:file_name]}:#{source[:line_number]}"
|
|
278
|
+
end
|
|
279
|
+
|
|
280
|
+
message = "Step: #{step_name.in_quotes} [#{line_info}]"
|
|
281
|
+
|
|
282
|
+
cause_message = if error.is_a?(LoadStepError)
|
|
283
|
+
"\n#{error.message}"
|
|
284
|
+
else
|
|
285
|
+
"\n\nCaused by: \n #{error.message.gsub("\n", "\n ")}"
|
|
286
|
+
end
|
|
287
|
+
|
|
288
|
+
super(message + cause_message)
|
|
241
289
|
end
|
|
242
290
|
end
|
|
243
291
|
|
|
@@ -317,5 +365,131 @@ module SpecForge
|
|
|
317
365
|
#
|
|
318
366
|
class InvalidOASDocument < Error
|
|
319
367
|
end
|
|
368
|
+
|
|
369
|
+
#
|
|
370
|
+
# Raised when one or more expectations fail during step execution
|
|
371
|
+
#
|
|
372
|
+
# Contains the list of failed RSpec examples for reporting purposes.
|
|
373
|
+
#
|
|
374
|
+
class ExpectationFailure < Error
|
|
375
|
+
attr_reader :failed_examples
|
|
376
|
+
|
|
377
|
+
def initialize(failed_examples)
|
|
378
|
+
@failed_examples = failed_examples
|
|
379
|
+
|
|
380
|
+
super("Failed expectations (#{@failed_examples.size})")
|
|
381
|
+
end
|
|
382
|
+
end
|
|
383
|
+
|
|
384
|
+
#
|
|
385
|
+
# Raised when JSON shape validation fails
|
|
386
|
+
#
|
|
387
|
+
# Contains structured failure information for all validation errors
|
|
388
|
+
# discovered during shape checking.
|
|
389
|
+
#
|
|
390
|
+
# @example Single failure
|
|
391
|
+
# failures = [{path: ".id", expected_type: String, actual_type: Integer, actual_value: 42}]
|
|
392
|
+
# raise SchemaValidationFailure.new(failures)
|
|
393
|
+
#
|
|
394
|
+
# @example Multiple failures
|
|
395
|
+
# failures = [
|
|
396
|
+
# {path: ".id", expected_type: String, actual_type: Integer, actual_value: 42},
|
|
397
|
+
# {path: ".email", expected_type: String, actual_type: NilClass, actual_value: nil}
|
|
398
|
+
# ]
|
|
399
|
+
# raise SchemaValidationFailure.new(failures)
|
|
400
|
+
#
|
|
401
|
+
class SchemaValidationFailure < Error
|
|
402
|
+
attr_reader :failures
|
|
403
|
+
|
|
404
|
+
def initialize(failures)
|
|
405
|
+
@failures = failures
|
|
406
|
+
|
|
407
|
+
message =
|
|
408
|
+
if failures.size == 1
|
|
409
|
+
format_failure(failures.first)
|
|
410
|
+
else
|
|
411
|
+
failures.join_map("\n") do |failure|
|
|
412
|
+
format_failure(failure)
|
|
413
|
+
end
|
|
414
|
+
end
|
|
415
|
+
|
|
416
|
+
super(message)
|
|
417
|
+
end
|
|
418
|
+
|
|
419
|
+
private
|
|
420
|
+
|
|
421
|
+
def format_failure(failure)
|
|
422
|
+
expected_types =
|
|
423
|
+
if failure[:expected_type].size == 1
|
|
424
|
+
failure[:expected_type].first
|
|
425
|
+
else
|
|
426
|
+
failure[:expected_type].to_or_sentence
|
|
427
|
+
end
|
|
428
|
+
|
|
429
|
+
"#{failure[:path]}: expected #{expected_types}, got #{failure[:actual_type]} (#{failure[:actual_value].inspect})"
|
|
430
|
+
end
|
|
431
|
+
end
|
|
432
|
+
|
|
433
|
+
#
|
|
434
|
+
# Raised when JSON content validation fails
|
|
435
|
+
#
|
|
436
|
+
# Contains structured failure information for content mismatches.
|
|
437
|
+
#
|
|
438
|
+
class ContentValidationFailure < Error
|
|
439
|
+
attr_reader :failures
|
|
440
|
+
|
|
441
|
+
def initialize(failures)
|
|
442
|
+
@failures = failures
|
|
443
|
+
super(format_failures(failures))
|
|
444
|
+
end
|
|
445
|
+
|
|
446
|
+
private
|
|
447
|
+
|
|
448
|
+
def format_failures(failures)
|
|
449
|
+
if failures.size == 1
|
|
450
|
+
failure = failures.first
|
|
451
|
+
"#{failure[:path]}: #{failure[:message]}"
|
|
452
|
+
else
|
|
453
|
+
failures.join_map("\n") { |f| "#{f[:path]}: #{f[:message]}" }
|
|
454
|
+
end
|
|
455
|
+
end
|
|
456
|
+
end
|
|
457
|
+
|
|
458
|
+
#
|
|
459
|
+
# Raised when HTTP header validation fails
|
|
460
|
+
#
|
|
461
|
+
# Contains structured failure information for header mismatches.
|
|
462
|
+
#
|
|
463
|
+
class HeaderValidationFailure < Error
|
|
464
|
+
attr_reader :failures
|
|
465
|
+
|
|
466
|
+
def initialize(failures)
|
|
467
|
+
@failures = failures
|
|
468
|
+
super(format_failures(failures))
|
|
469
|
+
end
|
|
470
|
+
|
|
471
|
+
private
|
|
472
|
+
|
|
473
|
+
def format_failures(failures)
|
|
474
|
+
if failures.size == 1
|
|
475
|
+
failure = failures.first
|
|
476
|
+
"#{failure[:header].in_quotes}: #{failure[:message]}"
|
|
477
|
+
else
|
|
478
|
+
failures.join_map("\n") { |f| "#{f[:header].in_quotes}: #{f[:message]}" }
|
|
479
|
+
end
|
|
480
|
+
end
|
|
481
|
+
end
|
|
482
|
+
|
|
483
|
+
#
|
|
484
|
+
# Raised when no blueprints are found during loading
|
|
485
|
+
#
|
|
486
|
+
# This typically occurs when attempting to generate documentation
|
|
487
|
+
# but the blueprints directory is empty or doesn't contain valid files.
|
|
488
|
+
#
|
|
489
|
+
class NoBlueprintsError < Error
|
|
490
|
+
def initialize
|
|
491
|
+
super("No blueprints found. Please ensure your blueprints directory contains valid blueprint files.")
|
|
492
|
+
end
|
|
493
|
+
end
|
|
320
494
|
end
|
|
321
495
|
end
|