ashikawa-core 0.6.0 → 0.7.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.
Files changed (68) hide show
  1. data/.rspec +0 -1
  2. data/.travis.yml +2 -0
  3. data/CONTRIBUTING.md +14 -29
  4. data/Gemfile.devtools +9 -10
  5. data/README.md +30 -9
  6. data/Rakefile +1 -1
  7. data/ashikawa-core.gemspec +9 -8
  8. data/config/flay.yml +2 -2
  9. data/config/flog.yml +1 -1
  10. data/config/roodi.yml +4 -5
  11. data/config/site.reek +40 -16
  12. data/config/yardstick.yml +1 -1
  13. data/lib/ashikawa-core/collection.rb +109 -22
  14. data/lib/ashikawa-core/connection.rb +42 -110
  15. data/lib/ashikawa-core/cursor.rb +13 -6
  16. data/lib/ashikawa-core/database.rb +67 -17
  17. data/lib/ashikawa-core/document.rb +41 -10
  18. data/lib/ashikawa-core/edge.rb +50 -0
  19. data/lib/ashikawa-core/exceptions/client_error/bad_syntax.rb +24 -0
  20. data/lib/ashikawa-core/exceptions/{collection_not_found.rb → client_error/resource_not_found/collection_not_found.rb} +3 -1
  21. data/lib/ashikawa-core/exceptions/{document_not_found.rb → client_error/resource_not_found/document_not_found.rb} +3 -1
  22. data/lib/ashikawa-core/exceptions/{index_not_found.rb → client_error/resource_not_found/index_not_found.rb} +3 -1
  23. data/lib/ashikawa-core/exceptions/client_error/resource_not_found.rb +25 -0
  24. data/lib/ashikawa-core/exceptions/client_error.rb +23 -0
  25. data/lib/ashikawa-core/exceptions/server_error/json_error.rb +25 -0
  26. data/lib/ashikawa-core/exceptions/server_error.rb +23 -0
  27. data/lib/ashikawa-core/figure.rb +59 -2
  28. data/lib/ashikawa-core/index.rb +23 -7
  29. data/lib/ashikawa-core/query.rb +10 -10
  30. data/lib/ashikawa-core/request_preprocessor.rb +49 -0
  31. data/lib/ashikawa-core/response_preprocessor.rb +111 -0
  32. data/lib/ashikawa-core/version.rb +1 -1
  33. data/lib/ashikawa-core.rb +0 -1
  34. data/spec/acceptance/basic_spec.rb +61 -22
  35. data/spec/acceptance/index_spec.rb +11 -4
  36. data/spec/acceptance/query_spec.rb +4 -1
  37. data/spec/acceptance/spec_helper.rb +0 -2
  38. data/spec/acceptance_auth/spec_helper.rb +0 -2
  39. data/spec/fixtures/collections/60768679-count.json +13 -0
  40. data/spec/fixtures/collections/60768679-figures.json +35 -0
  41. data/spec/fixtures/collections/60768679-properties-volatile.json +12 -0
  42. data/spec/fixtures/collections/60768679-properties.json +12 -0
  43. data/spec/fixtures/collections/{4588.json → 60768679.json} +2 -2
  44. data/spec/fixtures/collections/all.json +5 -5
  45. data/spec/fixtures/cursor/26011191-2.json +1 -1
  46. data/spec/fixtures/cursor/26011191.json +1 -1
  47. data/spec/fixtures/documents/example_1-137249191.json +6 -0
  48. data/spec/fixtures/documents/new-example_1-137249191.json +6 -0
  49. data/spec/setup/arangodb.sh +1 -1
  50. data/spec/unit/collection_spec.rb +117 -42
  51. data/spec/unit/connection_spec.rb +161 -61
  52. data/spec/unit/cursor_spec.rb +39 -12
  53. data/spec/unit/database_spec.rb +119 -19
  54. data/spec/unit/document_spec.rb +4 -2
  55. data/spec/unit/edge_spec.rb +54 -0
  56. data/spec/unit/exception_spec.rb +36 -8
  57. data/spec/unit/figure_spec.rb +37 -11
  58. data/spec/unit/index_spec.rb +1 -1
  59. data/spec/unit/query_spec.rb +18 -18
  60. data/spec/unit/spec_helper.rb +4 -13
  61. data/tasks/adjustments.rake +3 -2
  62. metadata +59 -32
  63. data/lib/ashikawa-core/exceptions/unknown_path.rb +0 -15
  64. data/spec/fixtures/collections/4590-properties.json +0 -9
  65. data/spec/fixtures/collections/4590.json +0 -8
  66. data/spec/fixtures/collections/73482-figures.json +0 -23
  67. data/spec/fixtures/documents/4590-333.json +0 -5
  68. data/spec/fixtures/documents/new-4590-333.json +0 -5
