harmonize 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/Gemfile +13 -0
- data/MIT-LICENSE +20 -0
- data/README.md +153 -0
- data/Rakefile +31 -0
- data/app/models/harmonize/log.rb +16 -0
- data/app/models/harmonize/modification.rb +18 -0
- data/lib/harmonize/base.rb +95 -0
- data/lib/harmonize/configuration.rb +10 -0
- data/lib/harmonize/engine.rb +4 -0
- data/lib/harmonize/errors.rb +7 -0
- data/lib/harmonize/gemdata.rb +15 -0
- data/lib/harmonize/strategies/basic_crud_strategy.rb +69 -0
- data/lib/harmonize/strategies/strategy.rb +31 -0
- data/lib/harmonize/strategies.rb +6 -0
- data/lib/harmonize.rb +9 -0
- data/lib/rails/generators/harmonize/migration_generator.rb +29 -0
- data/lib/rails/generators/harmonize/templates/migration.rb +34 -0
- metadata +88 -0
data/Gemfile
ADDED
@@ -0,0 +1,13 @@
|
|
1
|
+
source "http://rubygems.org"
|
2
|
+
|
3
|
+
gem 'rails', '3.0.9'
|
4
|
+
gem 'hashie'
|
5
|
+
|
6
|
+
group :development, :test, :console do
|
7
|
+
gem 'sqlite3'
|
8
|
+
gem 'rspec-rails', '~> 2'
|
9
|
+
gem 'ruby-debug19', :require => 'ruby-debug'
|
10
|
+
gem 'capybara', '~> 1'
|
11
|
+
gem 'database_cleaner'
|
12
|
+
gem 'factory_girl_rails'
|
13
|
+
end
|
data/MIT-LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright 2011 YOURNAME
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,153 @@
|
|
1
|
+
# harmonize
|
2
|
+
|
3
|
+
hahr-muh-nahyz -
|
4
|
+
|
5
|
+
1. to bring into harmony, accord, or agreement: to harmonize one's views with the new situation.
|
6
|
+
2. Music . to accompany with appropriate harmony.
|
7
|
+
3. to be in agreement in action, sense, or feeling: Though of different political parties, all the delegates harmonized on civil rights.
|
8
|
+
4. to sing in harmony.
|
9
|
+
|
10
|
+
harmonize is a rails 3 engine that allows one to harmonize entire scopes of an ActiveRecord model with arbitrary external data sources.
|
11
|
+
|
12
|
+
harmonize works by allowing one to setup feeds from external data called "sources". harmonize applies the "source" feeds to sets of ActiveRecord instances called "targets". These sets of "source" and "target" data are then handed of to a "strategy" to determine how to use the source to modify the target. When applying a "strategy" harmonize creates a Harmonize::Log which has_many Harmonize::Modifications. These records are then used to report information about the harmonize process.
|
13
|
+
|
14
|
+
## Simple Example
|
15
|
+
|
16
|
+
Lets pretend we work for a company that has a large list of stores. This list of stores in maintained in a database we don't have access to directly. The database administrator wrote a script to export the data in CSV format and upload it to our application. In our application we have a Store model but we always want it to be in harmony with the latest CSV file uploaded by the administrator. Every store has a unique name and an address, and the upstream database uses the name as the primary_key.
|
17
|
+
|
18
|
+
class Store < ActiveRecord::Base
|
19
|
+
validates :name, :address, :presence => true
|
20
|
+
|
21
|
+
# Setup a harmonizer
|
22
|
+
harmonize do |config|
|
23
|
+
config.key = :name
|
24
|
+
end
|
25
|
+
|
26
|
+
# By default harmonize expects a class method to be implemented
|
27
|
+
# that knows how to provide the source data feed
|
28
|
+
def self.harmonize_source_default
|
29
|
+
# parse the csv file and return
|
30
|
+
# an collection of hash like objects
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
With our Store model wired up as above, we will get a new class method on our model called "harmonize_default!". When we call harmonize_default! harmonize will use the default strategy to harmonize the source records with the target records. In order to understand what actions are taken to bring the targets into harmony, we need to understand Harmonize::Strategies, but that is getting us ahead of ourselves. First lets look at how we configure harmonize.
|
35
|
+
|
36
|
+
## Harmonize::Configuration
|
37
|
+
|
38
|
+
Each call to harmonize creates a Harmonize::Configuration instance that defines how to configure a harmonizer. Here is an example using all the currently available configuration options using non-default settings:
|
39
|
+
|
40
|
+
class Store < ActiveRecord::Base
|
41
|
+
harmonize do |config|
|
42
|
+
config.harmonizer_name = :custom_name
|
43
|
+
config.key = :the_key
|
44
|
+
config.source = lambda do
|
45
|
+
get_latest_source_data_feed!
|
46
|
+
end
|
47
|
+
config.target = lambda do
|
48
|
+
where(:active => true)
|
49
|
+
end
|
50
|
+
config.strategy = YourCustomStrategy
|
51
|
+
config.strategy_arguments = { :options => 'needed', :to => 'initialize', :your => 'custom strategy' }
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
### Harmonizer::Configuration.harmonizer_name
|
56
|
+
|
57
|
+
harmonize uses the configured harmonize_name as the name of the harmonizer being configured. Each harmonize_name may only be used once. This allows the harmonize method to be called more than once per model.
|
58
|
+
|
59
|
+
The default harmonizer_name is :default
|
60
|
+
This option is used to name special methods used by harmonize. The harmonization method is named with the following convention: "harmonize\_#{harmonizer\_name}!".
|
61
|
+
|
62
|
+
### Harmonizer::Configuration.key
|
63
|
+
|
64
|
+
harmonize uses the configured key to determine what attribute to use to find existing target records.
|
65
|
+
|
66
|
+
The default key is :id
|
67
|
+
|
68
|
+
### Harmonizer::Configuration.source
|
69
|
+
|
70
|
+
harmonize uses the configured source to gather the latest set of source records. This can be set to a lambda or any other callable object. The only requirement is that it returns a collection of hash like objects.
|
71
|
+
|
72
|
+
The default source is a lambda
|
73
|
+
This lambda calls the default source method who's name is determined by the harmonizer_name. The convention is: "harmonizer\_source\_#{harmonizer_name}".
|
74
|
+
|
75
|
+
### Harmonizer::Configuration.target
|
76
|
+
|
77
|
+
harmonize uses the configured target to gather the latest set of target records. This can be set to a lambda or any other callable object. The only requirement is that it returns an ActiveRecord::Relation
|
78
|
+
|
79
|
+
The default target is a lambda. This lambda returns an ActiveRecord::Relation via the ActiveRecord::Base#scoped method. Hint...all (named) scopes return ActiveRecord::Relation instances.
|
80
|
+
|
81
|
+
### Harmonizer::Configuration.strategy
|
82
|
+
|
83
|
+
harmonize uses the configured strategy to determine which Harmonize::Strategies::Strategy subclass to use when harmonizing. harmonize uses this setting as well as the strategy_arguments setting to create an instance of the Strategy subclass.
|
84
|
+
|
85
|
+
The default strategy is Harmonize::Strategies::BasicCrudStrategy
|
86
|
+
|
87
|
+
### Harmonizer::Configuration.target
|
88
|
+
|
89
|
+
harmonize uses the configured strategy_arguments to determine which arguments to use when initializing the set Harmonize::Strategies::Strategy subclass.
|
90
|
+
|
91
|
+
The default strategy is {}.
|
92
|
+
|
93
|
+
## Harmonize::Strategies
|
94
|
+
|
95
|
+
harmonize figures out how to use your source data to modify your target by using a Harmonize::Strategies::Strategy subclass. A strategy is responsible for determining when and how to create, update, and destroy your target records based on the source records.
|
96
|
+
|
97
|
+
The default harmonize strategy is Harmonize::Strategies::BasicCrudStrategy. This strategy assumes your source data to be a "source of truth" which reflects exactly how the entire target record set should look. Here are the steps it takes to achieve harmony:
|
98
|
+
|
99
|
+
1. For each record in the source, use the configured "key" to find out if a record with that key already exists.
|
100
|
+
2. If an existing record was found in target, update it with any attributes provided by source
|
101
|
+
3. If no existing record was found in target, create a new record with any attributes provided by source
|
102
|
+
4. Save the record and create a Harmonize::Modification record making if we created or updated
|
103
|
+
5. If save has errors, update the Harmonize::Modification record to reflect that and store the error messages
|
104
|
+
6. Save the key of the modified (or errored) record in a collection
|
105
|
+
7. After all source records have been harmonized, destory any target record with a key not in the modified keys collection
|
106
|
+
8. Save Harmonize::Log for reporting the actions took in this harmonization
|
107
|
+
|
108
|
+
Currently this is the only strategy provided by harmonize, but more will be added when I need them or you send them to me as a pull request.
|
109
|
+
|
110
|
+
## Installation
|
111
|
+
|
112
|
+
Add harmonize to the gem file for your rails application:
|
113
|
+
|
114
|
+
gem 'harmonize'
|
115
|
+
|
116
|
+
Run the migration generator in your rails application root:
|
117
|
+
|
118
|
+
rails generate harmonize:migration
|
119
|
+
|
120
|
+
Configure your model to use harmonize and implement your source:
|
121
|
+
|
122
|
+
class MyModel < ActiveRecord::Base
|
123
|
+
harmonize
|
124
|
+
|
125
|
+
def self.harmonize_source_default
|
126
|
+
# a collection of hash like objects
|
127
|
+
end
|
128
|
+
end
|
129
|
+
|
130
|
+
Use, report bugs, fix them, and send pull requests!
|
131
|
+
|
132
|
+
## Plans
|
133
|
+
|
134
|
+
## TODO
|
135
|
+
|
136
|
+
* Maybe move key from Configuration to a strategy_argument as it is not a configuration option really, but a way to change stratgey behaviour.
|
137
|
+
|
138
|
+
## Contributors
|
139
|
+
|
140
|
+
* Bram Swenson <bram@craniumisjar.com>
|
141
|
+
|
142
|
+
## Notes
|
143
|
+
|
144
|
+
Check out the rspec suite for more details about how harmonize works: [BasicCrudStrategy Spec](http://github.com/bramswenson/harmonize/blob/master/spec/lib/harmonize/strategies/basic_crud_strategy_spec.rb)
|
145
|
+
|
146
|
+
Please let me know how you use harmonize!
|
147
|
+
|
148
|
+
Thanks in advance!
|
149
|
+
|
150
|
+
## License
|
151
|
+
|
152
|
+
This project rocks and uses MIT-LICENSE.
|
153
|
+
|
data/Rakefile
ADDED
@@ -0,0 +1,31 @@
|
|
1
|
+
# encoding: UTF-8
|
2
|
+
require 'rubygems'
|
3
|
+
begin
|
4
|
+
require 'bundler/setup'
|
5
|
+
rescue LoadError
|
6
|
+
puts 'You must `gem install bundler` and `bundle install` to run rake tasks'
|
7
|
+
end
|
8
|
+
|
9
|
+
require 'rake'
|
10
|
+
require 'rspec/core'
|
11
|
+
require 'rspec/core/rake_task'
|
12
|
+
require 'bundler/gem_tasks'
|
13
|
+
|
14
|
+
namespace :test do
|
15
|
+
RSpec::Core::RakeTask.new(:spec)
|
16
|
+
desc 'Setup the test database'
|
17
|
+
task :dbsetup do
|
18
|
+
results = %x(
|
19
|
+
cd spec/dummy &&
|
20
|
+
rm db/*.sqlite3 &&
|
21
|
+
RAILS_ENV=test rake db:migrate
|
22
|
+
)
|
23
|
+
puts "dbsetup: #{results}" unless results == ''
|
24
|
+
end
|
25
|
+
|
26
|
+
desc 'Setup the test database and run rspec'
|
27
|
+
task :run => %w( test:dbsetup test:spec )
|
28
|
+
end
|
29
|
+
|
30
|
+
task :default => 'test:run'
|
31
|
+
|
@@ -0,0 +1,16 @@
|
|
1
|
+
module Harmonize
|
2
|
+
class Log < ActiveRecord::Base
|
3
|
+
set_table_name :harmonize_logs
|
4
|
+
|
5
|
+
validates :key, :class_name, :harmonizer_name, :strategy, :strategy_arguments, :presence => true
|
6
|
+
|
7
|
+
ModificationOptions = { :class_name => "Harmonize::Modification", :foreign_key => :harmonize_log_id }
|
8
|
+
|
9
|
+
has_many :modifications, ModificationOptions.merge(:inverse_of => :log, :dependent => :destroy)
|
10
|
+
has_many :created, ModificationOptions.merge(:conditions => { :modification_type => 'create' })
|
11
|
+
has_many :updated, ModificationOptions.merge(:conditions => { :modification_type => 'update' })
|
12
|
+
has_many :destroyed, ModificationOptions.merge(:conditions => { :modification_type => 'destroy'})
|
13
|
+
has_many :errored, ModificationOptions.merge(:conditions => { :modification_type => 'error' })
|
14
|
+
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
module Harmonize
|
2
|
+
class Modification < ActiveRecord::Base
|
3
|
+
set_table_name :harmonize_modifications
|
4
|
+
|
5
|
+
validates :harmonize_log_id, :modification_type, :presence => true
|
6
|
+
validates :modification_type, :inclusion => %w( create update destroy error )
|
7
|
+
|
8
|
+
serialize :instance_errors
|
9
|
+
|
10
|
+
belongs_to :log, :class_name => "Harmonize::Log",
|
11
|
+
:foreign_key => :harmonize_log_id, :inverse_of => :modifications
|
12
|
+
|
13
|
+
scope :created, where(:modification_type => 'create')
|
14
|
+
scope :updated, where(:modification_type => 'update')
|
15
|
+
scope :destroyed, where(:modification_type => 'destroy')
|
16
|
+
scope :errored, where(:modification_type => 'error')
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,95 @@
|
|
1
|
+
module Harmonize
|
2
|
+
|
3
|
+
module Base
|
4
|
+
|
5
|
+
def self.included(base)
|
6
|
+
base.send :extend, ClassMethods
|
7
|
+
base.send :include, InstanceMethods
|
8
|
+
end
|
9
|
+
|
10
|
+
module ClassMethods
|
11
|
+
|
12
|
+
# Enable harmonization on a set of records
|
13
|
+
def harmonize(options = {})
|
14
|
+
configuration = Harmonize::Configuration.new(options)
|
15
|
+
yield(configuration) if block_given?
|
16
|
+
configure_harmonizer(configuration)
|
17
|
+
end
|
18
|
+
|
19
|
+
def harmonizers
|
20
|
+
@harmonizers ||= {}
|
21
|
+
end
|
22
|
+
|
23
|
+
def harmonize!(harmonizer_name)
|
24
|
+
raise UnknownHarmonizerName.new(harmonizer_name) unless harmonizers.has_key?(harmonizer_name)
|
25
|
+
harmonizer = harmonizers[harmonizer_name]
|
26
|
+
harmonize_log = create_harmonize_log(harmonizer_name)
|
27
|
+
strategy = harmonizer.strategy.new(*harmonizer.strategy_arguments)
|
28
|
+
strategy.harmonize(:harmonizer => harmonizer, :harmonize_log => harmonize_log)
|
29
|
+
close_harmonize_log(harmonize_log)
|
30
|
+
end
|
31
|
+
|
32
|
+
def remove_harmonizer!(harmonizer_name = :default)
|
33
|
+
harmonizers.delete(harmonizer_name)
|
34
|
+
end
|
35
|
+
|
36
|
+
private
|
37
|
+
|
38
|
+
def close_harmonize_log(harmonize_log)
|
39
|
+
harmonize_log.end = DateTime.now
|
40
|
+
harmonize_log.save!
|
41
|
+
harmonize_log
|
42
|
+
end
|
43
|
+
|
44
|
+
def create_harmonize_log(harmonizer_name)
|
45
|
+
harmonizer = harmonizers[harmonizer_name]
|
46
|
+
Harmonize::Log.create!(
|
47
|
+
:start => DateTime.now,
|
48
|
+
:class_name => self.class.name,
|
49
|
+
:harmonizer_name => harmonizer_name,
|
50
|
+
:key => harmonizer.key,
|
51
|
+
:strategy => harmonizer.strategy.inspect,
|
52
|
+
:strategy_arguments => harmonizer.strategy_arguments.inspect,
|
53
|
+
)
|
54
|
+
end
|
55
|
+
|
56
|
+
def default_harmonizer_options(harmonizer_name)
|
57
|
+
Harmonize::Configuration.new({
|
58
|
+
:source => lambda{ harmonizer_source_method(harmonizer_name) },
|
59
|
+
:target => lambda{ scoped }
|
60
|
+
})
|
61
|
+
end
|
62
|
+
|
63
|
+
def harmonizer_source_method(harmonizer_name)
|
64
|
+
method_name = "harmonizer_source_#{harmonizer_name}".to_sym
|
65
|
+
raise HarmonizerSourceUndefined.new(harmonizer_name) unless respond_to?(method_name)
|
66
|
+
send(method_name)
|
67
|
+
end
|
68
|
+
|
69
|
+
def validate_harmonizer_configuration(configuration)
|
70
|
+
configuration.reverse_merge(default_harmonizer_options(configuration.harmonizer_name))
|
71
|
+
end
|
72
|
+
|
73
|
+
def setup_harmonizer_method(harmonizer_name)
|
74
|
+
self.class.instance_eval do
|
75
|
+
define_method "harmonize_#{harmonizer_name}!" do
|
76
|
+
send(:harmonize!, harmonizer_name)
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
def configure_harmonizer(configuration)
|
82
|
+
configuration = validate_harmonizer_configuration(configuration)
|
83
|
+
raise DuplicateHarmonizerName.new(configuration.harmonizer_name.to_s) if harmonizers.has_key?(configuration.harmonizer_name)
|
84
|
+
harmonizers[configuration.harmonizer_name] = configuration
|
85
|
+
setup_harmonizer_method(configuration.harmonizer_name)
|
86
|
+
end
|
87
|
+
|
88
|
+
end
|
89
|
+
|
90
|
+
module InstanceMethods
|
91
|
+
end
|
92
|
+
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
@@ -0,0 +1,10 @@
|
|
1
|
+
module Harmonize
|
2
|
+
class Configuration < Hashie::Dash
|
3
|
+
property :source
|
4
|
+
property :target
|
5
|
+
property :key, :default => :id
|
6
|
+
property :harmonizer_name, :default => :default
|
7
|
+
property :strategy, :default => Harmonize::Strategies::BasicCrudStrategy
|
8
|
+
property :strategy_arguments, :default => Hash.new
|
9
|
+
end
|
10
|
+
end
|
@@ -0,0 +1,7 @@
|
|
1
|
+
module Harmonize
|
2
|
+
class HarmonizeError < StandardError ; end
|
3
|
+
class DuplicateHarmonizerName < HarmonizeError ; end
|
4
|
+
class UnknownHarmonizerName < HarmonizeError ; end
|
5
|
+
class HarmonizerSourceUndefined < HarmonizeError ; end
|
6
|
+
class HarmonizerTargetUndefined < HarmonizeError ; end
|
7
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
module Harmonize
|
2
|
+
module Gemdata
|
3
|
+
Name = 'harmonize'
|
4
|
+
Version = '0.0.1'
|
5
|
+
Authors = [ 'Bram Swenson' ]
|
6
|
+
Email = [ 'bram@craniumisajar.com' ]
|
7
|
+
Summary = <<-END_SUMMARY
|
8
|
+
Bring entire sets of models into harmony with external sources.
|
9
|
+
END_SUMMARY
|
10
|
+
Description = <<-END_DESCRIPTION
|
11
|
+
Bring entire sets of models into harmony with external sources.
|
12
|
+
END_DESCRIPTION
|
13
|
+
Homepage = 'http://github.com/bramswenson/harmonize'
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,69 @@
|
|
1
|
+
module Harmonize
|
2
|
+
module Strategies
|
3
|
+
|
4
|
+
class BasicCrudStrategy < Strategy
|
5
|
+
|
6
|
+
def find_target_instance(key, value)
|
7
|
+
target_relation = targets.where(key => value)
|
8
|
+
instance = target_relation.first rescue nil
|
9
|
+
instance ||= target_relation.build
|
10
|
+
instance
|
11
|
+
end
|
12
|
+
|
13
|
+
def create_modification(instance)
|
14
|
+
unless instance.new_record?
|
15
|
+
harmonize_log.modifications.create!(:modification_type => 'update', :before_time => DateTime.now)
|
16
|
+
else
|
17
|
+
harmonize_log.modifications.create!(:modification_type => 'create', :before_time => DateTime.now)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
def harmonize_target_instance!(target_instance, source, modification)
|
22
|
+
# TODO: this is a mess
|
23
|
+
error_message = nil
|
24
|
+
errored = false
|
25
|
+
begin
|
26
|
+
unless target_instance.update_attributes(source)
|
27
|
+
errored = true
|
28
|
+
error_message = target_instance.errors.full_messages
|
29
|
+
end
|
30
|
+
rescue ActiveRecord::ActiveRecordError, ActiveRecord::UnknownAttributeError => e
|
31
|
+
errored = true
|
32
|
+
error_message = e.message
|
33
|
+
end
|
34
|
+
unless errored
|
35
|
+
modification.update_attributes!(:after_time => DateTime.now, :instance_id => target_instance.id)
|
36
|
+
else
|
37
|
+
modification.update_attributes!(:modification_type => 'error', :instance_errors => error_message)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
def create_and_update_targets
|
42
|
+
sources.each.inject([]) do |keys, source|
|
43
|
+
target_instance = find_target_instance(harmonizer.key, source[harmonizer.key])
|
44
|
+
modification = create_modification(target_instance)
|
45
|
+
harmonize_target_instance!(target_instance, source, modification)
|
46
|
+
keys << source[harmonizer.key]
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
def destroy_targets_not_found_in_source(touched_keys)
|
51
|
+
# if we didn't get any touched_keys, destroy everything in targets scope
|
52
|
+
destroy_scope = touched_keys.empty? ? targets : targets.where("#{harmonizer.key} NOT IN (?)", touched_keys)
|
53
|
+
destroy_scope.find_each do |instance|
|
54
|
+
modification = harmonize_log.modifications.build(
|
55
|
+
:modification_type => 'destroy', :before_time => DateTime.now,
|
56
|
+
:instance_id => instance.id
|
57
|
+
)
|
58
|
+
instance.destroy
|
59
|
+
modification.update_attributes!(:after_time => DateTime.now)
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
def harmonize!
|
64
|
+
touched_keys = create_and_update_targets
|
65
|
+
destroy_targets_not_found_in_source(touched_keys)
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
module Harmonize
|
2
|
+
module Strategies
|
3
|
+
|
4
|
+
class Strategy
|
5
|
+
attr_accessor :harmonizer, :harmonize_log, :sources, :targets
|
6
|
+
|
7
|
+
def initialize(attributes = {})
|
8
|
+
update_attributes(attributes)
|
9
|
+
end
|
10
|
+
|
11
|
+
def harmonize(attributes)
|
12
|
+
update_attributes(attributes)
|
13
|
+
self.sources = harmonizer.source.call
|
14
|
+
self.targets = harmonizer.target.call
|
15
|
+
harmonize!
|
16
|
+
end
|
17
|
+
|
18
|
+
def harmonize!
|
19
|
+
end
|
20
|
+
|
21
|
+
private
|
22
|
+
|
23
|
+
def update_attributes(attributes)
|
24
|
+
attributes.each_pair do |k, v|
|
25
|
+
self.send("#{k}=", v)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
data/lib/harmonize.rb
ADDED
@@ -0,0 +1,9 @@
|
|
1
|
+
require 'hashie/dash'
|
2
|
+
require 'harmonize/errors'
|
3
|
+
module Harmonize
|
4
|
+
require 'harmonize/engine' if defined?(Rails) && Rails::VERSION::MAJOR == 3
|
5
|
+
autoload :Base, 'harmonize/base'
|
6
|
+
autoload :Strategies, 'harmonize/strategies'
|
7
|
+
autoload :Configuration, 'harmonize/configuration'
|
8
|
+
end
|
9
|
+
ActiveRecord::Base.send :include, Harmonize::Base if defined?(ActiveRecord::Base)
|
@@ -0,0 +1,29 @@
|
|
1
|
+
require 'rails/generators'
|
2
|
+
require 'rails/generators/migration'
|
3
|
+
|
4
|
+
module Harmonize
|
5
|
+
|
6
|
+
class MigrationGenerator < Rails::Generators::Base
|
7
|
+
include Rails::Generators::Migration
|
8
|
+
|
9
|
+
def self.source_root
|
10
|
+
File.join(File.dirname(__FILE__), 'templates')
|
11
|
+
end
|
12
|
+
|
13
|
+
def self.next_migration_number(dirname) #:nodoc:
|
14
|
+
if ActiveRecord::Base.timestamped_migrations
|
15
|
+
Time.now.utc.strftime("%Y%m%d%H%M%S")
|
16
|
+
else
|
17
|
+
"%.3d" % (current_migration_number(dirname) + 1)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
def create_migration_file
|
22
|
+
f = File.join(File.dirname(__FILE__), 'templates', 'migration.rb')
|
23
|
+
migration_template f, 'db/migrate/create_harmonize_tables.rb'
|
24
|
+
end
|
25
|
+
|
26
|
+
end
|
27
|
+
|
28
|
+
end
|
29
|
+
|
@@ -0,0 +1,34 @@
|
|
1
|
+
class CreateHarmonizeTables < ActiveRecord::Migration
|
2
|
+
def self.up
|
3
|
+
create_table :harmonize_logs do |t|
|
4
|
+
t.string :key
|
5
|
+
t.string :class_name
|
6
|
+
t.string :harmonizer_name
|
7
|
+
t.string :strategy
|
8
|
+
t.string :strategy_arguments
|
9
|
+
t.datetime :start
|
10
|
+
t.datetime :end
|
11
|
+
t.timestamps
|
12
|
+
end
|
13
|
+
add_index :harmonize_logs, [ :class_name, :harmonizer_name ]
|
14
|
+
|
15
|
+
create_table :harmonize_modifications do |t|
|
16
|
+
t.integer :harmonize_log_id
|
17
|
+
t.integer :instance_id
|
18
|
+
t.string :modification_type
|
19
|
+
t.datetime :before_time
|
20
|
+
t.datetime :after_time
|
21
|
+
t.text :instance_errors
|
22
|
+
t.timestamps
|
23
|
+
end
|
24
|
+
add_index :harmonize_modifications, :harmonize_log_id
|
25
|
+
add_index :harmonize_modifications, [ :harmonize_log_id, :instance_id ], :name => 'index_harmonize_modification_with_id'
|
26
|
+
add_index :harmonize_modifications, [ :harmonize_log_id, :instance_id, :modification_type ], :name => 'index_harmonize_modification_with_id_and_type'
|
27
|
+
end
|
28
|
+
|
29
|
+
def self.down
|
30
|
+
drop_table :harmonize_logs
|
31
|
+
drop_table :harmonize_modifications
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
metadata
ADDED
@@ -0,0 +1,88 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: harmonize
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
prerelease:
|
5
|
+
version: 0.0.1
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Bram Swenson
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
|
13
|
+
date: 2011-06-26 00:00:00 -07:00
|
14
|
+
default_executable:
|
15
|
+
dependencies:
|
16
|
+
- !ruby/object:Gem::Dependency
|
17
|
+
name: hashie
|
18
|
+
requirement: &id001 !ruby/object:Gem::Requirement
|
19
|
+
none: false
|
20
|
+
requirements:
|
21
|
+
- - ">="
|
22
|
+
- !ruby/object:Gem::Version
|
23
|
+
version: "0"
|
24
|
+
type: :runtime
|
25
|
+
prerelease: false
|
26
|
+
version_requirements: *id001
|
27
|
+
description: |
|
28
|
+
Bring entire sets of models into harmony with external sources.
|
29
|
+
|
30
|
+
email:
|
31
|
+
- bram@craniumisajar.com
|
32
|
+
executables: []
|
33
|
+
|
34
|
+
extensions: []
|
35
|
+
|
36
|
+
extra_rdoc_files: []
|
37
|
+
|
38
|
+
files:
|
39
|
+
- app/models/harmonize/log.rb
|
40
|
+
- app/models/harmonize/modification.rb
|
41
|
+
- lib/harmonize/base.rb
|
42
|
+
- lib/harmonize/configuration.rb
|
43
|
+
- lib/harmonize/engine.rb
|
44
|
+
- lib/harmonize/errors.rb
|
45
|
+
- lib/harmonize/gemdata.rb
|
46
|
+
- lib/harmonize/strategies/basic_crud_strategy.rb
|
47
|
+
- lib/harmonize/strategies/strategy.rb
|
48
|
+
- lib/harmonize/strategies.rb
|
49
|
+
- lib/harmonize.rb
|
50
|
+
- lib/rails/generators/harmonize/migration_generator.rb
|
51
|
+
- lib/rails/generators/harmonize/templates/migration.rb
|
52
|
+
- MIT-LICENSE
|
53
|
+
- Rakefile
|
54
|
+
- Gemfile
|
55
|
+
- README.md
|
56
|
+
has_rdoc: true
|
57
|
+
homepage: http://github.com/bramswenson/harmonize
|
58
|
+
licenses: []
|
59
|
+
|
60
|
+
post_install_message:
|
61
|
+
rdoc_options: []
|
62
|
+
|
63
|
+
require_paths:
|
64
|
+
- lib
|
65
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
66
|
+
none: false
|
67
|
+
requirements:
|
68
|
+
- - ">="
|
69
|
+
- !ruby/object:Gem::Version
|
70
|
+
hash: 2398958541788060618
|
71
|
+
segments:
|
72
|
+
- 0
|
73
|
+
version: "0"
|
74
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
75
|
+
none: false
|
76
|
+
requirements:
|
77
|
+
- - ">="
|
78
|
+
- !ruby/object:Gem::Version
|
79
|
+
version: "0"
|
80
|
+
requirements: []
|
81
|
+
|
82
|
+
rubyforge_project:
|
83
|
+
rubygems_version: 1.6.1
|
84
|
+
signing_key:
|
85
|
+
specification_version: 3
|
86
|
+
summary: Bring entire sets of models into harmony with external sources.
|
87
|
+
test_files: []
|
88
|
+
|