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
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 2522fab030888fc0ee9601790aabd2134c6f32dc
4
- data.tar.gz: 400562752f398045f858bbe9edf3c40d621fcc4c
3
+ metadata.gz: da19a1adc7e260053f9749aaaa5af277e706fbda
4
+ data.tar.gz: 08392381e86a454d91a3da30efac6b86599234be
5
5
  SHA512:
6
- metadata.gz: 8c1b397cab789fee57a5a98eb76e46f9c422b466747cd9be272591cc66aee47829253c484ffd500e9fe0755fc255d43df55742546404ecf618a536f27538ab18
7
- data.tar.gz: 19f830e89f65c7c4c8404a4ce49559920a6a3eac6e0bbe0eab34671f944a49bc0412d530aaad984fb0fd5cba7bd77ffbbd5c6dc8d1bc3e1248a5605972e0e8c3
6
+ metadata.gz: 40ec254c0113a1f8ee0bc2dbdb8624c2d5f3c0ea55cb1e981e45d91b38eb4167f590896cce392d9c5a8419ddea208ce29600f116b24fbf7f2108aa0181c54b47
7
+ data.tar.gz: b7b301e8c84692d620316a29e02ee654b6358427e5e92016957a7f66aeb9b249268eacffffe9d715bb28f7508f9ec3ca8746ef92f043326c56ecc7a0f4cab708
data/.hound.yml CHANGED
@@ -55,3 +55,6 @@ TrivialAccessors:
55
55
 
56
56
  RegexpLiteral:
57
57
  Enabled: false
58
+
59
+ StringLiterals:
60
+ EnforcedStyle: single_quotes
@@ -22,6 +22,8 @@ DuplicateMethodCall:
22
22
  exclude:
23
23
  - Guacamole::DocumentModelMapper#document_to_model
24
24
  - Guacamole::DocumentModelMapper#model_to_document
25
+ - Guacamole::Configuration#create_database_connection
26
+ - Guacamole::Callbacks::CallbackProxy#run_callbacks
25
27
  max_calls: 1
26
28
  allow_calls: []
27
29
  FeatureEnvy:
@@ -59,6 +61,7 @@ TooManyInstanceVariables:
59
61
  enabled: true
60
62
  exclude:
61
63
  - Guacamole::DocumentModelMapper
64
+ - Guacamole::Configuration::ConfigStruct
62
65
  max_instance_variables: 3
63
66
  TooManyMethods:
64
67
  enabled: true
@@ -72,6 +75,8 @@ TooManyStatements:
72
75
  - Guacamole::DocumentModelMapper#model_to_document
73
76
  - Guacamole::Collection::ClassMethods#create_document_from
74
77
  - Guacamole::Collection::ClassMethods#create_referenced_by_models_of
78
+ - Guacamole::Configuration#create_database_connection
79
+ - Guacamole::Configuration::ConfigStruct#init_from_uri_string
75
80
  max_statements: 5
76
81
  UncommunicativeMethodName:
77
82
  enabled: true
data/.travis.yml CHANGED
@@ -19,3 +19,6 @@ matrix:
19
19
  - rvm: jruby-head
20
20
  - rvm: rbx-19mode
21
21
  script: "bundle exec rake ci"
22
+ addons:
23
+ code_climate:
24
+ repo_token: cd5dd119d022e5b0d4fda98d25aa93473ed5b580c3f621475c3e605a858f5790
data/CHANGELOG.md CHANGED
@@ -1,3 +1,22 @@
1
+ ## Version 0.3.0
2
+
3
+ **Codename: Spread your Wings**
4
+
5
+ This release improved the configurability of Guacamole, updated Ashikawa::Core, enhanced the README and added callbacks.
6
+
7
+ Notable changes are:
8
+
9
+ * Implemented external callbacks
10
+ * Added support for `DATABASE_URL`
11
+ * Parse `guacamole.yml` with ERB
12
+ * Updated Ashikawa::Core to 0.12.0
13
+ * Improved README
14
+ * Fixed generators with `test_unit`
15
+ * Internal changes
16
+ * Removed `debugger` in favor of `pry`
17
+ * Removed `devtools`
18
+
19
+
1
20
  ## Version 0.2.0
