nsync 0.0.2
Sign up to get free protection for your applications and to get access to all the features.
- data/LICENSE +21 -0
- data/README.md +204 -0
- data/Rakefile +47 -0
- data/VERSION +1 -0
- data/jeweler_monkey_patch.rb +59 -0
- data/lib/nsync/active_record/consumer/methods.rb +47 -0
- data/lib/nsync/active_record/methods.rb +23 -0
- data/lib/nsync/active_record/producer/methods.rb +21 -0
- data/lib/nsync/class_methods.rb +17 -0
- data/lib/nsync/config.rb +96 -0
- data/lib/nsync/consumer.rb +293 -0
- data/lib/nsync/git_version_manager.rb +29 -0
- data/lib/nsync/producer/methods.rb +23 -0
- data/lib/nsync/producer.rb +129 -0
- data/lib/nsync.rb +30 -0
- data/nsync.gemspec +80 -0
- data/test/active_record_test.rb +153 -0
- data/test/classes.rb +22 -0
- data/test/helper.rb +12 -0
- data/test/nsync_config_test.rb +142 -0
- data/test/nsync_consumer_test.rb +260 -0
- data/test/nsync_producer_test.rb +261 -0
- data/test/repo.rb +64 -0
- metadata +165 -0
@@ -0,0 +1,293 @@
|
|
1
|
+
module Nsync
|
2
|
+
# The Nsync::Consumer is used to handle the consumption of data from an Nsync
|
3
|
+
# repo for the entire app. It reads in the differences between the current
|
4
|
+
# version of data in the database and the new data from the producer, finding
|
5
|
+
# and notifying all affected classes and objects.
|
6
|
+
#
|
7
|
+
# Basic Usage:
|
8
|
+
#
|
9
|
+
# Nsync::Config.run do |c|
|
10
|
+
# # The consumer uses a read-only, bare repository (one ending in .git)
|
11
|
+
# # This will automatically be created if it does not exist
|
12
|
+
# c.repo_path = "/local/path/to/hold/data.git"
|
13
|
+
# # The remote repository url from which to pull data
|
14
|
+
# c.repo_url = "git@examplegithost:username/data.git"
|
15
|
+
#
|
16
|
+
# # An object that implements the VersionManager interface
|
17
|
+
# # (see Nsync::GitVersionManager) for an example
|
18
|
+
# c.version_manager = MyCustomVersionManager.new
|
19
|
+
#
|
20
|
+
# # A lock file path to use for this app
|
21
|
+
# c.lock_file = "/tmp/app_name_nsync.lock"
|
22
|
+
#
|
23
|
+
# # The class mapping maps from the class names of the producer classes to
|
24
|
+
# # the class names of their associated consuming classes. A producer can
|
25
|
+
# # map to one or many consumers, and a consumer can be mapped to one or many
|
26
|
+
# # producers. Consumer classes should implement the Consumer interface.
|
27
|
+
# c.map_class "RawDataPostClass", "Post"
|
28
|
+
# c.map_class "RawDataInfo", "Info"
|
29
|
+
# end
|
30
|
+
#
|
31
|
+
# # Create a new consumer object, this will clone the repo if needed
|
32
|
+
# consumer = Nsync::Consumer.new
|
33
|
+
#
|
34
|
+
# # update this app to the latest data, pulling if necessary
|
35
|
+
# consumer.update
|
36
|
+
#
|
37
|
+
# # rollback the last change
|
38
|
+
# consumer.rollback
|
39
|
+
class Consumer
|
40
|
+
attr_accessor :repo
|
41
|
+
|
42
|
+
# There was an issue creating or accessing the repository
|
43
|
+
class CouldNotInitializeRepoError < RuntimeError; end
|
44
|
+
|
45
|
+
# Sets the repository to the repo at config.repo_path
|
46
|
+
#
|
47
|
+
# If config.repo_url is set and the directory at config.repo_path does not
|
48
|
+
# exist yet, a new bare repository will be cloned from config.repo_url
|
49
|
+
def initialize
|
50
|
+
unless get_or_create_repo
|
51
|
+
raise CouldNotInitializeRepoError
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
# Updates the data to the latest version
|
56
|
+
#
|
57
|
+
# If the repo has a remote origin, the latest changes will be fetched.
|
58
|
+
#
|
59
|
+
# NOTE: It is critical that the version_manager returns correct results
|
60
|
+
# as this method goes from what it says is the latest commit that was loaded in
|
61
|
+
# to HEAD.
|
62
|
+
def update
|
63
|
+
update_repo &&
|
64
|
+
apply_changes(config.version_manager.version,
|
65
|
+
repo.head.commit.id)
|
66
|
+
end
|
67
|
+
|
68
|
+
# Rolls back data to the previous loaded version
|
69
|
+
#
|
70
|
+
# NOTE: If you rollback and then update, the 'bad' commit will then be reloaded.
|
71
|
+
# This is primarily meant as a way to get back to a known good state quickly, while
|
72
|
+
# the issues are fixed in the producer.
|
73
|
+
def rollback
|
74
|
+
apply_changes(config.version_manager.version,
|
75
|
+
config.version_manager.previous_version)
|
76
|
+
end
|
77
|
+
|
78
|
+
# @return [Nsync::Config]
|
79
|
+
def config
|
80
|
+
Nsync.config
|
81
|
+
end
|
82
|
+
|
83
|
+
# Translates and applies the changes between commit id 'a' and commit id 'b' to
|
84
|
+
# the datastore. This is used internally by rollback and update. Don't use this
|
85
|
+
# unless you absolutely know what you are doing.
|
86
|
+
#
|
87
|
+
# If you must call this directly, understand that 'a' should almost always be the
|
88
|
+
# commit id of the current data that is loaded into the database. 'b' can be any
|
89
|
+
# commit in the graph, forward or backwards.
|
90
|
+
#
|
91
|
+
# @param [String] a current data version commit id
|
92
|
+
# @param [String] b new data version commit id
|
93
|
+
def apply_changes(a, b)
|
94
|
+
config.lock do
|
95
|
+
config.log.info("[NSYNC] Moving Nsync::Consumer from '#{a}' to '#{b}'")
|
96
|
+
clear_queues
|
97
|
+
diffs = nil
|
98
|
+
diffs = repo.diff(a, b)
|
99
|
+
|
100
|
+
changeset = changeset_from_diffs(diffs)
|
101
|
+
|
102
|
+
if config.ordering
|
103
|
+
config.ordering.each do |klass|
|
104
|
+
begin
|
105
|
+
klass = klass.constantize
|
106
|
+
changes = changeset[klass]
|
107
|
+
if changes
|
108
|
+
apply_changes_for_class(klass, changes)
|
109
|
+
end
|
110
|
+
rescue NameError
|
111
|
+
config.log.warn("[NSYNC] Could not find class '#{klass}' from ordering; skipping")
|
112
|
+
end
|
113
|
+
end
|
114
|
+
else
|
115
|
+
changeset.each do |klass, changes|
|
116
|
+
apply_changes_for_class(klass, changes)
|
117
|
+
end
|
118
|
+
end
|
119
|
+
run_after_finished
|
120
|
+
clear_queues
|
121
|
+
config.version_manager.version = b
|
122
|
+
end
|
123
|
+
end
|
124
|
+
|
125
|
+
|
126
|
+
# @private
|
127
|
+
class Change < Struct.new(:id, :diff)
|
128
|
+
def type
|
129
|
+
if diff.deleted_file
|
130
|
+
:deleted
|
131
|
+
elsif diff.new_file
|
132
|
+
:added
|
133
|
+
else
|
134
|
+
:modified
|
135
|
+
end
|
136
|
+
end
|
137
|
+
|
138
|
+
def data
|
139
|
+
@data ||= JSON.load(diff.b_blob.data)
|
140
|
+
rescue
|
141
|
+
{}
|
142
|
+
end
|
143
|
+
end
|
144
|
+
|
145
|
+
# Adds a callback to the list of callbacks to occur after main processing
|
146
|
+
# of the class specified by 'klass'. Can be used to handle data relations
|
147
|
+
# between objects of the same class.
|
148
|
+
#
|
149
|
+
# Example:
|
150
|
+
#
|
151
|
+
# class Post
|
152
|
+
# def nsync_update(consumer, event_type, filename, data)
|
153
|
+
# #... normal data update stuff ...
|
154
|
+
# post = self
|
155
|
+
# related_post_source_ids = data['related_post_ids']
|
156
|
+
# consumer.after_class_finished(Post, lambda {
|
157
|
+
# posts = Post.all(:conditions =>
|
158
|
+
# {:source_id => related_post_source_ids })
|
159
|
+
# post.related_posts = posts
|
160
|
+
# })
|
161
|
+
# end
|
162
|
+
# end
|
163
|
+
#
|
164
|
+
# @param [Class] klass
|
165
|
+
# @param [Proc] l
|
166
|
+
def after_class_finished(klass, l)
|
167
|
+
@after_class_finished_queues[klass] ||= []
|
168
|
+
@after_class_finished_queues[klass] << l
|
169
|
+
end
|
170
|
+
|
171
|
+
# Adds a callback to the list of callbacks to occur after main processing
|
172
|
+
# of the class that is currently being processed. This is essentially an
|
173
|
+
# alias for after_class_finished for the current class
|
174
|
+
#
|
175
|
+
# @param [Proc] l
|
176
|
+
def after_current_class_finished(l)
|
177
|
+
after_class_finished(@current_class_for_queue, l)
|
178
|
+
end
|
179
|
+
|
180
|
+
|
181
|
+
# Adds a callback to the list of callbacks to occur after all changes have
|
182
|
+
# been applied. This queue executes immediately prior to the current
|
183
|
+
# version being updated
|
184
|
+
#
|
185
|
+
# @param [Proc] l
|
186
|
+
def after_finished(l)
|
187
|
+
@after_finished_queue ||= []
|
188
|
+
@after_finished_queue << l
|
189
|
+
end
|
190
|
+
|
191
|
+
protected
|
192
|
+
def get_or_create_repo
|
193
|
+
if config.local? || File.exists?(config.repo_path)
|
194
|
+
return self.repo = Grit::Repo.new(config.repo_path)
|
195
|
+
end
|
196
|
+
|
197
|
+
config.lock do
|
198
|
+
git = Grit::Git.new(config.repo_path)
|
199
|
+
git.clone({:bare => true}, config.repo_url, config.repo_path)
|
200
|
+
self.repo = Grit::Repo.new(config.repo_path)
|
201
|
+
config.version_manager.version = git.rev_list({:reverse => true}, "master").split("\n").first
|
202
|
+
return self.repo
|
203
|
+
end
|
204
|
+
end
|
205
|
+
|
206
|
+
def update_repo
|
207
|
+
return true if config.local?
|
208
|
+
config.lock do
|
209
|
+
repo.remote_fetch('origin')
|
210
|
+
# from http://www.pragmatic-source.com/en/opensource/tips/automatic-synchronization-2-git-repositories
|
211
|
+
repo.git.reset({:soft => true}, 'FETCH_HEAD')
|
212
|
+
true
|
213
|
+
end
|
214
|
+
end
|
215
|
+
|
216
|
+
def apply_changes_for_class(klass, changes)
|
217
|
+
@current_class_for_queue = klass
|
218
|
+
if klass.respond_to?(:nsync_find)
|
219
|
+
changes.each do |change|
|
220
|
+
objects = klass.nsync_find(change.id)
|
221
|
+
if objects.empty? && change.type != :deleted
|
222
|
+
if klass.respond_to?(:nsync_add_data)
|
223
|
+
config.log.info("[NSYNC] Adding data #{diff_path(change.diff)} to #{klass}")
|
224
|
+
klass.nsync_add_data(self, change.type, diff_path(change.diff), change.data)
|
225
|
+
else
|
226
|
+
config.log.warn("[NSYNC] Class '#{klass}' has no method nsync_add_data; skipping")
|
227
|
+
end
|
228
|
+
else
|
229
|
+
objects.each do |obj|
|
230
|
+
if obj.respond_to?(:nsync_update)
|
231
|
+
obj.nsync_update(self, change.type, diff_path(change.diff),
|
232
|
+
change.data)
|
233
|
+
config.log.info("[NSYNC] Updating from #{diff_path(change.diff)} to #{obj.inspect}")
|
234
|
+
else
|
235
|
+
config.log.info("[NSYNC] Object #{obj.inspect} has no method nsync_update; skipping")
|
236
|
+
end
|
237
|
+
end
|
238
|
+
end
|
239
|
+
end
|
240
|
+
else
|
241
|
+
config.log.warn("[NSYNC] Consumer class '#{klass}' has no method nsync_find; skipping")
|
242
|
+
end
|
243
|
+
@current_class_for_queue = nil
|
244
|
+
run_after_class_finished(klass)
|
245
|
+
end
|
246
|
+
|
247
|
+
def clear_queues
|
248
|
+
@after_class_finished_queues = {}
|
249
|
+
@after_finished_queue = []
|
250
|
+
end
|
251
|
+
|
252
|
+
def run_after_class_finished(klass)
|
253
|
+
queue = @after_class_finished_queues[klass]
|
254
|
+
if queue
|
255
|
+
queue.each do |l|
|
256
|
+
l.call
|
257
|
+
end
|
258
|
+
end
|
259
|
+
end
|
260
|
+
|
261
|
+
def run_after_finished
|
262
|
+
if @after_finished_queue
|
263
|
+
@after_finished_queue.each do |l|
|
264
|
+
l.call
|
265
|
+
end
|
266
|
+
end
|
267
|
+
end
|
268
|
+
|
269
|
+
def changeset_from_diffs(diffs)
|
270
|
+
diffs.inject({}) do |h, diff|
|
271
|
+
next h if diff_path(diff) =~ /\.gitignore$/
|
272
|
+
|
273
|
+
classes, id = consumer_classes_and_id_from_path(diff_path(diff))
|
274
|
+
classes.each do |klass|
|
275
|
+
h[klass] ||= []
|
276
|
+
h[klass] << Change.new(id, diff)
|
277
|
+
end
|
278
|
+
h
|
279
|
+
end
|
280
|
+
end
|
281
|
+
|
282
|
+
def consumer_classes_and_id_from_path(path)
|
283
|
+
producer_class_name = File.dirname(path).camelize
|
284
|
+
id = File.basename(path, ".json")
|
285
|
+
[config.consumer_classes_for(producer_class_name), id]
|
286
|
+
end
|
287
|
+
|
288
|
+
def diff_path(diff)
|
289
|
+
diff.b_path || diff.a_path
|
290
|
+
end
|
291
|
+
end
|
292
|
+
end
|
293
|
+
|
@@ -0,0 +1,29 @@
|
|
1
|
+
module Nsync
|
2
|
+
class GitVersionManager
|
3
|
+
def initialize(repo_path = nil)
|
4
|
+
@repo_path = repo_path
|
5
|
+
end
|
6
|
+
|
7
|
+
def repo
|
8
|
+
# if the repo is not ready, lets just hope it works
|
9
|
+
@repo ||= Grit::Repo.new(@repo_path || Nsync.config.repo_path) rescue nil
|
10
|
+
end
|
11
|
+
|
12
|
+
def version
|
13
|
+
repo.head.commit.id if repo
|
14
|
+
end
|
15
|
+
|
16
|
+
def version=(val)
|
17
|
+
val
|
18
|
+
end
|
19
|
+
|
20
|
+
def previous_version
|
21
|
+
previous_versions[0]
|
22
|
+
end
|
23
|
+
|
24
|
+
def previous_versions
|
25
|
+
repo.commits("master", 10, 1).map(&:id) if repo
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
@@ -0,0 +1,23 @@
|
|
1
|
+
module Nsync
|
2
|
+
class Producer
|
3
|
+
module InstanceMethods
|
4
|
+
def nsync_write
|
5
|
+
nsync_opts = self.class.read_inheritable_attribute(:nsync_opts)
|
6
|
+
if !nsync_opts[:if] || nsync_opts[:if].call(self)
|
7
|
+
Nsync.config.producer_instance.write_file(nsync_filename, to_nsync_hash)
|
8
|
+
elsif Nsync.config.producer_instance.file_exists?(nsync_filename)
|
9
|
+
nsync_destroy
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
def nsync_destroy
|
14
|
+
Nsync.config.producer_instance.remove_file(nsync_filename)
|
15
|
+
end
|
16
|
+
|
17
|
+
def nsync_filename
|
18
|
+
nsync_opts = self.class.read_inheritable_attribute(:nsync_opts)
|
19
|
+
File.join(self.class.to_s.underscore, "#{send(nsync_opts[:id_key])}.json")
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,129 @@
|
|
1
|
+
module Nsync
|
2
|
+
# The Nsync::Producer is used to tend to the repo and allow files to be
|
3
|
+
# written out and changesets to be commited. It is a subclass of the
|
4
|
+
# Nsync::Consumer, which allows a Producer to consume itself. This gives it
|
5
|
+
# the ability to perform rollbacks and undo misstakes in that way
|
6
|
+
#
|
7
|
+
# Basic Usage:
|
8
|
+
# Nsync::Config.run do |c|
|
9
|
+
# # The producer uses a standard repository
|
10
|
+
# # This will automatically be created if it does not exist
|
11
|
+
# c.repo_path = "/local/path/to/hold/data"
|
12
|
+
# # The remote repository url will get data pushed to it
|
13
|
+
# c.repo_push_url = "git@examplegithost:username/data.git"
|
14
|
+
#
|
15
|
+
# # This must be Nsync::GitVersionManager if you want things like
|
16
|
+
# # rollback to work.
|
17
|
+
# c.version_manager = Nsync::GitVersionManager.new
|
18
|
+
#
|
19
|
+
# # A lock file path to use for this app
|
20
|
+
# c.lock_file = "/tmp/app_name_nsync.lock"
|
21
|
+
# end
|
22
|
+
#
|
23
|
+
# # make some changes that get written out to the repo
|
24
|
+
#
|
25
|
+
# @producer = Nsync::Producer.new
|
26
|
+
#
|
27
|
+
# @producer.commit("Some nice changes for you")
|
28
|
+
class Producer < Consumer
|
29
|
+
# Determines whether a file at 'filename' exists in the working tree
|
30
|
+
def file_exists?(filename)
|
31
|
+
File.exists?(File.join(config.repo_path, filename))
|
32
|
+
end
|
33
|
+
|
34
|
+
# Writes a file to the repo at 'filename' with content in json from the
|
35
|
+
# hash
|
36
|
+
#
|
37
|
+
# @param [String] filename path in the working tree to write to
|
38
|
+
# @param [Hash] hash a hash that can be converted to json to be written
|
39
|
+
def write_file(filename, hash)
|
40
|
+
config.cd do
|
41
|
+
dir = File.dirname(filename)
|
42
|
+
unless [".", "/"].include?(dir) || File.directory?(dir)
|
43
|
+
FileUtils.mkdir_p(File.join(config.repo_path, dir))
|
44
|
+
end
|
45
|
+
|
46
|
+
File.open(File.join(config.repo_path, filename), "w") do |f|
|
47
|
+
f.write( (hash.is_a?(Hash))? hash.to_json : hash )
|
48
|
+
end
|
49
|
+
repo.add(File.join(config.repo_path, filename))
|
50
|
+
config.log.info("[NSYNC] Updated file '#{filename}'")
|
51
|
+
end
|
52
|
+
true
|
53
|
+
end
|
54
|
+
|
55
|
+
# Removes a file from the repo at 'filename'
|
56
|
+
def remove_file(filename)
|
57
|
+
FileUtils.rm File.join(config.repo_path, filename)
|
58
|
+
config.log.info("[NSYNC] Removed file '#{filename}'")
|
59
|
+
end
|
60
|
+
|
61
|
+
# Commits and pushes the current changeset
|
62
|
+
def commit(message="Friendly data update")
|
63
|
+
config.lock do
|
64
|
+
config.cd do
|
65
|
+
repo.commit_all(message)
|
66
|
+
config.log.info("[NSYNC] Committed '#{message}' to repo")
|
67
|
+
end
|
68
|
+
end
|
69
|
+
push
|
70
|
+
end
|
71
|
+
|
72
|
+
# Pushes all changes to the repo_push_url
|
73
|
+
def push
|
74
|
+
if config.remote_push?
|
75
|
+
config.cd do
|
76
|
+
repo.git.push({}, config.repo_push_url, "+master")
|
77
|
+
config.log.info("[NSYNC] Pushed changes")
|
78
|
+
end
|
79
|
+
end
|
80
|
+
true
|
81
|
+
end
|
82
|
+
|
83
|
+
# Returns data to its state at HEAD~1 and sets HEAD to that
|
84
|
+
# This new HEAD state is pushed to the repo_push_url. Hooray git.
|
85
|
+
def rollback
|
86
|
+
commit_to_rollback = config.version_manager.version
|
87
|
+
commit_to_rollback_to = config.version_manager.previous_version
|
88
|
+
config.cd do
|
89
|
+
repo.git.reset({:hard => true}, commit_to_rollback_to)
|
90
|
+
apply_changes(commit_to_rollback, commit_to_rollback_to)
|
91
|
+
end
|
92
|
+
push
|
93
|
+
end
|
94
|
+
|
95
|
+
protected
|
96
|
+
|
97
|
+
def update(*args)
|
98
|
+
super
|
99
|
+
end
|
100
|
+
|
101
|
+
def apply_changes(*args)
|
102
|
+
super
|
103
|
+
end
|
104
|
+
|
105
|
+
def get_or_create_repo
|
106
|
+
if File.exists?(config.repo_path)
|
107
|
+
return self.repo = Grit::Repo.new(config.repo_path)
|
108
|
+
end
|
109
|
+
|
110
|
+
config.lock do
|
111
|
+
self.repo = Grit::Repo.init(config.repo_path)
|
112
|
+
write_file(".gitignore", "")
|
113
|
+
end
|
114
|
+
commit("Initial Commit")
|
115
|
+
end
|
116
|
+
|
117
|
+
def consumer_classes_and_id_from_path(path)
|
118
|
+
producer_class_name = File.dirname(path).camelize
|
119
|
+
id = File.basename(path, ".json")
|
120
|
+
classes = config.consumer_classes_for(producer_class_name)
|
121
|
+
|
122
|
+
# refinement to allow the producer to consume itself
|
123
|
+
if classes.empty?
|
124
|
+
classes = [producer_class_name.constantize].compact
|
125
|
+
end
|
126
|
+
[classes, id]
|
127
|
+
end
|
128
|
+
end
|
129
|
+
end
|
data/lib/nsync.rb
ADDED
@@ -0,0 +1,30 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'fileutils'
|
3
|
+
|
4
|
+
# just for now
|
5
|
+
gem 'activesupport', "~> 2.3.5"
|
6
|
+
require 'active_support'
|
7
|
+
|
8
|
+
gem "schleyfox-grit", ">= 2.3.0.1"
|
9
|
+
require 'grit'
|
10
|
+
|
11
|
+
#up the timeout, as these repos can get quite large
|
12
|
+
Grit::Git.git_timeout = 60 # 1 minute should do
|
13
|
+
Grit::Git.git_max_size = 100.megabytes # tweak this up for very large changesets
|
14
|
+
|
15
|
+
gem "schleyfox-lockfile", ">= 1.0.0"
|
16
|
+
require 'lockfile'
|
17
|
+
|
18
|
+
begin
|
19
|
+
require 'yajl/json_gem'
|
20
|
+
rescue LoadError
|
21
|
+
puts "Yajl not installed; falling back to json"
|
22
|
+
require 'json'
|
23
|
+
end
|
24
|
+
|
25
|
+
require 'nsync/config'
|
26
|
+
require 'nsync/consumer'
|
27
|
+
require 'nsync/git_version_manager'
|
28
|
+
require 'nsync/producer'
|
29
|
+
require 'nsync/class_methods'
|
30
|
+
require 'nsync/active_record/methods.rb'
|
data/nsync.gemspec
ADDED
@@ -0,0 +1,80 @@
|
|
1
|
+
# Generated by jeweler
|
2
|
+
# DO NOT EDIT THIS FILE
|
3
|
+
# Instead, edit Jeweler::Tasks in Rakefile, and run `rake gemspec`
|
4
|
+
# -*- encoding: utf-8 -*-
|
5
|
+
|
6
|
+
Gem::Specification.new do |s|
|
7
|
+
s.name = %q{nsync}
|
8
|
+
s.version = "0.0.2"
|
9
|
+
|
10
|
+
s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
|
11
|
+
s.authors = ["Ben Hughes"]
|
12
|
+
s.date = %q{2010-12-06}
|
13
|
+
s.description = %q{Nsync is designed to allow you to have a separate data
|
14
|
+
processing app with its own data processing optimized database and a consumer
|
15
|
+
app with its own database, while keeping the data as in sync as you want it.}
|
16
|
+
s.email = %q{ben@pixelmachine.org}
|
17
|
+
s.extra_rdoc_files = [
|
18
|
+
"LICENSE",
|
19
|
+
"README.md"
|
20
|
+
]
|
21
|
+
s.files = [
|
22
|
+
"LICENSE",
|
23
|
+
"README.md",
|
24
|
+
"Rakefile",
|
25
|
+
"VERSION",
|
26
|
+
"jeweler_monkey_patch.rb",
|
27
|
+
"lib/nsync.rb",
|
28
|
+
"lib/nsync/active_record/consumer/methods.rb",
|
29
|
+
"lib/nsync/active_record/methods.rb",
|
30
|
+
"lib/nsync/active_record/producer/methods.rb",
|
31
|
+
"lib/nsync/class_methods.rb",
|
32
|
+
"lib/nsync/config.rb",
|
33
|
+
"lib/nsync/consumer.rb",
|
34
|
+
"lib/nsync/git_version_manager.rb",
|
35
|
+
"lib/nsync/producer.rb",
|
36
|
+
"lib/nsync/producer/methods.rb",
|
37
|
+
"nsync.gemspec"
|
38
|
+
]
|
39
|
+
s.homepage = %q{http://github.com/schleyfox/nsync}
|
40
|
+
s.require_paths = ["lib"]
|
41
|
+
s.rubygems_version = %q{1.3.6}
|
42
|
+
s.summary = %q{Keep your data processors and apps in sync}
|
43
|
+
s.test_files = [
|
44
|
+
"test/active_record_test.rb",
|
45
|
+
"test/classes.rb",
|
46
|
+
"test/helper.rb",
|
47
|
+
"test/nsync_config_test.rb",
|
48
|
+
"test/nsync_consumer_test.rb",
|
49
|
+
"test/nsync_producer_test.rb",
|
50
|
+
"test/repo.rb"
|
51
|
+
]
|
52
|
+
|
53
|
+
if s.respond_to? :specification_version then
|
54
|
+
current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
|
55
|
+
s.specification_version = 3
|
56
|
+
|
57
|
+
if Gem::Version.new(Gem::RubyGemsVersion) >= Gem::Version.new('1.2.0') then
|
58
|
+
s.add_runtime_dependency(%q<json>, [">= 0"])
|
59
|
+
s.add_runtime_dependency(%q<activesupport>, ["~> 2.3.5"])
|
60
|
+
s.add_runtime_dependency(%q<schleyfox-grit>, [">= 2.3.0.1"])
|
61
|
+
s.add_runtime_dependency(%q<schleyfox-lockfile>, [">= 1.0.0"])
|
62
|
+
s.add_development_dependency(%q<shoulda>, [">= 0"])
|
63
|
+
s.add_development_dependency(%q<mocha>, [">= 0"])
|
64
|
+
else
|
65
|
+
s.add_dependency(%q<json>, [">= 0"])
|
66
|
+
s.add_dependency(%q<activesupport>, ["~> 2.3.5"])
|
67
|
+
s.add_dependency(%q<schleyfox-grit>, [">= 2.3.0.1"])
|
68
|
+
s.add_dependency(%q<schleyfox-lockfile>, [">= 1.0.0"])
|
69
|
+
s.add_dependency(%q<shoulda>, [">= 0"])
|
70
|
+
s.add_dependency(%q<mocha>, [">= 0"])
|
71
|
+
end
|
72
|
+
else
|
73
|
+
s.add_dependency(%q<json>, [">= 0"])
|
74
|
+
s.add_dependency(%q<activesupport>, ["~> 2.3.5"])
|
75
|
+
s.add_dependency(%q<schleyfox-grit>, [">= 2.3.0.1"])
|
76
|
+
s.add_dependency(%q<schleyfox-lockfile>, [">= 1.0.0"])
|
77
|
+
s.add_dependency(%q<shoulda>, [">= 0"])
|
78
|
+
s.add_dependency(%q<mocha>, [">= 0"])
|
79
|
+
end
|
80
|
+
end
|