@@ -1,136 +1,79 @@
1
- require "rest-client"
2
- require "json"
1
+ require "forwardable"
2
+ require "faraday"
3
+ require "null_logger"
3
4
  require "uri"
4
- require "ashikawa-core/exceptions/index_not_found"
5
- require "ashikawa-core/exceptions/document_not_found"
6
- require "ashikawa-core/exceptions/collection_not_found"
7
- require "ashikawa-core/exceptions/unknown_path"
5
+ require "ashikawa-core/request_preprocessor"
6
+ require "ashikawa-core/response_preprocessor"
8
7
 
9
8
  module Ashikawa
10
9
  module Core
11
10
  # A Connection via HTTP to a certain host
12
11
  class Connection
12
+ extend Forwardable
13
+
13
14
  # The host part of the connection
14
15
  #
16
+ # @!method host
15
17
  # @return [String]
16
18
  # @api public
17
19
  # @example Get the host part of the connection
18
20
  # connection = Connection.new("http://localhost:8529")
19
21
  # connection.host # => "localhost"
20
- attr_reader :host
22
+ def_delegator :@connection, :host
21
23
 
22
24
  # The scheme of the connection
23
25
  #
26
+ # @!method scheme
24
27
  # @return [String]
25
28
  # @api public
26
29
  # @example Get the scheme of the connection
27
30
  # connection = Connection.new("http://localhost:8529")
28
31
  # connection.scheme # => "http"
29
- attr_reader :scheme
32
+ def_delegator :@connection, :scheme
30
33
 
31
34
  # The port of the connection
32
35
  #
36
+ # @!method port
33
37
  # @return [Fixnum]
34
38
  # @api public
35
39
  # @example Get the port of the connection
36
40
  # connection = Connection.new("http://localhost:8529")
37
41
  # connection.port # => 8529
38
- attr_reader :port
39
-
40
- # Username of the connection if using authentication
41
- # @note you can set these properties with the `authenticate_with` method
42
- #
43
- # @return String
44
- # @api public
45
- # @example Get the username of the connection
46
- # connection = Connection.new("http://localhost:8529")
47
- # connection.authenticate_with(:username => 'james', :password => 'bond')
48
- # connection.username # => 'james'
49
- attr_reader :username
50
-
51
- # Password of the connection if using authentication
52
- # @note you can set these properties with the `authenticate_with` method
53
- #
54
- # @return String
55
- # @api public
56
- # @example Get the password of the connection
57
- # connection = Connection.new("http://localhost:8529")
58
- # connection.authenticate_with(:username => 'james', :password => 'bond')
59
- # connection.password # => 'bond'
60
- attr_reader :password
42
+ def_delegator :@connection, :port
61
43
 
62
44
  # Initialize a Connection with a given API String
63
45
  #
64
46
  # @param [String] api_string scheme, hostname and port as a String
47
+ # @option opts [Object] adapter The Faraday adapter you want to use. Defaults to Default Adapter
48
+ # @option opts [Object] logger The logger you want to use. Defaults to Null Logger.
65
49
  # @api public
66
50
  # @example Create a new Connection
67
51
  # connection = Connection.new("http://localhost:8529")
68
- def initialize(api_string = "http://localhost:8529")
69
- uri = URI(api_string)
70
- @host = uri.host
71
- @port = uri.port
72
- @scheme = uri.scheme
52
+ def initialize(api_string, opts = {})
53
+ logger = opts[:logger] || NullLogger.instance
54
+ adapter = opts[:adapter] || Faraday.default_adapter
55
+ @connection = Faraday.new("#{api_string}/_api") do |connection|
56
+ connection.request :ashikawa_request, logger
57
+ connection.response :ashikawa_response, logger
58
+ connection.adapter *adapter
59
+ end
73
60
  end
