spec_forge 0.6.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 (83) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +112 -2
  3. data/README.md +133 -8
  4. data/flake.lock +3 -3
  5. data/flake.nix +3 -3
  6. data/lib/spec_forge/attribute/factory.rb +1 -1
  7. data/lib/spec_forge/callbacks.rb +9 -0
  8. data/lib/spec_forge/cli/docs/generate.rb +72 -0
  9. data/lib/spec_forge/cli/docs.rb +92 -0
  10. data/lib/spec_forge/cli/init.rb +39 -7
  11. data/lib/spec_forge/cli/new.rb +13 -3
  12. data/lib/spec_forge/cli/run.rb +12 -4
  13. data/lib/spec_forge/cli/serve.rb +155 -0
  14. data/lib/spec_forge/cli.rb +14 -6
  15. data/lib/spec_forge/configuration.rb +2 -2
  16. data/lib/spec_forge/context/store.rb +23 -40
  17. data/lib/spec_forge/core_ext/array.rb +27 -0
  18. data/lib/spec_forge/documentation/builder.rb +383 -0
  19. data/lib/spec_forge/documentation/document/operation.rb +47 -0
  20. data/lib/spec_forge/documentation/document/parameter.rb +22 -0
  21. data/lib/spec_forge/documentation/document/request_body.rb +24 -0
  22. data/lib/spec_forge/documentation/document/response.rb +39 -0
  23. data/lib/spec_forge/documentation/document/response_body.rb +27 -0
  24. data/lib/spec_forge/documentation/document.rb +48 -0
  25. data/lib/spec_forge/documentation/generators/base.rb +81 -0
  26. data/lib/spec_forge/documentation/generators/openapi/base.rb +100 -0
  27. data/lib/spec_forge/documentation/generators/openapi/error_formatter.rb +149 -0
  28. data/lib/spec_forge/documentation/generators/openapi/v3_0.rb +65 -0
  29. data/lib/spec_forge/documentation/generators/openapi.rb +59 -0
  30. data/lib/spec_forge/documentation/generators.rb +17 -0
  31. data/lib/spec_forge/documentation/loader/cache.rb +138 -0
  32. data/lib/spec_forge/documentation/loader.rb +159 -0
  33. data/lib/spec_forge/documentation/openapi/base.rb +33 -0
  34. data/lib/spec_forge/documentation/openapi/v3_0/example.rb +44 -0
  35. data/lib/spec_forge/documentation/openapi/v3_0/media_type.rb +42 -0
  36. data/lib/spec_forge/documentation/openapi/v3_0/operation.rb +175 -0
  37. data/lib/spec_forge/documentation/openapi/v3_0/response.rb +65 -0
  38. data/lib/spec_forge/documentation/openapi/v3_0/schema.rb +80 -0
  39. data/lib/spec_forge/documentation/openapi/v3_0/tag.rb +71 -0
  40. data/lib/spec_forge/documentation/openapi.rb +23 -0
  41. data/lib/spec_forge/documentation.rb +27 -0
  42. data/lib/spec_forge/error.rb +17 -0
  43. data/lib/spec_forge/factory.rb +2 -2
  44. data/lib/spec_forge/filter.rb +3 -4
  45. data/lib/spec_forge/forge.rb +5 -4
  46. data/lib/spec_forge/http/backend.rb +2 -0
  47. data/lib/spec_forge/http/request.rb +14 -3
  48. data/lib/spec_forge/loader.rb +14 -24
  49. data/lib/spec_forge/normalizer/default.rb +51 -0
  50. data/lib/spec_forge/normalizer/definition.rb +248 -0
  51. data/lib/spec_forge/normalizer/validators.rb +99 -0
  52. data/lib/spec_forge/normalizer.rb +356 -199
  53. data/lib/spec_forge/normalizers/_shared.yml +74 -0
  54. data/lib/spec_forge/normalizers/configuration.yml +23 -0
  55. data/lib/spec_forge/normalizers/constraint.yml +8 -0
  56. data/lib/spec_forge/normalizers/expectation.yml +47 -0
  57. data/lib/spec_forge/normalizers/factory.yml +12 -0
  58. data/lib/spec_forge/normalizers/factory_reference.yml +15 -0
  59. data/lib/spec_forge/normalizers/global_context.yml +28 -0
  60. data/lib/spec_forge/normalizers/spec.yml +50 -0
  61. data/lib/spec_forge/runner/adapter.rb +183 -0
  62. data/lib/spec_forge/runner/debug_proxy.rb +3 -3
  63. data/lib/spec_forge/runner/state.rb +4 -5
  64. data/lib/spec_forge/runner.rb +40 -124
  65. data/lib/spec_forge/spec/expectation/constraint.rb +13 -5
  66. data/lib/spec_forge/spec/expectation.rb +7 -3
  67. data/lib/spec_forge/spec.rb +13 -58
  68. data/lib/spec_forge/version.rb +1 -1
  69. data/lib/spec_forge.rb +30 -23
  70. data/lib/templates/openapi.yml.tt +22 -0
  71. data/lib/templates/redoc.html.tt +28 -0
  72. data/lib/templates/swagger.html.tt +59 -0
  73. metadata +92 -14
  74. data/lib/spec_forge/normalizer/configuration.rb +0 -90
  75. data/lib/spec_forge/normalizer/constraint.rb +0 -60
  76. data/lib/spec_forge/normalizer/expectation.rb +0 -105
  77. data/lib/spec_forge/normalizer/factory.rb +0 -78
  78. data/lib/spec_forge/normalizer/factory_reference.rb +0 -85
  79. data/lib/spec_forge/normalizer/global_context.rb +0 -88
  80. data/lib/spec_forge/normalizer/spec.rb +0 -97
  81. /data/lib/templates/{forge_helper.tt → forge_helper.rb.tt} +0 -0
  82. /data/lib/templates/{new_factory.tt → new_factory.yml.tt} +0 -0
  83. /data/lib/templates/{new_spec.tt → new_spec.yml.tt} +0 -0
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SpecForge
4
+ module Documentation
5
+ module OpenAPI
6
+ #
7
+ # Base class for OpenAPI documentation objects
8
+ #
9
+ # Provides common functionality for OpenAPI specification objects
10
+ # like operations, responses, and schemas.
11
+ #
12
+ class Base
13
+ #
14
+ # The document object containing structured API data
15
+ #
16
+ # @return [Object] The document with endpoint information
17
+ #
18
+ attr_reader :document
19
+
20
+ #
21
+ # Creates a new OpenAPI base object
22
+ #
23
+ # @param document [Object] The document object containing API data
24
+ #
25
+ # @return [Base] A new base instance
26
+ #
27
+ def initialize(document)
28
+ @document = document
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,44 @@
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 Example object
9
+ #
10
+ # Creates example objects for request/response documentation with
11
+ # optional summary, description, and external reference support.
12
+ #
13
+ # @see https://spec.openapis.org/oas/v3.0.4.html#example-object
14
+ #
15
+ class Example < Data.define(:summary, :description, :value, :external_value)
16
+ #
17
+ # Creates a new OpenAPI example object
18
+ #
19
+ # @param summary [String, nil] Brief summary of the example's purpose
20
+ # @param description [String, nil] Detailed description of the example
21
+ # @param value [Object, nil] The actual example value
22
+ # @param external_value [String, nil] URL pointing to the example value
23
+ #
24
+ # @return [Example] A new example instance
25
+ #
26
+ def initialize(summary: nil, description: nil, value: nil, external_value: nil)
27
+ super
28
+ end
29
+
30
+ #
31
+ # Converts the example to an OpenAPI-compliant hash
32
+ #
33
+ # @return [Hash] OpenAPI-formatted example object
34
+ #
35
+ def to_h
36
+ super
37
+ .rename_key_unordered!(:external_value, :externalValue)
38
+ .compact_blank!
39
+ end
40
+ end
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,42 @@
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 Media Type object
9
+ #
10
+ # Handles media type definitions for request and response bodies,
11
+ # including schema definitions, examples, and encoding information.
12
+ #
13
+ # @see https://spec.openapis.org/oas/v3.0.4.html#media-type-object
14
+ #
15
+ class MediaType < Data.define(:schema, :example, :examples, :encoding)
16
+ #
17
+ # Creates a new OpenAPI media type object
18
+ #
19
+ # @param schema [Hash, nil] Schema definition for the media type
20
+ # @param example [Object, nil] Single example value
21
+ # @param examples [Hash, nil] Multiple named examples
22
+ # @param encoding [Hash, nil] Encoding information for the media type
23
+ #
24
+ # @return [MediaType] A new media type instance
25
+ #
26
+ def initialize(schema: nil, example: nil, examples: nil, encoding: nil)
27
+ super
28
+ end
29
+
30
+ #
31
+ # Converts the media type to an OpenAPI-compliant hash
32
+ #
33
+ # @return [Hash] OpenAPI-formatted media type object
34
+ #
35
+ def to_h
36
+ super.compact_blank!
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,175 @@
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 Operation object
9
+ #
10
+ # Handles the complete definition of API operations including parameters,
11
+ # request bodies, responses, and security requirements for OpenAPI specs.
12
+ #
13
+ # @see https://spec.openapis.org/oas/v3.0.4.html#operation-object
14
+ #
15
+ class Operation < OpenAPI::Base
16
+ #
17
+ # Converts the operation to an OpenAPI-compliant hash
18
+ #
19
+ # Builds the complete operation object with all required and optional
20
+ # fields properly formatted for OpenAPI specification.
21
+ #
22
+ # @return [Hash] OpenAPI-formatted operation object
23
+ #
24
+ def to_h
25
+ {
26
+ # Required
27
+ responses:,
28
+ security:
29
+ }.merge_compact(
30
+ # All optional
31
+ tags:,
32
+ summary:,
33
+ description:,
34
+ operationId:,
35
+ parameters:,
36
+ requestBody:
37
+ )
38
+ end
39
+
40
+ #
41
+ # Returns the operation's unique identifier
42
+ #
43
+ # @return [String] The operation ID
44
+ #
45
+ def id
46
+ # The object ID is added to make every ID unique
47
+ document.id.to_camelcase(:lower) + object_id.to_s
48
+ end
49
+
50
+ alias_method :operationId, :id
51
+
52
+ #
53
+ # Returns a human-readable summary of the operation
54
+ #
55
+ # @return [String, nil] Brief operation summary
56
+ #
57
+ def summary
58
+ document.id.humanize
59
+ end
60
+
61
+ #
62
+ # Returns detailed description of the operation
63
+ #
64
+ # @return [String] Detailed operation description
65
+ #
66
+ def description
67
+ document.description
68
+ end
69
+
70
+ #
71
+ # Returns security requirements for the operation
72
+ #
73
+ # @return [Array] Array of security requirement objects
74
+ #
75
+ def security
76
+ # User defined
77
+ []
78
+ end
79
+
80
+ #
81
+ # Returns tags for categorizing the operation
82
+ #
83
+ # @return [Array] Array of tag names
84
+ #
85
+ def tags
86
+ # User defined
87
+ []
88
+ end
89
+
90
+ #
91
+ # Returns parameter definitions for the operation
92
+ #
93
+ # Transforms document parameters into OpenAPI parameter objects
94
+ # with proper schema types and location information.
95
+ #
96
+ # @return [Array] Array of parameter objects
97
+ #
98
+ def parameters
99
+ document.parameters.values.map do |parameter|
100
+ schema = Schema.new(type: parameter.type).to_h
101
+
102
+ {
103
+ schema:,
104
+ name: parameter.name,
105
+ in: parameter.location,
106
+ required: parameter.location == "path" || false
107
+ }
108
+ end
109
+ end
110
+
111
+ #
112
+ # Returns request body definition for the operation
113
+ #
114
+ # Groups requests by content type and creates proper OpenAPI
115
+ # request body object with examples and schemas.
116
+ #
117
+ # @return [Hash, nil] Request body object
118
+ #
119
+ def request_body
120
+ requests = document.requests
121
+ return if requests.blank?
122
+
123
+ requests = requests.group_by(&:content_type)
124
+
125
+ content =
126
+ requests.transform_values do |grouped_requests|
127
+ media_type_from_requests(grouped_requests)
128
+ end
129
+
130
+ {
131
+ description: "",
132
+ content:
133
+ }
134
+ end
135
+
136
+ alias_method :requestBody, :request_body
137
+
138
+ #
139
+ # Returns response definitions for the operation
140
+ #
141
+ # Groups responses by status code and transforms them into
142
+ # OpenAPI response objects with proper formatting.
143
+ #
144
+ # @return [Hash] Hash mapping status codes to response objects
145
+ #
146
+ def responses
147
+ document.responses
148
+ .group_by(&:status)
149
+ .transform_values! do |responses|
150
+ response = responses.first
151
+ Response.new(response).to_h
152
+ end
153
+ end
154
+
155
+ private
156
+
157
+ def media_type_from_requests(requests)
158
+ request = requests.first
159
+ schema = Schema.new(type: request.type, content: request.content).to_h
160
+
161
+ examples =
162
+ requests.to_h do |request|
163
+ example_name = request.name.to_camelcase(:lower)
164
+ example = Example.new(summary: request.name, value: request.content).to_h
165
+
166
+ [example_name, example]
167
+ end
168
+
169
+ MediaType.new(schema:, examples:).to_h
170
+ end
171
+ end
172
+ end
173
+ end
174
+ end
175
+ end
@@ -0,0 +1,65 @@
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 Response object
9
+ #
10
+ # Handles response definitions including status descriptions, content types,
11
+ # headers, and links for OpenAPI specifications.
12
+ #
13
+ # @see https://spec.openapis.org/oas/v3.0.4.html#response-object
14
+ #
15
+ class Response < OpenAPI::Base
16
+ #
17
+ # Converts the response to an OpenAPI-compliant hash
18
+ #
19
+ # Builds the complete response object with required description and
20
+ # optional content, headers, and links.
21
+ #
22
+ # @return [Hash] OpenAPI-formatted response object
23
+ #
24
+ def to_h
25
+ {
26
+ # Required
27
+ description: "",
28
+ content:
29
+ }.merge_compact(
30
+ # Optional
31
+ headers:
32
+ )
33
+ end
34
+
35
+ #
36
+ # Returns content definitions for the response
37
+ #
38
+ # Creates media type objects with schemas and merges with any
39
+ # documentation-provided content definitions.
40
+ #
41
+ # @return [Hash] Content definitions by media type
42
+ #
43
+ def content
44
+ schema = Schema.new(type: document.body.type).to_h
45
+
46
+ {
47
+ document.content_type => MediaType.new(schema:).to_h
48
+ }
49
+ end
50
+
51
+ #
52
+ # Returns header definitions for the response
53
+ #
54
+ # Merges document headers with documentation-provided headers.
55
+ #
56
+ # @return [Hash, nil] Header definitions
57
+ #
58
+ def headers
59
+ document.headers.presence
60
+ end
61
+ end
62
+ end
63
+ end
64
+ end
65
+ end
@@ -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"
@@ -300,5 +300,22 @@ module SpecForge
300
300
  super(message)
