ashikawa-core 0.7.2 → 0.8.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (55) hide show
  1. checksums.yaml +7 -0
  2. data/.coveralls.yml +1 -0
  3. data/.ruby-version +1 -0
  4. data/.travis.yml +0 -4
  5. data/Gemfile +1 -1
  6. data/Gemfile.devtools +24 -18
  7. data/README.md +6 -11
  8. data/ashikawa-core.gemspec +7 -6
  9. data/config/flay.yml +2 -2
  10. data/config/flog.yml +2 -1
  11. data/config/reek.yml +68 -84
  12. data/config/rubocop.yml +99 -0
  13. data/config/yardstick.yml +1 -1
  14. data/lib/ashikawa-core/collection.rb +74 -21
  15. data/lib/ashikawa-core/configuration.rb +26 -0
  16. data/lib/ashikawa-core/connection.rb +5 -2
  17. data/lib/ashikawa-core/cursor.rb +28 -16
  18. data/lib/ashikawa-core/database.rb +89 -12
  19. data/lib/ashikawa-core/document.rb +32 -5
  20. data/lib/ashikawa-core/edge.rb +3 -0
  21. data/lib/ashikawa-core/exceptions/client_error.rb +3 -3
  22. data/lib/ashikawa-core/exceptions/server_error.rb +3 -3
  23. data/lib/ashikawa-core/figure.rb +17 -5
  24. data/lib/ashikawa-core/index.rb +4 -0
  25. data/lib/ashikawa-core/key_options.rb +54 -0
  26. data/lib/ashikawa-core/query.rb +39 -74
  27. data/lib/ashikawa-core/request_preprocessor.rb +2 -2
  28. data/lib/ashikawa-core/response_preprocessor.rb +21 -12
  29. data/lib/ashikawa-core/transaction.rb +113 -0
  30. data/lib/ashikawa-core/version.rb +1 -1
  31. data/spec/acceptance/basic_spec.rb +40 -12
  32. data/spec/acceptance/index_spec.rb +2 -1
  33. data/spec/acceptance/query_spec.rb +18 -17
  34. data/spec/acceptance/transactions_spec.rb +30 -0
  35. data/spec/fixtures/collections/all.json +90 -30
  36. data/spec/fixtures/cursor/edges.json +23 -0
  37. data/spec/setup/arangodb.sh +7 -6
  38. data/spec/unit/collection_spec.rb +89 -13
  39. data/spec/unit/connection_spec.rb +23 -14
  40. data/spec/unit/cursor_spec.rb +15 -4
  41. data/spec/unit/database_spec.rb +58 -17
  42. data/spec/unit/document_spec.rb +24 -4
  43. data/spec/unit/edge_spec.rb +1 -1
  44. data/spec/unit/exception_spec.rb +4 -2
  45. data/spec/unit/figure_spec.rb +17 -10
  46. data/spec/unit/index_spec.rb +1 -1
  47. data/spec/unit/key_options_spec.rb +25 -0
  48. data/spec/unit/query_spec.rb +1 -1
  49. data/spec/unit/spec_helper.rb +20 -2
  50. data/spec/unit/transaction_spec.rb +153 -0
  51. data/tasks/adjustments.rake +23 -14
  52. metadata +31 -41
  53. data/.rvmrc +0 -1
  54. data/config/roodi.yml +0 -17
  55. data/spec/spec_helper.rb +0 -27
@@ -1,9 +1,12 @@
1
1
  require 'ashikawa-core/exceptions/client_error/resource_not_found/document_not_found'
2
+ require 'equalizer'
2
3
 
3
4
  module Ashikawa
4
5
  module Core
5
6
  # A certain Document within a certain Collection
6
7
  class Document
8
+ include Equalizer.new(:id, :revision)
9
+
7
10
  # The ID of the document (this includes the Collection prefix)
8
11
  #
9
12
  # @return [String]
@@ -52,7 +55,7 @@ module Ashikawa
52
55
  # document = Ashikawa::Core::Document.new(database, raw_document)
53
56
  # document.check_if_persisted!
54
57
  def check_if_persisted!
55
- raise DocumentNotFoundException if @id.nil?
58
+ raise DocumentNotFoundException if @id == :not_persisted
56
59
  end
57
60
 
58
61
  # Get the value of an attribute of the document
@@ -99,9 +102,22 @@ module Ashikawa
99
102
  # @api public
100
103
  # @example Get the hash representation of a document
101
104
  # document = Ashikawa::Core::Document.new(database, raw_document)
