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
|
@@ -1,65 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module SpecForge
|
|
4
|
-
module Documentation
|
|
5
|
-
module Generators
|
|
6
|
-
module OpenAPI
|
|
7
|
-
# https://spec.openapis.org/oas/v3.0.4.html
|
|
8
|
-
class V3_0 < Base # standard:disable Naming/ClassAndModuleCamelCase
|
|
9
|
-
#
|
|
10
|
-
# Current OpenAPI 3.0 version supported by this generator
|
|
11
|
-
#
|
|
12
|
-
# @api private
|
|
13
|
-
#
|
|
14
|
-
CURRENT_VERSION = "3.0.4"
|
|
15
|
-
|
|
16
|
-
#
|
|
17
|
-
# Alias for OpenAPI V3.0 classes for cleaner code
|
|
18
|
-
#
|
|
19
|
-
# @api private
|
|
20
|
-
#
|
|
21
|
-
OAS = Documentation::OpenAPI::V3_0
|
|
22
|
-
|
|
23
|
-
#
|
|
24
|
-
# Generates an OpenAPI 3.0 specification from the input document
|
|
25
|
-
#
|
|
26
|
-
# Creates a complete OpenAPI specification by combining the document's
|
|
27
|
-
# endpoint data with configuration files and ensuring compliance with
|
|
28
|
-
# OpenAPI 3.0.4 standards.
|
|
29
|
-
#
|
|
30
|
-
# @return [Hash] Complete OpenAPI 3.0 specification
|
|
31
|
-
#
|
|
32
|
-
def generate
|
|
33
|
-
output = {
|
|
34
|
-
openapi: CURRENT_VERSION,
|
|
35
|
-
paths:
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
output.deep_stringify_keys!
|
|
39
|
-
output.deep_merge!(config)
|
|
40
|
-
|
|
41
|
-
output
|
|
42
|
-
end
|
|
43
|
-
|
|
44
|
-
#
|
|
45
|
-
# Transforms document endpoints into OpenAPI paths structure
|
|
46
|
-
#
|
|
47
|
-
# Converts the internal endpoint representation into the OpenAPI paths
|
|
48
|
-
# format, with each path containing operations organized by HTTP method.
|
|
49
|
-
#
|
|
50
|
-
# @return [Hash] OpenAPI paths object with operations
|
|
51
|
-
#
|
|
52
|
-
def paths
|
|
53
|
-
paths = input.endpoints.deep_dup
|
|
54
|
-
|
|
55
|
-
paths.each do |path, operations|
|
|
56
|
-
operations.transform_values! do |document|
|
|
57
|
-
OAS::Operation.new(document).to_h
|
|
58
|
-
end
|
|
59
|
-
end
|
|
60
|
-
end
|
|
61
|
-
end
|
|
62
|
-
end
|
|
63
|
-
end
|
|
64
|
-
end
|
|
65
|
-
end
|
|
@@ -1,59 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require_relative "openapi/base"
|
|
4
|
-
require_relative "openapi/error_formatter"
|
|
5
|
-
require_relative "openapi/v3_0"
|
|
6
|
-
|
|
7
|
-
module SpecForge
|
|
8
|
-
module Documentation
|
|
9
|
-
module Generators
|
|
10
|
-
#
|
|
11
|
-
# Namespace for OpenAPI generators
|
|
12
|
-
#
|
|
13
|
-
# Contains version-specific OpenAPI generators and helper methods
|
|
14
|
-
# for selecting the appropriate generator.
|
|
15
|
-
#
|
|
16
|
-
module OpenAPI
|
|
17
|
-
#
|
|
18
|
-
# Current OpenAPI version used as default
|
|
19
|
-
#
|
|
20
|
-
# Points to the latest supported OpenAPI version for new specifications.
|
|
21
|
-
#
|
|
22
|
-
# @api private
|
|
23
|
-
#
|
|
24
|
-
CURRENT_VERSION = V3_0::CURRENT_VERSION
|
|
25
|
-
|
|
26
|
-
#
|
|
27
|
-
# Mapping of OpenAPI versions to their generator classes
|
|
28
|
-
#
|
|
29
|
-
# Used for version selection when generating OpenAPI documentation.
|
|
30
|
-
# Keys are SemVersion objects, values are generator classes.
|
|
31
|
-
#
|
|
32
|
-
# @api private
|
|
33
|
-
#
|
|
34
|
-
VERSIONS = {
|
|
35
|
-
V3_0.to_sem_version => V3_0
|
|
36
|
-
}.freeze
|
|
37
|
-
|
|
38
|
-
#
|
|
39
|
-
# Selects an OpenAPI generator by version
|
|
40
|
-
#
|
|
41
|
-
# @param version [String] OpenAPI version (e.g., "3.0")
|
|
42
|
-
#
|
|
43
|
-
# @return [Class] The appropriate generator class
|
|
44
|
-
# @raise [ArgumentError] If the version is not supported
|
|
45
|
-
#
|
|
46
|
-
def self.[](version)
|
|
47
|
-
version = SemVersion.from_loose_version(version)
|
|
48
|
-
generator = VERSIONS.value_where { |k, _v| k.satisfies?("~> #{version}") }
|
|
49
|
-
|
|
50
|
-
if generator.nil?
|
|
51
|
-
raise ArgumentError, "Invalid OpenAPI version provided: #{version.to_s.in_quotes}"
|
|
52
|
-
end
|
|
53
|
-
|
|
54
|
-
generator
|
|
55
|
-
end
|
|
56
|
-
end
|
|
57
|
-
end
|
|
58
|
-
end
|
|
59
|
-
end
|
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module SpecForge
|
|
4
|
-
module Documentation
|
|
5
|
-
#
|
|
6
|
-
# Documentation rendering functionality
|
|
7
|
-
#
|
|
8
|
-
# Contains generator classes for transforming SpecForge documents
|
|
9
|
-
# into various output formats like OpenAPI specifications.
|
|
10
|
-
#
|
|
11
|
-
module Generators
|
|
12
|
-
end
|
|
13
|
-
end
|
|
14
|
-
end
|
|
15
|
-
|
|
16
|
-
require_relative "generators/base"
|
|
17
|
-
require_relative "generators/openapi"
|
|
@@ -1,159 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require_relative "loader/cache"
|
|
4
|
-
|
|
5
|
-
module SpecForge
|
|
6
|
-
module Documentation
|
|
7
|
-
#
|
|
8
|
-
# Extracts API documentation data from SpecForge tests
|
|
9
|
-
#
|
|
10
|
-
# This class runs all tests and captures successful test contexts
|
|
11
|
-
# to extract endpoint information, including request/response data.
|
|
12
|
-
#
|
|
13
|
-
# @example Extracting documentation data
|
|
14
|
-
# endpoints = Loader.extract_from_tests
|
|
15
|
-
#
|
|
16
|
-
class Loader
|
|
17
|
-
#
|
|
18
|
-
# Loads a documentation document with optional caching
|
|
19
|
-
#
|
|
20
|
-
# Extracts endpoint data from SpecForge tests, either from cache (if valid
|
|
21
|
-
# and requested) or by running fresh tests. Returns a structured document
|
|
22
|
-
# ready for generator consumption.
|
|
23
|
-
#
|
|
24
|
-
# @param use_cache [Boolean] Whether to use cached data if available
|
|
25
|
-
#
|
|
26
|
-
# @return [Document] Structured document containing endpoint data
|
|
27
|
-
#
|
|
28
|
-
def self.load_document(use_cache: false)
|
|
29
|
-
cache = Cache.new
|
|
30
|
-
|
|
31
|
-
endpoints =
|
|
32
|
-
if use_cache && cache.valid?
|
|
33
|
-
puts "Loading from cache..."
|
|
34
|
-
cache.read
|
|
35
|
-
else
|
|
36
|
-
puts "Cache invalid - Regenerating..." if use_cache
|
|
37
|
-
|
|
38
|
-
endpoints = extract_from_tests
|
|
39
|
-
cache.create(endpoints)
|
|
40
|
-
|
|
41
|
-
endpoints
|
|
42
|
-
end
|
|
43
|
-
|
|
44
|
-
Builder.document_from_endpoints(endpoints)
|
|
45
|
-
end
|
|
46
|
-
|
|
47
|
-
#
|
|
48
|
-
# Runs tests and extracts endpoint data
|
|
49
|
-
#
|
|
50
|
-
# @return [Array<Hash>] Extracted endpoint data from successful tests
|
|
51
|
-
#
|
|
52
|
-
def self.extract_from_tests
|
|
53
|
-
new
|
|
54
|
-
.run_tests
|
|
55
|
-
.extract_and_normalize_data
|
|
56
|
-
end
|
|
57
|
-
|
|
58
|
-
#
|
|
59
|
-
# Initializes a new loader
|
|
60
|
-
#
|
|
61
|
-
# Sets up a unique callback and prepares storage for successful test results
|
|
62
|
-
#
|
|
63
|
-
# @return [Loader] A new loader instance
|
|
64
|
-
#
|
|
65
|
-
def initialize
|
|
66
|
-
@callback_name = "__sf_docs_#{SpecForge.generate_id(self)}"
|
|
67
|
-
@successes = []
|
|
68
|
-
end
|
|
69
|
-
|
|
70
|
-
#
|
|
71
|
-
# Runs all tests and captures successful test results
|
|
72
|
-
#
|
|
73
|
-
# Registers a callback to capture test context and runs all tests
|
|
74
|
-
#
|
|
75
|
-
# @return [self] Returns self for method chaining
|
|
76
|
-
#
|
|
77
|
-
def run_tests
|
|
78
|
-
@successes.clear
|
|
79
|
-
|
|
80
|
-
Callbacks.register(@callback_name) do |context|
|
|
81
|
-
next if context.expectation.documentation == false || context.spec.documentation == false
|
|
82
|
-
|
|
83
|
-
@successes << context if context.example.execution_result.status == :passed
|
|
84
|
-
end
|
|
85
|
-
|
|
86
|
-
forges = prepare_forges
|
|
87
|
-
|
|
88
|
-
Runner.run(forges, exit_on_finish: false, exit_on_failure: true)
|
|
89
|
-
|
|
90
|
-
self
|
|
91
|
-
ensure
|
|
92
|
-
Callbacks.deregister(@callback_name)
|
|
93
|
-
end
|
|
94
|
-
|
|
95
|
-
#
|
|
96
|
-
# Prepares forge objects for test execution
|
|
97
|
-
#
|
|
98
|
-
# Adds the documentation callback to each forge
|
|
99
|
-
#
|
|
100
|
-
# @return [Array<Forge>] Array of prepared forge objects
|
|
101
|
-
#
|
|
102
|
-
def prepare_forges
|
|
103
|
-
forges = Runner.prepare
|
|
104
|
-
|
|
105
|
-
forges.each do |forge|
|
|
106
|
-
forge.global[:callbacks] << {after_each: @callback_name}
|
|
107
|
-
end
|
|
108
|
-
|
|
109
|
-
forges
|
|
110
|
-
end
|
|
111
|
-
|
|
112
|
-
#
|
|
113
|
-
# Extracts and normalizes endpoint data from test results
|
|
114
|
-
#
|
|
115
|
-
# @return [Array<Hash>] Normalized endpoint data
|
|
116
|
-
#
|
|
117
|
-
def extract_and_normalize_data
|
|
118
|
-
@successes.map { |d| extract_endpoint(d) }
|
|
119
|
-
end
|
|
120
|
-
|
|
121
|
-
private
|
|
122
|
-
|
|
123
|
-
def extract_endpoint(context)
|
|
124
|
-
request_hash = context.request.to_h
|
|
125
|
-
response_hash = context.response.to_hash
|
|
126
|
-
|
|
127
|
-
# Only pull the headers that the user explicitly checked for.
|
|
128
|
-
# This keeps the extra unrelated headers from being included
|
|
129
|
-
response_headers = context.expectation
|
|
130
|
-
.constraints
|
|
131
|
-
.headers
|
|
132
|
-
.keys
|
|
133
|
-
.map { |h| h.to_s.downcase }
|
|
134
|
-
|
|
135
|
-
response_headers = response_hash[:response_headers].slice(*response_headers)
|
|
136
|
-
|
|
137
|
-
{
|
|
138
|
-
# Metadata
|
|
139
|
-
spec_name: context.spec.name,
|
|
140
|
-
expectation_name: context.expectation.name,
|
|
141
|
-
|
|
142
|
-
# Request data
|
|
143
|
-
base_url: request_hash[:base_url],
|
|
144
|
-
url: request_hash[:url],
|
|
145
|
-
http_verb: request_hash[:http_verb],
|
|
146
|
-
content_type: request_hash[:content_type],
|
|
147
|
-
request_body: request_hash[:body],
|
|
148
|
-
request_headers: request_hash[:headers],
|
|
149
|
-
request_query: request_hash[:query],
|
|
150
|
-
|
|
151
|
-
# Response data
|
|
152
|
-
response_status: response_hash[:status],
|
|
153
|
-
response_body: response_hash[:body],
|
|
154
|
-
response_headers:
|
|
155
|
-
}
|
|
156
|
-
end
|
|
157
|
-
end
|
|
158
|
-
end
|
|
159
|
-
end
|
|
@@ -1,33 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module SpecForge
|
|
4
|
-
module Documentation
|
|
5
|
-
module OpenAPI
|
|
6
|
-
#
|
|
7
|
-
# Base class for OpenAPI documentation objects
|
|
8
|
-
#
|
|
9
|
-
# Provides common functionality for OpenAPI specification objects
|
|
10
|
-
# like operations, responses, and schemas.
|
|
11
|
-
#
|
|
12
|
-
class Base
|
|
13
|
-
#
|
|
14
|
-
# The document object containing structured API data
|
|
15
|
-
#
|
|
16
|
-
# @return [Object] The document with endpoint information
|
|
17
|
-
#
|
|
18
|
-
attr_reader :document
|
|
19
|
-
|
|
20
|
-
#
|
|
21
|
-
# Creates a new OpenAPI base object
|
|
22
|
-
#
|
|
23
|
-
# @param document [Object] The document object containing API data
|
|
24
|
-
#
|
|
25
|
-
# @return [Base] A new base instance
|
|
26
|
-
#
|
|
27
|
-
def initialize(document)
|
|
28
|
-
@document = document
|
|
29
|
-
end
|
|
30
|
-
end
|
|
31
|
-
end
|
|
32
|
-
end
|
|
33
|
-
end
|
data/lib/spec_forge/filter.rb
DELETED
|
@@ -1,86 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module SpecForge
|
|
4
|
-
#
|
|
5
|
-
# Provides filtering capabilities for test suites based on different criteria
|
|
6
|
-
#
|
|
7
|
-
# The Filter class allows running specific tests by filtering forges, specs,
|
|
8
|
-
# and expectations based on file name, spec name, and expectation name.
|
|
9
|
-
#
|
|
10
|
-
# @example Filtering specs by name
|
|
11
|
-
# forges = Loader.load_from_files
|
|
12
|
-
# filtered = Filter.apply(forges, file_name: "users", spec_name: "create_user")
|
|
13
|
-
#
|
|
14
|
-
class Filter
|
|
15
|
-
class << self
|
|
16
|
-
#
|
|
17
|
-
# Prints out a message if any of the filters were used
|
|
18
|
-
#
|
|
19
|
-
# @param forges [Array<Forge>] The collection of forges that was filtered
|
|
20
|
-
# @param file_name [String, nil] Optional file name that was used by the filter
|
|
21
|
-
# @param spec_name [String, nil] Optional spec name that was used by the filter
|
|
22
|
-
# @param expectation_name [String, nil] Optional expectation name that was used by the filter
|
|
23
|
-
#
|
|
24
|
-
def announce(forges, file_name:, spec_name:, expectation_name:)
|
|
25
|
-
filters = {file_name:, spec_name:, expectation_name:}.compact_blank
|
|
26
|
-
return if filters.size == 0
|
|
27
|
-
|
|
28
|
-
filters_display = filters.join_map(", ") { |k, v| "#{k.in_quotes} => #{v.in_quotes}" }
|
|
29
|
-
|
|
30
|
-
expectation_count = forges.sum do |forge|
|
|
31
|
-
forge.specs.sum { |spec| spec.expectations.size }
|
|
32
|
-
end
|
|
33
|
-
|
|
34
|
-
puts "Applied filter #{filters_display}"
|
|
35
|
-
puts "Found #{expectation_count} #{"expectation".pluralize(expectation_count)}"
|
|
36
|
-
end
|
|
37
|
-
|
|
38
|
-
#
|
|
39
|
-
# Filters a collection of forges based on specified criteria
|
|
40
|
-
#
|
|
41
|
-
# This method allows running specific tests by filtering forges, specs,
|
|
42
|
-
# and expectations based on file name, spec name, and expectation name.
|
|
43
|
-
# It returns only the forges, specs, and expectations that match the criteria.
|
|
44
|
-
#
|
|
45
|
-
# @param forges [Array<Forge>] The collection of forges to filter
|
|
46
|
-
# @param file_name [String, nil] Optional file name to filter by
|
|
47
|
-
# @param spec_name [String, nil] Optional spec name to filter by
|
|
48
|
-
# @param expectation_name [String, nil] Optional expectation name to filter by
|
|
49
|
-
#
|
|
50
|
-
# @return [Array<Forge>] The filtered collection of forges
|
|
51
|
-
#
|
|
52
|
-
# @raise [ArgumentError] If filtering parameters are provided in an invalid combination
|
|
53
|
-
#
|
|
54
|
-
def apply(forges, file_name: nil, spec_name: nil, expectation_name: nil)
|
|
55
|
-
# Guard against invalid partial filters
|
|
56
|
-
if expectation_name && spec_name.blank?
|
|
57
|
-
raise ArgumentError, "The spec's name is required when filtering by an expectation's name"
|
|
58
|
-
end
|
|
59
|
-
|
|
60
|
-
if spec_name && file_name.blank?
|
|
61
|
-
raise ArgumentError, "The spec's filename is required when filtering by a spec's name"
|
|
62
|
-
end
|
|
63
|
-
|
|
64
|
-
forges.filter_map do |forge|
|
|
65
|
-
specs = forge.specs.filter_map do |spec|
|
|
66
|
-
next if file_name && spec.file_name != file_name # File filter
|
|
67
|
-
next if spec_name && spec.name != spec_name # Name filter
|
|
68
|
-
|
|
69
|
-
# Expectation filter
|
|
70
|
-
next spec unless expectation_name
|
|
71
|
-
|
|
72
|
-
spec.expectations.select! { |e| e.name == expectation_name }
|
|
73
|
-
next if spec.expectations.empty?
|
|
74
|
-
|
|
75
|
-
spec
|
|
76
|
-
end
|
|
77
|
-
|
|
78
|
-
next if specs.empty?
|
|
79
|
-
|
|
80
|
-
forge.specs = specs
|
|
81
|
-
forge
|
|
82
|
-
end
|
|
83
|
-
end
|
|
84
|
-
end
|
|
85
|
-
end
|
|
86
|
-
end
|
|
@@ -1,248 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module SpecForge
|
|
4
|
-
class Normalizer
|
|
5
|
-
#
|
|
6
|
-
# Manages structure definitions for the Normalizer
|
|
7
|
-
#
|
|
8
|
-
# Handles loading structure definitions from YAML files, processing references
|
|
9
|
-
# between structures, and normalizing structure formats for consistent validation.
|
|
10
|
-
#
|
|
11
|
-
# @example Loading all structure definitions
|
|
12
|
-
# structures = SpecForge::Normalizer::Definition.from_files
|
|
13
|
-
#
|
|
14
|
-
class Definition
|
|
15
|
-
#
|
|
16
|
-
# Mapping of structure names to their human-readable labels
|
|
17
|
-
#
|
|
18
|
-
# @return [Hash<Symbol, String>]
|
|
19
|
-
#
|
|
20
|
-
LABELS = {
|
|
21
|
-
factory_reference: "factory reference",
|
|
22
|
-
global_context: "global context"
|
|
23
|
-
}.freeze
|
|
24
|
-
|
|
25
|
-
#
|
|
26
|
-
# Core structure definition used to validate other structures
|
|
27
|
-
#
|
|
28
|
-
# Defines the valid attributes and types for structure definitions,
|
|
29
|
-
# creating a meta-structure that validates other structure definitions.
|
|
30
|
-
#
|
|
31
|
-
# @return [Hash]
|
|
32
|
-
#
|
|
33
|
-
STRUCTURE = {
|
|
34
|
-
type: {
|
|
35
|
-
type: [String, Array, Class],
|
|
36
|
-
default: nil,
|
|
37
|
-
validator: :present?
|
|
38
|
-
},
|
|
39
|
-
default: {
|
|
40
|
-
type: [String, NilClass, Numeric, Array, Hash, TrueClass, FalseClass],
|
|
41
|
-
required: false
|
|
42
|
-
},
|
|
43
|
-
required: {
|
|
44
|
-
type: [TrueClass, FalseClass],
|
|
45
|
-
required: false
|
|
46
|
-
},
|
|
47
|
-
aliases: {
|
|
48
|
-
type: Array,
|
|
49
|
-
required: false,
|
|
50
|
-
structure: {type: String}
|
|
51
|
-
},
|
|
52
|
-
structure: {
|
|
53
|
-
type: Hash,
|
|
54
|
-
required: false
|
|
55
|
-
},
|
|
56
|
-
validator: {
|
|
57
|
-
type: String,
|
|
58
|
-
required: false
|
|
59
|
-
}
|
|
60
|
-
}.freeze
|
|
61
|
-
|
|
62
|
-
#
|
|
63
|
-
# Loads normalizer definitions from YAML files
|
|
64
|
-
#
|
|
65
|
-
# Reads all YAML files in the normalizers directory, processes shared
|
|
66
|
-
# references, and prepares them for use by the Normalizer.
|
|
67
|
-
#
|
|
68
|
-
# @return [Hash] A hash mapping structure names to their definitions
|
|
69
|
-
#
|
|
70
|
-
def self.from_files
|
|
71
|
-
base_path = Pathname.new(File.expand_path("../normalizers", __dir__))
|
|
72
|
-
paths = Dir[base_path.join("**/*.yml")].sort
|
|
73
|
-
|
|
74
|
-
normalizers =
|
|
75
|
-
paths.each_with_object({}) do |path, hash|
|
|
76
|
-
path = Pathname.new(path)
|
|
77
|
-
|
|
78
|
-
# Include the directory name in the path to include normalizers in directories
|
|
79
|
-
name = path.relative_path_from(base_path).to_s.delete_suffix(".yml").to_sym
|
|
80
|
-
|
|
81
|
-
input = YAML.safe_load_file(path, symbolize_names: true)
|
|
82
|
-
raise Error, "Normalizer defined at #{path.to_s.in_quotes} is empty" if input.blank?
|
|
83
|
-
|
|
84
|
-
hash[name] = new(input, label: LABELS[name] || name.to_s.humanize.downcase)
|
|
85
|
-
end
|
|
86
|
-
|
|
87
|
-
# Pull the shared structures and prepare it
|
|
88
|
-
structures = normalizers.delete(:_shared).normalize
|
|
89
|
-
|
|
90
|
-
# Merge in the normalizers to allow referencing other normalizers
|
|
91
|
-
structures.merge!(normalizers.transform_values(&:input))
|
|
92
|
-
|
|
93
|
-
# Now prepare all of the other definitions with access to references
|
|
94
|
-
normalizers.transform_values!(with_key: true) do |definition, name|
|
|
95
|
-
structure = definition.normalize(structures)
|
|
96
|
-
|
|
97
|
-
{
|
|
98
|
-
label: definition.label,
|
|
99
|
-
structure:
|
|
100
|
-
}
|
|
101
|
-
end
|
|
102
|
-
|
|
103
|
-
normalizers
|
|
104
|
-
end
|
|
105
|
-
|
|
106
|
-
##########################################################################
|
|
107
|
-
|
|
108
|
-
attr_reader :input, :label
|
|
109
|
-
|
|
110
|
-
def initialize(input, label: "")
|
|
111
|
-
@input = input
|
|
112
|
-
@label = label
|
|
113
|
-
end
|
|
114
|
-
|
|
115
|
-
#
|
|
116
|
-
# Normalizes a structure definition
|
|
117
|
-
#
|
|
118
|
-
# Processes references, resolves types, and ensures all attributes
|
|
119
|
-
# have a consistent format for validation.
|
|
120
|
-
#
|
|
121
|
-
# @param shared_structures [Hash] Optional shared structures for resolving references
|
|
122
|
-
#
|
|
123
|
-
# @return [Hash] The normalized structure definition
|
|
124
|
-
#
|
|
125
|
-
def normalize(shared_structures = {})
|
|
126
|
-
hash = @input.deep_dup
|
|
127
|
-
|
|
128
|
-
# First, we'll deeply replace any references
|
|
129
|
-
replace_references(hash, shared_structures)
|
|
130
|
-
|
|
131
|
-
# Second, normalize the root level keys
|
|
132
|
-
hash.transform_values!(with_key: true) do |attribute, name|
|
|
133
|
-
next if STRUCTURE.key?(name)
|
|
134
|
-
|
|
135
|
-
normalize_attribute(name, attribute)
|
|
136
|
-
end
|
|
137
|
-
|
|
138
|
-
# Third, normalize the underlying structures
|
|
139
|
-
hash.each do |name, attribute|
|
|
140
|
-
next unless attribute.is_a?(Hash)
|
|
141
|
-
|
|
142
|
-
structure = attribute[:structure]
|
|
143
|
-
next if structure.blank?
|
|
144
|
-
|
|
145
|
-
attribute[:structure] = normalize_structure(name, attribute)
|
|
146
|
-
end
|
|
147
|
-
|
|
148
|
-
hash
|
|
149
|
-
end
|
|
150
|
-
|
|
151
|
-
private
|
|
152
|
-
|
|
153
|
-
def replace_references(attributes, shared_structures)
|
|
154
|
-
return if shared_structures.blank?
|
|
155
|
-
|
|
156
|
-
# The goal is to walk down the hash and recursively replace any references
|
|
157
|
-
attributes.each do |attribute_name, attribute|
|
|
158
|
-
# Replace the top level reference
|
|
159
|
-
replace_with_reference(attribute_name, attribute, shared_structures:)
|
|
160
|
-
next unless attribute.is_a?(Hash) && attribute[:structure].present?
|
|
161
|
-
|
|
162
|
-
# Allow structures to reference other structures
|
|
163
|
-
if attribute.dig(:structure, :reference)
|
|
164
|
-
replace_with_reference(
|
|
165
|
-
"#{attribute_name}'s structure",
|
|
166
|
-
attribute[:structure],
|
|
167
|
-
shared_structures:
|
|
168
|
-
)
|
|
169
|
-
end
|
|
170
|
-
|
|
171
|
-
# Recursively replace any structures that have references
|
|
172
|
-
if [Array, "array"].include?(attribute[:type])
|
|
173
|
-
result = replace_references(attribute.slice(:structure), shared_structures)
|
|
174
|
-
attribute.merge!(result)
|
|
175
|
-
elsif [Hash, "hash"].include?(attribute[:type])
|
|
176
|
-
replace_references(attribute[:structure], shared_structures)
|
|
177
|
-
end
|
|
178
|
-
end
|
|
179
|
-
end
|
|
180
|
-
|
|
181
|
-
def replace_with_reference(attribute_name, attribute, shared_structures: {})
|
|
182
|
-
return unless attribute.is_a?(Hash) && attribute.key?(:reference)
|
|
183
|
-
|
|
184
|
-
reference_name = attribute.delete(:reference)
|
|
185
|
-
reference = shared_structures[reference_name.to_sym]
|
|
186
|
-
|
|
187
|
-
if reference.nil?
|
|
188
|
-
structures_names = shared_structures.keys.map(&:in_quotes).to_or_sentence
|
|
189
|
-
|
|
190
|
-
raise Error, "Attribute #{attribute_name.in_quotes}: Invalid reference name. Got #{reference_name&.in_quotes}, expected one of #{structures_names} in #{@label}"
|
|
191
|
-
end
|
|
192
|
-
|
|
193
|
-
# Allows overwriting data on the reference
|
|
194
|
-
attribute.reverse_merge!(reference)
|
|
195
|
-
end
|
|
196
|
-
|
|
197
|
-
def normalize_attribute(attribute_name, attribute)
|
|
198
|
-
case attribute
|
|
199
|
-
when String, Array # Array is multiple types
|
|
200
|
-
hash = {type: resolve_type(attribute)}
|
|
201
|
-
|
|
202
|
-
default = Normalizer.default(structure: STRUCTURE)
|
|
203
|
-
hash.merge!(default)
|
|
204
|
-
when Hash
|
|
205
|
-
hash = Normalizer.raise_errors! do
|
|
206
|
-
Normalizer.new(
|
|
207
|
-
"#{attribute_name.in_quotes} in #{@label}",
|
|
208
|
-
attribute,
|
|
209
|
-
structure: STRUCTURE
|
|
210
|
-
).normalize
|
|
211
|
-
end
|
|
212
|
-
|
|
213
|
-
hash[:type] = resolve_type(attribute[:type])
|
|
214
|
-
|
|
215
|
-
if hash[:structure].present?
|
|
216
|
-
hash[:structure] = normalize_structure(attribute_name, hash) || {}
|
|
217
|
-
end
|
|
218
|
-
|
|
219
|
-
hash
|
|
220
|
-
else
|
|
221
|
-
raise ArgumentError, "Attribute #{attribute_name.in_quotes}: Expected String or Hash, got #{attribute.inspect}"
|
|
222
|
-
end
|
|
223
|
-
end
|
|
224
|
-
|
|
225
|
-
def normalize_structure(name, hash)
|
|
226
|
-
if hash[:type] == Array
|
|
227
|
-
normalize_attribute(name, hash[:structure])
|
|
228
|
-
elsif hash[:type] == Hash
|
|
229
|
-
hash[:structure].transform_values(with_key: true) { |v, k| normalize_attribute(k, v) }
|
|
230
|
-
end
|
|
231
|
-
end
|
|
232
|
-
|
|
233
|
-
def resolve_type(type)
|
|
234
|
-
if type == "boolean"
|
|
235
|
-
[TrueClass, FalseClass]
|
|
236
|
-
elsif type.instance_of?(Array)
|
|
237
|
-
type.map { |t| resolve_type(t) }
|
|
238
|
-
elsif type.is_a?(String)
|
|
239
|
-
type.classify.constantize
|
|
240
|
-
else
|
|
241
|
-
type
|
|
242
|
-
end
|
|
243
|
-
rescue NameError => e
|
|
244
|
-
raise Error, "#{e}. #{type.inspect} is not a valid type found in #{@label}"
|
|
245
|
-
end
|
|
246
|
-
end
|
|
247
|
-
end
|
|
248
|
-
end
|