grumlin 0.1.3 → 0.2.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: 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