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.
Files changed (116) hide show
  1. checksums.yaml +4 -4
  2. data/.standard.yml +3 -3
  3. data/CHANGELOG.md +217 -2
  4. data/README.md +162 -25
  5. data/flake.lock +3 -3
  6. data/flake.nix +11 -5
  7. data/lib/spec_forge/attribute/chainable.rb +208 -20
  8. data/lib/spec_forge/attribute/factory.rb +92 -15
  9. data/lib/spec_forge/attribute/faker.rb +62 -13
  10. data/lib/spec_forge/attribute/global.rb +96 -0
  11. data/lib/spec_forge/attribute/literal.rb +15 -2
  12. data/lib/spec_forge/attribute/matcher.rb +186 -11
  13. data/lib/spec_forge/attribute/parameterized.rb +45 -12
  14. data/lib/spec_forge/attribute/regex.rb +55 -5
  15. data/lib/spec_forge/attribute/resolvable.rb +48 -5
  16. data/lib/spec_forge/attribute/resolvable_array.rb +62 -4
  17. data/lib/spec_forge/attribute/resolvable_hash.rb +62 -4
  18. data/lib/spec_forge/attribute/store.rb +65 -0
  19. data/lib/spec_forge/attribute/transform.rb +33 -5
  20. data/lib/spec_forge/attribute/variable.rb +37 -6
  21. data/lib/spec_forge/attribute.rb +166 -66
  22. data/lib/spec_forge/backtrace_formatter.rb +26 -3
  23. data/lib/spec_forge/callbacks.rb +88 -0
  24. data/lib/spec_forge/cli/actions.rb +27 -0
  25. data/lib/spec_forge/cli/command.rb +78 -24
  26. data/lib/spec_forge/cli/docs/generate.rb +72 -0
  27. data/lib/spec_forge/cli/docs.rb +92 -0
  28. data/lib/spec_forge/cli/init.rb +51 -9
  29. data/lib/spec_forge/cli/new.rb +67 -6
  30. data/lib/spec_forge/cli/run.rb +32 -4
  31. data/lib/spec_forge/cli/serve.rb +155 -0
  32. data/lib/spec_forge/cli.rb +26 -7
  33. data/lib/spec_forge/configuration.rb +96 -24
  34. data/lib/spec_forge/context/callbacks.rb +91 -0
  35. data/lib/spec_forge/context/global.rb +72 -0
  36. data/lib/spec_forge/context/store.rb +131 -0
  37. data/lib/spec_forge/context/variables.rb +91 -0
  38. data/lib/spec_forge/context.rb +36 -0
  39. data/lib/spec_forge/core_ext/array.rb +27 -0
  40. data/lib/spec_forge/core_ext/rspec.rb +22 -4
  41. data/lib/spec_forge/documentation/builder.rb +383 -0
  42. data/lib/spec_forge/documentation/document/operation.rb +47 -0
  43. data/lib/spec_forge/documentation/document/parameter.rb +22 -0
  44. data/lib/spec_forge/documentation/document/request_body.rb +24 -0
  45. data/lib/spec_forge/documentation/document/response.rb +39 -0
  46. data/lib/spec_forge/documentation/document/response_body.rb +27 -0
  47. data/lib/spec_forge/documentation/document.rb +48 -0
  48. data/lib/spec_forge/documentation/generators/base.rb +81 -0
  49. data/lib/spec_forge/documentation/generators/openapi/base.rb +100 -0
  50. data/lib/spec_forge/documentation/generators/openapi/error_formatter.rb +149 -0
  51. data/lib/spec_forge/documentation/generators/openapi/v3_0.rb +65 -0
  52. data/lib/spec_forge/documentation/generators/openapi.rb +59 -0
  53. data/lib/spec_forge/documentation/generators.rb +17 -0
  54. data/lib/spec_forge/documentation/loader/cache.rb +138 -0
  55. data/lib/spec_forge/documentation/loader.rb +159 -0
  56. data/lib/spec_forge/documentation/openapi/base.rb +33 -0
  57. data/lib/spec_forge/documentation/openapi/v3_0/example.rb +44 -0
  58. data/lib/spec_forge/documentation/openapi/v3_0/media_type.rb +42 -0
  59. data/lib/spec_forge/documentation/openapi/v3_0/operation.rb +175 -0
  60. data/lib/spec_forge/documentation/openapi/v3_0/response.rb +65 -0
  61. data/lib/spec_forge/documentation/openapi/v3_0/schema.rb +80 -0
  62. data/lib/spec_forge/documentation/openapi/v3_0/tag.rb +71 -0
  63. data/lib/spec_forge/documentation/openapi.rb +23 -0
  64. data/lib/spec_forge/documentation.rb +27 -0
  65. data/lib/spec_forge/error.rb +284 -113
  66. data/lib/spec_forge/factory.rb +35 -16
  67. data/lib/spec_forge/filter.rb +86 -0
  68. data/lib/spec_forge/forge.rb +171 -0
  69. data/lib/spec_forge/http/backend.rb +101 -29
  70. data/lib/spec_forge/http/client.rb +23 -13
  71. data/lib/spec_forge/http/request.rb +85 -62
  72. data/lib/spec_forge/http/verb.rb +79 -0
  73. data/lib/spec_forge/http.rb +105 -0
  74. data/lib/spec_forge/loader.rb +244 -0
  75. data/lib/spec_forge/matchers.rb +130 -0
  76. data/lib/spec_forge/normalizer/default.rb +51 -0
  77. data/lib/spec_forge/normalizer/definition.rb +248 -0
  78. data/lib/spec_forge/normalizer/validators.rb +99 -0
  79. data/lib/spec_forge/normalizer.rb +486 -115
  80. data/lib/spec_forge/normalizers/_shared.yml +74 -0
  81. data/lib/spec_forge/normalizers/configuration.yml +23 -0
  82. data/lib/spec_forge/normalizers/constraint.yml +8 -0
  83. data/lib/spec_forge/normalizers/expectation.yml +47 -0
  84. data/lib/spec_forge/normalizers/factory.yml +12 -0
  85. data/lib/spec_forge/normalizers/factory_reference.yml +15 -0
  86. data/lib/spec_forge/normalizers/global_context.yml +28 -0
  87. data/lib/spec_forge/normalizers/spec.yml +50 -0
  88. data/lib/spec_forge/runner/adapter.rb +183 -0
  89. data/lib/spec_forge/runner/callbacks.rb +246 -0
  90. data/lib/spec_forge/runner/debug_proxy.rb +213 -0
  91. data/lib/spec_forge/runner/listener.rb +54 -0
  92. data/lib/spec_forge/runner/metadata.rb +58 -0
  93. data/lib/spec_forge/runner/state.rb +98 -0
  94. data/lib/spec_forge/runner.rb +50 -125
  95. data/lib/spec_forge/spec/expectation/constraint.rb +100 -21
  96. data/lib/spec_forge/spec/expectation.rb +47 -51
  97. data/lib/spec_forge/spec.rb +50 -108
  98. data/lib/spec_forge/type.rb +36 -4
  99. data/lib/spec_forge/version.rb +4 -1
  100. data/lib/spec_forge.rb +168 -76
  101. data/lib/templates/openapi.yml.tt +22 -0
  102. data/lib/templates/redoc.html.tt +28 -0
  103. data/lib/templates/swagger.html.tt +59 -0
  104. metadata +109 -16
  105. data/lib/spec_forge/normalizer/configuration.rb +0 -77
  106. data/lib/spec_forge/normalizer/constraint.rb +0 -47
  107. data/lib/spec_forge/normalizer/expectation.rb +0 -86
  108. data/lib/spec_forge/normalizer/factory.rb +0 -65
  109. data/lib/spec_forge/normalizer/factory_reference.rb +0 -71
  110. data/lib/spec_forge/normalizer/spec.rb +0 -74
  111. data/spec_forge/factories/user.yml +0 -4
  112. data/spec_forge/forge_helper.rb +0 -48
  113. data/spec_forge/specs/users.yml +0 -65
  114. /data/lib/templates/{forge_helper.tt → forge_helper.rb.tt} +0 -0
  115. /data/lib/templates/{new_factory.tt → new_factory.yml.tt} +0 -0
  116. /data/lib/templates/{new_spec.tt → new_spec.yml.tt} +0 -0
