grumlin 0.22.4 → 1.0.0.rc1
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 +4 -4
- data/.gitignore +0 -1
- data/.rubocop.yml +9 -9
- data/Gemfile.lock +10 -8
- data/README.md +102 -141
- data/Rakefile +1 -1
- data/bin/console +18 -3
- data/doc/middlewares.md +97 -0
- data/grumlin.gemspec +1 -0
- data/lib/async/channel.rb +54 -56
- data/lib/grumlin/benchmark/repository.rb +10 -14
- data/lib/grumlin/client.rb +92 -112
- data/lib/grumlin/config.rb +30 -15
- 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 +8 -0
- data/lib/grumlin/middlewares/build_query.rb +20 -0
- data/lib/grumlin/middlewares/builder.rb +15 -0
- data/lib/grumlin/middlewares/cast_results.rb +7 -0
- 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 +11 -0
- data/lib/grumlin/middlewares/run_query.rb +7 -0
- data/lib/grumlin/middlewares/serialize_to_bytecode.rb +9 -0
- data/lib/grumlin/middlewares/serialize_to_steps.rb +8 -0
- 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 +92 -0
- data/lib/grumlin/step_data.rb +12 -14
- data/lib/grumlin/steppable.rb +24 -22
- data/lib/grumlin/steps.rb +51 -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 +26 -27
- 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
- data/lib/grumlin.rb +23 -19
- metadata +32 -6
- data/lib/grumlin/action.rb +0 -92
- data/lib/grumlin/sugar.rb +0 -15
data/lib/grumlin/transport.rb
CHANGED
@@ -1,101 +1,99 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
# and https://github.com/socketry/async-websocket
|
7
|
-
|
8
|
-
include Console
|
9
|
-
|
10
|
-
attr_reader :url
|
11
|
-
|
12
|
-
# Transport is not reusable. Once closed should be recreated.
|
13
|
-
def initialize(url, parent: Async::Task.current, **client_options)
|
14
|
-
@url = url
|
15
|
-
@parent = parent
|
16
|
-
@client_options = client_options
|
17
|
-
@request_channel = Async::Channel.new
|
18
|
-
@response_channel = Async::Channel.new
|
19
|
-
end
|
3
|
+
class Grumlin::Transport
|
4
|
+
# A transport based on https://github.com/socketry/async
|
5
|
+
# and https://github.com/socketry/async-websocket
|
20
6
|
|
21
|
-
|
22
|
-
!@connection.nil?
|
23
|
-
end
|
7
|
+
include Console
|
24
8
|
|
25
|
-
|
26
|
-
raise ClientClosedError if @closed
|
27
|
-
raise AlreadyConnectedError if connected?
|
9
|
+
attr_reader :url
|
28
10
|
|
29
|
-
|
30
|
-
|
11
|
+
# Transport is not reusable. Once closed should be recreated.
|
12
|
+
def initialize(url, parent: Async::Task.current, **client_options)
|
13
|
+
@url = url
|
14
|
+
@parent = parent
|
15
|
+
@client_options = client_options
|
16
|
+
@request_channel = Async::Channel.new
|
17
|
+
@response_channel = Async::Channel.new
|
18
|
+
end
|
31
19
|
|
32
|
-
|
20
|
+
def connected?
|
21
|
+
!@connection.nil?
|
22
|
+
end
|
33
23
|
|
34
|
-
|
24
|
+
def connect
|
25
|
+
raise ClientClosedError if @closed
|
26
|
+
raise AlreadyConnectedError if connected?
|
35
27
|
|
36
|
-
|
37
|
-
|
28
|
+
@connection = Async::WebSocket::Client.connect(Async::HTTP::Endpoint.parse(@url), **@client_options)
|
29
|
+
logger.debug(self) { "Connected to #{@url}." }
|
38
30
|
|
39
|
-
|
40
|
-
raise NotConnectedError unless connected?
|
31
|
+
@response_task = @parent.async { run_response_task }
|
41
32
|
|
42
|
-
|
43
|
-
end
|
33
|
+
@request_task = @parent.async { run_request_task }
|
44
34
|
|
45
|
-
|
46
|
-
|
35
|
+
@response_channel
|
36
|
+
end
|
47
37
|
|
48
|
-
|
38
|
+
def write(message)
|
39
|
+
raise NotConnectedError unless connected?
|
49
40
|
|
50
|
-
|
51
|
-
|
41
|
+
@request_channel << message
|
42
|
+
end
|
52
43
|
|
53
|
-
|
54
|
-
|
55
|
-
rescue StandardError
|
56
|
-
nil
|
57
|
-
end
|
58
|
-
@connection = nil
|
44
|
+
def close
|
45
|
+
return if @closed
|
59
46
|
|
60
|
-
|
61
|
-
|
62
|
-
|
47
|
+
@closed = true
|
48
|
+
|
49
|
+
@request_channel.close
|
50
|
+
@response_channel.close
|
63
51
|
|
64
|
-
|
65
|
-
@
|
66
|
-
|
52
|
+
begin
|
53
|
+
@connection.close
|
54
|
+
rescue StandardError
|
55
|
+
nil
|
67
56
|
end
|
57
|
+
@connection = nil
|
68
58
|
|
69
|
-
|
59
|
+
@request_task&.stop(true)
|
60
|
+
@response_task&.stop(true)
|
61
|
+
end
|
62
|
+
|
63
|
+
def wait
|
64
|
+
@request_task.wait
|
65
|
+
@response_task.wait
|
66
|
+
end
|
70
67
|
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
68
|
+
private
|
69
|
+
|
70
|
+
def run_response_task
|
71
|
+
with_guard do
|
72
|
+
loop do
|
73
|
+
data = @connection.read
|
74
|
+
@response_channel << data
|
77
75
|
end
|
78
76
|
end
|
77
|
+
end
|
79
78
|
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
end
|
79
|
+
def run_request_task
|
80
|
+
with_guard do
|
81
|
+
@request_channel.each do |message|
|
82
|
+
@connection.write(message)
|
83
|
+
@connection.flush
|
86
84
|
end
|
87
85
|
end
|
86
|
+
end
|
88
87
|
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
end
|
98
|
-
close
|
88
|
+
def with_guard
|
89
|
+
yield
|
90
|
+
rescue Async::Stop, Async::TimeoutError, StandardError => e
|
91
|
+
logger.debug(self) { "Guard error, closing." }
|
92
|
+
begin
|
93
|
+
@response_channel.exception(e)
|
94
|
+
rescue Async::Channel::ChannelClosedError
|
95
|
+
nil
|
99
96
|
end
|
97
|
+
close
|
100
98
|
end
|
101
99
|
end
|
@@ -1,42 +1,40 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
begin
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
transaction.commit
|
25
|
-
end
|
3
|
+
class Grumlin::TraversalStart < Grumlin::Steppable
|
4
|
+
include Grumlin::WithExtension
|
5
|
+
|
6
|
+
class TraversalError < Grumlin::Error; end
|
7
|
+
class AlreadyBoundToTransactionError < TraversalError; end
|
8
|
+
|
9
|
+
def tx
|
10
|
+
raise AlreadyBoundToTransactionError if @session_id
|
11
|
+
|
12
|
+
transaction = tx_class.new(self.class, pool: @pool, middlewares: @middlewares)
|
13
|
+
return transaction unless block_given?
|
14
|
+
|
15
|
+
begin
|
16
|
+
yield transaction.begin
|
17
|
+
rescue Grumlin::Rollback
|
18
|
+
transaction.rollback
|
19
|
+
rescue StandardError
|
20
|
+
transaction.rollback
|
21
|
+
raise
|
22
|
+
else
|
23
|
+
transaction.commit
|
26
24
|
end
|
25
|
+
end
|
27
26
|
|
28
|
-
|
29
|
-
|
30
|
-
|
27
|
+
def to_s(*)
|
28
|
+
self.class.to_s
|
29
|
+
end
|
31
30
|
|
32
|
-
|
33
|
-
|
34
|
-
|
31
|
+
def inspect
|
32
|
+
self.class.inspect
|
33
|
+
end
|
35
34
|
|
36
|
-
|
35
|
+
private
|
37
36
|
|
38
|
-
|
39
|
-
|
40
|
-
end
|
37
|
+
def tx_class
|
38
|
+
Grumlin.features.supports_transactions? ? Grumlin::Transaction : Grumlin::DummyTransaction
|
41
39
|
end
|
42
40
|
end
|
@@ -1,11 +1,7 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
def initialize(value)
|
7
|
-
super(type: "OptionsStrategy", value: value)
|
8
|
-
end
|
9
|
-
end
|
3
|
+
class Grumlin::TraversalStrategies::OptionsStrategy < Grumlin::TypedValue
|
4
|
+
def initialize(value)
|
5
|
+
super(type: "OptionsStrategy", value: value)
|
10
6
|
end
|
11
7
|
end
|
data/lib/grumlin/traverser.rb
CHANGED
@@ -1,12 +1,10 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
|
4
|
-
|
5
|
-
attr_reader :bulk, :value
|
3
|
+
class Grumlin::Traverser
|
4
|
+
attr_reader :bulk, :value
|
6
5
|
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
end
|
6
|
+
def initialize(value)
|
7
|
+
@bulk = value.dig(:bulk, :@value) || 1
|
8
|
+
@value = Grumlin::Typing.cast(value[:value])
|
11
9
|
end
|
12
10
|
end
|
data/lib/grumlin/typed_value.rb
CHANGED
@@ -1,20 +1,18 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
|
4
|
-
|
5
|
-
attr_reader :type, :value
|
3
|
+
class Grumlin::TypedValue
|
4
|
+
attr_reader :type, :value
|
6
5
|
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
6
|
+
def initialize(type: nil, value: nil)
|
7
|
+
@type = type
|
8
|
+
@value = value
|
9
|
+
end
|
11
10
|
|
12
|
-
|
13
|
-
|
14
|
-
|
11
|
+
def inspect
|
12
|
+
"<#{type}.#{value}>"
|
13
|
+
end
|
15
14
|
|
16
|
-
|
17
|
-
|
18
|
-
end
|
15
|
+
def to_s
|
16
|
+
inspect
|
19
17
|
end
|
20
18
|
end
|
data/lib/grumlin/typing.rb
CHANGED
@@ -1,89 +1,87 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
module Grumlin
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
end
|
3
|
+
module Grumlin::Typing
|
4
|
+
TYPES = {
|
5
|
+
"g:List" => ->(value) { cast_list(value) },
|
6
|
+
"g:Set" => ->(value) { cast_list(value).to_set },
|
7
|
+
"g:Map" => ->(value) { cast_map(value) },
|
8
|
+
"g:Vertex" => ->(value) { cast_entity(Grumlin::Vertex, value) },
|
9
|
+
"g:Edge" => ->(value) { cast_entity(Grumlin::Edge, value) },
|
10
|
+
"g:Path" => ->(value) { cast_entity(Grumlin::Path, value) },
|
11
|
+
"g:Traverser" => ->(value) { cast_entity(Grumlin::Traverser, value) },
|
12
|
+
"g:Property" => ->(value) { cast_entity(Grumlin::Property, value) },
|
13
|
+
"g:Int64" => ->(value) { cast_int(value) },
|
14
|
+
"g:Int32" => ->(value) { cast_int(value) },
|
15
|
+
"g:Double" => ->(value) { cast_double(value) },
|
16
|
+
"g:Direction" => ->(value) { value },
|
17
|
+
"g:VertexProperty" => ->(value) { cast_entity(Grumlin::VertexProperty, value) },
|
18
|
+
"g:TraversalMetrics" => ->(value) { cast_map(value[:@value]) },
|
19
|
+
"g:Metrics" => ->(value) { cast_map(value[:@value]) },
|
20
|
+
"g:T" => ->(value) { Grumlin::Expressions::T.public_send(value) }
|
21
|
+
}.freeze
|
22
|
+
|
23
|
+
CASTABLE_TYPES = [Hash, String, Integer, TrueClass, FalseClass, NilClass].freeze
|
24
|
+
|
25
|
+
class << self
|
26
|
+
def cast(value)
|
27
|
+
verify_type!(value)
|
28
|
+
|
29
|
+
return value unless value.is_a?(Hash)
|
30
|
+
|
31
|
+
type = TYPES[value[:@type]]
|
32
|
+
|
33
|
+
verify_castable_hash!(value, type)
|
34
|
+
|
35
|
+
type.call(value[:@value])
|
36
|
+
end
|
38
37
|
|
39
|
-
|
38
|
+
private
|
40
39
|
|
41
|
-
|
42
|
-
|
43
|
-
|
40
|
+
def verify_type!(value)
|
41
|
+
raise TypeError, "#{value.inspect} cannot be casted" unless CASTABLE_TYPES.include?(value.class)
|
42
|
+
end
|
44
43
|
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
44
|
+
def verify_castable_hash!(value, type)
|
45
|
+
raise TypeError, "#{value} cannot be casted, @type is missing" if value[:@type].nil?
|
46
|
+
raise(UnknownTypeError, value[:@type]) if type.nil?
|
47
|
+
raise TypeError, "#{value} cannot be casted, @value is missing" if value[:@value].nil?
|
48
|
+
end
|
50
49
|
|
51
|
-
|
52
|
-
|
50
|
+
def cast_int(value)
|
51
|
+
raise TypeError, "#{value} is not an Integer" unless value.is_a?(Integer)
|
53
52
|
|
54
|
-
|
55
|
-
|
53
|
+
value
|
54
|
+
end
|
56
55
|
|
57
|
-
|
58
|
-
|
56
|
+
def cast_double(value)
|
57
|
+
raise TypeError, "#{value} is not a Double" unless value.is_a?(Float)
|
59
58
|
|
60
|
-
|
61
|
-
|
59
|
+
value
|
60
|
+
end
|
62
61
|
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
62
|
+
def cast_entity(entity, value)
|
63
|
+
entity.new(**value)
|
64
|
+
rescue ArgumentError, TypeError
|
65
|
+
raise TypeError, "#{value} cannot be casted to #{entity.name}"
|
66
|
+
end
|
68
67
|
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
68
|
+
def cast_map(value)
|
69
|
+
Hash[*value].transform_keys do |key|
|
70
|
+
next key.to_sym if key.respond_to?(:to_sym)
|
71
|
+
next cast(key) if key[:@type] # TODO: g.V.group.by(:none_existing_property).next
|
73
72
|
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
73
|
+
raise UnknownMapKey, key, value
|
74
|
+
end.transform_values { |v| cast(v) }
|
75
|
+
rescue ArgumentError
|
76
|
+
raise TypeError, "#{value} cannot be casted to Hash"
|
77
|
+
end
|
79
78
|
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
79
|
+
def cast_list(value)
|
80
|
+
value.each_with_object([]) do |item, result|
|
81
|
+
casted_value = cast(item)
|
82
|
+
next (result << casted_value) unless casted_value.instance_of?(Grumlin::Traverser)
|
84
83
|
|
85
|
-
|
86
|
-
end
|
84
|
+
casted_value.bulk.times { result << casted_value.value }
|
87
85
|
end
|
88
86
|
end
|
89
87
|
end
|
data/lib/grumlin/version.rb
CHANGED
data/lib/grumlin/vertex.rb
CHANGED
@@ -1,24 +1,22 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
|
4
|
-
|
5
|
-
attr_reader :label, :id
|
3
|
+
class Grumlin::Vertex
|
4
|
+
attr_reader :label, :id
|
6
5
|
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
6
|
+
def initialize(label:, id:)
|
7
|
+
@label = label
|
8
|
+
@id = Grumlin::Typing.cast(id)
|
9
|
+
end
|
11
10
|
|
12
|
-
|
13
|
-
|
14
|
-
|
11
|
+
def ==(other)
|
12
|
+
self.class == other.class && @label == other.label && @id == other.id
|
13
|
+
end
|
15
14
|
|
16
|
-
|
17
|
-
|
18
|
-
|
15
|
+
def inspect
|
16
|
+
"v[#{@id}]"
|
17
|
+
end
|
19
18
|
|
20
|
-
|
21
|
-
|
22
|
-
end
|
19
|
+
def to_s
|
20
|
+
inspect
|
23
21
|
end
|
24
22
|
end
|
@@ -1,24 +1,22 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
|
4
|
-
|
5
|
-
attr_reader :label, :value
|
3
|
+
class Grumlin::VertexProperty
|
4
|
+
attr_reader :label, :value
|
6
5
|
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
6
|
+
def initialize(value)
|
7
|
+
@label = value[:label]
|
8
|
+
@value = Grumlin::Typing.cast(value[:value])
|
9
|
+
end
|
11
10
|
|
12
|
-
|
13
|
-
|
14
|
-
|
11
|
+
def inspect
|
12
|
+
"vp[#{label}->#{value}]"
|
13
|
+
end
|
15
14
|
|
16
|
-
|
17
|
-
|
18
|
-
|
15
|
+
def to_s
|
16
|
+
inspect
|
17
|
+
end
|
19
18
|
|
20
|
-
|
21
|
-
|
22
|
-
end
|
19
|
+
def ==(other)
|
20
|
+
self.class == other.class && @label == other.label && @value == other.value
|
23
21
|
end
|
24
22
|
end
|
@@ -1,27 +1,25 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
module Grumlin
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
end
|
3
|
+
module Grumlin::WithExtension
|
4
|
+
def with(name, value)
|
5
|
+
prev = self
|
6
|
+
strategy = if is_a?(with_step_class)
|
7
|
+
prev = previous_step
|
8
|
+
Grumlin::TraversalStrategies::OptionsStrategy.new(args.first.value.merge(name => value))
|
9
|
+
else
|
10
|
+
Grumlin::TraversalStrategies::OptionsStrategy.new({ name => value })
|
11
|
+
end
|
12
|
+
with_step_class.new(:withStrategies, args: [strategy], previous_step: prev)
|
13
|
+
end
|
15
14
|
|
16
|
-
|
15
|
+
private
|
17
16
|
|
18
|
-
|
19
|
-
|
20
|
-
|
17
|
+
def with_step_class
|
18
|
+
@with_step_class ||= Class.new(shortcuts.step_class) do
|
19
|
+
include Grumlin::WithExtension
|
21
20
|
|
22
|
-
|
23
|
-
|
24
|
-
end
|
21
|
+
def with_step_class
|
22
|
+
self.class
|
25
23
|
end
|
26
24
|
end
|
27
25
|
end
|