grumlin 0.1.3 → 0.2.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d8db95b253561a642cf2dde6a4ac063e1b45af6c15604c19d5721a71eba882b2
4
- data.tar.gz: 035b95544c95e7609ddd2ac5d16b2096a67ea4c4e58b18d508176139efc1d5bf
3
+ metadata.gz: c0290e4d77bd50f701a803390ff2a0489b039f4beb231e8883ced690180c7abc
4
+ data.tar.gz: b4948d687c09ffea5bf9da26eda8644fc3fcffa1954e4e61fe2d628192984e7e
5
5
  SHA512:
6
- metadata.gz: de9132526ee1da7f7319662ebd646a22d72b1a949730380c0449e185a2bfbb8bdc4c7f79fb6d045068c745093a310b8e828922306e2f72c677ed52e3aa07dd07
7
- data.tar.gz: edf8f9bd0ed7ba23d7f5acac79ff7f4baa7466e11f4d1f08dde4ec2e0a052e7e08b4bb40e8621d51ce00cc26dda22ca0ed6e082474128d5310aa465c9787d197
6
+ metadata.gz: 3846fe491a606989f51f0f609e8c7ae2049b8a1005272ea531f085402e7d998a7f8723ccdafae01ce7d913f9ad9272f9da1e86a307cb7aeb5066ef4ad4024e4b
7
+ data.tar.gz: 1b20d251d16c95cb12657c6becbbb4592b8e206e9e83a7defee42281bbdd7c3f84d04785b2cc89bcc1328d05d11aa14b865b6bc6832899d3a73ce7b86283be5b
data/.overcommit.yml ADDED
@@ -0,0 +1,8 @@
1
+ PreCommit:
2
+ RuboCop:
3
+ enabled: true
4
+ on_warn: fail # Treat all warnings as failures
5
+
6
+ PrePush:
7
+ RSpec:
8
+ enabled: true
data/.rubocop.yml CHANGED
@@ -33,3 +33,6 @@ RSpec/NestedGroups:
33
33
 
34
34
  RSpec/ExampleLength:
35
35
  Enabled: false
36
+
37
+ Style/MultilineBlockChain:
38
+ Enabled: false
data/Gemfile CHANGED
@@ -4,6 +4,7 @@ source "https://rubygems.org"
4
4
 
5
5
  gemspec
6
6
 
7
+ gem "nokogiri"
7
8
  gem "rubocop"
8
9
  gem "rubocop-performance"
9
10
  gem "rubocop-rspec"
@@ -12,5 +13,6 @@ gem "solargraph"
12
13
 
13
14
  gem "async-rspec"
14
15
  gem "factory_bot"
16
+ gem "overcommit"
15
17
  gem "rspec"
16
18
  gem "simplecov"
data/Gemfile.lock CHANGED
@@ -1,8 +1,8 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- grumlin (0.1.3)
5
- async-websocket (~> 0.18)
4
+ grumlin (0.2.0)
5
+ async-websocket (~> 0.19)
6
6
 
7
7
  GEM
8
8
  remote: https://rubygems.org/
@@ -14,33 +14,34 @@ GEM
14
14
  tzinfo (~> 2.0)
15
15
  zeitwerk (~> 2.3)
16
16
  ast (2.4.2)
17
- async (1.29.0)
17
+ async (1.30.0)
18
18
  console (~> 1.10)
19
19
  nio4r (~> 2.3)
20
20
  timers (~> 4.1)
21
- async-http (0.56.2)
22
- async (~> 1.25)
23
- async-io (~> 1.28)
24
- async-pool (~> 0.2)
21
+ async-http (0.56.5)
22
+ async (>= 1.25)
23
+ async-io (>= 1.28)
24
+ async-pool (>= 0.2)
25
25
  protocol-http (~> 0.22.0)
26
26
  protocol-http1 (~> 0.14.0)
27
27
  protocol-http2 (~> 0.14.0)
28
- async-io (1.31.0)
29
- async (~> 1.14)
30
- async-pool (0.3.6)
31
- async (~> 1.25)
28
+ async-io (1.32.2)
29
+ async
30
+ async-pool (0.3.8)
31
+ async (>= 1.25)
32
32
  async-rspec (1.16.0)
33
33
  rspec (~> 3.0)
34
34
  rspec-files (~> 1.0)
35
35
  rspec-memory (~> 1.0)
36
- async-websocket (0.18.0)
36
+ async-websocket (0.19.0)
37
37
  async-http (~> 0.54)
38
38
  async-io (~> 1.23)
39
39
  protocol-websocket (~> 0.7.0)
40
- backport (1.1.2)
40
+ backport (1.2.0)
41
41
  benchmark (0.1.1)
42
+ childprocess (4.0.0)
42
43
  concurrent-ruby (1.1.8)
43
- console (1.12.0)
44
+ console (1.13.1)
44
45
  fiber-local
45
46
  diff-lcs (1.4.4)
46
47
  docile (1.4.0)
@@ -50,6 +51,7 @@ GEM
50
51
  fiber-local (1.0.0)
51
52
  i18n (1.8.10)
52
53
  concurrent-ruby (~> 1.0)
54
+ iniparse (1.5.0)
53
55
  jaro_winkler (1.5.4)
54
56
  kramdown (2.3.1)
55
57
  rexml
@@ -57,13 +59,16 @@ GEM
57
59
  kramdown (~> 2.0)
58
60
  minitest (5.14.4)
