ashikawa-core 0.7.2 → 0.8.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 (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