├── .gitignore ├── .ruby-gemset ├── .ruby-version ├── .travis.yml ├── Gemfile ├── Gemfile.lock ├── LICENSE ├── README.md ├── RELEASE.md ├── Rakefile ├── example ├── scripts │ └── test.groovy └── test.rb ├── gremlin_client.gemspec ├── lib ├── gremlin_client.rb └── gremlin_client │ ├── connection.rb │ ├── connection_timeout_error.rb │ ├── execution_timeout_error.rb │ ├── server_error.rb │ └── version.rb └── spec ├── connection_spec.rb ├── exceptions_spec.rb ├── spec_helper.rb └── status_codes_spec.rb /.gitignore: -------------------------------------------------------------------------------- 1 | *.gem 2 | *.rbc 3 | /.config 4 | /coverage/ 5 | /InstalledFiles 6 | /pkg/ 7 | /spec/reports/ 8 | /spec/examples.txt 9 | /test/tmp/ 10 | /test/version_tmp/ 11 | /tmp/ 12 | 13 | # Used by dotenv library to load environment variables. 14 | # .env 15 | 16 | ## Specific to RubyMotion: 17 | .dat* 18 | .repl_history 19 | build/ 20 | *.bridgesupport 21 | build-iPhoneOS/ 22 | build-iPhoneSimulator/ 23 | 24 | ## Specific to RubyMotion (use of CocoaPods): 25 | # 26 | # We recommend against adding the Pods directory to your .gitignore. However 27 | # you should judge for yourself, the pros and cons are mentioned at: 28 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 29 | # 30 | # vendor/Pods/ 31 | 32 | ## Documentation cache and generated files: 33 | /.yardoc/ 34 | /_yardoc/ 35 | /doc/ 36 | /rdoc/ 37 | 38 | ## Environment normalization: 39 | /.bundle/ 40 | /vendor/bundle 41 | /lib/bundler/man/ 42 | 43 | # for a library or gem, you might want to ignore these files since the code is 44 | # intended to run in multiple environments; otherwise, check them in: 45 | # Gemfile.lock 46 | # .ruby-version 47 | # .ruby-gemset 48 | 49 | # unless supporting rvm < 1.11.0 or doing something fancy, ignore this: 50 | .rvmrc 51 | 52 | **/.*.swp 53 | -------------------------------------------------------------------------------- /.ruby-gemset: -------------------------------------------------------------------------------- 1 | gremlin_client 2 | -------------------------------------------------------------------------------- /.ruby-version: -------------------------------------------------------------------------------- 1 | 2.6.6 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: ruby 2 | 3 | notifications: 4 | email: 5 | on_sucess: false 6 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | # Specify your gem's dependencies in keyword_search.gemspec 4 | gemspec 5 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | PATH 2 | remote: . 3 | specs: 4 | gremlin_client (0.1.6) 5 | oj (~> 2.16) 6 | websocket-client-simple (~> 0.3) 7 | 8 | GEM 9 | remote: https://rubygems.org/ 10 | specs: 11 | ast (2.4.0) 12 | coderay (1.1.2) 13 | coveralls (0.8.22) 14 | json (>= 1.8, < 3) 15 | simplecov (~> 0.16.1) 16 | term-ansicolor (~> 1.3) 17 | thor (~> 0.19.4) 18 | tins (~> 1.6) 19 | diff-lcs (1.3) 20 | docile (1.3.1) 21 | event_emitter (0.2.6) 22 | ffi (1.9.25) 23 | formatador (0.2.5) 24 | guard (2.14.2) 25 | formatador (>= 0.2.4) 26 | listen (>= 2.7, < 4.0) 27 | lumberjack (>= 1.0.12, < 2.0) 28 | nenv (~> 0.1) 29 | notiffany (~> 0.0) 30 | pry (>= 0.9.12) 31 | shellany (~> 0.0) 32 | thor (>= 0.18.1) 33 | guard-compat (1.2.1) 34 | guard-rspec (4.7.3) 35 | guard (~> 2.1) 36 | guard-compat (~> 1.1) 37 | rspec (>= 2.99.0, < 4.0) 38 | jaro_winkler (1.5.1) 39 | json (2.1.0) 40 | listen (3.1.5) 41 | rb-fsevent (~> 0.9, >= 0.9.4) 42 | rb-inotify (~> 0.9, >= 0.9.7) 43 | ruby_dep (~> 1.2) 44 | lumberjack (1.0.13) 45 | method_source (0.9.0) 46 | nenv (0.3.0) 47 | notiffany (0.1.1) 48 | nenv (~> 0.1) 49 | shellany (~> 0.0) 50 | oj (2.18.5) 51 | parallel (1.12.1) 52 | parser (2.5.1.2) 53 | ast (~> 2.4.0) 54 | powerpack (0.1.2) 55 | pry (0.11.3) 56 | coderay (~> 1.1.0) 57 | method_source (~> 0.9.0) 58 | rainbow (3.0.0) 59 | rake (13.0.1) 60 | rb-fsevent (0.10.3) 61 | rb-inotify (0.9.10) 62 | ffi (>= 0.5.0, < 2) 63 | rspec (3.8.0) 64 | rspec-core (~> 3.8.0) 65 | rspec-expectations (~> 3.8.0) 66 | rspec-mocks (~> 3.8.0) 67 | rspec-core (3.8.0) 68 | rspec-support (~> 3.8.0) 69 | rspec-expectations (3.8.1) 70 | diff-lcs (>= 1.2.0, < 2.0) 71 | rspec-support (~> 3.8.0) 72 | rspec-mocks (3.8.0) 73 | diff-lcs (>= 1.2.0, < 2.0) 74 | rspec-support (~> 3.8.0) 75 | rspec-support (3.8.0) 76 | rubocop (0.59.0) 77 | jaro_winkler (~> 1.5.1) 78 | parallel (~> 1.10) 79 | parser (>= 2.5, != 2.5.1.1) 80 | powerpack (~> 0.1) 81 | rainbow (>= 2.2.2, < 4.0) 82 | ruby-progressbar (~> 1.7) 83 | unicode-display_width (~> 1.0, >= 1.0.1) 84 | ruby-progressbar (1.10.0) 85 | ruby_dep (1.5.0) 86 | shellany (0.0.1) 87 | simplecov (0.16.1) 88 | docile (~> 1.1) 89 | json (>= 1.8, < 3) 90 | simplecov-html (~> 0.10.0) 91 | simplecov-html (0.10.2) 92 | term-ansicolor (1.6.0) 93 | tins (~> 1.0) 94 | thor (0.19.4) 95 | tins (1.16.3) 96 | unicode-display_width (1.4.0) 97 | websocket (1.2.8) 98 | websocket-client-simple (0.3.0) 99 | event_emitter 100 | websocket 101 | 102 | PLATFORMS 103 | ruby 104 | 105 | DEPENDENCIES 106 | bundler (~> 1.13) 107 | coveralls (~> 0.8) 108 | gremlin_client! 109 | guard-rspec (~> 4.7) 110 | rake (~> 13.0) 111 | rspec (~> 3.5) 112 | rubocop (~> 0.49) 113 | 114 | BUNDLED WITH 115 | 1.16.4 116 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Ruby Gremlin Client 2 | 3 | 4 | [![Build Status](https://travis-ci.org/marcelocf/gremlin_client.svg?branch=master)](https://travis-ci.org/marcelocf/gremlin_client) 5 | [![Gem Version](https://badge.fury.io/rb/gremlin_client.svg)](https://badge.fury.io/rb/gremlin_client) 6 | ![](http://ruby-gem-downloads-badge.herokuapp.com/gremlin_client?color=brightgreen) 7 | [![Coverage Status](https://coveralls.io/repos/github/marcelocf/gremlin_client/badge.svg?branch=master)](https://coveralls.io/github/marcelocf/gremlin_client?branch=master) 8 | 9 | 10 | 11 | ``` 12 | =================================== WARNING =================================== 13 | || If you are using version 0.1.2 or prior please update! Your results won't || 14 | || be consistent while you don't do so! || 15 | =================================== WARNING =================================== 16 | ``` 17 | Details on the above at 18 | [#3](https://github.com/marcelocf/gremlin_client/issues/3) 19 | 20 | 21 | Gremlin client in ruby for the WebSocketChannelizer. 22 | 23 | This client is not thread safe by itself! If you want to make it safer for your app, please make sure 24 | to use something like [ConnectionPool gem](https://github.com/mperham/connection_pool). 25 | 26 | ## Usage: 27 | 28 | ```bash 29 | gem instal gremlin_client 30 | ``` 31 | 32 | ```ruby 33 | conn = GremlinClient::Connection.new(host: 'localhost', port:123) 34 | resp = conn.send_query("g.V().has('myVar', myValue)", {myValue: 'this_is_processed_by_gremlin_server'}) 35 | ``` 36 | 37 | Alternativelly, you can use groovy files instead: 38 | 39 | ```ruby 40 | resp = conn.file_send("query.groovy", {var1: 12}) 41 | ``` 42 | 43 | ```groovy 44 | g.V().has("something", var1) 45 | ``` 46 | 47 | You can even specify the folder where to load those files in the constructor: 48 | 49 | ```ruby 50 | conn = GremlinClient::Connection.new(gremlin_script_path: 'scripts/gremlin') 51 | ``` 52 | 53 | 54 | ## TODO: 55 | 56 | The following things are priority in our list of things to do, but we haven't had time to implement 57 | yet: 58 | 59 | * rspec 60 | * SSL support 61 | * authentication 62 | 63 | The following is very nice to have, but since we are testing against Titan 1.0.0, which has a pretty 64 | old version of Gremlin, we still rely on groovy to do more complex parsing. But as soon as JanusGraph 65 | is release it would be nice to start working on: 66 | 67 | * ruby-side syntax like `g.V.hasLabel("omg")..` 68 | * compiled Gremlin query generation 69 | -------------------------------------------------------------------------------- /RELEASE.md: -------------------------------------------------------------------------------- 1 | # Release Notes 2 | 3 | ## v0.1.6 - 2018-09-10 4 | 5 | * add path option to Amazon neptune (thank you @yoku and @garymacindoe) 6 | * depedency version bump (security) 7 | 8 | ## v0.1.5 9 | 10 | ## v0.1.4 - 2017-04-14 11 | 12 | * change from native JSON to Oj 13 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'bundler/gem_tasks' 2 | require 'rake/testtask' 3 | 4 | 5 | # rspec: 6 | begin 7 | require 'rspec/core/rake_task' 8 | 9 | RSpec::Core::RakeTask.new(:spec) 10 | 11 | task :default => :spec 12 | rescue LoadError 13 | # no rspec available 14 | end 15 | -------------------------------------------------------------------------------- /example/scripts/test.groovy: -------------------------------------------------------------------------------- 1 | what 2 | -------------------------------------------------------------------------------- /example/test.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require 'pp' 4 | require 'gremlin_client' 5 | 6 | 7 | conn = GremlinClient::Connection.new(gremlin_script_path: 'example/scripts') 8 | pp conn.send_file('test.groovy', {what: 10}) 9 | -------------------------------------------------------------------------------- /gremlin_client.gemspec: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | lib = File.expand_path('../lib', __FILE__) 3 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 4 | 5 | require 'gremlin_client/version' 6 | 7 | Gem::Specification.new do |spec| 8 | spec.name = "gremlin_client" 9 | spec.version = GremliClient::VERSION 10 | spec.authors = ["Marcelo Coraça de Freitas"] 11 | spec.email = ["marcelo.freitas@finc.com"] 12 | spec.summary = %q{Simple Gremlin server client for the WebSocketChannelizer} 13 | spec.homepage = %q{https://github.com/marcelocf/gremlin_client} 14 | spec.license = "Apache-2.0" 15 | 16 | spec.files = `git ls-files`.split($/) 17 | spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) } 18 | spec.test_files = spec.files.grep(%r{^(spec)/}) 19 | spec.rdoc_options = ["--charset=UTF-8"] 20 | spec.require_paths = ["lib"] 21 | 22 | spec.add_dependency 'websocket-client-simple', '~> 0.3' 23 | spec.add_dependency 'oj', '~> 2.16' 24 | 25 | spec.add_development_dependency 'bundler', '~> 1.13' 26 | spec.add_development_dependency 'rake', '~> 13.0' 27 | spec.add_development_dependency 'rspec', '~> 3.5' 28 | spec.add_development_dependency 'guard-rspec', '~> 4.7' 29 | spec.add_development_dependency 'rubocop', '~> 0.49' 30 | spec.add_development_dependency 'coveralls', '~> 0.8' 31 | end 32 | -------------------------------------------------------------------------------- /lib/gremlin_client.rb: -------------------------------------------------------------------------------- 1 | # Module encapsulating our code 2 | 3 | require 'json' 4 | require 'pathname' 5 | require 'websocket-client-simple' 6 | 7 | require 'gremlin_client/server_error' 8 | require 'gremlin_client/connection_timeout_error' 9 | require 'gremlin_client/execution_timeout_error' 10 | require 'gremlin_client/connection' 11 | module GremlinClient 12 | end 13 | -------------------------------------------------------------------------------- /lib/gremlin_client/connection.rb: -------------------------------------------------------------------------------- 1 | require 'oj' 2 | module GremlinClient 3 | # represents the connection to our gremlin server 4 | class Connection 5 | 6 | attr_reader :connection_timeout, :timeout, :gremlin_script_path 7 | 8 | STATUS = { 9 | success: 200, 10 | no_content: 204, 11 | partial_content: 206, 12 | 13 | unauthorized: 401, 14 | authenticate: 407, 15 | malformed_request: 498, 16 | invalid_request_arguments: 499, 17 | server_error: 500, 18 | script_evaluation_error: 597, 19 | server_timeout: 598, 20 | server_serialization_error: 599 21 | } 22 | 23 | class << self 24 | # a centralized place for you to store a connection pool of those objects 25 | # recommendeded one is: https://github.com/mperham/connection_pool 26 | attr_accessor :pool 27 | end 28 | 29 | # initialize a new connection using: 30 | # host => hostname/ip where to connect 31 | # port => listen port of the server 32 | # timeout => how long the client might wait for response from the server 33 | def initialize( 34 | host: 'localhost', 35 | port: 8182, 36 | path: '/', 37 | connection_timeout: 1, 38 | timeout: 10, 39 | gremlin_script_path: '.', 40 | autoconnect: true 41 | ) 42 | @host = host 43 | @port = port 44 | @path = path 45 | @connection_timeout = connection_timeout 46 | @timeout = timeout 47 | @gremlin_script_path = gremlin_script_path 48 | @gremlin_script_path = Pathname.new(@gremlin_script_path) unless @gremlin_script_path.is_a?(Pathname) 49 | @autoconnect = autoconnect 50 | connect if @autoconnect 51 | end 52 | 53 | # creates a new connection object 54 | def connect 55 | gremlin = self 56 | WebSocket::Client::Simple.connect("ws://#{@host}:#{@port}#{@path}") do |ws| 57 | @ws = ws 58 | 59 | @ws.on :message do |msg| 60 | gremlin.receive_message(msg) 61 | end 62 | 63 | @ws.on :error do |e| 64 | gremlin.receive_error(e) 65 | end 66 | end 67 | end 68 | 69 | def reconnect 70 | @ws.close unless @ws.nil? 71 | connect 72 | end 73 | 74 | def send_query(command, bindings={}) 75 | wait_connection 76 | reset_request 77 | @ws.send(build_message(command, bindings), { type: 'text' }) 78 | wait_response 79 | return treat_response 80 | end 81 | 82 | def send_file(filename, bindings={}) 83 | send_query(IO.read(resolve_path(filename)), bindings) 84 | end 85 | 86 | def open? 87 | @ws.open? 88 | rescue ::NoMethodError 89 | # #2 => it appears to happen in some situations when the situation is dropped 90 | return false 91 | end 92 | 93 | def close 94 | @ws.close 95 | end 96 | 97 | 98 | # this has to be public so the websocket client thread sees it 99 | def receive_message(msg) 100 | response = Oj.load(msg.data) 101 | # this check is important in case a request timeout and we make new ones after 102 | if response['requestId'] == @request_id 103 | if @response.nil? 104 | @response = response 105 | else 106 | @response['result']['data'].concat response['result']['data'] 107 | @response['result']['meta'].merge! response['result']['meta'] 108 | @response['status'] = response['status'] 109 | end 110 | end 111 | end 112 | 113 | def receive_error(e) 114 | @error = e 115 | end 116 | 117 | protected 118 | 119 | def wait_connection(skip_reconnect = false) 120 | w_from = Time.now.to_i 121 | while !open? && Time.now.to_i - @connection_timeout < w_from 122 | sleep 0.001 123 | end 124 | unless open? 125 | # reconnection code 126 | if @autoconnect && !skip_reconnect 127 | reconnect 128 | return wait_connection(true) 129 | end 130 | fail ::GremlinClient::ConnectionTimeoutError.new(@connection_timeout) 131 | end 132 | end 133 | 134 | def reset_request 135 | @request_id= SecureRandom.uuid 136 | @started_at = Time.now.to_i 137 | @error = nil 138 | @response = nil 139 | end 140 | 141 | def is_finished? 142 | return true unless @error.nil? 143 | return false if @response.nil? 144 | return false if @response['status'].nil? 145 | return @response['status']['code'] != STATUS[:partial_content] 146 | end 147 | 148 | def wait_response 149 | while !is_finished? && (Time.now.to_i - @started_at < @timeout) 150 | sleep 0.001 151 | end 152 | 153 | fail ::GremlinClient::ServerError.new(nil, @error) unless @error.nil? 154 | fail ::GremlinClient::ExecutionTimeoutError.new(@timeout) if @response.nil? 155 | end 156 | 157 | # we validate our response here to make sure it is going to be 158 | # raising exceptions in the right thread 159 | def treat_response 160 | # note that the partial_content status should be processed differently. 161 | # look at http://tinkerpop.apache.org/docs/3.0.1-incubating/ for more info 162 | ok_status = [:success, :no_content, :partial_content].map { |st| STATUS[st] } 163 | unless ok_status.include?(@response['status']['code']) 164 | fail ::GremlinClient::ServerError.new(@response['status']['code'], @response['status']['message']) 165 | end 166 | @response['result'] 167 | end 168 | 169 | def build_message(command, bindings) 170 | message = { 171 | requestId: @request_id, 172 | op: 'eval', 173 | processor: '', 174 | args: { 175 | gremlin: command, 176 | bindings: bindings, 177 | language: 'gremlin-groovy' 178 | } 179 | } 180 | Oj.dump(message, mode: :compat) 181 | end 182 | 183 | def resolve_path(filename) 184 | return filename if filename.is_a?(String) && filename[0,1] == '/' 185 | @gremlin_script_path.join(filename).to_s 186 | end 187 | end 188 | end 189 | -------------------------------------------------------------------------------- /lib/gremlin_client/connection_timeout_error.rb: -------------------------------------------------------------------------------- 1 | 2 | module GremlinClient 3 | # to track error of timeout while waiting for connection 4 | class ConnectionTimeoutError < StandardError 5 | attr_reader :timeout 6 | def initialize(timeout) 7 | @timeout = timeout 8 | end 9 | def to_s 10 | "#{@timeout}s" 11 | end 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /lib/gremlin_client/execution_timeout_error.rb: -------------------------------------------------------------------------------- 1 | 2 | module GremlinClient 3 | # to track error of timeout executing a specific query 4 | class ExecutionTimeoutError < StandardError 5 | attr_reader :timeout 6 | def initialize(timeout) 7 | @timeout = timeout 8 | end 9 | def to_s 10 | "#{@timeout}s" 11 | end 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /lib/gremlin_client/server_error.rb: -------------------------------------------------------------------------------- 1 | 2 | module GremlinClient 3 | # To process error messages coming from the server 4 | class ServerError < StandardError 5 | attr_reader :message, :code 6 | def initialize(code, message) 7 | @code = code 8 | @message = message 9 | end 10 | 11 | def to_s 12 | "#{@message} (code #{@code})" 13 | end 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /lib/gremlin_client/version.rb: -------------------------------------------------------------------------------- 1 | module GremliClient 2 | VERSION = "0.1.6" 3 | end 4 | -------------------------------------------------------------------------------- /spec/connection_spec.rb: -------------------------------------------------------------------------------- 1 | 2 | # frozen_string_literal: true 3 | 4 | require 'spec_helper' 5 | 6 | # Tests on the freetext feature 7 | RSpec.describe :connection do 8 | class MockedSocket 9 | def close 10 | nil 11 | end 12 | end 13 | 14 | module Message 15 | def self.status=(name) 16 | @status_code = GremlinClient::Connection::STATUS[name] 17 | end 18 | 19 | def self.request_id=(requestId) 20 | @request_id = requestId 21 | end 22 | 23 | def self.called=(c) 24 | @called = c 25 | end 26 | 27 | def self.data 28 | @called ||= 0 29 | @called += 1 30 | #rid = ", \"requestId\" : \"#{@request_id}\"" unless @request_id.nil? 31 | #stt = ", \"status\" : { \"code\" : #{@status_code} }" unless @status_code.nil? 32 | #"{\'{\"example\" : \"data #{@called}\"#{rid}#{stt}}" 33 | { 34 | requestId: @request_id, 35 | status: { code: @status_code }, 36 | result: { 37 | data: [@called], 38 | meta: {}, 39 | }, 40 | }.to_json 41 | end 42 | end 43 | 44 | before do 45 | sock = MockedSocket.new 46 | allow(sock).to receive(:on).and_yield(Message) 47 | allow(WebSocket::Client::Simple).to receive(:connect).and_yield(sock) 48 | end 49 | 50 | describe :initialize do 51 | it :websocket do 52 | expect(WebSocket::Client::Simple).to receive(:connect).with('ws://localhost:8182/') 53 | conn = GremlinClient::Connection.new 54 | end 55 | 56 | it :websocket do 57 | expect(WebSocket::Client::Simple).to receive(:connect).with('ws://SERVER_A:123/path/to/gremlin') 58 | conn = GremlinClient::Connection.new(host: :SERVER_A, port: 123, path: '/path/to/gremlin') 59 | end 60 | 61 | it :gremlin_script_path do 62 | conn = GremlinClient::Connection.new 63 | expect(conn.gremlin_script_path).to eq(Pathname.new('.')) 64 | conn = GremlinClient::Connection.new(gremlin_script_path: '/etc/groovy') 65 | expect(conn.gremlin_script_path).to eq(Pathname.new('/etc/groovy')) 66 | end 67 | 68 | it :connection_timeout do 69 | conn = GremlinClient::Connection.new 70 | expect(conn.connection_timeout).to eq(1) 71 | conn = GremlinClient::Connection.new(connection_timeout: 11) 72 | expect(conn.connection_timeout).to eq(11) 73 | end 74 | 75 | it :timeout do 76 | conn = GremlinClient::Connection.new 77 | expect(conn.timeout).to eq(10) 78 | conn = GremlinClient::Connection.new(timeout: 1) 79 | expect(conn.timeout).to eq(1) 80 | end 81 | 82 | 83 | it :socket_listeners do 84 | Message.called = 0 85 | conn = GremlinClient::Connection.new 86 | expect(conn.instance_variable_get('@response')['result']['data']).to eq([1]) 87 | expect(conn.instance_variable_get('@error').data).to eq( 88 | "{\"requestId\":null,\"status\":{\"code\":null},\"result\":{\"data\":[2],\"meta\":{}}}" 89 | ) 90 | end 91 | end 92 | 93 | 94 | describe :send do 95 | it :string do 96 | conn = GremlinClient::Connection.new 97 | sock = conn.instance_variable_get('@ws') 98 | expect(conn).to receive(:wait_connection) 99 | expect(conn).to receive(:reset_request) 100 | expect(conn).to receive(:build_message).with(:query, :bindings).and_return(:my_message) 101 | expect(sock).to receive(:send).with(:my_message, { type: 'text' }) 102 | expect(conn).to receive(:wait_response) 103 | expect(conn).to receive(:treat_response) 104 | 105 | conn.send_query(:query, :bindings) 106 | end 107 | 108 | it :file do 109 | conn = GremlinClient::Connection.new 110 | expect(IO).to receive(:read).with('filename').and_return(:file_contents) 111 | expect(conn).to receive(:send_query).with(:file_contents, :bindings) 112 | conn.send_file('filename', :bindings) 113 | end 114 | end 115 | 116 | 117 | it :open? do 118 | conn = GremlinClient::Connection.new 119 | expect(conn.instance_variable_get('@ws')).to receive(:open?).and_return(:from_websocket) 120 | expect(conn.open?).to eq(:from_websocket) 121 | end 122 | 123 | it :close do 124 | conn = GremlinClient::Connection.new 125 | expect(conn.instance_variable_get('@ws')).to receive(:close).and_return(:from_websocket) 126 | expect(conn.close).to eq(:from_websocket) 127 | end 128 | 129 | describe :receive_message do 130 | it :no_request_id do 131 | Message.called = 0 132 | Message.request_id = nil 133 | conn = GremlinClient::Connection.new 134 | conn.send(:reset_request) 135 | conn.receive_message(Message) 136 | expect(conn.instance_variable_get('@response')).to be_nil 137 | end 138 | 139 | it :different_request_id do 140 | Message.called = 0 141 | Message.request_id = '123' 142 | conn = GremlinClient::Connection.new 143 | conn.send(:reset_request) 144 | conn.instance_variable_set('@request_id', '123') 145 | conn.receive_message(Message) 146 | expect(conn.instance_variable_get('@response')).to eq({ 147 | 'requestId' => '123', 148 | 'status' => { 'code' => nil }, 149 | 'result' => { 'data' => [2], 'meta' => {} } 150 | }) 151 | # exit this block reseting this value 152 | Message.request_id = nil 153 | end 154 | 155 | it :partial_message do 156 | Message.called = 0 157 | 158 | conn = GremlinClient::Connection.new 159 | conn.send(:reset_request) 160 | Message.request_id = conn.instance_variable_get('@request_id') 161 | Message.status = :partial_content 162 | conn.receive_message(Message) 163 | expect(conn.send('is_finished?')).to be false 164 | Message.status = :success 165 | conn.receive_message(Message) 166 | expect(conn.send('is_finished?')).to be true 167 | expect(conn.instance_variable_get('@response')).to eq({ 168 | 'requestId' => conn.instance_variable_get('@request_id'), 169 | 'status' => { 'code' => 200 }, 170 | # not 1, 2 because it is called once uppon init 171 | 'result' => { 'data' => [2, 3], 'meta' => {} } 172 | }) 173 | # exit this block reseting this value 174 | Message.request_id = nil 175 | # clear the status 176 | Message.status = nil 177 | end 178 | end 179 | 180 | it :receive_error do 181 | conn = GremlinClient::Connection.new 182 | conn.receive_error(:this_is_a_bad_error) 183 | expect(conn.instance_variable_get('@error')).to eq(:this_is_a_bad_error) 184 | end 185 | 186 | describe :wait_connection do 187 | it :timeouts do 188 | conn = GremlinClient::Connection.new 189 | expect(conn).to receive(:open?).and_return(false).at_least(:once) 190 | expect{conn.send(:wait_connection)}.to raise_exception(::GremlinClient::ConnectionTimeoutError) 191 | end 192 | 193 | it :success do 194 | conn = GremlinClient::Connection.new 195 | expect(conn).to receive(:open?).and_return(true).twice 196 | conn.send(:wait_connection) 197 | end 198 | 199 | it :fails_with_longer_timeout do 200 | conn = GremlinClient::Connection.new(connection_timeout: 3, autoconnect: false) 201 | conn.connect 202 | started_at = Time.now.to_i 203 | expect(conn).to receive(:open?).and_return(false).at_least(:once) 204 | expect{conn.send(:wait_connection)}.to raise_exception(::GremlinClient::ConnectionTimeoutError) 205 | expect(Time.now.to_i - started_at).to be_within(1).of(3) 206 | end 207 | 208 | it :fails_with_autonnect do 209 | conn = GremlinClient::Connection.new(connection_timeout: 2, autoconnect: true) 210 | started_at = Time.now.to_i 211 | expect(conn).to receive(:open?).and_return(false).at_least(:once) 212 | expect{conn.send(:wait_connection)}.to raise_exception(::GremlinClient::ConnectionTimeoutError) 213 | expect(Time.now.to_i - started_at).to be_within(1).of(4) 214 | end 215 | end 216 | 217 | 218 | it :reset_request do 219 | conn = GremlinClient::Connection.new 220 | conn.instance_variable_set('@request_id', :old_id) 221 | conn.instance_variable_set('@started_at', :old_started_at) 222 | conn.instance_variable_set('@error', :old_error) 223 | conn.instance_variable_set('@response', :old_response) 224 | 225 | conn.send(:reset_request) 226 | 227 | expect(conn.instance_variable_get('@request_id')).not_to eq(:old_id) 228 | expect(conn.instance_variable_get('@request_id').length).to be(36) # uuid is 36 chars long 229 | expect(conn.instance_variable_get('@started_at')).to be_within(1).of(Time.now.to_i) 230 | expect(conn.instance_variable_get('@error')).to be_nil 231 | expect(conn.instance_variable_get('@response')).to be_nil 232 | end 233 | 234 | 235 | describe :wait_response do 236 | it :no_message do 237 | conn = GremlinClient::Connection.new(timeout: 1) 238 | conn.send(:reset_request) 239 | expect{conn.send(:wait_response)}.to raise_exception(::GremlinClient::ExecutionTimeoutError) 240 | end 241 | 242 | it :wrong_id_message do 243 | conn = GremlinClient::Connection.new(timeout: 1) 244 | conn.send(:reset_request) 245 | Message.request_id = :invalid_id 246 | conn.receive_message(Message) 247 | expect{conn.send(:wait_response)}.to raise_exception(::GremlinClient::ExecutionTimeoutError) 248 | end 249 | 250 | it :with_message do 251 | conn = GremlinClient::Connection.new(timeout: 1) 252 | conn.send(:reset_request) 253 | Message.called = 0 254 | Message.request_id = conn.instance_variable_get('@request_id') 255 | conn.receive_message(Message) 256 | conn.send(:wait_response) 257 | expect(conn.instance_variable_get('@response')).to eq({ 258 | 'requestId' => conn.instance_variable_get('@request_id'), 259 | 'status' => { 'code' => nil } , 260 | 'result' => { 'data' => [1], 'meta' => {} } 261 | }) 262 | end 263 | 264 | it :with_error do 265 | conn = GremlinClient::Connection.new(timeout: 1) 266 | conn.send(:reset_request) 267 | Message.called = 0 268 | Message.request_id = conn.instance_variable_get('@request_id') 269 | conn.receive_error(Message) 270 | expect{conn.send(:wait_response)}.to raise_exception(::GremlinClient::ServerError) 271 | end 272 | end 273 | 274 | describe :treat_response do 275 | it :success_statuses do 276 | def test_status(name) 277 | conn = GremlinClient::Connection.new 278 | conn.send(:reset_request) 279 | Message.request_id = conn.instance_variable_get('@request_id') 280 | Message.status = name 281 | conn.receive_message(Message) 282 | conn.send(:treat_response) 283 | end 284 | test_status(:success) 285 | test_status(:no_content) 286 | test_status(:partial_content) 287 | end 288 | 289 | it :error_statuses do 290 | def test_status(name) 291 | conn = GremlinClient::Connection.new 292 | conn.send(:reset_request) 293 | Message.request_id = conn.instance_variable_get('@request_id') 294 | Message.status = name 295 | conn.receive_message(Message) 296 | expect{conn.send(:treat_response)}.to raise_exception(::GremlinClient::ServerError) 297 | end 298 | 299 | [ 300 | :unauthorized, 301 | :authenticate, 302 | :malformed_request, 303 | :invalid_request_arguments, 304 | :server_error, 305 | :script_evaluation_error, 306 | :server_timeout, 307 | :server_serialization_error 308 | ].each { |name| test_status(name) } 309 | end 310 | end 311 | 312 | it :build_message do 313 | conn = GremlinClient::Connection.new 314 | conn.send(:reset_request) 315 | expect(JSON.parse(conn.send(:build_message, :query, :bindings))).to eq({ 316 | 'requestId' => conn.instance_variable_get('@request_id'), 317 | 'op' => 'eval', 318 | 'processor' => '', 319 | 'args' => { 320 | 'gremlin' => 'query', 321 | 'bindings' => 'bindings', 322 | 'language' => 'gremlin-groovy' 323 | } 324 | }) 325 | end 326 | 327 | 328 | it :resolve_path do 329 | conn = GremlinClient::Connection.new 330 | expect(conn.send(:resolve_path, 'test')).to eq('test') 331 | 332 | conn = GremlinClient::Connection.new(gremlin_script_path: '/test/path') 333 | expect(conn.send(:resolve_path, 'test')).to eq('/test/path/test') 334 | 335 | 336 | conn = GremlinClient::Connection.new(gremlin_script_path: 'test/path') 337 | expect(conn.send(:resolve_path, 'test')).to eq('test/path/test') 338 | end 339 | end 340 | -------------------------------------------------------------------------------- /spec/exceptions_spec.rb: -------------------------------------------------------------------------------- 1 | 2 | # frozen_string_literal: true 3 | 4 | require 'spec_helper' 5 | 6 | # test on formating messages from exceptions 7 | RSpec.describe :exceptions do 8 | it :connection_timeout_error do 9 | expect(::GremlinClient::ConnectionTimeoutError.new(10).to_s).to eq('10s') 10 | expect(::GremlinClient::ConnectionTimeoutError.new(1).to_s).to eq('1s') 11 | expect(::GremlinClient::ConnectionTimeoutError.new(1123).to_s).to eq('1123s') 12 | end 13 | 14 | it :execution_timeout_error do 15 | expect(::GremlinClient::ExecutionTimeoutError.new(10).to_s).to eq('10s') 16 | expect(::GremlinClient::ExecutionTimeoutError.new(1).to_s).to eq('1s') 17 | expect(::GremlinClient::ExecutionTimeoutError.new(1123).to_s).to eq('1123s') 18 | end 19 | 20 | it :server_error do 21 | expect(::GremlinClient::ServerError.new(:code, :message).to_s).to eq('message (code code)'); 22 | expect(::GremlinClient::ServerError.new(312, 'this exploded here').to_s).to eq('this exploded here (code 312)'); 23 | end 24 | end 25 | 26 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | require 'coveralls' 2 | require 'pp' 3 | Coveralls.wear! 4 | require 'gremlin_client' 5 | -------------------------------------------------------------------------------- /spec/status_codes_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'spec_helper' 4 | 5 | # Tests on the freetext feature 6 | RSpec.describe :status_codes do 7 | it :declared_every_code do 8 | { 9 | success: 200, 10 | no_content: 204, 11 | partial_content: 206, 12 | unauthorized: 401, 13 | authenticate: 407, 14 | malformed_request: 498, 15 | invalid_request_arguments: 499, 16 | server_error: 500, 17 | script_evaluation_error: 597, 18 | server_timeout: 598, 19 | server_serialization_error: 599 20 | }.each_pair do |key, code| 21 | expect(GremlinClient::Connection::STATUS[key]).to eq(code) 22 | end 23 | end 24 | 25 | it :doesnt_have_extra_codes do 26 | expect(GremlinClient::Connection::STATUS.count).to be(11) 27 | end 28 | end 29 | --------------------------------------------------------------------------------