grumlin 0.23.0 → 1.0.0.rc1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.rubocop.yml +9 -9
- data/Gemfile.lock +1 -1
- data/README.md +94 -141
- data/Rakefile +1 -1
- data/bin/console +18 -3
- data/doc/middlewares.md +49 -10
- data/lib/async/channel.rb +54 -56
- data/lib/grumlin/benchmark/repository.rb +10 -14
- data/lib/grumlin/client.rb +93 -95
- data/lib/grumlin/config.rb +33 -33
- data/lib/grumlin/dummy_transaction.rb +13 -15
- data/lib/grumlin/edge.rb +18 -20
- data/lib/grumlin/expressions/cardinality.rb +5 -9
- data/lib/grumlin/expressions/column.rb +5 -9
- data/lib/grumlin/expressions/expression.rb +7 -11
- data/lib/grumlin/expressions/operator.rb +5 -9
- data/lib/grumlin/expressions/order.rb +5 -9
- data/lib/grumlin/expressions/p.rb +27 -31
- data/lib/grumlin/expressions/pop.rb +5 -9
- data/lib/grumlin/expressions/scope.rb +5 -9
- data/lib/grumlin/expressions/t.rb +5 -9
- data/lib/grumlin/expressions/text_p.rb +5 -9
- data/lib/grumlin/expressions/with_options.rb +17 -21
- data/lib/grumlin/features/feature_list.rb +8 -12
- data/lib/grumlin/features/neptune_features.rb +5 -9
- data/lib/grumlin/features/tinkergraph_features.rb +5 -9
- data/lib/grumlin/features.rb +8 -10
- data/lib/grumlin/middlewares/apply_shortcuts.rb +4 -8
- data/lib/grumlin/middlewares/build_query.rb +16 -20
- data/lib/grumlin/middlewares/builder.rb +15 -0
- data/lib/grumlin/middlewares/cast_results.rb +3 -7
- data/lib/grumlin/middlewares/find_blocklisted_steps.rb +14 -0
- data/lib/grumlin/middlewares/find_mutating_steps.rb +9 -0
- data/lib/grumlin/middlewares/middleware.rb +6 -10
- data/lib/grumlin/middlewares/run_query.rb +3 -7
- data/lib/grumlin/middlewares/serialize_to_bytecode.rb +5 -9
- data/lib/grumlin/middlewares/serialize_to_steps.rb +4 -8
- data/lib/grumlin/path.rb +11 -13
- data/lib/grumlin/property.rb +14 -16
- data/lib/grumlin/query_validators/blocklisted_steps_validator.rb +22 -0
- data/lib/grumlin/query_validators/validator.rb +36 -0
- data/lib/grumlin/repository/error_handling_strategy.rb +36 -40
- data/lib/grumlin/repository/instance_methods.rb +115 -118
- data/lib/grumlin/repository.rb +82 -58
- data/lib/grumlin/request_dispatcher.rb +55 -57
- data/lib/grumlin/request_error_factory.rb +53 -55
- data/lib/grumlin/shortcut.rb +19 -21
- data/lib/grumlin/shortcuts/properties.rb +12 -16
- data/lib/grumlin/shortcuts/storage.rb +67 -74
- data/lib/grumlin/shortcuts/upserts.rb +18 -22
- data/lib/grumlin/shortcuts.rb +23 -25
- data/lib/grumlin/shortcuts_applyer.rb +27 -29
- data/lib/grumlin/step.rb +88 -90
- data/lib/grumlin/step_data.rb +12 -14
- data/lib/grumlin/steppable.rb +23 -25
- data/lib/grumlin/steps.rb +52 -54
- data/lib/grumlin/steps_serializers/bytecode.rb +53 -56
- data/lib/grumlin/steps_serializers/human_readable_bytecode.rb +17 -21
- data/lib/grumlin/steps_serializers/serializer.rb +7 -11
- data/lib/grumlin/steps_serializers/string.rb +26 -30
- data/lib/grumlin/test/rspec/db_cleaner_context.rb +8 -12
- data/lib/grumlin/test/rspec/gremlin_context.rb +18 -16
- data/lib/grumlin/test/rspec.rb +1 -5
- data/lib/grumlin/transaction.rb +34 -36
- data/lib/grumlin/transport.rb +71 -73
- data/lib/grumlin/traversal_start.rb +31 -33
- data/lib/grumlin/traversal_strategies/options_strategy.rb +3 -7
- data/lib/grumlin/traverser.rb +5 -7
- data/lib/grumlin/typed_value.rb +11 -13
- data/lib/grumlin/typing.rb +70 -72
- data/lib/grumlin/version.rb +1 -1
- data/lib/grumlin/vertex.rb +14 -16
- data/lib/grumlin/vertex_property.rb +14 -16
- data/lib/grumlin/with_extension.rb +17 -19
- metadata +9 -6
- data/lib/grumlin/middlewares/frozen_builder.rb +0 -18
- data/lib/grumlin/sugar.rb +0 -15
@@ -1,73 +1,71 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
598 => ServerTimeoutError,
|
3
|
+
class Grumlin::RequestErrorFactory
|
4
|
+
ERRORS = {
|
5
|
+
499 => Grumlin::InvalidRequestArgumentsError,
|
6
|
+
500 => Grumlin::ServerError,
|
7
|
+
597 => Grumlin::ScriptEvaluationError,
|
8
|
+
599 => Grumlin::ServerSerializationError,
|
9
|
+
598 => Grumlin::ServerTimeoutError,
|
11
10
|
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
11
|
+
401 => Grumlin::ClientSideError,
|
12
|
+
407 => Grumlin::ClientSideError,
|
13
|
+
498 => Grumlin::ClientSideError
|
14
|
+
}.freeze
|
16
15
|
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
16
|
+
# Neptune presumably returns message as a JSON string of format
|
17
|
+
# {"detailedMessage":"",
|
18
|
+
# "requestId":"UUID",
|
19
|
+
# "code":"ConcurrentModificationException"}
|
20
|
+
# Currently we simply search for substrings to identify the exact error
|
21
|
+
# TODO: parse json and use `code` instead
|
23
22
|
|
24
|
-
|
25
|
-
|
23
|
+
VERTEX_ALREADY_EXISTS = "Vertex with id already exists:"
|
24
|
+
EDGE_ALREADY_EXISTS = "Edge with id already exists:"
|
26
25
|
|
27
|
-
|
26
|
+
CONCURRENT_VERTEX_INSERT_FAILED = "Failed to complete Insert operation for a Vertex due to conflicting concurrent"
|
28
27
|
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
28
|
+
CONCURRENT_VERTEX_PROPERTY_INSERT_FAILED =
|
29
|
+
"Failed to complete Insert operation for a VertexProperty due to conflicting concurrent"
|
30
|
+
CONCURRENT_EDGE_PROPERTY_INSERT_FAILED =
|
31
|
+
"Failed to complete Insert operation for a EdgeProperty due to conflicting concurrent"
|
33
32
|
|
34
|
-
|
35
|
-
|
33
|
+
CONCURRENT_EDGE_INSERT_FAILED = "Failed to complete Insert operation for an Edge due to conflicting concurrent"
|
34
|
+
CONCURRENCT_MODIFICATION_FAILED = "Failed to complete operation due to conflicting concurrent"
|
36
35
|
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
36
|
+
class << self
|
37
|
+
def build(request, response)
|
38
|
+
status = response[:status]
|
39
|
+
query = request[:request]
|
41
40
|
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
41
|
+
if (error = ERRORS[status[:code]])
|
42
|
+
return (
|
43
|
+
already_exists_error(status) ||
|
44
|
+
concurrent_modification_error(status) ||
|
45
|
+
error
|
46
|
+
).new(status, query)
|
47
|
+
end
|
49
48
|
|
50
|
-
|
49
|
+
return unless Grumlin::RequestDispatcher::SUCCESS[status[:code]].nil?
|
51
50
|
|
52
|
-
|
53
|
-
|
51
|
+
Grumlin::UnknownResponseStatus.new(status)
|
52
|
+
end
|
54
53
|
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
54
|
+
def already_exists_error(status)
|
55
|
+
return Grumlin::VertexAlreadyExistsError if status[:message]&.include?(VERTEX_ALREADY_EXISTS)
|
56
|
+
return Grumlin::EdgeAlreadyExistsError if status[:message]&.include?(EDGE_ALREADY_EXISTS)
|
57
|
+
end
|
59
58
|
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
end
|
69
|
-
return ConcurrentModificationError if status[:message]&.include?(CONCURRENCT_MODIFICATION_FAILED)
|
59
|
+
def concurrent_modification_error(status) # rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
|
60
|
+
return Grumlin::ConcurrentVertexInsertFailedError if status[:message]&.include?(CONCURRENT_VERTEX_INSERT_FAILED)
|
61
|
+
if status[:message]&.include?(CONCURRENT_VERTEX_PROPERTY_INSERT_FAILED)
|
62
|
+
return Grumlin::ConcurrentVertexPropertyInsertFailedError
|
63
|
+
end
|
64
|
+
return Grumlin::ConcurrentEdgeInsertFailedError if status[:message]&.include?(CONCURRENT_EDGE_INSERT_FAILED)
|
65
|
+
if status[:message]&.include?(CONCURRENT_EDGE_PROPERTY_INSERT_FAILED)
|
66
|
+
return Grumlin::ConcurrentEdgePropertyInsertFailedError
|
70
67
|
end
|
68
|
+
return Grumlin::ConcurrentModificationError if status[:message]&.include?(CONCURRENCT_MODIFICATION_FAILED)
|
71
69
|
end
|
72
70
|
end
|
73
71
|
end
|
data/lib/grumlin/shortcut.rb
CHANGED
@@ -1,32 +1,30 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
|
4
|
-
|
5
|
-
extend Forwardable
|
3
|
+
class Grumlin::Shortcut
|
4
|
+
extend Forwardable
|
6
5
|
|
7
|
-
|
6
|
+
attr_reader :name, :block
|
8
7
|
|
9
|
-
|
10
|
-
|
8
|
+
def_delegator :@block, :arity
|
9
|
+
def_delegator :@block, :source_location
|
11
10
|
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
11
|
+
def initialize(name, lazy: true, &block)
|
12
|
+
@name = name
|
13
|
+
@lazy = lazy
|
14
|
+
@block = block
|
15
|
+
end
|
17
16
|
|
18
|
-
|
19
|
-
|
20
|
-
|
17
|
+
def ==(other)
|
18
|
+
@name == other.name && @block == other.block
|
19
|
+
end
|
21
20
|
|
22
|
-
|
23
|
-
|
24
|
-
|
21
|
+
def lazy?
|
22
|
+
@lazy
|
23
|
+
end
|
25
24
|
|
26
|
-
|
25
|
+
# TODO: to_s, inspect, preview
|
27
26
|
|
28
|
-
|
29
|
-
|
30
|
-
end
|
27
|
+
def apply(object, *args, **params)
|
28
|
+
object.instance_exec(*args, **params, &@block)
|
31
29
|
end
|
32
30
|
end
|
@@ -1,24 +1,20 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
module Grumlin
|
4
|
-
|
5
|
-
module Properties
|
6
|
-
extend Grumlin::Shortcuts
|
3
|
+
module Grumlin::Shortcuts::Properties
|
4
|
+
extend Grumlin::Shortcuts
|
7
5
|
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
6
|
+
shortcut :props do |cardinality = nil, **props|
|
7
|
+
props.reduce(self) do |tt, (prop, value)|
|
8
|
+
next tt if value.nil? # nils are not supported
|
9
|
+
next tt.property(prop, value) if cardinality.nil?
|
12
10
|
|
13
|
-
|
14
|
-
|
15
|
-
|
11
|
+
tt.property(cardinality, prop, value)
|
12
|
+
end
|
13
|
+
end
|
16
14
|
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
end
|
21
|
-
end
|
15
|
+
shortcut :hasAll do |**props|
|
16
|
+
props.reduce(self) do |tt, (prop, value)|
|
17
|
+
tt.has(prop, value)
|
22
18
|
end
|
23
19
|
end
|
24
20
|
end
|
@@ -1,99 +1,92 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
|
4
|
-
|
5
|
-
class Storage
|
6
|
-
extend Forwardable
|
7
|
-
|
8
|
-
class << self
|
9
|
-
def [](other)
|
10
|
-
new(other)
|
11
|
-
end
|
3
|
+
class Grumlin::Shortcuts::Storage
|
4
|
+
extend Forwardable
|
12
5
|
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
6
|
+
class << self
|
7
|
+
def [](other)
|
8
|
+
new(other)
|
9
|
+
end
|
17
10
|
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
end
|
23
|
-
end
|
11
|
+
def empty
|
12
|
+
@empty ||= new
|
13
|
+
end
|
14
|
+
end
|
24
15
|
|
25
|
-
|
26
|
-
|
27
|
-
|
16
|
+
def initialize(storage = {})
|
17
|
+
@storage = storage
|
18
|
+
storage.each do |n, s|
|
19
|
+
add(n, s)
|
20
|
+
end
|
21
|
+
end
|
28
22
|
|
29
|
-
|
30
|
-
|
31
|
-
|
23
|
+
def_delegator :@storage, :[]
|
24
|
+
def_delegator :@storage, :include?, :known?
|
25
|
+
def_delegator :@storage, :keys, :names
|
26
|
+
def_delegator :self, :__, :g
|
32
27
|
|
33
|
-
|
34
|
-
|
28
|
+
def ==(other)
|
29
|
+
@storage == other.storage
|
30
|
+
end
|
35
31
|
|
36
|
-
|
32
|
+
def add(name, shortcut)
|
33
|
+
@storage[name] = shortcut
|
37
34
|
|
38
|
-
|
39
|
-
next sc.new(name, args: args, params: params, previous_step: self, pool: Grumlin.default_pool)
|
40
|
-
end
|
41
|
-
extend_traversal_classes(shortcut) unless shortcut.lazy?
|
42
|
-
end
|
35
|
+
sc = step_class
|
43
36
|
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
37
|
+
shortcut_methods_module.define_method(name) do |*args, **params|
|
38
|
+
next sc.new(name, args: args, params: params, previous_step: self, pool: Grumlin.default_pool)
|
39
|
+
end
|
40
|
+
extend_traversal_classes(shortcut) unless shortcut.lazy?
|
41
|
+
end
|
49
42
|
|
50
|
-
|
51
|
-
|
52
|
-
|
43
|
+
def add_from(other)
|
44
|
+
other.storage.each do |name, shortcut|
|
45
|
+
add(name, shortcut)
|
46
|
+
end
|
47
|
+
end
|
53
48
|
|
54
|
-
|
55
|
-
|
56
|
-
|
49
|
+
def __
|
50
|
+
traversal_start_class.new(pool: Grumlin.default_pool)
|
51
|
+
end
|
57
52
|
|
58
|
-
|
59
|
-
|
60
|
-
|
53
|
+
def traversal_start_class
|
54
|
+
@traversal_start_class ||= shortcut_aware_class(Grumlin::TraversalStart)
|
55
|
+
end
|
61
56
|
|
62
|
-
|
63
|
-
|
64
|
-
|
57
|
+
def step_class
|
58
|
+
@step_class ||= shortcut_aware_class(Grumlin::Step)
|
59
|
+
end
|
65
60
|
|
66
|
-
|
61
|
+
protected
|
67
62
|
|
68
|
-
|
63
|
+
attr_reader :storage
|
69
64
|
|
70
|
-
|
65
|
+
private
|
71
66
|
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
end
|
79
|
-
end
|
67
|
+
def shortcut_methods_module
|
68
|
+
@shortcut_methods_module ||= begin
|
69
|
+
shorts = self
|
70
|
+
Module.new do
|
71
|
+
define_method :shortcuts do
|
72
|
+
shorts
|
80
73
|
end
|
81
74
|
end
|
75
|
+
end
|
76
|
+
end
|
82
77
|
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
78
|
+
def shortcut_aware_class(base)
|
79
|
+
methods = shortcut_methods_module
|
80
|
+
Class.new(base) do
|
81
|
+
include methods
|
82
|
+
end
|
83
|
+
end
|
89
84
|
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
end
|
94
|
-
step_class.include(m)
|
95
|
-
traversal_start_class.include(m)
|
96
|
-
end
|
85
|
+
def extend_traversal_classes(shortcut)
|
86
|
+
m = Module.new do
|
87
|
+
define_method(shortcut.name, &shortcut.block)
|
97
88
|
end
|
89
|
+
step_class.include(m)
|
90
|
+
traversal_start_class.include(m)
|
98
91
|
end
|
99
92
|
end
|
@@ -1,28 +1,24 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
module Grumlin
|
4
|
-
|
5
|
-
module Upserts
|
6
|
-
extend Grumlin::Shortcuts
|
3
|
+
module Grumlin::Shortcuts::Upserts
|
4
|
+
extend Grumlin::Shortcuts
|
7
5
|
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
6
|
+
shortcut :upsertV do |label, id, create_properties = {}, update_properties = {}|
|
7
|
+
self.V(id)
|
8
|
+
.fold
|
9
|
+
.coalesce(
|
10
|
+
__.unfold,
|
11
|
+
__.addV(label).props(Cardinality.single, **create_properties.merge(T.id => id))
|
12
|
+
).props(Cardinality.single, **update_properties)
|
13
|
+
end
|
16
14
|
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
end
|
26
|
-
end
|
15
|
+
shortcut :upsertE do |label, from, to, create_properties = {}, update_properties = {}|
|
16
|
+
self.V(from)
|
17
|
+
.outE(label).where(__.inV.hasId(to))
|
18
|
+
.fold
|
19
|
+
.coalesce(
|
20
|
+
__.unfold,
|
21
|
+
__.addE(label).from(__.V(from)).to(__.V(to)).props(**create_properties)
|
22
|
+
).props(**update_properties)
|
27
23
|
end
|
28
24
|
end
|
data/lib/grumlin/shortcuts.rb
CHANGED
@@ -1,36 +1,34 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
module Grumlin
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
end
|
3
|
+
module Grumlin::Shortcuts
|
4
|
+
def self.extended(base)
|
5
|
+
base.include(Grumlin::Expressions)
|
6
|
+
end
|
8
7
|
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
8
|
+
def inherited(subclass)
|
9
|
+
super
|
10
|
+
subclass.shortcuts_from(self)
|
11
|
+
end
|
13
12
|
|
14
|
-
|
15
|
-
|
16
|
-
|
13
|
+
def shortcut(name, shortcut = nil, override: false, lazy: true, &block)
|
14
|
+
name = name.to_sym
|
15
|
+
lazy = false if override
|
17
16
|
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
17
|
+
if Grumlin::Step::REGULAR_STEPS.include?(name) && !override
|
18
|
+
raise ArgumentError,
|
19
|
+
"overriding standard gremlin steps is not allowed, if you know what you're doing, pass `override: true`"
|
20
|
+
end
|
22
21
|
|
23
|
-
|
22
|
+
raise ArgumentError, "either shortcut or block must be passed" if [shortcut, block].count(&:nil?) != 1
|
24
23
|
|
25
|
-
|
26
|
-
|
24
|
+
shortcuts.add(name, shortcut || Grumlin::Shortcut.new(name, lazy: lazy, &block))
|
25
|
+
end
|
27
26
|
|
28
|
-
|
29
|
-
|
30
|
-
|
27
|
+
def shortcuts_from(other_shortcuts)
|
28
|
+
shortcuts.add_from(other_shortcuts.shortcuts)
|
29
|
+
end
|
31
30
|
|
32
|
-
|
33
|
-
|
34
|
-
end
|
31
|
+
def shortcuts
|
32
|
+
@shortcuts ||= Storage.new
|
35
33
|
end
|
36
34
|
end
|
@@ -1,41 +1,39 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
|
4
|
-
class
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
steps
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
processed_steps.add(step.name, args: step.args, params: step.params)
|
19
|
-
end
|
3
|
+
class Grumlin::ShortcutsApplyer
|
4
|
+
class << self
|
5
|
+
def call(steps)
|
6
|
+
return steps if !steps.is_a?(Grumlin::Steps) || !steps.uses_shortcuts?
|
7
|
+
|
8
|
+
shortcuts = steps.shortcuts
|
9
|
+
|
10
|
+
steps = [
|
11
|
+
*process_steps(steps.configuration_steps, shortcuts),
|
12
|
+
*process_steps(steps.steps, shortcuts)
|
13
|
+
]
|
14
|
+
|
15
|
+
Grumlin::Steps.new(shortcuts).tap do |processed_steps|
|
16
|
+
steps.each do |step|
|
17
|
+
processed_steps.add(step.name, args: step.args, params: step.params)
|
20
18
|
end
|
21
19
|
end
|
20
|
+
end
|
22
21
|
|
23
|
-
|
22
|
+
private
|
24
23
|
|
25
|
-
|
26
|
-
|
27
|
-
|
24
|
+
def process_steps(steps, shortcuts) # rubocop:disable Metrics/AbcSize
|
25
|
+
steps.each_with_object([]) do |step, result|
|
26
|
+
args = step.args.map { |arg| call(arg) }
|
28
27
|
|
29
|
-
|
30
|
-
|
28
|
+
shortcut = shortcuts[step.name]
|
29
|
+
next result << Grumlin::StepData.new(step.name, args: args, params: step.params) unless shortcut&.lazy?
|
31
30
|
|
32
|
-
|
33
|
-
|
34
|
-
|
31
|
+
t = shortcuts.__
|
32
|
+
step = shortcut.apply(t, *args, **step.params)
|
33
|
+
next if step.nil? || step == t # Shortcut did not add any steps
|
35
34
|
|
36
|
-
|
37
|
-
|
38
|
-
end
|
35
|
+
new_steps = call(Grumlin::Steps.from(step))
|
36
|
+
result.concat(new_steps.configuration_steps, new_steps.steps)
|
39
37
|
end
|
40
38
|
end
|
41
39
|
end
|