HornsAndHooves-moribus 0.1.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 (63) hide show
  1. checksums.yaml +7 -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 +17 -0
  9. data/HornsAndHooves-moribus.gemspec +31 -0
  10. data/LICENSE +21 -0
  11. data/README.md +110 -0
  12. data/Rakefile +15 -0
  13. data/lib/colorized_text.rb +33 -0
  14. data/lib/moribus.rb +138 -0
  15. data/lib/moribus/aggregated_behavior.rb +80 -0
  16. data/lib/moribus/aggregated_cache_behavior.rb +76 -0
  17. data/lib/moribus/alias_association.rb +111 -0
  18. data/lib/moribus/extensions.rb +37 -0
  19. data/lib/moribus/extensions/delegate_associated.rb +48 -0
  20. data/lib/moribus/extensions/has_aggregated_extension.rb +94 -0
  21. data/lib/moribus/extensions/has_current_extension.rb +17 -0
  22. data/lib/moribus/macros.rb +135 -0
  23. data/lib/moribus/tracked_behavior.rb +91 -0
  24. data/lib/moribus/version.rb +3 -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 +53 -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 +31 -0
  40. data/spec/dummy/config/environments/production.rb +70 -0
  41. data/spec/dummy/config/environments/test.rb +34 -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 +247 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: de348b300137b8ce46c31b042de38e77a0880a5b
