moribus 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (63) hide show
  1. checksums.yaml +15 -0
  2. data/.gitignore +35 -0
  3. data/.rspec +4 -0
  4. data/.ruby-gemset +1 -0
  5. data/.ruby-version +1 -0
  6. data/.simplecov +42 -0
  7. data/.travis.yml +8 -0
  8. data/Gemfile +19 -0
  9. data/LICENSE +20 -0
  10. data/README.md +104 -0
  11. data/Rakefile +15 -0
  12. data/lib/colorized_text.rb +33 -0
  13. data/lib/moribus.rb +133 -0
  14. data/lib/moribus/aggregated_behavior.rb +80 -0
  15. data/lib/moribus/aggregated_cache_behavior.rb +76 -0
  16. data/lib/moribus/alias_association.rb +106 -0
  17. data/lib/moribus/extensions.rb +37 -0
  18. data/lib/moribus/extensions/delegate_associated.rb +48 -0
  19. data/lib/moribus/extensions/has_aggregated_extension.rb +94 -0
  20. data/lib/moribus/extensions/has_current_extension.rb +17 -0
  21. data/lib/moribus/macros.rb +120 -0
  22. data/lib/moribus/tracked_behavior.rb +91 -0
  23. data/lib/moribus/version.rb +3 -0
  24. data/moribus.gemspec +33 -0
  25. data/spec/dummy/README.rdoc +261 -0
  26. data/spec/dummy/Rakefile +7 -0
  27. data/spec/dummy/app/assets/javascripts/application.js +15 -0
  28. data/spec/dummy/app/assets/stylesheets/application.css +13 -0
  29. data/spec/dummy/app/controllers/application_controller.rb +3 -0
  30. data/spec/dummy/app/helpers/application_helper.rb +2 -0
  31. data/spec/dummy/app/mailers/.gitkeep +0 -0
  32. data/spec/dummy/app/models/.gitkeep +0 -0
  33. data/spec/dummy/app/views/layouts/application.html.erb +14 -0
  34. data/spec/dummy/config.ru +4 -0
  35. data/spec/dummy/config/application.rb +61 -0
  36. data/spec/dummy/config/boot.rb +10 -0
  37. data/spec/dummy/config/database.yml +25 -0
  38. data/spec/dummy/config/environment.rb +5 -0
  39. data/spec/dummy/config/environments/development.rb +37 -0
  40. data/spec/dummy/config/environments/production.rb +67 -0
  41. data/spec/dummy/config/environments/test.rb +37 -0
  42. data/spec/dummy/config/initializers/backtrace_silencers.rb +7 -0
  43. data/spec/dummy/config/initializers/inflections.rb +15 -0
  44. data/spec/dummy/config/initializers/mime_types.rb +5 -0
  45. data/spec/dummy/config/initializers/secret_token.rb +7 -0
  46. data/spec/dummy/config/initializers/session_store.rb +8 -0
  47. data/spec/dummy/config/initializers/wrap_parameters.rb +14 -0
  48. data/spec/dummy/config/locales/en.yml +5 -0
  49. data/spec/dummy/config/routes.rb +58 -0
  50. data/spec/dummy/db/test.sqlite3 +0 -0
  51. data/spec/dummy/lib/assets/.gitkeep +0 -0
  52. data/spec/dummy/log/.gitkeep +0 -0
  53. data/spec/dummy/public/404.html +26 -0
  54. data/spec/dummy/public/422.html +26 -0
  55. data/spec/dummy/public/500.html +25 -0
  56. data/spec/dummy/public/favicon.ico +0 -0
  57. data/spec/dummy/script/rails +6 -0
  58. data/spec/moribus/alias_association_spec.rb +88 -0
  59. data/spec/moribus/macros_spec.rb +7 -0
  60. data/spec/moribus_spec.rb +332 -0
  61. data/spec/spec_helper.rb +15 -0
  62. data/spec/support/moribus_spec_model.rb +57 -0
  63. metadata +209 -0
