guacamole 0.2.0 → 0.3.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.
- 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
|
+
|