4
+ data.tar.gz: 675265b328868a116411c59fe89f7f89e369fcdd
5
+ SHA512:
6
+ metadata.gz: 0cfa65026b301739a2cb71b0027160f26978b1212f66df68d8dd681815a5e472a0f3a59d737a9e59a8c74cf9df77892093f4e16c32b0b58678880acc04bf3149
7
+ data.tar.gz: 862712050f65298176c83c3191582b38574d10cdc12025574c8309c3056b9ca472bd1ed93892563037b51ca61ad8a0169cb7be876d9c96a8e6a0ebefb73028c1
data/.gitignore ADDED
@@ -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 documentation
3
+ --order rand
4
+ --profile
data/.ruby-gemset ADDED
@@ -0,0 +1 @@
1
+ moribus
data/.ruby-version ADDED
@@ -0,0 +1 @@
1
+ 2.1.2
data/.simplecov ADDED
@@ -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
data/.travis.yml ADDED
@@ -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,17 @@
1
+ source "https://rubygems.org"
2
+
3
+ # Specify your gem's dependencies in moribus.gemspec
4
+ gemspec
5
+
6
+ group :development do
7
+ gem "redcarpet"
8
+ gem "yard"
9
+ gem "pry"
10
+ end
11
+
12
+ group :test do
13
+ gem "simplecov", require: false
14
+ gem "simplecov-rcov-text", require: false
15
+
16
+ gem "timecop"
17
+ end
@@ -0,0 +1,31 @@
1
+ # -*- encoding: utf-8 -*-
2
+
3
+ $:.push File.expand_path("../lib", __FILE__)
4
+ require "moribus/version"
5
+
6
+ Gem::Specification.new do |s|
7
+ s.name = "HornsAndHooves-moribus"
8
+ s.version = Moribus::VERSION
9
+ s.authors = ["HornsAndHooves", "Artem Kuzko", "Sergey Potapov"]
10
+ s.email = ["a.kuzko@gmail.com", "blake131313@gmail.com"]
11
+ s.homepage = "https://github.com/HornsAndHooves/moribus"
12
+ s.licenses = ["MIT"]
13
+ s.summary = %q{Introduces Aggregated and Tracked behavior to ActiveRecord::Base models}
14
+ s.description = %q{Introduces Aggregated and Tracked behavior to ActiveRecord::Base models, as well
15
+ as Macros and Extensions modules for more efficient usage.}
16
+
17
+ s.files = `git ls-files`.split("\n")
18
+ s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
19
+ s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
20
+ s.require_paths = ["lib"]
21
+
22
+ # specify any dependencies here; for example:
23
+ s.add_dependency "rails", "~> 4.0.5"
24
+ s.add_dependency "power_enum", ">= 2.7.0"
25
+ s.add_dependency "yard", ">= 0"
26
+
27
+ s.add_development_dependency "rake"
28
+ s.add_development_dependency "rspec"
29
+ s.add_development_dependency "rspec-rails"
30
+ s.add_development_dependency "sqlite3"
31
+ end
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2014 HornsAndHooves
4
+ Copyright (c) 2013 TMXCredit, authors Artem Kuzko, Sergey Potapov
5
+
6
+ Permission is hereby granted, free of charge, to any person obtaining a copy of
7
+ this software and associated documentation files (the "Software"), to deal in
8
+ the Software without restriction, including without limitation the rights to
9
+ use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
10
+ the Software, and to permit persons to whom the Software is furnished to do so,
11
+ subject to the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be included in all
14
+ copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
18
+ FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
19
+ COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
20
+ IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
21
+ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,110 @@
1
+ # Moribus
2
+
3
+ [![Build Status](https://secure.travis-ci.org/HornsAndHooves/moribus.png)](http://travis-ci.org/HornsAndHooves/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
+ a 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 is found, it 'replaces' itself with 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 actually get
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 the
36
+ 'is_current' column as 'true'. Thus, under the hood, new attributes
37
+ will supersede old attributes, leaving the old record as historical.
38
+
39
+ ##Macros, Associations and Combination
40
+
41
+ Despite the fact that Moribus may be used by models on its own,
42
+ its main purpose is to be used within associations, and in conjunction
43
+ with associations. The best way to demonstrate this is by example.
44
+
45
+ Let's 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 entity
48
+ may be represented with three models: `User` - with 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 these models will appear 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
+ ## Use it
92
+
93
+ gem 'HornsAndHooves-moribus', require: 'moribus'
94
+
95
+ ## Run tests
96
+
97
+ ```sh
98
+ rake spec
99
+ ```
100
+
101
+ ## Credits
102
+
103
+ * [Artem Kuzko](https://github.com/akuzko)
104
+ * [Potapov Sergey](https://github.com/greyblake)
105
+
106
+ ## Copyright
107
+
108
+ Copyright (c) 2014 HornsAndHooves.
109
+
110
+ Copyright (c) 2013 TMX Credit.
data/Rakefile ADDED
@@ -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
data/lib/moribus.rb ADDED
@@ -0,0 +1,138 @@
1
+ require 'power_enum'
2
+
3
+ # Introduces Aggregated and Tracked behavior to ActiveRecord::Base models, as well
4
+ # as Macros and Extensions modules for more efficient usage. Effectively replaces
5
+ # both Aggregatable and Trackable modules.
6
+ module Moribus
7
+ extend ActiveSupport::Concern
8
+ extend ActiveSupport::Autoload
9
+
10
+ autoload :AliasAssociation
11
+ autoload :AggregatedBehavior
12
+ autoload :AggregatedCacheBehavior
13
+ autoload :TrackedBehavior
14
+ autoload :Macros
15
+ autoload :Extensions
16
+
17
+ included do
18
+ include AliasAssociation
19
+ include Extensions
20
+ extend Macros
21
+ end
22
+
23
+ # :nodoc:
24
+ module ClassMethods
25
+ # Adds aggregated behavior to a model.
26
+ def acts_as_aggregated(options = {})
27
+ options.symbolize_keys!
28
+
29
+ options.assert_valid_keys(:cache_by, :non_content_columns)
30
+ include AggregatedBehavior
31
+
32
+ if options[:cache_by].present?
33
+ @aggregated_caching_column = options[:cache_by]
34
+ include AggregatedCacheBehavior
35
+ end
36
+
37
+ if options[:non_content_columns]
38
+ self.aggregated_behaviour_non_content_columns += Array.wrap(options[:non_content_columns]).map(&:to_s)
39
+ end
40
+ end
41
+ private :acts_as_aggregated
42
+
43
+ # Adds tracked behavior to a model
44
+ def acts_as_tracked(options = {})
45
+ options.symbolize_keys!
46
+
47
+ options.assert_valid_keys(:preceding_key)
48
+ include TrackedBehavior
49
+
50
+ @preceding_key_column = options[:preceding_key]
51
+ end
52
+ private :acts_as_tracked
53
+
54
+ # Return +true+ if self was declared as +acts_as_aggregated+.
55
+ def acts_as_aggregated?
56
+ self < AggregatedBehavior
57
+ end
58
+
59
+ # Return +true+ if self was declared as +acts_as_tracked+.
60
+ def acts_as_tracked?
61
+ self < TrackedBehavior
62
+ end
63
+ end
64
+
65
+ # Marks +self+ as a new record. Sets +id+ attribute to nil, but memorizes
66
+ # the old value in case of exception.
67
+ def to_new_record!
68
+ store_before_to_new_record_values
69
+ reset_persistence_values
70
+ @new_record = true
71
+ end
72
+
73
+ # Marks +self+ as persistent record. If another record is passed, uses its
74
+ # persistence attributes (id, timestamps). If nil is passed as an argument,
75
+ # marks +self+ as persisted record and sets +id+ to memorized value.
76
+ def to_persistent!(existing = nil)
77
+ if existing
78
+ self.id = existing.id
79
+ self.created_at = existing.created_at if respond_to?(:created_at)
80
+ self.updated_at = existing.updated_at if respond_to?(:updated_at)
81
+ @changed_attributes = {}
82
+ else
83
+ restore_before_to_new_record_values
84
+ end
85
+ @new_record = false
86
+ true
87
+ end
88
+
89
+ # Stores original record id for tracking purposes.
90
+ def set_parent
91
+ tbc = self.class.preceding_key_column
92
+ if tbc && self.respond_to?(tbc)
93
+ write_attribute(tbc, @_before_to_new_record_values[:id])
94
+ end
95
+ end
96
+
97
+ # Helper method used by has_aggregated (in fact, belongs_to)
98
+ # association during autosave.
99
+ def updated_as_aggregated?
100
+ !!@updated_as_aggregated
101
+ end
102
+
103
+ # Save persistence values of id, updated_at and created_at to instance
104
+ # variable to have an ability to set them back if object fails to
105
+ # be saved.
106
+ def store_before_to_new_record_values
107
+ values = {:id => id}
108
+ values[:updated_at] = updated_at if respond_to?(:updated_at)
109
+ values[:created_at] = created_at if respond_to?(:created_at)
110
+ @_before_to_new_record_values = values
111
+ end
112
+ private :store_before_to_new_record_values
113
+
114
+ # Set persistence values of id, updated_at and created_at back.
115
+ def restore_before_to_new_record_values
116
+ values = @_before_to_new_record_values
117
+ self.id = values[:id]
118
+ self.created_at = values[:created_at] if respond_to?(:updated_at=)
119
+ self.updated_at = values[:updated_at] if respond_to?(:updated_at=)
120
+ end
121
+ private :restore_before_to_new_record_values
122
+
123
+ # Set id, updated_at and created_at to nil in order to
124
+ # update them when new record is created.
125
+ def reset_persistence_values
126
+ self.id = nil
127
+ self.updated_at = nil if respond_to?(:updated_at=)
128
+ self.created_at = nil if respond_to?(:created_at=)
129
+
130
+ # mark all other attributes is changing
131
+ (attributes.keys - changes.keys).each{ |key| self.send(:"#{key}_will_change!") }
132
+ end
133
+ private :reset_persistence_values
134
+ end
135
+
136
+ ActiveRecord::Base.class_eval do
137
+ include Moribus
138
+ end