harmonize 0.0.1 → 0.0.2
Sign up to get free protection for your applications and to get access to all the features.
- data/README.md +51 -26
- data/lib/harmonize/base.rb +37 -11
- data/lib/harmonize/errors.rb +1 -0
- data/lib/harmonize/gemdata.rb +1 -1
- data/lib/harmonize/strategies/basic_crud_strategy.rb +67 -49
- metadata +3 -3
data/README.md
CHANGED
@@ -9,17 +9,18 @@ hahr-muh-nahyz -
|
|
9
9
|
|
10
10
|
harmonize is a rails 3 engine that allows one to harmonize entire scopes of an ActiveRecord model with arbitrary external data sources.
|
11
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
|
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
13
|
|
14
14
|
## Simple Example
|
15
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
|
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 attribute :name as the primary\_key.
|
17
17
|
|
18
18
|
class Store < ActiveRecord::Base
|
19
19
|
validates :name, :address, :presence => true
|
20
20
|
|
21
21
|
# Setup a harmonizer
|
22
22
|
harmonize do |config|
|
23
|
+
# use :name from the source data as the lookup key for target records
|
23
24
|
config.key = :name
|
24
25
|
end
|
25
26
|
|
@@ -31,7 +32,7 @@ Lets pretend we work for a company that has a large list of stores. This list of
|
|
31
32
|
end
|
32
33
|
end
|
33
34
|
|
34
|
-
With our Store model wired up as above, we will get a new class method on our model called "
|
35
|
+
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
|
|
36
37
|
## Harmonize::Configuration
|
37
38
|
|
@@ -41,54 +42,72 @@ Each call to harmonize creates a Harmonize::Configuration instance that defines
|
|
41
42
|
harmonize do |config|
|
42
43
|
config.harmonizer_name = :custom_name
|
43
44
|
config.key = :the_key
|
44
|
-
config.source =
|
45
|
-
|
46
|
-
end
|
47
|
-
config.target = lambda do
|
48
|
-
where(:active => true)
|
49
|
-
end
|
45
|
+
config.source = :get_latest_source_data_feed!
|
46
|
+
config.target = :some_scope_name
|
50
47
|
config.strategy = YourCustomStrategy
|
51
48
|
config.strategy_arguments = { :options => 'needed', :to => 'initialize', :your => 'custom strategy' }
|
52
49
|
end
|
53
50
|
end
|
54
51
|
|
55
|
-
### Harmonizer::Configuration.
|
52
|
+
### Harmonizer::Configuration.harmonizer\_name
|
56
53
|
|
57
|
-
harmonize uses the configured
|
54
|
+
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. This option is used to name special methods used by harmonize. The harmonization method is named with the following convention: "harmonize\_#{harmonizer\_name}!".
|
58
55
|
|
59
|
-
The default
|
60
|
-
|
56
|
+
The default setting is:
|
57
|
+
|
58
|
+
:default
|
59
|
+
|
60
|
+
This setting can be any symbol.
|
61
61
|
|
62
62
|
### Harmonizer::Configuration.key
|
63
63
|
|
64
|
-
harmonize uses the configured key to determine what attribute to use to find existing target records.
|
64
|
+
harmonize uses the configured key to determine what attribute in the source data feed to use to find existing target records.
|
65
|
+
|
66
|
+
The default setting is:
|
67
|
+
|
68
|
+
:id
|
65
69
|
|
66
|
-
|
70
|
+
This setting can be any attribute that will be found in source records.
|
67
71
|
|
68
72
|
### Harmonizer::Configuration.source
|
69
73
|
|
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.
|
74
|
+
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. By default this setting calls a method name with the following convention: "harmonize\_source\_#{harmonizer\_name}".
|
71
75
|
|
72
|
-
The default
|
73
|
-
|
76
|
+
The default setting is:
|
77
|
+
|
78
|
+
harmonize_source_default
|
79
|
+
|
80
|
+
This setting can be any class method defined in the model that returns properly structured data.
|
74
81
|
|
75
82
|
### Harmonizer::Configuration.target
|
76
83
|
|
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
|
84
|
+
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. Hint, all (named) scopes return ActiveRecord::Relation instances.
|
85
|
+
|
86
|
+
The default setting is:
|
78
87
|
|
79
|
-
|
88
|
+
scoped
|
89
|
+
|
90
|
+
This setting can be any class method defined in the model that returns an ActiveRecord::Relation.
|
80
91
|
|
81
92
|
### Harmonizer::Configuration.strategy
|
82
93
|
|
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
|
94
|
+
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.
|
95
|
+
|
96
|
+
The default setting is:
|
84
97
|
|
85
|
-
|
98
|
+
Harmonize::Strategies::BasicCrudStrategy
|
99
|
+
|
100
|
+
This setting can be any class that returns complies with the Strategy api.
|
86
101
|
|
87
102
|
### Harmonizer::Configuration.target
|
88
103
|
|
89
|
-
harmonize uses the configured
|
104
|
+
harmonize uses the configured strategy\_arguments to determine which arguments to use when initializing the set Harmonize::Strategies::Strategy subclass.
|
105
|
+
|
106
|
+
The default settings is:
|
90
107
|
|
91
|
-
|
108
|
+
{}
|
109
|
+
|
110
|
+
This setting can be a hash of initialize options for the defined Strategy.
|
92
111
|
|
93
112
|
## Harmonize::Strategies
|
94
113
|
|
@@ -107,7 +126,7 @@ The default harmonize strategy is Harmonize::Strategies::BasicCrudStrategy. This
|
|
107
126
|
|
108
127
|
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
128
|
|
110
|
-
## Installation
|
129
|
+
## Installation and Usage
|
111
130
|
|
112
131
|
Add harmonize to the gem file for your rails application:
|
113
132
|
|
@@ -127,13 +146,19 @@ Configure your model to use harmonize and implement your source:
|
|
127
146
|
end
|
128
147
|
end
|
129
148
|
|
149
|
+
Bring your data into harmony
|
150
|
+
|
151
|
+
MyModel.harmonize!(:default)
|
152
|
+
# or the custom method named after your harmonizer
|
153
|
+
MyModel.harmonize_default!
|
154
|
+
|
130
155
|
Use, report bugs, fix them, and send pull requests!
|
131
156
|
|
132
157
|
## Plans
|
133
158
|
|
134
159
|
## TODO
|
135
160
|
|
136
|
-
* Maybe move key from Configuration to a
|
161
|
+
* 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
162
|
|
138
163
|
## Contributors
|
139
164
|
|
data/lib/harmonize/base.rb
CHANGED
@@ -21,7 +21,7 @@ module Harmonize
|
|
21
21
|
end
|
22
22
|
|
23
23
|
def harmonize!(harmonizer_name)
|
24
|
-
raise UnknownHarmonizerName.new(harmonizer_name) unless harmonizers.has_key?(harmonizer_name)
|
24
|
+
raise UnknownHarmonizerName.new(harmonizer_name.to_s) unless harmonizers.has_key?(harmonizer_name)
|
25
25
|
harmonizer = harmonizers[harmonizer_name]
|
26
26
|
harmonize_log = create_harmonize_log(harmonizer_name)
|
27
27
|
strategy = harmonizer.strategy.new(*harmonizer.strategy_arguments)
|
@@ -53,21 +53,47 @@ module Harmonize
|
|
53
53
|
)
|
54
54
|
end
|
55
55
|
|
56
|
-
def
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
})
|
56
|
+
def harmonize_source_method(harmonizer_name, method_name = nil)
|
57
|
+
method_name ||= "harmonize_source_#{harmonizer_name}".to_sym
|
58
|
+
raise HarmonizerSourceUndefined.new(harmonizer_name.to_s) unless respond_to?(method_name)
|
59
|
+
send(method_name)
|
61
60
|
end
|
62
61
|
|
63
|
-
def
|
64
|
-
method_name
|
65
|
-
raise
|
66
|
-
send(method_name)
|
62
|
+
def harmonize_target_method(harmonizer_name, method_name = nil)
|
63
|
+
method_name ||= :scoped
|
64
|
+
raise HarmonizerTargetUndefined.new(harmonizer_name.to_s) unless respond_to?(method_name)
|
65
|
+
target_scope = send(method_name)
|
66
|
+
raise HarmonizerTargetInvalid.new(harmonizer_name.to_s) unless target_scope.is_a?(ActiveRecord::Relation)
|
67
|
+
target_scope
|
68
|
+
end
|
69
|
+
|
70
|
+
def validate_harmonize_source(configuration)
|
71
|
+
case configuration.source.class.name
|
72
|
+
when "Proc"
|
73
|
+
configuration
|
74
|
+
when "Symbol"
|
75
|
+
configuration.source = lambda { harmonize_source_method(configuration.harmonizer_name, configuration.source) }
|
76
|
+
else
|
77
|
+
configuration.source = lambda { harmonize_source_method(configuration.harmonizer_name) }
|
78
|
+
end
|
79
|
+
configuration
|
80
|
+
end
|
81
|
+
|
82
|
+
def validate_harmonize_target(configuration)
|
83
|
+
case configuration.target.class.name
|
84
|
+
when "Proc"
|
85
|
+
configuration
|
86
|
+
when "Symbol"
|
87
|
+
configuration.target = lambda { harmonize_target_method(configuration.harmonizer_name, configuration.target) }
|
88
|
+
else
|
89
|
+
configuration.target = lambda { harmonize_target_method(configuration.harmonizer_name) }
|
90
|
+
end
|
91
|
+
configuration
|
67
92
|
end
|
68
93
|
|
69
94
|
def validate_harmonizer_configuration(configuration)
|
70
|
-
configuration
|
95
|
+
configuration = validate_harmonize_source(configuration)
|
96
|
+
configuration = validate_harmonize_target(configuration)
|
71
97
|
end
|
72
98
|
|
73
99
|
def setup_harmonizer_method(harmonizer_name)
|
data/lib/harmonize/errors.rb
CHANGED
data/lib/harmonize/gemdata.rb
CHANGED
@@ -3,67 +3,85 @@ module Harmonize
|
|
3
3
|
|
4
4
|
class BasicCrudStrategy < Strategy
|
5
5
|
|
6
|
-
def
|
7
|
-
|
8
|
-
|
9
|
-
instance ||= target_relation.build
|
10
|
-
instance
|
6
|
+
def harmonize!
|
7
|
+
touched_keys = create_and_update_targets
|
8
|
+
destroy_targets_not_found_in_source(touched_keys)
|
11
9
|
end
|
12
10
|
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
11
|
+
private
|
12
|
+
|
13
|
+
def create_and_update_targets
|
14
|
+
sources.each.inject([]) do |keys, source|
|
15
|
+
target_instance = find_target_instance(harmonizer.key, source[harmonizer.key])
|
16
|
+
modification = initialize_modification(target_instance)
|
17
|
+
modification = harmonize_target_instance!(target_instance, source, modification)
|
18
|
+
modification.save! if modification
|
19
|
+
keys << source[harmonizer.key]
|
20
|
+
end
|
18
21
|
end
|
19
|
-
end
|
20
22
|
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
23
|
+
def destroy_targets_not_found_in_source(touched_keys)
|
24
|
+
# if we didn't get any touched_keys, destroy everything in targets scope
|
25
|
+
destroy_scope = touched_keys.empty? ? targets : targets.where("#{harmonizer.key} NOT IN (?)", touched_keys)
|
26
|
+
destroy_scope.find_each do |instance|
|
27
|
+
modification = harmonize_log.modifications.build(
|
28
|
+
:modification_type => 'destroy', :before_time => DateTime.now,
|
29
|
+
:instance_id => instance.id
|
30
|
+
)
|
31
|
+
instance.destroy
|
32
|
+
modification.update_attributes!(:after_time => DateTime.now)
|
29
33
|
end
|
30
|
-
rescue ActiveRecord::ActiveRecordError, ActiveRecord::UnknownAttributeError => e
|
31
|
-
errored = true
|
32
|
-
error_message = e.message
|
33
34
|
end
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
35
|
+
|
36
|
+
def find_target_instance(key, value)
|
37
|
+
target_relation = targets.where(key => value)
|
38
|
+
instance = target_relation.first rescue nil
|
39
|
+
instance ||= target_relation.build
|
40
|
+
instance
|
38
41
|
end
|
39
|
-
end
|
40
42
|
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
43
|
+
def initialize_modification(instance)
|
44
|
+
unless instance.new_record?
|
45
|
+
harmonize_log.modifications.new(:modification_type => 'update', :before_time => DateTime.now)
|
46
|
+
else
|
47
|
+
harmonize_log.modifications.new(:modification_type => 'create', :before_time => DateTime.now)
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
def harmonize_target_instance!(target_instance, source, modification)
|
52
|
+
target_instance, error_message = update_target_attributes(target_instance, source)
|
53
|
+
return false unless (target_instance.changed? or error_message)
|
54
|
+
target_instance, error_message = save_target(target_instance) unless error_message
|
55
|
+
if error_message
|
56
|
+
modification.attributes = { :modification_type => 'error', :instance_errors => error_message }
|
57
|
+
else
|
58
|
+
modification.attributes = { :after_time => DateTime.now, :instance_id => target_instance.id }
|
59
|
+
end
|
60
|
+
modification
|
47
61
|
end
|
48
|
-
end
|
49
62
|
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
63
|
+
def update_target_attributes(target_instance, source)
|
64
|
+
error_message = false
|
65
|
+
begin
|
66
|
+
target_instance.attributes = source
|
67
|
+
rescue ActiveRecord::UnknownAttributeError => e
|
68
|
+
error_message = e.message
|
69
|
+
end
|
70
|
+
return target_instance, error_message
|
71
|
+
end
|
72
|
+
|
73
|
+
def save_target(target_instance)
|
74
|
+
error_message = false
|
75
|
+
begin
|
76
|
+
unless target_instance.save
|
77
|
+
error_message = target_instance.errors.full_messages
|
78
|
+
end
|
79
|
+
rescue ActiveRecord::ActiveRecordError => e
|
80
|
+
error_message = e.message
|
81
|
+
end
|
82
|
+
return target_instance, error_message
|
60
83
|
end
|
61
|
-
end
|
62
84
|
|
63
|
-
def harmonize!
|
64
|
-
touched_keys = create_and_update_targets
|
65
|
-
destroy_targets_not_found_in_source(touched_keys)
|
66
|
-
end
|
67
85
|
end
|
68
86
|
end
|
69
87
|
end
|
metadata
CHANGED
@@ -2,7 +2,7 @@
|
|
2
2
|
name: harmonize
|
3
3
|
version: !ruby/object:Gem::Version
|
4
4
|
prerelease:
|
5
|
-
version: 0.0.
|
5
|
+
version: 0.0.2
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
8
8
|
- Bram Swenson
|
@@ -10,7 +10,7 @@ autorequire:
|
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
12
|
|
13
|
-
date: 2011-06-
|
13
|
+
date: 2011-06-27 00:00:00 -07:00
|
14
14
|
default_executable:
|
15
15
|
dependencies:
|
16
16
|
- !ruby/object:Gem::Dependency
|
@@ -67,7 +67,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
67
67
|
requirements:
|
68
68
|
- - ">="
|
69
69
|
- !ruby/object:Gem::Version
|
70
|
-
hash:
|
70
|
+
hash: 3057359512407760229
|
71
71
|
segments:
|
72
72
|
- 0
|
73
73
|
version: "0"
|