301
301
  end
302
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
319
+ end
303
320
  end
304
321
  end
@@ -33,7 +33,7 @@ module SpecForge
33
33
  factories = []
34
34
 
35
35
  Dir[path].each do |file_path|
36
- hash = YAML.load_file(file_path).deep_symbolize_keys
36
+ hash = YAML.load_file(file_path, symbolize_names: true)
37
37
 
38
38
  hash.each do |factory_name, factory_hash|
39
39
  factory_hash[:name] = factory_name
@@ -72,7 +72,7 @@ module SpecForge
72
72
  #
73
73
  def initialize(name:, **input)
74
74
  @name = name
75
- input = Normalizer.normalize_factory!(input)
75
+ input = Normalizer.normalize!(input, using: :factory)
76
76
 
77
77
  @input = input
78
78
  @model_class = input[:model_class]
@@ -22,7 +22,7 @@ module SpecForge
22
22
  # @param expectation_name [String, nil] Optional expectation name that was used by the filter
23
23
  #
24
24
  def announce(forges, file_name:, spec_name:, expectation_name:)
25
- filters = {file_name:, spec_name:, expectation_name:}.reject { |k, v| v.blank? }
25
+ filters = {file_name:, spec_name:, expectation_name:}.compact_blank
26
26
  return if filters.size == 0
