grumlin 0.15.6 → 0.17.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 195f614a2cbebbe43f6dd2aedb3aeb54891c4377f50f85f21f44ee3325c87f58
4
- data.tar.gz: 0f0a3c0b5aa8f03fb4d613c406b08497685ee9fb4cbea2e8a0b99385f749ebf3
3
+ metadata.gz: 2d63d3923f895d20e89fd1647495d99bb3a97888689c8f25c34973355d7c80e1
4
+ data.tar.gz: 878546a65736522f3c1f00f42741b6b207e992a1983321205d05406e0a74ed6b
5
5
  SHA512:
6
- metadata.gz: 21a7d91e9ce0a9e143f4bba9bb15d4bd31f6337f72500038bf09cd471ab05535d9d3ade4cfba6d6b6d779e09600b6352ed83b3e4bd70aeadd1417e51ba663937
7
- data.tar.gz: 9cd14a8f18b856eca87d3b51b48c3f3c0d8d454b76fae92466bd7aee370019bbbb0b398236187acfcfed2322520aa4a3aa68a0e6bfb8b02a073e27f0db3cf525
6
+ metadata.gz: 60bf726b74019b36bb0437c78b97ea1ba78703058ba2a619805e0081d49158f30bb67cc7918b959cf0375c37a5600632a1df805563edccb0266b71c9ab70f1de
7
+ data.tar.gz: cc7dc9ecfd45c67b28301593d3ba60e471e7a9993460ce39dcb912bbed5fb0aa01ed25bcf52f49634f704d6732ba563d849fb8d492276f19fe83e5e8531a8181
data/.rubocop.yml CHANGED
@@ -80,3 +80,10 @@ Style/Documentation:
80
80
 
81
81
  Style/MultilineBlockChain:
82
82
  Enabled: false