59
61
  nio4r (2.5.7)
60
- nokogiri (1.11.5-x86_64-linux)
62
+ nokogiri (1.11.7-x86_64-linux)
61
63
  racc (~> 1.4)
64
+ overcommit (0.57.0)
65
+ childprocess (>= 0.6.3, < 5)
66
+ iniparse (~> 1.4)
62
67
  parallel (1.20.1)
63
68
  parser (3.0.1.1)
64
69
  ast (~> 2.4.1)
65
70
  protocol-hpack (1.4.2)
66
- protocol-http (0.22.0)
71
+ protocol-http (0.22.5)
67
72
  protocol-http1 (0.14.1)
68
73
  protocol-http (~> 0.22)
69
74
  protocol-http2 (0.14.2)
@@ -95,16 +100,16 @@ GEM
95
100
  diff-lcs (>= 1.2.0, < 2.0)
96
101
  rspec-support (~> 3.10.0)
97
102
  rspec-support (3.10.2)
98
- rubocop (1.15.0)
103
+ rubocop (1.16.1)
99
104
  parallel (~> 1.10)
100
105
  parser (>= 3.0.0.0)
101
106
  rainbow (>= 2.2.2, < 4.0)
102
107
  regexp_parser (>= 1.8, < 3.0)
103
108
  rexml
104
- rubocop-ast (>= 1.5.0, < 2.0)
109
+ rubocop-ast (>= 1.7.0, < 2.0)
105
110
  ruby-progressbar (~> 1.7)
106
111
  unicode-display_width (>= 1.4.0, < 3.0)
107
- rubocop-ast (1.5.0)
112
+ rubocop-ast (1.7.0)
108
113
  parser (>= 3.0.1.1)
109
114
  rubocop-performance (1.11.3)
110
115
  rubocop (>= 1.7.0, < 2.0)
@@ -119,10 +124,11 @@ GEM
119
124
  simplecov_json_formatter (~> 0.1)
120
125
  simplecov-html (0.12.3)
121
126
  simplecov_json_formatter (0.1.3)
122
- solargraph (0.40.4)
123
- backport (~> 1.1)
127
+ solargraph (0.42.4)
128
+ backport (~> 1.2)
124
129
  benchmark
125
130
  bundler (>= 1.17.2)
131
+ diff-lcs (~> 1.4)
126
132
  e2mmap
127
133
  jaro_winkler (~> 1.5)
128
134
  kramdown (~> 2.3)
@@ -149,6 +155,8 @@ DEPENDENCIES
149
155
  async-rspec
150
156
  factory_bot
151
157
  grumlin!
158
+ nokogiri
159
+ overcommit
152
160
  rspec
153
161
  rubocop
154
162
  rubocop-performance
data/bin/console CHANGED
@@ -5,9 +5,12 @@ require "bundler/setup"
5
5
  require "grumlin"
6
6
  require "irb"
7
7
 
8
+ Grumlin.configure do |config|
9
+ config.url = ENV["GREMLIN_URL"] || "ws://localhost:8182/gremlin"
10
+ end
11
+
8
12
  Async do
9
- client = Grumlin::Client.new("ws://localhost:8182/gremlin", mode: :bytecode)
10
- g = Grumlin::Traversal.new(client)
13
+ g = Grumlin::Traversal.new
11
14
 
12
15
  IRB.setup(nil)
13
16
  workspace = IRB::WorkSpace.new(binding)
@@ -16,5 +19,5 @@ Async do
16
19
  rescue StandardError
17
20
  raise
18
21
  ensure
19
- client.disconnect
22
+ Grumlin.config.default_client&.disconnect
20
23
  end
data/bin/setup CHANGED
@@ -4,5 +4,6 @@ IFS=$'\n\t'
4
4
  set -vx
5
5
 
6
6
  bundle install
7
+ bundle exec overcommit --sign
7
8
 
8
9
  # Do any other automated setup that you need to do here
data/grumlin.gemspec CHANGED
@@ -23,5 +23,5 @@ Gem::Specification.new do |spec|
23
23
  end
24
24
  spec.require_paths = ["lib"]
25
25
 
26
- spec.add_dependency "async-websocket", "~> 0.18"
26
+ spec.add_dependency "async-websocket", "~> 0.19"
27
27
  end
data/lib/grumlin.rb CHANGED
@@ -5,20 +5,53 @@ require "json"
5
5
 
6
6
  require "async"
7
7
  require "async/queue"
8
+ require "async/barrier"
8
9
  require "async/http/endpoint"
9
10
  require "async/websocket/client"
10
11
 
11
12
  require_relative "grumlin/version"
12
13
  require_relative "grumlin/exceptions"
13
14
 
15
+ require_relative "grumlin/transport/async"
16
+
14
17
  require_relative "grumlin/vertex"
15
18
  require_relative "grumlin/edge"
19
+ require_relative "grumlin/path"
16
20
  require_relative "grumlin/typing"
17
21
  require_relative "grumlin/client"
18
22
  require_relative "grumlin/traversal"
23
+
24
+ require_relative "grumlin/anonymous_step"
19
25
  require_relative "grumlin/step"
26
+
20
27
  require_relative "grumlin/translator"
21
- require_relative "grumlin/traversing_context"
28
+ require_relative "grumlin/t"
29
+ require_relative "grumlin/order"
30
+ require_relative "grumlin/u"
31
+ require_relative "grumlin/p"
32
+ require_relative "grumlin/pop"
33
+ require_relative "grumlin/sugar"
22
34
 
