ashikawa-core 0.12.0 → 0.13.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,222 @@
1
+ # -*- encoding : utf-8 -*-
2
+
3
+ require 'ashikawa-core/vertex_collection'
4
+ require 'ashikawa-core/edge_collection'
5
+
6
+ module Ashikawa
7
+ module Core
8
+ # A certain graph in the database.
9
+ #
10
+ # @note All CRUD operations on related collections (edges and vertices) must be performed
11
+ # through their corresponding graph class. Not doing so will eventually lead to inconsistency
12
+ # and data corruption.
13
+ # @see http://docs.arangodb.org/HttpGharial/README.html
14
+ class Graph
15
+ extend Forwardable
16
+
17
+ # Sending requests is delegated to the database
18
+ def_delegator :@database, :send_request
19
+
20
+ # Prepared AQL statement for neighbors function on a specific edge collections
21
+ SPECIFIC_NEIGHBORS_AQL = <<-AQL.gsub(/^[ \t]*/, '')
22
+ FOR n IN GRAPH_NEIGHBORS(@graph, { _key:@vertex_key }, {edgeCollectionRestriction: @edge_collection})
23
+ RETURN n.vertex
24
+ AQL
25
+
26
+ # Prepared AQL statement for neighbors function on ALL edge collections
27
+ ALL_NEIGHBORS_AQL = <<-AQL.gsub(/^[ \t]*/, '')
28
+ FOR n IN GRAPH_NEIGHBORS(@graph, { _key:@vertex_key }, {})
29
+ RETURN n.vertex
30
+ AQL
31
+
32
+ # The database the Graph belongs to
33
+ #
34
+ # @return [Database] The associated database
35
+ # @api public
36
+ # @example
37
+ # database = Ashikawa::Core::Database.new('http://localhost:8529')
38
+ # raw_graph = {
39
+ # 'name' => 'example_1',
40
+ # 'edgeDefinitions' => [],
41
+ # 'orphanCollections' => []
42
+ # }
43
+ # graph = Ashikawa::Core::Graph.new(database, raw_collection)
44
+ # graph.database #=> #<Database: ...>
45
+ attr_reader :database
46
+
47
+ # The name of the graph
48
+ #
49
+ # @return [String] The name of the graph
50
+ # @api public
51
+ # @example
52
+ # database = Ashikawa::Core::Database.new('http://localhost:8529')
53
+ # raw_graph = {
54
+ # 'name' => 'example_1',
55
+ # 'edgeDefinitions' => [],
56
+ # 'orphanCollections' => []
57
+ # }
58
+ # graph = Ashikawa::Core::Graph.new(database, raw_collection)
59
+ # graph.name #=> 'example_1
60
+ attr_reader :name
61
+
62
+ # The revision of the Graph
63
+ #
64
+ # @return [String] The revision of the Graph
65
+ # @api public
66
+ attr_reader :revision
67
+
68
+ # The edge definitions for this Graph
69
+ #
70
+ # @return [Hash] The edge definitons of this Graph as a simple data structure
71
+ # @api public
72
+ attr_reader :edge_definitions
73
+
74
+ # Initialize a new graph instance
75
+ #
76
+ # @param [Database] database A reference to the database this graph belongs to
77
+ # @param [Hash] raw_graph The parsed JSON response from the database representing the graph
78
+ def initialize(database, raw_graph)
79
+ @database = database
80
+ parse_raw_graph(raw_graph)
81
+ end
82
+
83
+ def delete(options = {})
84
+ drop_collections = options.fetch(:drop_collections) { false }
85
+ send_request("gharial/#@name", delete: { dropCollections: drop_collections })
86
+ end
87
+
88
+ # Gets a list of vertex collections
89
+ #
90
+ # Due to the fact we need to fetch each of the collections by hand this will just return an
91
+ # enumerator which will lazily fetch the collections from the database.
92
+ #
93
+ # @return [Enumerator] An Enumerator referencing the vertex collections
94
+ def vertex_collections
95
+ Enumerator.new do |yielder|
96
+ vertex_collection_names.each do |collection_name|
97
+ yielder.yield vertex_collection(collection_name)
98
+ end
99
+ end
100
+ end
101
+
102
+ # The list of names of the vertex collections
103
+ #
104
+ # @return [Array] Names of all vertex collections
105
+ def vertex_collection_names
106
+ @orphan_collections | @edge_definitions.map { |edge_def| edge_def.values_at('from', 'to') }.flatten
107
+ end
108
+
109
+ # Adds a vertex collection to this graph
110
+ #
111
+ # If the collection does not yet exist it will be created. If it already exists it will just be added
112
+ # to the list of vertex collections.
113
+ #
114
+ # @param [String] collection_name The name of the vertex collection
115
+ # @return [VertexCollection] The newly created collection
116
+ def add_vertex_collection(collection_name)
117
+ response = send_request("gharial/#@name/vertex", post: { collection: collection_name })
118
+ parse_raw_graph(response['graph'])
119
+ vertex_collection(collection_name)
120
+ end
121
+
122
+ # Fetches a vertex collection associated with graph from the database
123
+ #
124
+ # @param [String] collection_name The name of the collection
125
+ # @return [VertexCollection] The fetched VertexCollection
126
+ def vertex_collection(collection_name)
127
+ raw_collection = send_request("collection/#{collection_name}")
128
+ VertexCollection.new(database, raw_collection, self)
129
+ end
130
+
131
+ # Checks if a collection is present in the list of vertices
132
+ #
133
+ # @param [String] collection_name The name of the collection to query
134
+ # @return [Boolean] True if the collection is present, false otherwise
135
+ def has_vertex_collection?(collection_name)
136
+ vertex_collection_names.any? { |name| name == collection_name }
137
+ end
138
+
139
+ # Gets a list of edge collections
140
+ #
141
+ # Due to the fact we need to fetch each of the collections by hand this will just return an
142
+ # enumerator which will lazily fetch the collections from the database.
143
+ #
144
+ # @return [Enumerator] An Enumerator referencing the edge collections
145
+ def edge_collections
146
+ Enumerator.new do |yielder|
147
+ edge_collection_names.each do |collection_name|
148
+ yielder.yield edge_collection(collection_name)
149
+ end
150
+ end
151
+ end
152
+
153
+ # The list of names of the edge collections
154
+ #
155
+ # @return [Array] Names of all edge collections
156
+ def edge_collection_names
157
+ @edge_definitions.map { |edge_def| edge_def['collection'] }
158
+ end
159
+
160
+ # Adds an edge definition to this Graph
161
+ #
162
+ # @param [Symbol] collection_name The name of the resulting edge collection
163
+ # @param [Hash] directions The specification between which vertices the edges should be created
164
+ # @option [Array<Symbol>] :from A list of collections names from which the edge directs
165
+ # @option [Array<Symbol>] :to A list of collections names to which the edge directs
166
+ def add_edge_definition(collection_name, directions)
167
+ create_options = {
168
+ collection: collection_name,
169
+ from: directions[:from],
170
+ to: directions[:to]
171
+ }
172
+
173
+ response = send_request("gharial/#@name/edge", post: create_options)
174
+ parse_raw_graph(response['graph'])
175
+ edge_collection(collection_name)
176
+ end
177
+
178
+ # Fetches an edge collection from the database
179
+ #
180
+ # @param [String] collection_name The name of the desired edge
181
+ # @return [EdgeCollection] The edge collection for the given name
182
+ def edge_collection(collection_name)
183
+ response = send_request("collection/#{collection_name}")
184
+ EdgeCollection.new(database, response, self)
185
+ end
186
+
187
+ # Return a Cursor representing the neighbors for the given document and optional edge collections
188
+ #
189
+ # @param [Document] vertex The start vertex
190
+ # @param [options] options Additional options like restrictions on the edge collections
191
+ # @option [Array<Symbol>] :edges A list of edge collection to restrict the neighbors function on
192
+ # @return [Cursor] The cursor to the query result
193
+ def neighbors(vertex, options = {})
194
+ bind_vars = {
195
+ graph: name,
196
+ vertex_key: vertex.key
197
+ }
198
+ aql_string = ALL_NEIGHBORS_AQL
199
+
200
+ if options.has_key?(:edges)
201
+ aql_string = SPECIFIC_NEIGHBORS_AQL
202
+ bind_vars[:edge_collection] = [options[:edges]].flatten
203
+ end
204
+
205
+ database.query.execute(aql_string, bind_vars: bind_vars)
206
+ end
207
+
208
+ private
209
+
210
+ # Parses the raw graph structure as returned from the database
211
+ #
212
+ # @param [Hash] raw_graph The structure as returned from the database
213
+ # @api private
214
+ def parse_raw_graph(raw_graph)
215
+ @name = raw_graph['name'] || raw_graph['_key']
216
+ @revision = raw_graph['_rev']
217
+ @edge_definitions = raw_graph.fetch('edgeDefinitions') { [] }
218
+ @orphan_collections = raw_graph.fetch('orphanCollections') { [] }
219
+ end
220
+ end
221
+ end
222
+ end
@@ -2,6 +2,16 @@
2
2
  module Ashikawa
