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,104 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module SpecForge
|
4
|
+
class Normalizer
|
5
|
+
class Config < Normalizer
|
6
|
+
STRUCTURE = {
|
7
|
+
environment: {
|
8
|
+
# Allows for a shorthand:
|
9
|
+
# environment: rails
|
10
|
+
# Long form:
|
11
|
+
# environment:
|
12
|
+
# use: rails
|
13
|
+
type: [String, Hash],
|
14
|
+
default: "rails",
|
15
|
+
structure: {
|
16
|
+
use: {type: String, default: "rails"},
|
17
|
+
preload: {type: String, default: ""},
|
18
|
+
models_path: {
|
19
|
+
type: String,
|
20
|
+
aliases: %i[models],
|
21
|
+
default: ""
|
22
|
+
}
|
23
|
+
}
|
24
|
+
},
|
25
|
+
base_url: {type: String}, # Required
|
26
|
+
authorization: {
|
27
|
+
type: Hash,
|
28
|
+
default: {
|
29
|
+
# Default is a key on this hash
|
30
|
+
default: {}
|
31
|
+
},
|
32
|
+
structure: {
|
33
|
+
default: {
|
34
|
+
type: Hash,
|
35
|
+
structure: {
|
36
|
+
header: {type: String, default: ""},
|
37
|
+
value: {type: String, default: ""}
|
38
|
+
}
|
39
|
+
}
|
40
|
+
}
|
41
|
+
},
|
42
|
+
factories: {
|
43
|
+
type: Hash,
|
44
|
+
default: {},
|
45
|
+
structure: {
|
46
|
+
paths: {
|
47
|
+
type: Array,
|
48
|
+
default: []
|
49
|
+
},
|
50
|
+
auto_discover: {
|
51
|
+
type: [TrueClass, FalseClass],
|
52
|
+
default: true
|
53
|
+
}
|
54
|
+
}
|
55
|
+
}
|
56
|
+
}.freeze
|
57
|
+
end
|
58
|
+
|
59
|
+
# On Normalizer
|
60
|
+
class << self
|
61
|
+
#
|
62
|
+
# Generates an empty config hash
|
63
|
+
#
|
64
|
+
# @return [Hash]
|
65
|
+
#
|
66
|
+
def default_config
|
67
|
+
Config.default
|
68
|
+
end
|
69
|
+
|
70
|
+
#
|
71
|
+
# Normalizes a config hash by standardizing its keys while ensuring the required data
|
72
|
+
# is provided or defaulted.
|
73
|
+
# Raises InvalidStructureError if anything is missing/invalid type
|
74
|
+
#
|
75
|
+
# @param input [Hash] The hash to normalize
|
76
|
+
#
|
77
|
+
# @return [Hash] A normalized hash as a new instance
|
78
|
+
#
|
79
|
+
def normalize_config!(input)
|
80
|
+
raise_errors! do
|
81
|
+
normalize_config(input)
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
#
|
86
|
+
# Normalize a config hash
|
87
|
+
# Used internally by .normalize_config, but is available for utility
|
88
|
+
#
|
89
|
+
# @param config [Hash] Config representation as a Hash
|
90
|
+
#
|
91
|
+
# @return [Array] Two item array
|
92
|
+
# First - The normalized hash
|
93
|
+
# Second - Array of errors, if any
|
94
|
+
#
|
95
|
+
# @private
|
96
|
+
#
|
97
|
+
def normalize_config(config)
|
98
|
+
raise InvalidTypeError.new(config, Hash, for: "config") unless Type.hash?(config)
|
99
|
+
|
100
|
+
Normalizer::Config.new("config", config).normalize
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module SpecForge
|
4
|
+
class Normalizer
|
5
|
+
class Constraint < Normalizer
|
6
|
+
STRUCTURE = {
|
7
|
+
status: {
|
8
|
+
type: Integer
|
9
|
+
},
|
10
|
+
json: {
|
11
|
+
type: Hash,
|
12
|
+
default: {}
|
13
|
+
}
|
14
|
+
}.freeze
|
15
|
+
end
|
16
|
+
|
17
|
+
# On Normalizer
|
18
|
+
class << self
|
19
|
+
#
|
20
|
+
# Generates an empty constraint hash
|
21
|
+
#
|
22
|
+
# @return [Hash]
|
23
|
+
#
|
24
|
+
def default_constraint
|
25
|
+
Constraint.default
|
26
|
+
end
|
27
|
+
|
28
|
+
#
|
29
|
+
# Normalize a constraint hash
|
30
|
+
# Used internally by .normalize_spec, but is available for utility
|
31
|
+
#
|
32
|
+
# @param constraint [Hash] Constraint representation as a Hash
|
33
|
+
#
|
34
|
+
# @return [Array] Two item array
|
35
|
+
# First - The normalized hash
|
36
|
+
# Second - Array of errors, if any
|
37
|
+
#
|
38
|
+
# @private
|
39
|
+
#
|
40
|
+
def normalize_constraint(constraint)
|
41
|
+
raise InvalidTypeError.new(constraint, Hash, for: "expect") unless Type.hash?(constraint)
|
42
|
+
|
43
|
+
Normalizer::Constraint.new("expect", constraint).normalize
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
@@ -0,0 +1,85 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module SpecForge
|
4
|
+
class Normalizer
|
5
|
+
class Expectation < Normalizer
|
6
|
+
STRUCTURE = {
|
7
|
+
name: {type: String, default: ""},
|
8
|
+
base_url: Normalizer::SHARED_ATTRIBUTES[:base_url],
|
9
|
+
url: Normalizer::SHARED_ATTRIBUTES[:url],
|
10
|
+
http_method: Normalizer::SHARED_ATTRIBUTES[:http_method],
|
11
|
+
headers: Normalizer::SHARED_ATTRIBUTES[:headers],
|
12
|
+
query: Normalizer::SHARED_ATTRIBUTES[:query],
|
13
|
+
body: Normalizer::SHARED_ATTRIBUTES[:body],
|
14
|
+
variables: Normalizer::SHARED_ATTRIBUTES[:variables],
|
15
|
+
expect: {type: Hash}
|
16
|
+
}.freeze
|
17
|
+
end
|
18
|
+
|
19
|
+
# On Normalizer
|
20
|
+
class << self
|
21
|
+
#
|
22
|
+
# Generates an empty expectation hash
|
23
|
+
#
|
24
|
+
# @return [Hash]
|
25
|
+
#
|
26
|
+
def default_expectation
|
27
|
+
Expectation.default
|
28
|
+
end
|
29
|
+
|
30
|
+
#
|
31
|
+
# Normalize an array of expectation hashes
|
32
|
+
#
|
33
|
+
# @raises InvalidStructureError if anything is missing/invalid type
|
34
|
+
#
|
35
|
+
# @param input [Hash] The hash to normalize
|
36
|
+
#
|
37
|
+
# @return [Hash] A normalized hash as a new instance
|
38
|
+
#
|
39
|
+
def normalize_expectations!(input)
|
40
|
+
raise_errors! do
|
41
|
+
normalize_expectations(input)
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
#
|
46
|
+
# Normalize an array of expectation hashes
|
47
|
+
# Used internally by .normalize_spec, but is available for utility
|
48
|
+
#
|
49
|
+
# @param expectations [Array<Hash>] An array of expectation hashes
|
50
|
+
#
|
51
|
+
# @return [Array] Two item array
|
52
|
+
# First - The normalized Array<Hash>
|
53
|
+
# Second - Array of errors, if any
|
54
|
+
#
|
55
|
+
# @private
|
56
|
+
#
|
57
|
+
def normalize_expectations(expectations)
|
58
|
+
if !Type.array?(expectations)
|
59
|
+
raise InvalidTypeError.new(expectations, Array, for: "\"expectations\" on spec")
|
60
|
+
end
|
61
|
+
|
62
|
+
final_errors = Set.new
|
63
|
+
final_output = expectations.map.with_index do |expectation, index|
|
64
|
+
normalizer = Normalizer::Expectation.new("expectation (item #{index})", expectation)
|
65
|
+
output, errors = normalizer.normalize
|
66
|
+
|
67
|
+
# If expect is not provided, skip the constraints
|
68
|
+
if (constraint = expectation[:expect])
|
69
|
+
constraint_output, constraint_errors = Normalizer::Constraint.new(
|
70
|
+
"expect (item #{index})", constraint
|
71
|
+
).normalize
|
72
|
+
|
73
|
+
output[:expect] = constraint_output
|
74
|
+
errors.merge(constraint_errors) if constraint_errors.size > 0
|
75
|
+
end
|
76
|
+
|
77
|
+
final_errors.merge(errors) if errors.size > 0
|
78
|
+
output
|
79
|
+
end
|
80
|
+
|
81
|
+
[final_output, final_errors]
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
@@ -0,0 +1,65 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module SpecForge
|
4
|
+
class Normalizer
|
5
|
+
class Factory < Normalizer
|
6
|
+
STRUCTURE = {
|
7
|
+
model_class: {
|
8
|
+
type: String,
|
9
|
+
aliases: %i[class],
|
10
|
+
default: ""
|
11
|
+
},
|
12
|
+
variables: Normalizer::SHARED_ATTRIBUTES[:variables],
|
13
|
+
attributes: {
|
14
|
+
type: Hash,
|
15
|
+
default: {}
|
16
|
+
}
|
17
|
+
}.freeze
|
18
|
+
end
|
19
|
+
|
20
|
+
# On Normalizer
|
21
|
+
class << self
|
22
|
+
#
|
23
|
+
# Generates an empty factory hash
|
24
|
+
#
|
25
|
+
# @return [Hash]
|
26
|
+
#
|
27
|
+
def default_factory
|
28
|
+
Factory.default
|
29
|
+
end
|
30
|
+
|
31
|
+
#
|
32
|
+
# Normalizes a factory hash by standardizing its keys while ensuring the required data
|
33
|
+
# is provided or defaulted.
|
34
|
+
# Raises InvalidStructureError if anything is missing/invalid type
|
35
|
+
#
|
36
|
+
# @param input [Hash] The hash to normalize
|
37
|
+
#
|
38
|
+
# @return [Hash] A normalized hash as a new instance
|
39
|
+
#
|
40
|
+
def normalize_factory!(input)
|
41
|
+
raise_errors! do
|
42
|
+
normalize_factory(input)
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
#
|
47
|
+
# Normalize a factory hash
|
48
|
+
# Used internally by .normalize_factory, but is available for utility
|
49
|
+
#
|
50
|
+
# @param factory [Hash] Factory representation as a Hash
|
51
|
+
#
|
52
|
+
# @return [Array] Two item array
|
53
|
+
# First - The normalized hash
|
54
|
+
# Second - Array of errors, if any
|
55
|
+
#
|
56
|
+
# @private
|
57
|
+
#
|
58
|
+
def normalize_factory(factory)
|
59
|
+
raise InvalidTypeError.new(factory, Hash, for: "factory") unless Type.hash?(factory)
|
60
|
+
|
61
|
+
Normalizer::Factory.new("factory", factory).normalize
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
@@ -0,0 +1,66 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module SpecForge
|
4
|
+
class Normalizer
|
5
|
+
class FactoryReference < Normalizer
|
6
|
+
STRUCTURE = {
|
7
|
+
attributes: {
|
8
|
+
type: Hash,
|
9
|
+
default: {}
|
10
|
+
},
|
11
|
+
build_strategy: {
|
12
|
+
type: String,
|
13
|
+
aliases: %i[strategy],
|
14
|
+
default: "create"
|
15
|
+
}
|
16
|
+
}.freeze
|
17
|
+
end
|
18
|
+
|
19
|
+
# On Normalizer
|
20
|
+
class << self
|
21
|
+
#
|
22
|
+
# Generates an empty Attribute::Factory hash
|
23
|
+
#
|
24
|
+
# @return [Hash]
|
25
|
+
#
|
26
|
+
def default_factory_reference
|
27
|
+
FactoryReference.default
|
28
|
+
end
|
29
|
+
|
30
|
+
#
|
31
|
+
# Normalizes a Attribute::Factory hash by standardizing
|
32
|
+
# its keys while ensuring the required data is provided or defaulted.
|
33
|
+
# Raises InvalidStructureError if anything is missing/invalid type
|
34
|
+
#
|
35
|
+
# @param input [Hash] The hash to normalize
|
36
|
+
#
|
37
|
+
# @return [Hash] A normalized hash as a new instance
|
38
|
+
#
|
39
|
+
def normalize_factory_reference!(input, **)
|
40
|
+
raise_errors! do
|
41
|
+
normalize_factory_reference(input, **)
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
#
|
46
|
+
# Normalize a factory hash
|
47
|
+
# Used internally by .normalize_factory_reference, but is available for utility
|
48
|
+
#
|
49
|
+
# @param factory [Hash] Attribute::Factory representation as a Hash
|
50
|
+
#
|
51
|
+
# @return [Array] Two item array
|
52
|
+
# First - The normalized hash
|
53
|
+
# Second - Array of errors, if any
|
54
|
+
#
|
55
|
+
# @private
|
56
|
+
#
|
57
|
+
def normalize_factory_reference(factory, label: "factory reference")
|
58
|
+
if !Type.hash?(factory)
|
59
|
+
raise InvalidTypeError.new(factory, Hash, for: "factory reference")
|
60
|
+
end
|
61
|
+
|
62
|
+
Normalizer::FactoryReference.new(label, factory).normalize
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
@@ -0,0 +1,73 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module SpecForge
|
4
|
+
class Normalizer
|
5
|
+
class Spec < Normalizer
|
6
|
+
STRUCTURE = {
|
7
|
+
base_url: Normalizer::SHARED_ATTRIBUTES[:base_url],
|
8
|
+
url: Normalizer::SHARED_ATTRIBUTES[:url],
|
9
|
+
http_method: Normalizer::SHARED_ATTRIBUTES[:http_method],
|
10
|
+
headers: Normalizer::SHARED_ATTRIBUTES[:headers],
|
11
|
+
query: Normalizer::SHARED_ATTRIBUTES[:query],
|
12
|
+
body: Normalizer::SHARED_ATTRIBUTES[:body],
|
13
|
+
variables: Normalizer::SHARED_ATTRIBUTES[:variables],
|
14
|
+
expectations: {type: Array}
|
15
|
+
}.freeze
|
16
|
+
end
|
17
|
+
|
18
|
+
# On Normalizer
|
19
|
+
class << self
|
20
|
+
#
|
21
|
+
# Generates an empty spec hash
|
22
|
+
#
|
23
|
+
# @return [Hash]
|
24
|
+
#
|
25
|
+
def default_spec
|
26
|
+
Spec.default
|
27
|
+
end
|
28
|
+
|
29
|
+
#
|
30
|
+
# Normalizes a complete spec hash by standardizing its keys while ensuring the required data
|
31
|
+
# is provided or defaulted.
|
32
|
+
# Raises InvalidStructureError if anything is missing/invalid type
|
33
|
+
#
|
34
|
+
# @param input [Hash] The hash to normalize
|
35
|
+
#
|
36
|
+
# @return [Hash] A normalized hash as a new instance
|
37
|
+
#
|
38
|
+
def normalize_spec!(input)
|
39
|
+
raise_errors! do
|
40
|
+
output, errors = normalize_spec(input)
|
41
|
+
|
42
|
+
# Process expectations
|
43
|
+
if (expectations = input[:expectations]) && Type.array?(expectations)
|
44
|
+
expectation_output, expectation_errors = normalize_expectations(expectations)
|
45
|
+
|
46
|
+
output[:expectations] = expectation_output
|
47
|
+
errors += expectation_errors if expectation_errors.size > 0
|
48
|
+
end
|
49
|
+
|
50
|
+
[output, errors]
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
#
|
55
|
+
# Normalize a spec hash
|
56
|
+
# Used internally by .normalize_spec, but is available for utility
|
57
|
+
#
|
58
|
+
# @param spec [Hash] Spec representation as a Hash
|
59
|
+
#
|
60
|
+
# @return [Array] Two item array
|
61
|
+
# First - The normalized hash
|
62
|
+
# Second - Array of errors, if any
|
63
|
+
#
|
64
|
+
# @private
|
65
|
+
#
|
66
|
+
def normalize_spec(spec)
|
67
|
+
raise InvalidTypeError.new(spec, Hash, for: "spec") unless Type.hash?(spec)
|
68
|
+
|
69
|
+
Normalizer::Spec.new("spec", spec).normalize
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
@@ -0,0 +1,183 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module SpecForge
|
4
|
+
class Normalizer
|
5
|
+
SHARED_ATTRIBUTES = {
|
6
|
+
base_url: {
|
7
|
+
type: String,
|
8
|
+
default: ""
|
9
|
+
},
|
10
|
+
url: {
|
11
|
+
type: String,
|
12
|
+
aliases: %i[path],
|
13
|
+
default: ""
|
14
|
+
},
|
15
|
+
http_method: {
|
16
|
+
type: String,
|
17
|
+
aliases: %i[method],
|
18
|
+
default: ""
|
19
|
+
},
|
20
|
+
headers: {
|
21
|
+
type: Hash,
|
22
|
+
default: {}
|
23
|
+
},
|
24
|
+
query: {
|
25
|
+
type: Hash,
|
26
|
+
aliases: %i[params],
|
27
|
+
default: {}
|
28
|
+
},
|
29
|
+
body: {
|
30
|
+
type: Hash,
|
31
|
+
aliases: %i[data],
|
32
|
+
default: {}
|
33
|
+
},
|
34
|
+
variables: {
|
35
|
+
type: Hash,
|
36
|
+
default: {}
|
37
|
+
}
|
38
|
+
}.freeze
|
39
|
+
|
40
|
+
STRUCTURE = {}
|
41
|
+
|
42
|
+
class << self
|
43
|
+
#
|
44
|
+
# Raises any errors collected by the block
|
45
|
+
#
|
46
|
+
# @raises InvalidStructureError
|
47
|
+
#
|
48
|
+
# @private
|
49
|
+
#
|
50
|
+
def raise_errors!(&block)
|
51
|
+
errors = Set.new
|
52
|
+
|
53
|
+
begin
|
54
|
+
output, new_errors = yield
|
55
|
+
errors.merge(new_errors) if new_errors.size > 0
|
56
|
+
rescue => e
|
57
|
+
errors << e
|
58
|
+
end
|
59
|
+
|
60
|
+
raise InvalidStructureError.new(errors) if errors.size > 0
|
61
|
+
|
62
|
+
output
|
63
|
+
end
|
64
|
+
|
65
|
+
#
|
66
|
+
# Returns a default version of this normalizer
|
67
|
+
#
|
68
|
+
# @private
|
69
|
+
#
|
70
|
+
def default
|
71
|
+
new("", "").default
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
attr_reader :label, :input, :structure
|
76
|
+
|
77
|
+
#
|
78
|
+
# Creates a normalizer for normalizing Hash data based on a structure
|
79
|
+
#
|
80
|
+
# @param label [String] A label that describes the data itself
|
81
|
+
# @param input [Hash] The data to normalize
|
82
|
+
# @param structure [Hash] The structure to normalize the data to
|
83
|
+
#
|
84
|
+
def initialize(label, input, structure: self.class::STRUCTURE)
|
85
|
+
@label = label
|
86
|
+
@input = input
|
87
|
+
@structure = structure
|
88
|
+
end
|
89
|
+
|
90
|
+
#
|
91
|
+
# Normalizes the data and returns the result
|
92
|
+
#
|
93
|
+
# @return [Hash] The normalized data
|
94
|
+
#
|
95
|
+
def normalize
|
96
|
+
normalize_to_structure
|
97
|
+
end
|
98
|
+
|
99
|
+
#
|
100
|
+
# Returns a hash with the default structure
|
101
|
+
#
|
102
|
+
# @return [Hash]
|
103
|
+
#
|
104
|
+
def default
|
105
|
+
structure.transform_values do |value|
|
106
|
+
if (default = value[:default])
|
107
|
+
default.dup
|
108
|
+
elsif value[:type] == Integer # Can't call new on int
|
109
|
+
0
|
110
|
+
else
|
111
|
+
value[:type].new
|
112
|
+
end
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
protected
|
117
|
+
|
118
|
+
def normalize_to_structure
|
119
|
+
output, errors = {}, Set.new
|
120
|
+
|
121
|
+
structure.each do |key, attribute|
|
122
|
+
type_class = attribute[:type]
|
123
|
+
aliases = attribute[:aliases] || []
|
124
|
+
sub_structure = attribute[:structure]
|
125
|
+
default = attribute[:default]
|
126
|
+
required = !attribute.key?(:default)
|
127
|
+
|
128
|
+
# Get the value
|
129
|
+
value = value_from_keys(input, [key] + aliases)
|
130
|
+
|
131
|
+
# Default the value if needed
|
132
|
+
value = default.dup if !required && value.nil?
|
133
|
+
|
134
|
+
# Type + existence check
|
135
|
+
if !valid_class?(value, type_class)
|
136
|
+
raise InvalidTypeError.new(value, type_class, for: "\"#{key}\" on #{label}")
|
137
|
+
end
|
138
|
+
|
139
|
+
value =
|
140
|
+
case [value.class, sub_structure.class]
|
141
|
+
when [Hash, Hash]
|
142
|
+
new_value, new_errors = self.class
|
143
|
+
.new(label, value, structure: sub_structure)
|
144
|
+
.normalize
|
145
|
+
|
146
|
+
errors += new_errors if new_errors.size > 0
|
147
|
+
new_value
|
148
|
+
else
|
149
|
+
value
|
150
|
+
end
|
151
|
+
|
152
|
+
# Store
|
153
|
+
output[key] = value
|
154
|
+
rescue => e
|
155
|
+
errors << e
|
156
|
+
end
|
157
|
+
|
158
|
+
[output, errors]
|
159
|
+
end
|
160
|
+
|
161
|
+
def value_from_keys(hash, keys)
|
162
|
+
hash.find { |k, v| keys.include?(k) }&.second
|
163
|
+
end
|
164
|
+
|
165
|
+
def valid_class?(value, expected_type)
|
166
|
+
if expected_type.instance_of?(Array)
|
167
|
+
expected_type.any? { |type| value.is_a?(type) }
|
168
|
+
else
|
169
|
+
value.is_a?(expected_type)
|
170
|
+
end
|
171
|
+
end
|
172
|
+
end
|
173
|
+
end
|
174
|
+
|
175
|
+
#######################################################################
|
176
|
+
# These need to be required after the base class due to them requiring
|
177
|
+
# a constant
|
178
|
+
require_relative "normalizer/config"
|
179
|
+
require_relative "normalizer/constraint"
|
180
|
+
require_relative "normalizer/expectation"
|
181
|
+
require_relative "normalizer/factory_reference"
|
182
|
+
require_relative "normalizer/factory"
|
183
|
+
require_relative "normalizer/spec"
|
@@ -0,0 +1,91 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module SpecForge
|
4
|
+
class Runner
|
5
|
+
#
|
6
|
+
# Creates a spec runner and defines the spec with RSpec
|
7
|
+
#
|
8
|
+
# @param spec [Spec] The spec to run
|
9
|
+
#
|
10
|
+
def initialize(spec)
|
11
|
+
define_spec(spec)
|
12
|
+
end
|
13
|
+
|
14
|
+
#
|
15
|
+
# Runs any RSpec specs
|
16
|
+
#
|
17
|
+
def run
|
18
|
+
RSpec::Core::Runner.disable_autorun!
|
19
|
+
RSpec::Core::Runner.run([], $stderr, $stdout)
|
20
|
+
end
|
21
|
+
|
22
|
+
#
|
23
|
+
# Defines a spec with RSpec
|
24
|
+
#
|
25
|
+
# @param spec_forge [Spec] The spec to define
|
26
|
+
#
|
27
|
+
def define_spec(spec_forge)
|
28
|
+
runner_forge = self
|
29
|
+
|
30
|
+
RSpec.describe(spec_forge.name) do
|
31
|
+
spec_forge.expectations.each do |expectation_forge|
|
32
|
+
describe(expectation_forge.name) do
|
33
|
+
runner_forge.define_variables(self, expectation_forge)
|
34
|
+
runner_forge.define_examples(self, expectation_forge)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
#
|
41
|
+
# Defines any variables as let statements in RSpec
|
42
|
+
#
|
43
|
+
# @param context [RSpec::ExampleGroup] The rspec example group for this spec
|
44
|
+
# @param expectation [Expectation] The expectation that holds the variables
|
45
|
+
#
|
46
|
+
def define_variables(context, expectation)
|
47
|
+
expectation.variables.each do |variable_name, attribute|
|
48
|
+
context.let(variable_name, &attribute.to_proc)
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
#
|
53
|
+
# Defines the expectation itself using the constraint
|
54
|
+
#
|
55
|
+
# @param context [RSpec::ExampleGroup] The RSpec example group for this spec
|
56
|
+
# @param expectation [Expectation] The expectation that holds the constraint
|
57
|
+
#
|
58
|
+
def define_examples(context, expectation)
|
59
|
+
context.instance_exec(expectation) do |expectation|
|
60
|
+
# Ensures the only one API call occurs per expectation
|
61
|
+
before(:all) { @response = expectation.http_client.call }
|
62
|
+
|
63
|
+
constraints = expectation.constraints.resolve
|
64
|
+
request = expectation.http_client.request
|
65
|
+
|
66
|
+
# Define the example group
|
67
|
+
context "#{request.http_method} #{request.url}" do
|
68
|
+
subject(:response) { @response }
|
69
|
+
|
70
|
+
# Status check
|
71
|
+
expected_status = constraints[:status]
|
72
|
+
it "expects the response to return a status code of #{expected_status}" do
|
73
|
+
expect(response.status).to eq(expected_status)
|
74
|
+
end
|
75
|
+
|
76
|
+
# JSON check
|
77
|
+
expected_json = constraints[:json]
|
78
|
+
if expected_json.size > 0
|
79
|
+
it "expects the body to return valid JSON" do
|
80
|
+
expect(response.body).to be_kind_of(Hash)
|
81
|
+
end
|
82
|
+
|
83
|
+
it "expects the body to include values" do
|
84
|
+
expect(response.body).to include(expected_json)
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|