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
data/flake.nix
ADDED
@@ -0,0 +1,41 @@
|
|
1
|
+
{
|
2
|
+
description = "Ruby 3.3.6 development environment";
|
3
|
+
|
4
|
+
inputs = {
|
5
|
+
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
|
6
|
+
flake-utils.url = "github:numtide/flake-utils";
|
7
|
+
};
|
8
|
+
|
9
|
+
outputs = { self, nixpkgs, flake-utils }:
|
10
|
+
flake-utils.lib.eachDefaultSystem (system:
|
11
|
+
let
|
12
|
+
pkgs = nixpkgs.legacyPackages.${system};
|
13
|
+
in
|
14
|
+
{
|
15
|
+
devShells.default = pkgs.mkShell {
|
16
|
+
buildInputs = with pkgs; [
|
17
|
+
(ruby_3_3.override {
|
18
|
+
jemallocSupport = true;
|
19
|
+
docSupport = false;
|
20
|
+
})
|
21
|
+
|
22
|
+
# Dependencies for native gems
|
23
|
+
pkg-config
|
24
|
+
openssl
|
25
|
+
readline
|
26
|
+
zstd
|
27
|
+
libyaml
|
28
|
+
];
|
29
|
+
|
30
|
+
shellHook = ''
|
31
|
+
export GEM_HOME="$PWD/vendor/bundle"
|
32
|
+
export GEM_PATH="$GEM_HOME"
|
33
|
+
export PATH="$GEM_HOME/bin:$PATH"
|
34
|
+
|
35
|
+
echo "checking gems"
|
36
|
+
bundle check || bundle install
|
37
|
+
'';
|
38
|
+
};
|
39
|
+
}
|
40
|
+
);
|
41
|
+
}
|
@@ -0,0 +1,86 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module SpecForge
|
4
|
+
class Attribute
|
5
|
+
module Chainable
|
6
|
+
NUMBER_REGEX = /^\d+$/i
|
7
|
+
|
8
|
+
attr_reader :invocation_chain, :base_object
|
9
|
+
|
10
|
+
#
|
11
|
+
# Represents any attribute that is a series of chained invocations:
|
12
|
+
#
|
13
|
+
# <keyword>.<header>.<segment(hash_key | method | index)>...
|
14
|
+
#
|
15
|
+
# This module is not used as is, but is included in another class.
|
16
|
+
# Note: There can be any n number of segments.
|
17
|
+
#
|
18
|
+
def initialize(...)
|
19
|
+
super
|
20
|
+
|
21
|
+
# Drop the keyword
|
22
|
+
sections = input.split(".")[1..]
|
23
|
+
|
24
|
+
# The "header" is the first element in this array
|
25
|
+
@invocation_chain = sections || []
|
26
|
+
end
|
27
|
+
|
28
|
+
def value
|
29
|
+
invoke_chain
|
30
|
+
end
|
31
|
+
|
32
|
+
#
|
33
|
+
# Custom implementation to ensure the underlying values are resolved
|
34
|
+
# without breaking #value's functionality
|
35
|
+
#
|
36
|
+
def resolve
|
37
|
+
@resolved ||= __resolve(invoke_chain(resolve: true))
|
38
|
+
end
|
39
|
+
|
40
|
+
private
|
41
|
+
|
42
|
+
def invoke_chain(resolve: false)
|
43
|
+
current_value = @base_object
|
44
|
+
|
45
|
+
invocation_chain.each do |step|
|
46
|
+
object = retrieve_value(current_value, resolve:)
|
47
|
+
current_value = invoke(step, object)
|
48
|
+
end
|
49
|
+
|
50
|
+
retrieve_value(current_value, resolve:)
|
51
|
+
end
|
52
|
+
|
53
|
+
def retrieve_value(object, resolve: false)
|
54
|
+
return object if !object.is_a?(Attribute)
|
55
|
+
|
56
|
+
resolve ? object.resolve : object.value
|
57
|
+
end
|
58
|
+
|
59
|
+
def invoke(step, object)
|
60
|
+
if hash_key?(object, step)
|
61
|
+
object[step.to_sym]
|
62
|
+
elsif index?(object, step)
|
63
|
+
object[step.to_i]
|
64
|
+
elsif method?(object, step)
|
65
|
+
object.public_send(step)
|
66
|
+
else
|
67
|
+
raise InvalidInvocationError.new(step, object)
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
def hash_key?(object, key)
|
72
|
+
# This is to support the silly delegator
|
73
|
+
method?(object, :key?) && object.key?(key.to_sym)
|
74
|
+
end
|
75
|
+
|
76
|
+
def method?(object, method_name)
|
77
|
+
object.respond_to?(method_name)
|
78
|
+
end
|
79
|
+
|
80
|
+
def index?(object, step)
|
81
|
+
# This is to support the silly delegator
|
82
|
+
method?(object, :index) && step.match?(NUMBER_REGEX)
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
@@ -0,0 +1,63 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module SpecForge
|
4
|
+
class Attribute
|
5
|
+
class Factory < Parameterized
|
6
|
+
include Chainable
|
7
|
+
|
8
|
+
KEYWORD_REGEX = /^factories\./i
|
9
|
+
|
10
|
+
BUILD_STRATEGIES = %w[
|
11
|
+
build
|
12
|
+
create
|
13
|
+
attributes_for
|
14
|
+
build_stubbed
|
15
|
+
].freeze
|
16
|
+
|
17
|
+
attr_reader :factory_name
|
18
|
+
|
19
|
+
#
|
20
|
+
# Represents any attribute that is a factory reference
|
21
|
+
#
|
22
|
+
# factories.<factory_name>
|
23
|
+
#
|
24
|
+
def initialize(...)
|
25
|
+
super
|
26
|
+
|
27
|
+
@factory_name = invocation_chain.shift&.to_sym
|
28
|
+
|
29
|
+
# Check the arguments before preparing them
|
30
|
+
arguments[:keyword] = Normalizer.normalize_factory_reference!(arguments[:keyword])
|
31
|
+
|
32
|
+
prepare_arguments!
|
33
|
+
end
|
34
|
+
|
35
|
+
def value
|
36
|
+
@base_object = create_factory_object
|
37
|
+
super
|
38
|
+
end
|
39
|
+
|
40
|
+
def resolve
|
41
|
+
@base_object = create_factory_object
|
42
|
+
super
|
43
|
+
end
|
44
|
+
|
45
|
+
private
|
46
|
+
|
47
|
+
def create_factory_object
|
48
|
+
attributes = arguments[:keyword]
|
49
|
+
return FactoryBot.create(@factory_name) if attributes.blank?
|
50
|
+
|
51
|
+
# Determine build strat
|
52
|
+
build_strategy = attributes[:build_strategy].resolve
|
53
|
+
|
54
|
+
# stubbed => build_stubbed
|
55
|
+
build_strategy.prepend("build_") if build_strategy == "stubbed"
|
56
|
+
raise InvalidBuildStrategy, build_strategy unless BUILD_STRATEGIES.include?(build_strategy)
|
57
|
+
|
58
|
+
attributes = attributes[:attributes].resolve
|
59
|
+
FactoryBot.public_send(build_strategy, @factory_name, **attributes)
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
@@ -0,0 +1,54 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module SpecForge
|
4
|
+
class Attribute
|
5
|
+
class Faker < Parameterized
|
6
|
+
KEYWORD_REGEX = /^faker\./i
|
7
|
+
|
8
|
+
attr_reader :faker_class, :faker_method
|
9
|
+
|
10
|
+
#
|
11
|
+
# Represents any attribute that is a faker call
|
12
|
+
#
|
13
|
+
# faker.<faker_class>.<faker_method>
|
14
|
+
#
|
15
|
+
def initialize(...)
|
16
|
+
super
|
17
|
+
|
18
|
+
# As of right now, Faker only goes 2 sub classes deep. I've added +2 padding just in case
|
19
|
+
# faker.class.method
|
20
|
+
# faker.class.subclass.method
|
21
|
+
sections = input.split(".")[0..5]
|
22
|
+
|
23
|
+
class_name = sections[0..-2].join("::").underscore.classify
|
24
|
+
method_name = sections.last
|
25
|
+
|
26
|
+
# Load the class
|
27
|
+
@faker_class = begin
|
28
|
+
"::#{class_name}".constantize
|
29
|
+
rescue NameError
|
30
|
+
raise InvalidFakerClassError, class_name
|
31
|
+
end
|
32
|
+
|
33
|
+
# Load the method
|
34
|
+
@faker_method = begin
|
35
|
+
faker_class.method(method_name)
|
36
|
+
rescue NameError
|
37
|
+
raise InvalidFakerMethodError.new(method_name, faker_class)
|
38
|
+
end
|
39
|
+
|
40
|
+
prepare_arguments!
|
41
|
+
end
|
42
|
+
|
43
|
+
def value
|
44
|
+
if uses_positional_arguments?(faker_method)
|
45
|
+
faker_method.call(*arguments[:positional].resolve)
|
46
|
+
elsif uses_keyword_arguments?(faker_method)
|
47
|
+
faker_method.call(**arguments[:keyword].resolve)
|
48
|
+
else
|
49
|
+
faker_method.call
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module SpecForge
|
4
|
+
class Attribute
|
5
|
+
class Literal < Attribute
|
6
|
+
REGEX_REGEX = /^\/.+\/[mnix\s]*$/i
|
7
|
+
|
8
|
+
attr_reader :value
|
9
|
+
|
10
|
+
#
|
11
|
+
# Represents any attribute that is a literal value.
|
12
|
+
# A literal value can be any value YAML value, except Array and Hash
|
13
|
+
#
|
14
|
+
def initialize(input)
|
15
|
+
super
|
16
|
+
|
17
|
+
@value =
|
18
|
+
case input
|
19
|
+
when REGEX_REGEX
|
20
|
+
Regexp.new(input)
|
21
|
+
else
|
22
|
+
input
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,118 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module SpecForge
|
4
|
+
class Attribute
|
5
|
+
class Matcher < Parameterized
|
6
|
+
class Methods
|
7
|
+
include RSpec::Matchers
|
8
|
+
end
|
9
|
+
|
10
|
+
MATCHER_METHODS = Methods.new.freeze
|
11
|
+
KEYWORD_REGEX = /^matcher\.|^be\.|^kind_of\./i
|
12
|
+
|
13
|
+
LITERAL_MAPPINGS = {
|
14
|
+
"nil" => nil,
|
15
|
+
"true" => true,
|
16
|
+
"false" => false
|
17
|
+
}.freeze
|
18
|
+
|
19
|
+
attr_reader :matcher_method
|
20
|
+
|
21
|
+
#
|
22
|
+
# Represents any attribute that is a matcher call.
|
23
|
+
#
|
24
|
+
# matcher.<method>
|
25
|
+
# be.<method>
|
26
|
+
# kind_of.<method>
|
27
|
+
#
|
28
|
+
def initialize(...)
|
29
|
+
super
|
30
|
+
|
31
|
+
namespace, method = extract_namespace_and_method
|
32
|
+
|
33
|
+
@matcher_method =
|
34
|
+
case namespace
|
35
|
+
when "be"
|
36
|
+
resolve_be_matcher(method)
|
37
|
+
when "kind_of"
|
38
|
+
resolve_kind_of_matcher(method)
|
39
|
+
else
|
40
|
+
resolve_matcher(method)
|
41
|
+
end
|
42
|
+
|
43
|
+
prepare_arguments!
|
44
|
+
end
|
45
|
+
|
46
|
+
def value
|
47
|
+
if uses_positional_arguments?(matcher_method)
|
48
|
+
positional = arguments[:positional].resolve.each do |value|
|
49
|
+
value.deep_stringify_keys! if value.respond_to?(:deep_stringify_keys!)
|
50
|
+
end
|
51
|
+
|
52
|
+
matcher_method.call(*positional)
|
53
|
+
elsif uses_keyword_arguments?(matcher_method)
|
54
|
+
matcher_method.call(**arguments[:keyword].resolve.deep_stringify_keys)
|
55
|
+
else
|
56
|
+
matcher_method.call
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
private
|
61
|
+
|
62
|
+
def extract_namespace_and_method
|
63
|
+
sections = input.split(".", 2)
|
64
|
+
|
65
|
+
if sections.size > 1
|
66
|
+
sections[..1]
|
67
|
+
else
|
68
|
+
[nil, sections.first]
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
def resolve_matcher(method_name, namespace: MATCHER_METHODS)
|
73
|
+
namespace.public_method(method_name)
|
74
|
+
end
|
75
|
+
|
76
|
+
def resolve_be_matcher(method)
|
77
|
+
# Resolve any custom matchers
|
78
|
+
resolved_matcher =
|
79
|
+
case method
|
80
|
+
|
81
|
+
# be.>(*args)
|
82
|
+
when "greater_than", "greater"
|
83
|
+
resolve_matcher(:>, namespace: MATCHER_METHODS.be)
|
84
|
+
|
85
|
+
# be.>=(*args)
|
86
|
+
when "greater_than_or_equal", "greater_or_equal"
|
87
|
+
resolve_matcher(:>=, namespace: MATCHER_METHODS.be)
|
88
|
+
|
89
|
+
# be.<(*args)
|
90
|
+
when "less_than", "less"
|
91
|
+
resolve_matcher(:<, namespace: MATCHER_METHODS.be)
|
92
|
+
|
93
|
+
# be.<=(*args)
|
94
|
+
when "less_than_or_equal", "less_or_equal"
|
95
|
+
resolve_matcher(:<=, namespace: MATCHER_METHODS.be)
|
96
|
+
|
97
|
+
# be(nil), be(true), be(false)
|
98
|
+
when "nil", "true", "false"
|
99
|
+
arguments[:positional].insert(0, LITERAL_MAPPINGS[method])
|
100
|
+
resolve_matcher(:be)
|
101
|
+
end
|
102
|
+
|
103
|
+
# Return the matcher if we found one
|
104
|
+
return resolved_matcher if resolved_matcher
|
105
|
+
|
106
|
+
# No matcher found, we're going to assume it's prefixed with "be_"
|
107
|
+
resolve_matcher(:"be_#{method}")
|
108
|
+
end
|
109
|
+
|
110
|
+
def resolve_kind_of_matcher(method)
|
111
|
+
type_class = Object.const_get(method.capitalize)
|
112
|
+
arguments[:positional].insert(0, type_class)
|
113
|
+
|
114
|
+
resolve_matcher(:be_kind_of)
|
115
|
+
end
|
116
|
+
end
|
117
|
+
end
|
118
|
+
end
|
@@ -0,0 +1,76 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module SpecForge
|
4
|
+
class Attribute
|
5
|
+
class Parameterized < Attribute
|
6
|
+
def self.from_hash(hash)
|
7
|
+
metadata = hash.first
|
8
|
+
|
9
|
+
input = metadata.first
|
10
|
+
arguments = metadata.second
|
11
|
+
|
12
|
+
case arguments
|
13
|
+
when ArrayLike
|
14
|
+
new(input, arguments)
|
15
|
+
when HashLike
|
16
|
+
# Offset for positional arguments. No support for both at this time
|
17
|
+
new(input, [], arguments)
|
18
|
+
else
|
19
|
+
# Single value
|
20
|
+
new(input, [arguments])
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
attr_reader :arguments
|
25
|
+
|
26
|
+
#
|
27
|
+
# Represents any attribute that is written in expanded form.
|
28
|
+
# Expanded form is just a fancy name for a hash.
|
29
|
+
#
|
30
|
+
# keyword:
|
31
|
+
# <attribute>:
|
32
|
+
# <keyword_arg>: <value>
|
33
|
+
#
|
34
|
+
# positional:
|
35
|
+
# <attribute>:
|
36
|
+
# - <positional_arg>
|
37
|
+
# - <positional_arg>
|
38
|
+
#
|
39
|
+
# @param input [Hash] The key that contains these arguments
|
40
|
+
# @param positional [Array] Any positional arguments
|
41
|
+
# @param keyword [Hash] Any keyword arguments
|
42
|
+
#
|
43
|
+
def initialize(input, positional = [], keyword = {})
|
44
|
+
super(input.to_s.downcase)
|
45
|
+
|
46
|
+
@arguments = {positional:, keyword:}
|
47
|
+
end
|
48
|
+
|
49
|
+
def bind_variables(variables)
|
50
|
+
arguments[:positional].each { |v| Attribute.bind_variables(v, variables) }
|
51
|
+
arguments[:keyword].each_value { |v| Attribute.bind_variables(v, variables) }
|
52
|
+
end
|
53
|
+
|
54
|
+
protected
|
55
|
+
|
56
|
+
#
|
57
|
+
# Converts the arguments into Attributes
|
58
|
+
#
|
59
|
+
# @note This needs to be called by the inheriting class.
|
60
|
+
# This is to allow inheriting classes to normalize their arguments before
|
61
|
+
# they are converted to Attributes
|
62
|
+
#
|
63
|
+
def prepare_arguments!
|
64
|
+
@arguments = Attribute.from(arguments)
|
65
|
+
end
|
66
|
+
|
67
|
+
def uses_positional_arguments?(method)
|
68
|
+
method.parameters.any? { |a| [:req, :opt, :rest].include?(a.first) }
|
69
|
+
end
|
70
|
+
|
71
|
+
def uses_keyword_arguments?(method)
|
72
|
+
method.parameters.any? { |a| [:keyreq, :key, :keyrest].include?(a.first) }
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module SpecForge
|
4
|
+
class Attribute
|
5
|
+
#
|
6
|
+
# Helpers for ResolvableHash and ResolvableArray
|
7
|
+
#
|
8
|
+
module Resolvable
|
9
|
+
# @private
|
10
|
+
def to_proc
|
11
|
+
this = self
|
12
|
+
-> { this.resolve }
|
13
|
+
end
|
14
|
+
|
15
|
+
# @private
|
16
|
+
def resolvable_proc
|
17
|
+
->(v) { v.respond_to?(:resolve) ? v.resolve : v }
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module SpecForge
|
4
|
+
class Attribute
|
5
|
+
#
|
6
|
+
# Represents an Array that may contain Attributes
|
7
|
+
#
|
8
|
+
class ResolvableArray < SimpleDelegator
|
9
|
+
include Resolvable
|
10
|
+
|
11
|
+
def value
|
12
|
+
__getobj__
|
13
|
+
end
|
14
|
+
|
15
|
+
def resolve
|
16
|
+
value.map(&resolvable_proc)
|
17
|
+
end
|
18
|
+
|
19
|
+
def bind_variables(variables)
|
20
|
+
value.each { |v| Attribute.bind_variables(v, variables) }
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module SpecForge
|
4
|
+
class Attribute
|
5
|
+
#
|
6
|
+
# Represents a hash that may contain Attributes
|
7
|
+
#
|
8
|
+
class ResolvableHash < SimpleDelegator
|
9
|
+
include Resolvable
|
10
|
+
|
11
|
+
def value
|
12
|
+
__getobj__
|
13
|
+
end
|
14
|
+
|
15
|
+
def resolve
|
16
|
+
value.transform_values(&resolvable_proc)
|
17
|
+
end
|
18
|
+
|
19
|
+
def bind_variables(variables)
|
20
|
+
value.each_value { |v| Attribute.bind_variables(v, variables) }
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module SpecForge
|
4
|
+
class Attribute
|
5
|
+
class Transform < Parameterized
|
6
|
+
KEYWORD_REGEX = /^transform\./i
|
7
|
+
|
8
|
+
TRANSFORM_METHODS = %w[
|
9
|
+
join
|
10
|
+
].freeze
|
11
|
+
|
12
|
+
attr_reader :function
|
13
|
+
|
14
|
+
#
|
15
|
+
# Represents any attribute that is a transform call
|
16
|
+
#
|
17
|
+
# transform.<function>
|
18
|
+
#
|
19
|
+
def initialize(...)
|
20
|
+
super
|
21
|
+
|
22
|
+
# Remove prefix
|
23
|
+
@function = @input.sub("transform.", "")
|
24
|
+
|
25
|
+
raise InvalidTransformFunctionError, input unless TRANSFORM_METHODS.include?(function)
|
26
|
+
|
27
|
+
prepare_arguments!
|
28
|
+
end
|
29
|
+
|
30
|
+
def value
|
31
|
+
case function
|
32
|
+
when "join"
|
33
|
+
# Technically supports any attribute, but I ain't gonna test all them edge cases
|
34
|
+
arguments[:positional].resolve.join
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module SpecForge
|
4
|
+
class Attribute
|
5
|
+
class Variable < Attribute
|
6
|
+
include Chainable
|
7
|
+
|
8
|
+
KEYWORD_REGEX = /^variables\./i
|
9
|
+
|
10
|
+
attr_reader :variable_name
|
11
|
+
|
12
|
+
#
|
13
|
+
# Represents any attribute that is a variable reference
|
14
|
+
#
|
15
|
+
# variables.<variable_name>
|
16
|
+
#
|
17
|
+
def initialize(...)
|
18
|
+
super
|
19
|
+
|
20
|
+
# Remove the variable name from the chain
|
21
|
+
@variable_name = invocation_chain.shift&.to_sym
|
22
|
+
end
|
23
|
+
|
24
|
+
def bind_variables(variables)
|
25
|
+
raise InvalidTypeError.new(variables, Hash, for: "'variables'") unless Type.hash?(variables)
|
26
|
+
|
27
|
+
# Don't nil check here.
|
28
|
+
raise MissingVariableError, variable_name unless variables.key?(variable_name)
|
29
|
+
|
30
|
+
@base_object = variables[variable_name]
|
31
|
+
|
32
|
+
self
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|