105
+ # document.hash #=> { :name => "Lebowski", :occupation => "Not occupied" }
106
+ def hash
107
+ @content
108
+ end
109
+
110
+ # Convert the document into a hash
111
+ #
112
+ # @return [Hash]
113
+ # @api public
114
+ # @deprecated Use {#hash} instead.
115
+ # @example Get the hash representation of a document
116
+ # document = Ashikawa::Core::Document.new(database, raw_document)
102
117
  # document.to_hash #=> { :name => "Lebowski", :occupation => "Not occupied" }
103
118
  def to_hash
104
- @content
119
+ warn "`to_hash` is deprecated, please use `hash`"
120
+ hash
105
121
  end
106
122
 
107
123
  # Save the changes to the database
@@ -112,11 +128,22 @@ module Ashikawa
112
128
  # document = Ashikawa::Core::Document.new(database, raw_document)
113
129
  # document['occupation'] = 'Not occupied'
114
130
  # document.save
115
- def save()
131
+ def save
116
132
  check_if_persisted!
117
133
  send_request_for_document(:put => @content)
118
134
  end
119
135
 
136
+ # Get a fresh version of this document from the database
137
+ #
138
+ # @return self
139
+ # @api public
140
+ # @example Refresh the document
141
+ # document = Ashikawa::Core::Document.new(database, raw_document)
142
+ # document.refresh!
143
+ def refresh!
144
+ parse_raw_document(send_request_for_document)
145
+ end
146
+
120
147
  protected
121
148
 
122
149
  # Parse information returned from the server
@@ -125,9 +152,9 @@ module Ashikawa
125
152
  # @return self
126
153
  # @api private
127
154
  def parse_raw_document(raw_document)
128
- @id = raw_document['_id']
155
+ @id = raw_document['_id'] || :not_persisted
129
156
  @key = raw_document['_key']
130
- @revision = raw_document['_rev']
157
+ @revision = raw_document['_rev'] || :not_persisted
131
158
  @content = raw_document.delete_if { |key, value| key.start_with?("_") }
132
159
  self
133
160
  end
@@ -1,9 +1,12 @@
1
1
  require 'ashikawa-core/document'
2
+ require 'equalizer'
2
3
 
3
4
  module Ashikawa
4
5
  module Core
5
6
  # A certain Edge within a certain Collection
6
7
  class Edge < Document
8
+ include Equalizer.new(:id, :revision, :from_id, :to_id)
9
+
7
10
  # The ID of the 'from' document
8
11
  #
9
12
  # @return [String]
@@ -7,8 +7,8 @@ module Ashikawa
7
7
  # @param [Integer] status
8
8
  # @return RuntimeError
9
9
  # @api private
10
- def initialize(status)
11
- @status = status
10
+ def initialize(description)
11
+ @description = description
12
12
  end
13
13
 
14
14
  # String representation of the exception
@@ -16,7 +16,7 @@ module Ashikawa
16
16
  # @return String
17
17
  # @api private
18
18
  def to_s
19
- "Status #{@status}: An Error occured in the client"
19
+ @description
20
20
  end
21
21
  end
22
22
  end
@@ -7,8 +7,8 @@ module Ashikawa
7
7
  # @param [Integer] status
8
8
  # @return RuntimeError
9
9
  # @api private
10
- def initialize(status)
11
- @status = status
10
+ def initialize(description)
11
+ @description = description
12
12
  end
13
13
 
14
14
  # String representation of the exception
@@ -16,7 +16,7 @@ module Ashikawa
16
16
  # @return String
17
17
  # @api private
18
18
  def to_s
19
- "Status #{@status}: An Error occured on the server"
19
+ @description
20
20
  end
21
21
  end
22
22
  end
@@ -9,11 +9,12 @@ module Ashikawa
9
9
  # @example Create a new figure from a raw figure
10
10
  # figure = Ashikawa::Core::Figure.new(raw_figure)
11
11
  def initialize(raw_figure)
12
- @datafiles = raw_figure["datafiles"]
13
- @alive = raw_figure["alive"]
14
- @dead = raw_figure["dead"]
15
- @shapes = raw_figure["shapes"]
16
- @journals = raw_figure["journals"]
12
+ @datafiles = raw_figure["datafiles"]
13
+ @alive = raw_figure["alive"]
14
+ @dead = raw_figure["dead"]
15
+ @shapes = raw_figure["shapes"]
16
+ @journals = raw_figure["journals"]
17
+ @attributes = raw_figure["attributes"]
17
18
  end