83
+
84
+
85
+ # TODO:
86
+ # Style/SymbolArray:
87
+ # EnforcedStyle: brackets
88
+ # Style/WordArray:
89
+ # EnforcedStyle: brackets
data/CHANGELOG.md CHANGED
@@ -1,3 +1,8 @@
1
+ ## [0.16.0] - 2022-03-11
2
+
3
+ - Query building is rewritten from scratch. No public APIs were changed. [Details](https://github.com/babbel/grumlin/pull/64)
4
+ - Add support for [TextP](https://tinkerpop.apache.org/javadocs/current/core/org/apache/tinkerpop/gremlin/process/traversal/TextP.html)
5
+
1
6
  ## [0.15.4] - 2022-01-20
2
7
 
3
8
  - Move step and expression definitions to a yaml file for better diffs
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- grumlin (0.15.6)
4
+ grumlin (0.17.0)
5
5
  async-pool (~> 0.3)
6
6
  async-websocket (~> 0.19)
7
7
  oj (~> 3.12)
@@ -44,7 +44,7 @@ GEM
44
44
  benchmark (0.1.1)
45
45
  childprocess (4.0.0)
46
46
  concurrent-ruby (1.1.8)
47
- console (1.14.0)
47
+ console (1.15.0)
48
48
  fiber-local
49
49
  diff-lcs (1.4.4)
50
50
  docile (1.4.0)
data/README.md CHANGED
@@ -150,7 +150,7 @@ shortcuts to make gremlin code more rubyish. Can be used as a drop in replacemen
150
150
  or `Grumlin::Shortcuts` can be inherited**, successors don't need to extend them again and have access to shortcuts
151
151
  defined in the ancestor.
152
152
 
153
- **Using**:
153
+ **Definition**
154
154
 
155
155
  ```ruby
156
156
  class MyRepository
@@ -167,15 +167,45 @@ class MyRepository
167
167
  hasAll(T.label => :triangle, color: color)
168
168
  end
169
169
 
170
- # g and __ are already aware of shortcuts
171
- def red_triangles
170
+ # g and __ are already aware of shortcuts
171
+ query(:triangles_with_color, return_mode: :list) do |color| # :list is the default return mode, also possible: :none, :single, :traversal
172
172
  g.V.hasLabel(:triangle)
173
- .hasColor("red")
174
- .toList
173
+ .hasColor(color)
175
174
  end
175
+ # Note that when using the `query` one does not need to call a termination step like `next` or `toList`,
176
+ # repository does it automatically in according to the `return_mode` parameter.
176
177
  end
177
178
  ```
178
179
 
180
+ Each `return_mode` is mapped to a particular termination step:
181
+ - `:list` - `toList`
182
+ - `:single` - `next`
183
+ - `:none` - `iterate`
184
+ - `:traversal` - do not execute the query and return the traversal as is
185
+
186
+ **Usage**
187
+
188
+ To execute the query defined in a query block one simply needs to call a method with the same name:
189
+
190
+ `MyRepository.new.triangles_with_color(:red)`
191
+
192
+ One can also override the `return_mode`:
193
+
194
+ `MyRepository.new.triangles_with_color(:red, query_params: { return_mode: :single })`
195
+
196
+ or even pass a block to the method and a raw traversal will be yielded:
197
+ ```ruby
198
+ MyRepository.new.triangles_with_color(:red) do |t|
199
+ t.has(:other_property, :some_value).toList
200
+ end
201
+ ```
202
+ it may be useful for debugging. Note that one needs to call a termination step manually in this case.
203
+
204
+ `query` also provides a helper for profiling requests:
205
+ `MyRepository.new.triangles_with_color(:red, query_params: { profile: true })`
206
+
207
+ method will return profiling data of the results.
208
+
179
209
  #### IRB
180
210
 
181
211
  An example of how to start an IRB session with support for executing gremlin queries:
data/lib/definitions.yml CHANGED
@@ -46,6 +46,7 @@ steps:
46
46
  - outE
47
47
  - outV
48
48
  - path
49
+ - profile
49
50
  - project
50
51
  - properties
51
52
  - property
@@ -0,0 +1,124 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Grumlin
4
+ class Action
5
+ START_STEPS = Grumlin.definitions.dig(:steps, :start).map(&:to_sym).freeze
6
+ CONFIGURATION_STEPS = Grumlin.definitions.dig(:steps, :configuration).map(&:to_sym).freeze
7
+ REGULAR_STEPS = Grumlin.definitions.dig(:steps, :regular).map(&:to_sym).freeze
8
+
9
+ ALL_STEPS = START_STEPS + CONFIGURATION_STEPS + REGULAR_STEPS
10
+
11
+ attr_reader :name, :args, :params, :shortcuts, :next_step, :configuration_steps, :previous_step
12
+
13
+ def initialize(name, args: [], params: {}, previous_step: nil, shortcuts: {}, pool: Grumlin.default_pool)
14
+ @name = name.to_sym
15
+ @args = args # TODO: add recursive validation: only json types or Action
16
+ @params = params # TODO: add recursive validation: only json types
17
+ @previous_step = previous_step
18
+ @shortcuts = shortcuts
19
+ @pool = pool
20
+ end
21
+
22
+ ALL_STEPS.each do |step|
23
+ define_method step do |*args, **params|
24
+ step(step, *args, **params)
25
+ end
26
+ end
27
+
28
+ def step(name, *args, **params)
29
+ Action.new(name, args: args, params: params, previous_step: self, shortcuts: @shortcuts, pool: @pool)
30
+ end
31
+
32
+ def configuration_step?
33
+ CONFIGURATION_STEPS.include?(@name)
34
+ end
35
+
36
+ def start_step?
37
+ START_STEPS.include?(@name)
38
+ end
39
+
40
+ def regular_step?
41
+ REGULAR_STEPS.include?(@name)
42
+ end
43
+
44
+ def supported_step?
45
+ ALL_STEPS.include?(@name)
46
+ end
47
+
48
+ def shortcut?
49
+ @shortcuts.key?(@name)
50
+ end
51
+
52
+ def arguments
53
+ @arguments ||= [*@args].tap do |args|
54
+ args << @params if @params.any?
55
+ end
56
+ end
57
+
58
+ def method_missing(name, *args, **params)
59
+ return step(name, *args, **params) if @shortcuts.key?(name)
60
+
61
+ super
62
+ end
63
+
64
+ def ==(other)
65
+ self.class == other.class &&
66
+ @name == other.name &&
67
+ @args == other.args &&
68
+ @params == other.params &&
69
+ @previous_step == other.previous_step &&
70
+ @shortcuts == other.shortcuts
71
+ end
72
+
73
+ def steps
74
+ @steps ||= Steps.from(self)
75
+ end
76
+
77
+ def to_s(**params)
78
+ StepsSerializers::String.new(steps, **params).serialize
79
+ end
80
+
81
+ # TODO: add human readable mode
82
+ def inspect
83
+ conf_steps, regular_steps = StepsSerializers::HumanReadableBytecode.new(steps).serialize
84
+ "#{conf_steps.any? ? conf_steps : nil}#{regular_steps}"
85
+ end
86
+
87
+ def bytecode(no_return: false)
88
+ StepsSerializers::Bytecode.new(steps, no_return: no_return)
89
+ end
90
+
91
+ def next
92
+ to_enum.next
93
+ end
94
+
95
+ def hasNext # rubocop:disable Naming/MethodName
96
+ to_enum.peek
97
+ true
98
+ rescue StopIteration
99
+ false
100
+ end
101
+
102
+ def to_enum
103
+ @to_enum ||= toList.to_enum
104
+ end
105
+
106
+ def toList
107
+ @pool.acquire do |client|
108
+ client.write(bytecode)
109
+ end
110
+ end
111
+
112
+ def iterate
113
+ @pool.acquire do |client|
114
+ client.write(bytecode(no_return: true))
115
+ end
116
+ end
117
+
118
+ private
119
+
120
+ def respond_to_missing?(name, _include_private = false)
121
+ @shortcuts.key?(name)
122
+ end
123
+ end
124
+ end
@@ -97,7 +97,7 @@ module Grumlin
97
97
  def write(bytecode)
98
98
  raise NotConnectedError unless connected?
99
99
 
100
- request = to_query(bytecode.to_bytecode)
100
+ request = to_query(bytecode)
101
101
  channel = @request_dispatcher.add_request(request)
102
102
  @transport.write(request)
103
103
 
@@ -130,7 +130,7 @@ module Grumlin
130
130
  op: "bytecode",
131
131
  processor: "traversal",
132
132
  args: {
133
- gremlin: bytecode,
133
+ gremlin: { :@type => "g:Bytecode", :@value => bytecode.serialize },
134
134
  aliases: { g: :g }
135
135
  }
136
136
  }
@@ -2,28 +2,23 @@
2
2
 
3
3
  module Grumlin
4
4
  module Expressions
5
- module P
6
- class << self
7
- class Predicate < TypedValue
8
- def initialize(name, args:, arg_type: nil)
9
- super(type: "P")
10
- @name = name
11
- @args = args
12
- @arg_type = arg_type
13
- end
5
+ class P
6
+ class Predicate
7
+ attr_reader :namespace, :name, :value, :type
14
8
 
15
- def value
16
- @value ||= {
17
- predicate: @name,
18
- value: TypedValue.new(type: @arg_type, value: @args).to_bytecode
19
- }
20
- end
9
+ def initialize(namespace, name, value:, type: nil)
10
+ @namespace = namespace
11
+ @name = name
12
+ @value = value
13
+ @type = type
21
14
  end
15
+ end
22
16
 
17
+ class << self
23
18
  # TODO: support more predicates
24
19
  %i[eq neq].each do |predicate|
25
20
  define_method predicate do |*args|
26
- Predicate.new(predicate, args: args[0])
21
+ Predicate.new("P", predicate, value: args[0])
27
22
  end
28
23
  end
29
24
 
@@ -36,7 +31,7 @@ module Grumlin
36
31
  else
37
32
  args.to_a
38
33
  end
39
- Predicate.new(predicate, args: args, arg_type: "List")
34
+ Predicate.new("P", predicate, value: args, type: "List")
40
35
  end
41
36
  end
42
37
  end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Grumlin
4
+ module Expressions
5
+ class TextP < P
6
+ class << self
7
+ %i[containing endingWith notContaining notEndingWith notStartingWith startingWith].each do |predicate|
8
+ define_method predicate do |*args|
9
+ P::Predicate.new("TextP", predicate, value: args[0])
10
+ end
11
+ end
12
+ end
13
+ end
14
+ end
15
+ end
@@ -2,16 +2,30 @@
2
2
 
3
3
  module Grumlin
4
4
  module Expressions
5
- module WithOptions
5
+ class WithOptions
6
6
  WITH_OPTIONS = Grumlin.definitions.dig(:expressions, :with_options).freeze
7
7
 
8
8
  class << self
9
9
  WITH_OPTIONS.each do |k, v|
10
10
  define_method k do
11
- v
11
+ name = "@#{k}"
12
+ return instance_variable_get(name) if instance_variable_defined?(name)
13
+
14
+ instance_variable_set(name, WithOptions.new(k, v))
12
15
  end
13
16
  end
14
17
  end
18
+
19
+ attr_reader :name, :value
20
+
21
+ def initialize(name, value)
22
+ @name = name
23
+ @value = value
24
+ end
25
+
26
+ def to_s
27
+ "WithOptions.#{@name}"
28
+ end
15
29
  end
16
30
  end
17
31
  end
@@ -2,13 +2,20 @@
2
2
 
3
3
  module Grumlin
4
4
  module Repository
5
+ RETURN_MODES = {
6
+ list: :toList,
7
+ none: :iterate,
8
+ single: :next,
9
+ traversal: :nil
10
+ }.freeze
11
+
5
12
  module InstanceMethods
6
13
  def __
7
- with_shortcuts(Grumlin::Expressions::U)
14
+ TraversalStart.new(self.class.shortcuts)
8
15
  end
9
16
 
10
17
  def g
11
- with_shortcuts(Grumlin::Traversal.new)
18
+ TraversalStart.new(self.class.shortcuts)
12
19
  end
13
20
  end
14
21
 
@@ -19,5 +26,53 @@ module Grumlin
19
26
 
20
27
  base.shortcuts_from(Grumlin::Shortcuts::Properties)
21
28
  end
29
+
30
+ def query(name, return_mode: :list, postprocess_with: nil, &query_block) # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
31
+ return_mode = validate_return_mode!(return_mode)
32
+ postprocess_with = validate_postprocess_with!(postprocess_with)
33
+
34
+ define_method name do |*args, query_params: {}, **params, &block|
35
+ t = instance_exec(*args, **params, &query_block)
36
+ return t if self.class.empty_result?(t)
37
+
38
+ unless t.is_a?(Grumlin::Action)
39
+ raise WrongQueryResult,
40
+ "queries must return #{Grumlin::Action}, nil or an empty collection. Given: #{t.class}"
41
+ end
42
+
43
+ return block.call(t) unless block.nil?
44
+
45
+ return t.profile.next if query_params[:profile] == true
46
+
47
+ return_mode = self.class.validate_return_mode!(query_params[:return_mode] || return_mode)
48
+
49
+ return t if return_mode == :traversal
50
+
51
+ t.public_send(RETURN_MODES[return_mode]).tap do |result|
52
+ return postprocess_with.call(result) if postprocess_with.respond_to?(:call)
53
+ return send(postprocess_with, result) unless postprocess_with.nil?
54
+ end
55
+ end
56
+ end
57
+
58
+ def validate_return_mode!(return_mode)
59
+ return return_mode if RETURN_MODES.key?(return_mode)
60
+
61
+ raise ArgumentError, "unsupported return mode #{return_mode}. Supported modes: #{RETURN_MODES.keys}"
62
+ end
63
+
64
+ def validate_postprocess_with!(postprocess_with)
65
+ if postprocess_with.nil? || postprocess_with.is_a?(Symbol) ||
66
+ postprocess_with.is_a?(String) || postprocess_with.respond_to?(:call)
67
+ return postprocess_with
68
+ end
69
+
70
+ raise ArgumentError,
71
+ "postprocess_with must be a String, Symbol or a callable object, given: #{postprocess_with.class}"
72
+ end
73
+
74
+ def empty_result?(result)
75
+ result.nil? || (result.respond_to?(:empty?) && result.empty?)
76
+ end
22
77
  end
23
78
  end
@@ -5,13 +5,17 @@ module Grumlin
5
5
  module Properties
6
6
  extend Grumlin::Shortcuts
7
7
 
8
- shortcut :props do |*_args, **props|
8
+ shortcut :props do |props|
9
+ next if props.nil? # TODO: fixme, add proper support for **params
10
+
9
11
  props.reduce(self) do |tt, (prop, value)|
10
12
  tt.property(prop, value)
11
13
  end
12
14
  end
13
15
 
14
- shortcut :hasAll do |*, **props|
16
+ shortcut :hasAll do |props|
17
+ next if props.nil? # TODO: fixme, add proper support for **params
18
+
15
19
  props.reduce(self) do |tt, (prop, value)|
16
20
  tt.has(prop, value)
17
21
  end
@@ -2,14 +2,7 @@
2
2
 
3
3
  module Grumlin
4
4
  module Shortcuts
5
- module InstanceMethods
6
- def with_shortcuts(obj)
7
- ShortcutProxy.new(obj, self.class.shortcuts, parent: self)
8
- end
9
- end
10
-
11
5
  def self.extended(base)
12
- base.include(InstanceMethods)
13
6
  base.include(Grumlin::Expressions)
14
7
  end
15
8
 
@@ -21,7 +14,7 @@ module Grumlin
21
14
  def shortcut(name, shortcut = nil, &block) # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
22
15
  name = name.to_sym
23
16
  # TODO: blocklist of names to avoid conflicts with standard methods?
24
- if Grumlin::AnonymousStep::SUPPORTED_STEPS.include?(name)
17
+ if Grumlin::Action::REGULAR_STEPS.include?(name)
25
18
  raise ArgumentError,
26
19
  "cannot use names of standard gremlin steps"
27
20
  end
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Grumlin
4
+ class ShortcutsApplyer
5
+ class << self
6
+ def call(steps)
7
+ new.call(steps)
8
+ end
9
+ end
10
+
11
+ def call(steps)
12
+ return steps unless steps.uses_shortcuts?
13
+
14
+ shortcuts = steps.shortcuts
15
+
16
+ configuration_steps = process_steps(steps.configuration_steps, shortcuts)
17
+ regular_steps = process_steps(steps.steps, shortcuts)
18
+
19
+ Steps.new(shortcuts).tap do |processed_steps|
20
+ (configuration_steps + regular_steps).each do |step|
21
+ processed_steps.add(step.name, step.arguments)
22
+ end
23
+ end
24
+ end
25
+
26
+ private
27
+
28
+ def process_steps(steps, shortcuts) # rubocop:disable Metrics/AbcSize
29
+ steps.each_with_object([]) do |step, result|
30
+ arguments = step.arguments.map do |arg|
31
+ arg.is_a?(Steps) ? ShortcutsApplyer.call(arg) : arg
32
+ end
33
+
34
+ if shortcuts.include?(step.name)
35
+ t = TraversalStart.new(shortcuts)
36
+ action = shortcuts[step.name].apply(t, *arguments)
37
+ next if action.nil? || action == t # Shortcut did not add any steps
38
+
39
+ new_steps = ShortcutsApplyer.call(Steps.from(action))
40
+ result.concat(new_steps.configuration_steps)
41
+ result.concat(new_steps.steps)
42
+ else
43
+ result << StepData.new(step.name, arguments)
44
+ end
45
+ end
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Grumlin
4
+ class StepData
5
+ attr_reader :name, :arguments
6
+
7
+ def initialize(name, arguments)
8
+ @name = name
9
+ @arguments = arguments
10
+ end
11
+
12
+ def ==(other)
13
+ self.class == other.class &&
14
+ @name == other.name &&
15
+ @arguments == other.arguments
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,77 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Grumlin
4
+ class Steps
5
+ CONFIGURATION_STEPS = Action::CONFIGURATION_STEPS
6
+ ALL_STEPS = Action::ALL_STEPS
7
+
8
+ def self.from(action)
9
+ raise ArgumentError, "expected: #{Action}, given: #{action.class}" unless action.is_a?(Action)
10
+
11
+ shortcuts = action.shortcuts
12
+ actions = []
13
+
14
+ until action.nil?
15
+ actions.unshift(action)
16
+ action = action.previous_step
17
+ end
18
+
19
+ new(shortcuts).tap do |chain|
20
+ actions.each do |act|
21
+ chain.add(act.name, act.arguments)
22
+ end
23
+ end
24
+ end
25
+
26
+ attr_reader :configuration_steps, :steps, :shortcuts
27
+
28
+ def initialize(shortcuts, configuration_steps: [], steps: [])
29
+ @shortcuts = shortcuts
30
+ @configuration_steps = configuration_steps
31
+ @steps = steps
32
+ end
33
+
34
+ def add(name, arguments)
35
+ return add_configuration_step(name, arguments) if CONFIGURATION_STEPS.include?(name)
36
+
37
+ StepData.new(name, cast_arguments(arguments)).tap do |step|
38
+ @steps << step
39
+ end
40
+ end
41
+
42
+ def uses_shortcuts?
43
+ shortcuts?(@configuration_steps) || shortcuts?(@steps)
44
+ end
45
+
46
+ def ==(other)
47
+ self.class == other.class &&
48
+ @shortcuts == other.shortcuts &&
49
+ @configuration_steps == other.configuration_steps &&
50
+ @steps == other.steps
51
+ end
52
+
53
+ # TODO: add #bytecode, to_s, inspect
54
+
55
+ private
56
+
57
+ def shortcuts?(steps_ary)
58
+ steps_ary.any? do |step|
59
+ @shortcuts.include?(step.name) || step.arguments.any? do |arg|
60
+ arg.is_a?(Steps) ? arg.uses_shortcuts? : false
61
+ end
62
+ end
63
+ end
64
+
65
+ def add_configuration_step(name, arguments)
66
+ raise ArgumentError, "cannot use configuration steps after start step was used" unless @steps.empty?
67
+
68
+ StepData.new(name, cast_arguments(arguments)).tap do |step|
69
+ @configuration_steps << step
70
+ end
71
+ end
72
+
73
+ def cast_arguments(arguments)
74
+ arguments.map { |arg| arg.is_a?(Action) ? Steps.from(arg) : arg }
75
+ end
76
+ end
77
+ end
@@ -0,0 +1,65 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Grumlin
4
+ module StepsSerializers
5
+ class Bytecode < Serializer
6
+ # constructor params: no_return: true|false, default false
7
+ # TODO: add pretty
8
+
9
+ NONE_STEP = StepData.new("none", [])
10
+
11
+ def serialize
12
+ steps = ShortcutsApplyer.call(@steps)
13
+ no_return = @params[:no_return] || false
14
+
15
+ {
16
+ step: (steps.steps + (no_return ? [NONE_STEP] : [])).map { |s| serialize_step(s) }
17
+ }.tap do |v|
18
+ v.merge!(source: steps.configuration_steps.map { |s| serialize_step(s) }) if steps.configuration_steps.any?
19
+ end
20
+ end
21
+
22
+ private
23
+
24
+ def serialize_step(step)
25
+ [step.name, *step.arguments.map { |arg| serialize_arg(arg) }]
26
+ end
27
+
28
+ def serialize_arg(arg)
29
+ return serialize_typed_value(arg) if arg.is_a?(TypedValue)
30
+ return serialize_predicate(arg) if arg.is_a?(Expressions::P::Predicate)
31
+ return arg.value if arg.is_a?(Expressions::WithOptions)
32
+
33
+ return arg unless arg.is_a?(Steps)
34
+
35
+ { :@type => "g:Bytecode", :@value => Bytecode.new(arg, **@params.merge(no_return: false)).serialize }
36
+ end
37
+
38
+ def serialize_typed_value(value)
39
+ return value.value if value.type.nil?
40
+
41
+ {
42
+ "@type": "g:#{value.type}",
43
+ "@value": value.value
44
+ }
45
+ end
46
+
47
+ def serialize_predicate(value)
48
+ {
49
+ "@type": "g:#{value.namespace}",
50
+ "@value": {
51
+ predicate: value.name,
52
+ value: if value.type.nil?
53
+ value.value
54
+ else
55
+ {
56
+ "@type": "g:#{value.type}",
57
+ "@value": value.value
58
+ }
59
+ end
60
+ }
61
+ }
62
+ end
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Grumlin
4
+ module StepsSerializers
5
+ class HumanReadableBytecode < Serializer
6
+ def serialize
7
+ steps = ShortcutsApplyer.call(@steps)
8
+ [serialize_steps(steps.configuration_steps), serialize_steps(steps.steps)]
9
+ end
10
+
11
+ def serialize_steps(steps)
12
+ steps.map { |s| serialize_step(s) }
13
+ end
14
+
15
+ private
16
+
17
+ def serialize_step(step)
18
+ [step.name, *step.arguments.map { |arg| serialize_arg(arg) }]
19
+ end
20
+
21
+ def serialize_arg(arg)
22
+ return arg.to_s if arg.is_a?(TypedValue)
23
+ return serialize_predicate(arg) if arg.is_a?(Expressions::P::Predicate)
24
+ return arg.value if arg.is_a?(Expressions::WithOptions)
25
+
26
+ return arg unless arg.is_a?(Steps)
27
+
28
+ HumanReadableBytecode.new(arg, **@params.merge(no_return: false)).serialize[1]
29
+ end
30
+
31
+ def serialize_predicate(arg)
32
+ "#{arg.name}(#{arg.value})"
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Grumlin
4
+ module StepsSerializers
5
+ class Serializer
6
+ def initialize(steps, **params)
7
+ @steps = steps
8
+ @params = params
9
+ end
10
+
11
+ def serialize
12
+ raise NotImplementedError
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Grumlin
4
+ module StepsSerializers
5
+ class String < Serializer
6
+ # constructor params: apply_shortcuts: true|false, default: false
7
+ # constructor params: anonymous: true|false, default: false
8
+ # TODO: add pretty
9
+
10
+ def serialize
11
+ steps = @params[:apply_shortcuts] ? ShortcutsApplyer.call(@steps) : @steps
12
+
13
+ configuration_steps = serialize_steps(steps.configuration_steps)
14
+ regular_steps = serialize_steps(steps.steps)
15
+
16
+ "#{prefix}.#{(configuration_steps + regular_steps).join(".")}"
17
+ end
18
+
19
+ private
20
+
21
+ def prefix
22
+ @prefix ||= @params[:anonymous] ? "__" : "g"
23
+ end
24
+
25
+ def serialize_arg(arg)
26
+ return "\"#{arg}\"" if arg.is_a?(::String) || arg.is_a?(Symbol)
27
+ return "#{arg.type}.#{arg.value}" if arg.is_a?(Grumlin::TypedValue)
28
+ return arg.to_s if arg.is_a?(Grumlin::Expressions::WithOptions)
29
+
30
+ return arg unless arg.is_a?(Steps)
31
+
32
+ StepsSerializers::String.new(arg, anonymous: true, **@params).serialize
33
+ end
34
+
35
+ def serialize_steps(steps)
36
+ steps.map do |step|
37
+ "#{step.name}(#{step.arguments.map { |a| serialize_arg(a) }.join(", ")})"
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end
data/lib/grumlin/sugar.rb CHANGED
@@ -6,12 +6,12 @@ module Grumlin
6
6
  base.include(Grumlin::Expressions)
7
7
  end
8
8
 
9
- def __
10
- Grumlin::Expressions::U
9
+ def __(shortcuts = {})
10
+ Grumlin::TraversalStart.new(shortcuts) # TODO: allow only regular and start steps
11
11
  end
12
12
 
13
- def g
14
- Grumlin::Traversal.new
13
+ def g(shortcuts = {})
14
+ Grumlin::TraversalStart.new(shortcuts)
15
15
  end
16
16
  end
17
17
  end
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Grumlin
4
+ class TraversalStart
5
+ START_STEPS = Grumlin.definitions.dig(:steps, :start).map(&:to_sym).freeze
6
+ REGULAR_STEPS = Grumlin.definitions.dig(:steps, :regular).map(&:to_sym).freeze
7
+ CONFIGURATION_STEPS = Grumlin.definitions.dig(:steps, :configuration).map(&:to_sym).freeze
8
+
9
+ ALL_STEPS = START_STEPS + CONFIGURATION_STEPS + REGULAR_STEPS
10
+
11
+ ALL_STEPS.each do |step|
12
+ define_method step do |*args, **params|
13
+ step(step, *args, **params)
14
+ end
15
+ end
16
+
17
+ attr_reader :shortcuts
18
+
19
+ def initialize(shortcuts)
20
+ @shortcuts = shortcuts
21
+ end
22
+
23
+ def step(name, *args, **params)
24
+ Action.new(name, args: args, params: params, shortcuts: @shortcuts)
25
+ end
26
+
27
+ def method_missing(name, *args, **params)
28
+ return step(name, *args, **params) if @shortcuts.key?(name)
29
+
30
+ super
31
+ end
32
+
33
+ def __
34
+ TraversalStart.new(@shortcuts) # TODO: allow only regular and start steps
35
+ end
36
+
37
+ def to_s(*)
38
+ self.class.to_s
39
+ end
40
+
41
+ def inspect
42
+ self.class.inspect
43
+ end
44
+
45
+ private
46
+
47
+ def respond_to_missing?(name, _include_private = false)
48
+ @shortcuts.key?(name)
49
+ end
50
+ end
51
+ end
@@ -9,17 +9,6 @@ module Grumlin
9
9
  @value = value
10
10
  end
11
11
 
12
- def to_bytecode
13
- @to_bytecode ||= if type.nil?
14
- value
15
- else
16
- {
17
- "@type": "g:#{type}",
18
- "@value": value
19
- }
20
- end
21
- end
22
-
23
12
  def inspect
24
13
  "<#{type}.#{value}>"
25
14
  end
@@ -27,9 +16,5 @@ module Grumlin
27
16
  def to_s
28
17
  inspect
29
18
  end
30
-
31
- def to_readable_bytecode
32
- inspect
33
- end
34
19
  end
35
20
  end
@@ -16,6 +16,8 @@ module Grumlin
16
16
  "g:Double" => ->(value) { cast_double(value) },
17
17
  "g:Direction" => ->(value) { value },
18
18
  # "g:VertexProperty"=> ->(value) { value }, # TODO: implement me
19
+ "g:TraversalMetrics" => ->(value) { cast_map(value[:@value]) },
20
+ "g:Metrics" => ->(value) { cast_map(value[:@value]) },
19
21
  "g:T" => ->(value) { value.to_sym }
20
22
  }.freeze
