grumlin 0.1.1 → 0.4.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: 1696d48d04fdfc23de590598ed655a9d2fc59e66e87eb04c5cd579df3805f972
4
- data.tar.gz: cfa3d4f3ce67d35d5d1b0e5af78024ddb3461b9f4272b1a49015a3527fcc85f0
3
+ metadata.gz: 11e434ab0f5cd463d357e69dd3cbf5083f85a1bdd32e4d2c96023ce262704020
4
+ data.tar.gz: f24dd70f8b0abe9b370f77587d590802b32aa3e7b8a20380b9518004868cc6c7
5
5
  SHA512:
6
- metadata.gz: 97b8df1e4b6c02d874c9659d7c506704d21f9cfaf419c427c3e00378e3b8a3f3ca30ad53e04b0e6b18dc9161302ed63ddcfcd4df6948edd777beba435424fcf5
7
- data.tar.gz: c6b0ad7814fdefa25909c4c008ead2a130e807760eed5528d728e854ea88e2c08dbcd9dd5c30caec2c737178285e6c929c0348cf24ce01e4468c09ce72a3723d
6
+ metadata.gz: 5bfbd0d4db9ef46d35839b8ca5654cdc1f5038ee2c4ca004b678471380095d1fb587fa89a8df0c5a4769e3943ceb4e0b88734b701593ccf1047a05ab85baaaf5
7
+ data.tar.gz: a7d87e630e6b2076bedd079365e709018b3fc61f1577564c504593ffdeb4deab88f56d8785c30877489171b6c9d7fd245982475a75f4c639647c923e2ca296ef
@@ -48,7 +48,7 @@ jobs:
48
48
  needs:
49
49
  - lint
50
50
  - test
51
- # if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v')
51
+ if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v')
52
52
  steps:
53
53
  - uses: actions/checkout@v2
54
54
 
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,9 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- grumlin (0.1.1)
5
- async-websocket (~> 0.18)
4
+ grumlin (0.4.0)
5
+ async-pool (~> 0.3)
6
+ async-websocket (~> 0.19)
6
7
 
7
8
  GEM
8
9
  remote: https://rubygems.org/
@@ -14,33 +15,34 @@ GEM
14
15
  tzinfo (~> 2.0)
15
16
  zeitwerk (~> 2.3)
16
17
  ast (2.4.2)
17
- async (1.29.0)
18
+ async (1.30.1)
18
19
  console (~> 1.10)
19
20
  nio4r (~> 2.3)
20
21
  timers (~> 4.1)
21
- async-http (0.56.2)
22
- async (~> 1.25)
23
- async-io (~> 1.28)
24
- async-pool (~> 0.2)
22
+ async-http (0.56.5)
23
+ async (>= 1.25)
24
+ async-io (>= 1.28)
25
+ async-pool (>= 0.2)
25
26
  protocol-http (~> 0.22.0)
26
27
  protocol-http1 (~> 0.14.0)
27
28
  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)
29
+ async-io (1.32.2)
30
+ async
31
+ async-pool (0.3.8)
32
+ async (>= 1.25)
32
33
  async-rspec (1.16.0)
33
34
  rspec (~> 3.0)
34
35
  rspec-files (~> 1.0)
35
36
  rspec-memory (~> 1.0)
36
- async-websocket (0.18.0)
37
+ async-websocket (0.19.0)
37
38
  async-http (~> 0.54)
38
39
  async-io (~> 1.23)
39
40
  protocol-websocket (~> 0.7.0)
40
- backport (1.1.2)
41
+ backport (1.2.0)
41
42
  benchmark (0.1.1)
43
+ childprocess (4.0.0)
42
44
  concurrent-ruby (1.1.8)
43
- console (1.12.0)
45
+ console (1.13.1)
44
46
  fiber-local
45
47
  diff-lcs (1.4.4)
46
48
  docile (1.4.0)
@@ -50,20 +52,24 @@ GEM
50
52
  fiber-local (1.0.0)
51
53
  i18n (1.8.10)
52
54
  concurrent-ruby (~> 1.0)
55
+ iniparse (1.5.0)
53
56
  jaro_winkler (1.5.4)
54
57
  kramdown (2.3.1)
55
58
  rexml
56
59
  kramdown-parser-gfm (1.1.0)
57
60
  kramdown (~> 2.0)
58
61
  minitest (5.14.4)
59
- nio4r (2.5.7)
60
- nokogiri (1.11.5-x86_64-linux)
62
+ nio4r (2.5.8)
63
+ nokogiri (1.11.7-x86_64-linux)
61
64
  racc (~> 1.4)
