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,131 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module SpecForge
|
4
|
+
class Context
|
5
|
+
#
|
6
|
+
# Manages storage of API responses for use in subsequent tests
|
7
|
+
#
|
8
|
+
# This class provides a mechanism to store HTTP requests and responses
|
9
|
+
# during test execution, allowing values to be referenced in later tests
|
10
|
+
# through the `store.id.body.attribute` syntax.
|
11
|
+
#
|
12
|
+
# @example Storing and retrieving a response in specs
|
13
|
+
# # In one expectation:
|
14
|
+
# store_as: user_creation
|
15
|
+
#
|
16
|
+
# # In a later test:
|
17
|
+
# query:
|
18
|
+
# id: store.user_creation.body.id
|
19
|
+
#
|
20
|
+
class Store
|
21
|
+
#
|
22
|
+
# Represents a stored entry containing arbitrary data from test execution
|
23
|
+
#
|
24
|
+
# Entries are created during test execution to store custom data that can be
|
25
|
+
# accessed in subsequent tests. Unlike the original rigid Data structure, this
|
26
|
+
# OpenStruct-based approach allows storing any key-value pairs, making it perfect
|
27
|
+
# for complex test scenarios that need custom configuration, metadata, or
|
28
|
+
# computed values.
|
29
|
+
#
|
30
|
+
# @example Storing custom configuration data
|
31
|
+
# SpecForge.context.store.set(
|
32
|
+
# "app_config",
|
33
|
+
# api_version: "v2.1",
|
34
|
+
# feature_flags: { advanced_search: true }
|
35
|
+
# )
|
36
|
+
#
|
37
|
+
# @example Accessing stored data in tests
|
38
|
+
# headers:
|
39
|
+
# X-API-Version: store.app_config.api_version
|
40
|
+
# query:
|
41
|
+
# search_enabled: store.app_config.feature_flags.advanced_search
|
42
|
+
#
|
43
|
+
class Entry < OpenStruct
|
44
|
+
#
|
45
|
+
# Creates a new store entry
|
46
|
+
#
|
47
|
+
# @param scope [Symbol] Scope of this entry, either :file or :spec
|
48
|
+
#
|
49
|
+
# @return [Entry] A new entry instance
|
50
|
+
#
|
51
|
+
def initialize(scope: :file, **)
|
52
|
+
super
|
53
|
+
end
|
54
|
+
|
55
|
+
#
|
56
|
+
# Returns all available methods that can be called
|
57
|
+
#
|
58
|
+
# @return [Array] The method names
|
59
|
+
#
|
60
|
+
def available_methods
|
61
|
+
@table.keys
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
#
|
66
|
+
# Creates a new empty store
|
67
|
+
#
|
68
|
+
# @return [Store] A new store instance
|
69
|
+
#
|
70
|
+
def initialize
|
71
|
+
@inner = {}
|
72
|
+
end
|
73
|
+
|
74
|
+
#
|
75
|
+
# Retrieves a stored entry by ID
|
76
|
+
#
|
77
|
+
# @param id [String, Symbol] The identifier for the stored entry
|
78
|
+
#
|
79
|
+
# @return [Entry, nil] The stored entry or nil if not found
|
80
|
+
#
|
81
|
+
def [](id)
|
82
|
+
@inner[id]
|
83
|
+
end
|
84
|
+
|
85
|
+
#
|
86
|
+
# Returns the number of entries in the store
|
87
|
+
#
|
88
|
+
# @return [Integer] The count of stored entries
|
89
|
+
#
|
90
|
+
def size
|
91
|
+
@inner.size
|
92
|
+
end
|
93
|
+
|
94
|
+
#
|
95
|
+
# Stores an entry with the specified ID
|
96
|
+
#
|
97
|
+
# @param id [String, Symbol] The identifier to store the entry under
|
98
|
+
#
|
99
|
+
# @return [self]
|
100
|
+
#
|
101
|
+
def set(id, **)
|
102
|
+
@inner[id] = Entry.new(**)
|
103
|
+
|
104
|
+
self
|
105
|
+
end
|
106
|
+
|
107
|
+
#
|
108
|
+
# Removes all entries from the store
|
109
|
+
#
|
110
|
+
def clear
|
111
|
+
@inner.clear
|
112
|
+
end
|
113
|
+
|
114
|
+
#
|
115
|
+
# Removes all spec entries from the store
|
116
|
+
#
|
117
|
+
def clear_specs
|
118
|
+
@inner.delete_if { |_k, v| v.scope == :spec }
|
119
|
+
end
|
120
|
+
|
121
|
+
#
|
122
|
+
# Returns a hash representation of store
|
123
|
+
#
|
124
|
+
# @return [Hash]
|
125
|
+
#
|
126
|
+
def to_h
|
127
|
+
@inner.transform_values(&:to_h).deep_stringify_keys
|
128
|
+
end
|
129
|
+
end
|
130
|
+
end
|
131
|
+
end
|
@@ -0,0 +1,91 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module SpecForge
|
4
|
+
class Context
|
5
|
+
#
|
6
|
+
# Manages variable resolution across different expectations in SpecForge tests.
|
7
|
+
#
|
8
|
+
# The Variables class handles two layers of variable definitions:
|
9
|
+
# - Base variables: The core set of variables defined at the spec level
|
10
|
+
# - Overlay variables: Additional variables defined at the expectation level
|
11
|
+
# that can override base variables with the same name.
|
12
|
+
#
|
13
|
+
# @example Basic usage
|
14
|
+
# variables = Variables.new(
|
15
|
+
# base: {user_id: 123, name: "Test User"},
|
16
|
+
# overlay: {
|
17
|
+
# "expectation_1": {name: "Override User"}
|
18
|
+
# }
|
19
|
+
# )
|
20
|
+
#
|
21
|
+
# variables[:user_id] #=> 123
|
22
|
+
# variables[:name] #=> "Test User"
|
23
|
+
#
|
24
|
+
# variables.use_overlay("expectation_1")
|
25
|
+
# variables[:name] #=> "Override User"
|
26
|
+
# variables[:user_id] #=> 123 (unchanged)
|
27
|
+
#
|
28
|
+
class Variables < Hash
|
29
|
+
attr_reader :base, :overlay
|
30
|
+
|
31
|
+
#
|
32
|
+
# Creates a new Variables container with base and overlay definitions
|
33
|
+
#
|
34
|
+
# @param base [Hash] The base set of variables (typically defined at spec level)
|
35
|
+
# @param overlay [Hash<String, Hash>] A hash of overlay variable sets keyed by ID
|
36
|
+
#
|
37
|
+
# @return [Variables]
|
38
|
+
#
|
39
|
+
def initialize(base: {}, overlay: {})
|
40
|
+
set(base:, overlay:)
|
41
|
+
end
|
42
|
+
|
43
|
+
#
|
44
|
+
# Sets the base and overlay variable hashes
|
45
|
+
#
|
46
|
+
# @param base [Hash] The new base variable hash
|
47
|
+
# @param overlay [Hash<String, Hash>] The new overlay variable hashes
|
48
|
+
#
|
49
|
+
# @return [self]
|
50
|
+
#
|
51
|
+
def set(base:, overlay: {})
|
52
|
+
@base = Attribute.from(base)
|
53
|
+
@overlay = overlay
|
54
|
+
|
55
|
+
resolve_into_self(@base)
|
56
|
+
self
|
57
|
+
end
|
58
|
+
|
59
|
+
#
|
60
|
+
# Applies a specific overlay to the base variables
|
61
|
+
# If the overlay doesn't exist or is empty, no changes are made.
|
62
|
+
#
|
63
|
+
# @param id [String] The ID of the overlay to apply
|
64
|
+
#
|
65
|
+
# @return [nil]
|
66
|
+
#
|
67
|
+
def use_overlay(id)
|
68
|
+
active = @base
|
69
|
+
|
70
|
+
if (overlay = @overlay[id]) && overlay.present?
|
71
|
+
active = active.deep_merge(overlay)
|
72
|
+
end
|
73
|
+
|
74
|
+
resolve_into_self(active)
|
75
|
+
self
|
76
|
+
end
|
77
|
+
|
78
|
+
private
|
79
|
+
|
80
|
+
def resolve_into_self(hash)
|
81
|
+
# Start fresh
|
82
|
+
clear
|
83
|
+
|
84
|
+
# Load the resolved values into self
|
85
|
+
hash.each do |key, value|
|
86
|
+
self[key] = Attribute.from(value).resolved
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module SpecForge
|
4
|
+
#
|
5
|
+
# Core data structure that maintains context during test execution
|
6
|
+
#
|
7
|
+
# Context stores and provides access to global variables, test variables, and
|
8
|
+
# shared state across specs.
|
9
|
+
# It acts as a central repository for test data during execution.
|
10
|
+
#
|
11
|
+
# @example Accessing the current context
|
12
|
+
# SpecForge.context.variables[:user_id] #=> 123
|
13
|
+
#
|
14
|
+
class Context < Data.define(:global, :store, :variables)
|
15
|
+
#
|
16
|
+
# Creates a new context with default values
|
17
|
+
#
|
18
|
+
# @param global [Hash] Global variables shared across all specs
|
19
|
+
# @param variables [Hash] Test variables specific to the current context
|
20
|
+
#
|
21
|
+
# @return [Context] A new context instance
|
22
|
+
#
|
23
|
+
def initialize(global: {}, variables: {})
|
24
|
+
super(
|
25
|
+
global: Global.new(**global),
|
26
|
+
store: Store.new,
|
27
|
+
variables: Variables.new(**variables)
|
28
|
+
)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
require_relative "context/callbacks"
|
34
|
+
require_relative "context/global"
|
35
|
+
require_relative "context/store"
|
36
|
+
require_relative "context/variables"
|
@@ -0,0 +1,27 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
#
|
4
|
+
# Extensions to Ruby's Array class for SpecForge functionality
|
5
|
+
#
|
6
|
+
# Adds utility methods used throughout SpecForge for array manipulation
|
7
|
+
# and data processing.
|
8
|
+
#
|
9
|
+
class Array
|
10
|
+
#
|
11
|
+
# Merges an array of hashes into a single hash
|
12
|
+
#
|
13
|
+
# Performs a deep merge on each hash in the array, combining them
|
14
|
+
# into a single hash with all keys and values.
|
15
|
+
#
|
16
|
+
# @return [Hash] A hash containing the merged contents of all hashes in the array
|
17
|
+
#
|
18
|
+
# @example Merging an array of hashes
|
19
|
+
# [{a: 1}, {b: 2}, {a: 3}].to_merged_h
|
20
|
+
# # => {a: 3, b: 2}
|
21
|
+
#
|
22
|
+
def to_merged_h
|
23
|
+
each_with_object({}) do |hash, output|
|
24
|
+
output.deep_merge!(hash)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -2,19 +2,37 @@
|
|
2
2
|
|
3
3
|
return if defined?(SPEC_FORGE_INTERNAL_TESTING)
|
4
4
|
|
5
|
+
#
|
6
|
+
# RSpec's core testing framework module
|
7
|
+
# Provides the fundamental structure and functionality for RSpec tests
|
8
|
+
#
|
5
9
|
module RSpec
|
10
|
+
#
|
11
|
+
# Core implementation details and extensions for RSpec
|
12
|
+
# Contains the fundamental building blocks of the RSpec testing framework
|
13
|
+
#
|
6
14
|
module Core
|
15
|
+
#
|
16
|
+
# Handles notifications and reporting for RSpec test runs
|
17
|
+
# Manages how test results and metadata are processed and communicated
|
18
|
+
#
|
7
19
|
module Notifications
|
8
20
|
#
|
9
|
-
#
|
10
|
-
#
|
21
|
+
# A monkey patch of an internal RSpec class to allow SpecForge to replace parts of
|
22
|
+
# RSpec's reporting output in order to provide useful feedback to the user.
|
23
|
+
# This replaces "rspec" in commands with "spec_forge", removes any line numbers, and
|
24
|
+
# ensures that failures properly report the YAML file that it occurred in.
|
11
25
|
#
|
12
26
|
class SummaryNotification
|
27
|
+
#
|
28
|
+
# Create an alias to RSpec original colorized_rerun_commands so it can be called at a
|
29
|
+
# later point.
|
30
|
+
#
|
31
|
+
alias_method :og_colorized_rerun_commands, :colorized_rerun_commands
|
32
|
+
|
13
33
|
# Customizes RSpec's failure output to:
|
14
34
|
# 1. Use 'spec_forge' instead of 'rspec' for rerun commands
|
15
35
|
# 2. Remove line numbers since SpecForge uses dynamic spec generation
|
16
|
-
alias_method :og_colorized_rerun_commands, :colorized_rerun_commands
|
17
|
-
|
18
36
|
def colorized_rerun_commands(colorizer)
|
19
37
|
# Updating these at this point fixes the re-run for some failures - it depends
|
20
38
|
failed_examples.each do |example|
|
@@ -0,0 +1,383 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module SpecForge
|
4
|
+
module Documentation
|
5
|
+
#
|
6
|
+
# Transforms extracted test data into a structured document
|
7
|
+
#
|
8
|
+
# This class processes raw endpoint data from tests into a hierarchical document
|
9
|
+
# structure suitable for rendering as API documentation.
|
10
|
+
#
|
11
|
+
# @example Creating a document from test data
|
12
|
+
# document = Builder.document_from_endpoints(endpoints)
|
13
|
+
#
|
14
|
+
class Builder
|
15
|
+
# Source: https://gist.github.com/johnelliott/cf77003f72f889abbc3f32785fa3df8d
|
16
|
+
UUID_REGEX = /^[0-9A-F]{8}-[0-9A-F]{4}-4[0-9A-F]{3}-[89AB][0-9A-F]{3}-[0-9A-F]{12}$/i
|
17
|
+
|
18
|
+
#
|
19
|
+
# Regular expression for matching floating point numbers in strings
|
20
|
+
#
|
21
|
+
# Matches decimal numbers with optional negative sign, used for type detection
|
22
|
+
# when analyzing API response data.
|
23
|
+
#
|
24
|
+
# @api private
|
25
|
+
#
|
26
|
+
INTEGER_REGEX = /^-?\d+$/
|
27
|
+
|
28
|
+
#
|
29
|
+
# Regular expression for matching integer numbers in strings
|
30
|
+
#
|
31
|
+
# Matches whole numbers with optional negative sign, used for type detection
|
32
|
+
# when analyzing API response data.
|
33
|
+
#
|
34
|
+
# @api private
|
35
|
+
#
|
36
|
+
FLOAT_REGEX = /^-?\d+\.\d+$/
|
37
|
+
|
38
|
+
#
|
39
|
+
# Creates a document from endpoint data
|
40
|
+
#
|
41
|
+
# @param endpoints [Array<Hash>] Array of endpoint data extracted from tests
|
42
|
+
#
|
43
|
+
# @return [Document] A structured documentation document
|
44
|
+
#
|
45
|
+
def self.document_from_endpoints(endpoints = [])
|
46
|
+
new(endpoints).export_as_document
|
47
|
+
end
|
48
|
+
|
49
|
+
#
|
50
|
+
# The processed endpoints organized by path and HTTP method
|
51
|
+
#
|
52
|
+
# Contains all endpoint data after grouping, sanitizing, merging,
|
53
|
+
# and flattening operations for document generation.
|
54
|
+
#
|
55
|
+
# @return [Hash] Processed endpoints ready for document creation
|
56
|
+
#
|
57
|
+
attr_reader :endpoints
|
58
|
+
|
59
|
+
#
|
60
|
+
# Initializes a new builder with endpoint data
|
61
|
+
#
|
62
|
+
# @param endpoints [Array<Hash>] Array of endpoint data extracted from tests
|
63
|
+
#
|
64
|
+
# @return [Builder] A new builder instance
|
65
|
+
#
|
66
|
+
def initialize(endpoints)
|
67
|
+
@endpoints = prepare_endpoints(endpoints)
|
68
|
+
end
|
69
|
+
|
70
|
+
#
|
71
|
+
# Prepares endpoint data for document creation
|
72
|
+
#
|
73
|
+
# Groups endpoints by path and HTTP method, sanitizes error responses,
|
74
|
+
# merges similar operations, and flattens the result.
|
75
|
+
#
|
76
|
+
# @param endpoints [Array<Hash>] Raw endpoint data from tests
|
77
|
+
#
|
78
|
+
# @return [Hash] Processed endpoints organized by path and method
|
79
|
+
#
|
80
|
+
def prepare_endpoints(endpoints)
|
81
|
+
# Step one, group the endpoints by their paths and verb
|
82
|
+
# { path: {get: [], post: []}, path_2: {get: []}, ... }
|
83
|
+
grouped = group_endpoints(endpoints)
|
84
|
+
|
85
|
+
grouped.each_value do |endpoint|
|
86
|
+
# Operations are those arrays
|
87
|
+
endpoint.transform_values! do |operations|
|
88
|
+
# Step two, clear data from any error (4xx, 5xx) operations
|
89
|
+
operations = sanitize_error_operations(operations)
|
90
|
+
|
91
|
+
# Step three, merge all of the operations into one single hash
|
92
|
+
operations = merge_operations(operations)
|
93
|
+
|
94
|
+
# Step four, flatten the operations into one
|
95
|
+
flatten_operations(operations)
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
#
|
101
|
+
# Exports the processed endpoints as a document
|
102
|
+
#
|
103
|
+
# @return [Document] A document containing the processed endpoints
|
104
|
+
#
|
105
|
+
def export_as_document
|
106
|
+
Document.new(endpoints:)
|
107
|
+
end
|
108
|
+
|
109
|
+
private
|
110
|
+
|
111
|
+
def determine_type(value)
|
112
|
+
case value
|
113
|
+
when true, false
|
114
|
+
"boolean"
|
115
|
+
when Float
|
116
|
+
# According to the docs: A Float object represents a sometimes-inexact real number
|
117
|
+
# using the native architecture’s double-precision floating point representation.
|
118
|
+
# So a double it is!
|
119
|
+
"double"
|
120
|
+
when Integer
|
121
|
+
"integer"
|
122
|
+
when Array
|
123
|
+
"array"
|
124
|
+
when NilClass
|
125
|
+
"null"
|
126
|
+
when DateTime, Time
|
127
|
+
"datetime"
|
128
|
+
when Date
|
129
|
+
"date"
|
130
|
+
when String, Symbol
|
131
|
+
if value.match?(UUID_REGEX)
|
132
|
+
"uuid"
|
133
|
+
elsif value.match?(INTEGER_REGEX)
|
134
|
+
"integer"
|
135
|
+
elsif value.match?(FLOAT_REGEX)
|
136
|
+
"double"
|
137
|
+
elsif value == "true" || value == "false"
|
138
|
+
"boolean"
|
139
|
+
else
|
140
|
+
"string"
|
141
|
+
end
|
142
|
+
when URI
|
143
|
+
"uri"
|
144
|
+
when Numeric
|
145
|
+
"number"
|
146
|
+
else
|
147
|
+
"object"
|
148
|
+
end
|
149
|
+
end
|
150
|
+
|
151
|
+
#
|
152
|
+
# Groups endpoints by path and HTTP method
|
153
|
+
#
|
154
|
+
# @param endpoints [Array<Hash>] Array of endpoint data
|
155
|
+
#
|
156
|
+
# @return [Hash] Endpoints grouped by path and method
|
157
|
+
#
|
158
|
+
# @private
|
159
|
+
#
|
160
|
+
def group_endpoints(endpoints)
|
161
|
+
grouped = Hash.new_nested_hash(depth: 1)
|
162
|
+
|
163
|
+
# Convert the endpoints from a flat array of objects into a hash
|
164
|
+
endpoints.each do |input|
|
165
|
+
# "/users" => {}
|
166
|
+
endpoint_hash = grouped[input[:url]]
|
167
|
+
|
168
|
+
# "GET" => []
|
169
|
+
(endpoint_hash[input[:http_verb]] ||= []) << input
|
170
|
+
end
|
171
|
+
|
172
|
+
grouped
|
173
|
+
end
|
174
|
+
|
175
|
+
#
|
176
|
+
# Sanitizes operations that represent error responses
|
177
|
+
#
|
178
|
+
# Removes request details from operations with 4xx/5xx responses
|
179
|
+
# to prevent invalid data from appearing in documentation.
|
180
|
+
#
|
181
|
+
# @param operations [Array<Hash>] Array of operations
|
182
|
+
#
|
183
|
+
# @return [Array<Hash>] Sanitized operations
|
184
|
+
#
|
185
|
+
# @private
|
186
|
+
#
|
187
|
+
def sanitize_error_operations(operations)
|
188
|
+
operations.each do |operation|
|
189
|
+
next unless operation[:response_status] >= 400
|
190
|
+
|
191
|
+
# This keeps tests that handle errors from including their invalid attributes
|
192
|
+
# and such in the output.
|
193
|
+
operation[:request_query] = {}
|
194
|
+
operation[:request_headers] = {}
|
195
|
+
operation[:request_body] = {}
|
196
|
+
end
|
197
|
+
end
|
198
|
+
|
199
|
+
#
|
200
|
+
# Merges similar operations into a single operation
|
201
|
+
#
|
202
|
+
# @param operations [Array<Hash>] Array of operations
|
203
|
+
#
|
204
|
+
# @return [Array<Hash>] Merged operations
|
205
|
+
#
|
206
|
+
# @private
|
207
|
+
#
|
208
|
+
def merge_operations(operations)
|
209
|
+
operations.group_by { |o| o[:response_status] }
|
210
|
+
.transform_values { |o| o.to_merged_h }
|
211
|
+
.values
|
212
|
+
end
|
213
|
+
|
214
|
+
#
|
215
|
+
# Flattens multiple operations into a single operation structure
|
216
|
+
#
|
217
|
+
# @param operations [Array<Hash>] Array of operations
|
218
|
+
#
|
219
|
+
# @return [Hash] Flattened operation
|
220
|
+
#
|
221
|
+
# @private
|
222
|
+
#
|
223
|
+
def flatten_operations(operations)
|
224
|
+
id = operations.key_map(:spec_name).reject(&:blank?).first
|
225
|
+
|
226
|
+
description = operations.key_map(:expectation_name)
|
227
|
+
.reject(&:blank?)
|
228
|
+
.first
|
229
|
+
&.split(" - ")
|
230
|
+
&.second || ""
|
231
|
+
|
232
|
+
parameters = normalize_parameters(operations)
|
233
|
+
requests = normalize_requests(operations)
|
234
|
+
responses = normalize_responses(operations)
|
235
|
+
|
236
|
+
{
|
237
|
+
id:,
|
238
|
+
description:,
|
239
|
+
parameters:,
|
240
|
+
requests:,
|
241
|
+
responses:
|
242
|
+
}
|
243
|
+
end
|
244
|
+
|
245
|
+
#
|
246
|
+
# Normalizes request parameters from operations
|
247
|
+
#
|
248
|
+
# Extracts and categorizes parameters as path or query parameters
|
249
|
+
# and determines their data types.
|
250
|
+
#
|
251
|
+
# @param operations [Array<Hash>] Array of operations
|
252
|
+
#
|
253
|
+
# @return [Hash] Normalized parameters
|
254
|
+
#
|
255
|
+
# @private
|
256
|
+
#
|
257
|
+
def normalize_parameters(operations)
|
258
|
+
parameters = {}
|
259
|
+
|
260
|
+
operations.each do |operation|
|
261
|
+
# Store the URL so it can be determined if the param is in the path or not
|
262
|
+
url = operation[:url]
|
263
|
+
params = operation[:request_query].transform_values { |value| {value:, url:} }
|
264
|
+
|
265
|
+
parameters.merge!(params)
|
266
|
+
end
|
267
|
+
|
268
|
+
parameters.transform_values!(with_key: true) do |data, key|
|
269
|
+
key_in_path = data[:url].include?("{#{key}}")
|
270
|
+
|
271
|
+
{
|
272
|
+
location: key_in_path ? "path" : "query",
|
273
|
+
type: determine_type(data[:value])
|
274
|
+
}
|
275
|
+
end
|
276
|
+
end
|
277
|
+
|
278
|
+
#
|
279
|
+
# Normalizes request bodies from operations
|
280
|
+
#
|
281
|
+
# Extracts request bodies from successful operations and
|
282
|
+
# determines their data types.
|
283
|
+
#
|
284
|
+
# @param operations [Array<Hash>] Array of operations
|
285
|
+
#
|
286
|
+
# @return [Array<Hash>] Normalized request bodies
|
287
|
+
#
|
288
|
+
# @private
|
289
|
+
#
|
290
|
+
def normalize_requests(operations)
|
291
|
+
successful_operations = operations.select { |o| o[:response_status] < 400 }
|
292
|
+
return [] if successful_operations.blank?
|
293
|
+
|
294
|
+
successful_operations.filter_map.with_index do |operation, index|
|
295
|
+
content = operation[:request_body]
|
296
|
+
next if content.blank?
|
297
|
+
|
298
|
+
name = operation[:expectation_name].split(" - ").second
|
299
|
+
|
300
|
+
{
|
301
|
+
name: name || "Example #{index}",
|
302
|
+
content_type: operation[:content_type],
|
303
|
+
type: determine_type(content),
|
304
|
+
content:
|
305
|
+
}
|
306
|
+
end
|
307
|
+
end
|
308
|
+
|
309
|
+
#
|
310
|
+
# Normalizes responses from operations
|
311
|
+
#
|
312
|
+
# Extracts response details including status, headers, and body
|
313
|
+
# and determines their data types.
|
314
|
+
#
|
315
|
+
# @param operations [Array<Hash>] Array of operations
|
316
|
+
#
|
317
|
+
# @return [Array<Hash>] Normalized responses
|
318
|
+
#
|
319
|
+
# @private
|
320
|
+
#
|
321
|
+
def normalize_responses(operations)
|
322
|
+
operations.map do |operation|
|
323
|
+
{
|
324
|
+
content_type: operation[:content_type],
|
325
|
+
status: operation[:response_status],
|
326
|
+
headers: normalize_headers(operation[:response_headers]),
|
327
|
+
body: normalize_response_body(operation[:response_body])
|
328
|
+
}
|
329
|
+
end
|
330
|
+
end
|
331
|
+
|
332
|
+
#
|
333
|
+
# Normalizes response headers
|
334
|
+
#
|
335
|
+
# @param headers [Hash] Response headers
|
336
|
+
#
|
337
|
+
# @return [Hash] Normalized headers with types
|
338
|
+
#
|
339
|
+
# @private
|
340
|
+
#
|
341
|
+
def normalize_headers(headers)
|
342
|
+
headers.transform_values do |value|
|
343
|
+
{type: determine_type(value)}
|
344
|
+
end
|
345
|
+
end
|
346
|
+
|
347
|
+
#
|
348
|
+
# Normalizes response body structure
|
349
|
+
#
|
350
|
+
# @param body [Hash, Array, String] Response body
|
351
|
+
#
|
352
|
+
# @return [Hash] Normalized body structure with type information
|
353
|
+
#
|
354
|
+
# @private
|
355
|
+
#
|
356
|
+
def normalize_response_body(body)
|
357
|
+
proc = lambda do |value|
|
358
|
+
{type: determine_type(value)}
|
359
|
+
end
|
360
|
+
|
361
|
+
case body
|
362
|
+
when Hash
|
363
|
+
{
|
364
|
+
type: "object",
|
365
|
+
content: body.deep_transform_values(&proc)
|
366
|
+
}
|
367
|
+
when Array
|
368
|
+
{
|
369
|
+
type: "array",
|
370
|
+
content: body.map(&proc)
|
371
|
+
}
|
372
|
+
when String
|
373
|
+
{
|
374
|
+
type: "string",
|
375
|
+
content: body
|
376
|
+
}
|
377
|
+
else
|
378
|
+
raise "Unexpected body: #{body.inspect}"
|
379
|
+
end
|
380
|
+
end
|
381
|
+
end
|
382
|
+
end
|
383
|
+
end
|