moribus 0.0.1

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 (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