21
23
 
@@ -67,7 +69,7 @@ module Grumlin
67
69
  def cast_map(value)
68
70
  Hash[*value].transform_keys do |key|
69
71
  next key.to_sym if key.respond_to?(:to_sym)
70
- next cast(key) if key[:@type]
72
+ next cast(key) if key[:@type] # TODO: g.V.group.by(:none_existing_property).next
71
73
 
72
74
  raise UnknownMapKey, key, value
73
75
  end.transform_values { |v| cast(v) }
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Grumlin
4
- VERSION = "0.15.6"
4
+ VERSION = "0.17.0"
5
5
  end
data/lib/grumlin.rb CHANGED
@@ -100,6 +100,10 @@ module Grumlin
100
100
  end
101
101
  end
102
102
 
103
+ class RepositoryError < Error; end
104
+
105
+ class WrongQueryResult < RepositoryError; end
106
+
103
107
  class Config
104
108
  attr_accessor :url, :pool_size, :client_concurrency, :client_factory
105
109
 
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: grumlin
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.15.6
4
+ version: 0.17.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Gleb Sinyavskiy
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2022-03-09 00:00:00.000000000 Z
11
+ date: 2022-03-30 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: async-pool
@@ -98,8 +98,7 @@ files:
98
98
  - lib/async/channel.rb