@@ -0,0 +1,80 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SpecForge
4
+ module Documentation
5
+ module OpenAPI
6
+ module V3_0 # standard:disable Naming/ClassAndModuleCamelCase
7
+ #
8
+ # Represents an OpenAPI 3.0 Schema object
9
+ #
10
+ # Handles schema definitions for data types, converting internal type
11
+ # representations to OpenAPI-compliant schema objects.
12
+ #
13
+ # @see https://spec.openapis.org/oas/v3.0.4.html#schema-object
14
+ #
15
+ class Schema
16
+ #
17
+ # The schema type (string, integer, object, etc.)
18
+ #
19
+ # @return [String, nil] The OpenAPI schema type
20
+ #
21
+ attr_reader :type
22
+
23
+ #
24
+ # The schema format (date-time, int64, etc.)
25
+ #
26
+ # @return [String, nil] The OpenAPI schema format
27
+ #
28
+ attr_reader :format
29
+
30
+ #
31
+ # Creates a new OpenAPI schema object
32
+ #
33
+ # @param options [Hash] Schema configuration options
34
+ # @option options [String] :type The data type to convert to OpenAPI format
35
+ #
36
+ # @return [Schema] A new schema instance
37
+ #
38
+ def initialize(options = {})
39
+ @type, @format = transform_type(options[:type])
40
+ end
41
+
42
+ #
43
+ # Converts the schema to an OpenAPI-compliant hash
44
+ #
45
+ # @return [Hash] OpenAPI-formatted schema object
46
+ #
47
+ def to_h
48
+ {
49
+ type:,
50
+ format:
51
+ }.compact_blank!
52
+ end
53
+
54
+ private
55
+
56
+ def transform_type(format)
57
+ case format
58
+ when "datetime", "time"
59
+ ["string", "date-time"]
60
+ when "int64", "i64"
61
+ ["integer", "int64"]
62
+ when "int32", "i32"
63
+ ["integer", "int32"]
64
+ when "double", "float"
65
+ ["number", format]
66
+ when "object"
67
+ ["object"]
68
+ when "array"
69
+ ["array"]
70
+ when "boolean", "number", "integer", "string"
71
+ [format]
72
+ else
73
+ ["string", format]
74
+ end
75
+ end
76
+ end
77
+ end
78
+ end
79
+ end
80
+ end
@@ -0,0 +1,71 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SpecForge
4
+ module Documentation
5
+ module OpenAPI
6
+ module V3_0 # standard:disable Naming/ClassAndModuleCamelCase
7
+ #
8
+ # Represents an OpenAPI 3.0 Tag object
9
+ #
10
+ # Handles tag definitions for categorizing and organizing API operations
11
+ # with optional descriptions and external documentation links.
12
+ #
13
+ # @see https://spec.openapis.org/oas/v3.0.4.html#tag-object
14
+ #
15
+ class Tag < Data.define(:name, :description, :external_docs)
16
+ #
17
+ # Creates a tag object from name and data
18
+ #
19
+ # Handles both string descriptions and hash configurations with
20
+ # external documentation references.
21
+ #
22
+ # @param name [String, Symbol] The tag name
23
+ # @param data [String, Hash] Either a description string or full config hash
24
+ #
25
+ # @return [Tag] A new tag instance
26
+ #
27
+ def self.parse(name, data)
28
+ name = name.to_s
29
+
30
+ case data
31
+ when String
32
+ description = data
33
+ when Hash
34
+ description = data[:description]
35
+ external_docs = data[:external_docs]
36
+ end
37
+
38
+ new(name:, description:, external_docs:)
39
+ end
40
+
41
+ #
42
+ # Creates a new OpenAPI tag object
43
+ #
44
+ # @param name [String] The tag name
45
+ # @param description [String, nil] Optional tag description
46
+ # @param external_docs [Hash, nil] Optional external documentation reference
47
+ #
48
+ # @return [Tag] A new tag instance
49
+ #
50
+ def initialize(name:, description: nil, external_docs: nil)
51
+ super
52
+ end
53
+
54
+ #
55
+ # Converts the tag to an OpenAPI-compliant hash
56
+ #
57
+ # Transforms internal attribute names to match OpenAPI specification
58
+ # and removes any blank values for clean output.
59
+ #
60
+ # @return [Hash] OpenAPI-formatted tag object
61
+ #
62
+ def to_h
63
+ super
64
+ .rename_key_unordered!(:external_docs, :externalDocs)
65
+ .compact_blank!
66
+ end
67
+ end
68
+ end
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SpecForge
4
+ module Documentation
5
+ #
6
+ # OpenAPI documentation generation functionality
7
+ #
8
+ # Contains classes and modules for generating OpenAPI specifications
9
+ # from SpecForge test data. Supports multiple OpenAPI versions.
10
+ #
11
+ module OpenAPI
12
+ end
13
+ end
14
+ end
15
+
16
+ require_relative "openapi/base"
17
+
18
+ require_relative "openapi/v3_0/example"
19
+ require_relative "openapi/v3_0/media_type"
20
+ require_relative "openapi/v3_0/operation"
21
+ require_relative "openapi/v3_0/response"
22
+ require_relative "openapi/v3_0/schema"
23
+ require_relative "openapi/v3_0/tag"
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SpecForge
4
+ #
5
+ # API documentation generation functionality
6
+ #
7
+ # This module provides tools for extracting API documentation from SpecForge
8
+ # test files and generating various output formats like OpenAPI specifications.
9
+ # It handles the complete pipeline from test execution to documentation rendering.
10
+ #
11
+ # @example Generating OpenAPI documentation
12
+ # # From CLI
13
+ # spec_forge docs generate
14
+ #
15
+ # # Programmatically
16
+ # document = Documentation::Loader.load_document
17
+ # spec = Documentation::Generators::OpenAPI["3.0"].new(document).generate
18
+ #
19
+ module Documentation
20
+ end
21
+ end
22
+
23
+ require_relative "documentation/builder"
24
+ require_relative "documentation/document"
25
+ require_relative "documentation/loader"
26
+ require_relative "documentation/openapi"
27
+ require_relative "documentation/generators"
@@ -1,150 +1,321 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module SpecForge
4
- # Pass into to_sentence
5
- OR_CONNECTOR = {
6
- last_word_connector: ", or ",
7
- two_words_connector: " or ",
8
- # This is a minor performance improvement to avoid locales being loaded
9
- # This will need to be removed if locales are added
10
- locale: false
11
- }.freeze
12
-
13
- private_constant :OR_CONNECTOR
14
-
15
- class Error < StandardError; end
16
-
17
4
  #
