tatami 0.0.1 → 0.0.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/Rakefile +9 -0
- data/couches/cars/cars.coffee +39 -0
- data/couches/cars/erlang_design.erl +9 -0
- data/couches/flot/flot.coffee +30 -0
- data/lib/tatami.rb +5 -5
- data/lib/tatami/app.rb +20 -0
- data/lib/tatami/couch.rb +322 -0
- data/lib/tatami/flot.rb +20 -0
- data/lib/tatami/tatami.rb +5 -0
- data/lib/tatami/version.rb +1 -1
- data/test/couch_test.rb +213 -0
- data/test/fixtures/d3/css/app.css +1 -0
- data/test/fixtures/d3/index.html +12 -0
- data/test/fixtures/d3/js/app.js +6 -0
- data/test/fixtures/flot.yml +23 -0
- data/test/flot_test.rb +45 -0
- data/test/flot_views.rb +28 -0
- data/test/view_test.rb +42 -0
- metadata +25 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 7a2dbb07adb70e19071bbd27c4fed18682cddd1e
|
4
|
+
data.tar.gz: 575192e6a8d4dbb989085f9ccabf23cd3ef670d9
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: a4abd3c393ca07b7fb0acaea1c7c81cadb9a07306e3f3150a24f40a131cb9ab8c68776fee6f36700fc34ef936d3476642885d94280f75fec25e16be17639bec9
|
7
|
+
data.tar.gz: 32bb2cdf1e33aff92f588e1caf8c619ebdf62a77f9888e5743c641c3378b368807fb3fdc679cc7e1c3a66f66ac529650d49515c6c12a2fc1ac765013d5b1f1eb
|
data/Rakefile
CHANGED
@@ -0,0 +1,39 @@
|
|
1
|
+
# views simple map
|
2
|
+
(doc) ->
|
3
|
+
emit "a", doc.a
|
4
|
+
emit "b", doc.a
|
5
|
+
|
6
|
+
# views simple reduce
|
7
|
+
# keys = [[key1,docid1],..] when rereduce,else null
|
8
|
+
(keys, values, rereduce) ->
|
9
|
+
sum(values)
|
10
|
+
|
11
|
+
# views count_sum map
|
12
|
+
(doc) ->
|
13
|
+
emit doc.date, [1,doc.count]
|
14
|
+
|
15
|
+
# views count_sum reduce
|
16
|
+
(keys, values, rereduce) ->
|
17
|
+
log values
|
18
|
+
s1 = 0
|
19
|
+
s2 = 0
|
20
|
+
for v in values
|
21
|
+
s1 = s1 + v[0]
|
22
|
+
s2 = s2 + v[1]
|
23
|
+
[s1,s2]
|
24
|
+
|
25
|
+
# views first_of_group map
|
26
|
+
(doc) ->
|
27
|
+
emit doc.date, doc.count
|
28
|
+
|
29
|
+
# views first_of_group reduce
|
30
|
+
(keys, values, rereduce) ->
|
31
|
+
values[0]
|
32
|
+
|
33
|
+
# views perf map
|
34
|
+
(doc) ->
|
35
|
+
emit null, 1
|
36
|
+
|
37
|
+
# views perf reduce
|
38
|
+
(keys, values, rereduce) ->
|
39
|
+
sum(values)
|
@@ -0,0 +1,30 @@
|
|
1
|
+
# views count map
|
2
|
+
(doc) ->
|
3
|
+
time = doc.time
|
4
|
+
typ = doc.typ
|
5
|
+
for v1, atts of doc
|
6
|
+
if v1[0] != "_" and !(v1 in ['typ','time'])
|
7
|
+
for v2, value of atts
|
8
|
+
emit "#{typ}/#{v1}/#{v2}", 1
|
9
|
+
|
10
|
+
# views count reduce
|
11
|
+
(keys, values, rereduce) ->
|
12
|
+
sum(values)
|
13
|
+
|
14
|
+
# views typ12 map
|
15
|
+
(doc) ->
|
16
|
+
time = doc.time
|
17
|
+
typ = doc.typ
|
18
|
+
for v1, atts of doc
|
19
|
+
if v1[0] != "_" and !(v1 in ['typ','time'])
|
20
|
+
for v2, value of atts
|
21
|
+
emit [typ,v1,v2], [time,value]
|
22
|
+
|
23
|
+
# lists iter
|
24
|
+
(head, req) ->
|
25
|
+
start
|
26
|
+
"headers":
|
27
|
+
"Content-Type": "text/html"
|
28
|
+
loop
|
29
|
+
return unless r = getRow()
|
30
|
+
send ",[#{r.value}]"
|
data/lib/tatami.rb
CHANGED
@@ -1,5 +1,5 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
module Tatami
|
4
|
-
# Your code goes here...
|
5
|
-
end
|
1
|
+
require_relative "tatami/version"
|
2
|
+
require_relative 'tatami/couch'
|
3
|
+
module Tatami
|
4
|
+
# Your code goes here...
|
5
|
+
end
|
data/lib/tatami/app.rb
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
class Tatami::Couch::App
|
2
|
+
|
3
|
+
def attachments file_path
|
4
|
+
Dir.chdir file_path do
|
5
|
+
Dir["**/*.*"].each_with_object({}) do |path,o|
|
6
|
+
ext = File.extname(path)[1..-1]
|
7
|
+
base64 = [IO.read(path)].pack('m')
|
8
|
+
o[path] = {
|
9
|
+
"content_type" => content_type(ext),
|
10
|
+
"data" => base64
|
11
|
+
}
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
def content_type ext
|
17
|
+
subtype = ext.sub(/^js$/, 'javascript')
|
18
|
+
"text/#{subtype}"
|
19
|
+
end
|
20
|
+
end
|
data/lib/tatami/couch.rb
ADDED
@@ -0,0 +1,322 @@
|
|
1
|
+
require 'faraday'
|
2
|
+
require 'json/ext'
|
3
|
+
require 'map'
|
4
|
+
module Tatami
|
5
|
+
class Couch
|
6
|
+
def conn
|
7
|
+
@conn ||= Faraday.new(:url => @url) do |faraday|
|
8
|
+
faraday.request :url_encoded # form-encode POST params
|
9
|
+
faraday.response :logger if @opts[:log]
|
10
|
+
faraday.adapter Faraday.default_adapter # make requests with Net::HTTP
|
11
|
+
end
|
12
|
+
end
|
13
|
+
# delete and recreate the database
|
14
|
+
def blank!
|
15
|
+
conn.delete "/#{db_env}"
|
16
|
+
conn.put "/#{db_env}"
|
17
|
+
self
|
18
|
+
end
|
19
|
+
|
20
|
+
attr_reader :db
|
21
|
+
def db_env()
|
22
|
+
@env ? "#{@db}_#{@env}" : @db
|
23
|
+
end
|
24
|
+
|
25
|
+
# @option opts [Boolean] :log
|
26
|
+
# @option opts [String] :db
|
27
|
+
def initialize db, url='http://localhost:5984', opts={}
|
28
|
+
@env = opts.delete(:env)
|
29
|
+
@db, @url, @opts = db, url, opts
|
30
|
+
end
|
31
|
+
|
32
|
+
def get path, opts={}
|
33
|
+
puts "GET #{path}" if opts[:log]
|
34
|
+
data = conn.get(path).body
|
35
|
+
cast_response data, opts[:format]
|
36
|
+
end
|
37
|
+
|
38
|
+
def cast_response data, format
|
39
|
+
case format
|
40
|
+
when :string
|
41
|
+
data
|
42
|
+
when :object
|
43
|
+
JSON.parse(data)
|
44
|
+
else
|
45
|
+
Map.new JSON.parse(data)
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
def get_db_string path
|
50
|
+
conn.get("/#{db_env}#{path}").body
|
51
|
+
end
|
52
|
+
|
53
|
+
def get_db path, opts={}
|
54
|
+
get "/#{db_env}#{path}", opts
|
55
|
+
end
|
56
|
+
|
57
|
+
def verb method, url, body, opts={}
|
58
|
+
puts "#{method.upcase} #{url} #{body.inspect}" if opts[:log]
|
59
|
+
resp = conn.send method, url, JSON.generate(body), 'Content-Type' => 'application/json'
|
60
|
+
cast_response resp.body, opts[:format]
|
61
|
+
end
|
62
|
+
|
63
|
+
def post_db url, body, opts={}
|
64
|
+
verb :post, "/#{db_env}#{url}", body, opts
|
65
|
+
end
|
66
|
+
|
67
|
+
def put_db(url, body) verb :put, "/#{db_env}#{url}", body end
|
68
|
+
|
69
|
+
def post_db_string url, body
|
70
|
+
conn.post("/#{db_env}#{url}", JSON.generate(body), 'Content-Type' => 'application/json').body
|
71
|
+
end
|
72
|
+
|
73
|
+
def version() get('/')[:version] end
|
74
|
+
def db_info() get_db "" end
|
75
|
+
#db_name: cars
|
76
|
+
#doc_count: 0
|
77
|
+
#doc_del_count: 0
|
78
|
+
#update_seq: 0
|
79
|
+
#purge_seq: 0
|
80
|
+
#compact_running: false
|
81
|
+
#disk_size: 79
|
82
|
+
#data_size: 0
|
83
|
+
#instance_start_time: '1364126614255010'
|
84
|
+
#disk_format_version: 6
|
85
|
+
#committed_update_seq: 0
|
86
|
+
|
87
|
+
def with_fixtures! names, *docs
|
88
|
+
blank!
|
89
|
+
load_design
|
90
|
+
documents = docs.map do |values|
|
91
|
+
Hash[ names.zip(values) ]
|
92
|
+
end
|
93
|
+
doc_new_bulk documents
|
94
|
+
end
|
95
|
+
|
96
|
+
def with_documents! *docs
|
97
|
+
blank!
|
98
|
+
load_design
|
99
|
+
doc_new_bulk docs
|
100
|
+
end
|
101
|
+
|
102
|
+
# PUT /db/doc
|
103
|
+
# @return [Doc]
|
104
|
+
# ok true
|
105
|
+
# id 0c8e32db863b87c9f6233d572854d6db
|
106
|
+
# rev 1-08c111cb2ccfbf117ed931bef444f730
|
107
|
+
def doc_new id, attributes
|
108
|
+
id ||= next_id
|
109
|
+
put_db "/#{id}", attributes
|
110
|
+
end
|
111
|
+
|
112
|
+
# @param [String] file_root
|
113
|
+
def doc_attach_app id, file_root
|
114
|
+
# curl -X PUT -v http://127.0.0.1:5984/cars/flot/index.html --data-binary "@test/fixtures/index.html" --header "Accept: text/html" --header "Transfer-Encoding: chunked" --header "Expect:"
|
115
|
+
require 'tatami/app'
|
116
|
+
put_db "/#{id}", "_attachments" => App.new.attachments(file_root)
|
117
|
+
end
|
118
|
+
|
119
|
+
def doc_create_or_update id, attributes
|
120
|
+
if old = doc(id)
|
121
|
+
doc_update old, attributes
|
122
|
+
else
|
123
|
+
doc_new id, attributes
|
124
|
+
end
|
125
|
+
end
|
126
|
+
|
127
|
+
# @return [String] autogenerated available id (caches 10)
|
128
|
+
def next_id
|
129
|
+
@next_ids ||= []
|
130
|
+
if @next_ids.empty?
|
131
|
+
@next_ids = get("_uuids?count=10")[:uuids]
|
132
|
+
end
|
133
|
+
@next_ids.pop
|
134
|
+
end
|
135
|
+
|
136
|
+
# _id 0c8e32db863b87c9f6233d5728545922
|
137
|
+
# _rev 1-08c111cb2ccfbf117ed931bef444f730
|
138
|
+
# error: not_found
|
139
|
+
# reason: missing
|
140
|
+
# name Frank
|
141
|
+
# GET /db/doc
|
142
|
+
def doc id
|
143
|
+
r = get_db "/#{id}"
|
144
|
+
r[:error] == "not_found" ? nil : r
|
145
|
+
end
|
146
|
+
|
147
|
+
# GET /db/_design/design-doc/_show/show-name
|
148
|
+
# GET /db/_design/design-doc/_show/show-name/doc
|
149
|
+
def show name, doc=nil, design=db_env
|
150
|
+
d = doc ? "/#{doc}" : ""
|
151
|
+
get_db_string "/_design/#{design}/_show/#{name}#{d}"
|
152
|
+
end
|
153
|
+
|
154
|
+
# @param [Hash] old
|
155
|
+
# @param [Hash] new
|
156
|
+
def doc_update old, new
|
157
|
+
attributes = new.merge(:_rev => old[:_rev])
|
158
|
+
doc_new old[:_id], attributes
|
159
|
+
end
|
160
|
+
|
161
|
+
def doc_merge_cached id, attributes
|
162
|
+
@cache ||= Map.new
|
163
|
+
@cache[id] ||= begin
|
164
|
+
doc(id) || Map.new
|
165
|
+
end
|
166
|
+
@cache.combine! id => attributes
|
167
|
+
r = doc_new id, @cache[id]
|
168
|
+
@cache[id][:_rev] = r[:rev]
|
169
|
+
end
|
170
|
+
|
171
|
+
# creates new type in _design/[db-name] document
|
172
|
+
# @param [Symbol] type :view, :list, :show
|
173
|
+
def design_new type, name, content
|
174
|
+
doc_merge_cached "_design/#{db_env}", :language => 'coffeescript',
|
175
|
+
"#{type}s" =>
|
176
|
+
{ name => content }
|
177
|
+
end
|
178
|
+
|
179
|
+
# views name map
|
180
|
+
# views name reduce
|
181
|
+
# lists name
|
182
|
+
def load_design pattern="*"
|
183
|
+
[ %w[erlang %% erl],
|
184
|
+
%w[coffeescript # coffee]
|
185
|
+
].each do |language, comment, ext |
|
186
|
+
Dir.glob("couches/#{@db}/#{pattern}.#{ext}").each do |path|
|
187
|
+
source = IO.read(path)
|
188
|
+
name = File.basename(path)[/[^\.]+/]
|
189
|
+
map = Map.new(language: language)
|
190
|
+
source.split(/^(?=#{comment} (?:views|lists) \S+)/m).each do |element|
|
191
|
+
element.split("\n").first =~ /#{comment} (\S+) (\S+) ?(map|reduce)?/
|
192
|
+
values = [$1,$2,$3].compact << element
|
193
|
+
map.set *values
|
194
|
+
end
|
195
|
+
doc_create_or_update "_design/#{name}", map
|
196
|
+
end
|
197
|
+
end
|
198
|
+
end
|
199
|
+
|
200
|
+
def doc_merge id, attributes
|
201
|
+
old = doc id
|
202
|
+
doc_new old[:_id], old.combine(attributes)
|
203
|
+
end
|
204
|
+
|
205
|
+
def doc_new_bulk documents
|
206
|
+
post_db "/_bulk_docs", {:docs => documents}
|
207
|
+
end
|
208
|
+
|
209
|
+
def bulk_import limit=100, &b
|
210
|
+
BulkImport.new self, limit, &b
|
211
|
+
end
|
212
|
+
|
213
|
+
class BulkImport
|
214
|
+
def initialize couch, limit
|
215
|
+
@couch, @limit, @cache = couch, limit, []
|
216
|
+
yield self
|
217
|
+
docs_new
|
218
|
+
end
|
219
|
+
|
220
|
+
def doc_new attributes
|
221
|
+
docs_new if @cache.size > @limit
|
222
|
+
@cache << attributes
|
223
|
+
end
|
224
|
+
|
225
|
+
def docs_new
|
226
|
+
@couch.doc_new_bulk @cache
|
227
|
+
@cache = []
|
228
|
+
end
|
229
|
+
end
|
230
|
+
|
231
|
+
# GET /db/_design/design-doc/_view/view-name
|
232
|
+
def view name
|
233
|
+
@views ||= Map.new
|
234
|
+
@views[name] ||= View.new self, name.to_s
|
235
|
+
end
|
236
|
+
|
237
|
+
class View
|
238
|
+
def initialize couch, name
|
239
|
+
@couch = couch
|
240
|
+
name.prepend "#{@couch.db}/" unless name['/']
|
241
|
+
@design, @view = name.split('/')
|
242
|
+
@base = "/_design/#{@design}/_view/#{@view}"
|
243
|
+
end
|
244
|
+
|
245
|
+
# @param [] value
|
246
|
+
# @return [String] json
|
247
|
+
def options_to_param opts
|
248
|
+
if single_key=opts.delete(:singlekey)
|
249
|
+
opts[:startkey] = single_key
|
250
|
+
opts[:endkey] = (single_key + [{}] )
|
251
|
+
end
|
252
|
+
opts.map{|option,value| "#{option}=#{ JSON.generate([value])[1..-2] }"}.join('&')
|
253
|
+
end
|
254
|
+
|
255
|
+
# {total_rows: 1
|
256
|
+
# offset: 0
|
257
|
+
# rows: [{id: 45d73462c193b4bd5b5e643b33010de0
|
258
|
+
# key: the_key
|
259
|
+
# value: the_value}, ...
|
260
|
+
# ]
|
261
|
+
|
262
|
+
# @option opts [Boolean] :format :string, :object, (:map)
|
263
|
+
# @option opts [Boolean] :log puts url
|
264
|
+
# @option opts [Array] :keys make a post request
|
265
|
+
# @option opts [Array] :single_key calculates startkey and endkey
|
266
|
+
# @return [Map]
|
267
|
+
def data opts={}
|
268
|
+
opts_add = {log: opts.delete(:log), format: opts.delete(:format) }
|
269
|
+
list = opts[:list] ? "_list/#{opts.delete(:list)}" : "_view"
|
270
|
+
if opts[:keys]
|
271
|
+
@couch.post_db "/_design/#{@design}/#{list}/#{@view}", opts, opts_add
|
272
|
+
else
|
273
|
+
@couch.get_db "/_design/#{@design}/#{list}/#{@view}?#{options_to_param opts}", opts_add
|
274
|
+
end
|
275
|
+
end
|
276
|
+
|
277
|
+
def rows opts={}
|
278
|
+
data(opts)[:rows]
|
279
|
+
end
|
280
|
+
|
281
|
+
# @param [String] map
|
282
|
+
# @p
|
283
|
+
# @return [View]
|
284
|
+
def create map, reduce=nil
|
285
|
+
v = {:map => map}
|
286
|
+
v[:reduce] = reduce if reduce
|
287
|
+
@couch.design_new :view, @view, v
|
288
|
+
self
|
289
|
+
end
|
290
|
+
|
291
|
+
def key_values opts={}
|
292
|
+
rows(opts).map{|r| [ r[:key],r[:value] ]}
|
293
|
+
end
|
294
|
+
|
295
|
+
def values opts={}
|
296
|
+
rows(opts).map{|d| d[:value]}
|
297
|
+
end
|
298
|
+
|
299
|
+
def keys opts={}
|
300
|
+
rows(opts).map{|d| d[:key]}
|
301
|
+
end
|
302
|
+
|
303
|
+
# @return value of the first row
|
304
|
+
def reduced opts={}
|
305
|
+
data(opts).get :rows, 0, :value
|
306
|
+
end
|
307
|
+
|
308
|
+
# @return [Map] rows as key => value
|
309
|
+
def reduce opts={}
|
310
|
+
rows(opts).each_with_object(Map.new)do |row,o|
|
311
|
+
o[row[:key]] = row[:value]
|
312
|
+
end
|
313
|
+
end
|
314
|
+
|
315
|
+
# special function for arrays as keys
|
316
|
+
def reduced_value *values
|
317
|
+
reduced :group_level => values.size, :startkey => values, :endkey => values.dup << {}
|
318
|
+
end
|
319
|
+
|
320
|
+
end
|
321
|
+
end
|
322
|
+
end
|