guacamole 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (47) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +18 -0
  3. data/.rspec +3 -0
  4. data/.ruby-version +1 -0
  5. data/.travis.yml +18 -0
  6. data/CONTRIBUTING.md +26 -0
  7. data/Gemfile +16 -0
  8. data/Gemfile.devtools +55 -0
  9. data/Guardfile +13 -0
  10. data/LICENSE +202 -0
  11. data/README.md +130 -0
  12. data/Rakefile +7 -0
  13. data/config/devtools.yml +4 -0
  14. data/config/flay.yml +3 -0
  15. data/config/flog.yml +2 -0
  16. data/config/mutant.yml +3 -0
  17. data/config/reek.yml +104 -0
  18. data/config/rubocop.yml +68 -0
  19. data/config/yardstick.yml +2 -0
  20. data/guacamole.gemspec +28 -0
  21. data/lib/guacamole.rb +19 -0
  22. data/lib/guacamole/collection.rb +246 -0
  23. data/lib/guacamole/configuration.rb +123 -0
  24. data/lib/guacamole/document_model_mapper.rb +95 -0
  25. data/lib/guacamole/model.rb +210 -0
  26. data/lib/guacamole/query.rb +72 -0
  27. data/lib/guacamole/railtie.rb +27 -0
  28. data/lib/guacamole/railtie/database.rake +5 -0
  29. data/lib/guacamole/version.rb +5 -0
  30. data/log/.gitkeep +0 -0
  31. data/spec/acceptance/.gitkeep +0 -0
  32. data/spec/acceptance/basic_spec.rb +112 -0
  33. data/spec/acceptance/config/guacamole.yml +11 -0
  34. data/spec/acceptance/spec_helper.rb +61 -0
  35. data/spec/fabricators/article_fabricator.rb +10 -0
  36. data/spec/fabricators/comment_fabricator.rb +5 -0
  37. data/spec/setup/arangodb.sh +65 -0
  38. data/spec/spec_helper.rb +19 -0
  39. data/spec/unit/.gitkeep +0 -0
  40. data/spec/unit/collection_spec.rb +380 -0
  41. data/spec/unit/configuration_spec.rb +119 -0
  42. data/spec/unit/document_model_mapper_spec.rb +120 -0
  43. data/spec/unit/example_spec.rb +8 -0
  44. data/spec/unit/model_spec.rb +116 -0
  45. data/spec/unit/query_spec.rb +140 -0
  46. data/tasks/adjustments.rake +35 -0
  47. metadata +191 -0
