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.
Files changed (133) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +75 -1
  3. data/README.md +124 -202
  4. data/bin/spec_forge +1 -1
  5. data/flake.lock +76 -4
  6. data/flake.nix +5 -4
  7. data/lib/spec_forge/attribute/chainable.rb +6 -6
  8. data/lib/spec_forge/attribute/environment.rb +45 -0
  9. data/lib/spec_forge/attribute/factory.rb +26 -17
  10. data/lib/spec_forge/attribute/faker.rb +6 -1
  11. data/lib/spec_forge/attribute/generate.rb +114 -0
  12. data/lib/spec_forge/attribute/literal.rb +1 -14
  13. data/lib/spec_forge/attribute/matcher.rb +6 -2
  14. data/lib/spec_forge/attribute/parameterized.rb +20 -22
  15. data/lib/spec_forge/attribute/resolvable_array.rb +16 -16
  16. data/lib/spec_forge/attribute/resolvable_hash.rb +17 -16
  17. data/lib/spec_forge/attribute/resolvable_struct.rb +67 -0
  18. data/lib/spec_forge/attribute/template.rb +118 -0
  19. data/lib/spec_forge/attribute/transform.rb +14 -19
  20. data/lib/spec_forge/attribute/variable.rb +31 -31
  21. data/lib/spec_forge/attribute.rb +54 -100
  22. data/lib/spec_forge/blueprint.rb +27 -0
  23. data/lib/spec_forge/cli/docs/generate.rb +28 -8
  24. data/lib/spec_forge/cli/docs.rb +5 -2
  25. data/lib/spec_forge/cli/init.rb +4 -4
  26. data/lib/spec_forge/cli/new.rb +78 -27
  27. data/lib/spec_forge/cli/run.rb +84 -52
  28. data/lib/spec_forge/cli/serve.rb +5 -0
  29. data/lib/spec_forge/cli.rb +6 -14
  30. data/lib/spec_forge/configuration.rb +209 -79
  31. data/lib/spec_forge/documentation/{loader → builder}/cache.rb +26 -23
  32. data/lib/spec_forge/documentation/builder/compiler.rb +373 -0
  33. data/lib/spec_forge/documentation/builder/extractor.rb +75 -0
  34. data/lib/spec_forge/documentation/builder.rb +77 -329
  35. data/lib/spec_forge/documentation/document/operation.rb +4 -4
  36. data/lib/spec_forge/documentation/document.rb +0 -6
  37. data/lib/spec_forge/documentation/generator.rb +88 -0
  38. data/lib/spec_forge/documentation/{generators/openapi → openapi/v3_0}/error_formatter.rb +2 -2
  39. data/lib/spec_forge/documentation/openapi/v3_0/example.rb +1 -1
  40. data/lib/spec_forge/documentation/openapi/v3_0/media_type.rb +1 -1
  41. data/lib/spec_forge/documentation/openapi/v3_0/operation.rb +21 -5
  42. data/lib/spec_forge/documentation/openapi/v3_0/response.rb +28 -6
  43. data/lib/spec_forge/documentation/openapi/v3_0/schema.rb +20 -2
  44. data/lib/spec_forge/documentation/openapi/v3_0/tag.rb +1 -1
  45. data/lib/spec_forge/documentation/openapi/v3_0.rb +116 -0
  46. data/lib/spec_forge/documentation/openapi.rb +40 -12
  47. data/lib/spec_forge/documentation.rb +1 -7
  48. data/lib/spec_forge/error.rb +215 -41
  49. data/lib/spec_forge/factory.rb +38 -18
  50. data/lib/spec_forge/forge/action.rb +41 -0
  51. data/lib/spec_forge/forge/actions/call.rb +33 -0
  52. data/lib/spec_forge/forge/actions/debug.rb +47 -0
  53. data/lib/spec_forge/forge/actions/expect.rb +44 -0
  54. data/lib/spec_forge/forge/actions/request.rb +65 -0
  55. data/lib/spec_forge/forge/actions/store.rb +31 -0
  56. data/lib/spec_forge/forge/callbacks.rb +80 -0
  57. data/lib/spec_forge/forge/context.rb +41 -0
  58. data/lib/spec_forge/forge/display.rb +503 -0
  59. data/lib/spec_forge/forge/hooks.rb +131 -0
  60. data/lib/spec_forge/forge/runner/array_io.rb +81 -0
  61. data/lib/spec_forge/forge/runner/content_validator.rb +92 -0
  62. data/lib/spec_forge/forge/runner/header_validator.rb +66 -0
  63. data/lib/spec_forge/forge/runner/reporter.rb +56 -0
  64. data/lib/spec_forge/forge/runner/schema_validator.rb +113 -0
  65. data/lib/spec_forge/forge/runner.rb +118 -0
  66. data/lib/spec_forge/forge/timer.rb +94 -0
  67. data/lib/spec_forge/forge/variables.rb +38 -0
  68. data/lib/spec_forge/forge.rb +207 -133
  69. data/lib/spec_forge/http/backend.rb +49 -146
  70. data/lib/spec_forge/http/client.rb +14 -17
  71. data/lib/spec_forge/http/request.rb +37 -84
  72. data/lib/spec_forge/http/verb.rb +4 -0
  73. data/lib/spec_forge/http.rb +0 -5
  74. data/lib/spec_forge/loader/filter.rb +85 -0
  75. data/lib/spec_forge/loader/step_processor.rb +282 -0
  76. data/lib/spec_forge/loader.rb +105 -220
  77. data/lib/spec_forge/normalizer/default.rb +1 -1
  78. data/lib/spec_forge/normalizer/structure.rb +140 -0
  79. data/lib/spec_forge/normalizer/transformers.rb +168 -0
  80. data/lib/spec_forge/normalizer/validators.rb +50 -8
  81. data/lib/spec_forge/normalizer.rb +76 -119
  82. data/lib/spec_forge/normalizers/callback.yml +38 -0
  83. data/lib/spec_forge/normalizers/configuration.yml +59 -9
  84. data/lib/spec_forge/normalizers/factory.yml +53 -2
  85. data/lib/spec_forge/normalizers/factory_reference.yml +63 -2
  86. data/lib/spec_forge/normalizers/json_schema.yml +79 -0
  87. data/lib/spec_forge/normalizers/step.yml +506 -0
  88. data/lib/spec_forge/step/call.rb +36 -0
  89. data/lib/spec_forge/step/expect.rb +110 -0
  90. data/lib/spec_forge/step/source.rb +22 -0
  91. data/lib/spec_forge/step.rb +129 -0
  92. data/lib/spec_forge/type.rb +115 -66
  93. data/lib/spec_forge/version.rb +1 -1
  94. data/lib/spec_forge.rb +44 -106
  95. data/lib/templates/forge_helper.rb.tt +43 -22
  96. data/lib/templates/new_blueprint.yml.tt +54 -0
  97. metadata +75 -44
  98. data/lib/spec_forge/attribute/global.rb +0 -96
  99. data/lib/spec_forge/attribute/store.rb +0 -65
  100. data/lib/spec_forge/backtrace_formatter.rb +0 -50
  101. data/lib/spec_forge/callbacks.rb +0 -88
  102. data/lib/spec_forge/context/callbacks.rb +0 -91
  103. data/lib/spec_forge/context/global.rb +0 -72
  104. data/lib/spec_forge/context/store.rb +0 -131
  105. data/lib/spec_forge/context/variables.rb +0 -91
  106. data/lib/spec_forge/context.rb +0 -36
  107. data/lib/spec_forge/core_ext/rspec.rb +0 -55
  108. data/lib/spec_forge/core_ext.rb +0 -5
  109. data/lib/spec_forge/documentation/generators/base.rb +0 -81
  110. data/lib/spec_forge/documentation/generators/openapi/base.rb +0 -100
  111. data/lib/spec_forge/documentation/generators/openapi/v3_0.rb +0 -65
  112. data/lib/spec_forge/documentation/generators/openapi.rb +0 -59
  113. data/lib/spec_forge/documentation/generators.rb +0 -17
  114. data/lib/spec_forge/documentation/loader.rb +0 -159
  115. data/lib/spec_forge/documentation/openapi/base.rb +0 -33
  116. data/lib/spec_forge/filter.rb +0 -86
  117. data/lib/spec_forge/normalizer/definition.rb +0 -248
  118. data/lib/spec_forge/normalizers/_shared.yml +0 -76
  119. data/lib/spec_forge/normalizers/constraint.yml +0 -8
  120. data/lib/spec_forge/normalizers/expectation.yml +0 -47
  121. data/lib/spec_forge/normalizers/global_context.yml +0 -28
  122. data/lib/spec_forge/normalizers/spec.yml +0 -50
  123. data/lib/spec_forge/runner/adapter.rb +0 -181
  124. data/lib/spec_forge/runner/callbacks.rb +0 -246
  125. data/lib/spec_forge/runner/debug_proxy.rb +0 -215
  126. data/lib/spec_forge/runner/listener.rb +0 -54
  127. data/lib/spec_forge/runner/metadata.rb +0 -58
  128. data/lib/spec_forge/runner/state.rb +0 -98
  129. data/lib/spec_forge/runner.rb +0 -75
  130. data/lib/spec_forge/spec/expectation/constraint.rb +0 -127
  131. data/lib/spec_forge/spec/expectation.rb +0 -68
  132. data/lib/spec_forge/spec.rb +0 -68
  133. 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
