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.
Files changed (50) hide show
  1. checksums.yaml +4 -4
  2. data/.hound.yml +3 -0
  3. data/{config/reek.yml → .reek.yml} +5 -0
  4. data/.travis.yml +3 -0
  5. data/CHANGELOG.md +19 -0
  6. data/GOALS.md +20 -0
  7. data/Gemfile +1 -11
  8. data/Guardfile +0 -4
  9. data/README.md +147 -11
  10. data/Rakefile +33 -3
  11. data/guacamole.gemspec +12 -1
  12. data/lib/guacamole.rb +1 -0
  13. data/lib/guacamole/callbacks.rb +259 -0
  14. data/lib/guacamole/collection.rb +50 -32
  15. data/lib/guacamole/configuration.rb +92 -15
  16. data/lib/guacamole/model.rb +39 -1
  17. data/lib/guacamole/railtie.rb +9 -3
  18. data/lib/guacamole/version.rb +1 -1
  19. data/lib/rails/generators/guacamole/callbacks/callbacks_generator.rb +26 -0
  20. data/lib/rails/generators/guacamole/callbacks/templates/callbacks.rb.tt +13 -0
  21. data/lib/rails/generators/rspec/callbacks/callbacks_generator.rb +14 -0
  22. data/lib/rails/generators/rspec/callbacks/templates/callbacks_spec.rb.tt +7 -0
  23. data/lib/rails/generators/test_unit/callbacks/callbacks_generator.rb +13 -0
  24. data/lib/rails/generators/test_unit/callbacks/templates/callbacks_test.rb.tt +9 -0
  25. data/lib/rails/generators/test_unit/collection/collection_generator.rb +13 -0
  26. data/lib/rails/generators/test_unit/collection/templates/collection_test.rb.tt +10 -0
  27. data/spec/acceptance/aql_spec.rb +0 -12
  28. data/spec/acceptance/basic_spec.rb +16 -25
  29. data/spec/acceptance/callbacks_spec.rb +181 -0
  30. data/spec/acceptance/config/guacamole.yml +1 -1
  31. data/spec/acceptance/spec_helper.rb +24 -6
  32. data/spec/fabricators/article.rb +12 -0
  33. data/spec/fabricators/comment.rb +7 -0
  34. data/spec/fabricators/pony.rb +6 -4
  35. data/spec/fabricators/pony_fabricator.rb +7 -0
  36. data/spec/spec_helper.rb +5 -4
  37. data/spec/support/guacamole.yml.erb +5 -0
  38. data/spec/unit/callbacks_spec.rb +139 -0
  39. data/spec/unit/collection_spec.rb +85 -66
  40. data/spec/unit/configuration_spec.rb +165 -21
  41. data/spec/unit/identiy_map_spec.rb +2 -2
  42. data/spec/unit/model_spec.rb +36 -3
  43. metadata +181 -12
  44. data/Gemfile.devtools +0 -67
  45. data/config/devtools.yml +0 -4
  46. data/config/flay.yml +0 -3
  47. data/config/flog.yml +0 -2
  48. data/config/mutant.yml +0 -3
  49. data/config/yardstick.yml +0 -2
  50. data/tasks/adjustments.rake +0 -34
@@ -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 replace though,
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 connection.fetch(key)
100
+ mapper.document_to_model fetch_document(key)
101
101
  end
102
102
 
103
- # Persist a model in the collection or replace it in the database, depending if it is already persisted
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 {#replace replace} for explicit usage.
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, replace it
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? ? replace(model) : create(model)
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
- add_timestamps_to_model(model)
143
- create_document_from(model)
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
- key = if model_or_key.respond_to? :key
157
- model_or_key.key
158
- else
159
- model_or_key
160
- end
161
- fetch_document(key).delete
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
- # Replace a model in the database with its new version
182
+ # Update a model in the database with its new version
166
183
  #
167
- # Replaces the currently saved version of the model with
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 replaced
189
+ # @param [Model] model The model to be updated
173
190
  # @return [Model] The model
174
- # @example Get a podcast, update its title, replace it
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.replace(podcast)
178
- def replace(model)
194
+ # PodcastsCollection.update(podcast)
195
+ def update(model)
179
196
  return false unless model.valid?
180
197
 
181
- model.updated_at = Time.now
182
- replace_document_from(model)
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
- config = YAML.load_file(file_name)[current_environment.to_s]
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
- self.database = create_database_connection_from(config)
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
@@ -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
- include ActiveModel::Naming
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|
@@ -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.load-config' do
27
- config_file = Rails.root.join('config', 'guacamole.yml')
28
- if config_file.file?
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
 
@@ -1,5 +1,5 @@
1
1
  # -*- encoding : utf-8 -*-
2
2
  module Guacamole
3
3
  # Current version of the gem
4
- VERSION = '0.2.0'
4
+ VERSION = '0.3.0'
5
5
  end
@@ -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
+