99
99
  - lib/definitions.yml
100
100
  - lib/grumlin.rb
101
- - lib/grumlin/anonymous_step.rb
102
- - lib/grumlin/bytecode.rb
101
+ - lib/grumlin/action.rb
103
102
  - lib/grumlin/client.rb
104
103
  - lib/grumlin/edge.rb
105
104
  - lib/grumlin/expressions/expression.rb
@@ -109,23 +108,28 @@ files:
109
108
  - lib/grumlin/expressions/pop.rb
110
109
  - lib/grumlin/expressions/scope.rb
111
110
  - lib/grumlin/expressions/t.rb
112
- - lib/grumlin/expressions/u.rb
111
+ - lib/grumlin/expressions/text_p.rb
113
112
  - lib/grumlin/expressions/with_options.rb
114
113
  - lib/grumlin/path.rb
115
114
  - lib/grumlin/property.rb
116
115
  - lib/grumlin/repository.rb
117
116
  - lib/grumlin/request_dispatcher.rb
118
117
  - lib/grumlin/shortcut.rb
119
- - lib/grumlin/shortcut_proxy.rb
120
118
  - lib/grumlin/shortcuts.rb
121
119
  - lib/grumlin/shortcuts/properties.rb
122
- - lib/grumlin/step.rb
120
+ - lib/grumlin/shortcuts_applyer.rb
121
+ - lib/grumlin/step_data.rb
122
+ - lib/grumlin/steps.rb
123
+ - lib/grumlin/steps_serializers/bytecode.rb
124
+ - lib/grumlin/steps_serializers/human_readable_bytecode.rb
125
+ - lib/grumlin/steps_serializers/serializer.rb
126
+ - lib/grumlin/steps_serializers/string.rb
123
127
  - lib/grumlin/sugar.rb