@@ -2,76 +2,125 @@
2
2
 
3
3
  module SpecForge
4
4
  #
5
- # Provides helper methods for checking types
6
- # Useful for working with both regular objects and Attribute delegators
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
- # Checks if the object is a Hash or a ResolvableHash delegator
11
- #
12
- # @param object [Object] The object to check
13
- #
14
- # @return [Boolean] True if the object is a hash-like structure
15
- #
16
- def self.hash?(object)
17
- object.is_a?(Hash) || object.is_a?(Attribute::ResolvableHash)
18
- end
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
- # Checks if the object is an Array or a ResolvableArray delegator
22
- #
23
- # @param object [Object] The object to check
24
- #
25
- # @return [Boolean] True if the object is an array-like structure
26
- #
27
- def self.array?(object)
28
- object.is_a?(Array) || object.is_a?(Attribute::ResolvableArray)
29
- end
30
- end
31
- end
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
- # Represents Hash/ResolvableHash in a form that can be used in a case statement
35
- # Allows for type switching on hash-like objects
36
- #
37
- # @example
38
- # case value
39
- # when HashLike
40
- # # Handle hash-like objects
41
- # end
42
- #
43
- class HashLike
44
- #
45
- # Provides custom type matching for use in case statements
46
- #
47
- # @param object [Object] The object to check against the type
48
- #
49
- # @return [Boolean] Whether the object matches the type
50
- #
51
- def self.===(object)
52
- SpecForge::Type.hash?(object)
53
- end
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
- # Represents Array/ResolvableArray in a form that can be used in a case statement
58
- # Allows for type switching on array-like objects
59
- #
60
- # @example
61
- # case value
62
- # when ArrayLike
63
- # # Handle array-like objects
64
- # end
65
- #
66
- class ArrayLike
67
- #
68
- # Provides custom type matching for use in case statements
69
- #
70
- # @param object [Object] The object to check against the type
71
- #
72
- # @return [Boolean] Whether the object matches the type
73
- #
74
- def self.===(object)
75
- SpecForge::Type.array?(object)
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
@@ -4,5 +4,5 @@ module SpecForge
4
4
  #
5
5
  # Current version of SpecForge
6
6
  #
7
- VERSION = "0.7.1"
7
+ VERSION = "1.0.0"
8
8
  end
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"