ashikawa-core 0.12.0 → 0.13.0

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.
@@ -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