@@ -0,0 +1,15 @@
1
+ ---
2
+ !binary "U0hBMQ==":
3
+ metadata.gz: !binary |-
4
+ ZDcwN2E3NWU5NjI0MDk4OTU1NWE4OGVlYjEwZmViNmRkZTkzMDZjNQ==
5
+ data.tar.gz: !binary |-
6
+ OGM1MjU5YzVmMzA0YzViZWNlNDBkZWNjYjljNTQ1YWNlOGNkODkzOA==
7
+ SHA512:
8
+ metadata.gz: !binary |-
9
+ YjRmMTY0MWJiNjcwNjU0ZDI3OGIyNDk4ZDQ3ZTVkNmY5YTJjNzU0NTE3NWI0
10
+ NzFmNWM3MWQ3YmNkNzliZmUzZWMzNjg2ZWQzOGNlN2FlMDA4NzJiNWYwMDAy
11
+ OTg1OWNhYmUwNTA2NDNmZjJiYzM0NGUwODk2OTZiMGIyM2FkMGQ=
12
+ data.tar.gz: !binary |-
13
+ YWQ4MmRjYzBkZGE4MWVlNmExNmJiNDk3ZjFjY2Y2OGZlMTZmOGRmMmRiOTll
14
+ Yjk1YzFkNjA1OGIxMTJjNTRmMTYyZWQ4ZWRlMTg2ZWQzODQ5ZWFhZDhlNWQ3
15
+ MDNmOGVkZTYwZjE4OGQ4ZjI4MGQxNDk1ZThkYjIzODhhMTliM2U=
@@ -0,0 +1,35 @@
1
+ # simplecov generated
2
+ coverage
3
+ coverage.data
4
+ !coverage/.resultset.json
5
+
6
+ # rdoc generated
7
+ rdoc
8
+
9
+ # yard generated
10
+ doc
11
+ .yardoc
12
+
13
+ # bundler
14
+ .bundle
15
+
16
+ # jeweler generated
17
+ pkg
18
+
19
+ *.gem
20
+ Gemfile.lock
21
+
22
+ # IDE generated
23
+ .idea
24
+
25
+ log/*.log
26
+ spec/dummy/log/*.log
27
+
28
+ # exclude everything in tmp
29
+ tmp/*
30
+ # except the metric_fu directory
31
+ !tmp/metric_fu/
32
+ # but exclude everything *in* the metric_fu directory
33
+ tmp/metric_fu/*
34
+ # except for the _data directory to track metrical outputs
35
+ !tmp/metric_fu/_data/
data/.rspec ADDED
@@ -0,0 +1,4 @@
1
+ --color
2
+ --format nested
3
+ --order rand
4
+ --profile
@@ -0,0 +1 @@
1
+ moribus
@@ -0,0 +1 @@
1
+ 1.9.3
@@ -0,0 +1,42 @@
1
+ require "simplecov-rcov-text"
2
+ require "colorized_text"
3
+ include ColorizedText
4
+
5
+ SimpleCov.formatter = SimpleCov::Formatter::MultiFormatter[
6
+ SimpleCov::Formatter::RcovTextFormatter,
7
+ SimpleCov::Formatter::HTMLFormatter
8
+ ]
9
+ SimpleCov.start do
10
+ add_filter "/spec/"
11
+
12
+ # Fail the build when coverage is weak:
13
+ at_exit do
14
+ SimpleCov.result.format!
15
+ threshold, actual = 98.475, SimpleCov.result.covered_percent
16
+ if actual < threshold
17
+ msg = "\nLow coverage: "
18
+ msg << red("#{actual}%")
19
+ msg << " is #{red 'under'} the threshold: "
20
+ msg << green("#{threshold}%.")
21
+ msg << "\n"
22
+ $stderr.puts msg
23
+ exit 1
24
+ else
25
+ # Precision: three decimal places:
26
+ actual_trunc = (actual * 1000).floor / 1000.0
27
+ msg = "\nCoverage: "
28
+ msg << green("#{actual}%")
29
+ msg << " is #{green 'over'} the threshold: "
30
+ if actual_trunc > threshold
31
+ msg << bold(yellow("#{threshold}%. "))
32
+ msg << "Please update the threshold to: "
33
+ msg << bold(green("#{actual_trunc}% "))
34
+ msg << "in ./.simplecov."
35
+ else
36
+ msg << green("#{threshold}%.")
37
+ end
38
+ msg << "\n"
39
+ $stdout.puts msg
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,8 @@
1
+ language: ruby
2
+ script: "bundle exec rake spec"
3
+ rvm:
4
+ - 1.9.3
5
+ - 2.0.0
6
+ notifications:
7
+ email:
8
+ - a.kuzko@gmail.com
data/Gemfile ADDED
@@ -0,0 +1,19 @@
1
+ source "https://rubygems.org"
2
+
3
+ # Specify your gem's dependencies in moribus.gemspec
4
+ gemspec
5
+
6
+ gem "power_enum", "~> 1.3"
7
+
8
+ group :development do
9
+ gem "redcarpet"
10
+ gem "yard"
11
+ gem "pry"
12
+ end
13
+
14
+ group :test do
15
+ gem "simplecov", :require => false
16
+ gem "simplecov-rcov-text", :require => false
17
+
18
+ gem "timecop"
19
+ end
data/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2013 TMXCredit, authors Artem Kuzko, Sergey Potapov
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy of
6
+ this software and associated documentation files (the "Software"), to deal in
7
+ the Software without restriction, including without limitation the rights to
8
+ use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
9
+ the Software, and to permit persons to whom the Software is furnished to do so,
10
+ subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
17
+ FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
18
+ COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
19
+ IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
20
+ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,104 @@
1
+ # Moribus
2
+
3
+ [![Build Status](https://secure.travis-ci.org/TMXCredit/moribus.png)](http://travis-ci.org/TMXCredit/moribus)
4
+
5
+ Moribus is a set of tools for managing complex graphs of ActiveRecord objects
6
+ for which there are many inbound foreign keys, attributes and associations with
7
+ high rates of change, and business demands for well-tracked change history.
8
+
9
+ ##AggregatedBehavior
10
+
11
+ AggregatedBehavior implements a pattern in which an object's identity
12
+ is modeled apart from its attributes and outbound associations. This enables
13
+ higher level of normalization of your data, since one set of properties
14
+ may be shared among multiple objects. This set of properties - attributes
15
+ and outbound associations - are modeled on an object called an "info object".
16
+ And we say that this info object is aggregated by host object(s) and it acts
17
+ (behaves) as aggregated. When an aggregated object is about to be saved, it
18
+ looks up for an existing record with the same attributes in the database under
19
+ the hood, and if it founds, it 'replaces' itself by that record. This allows
20
+ you to work with attributes of your entity as if they are properties of an
21
+ actual model and normalize your data at the same time.
22
+
23
+ Inbound foreign keys will always point at the same object in memory, and the
24
+ object will never be stale, as it has no attributes of its own that are subject
25
+ to change. This is useful for objects with many inbound foreign keys and
26
+ high-traffic attributes/associations, such as statuses. Without this pattern
27
+ it would be difficult to avoid many StaleObjectErrors.
28
+
29
+ ##TrackedBehavior
30
+
31
+ TrackedBehavior implements history tracking on the stack of objects
32
+ representing the identity object's attributes and outbound associations.
33
+ When a model behaves as a tracked behavior, it will never get actually
34
+ updated. Instead, it will update it's own 'is_current' column to false
35
+ and will be saved as a new record with new attribute values and
36
+ 'is_current' column as 'true'. Thus, under the hood, new attributes
37
+ will supersede old attributes, leaving old record as history one.
38
+
39
+ ##Macros, Associations and Combination
40
+
41
+ Despite the fact that Behaviors may be used by models on they're own,
42
+ they main purpose is to be used within associations and in conjunction.
43
+ The best way to demonstrate this is by example.
44
+
45
+ Lets assume we have a User entity with attributes that should be tracked
46
+ and normalized. Those attributes may be, for example, `:first_name`,
47
+ `:last_name` and `:status` as enumerated integer value. This makes entity
48
+ may be represented with three models: `User` - as main model for interactions,
49
+ tracked `UserInfo` (`user_id`, `person_name_id`, `status`) for tracking, and
50
+ aggregated `UserName` (`first_name`, `last_name`) for name normalization.
51
+ Class definitions for that models will look as follows:
52
+
53
+ ```ruby
54
+ class User < ActiveRecord::Base
55
+ has_one_current :user_info
56
+ delegate_associated :user_name, :to => :user_info
57
+ end
58
+
59
+ class UserInfo < ActiveRecord::Base
60
+ has_aggregated :person_name
61
+ acts_as_tracked
62
+ end
63
+
64
+ class UserName < ActiveRecord::Base
65
+ acts_as_aggregated
66
+ end
67
+ ```
68
+
69
+ Despite the fact that internal representation is more complicated now,
70
+ top-level operations will look exactly the same:
71
+
72
+ ```ruby
73
+ user = User.create(:first_name => 'John', :last_name => 'Smith', :status => 0)
74
+ # This creates User(id: 1) record, PersonName(id: 1, first_name: 'John', last_name: 'Smith')
75
+ # record and UserInfo(id: 1, user_id: 1, person_name_id: 1, status: 0, is_current: true)
76
+
77
+ user.update_attributes(:status => 1)
78
+ # This creates new UserInfo(id: 2, user_id: 1, person_name_id: 1, status: 1, is_current: true)
79
+ # record, and changes UserInfo(id: 1) record's 'is_current' attribute to false.
80
+
81
+ user.update_attributes(:first_name => 'Mike')
82
+ # This creates new PersonName(id: 2, first_name: 'Mike', last_name: 'Smith') record and new
83
+ # current UserInfo(id: 3, user_id: 1, person_name_id: 2, :status: 1, is_current: true)
84
+
85
+ # Now, if we want to create another 'John Smith' user:
86
+ user2 = User.create(:first_name => 'John', :last_name => 'Smith', :status => 5)
87
+ # This creates User(id: 2) record and UserInfo(id: 4, user_id: 2, person_name_id: 1, status: 5, is_current: true)
88
+ # record that reuses existing UserName information.
89
+ ```
90
+
91
+ ## Run tests
92
+
93
+ ```sh
94
+ rake spec
95
+ ```
96
+
97
+ ## Credits
98
+
99
+ * [Artem Kuzko](https://github.com/akuzko)
100
+ * [Potapov Sergey](https://github.com/greyblake)
101
+
102
+ ## Copyright
103
+
104
+ Copyright (c) 2013 TMX Credit.
@@ -0,0 +1,15 @@
1
+ require "bundler/gem_tasks"
2
+
3
+ require 'rspec/core/rake_task'
4
+ task :default => :spec
5
+ RSpec::Core::RakeTask.new
6
+
7
+ require 'rdoc/task'
8
+ Rake::RDocTask.new do |rdoc|
9
+ version = File.exist?('VERSION') ? File.read('VERSION') : ""
10
+
11
+ rdoc.rdoc_dir = 'rdoc'
12
+ rdoc.title = "moribus #{version}"
13
+ rdoc.rdoc_files.include('README*')
14
+ rdoc.rdoc_files.include('lib/**/*.rb')
15
+ end
@@ -0,0 +1,33 @@
1
+ # Colorizes text with ASCII colors.
2
+ # == Usage:
3
+ # include ColorizedText
4
+ #
5
+ # puts green "OK" # => green output
6
+ # puts bold "Running... # => bold output
7
+ # puts bold green "OK!!!" # => bold green output
8
+ module ColorizedText
9
+ # Colorize text using ASCII color code
10
+ def colorize(text, code)
11
+ "\033[#{code}m#{text}\033[0m"
12
+ end
13
+
14
+ # :nodoc:
15
+ def yellow(text)
16
+ colorize(text, 33)
17
+ end
18
+
19
+ # :nodoc:
20
+ def green(text)
21
+ colorize(text, 32)
22
+ end
23
+
24
+ # :nodoc:
25
+ def red(text)
26
+ colorize(text, 31)
27
+ end
28
+
29
+ # :nodoc:
30
+ def bold(text)
31
+ colorize(text, 1)
32
+ end
33
+ end
@@ -0,0 +1,133 @@
1
+ # Introduces Aggregated and Tracked behavior to ActiveRecord::Base models, as well
2
+ # as Macros and Extensions modules for more efficient usage. Effectively replaces
3
+ # both Aggregatable and Trackable modules.
4
+ module Moribus
5
+ extend ActiveSupport::Concern
6
+ extend ActiveSupport::Autoload
7
+
8
+ autoload :AliasAssociation
9
+ autoload :AggregatedBehavior
10
+ autoload :AggregatedCacheBehavior
11
+ autoload :TrackedBehavior
12
+ autoload :Macros
13
+ autoload :Extensions
14
+
15
+ included do
16
+ include AliasAssociation
17
+ include Extensions
18
+ extend Macros
19
+ end
20
+
21
+ # :nodoc:
22
+ module ClassMethods
23
+ # Adds aggregated behavior to a model.
24
+ def acts_as_aggregated(options = {})
25
+ options.symbolize_keys!
26
+
27
+ options.assert_valid_keys(:cache_by, :non_content_columns)
28
+ include AggregatedBehavior
29
+
30
+ if options[:cache_by].present?
31
+ @aggregated_caching_column = options[:cache_by]
32
+ include AggregatedCacheBehavior
33
+ end
34
+
35
+ if options[:non_content_columns]
36
+ self.aggregated_behaviour_non_content_columns += Array.wrap(options[:non_content_columns]).map(&:to_s)
37
+ end
38
+ end
39
+ private :acts_as_aggregated
40
+
41
+ # Adds tracked behavior to a model
42
+ def acts_as_tracked(options = {})
43
+ options.symbolize_keys!
44
+
45
+ options.assert_valid_keys(:preceding_key)
46
+ include TrackedBehavior
47
+
48
+ @preceding_key_column = options[:preceding_key]
49
+ end
50
+ private :acts_as_tracked
51
+
52
+ # Return +true+ if self was declared as +acts_as_aggregated+.
53
+ def acts_as_aggregated?
54
+ self < AggregatedBehavior
55
+ end
56
+
57
+ # Return +true+ if self was declared as +acts_as_tracked+.
58
+ def acts_as_tracked?
59
+ self < TrackedBehavior
60
+ end
61
+ end
62
+
63
+ # Marks +self+ as a new record. Sets +id+ attribute to nil, but memorizes
64
+ # the old value in case of exception.
65
+ def to_new_record!
66
+ store_before_to_new_record_values
67
+ reset_persistence_values
68
+ @new_record = true
69
+ end
70
+
71
+ # Marks +self+ as persistent record. If another record is passed, uses its
72
+ # persistence attributes (id, timestamps). If nil is passed as an argument,
73
+ # marks +self+ as persisted record and sets +id+ to memorized value.
74
+ def to_persistent!(existing = nil)
75
+ if existing
76
+ self.id = existing.id
77
+ self.created_at = existing.created_at if respond_to?(:created_at)
78
+ self.updated_at = existing.updated_at if respond_to?(:updated_at)
79
+ @changed_attributes = {}
80
+ else
81
+ restore_before_to_new_record_values
82
+ end
83
+ @new_record = false
84
+ true
85
+ end
86
+
87
+ # Stores original record id for tracking purposes.
88
+ def set_parent
89
+ tbc = self.class.preceding_key_column
90
+ if tbc && self.respond_to?(tbc)
91
+ write_attribute(tbc, @_before_to_new_record_values[:id])
92
+ end
93
+ end
94
+
95
+ # Helper method used by has_aggregated (in fact, belongs_to)
96
+ # association during autosave.
97
+ def updated_as_aggregated?
98
+ !!@updated_as_aggregated
99
+ end
100
+
101
+ # Save persistence values of id, updated_at and created_at to instance
102
+ # variable to have an ability to set them back if object fails to
103
+ # be saved.
104
+ def store_before_to_new_record_values
105
+ values = {:id => id}
106
+ values[:updated_at] = updated_at if respond_to?(:updated_at)
107
+ values[:created_at] = created_at if respond_to?(:created_at)
108
+ @_before_to_new_record_values = values
109
+ end
110
+ private :store_before_to_new_record_values
111
+
112
+ # Set persistence values of id, updated_at and created_at back.
113
+ def restore_before_to_new_record_values
114
+ values = @_before_to_new_record_values
115
+ self.id = values[:id]
116
+ self.created_at = values[:created_at] if respond_to?(:updated_at=)
117
+ self.updated_at = values[:updated_at] if respond_to?(:updated_at=)
118
+ end
119
+ private :restore_before_to_new_record_values
120
+
121
+ # Set id, updated_at and created_at to nil in order to
122
+ # update them when new record is created.
123
+ def reset_persistence_values
124
+ self.id = nil
125
+ self.updated_at = nil if respond_to?(:updated_at=)
126
+ self.created_at = nil if respond_to?(:created_at=)
127
+ end
128
+ private :reset_persistence_values
129
+ end
130
+
131
+ ActiveRecord::Base.class_eval do
132
+ include Moribus
133
+ end