keymaker 0.0.1
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.
- 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
|