23
35
  module Grumlin
36
+ class Config
37
+ attr_accessor :url
38
+
39
+ def default_client
40
+ @default_client ||= Grumlin::Client.new(url)
41
+ end
42
+
43
+ def reset!
44
+ @default_client = nil
45
+ end
46
+ end
47
+
48
+ class << self
49
+ def configure
50
+ yield config
51
+ end
52
+
53
+ def config
54
+ @config ||= Config.new
55
+ end
56
+ end
24
57
  end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Grumlin
4
+ class AnonymousStep
5
+ attr_reader :name, :args
6
+
7
+ def initialize(name, *args, previous_steps: [])
8
+ @name = name
9
+ @previous_steps = previous_steps
10
+ @args = args
11
+ end
12
+
13
+ %w[addV addE V E limit count drop property valueMap select from to as order by has hasLabel values hasNot
14
+ not outE groupCount label group in out fold unfold inV path dedup project coalesce repeat emit
15
+ elementMap where].each do |step|
16
+ define_method step do |*args|
17
+ add_step(step, args, previous_steps: steps)
18
+ end
19
+ end
20
+
21
+ alias addVertex addV
22
+ alias addEdge addE
23
+
24
+ def inspect
25
+ @inspect ||= to_bytecode.to_s
26
+ end
27
+
28
+ alias to_s inspect
29
+
30
+ def to_bytecode
31
+ @to_bytecode ||= (@previous_steps.last&.to_bytecode || []) + [Translator.to_bytecode(self)]
32
+ end
33
+
34
+ def steps
35
+ (@previous_steps + [self])
36
+ end
37
+
38
+ private
39
+
40
+ def add_step(step_name, args, previous_steps:)
41
+ self.class.new(step_name, *args, previous_steps: previous_steps)
42
+ end
43
+ end
44
+ end
@@ -1,10 +1,14 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Grumlin
4
- class Client # rubocop:disable Metrics/ClassLength
5
- SUCCESS_STATUS = 200
6
- NO_CONTENT_STATUS = 204
7
- PARTIAL_CONTENT_STATUS = 206
4
+ class Client
5
+ extend Forwardable
6
+
7
+ SUCCESS = {
8
+ 200 => :success,
9
+ 204 => :no_content,
10
+ 206 => :partial_content
11
+ }.freeze
8
12
 
9
13
  ERRORS = {
10
14
  499 => InvalidRequestArgumentsError,
@@ -18,126 +22,80 @@ module Grumlin
18
22
  498 => ClientSideError
19
23
  }.freeze
20
24
 
21
- def initialize(url, task: Async::Task.current, autoconnect: true, mode: :bytecode)
22
- @task = task
23
- @endpoint = Async::HTTP::Endpoint.parse(url)
24
- @mode = mode
25
-
26
- @requests = {}
27
- @query_queue = Async::Queue.new
28
-
25
+ def initialize(url, autoconnect: true)
26
+ @url = url
27
+ @transport = Transport::Async.new(url)
29
28
  connect if autoconnect
30
29
  end
31
30
 
32
- def connect # rubocop:disable Metrics/MethodLength
33
- raise AlreadyConnectedError unless @connection_task.nil?
31
+ def_delegators :@transport, :connect, :disconnect, :requests
34
32
 
35
- @connection_task = @task.async do |subtask|
36
- Async::WebSocket::Client.connect(@endpoint) do |connection|
37
- subtask.async { query_task(connection) }
38
- response_task(connection)
39
- end
40
- rescue StandardError => e
41
- @requests.each_value do |queue|
42
- queue << [:error, e]
43
- end
44
- disconnect
45
- end
33
+ # TODO: support yielding
34
+ def submit(*args)
35
+ request_id = SecureRandom.uuid
36
+ queue = @transport.submit(to_query(request_id, args))
37
+ wait_for_response(request_id, queue)
38
+ ensure
39
+ @transport.close_request(request_id)
46
40
  end
47
41
 
48
- def disconnect
49
- raise NotConnectedError if @connection_task.nil?
50
-
51
- @connection_task&.stop
52
- @connection_task&.wait
53
- @connection_task = nil
54
- @requests = {}
42
+ def inspect
43
+ "<#{self.class} @url=#{@url}>"
55
44
  end
56
45
 
57
- def query(*args) # rubocop:disable Metrics/MethodLength
58
- response_queue, request_id = schedule_query(args)
59
- result = []
46
+ alias to_s inspect
60
47
 
61
- response_queue.each do |status, response|
62
- reraise_error!(response) if status == :error
48
+ private
63
49
 
64
- status = response[:status]
50
+ def wait_for_response(request_id, queue, result: []) # rubocop:disable Metrics/MethodLength
51
+ queue.each do |status, response|
52
+ check_errors!(request_id, status, response)
65
53
 
66
- if status[:code] == NO_CONTENT_STATUS
67
- close_request(request_id)
54
+ case SUCCESS[response.dig(:status, :code)]
55
+ when :success
56
+ return result + Typing.cast(response.dig(:result, :data))
57
+ when :partial_content then result += Typing.cast(response.dig(:result, :data))
58
+ when :no_content
68
59
  return []
69
60
  end
