ashikawa-core 0.6.0 → 0.7.0

Sign up to get free protection for your applications and to get access to all the features.
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