65
+ overcommit (0.57.0)
66
+ childprocess (>= 0.6.3, < 5)
67
+ iniparse (~> 1.4)
62
68
  parallel (1.20.1)
63
69
  parser (3.0.1.1)
64
70
  ast (~> 2.4.1)
65
71
  protocol-hpack (1.4.2)
66
- protocol-http (0.22.0)
72
+ protocol-http (0.22.5)
67
73
  protocol-http1 (0.14.1)
68
74
  protocol-http (~> 0.22)
69
75
  protocol-http2 (0.14.2)
@@ -95,16 +101,16 @@ GEM
95
101
  diff-lcs (>= 1.2.0, < 2.0)
96
102
  rspec-support (~> 3.10.0)
97
103
  rspec-support (3.10.2)
98
- rubocop (1.15.0)
104
+ rubocop (1.16.1)
99
105
  parallel (~> 1.10)
100
106
  parser (>= 3.0.0.0)
101
107
  rainbow (>= 2.2.2, < 4.0)
102
108
  regexp_parser (>= 1.8, < 3.0)
103
109
  rexml
104
- rubocop-ast (>= 1.5.0, < 2.0)
110
+ rubocop-ast (>= 1.7.0, < 2.0)
105
111
  ruby-progressbar (~> 1.7)
106
112
  unicode-display_width (>= 1.4.0, < 3.0)
107
- rubocop-ast (1.5.0)
113
+ rubocop-ast (1.7.0)
108
114
  parser (>= 3.0.1.1)
109
115
  rubocop-performance (1.11.3)
110
116
  rubocop (>= 1.7.0, < 2.0)
@@ -119,10 +125,11 @@ GEM
119
125
  simplecov_json_formatter (~> 0.1)
120
126
  simplecov-html (0.12.3)
121
127
  simplecov_json_formatter (0.1.3)
122
- solargraph (0.40.4)
123
- backport (~> 1.1)
128
+ solargraph (0.43.0)
129
+ backport (~> 1.2)
124
130
  benchmark
125
131
  bundler (>= 1.17.2)
132
+ diff-lcs (~> 1.4)
126
133
  e2mmap
127
134
  jaro_winkler (~> 1.5)
128
135
  kramdown (~> 2.3)
@@ -149,6 +156,8 @@ DEPENDENCIES
149
156
  async-rspec
150
157
  factory_bot
151
158
  grumlin!
159
+ nokogiri
160
+ overcommit
152
161
  rspec
153
162
  rubocop
154
163
  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_pool.close
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
@@ -8,8 +8,8 @@ Gem::Specification.new do |spec|
8
8
  spec.authors = ["Gleb Sinyavskiy"]
9
9
  spec.email = ["zhulik.gleb@gmail.com"]
10
10
 
11
- spec.summary = "A ruby client for Gremlin query language."
12
- spec.description = "A ruby client for Gremlin query language."
11
+ spec.summary = "Gremlin query language DSL for Ruby."
12
+ spec.description = "Gremlin query language DSL for Ruby."
13
13
  spec.homepage = "https://github.com/zhulik/grumlin"
14
14
  spec.license = "MIT"
15
15
  spec.required_ruby_version = Gem::Requirement.new(">= 2.6.0")
@@ -23,5 +23,6 @@ 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-pool", "~> 0.3"
27
+ spec.add_dependency "async-websocket", "~> 0.19"
27
28
  end
