grumlin 0.16.1 → 0.18.1

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: ce713ebd08c676dfe38124e0286963ea18ee8d5d51f678096fd5f2ae8c28c51c
4
- data.tar.gz: 6cd25c9ac701a2276fdb470b55b4ebaf123323521c6cd8d4d87365b68d604426
3
+ metadata.gz: 573e2c9cda13465c0560d36e9e10d8bcdda5e0ea01ea4bbf89cb8d6cb943f6de
4
+ data.tar.gz: 8dd6fa7264875f372c6117352f2e87c7984457ff3f94da57f8f19d91344c4dd2
5
5
  SHA512:
6
- metadata.gz: 81b5b522b5d183240d27cb36285f448e4918386cd267ea84fcffb2cc763d156d272844a4ffab2e00999d5c5be129930d905220506e07702ff150c49ac7388d1b
7
- data.tar.gz: f4d56b0a66d91f8f454964a8d42de46fa0b6b85595c48e67e09875e01bfdb96eb131e0c6cb5ec395ebeb7dda59c45ff67c445f88e75a12a03436689d9ba14279
6
+ metadata.gz: 2eea2219d92cd29ef311993dcd0bcd0e9d8906d39474d93733c8406952e93babf85d4468b6acd15ac1d114c940c49533603960fa496c8f757223d016b5619a03
7
+ data.tar.gz: 39c8821a89cb1194d973c7b2dee5e96ad90288e2a611ef42a080586987759949453a2c7bade5be56d500588a8655a485c6c415c075c1295534bbc8a419bf2d43
data/.rubocop.yml CHANGED
@@ -44,6 +44,7 @@ Naming/MethodParameterName:
44
44
  - outV
45
45
  - inVLabel
46
46
  - outVLabel
47
+ - to
47
48
 
48
49
  RSpec/NamedSubject:
49
50
  Enabled: false
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- grumlin (0.16.1)
4
+ grumlin (0.18.1)
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
@@ -77,8 +77,7 @@ end
77
77
 
78
78
  **Shortcuts** is a way to share and organize gremlin code. They let developers define their own steps consisting of
79
79
  sequences of standard gremlin steps, other shortcuts and even add new initially unsupported by Grumlin steps.
80
- Remember ActiveRecord scopes? Shortcuts are very similar. `Grumlin::Shortcuts#with_shortcuts` wraps a given object into
81
- a proxy object that simply proxies all methods existing in the wrapped object to it and handles shortcuts.
80
+ Remember ActiveRecord scopes? Shortcuts are very similar.
82
81
 
83
82
  **Important**: if a shortcut's name matches a name of a method defined on the wrapped object, this shortcut will be
84
83
  be ignored because methods have higher priority. You cannot override supported by Grumlin steps with shortcuts,
@@ -128,17 +127,18 @@ class MyRepository
128
127
 
129
128
  # Wrapping a traversal
130
129
  def red_triangles
131
- with_shortcuts(g).V.hasLabel(:triangle)
132
- .hasColor("red")
133
- .toList
130
+ g(self.class.shortcuts).V.hasLabel(:triangle)
131
+ .hasColor("red")
132
+ .toList
134
133
  end
135
134
 
136
135
  # Wrapping _
137
136
  def something_else
138
- with_shortcuts(g).V.hasColor("red")
139
- .repeat(with_shortcuts(__)
140
- .out(:has)
141
- .hasColor("blue")).toList
137
+ g(self.class.shortcuts).V.hasColor("red")
138
+ .repeat(__(self.class.shortcuts))
139
+ .out(:has)
140
+ .hasColor("blue")
141
+ .toList
142
142
  end
143
143
  end
