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.
- checksums.yaml +7 -0
- data/.gitignore +18 -0
- data/.rspec +3 -0
- data/.ruby-version +1 -0
- data/.travis.yml +18 -0
- data/CONTRIBUTING.md +26 -0
- data/Gemfile +16 -0
- data/Gemfile.devtools +55 -0
- data/Guardfile +13 -0
- data/LICENSE +202 -0
- data/README.md +130 -0
- data/Rakefile +7 -0
- data/config/devtools.yml +4 -0
- data/config/flay.yml +3 -0
- data/config/flog.yml +2 -0
- data/config/mutant.yml +3 -0
- data/config/reek.yml +104 -0
- data/config/rubocop.yml +68 -0
- data/config/yardstick.yml +2 -0
- data/guacamole.gemspec +28 -0
- data/lib/guacamole.rb +19 -0
- data/lib/guacamole/collection.rb +246 -0
- data/lib/guacamole/configuration.rb +123 -0
- data/lib/guacamole/document_model_mapper.rb +95 -0
- data/lib/guacamole/model.rb +210 -0
- data/lib/guacamole/query.rb +72 -0
- data/lib/guacamole/railtie.rb +27 -0
- data/lib/guacamole/railtie/database.rake +5 -0
- data/lib/guacamole/version.rb +5 -0
- data/log/.gitkeep +0 -0
- data/spec/acceptance/.gitkeep +0 -0
- data/spec/acceptance/basic_spec.rb +112 -0
- data/spec/acceptance/config/guacamole.yml +11 -0
- data/spec/acceptance/spec_helper.rb +61 -0
- data/spec/fabricators/article_fabricator.rb +10 -0
- data/spec/fabricators/comment_fabricator.rb +5 -0
- data/spec/setup/arangodb.sh +65 -0
- data/spec/spec_helper.rb +19 -0
- data/spec/unit/.gitkeep +0 -0
- data/spec/unit/collection_spec.rb +380 -0
- data/spec/unit/configuration_spec.rb +119 -0
- data/spec/unit/document_model_mapper_spec.rb +120 -0
- data/spec/unit/example_spec.rb +8 -0
- data/spec/unit/model_spec.rb +116 -0
- data/spec/unit/query_spec.rb +140 -0
- data/tasks/adjustments.rake +35 -0
- metadata +191 -0
data/Rakefile
ADDED
data/config/devtools.yml
ADDED
data/config/flay.yml
ADDED
data/config/flog.yml
ADDED
data/config/mutant.yml
ADDED
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
|
data/config/rubocop.yml
ADDED
@@ -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
|
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
|