18
19
 
19
20
  # The number of active datafiles
@@ -125,6 +126,17 @@ module Ashikawa
125
126
  def journals_file_size
126
127
  @journals["fileSize"]
127
128
  end
129
+
130
+ # Number of different attributes that are or have been used in the collection
131
+ #
132
+ # @return Fixnum
133
+ # @api public
134
+ # @example Get the number of attributes
135
+ # figure = Ashikawa::Core::Figure.new(raw_figure)
136
+ # figure.attributes_count #=> 12
137
+ def attributes_count
138
+ @attributes["count"]
139
+ end
128
140
  end
129
141
  end
130
142
  end
@@ -1,7 +1,11 @@
1
+ require 'equalizer'
2
+
1
3
  module Ashikawa
2
4
  module Core
3
5
  # An index on a certain collection
4
6
  class Index
7
+ include Equalizer.new(:id, :on, :type, :unique)
8
+
5
9
  # The fields the index is defined on as symbols
6
10
  #
7
11
  # @return [Array<Symbol>]
@@ -0,0 +1,54 @@
1
+ module Ashikawa
2
+ module Core
3
+ # Options for controlling keys of a collection
4
+ class KeyOptions
5
+ # Either traditional or autoincrement
6
+ #
7
+ # @return Symbol
8
+ # @api public
9
+ # @example Get the type of the KeyOptions
10
+ # keyOptions = KeyOptions.new({ :type => :autoincrement })
11
+ # keyOptions.type # => :autoincrement
12
+ attr_reader :type
13
+
14
+ # A specific start value
15
+ #
16
+ # @return Integer
17
+ # @api public
18
+ # @example Get the type of the KeyOptions
19
+ # keyOptions = KeyOptions.new({ :offset => 12 })
20
+ # keyOptions.offset # => 12
21
+ attr_reader :offset
22
+
23
+ # Size of increment steps
24
+ #
25
+ # @return Integer
26
+ # @api public
27
+ # @example Get the type of the KeyOptions
28
+ # keyOptions = KeyOptions.new({ :increment => 12 })
29
+ # keyOptions.increment # => 12
30
+ attr_reader :increment
31
+
32
+ # Is the user allowed to set keys by him- or herself?
33
+ #
34
+ # @return Boolean
35
+ # @api public
36
+ # @example Get the type of the KeyOptions
37
+ # keyOptions = KeyOptions.new({ :allowUserKeys => true })
38
+ # keyOptions.allow_user_keys # => true
39
+ attr_reader :allow_user_keys
40
+
41
+ # Create a new KeyOptions object from the raw key options
42
+ #
43
+ # @api public
44
+ # @example Create a new KeyOptions object
45
+ # KeyOptions.new({ :type => :autoincrement })
46
+ def initialize(raw_key_options)
47
+ @type = raw_key_options["type"]
48
+ @offset = raw_key_options["offset"]
49
+ @increment = raw_key_options["increment"]
50
+ @allow_user_keys = raw_key_options["allowUserKeys"]
51
+ end
52
+ end
53
+ end
54
+ end
@@ -3,7 +3,6 @@ require 'ashikawa-core/document'
3
3
  require 'ashikawa-core/exceptions/no_collection_provided'
4
4
  require 'ashikawa-core/exceptions/client_error/bad_syntax'
5
5
  require 'forwardable'
6
- require 'backports'
7
6
 
8
7
  module Ashikawa
9
8
  module Core
@@ -11,6 +10,17 @@ module Ashikawa
11
10
  class Query
12
11
  extend Forwardable
13
12
 
13
+ ALLOWED_KEYS_FOR_PATH = {
14
+ "simple/all" => [:limit, :skip, :collection],
15
+ "simple/by-example" => [:limit, :skip, :example, :collection],
16
+ "simple/near" => [:latitude, :longitude, :distance, :skip, :limit, :geo, :collection],
17
+ "simple/within" => [:latitude, :longitude, :radius, :distance, :skip, :limit, :geo, :collection],
18
+ "simple/range" => [:attribute, :left, :right, :closed, :limit, :skip, :collection],
19
+ "cursor" => [:query, :count, :batch_size, :collection],
20
+ "query" => [:query],
21
+ "simple/first-example" => [:example, :collection]
22
+ }
23
+
14
24
  # Delegate sending requests to the connection
15
25
  def_delegator :@connection, :send_request
16
26
 