3
3
  module Core
4
4
  # Current version of Ashikawa::Core
5
- VERSION = '0.12.0'
5
+ VERSION = '0.13.0'
6
+
7
+ # The lowest supported ArangoDB major version
8
+ ARANGODB_MAJOR_VERSION = 2
9
+
10
+ # The lowest supported ArangoDB minor version
11
+ ARANGODB_MINOR_VERSION = 2
12
+
13
+ def self.api_compatibility_version
14
+ (ARANGODB_MAJOR_VERSION * 10_000 + ARANGODB_MINOR_VERSION * 100).to_s
15
+ end
6
16
  end
7
17
  end
@@ -0,0 +1,35 @@
1
+ # -*- encoding : utf-8 -*-
2
+
3
+ require 'ashikawa-core/collection'
4
+ require 'ashikawa-core/exceptions/client_error/resource_not_found/collection_not_in_graph'
5
+
6
+ module Ashikawa
7
+ module Core
8
+ # A vertex collection as it is returned from a graph
9
+ #
10
+ # @note This is basically just a regular collection with some additional attributes and methods to ease
11
+ # working with collections in the graph module.
12
+ class VertexCollection < Collection
13
+ # The Graph instance this VertexCollection was originally fetched from
14
+ #
15
+ # @return [Graph] The Graph instance the collection was fetched from
16
+ # @api public
17
+ attr_reader :graph
18
+
19
+ # Create a new VertexCollection object
20
+ #
21
+ # @param [Database] database The database the connection belongs to
22
+ # @param [Hash] raw_collection The raw collection returned from the server
23
+ # @param [Graph] graph The graph from which this collection was fetched
24
+ # @raise [CollectionNotInGraphException] If the collection has not beed added to the graph yet
25
+ # @note You should not create instance manually but rather use Graph#add_vertex_collection
26
+ # @api public
27
+ def initialize(database, raw_collection, graph)
28
+ super(database, raw_collection)
29
+ @graph = graph
30
+
31
+ raise CollectionNotInGraphException unless @graph.has_vertex_collection?(name)
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,26 @@
1
+ # -*- encoding : utf-8 -*-
2
+
3
+ module Ashikawa
4
+ module Core
5
+ # Sets the ArangoDB API compatibility header
6
+ class XArangoVersion < Faraday::Middleware
7
+ # The name of the x-arango-version header field
8
+ HEADER = 'X-Arango-Version'.freeze
9
+
10
+ # Initializes the middleware
11
+ #
12
+ # @param [Callable] app The faraday app
13
+ def initialize(app)
14
+ super(app)
15
+ end
16
+
17
+ # Sets the `x-arango-version` for each request
18
+ def call(env)
19
+ env[:request_headers][HEADER] = Ashikawa::Core.api_compatibility_version
20
+ @app.call(env)
21
+ end
22
+ end
23
+
24
+ Faraday::Request.register_middleware x_arango_version: -> { XArangoVersion }
25
+ end
26
+ end
@@ -0,0 +1,89 @@
1
+ # -*- encoding : utf-8 -*-
2
+ require 'acceptance/spec_helper'
3
+
4
+ describe 'Graphs' do
5
+ subject { DATABASE.graph 'ponyville' }
6
+
7
+ let(:ponies) { subject.add_vertex_collection(:ponies) }
8
+ let(:places) { subject.add_vertex_collection(:places) }
9
+
10
+ let(:pinkie_pie) { ponies.create_document(name: 'Pinkie Pie', color: 'pink') }
11
+ let(:rainbow_dash) { ponies.create_document(name: 'Rainbow Dash', color: 'blue') }
12
+
13
+ let(:crystal_empire) { places.create_document(name: 'Crystal Empire') }
14
+ let(:cloudsdale) { places.create_document(name: 'Cloudsdale') }
15
+ let(:manehatten) { places.create_document(name: 'Manehatten') }
16
+
17
+ let(:friends_with) { subject.add_edge_definition(:friends_with, from: [:ponies], to: [:ponies]) }
18
+ let(:visited) { subject.add_edge_definition(:visited, from: [:ponies], to: [:places]) }
19
+
20
+ before do
21
+ # required to create the required collections
22
+ ponies
23
+ places
24
+ friends_with
25
+ visited
26
+ end
27
+
28
+ after do
29
+ subject.delete(drop_collections: true)
30
+ end
31
+
32
+ it 'should have some basic information about the graph' do
33
+ edge_definitions = [
34
+ {
35
+ 'collection' => 'visited',
36
+ 'from' => ['ponies'],
37
+ 'to' => ['places']
38
+ },
39
+ {
40
+ 'collection' => 'friends_with',
41
+ 'from' => ['ponies'],
42
+ 'to' => ['ponies']
43
+ }
44
+ ]
45
+
46
+ expect(subject.name).to eq 'ponyville'
47
+ expect(subject.revision).not_to be_nil
48
+ expect(subject.edge_definitions).to match_array edge_definitions
49
+ end
50
+
51
+ it 'should know the vertex collections' do
52
+ expect(subject.vertex_collections).to include ponies
53
+ expect(subject.vertex_collections).to include places
54
+ end
55
+
56
+ it 'should know the edge collections' do
57
+ expect(subject.edge_collections).to include friends_with
58
+ expect(subject.edge_collections).to include visited
59
+ end
60
+
61
+ context 'connected vertices' do
62
+ before :each do
63
+ # There are only directed graphs
64
+ subject.edge_collection(:friends_with).add(from: pinkie_pie, to: rainbow_dash)
65
+ subject.edge_collection(:friends_with).add(from: rainbow_dash, to: pinkie_pie)
66
+
67
+ subject.edge_collection(:visited).add(from: pinkie_pie, to: crystal_empire)
68
+ subject.edge_collection(:visited).add(from: rainbow_dash, to: cloudsdale)
69
+ subject.edge_collection(:visited).add(from: rainbow_dash, to: crystal_empire)
70
+ subject.edge_collection(:visited).add(from: rainbow_dash, to: manehatten)
71
+ end
72
+
73
+ it 'should know all their neighbors' do
74
+ neighbors = ['Pinkie Pie', 'Pinkie Pie', 'Cloudsdale', 'Manehatten', 'Crystal Empire']
75
+ expect(subject.neighbors(rainbow_dash).map { |d| d['name'] }).to eq neighbors
76
+ end
77
+
78
+ it 'should know neighbors by type' do
79
+ neighbors = ['Cloudsdale', 'Manehatten', 'Crystal Empire']
80
+ expect(subject.neighbors(rainbow_dash, edges: :visited).map { |d| d['name'] }).to eq neighbors
81
+ end
82
+
83
+ it 'should remove edges between vertices' do
84
+ neighbors = ['Cloudsdale', 'Crystal Empire']
85
+ subject.edge_collection(:visited).remove(from: rainbow_dash, to: manehatten)
86
+ expect(subject.neighbors(rainbow_dash, edges: :visited).map { |d| d['name'] }).to eq neighbors
87
+ end
88
+ end
89
+ end
@@ -5,6 +5,8 @@ $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
5
5
  require 'ashikawa-core'