74
61
 
75
62
  # Sends a request to a given path returning the parsed result
76
63
  # @note prepends the api_string automatically
77
64
  #
78
- # @param [String] path the path you wish to send a request to.
79
- # @option params [Hash] :post POST data in case you want to send a POST request.
80
- # @return [Hash] parsed JSON response from the server
65
+ # @param [string] path the path you wish to send a request to.
66
+ # @option params [hash] :post post data in case you want to send a post request.
67
+ # @return [hash] parsed json response from the server
81
68
  # @api public
82
69
  # @example get request
83
70
  # connection.send_request('/collection/new_collection')
84
71
  # @example post request
85
72
  # connection.send_request('/collection/new_collection', :post => { :name => 'new_collection' })
86
73
  def send_request(path, params = {})
87
- begin
88
- raw = raw_result_for(path, params)
89
- rescue RestClient::ResourceNotFound
90
- resource_not_found_for(path)
91
- end
92
- JSON.parse(raw)
93
- end
94
-
95
- # Raise the fitting ResourceNotFoundException
96
- #
97
- # @raise [DocumentNotFoundException, CollectionNotFoundException, IndexNotFoundException]
98
- # @return nil
99
- # @api private
100
- def resource_not_found_for(path)
101
- path = path.split("/").delete_if { |part| part == "" }
102
- resource = path.first
103
-
104
- raise case resource
105
- when "document" then DocumentNotFoundException
106
- when "collection" then CollectionNotFoundException
107
- when "index" then IndexNotFoundException
108
- else UnknownPath
109
- end
110
- end
111
-
112
- # Sends a request to a given path returning the raw result
113
- # @note prepends the api_string automatically
114
- #
115
- # @example get request
116
- # connection.raw_result_for('/collection/new_collection')
117
- # @example post request
118
- # connection.raw_result_for('/collection/new_collection', :post => { :name => 'new_collection' })
119
- # @param [String] path the path you wish to send a request to.
120
- # @option params [Hash] :post POST data in case you want to send a POST request.
121
- # @return [String] raw response from the server
122
- # @api public
123
- def raw_result_for(path, params = {})
124
- path = full_path(path)
125
- method = [:post, :put, :delete].find { |method_name|
126
- params.has_key?(method_name)
127
- } || :get
128
-
129
- if [:post, :put].include?(method)
130
- RestClient.send(method, path, params[method].to_json)
131
- else
132
- RestClient.send(method, path)
133
- end
74
+ method = http_verb(params)
75
+ result = @connection.public_send(method, path, params[method])
76
+ result.body
134
77
  end
135
78
 
136
79
  # Checks if authentication for this Connection is active or not
@@ -143,7 +86,7 @@ module Ashikawa
143
86
  # connection.authenticate_with(:username => 'james', :password => 'bond')
144
87
  # connection.authentication? #=> true
145
88
  def authentication?
146
- !!@username
89
+ !!@authentication
147
90
  end
148
91
 
149
92
  # Authenticate with given username and password
@@ -157,33 +100,22 @@ module Ashikawa
157
100
  # connection = Connection.new("http://localhost:8529")
158
101
  # connection.authenticate_with(:username => 'james', :password => 'bond')
159
102
  def authenticate_with(options = {})
160
- if options.key? :username and options.key? :password
161
- @username = options[:username]
162
- @password = options[:password]
163
- else
164
- raise ArgumentError, 'missing username or password'
165
- end
166
-
103
+ raise ArgumentError, 'missing username or password' unless options.key? :username and options.key? :password
104
+ @authentication = @connection.basic_auth(options[:username], options[:password])
167
105
  self
168
106
  end
169
107
 
170
- # Return the full path for a given API path
171
- #
172
- # @param [String] path The API path
173
- # @return [String] Full path
174
- # @api public
175
- # @example Get the full path
176
- # connection = Connection.new("http://localhost:8529")
177
- # connection.full_path('documents') #=> "http://localhost:8529/_api/documents"
178
- # connection.full_path('/documents') #=> "http://localhost:8529/_api/documents"
179
- def full_path(path)
180
- prefix = if authentication?
181
- "#{@scheme}://#{@username}:#{@password}@#{@host}:#{@port}"
182
- else
183
- "#{@scheme}://#{@host}:#{@port}"
184
- end
108
+ private
185
109
 