70
-
71
- check_errors!(status, request_id)
72
-
73
- page = Typing.cast(response.dig(:result, :data))
74
-
75
- case status[:code]
76
- when SUCCESS_STATUS
77
- close_request(request_id)
78
- return result + page
79
- when PARTIAL_CONTENT_STATUS
80
- result += page
81
- else
82
- raise UnknownResponseStatus, status
83
- end
84
61
  end
62
+ rescue ::Async::Stop
63
+ retry if @transport.ongoing_request?(request_id)
64
+ raise UnknownRequestStopped, "#{request_id} is not in the ongoing requests list"
85
65
  end
86
66
 
87
- private
88
-
89
- def schedule_query(args)
90
- uuid = SecureRandom.uuid
91
- queue = Async::Queue.new
92
- @requests[uuid] = queue
93
- @query_queue << to_query(uuid, args)
94
-
95
- [queue, uuid]
96
- end
97
-
98
- def to_query(uuid, message)
99
- case message.first
67
+ def to_query(request_id, message)
68
+ case message.first # TODO: properly handle unknown type of message
100
69
  when String
101
- string_query_message(uuid, *message)
70
+ string_query_message(request_id, *message)
102
71
  when Grumlin::Step
103
- build_query(uuid, message)
72
+ bytecode_query_message(request_id, Translator.to_bytecode_query(message))
104
73
  end
105
74
  end
106
75
 
107
- def check_errors!(status, request_id)
108
- error = ERRORS[status[:code]]
109
- close_request(request_id)
110
- raise(error, status) if error
111
- end
76
+ def check_errors!(_request_id, status, response)
77
+ reraise_error!(response) if status == :error
78
+
79
+ status = response[:status]
112
80
 
113
- def close_request(request_id)
114
- @requests.delete(request_id)
81
+ if (error = ERRORS[status[:code]])
82
+ raise(error, status)
83
+ end
84
+
85
+ return unless SUCCESS[status[:code]].nil?
86
+
87
+ raise(UnknownResponseStatus, status)
115
88
  end
116
89
 
117
90
  def reraise_error!(error)
118
91
  raise error
119
92
  rescue StandardError
120
- raise ConnectionError
121
- end
122
-
123
- def query_task(connection)
124
- loop do
125
- connection.write @query_queue.dequeue
126
- connection.flush
127
- end
128
- end
129
-
130
- def response_task(connection)
131
- loop do
132
- response = connection.read
133
- response_queue = @requests[response[:requestId]]
134
- response_queue << [:response, response]
135
- end
93
+ raise UnknownError
136
94
  end
137
95
 
