couchrest_changes 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
data/Rakefile ADDED
@@ -0,0 +1,93 @@
1
+ require "rubygems"
2
+ require "highline/import"
3
+ require "pty"
4
+ require "fileutils"
5
+ require 'rake/testtask'
6
+
7
+ ##
8
+ ## HELPER
9
+ ##
10
+
11
+ def run(cmd)
12
+ PTY.spawn(cmd) do |output, input, pid|
13
+ begin
14
+ while line = output.gets do
15
+ puts line
16
+ end
17
+ rescue Errno::EIO
18
+ end
19
+ end
20
+ rescue PTY::ChildExited
21
+ end
22
+
23
+ ##
24
+ ## GEM BUILDING AND INSTALLING
25
+ ##
26
+
27
+ $spec_path = 'couchrest_changes.gemspec'
28
+ $spec = eval(File.read($spec_path))
29
+ $base_dir = File.dirname(__FILE__)
30
+ $gem_path = File.join($base_dir, 'pkg', "#{$spec.name}-#{$spec.version}.gem")
31
+
32
+ def built_gem_path
33
+ Dir[File.join($base_dir, "#{$spec.name}-*.gem")].sort_by{|f| File.mtime(f)}.last
34
+ end
35
+
36
+ desc "Build #{$spec.name}-#{$spec.version}.gem into the pkg directory"
37
+ task 'build' do
38
+ FileUtils.mkdir_p(File.join($base_dir, 'pkg'))
39
+ FileUtils.rm($gem_path) if File.exists?($gem_path)
40
+ run "gem build -V '#{$spec_path}'"
41
+ file_name = File.basename(built_gem_path)
42
+ FileUtils.mv(built_gem_path, 'pkg')
43
+ say "#{$spec.name} #{$spec.version} built to pkg/#{file_name}"
44
+ end
45
+
46
+ desc "Install #{$spec.name}-#{$spec.version}.gem into either system-wide or user gems"
47
+ task 'install' do
48
+ if !File.exists?($gem_path)
49
+ say("Could not file #{$gem_path}. Try running 'rake build'")
50
+ else
51
+ if ENV["USER"] == "root"
52
+ run "gem install '#{$gem_path}'"
53
+ else
54
+ home_gem_path = Gem.path.grep(/home/).first
55
+ say("You are installing as an unprivileged user, which will result in the installation being placed in '#{home_gem_path}'.")
56
+ if agree("Do you want to continue installing to #{home_gem_path}? ")
57
+ run "gem install '#{$gem_path}' --user-install"
58
+ end
59
+ end
60
+ end
61
+ end
62
+
63
+ desc "Uninstall #{$spec.name}-#{$spec.version}.gem from either system-wide or user gems"
64
+ task 'uninstall' do
65
+ if ENV["USER"] == "root"
66
+ say("Removing #{$spec.name}-#{$spec.version}.gem from system-wide gems")
67
+ run "gem uninstall '#{$spec.name}' --version #{$spec.version} --verbose -x -I"
68
+ else
69
+ say("Removing #{$spec.name}-#{$spec.version}.gem from user's gems")
70
+ run "gem uninstall '#{$spec.name}' --version #{$spec.version} --verbose --user-install -x -I"
71
+ end
72
+ end
73
+
74
+ ##
75
+ ## TESTING
76
+ ##
77
+
78
+ Rake::TestTask.new do |t|
79
+ t.pattern = "test/unit/*_test.rb"
80
+ end
81
+ task :default => :test
82
+
83
+ ##
84
+ ## DOCUMENTATION
85
+ ##
86
+
87
+ # require 'rdoc/task'
88
+
89
+ # Rake::RDocTask.new do |rd|
90
+ # rd.main = "README.rdoc"
91
+ # rd.rdoc_files.include("README.rdoc","lib/**/*.rb","bin/**/*")
92
+ # rd.title = 'Your application title'
93
+ # end
data/Readme.md ADDED
@@ -0,0 +1,60 @@
1
+ CouchRest::Changes - keeping track of changes to your couch
2
+ ------------------------------------------------------------
3
+
4
+ ``CouchRest::Changes`` let's you observe a couch database for changes and react upon them.
5
+
6
+ Following the changes of a couch is as easy as
7
+ ```ruby
8
+ users = CouchRest::Changes.new('users')
9
+ ```
10
+
11
+ Callbacks can be defined in blocks:
12
+ ```ruby
13
+ users.created do |hash|
14
+ puts "A new user was created with the id: #{hash[:id]}
15
+ end
16
+ ```
17
+
18
+ To start listening just call
19
+ ```ruby
20
+ users.listen
21
+ ```
22
+
23
+ This program is written in Ruby and is distributed under the following license:
24
+
25
+ > GNU Affero General Public License
26
+ > Version 3.0 or higher
27
+ > http://www.gnu.org/licenses/agpl-3.0.html
28
+
29
+ Installation
30
+ ---------------------
31
+
32
+ Just add couchrest_changes to your gemfile.
33
+
34
+ Configuration
35
+ ---------------------
36
+
37
+ ``couchrest_changes`` can be configured through ``CouchRest::Changes::Config``
38
+
39
+ The default options are similar to the ones used by CouchRest::Model:
40
+
41
+
42
+ ```yaml
43
+ # couch connection configuration
44
+ connection:
45
+ protocol: "http"
46
+ host: "localhost"
47
+ port: 5984
48
+ username: ~
49
+ password: ~
50
+ prefix: ""
51
+ suffix: ""
52
+
53
+ # file to store the last processed user record in so we can resume after
54
+ # a restart:
55
+ seq_file: "/var/log/couch_changes_users.seq"
56
+
57
+ # Configure log_file like this if you want to log to a file instead of syslog:
58
+ # log_file: "/var/log/couch_changes.log"
59
+ log_level: debug
60
+ ```
@@ -0,0 +1,114 @@
1
+ require 'yaml'
2
+
3
+ module CouchRest
4
+ class Changes
5
+ module Config
6
+ extend self
7
+
8
+ attr_accessor :connection
9
+ attr_accessor :seq_file
10
+ attr_accessor :log_file
11
+ attr_writer :log_level
12
+ attr_accessor :logger
13
+ attr_accessor :options
14
+
15
+ def load(base_dir, *configs)
16
+ @base_dir = Pathname.new(base_dir)
17
+ loaded = configs.collect do |file_path|
18
+ file = find_file(file_path)
19
+ load_config(file)
20
+ end
21
+ init_logger
22
+ log_loaded_configs(loaded.compact)
23
+ logger.info "Observing #{couch_host_without_password}"
24
+ return self
25
+ end
26
+
27
+ def couch_host(conf = nil)
28
+ conf ||= connection
29
+ userinfo = [conf[:username], conf[:password]].compact.join(':')
30
+ userinfo += '@' unless userinfo.empty?
31
+ "#{conf[:protocol]}://#{userinfo}#{conf[:host]}:#{conf[:port]}"
32
+ end
33
+
34
+ def couch_host_without_password
35
+ couch_host connection.merge({:password => nil})
36
+ end
37
+
38
+ def complete_db_name(db_name)
39
+ [connection[:prefix], db_name, connection[:suffix]].
40
+ compact.
41
+ reject{|part| part == ""}.
42
+ join('_')
43
+ end
44
+
45
+ private
46
+
47
+ def init_logger
48
+ if log_file
49
+ require 'logger'
50
+ @logger = Logger.new(log_file)
51
+ else
52
+ require 'syslog/logger'
53
+ @logger = Syslog::Logger.new('leap_key_daemon')
54
+ end
55
+ @logger.level = Logger.const_get(log_level.upcase)
56
+ end
57
+
58
+ def load_config(file_path)
59
+ return unless file_path
60
+ load_settings YAML.load(File.read(file_path)), file_path
61
+ return file_path
62
+ end
63
+
64
+ def load_settings(hash, file_path)
65
+ return unless hash
66
+ hash.each do |key, value|
67
+ begin
68
+ apply_setting(key, value)
69
+ rescue NoMethodError => exc
70
+ # log might not have been configured yet correctly
71
+ # so better also print this
72
+ STDERR.puts "Error in file #{file_path}"
73
+ STDERR.puts "'#{key}' is not a valid option"
74
+ init_logger
75
+ logger.warn "Error in file #{file_path}"
76
+ logger.warn "'#{key}' is not a valid option"
77
+ logger.debug exc
78
+ end
79
+ end
80
+ end
81
+
82
+ def apply_setting(key, value)
83
+ if value.is_a? Hash
84
+ value = symbolize_keys(value)
85
+ end
86
+ self.send("#{key}=", value)
87
+ end
88
+
89
+ def self.symbolize_keys(hsh)
90
+ newhsh = {}
91
+ hsh.keys.each do |key|
92
+ newhsh[key.to_sym] = hsh[key]
93
+ end
94
+ newhsh
95
+ end
96
+
97
+ def find_file(file_path)
98
+ return unless file_path
99
+ filenames = [Pathname.new(file_path), @base_dir + file_path]
100
+ filenames.find{|f| f.file?}
101
+ end
102
+
103
+ def log_loaded_configs(files)
104
+ files.each do |file|
105
+ logger.info "Loaded config from #{file} ."
106
+ end
107
+ end
108
+
109
+ def log_level
110
+ @log_level || 'info'
111
+ end
112
+ end
113
+ end
114
+ end
@@ -0,0 +1,5 @@
1
+ module CouchRest
2
+ class Changes
3
+ VERSION = "0.0.1"
4
+ end
5
+ end
@@ -0,0 +1,116 @@
1
+ require 'couchrest'
2
+ require 'fileutils'
3
+ require 'pathname'
4
+
5
+ require 'couchrest/changes/config'
6
+
7
+ module CouchRest
8
+ class Changes
9
+
10
+ attr_writer :logger
11
+
12
+ def initialize(db_name)
13
+ db_name = Config.complete_db_name(db_name)
14
+ logger.info "Tracking #{db_name}"
15
+ @db = CouchRest.new(Config.couch_host).database(db_name)
16
+ @seq_filename = Config.seq_file
17
+ read_seq(@seq_filename)
18
+ end
19
+
20
+ # triggered when a document was newly created
21
+ def created(hash = {}, &block)
22
+ run_or_define_hook :created, hash, &block
23
+ end
24
+
25
+ # triggered when a document was deleted
26
+ def deleted(hash = {}, &block)
27
+ run_or_define_hook :deleted, hash, &block
28
+ end
29
+
30
+ # triggered when an existing document was updated
31
+ def updated(hash = {}, &block)
32
+ run_or_define_hook :updated, hash, &block
33
+ end
34
+
35
+ # triggered whenever a document was changed
36
+ def changed(hash = {}, &block)
37
+ run_or_define_hook :changed, hash, &block
38
+ end
39
+
40
+ def listen
41
+ logger.info "listening..."
42
+ logger.debug "Starting at sequence #{since}"
43
+ result = db.changes :feed => :continuous, :since => since, :heartbeat => 1000 do |hash|
44
+ callbacks(hash)
45
+ store_seq(hash["seq"])
46
+ end
47
+ logger.info "couch stream ended unexpectedly."
48
+ logger.debug result.inspect
49
+ end
50
+
51
+ protected
52
+
53
+ def logger
54
+ logger ||= Config.logger
55
+ end
56
+
57
+ def db
58
+ @db
59
+ end
60
+
61
+ def since
62
+ @since ||= 0 # fetch_last_seq
63
+ end
64
+
65
+ def callbacks(hash)
66
+ # let's not track design document changes
67
+ return if hash['id'].start_with? '_design/'
68
+ return unless changes = hash["changes"]
69
+ changed(hash)
70
+ return deleted(hash) if hash["deleted"]
71
+ return created(hash) if changes[0]["rev"].start_with?('1-')
72
+ updated(hash)
73
+ end
74
+
75
+ def run_or_define_hook(event, hash = {}, &block)
76
+ @callbacks ||= {}
77
+ if block_given?
78
+ @callbacks[event] = block
79
+ else
80
+ @callbacks[event] && @callbacks[event].call(hash)
81
+ end
82
+ end
83
+
84
+ def read_seq(seq_filename)
85
+ logger.debug "Looking up sequence here: #{seq_filename}"
86
+ FileUtils.touch(seq_filename)
87
+ unless File.writable?(seq_filename)
88
+ raise StandardError.new("Can't access sequence file")
89
+ end
90
+ @since = File.read(seq_filename)
91
+ if @since == ''
92
+ @since = nil
93
+ logger.debug "Found no sequence in the file."
94
+ else
95
+ logger.debug "Found sequence: #{@since}"
96
+ end
97
+ rescue Errno::ENOENT => e
98
+ logger.warn "No sequence file found. Starting from scratch"
99
+ end
100
+
101
+ def store_seq(seq)
102
+ File.write @seq_filename, MultiJson.dump(seq)
103
+ end
104
+
105
+ #
106
+ # UNUSED: this is useful for only following new sequences.
107
+ # might also require .to_json to work on bigcouch.
108
+ #
109
+ def fetch_last_seq
110
+ hash = db.changes :limit => 1, :descending => true
111
+ logger.info "starting at seq: " + hash["last_seq"]
112
+ return hash["last_seq"]
113
+ end
114
+
115
+ end
116
+ end
@@ -0,0 +1,8 @@
1
+ require 'rubygems'
2
+ require 'minitest/autorun'
3
+
4
+ BASE_DIR = File.expand_path('../..', __FILE__)
5
+ $:.unshift File.expand_path('lib', BASE_DIR)
6
+
7
+ require 'mocha/setup'
8
+
metadata ADDED
@@ -0,0 +1,166 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: couchrest_changes
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Azul
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2013-12-20 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: couchrest
16
+ requirement: !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ~>
20
+ - !ruby/object:Gem::Version
21
+ version: 1.1.3
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ~>
28
+ - !ruby/object:Gem::Version
29
+ version: 1.1.3
30
+ - !ruby/object:Gem::Dependency
31
+ name: yajl-ruby
32
+ requirement: !ruby/object:Gem::Requirement
33
+ none: false
34
+ requirements:
35
+ - - ! '>='
36
+ - !ruby/object:Gem::Version
37
+ version: '0'
38
+ type: :runtime
39
+ prerelease: false
40
+ version_requirements: !ruby/object:Gem::Requirement
41
+ none: false
42
+ requirements:
43
+ - - ! '>='
44
+ - !ruby/object:Gem::Version
45
+ version: '0'
46
+ - !ruby/object:Gem::Dependency
47
+ name: syslog_logger
48
+ requirement: !ruby/object:Gem::Requirement
49
+ none: false
50
+ requirements:
51
+ - - ~>
52
+ - !ruby/object:Gem::Version
53
+ version: 2.0.0
54
+ type: :runtime
55
+ prerelease: false
56
+ version_requirements: !ruby/object:Gem::Requirement
57
+ none: false
58
+ requirements:
59
+ - - ~>
60
+ - !ruby/object:Gem::Version
61
+ version: 2.0.0
62
+ - !ruby/object:Gem::Dependency
63
+ name: minitest
64
+ requirement: !ruby/object:Gem::Requirement
65
+ none: false
66
+ requirements:
67
+ - - ~>
68
+ - !ruby/object:Gem::Version
69
+ version: 3.2.0
70
+ type: :development
71
+ prerelease: false
72
+ version_requirements: !ruby/object:Gem::Requirement
73
+ none: false
74
+ requirements:
75
+ - - ~>
76
+ - !ruby/object:Gem::Version
77
+ version: 3.2.0
78
+ - !ruby/object:Gem::Dependency
79
+ name: mocha
80
+ requirement: !ruby/object:Gem::Requirement
81
+ none: false
82
+ requirements:
83
+ - - ! '>='
84
+ - !ruby/object:Gem::Version
85
+ version: '0'
86
+ type: :development
87
+ prerelease: false
88
+ version_requirements: !ruby/object:Gem::Requirement
89
+ none: false
90
+ requirements:
91
+ - - ! '>='
92
+ - !ruby/object:Gem::Version
93
+ version: '0'
94
+ - !ruby/object:Gem::Dependency
95
+ name: rake
96
+ requirement: !ruby/object:Gem::Requirement
97
+ none: false
98
+ requirements:
99
+ - - ! '>='
100
+ - !ruby/object:Gem::Version
101
+ version: '0'
102
+ type: :development
103
+ prerelease: false
104
+ version_requirements: !ruby/object:Gem::Requirement
105
+ none: false
106
+ requirements:
107
+ - - ! '>='
108
+ - !ruby/object:Gem::Version
109
+ version: '0'
110
+ - !ruby/object:Gem::Dependency
111
+ name: highline
112
+ requirement: !ruby/object:Gem::Requirement
113
+ none: false
114
+ requirements:
115
+ - - ! '>='
116
+ - !ruby/object:Gem::Version
117
+ version: '0'
118
+ type: :development
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ none: false
122
+ requirements:
123
+ - - ! '>='
124
+ - !ruby/object:Gem::Version
125
+ version: '0'
126
+ description: Watches the couch database for changes and triggers callbacks defined
127
+ for creation, deletes and updates.
128
+ email:
129
+ - azul@leap.se
130
+ executables: []
131
+ extensions: []
132
+ extra_rdoc_files: []
133
+ files:
134
+ - lib/couchrest/changes/config.rb
135
+ - lib/couchrest/changes/version.rb
136
+ - lib/couchrest/changes.rb
137
+ - Rakefile
138
+ - Readme.md
139
+ - test/test_helper.rb
140
+ homepage: https://leap.se
141
+ licenses: []
142
+ post_install_message:
143
+ rdoc_options: []
144
+ require_paths:
145
+ - lib
146
+ required_ruby_version: !ruby/object:Gem::Requirement
147
+ none: false
148
+ requirements:
149
+ - - ! '>='
150
+ - !ruby/object:Gem::Version
151
+ version: '0'
152
+ required_rubygems_version: !ruby/object:Gem::Requirement
153
+ none: false
154
+ requirements:
155
+ - - ! '>='
156
+ - !ruby/object:Gem::Version
157
+ version: '0'
158
+ requirements: []
159
+ rubyforge_project:
160
+ rubygems_version: 1.8.25
161
+ signing_key:
162
+ specification_version: 3
163
+ summary: CouchRest::Changes - Observe a couch database for changes and react upon
164
+ them
165
+ test_files:
166
+ - test/test_helper.rb