spec_forge 0.1.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 +7 -0
- data/.envrc +1 -0
- data/.rspec +1 -0
- data/.standard.yml +3 -0
- data/CHANGELOG.md +5 -0
- data/CODE_OF_CONDUCT.md +132 -0
- data/LICENSE.txt +21 -0
- data/README.md +426 -0
- data/Rakefile +6 -0
- data/bin/spec_forge +5 -0
- data/flake.lock +61 -0
- data/flake.nix +41 -0
- data/lib/spec_forge/attribute/chainable.rb +86 -0
- data/lib/spec_forge/attribute/factory.rb +63 -0
- data/lib/spec_forge/attribute/faker.rb +54 -0
- data/lib/spec_forge/attribute/literal.rb +27 -0
- data/lib/spec_forge/attribute/matcher.rb +118 -0
- data/lib/spec_forge/attribute/parameterized.rb +76 -0
- data/lib/spec_forge/attribute/resolvable.rb +21 -0
- data/lib/spec_forge/attribute/resolvable_array.rb +24 -0
- data/lib/spec_forge/attribute/resolvable_hash.rb +24 -0
- data/lib/spec_forge/attribute/transform.rb +39 -0
- data/lib/spec_forge/attribute/variable.rb +36 -0
- data/lib/spec_forge/attribute.rb +208 -0
- data/lib/spec_forge/cli/actions.rb +23 -0
- data/lib/spec_forge/cli/command.rb +127 -0
- data/lib/spec_forge/cli/init.rb +29 -0
- data/lib/spec_forge/cli/new.rb +161 -0
- data/lib/spec_forge/cli/run.rb +17 -0
- data/lib/spec_forge/cli.rb +43 -0
- data/lib/spec_forge/config.rb +84 -0
- data/lib/spec_forge/environment.rb +71 -0
- data/lib/spec_forge/error.rb +150 -0
- data/lib/spec_forge/factory.rb +104 -0
- data/lib/spec_forge/http/backend.rb +106 -0
- data/lib/spec_forge/http/client.rb +33 -0
- data/lib/spec_forge/http/request.rb +93 -0
- data/lib/spec_forge/http/verb.rb +118 -0
- data/lib/spec_forge/http.rb +6 -0
- data/lib/spec_forge/normalizer/config.rb +104 -0
- data/lib/spec_forge/normalizer/constraint.rb +47 -0
- data/lib/spec_forge/normalizer/expectation.rb +85 -0
- data/lib/spec_forge/normalizer/factory.rb +65 -0
- data/lib/spec_forge/normalizer/factory_reference.rb +66 -0
- data/lib/spec_forge/normalizer/spec.rb +73 -0
- data/lib/spec_forge/normalizer.rb +183 -0
- data/lib/spec_forge/runner.rb +91 -0
- data/lib/spec_forge/spec/expectation/constraint.rb +52 -0
- data/lib/spec_forge/spec/expectation.rb +53 -0
- data/lib/spec_forge/spec.rb +77 -0
- data/lib/spec_forge/type.rb +45 -0
- data/lib/spec_forge/version.rb +5 -0
- data/lib/spec_forge.rb +90 -0
- data/lib/templates/config.tt +19 -0
- data/spec_forge/config.yml +19 -0
- data/spec_forge/factories/user.yml +4 -0
- data/spec_forge/specs/users.yml +63 -0
- metadata +234 -0
@@ -0,0 +1,71 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module SpecForge
|
4
|
+
class Environment
|
5
|
+
attr_reader :environment, :framework
|
6
|
+
|
7
|
+
#
|
8
|
+
# Creates a new environment loader
|
9
|
+
#
|
10
|
+
# @param environment [Config::Environment] The environment to load
|
11
|
+
#
|
12
|
+
def initialize(environment = SpecForge.config.environment)
|
13
|
+
@environment = environment
|
14
|
+
@framework = environment.use
|
15
|
+
end
|
16
|
+
|
17
|
+
#
|
18
|
+
# Loads the environment
|
19
|
+
#
|
20
|
+
def load
|
21
|
+
load_framework
|
22
|
+
load_preload
|
23
|
+
|
24
|
+
self
|
25
|
+
end
|
26
|
+
|
27
|
+
private
|
28
|
+
|
29
|
+
def load_framework
|
30
|
+
case framework
|
31
|
+
when "rails"
|
32
|
+
load_rails
|
33
|
+
else
|
34
|
+
load_generic
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
def load_rails
|
39
|
+
path = SpecForge.root.join("config", "environment.rb")
|
40
|
+
|
41
|
+
if File.exist?(path)
|
42
|
+
require path
|
43
|
+
else
|
44
|
+
warn <<~WARNING.chomp
|
45
|
+
SpecForge warning: Config attribute "environment" set to "rails" but Rails environment (config/environment.rb) does not exist.
|
46
|
+
Factories or model-dependent features may not function as expected.
|
47
|
+
- For non-Rails projects, set your environment's 'models_path' or 'preload' in your config.yml
|
48
|
+
- To disable this warning, set 'environment: ""' in your config.yml.
|
49
|
+
WARNING
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
def load_generic
|
54
|
+
return unless environment.models_path? && environment.models_path.present?
|
55
|
+
|
56
|
+
models_path = SpecForge.root.join(environment.models_path)
|
57
|
+
return if !File.exist?(models_path)
|
58
|
+
|
59
|
+
Dir[models_path.join("**/*.rb")].each { |file| require file }
|
60
|
+
end
|
61
|
+
|
62
|
+
def load_preload
|
63
|
+
return unless environment.preload? && environment.preload.present?
|
64
|
+
|
65
|
+
path = SpecForge.root.join(environment.preload)
|
66
|
+
return if !File.exist?(path)
|
67
|
+
|
68
|
+
require path
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
@@ -0,0 +1,150 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
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
|
+
#
|
18
|
+
# Raised by Attribute::Faker when a provided classname does not exist in Faker
|
19
|
+
#
|
20
|
+
class InvalidFakerClassError < Error
|
21
|
+
CLASS_CHECKER = DidYouMean::SpellChecker.new(
|
22
|
+
dictionary: Faker::Base.descendants.map { |c| c.to_s.downcase.gsub!("::", ".") }
|
23
|
+
)
|
24
|
+
|
25
|
+
def initialize(input)
|
26
|
+
corrections = CLASS_CHECKER.correct(input)
|
27
|
+
|
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
|
33
|
+
)
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
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)
|
44
|
+
|
45
|
+
super(<<~STRING.chomp
|
46
|
+
Undefined Faker method "#{input}" for "#{klass}". #{DidYouMean::Formatter.message_for(corrections)}
|
47
|
+
|
48
|
+
For available methods for this class, please check https://github.com/faker-ruby/faker#generators.
|
49
|
+
STRING
|
50
|
+
)
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
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
|
+
)
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
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}"
|
82
|
+
end
|
83
|
+
|
84
|
+
super(<<~STRING.chomp
|
85
|
+
Cannot invoke "#{step}" on #{object.class}.
|
86
|
+
|
87
|
+
Valid operations include: #{valid_operations}
|
88
|
+
STRING
|
89
|
+
)
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
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)
|
100
|
+
end
|
101
|
+
|
102
|
+
message = "Expected #{expected_type}, got #{object.class}"
|
103
|
+
message += " for #{opts[:for]}" if opts[:for].present?
|
104
|
+
|
105
|
+
super(message)
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
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")
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
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"
|
129
|
+
end
|
130
|
+
|
131
|
+
super(message)
|
132
|
+
end
|
133
|
+
end
|
134
|
+
|
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)
|
141
|
+
|
142
|
+
super(<<~STRING.chomp
|
143
|
+
Unknown build strategy "#{build_strategy}" referenced in spec.
|
144
|
+
|
145
|
+
Valid strategies include: #{valid_strategies}
|
146
|
+
STRING
|
147
|
+
)
|
148
|
+
end
|
149
|
+
end
|
150
|
+
end
|
@@ -0,0 +1,104 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module SpecForge
|
4
|
+
class Factory
|
5
|
+
#
|
6
|
+
# Loads the factories from their yml files and registers them with FactoryBot
|
7
|
+
#
|
8
|
+
# @param path [String, Path] The base path where the factories directory are located
|
9
|
+
#
|
10
|
+
def self.load_and_register(base_path)
|
11
|
+
if (paths = SpecForge.config.factories.paths) && paths.size > 0
|
12
|
+
FactoryBot.definition_file_paths = paths
|
13
|
+
end
|
14
|
+
|
15
|
+
FactoryBot.find_definitions if SpecForge.config.factories.auto_discover?
|
16
|
+
|
17
|
+
factories = load_from_path(base_path.join("factories", "**/*.yml"))
|
18
|
+
factories.each(&:register)
|
19
|
+
end
|
20
|
+
|
21
|
+
#
|
22
|
+
# Loads any factories defined in the path. A single file can contain one or more factories
|
23
|
+
#
|
24
|
+
# @param path [String, Path] The path where the factories are located
|
25
|
+
#
|
26
|
+
# @return [Array<Factory>] An array of factories that were loaded.
|
27
|
+
# Note: This factories have not been registered with FactoryBot.
|
28
|
+
# See #register
|
29
|
+
#
|
30
|
+
def self.load_from_path(path)
|
31
|
+
factories = []
|
32
|
+
|
33
|
+
Dir[path].map do |file_path|
|
34
|
+
hash = YAML.load_file(file_path).deep_symbolize_keys
|
35
|
+
|
36
|
+
hash.each do |factory_name, factory_hash|
|
37
|
+
factory_hash[:name] = factory_name
|
38
|
+
|
39
|
+
factories << new(**factory_hash)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
factories
|
44
|
+
end
|
45
|
+
|
46
|
+
############################################################################
|
47
|
+
|
48
|
+
attr_reader :name, :input, :model_class, :variables, :attributes
|
49
|
+
|
50
|
+
#
|
51
|
+
# Creates a new Factory
|
52
|
+
#
|
53
|
+
# @param name [String] The name of the factory
|
54
|
+
# @param **input [Hash] Attributes to define the factory. See Normalizer::Factory
|
55
|
+
#
|
56
|
+
def initialize(name:, **input)
|
57
|
+
@name = name
|
58
|
+
input = Normalizer.normalize_factory!(input)
|
59
|
+
|
60
|
+
@input = input
|
61
|
+
@model_class = input[:model_class]
|
62
|
+
|
63
|
+
@variables = extract_variables(input)
|
64
|
+
@attributes = extract_attributes(input)
|
65
|
+
end
|
66
|
+
|
67
|
+
#
|
68
|
+
# Registers this factory with FactoryBot.
|
69
|
+
# Once registered, you can call FactoryBot.build and other methods
|
70
|
+
#
|
71
|
+
# @return [Self]
|
72
|
+
#
|
73
|
+
def register
|
74
|
+
dsl = FactoryBot::Syntax::Default::DSL.new
|
75
|
+
|
76
|
+
options = {}
|
77
|
+
options[:class] = model_class if model_class
|
78
|
+
|
79
|
+
# This creates the factory in FactoryBot
|
80
|
+
factory_forge = self
|
81
|
+
dsl.factory(name, options) do
|
82
|
+
factory_forge.attributes.each do |name, attribute|
|
83
|
+
add_attribute(name, &attribute.to_proc)
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
self
|
88
|
+
end
|
89
|
+
|
90
|
+
private
|
91
|
+
|
92
|
+
def extract_variables(input)
|
93
|
+
variables = Attribute.from(input[:variables])
|
94
|
+
|
95
|
+
# Update the variables that reference other variables lol
|
96
|
+
Attribute.bind_variables(variables, variables)
|
97
|
+
end
|
98
|
+
|
99
|
+
def extract_attributes(input)
|
100
|
+
attributes = Attribute.from(input[:attributes])
|
101
|
+
Attribute.bind_variables(attributes, variables)
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|
@@ -0,0 +1,106 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module SpecForge
|
4
|
+
module HTTP
|
5
|
+
class Backend
|
6
|
+
attr_reader :connection
|
7
|
+
|
8
|
+
#
|
9
|
+
# Configures Faraday with the config values
|
10
|
+
#
|
11
|
+
# @param request [HTTP::Request]
|
12
|
+
#
|
13
|
+
def initialize(request)
|
14
|
+
@connection =
|
15
|
+
Faraday.new(url: request.base_url) do |builder|
|
16
|
+
# Authorization
|
17
|
+
builder.headers[request.authorization.header] = request.authorization.value
|
18
|
+
|
19
|
+
# Content-Type
|
20
|
+
if !request.headers.key?("Content-Type")
|
21
|
+
builder.request :json
|
22
|
+
builder.response :json
|
23
|
+
end
|
24
|
+
|
25
|
+
# Headers / Content Type
|
26
|
+
builder.headers.merge!(request.headers)
|
27
|
+
|
28
|
+
# Params
|
29
|
+
builder.params.merge!(request.query.resolve)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
#
|
34
|
+
# Executes a DELETE request to <base_url>/<provided_url>
|
35
|
+
#
|
36
|
+
# @param url [String] The URL path to DELETE
|
37
|
+
# @param query [Hash] Any query attributes to send
|
38
|
+
# @param body [Hash] Any body data to send
|
39
|
+
#
|
40
|
+
# @return [Hash] The response
|
41
|
+
#
|
42
|
+
def delete(url, query: {}, body: {})
|
43
|
+
connection.delete(url) { |request| update_request(request, query, body) }
|
44
|
+
end
|
45
|
+
|
46
|
+
#
|
47
|
+
# Executes a GET request to <base_url>/<provided_url>
|
48
|
+
#
|
49
|
+
# @param url [String] The URL path to GET
|
50
|
+
# @param query [Hash] Any query attributes to send
|
51
|
+
# @param body [Hash] Any body data to send
|
52
|
+
#
|
53
|
+
# @return [Hash] The response
|
54
|
+
#
|
55
|
+
def get(url, query: {}, body: {})
|
56
|
+
connection.get(url) { |request| update_request(request, query, body) }
|
57
|
+
end
|
58
|
+
|
59
|
+
#
|
60
|
+
# Executes a PATCH request to <base_url>/<provided_url>
|
61
|
+
#
|
62
|
+
# @param url [String] The URL path to PATCH
|
63
|
+
# @param query [Hash] Any query attributes to send
|
64
|
+
# @param body [Hash] Any body data to send
|
65
|
+
#
|
66
|
+
# @return [Hash] The response
|
67
|
+
#
|
68
|
+
def patch(url, query: {}, body: {})
|
69
|
+
connection.patch(url) { |request| update_request(request, query, body) }
|
70
|
+
end
|
71
|
+
|
72
|
+
#
|
73
|
+
# Executes a POST request to <base_url>/<provided_url>
|
74
|
+
#
|
75
|
+
# @param url [String] The URL path to POST
|
76
|
+
# @param query [Hash] Any query attributes to send
|
77
|
+
# @param body [Hash] Any body data to send
|
78
|
+
#
|
79
|
+
# @return [Hash] The response
|
80
|
+
#
|
81
|
+
def post(url, query: {}, body: {})
|
82
|
+
connection.post(url) { |request| update_request(request, query, body) }
|
83
|
+
end
|
84
|
+
|
85
|
+
#
|
86
|
+
# Executes a PUT request to <base_url>/<provided_url>
|
87
|
+
#
|
88
|
+
# @param url [String] The URL path to PUT
|
89
|
+
# @param query [Hash] Any query attributes to send
|
90
|
+
# @param body [Hash] Any body data to send
|
91
|
+
#
|
92
|
+
# @return [Hash] The response
|
93
|
+
#
|
94
|
+
def put(url, query: {}, body: {})
|
95
|
+
connection.put(url) { |request| update_request(request, query, body) }
|
96
|
+
end
|
97
|
+
|
98
|
+
private
|
99
|
+
|
100
|
+
def update_request(request, query, body)
|
101
|
+
request.params.merge!(query)
|
102
|
+
request.body = body.to_json
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module SpecForge
|
4
|
+
module HTTP
|
5
|
+
class Client
|
6
|
+
attr_reader :request
|
7
|
+
|
8
|
+
#
|
9
|
+
# Creates a new HTTP client to middleman between the tests and the backend
|
10
|
+
#
|
11
|
+
# @param ** [Hash] Request attributes
|
12
|
+
#
|
13
|
+
def initialize(**)
|
14
|
+
@request = Request.new(**)
|
15
|
+
@adapter = Backend.new(request)
|
16
|
+
end
|
17
|
+
|
18
|
+
#
|
19
|
+
# Triggers an HTTP request to the URL
|
20
|
+
#
|
21
|
+
# @return [Hash] The response
|
22
|
+
#
|
23
|
+
def call
|
24
|
+
@adapter.public_send(
|
25
|
+
request.http_verb,
|
26
|
+
request.url,
|
27
|
+
query: request.query.transform_values(&:resolve),
|
28
|
+
body: request.body.transform_values(&:resolve)
|
29
|
+
)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,93 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module SpecForge
|
4
|
+
module HTTP
|
5
|
+
attributes = [:base_url, :url, :http_method, :headers, :query, :body, :authorization]
|
6
|
+
|
7
|
+
class Request < Data.define(*attributes)
|
8
|
+
HEADER = /^[A-Z][A-Za-z0-9!-]*$/
|
9
|
+
|
10
|
+
#
|
11
|
+
# Initializes a new Request instance with the given options
|
12
|
+
#
|
13
|
+
# @param [Hash] options The options to create the Request with
|
14
|
+
#
|
15
|
+
# @option options [String] :url The request URL
|
16
|
+
#
|
17
|
+
# @option options [String, Verb] :http_method The HTTP method to use
|
18
|
+
#
|
19
|
+
# @option options [Hash] :headers Any headers
|
20
|
+
#
|
21
|
+
# @option options [Hash] :query The query parameters for the request (defaults to {})
|
22
|
+
#
|
23
|
+
# @option options [Hash] :body The request body (defaults to {})
|
24
|
+
#
|
25
|
+
def initialize(**options)
|
26
|
+
base_url = extract_base_url(options)
|
27
|
+
url = extract_url(options)
|
28
|
+
headers = normalize_headers(options)
|
29
|
+
http_method = normalize_http_method(options)
|
30
|
+
query = normalize_query(options)
|
31
|
+
body = normalize_body(options)
|
32
|
+
authorization = extract_authorization(options)
|
33
|
+
|
34
|
+
super(base_url:, url:, http_method:, headers:, query:, body:, authorization:)
|
35
|
+
end
|
36
|
+
|
37
|
+
def http_verb
|
38
|
+
http_method.name.downcase
|
39
|
+
end
|
40
|
+
|
41
|
+
private
|
42
|
+
|
43
|
+
def extract_base_url(options)
|
44
|
+
options[:base_url]&.value&.presence || SpecForge.config.base_url
|
45
|
+
end
|
46
|
+
|
47
|
+
def extract_url(options)
|
48
|
+
options[:url].value
|
49
|
+
end
|
50
|
+
|
51
|
+
def normalize_http_method(options)
|
52
|
+
method = options[:http_method].value.presence || "GET"
|
53
|
+
|
54
|
+
if method.is_a?(String)
|
55
|
+
Verb.from(method)
|
56
|
+
else
|
57
|
+
method
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
def normalize_headers(options)
|
62
|
+
headers = options[:headers].transform_keys do |key|
|
63
|
+
key = key.to_s
|
64
|
+
|
65
|
+
# If the key is already like a header, don't change it
|
66
|
+
if key.match?(HEADER)
|
67
|
+
key
|
68
|
+
else
|
69
|
+
# content_type => Content-Type
|
70
|
+
key.downcase.titleize.gsub(/\s+/, "-")
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
headers = Attribute.bind_variables(headers, options[:variables])
|
75
|
+
Attribute::ResolvableHash.new(headers)
|
76
|
+
end
|
77
|
+
|
78
|
+
def normalize_query(options)
|
79
|
+
query = Attribute.bind_variables(options[:query], options[:variables])
|
80
|
+
Attribute::ResolvableHash.new(query)
|
81
|
+
end
|
82
|
+
|
83
|
+
def normalize_body(options)
|
84
|
+
body = Attribute.bind_variables(options[:body], options[:variables])
|
85
|
+
Attribute::ResolvableHash.new(body)
|
86
|
+
end
|
87
|
+
|
88
|
+
def extract_authorization(options)
|
89
|
+
SpecForge.config.authorization.default
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
@@ -0,0 +1,118 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module SpecForge
|
4
|
+
module HTTP
|
5
|
+
class Verb < Data.define(:name)
|
6
|
+
class Delete < Verb
|
7
|
+
def initialize = super(name: "DELETE")
|
8
|
+
end
|
9
|
+
|
10
|
+
class Get < Verb
|
11
|
+
def initialize = super(name: "GET")
|
12
|
+
end
|
13
|
+
|
14
|
+
class Patch < Verb
|
15
|
+
def initialize = super(name: "PATCH")
|
16
|
+
end
|
17
|
+
|
18
|
+
class Post < Verb
|
19
|
+
def initialize = super(name: "POST")
|
20
|
+
end
|
21
|
+
|
22
|
+
class Put < Verb
|
23
|
+
def initialize = super(name: "PUT")
|
24
|
+
end
|
25
|
+
|
26
|
+
DELETE = Delete.new
|
27
|
+
GET = Get.new
|
28
|
+
PATCH = Patch.new
|
29
|
+
POST = Post.new
|
30
|
+
PUT = Put.new
|
31
|
+
|
32
|
+
VERBS = {
|
33
|
+
delete: DELETE,
|
34
|
+
get: GET,
|
35
|
+
patch: PATCH,
|
36
|
+
post: POST,
|
37
|
+
put: PUT
|
38
|
+
}.freeze
|
39
|
+
|
40
|
+
#
|
41
|
+
# Retrieves the corresponding Verb instance based on the provided HTTP name
|
42
|
+
#
|
43
|
+
# @param name [String, Symbol] The HTTP name to look up (case-insensitive)
|
44
|
+
#
|
45
|
+
# @return [Verb, nil] The corresponding Verb instance, or nil if not found
|
46
|
+
#
|
47
|
+
def self.from(name)
|
48
|
+
VERBS[name.downcase.to_sym]
|
49
|
+
end
|
50
|
+
|
51
|
+
#
|
52
|
+
# Returns if this Verb name matches another Verb's name, or the name
|
53
|
+
# as a String or Symbol
|
54
|
+
#
|
55
|
+
# @param other [Object] The thing to check against this object
|
56
|
+
#
|
57
|
+
# @return [Boolean]
|
58
|
+
#
|
59
|
+
def ==(other)
|
60
|
+
case other
|
61
|
+
when Verb
|
62
|
+
name == other.name
|
63
|
+
when String, Symbol
|
64
|
+
self == self.class.from(other)
|
65
|
+
else
|
66
|
+
false
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
alias_method :to_s, :name
|
71
|
+
|
72
|
+
#
|
73
|
+
# Returns if this Verb is a DELETE
|
74
|
+
#
|
75
|
+
# @return [Boolean]
|
76
|
+
#
|
77
|
+
def delete?
|
78
|
+
name == "DELETE"
|
79
|
+
end
|
80
|
+
|
81
|
+
#
|
82
|
+
# Returns if this Verb is a GET
|
83
|
+
#
|
84
|
+
# @return [Boolean]
|
85
|
+
#
|
86
|
+
def get?
|
87
|
+
name == "GET"
|
88
|
+
end
|
89
|
+
|
90
|
+
#
|
91
|
+
# Returns if this Verb is a PATCH
|
92
|
+
#
|
93
|
+
# @return [Boolean]
|
94
|
+
#
|
95
|
+
def patch?
|
96
|
+
name == "PATCH"
|
97
|
+
end
|
98
|
+
|
99
|
+
#
|
100
|
+
# Returns if this Verb is a POST
|
101
|
+
#
|
102
|
+
# @return [Boolean]
|
103
|
+
#
|
104
|
+
def post?
|
105
|
+
name == "POST"
|
106
|
+
end
|
107
|
+
|
108
|
+
#
|
109
|
+
# Returns if this Verb is a PUT
|
110
|
+
#
|
111
|
+
# @return [Boolean]
|
112
|
+
#
|
113
|
+
def put?
|
114
|
+
name == "PUT"
|
115
|
+
end
|
116
|
+
end
|
117
|
+
end
|
118
|
+
end
|