144
144
  ```
@@ -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,53 @@ 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
+ `Grumlin::Repository` also provides a set of generic CRUD operations:
187
+ - `add_vertex(label, id = nil, **properties)`
188
+ - `add_edge(label, id = nil, from:, to:, **properties)`
189
+ - `drop_vertex(id)`
190
+ - `drop_edge(id = nil, from: nil, to: nil, label: nil)`
191
+ - `upsert_vertex(label, id, create_properties: {}, update_properties: {})`
192
+ - `upsert_edge(label, from:, to:, create_properties: {}, update_properties: {})`
193
+
194
+ **Usage**
195
+
196
+ To execute the query defined in a query block one simply needs to call a method with the same name:
197
+
198
+ `MyRepository.new.triangles_with_color(:red)`
199
+
200
+ One can also override the `return_mode`:
201
+
202
+ `MyRepository.new.triangles_with_color(:red, query_params: { return_mode: :single })`
203
+
204
+ or even pass a block to the method and a raw traversal will be yielded:
205
+ ```ruby
206
+ MyRepository.new.triangles_with_color(:red) do |t|
207
+ t.has(:other_property, :some_value).toList
208
+ end
209
+ ```
210
+ it may be useful for debugging. Note that one needs to call a termination step manually in this case.
211
+
212
+ `query` also provides a helper for profiling requests:
213
+ `MyRepository.new.triangles_with_color(:red, query_params: { profile: true })`
214
+
215
+ method will return profiling data of the results.
216
+
179
217
  #### IRB
180
218
 
181
219
  An example of how to start an IRB session with support for executing gremlin queries:
@@ -49,12 +49,6 @@ module Grumlin
49
49
  @shortcuts.key?(@name)
50
50
  end
51
51
 
52
- def arguments
53
- @arguments ||= [*@args].tap do |args|
54
- args << @params if @params.any?
55
- end
56
- end
57
-
58
52
  def method_missing(name, *args, **params)
59
53
  return step(name, *args, **params) if @shortcuts.key?(name)
60
54
 
@@ -2,7 +2,16 @@
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
13
+ include Grumlin::Expressions
14
+
6
15
  def __
7
16
  TraversalStart.new(self.class.shortcuts)
8
17
  end
@@ -10,14 +19,130 @@ module Grumlin
10
19
  def g
11
20
  TraversalStart.new(self.class.shortcuts)
12
21
  end
22
+
23
+ def drop_vertex(id)
24
+ g.V(id).drop.iterate
25
+ end
26
+
27
+ def drop_edge(id = nil, from: nil, to: nil, label: nil) # rubocop:disable Metrics/AbcSize
28
+ raise ArgumentError, "either id or from:, to: and label: must be passed" if [id, from, to, label].all?(&:nil?)
29
+ return g.E(id).drop.iterate unless id.nil?
30
+
31
+ raise ArgumentError, "from:, to: and label: must be passed" if [from, to, label].any?(&:nil?)
32
+
33
+ g.V(from).outE(label).where(__.inV.hasId(to)).limit(1).drop.iterate
34
+ end
35
+
36
+ def add_vertex(label, id = nil, **properties)
37
+ id ||= properties[T.id]
38
+ properties = except(properties, T.id)
39
+
40
+ t = g.addV(label)
41
+ t = t.props(T.id => id) unless id.nil?
42
+ t.props(**properties).next
43
+ end
44
+
45
+ def add_edge(label, id = nil, from:, to:, **properties)
46
+ id ||= properties[T.id]
47
+ properties = except(properties, T.label)
48
+ properties[T.id] = id
49
+
50
+ g.addE(label).from(__.V(from)).to(__.V(to)).props(**properties).next
51
+ end
52
+
53
+ def upsert_vertex(label, id, create_properties: {}, update_properties: {}) # rubocop:disable Metrics/AbcSize
54
+ create_properties = except(create_properties, T.id, T.label)
55
+ update_properties = except(update_properties, T.id, T.label)
56
+ g.V(id)
57
+ .fold
58
+ .coalesce(
59
+ __.unfold,
60
+ __.addV(label).props(**create_properties.merge(T.id => id))
61
+ ).props(**update_properties)
62
+ .next
63
+ end
64
+
65
+ # Only from and to are used to find the existing edge, if one wants to assign an id to a created edge,
66
+ # it must be passed as T.id in via create_properties.
67
+ def upsert_edge(label, from:, to:, create_properties: {}, update_properties: {}) # rubocop:disable Metrics/AbcSize
68
+ create_properties = except(create_properties, T.label)
69
+ update_properties = except(update_properties, T.id, T.label)
70
+
71
+ g.V(from)
72
+ .outE(label).where(__.inV.hasId(to))
73
+ .fold
74
+ .coalesce(
75
+ __.unfold,
76
+ __.addE(label).from(__.V(from)).to(__.V(to)).props(**create_properties)
77
+ ).props(**update_properties).next
78
+ end
79
+
80
+ private
81
+
82
+ # A polyfill for Hash#except for ruby 2.x environments without ActiveSupport
83
+ # TODO: delete and use native Hash#except when after ruby 2.7 is deprecated.
84
+ def except(hash, *keys)
85
+ return hash.except(*keys) if hash.respond_to?(:except)
86
+
87
+ hash.each_with_object({}) do |(k, v), res|
88
+ res[k] = v unless keys.include?(k)
89
+ end
90
+ end
13
91
  end
14
92
 
15
93
  def self.extended(base)
16
94
  base.extend(Grumlin::Shortcuts)
17
- base.include(Grumlin::Expressions)
18
95
  base.include(InstanceMethods)
19
96
 
20
97
  base.shortcuts_from(Grumlin::Shortcuts::Properties)
21
98
  end
99
+
100
+ def query(name, return_mode: :list, postprocess_with: nil, &query_block) # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
101
+ return_mode = validate_return_mode!(return_mode)
102
+ postprocess_with = validate_postprocess_with!(postprocess_with)
103
+
104
+ define_method name do |*args, query_params: {}, **params, &block|
105
+ t = instance_exec(*args, **params, &query_block)
106
+ return t if self.class.empty_result?(t)
107
+
108
+ unless t.is_a?(Grumlin::Action)
109
+ raise WrongQueryResult,
110
+ "queries must return #{Grumlin::Action}, nil or an empty collection. Given: #{t.class}"
111
+ end
112
+
113
+ return block.call(t) unless block.nil?
114
+
115
+ return t.profile.next if query_params[:profile] == true
116
+
117
+ return_mode = self.class.validate_return_mode!(query_params[:return_mode] || return_mode)
118
+
119
+ return t if return_mode == :traversal
120
+
121
+ t.public_send(RETURN_MODES[return_mode]).tap do |result|
122
+ return postprocess_with.call(result) if postprocess_with.respond_to?(:call)
123
+ return send(postprocess_with, result) unless postprocess_with.nil?
124
+ end
125
+ end
126
+ end
127
+
128
+ def validate_return_mode!(return_mode)
129
+ return return_mode if RETURN_MODES.key?(return_mode)
130
+
131
+ raise ArgumentError, "unsupported return mode #{return_mode}. Supported modes: #{RETURN_MODES.keys}"
132
+ end
133
+
134
+ def validate_postprocess_with!(postprocess_with)
135
+ if postprocess_with.nil? || postprocess_with.is_a?(Symbol) ||
136
+ postprocess_with.is_a?(String) || postprocess_with.respond_to?(:call)
137
+ return postprocess_with
138
+ end
139
+
140
+ raise ArgumentError,
141
+ "postprocess_with must be a String, Symbol or a callable object, given: #{postprocess_with.class}"
142
+ end
143
+
144
+ def empty_result?(result)
145
+ result.nil? || (result.respond_to?(:empty?) && result.empty?)
146
+ end
22
147
  end
23
148
  end
@@ -5,17 +5,13 @@ module Grumlin
5
5
  module Properties
6
6
  extend Grumlin::Shortcuts
7
7
 
8
- shortcut :props do |props|
9
- next if props.nil? # TODO: fixme, add proper support for **params
10
-
11
- props.reduce(self) do |tt, (prop, value)|
8
+ shortcut :props do |**props|
9
+ props.compact.reduce(self) do |tt, (prop, value)|
12
10
  tt.property(prop, value)
13
11
  end
14
12
  end
15
13
 
16
- shortcut :hasAll do |props|
17
- next if props.nil? # TODO: fixme, add proper support for **params
18
-
14
+ shortcut :hasAll do |**props|
19
15
  props.reduce(self) do |tt, (prop, value)|
20
16
  tt.has(prop, value)
21
17
  end
@@ -18,7 +18,7 @@ module Grumlin
18
18
 
19
19
  Steps.new(shortcuts).tap do |processed_steps|
20
20
  (configuration_steps + regular_steps).each do |step|
21
- processed_steps.add(step.name, step.arguments)
21
+ processed_steps.add(step.name, args: step.args, params: step.params)
22
22
  end
23
23
  end
24
24
  end
@@ -27,20 +27,20 @@ module Grumlin
27
27
 
28
28
  def process_steps(steps, shortcuts) # rubocop:disable Metrics/AbcSize
29
29
  steps.each_with_object([]) do |step, result|
30
- arguments = step.arguments.map do |arg|
30
+ args = step.args.map do |arg|
31
31
  arg.is_a?(Steps) ? ShortcutsApplyer.call(arg) : arg
32
32
  end
33
33
 
34
34
  if shortcuts.include?(step.name)
35
35
  t = TraversalStart.new(shortcuts)
36
- action = shortcuts[step.name].apply(t, *arguments)
36
+ action = shortcuts[step.name].apply(t, *args, **step.params)
37
37
  next if action.nil? || action == t # Shortcut did not add any steps
38
38
 
39
39
  new_steps = ShortcutsApplyer.call(Steps.from(action))
40
40
  result.concat(new_steps.configuration_steps)
41
41
  result.concat(new_steps.steps)
42
42
  else
43
- result << StepData.new(step.name, arguments)
43
+ result << StepData.new(step.name, args: args, params: step.params)
44
44
  end
45
45
  end
46
46
  end
@@ -2,17 +2,19 @@
2
2
 
3
3
  module Grumlin
4
4
  class StepData
5
- attr_reader :name, :arguments
5
+ attr_reader :name, :args, :params
6
6
 
7
- def initialize(name, arguments)
7
+ def initialize(name, args: [], params: {})
8
8
  @name = name
9
- @arguments = arguments
9
+ @args = args
10
+ @params = params
10
11
  end
11
12
 
12
13
  def ==(other)
13
14
  self.class == other.class &&
14
15
  @name == other.name &&
15
- @arguments == other.arguments
16
+ @args == other.args &&
17
+ @params == other.params
16
18
  end
17
19
  end
18
20
  end
data/lib/grumlin/steps.rb CHANGED
@@ -18,7 +18,7 @@ module Grumlin
18
18
 
19
19
  new(shortcuts).tap do |chain|
20
20
  actions.each do |act|
21
- chain.add(act.name, act.arguments)
21
+ chain.add(act.name, args: act.args, params: act.params)
22
22
  end
23
23
  end
24
24
  end
@@ -31,10 +31,10 @@ module Grumlin
31
31
  @steps = steps
32
32
  end
33
33
 
34
- def add(name, arguments)
35
- return add_configuration_step(name, arguments) if CONFIGURATION_STEPS.include?(name)
34
+ def add(name, args: [], params: {})
35
+ return add_configuration_step(name, args: args, params: params) if CONFIGURATION_STEPS.include?(name)
36
36
 
37
- StepData.new(name, cast_arguments(arguments)).tap do |step|
37
+ StepData.new(name, args: cast_arguments(args), params: params).tap do |step|
38
38
  @steps << step
39
39
  end
40
40
  end
@@ -56,16 +56,16 @@ module Grumlin
56
56
 
57
57
  def shortcuts?(steps_ary)
58
58
  steps_ary.any? do |step|
59
- @shortcuts.include?(step.name) || step.arguments.any? do |arg|
59
+ @shortcuts.include?(step.name) || step.args.any? do |arg|
60
60
  arg.is_a?(Steps) ? arg.uses_shortcuts? : false
61
61
  end
62
62
  end
63
63
  end
64
64
 
65
- def add_configuration_step(name, arguments)
65
+ def add_configuration_step(name, args: [], params: {})
66
66
  raise ArgumentError, "cannot use configuration steps after start step was used" unless @steps.empty?
67
67
 
68
- StepData.new(name, cast_arguments(arguments)).tap do |step|
68
+ StepData.new(name, args: cast_arguments(args), params: params).tap do |step|
69
69
  @configuration_steps << step
70
70
  end
71
71
  end
@@ -6,7 +6,7 @@ module Grumlin
6
6
  # constructor params: no_return: true|false, default false
7
7
  # TODO: add pretty
8
8
 
9
- NONE_STEP = StepData.new("none", [])
9
+ NONE_STEP = StepData.new("none")
10
10
 
11
11
  def serialize
12
12
  steps = ShortcutsApplyer.call(@steps)
@@ -22,7 +22,7 @@ module Grumlin
22
22
  private
23
23
 
24
24
  def serialize_step(step)
25
- [step.name, *step.arguments.map { |arg| serialize_arg(arg) }]
25
+ [step.name, *step.args.map { |arg| serialize_arg(arg) }, step.params.any? ? step.params : nil].compact
26
26
  end
27
27
 
28
28
  def serialize_arg(arg)
@@ -2,22 +2,16 @@
2
2
 
3
3
  module Grumlin
4
4
  module StepsSerializers
5
- class HumanReadableBytecode < Serializer
5
+ class HumanReadableBytecode < Bytecode
6
6
  def serialize
7
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) }
8
+ [steps.configuration_steps, steps.steps].map do |stps|
9
+ stps.map { |s| serialize_step(s) }
10
+ end
13
11
  end
14
12
 
15
13
  private
16
14
 
17
- def serialize_step(step)
18
- [step.name, *step.arguments.map { |arg| serialize_arg(arg) }]
19
- end
20
-
21
15
  def serialize_arg(arg)
22
16
  return arg.to_s if arg.is_a?(TypedValue)
23
17
  return serialize_predicate(arg) if arg.is_a?(Expressions::P::Predicate)
@@ -10,14 +10,21 @@ module Grumlin
10
10
  def serialize
11
11
  steps = @params[:apply_shortcuts] ? ShortcutsApplyer.call(@steps) : @steps
12
12
 
13
- configuration_steps = serialize_steps(steps.configuration_steps)
14
- regular_steps = serialize_steps(steps.steps)
13
+ steps = [steps.configuration_steps, steps.steps].map do |stps|
14
+ stps.map { |step| serialize_step(step) }
15
+ end
15
16
 
16
- "#{prefix}.#{(configuration_steps + regular_steps).join(".")}"
17
+ "#{prefix}.#{(steps[0] + steps[1]).join(".")}"
17
18
  end
18
19
 
19
20
  private
20
21
 
22
+ def serialize_step(step)
23
+ "#{step.name}(#{(step.args + [step.params.any? ? step.params : nil].compact).map do |a|
24
+ serialize_arg(a)
25
+ end.join(", ")})"
26
+ end
27
+
21
28
  def prefix
22
29
  @prefix ||= @params[:anonymous] ? "__" : "g"
23
30
  end
@@ -31,12 +38,6 @@ module Grumlin
31
38
 
32
39
  StepsSerializers::String.new(arg, anonymous: true, **@params).serialize
33
40
  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
41
  end
41
42
  end
42
43
  end
@@ -18,7 +18,7 @@ module Grumlin
18
18
  # "g:VertexProperty"=> ->(value) { value }, # TODO: implement me
19
19
  "g:TraversalMetrics" => ->(value) { cast_map(value[:@value]) },
20
20
  "g:Metrics" => ->(value) { cast_map(value[:@value]) },
21
- "g:T" => ->(value) { value.to_sym }
21
+ "g:T" => ->(value) { Grumlin::Expressions::T.public_send(value) }
22
22
  }.freeze
23
23
 
24
24
  CASTABLE_TYPES = [Hash, String, Integer, TrueClass, FalseClass].freeze
@@ -69,7 +69,7 @@ module Grumlin
69
69
  def cast_map(value)
70
70
  Hash[*value].transform_keys do |key|
71
71
  next key.to_sym if key.respond_to?(:to_sym)
72
- next cast(key) if key[:@type]
72
+ next cast(key) if key[:@type] # TODO: g.V.group.by(:none_existing_property).next
73
73
 
74
74
  raise UnknownMapKey, key, value
75
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.16.1"
4
+ VERSION = "0.18.1"
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.16.1
4
+ version: 0.18.1
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-15 00:00:00.000000000 Z
11
+ date: 2022-04-14 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: async-pool
@@ -155,7 +155,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
155
155
  - !ruby/object:Gem::Version
156
156
  version: '0'
157
157
  requirements: []
158
- rubygems_version: 3.2.32
158
+ rubygems_version: 3.2.33
159
159
  signing_key:
160
160
  specification_version: 4
161
161
  summary: Gremlin graph traversal language DSL and client for Ruby.