guacamole 0.0.1

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 (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