186
- "#{prefix}/_api/#{path.gsub(/^\//, '')}"
110
+ # Return the HTTP Verb for the given parameters
111
+ #
112
+ # @param [Hash] params The params given to the method
113
+ # @return [Symbol] The HTTP verb used
114
+ # @api private
115
+ def http_verb(params)
116
+ [:post, :put, :delete].find { |method_name|
117
+ params.has_key?(method_name)
118
+ } || :get
187
119
  end
188
120
  end
189
121
  end
@@ -8,11 +8,11 @@ module Ashikawa
8
8
  include Enumerable
9
9
 
10
10
  # The ID of the cursor
11
- # @return [Int]
11
+ # @return [String]
12
12
  # @api public
13
13
  # @example Get the id of the cursor
14
14
  # cursor = Ashikawa::Core::Cursor.new(database, raw_cursor)
15
- # cursor.id #=> 1337
15
+ # cursor.id #=> "1337"
16
16
  attr_reader :id
17
17
 
18
18
  # The number of documents
@@ -38,14 +38,20 @@ module Ashikawa
38
38
  # Iterate over the documents found by the cursor
39
39
  #
40
40
  # @yield [document]
41
- # @return nil
41
+ # @return [nil, Enumerator] If no block is given, an Enumerator is returned
42
42
  # @api public
43
43
  # @example Print all documents
44
44
  # cursor = Ashikawa::Core::Cursor.new(database, raw_cursor)
45
45
  # cursor.each do |document|
46
46
  # p document
47
47
  # end
48
+ # @example Get an enumerator to iterate over all documents
49
+ # cursor = Ashikawa::Core::Cursor.new(database, raw_cursor)
50
+ # enumerator = cursor.each
51
+ # enumerator.next #=> #<Document ...>
48
52
  def each
53
+ return to_enum(__callee__) unless block_given?
54
+
49
55
  begin
50
56
  @current.each do |raw_document|
51
57
  yield Document.new(@database, raw_document)
@@ -61,17 +67,18 @@ module Ashikawa
61
67
  # cursor = Ashikawa::Core::Cursor.new(database, raw_cursor)
62
68
  # cursor.delete
63
69
  def delete
64
- @database.send_request("/cursor/#{@id}", :delete => {})
70
+ @database.send_request("cursor/#{@id}", :delete => {})
65
71
  end
66
72
 
67
73
  private
68
74
 
69
75
  # Pull the raw data from the cursor into this object
70
76
  #
77
+ # @param [Hash] raw_cursor
71
78
  # @return self
72
79
  # @api private
73
80
  def parse_raw_cursor(raw_cursor)
74
- @id = raw_cursor['id'].to_i if raw_cursor.has_key?('id')
81
+ @id = raw_cursor['id']
75
82
  @has_more = raw_cursor['hasMore']
76
83
  @length = raw_cursor['count'].to_i if raw_cursor.has_key?('count')
77
84
  @current = raw_cursor['result']
@@ -84,7 +91,7 @@ module Ashikawa
84
91
  # @api private
85
92
  def next_batch
86
93
  return false unless @has_more
87
- raw_cursor = @database.send_request("/cursor/#{@id}", :put => {})
94
+ raw_cursor = @database.send_request("cursor/#{@id}", :put => {})
88
95
  parse_raw_cursor(raw_cursor)
89
96
  end
90
97
  end
@@ -1,4 +1,4 @@
1
- require "ashikawa-core/exceptions/collection_not_found"
1
+ require "ashikawa-core/exceptions/client_error/resource_not_found/collection_not_found"
2
2
  require "ashikawa-core/collection"
3
3
  require "ashikawa-core/connection"
4
4
  require "ashikawa-core/cursor"
@@ -6,8 +6,16 @@ require "forwardable"
6
6
 
7
7
  module Ashikawa
8
8
  module Core
9
+ # Configuration of Ashikawa::Core
10
+ Configuration = Struct.new(:url, :connection, :logger, :adapter)
11
+
9
12
  # An ArangoDB database
10
13
  class Database
14
+ COLLECTION_TYPES = {
15
+ :document => 2,
16
+ :edge => 3
17
+ }
18
+
11
19
  extend Forwardable