27
27
 
28
28
  filters_display = filters.join_map(", ") { |k, v| "#{k.in_quotes} => #{v.in_quotes}" }
@@ -69,10 +69,9 @@ module SpecForge
69
69
  # Expectation filter
70
70
  next spec unless expectation_name
71
71
 
72
- expectations = spec.expectations.select { |e| e.name == expectation_name }
73
- next if expectations.empty?
72
+ spec.expectations.select! { |e| e.name == expectation_name }
73
+ next if spec.expectations.empty?
74
74
 
75
- spec.expectations = expectations
76
75
  spec
77
76
  end
78
77
 
@@ -117,8 +117,7 @@ module SpecForge
117
117
  # }
118
118
  #
119
119
  specs.each_with_object({}) do |spec, hash|
120
- overlay = spec[:expectations].to_h { |e| [e[:id], e.delete(:variables)] }
121
- .reject { |_k, v| v.blank? }
120
+ overlay = spec[:expectations].to_h { |e| [e[:id], e.delete(:variables)] }.compact_blank
122
121
 
123
122
  hash[spec[:id]] = {base: spec.delete(:variables), overlay:}
124
123
  end
@@ -153,13 +152,15 @@ module SpecForge
153
152
  overlay = spec[:expectations].to_h do |expectation|
154
153
  [
155
154
  expectation[:id],
156
- expectation.extract!(*HTTP::REQUEST_ATTRIBUTES).reject { |_k, v| v.blank? }
155
+ expectation.extract!(*HTTP::REQUEST_ATTRIBUTES).compact_blank
157
156
  ]
158
157
  end
159
158
 
160
- overlay.reject! { |_k, v| v.blank? }
159
+ overlay.compact_blank!
161
160
 
162
161
  base = spec.extract!(*HTTP::REQUEST_ATTRIBUTES)
162
+ base.compact_blank!
163
+
163
164
  base = config.deep_merge(base)
164
165
  base[:http_verb] ||= "GET"
165
166
 
@@ -144,6 +144,8 @@ module SpecForge
144
144
  #
145
145
  def update_request(request, headers, query, body)
146
146
  request.headers.merge!(headers)
147
+ request.headers.transform_values!(&:to_s)
148
+
147
149
  request.params.merge!(query)
148
150
  request.body = body.to_json
149
151
  end