2
21
 
3
22
  **Codename: Into the Storm**
data/GOALS.md ADDED
@@ -0,0 +1,20 @@
1
+ # Goals of this project
2
+
3
+ * Support building web applications with Ruby on Rails or other Rack-based frameworks
4
+ * Support for Rails Views for example
5
+ * Allow Rails developers to get into Guacamole as easy as possible
6
+ * Reflect the nature of NoSQL in general and ArangoDB in particular
7
+ * Use the Datamapper pattern, because it allows us to leverage ArangoDB features like nesting in a really nice way
8
+ * Support ArangoDB features like transactions, the query language and graphs
9
+
10
+ *The two main goals may conflict from time to time.*
11
+
12
+ ## Features of Guacamole 1.0
13
+
14
+ **This is definitely a moving target and up to discussion. For non-moving targets refer to our [milestones](https://github.com/triAGENS/guacamole/issues/milestones).**
15
+
16
+ * **Embrace the Multi Model nature of ArangoDB:** It will be possible to connect entities with edges. These edges will have custom classes. It will be possible to do graph queries using these connections. There is already a [discussion about how to do this](https://github.com/triAGENS/guacamole/issues/74).
17
+ * **Support for the ArangoDB Query Language:** ArangoDB has a powerful querying language. We want to provide support for it in the most Ruby-way possible. There is already a [discussion about how to do this](https://github.com/moonglum/brazil/issues/8).
18
+ * **Support for Transactions:** ArangoDB has support for transactions, we want to make it as easy as possible to use them.
19
+ * **Support for Migrations:** Migrations work differently in a database with flexible schema. Some migrations are possible on the fly, but there will still be migrations that need to be executed explicitly. We want to support Rails-like migrations for these types of queries.
20
+ * **Support for flexible mapping:** Right now the mapping is very strict, we will allow more flexibility here including renaming or excluding certain attributes.
data/Gemfile CHANGED
@@ -3,14 +3,4 @@ source 'https://rubygems.org'
3
3
 
4
4
  gemspec
5
5
 
6
- # Devtools
7
- group :development, :test do
8
- gem 'devtools', git: 'https://github.com/rom-rb/devtools.git'
9
- gem 'guard-yard'
10
- end
11
- eval_gemfile 'Gemfile.devtools'
12
-
13
- # Local debugging
14
- group :debug do
15
- gem 'debugger'
16
- end
6
+ gem 'rspec-its', git: 'https://github.com/rspec/rspec-its.git'
data/Guardfile CHANGED
@@ -7,7 +7,3 @@ guard 'rspec', spec_paths: ['spec/unit'] do
7
7
  watch(%r{spec/.+\.rb})
8
8
  watch(%r{lib/guacamole/(.+)\.rb$}) { |m| "spec/unit/#{m[1]}_spec.rb" }
9
9
  end
10
-
11
- guard 'yard', port: '8808' do
12
- watch(%r{lib/.+\.rb})
13
- end
data/README.md CHANGED
@@ -1,6 +1,7 @@
1
1
  [![RubyDoc](http://img.shields.io/badge/📄-RubyDoc-be1d77.svg)](http://rubydoc.info/gems/guacamole/frames)
2
2
  [![Build Status](http://img.shields.io/travis/triAGENS/guacamole.svg)](https://travis-ci.org/triAGENS/guacamole)
3
3
  [![Code Climate](http://img.shields.io/codeclimate/github/triAGENS/guacamole.svg)](https://codeclimate.com/github/triAGENS/guacamole)
4
+ [![Inline docs](http://inch-ci.org/github/triAGENS/guacamole.svg)](http://inch-ci.org/github/triAGENS/guacamole)
4
5
  [![Gem Version](http://img.shields.io/gem/v/guacamole.svg)](https://rubygems.org/gems/guacamole)
5
6
 
6
7
  # Guacamole
@@ -24,7 +25,7 @@ Since Guacamole is in an alpha state we suggest you to create a new Rails applic
24
25
  First of all create your shiny new application, without ActiveRecord of course:
25
26
 
26
27
  ```shell
27
- rails new -O $my_awesome_app
28
+ rails new --skip-active-record $my_awesome_app
28
29
  ```
29
30
 
30
31
  Add this line to your application's Gemfile:
@@ -39,6 +40,24 @@ And then install the new dependencies:
39
40
  bundle install
40
41
  ```
41
42
 
43
+ ### Adding Guacamole to an existing Rails application
44
+
45
+ Maybe you're bold and want to add Guacamole to an existing Rails application. In this case some things are different, because you already have an ORM configured. Throughout the remaining README you will find examples of using generators and rake tasks to support you. All the generators must be invoked with the `--orm guacamole` flag. Without this you will generate both, ActiveRecord and Guacamole files:
46
+
47
+ ```shell
48
+ bundle exec rails generate model pony name:string birthday:date color:string --orm guacamole
49
+ ```
50
+
51
+ Guacamole will not overwrite existing rake tasks and thus you need to invoke them with under the guacamole namespace:
52
+
53
+ ```shell
54
+ rake db:guacamole:create
55
+ rake db:guacamole:purge
56
+ rake db:guacamole:drop
57
+ ```
58
+
59
+ Everything else should work as described in the README. If you encounter any errors while working with an existing Rails application, please let us know.
60
+
42
61
  ### Configuration
43
62
 
44
63
  After you created the application and installed the dependencies the first thing you need is a configuration file. The database connection is pretty much configured as expected: With a YAML file. Luckily you don't have to create this file by yourself but you can use a generator to do it for you:
@@ -59,6 +78,8 @@ development:
59
78
  database: 'pony_blog_development'
60
79
  ```
61
80
 
81
+ **Note**: If you use something like [dotenv](https://github.com/bkeepers/dotenv) we will process the config file with ERB before loading the YAML. Another way to configure the database connection is to provide a connection URI like this: `http://user:pass@localhost:8529/_db/pony_ville_db`. If you don't use authentication, just skip the user/password part. The connection URI must be provided as the environment variable `DATABASE_URL` and has precedence over the config file.
82
+
62
83
  After you created a configuration file you can create the database as in any other Rails project:
63
84
 
64
85
  ```shell
@@ -154,7 +175,7 @@ As the model doesn't know anything about the database you cannot define database
154
175
 
155
176
  ### Collections
156
177
 
157
- Collections are your gateway to the database. They persist your models and offer querying for them. They will translate the raw data from the database to your domain models and vice versa. By convention they are the pluralized version of the model with the suffix `Collection`. So given the model from above, this could be the according collection:
178
+ Collections are your gateway to the database. They persist your models and offer querying for them. They will translate the raw data from the database to your domain models and vice versa. By convention they are the pluralized version of the model with the suffix `Collection`. So given the model from above, this could be the following collection:
158
179
 
159
180
  ```ruby
160
181
  class PoniesCollection
@@ -309,13 +330,13 @@ As you can see, from the model perspective there is nothing special about an emb
309
330
 
310
331
  ```json
311
332
  {
312
- "_id": [...],
313
- "_rev": [...],
314
- "_key": [...],
333
+ "_id": "...",
334
+ "_rev": "...",
335
+ "_key": "...",
315
336
  "title": "The grand blog post",
316
337
  "body": "Lorem ipsum [...]",
317
338
  "create_at": "2014-05-03T16:55:43+02:00",
318
- "updated_at": "2014-05-03T16:55:43+02:00"
339
+ "updated_at": "2014-05-03T16:55:43+02:00",
319
340
  "comments": [
320
341
  {
321
342
  "text": "This was really a grand blog post",
@@ -328,6 +349,7 @@ As you can see, from the model perspective there is nothing special about an emb
328
349
  "updated_at": "2014-05-04T16:55:43+02:00"
329
350
  }
330
351
  ]
352
+ }
331
353
  ```
332
354
 
333
355
  **Note**: Again this will only work if you stick with the convention. So far there is no support to configure this more fine grained.
@@ -371,7 +393,7 @@ class PostsCollection
371
393
  include Guacamole::Collection
372
394
 
373
395
  map do
374
- references :user
396
+ references :author
375
397
  end
376
398
  end
377
399
  ```
@@ -394,6 +416,120 @@ AuthorsCollection.save author
394
416
  # => Will save both the author and the post
395
417
  ```
396
418
 
419
+ ### Callbacks
420
+
421
+ Guacamole allows you to define callbacks for various actions performed on a model. Those callbacks need to be defined in a dedicate class per model. For example to hash the password of a user prior creating the document in the database you would write something like the following:
422
+
423
+ ```ruby
424
+ class UserCallbacks
425
+ include Guacamole::Callbacks
426
+
427
+ before_create :encrypt_password
428
+
429
+ def encrypt_password
430
+ object.encrypted_password = BCrypt::Password.create(object.password)
431
+ end
432
+ end
433
+ ```
434
+
435
+ Whenever callbacks needs to be executed for a model we create an instance of the specified callback with that model. You have access to the model via the `object` method. To specify a callback for a model you must use the `callbacks` method in that model:
436
+
437
+ ```ruby
438
+ class User
439
+ include Guacamole::Model
440
+
441
+ callbacks :user_callbacks
442
+ end
443
+ ```
444
+
445
+ You can define define callbacks for the following actions:
446
+
447
+ * `before_validate`
448
+ * `around_validate`
449
+ * `after_validate`
450
+ * `before_save`
451
+ * `around_save`
452
+ * `after_save`
453
+ * `before_create`
454
+ * `around_create`
455
+ * `after_create`
456
+ * `before_update`
457
+ * `around_update`
458
+ * `after_update`
459
+ * `before_delete`
460
+ * `around_delete`
461
+ * `after_delete`
462
+
463
+ The order of the callback execution is as follows:
464
+
465
+ **Creating an object**
466
+
467
+ * `before_validation`
468
+ * `after_validation`
469
+ * `before_save`
470
+ * `around_save`
471
+ * `before_create`
472
+ * `around_create`
473
+ * `after_create`
474
+ * `after_save`
475
+
476
+ **Updating an object**
477
+
478
+ * `before_validation`
479
+ * `after_validation`
480
+ * `before_save`
481
+ * `around_save`
482
+ * `before_update`
483
+ * `around_update`
484
+ * `after_update`
485
+ * `after_save`
486
+
487
+ **Destroying an object**
488
+
489
+ * `before_delete`
490
+ * `around_delete`
491
+ * `after_delete`
492
+
493
+ The order of the callback execution is as follows:
494
+
495
+ **Creating an object**
496
+
497
+ 1. `before_validation`
498
+ 2. `after_validation`
499
+ 3. `before_save`
500
+ 4. `around_save`
501
+ 5. `before_create`
502
+ 6. `around_create`
503
+ 7. `after_create`
504
+ 8. `after_save`
505
+
506
+ **Updating an object**
507
+
508
+ 1. `before_validation`
509
+ 2. `after_validation`
510
+ 3. `before_save`
511
+ 4. `around_save`
512
+ 5. `before_update`
513
+ 6. `around_update`
514
+ 7. `after_update`
515
+ 8. `after_save`
516
+
517
+ **Destroying an object**
518
+
519
+ 1. `before_delete`
520
+ 2. `around_delete`
521
+ 3. `after_delete`
522
+
523
+ #### Generator Support
524
+
525
+ Of course we provide a generator to help you setting the required files up:
526
+
527
+ ```shell
528
+ rails generate guacamole:callbacks pony
529
+ ```
530
+
531
+ This will create the callback file, a test template and adds the appropriate register call to the model file.
532
+
397
533
  ## Integration into the Rails Ecosystem™
398
534
 
399
535
  Guacamole is a very young project. A lot of stuff is missing but still, if you want to get started with ArangoDB and are using Ruby/Rails it will give you a nice head start. Besides a long TODO list we want to hint to some points to help you integrate Guacamole with the rest of the Rails ecosystem:
@@ -402,17 +538,17 @@ Guacamole is a very young project. A lot of stuff is missing but still, if you w
402
538
 
403
539
  Currently we're not providing any testing helper, thus you need to make sure to cleanup the database yourself before each run. You can look at the [`spec/acceptance/spec_helper.rb`](https://github.com/triAGENS/guacamole/blob/master/spec/acceptance/spec_helper.rb) of Guacamole for inspiration of how to do that.
404
540
 
405
- For test data generation we're using the awesome [Fabrication gem](http://www.fabricationgem.org/). Again you find some usage examples in Guacamole's own acceptance tests. We didn't tested Factory Girl yet, but it eventually will work, too.
541
+ For test data generation we're using the awesome [Fabrication gem](http://www.fabricationgem.org/). Again you find some usage examples in Guacamole's own acceptance tests. We haven't tested Factory Girl yet but it eventually will work too.
406
542
 
407
543
  ### Authentication
408
544
 
409
- Any integration into an authentication framework need to be done by you. At this time we have nothing to share with you about this topic.
545
+ Any integration into an authentication framework needs to be done by you. At this time we have nothing to share with you about this topic.
410
546
 
411
547
  ### Forms
412
548
 
413
- While we not tested them they should probably work due to the ActiveModel compliance. But again, this not confirmed and you need to try it out by yourself.
549
+ While we haven't tested them, they should probably work due to the ActiveModel compliance. But again, this is not confirmed and you need to try it out yourself.
414
550
 
415
- If you give Guacamole a try, please feel free to ask us any question or give us feedback to anything on your mind. This is really crucial for us and we would be more than happy to hear back from you.
551
+ If you give Guacamole a try, please feel free to ask us any question or give us feedback about anything on your mind. This is really crucial for us and we would be more than happy to hear back from you.
416
552
 
417
553
  ## Todos
418
554
 
data/Rakefile CHANGED
@@ -1,10 +1,37 @@
1
1
  # -*- encoding : utf-8 -*-
2
2
  require 'bundler/gem_tasks'
3
+ require 'rspec/core/rake_task'
4
+ require 'yard/rake/yardoc_task'
5
+ require 'inch/rake'
6
+ require 'reek/rake/task'
3
7
 
4
- require 'devtools'
5
- Devtools.init_rake_tasks
8
+ desc 'Run all specs'
9
+ task spec: ['spec:unit', 'spec:acceptance']
6
10
 
7
- import('./tasks/adjustments.rake')
11
+ namespace :spec do
12
+ desc 'Run unit specs'
13
+ RSpec::Core::RakeTask.new(:unit) do |task|
14
+ task.pattern = 'spec/unit/**/*_spec.rb'
15
+ end
16
+
17
+ desc 'Run acceptance specs – requires running instance of ArangoDB'
18
+ RSpec::Core::RakeTask.new(:acceptance) do |task|
19
+ task.pattern = 'spec/acceptance/**/*_spec.rb'
20
+ end
21
+ end
22
+
23
+ YARD::Rake::YardocTask.new(:doc)
24
+
25
+ namespace :metrics do
26
+ Inch::Rake::Suggest.new do |t|
27
+ t.args << '--pedantic'
28
+ end
29
+
30
+ Reek::Rake::Task.new do |t|
31
+ t.fail_on_error = true
32
+ t.config_files = '.reek.yml'
33
+ end
34
+ end
8
35
 
9
36
  desc 'Start a REPL with guacamole loaded (not the Rails part)'
10
37
  task :console do
@@ -15,3 +42,6 @@ task :console do
15
42
  ARGV.clear
16
43
  Pry.start
17
44
  end
45
+
46
+ task default: :spec
47
+ task ci: ['spec', 'metrics:reek']
data/guacamole.gemspec CHANGED
@@ -18,14 +18,25 @@ Gem::Specification.new do |spec|
18
18
  spec.test_files = spec.files.grep(%r{^(spec)/})
19
19
  spec.require_paths = ['lib']
20
20
 
21
- spec.add_dependency 'ashikawa-core', '~> 0.11.0'
21
+ spec.add_dependency 'ashikawa-core', '~> 0.12.0'
22
22
  spec.add_dependency 'virtus', '~> 1.0.1'
23
23
  spec.add_dependency 'activesupport', '>= 4.0.0'
24
24
  spec.add_dependency 'activemodel', '>= 4.0.0'
25
25
  spec.add_dependency 'hamster', '~> 1.0.1.pre.rc.1'
26
26
 
27
+ spec.add_development_dependency 'bcrypt', '3.1.7'
28
+ spec.add_development_dependency 'codeclimate-test-reporter', '~> 0.3.0'
27
29
  spec.add_development_dependency 'fabrication', '~> 2.8.1'
28
30
  spec.add_development_dependency 'faker', '~> 1.2.0'
31
+ spec.add_development_dependency 'guard', '~> 2.6.1'
32
+ spec.add_development_dependency 'guard-bundler', '~> 2.0.0'
33
+ spec.add_development_dependency 'guard-rspec', '~> 4.2.10'
34
+ spec.add_development_dependency 'inch', '~> 0.4.6'
29
35
  spec.add_development_dependency 'logging', '~> 1.8.1'
30
36
  spec.add_development_dependency 'pry', '~> 0.9.12'
37
+ spec.add_development_dependency 'rake', '~> 10.3.2'
38
+ spec.add_development_dependency 'reek', '~> 1.3.7'
39
+ spec.add_development_dependency 'rspec', '~> 3.0.0'
40
+ spec.add_development_dependency 'timecop', '~> 0.7.1'
41
+ spec.add_development_dependency 'yard', '~> 0.8.7.4'
31
42
  end
data/lib/guacamole.rb CHANGED
@@ -5,6 +5,7 @@ require 'guacamole/exceptions'
5
5
  require 'guacamole/configuration'
6
6
  require 'guacamole/model'
7
7
  require 'guacamole/collection'
8
+ require 'guacamole/callbacks'
8
9
  require 'guacamole/document_model_mapper'
9
10
  require 'guacamole/identity_map'
10
11
 
@@ -0,0 +1,259 @@
1
+ # -*- encoding : utf-8 -*-
2
+
3
+ require 'active_support'
4
+ require 'active_support/concern'
5
+ require 'active_support/core_ext/string/inflections'
6
+ require 'active_model' # Cherry Pick not possible
7
+
8
+ module Guacamole
9
+ # Define callbacks for different life cycle events of a model
10
+ #
11
+ # Callbacks in Guacamole are defined in dedicated classes only. There will be no interface to implement them
12
+ # in the model or the collection context. This was done due to the nature of the data mapper pattern:
13
+ #
14
+ # * The model doesn't know anything about the database thus defining persistence related callbacks (i.e. `before_create`)
15
+ # would weaken this separation.
16
+ # * Validation happens in the context of the model and thus defining all callbacks in the collection would still
17
+ # be somewhat awkward.
18
+ #
19
+ # Due to those reasons Guacamole employs the concept of **external callbacks**. Just define a class and include
20
+ # the `Guacamole::Callbacks` module and you can define all kinds of callbacks. Under the hood `ActiveModel::Callbacks`
21
+ # is used to provide the callback execution functionality.
22
+ #
23
+ # @note Wether you define a callback class for your model or not, internally there always will be a callback.
24
+ #
25
+ # Since maintaining the time stamp attributes of your models are implemented as callbacks as well your callback
26
+ # class will have those methods included.
27
+ #
28
+ # Binding a callback class to a model will happen in the `Model.callbacks` method.
29
+ #
30
+ # Each callback class will be instantiated with the appropriate model instance. That instance is accessible through
31
+ # the `object` method.
32
+ #
33
+ # @example Define a callback to hash the password prior creation
34
+ # class UserCallbacks
35
+ # include Guacamole::Callbacks
36
+ #
37
+ # before_create :encrypt_password
38
+ #
39
+ # def encrypt_password
40
+ # object.encrypted_password = BCrypt::Password.create(object.password)
41
+ # end
42
+ # end
43
+ #
44
+ # @!method self.before_validate(method_name)
45
+ # Registers a method to be run before the validation will happen
46
+ #
47
+ # @param [Symbol] method_name The name of the method to be executed
48
+ # @api public
49
+ #
50
+ # @!method self.around_validate(method_name)
51
+ # Registers a method to be run before and after the validation will happen.
52
+ #
53
+ # @param [Symbol] method_name The name of the method to be executed
54
+ # @api public
55
+ # @note You must `yield` at some point in the method.
56
+ #
57
+ # @!method self.after_validate(method_name)
58
+ # Registers a method to be run after the validation happened
59
+ #
60
+ # @param [Symbol] method_name The name of the method to be executed
61
+ # @api public
62
+ #
63
+ # @!method self.before_save(method_name)
64
+ # Registers a method to be run before the collection class saves the model
65
+ #
66
+ # Saving a model will always happen, no matter if the model will be created or
67
+ # updated.
68
+ #
69
+ # @param [Symbol] method_name The name of the method to be executed
70
+ # @api public
71
+ #
72
+ # @!method self.around_save(method_name)
73
+ # Registers a method to be run before and after saving the model
74
+ #
75
+ # Saving a model will always happen, no matter if the model will be created or
76
+ # updated.
77
+ #
78
+ # @param [Symbol] method_name The name of the method to be executed
79
+ # @api public
80
+ # @note You must `yield` at some point in the method.
81
+ #
82
+ # @!method self.after_save(method_name)
83
+ # Registers a method to be run after the collection class has saved the model
84
+ #
85
+ # Saving a model will always happen, no matter if the model will be created or
86
+ # updated.
87
+ #
88
+ # @param [Symbol] method_name The name of the method to be executed
89
+ # @api public
90
+ #
91
+ # @!method self.before_create(method_name)
92
+ # Registers a method to be run before initially creating the model in the database
93
+ #
94
+ # @param [Symbol] method_name The name of the method to be executed
95
+ # @api public
96
+ #
97
+ # @!method self.around_create(method_name)
98
+ # Registers a method to be run before and after creating the model
99
+ #
100
+ # @param [Symbol] method_name The name of the method to be executed
101
+ # @api public
102
+ # @note You must `yield` at some point in the method.
103
+ #
104
+ # @!method self.after_create(method_name)
105
+ # Registers a method to be run after the creation of the model
106
+ #
107
+ # @param [Symbol] method_name The name of the method to be executed
108
+ # @api public
109
+ #
110
+ # @!method self.before_update(method_name)
111
+ # Registers a method to be run before updating the model
112
+ #
113
+ # @param [Symbol] method_name The name of the method to be executed
114
+ # @api public
115
+ #
116
+ # @!method self.around_update(method_name)
117
+ # Registers a method to be run before and after updating the model
118
+ #
119
+ # @param [Symbol] method_name The name of the method to be executed
120
+ # @api public
121
+ # @note You must `yield` at some point in the method.
122
+ #
123
+ # @!method self.after_update(method_name)
124
+ # Registers a method to be run after the model has been updated
125
+ #
126
+ # @param [Symbol] method_name The name of the method to be executed
127
+ # @api public
128
+ #
129
+ # @!method self.before_delete(method_name)
130
+ # Registers a method to be run before deleting the model
131
+ #
132
+ # @param [Symbol] method_name The name of the method to be executed
133
+ # @api public
134
+ #
135
+ # @!method self.around_delete(method_name)
136
+ # Registers a method to be run before and after the deletion
137
+ #
138
+ # @param [Symbol] method_name The name of the method to be executed
139
+ # @api public
140
+ # @note You must `yield` at some point in the method.
141
+ #
142
+ # @!method self.after_delete(method_name)
143
+ # Registers a method to be run after the deletion of the model has happened
144
+ #
145
+ # @param [Symbol] method_name The name of the method to be executed
146
+ # @api public
147
+ module Callbacks
148
+ extend ActiveSupport::Concern
149
+ # @!parse extend ActiveModel:Callbacks
150
+
151
+ included do
152
+ extend ActiveModel::Callbacks
153
+
154
+ define_model_callbacks :validate, :save, :create, :update, :delete
155
+
156
+ before_create :add_create_timestamps
157
+ before_update :update_updated_at_timestamp
158
+ end
159
+
160
+ # The default callback to be used if no custom callback was defined. This is done because it simplifies
161
+ # the callback invocation code and allows us to use callbacks for adding time stamps to the models.
162
+ class DefaultCallback
163
+ include Guacamole::Callbacks
164
+ end
165
+
166
+ # A proxy class around the callback class itself.
167
+ #
168
+ # The sole reason for its existence is to specify multiple callback runs at once. The alternative
169
+ # would have been to nest the `run_callbacks` calls within the caller. It was decided to have bit
170
+ # more complex proxy class to hide those details from the caller.
171
+ #
172
+ # @example
173
+ # callbacks = Callbacks.callbacks_for(model)
174
+ # callbacks.run_callbacks :save, :create do
175
+ # CakeCollection.create model
176
+ # end
177
+ # @private
178
+ class CallbackProxy
179
+ attr_reader :callbacks
180
+
181
+ # Create a new proxy with the original callbacks class as input
182
+ #
183
+ # @param [Callbacks] callbacks The original callback class to be executed
184
+ def initialize(callbacks)
185
+ @callbacks = callbacks
186
+ end
187
+
188
+ # Runs the given kinds of callbacks
189
+ #
190
+ # @param [Array<Symbol>] callbacks_to_run One or more kinds of callbacks to be run
191
+ # @yield Will call the code block wrapped by the given callbacks
192
+ def run_callbacks(*callbacks_to_run, &block)
193
+ outer = callbacks_to_run.pop
194
+
195
+ if callbacks_to_run.empty?
196
+ @callbacks.run_callbacks(outer, &block)
197
+ else
198
+ @callbacks = run_callbacks(*callbacks_to_run) do
199
+ @callbacks.run_callbacks(outer, &block)
200
+ end
201
+ end
202
+ end
203
+ end
204
+
205
+ class << self
206
+ # Register a callback class to be used with the model
207
+ #
208
+ # @api private
209
+ # @param [Model] model_class The class of a model
210
+ # @param [Callbacks] callback_class The class of the callbacks
211
+ def register_callback(model_class, callback_class)
212
+ registry[model_class] = callback_class
213
+ end
214
+
215
+ # Retrieve the callback instance for the given model
216
+ #
217
+ # @api private
218
+ # @params [Model] model The model instance for which callbacks must be executed
219
+ # @return [Callbacks] A callback instance with the given model accessible via `object`
220
+ def callbacks_for(model)
221
+ CallbackProxy.new registry[model.class].new(model)
222
+ end
223
+
224
+ # The internal storage of the callback-model pairs
225
+ #
226
+ # @api private
227
+ # @return [Hash] A hash with the default set to the `DefaultCallback`
228
+ def registry
229
+ @registry ||= Hash.new(DefaultCallback)
230
+ end
231
+ end
232
+
233
+ # Create a new callback instance with the given model instance
234
+ #
235
+ # @param [Model] model_instance The model instance the callbacks should be executed for
236
+ def initialize(model_instance)
237
+ @object = model_instance
238
+ end
239
+
240
+ # Provides access to the model instance.
241
+ #
242
+ # @retun [Model] A model instance
243
+ # @api public
244
+ def object
245
+ @object
246
+ end
247
+
248
+ # Sets `created_at` and `updated_at` to `Time.now`
249
+ def add_create_timestamps
250
+ object.created_at = Time.now
251
+ update_updated_at_timestamp
252
+ end
253
+
254
+ # Sets `updated_at` to `Time.now`
255
+ def update_updated_at_timestamp
256
+ object.updated_at = Time.now
257
+ end
258
+ end
259
+ end