wewoo 0.1.5
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 +7 -0
- data/.gitignore +21 -0
- data/Gemfile +6 -0
- data/Guardfile +11 -0
- data/LICENSE.txt +22 -0
- data/README.md +176 -0
- data/Rakefile +1 -0
- data/bin/wewoo +15 -0
- data/lib/wewoo.rb +19 -0
- data/lib/wewoo/adapter.rb +58 -0
- data/lib/wewoo/configuration.rb +24 -0
- data/lib/wewoo/edge.rb +54 -0
- data/lib/wewoo/element.rb +28 -0
- data/lib/wewoo/graph.rb +233 -0
- data/lib/wewoo/result_set.rb +99 -0
- data/lib/wewoo/version.rb +3 -0
- data/lib/wewoo/vertex.rb +63 -0
- data/spec/spec_helper.rb +14 -0
- data/spec/support/graph_sets.rb +33 -0
- data/spec/wewoo/edge_spec.rb +43 -0
- data/spec/wewoo/graph_spec.rb +265 -0
- data/spec/wewoo/gremlin_spec.rb +434 -0
- data/spec/wewoo/result_set_spec.rb +225 -0
- data/spec/wewoo/vertex_spec.rb +75 -0
- data/wewoo.gemspec +35 -0
- metadata +237 -0
data/lib/wewoo/graph.rb
ADDED
@@ -0,0 +1,233 @@
|
|
1
|
+
require 'typhoeus'
|
2
|
+
require 'json'
|
3
|
+
require 'map'
|
4
|
+
require 'wewoo/adapter'
|
5
|
+
|
6
|
+
module Wewoo
|
7
|
+
class Graph
|
8
|
+
include Adapter
|
9
|
+
|
10
|
+
class UnknownGraphError < RuntimeError; end
|
11
|
+
class GraphElementNotFoundError < RuntimeError; end
|
12
|
+
|
13
|
+
attr_reader :url, :name
|
14
|
+
|
15
|
+
def self.available_graphs
|
16
|
+
res = Adapter::get( "#{Configuration.url}/graphs" )
|
17
|
+
res.graphs
|
18
|
+
end
|
19
|
+
|
20
|
+
def initialize( graph_name, host:nil, port:nil )
|
21
|
+
Configuration.url( host, port )
|
22
|
+
|
23
|
+
@name = graph_name
|
24
|
+
@base_url = "#{Configuration.url}/graphs"
|
25
|
+
@url = "#{@base_url}/#{@name}"
|
26
|
+
validate
|
27
|
+
end
|
28
|
+
|
29
|
+
def save( filename )
|
30
|
+
dir = File.dirname( filename )
|
31
|
+
FileUtils.mkdir_p( dir ) unless File.exists?( dir )
|
32
|
+
|
33
|
+
q( "g.saveGraphML( '#{filename}' )" )
|
34
|
+
end
|
35
|
+
|
36
|
+
def load( filename, append:false )
|
37
|
+
raise "Graph file does not exist. #{filename}" unless File.exists?(filename)
|
38
|
+
|
39
|
+
clear unless append
|
40
|
+
q( "g.loadGraphML( '#{filename}' )" )
|
41
|
+
end
|
42
|
+
|
43
|
+
def clear
|
44
|
+
query( 'g.V.remove()')
|
45
|
+
end
|
46
|
+
|
47
|
+
def key_indices
|
48
|
+
Map[ *get( u(:keyindices ) ).flatten]
|
49
|
+
end
|
50
|
+
|
51
|
+
def set_key_index( type, key )
|
52
|
+
post( u( %W[keyindices #{type} #{key}] ) )
|
53
|
+
end
|
54
|
+
|
55
|
+
def drop_index( index_name )
|
56
|
+
delete( u( %W[indices #{index_name}] ) )
|
57
|
+
end
|
58
|
+
|
59
|
+
def ensure_index( key, type=:vertex )
|
60
|
+
unless key_indices[type.to_s].include? key.to_s
|
61
|
+
post( u( %W[keyindices #{type} #{key}] ) )
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
def index( index_name, clazz:'vertex', options:{} )
|
66
|
+
post( u(%W[indices #{index_name}]), params: {class: clazz}.merge(options))
|
67
|
+
end
|
68
|
+
|
69
|
+
def query( command, page:nil, per_page:nil )
|
70
|
+
# command = "g." + command unless command[/\Ag\./]
|
71
|
+
ResultSet.new( self, get( u(%w[tp gremlin]),
|
72
|
+
params:{script: command}.merge(page_params(page, per_page)),
|
73
|
+
headers: { 'Content-Type'=> 'application/json'} ) ).hydrate
|
74
|
+
end
|
75
|
+
alias :q :query
|
76
|
+
|
77
|
+
def add_vertex( props={} )
|
78
|
+
vertex = Vertex.from_hash( self,
|
79
|
+
post( u( %W[vertices] ) ) )
|
80
|
+
update_vertex( vertex.id, props )
|
81
|
+
end
|
82
|
+
|
83
|
+
# BOZO!! - not really an update as all props will be replaced!
|
84
|
+
def update_vertex( id, props )
|
85
|
+
v = find_vertex( id )
|
86
|
+
props = v.props.merge( props )
|
87
|
+
res = put( u( %W[vertices #{id}] ),
|
88
|
+
body: props.to_json,
|
89
|
+
headers: { 'Content-Type'=> 'application/json'} )
|
90
|
+
Vertex.from_hash( self, res )
|
91
|
+
end
|
92
|
+
|
93
|
+
def vertices( page:nil, per_page:nil )
|
94
|
+
get( u(:vertices), {params: page_params(page, per_page)} ).map do |res|
|
95
|
+
Vertex.from_hash( self, res )
|
96
|
+
end
|
97
|
+
end
|
98
|
+
alias :V :vertices
|
99
|
+
|
100
|
+
def find_vertices( key, value, page:nil, per_page:nil )
|
101
|
+
params = { key: key, value: map_value(value) }.merge page_params( page, per_page )
|
102
|
+
res = get( u(:vertices), params: params )
|
103
|
+
|
104
|
+
res.map do |res|
|
105
|
+
Vertex.from_hash( self, res )
|
106
|
+
end
|
107
|
+
end
|
108
|
+
def find_first_vertex( key, value )
|
109
|
+
find_vertices( key, value ).first
|
110
|
+
end
|
111
|
+
def find_vertex_by_gid( gid )
|
112
|
+
find_first_vertex( :gid, gid )
|
113
|
+
end
|
114
|
+
def find_vertex( id )
|
115
|
+
Vertex.from_hash( self, get( u %W[vertices #{id}] ) )
|
116
|
+
rescue InvalidRequestError => ex
|
117
|
+
raise GraphElementNotFoundError, ex.message
|
118
|
+
end
|
119
|
+
alias :v :find_vertex
|
120
|
+
|
121
|
+
def vertex_exists?( id )
|
122
|
+
v(id) && true
|
123
|
+
rescue
|
124
|
+
false
|
125
|
+
end
|
126
|
+
|
127
|
+
def edge_exists?( id )
|
128
|
+
e(id) && true
|
129
|
+
true
|
130
|
+
rescue
|
131
|
+
false
|
132
|
+
end
|
133
|
+
|
134
|
+
def remove_vertex( id )
|
135
|
+
delete( u(%W[vertices #{id}] ) )
|
136
|
+
end
|
137
|
+
|
138
|
+
def update_edge( id, props )
|
139
|
+
e = find_edge( id )
|
140
|
+
props = e.props.merge( props )
|
141
|
+
|
142
|
+
res = put( u( %W[edges #{id}] ),
|
143
|
+
body: props.to_json,
|
144
|
+
headers: { 'Content-Type'=> 'application/json'} )
|
145
|
+
Edge.from_hash( self, res )
|
146
|
+
end
|
147
|
+
|
148
|
+
def add_edge( from, to, label, props={} )
|
149
|
+
params = {
|
150
|
+
'_outV' => (from.is_a? Vertex) ? from.id : from,
|
151
|
+
'_inV' => (to.is_a? Vertex) ? to.id : to,
|
152
|
+
'_label' => label
|
153
|
+
}.merge( props )
|
154
|
+
res = post( u( %W[edges] ),
|
155
|
+
body: params.to_json,
|
156
|
+
headers: { 'Content-Type'=> 'application/json'} )
|
157
|
+
Edge.from_hash( self, res )
|
158
|
+
end
|
159
|
+
|
160
|
+
def remove_edge( id )
|
161
|
+
delete( u %W[edges #{id}] )
|
162
|
+
end
|
163
|
+
|
164
|
+
def find_edge( id )
|
165
|
+
Edge.from_hash( self, get( u %W[edges #{id}] ) )
|
166
|
+
rescue InvalidRequestError => ex
|
167
|
+
raise GraphElementNotFoundError, ex.message
|
168
|
+
end
|
169
|
+
alias :e :find_edge
|
170
|
+
|
171
|
+
def find_edges( key, value, page:nil, per_page:nil )
|
172
|
+
params = { key: key,
|
173
|
+
value: map_value( value )
|
174
|
+
}.merge page_params( page, per_page )
|
175
|
+
get( u(:edges), params: params ).map { |e| Edge.from_hash( self, e ) }
|
176
|
+
end
|
177
|
+
|
178
|
+
def find_first_edge( key, value )
|
179
|
+
find_edges( key, value, page:1, per_page:1 ).first
|
180
|
+
end
|
181
|
+
|
182
|
+
def edges( page:nil, per_page:nil )
|
183
|
+
get( u(:edges), params: page_params(page, per_page) ).map do |res|
|
184
|
+
Edge.from_hash( self, res )
|
185
|
+
end
|
186
|
+
end
|
187
|
+
alias :E :edges
|
188
|
+
|
189
|
+
def to_s
|
190
|
+
res = get( url, headers: { 'Content-Type' =>
|
191
|
+
'application/vnd.rexster-typed-v1+json' } )
|
192
|
+
stats = res.graph.match( /.*\[vertices:(\d+) edges:(\d+)\]/ ).captures
|
193
|
+
|
194
|
+
"#{name} [Vertices:#{stats.first}, Edges:#{stats.last}]"
|
195
|
+
end
|
196
|
+
alias inspect to_s
|
197
|
+
|
198
|
+
private
|
199
|
+
|
200
|
+
def map_value( value )
|
201
|
+
case value
|
202
|
+
when TrueClass
|
203
|
+
when FalseClass
|
204
|
+
"(b,#{value})"
|
205
|
+
when Fixnum
|
206
|
+
"(i,#{value})"
|
207
|
+
when Float
|
208
|
+
"(d,#{value})"
|
209
|
+
else
|
210
|
+
value
|
211
|
+
end
|
212
|
+
end
|
213
|
+
|
214
|
+
def u( path )
|
215
|
+
File.join( url, ((path.is_a? Array) ? path : path.to_s ) )
|
216
|
+
end
|
217
|
+
|
218
|
+
def validate
|
219
|
+
res = get( @base_url )
|
220
|
+
|
221
|
+
unless res.graphs.include?( @name.to_s )
|
222
|
+
raise UnknownGraphError, "Unable to locate graph named `#{@name}"
|
223
|
+
end
|
224
|
+
end
|
225
|
+
|
226
|
+
def page_params( page, per_page )
|
227
|
+
return {} unless page and per_page
|
228
|
+
|
229
|
+
{ 'rexster.offset.start' => (page-1)*per_page,
|
230
|
+
'rexster.offset.end' => page*per_page }
|
231
|
+
end
|
232
|
+
end
|
233
|
+
end
|
@@ -0,0 +1,99 @@
|
|
1
|
+
# BOZO !! Refactor
|
2
|
+
module Wewoo
|
3
|
+
class ResultSet
|
4
|
+
|
5
|
+
class NoGraphElementError < RuntimeError; end
|
6
|
+
|
7
|
+
def initialize( graph, results )
|
8
|
+
@results = results
|
9
|
+
@graph = graph
|
10
|
+
end
|
11
|
+
|
12
|
+
def hydrate
|
13
|
+
collapse( _hydrate( @results ) )
|
14
|
+
end
|
15
|
+
|
16
|
+
def graph_element?( item )
|
17
|
+
item.is_a? Hash and
|
18
|
+
item.key?('_type') and
|
19
|
+
%w(vertex edge).include? item['_type']
|
20
|
+
end
|
21
|
+
|
22
|
+
def build_element( item )
|
23
|
+
type = item['_type']
|
24
|
+
Object.const_get( "Wewoo::#{type.capitalize}" )
|
25
|
+
.from_hash( @graph, item )
|
26
|
+
rescue => boom
|
27
|
+
raise NoGraphElementError, "Unbuildable"
|
28
|
+
end
|
29
|
+
|
30
|
+
private
|
31
|
+
|
32
|
+
def collapse( res )
|
33
|
+
(res.is_a? Enumerable and
|
34
|
+
res.count == 1 and
|
35
|
+
res.first.is_a? Enumerable) ? res.first : res
|
36
|
+
end
|
37
|
+
|
38
|
+
def usable( item )
|
39
|
+
graph_element?( item ) ? build_element( item ) : item
|
40
|
+
end
|
41
|
+
|
42
|
+
def value_hash?( hash )
|
43
|
+
hash.values.first.is_a? Hash
|
44
|
+
end
|
45
|
+
|
46
|
+
def simple_response?( res )
|
47
|
+
res.is_a? Hash and res.key?('success')
|
48
|
+
end
|
49
|
+
|
50
|
+
def key_value_hash?( res )
|
51
|
+
res.is_a? Hash and res.key?('_key') and res.key?('_value')
|
52
|
+
end
|
53
|
+
|
54
|
+
def _deep_hydrate( item, acc )
|
55
|
+
if graph_element?( item )
|
56
|
+
acc << build_element( item )
|
57
|
+
elsif item.is_a? Array
|
58
|
+
tuples = []
|
59
|
+
item.each do |row|
|
60
|
+
_deep_hydrate( row, tuples )
|
61
|
+
end
|
62
|
+
acc << tuples
|
63
|
+
elsif item.is_a? Hash
|
64
|
+
if key_value_hash?( item )
|
65
|
+
key = usable(item['_key'])
|
66
|
+
if item['_value'].is_a? Enumerable
|
67
|
+
acc[key] = []
|
68
|
+
item['_value'].each do |row|
|
69
|
+
acc[key] << usable(row)
|
70
|
+
end
|
71
|
+
else
|
72
|
+
acc[key] = usable(item['_value'])
|
73
|
+
end
|
74
|
+
elsif item.values.first.is_a? Hash
|
75
|
+
res = {}
|
76
|
+
item.values.each do |item|
|
77
|
+
_deep_hydrate( item, res )
|
78
|
+
end
|
79
|
+
acc << res
|
80
|
+
else
|
81
|
+
acc << item
|
82
|
+
end
|
83
|
+
else
|
84
|
+
acc << item
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
def _hydrate( res )
|
89
|
+
return res['success'] if simple_response?( res )
|
90
|
+
if res.is_a? Array
|
91
|
+
acc = []
|
92
|
+
res.each{ |row| _deep_hydrate( row, acc ) }
|
93
|
+
acc
|
94
|
+
else
|
95
|
+
raise "Unexpected match!!"
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
data/lib/wewoo/vertex.rb
ADDED
@@ -0,0 +1,63 @@
|
|
1
|
+
module Wewoo
|
2
|
+
# BOZO !! Need to support chaining!
|
3
|
+
class Vertex < Wewoo::Element
|
4
|
+
include Adapter
|
5
|
+
|
6
|
+
def outE ( *labels ); @outE || get_edges( :outE , labels ); end
|
7
|
+
def inE ( *labels ); @inE || get_edges( :inE , labels ); end
|
8
|
+
def bothE( *labels ); @bothE || get_edges( :bothE, labels ); end
|
9
|
+
|
10
|
+
def out ( *labels ); @out || get_vertices( :out , labels ); end
|
11
|
+
def in ( *labels ); @in || get_vertices( :in , labels ); end
|
12
|
+
def both( *labels ); @both || get_vertices( :both, labels ); end
|
13
|
+
|
14
|
+
def ==( o )
|
15
|
+
self.class == o.class and self.id == o.id and self.props == o.props
|
16
|
+
end
|
17
|
+
alias :eql? :==
|
18
|
+
def hash; id; end
|
19
|
+
def <=>(o); self.class == o.class ? self.id <=> o.id : super; end
|
20
|
+
|
21
|
+
def destroy
|
22
|
+
graph.remove_vertex( id )
|
23
|
+
end
|
24
|
+
|
25
|
+
def to_s
|
26
|
+
"v(#{self.id})"
|
27
|
+
end
|
28
|
+
alias :inspect :to_s
|
29
|
+
|
30
|
+
def dump
|
31
|
+
props.clone().merge( id: id )
|
32
|
+
end
|
33
|
+
|
34
|
+
private
|
35
|
+
|
36
|
+
def get_edges( direction, labels )
|
37
|
+
str_labels = labels ? labels.map(&:to_s) : []
|
38
|
+
get( u direction ).map { |hash|
|
39
|
+
if labels.empty? or str_labels.include? hash['_label']
|
40
|
+
Edge.from_hash( graph, hash )
|
41
|
+
end
|
42
|
+
}.compact
|
43
|
+
end
|
44
|
+
|
45
|
+
def get_vertices( direction, labels=[] )
|
46
|
+
get_edges( "#{direction}E",labels ).map{ |e|
|
47
|
+
if direction == :both
|
48
|
+
e.in == self ? e.out : e.in
|
49
|
+
else
|
50
|
+
e.get_vertex( direction==:in ? :out : :in )
|
51
|
+
end
|
52
|
+
}
|
53
|
+
end
|
54
|
+
|
55
|
+
def self.from_hash( graph, hash )
|
56
|
+
new( graph, hash.delete('_id'), Map( hash ) )
|
57
|
+
end
|
58
|
+
|
59
|
+
def u( path )
|
60
|
+
File.join( graph.url, %W[vertices #{id} #{path}] )
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
require 'simplecov'
|
2
|
+
if ENV['COV']
|
3
|
+
SimpleCov.start do
|
4
|
+
end
|
5
|
+
end
|
6
|
+
|
7
|
+
require 'wewoo'
|
8
|
+
require 'support/graph_sets'
|
9
|
+
|
10
|
+
RSpec.configure do |config|
|
11
|
+
config.treat_symbols_as_metadata_keys_with_true_values = true
|
12
|
+
config.filter_run focus: true
|
13
|
+
config.run_all_when_everything_filtered = true
|
14
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
def build_test_graph( g )
|
2
|
+
g.clear
|
3
|
+
|
4
|
+
@v1 = g.add_vertex( gid: 1 , name:'fred', age:20, rating: 30.5, busy: true )
|
5
|
+
@v2 = g.add_vertex( gid: 2 , name:'joe' , age:30, rating: 20.5, busy: false )
|
6
|
+
@v3 = g.add_vertex( gid: 3 , name:'max' , age:40, rating: 10.5, busy: true )
|
7
|
+
@v4 = g.add_vertex( gid: "4", name:'blee', age:50, rating: 5.5 , busy: true )
|
8
|
+
|
9
|
+
@e1 = g.add_edge( @v1, @v2, :friend, rating: 1.5 )
|
10
|
+
@e2 = g.add_edge( @v1, @v3, :friend, rating: 2.5 )
|
11
|
+
@e3 = g.add_edge( @v1, @v4, :love , rating: 3.5 )
|
12
|
+
@e4 = g.add_edge( @v2, @v3, :friend, rating: 5.5 )
|
13
|
+
@e5 = g.add_edge( @v3, @v4, :friend, rating: 5.5 )
|
14
|
+
end
|
15
|
+
|
16
|
+
def build_sample_graph( g )
|
17
|
+
g.clear
|
18
|
+
#g.ensure_index(:name,:vertex)
|
19
|
+
|
20
|
+
@v1 = g.add_vertex( name: :marko , age: 29 )
|
21
|
+
@v2 = g.add_vertex( name: :vadas , age: 27 )
|
22
|
+
@v3 = g.add_vertex( name: :lop , lang: :java )
|
23
|
+
@v4 = g.add_vertex( name: :josh , age: 32 )
|
24
|
+
@v5 = g.add_vertex( name: :ripple, lang: :java )
|
25
|
+
@v6 = g.add_vertex( name: :peter , age: 35 )
|
26
|
+
|
27
|
+
@e7 = g.add_edge( @v1, @v2, :knows , weight: 0.5 )
|
28
|
+
@e8 = g.add_edge( @v1, @v4, :knows , weight: 1.0 )
|
29
|
+
@e9 = g.add_edge( @v1, @v3, :created, weight: 0.4 )
|
30
|
+
@e10 = g.add_edge( @v4, @v5, :created, weight: 1.0 )
|
31
|
+
@e11 = g.add_edge( @v4, @v3, :created, weight: 0.4 )
|
32
|
+
@e12 = g.add_edge( @v6, @v3, :created, weight: 0.2 )
|
33
|
+
end
|