@@ -37,10 +47,8 @@ module Ashikawa
37
47
  # @example Get an array with all documents
38
48
  # query = Ashikawa::Core::Query.new(collection)
39
49
  # query.all # => #<Cursor id=33>
40
- def all(options={})
41
- simple_query_request("simple/all",
42
- options,
43
- [:limit, :skip])
50
+ def all(options = {})
51
+ simple_query_request("simple/all", options)
44
52
  end
45
53
 
46
54
  # Looks for documents in a collection which match the given criteria
@@ -55,10 +63,8 @@ module Ashikawa
55
63
  # @example Find all documents in a collection that are red
56
64
  # query = Ashikawa::Core::Query.new(collection)
57
65
  # query.by_example({ "color" => "red" }, :options => { :limit => 1 }) #=> #<Cursor id=2444>
58
- def by_example(example={}, options={})
59
- simple_query_request("simple/by-example",
60
- { :example => example }.merge(options),
61
- [:limit, :skip, :example])
66
+ def by_example(example = {}, options = {})
67
+ simple_query_request("simple/by-example", { :example => example }.merge(options))
62
68
  end
63
69
 
64
70
  # Looks for one document in a collection which matches the given criteria
@@ -71,9 +77,9 @@ module Ashikawa
71
77
  # query = Ashikawa::Core::Query.new(collection)
72
78
  # query.first_example({ "color" => "red"}) # => #<Document id=2444 color="red">
73
79
  def first_example(example = {})
74
- simple_query_request("simple/first-example",
75
- { :example => example },
76
- [:example])
80
+ request = prepare_request("simple/first-example", { :example => example, :collection => collection.name })
81
+ response = send_request("simple/first-example", { :put => request })
82
+ Document.new(database, response["document"])
77
83
  end
78
84
 
79
85
  # Looks for documents in a collection based on location
@@ -90,10 +96,8 @@ module Ashikawa
90
96
  # @example Find all documents at Infinite Loop
91
97
  # query = Ashikawa::Core::Query.new(collection)
92
98
  # query.near(:latitude => 37.331693, :longitude => -122.030468)
93
- def near(options={})
94
- simple_query_request("simple/near",
95
- options,
96
- [:latitude, :longitude, :distance, :skip, :limit, :geo])
99
+ def near(options = {})
100
+ simple_query_request("simple/near", options)
97
101
  end
98
102
 
99
103
  # Looks for documents in a collection within a radius
@@ -111,10 +115,8 @@ module Ashikawa
111
115
  # @example Find all documents within a radius of 100 to Infinite Loop
112
116
  # query = Ashikawa::Core::Query.new(collection)
113
117
  # query.within(:latitude => 37.331693, :longitude => -122.030468, :radius => 100)
114
- def within(options={})
115
- simple_query_request("simple/within",
116
- options,
117
- [:latitude, :longitude, :radius, :distance, :skip, :limit, :geo])
118
+ def within(options = {})
119
+ simple_query_request("simple/within", options)
118
120
  end
119
121
 
120
122
  # Looks for documents in a collection with an attribute between two values
@@ -131,10 +133,8 @@ module Ashikawa
131
133
  # @example Find all documents within a radius of 100 to Infinite Loop
132
134
  # query = Ashikawa::Core::Query.new(collection)
133
135
  # query.within(:latitude => 37.331693, :longitude => -122.030468, :radius => 100)
134
- def in_range(options={})
135
- simple_query_request("simple/range",
136
- options,
137
- [:attribute, :left, :right, :closed, :limit, :skip])
136
+ def in_range(options = {})
137
+ simple_query_request("simple/range", options)
138
138
  end
139
139
 
140
140
  # Send an AQL query to the database
@@ -148,9 +148,7 @@ module Ashikawa
148
148
  # query = Ashikawa::Core::Query.new(collection)
149
149
  # query.execute("FOR u IN users LIMIT 2") # => #<Cursor id=33>
150
150
  def execute(query, options = {})
151
- post_request("cursor",
152
- options.merge({ :query => query }),
153
- [:query, :count, :batch_size])
151
+ wrapped_request("cursor", :post, options.merge({ :query => query }))
154
152
  end
155
153
 
156
154
  # Test if an AQL query is valid
@@ -162,7 +160,7 @@ module Ashikawa
162
160
  # query = Ashikawa::Core::Query.new(collection)
163
161
  # query.valid?("FOR u IN users LIMIT 2") # => true
164
162
  def valid?(query)