12
20
 
13
21
  # Delegate sending requests to the connection
@@ -19,19 +27,26 @@ module Ashikawa
19
27
 
20
28
  # Initializes the connection to the database
21
29
  #
22
- # @param [Connection, String] connection A Connection object or a String to create a Connection object.
23
30
  # @api public
24
31
  # @example Access a Database by providing the URL
25
- # database = Ashikawa::Core::Database.new("http://localhost:8529")
32
+ # database = Ashikawa::Core::Database.new do |config|
33
+ # config.url = "http://localhost:8529"
34
+ # end
26
35
  # @example Access a Database by providing a Connection
27
- # connection = Connection.new("http://localhost:8529")
28
- # database = Ashikawa::Core::Database.new connection
29
- def initialize(connection)
30
- if connection.class == String
31
- @connection = Ashikawa::Core::Connection.new(connection)
32
- else
33
- @connection = connection
34
- end
36
+ # connection = Connection.new("http://localhost:8529")
37
+ # database = Ashikawa::Core::Database.new do |config|
38
+ # config.connection = connection
39
+ # end
40
+ # @example Access a Database with a logger and custom HTTP adapter
41
+ # database = Ashikawa::Core::Database.new do |config|
42
+ # config.url = "http://localhost:8529"
43
+ # config.adapter = my_adapter
44
+ # config.logger = my_logger
45
+ # end
46
+ def initialize()
47
+ configuration = Ashikawa::Core::Configuration.new
48
+ yield(configuration)
49
+ @connection = configuration.connection || setup_new_connection(configuration.url, configuration.logger, configuration.adapter)
35
50
  end
36
51
 
37
52
  # Returns a list of all collections defined in the database
@@ -44,8 +59,26 @@ module Ashikawa
44
59
  # database["b"]
45
60
  # database.collections # => [ #<Collection name="a">, #<Collection name="b">]
46
61
  def collections
47
- server_response = send_request("/collection")
48
- server_response["collections"].map { |collection| Ashikawa::Core::Collection.new(self, collection) }
62
+ response = send_request("collection")
63
+ response["collections"].map { |collection| Ashikawa::Core::Collection.new(self, collection) }
64
+ end
65
+
66
+ # Create a Collection based on name
67
+ #
68
+ # @param [String] collection_identifier The desired name of the collection
69
+ # @option opts [Boolean] :is_volatile Should the collection be volatile? Default is false
70
+ # @option opts [Boolean] :content_type What kind of content should the collection have? Default is :document
71
+ # @return [Collection]
72
+ # @api public
73
+ # @example Create a new, volatile collection
74
+ # database = Ashikawa::Core::Database.new("http://localhost:8529")
75
+ # database.create_collection("a", :isVolatile => true) # => #<Collection name="a">
76
+ def create_collection(collection_identifier, opts={})
77
+ params = { :name => collection_identifier }
78
+ params[:isVolatile] = true if opts[:is_volatile] == true
79
+ params[:type] = COLLECTION_TYPES[opts[:content_type]] if opts.has_key?(:content_type)
80
+ response = send_request("collection", :post => params)
81
+ Ashikawa::Core::Collection.new(self, response)
49
82
  end
50
83
 
51
84
  # Get or create a Collection based on name or ID
@@ -59,16 +92,18 @@ module Ashikawa
59
92
  # @example Get a Collection from the database by ID
60
93
  # database = Ashikawa::Core::Database.new("http://localhost:8529")
61
94
  # database["7254820"] # => #<Collection id=7254820>
62
- def [](collection_identifier)
95
+ def collection(collection_identifier)
63
96
  begin
64
- server_response = send_request("/collection/#{collection_identifier}")
97
+ response = send_request("collection/#{collection_identifier}")
65
98
  rescue CollectionNotFoundException
66
- server_response = send_request("/collection", :post => { :name => collection_identifier })
99
+ response = send_request("collection", :post => { :name => collection_identifier })
67
100
  end
68
101
 
69
- Ashikawa::Core::Collection.new(self, server_response)
102
+ Ashikawa::Core::Collection.new(self, response)
70
103
  end
71
104
 
105
+ alias :[] :collection
106
+
72
107
  # Return a Query initialized with this database
73
108
  #