@@ -0,0 +1,64 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Async
4
+ # Channel is a wrapper around Async::Queue that provides
5
+ # a protocol and handy tools for passing data, exceptions and closing.
6
+ # It is designed to be used with only one publisher and one subscriber
7
+ class Channel
8
+ class ChannelError < StandardError; end
9
+
10
+ class ChannelClosedError < ChannelError; end
11
+
12
+ def initialize
13
+ @queue = Async::Queue.new
14
+ @closed = false
15
+ end
16
+
17
+ def closed?
18
+ @closed
19
+ end
20
+
21
+ # Methods for a publisher
22
+ def <<(payload)
23
+ raise(ChannelClosedError, "Cannot send to a closed channel") if @closed
24
+
25
+ @queue << [:payload, payload]
26
+ end
27
+
28
+ def exception(exception)
29
+ raise(ChannelClosedError, "Cannot send to a closed channel") if closed?
30
+
31
+ @queue << [:exception, exception]
32
+ end
33
+
34
+ def close
35
+ raise(ChannelClosedError, "Cannot close a closed channel") if closed?
36
+
37
+ @queue << [:close]
38
+ @closed = true
39
+ end
40
+
41
+ # Methods for a subscriber
42
+ def dequeue
43
+ each do |payload| # rubocop:disable Lint/UnreachableLoop this is intended
44
+ return payload
45
+ end
46
+ end
47
+
48
+ def each # rubocop:disable Metrics/MethodLength
49
+ raise(ChannelClosedError, "Cannot receive from a closed channel") if closed?
50
+
51
+ @queue.each do |type, payload|
52
+ case type
53
+ when :exception
54
+ payload.set_backtrace(caller + (payload.backtrace || [])) # A hack to preserve full backtrace
55
+ raise payload
56
+ when :payload
57
+ yield payload
58
+ when :close
59
+ break
60
+ end
61
+ end
62
+ end
63
+ end
64
+ end
data/lib/grumlin.rb CHANGED
@@ -4,21 +4,67 @@ require "securerandom"
4
4
  require "json"
5
5
 
6
6
  require "async"
7
+ require "async/pool"
8
+ require "async/pool/resource"
9
+ require "async/pool/controller"
7
10
  require "async/queue"
11
+ require "async/barrier"
8
12
  require "async/http/endpoint"
9
13
  require "async/websocket/client"
10
14
 
15
+ require_relative "async/channel"
16
+
11
17
  require_relative "grumlin/version"
12
18
  require_relative "grumlin/exceptions"
13
19
 
20
+ require_relative "grumlin/transport"
21
+ require_relative "grumlin/client"
22
+
14
23
  require_relative "grumlin/vertex"
15
24
  require_relative "grumlin/edge"
25
+ require_relative "grumlin/path"
16
26
  require_relative "grumlin/typing"
17
- require_relative "grumlin/client"
18
27
  require_relative "grumlin/traversal"
19
- require_relative "grumlin/step"
28
+ require_relative "grumlin/request_dispatcher"
20
29
  require_relative "grumlin/translator"
21
- require_relative "grumlin/traversing_context"
30
+
31
+ require_relative "grumlin/anonymous_step"
32
+ require_relative "grumlin/step"
33
+
34
+ require_relative "grumlin/t"
35
+ require_relative "grumlin/order"
36
+ require_relative "grumlin/u"
37
+ require_relative "grumlin/p"
38
+ require_relative "grumlin/pop"
39
+ require_relative "grumlin/sugar"
22
40
 
23
41
  module Grumlin
42
+ class Config
43
+ attr_accessor :url, :pool_size, :client_concurrency, :client_factory
44
+
45
+ # For some reason, client_concurrency must be greater than pool_size
46
+ def initialize
47
+ @pool_size = 10
48
+ @client_concurrency = 20
49
+ @client_factory = ->(url, parent) { Grumlin::Client.new(url, parent: parent) }
50
+ end
51
+
52
+ def default_pool
53
+ @default_pool ||= Async::Pool::Controller.new(Grumlin::Client::PoolResource, limit: pool_size)
54
+ end
55
+
56
+ def reset!
57
+ @default_pool = nil
58
+ end
59
+ end
60
+
61
+ class << self
62
+ def configure
63
+ yield config
64
+ end
65
+
66
+ def config
67
+ @config ||= Config.new
68
+ end
69
+ end
24
70
  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,172 +1,108 @@
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
8
-
9
- ERRORS = {
10
- 499 => InvalidRequestArgumentsError,
11
- 500 => ServerError,
12
- 597 => ScriptEvaluationError,
13
- 599 => ServerSerializationError,
14
- 598 => ServerTimeoutError,
15
-
16
- 401 => ClientSideError,
17
- 407 => ClientSideError,
18
- 498 => ClientSideError
19
- }.freeze
20
-
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
-
29
- connect if autoconnect
30
- end
31
-
32
- def connect # rubocop:disable Metrics/MethodLength
33
- raise AlreadyConnectedError unless @connection_task.nil?
4
+ class Client
5
+ class PoolResource < Async::Pool::Resource
6
+ attr_reader :client
34
7
 
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
8
+ def self.call
9
+ config = Grumlin.config
10
+ new(config.url, client_factory: config.client_factory, concurrency: config.client_concurrency)
45
11
  end
46
- end
47
-
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 = {}
55
- end
56
-
57
- def query(*args) # rubocop:disable Metrics/MethodLength
58
- response_queue, request_id = schedule_query(args)
59
- result = []
60
12
 