data/Rakefile ADDED
@@ -0,0 +1,7 @@
1
+ # -*- encoding : utf-8 -*-
2
+ require 'bundler/gem_tasks'
3
+
4
+ require 'devtools'
5
+ Devtools.init_rake_tasks
6
+
7
+ import('./tasks/adjustments.rake')
@@ -0,0 +1,4 @@
1
+ ---
2
+ unit_test_timeout: 0.1
3
+ fail_on_branch:
4
+ - "master"
data/config/flay.yml ADDED
@@ -0,0 +1,3 @@
1
+ ---
2
+ threshold: 7
3
+ total_score: 74
data/config/flog.yml ADDED
@@ -0,0 +1,2 @@
1
+ ---
2
+ threshold: 0
data/config/mutant.yml ADDED
@@ -0,0 +1,3 @@
1
+ ---
2
+ name: guacamole
3
+ namespace: Guacamole
data/config/reek.yml ADDED
@@ -0,0 +1,104 @@
1
+ ---
2
+ Attribute:
3
+ enabled: false
4
+ exclude: []
5
+ BooleanParameter:
6
+ enabled: true
7
+ exclude: []
8
+ ClassVariable:
9
+ enabled: true
10
+ exclude: []
11
+ ControlParameter:
12
+ enabled: false
13
+ exclude: []
14
+ DataClump:
15
+ enabled: true
16
+ exclude: []
17
+ max_copies: 2
18
+ min_clump_size: 2
19
+ DuplicateMethodCall:
20
+ enabled: true
21
+ exclude: []
22
+ max_calls: 1
23
+ allow_calls: []
24
+ FeatureEnvy:
25
+ enabled: false
26
+ exclude: []
27
+ IrresponsibleModule:
28
+ enabled: true
29
+ exclude: []
30
+ LongParameterList:
31
+ enabled: true
32
+ exclude: []
33
+ max_params: 2
34
+ overrides:
35
+ initialize:
36
+ max_params: 3
37
+ LongYieldList:
38
+ enabled: true
39
+ exclude: []
40
+ max_params: 2
41
+ NestedIterators:
42
+ enabled: true
43
+ exclude: []
44
+ max_allowed_nesting: 2
45
+ ignore_iterators: []
46
+ NilCheck:
47
+ enabled: true
48
+ exclude: []
49
+ RepeatedConditional:
50
+ enabled: true
51
+ exclude: []
52
+ max_ifs: 2
53
+ TooManyInstanceVariables:
54
+ enabled: true
55
+ exclude: []
56
+ max_instance_variables: 3
57
+ TooManyMethods:
58
+ enabled: true
59
+ exclude: []
60
+ max_methods: 10
61
+ TooManyStatements:
62
+ enabled: true
63
+ exclude:
64
+ - each
65
+ max_statements: 5
66
+ UncommunicativeMethodName:
67
+ enabled: true
68
+ exclude: []
69
+ reject:
70
+ - !ruby/regexp /^[a-z]$/
71
+ - !ruby/regexp /[0-9]$/
72
+ - !ruby/regexp /[A-Z]/
73
+ accept: []
74
+ UncommunicativeModuleName:
75
+ enabled: true
76
+ exclude: []
77
+ reject:
78
+ - !ruby/regexp /^.$/
79
+ - !ruby/regexp /[0-9]$/
80
+ accept: []
81
+ UncommunicativeParameterName:
82
+ enabled: true
83
+ exclude: []
84
+ reject:
85
+ - !ruby/regexp /^.$/
86
+ - !ruby/regexp /[0-9]$/
87
+ - !ruby/regexp /[A-Z]/
88
+ accept: []
89
+ UncommunicativeVariableName:
90
+ enabled: true
91
+ exclude: []
92
+ reject:
93
+ - !ruby/regexp /^.$/
94
+ - !ruby/regexp /[0-9]$/
95
+ - !ruby/regexp /[A-Z]/
96
+ accept: []
97
+ UnusedParameters:
98
+ enabled: true
99
+ exclude:
100
+ - Guacamole::Model#==
101
+ UtilityFunction:
102
+ enabled: false
103
+ exclude: []
104
+ max_helper_calls: 0
@@ -0,0 +1,68 @@
1
+ AllCops:
2
+ Includes:
3
+ - '**/*.rake'
4
+ - 'Guardfile'
5
+ - 'Rakefile'
6
+ - 'Gemfile'
7
+ - 'Gemfile.devtools'
8
+ Excludes:
9
+ - '**/spec/setup/**'
10
+ - '**/vendor/**'
11
+ - '**/benchmarks/**'
12
+
13
+ # Avoid parameter lists longer than five parameters.
14
+ ParameterLists:
15
+ Max: 3
16
+ CountKeywordArgs: true
17
+
18
+ # Avoid more than `Max` levels of nesting.
19
+ BlockNesting:
20
+ Max: 3
21
+
22
+ # Align with the style guide.
23
+ CollectionMethods:
24
+ PreferredMethods:
25
+ collect: 'map'
26
+ inject: 'reduce'
27
+ find: 'detect'
28
+ find_all: 'select'
29
+
30
+ SignalException:
31
+ # Valid values are: semantic, only_raise and only_fail
32
+ EnforcedStyle: only_raise
33
+
34
+ # Do not force public/protected/private keyword to be indented at the same
35
+ # level as the def keyword. My personal preference is to outdent these keywords
36
+ # because I think when scanning code it makes it easier to identify the
37
+ # sections of code and visually separate them. When the keyword is at the same
38
+ # level I think it sort of blends in with the def keywords and makes it harder
39
+ # to scan the code and see where the sections are.
40
+ AccessControl:
41
+ Enabled: false
42
+
43
+ # Limit line length
44
+ LineLength:
45
+ Max: 120
46
+
47
+ # Disable documentation checking until a class needs to be documented once
48
+ Documentation:
49
+ Enabled: false
50
+
51
+ # Do not favor modifier if/unless usage when you have a single-line body
52
+ IfUnlessModifier:
53
+ Enabled: false
54
+
55
+ # Allow case equality operator (in limited use within the specs)
56
+ CaseEquality:
57
+ Enabled: false
58
+
59
+ # Constants do not always have to use SCREAMING_SNAKE_CASE
60
+ ConstantName:
61
+ Enabled: false
62
+
63
+ # Not all trivial readers/writers can be defined with attr_* methods
64
+ TrivialAccessors:
65
+ Enabled: false
66
+
67
+ RegexpLiteral:
68
+ Enabled: false
@@ -0,0 +1,2 @@
1
+ ---
2
+ threshold: 100
data/guacamole.gemspec ADDED
@@ -0,0 +1,28 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'guacamole/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = 'guacamole'
8
+ spec.version = Guacamole::VERSION
9
+ spec.authors = ['Lucas Dohmen', 'Dirk Breuer']
10
+ spec.email = ['moonglum@moonbeamlabs.com', 'dirk.breuer@gmail.com']
11
+ spec.description = %q{ODM for ArangoDB}
12
+ spec.summary = %q{An ODM for ArangoDB that uses the DataMapper pattern.}
13
+ spec.homepage = 'https://github.com/triAGENS/guacamole'
14
+ spec.license = 'Apache License 2.0'
15
+
16
+ spec.files = `git ls-files`.split($/)
17
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
18
+ spec.test_files = spec.files.grep(%r{^(spec)/})
19
+ spec.require_paths = ['lib']
20
+
21
+ spec.add_dependency 'ashikawa-core', '~> 0.9.0'
22
+ spec.add_dependency 'virtus', '~> 1.0.0.rc2'
23
+ spec.add_dependency 'activesupport', '>= 4.0.0'
24
+ spec.add_dependency 'activemodel', '>= 4.0.0'
25
+
26
+ spec.add_development_dependency 'fabrication', '~> 2.8.1'
27
+ spec.add_development_dependency 'logging', '~> 1.8.1'
28
+ end
data/lib/guacamole.rb ADDED
@@ -0,0 +1,19 @@
1
+ # -*- encoding : utf-8 -*-
2
+
3
+ require 'active_support/core_ext'
4
+
5
+ require 'guacamole/version'
6
+ require 'guacamole/configuration'
7
+ require 'guacamole/model'
8
+ require 'guacamole/collection'
9
+ require 'guacamole/document_model_mapper'
10
+
11
+ if defined?(Rails)
12
+ require 'guacamole/railtie'
13
+ end
14
+
15
+ # An ODM for ArangoDB
16
+ #
17
+ # For more general information, see README or Homepage
18
+ module Guacamole
19
+ end
@@ -0,0 +1,246 @@
1
+ # -*- encoding : utf-8 -*-
2
+
3
+ require 'guacamole/query'
4
+
5
+ require 'ashikawa-core'
6
+ require 'active_support/concern'
7
+ require 'active_support/core_ext/string/inflections'
8
+
9
+ module Guacamole
10
+ # A collection persists and offers querying for models
11
+ #
12
+ # You use this as a mixin in your collection classes. Per convention,
13
+ # they are the plural form of your model with the suffix `Collection`.
14
+ # For example the collection of `Blogpost` models would be `BlogpostsCollection`.
15
+ # Including `Guacamole::Collection` will add a number of class methods to
16
+ # the collection. See the `ClassMethods` submodule for details
17
+ module Collection
18
+ extend ActiveSupport::Concern
19
+
20
+ # The class methods added to the class via the mixin
21
+ #
22
+ # @!method model_to_document(model)
23
+ # Convert a model to a document to save it to the database
24
+ #
25
+ # You can use this method for your hand made storage or update methods.
26
+ # Most of the time it makes more sense to call save or replace though,
27
+ # they do the conversion and handle the communication with the database
28
+ #
29
+ # @param [Model] model The model to be converted
30
+ # @return [Ashikawa::Core::Document] The converted document
31
+ module ClassMethods
32
+ extend Forwardable
33
+ def_delegators :mapper, :model_to_document
34
+ def_delegator :connection, :fetch, :fetch_document
35
+
36
+ attr_accessor :connection, :mapper, :database
37
+
38
+ # The raw `Database` object that was configured
39
+ #
40
+ # You can use this method for low level communication with the database.
41
+ # Details can be found in the Ashikawa::Core documentation.
42
+ #
43
+ # @see http://rubydoc.info/gems/ashikawa-core/Ashikawa/Core/Database
44
+ # @return [Ashikawa::Core::Database]
45
+ def database
46
+ @database ||= Guacamole.configuration.database
47
+ end
48
+
49
+ # The raw `Collection` object for this collection
50
+ #
51
+ # You can use this method for low level communication with the collection.
52
+ # Details can be found in the Ashikawa::Core documentation.
53
+ #
54
+ # @see http://rubydoc.info/gems/ashikawa-core/Ashikawa/Core/Collection
55
+ # @return [Ashikawa::Core::Collection]
56
+ def connection
57
+ @connection ||= database[collection_name]
58
+ end
59
+
60
+ # The DocumentModelMapper for this collection
61
+ #
62
+ # @api private
63
+ # @return [DocumentModelMapper]
64
+ def mapper
65
+ @mapper ||= Guacamole.configuration.default_mapper.new(model_class)
66
+ end
67
+
68
+ # The name of the collection in ArangoDB
69
+ #
70
+ # Use this method in your hand crafted AQL queries, for debugging etc.
71
+ #
72
+ # @return [String] The name
73
+ def collection_name
74
+ @collection_name ||= name.gsub(/Collection\z/, '').underscore
75
+ end
76
+
77
+ # The class of the resulting models
78
+ #
79
+ # @return [Class] The model class
80
+ def model_class
81
+ @model_class ||= collection_name.singularize.camelcase.constantize
82
+ end
83
+
84
+ # Find a model by its key
85
+ #
86
+ # The key is the unique identifier of a document within a collection,
87
+ # this concept is similar to the concept of IDs in most databases.
88
+ #
89
+ # @param [String] key
90
+ # @return [Model] The model with the given key
91
+ # @example Find a podcast by its key
92
+ # podcast = PodcastsCollection.by_key('27214247')
93
+ def by_key(key)
94
+ raise Ashikawa::Core::DocumentNotFoundException unless key
95
+
96
+ mapper.document_to_model connection.fetch(key)
97
+ end
98
+
99
+ # Persist a model in the collection
100
+ #
101
+ # The model will be saved in the collection. Timestamps, revision
102
+ # and key will be set on the model.
103
+ #
104
+ # @param [Model] model The model to be saved
105
+ # @return [Model] The provided model
106
+ # @example Save a podcast to the database
107
+ # podcast = Podcast.new(title: 'Best Show', guest: 'Dirk Breuer')
108
+ # PodcastsCollection.save(podcast)
109
+ # podcast.key #=> '27214247'
110
+ def save(model)
111
+ return false unless model.valid?
112
+
113
+ add_timestamps_to_model(model)
114
+ create_document_from(model)
115
+ model
116
+ end
117
+
118
+ # Delete a model from the database
119
+ #
120
+ # @param [String, Model] model_or_key The key of the model or a model
121
+ # @return [String] The key
122
+ # @example Delete a podcast by key
123
+ # PodcastsCollection.delete(podcast.key)
124
+ # @example Delete a podcast by model
125
+ # PodcastsCollection.delete(podcast)
126
+ def delete(model_or_key)
127
+ key = if model_or_key.respond_to? :key
128
+ model_or_key.key
129
+ else
130
+ model_or_key
131
+ end
132
+ fetch_document(key).delete
133
+ key
134
+ end
135
+
136
+ # Replace a model in the database with its new version
137
+ #
138
+ # Replaces the currently saved version of the model with
139
+ # its new version. It searches for the entry in the database
140
+ # by key. This will change the updated_at timestamp and revision
141
+ # of the provided model.
142
+ #
143
+ # @param [Model] model The model to be replaced
144
+ # @return [Model] The model
145
+ # @example Get a podcast, update its title, replace it
146
+ # podcast = PodcastsCollection.by_key('27214247')
147
+ # podcast.title = 'Even better'
148
+ # PodcastsCollection.replace(podcast)
149
+ def replace(model)
150
+ return false unless model.valid?
151
+
152
+ model.updated_at = Time.now
153
+ replace_document_from(model)
154
+ model
155
+ end
156
+
157
+ # Find models by the provided attributes
158
+ #
159
+ # Search for models in the collection where the attributes are equal
160
+ # to those that you provided.
161
+ # This returns a Query object, where you can provide additional information
162
+ # like limiting the results. See the documentation of Query or the examples
163
+ # for more information.
164
+ # All methods of the Enumerable module and `.to_a` will lead to the execution
165
+ # of the query.
166
+ #
167
+ # @param [Hash] example The attributes and their values
168
+ # @return [Query]
169
+ # @example Get all podcasts with the title 'Best Podcast'
170
+ # podcasts = PodcastsCollection.by_example(title: 'Best Podcast').to_a
171
+ # @example Get the second batch of podcasts for batches of 10 with the title 'Best Podcast'
172
+ # podcasts = PodcastsCollection.by_example(title: 'Best Podcast').skip(10).limit(10).to_a
173
+ # @example Iterate over all podcasts with the title 'Best Podcasts'
174
+ # PodcastsCollection.by_example(title: 'Best Podcast').each do |podcast|
175
+ # p podcast
176
+ # end
177
+ def by_example(example)
178
+ query = all
179
+ query.example = example
180
+ query
181
+ end
182
+
183
+ # Get all Models stored in the collection
184
+ #
185
+ # The result can be limited (and should be for most datasets)
186
+ # This can be done one the returned Query object.
187
+ # All methods of the Enumerable module and `.to_a` will lead to the execution
188
+ # of the query.
189
+ #
190
+ # @return [Query]
191
+ # @example Get all podcasts
192
+ # podcasts = PodcastsCollection.all.to_a
193
+ # @example Get the first 50 podcasts
194
+ # podcasts = PodcastsCollection.all.limit(50).to_a
195
+ def all
196
+ Query.new(connection.query, mapper)
197
+ end
198
+
199
+ # Specify details on the mapping
200
+ #
201
+ # The method is called with a block where you can specify
202
+ # details about the way that the data from the database
203
+ # is mapped to models.
204
+ #
205
+ # See `DocumentModelMapper` for details on how to configure
206
+ # the mapper.
207
+ def map(&block)
208
+ mapper.instance_eval(&block)
209
+ end
210
+
211
+ # Timestamp a fresh model
212
+ #
213
+ # @api private
214
+ def add_timestamps_to_model(model)
215
+ timestamp = Time.now
216
+ model.created_at = timestamp
217
+ model.updated_at = timestamp
218
+ end
219
+
220
+ # Create a document from a model
221
+ #
222
+ # @api private
223
+ def create_document_from(model)
224
+ document = connection.create_document(model_to_document(model))
225
+
226
+ model.key = document.key
227
+ model.rev = document.revision
228
+
229
+ document
230
+ end
231
+
232
+ # Replace a document in the database with this model
233
+ #
234
+ # @api private
235
+ def replace_document_from(model)
236
+ document = model_to_document(model)
237
+ response = connection.replace(model.key, document)
238
+
239
+ model.rev = response['_rev']
240
+
241
+ document
242
+ end
243
+
244
+ end
245
+ end
246
+ end