74
109
  # @return [Query]
@@ -79,6 +114,21 @@ module Ashikawa
79
114
  def query
80
115
  Query.new(self)
81
116
  end
117
+
118
+ # Setup the connection object
119
+ #
120
+ # @param [String] url
121
+ # @param [Logger] logger
122
+ # @param [Adapter] adapter
123
+ # @return [Connection]
124
+ # @api private
125
+ def setup_new_connection(url, logger, adapter)
126
+ raise(ArgumentError, "Please provide either an url or a connection to setup the database") if url.nil?
127
+ Ashikawa::Core::Connection.new(url, {
128
+ :logger => logger,
129
+ :adapter => adapter
130
+ })
131
+ end
82
132
  end
83
133
  end
84
134
  end
@@ -1,21 +1,30 @@
1
- require 'ashikawa-core/exceptions/document_not_found'
1
+ require 'ashikawa-core/exceptions/client_error/resource_not_found/document_not_found'
2
2
 
3
3
  module Ashikawa
4
4
  module Core
5
5
  # A certain Document within a certain Collection
6
6
  class Document
7
- # The ID of the document without the Collection prefix
7
+ # The ID of the document (this includes the Collection prefix)
8
8
  #
9
- # @return [Int]
9
+ # @return [String]
10
10
  # @api public
11
11
  # @example Get the ID for a Document
12
12
  # document = Ashikawa::Core::Document.new(database, raw_document)
13
- # document.id # => 2345678
13
+ # document.id # => "my_fancy_collection/2345678"
14
14
  attr_reader :id
15
15
 
16
+ # The key of the document (No collection prefix)
17
+ #
18
+ # @return [String]
19
+ # @api public
20
+ # @example Get the key for a Document
21
+ # document = Ashikawa::Core::Document.new(database, raw_document)
22
+ # document.key # => "2345678"
23
+ attr_reader :key
24
+
16
25
  # The current revision of the document
17
26
  #
18
- # @return [Int]
27
+ # @return [String]
19
28
  # @api public
20
29
  # @example Get the Revision for a Document
21
30
  # document = Ashikawa::Core::Document.new(database, raw_document)
@@ -31,9 +40,7 @@ module Ashikawa
31
40
  # document = Ashikawa::Core::Document.new(database, raw_document)
32
41
  def initialize(database, raw_document)
33
42
  @database = database
34
- @collection_id, @id = raw_document['_id'].split('/').map { |id| id.to_i } unless raw_document['_id'].nil?
35
- @revision = raw_document['_rev'].to_i unless raw_document['_rev'].nil?
36
- @content = raw_document.delete_if { |key, value| key.start_with?("_") }
43
+ parse_raw_document(raw_document)
37
44
  end
38
45
 
39
46
  # Raises an exception if the document is not persisted
@@ -69,7 +76,7 @@ module Ashikawa
69
76
  # document.delete
70
77
  def delete
71
78
  check_if_persisted!
72
- @database.send_request("document/#{@collection_id}/#{@id}", :delete => {})
79
+ send_request_for_document(:delete => {})
73
80
  end
74
81
 
75
82
  # Update the value of an attribute (Does not write to database)
@@ -107,7 +114,31 @@ module Ashikawa
107
114
  # document.save
108
115
  def save()
109
116
  check_if_persisted!
110
- @database.send_request("document/#{@collection_id}/#{@id}", :put => @content)
117
+ send_request_for_document(:put => @content)
118
+ end
119
+
120
+ protected
121
+
122
+ # Parse information returned from the server
123
+ #
124
+ # @param [Hash] raw_document
125
+ # @return self
126
+ # @api private
127
+ def parse_raw_document(raw_document)
128
+ @id = raw_document['_id']
129
+ @key = raw_document['_key']
130
+ @revision = raw_document['_rev']
131
+ @content = raw_document.delete_if { |key, value| key.start_with?("_") }
132
+ self
133
+ end
134
+
135
+ # Send a request for this document with the given opts
136
+ #
137
+ # @param [Hash] opts Options for this request
138
+ # @return [Hash] The parsed response from the server
139
+ # @api private
140
+ def send_request_for_document(opts = {})
141
+ @database.send_request("document/#{@id}", opts)
111
142
  end
112
143
  end
113
144
  end