61
- response_queue.each do |status, response|
62
- reraise_error!(response) if status == :error
63
-
64
- status = response[:status]
65
-
66
- if status[:code] == NO_CONTENT_STATUS
67
- close_request(request_id)
68
- return []
69
- end
13
+ def initialize(url, client_factory:, concurrency: 1, parent: Async::Task.current)
14
+ super(concurrency)
15
+ @client = client_factory.call(url, parent).tap(&:connect)
16
+ end
70
17
 
71
- check_errors!(status, request_id)
18
+ def closed?
19
+ !@client.connected?
20
+ end
72
21
 
73
- page = Typing.cast(response.dig(:result, :data))
22
+ def close
23
+ @client.close
24
+ end
74
25
 
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
26
+ def write(*args)
27
+ @client.write(*args)
84
28
  end
85
29
  end
86
30
 
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]
31
+ def initialize(url, parent: Async::Task.current, **client_options)
32
+ @url = url
33
+ @client_options = client_options
34
+ @parent = parent
35
+ reset!
96
36
  end
97
37
 
98
- def to_query(uuid, message)
99
- case message.first
100
- when String
101
- string_query_message(uuid, *message)
102
- when Grumlin::Step
103
- build_query(uuid, message)
38
+ def connect
39
+ @transport = build_transport
40
+ response_channel = @transport.connect
41
+ @request_dispatcher = RequestDispatcher.new
42
+ @parent.async do
43
+ response_channel.each do |response|
44
+ @request_dispatcher.add_response(response)
45
+ end
46
+ rescue StandardError
47
+ close
104
48
  end
105
49
  end
106
50
 
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
51
+ def close
52
+ @transport.close
53
+ raise ResourceLeakError, "Request list is not empty: #{requests}" if @request_dispatcher.requests.any?
112
54
 
113
- def close_request(request_id)
114
- @requests.delete(request_id)
55
+ reset!
115
56
  end
116
57
 
117
- def reraise_error!(error)
118
- raise error
119
- rescue StandardError
120
- raise ConnectionError
58
+ def connected?
59
+ @transport&.connected? || false
121
60
  end
122
61
 
123
- def query_task(connection)
124
- loop do
125
- connection.write @query_queue.dequeue
126
- connection.flush
127
- end
128
- end
62
+ # TODO: support yielding
63
+ def write(*args) # rubocop:disable Metrics/MethodLength
64
+ raise NotConnectedError unless connected?
65
+
66
+ request_id = SecureRandom.uuid
67
+ request = to_query(request_id, args)
68
+ channel = @request_dispatcher.add_request(request)
69
+ @transport.write(request)
129
70
 
130
- def response_task(connection)
131
- loop do
132
- response = connection.read
133
- response_queue = @requests[response[:requestId]]
134
- response_queue << [:response, response]
71
+ begin
72
+ channel.dequeue.flat_map { |item| Typing.cast(item) }
73
+ rescue Async::Stop
74
+ retry if @request_dispatcher.ongoing_request?(request_id)
75
+ raise Grumlin::UnknownRequestStoppedError, "#{request_id} is not in the ongoing requests list"
135
76
  end
136
77
  end
137
78
 
138
- def string_query_message(uuid, query, bindings)
139
- {
140
- requestId: uuid,
141
- op: "eval",
142
- processor: "",
143
- args: {
144
- gremlin: query,
145
- bindings: bindings,
146
- language: "gremlin-groovy"
147
- }
148
- }
79
+ def inspect
80
+ "<#{self.class} url=#{@url} connected=#{connected?}>"
149
81
  end
150
82
 
151
- def bytecode_query_message(uuid, bytecode)
83
+ alias to_s inspect
84
+
85
+ private
86
+
87
+ def to_query(request_id, message)
152
88
  {
153
- requestId: uuid,
89
+ requestId: request_id,
154
90
  op: "bytecode",
155
91
  processor: "traversal",
156
92
  args: {
157
- gremlin: { "@type": "g:Bytecode", "@value": { step: bytecode } },
93
+ gremlin: Typing.to_bytecode(Translator.to_bytecode_query(message)),
158
94
  aliases: { g: :g }
159
95
  }
160
96
  }
161
97
  end
162
98
 
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
99
+ def reset!
100
+ @request_dispatcher = nil
101
+ @transport = nil
102
+ end
103
+
104
+ def build_transport
105
+ Transport.new(@url, parent: @parent, **@client_options)
170
106
  end
171
107
  end
172
108
  end