couchdb-ruby-server 1.0.0

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.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: aa9cbfeea39a79efb59cfa9b1d278ee06ea5451c
4
+ data.tar.gz: 1fa6648489a455bc477d446af41b2f84c48eb215
5
+ SHA512:
6
+ metadata.gz: 196f7dd41438a62fe78b9d98c3f6824b04e29639f82fc9994017ad95d3594c8476f01211bdc985405283d1a80c9674f9a443191a8b72afc2ae57a6a8fee287a9
7
+ data.tar.gz: 1e4608f3551c2937ce0acc9f2a7f13075e2ccb9b079b3c1935cf7e1537af679053c32649c310aa3393ec442ea5eab0d1777053dba02fa6da703bdf6a67a542b7
data/.gitignore ADDED
@@ -0,0 +1,21 @@
1
+ *~
2
+ ~*
3
+ .DS_Store
4
+ ._.DS_Store
5
+ thumbs.db
6
+ *.log
7
+ *.pid
8
+ *.dump
9
+ *.prof
10
+ *.prof.gif
11
+ *.prof.symbols
12
+ .svn/
13
+ .hg/
14
+ *.sublime-workspace
15
+ .env
16
+ .rbenv-version
17
+ .ruby-version
18
+ Gemfile.lock
19
+ core
20
+ *.gem
21
+ /doc/
data/.hound.yml ADDED
@@ -0,0 +1,4 @@
1
+ # NOTE: it works on pull requests only
2
+ ruby:
3
+ config_file: .rubocop.yml
4
+ enabled: true
data/.travis.yml ADDED
@@ -0,0 +1,13 @@
1
+ language: ruby
2
+ rvm:
3
+ - "1.8.7"
4
+ - "1.9.3"
5
+ - "2.0.0"
6
+ - "2.1" # latest 2.1.x
7
+ - "2.2" # latest 2.2.x
8
+ - "jruby-18mode"
9
+ - "jruby-19mode"
10
+ script:
11
+ CODECLIMATE_REPO_TOKEN=PLACE_TOKEN_HERE bundle exec rake test
12
+ branches:
13
+ - "master"
data/Gemfile ADDED
@@ -0,0 +1,5 @@
1
+ source 'https://rubygems.org'
2
+ gemspec
3
+ gem 'codeclimate-test-reporter', :group => :development,
4
+ :require => nil,
5
+ :platforms => [:ruby_20]
data/HISTORY.md ADDED
@@ -0,0 +1,4 @@
1
+ # 1.0.0
2
+ 2015-MM-DD
3
+
4
+ * First public release
data/LICENSE.txt ADDED
@@ -0,0 +1,20 @@
1
+ MIT License, http://opensource.org/licenses/MIT
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,38 @@
1
+ # couchdb-ruby-server
2
+ CouchDB query server to run ruby design docs.
3
+
4
+ [![Build Status](https://travis-ci.org/sinm/couchdb-ruby-server.svg?branch=master)](https://travis-ci.org/sinm/couchdb-ruby-server) [![Gem Version](https://badge.fury.io/rb/couchdb-ruby-server.svg)](http://badge.fury.io/rb/couchdb-ruby-server) [![Dependency Status](https://gemnasium.com/sinm/couchdb-ruby-server.svg)](https://gemnasium.com/sinm/couchdb-ruby-server) [![GitHub license](https://img.shields.io/badge/license-MIT-blue.svg)](https://raw.githubusercontent.com/sinm/couchdb-ruby-server/master/LICENSE.txt) [![Code Climate](https://codeclimate.com/github/sinm/couchdb-ruby-server/badges/gpa.svg)](https://codeclimate.com/github/sinm/couchdb-ruby-server) [![Test Coverage](https://codeclimate.com/github/sinm/couchdb-ruby-server/badges/coverage.svg)](https://codeclimate.com/github/sinm/couchdb-ruby-server) [![Inline docs](http://inch-ci.org/github/sinm/couchdb-ruby-server.svg?branch=master)](http://inch-ci.org/github/sinm/couchdb-ruby-server)
5
+
6
+ ## Installation
7
+ `gem install couchdb-ruby-server` as usual.
8
+
9
+ Edit your couchdb's local.ini and set this in the `[query_servers]` group:
10
+
11
+ ruby = couchdb-ruby-server
12
+
13
+ Restart CouchDB then.
14
+
15
+ ## Usage
16
+ Design functions (map, reduce, update, etc.) are well defined as `lambda` or `proc` blocks:
17
+
18
+ lambda { |doc| emit [1,2,3], doc }
19
+
20
+ It looks like any object responding to `#call` will survive.
21
+
22
+ [Couchapp](couchapp.org) works well with `rb` files, so use it to manage design files. Just do `echo ruby > language` inside your app's root directory and you're in.
23
+
24
+ You may load your own code into the query server context by providing path to a library as an `--require LIB` option. This way you could deserialize documents right into the instances of the library classes instead of plain hashes.
25
+
26
+ Actually any tagname for the query server may be set in `local.ini` instead of `ruby` so it's possible to run several query servers e.g. per application. Just remember to copy that tagname into couchapp's `language` file.
27
+
28
+ ## Tests
29
+ `bundle exec rake test`. Gem following original [tests](https://github.com/apache/couchdb/blob/master/test/view_server/query_server_spec.rb).
30
+
31
+ ## Documentation
32
+ This README is all i could say in a rush. No other documentation provided at this moment, see the sources.
33
+
34
+ ## If you've found a bug or drawback
35
+ Don't hesistate to leave a report.
36
+
37
+ ## License
38
+ MIT for now.
data/Rakefile ADDED
@@ -0,0 +1,16 @@
1
+ #!/usr/bin/env rake
2
+ require 'rake/testtask'
3
+
4
+ Rake::TestTask.new :test do |t|
5
+ t.pattern = 'spec/**/*_spec.rb'
6
+ t.libs.push 'spec'
7
+ # bundle exec rake test TESTOPTS='-v'
8
+ end
9
+
10
+ task :build => :test do
11
+ system('gem build couchdb-ruby-server.gemspec')
12
+ # gem install couchdb-ruby-server-1.0.0.gem
13
+ # gem push couchdb-ruby-server-1.0.0.gem
14
+ end
15
+
16
+ task :default => :test
data/TODO.md ADDED
@@ -0,0 +1,4 @@
1
+ # TODO file
2
+
3
+ 3. Client and server libs -- see what i have to copy here
4
+ 4. Implement $SAFE=4 thread, set safe level via args
@@ -0,0 +1,18 @@
1
+ #!/usr/bin/env RUBY_GC_MALLOC_LIMIT=24000000 ruby
2
+ require 'couchdb-ruby-server'
3
+ require 'argparser'
4
+
5
+ args = ArgParser.new :program => 'couchdb-ruby-server',
6
+ :version => CouchdbRubyServer::VERSION,
7
+ :info => 'CouchDB query server to run ruby design docs.',
8
+ :licence => 'MIT',
9
+ :copyright=> '2014-2015 sinmsinm@gmail.com',
10
+ :options => [{ :names => %w[r require],
11
+ :argument => 'LIB',
12
+ :multiple => true,
13
+ :help => 'Aux library to require.' }]
14
+ args['require'].value.each do |r|
15
+ require r
16
+ end
17
+
18
+ CouchdbRubyServer.run
@@ -0,0 +1,28 @@
1
+ # coding: utf-8
2
+ require File.expand_path('../lib/couchdb-ruby-server/version.rb', __FILE__)
3
+
4
+ Gem::Specification.new do |s|
5
+ s.name = 'couchdb-ruby-server'
6
+ s.version = CouchdbRubyServer::VERSION
7
+ s.authors = ['sinm']
8
+ s.email = 'sinm.sinm@gmail.com'
9
+ s.summary = 'CouchDB query server to run ruby design docs'
10
+ s.description = '== CouchDB query server to run ruby design docs'
11
+ s.homepage = 'https://github.com/sinm/couchdb-ruby-server'
12
+ s.license = 'MIT'
13
+ s.files = `git ls-files -z`.split("\x0")
14
+ s.test_files = `git ls-files -z spec/`.split("\0")
15
+ s.require_paths = ['lib']
16
+ s.bindir = 'bin'
17
+ s.executables << 'couchdb-ruby-server'
18
+ # s.extra_rdoc_files = 'README.md'
19
+ # s.rdoc_options << '--title' << 'couchdb-ruby-server' <<
20
+ # '--main' << 'README' <<
21
+ # '--markup' << 'markdown' <<
22
+ # '--line-numbers'
23
+ s.add_development_dependency 'bundler', '~> 1'
24
+ s.add_development_dependency 'rake', '~> 10'
25
+ s.add_development_dependency 'minitest', '~> 4'
26
+ s.add_dependency 'argparser', '~> 1'
27
+ s.add_dependency 'oj', '~> 2.7'
28
+ end
@@ -0,0 +1,27 @@
1
+ {
2
+ "folders":
3
+ [
4
+ {
5
+ "path": "/Users/sinm/Projects/couchdb-ruby-server"
6
+ }
7
+ ],
8
+ "build_systems":
9
+ [
10
+ {
11
+ "name": "Run tests",
12
+ "cmd": ["bundle", "exec", "rake", "test"],
13
+ "selector": "source.rb"
14
+ }
15
+ ],
16
+ "settings":
17
+ {
18
+ "ensure_newline_at_eof_on_save": true,
19
+ "rulers":
20
+ [
21
+ 80
22
+ ],
23
+ "tab_size": 2,
24
+ "translate_tabs_to_spaces": true,
25
+ "trim_trailing_white_space_on_save": true
26
+ }
27
+ }
@@ -0,0 +1,330 @@
1
+ # coding: utf-8
2
+
3
+
4
+ $stdin.sync = true
5
+ $stdout.sync = true
6
+
7
+ module CouchdbRubyServer
8
+ class Error < RuntimeError
9
+ attr_reader :key
10
+ def initialize key
11
+ @key = key
12
+ end
13
+ end
14
+ class Fatal < Error
15
+ end
16
+
17
+ def self.update_case fn, args
18
+ result = fn.call(*args)
19
+ return ["up", result[0], maybe_wrap_response(result[1])]
20
+ end
21
+
22
+ MIME_TYPES = {
23
+ :all => "*/*",
24
+ :text => "text/plain; charset=utf-8",
25
+ :html => "text/html; charset=utf-8",
26
+ :xhtml => "application/xhtml+xml",
27
+ :xml => "application/xml",
28
+ :js => "text/javascript",
29
+ :css => "text/css",
30
+ :ics => "text/calendar",
31
+ :csv => "text/csv; charset=utf-8",
32
+ :rss => "application/rss+xml",
33
+ :atom => "application/atom+xml",
34
+ :yaml => "application/x-yaml",
35
+ :multipart_form => "multipart/form-data",
36
+ :url_encoded_form => "application/x-www-form-urlencoded",
37
+ :json => "application/json; charset=utf-8"
38
+ }
39
+
40
+ TYPES_MIME = Hash[MIME_TYPES.map {|k, v| [v, k] }]
41
+
42
+ def self.to_json(obj)
43
+ Oj.dump(obj)
44
+ end
45
+
46
+ def self.json_parse(str)
47
+ return nil if str.nil?
48
+ Oj.load str
49
+ end
50
+
51
+ def self.log message
52
+ respond ['log', "#{message}"]
53
+ end
54
+
55
+ def self.respond obj
56
+ puts to_json obj
57
+ rescue
58
+ log "respond error converting object to JSON: #{$!.message}\n#{$!.backtrace.join("\n")}"
59
+ puts 'false'
60
+ end
61
+
62
+ def self.respond_error error, reason
63
+ if reason.respond_to?(:message) && reason.respond_to?(:backtrace)
64
+ reason = "#{reason.class.name}: #{reason.message}\n#{reason.backtrace.join("\n")}"
65
+ end
66
+ respond ['error', error, reason]
67
+ end
68
+
69
+ def self.respond_fatal error, reason
70
+ respond_error error, reason
71
+ exit(2)
72
+ end
73
+
74
+ COMPILED_PROCS = {}
75
+
76
+ def self.compile_proc source, binding, name
77
+ source_hash = source.hash
78
+ fn = COMPILED_PROCS[source_hash]
79
+ return fn if fn
80
+ begin
81
+ fn = eval source, binding, name, 1
82
+ raise "#{name} must respond_to #call" unless fn.respond_to?(:call)
83
+ COMPILED_PROCS[source_hash] = fn
84
+ rescue Exception
85
+ respond_error :compilation_error, $!
86
+ end
87
+ fn
88
+ end
89
+
90
+ @@map_results = []
91
+ @@callables = []
92
+ @@ddocs = {}
93
+ #list state
94
+ @@chunks = []
95
+ @@start_resp = {}
96
+ @@got_row = false
97
+ @@last_row = false
98
+ @@provides_used = []
99
+ @@resp_mime = nil
100
+ #view filter
101
+ @@view_emit = nil
102
+
103
+ def self.emit(key, value)
104
+ if @@view_emit.nil?
105
+ @@map_results.push [key, value]
106
+ else
107
+ @@view_emit = true
108
+ end
109
+ end
110
+
111
+ def self.provides(*args, &block)
112
+ @@provides_used << {:format => args[0], :block => block}.merge(
113
+ args.size > 1 ? {:mime => args[1]} : {}
114
+ )
115
+ nil
116
+ end
117
+
118
+ def self.start(resp)
119
+ @@start_resp = resp if resp
120
+ nil
121
+ end
122
+
123
+ def self.send(chunk)
124
+ @@chunks << "#{chunk}"
125
+ nil
126
+ end
127
+
128
+ def self.get_row
129
+ return nil if @@last_row
130
+ if not @@got_row
131
+ @@got_row = true
132
+ render_headers
133
+ respond ["start", @@chunks, @@start_resp]
134
+ @@start_resp = {}
135
+ else
136
+ respond ["chunks", @@chunks]
137
+ end
138
+ @@chunks = []
139
+ json = json_parse ARGF.gets
140
+ if json[0] == 'list_end'
141
+ @@last_row = true
142
+ return nil
143
+ elsif json[0] != 'list_row'
144
+ respond_fatal :list_error, "not a row: #{json[0]}"
145
+ end
146
+ json[1]
147
+ end
148
+
149
+
150
+ def self.maybe_wrap_response resp
151
+ {:body=>''}.merge(
152
+ resp.respond_to?(:to_hash) ? resp.to_hash : {:body=>"#{resp}"})
153
+ end
154
+
155
+ def self.run_reduce(srcs, ks, vs, rereduce, b)
156
+ fns = srcs.collect { |src| compile_proc(src, b, "reduce (#{src[0..100]}...)") || return }
157
+ reductions = fns.collect { |fn|
158
+ begin
159
+ fn.call(ks, vs, rereduce)
160
+ rescue
161
+ log "#{rereduce ? 'rereduce' : 'reduce'} raised exception", $!
162
+ nil
163
+ end
164
+ }
165
+ respond [true, reductions]
166
+ end
167
+
168
+ def self.reset_list
169
+ @@got_row = false
170
+ @@last_row = false
171
+ @@chunks = []
172
+ @@start_resp = {}
173
+ end
174
+
175
+ def self.reset_provides
176
+ @@provides_used = []
177
+ @@resp_mime = nil
178
+ end
179
+
180
+ def self.render_headers
181
+ @@start_resp[:headers] ||= {}
182
+ @@start_resp[:headers]["Content-Type"] ||= @@resp_mime if @@resp_mime
183
+ end
184
+
185
+ def self.run_provides req
186
+ format = :unknown
187
+ best_fun = if req['query'] and req['query']['format']
188
+ format = req['query']['format']
189
+ @@provides_used.find do |p|
190
+ p[:format].to_s == format
191
+ end
192
+ elsif req['headers'] && (format = req['headers']['Accept'])
193
+ f = nil
194
+ format.split(',').each do |a|
195
+ break if (f = a =~ /^\*\/\*/ ? @@provides_used.first :
196
+ @@provides_used.find do |p|
197
+ a == p[:mime] or p[:format] == TYPES_MIME[a]
198
+ end)
199
+ end
200
+ f
201
+ else
202
+ @@provides_used.first
203
+ end
204
+ raise "format #{format} not supported" unless best_fun
205
+ raise "no block in provides" unless best_fun[:block] &&
206
+ best_fun[:block].respond_to?(:call)
207
+ @@resp_mime = best_fun[:mime] || MIME_TYPES[best_fun[:format]]
208
+ best_fun[:block].call
209
+ end
210
+
211
+ def self.run
212
+ log "CouchdbRubyServer started, RUBY_VERSION: #{RUBY_VERSION}"
213
+ log "RUBY_GC_MALLOC_LIMIT=#{ENV['RUBY_GC_MALLOC_LIMIT']}"
214
+ while cmd = $stdin.gets
215
+ cmd = json_parse cmd
216
+ case cmd[0]
217
+ when "reset"
218
+ @@callables = []
219
+ respond(true)
220
+ when "add_fun"
221
+ fn = compile_proc cmd[1], binding, "map (#{cmd[1][0..100]}...)"
222
+ next unless fn
223
+ @@callables << fn
224
+ respond(true)
225
+ when "map_doc"
226
+ results = []
227
+ @@view_emit = nil
228
+ doc = cmd[1]
229
+ doc.freeze
230
+ @@callables.each do |callable|
231
+ @@map_results = []
232
+ begin
233
+ callable.call(doc)
234
+ results.push(@@map_results)
235
+ rescue
236
+ log "map raised exception with doc._id #{doc['_id']}", $!
237
+ results.push([])
238
+ end
239
+ end
240
+ respond(results)
241
+ when "reduce"
242
+ ks, vs = cmd[2].transpose
243
+ run_reduce(cmd[1], ks, vs, false, binding)
244
+ when "rereduce"
245
+ run_reduce(cmd[1], nil, cmd[2], true, binding)
246
+ when "ddoc"
247
+ cmd.shift
248
+ ddoc_id = cmd.shift
249
+ if ddoc_id == 'new'
250
+ ddoc_id = cmd.shift
251
+ @@ddocs[ddoc_id] = cmd.shift
252
+ respond true
253
+ else
254
+ ddoc = @@ddocs[ddoc_id]
255
+ if not ddoc
256
+ respond_fatal :query_protocol_error, "uncached design doc: #{ddoc_id}"
257
+ end
258
+ point = ddoc
259
+ path = cmd.shift
260
+ begin
261
+ path.each do |level|
262
+ point = (point[level] || raise("missing #{level} function in #{ddoc_id}"))
263
+ end
264
+ rescue
265
+ respond_error :not_found, $!
266
+ next
267
+ end
268
+ next unless (fn = compile_proc point, binding, "#{ddoc['_id']}/#{path.join("/")}")
269
+ begin
270
+ response = nil
271
+ args = cmd.shift
272
+ case path[0]
273
+ when "updates"
274
+ respond update_case fn, args
275
+ when "lists"
276
+ #head = args[0]
277
+ reset_list
278
+ reset_provides
279
+ tail = fn.call(*args)
280
+ begin
281
+ tail = run_provides(args[1]) unless @@provides_used.empty?
282
+ rescue
283
+ respond_error :not_acceptable, $!
284
+ next
285
+ end
286
+ get_row unless @@got_row
287
+ @@chunks << tail unless tail.nil?
288
+ respond ["end", @@chunks]
289
+ when "shows"
290
+ respond ["resp", maybe_wrap_response(fn.call(*args))]
291
+ when "filters"
292
+ res = []
293
+ args[0].each do |doc|
294
+ res.push(fn.call(doc, args[1]) ? true : false)
295
+ end
296
+ respond [true, res]
297
+ when "views"
298
+ res = []
299
+ args[0].each do |doc|
300
+ @@view_emit = false
301
+ fn.call doc
302
+ res.push(@@view_emit ? true : false)
303
+ end
304
+ respond [true, res]
305
+ when "validate_doc_update"
306
+ begin
307
+ fn.call(*args)
308
+ respond 1
309
+ rescue
310
+ respond({:forbidden => "bad doc"})
311
+ end
312
+ else
313
+ respond_fatal :unknown_command, "unknown ddoc command #{path[0]}"
314
+ next
315
+ end # case ddoc
316
+ rescue Fatal
317
+ respond_fatal $!.key, $!.message
318
+ rescue Error
319
+ respond_error $!.key, $!.message
320
+ rescue
321
+ respond_error :render_error, $!
322
+ end
323
+ end
324
+ else
325
+ respond_fatal :unknown_command, "unknown command #{cmd[0]}"
326
+ end
327
+ end
328
+ end
329
+
330
+ end