couchrest_changes 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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