138
- def string_query_message(uuid, query, bindings)
96
+ def string_query_message(request_id, query, bindings)
139
97
  {
140
- requestId: uuid,
98
+ requestId: request_id,
141
99
  op: "eval",
142
100
  processor: "",
143
101
  args: {
@@ -148,25 +106,16 @@ module Grumlin
148
106
  }
149
107
  end
150
108
 
151
- def bytecode_query_message(uuid, bytecode)
109
+ def bytecode_query_message(request_id, bytecode)
152
110
  {
153
- requestId: uuid,
111
+ requestId: request_id,
154
112
  op: "bytecode",
155
113
  processor: "traversal",
156
114
  args: {
157
- gremlin: { "@type": "g:Bytecode", "@value": { step: bytecode } },
115
+ gremlin: Typing.to_bytecode(bytecode),
158
116
  aliases: { g: :g }
159
117
  }
160
118
  }
161
119
  end
162
-
163
- def build_query(uuid, steps)
164
- case @mode
165
- when :string
166
- string_query_message(uuid, *Translator.to_string_query(steps))
167
- else
168
- bytecode_query_message(uuid, Translator.to_bytecode_query(steps))
169
- end
170
- end
171
120
  end
172
121
  end
data/lib/grumlin/edge.rb CHANGED
@@ -15,11 +15,11 @@ module Grumlin
15
15
  end
16
16
 
17
17
  def ==(other)
18
- @label == other.label && @id == other.id
18
+ self.class == other.class && @label == other.label && @id == other.id
19
19
  end
20
20
 
21
21
  def inspect
22
- "<E #{@label}(#{@id})>"
22
+ "e[#{@id}][#{@inV}-#{@label}->#{@outV}]"
23
23
  end
24
24
  alias to_s inspect
25
25
  end
@@ -3,8 +3,14 @@
3
3
  module Grumlin
4
4
  class Error < StandardError; end
5
5
 
6
+ class UnknownError < Error; end
7
+
6
8
  class ConnectionError < Error; end
7
9
 
10
+ class CannotConnectError < ConnectionError; end
11
+
12
+ class DisconnectError < ConnectionError; end
13
+
8
14
  class ConnectionStatusError < Error; end
9
15
 
10
16
  class NotConnectedError < ConnectionStatusError; end
@@ -24,8 +30,6 @@ module Grumlin
24
30
 
25
31
  class UnknownTypeError < ProtocolError; end
26
32
 
27
- class ConnectionClosedError < Error; end
28
-
29
33
  class StatusError < Error
30
34
  attr_reader :status
31
35
 
@@ -48,4 +52,20 @@ module Grumlin
48
52
  class ServerSerializationError < ServerSideError; end
49
53
 
50
54
  class ServerTimeoutError < ServerSideError; end
55
+
56
+ class InternalClientError < Error; end
57
+
58
+ class UnknownRequestStoppedError < InternalClientError; end
59
+
60
+ class ResourceLeakError < InternalClientError; end
61
+
62
+ class UnknownMapKey < InternalClientError
63
+ attr_reader :key, :map
64
+
65
+ def initialize(key, map)
66
+ @key = key
67
+ @map = map
68
+ super("Cannot cast key #{key} in map #{map}")
69
+ end
70
+ end
51
71
  end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Grumlin
4
+ module Order
5
+ module Order
6
+ DESC = { "@type": "g:Order", "@value": "desc" }.freeze
7
+ ASC = { "@type": "g:Order", "@value": "desc" }.freeze
8
+
9
+ extend self # rubocop:disable Style/ModuleFunction
10
+
11
+ def asc
12
+ ASC
13
+ end
14
+
15
+ def desc
16
+ DESC
17
+ end
18
+ end
19
+
20
+ extend Order
21
+ end
22
+ end
data/lib/grumlin/p.rb ADDED
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Grumlin
4
+ module P
5
+ module P
6
+ %w[within].each do |step|
7
+ define_method step do |*args|
8
+ { # TODO: replace with a class?
9
+ "@type": "g:P",
10
+ "@value": { predicate: "within", value: { "@type": "g:List", "@value": args } }
11
+ }
12
+ end
13
+ end
14
+ end
15
+
16
+ extend P
17
+ end
18
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Grumlin
4
+ class Path
5
+ def initialize(path)
6
+ @labels = Typing.cast(path[:labels])
7
+ @objects = Typing.cast(path[:objects])
8
+ end
9
+
10
+ def inspect
11
+ "p[#{@objects}]"
12
+ end
13
+ alias to_s inspect
14
+ end
15
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Grumlin
4
+ module Pop
5
+ module Pop
6
+ extend self # rubocop:disable Style/ModuleFunction
7
+
8
+ FIRST = { "@type": "g:Pop", "@value": "first" }.freeze
9
+ LAST = { "@type": "g:Pop", "@value": "last" }.freeze
10
+ ALL = { "@type": "g:Pop", "@value": "all" }.freeze
11
+ MIXED = { "@type": "g:Pop", "@value": "mixed" }.freeze
12
+
13
+ def first
14
+ FIRST
15
+ end
16
+
17
+ def last
18
+ LAST
19
+ end
20
+
21
+ def all
22
+ ALL
23
+ end
24
+
25
+ def mixed
26
+ MIXED
27
+ end
28
+ end
29
+
30
+ extend Pop
31
+ end
32
+ end
data/lib/grumlin/step.rb CHANGED
@@ -1,46 +1,31 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Grumlin
4
- class Step
5
- attr_reader :client, :name, :args
4
+ class Step < AnonymousStep
5
+ attr_reader :client
6
6
 
7
- # TODO: add support for bytecode
8
7
  def initialize(client, name, *args, previous_steps: [])
8
+ super(name, *args, previous_steps: previous_steps)
9
9
  @client = client
10
- @name = name
11
- @previous_steps = previous_steps
12
- @args = args
13
10
  end
14
11
 
15
- %w[addV addE V E limit count drop property valueMap select from to as].each do |step|
16
- define_method step do |*args|
17
- Step.new(@client, step, *args, previous_steps: steps)
18
- end
12
+ def next
13
+ @enum ||= toList.to_enum
14
+ @enum.next
19
15
  end
20
16
 
21
- alias addVertex addV
22
- alias addEdge addE
23
-
24
- # TODO: add support for next
25
- # TODO: add support for iterate
26
- # TODO: memoization
27
17
  def toList # rubocop:disable Naming/MethodName
28
- @client.query(*steps)
29
- end
30
-
31
- def inspect
32
- "<Step #{self}>" # TODO: substitute bindings
18
+ @toList ||= @client.submit(*steps) # rubocop:disable Naming/VariableName
33
19
  end
34
20
 
35
- # TODO: memoization
36
- def to_s(*)
37
- Translator.to_string(steps)
21
+ def iterate
22
+ @client.submit(*(steps + [nil]))
38
23
  end
39
24
 
40
- alias to_gremlin to_s
25
+ private
41
26
 
42
- def steps
43
- (@previous_steps + [self])
27
+ def add_step(step_name, args, previous_steps:)
28
+ self.class.new(@client, step_name, *args, previous_steps: previous_steps)
44
29
  end
45
30
  end
46
31
  end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Grumlin
4
+ module Sugar
5
+ # TODO: how to use it in specs?
6
+ HELPERS = [
7
+ Grumlin::U,
8
+ Grumlin::T,
9
+ Grumlin::P,
10
+ Grumlin::Pop,
11
+ Grumlin::Order
12
+ ].freeze
13
+
14
+ def self.included(base)
15
+ base.extend ClassMethods
16
+ end
17
+
18
+ module ClassMethods
19
+ def const_missing(name)
20
+ helper = HELPERS.find { |h| h.const_defined?(name) }
21
+ super if helper.nil?
22
+
23
+ const_set(name, helper)
24
+ end
25
+ end
26
+
27
+ def const_missing(name)
28
+ helper = HELPERS.find { |h| h.const_defined?(name) }
29
+ super if helper.nil?
30
+
31
+ const_set(name, helper)
32
+ end
33
+
34
+ def __
35
+ Grumlin::U
36
+ end
37
+
38
+ def g
39
+ Grumlin::Traversal.new
40
+ end
41
+ end
42
+ end
data/lib/grumlin/t.rb ADDED
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Grumlin
4
+ module T
5
+ module T
6
+ T_ID = { :@type => "g:T", :@value => "id" }.freeze # TODO: replace with a class?
7
+ T_LABEL = { :@type => "g:T", :@value => "label" }.freeze # TODO: replace with a class?
8
+
9
+ extend self # rubocop:disable Style/ModuleFunction
10
+
11
+ def id
12
+ T_ID
13
+ end
14
+
15
+ def label
16
+ T_LABEL
17
+ end
18
+ end
19
+
20
+ extend T
21
+ end
22
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "grumlin/test/rspec/db_cleaner_context"
4
+ require "grumlin/test/rspec/gremlin_context"
5
+
6
+ module Grumlin
7
+ module Test
8
+ module RSpec
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Grumlin
4
+ module Test
5
+ module RSpec
6
+ module DBCleanerContext
7
+ end
8
+
9
+ ::RSpec.shared_context DBCleanerContext do
10
+ include DBCleanerContext
11
+
12
+ before do
13
+ g.V().drop.iterate
14
+ end
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Grumlin
4
+ module Test
5
+ module RSpec
6
+ module GremlinContext
7
+ end
8
+
9
+ ::RSpec.shared_context GremlinContext do
10
+ include GremlinContext
11
+
12
+ let(:g) { Grumlin::Traversal.new }
13
+
14
+ after do
15
+ expect(Grumlin.config.default_client.requests).to be_empty
16
+ Grumlin.config.default_client.disconnect
17
+ Grumlin.config.reset!
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
@@ -3,38 +3,39 @@
3
3
  module Grumlin
4
4
  module Translator
5
5
  class << self
6
- def to_string_query(steps) # rubocop:disable Metrics/MethodLength
7
- counter = 0
8
- string_steps, bindings = steps.each_with_object([[], {}]) do |step, (acc_g, acc_b)|
9
- args = step.args.map do |arg|
10
- binding_name(counter).tap do |b|
11
- acc_b[b] = arg
12
- counter += 1
13
- end
14
- end.join(", ")
15
-
16
- acc_g << "#{step.name}(#{args})"
17
- end
6
+ def to_bytecode(steps)
7
+ return arg_to_bytecode(steps) if steps.is_a?(AnonymousStep)
18
8
 
19
- ["g.#{string_steps.join(".")}", bindings]
9
+ steps.map do |step|
10
+ arg_to_bytecode(step)
11
+ end
20
12
  end
21
13
 
22
14
  def to_bytecode_query(steps)
23
15
  steps.map do |step|
24
- [step.name, *step.args.flatten]
16
+ arg_to_query_bytecode(step)
25
17
  end
26
18
  end
27
19
 
28
- def to_string(steps)
29
- "g." + steps.map do |step| # rubocop:disable Style/StringConcatenation
30
- "#{step.name}(#{step.args.map(&:inspect).join(", ")})"
31
- end.join(".")
20
+ private
21
+
22
+ def arg_to_bytecode(arg)
23
+ return arg unless arg.is_a?(AnonymousStep)
24
+
25
+ args = arg.args.flatten.map do |a|
26
+ a.instance_of?(AnonymousStep) ? to_bytecode(a.steps) : arg_to_bytecode(a)
27
+ end
28
+ [arg.name, *args]
32
29
  end
33
30
 
34
- private
31
+ def arg_to_query_bytecode(arg)
32
+ return ["none"] if arg.nil?
33
+ return arg unless arg.is_a?(AnonymousStep)
35
34
 
36
- def binding_name(num)
37
- "b_#{num.to_s(16)}"
35
+ args = arg.args.flatten.map do |a|
36
+ a.instance_of?(AnonymousStep) ? Typing.to_bytecode(to_bytecode(a.steps)) : arg_to_query_bytecode(a)
37
+ end
38
+ [arg.name, *args]
38
39
  end
39
40
  end
40
41
  end
@@ -0,0 +1,95 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Grumlin
4
+ module Transport
5
+ # A transport based on https://github.com/socketry/async
6
+ # and https://github.com/socketry/async-websocket
7
+ class Async
8
+ attr_reader :requests
9
+
10
+ def initialize(url, task: ::Async::Task.current)
11
+ @task = task
12
+ @endpoint = ::Async::HTTP::Endpoint.parse(url)
13
+
14
+ @requests = {}
15
+ @query_queue = ::Async::Queue.new
16
+ end
17
+
18
+ def connect
19
+ raise AlreadyConnectedError if connected?
20
+
21
+ @client = ::Async::WebSocket::Client.open(@endpoint)
22
+ @connection = @client.connect(@endpoint.authority, @endpoint.path)
23
+
24
+ @tasks_barrier = ::Async::Barrier.new(parent: @task)
25
+
26
+ @tasks_barrier.async { query_task }
27
+ @tasks_barrier.async { response_task }
28
+ rescue StandardError
29
+ raise ConnectionError
30
+ end
31
+
32
+ def disconnect
33
+ raise NotConnectedError unless connected?
34
+
35
+ @tasks_barrier.tasks.each(&:stop)
36
+ @tasks_barrier.wait
37
+
38
+ @connection.close
39
+ @client.close
40
+
41
+ @client = nil
42
+ @connection = nil
43
+ @tasks_barrier = nil
44
+
45
+ raise ResourceLeakError, "ongoing requests list is not empty: #{@requests.count} items" unless @requests.empty?
46
+ raise ResourceLeakError, "query queue empty: #{@query.count} items" unless @query_queue.empty?
47
+ end
48
+
49
+ # Raw message
50
+ def submit(message)
51
+ raise NotConnectedError unless connected?
52
+
53
+ uuid = message[:requestId]
54
+ ::Async::Queue.new.tap do |queue|
55
+ @requests[uuid] = queue
56
+ @query_queue << message
57
+ end
58
+ end
59
+
60
+ def close_request(request_id)
61
+ @requests.delete(request_id)
62
+ end
63
+
64
+ def ongoing_request?(request_id)
65
+ @requests.key?(request_id)
66
+ end
67
+
68
+ def connected?
69
+ !@connection.nil?
70
+ end
71
+
72
+ private
73
+
74
+ def query_task
75
+ @query_queue.each do |query|
76
+ @connection.write(query)
77
+ @connection.flush
78
+ end
79
+ rescue StandardError
80
+ raise DisconnectError
81
+ end
82
+
83
+ def response_task
84
+ loop do
85
+ response = @connection.read
86
+ # TODO: sometimes response does not include requestID, no idea how to handle it so far.
87
+ response_queue = @requests[response[:requestId]]
88
+ response_queue << [:response, response]
89
+ end
90
+ rescue StandardError
91
+ raise DisconnectError
92
+ end
93
+ end
94
+ end
95
+ end
@@ -4,18 +4,15 @@ module Grumlin
4
4
  class Traversal
5
5
  attr_reader :connection
6
6
 
7
- def initialize(client_or_url, &block)
7
+ def initialize(client_or_url = Grumlin.config.default_client)
8
8
  @client = if client_or_url.is_a?(String)
9
9
  Grumlin::Client.new(client_or_url)
10
10
  else
11
11
  client_or_url
12
12
  end
13
-
14
- return if block.nil?
15
-
16
- TraversingContext.new(self).instance_exec(&block)
17
13
  end
18
14
 
15
+ # TODO: add other start steps
19
16
  %w[addV addE V E].each do |step|
20
17
  define_method step do |*args|
21
18
  Step.new(@client, step, *args)
@@ -4,15 +4,19 @@ module Grumlin
4
4
  module Typing
5
5
  TYPES = {
6
6
  "g:List" => ->(value) { value.map { |item| cast(item) } },
7
+ "g:Set" => ->(value) { Set.new(value.map { |item| cast(item) }) },
7
8
  "g:Map" => ->(value) { cast_map(value) },
8
9
  "g:Vertex" => ->(value) { cast_entity(Grumlin::Vertex, value) },
9
10
  "g:Edge" => ->(value) { cast_entity(Grumlin::Edge, value) },
11
+ "g:Path" => ->(value) { cast_entity(Grumlin::Path, value) },
10
12
  "g:Int64" => ->(value) { cast_int(value) },
11
13
  "g:Int32" => ->(value) { cast_int(value) },
12
- "g:Traverser" => ->(value) { cast(value[:value]) } # TODO: wtf is bulk?
14
+ "g:Double" => ->(value) { cast_double(value) },
15
+ "g:Traverser" => ->(value) { cast(value[:value]) }, # TODO: wtf is bulk?
16
+ "g:T" => ->(value) { value.to_sym }
13
17
  }.freeze
14
18
 
15
- CASTABLE_TYPES = [Hash, String, Integer].freeze
19
+ CASTABLE_TYPES = [Hash, String, Integer, TrueClass, FalseClass].freeze
16
20
 
17
21
  class << self
18
22
  def cast(value)
@@ -27,6 +31,13 @@ module Grumlin
27
31
  type.call(value[:@value])
28
32
  end
29
33
 
34
+ def to_bytecode(step)
35
+ {
36
+ "@type": "g:Bytecode",
37
+ "@value": { step: step }
38
+ }
39
+ end
40
+
30
41
  private
31
42
 
32
43
  def castable_type?(value); end
@@ -47,6 +58,12 @@ module Grumlin
47
58
  value
48
59
  end
49
60
 
61
+ def cast_double(value)
62
+ raise TypeError, "#{value} is not a Double" unless value.is_a?(Float)
63
+
64
+ value
65
+ end
66
+
50
67
  def cast_entity(entity, value)
51
68
  entity.new(**value)
52
69
  rescue ArgumentError, TypeError
@@ -54,12 +71,15 @@ module Grumlin
54
71
  end
55
72
 
56
73
  def cast_map(value)
57
- Hash[*value].transform_keys(&:to_sym).transform_values { |v| cast(v) }
74
+ Hash[*value].transform_keys do |key|
75
+ next key.to_sym if key.respond_to?(:to_sym)
76
+ next cast(key) if key[:@type]
77
+
78
+ raise UnknownMapKey, key, value
79
+ end.transform_values { |v| cast(v) }
58
80
  rescue ArgumentError
59
81
  raise TypeError, "#{value} cannot be casted to Hash"
60
82
  end
61
-
62
- def cast_traverser(value); end
63
83
  end
64
84
  end
65
85
  end
data/lib/grumlin/u.rb ADDED
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Grumlin
4
+ module U
5
+ module U
6
+ extend self # rubocop:disable Style/ModuleFunction
7
+
8
+ %w[addV V has count out values unfold].each do |step|
9
+ define_method step do |*args|
10
+ AnonymousStep.new(step, *args)
11
+ end
12
+ end
13
+ end
14
+
15
+ # TODO: add alias __
16
+ extend U
17
+ end
18
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Grumlin
4
- VERSION = "0.1.3"
4
+ VERSION = "0.2.0"
5
5
  end
@@ -10,11 +10,11 @@ module Grumlin
10
10
  end
11
11
 
12
12
  def ==(other)
13
- @label == other.label && @id == other.id
13
+ self.class == other.class && @label == other.label && @id == other.id
14
14
  end
15
15
 
16
16
  def inspect
17
- "<V #{@label}(#{@id})>"
17
+ "v[#{@id}]"
18
18
  end
19
19
  alias to_s inspect
20
20
  end
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.1.3
4
+ version: 0.2.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: 2021-05-27 00:00:00.000000000 Z
11
+ date: 2021-07-26 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: async-websocket
@@ -16,14 +16,14 @@ dependencies:
16
16
  requirements:
17
17
  - - "~>"
18
18
  - !ruby/object:Gem::Version
19
- version: '0.18'
19
+ version: '0.19'
20
20
  type: :runtime
21
21
  prerelease: false
22
22
  version_requirements: !ruby/object:Gem::Requirement
23
23
  requirements:
24
24
  - - "~>"
25
25
  - !ruby/object:Gem::Version
26
- version: '0.18'
26
+ version: '0.19'
27
27
  description: Gremlin query language DSL for Ruby.
28
28
  email:
29
29
  - zhulik.gleb@gmail.com
@@ -33,6 +33,7 @@ extra_rdoc_files: []
33
33
  files:
34
34
  - ".github/workflows/main.yml"
35
35
  - ".gitignore"
36
+ - ".overcommit.yml"
36
37
  - ".rspec"
37
38
  - ".rubocop.yml"
38
39
  - CHANGELOG.md
@@ -43,20 +44,30 @@ files:
43
44
  - README.md
44
45
  - bin/console
45
46
  - bin/setup
46
- - bin/stress
47
47
  - docker-compose.yml
48
48
  - gremlin_server/Dockerfile
49
49
  - gremlin_server/tinkergraph-empty.properties
50
50
  - grumlin.gemspec
51
51
  - lib/grumlin.rb
52
+ - lib/grumlin/anonymous_step.rb
52
53
  - lib/grumlin/client.rb
53
54
  - lib/grumlin/edge.rb
54
55
  - lib/grumlin/exceptions.rb
56
+ - lib/grumlin/order.rb
57
+ - lib/grumlin/p.rb
58
+ - lib/grumlin/path.rb
59
+ - lib/grumlin/pop.rb
55
60
  - lib/grumlin/step.rb
61
+ - lib/grumlin/sugar.rb
62
+ - lib/grumlin/t.rb
63
+ - lib/grumlin/test/rspec.rb
64
+ - lib/grumlin/test/rspec/db_cleaner_context.rb
65
+ - lib/grumlin/test/rspec/gremlin_context.rb
56
66
  - lib/grumlin/translator.rb
67
+ - lib/grumlin/transport/async.rb
57
68
  - lib/grumlin/traversal.rb
58
- - lib/grumlin/traversing_context.rb
59
69
  - lib/grumlin/typing.rb
70
+ - lib/grumlin/u.rb
60
71
  - lib/grumlin/version.rb
61
72
  - lib/grumlin/vertex.rb
62
73
  homepage: https://github.com/zhulik/grumlin
@@ -81,7 +92,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
81
92
  - !ruby/object:Gem::Version
82
93
  version: '0'
83
94
  requirements: []
84
- rubygems_version: 3.2.15
95
+ rubygems_version: 3.2.22
85
96
  signing_key:
86
97
  specification_version: 4
87
98
  summary: Gremlin query language DSL for Ruby.
data/bin/stress DELETED
@@ -1,51 +0,0 @@
1
- #!/usr/bin/env ruby
2
- # frozen_string_literal: true
3
-
4
- require "bundler/setup"
5
- require "grumlin"
6
- require "irb"
7
-
8
- def queries(client, uuids)
9
- total = 0
10
- g = Grumlin::Traversal.new(client)
11
-
12
- loop do
13
- uuid = uuids.sample
14
- result = g.V(uuid).toList[0]
15
- raise "!!!" if result.id != uuid
16
-
17
- total += 1
18
- end
19
- rescue Async::Stop
20
- total
21
- end
22
-
23
- def prepare_dataset(client)
24
- uuids = Array.new(1000) { SecureRandom.uuid }
25
-
26
- Grumlin::Traversal.new(client) do
27
- g.V().drop
28
-
29
- uuids.each do |uuid|
30
- g.addV("test_vertex").property(id, uuid).toList
31
- end
32
- end
33
-
34
- uuids
35
- end
36
-
37
- Async do |task|
38
- client = Grumlin::Client.new("ws://localhost:8182/gremlin", mode: :bytecode)
39
-
40
- uuids = prepare_dataset(client)
41
- tasks = Array.new(20) { task.async { queries(client, uuids) } }
42
-
43
- task.sleep(60)
44
-
45
- tasks.each(&:stop)
46
- total = tasks.sum(&:wait)
47
-
48
- p("#{total} requests performed")
49
- ensure
50
- client.disconnect
51
- end
@@ -1,17 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Grumlin
4
- class TraversingContext
5
- ID = { :@type => "g:T", :@value => "id" }.freeze
6
-
7
- attr_reader :g
8
-
9
- def initialize(traversal)
10
- @g = traversal
11
- end
12
-
13
- def id
14
- ID
15
- end
16
- end
17
- end