18
- # Raised by Attribute::Faker when a provided classname does not exist in Faker
5
+ # Base error class for all SpecForge-specific exceptions
19
6
  #
20
- class InvalidFakerClassError < Error
21
- CLASS_CHECKER = DidYouMean::SpellChecker.new(
22
- dictionary: Faker::Base.descendants.map { |c| c.to_s.downcase.gsub!("::", ".") }
23
- )
7
+ class Error < StandardError
8
+ # Pass into to_sentence
9
+ OR_CONNECTOR = {
10
+ last_word_connector: ", or ",
11
+ two_words_connector: " or ",
12
+ # This is a minor performance improvement to avoid locales being loaded
13
+ # This will need to be removed if locales are added
14
+ locale: false
15
+ }.freeze
24
16
 
25
- def initialize(input)
26
- corrections = CLASS_CHECKER.correct(input)
17
+ private_constant :OR_CONNECTOR
27
18
 
28
- super(<<~STRING.chomp
29
- Undefined Faker class "#{input}". #{DidYouMean::Formatter.message_for(corrections)}
30
-
31
- For available classes, please check https://github.com/faker-ruby/faker#generators.
32
- STRING
19
+ #
20
+ # Raised when a provided Faker class name doesn't exist
21
+ # Provides helpful suggestions for similar class names
22
+ #
23
+ # @example
24
+ # Attribute::Faker.new("faker.invalid.method")
25
+ # # => InvalidFakerClassError: Undefined Faker class "invalid". Did you mean? name, games, ...
26
+ #
27
+ class InvalidFakerClassError < Error
28
+ #
29
+ # A spell checker for Faker classes
30
+ #
31
+ # @return [DidYouMean::SpellChecker]
32
+ #
33
+ CLASS_CHECKER = DidYouMean::SpellChecker.new(
34
+ dictionary: Faker::Base.descendants.map { |c| c.to_s.downcase.gsub!("::", ".") }
33
35
  )
36
+
37
+ def initialize(input)
38
+ corrections = CLASS_CHECKER.correct(input)
39
+
40
+ super(<<~STRING.chomp
41
+ Undefined Faker class "#{input}". #{DidYouMean::Formatter.message_for(corrections)}
42
+
43
+ For available classes, please check https://github.com/faker-ruby/faker#generators.
44
+ STRING
45
+ )
46
+ end
34
47
  end
35
- end
36
48
 
37
- #
38
- # Raised by Attribute::Faker when a provided method for a Faker class does not exist.
39
- #
40
- class InvalidFakerMethodError < Error
41
- def initialize(input, klass)
42
- spell_checker = DidYouMean::SpellChecker.new(dictionary: klass.public_methods)
43
- corrections = spell_checker.correct(input)
49
+ #
50
+ # Raised when a provided method for a Faker class doesn't exist
51
+ # Provides helpful suggestions for similar method names
52
+ #
53
+ # @example
54
+ # Attribute::Faker.new("faker.name.invlaid")
55
+ # # => InvalidFakerMethodError: Undefined Faker method "invlaid" for "Faker::Name".
56
+ # Did you mean? first_name, last_name, ...
57
+ #
58
+ class InvalidFakerMethodError < Error
59
+ def initialize(input, klass)
60
+ spell_checker = DidYouMean::SpellChecker.new(dictionary: klass.public_methods)
61
+ corrections = spell_checker.correct(input)
44
62
 
45
- super(<<~STRING.chomp
46
- Undefined Faker method "#{input}" for "#{klass}". #{DidYouMean::Formatter.message_for(corrections)}
63
+ super(<<~STRING.chomp
64
+ Undefined Faker method "#{input}" for "#{klass}". #{DidYouMean::Formatter.message_for(corrections)}
47
65
 
48
- For available methods for this class, please check https://github.com/faker-ruby/faker#generators.
49
- STRING
50
- )
66
+ For available methods for this class, please check https://github.com/faker-ruby/faker#generators.
67
+ STRING
68
+ )
69
+ end
51
70
  end
52
- end
53
71
 
54
- #
55
- # Raised by Attribute::Transform when the provided transform function is not valid
56
- #
57
- class InvalidTransformFunctionError < Error
58
- def initialize(input)
59
- # TODO: Update link to docs
60
- super(<<~STRING.chomp
61
- Undefined transform function "#{input}".
62
-
63
- For available functions, please check https://github.com/itsthedevman/spec_forge.
64
- STRING
65
- )
72
+ #
73
+ # Raised when an unknown transform function is referenced
74
+ # Indicates when a transform name isn't supported
75
+ #
76
+ class InvalidTransformFunctionError < Error
77
+ def initialize(input)
78
+ # TODO: Update link to docs
79
+ super(<<~STRING.chomp
80
+ Undefined transform function "#{input}".
81
+
82
+ For available functions, please check https://github.com/itsthedevman/spec_forge.
83
+ STRING
84
+ )
85
+ end
66
86
  end
67
- end
68
87
 
69
- #
70
- # Raised by Attribute::Chainable when an step in the invocation chain is invalid
71
- #
72
- class InvalidInvocationError < Error
73
- def initialize(step, object)
74
- valid_operations =
75
- case object
76
- when ArrayLike
77
- "Array index (0, 1, 2, etc.) or any Array methods (first, last, size, etc.)"
78
- when HashLike
79
- "Any Hash key: #{object.keys.join(", ")}"
80
- else
81
- "Any method available on #{object.class}"
88
+ #
89
+ # Raised when a step in an invocation chain is invalid
90
+ # Provides detailed information about where in the chain the error occurred
91
+ #
92
+ # @example
93
+ # variable_attr = Attribute::Variable.new("variables.user.invalid_method")
94
+ # variable_attr.resolved
95
+ # # => InvalidInvocationError: Cannot invoke "invalid_method" on User
96
+ #
97
+ class InvalidInvocationError < Error
98
+ def initialize(step, object, resolution_path = {})
99
+ @step = step
100
+ @object = object
101
+ @resolution_path = resolution_path
102
+
103
+ object_class =
104
+ case object
105
+ when Data
106
+ object.class.name || "Data"
107
+ when Struct
108
+ object.class.name || "Struct"
109
+ else
110
+ object.class
111
+ end
112
+
113
+ super(<<~STRING.chomp
114
+ Cannot invoke "#{step}" on #{object_class}
115
+ #{resolution_path_message}
116
+ STRING
117
+ )
118
+ end
119
+
120
+ #
121
+ # Creates a new InvalidInvocationError with a new resolution path
122
+ #
123
+ # @param path [Hash] The steps taken up until this point
124
+ #
125
+ def with_resolution_path(path)
126
+ self.class.new(@step, @object, path)
127
+ end
128
+
129
+ private
130
+
131
+ def resolution_path_message
132
+ return "" if @resolution_path.empty?
133
+
134
+ message =
135
+ @resolution_path.map.with_index do |(path, description), index|
136
+ "#{index + 1}. #{path} --> #{description}"
137
+ end.join("\n")
138
+
139
+ "\nResolution path:\n#{message}"
140
+ end
141
+ end
142
+
143
+ #
144
+ # An extended version of TypeError with better error messages
145
+ # Makes it easier to understand type mismatches in the codebase
146
+ #
147
+ # @example
148
+ # raise Error::InvalidTypeError.new(123, String, for: "name parameter")
149
+ # # => Expected String, got Integer for name parameter
150
+ #
151
+ class InvalidTypeError < Error
152
+ def initialize(object, expected_type, **opts)
153
+ if expected_type.instance_of?(Array)
154
+ expected_type = expected_type.to_sentence(**OR_CONNECTOR)
82
155
  end
83
156
 
84
- super(<<~STRING.chomp
85
- Cannot invoke "#{step}" on #{object.class}.
157
+ message = "Expected #{expected_type}, got #{object.class}"
158
+ message += " for #{opts[:for]}" if opts[:for].present?
86
159
 
87
- Valid operations include: #{valid_operations}
88
- STRING
89
- )
160
+ super(message)
161
+ end
90
162
  end
91
- end
92
163
 
93
- #
94
- # An extended version of TypeError to make things easier when reporting invalid types
95
- #
96
- class InvalidTypeError < Error
97
- def initialize(object, expected_type, **opts)
98
- if expected_type.instance_of?(Array)
99
- expected_type = expected_type.to_sentence(**OR_CONNECTOR)
164
+ #
165
+ # Raised when a variable reference cannot be resolved
166
+ # Indicates when a spec or expectation references an undefined variable
167
+ #
168
+ class MissingVariableError < Error
169
+ def initialize(variable_name)
170
+ super("Undefined variable \"#{variable_name}\" referenced in expectation")
100
171
  end
172
+ end
101
173
 
102
- message = "Expected #{expected_type}, got #{object.class}"
103
- message += " for #{opts[:for]}" if opts[:for].present?
174
+ #
175
+ # Raised when a YAML structure doesn't match expectations
176
+ # Acts as a container for multiple validation errors
177
+ #
178
+ class InvalidStructureError < Error
179
+ def initialize(errors)
180
+ message = errors.to_a.join_map("\n") do |error|
181
+ next error if error.is_a?(SpecForge::Error)
182
+
183
+ # Normal errors, let's get verbose
184
+ backtrace = SpecForge.backtrace_cleaner.clean(error.backtrace)
185
+ "#{error.inspect}\n # ./#{backtrace.join("\n # ./")}\n"
186
+ end
104
187
 
105
- super(message)
188
+ super(message)
189
+ end
106
190
  end
107
- end
108
191
 
109
- #
110
- # Raised by Attribute::Variable when the provided variable name is not defined
111
- #
112
- class MissingVariableError < Error
113
- def initialize(variable_name)
114
- super("Undefined variable \"#{variable_name}\" referenced in expectation")
192
+ #
193
+ # Raised when an unknown factory build strategy is provided
194
+ # Indicates when a strategy string doesn't match supported options
195
+ #
196
+ class InvalidBuildStrategy < Error
197
+ def initialize(build_strategy)
198
+ valid_strategies = Attribute::Factory::BUILD_STRATEGIES.to_sentence(**OR_CONNECTOR)
199
+
200
+ super(<<~STRING.chomp
201
+ Unknown build strategy "#{build_strategy}" referenced in spec.
202
+
203
+ Valid strategies include: #{valid_strategies}
204
+ STRING
205
+ )
206
+ end
115
207
  end
116
- end
117
208
 
118
- #
119
- # Raised by Normalizer when any errors are returned. Acts like a grouping of errors
120
- #
121
- class InvalidStructureError < Error
122
- def initialize(errors)
123
- message = errors.to_a.join_map("\n") do |error|
124
- next error if error.is_a?(SpecForge::Error)
125
-
126
- # Normal errors, let's get verbose
127
- backtrace = SpecForge.backtrace_cleaner.clean(error.backtrace)
128
- "#{error.inspect}\n # ./#{backtrace.join("\n # ./")}\n"
209
+ #
210
+ # Raised when a spec file cannot be loaded
211
+ # Provides detailed information about the cause of the loading error
212
+ #
213
+ class SpecLoadError < Error
214
+ def initialize(error, file_path, spec: nil)
215
+ message =
216
+ if spec
217
+ "Error loading spec #{spec[:name].in_quotes} in file #{file_path.in_quotes} (line #{spec[:line_number]})"
218
+ else
219
+ "Error loading spec file #{file_path.in_quotes}"
220
+ end
221
+
222
+ causes = error.message.split("\n").map(&:strip).reject(&:empty?)
223
+
224
+ message +=
225
+ if causes.size > 1
226
+ "\nCauses:\n - #{causes.join_map("\n - ")}"
227
+ else
228
+ "\nCause: #{error}"
229
+ end
230
+
231
+ super(message)
129
232
  end
233
+ end
130
234
 
131
- super(message)
235
+ #
236
+ # Raised when the provided namespace is not defined on the global context
237
+ #
238
+ class InvalidGlobalNamespaceError < Error
239
+ def initialize(provided_namespace)
240
+ super("Invalid global namespace #{provided_namespace.in_quotes}. Currently supported namespaces are: \"variables\"")
241
+ end
132
242
  end
133
- end
134
243
 
135
- #
136
- # Raised by Attribute::Factory when an unknown build strategy is provided
137
- #
138
- class InvalidBuildStrategy < Error
139
- def initialize(build_strategy)
140
- valid_strategies = Attribute::Factory::BUILD_STRATEGIES.to_sentence(**OR_CONNECTOR)
244
+ #
245
+ # Raised when the provided matcher name does not defined with RSpec
246
+ #
247
+ class UndefinedMatcherError < Error
248
+ def initialize(matcher_name)
249
+ matcher_categories = {
250
+ Equality: ["matcher.eq", "matcher.eql", "matcher.equal"],
251
+ Types: ["kind_of.string", "kind_of.integer", "kind_of.array", "kind_of.hash"],
252
+ Truthiness: ["be.true", "be.false", "be.nil"],
253
+ Comparison: ["be.within", "be.between", "be.greater_than", "be.less_than"],
254
+ Collections: ["matcher.include", "matcher.contain_exactly", "matcher.all"],
255
+ Strings: ["/regex/", "matcher.start_with", "matcher.end_with"]
256
+ }
141
257
 
142
- super(<<~STRING.chomp
143
- Unknown build strategy "#{build_strategy}" referenced in spec.
258
+ formatted_categories =
259
+ matcher_categories.join_map("\n") do |category, matchers|
260
+ " #{category}: #{matchers.join(", ")}"
261
+ end
144
262
 
145
- Valid strategies include: #{valid_strategies}
146
- STRING
147
- )
263
+ super(<<~STRING.chomp
264
+ Undefined matcher method "#{matcher_name}" is not available in RSpec matchers.
265
+
266
+ Common matchers you can use:
267
+ #{formatted_categories}
268
+
269
+ For the complete list of available matchers, check the RSpec documentation:
270
+ https://rspec.info/documentation/3.12/rspec-expectations/RSpec/Matchers.html
271
+ STRING
272
+ )
273
+ end
274
+ end
275
+
276
+ #
277
+ # Raised when a callback is referenced in config but hasn't been defined
278
+ #
279
+ class UndefinedCallbackError < Error
280
+ def initialize(callback_name, available_callbacks = [])
281
+ message = "The callback #{callback_name.in_quotes} was referenced but hasn't been defined."
282
+
283
+ message +=
284
+ if available_callbacks.any?
285
+ <<~STR.chomp
286
+
287
+ Available callbacks are: #{available_callbacks.join_map(", ", &:in_quotes)}
288
+ STR
289
+ else
290
+ <<~STR.chomp
291
+
292
+ No callbacks have been defined yet. Register callbacks with:
293
+
294
+ SpecForge.register_callback(:#{callback_name}) do |context|
295
+ # Your callback code
296
+ end
297
+ STR
298
+ end
299
+
300
+ super(message)
301
+ end
302
+ end
303
+
304
+ #
305
+ # Raised when an OpenAPI specification fails validation
306
+ #
307
+ # This error indicates that the generated OpenAPI document contains
308
+ # validation errors according to the OpenAPI specification standards.
309
+ # It's typically raised after attempting to validate a generated specification.
310
+ #
311
+ # @example
312
+ # begin
313
+ # generator.validate!(openapi_spec)
314
+ # rescue SpecForge::Error::InvalidOASDocument
315
+ # puts "Generated specification has validation errors"
316
+ # end
317
+ #
318
+ class InvalidOASDocument < Error
148
319
  end
149
320
  end
150
321
  end