124
128
  - lib/grumlin/test/rspec.rb
125
129
  - lib/grumlin/test/rspec/db_cleaner_context.rb
126
130
  - lib/grumlin/test/rspec/gremlin_context.rb
127
131
  - lib/grumlin/transport.rb
128
- - lib/grumlin/traversal.rb
132
+ - lib/grumlin/traversal_start.rb
129
133
  - lib/grumlin/traverser.rb
130
134
  - lib/grumlin/typed_value.rb
131
135
  - lib/grumlin/typing.rb
@@ -1,45 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Grumlin
4
- class AnonymousStep
5
- attr_reader :name, :previous_step, :configuration_steps
6
-
7
- SUPPORTED_STEPS = Grumlin.definitions.dig(:steps, :regular).map(&:to_sym).freeze
8
-
9
- def initialize(name, *args, configuration_steps: [], previous_step: nil, **params)
10
- @name = name
11
- @previous_step = previous_step
12
- @args = args
13
- @params = params
14
- @configuration_steps = configuration_steps
15
- end
16
-
17
- SUPPORTED_STEPS.each do |step|
18
- define_method(step) do |*args, **params|
19
- step(step, *args, **params)
20
- end
21
- end
22
-
23
- def step(name, *args, **params)
24
- self.class.new(name, *args, previous_step: self, configuration_steps: configuration_steps, **params)
25
- end
26
-
27
- def inspect
28
- bytecode.inspect
29
- end
30
-
31
- def to_s
32
- inspect
33
- end
34
-
35
- def bytecode(no_return: false)
36
- @bytecode ||= Bytecode.new(self, no_return: no_return)
37
- end
38
-
39
- def args
40
- [*@args].tap do |args|
41
- args << @params if @params.any?
42
- end
43
- end
44
- end
45
- end
@@ -1,70 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Grumlin
4
- # Incapsulates logic of converting step chains and step arguments to queries that can be sent to the server
5
- # and to human readable strings.
6
- class Bytecode < TypedValue
7
- class NoneStep
8
- def to_bytecode
9
- ["none"]
10
- end
11
- end
12
-
13
- NONE_STEP = NoneStep.new
14
-
15
- def initialize(step, no_return: false)
16
- super(type: "Bytecode")
17
- @step = step
18
- @no_return = no_return
19
- end
20
-
21
- def inspect
22
- configuration_steps = @step.configuration_steps.map do |s|
23
- serialize_arg(s, serialization_method: :to_readable_bytecode)
24
- end
25
- "#{configuration_steps.any? ? configuration_steps : nil}#{to_readable_bytecode}"
26
- end
27
-
28
- def to_s
29
- inspect
30
- end
31
-
32
- def to_readable_bytecode
33
- @to_readable_bytecode ||= steps.map { |s| serialize_arg(s, serialization_method: :to_readable_bytecode) }
34
- end
35
-
36
- def value
37
- @value ||= { step: (steps + (@no_return ? [NONE_STEP] : [])).map { |s| serialize_arg(s) } }.tap do |v|
38
- v.merge!(source: @step.configuration_steps.map { |s| serialize_arg(s) }) if @step.configuration_steps.any?
39
- end
40
- end
41
-
42
- private
43
-
44
- # Serializes step or a step argument to either an executable query or a human readable string representation
45
- # depending on the `serialization_method` parameter. It should be either `:to_readable_bytecode` for human readable
46
- # representation or `:to_bytecode` for query.
47
- def serialize_arg(arg, serialization_method: :to_bytecode)
48
- return arg.public_send(serialization_method) if arg.respond_to?(serialization_method)
49
- return arg unless arg.is_a?(AnonymousStep)
50
-
51
- arg.args.each.with_object([arg.name.to_s]) do |a, res|
52
- res << if a.respond_to?(:bytecode)
53
- a.bytecode.public_send(serialization_method)
54
- else
55
- serialize_arg(a, serialization_method: serialization_method)
56
- end
57
- end
58
- end
59
-
60
- def steps
61
- @steps ||= [].tap do |result|
62
- step = @step
63
- until step.nil?
64
- result.unshift(step)
65
- step = step.previous_step
66
- end
67
- end
68
- end
69
- end
70
- end
@@ -1,16 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Grumlin
4
- module Expressions
5
- # The module is called U because Underscore and implements __
6
- module U
7
- class << self
8
- Grumlin::AnonymousStep::SUPPORTED_STEPS.each do |step|
9
- define_method step do |*args, **params|
10
- AnonymousStep.new(step, *args, **params)
11
- end
12
- end
13
- end
14
- end
15
- end
16
- end
@@ -1,53 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Grumlin
4
- class ShortcutProxy
5
- extend Forwardable
6
-
7
- attr_reader :object, :shortcuts
8
-
9
- # shortcuts: {"name": ->(arg) {}}
10
- def initialize(object, shortcuts, parent: nil)
11
- @object = object
12
- @shortcuts = shortcuts
13
- @parent = parent
14
- end
15
-
16
- def method_missing(name, *args, **params)
17
- return @parent.public_send(name, *args, **params) if %i[__ g].include?(name) && !@parent.nil?
18
-
19
- return wrap_result(@object.public_send(name, *args, **params)) if @object.respond_to?(name)
20
-
21
- return wrap_result(@shortcuts[name].apply(self, *args, **params)) if @shortcuts.key?(name)
22
-
23
- super
24
- end
25
-
26
- # For some reason the interpreter thinks it's private
27
- public def respond_to_missing?(name, include_private = false) # rubocop:disable Style/AccessModifierDeclarations
28
- name = name.to_sym
29
-
30
- (%i[__ g].include?(name) &&
31
- @parent.respond_to?(name)) ||
32
- @object.respond_to?(name) ||
33
- @shortcuts.key?(name) ||
34
- super
35
- end
36
-
37
- def_delegator :@object, :to_s
38
-
39
- def inspect
40
- @object.inspect
41
- end
42
-
43
- private
44
-
45
- def wrap_result(result)
46
- if result.is_a?(AnonymousStep) || result.is_a?(Traversal)
47
- return self.class.new(result, @shortcuts, parent: @parent)
48
- end
49
-
50
- result
51
- end
52
- end
53
- end
data/lib/grumlin/step.rb DELETED
@@ -1,43 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Grumlin
4
- class Step < AnonymousStep
5
- attr_reader :client
6
-
7
- def initialize(pool, name, *args, configuration_steps: [], previous_step: nil, **params)
8
- super(name, *args, previous_step: previous_step, configuration_steps: configuration_steps, **params)
9
- @pool = pool
10
- end
11
-
12
- def next
13
- to_enum.next
14
- end
15
-
16
- def hasNext # rubocop:disable Naming/MethodName
17
- to_enum.peek
18
- true
19
- rescue StopIteration
20
- false
21
- end
22
-
23
- def to_enum
24
- @to_enum ||= toList.to_enum
25
- end
26
-
27
- def toList
28
- @pool.acquire do |client|
29
- client.write(bytecode)
30
- end
31
- end
32
-
33
- def iterate
34
- @pool.acquire do |client|
35
- client.write(bytecode(no_return: true))
36
- end
37
- end
38
-
39
- def step(step_name, *args, **params)
40
- self.class.new(@pool, step_name, *args, previous_step: self, configuration_steps: @configuration_steps, **params)
41
- end
42
- end
43
- end
@@ -1,36 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Grumlin
4
- class Traversal
5
- SUPPORTED_STEPS = Grumlin.definitions.dig(:steps, :start).map(&:to_sym).freeze
6
-
7
- CONFIGURATION_STEPS = Grumlin.definitions.dig(:steps, :configuration).map(&:to_sym).freeze
8
-
9
- attr_reader :configuration_steps
10
-
11
- def initialize(pool = Grumlin.default_pool, configuration_steps: [])
12
- @pool = pool
13
- @configuration_steps = configuration_steps
14
- end
15
-
16
- def inspect
17
- "#<#{self.class}>"
18
- end
19
-
20
- def to_s
21
- inspect
22
- end
23
-
24
- CONFIGURATION_STEPS.each do |step|
25
- define_method step do |*args, **params|
26
- self.class.new(@pool, configuration_steps: @configuration_steps + [AnonymousStep.new(step, *args, **params)])
27
- end
28
- end
29
-
30
- SUPPORTED_STEPS.each do |step|
31
- define_method step do |*args, **params|
32
- Step.new(@pool, step, *args, configuration_steps: @configuration_steps, **params)
33
- end
34
- end
35
- end
36
- end