HornsAndHooves-moribus 0.1.0

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