165
- !!post_request("query", { :query => query })
163
+ !!wrapped_request("query", :post, { :query => query })
166
164
  rescue Ashikawa::Core::BadSyntax
167
165
  false
168
166
  end
@@ -192,71 +190,38 @@ module Ashikawa
192
190
  # @param [Array<Symbol>] allowed_keys
193
191
  # @return [Hash] The filtered Hash
194
192
  # @api private
195
- def allowed_options(options, allowed_keys)
193
+ def prepare_request(path, options)
194
+ allowed_keys = ALLOWED_KEYS_FOR_PATH.fetch(path)
196
195
  options.keep_if { |key, _| allowed_keys.include?(key) }
197
- end
198
-
199
- # Transforms the keys into the required format
200
- #
201
- # @param [Hash] request_data
202
- # @return [Hash] Cleaned request data
203
- # @api private
204
- def prepare_request_data(request_data)
205
- Hash[request_data.map { |key, value|
196
+ Hash[options.map { |key, value|
206
197
  [key.to_s.gsub(/_(.)/) { $1.upcase }, value]
207
- }].reject { |_, value| value.nil? }
198
+ }]
208
199
  end
209
200
 
210
201
  # Send a simple query to the server
211
202
  #
212
203
  # @param [String] path The path for the request
213
- # @param [Hash] request_data The data send to the database
204
+ # @param [Hash] request The data send to the database
214
205
  # @param [Array<Symbol>] allowed_keys The keys allowed for this request
215
206
  # @return [String] Server response
216
207
  # @raise [NoCollectionProvidedException] If you provided a database, no collection
217
208
  # @api private
218
- def simple_query_request(path, request_data, allowed_keys)
219
- request_data = request_data.merge({ :collection => collection.name })
220
- put_request(path,
221
- request_data,
222
- allowed_keys << :collection)
209
+ def simple_query_request(path, request)
210
+ wrapped_request(path, :put, request.merge({ :collection => collection.name }))
223
211
  end
224
212
 
225
213
  # Perform a wrapped request
226
214
  #
227
215
  # @param [String] path The path for the request
228
216
  # @param [Symbol] request_method The request method to perform
229
- # @param [Hash] request_data The data send to the database
230
- # @param [Array] allowed_keys Keys allowed in request_data, if nil: All keys are allowed
231
- # @return [Cursor]
232
- # @api private
233
- def wrapped_request(path, request_method, request_data, allowed_keys)
234
- request_data = allowed_options(request_data, allowed_keys) unless allowed_keys.nil?
235
- request_data = prepare_request_data(request_data)
236
- server_response = send_request(path, { request_method => request_data })
237
- Cursor.new(database, server_response)
238
- end
239
-
240
- # Perform a wrapped put request
241
- #
242
- # @param [String] path The path for the request
243
- # @param [Hash] request_data The data send to the database
244
- # @param [Array] allowed_keys Keys allowed in request_data, if nil: All keys are allowed
245
- # @return [Cursor]
246
- # @api private
247
- def put_request(path, request_data, allowed_keys = nil)
248
- wrapped_request(path, :put, request_data, allowed_keys)
249
- end
250
-
251
- # Perform a wrapped post request
252
- #
253
- # @param [String] path The path for the request
254
- # @param [Hash] request_data The data send to the database
255
- # @param [Array] allowed_keys Keys allowed in request_data, if nil: All keys are allowed
217
+ # @param [Hash] request The data send to the database
218
+ # @param [Array] allowed_keys Keys allowed in request, if nil: All keys are allowed
256
219
  # @return [Cursor]
257
220
  # @api private
258
- def post_request(path, request_data, allowed_keys = nil)
259
- wrapped_request(path, :post, request_data, allowed_keys)
221
+ def wrapped_request(path, request_method, request)
222
+ request = prepare_request(path, request)
223
+ response = send_request(path, { request_method => request })
224
+ Cursor.new(database, response)
260
225
  end
261
226
  end
262
227
  end
@@ -1,5 +1,5 @@
1
1
  require "faraday"
2
- require "multi_json"
2
+ require "json"
3
3
 
4
4
  module Ashikawa
5
5
  module Core
@@ -23,7 +23,7 @@ module Ashikawa
23
23
  # @api private
24
24
  def call(env)
25
25
  body = env[:body]
26
- env[:body] = MultiJson.dump(body) if body
26
+ env[:body] = JSON.generate(body) if body
27
27
  log(env[:method], env[:url], body)
28
28
  @app.call(env)
29
29
  end