guacamole 0.2.0 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
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
+