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
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SpecForge
|
|
4
|
+
class Step
|
|
5
|
+
#
|
|
6
|
+
# Represents an expectation block within a step
|
|
7
|
+
#
|
|
8
|
+
# Holds the expected status, headers, and body (raw or JSON) that
|
|
9
|
+
# will be validated against the HTTP response. Provides methods
|
|
10
|
+
# to convert expectations into RSpec matchers.
|
|
11
|
+
#
|
|
12
|
+
class Expect < Data.define(:status, :headers, :raw, :json)
|
|
13
|
+
def initialize(status: nil, headers: nil, raw: nil, json: nil)
|
|
14
|
+
super(
|
|
15
|
+
status: status ? Attribute.from(status) : nil,
|
|
16
|
+
headers: extract_headers(headers),
|
|
17
|
+
raw: raw ? Attribute.from(raw) : nil,
|
|
18
|
+
json: extract_json(json)
|
|
19
|
+
)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
#
|
|
23
|
+
# Returns the total number of assertions in this expectation
|
|
24
|
+
#
|
|
25
|
+
# Counts all non-blank assertions (status, headers, raw, JSON size/schema/content).
|
|
26
|
+
#
|
|
27
|
+
# @return [Integer] Number of assertions
|
|
28
|
+
#
|
|
29
|
+
def size
|
|
30
|
+
[
|
|
31
|
+
status,
|
|
32
|
+
headers,
|
|
33
|
+
raw,
|
|
34
|
+
json[:size],
|
|
35
|
+
json[:schema],
|
|
36
|
+
json[:content]
|
|
37
|
+
].compact_blank.size
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
#
|
|
41
|
+
# Returns the status code as an RSpec matcher
|
|
42
|
+
#
|
|
43
|
+
# @return [RSpec::Matchers::BuiltIn::BaseMatcher, nil] Status matcher or nil
|
|
44
|
+
#
|
|
45
|
+
def status_matcher
|
|
46
|
+
return if status.blank?
|
|
47
|
+
|
|
48
|
+
status.resolve_as_matcher
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
#
|
|
52
|
+
# Returns headers as a hash of matchers
|
|
53
|
+
#
|
|
54
|
+
# @return [Hash, nil] Header matchers keyed by header name
|
|
55
|
+
#
|
|
56
|
+
def headers_matcher
|
|
57
|
+
return if headers.blank?
|
|
58
|
+
|
|
59
|
+
headers.transform_values(&Attribute.resolve_as_matcher_proc)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
#
|
|
63
|
+
# Returns the JSON size matcher
|
|
64
|
+
#
|
|
65
|
+
# @return [RSpec::Matchers::BuiltIn::BaseMatcher, nil] Size matcher or nil
|
|
66
|
+
#
|
|
67
|
+
def json_size_matcher
|
|
68
|
+
json[:size]&.resolve_as_matcher
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
#
|
|
72
|
+
# Returns the JSON schema structure for validation
|
|
73
|
+
#
|
|
74
|
+
# @return [Hash, nil] The schema definition or nil
|
|
75
|
+
#
|
|
76
|
+
def json_schema
|
|
77
|
+
json[:schema]
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
#
|
|
81
|
+
# Returns the JSON content matcher
|
|
82
|
+
#
|
|
83
|
+
# @return [RSpec::Matchers::BuiltIn::BaseMatcher, nil] Content matcher or nil
|
|
84
|
+
#
|
|
85
|
+
def json_content_matcher
|
|
86
|
+
json[:content]&.resolve_as_matcher
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
private
|
|
90
|
+
|
|
91
|
+
def extract_headers(headers)
|
|
92
|
+
return if headers.blank?
|
|
93
|
+
|
|
94
|
+
headers.stringify_keys.transform_values { |v| Attribute.from(v) }
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def extract_json(json)
|
|
98
|
+
return {} if json.blank?
|
|
99
|
+
|
|
100
|
+
output = {}
|
|
101
|
+
|
|
102
|
+
output[:content] = Attribute.from(json[:content]) if json[:content]
|
|
103
|
+
output[:size] = Attribute.from(json[:size]) if json[:size]
|
|
104
|
+
output[:schema] = json[:schema] || json[:shape]
|
|
105
|
+
|
|
106
|
+
output
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
end
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SpecForge
|
|
4
|
+
class Step
|
|
5
|
+
#
|
|
6
|
+
# Represents the source location of a step in a blueprint file
|
|
7
|
+
#
|
|
8
|
+
# Tracks which file and line number a step came from, which is
|
|
9
|
+
# useful for error messages and debugging output.
|
|
10
|
+
#
|
|
11
|
+
class Source < Data.define(:file_name, :line_number)
|
|
12
|
+
#
|
|
13
|
+
# Returns a formatted string representation of the source location
|
|
14
|
+
#
|
|
15
|
+
# @return [String] Format: "file_name:line_number"
|
|
16
|
+
#
|
|
17
|
+
def to_s
|
|
18
|
+
"#{file_name}:#{line_number}"
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SpecForge
|
|
4
|
+
#
|
|
5
|
+
# Represents a single executable step within a blueprint
|
|
6
|
+
#
|
|
7
|
+
# Steps are the fundamental unit of execution in SpecForge. Each step
|
|
8
|
+
# can contain a request, expectations, store operations, callbacks,
|
|
9
|
+
# and debug triggers. Steps are immutable value objects created from
|
|
10
|
+
# normalized YAML data.
|
|
11
|
+
#
|
|
12
|
+
class Step < Data.define(
|
|
13
|
+
:name,
|
|
14
|
+
:calls,
|
|
15
|
+
:debug,
|
|
16
|
+
:documentation,
|
|
17
|
+
:expects,
|
|
18
|
+
:hooks,
|
|
19
|
+
:included_by,
|
|
20
|
+
:request,
|
|
21
|
+
:source,
|
|
22
|
+
:store,
|
|
23
|
+
:tags
|
|
24
|
+
)
|
|
25
|
+
# @return [Boolean] Whether this step uses callbacks
|
|
26
|
+
# @return [Boolean] Whether debug mode is enabled for this step
|
|
27
|
+
# @return [Boolean] Whether this step registers callback hooks
|
|
28
|
+
# @return [Boolean] Whether this step has expectations
|
|
29
|
+
# @return [Boolean] Whether this step has a request action
|
|
30
|
+
# @return [Boolean] Whether this step has store operations
|
|
31
|
+
attr_predicate :calls, :debug, :expects, :hooks, :request, :store
|
|
32
|
+
|
|
33
|
+
#
|
|
34
|
+
# Creates a new Step from normalized YAML data
|
|
35
|
+
#
|
|
36
|
+
# Transforms raw step data into structured objects for execution.
|
|
37
|
+
# Converts requests, expectations, hooks, and other attributes
|
|
38
|
+
# into their runtime representations.
|
|
39
|
+
#
|
|
40
|
+
# @param step [Hash] Normalized step data from YAML
|
|
41
|
+
#
|
|
42
|
+
def initialize(**step)
|
|
43
|
+
step[:calls] = transform_calls(step[:calls])
|
|
44
|
+
step[:debug] = step[:debug] == true
|
|
45
|
+
step[:documentation] ||= nil
|
|
46
|
+
step[:expects] = transform_expect(step[:expects])
|
|
47
|
+
step[:hooks] = transform_hooks(step[:hooks])
|
|
48
|
+
step[:included_by] = transform_source(step[:included_by])
|
|
49
|
+
step[:request] = transform_request(step[:request])
|
|
50
|
+
step[:source] = transform_source(step[:source])
|
|
51
|
+
step[:store] = transform_store(step[:store])
|
|
52
|
+
step[:tags] ||= nil
|
|
53
|
+
|
|
54
|
+
super(step)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
private
|
|
58
|
+
|
|
59
|
+
def transform_source(source)
|
|
60
|
+
return if source.blank?
|
|
61
|
+
|
|
62
|
+
Source.new(file_name: source[:file_name], line_number: source[:line_number])
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def transform_calls(calls)
|
|
66
|
+
return if calls.blank?
|
|
67
|
+
|
|
68
|
+
calls.map { |call| Call.new(callback_name: call[:name], arguments: call[:arguments]) }
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def transform_request(input)
|
|
72
|
+
return if input.blank?
|
|
73
|
+
|
|
74
|
+
request = {}
|
|
75
|
+
|
|
76
|
+
if (url = input[:base_url]) && url.present?
|
|
77
|
+
request[:base_url] = Attribute.from(url)
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
if (url = input[:url]) && url.present?
|
|
81
|
+
request[:url] = Attribute.from(url)
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
if (verb = input[:http_verb]) && verb.present?
|
|
85
|
+
request[:http_verb] = verb
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
headers = (input[:headers] || {}).transform_keys(&:downcase)
|
|
89
|
+
|
|
90
|
+
if input[:json].present?
|
|
91
|
+
headers["content-type"] ||= "application/json"
|
|
92
|
+
elsif headers.present?
|
|
93
|
+
headers["content-type"] ||= "text/plain"
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
if headers.present?
|
|
97
|
+
request[:headers] = Attribute.from(headers)
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
if (query = input[:query]) && query.present?
|
|
101
|
+
request[:query] = Attribute.from(query)
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
if (json = input[:json]) && json.present?
|
|
105
|
+
request[:body] = Attribute.from(json)
|
|
106
|
+
elsif (raw = input[:raw]) && raw.present?
|
|
107
|
+
request[:body] = Attribute.from(raw)
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
HTTP::Request.new(**request)
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def transform_expect(expects)
|
|
114
|
+
return if expects.blank?
|
|
115
|
+
|
|
116
|
+
expects.map { |e| Expect.new(**e) }
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def transform_store(store)
|
|
120
|
+
return if store.blank?
|
|
121
|
+
|
|
122
|
+
store.transform_values { |v| Attribute.from(v) }
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def transform_hooks(hooks)
|
|
126
|
+
Step::Call.wrap_hooks(hooks)
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
end
|
data/lib/spec_forge/type.rb
CHANGED
|
@@ -2,76 +2,125 @@
|
|
|
2
2
|
|
|
3
3
|
module SpecForge
|
|
4
4
|
#
|
|
5
|
-
#
|
|
6
|
-
#
|
|
5
|
+
# Utilities for converting between Ruby classes and type strings
|
|
6
|
+
#
|
|
7
|
+
# Type provides bidirectional conversion between Ruby types (Integer, String, etc.)
|
|
8
|
+
# and their string representations ("integer", "string", etc.) used in YAML
|
|
9
|
+
# schema definitions. Supports nullable types with the "?" prefix and optional
|
|
10
|
+
# fields with the "*" prefix.
|
|
7
11
|
#
|
|
8
12
|
module Type
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
13
|
+
class << self
|
|
14
|
+
# Mapping from Ruby classes to type string names
|
|
15
|
+
#
|
|
16
|
+
# @return [Hash{Class => String}]
|
|
17
|
+
CLASS_TO_STRING = {
|
|
18
|
+
Integer => "integer",
|
|
19
|
+
Float => "float",
|
|
20
|
+
String => "string",
|
|
21
|
+
Hash => "hash",
|
|
22
|
+
Array => "array",
|
|
23
|
+
TrueClass => "boolean",
|
|
24
|
+
FalseClass => "boolean",
|
|
25
|
+
NilClass => "null"
|
|
26
|
+
}.freeze
|
|
19
27
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
28
|
+
# Mapping from type string names to Ruby classes
|
|
29
|
+
#
|
|
30
|
+
# @return [Hash{String => Array<Class>}]
|
|
31
|
+
STRING_TO_CLASS = {
|
|
32
|
+
"string" => [String],
|
|
33
|
+
"number" => [Integer, Float],
|
|
34
|
+
"numeric" => [Integer, Float],
|
|
35
|
+
"integer" => [Integer],
|
|
36
|
+
"float" => [Float],
|
|
37
|
+
"bool" => [TrueClass, FalseClass],
|
|
38
|
+
"boolean" => [TrueClass, FalseClass],
|
|
39
|
+
"array" => [Array],
|
|
40
|
+
"hash" => [Hash],
|
|
41
|
+
"object" => [Hash],
|
|
42
|
+
"null" => [NilClass],
|
|
43
|
+
"nil" => [NilClass]
|
|
44
|
+
}.freeze
|
|
32
45
|
|
|
33
|
-
#
|
|
34
|
-
#
|
|
35
|
-
#
|
|
36
|
-
#
|
|
37
|
-
#
|
|
38
|
-
#
|
|
39
|
-
#
|
|
40
|
-
#
|
|
41
|
-
#
|
|
42
|
-
#
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
#
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
end
|
|
46
|
+
#
|
|
47
|
+
# Converts a type string to a hash containing Ruby classes and optional flag
|
|
48
|
+
#
|
|
49
|
+
# Supports nullable types with the "?" prefix (e.g., "?string") and optional
|
|
50
|
+
# fields with the "*" prefix (e.g., "*string"). Flags can be combined in any
|
|
51
|
+
# order (e.g., "*?string" or "?*string").
|
|
52
|
+
#
|
|
53
|
+
# @param input [String] The type string (e.g., "integer", "?string", "*?boolean")
|
|
54
|
+
#
|
|
55
|
+
# @return [Hash] Hash with :types (Array<Class>) and :optional (Boolean)
|
|
56
|
+
#
|
|
57
|
+
# @raise [ArgumentError] If input is nil or unknown type
|
|
58
|
+
#
|
|
59
|
+
# @example
|
|
60
|
+
# Type.from_string("integer") # => { types: [Integer], optional: false }
|
|
61
|
+
# Type.from_string("?string") # => { types: [String, NilClass], optional: false }
|
|
62
|
+
# Type.from_string("*string") # => { types: [String], optional: true }
|
|
63
|
+
# Type.from_string("*?string") # => { types: [String, NilClass], optional: true }
|
|
64
|
+
#
|
|
65
|
+
def from_string(input)
|
|
66
|
+
raise ArgumentError, "Input is nil" if input.nil?
|
|
55
67
|
|
|
56
|
-
#
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
68
|
+
# Extract flags from start of string
|
|
69
|
+
potential_flags = input[..2]
|
|
70
|
+
optional = potential_flags.include?("*")
|
|
71
|
+
nullable = potential_flags.include?("?")
|
|
72
|
+
|
|
73
|
+
if optional || nullable
|
|
74
|
+
offset = [optional, nullable].count(true)
|
|
75
|
+
input = input[offset..]
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
types = STRING_TO_CLASS[input.downcase]&.dup
|
|
79
|
+
|
|
80
|
+
if types.nil?
|
|
81
|
+
raise ArgumentError,
|
|
82
|
+
"Unknown type: #{input.in_quotes}. Valid types: string, number/numeric, integer, float, boolean/bool, array, hash/object, null/nil"
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# Add NilClass if nullable
|
|
86
|
+
types << NilClass if nullable
|
|
87
|
+
types.uniq!
|
|
88
|
+
|
|
89
|
+
{types:, optional:}
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
#
|
|
93
|
+
# Converts Ruby classes to their type string representation
|
|
94
|
+
#
|
|
95
|
+
# Handles nullable types (includes NilClass) by adding "?" prefix.
|
|
96
|
+
# Normalizes boolean types (TrueClass/FalseClass) to single "boolean".
|
|
97
|
+
#
|
|
98
|
+
# @param types [Array<Class>] One or more Ruby classes
|
|
99
|
+
#
|
|
100
|
+
# @return [String, Array<String>] Type string or array if multiple distinct types
|
|
101
|
+
#
|
|
102
|
+
# @example
|
|
103
|
+
# Type.to_string(Integer) # => "integer"
|
|
104
|
+
# Type.to_string(String, NilClass) # => "?string"
|
|
105
|
+
# Type.to_string(TrueClass, FalseClass) # => "boolean"
|
|
106
|
+
# Type.to_string(Integer, String) # => ["integer", "string"]
|
|
107
|
+
#
|
|
108
|
+
def to_string(*types)
|
|
109
|
+
types = types.map { |k| CLASS_TO_STRING[k] }
|
|
110
|
+
|
|
111
|
+
null = CLASS_TO_STRING[NilClass] # Just in case the name changes
|
|
112
|
+
if types.delete(null)
|
|
113
|
+
# We removed the nil above, no other types means this is just nil. No need to continue processing
|
|
114
|
+
return null if types.empty?
|
|
115
|
+
|
|
116
|
+
types.map! { |t| "?#{t}" }
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
# Remove "boolean", "boolean" that happens with TrueClass/FalseClass
|
|
120
|
+
types.uniq!
|
|
121
|
+
|
|
122
|
+
(types.size == 1) ? types.first : types
|
|
123
|
+
end
|
|
124
|
+
end
|
|
76
125
|
end
|
|
77
126
|
end
|
data/lib/spec_forge/version.rb
CHANGED
data/lib/spec_forge.rb
CHANGED
|
@@ -13,58 +13,50 @@ require "faraday"
|
|
|
13
13
|
require "mime/types"
|
|
14
14
|
require "openapi3_parser"
|
|
15
15
|
require "pathname"
|
|
16
|
+
require "pastel"
|
|
16
17
|
require "rspec"
|
|
17
18
|
require "sem_version"
|
|
18
19
|
require "singleton"
|
|
19
20
|
require "thor"
|
|
20
21
|
require "webrick"
|
|
21
22
|
require "yaml"
|
|
23
|
+
require "zeitwerk"
|
|
24
|
+
|
|
25
|
+
require_path = ->(path) { Dir.glob(path).sort.each { |p| require p } }
|
|
26
|
+
|
|
27
|
+
# Require the overwrites
|
|
28
|
+
root_path = Pathname.new(__dir__)
|
|
29
|
+
|
|
30
|
+
core_ext_path = root_path.join("spec_forge", "core_ext")
|
|
31
|
+
require_path.call(core_ext_path.join("**/*.rb"))
|
|
32
|
+
|
|
33
|
+
types_path = root_path.join("spec_forge", "types")
|
|
34
|
+
require_path.call(types_path.join("**/*.rb"))
|
|
35
|
+
|
|
36
|
+
# Load the files
|
|
37
|
+
loader = Zeitwerk::Loader.for_gem
|
|
38
|
+
loader.inflector.inflect(
|
|
39
|
+
"cli" => "CLI",
|
|
40
|
+
"http" => "HTTP",
|
|
41
|
+
"openapi" => "OpenAPI",
|
|
42
|
+
"array_io" => "ArrayIO"
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
# spec_forge/forge/actions/*.rb -> SpecForge::Forge::*
|
|
46
|
+
loader.collapse(root_path.join("spec_forge", "forge", "actions"))
|
|
47
|
+
|
|
48
|
+
# spec_forge/types/*.rb -> SpecForge::*
|
|
49
|
+
loader.collapse(types_path)
|
|
50
|
+
|
|
51
|
+
# Loaded manually above
|
|
52
|
+
loader.ignore(core_ext_path)
|
|
53
|
+
loader.ignore(types_path)
|
|
54
|
+
|
|
55
|
+
loader.setup
|
|
56
|
+
# loader.eager_load(force: true)
|
|
22
57
|
|
|
23
|
-
#
|
|
24
|
-
# SpecForge: Write expressive API tests in YAML with the power of RSpec matchers
|
|
25
|
-
#
|
|
26
|
-
# SpecForge is a testing framework that allows writing API tests in a YAML format
|
|
27
|
-
# that reads like documentation. It combines the readability of YAML with the
|
|
28
|
-
# power of RSpec matchers, Faker data generation, and FactoryBot test objects.
|
|
29
|
-
#
|
|
30
|
-
# @example Basic spec in YAML
|
|
31
|
-
# get_user:
|
|
32
|
-
# path: /users/1
|
|
33
|
-
# expectations:
|
|
34
|
-
# - expect:
|
|
35
|
-
# status: 200
|
|
36
|
-
# json:
|
|
37
|
-
# name: kind_of.string
|
|
38
|
-
# email: /@/
|
|
39
|
-
#
|
|
40
|
-
# @example Running specs
|
|
41
|
-
# # Run all specs
|
|
42
|
-
# SpecForge.run
|
|
43
|
-
#
|
|
44
|
-
# # Run specific file
|
|
45
|
-
# SpecForge.run(file_name: "users")
|
|
46
|
-
#
|
|
47
|
-
# # Run specific spec
|
|
48
|
-
# SpecForge.run(file_name: "users", spec_name: "create_user")
|
|
49
|
-
#
|
|
50
58
|
module SpecForge
|
|
51
59
|
class << self
|
|
52
|
-
#
|
|
53
|
-
# Loads all factories and specs and runs the tests with optional filtering
|
|
54
|
-
#
|
|
55
|
-
# This is the main entry point for running SpecForge tests. It loads the
|
|
56
|
-
# forge_helper.rb file if it exists, configures the environment, loads
|
|
57
|
-
# factories and specs, and runs the tests through RSpec.
|
|
58
|
-
#
|
|
59
|
-
# @param file_name [String, nil] Optional name of spec file to run
|
|
60
|
-
# @param spec_name [String, nil] Optional name of spec to run
|
|
61
|
-
# @param expectation_name [String, nil] Optional name of expectation to run
|
|
62
|
-
#
|
|
63
|
-
def run(file_name: nil, spec_name: nil, expectation_name: nil)
|
|
64
|
-
forges = Runner.prepare(file_name:, spec_name:, expectation_name:)
|
|
65
|
-
Runner.run(forges, exit_on_finish: true)
|
|
66
|
-
end
|
|
67
|
-
|
|
68
60
|
#
|
|
69
61
|
# Returns the directory root for the working directory
|
|
70
62
|
#
|
|
@@ -83,6 +75,15 @@ module SpecForge
|
|
|
83
75
|
@forge_path ||= root.join("spec_forge")
|
|
84
76
|
end
|
|
85
77
|
|
|
78
|
+
#
|
|
79
|
+
# Returns SpecForge's blueprints directory
|
|
80
|
+
#
|
|
81
|
+
# @return [Pathname] The spec_forge blueprints directory path
|
|
82
|
+
#
|
|
83
|
+
def blueprints_path
|
|
84
|
+
@blueprints_path ||= forge_path.join("blueprints")
|
|
85
|
+
end
|
|
86
|
+
|
|
86
87
|
#
|
|
87
88
|
# Returns SpecForge's openapi directory
|
|
88
89
|
#
|
|
@@ -132,68 +133,5 @@ module SpecForge
|
|
|
132
133
|
cleaner
|
|
133
134
|
end
|
|
134
135
|
end
|
|
135
|
-
|
|
136
|
-
#
|
|
137
|
-
# Returns the current execution context
|
|
138
|
-
#
|
|
139
|
-
# @return [Context] The current context object
|
|
140
|
-
#
|
|
141
|
-
def context
|
|
142
|
-
@context ||= Context.new
|
|
143
|
-
end
|
|
144
|
-
|
|
145
|
-
#
|
|
146
|
-
# Registers a callback for a specific test lifecycle event
|
|
147
|
-
# Allows custom code execution at specific points during test execution
|
|
148
|
-
#
|
|
149
|
-
# @param name [Symbol, String] A unique identifier for this callback
|
|
150
|
-
# @yield A block to execute when the callback is triggered
|
|
151
|
-
# @yieldparam context [Object] An object containing context-specific state data, depending
|
|
152
|
-
# on which hook the callback is triggered from.
|
|
153
|
-
#
|
|
154
|
-
# @return [Proc] The registered callback
|
|
155
|
-
#
|
|
156
|
-
# @example Registering a custom debug handler
|
|
157
|
-
# SpecForge.register_callback(:clean_database) do |context|
|
|
158
|
-
# DatabaseCleaner.clean
|
|
159
|
-
# end
|
|
160
|
-
#
|
|
161
|
-
def register_callback(name, &)
|
|
162
|
-
Callbacks.register(name, &)
|
|
163
|
-
end
|
|
164
|
-
|
|
165
|
-
#
|
|
166
|
-
# Generates a unique ID for an object based on hash and object_id
|
|
167
|
-
#
|
|
168
|
-
# @param object [Object] The object to generate an ID for
|
|
169
|
-
#
|
|
170
|
-
# @return [String] A unique ID string
|
|
171
|
-
#
|
|
172
|
-
# @private
|
|
173
|
-
#
|
|
174
|
-
def generate_id(object)
|
|
175
|
-
"#{object.hash.abs.to_s(36)}_#{object.object_id.to_s(36)}"
|
|
176
|
-
end
|
|
177
136
|
end
|
|
178
137
|
end
|
|
179
|
-
|
|
180
|
-
require_relative "spec_forge/attribute"
|
|
181
|
-
require_relative "spec_forge/backtrace_formatter"
|
|
182
|
-
require_relative "spec_forge/callbacks"
|
|
183
|
-
require_relative "spec_forge/cli"
|
|
184
|
-
require_relative "spec_forge/configuration"
|
|
185
|
-
require_relative "spec_forge/context"
|
|
186
|
-
require_relative "spec_forge/core_ext"
|
|
187
|
-
require_relative "spec_forge/documentation"
|
|
188
|
-
require_relative "spec_forge/error"
|
|
189
|
-
require_relative "spec_forge/factory"
|
|
190
|
-
require_relative "spec_forge/filter"
|
|
191
|
-
require_relative "spec_forge/forge"
|
|
192
|
-
require_relative "spec_forge/http"
|
|
193
|
-
require_relative "spec_forge/loader"
|
|
194
|
-
require_relative "spec_forge/matchers"
|
|
195
|
-
require_relative "spec_forge/normalizer"
|
|
196
|
-
require_relative "spec_forge/runner"
|
|
197
|
-
require_relative "spec_forge/spec"
|
|
198
|
-
require_relative "spec_forge/type"
|
|
199
|
-
require_relative "spec_forge/version"
|