guacamole 0.2.0 → 0.3.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.hound.yml +3 -0
- data/{config/reek.yml → .reek.yml} +5 -0
- data/.travis.yml +3 -0
- data/CHANGELOG.md +19 -0
- data/GOALS.md +20 -0
- data/Gemfile +1 -11
- data/Guardfile +0 -4
- data/README.md +147 -11
- data/Rakefile +33 -3
- data/guacamole.gemspec +12 -1
- data/lib/guacamole.rb +1 -0
- data/lib/guacamole/callbacks.rb +259 -0
- data/lib/guacamole/collection.rb +50 -32
- data/lib/guacamole/configuration.rb +92 -15
- data/lib/guacamole/model.rb +39 -1
- data/lib/guacamole/railtie.rb +9 -3
- data/lib/guacamole/version.rb +1 -1
- data/lib/rails/generators/guacamole/callbacks/callbacks_generator.rb +26 -0
- data/lib/rails/generators/guacamole/callbacks/templates/callbacks.rb.tt +13 -0
- data/lib/rails/generators/rspec/callbacks/callbacks_generator.rb +14 -0
- data/lib/rails/generators/rspec/callbacks/templates/callbacks_spec.rb.tt +7 -0
- data/lib/rails/generators/test_unit/callbacks/callbacks_generator.rb +13 -0
- data/lib/rails/generators/test_unit/callbacks/templates/callbacks_test.rb.tt +9 -0
- data/lib/rails/generators/test_unit/collection/collection_generator.rb +13 -0
- data/lib/rails/generators/test_unit/collection/templates/collection_test.rb.tt +10 -0
- data/spec/acceptance/aql_spec.rb +0 -12
- data/spec/acceptance/basic_spec.rb +16 -25
- data/spec/acceptance/callbacks_spec.rb +181 -0
- data/spec/acceptance/config/guacamole.yml +1 -1
- data/spec/acceptance/spec_helper.rb +24 -6
- data/spec/fabricators/article.rb +12 -0
- data/spec/fabricators/comment.rb +7 -0
- data/spec/fabricators/pony.rb +6 -4
- data/spec/fabricators/pony_fabricator.rb +7 -0
- data/spec/spec_helper.rb +5 -4
- data/spec/support/guacamole.yml.erb +5 -0
- data/spec/unit/callbacks_spec.rb +139 -0
- data/spec/unit/collection_spec.rb +85 -66
- data/spec/unit/configuration_spec.rb +165 -21
- data/spec/unit/identiy_map_spec.rb +2 -2
- data/spec/unit/model_spec.rb +36 -3
- metadata +181 -12
- data/Gemfile.devtools +0 -67
- data/config/devtools.yml +0 -4
- data/config/flay.yml +0 -3
- data/config/flog.yml +0 -2
- data/config/mutant.yml +0 -3
- data/config/yardstick.yml +0 -2
- data/tasks/adjustments.rake +0 -34
data/lib/guacamole/collection.rb
CHANGED
@@ -24,7 +24,7 @@ module Guacamole
|
|
24
24
|
# Convert a model to a document to save it to the database
|
25
25
|
#
|
26
26
|
# You can use this method for your hand made storage or update methods.
|
27
|
-
# Most of the time it makes more sense to call save or
|
27
|
+
# Most of the time it makes more sense to call save or update though,
|
28
28
|
# they do the conversion and handle the communication with the database
|
29
29
|
#
|
30
30
|
# @param [Model] model The model to be converted
|
@@ -97,10 +97,10 @@ module Guacamole
|
|
97
97
|
def by_key(key)
|
98
98
|
raise Ashikawa::Core::DocumentNotFoundException unless key
|
99
99
|
|
100
|
-
mapper.document_to_model
|
100
|
+
mapper.document_to_model fetch_document(key)
|
101
101
|
end
|
102
102
|
|
103
|
-
# Persist a model in the collection or
|
103
|
+
# Persist a model in the collection or update it in the database, depending if it is already persisted
|
104
104
|
#
|
105
105
|
# * If {Model#persisted? model#persisted?} is `false`, the model will be saved in the collection.
|
106
106
|
# Timestamps, revision and key will be set on the model.
|
@@ -109,7 +109,7 @@ module Guacamole
|
|
109
109
|
# by key. This will change the updated_at timestamp and revision
|
110
110
|
# of the provided model.
|
111
111
|
#
|
112
|
-
# See also {#create create} and {#
|
112
|
+
# See also {#create create} and {#update update} for explicit usage.
|
113
113
|
#
|
114
114
|
# @param [Model] model The model to be saved
|
115
115
|
# @return [Model] The provided model
|
@@ -117,12 +117,12 @@ module Guacamole
|
|
117
117
|
# podcast = Podcast.new(title: 'Best Show', guest: 'Dirk Breuer')
|
118
118
|
# PodcastsCollection.save(podcast)
|
119
119
|
# podcast.key #=> '27214247'
|
120
|
-
# @example Get a podcast, update its title,
|
120
|
+
# @example Get a podcast, update its title, update it
|
121
121
|
# podcast = PodcastsCollection.by_key('27214247')
|
122
122
|
# podcast.title = 'Even better'
|
123
123
|
# PodcastsCollection.save(podcast)
|
124
124
|
def save(model)
|
125
|
-
model.persisted? ?
|
125
|
+
model.persisted? ? update(model) : create(model)
|
126
126
|
end
|
127
127
|
|
128
128
|
# Persist a model in the collection
|
@@ -139,13 +139,17 @@ module Guacamole
|
|
139
139
|
def create(model)
|
140
140
|
return false unless model.valid?
|
141
141
|
|
142
|
-
|
143
|
-
|
142
|
+
callbacks(model).run_callbacks :save, :create do
|
143
|
+
create_document_from(model)
|
144
|
+
end
|
144
145
|
model
|
145
146
|
end
|
146
147
|
|
147
148
|
# Delete a model from the database
|
148
149
|
#
|
150
|
+
# If you provide a key, we will fetch the model first to run the `:delete`
|
151
|
+
# callbacks for that model.
|
152
|
+
#
|
149
153
|
# @param [String, Model] model_or_key The key of the model or a model
|
150
154
|
# @return [String] The key
|
151
155
|
# @example Delete a podcast by key
|
@@ -153,33 +157,47 @@ module Guacamole
|
|
153
157
|
# @example Delete a podcast by model
|
154
158
|
# PodcastsCollection.delete(podcast)
|
155
159
|
def delete(model_or_key)
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
key
|
160
|
+
document, model = consistently_get_document_and_model(model_or_key)
|
161
|
+
|
162
|
+
callbacks(model).run_callbacks :delete do
|
163
|
+
document.delete
|
164
|
+
end
|
165
|
+
|
166
|
+
model.key
|
167
|
+
end
|
168
|
+
|
169
|
+
# Gets the document **and** model instance for either a given model or a key.
|
170
|
+
#
|
171
|
+
# @api private
|
172
|
+
# @param [String, Model] model_or_key The key of the model or a model
|
173
|
+
# @return [Array<Ashikawa::Core::Document, Model>] Both the document and model for the given input
|
174
|
+
def consistently_get_document_and_model(model_or_key)
|
175
|
+
if model_or_key.respond_to?(:key)
|
176
|
+
[fetch_document(model_or_key.key), model_or_key]
|
177
|
+
else
|
178
|
+
[document = fetch_document(model_or_key), mapper.document_to_model(document)]
|
179
|
+
end
|
163
180
|
end
|
164
181
|
|
165
|
-
#
|
182
|
+
# Update a model in the database with its new version
|
166
183
|
#
|
167
|
-
#
|
184
|
+
# Updates the currently saved version of the model with
|
168
185
|
# its new version. It searches for the entry in the database
|
169
186
|
# by key. This will change the updated_at timestamp and revision
|
170
187
|
# of the provided model.
|
171
188
|
#
|
172
|
-
# @param [Model] model The model to be
|
189
|
+
# @param [Model] model The model to be updated
|
173
190
|
# @return [Model] The model
|
174
|
-
# @example Get a podcast, update its title,
|
191
|
+
# @example Get a podcast, update its title, update it
|
175
192
|
# podcast = PodcastsCollection.by_key('27214247')
|
176
193
|
# podcast.title = 'Even better'
|
177
|
-
# PodcastsCollection.
|
178
|
-
def
|
194
|
+
# PodcastsCollection.update(podcast)
|
195
|
+
def update(model)
|
179
196
|
return false unless model.valid?
|
180
197
|
|
181
|
-
model.
|
182
|
-
|
198
|
+
callbacks(model).run_callbacks :save, :update do
|
199
|
+
replace_document_from(model)
|
200
|
+
end
|
183
201
|
model
|
184
202
|
end
|
185
203
|
|
@@ -269,15 +287,6 @@ module Guacamole
|
|
269
287
|
mapper.instance_eval(&block)
|
270
288
|
end
|
271
289
|
|
272
|
-
# Timestamp a fresh model
|
273
|
-
#
|
274
|
-
# @api private
|
275
|
-
def add_timestamps_to_model(model)
|
276
|
-
timestamp = Time.now
|
277
|
-
model.created_at = timestamp
|
278
|
-
model.updated_at = timestamp
|
279
|
-
end
|
280
|
-
|
281
290
|
# Create a document from a model
|
282
291
|
#
|
283
292
|
# @api private
|
@@ -347,6 +356,15 @@ module Guacamole
|
|
347
356
|
|
348
357
|
document
|
349
358
|
end
|
359
|
+
|
360
|
+
# Gets the callback class for the given model class
|
361
|
+
#
|
362
|
+
# @api private
|
363
|
+
# @param [Model] model The model to look up callbacks for
|
364
|
+
# @return [Callbacks] An instance of the registered callback class
|
365
|
+
def callbacks(model)
|
366
|
+
Callbacks.callbacks_for(model)
|
367
|
+
end
|
350
368
|
end
|
351
369
|
end
|
352
370
|
end
|
@@ -6,6 +6,7 @@ require 'logger'
|
|
6
6
|
require 'forwardable'
|
7
7
|
require 'ashikawa-core'
|
8
8
|
require 'yaml'
|
9
|
+
require 'erb'
|
9
10
|
|
10
11
|
require 'guacamole/document_model_mapper'
|
11
12
|
|
@@ -68,6 +69,39 @@ module Guacamole
|
|
68
69
|
#
|
69
70
|
# @return [Object] current environment
|
70
71
|
class Configuration
|
72
|
+
# A wrapper object to handle both configuration from a connection URI and a hash.
|
73
|
+
class ConfigStruct
|
74
|
+
attr_reader :url, :username, :password, :database
|
75
|
+
|
76
|
+
def initialize(config_hash_or_url)
|
77
|
+
case config_hash_or_url
|
78
|
+
when Hash
|
79
|
+
init_from_hash(config_hash_or_url)
|
80
|
+
when String
|
81
|
+
init_from_uri_string(config_hash_or_url)
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
private
|
86
|
+
|
87
|
+
def init_from_uri_string(uri_string)
|
88
|
+
uri = URI.parse(uri_string)
|
89
|
+
@username = uri.user
|
90
|
+
@password = uri.password
|
91
|
+
uri.user = nil
|
92
|
+
uri.path.match(%r{/_db/(?<db_name>\w+)/?}) { |match| @database = match[:db_name] }
|
93
|
+
|
94
|
+
@url = "#{uri.scheme}://#{uri.hostname}:#{uri.port}"
|
95
|
+
end
|
96
|
+
|
97
|
+
def init_from_hash(hash)
|
98
|
+
@username = hash['username']
|
99
|
+
@password = hash['password']
|
100
|
+
@database = hash['database']
|
101
|
+
@url = "#{hash['protocol']}://#{hash['host']}:#{hash['port']}"
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
71
105
|
# @!visibility protected
|
72
106
|
attr_accessor :database, :default_mapper, :logger
|
73
107
|
|
@@ -96,11 +130,51 @@ module Guacamole
|
|
96
130
|
#
|
97
131
|
# @param [String] file_name The file name of the configuration
|
98
132
|
def load(file_name)
|
99
|
-
|
133
|
+
yaml_content = process_file_with_erb(file_name)
|
134
|
+
config = YAML.load(yaml_content)[current_environment.to_s]
|
135
|
+
|
136
|
+
create_database_connection(build_config(config))
|
137
|
+
warn_if_database_was_not_yet_created
|
138
|
+
end
|
100
139
|
|
101
|
-
|
140
|
+
# Configures the database connection with a connection URI
|
141
|
+
#
|
142
|
+
# @params [String] connection_uri A URI to describe the database connection
|
143
|
+
def configure_with_uri(connection_uri)
|
144
|
+
create_database_connection build_config(connection_uri)
|
145
|
+
end
|
146
|
+
|
147
|
+
# Creates a config struct from either a hash or a DATABASE_URL
|
148
|
+
#
|
149
|
+
# @param [Hash, String] config Either a hash containing config params or a complete connection URI
|
150
|
+
# @return [ConfigStruct] A simple object with the required connection parameters
|
151
|
+
# @api private
|
152
|
+
def build_config(config)
|
153
|
+
ConfigStruct.new config
|
102
154
|
end
|
103
155
|
|
156
|
+
# Creates the actual Ashikawa::Core::Database instance
|
157
|
+
#
|
158
|
+
# @param [ConfigStruct] config The config object to extract the config parameters from
|
159
|
+
# @return [Ashikawa::Core::Database] The configured database instance
|
160
|
+
# @api private
|
161
|
+
def create_database_connection(config)
|
162
|
+
self.database = Ashikawa::Core::Database.new do |arango_config|
|
163
|
+
arango_config.url = config.url
|
164
|
+
arango_config.username = config.username
|
165
|
+
arango_config.password = config.password
|
166
|
+
arango_config.database_name = config.database if config.database
|
167
|
+
arango_config.logger = logger
|
168
|
+
end
|
169
|
+
end
|
170
|
+
|
171
|
+
# The current environment.
|
172
|
+
#
|
173
|
+
# If you're in a Rails application this will return the Rails environment. If Rails is
|
174
|
+
# not available it will use `RACK_ENV` and if that is not available it will fall back to
|
175
|
+
# `GUACAMOLE_ENV`. This allows you to use Guacamole not only in Rails.
|
176
|
+
#
|
177
|
+
# @return [String] The current environment
|
104
178
|
def current_environment
|
105
179
|
return Rails.env if defined?(Rails)
|
106
180
|
ENV['RACK_ENV'] || ENV['GUACAMOLE_ENV']
|
@@ -112,19 +186,6 @@ module Guacamole
|
|
112
186
|
@configuration ||= new
|
113
187
|
end
|
114
188
|
|
115
|
-
def create_database_connection_from(config)
|
116
|
-
Ashikawa::Core::Database.new do |arango_config|
|
117
|
-
arango_config.url = db_url_from(config)
|
118
|
-
arango_config.username = config['username']
|
119
|
-
arango_config.password = config['password']
|
120
|
-
arango_config.logger = logger
|
121
|
-
end
|
122
|
-
end
|
123
|
-
|
124
|
-
def db_url_from(config)
|
125
|
-
"#{config['protocol']}://#{config['host']}:#{config['port']}/_db/#{config['database']}"
|
126
|
-
end
|
127
|
-
|
128
189
|
def rails_logger
|
129
190
|
return Rails.logger if defined?(Rails)
|
130
191
|
end
|
@@ -134,6 +195,22 @@ module Guacamole
|
|
134
195
|
default_logger.level = Logger::INFO
|
135
196
|
default_logger
|
136
197
|
end
|
198
|
+
|
199
|
+
# Prints a warning to STDOUT and the logger if the configured database could not be found
|
200
|
+
#
|
201
|
+
# @note Ashikawa::Core doesn't know if the database is not present or the collection was not created.
|
202
|
+
# Thus we will just give the user a warning if the database was not found upon initialization.
|
203
|
+
def warn_if_database_was_not_yet_created
|
204
|
+
database.send_request 'version' # The /version is database specific
|
205
|
+
rescue Ashikawa::Core::ResourceNotFound
|
206
|
+
warning_msg = "[WARNING] The configured database ('#{database.name}') cannot be found. Please run `rake db:create` to create it."
|
207
|
+
logger.warn warning_msg
|
208
|
+
warn warning_msg
|
209
|
+
end
|
210
|
+
|
211
|
+
def process_file_with_erb(file_name)
|
212
|
+
ERB.new(File.read(file_name)).result
|
213
|
+
end
|
137
214
|
end
|
138
215
|
|
139
216
|
# A list of active experimental features. Refer to `AVAILABLE_EXPERIMENTAL_FEATURES` to see
|
data/lib/guacamole/model.rb
CHANGED
@@ -70,6 +70,19 @@ module Guacamole
|
|
70
70
|
# attribute :contributor_names, Array[String]
|
71
71
|
# end
|
72
72
|
#
|
73
|
+
# @!method self.callbacks(name_of_callbacks_class)
|
74
|
+
# Registers a single callback class to be used for this model
|
75
|
+
#
|
76
|
+
# @api public
|
77
|
+
# @param [Symbol] name_of_callbacks_class The name of the the callback class to be used
|
78
|
+
#
|
79
|
+
# @example
|
80
|
+
# class BlogPost
|
81
|
+
# include Guacamole::Model
|
82
|
+
#
|
83
|
+
# callbacks :blog_post_callbacks
|
84
|
+
# end
|
85
|
+
#
|
73
86
|
# @!method self.validates
|
74
87
|
# This method is a shortcut to all default validators and any custom validator classes ending in 'Validator'
|
75
88
|
#
|
@@ -164,6 +177,12 @@ module Guacamole
|
|
164
177
|
#
|
165
178
|
# @param [Model] other the model to compare with
|
166
179
|
# @api public
|
180
|
+
#
|
181
|
+
# @!method callbacks
|
182
|
+
# Returns the registered callback class instantiated with `self`
|
183
|
+
#
|
184
|
+
# @api private
|
185
|
+
# @return [Callback] The registered callback class instantiated with `self`
|
167
186
|
module Model
|
168
187
|
extend ActiveSupport::Concern
|
169
188
|
# @!parse include ActiveModel::Validations
|
@@ -172,9 +191,16 @@ module Guacamole
|
|
172
191
|
# I know that this is technically not true, but the reality is a parse error:
|
173
192
|
# @!parse include Virtus
|
174
193
|
|
194
|
+
module ClassMethods
|
195
|
+
def callbacks(name_of_callbacks_class)
|
196
|
+
callback_class = name_of_callbacks_class.to_s.camelcase.constantize
|
197
|
+
Callbacks.register_callback self, callback_class
|
198
|
+
end
|
199
|
+
end
|
200
|
+
|
175
201
|
included do
|
176
202
|
include ActiveModel::Validations
|
177
|
-
|
203
|
+
extend ActiveModel::Naming
|
178
204
|
include ActiveModel::Conversion
|
179
205
|
include Virtus.model
|
180
206
|
|
@@ -192,6 +218,18 @@ module Guacamole
|
|
192
218
|
key
|
193
219
|
end
|
194
220
|
|
221
|
+
def valid_with_callbacks?(context = nil)
|
222
|
+
callbacks.run_callbacks :validate do
|
223
|
+
valid_without_callbacks?(context)
|
224
|
+
end
|
225
|
+
end
|
226
|
+
alias_method :valid_without_callbacks?, :valid?
|
227
|
+
alias_method :valid?, :valid_with_callbacks?
|
228
|
+
|
229
|
+
def callbacks
|
230
|
+
Guacamole::Callbacks.callbacks_for(self)
|
231
|
+
end
|
232
|
+
|
195
233
|
def ==(other)
|
196
234
|
other.instance_of?(self.class) &&
|
197
235
|
attributes.all? do |attribute, value|
|
data/lib/guacamole/railtie.rb
CHANGED
@@ -18,15 +18,21 @@ module Guacamole
|
|
18
18
|
# Add app/collections to autoload_paths
|
19
19
|
initializer 'guacamole.setup_autoload_paths', before: :set_autoload_paths do |app|
|
20
20
|
app.config.autoload_paths += %W(#{app.config.root}/app/collections)
|
21
|
+
app.config.autoload_paths += %W(#{app.config.root}/app/callbacks)
|
21
22
|
end
|
22
23
|
|
23
24
|
# We're not doing migrations (yet)
|
24
25
|
config.send(:app_generators).orm :guacamole, migration: false
|
25
26
|
|
26
|
-
initializer 'guacamole.
|
27
|
-
|
28
|
-
|
27
|
+
initializer 'guacamole.configure_database_connection' do
|
28
|
+
if ENV['DATABASE_URL'].present?
|
29
|
+
Guacamole::Configuration.configure_with_uri ENV['DATABASE_URL']
|
30
|
+
elsif (config_file = Rails.root.join('config', 'guacamole.yml')).file?
|
29
31
|
Guacamole::Configuration.load config_file
|
32
|
+
else
|
33
|
+
warn_msg = '[WARNING] No configuration could be found. Either provide a `guacamole.yml` or a connection URI with `ENV["DATABASE_URL"]`'
|
34
|
+
warn warn_msg
|
35
|
+
Guacamole.logger.warn warn_msg
|
30
36
|
end
|
31
37
|
end
|
32
38
|
|
data/lib/guacamole/version.rb
CHANGED
@@ -0,0 +1,26 @@
|
|
1
|
+
# -*- encoding : utf-8 -*-
|
2
|
+
|
3
|
+
require 'rails/generators/guacamole_generator'
|
4
|
+
|
5
|
+
module Guacamole
|
6
|
+
module Generators
|
7
|
+
class CallbacksGenerator < Base
|
8
|
+
desc 'Creates a Guacamole callback class'
|
9
|
+
|
10
|
+
class_option :model_class, type: :string, required: false, banner: 'CLASS_NAME | Default: NAME', desc: 'The model class this callback is used for.'
|
11
|
+
|
12
|
+
check_class_collision
|
13
|
+
|
14
|
+
def create_callback_file
|
15
|
+
model_file_name = (options[:model_class] || class_name).underscore
|
16
|
+
inject_into_file "app/models/#{model_file_name}.rb",
|
17
|
+
"\n\n callbacks :#{class_name.underscore}_callbacks",
|
18
|
+
after: 'include Guacamole::Model'
|
19
|
+
|
20
|
+
template 'callbacks.rb.tt', File.join('app/callbacks', class_path, "#{file_name}_callbacks.rb")
|
21
|
+
end
|
22
|
+
|
23
|
+
hook_for :test_framework
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
<% module_namespacing do -%>
|
2
|
+
class <%= class_name %>Callbacks
|
3
|
+
include Guacamole::Callbacks
|
4
|
+
|
5
|
+
# Add your callbacks
|
6
|
+
# before_create :encrypt_password
|
7
|
+
|
8
|
+
# def encrypt_password
|
9
|
+
# object.encrypted_password = BCrypt::Password.create(object.password)
|
10
|
+
# end
|
11
|
+
end
|
12
|
+
<% end -%>
|
13
|
+
|
@@ -0,0 +1,14 @@
|
|
1
|
+
# -*- encoding : utf-8 -*-
|
2
|
+
|
3
|
+
require 'rails/generators/guacamole_generator'
|
4
|
+
|
5
|
+
module Rspec
|
6
|
+
module Generators
|
7
|
+
class CallbacksGenerator < ::Guacamole::Generators::Base
|
8
|
+
def create_collection_spec
|
9
|
+
template 'callbacks_spec.rb.tt', File.join('spec/callbacks', class_path, "#{file_name}_callbacks_spec.rb")
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|