keymaker 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/Gemfile +4 -0
- data/LICENSE.md +22 -0
- data/README.md +86 -0
- data/Rakefile +123 -0
- data/keymaker.gemspec +94 -0
- data/keymaker_integration_spec.rb +182 -0
- data/lib/keymaker.rb +43 -0
- data/lib/keymaker/add_node_to_index_request.rb +9 -0
- data/lib/keymaker/batch_get_nodes_request.rb +19 -0
- data/lib/keymaker/configuration.rb +90 -0
- data/lib/keymaker/create_node_request.rb +11 -0
- data/lib/keymaker/create_relationship_request.rb +26 -0
- data/lib/keymaker/delete_relationship_request.rb +17 -0
- data/lib/keymaker/execute_cypher_request.rb +9 -0
- data/lib/keymaker/execute_gremlin_request.rb +9 -0
- data/lib/keymaker/indexing.rb +34 -0
- data/lib/keymaker/node.rb +111 -0
- data/lib/keymaker/path_traverse_request.rb +34 -0
- data/lib/keymaker/remove_node_from_index_request.rb +9 -0
- data/lib/keymaker/request.rb +34 -0
- data/lib/keymaker/response.rb +38 -0
- data/lib/keymaker/serialization.rb +46 -0
- data/lib/keymaker/service.rb +147 -0
- data/lib/keymaker/update_node_properties_request.rb +15 -0
- data/spec/lib/keymaker_integration_spec.rb +189 -0
- data/spec/spec_helper.rb +7 -0
- data/spec/support/keymaker.rb +106 -0
- metadata +194 -0
@@ -0,0 +1,38 @@
|
|
1
|
+
module Keymaker
|
2
|
+
|
3
|
+
class Response
|
4
|
+
|
5
|
+
attr_accessor :request
|
6
|
+
attr_accessor :service
|
7
|
+
attr_accessor :faraday_response
|
8
|
+
|
9
|
+
def initialize(service, faraday_response)
|
10
|
+
self.service = service
|
11
|
+
self.faraday_response = faraday_response
|
12
|
+
end
|
13
|
+
|
14
|
+
def body
|
15
|
+
faraday_response.body || {}
|
16
|
+
end
|
17
|
+
|
18
|
+
def status
|
19
|
+
faraday_response.status
|
20
|
+
end
|
21
|
+
|
22
|
+
def neo4j_id
|
23
|
+
body["self"] && body["self"][/\d+$/].to_i
|
24
|
+
end
|
25
|
+
|
26
|
+
def on_success
|
27
|
+
if success?
|
28
|
+
yield self
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def success?
|
33
|
+
(200..207).include?(status)
|
34
|
+
end
|
35
|
+
|
36
|
+
end
|
37
|
+
|
38
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
module Keymaker::Serialization
|
2
|
+
include ActiveModel::Serialization
|
3
|
+
|
4
|
+
def self.included(base)
|
5
|
+
base.define_model_callbacks :save, :create
|
6
|
+
end
|
7
|
+
|
8
|
+
COERCION_PROCS = Hash.new(->(v){v}).tap do |procs|
|
9
|
+
procs[Integer] = ->(v){ v.to_i }
|
10
|
+
procs[DateTime] = ->(v) do
|
11
|
+
case v
|
12
|
+
when Time
|
13
|
+
Time.at(v)
|
14
|
+
when String
|
15
|
+
DateTime.strptime(v).to_time
|
16
|
+
else
|
17
|
+
Time.now.utc
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def process_attrs(attrs)
|
23
|
+
attrs.symbolize_keys!
|
24
|
+
self.class.properties.delete_if{|p| p == :node_id}.each do |property|
|
25
|
+
if property == :active_record_id
|
26
|
+
process_attr(property, attrs[:id].present? ? attrs[:id] : attrs[:active_record_id])
|
27
|
+
else
|
28
|
+
process_attr(property, attrs[property])
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def process_attr(key, value)
|
34
|
+
send("#{key}=", coerce(value,self.class.property_traits[key]))
|
35
|
+
end
|
36
|
+
|
37
|
+
def coerce(value,type)
|
38
|
+
COERCION_PROCS[type].call(value)
|
39
|
+
end
|
40
|
+
|
41
|
+
def attributes
|
42
|
+
Hash.new{|h,k| h[k] = send(k) }.tap do |hash|
|
43
|
+
self.class.properties.each{|property| hash[property.to_s] }
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
@@ -0,0 +1,147 @@
|
|
1
|
+
require "addressable/uri"
|
2
|
+
|
3
|
+
module Keymaker
|
4
|
+
|
5
|
+
class Service
|
6
|
+
|
7
|
+
attr_accessor :config
|
8
|
+
|
9
|
+
def initialize(config)
|
10
|
+
self.config = config
|
11
|
+
end
|
12
|
+
|
13
|
+
def connection=(connection)
|
14
|
+
@connection = connection
|
15
|
+
end
|
16
|
+
|
17
|
+
def connection
|
18
|
+
@connection ||= Faraday.new(url: config.service_root) do |conn|
|
19
|
+
conn.request :json
|
20
|
+
conn.use FaradayMiddleware::ParseJson, content_type: /\bjson$/
|
21
|
+
conn.adapter :net_http
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
# Create Node
|
26
|
+
def create_node(attrs)
|
27
|
+
create_node_request(attrs)
|
28
|
+
end
|
29
|
+
|
30
|
+
def create_node_request(opts)
|
31
|
+
CreateNodeRequest.new(self, opts).submit
|
32
|
+
end
|
33
|
+
|
34
|
+
# Update Node properties
|
35
|
+
def update_node_properties(node_id, attrs)
|
36
|
+
update_node_properties_request({node_id: node_id}.merge(attrs))
|
37
|
+
end
|
38
|
+
|
39
|
+
def update_node_properties_request(opts)
|
40
|
+
UpdateNodePropertiesRequest.new(self, opts).submit
|
41
|
+
end
|
42
|
+
|
43
|
+
# Create Relationship
|
44
|
+
def create_relationship(rel_type, start_node_id, end_node_id, data={})
|
45
|
+
create_relationship_request({node_id: start_node_id, rel_type: rel_type, end_node_id: end_node_id, data: data})
|
46
|
+
end
|
47
|
+
|
48
|
+
def create_relationship_request(opts)
|
49
|
+
CreateRelationshipRequest.new(self, opts).submit
|
50
|
+
end
|
51
|
+
|
52
|
+
# Delete Relationship
|
53
|
+
def delete_relationship(relationship_id)
|
54
|
+
delete_relationship_request(relationship_id: relationship_id)
|
55
|
+
end
|
56
|
+
|
57
|
+
def delete_relationship_request(opts)
|
58
|
+
DeleteRelationshipRequest.new(self, opts).submit
|
59
|
+
end
|
60
|
+
|
61
|
+
# Add Node to Index
|
62
|
+
def add_node_to_index(index_name, key, value, node_id)
|
63
|
+
add_node_to_index_request(index_name: index_name, key: key, value: value, node_id: node_id)
|
64
|
+
end
|
65
|
+
|
66
|
+
def add_node_to_index_request(opts)
|
67
|
+
AddNodeToIndexRequest.new(self, opts).submit
|
68
|
+
end
|
69
|
+
|
70
|
+
# Remove Node from Index
|
71
|
+
def remove_node_from_index(index_name, key, value, node_id)
|
72
|
+
remove_node_from_index_request(index_name: index_name, key: key, value: value, node_id: node_id)
|
73
|
+
end
|
74
|
+
|
75
|
+
def remove_node_from_index_request(opts)
|
76
|
+
RemoveNodeFromIndexRequest.new(self, opts).submit
|
77
|
+
end
|
78
|
+
|
79
|
+
# Path Traverse
|
80
|
+
def path_traverse(start_node_id, data={})
|
81
|
+
path_traverse_request({node_id: start_node_id}.merge(data))
|
82
|
+
end
|
83
|
+
|
84
|
+
def path_traverse_request(opts)
|
85
|
+
PathTraverseRequest.new(self, opts).submit
|
86
|
+
end
|
87
|
+
|
88
|
+
# Batch
|
89
|
+
## GET Nodes
|
90
|
+
def batch_get_nodes(node_ids)
|
91
|
+
batch_get_nodes_request(node_ids)
|
92
|
+
end
|
93
|
+
|
94
|
+
def batch_get_nodes_request(opts)
|
95
|
+
BatchGetNodesRequest.new(self, opts).submit
|
96
|
+
end
|
97
|
+
|
98
|
+
# Cypher Query
|
99
|
+
def execute_query(query, params)
|
100
|
+
execute_cypher_request({query: query, params: params})
|
101
|
+
end
|
102
|
+
|
103
|
+
def execute_cypher_request(opts)
|
104
|
+
ExecuteCypherRequest.new(self, opts).submit
|
105
|
+
end
|
106
|
+
|
107
|
+
# Gremlin Script
|
108
|
+
def execute_script(script, params={})
|
109
|
+
execute_gremlin_request({script: script, params: params})
|
110
|
+
end
|
111
|
+
|
112
|
+
def execute_gremlin_request(opts)
|
113
|
+
ExecuteGremlinRequest.new(self, opts).submit
|
114
|
+
end
|
115
|
+
|
116
|
+
# HTTP Verbs
|
117
|
+
|
118
|
+
def get(url, body)
|
119
|
+
faraday_response = connection.get(parse_url(url), body)
|
120
|
+
Keymaker::Response.new(self, faraday_response)
|
121
|
+
end
|
122
|
+
|
123
|
+
def delete(url)
|
124
|
+
faraday_response = connection.delete(parse_url(url))
|
125
|
+
Keymaker::Response.new(self, faraday_response)
|
126
|
+
end
|
127
|
+
|
128
|
+
def post(url, body)
|
129
|
+
faraday_response = connection.post(parse_url(url), body)
|
130
|
+
Keymaker::Response.new(self, faraday_response)
|
131
|
+
end
|
132
|
+
|
133
|
+
def put(url, body)
|
134
|
+
faraday_response = connection.put(parse_url(url), body)
|
135
|
+
Keymaker::Response.new(self, faraday_response)
|
136
|
+
end
|
137
|
+
|
138
|
+
def parse_url(url)
|
139
|
+
connection.build_url(url).tap do |uri|
|
140
|
+
if uri.port != config.port
|
141
|
+
raise RuntimeError, "bad port"
|
142
|
+
end
|
143
|
+
end
|
144
|
+
end
|
145
|
+
|
146
|
+
end
|
147
|
+
end
|
@@ -0,0 +1,189 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require "addressable/uri"
|
3
|
+
require 'keymaker'
|
4
|
+
|
5
|
+
describe Keymaker do
|
6
|
+
|
7
|
+
include_context "John and Sarah nodes"
|
8
|
+
|
9
|
+
context "indices" do
|
10
|
+
include_context "John and Sarah indexed nodes"
|
11
|
+
|
12
|
+
context "given a bad port number" do
|
13
|
+
|
14
|
+
let(:url) { john_node_url.dup.gsub("7475", "49152") }
|
15
|
+
|
16
|
+
after { service.connection = connection }
|
17
|
+
|
18
|
+
def do_it
|
19
|
+
connection.get(url) do |req|
|
20
|
+
req.options[:timeout] = 0
|
21
|
+
req.options[:open_timeout] = 0
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
it "raises an error" do
|
26
|
+
expect { do_it }.to raise_error
|
27
|
+
end
|
28
|
+
|
29
|
+
end
|
30
|
+
|
31
|
+
context "given an explicit connection" do
|
32
|
+
|
33
|
+
let(:url) { john_node_url }
|
34
|
+
|
35
|
+
before { service.connection = test_connection }
|
36
|
+
after { service.connection = connection }
|
37
|
+
|
38
|
+
def do_it
|
39
|
+
service.connection.get(url)
|
40
|
+
end
|
41
|
+
|
42
|
+
it "uses the connection for requests" do
|
43
|
+
faraday_stubs.get(Addressable::URI.parse(url).path) do
|
44
|
+
[200, {}, "{}"]
|
45
|
+
end
|
46
|
+
do_it
|
47
|
+
faraday_stubs.verify_stubbed_calls
|
48
|
+
end
|
49
|
+
|
50
|
+
end
|
51
|
+
|
52
|
+
describe "#add_node_to_index(index_name, key, value, node_id)" do
|
53
|
+
|
54
|
+
def do_it
|
55
|
+
service.add_node_to_index(:users, :email, email, node_id)
|
56
|
+
end
|
57
|
+
|
58
|
+
context "given existing values" do
|
59
|
+
|
60
|
+
let(:email) { john_email }
|
61
|
+
let(:node_id) { john_node_id }
|
62
|
+
let(:index_result) { connection.get(index_query_for_john_url).body[0]["self"] }
|
63
|
+
|
64
|
+
it "adds the node to the index" do
|
65
|
+
do_it
|
66
|
+
index_result.should == john_node_url
|
67
|
+
end
|
68
|
+
|
69
|
+
it "returns a status of 201" do
|
70
|
+
do_it.status.should == 201
|
71
|
+
end
|
72
|
+
|
73
|
+
end
|
74
|
+
|
75
|
+
context "given an invalid node id" do
|
76
|
+
|
77
|
+
let(:email) { john_email }
|
78
|
+
let(:node_id) { -22 }
|
79
|
+
|
80
|
+
it "returns a 500 status" do
|
81
|
+
do_it.status.should == 500
|
82
|
+
end
|
83
|
+
|
84
|
+
end
|
85
|
+
|
86
|
+
end
|
87
|
+
|
88
|
+
describe "#remove_node_from_index(index_name, key, value, node_id)" do
|
89
|
+
|
90
|
+
def do_it
|
91
|
+
service.remove_node_from_index(:users, :email, email, node_id)
|
92
|
+
end
|
93
|
+
|
94
|
+
context "given existing values" do
|
95
|
+
|
96
|
+
let(:email) { john_email }
|
97
|
+
let(:node_id) { john_node_id }
|
98
|
+
|
99
|
+
it "removes the node from the index" do
|
100
|
+
do_it
|
101
|
+
connection.get(index_query_for_john_url).body.should be_empty
|
102
|
+
end
|
103
|
+
|
104
|
+
it "returns a status of 204" do
|
105
|
+
do_it.status.should == 204
|
106
|
+
end
|
107
|
+
|
108
|
+
it "keeps the other node indices" do
|
109
|
+
do_it
|
110
|
+
connection.get(index_query_for_sarah_url).body.should_not be_empty
|
111
|
+
end
|
112
|
+
|
113
|
+
end
|
114
|
+
|
115
|
+
context "given unmatched values" do
|
116
|
+
|
117
|
+
let(:email) { "unknown@example.com" }
|
118
|
+
let(:node_id) { -22 }
|
119
|
+
|
120
|
+
it "returns a 404 status" do
|
121
|
+
do_it.status.should == 404
|
122
|
+
end
|
123
|
+
|
124
|
+
end
|
125
|
+
|
126
|
+
end
|
127
|
+
end
|
128
|
+
|
129
|
+
describe "#execute_query" do
|
130
|
+
|
131
|
+
def do_it
|
132
|
+
service.execute_query("START user=node:users(email={email}) RETURN user", email: john_email)
|
133
|
+
end
|
134
|
+
|
135
|
+
context "given existing values" do
|
136
|
+
|
137
|
+
before { service.add_node_to_index(:users, :email, john_email, john_node_id) }
|
138
|
+
let(:query_result) { do_it["data"][0][0]["self"] }
|
139
|
+
|
140
|
+
it "performs the cypher query and responds" do
|
141
|
+
query_result.should == john_node_url
|
142
|
+
end
|
143
|
+
|
144
|
+
end
|
145
|
+
|
146
|
+
end
|
147
|
+
|
148
|
+
context "nodes" do
|
149
|
+
|
150
|
+
include_context "Keymaker connections"
|
151
|
+
|
152
|
+
describe "#create_node" do
|
153
|
+
|
154
|
+
let(:properties) { { first_name: "john", last_name: "connor", email: "john@resistance.net" } }
|
155
|
+
|
156
|
+
def do_it
|
157
|
+
new_node_id = service.create_node(properties).neo4j_id
|
158
|
+
connection.get("/db/data/node/#{new_node_id}/properties").body
|
159
|
+
end
|
160
|
+
|
161
|
+
it "creates a node with properties" do
|
162
|
+
do_it.should == {"first_name"=>"john", "email"=>"john@resistance.net", "last_name"=>"connor"}
|
163
|
+
end
|
164
|
+
|
165
|
+
end
|
166
|
+
|
167
|
+
end
|
168
|
+
|
169
|
+
context "relationships" do
|
170
|
+
|
171
|
+
include_context "Keymaker connections"
|
172
|
+
|
173
|
+
describe "#create_relationship" do
|
174
|
+
|
175
|
+
def do_it
|
176
|
+
service.create_relationship(:loves, john_node_id, sarah_node_id).neo4j_id
|
177
|
+
connection.get("/db/data/node/#{john_node_id}/relationships/all/loves").body.first
|
178
|
+
end
|
179
|
+
|
180
|
+
it "creates the relationship between the two nodes" do
|
181
|
+
do_it["start"].should == john_node_url
|
182
|
+
do_it["end"].should == sarah_node_url
|
183
|
+
end
|
184
|
+
|
185
|
+
end
|
186
|
+
|
187
|
+
end
|
188
|
+
|
189
|
+
end
|