6
6
  require 'logging'
7
7
 
8
+ RANDOM_DB_PREFIX = 'ashikawa_spec_db_'
9
+
8
10
  PORT = ENV.fetch('ARANGODB_PORT', 8529)
9
11
  USERNAME = ENV.fetch('ARANGODB_USERNAME', 'root')
10
12
  PASSWORD = ENV.fetch('ARANGODB_PASSWORD', '')
@@ -33,7 +35,11 @@ end
33
35
  def database_with_random_name
34
36
  # This results in a database that has a valid name according to:
35
37
  # https://www.arangodb.org/manuals/2/NamingConventions.html#DatabaseNames
36
- database_with_name("a#{rand.to_s[2, 10]}")
38
+ database_with_name("#{RANDOM_DB_PREFIX}#{rand.to_s[2, 10]}")
39
+ end
40
+
41
+ def random_databases
42
+ SYSTEM_DATABASE.all_databases.grep(/^#{RANDOM_DB_PREFIX}/).map(&method(:database_with_name))
37
43
  end
38
44
 
39
45
  # The database for the general specs
@@ -50,11 +56,18 @@ RSpec.configure do |config|
50
56
  c.syntax = :expect
51
57
  end
52
58
 
53
- config.before(:each) do
59
+ config.before(:suite) do
54
60
  begin
55
61
  DATABASE.create
56
62
  rescue Ashikawa::Core::ClientError
57
63
  end
64
+ end
65
+
66
+ config.before(:each) do
58
67
  DATABASE.truncate
59
68
  end
69
+
70
+ config.after(:suite) do
71
+ random_databases.each(&:drop)
72
+ end
60
73
  end
@@ -235,21 +235,29 @@ describe Ashikawa::Core::Collection do
235
235
 
236
236
  its(:content_type) { should be(:document) }
237
237
 
238
+ context 'building the content classes' do
239
+ it 'should build documents' do
240
+ expect(Ashikawa::Core::Document).to receive(:new)
241
+ .with(database, raw_document)
242
+
243
+ subject.build_content_class(raw_document)
244
+ end
245
+ end
246
+
238
247
  context 'when using the key' do
239
248
  let(:key) { 333 }
240
249
 
241
250
  it 'should receive a document by ID via fetch' do
242
251
  expect(database).to receive(:send_request)
243
252
  .with('document/60768679/333', {})
244
- expect(Ashikawa::Core::Document).to receive(:new)
253
+ expect(subject).to receive(:build_content_class)
245
254
 
246
255
  subject.fetch(key)
247
256
  end
248
257
 
249
258
  it 'should receive a document by ID via []' do
250
- expect(database).to receive(:send_request)
251
- .with('document/60768679/333', {})
252
- expect(Ashikawa::Core::Document).to receive(:new)
259
+ expect(subject).to receive(:fetch)
260
+ .with(key)
253
261
 
254
262
  subject[key]
255
263
  end
@@ -318,18 +326,25 @@ describe Ashikawa::Core::Collection do
318
326
 
319
327
  its(:content_type) { should be(:edge) }
320
328
 
329
+ context 'building the content classes' do
330
+ it 'should build documents' do
331
+ expect(Ashikawa::Core::Edge).to receive(:new)
332
+ .with(database, raw_document)
333
+
334
+ subject.build_content_class(raw_document)
335
+ end
336
+ end
337
+
321
338
  it 'should receive an edge by ID via fetch' do
322
339
  expect(database).to receive(:send_request)
323
340
  .with('edge/60768679/333', {})
324
- expect(Ashikawa::Core::Edge).to receive(:new)
341
+ expect(subject).to receive(:build_content_class)
325
342
 
326
343
  subject.fetch(333)
327
344
  end
328
345
 
329
346
  it 'should receive an edge by ID via []' do
330
- expect(database).to receive(:send_request)
331
- .with('edge/60768679/333', {})
332
- expect(Ashikawa::Core::Edge).to receive(:new)
347
+ expect(subject).to receive(:fetch).with(333)
333
348
 
334
349
  subject[333]
335
350
  end