mattly-exegesis 0.0.6

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/README.rdoc ADDED
@@ -0,0 +1,23 @@
1
+ = exegesis
2
+
3
+ * http://github.com/mattly/exegesis
4
+
5
+ == DESCRIPTION:
6
+
7
+ An ODM (Object/Document Mapper) for Couchdb. Still very much a work in progress
8
+
9
+ == FEATURES/PROBLEMS:
10
+
11
+ * Encourages Per-"Account" databases. This is both a feature and a problem, since classes cannot know which database to pull from you cannot do f.e. "Article.find('foo')" as Article doesn't know what database to use.
12
+ * Does not provide it's own validators or callbacks. I have a solution in mind for this involving state machines.
13
+
14
+ == REQUIREMENTS:
15
+
16
+ * Johnson (and Spidermonkey, if you have CouchDB you have Spidermonkey)
17
+
18
+ For running the tests:
19
+
20
+ * Test::Unit (you got it)
21
+ * Context (http://github.com/jeremymcanally/context, can install from github gems)
22
+ * Matchy (http://github.com/jeremymcanally/matchy, github gem version out of date; clone, build & install for now)
23
+ * Zebra (http://github.com/giraffesoft/zerba, depends on jeremymcanally-matchy, which is out of date; clone, build & install for now)
data/VERSION.yml ADDED
@@ -0,0 +1,4 @@
1
+ ---
2
+ :major: 0
3
+ :minor: 0
4
+ :patch: 7
data/lib/exegesis.rb ADDED
@@ -0,0 +1,44 @@
1
+ require 'time'
2
+ require 'pathname'
3
+
4
+ require 'couchrest'
5
+ require 'active_support/inflector'
6
+
7
+ $:.unshift File.dirname(__FILE__) unless $:.include?(File.dirname(__FILE__)) || $:.include?(File.expand_path(File.dirname(__FILE__)))
8
+
9
+ module Exegesis
10
+ autoload :Document, 'exegesis/document'
11
+ autoload :Design, 'exegesis/design'
12
+
13
+ extend self
14
+
15
+ def designs_directory= dir
16
+ @designs_directory = Pathname.new(dir)
17
+ end
18
+
19
+ def designs_directory
20
+ @designs_directory ||= Pathname.new(ENV["PWD"])
21
+ @designs_directory
22
+ end
23
+
24
+ def design_file name
25
+ File.read(designs_directory + name)
26
+ end
27
+
28
+ def database_template= template
29
+ @db_template = template
30
+ end
31
+
32
+ def database_template
33
+ @db_template ||= "http://localhost:5984/%s"
34
+ end
35
+
36
+ def database_for name
37
+ database_template % name
38
+ end
39
+
40
+ def document_classes
41
+ @document_classes ||= Hash.new(Exegesis::Document)
42
+ end
43
+
44
+ end
@@ -0,0 +1,84 @@
1
+ $:.unshift File.dirname(__FILE__)
2
+ require 'design/design_docs'
3
+
4
+ module Exegesis
5
+ class Design
6
+ include Exegesis::Design::DesignDocs
7
+
8
+ attr_accessor :database
9
+
10
+ def initialize(db)
11
+ @database = db
12
+ end
13
+
14
+ def self.use_design_doc_name name
15
+ @design_doc_name = name.to_s
16
+ end
17
+
18
+ def self.design_doc_name
19
+ @design_doc_name ||= name.to_s.sub(/(Design)$/,'').downcase
20
+ end
21
+
22
+ def design_doc_name
23
+ self.class.design_doc_name
24
+ end
25
+
26
+ def get(id)
27
+ doc = Exegesis::Document.instantiate database.get(id)
28
+ doc.database = self.database
29
+ doc
30
+ end
31
+
32
+ def parse_opts(opts={})
33
+ if opts[:key]
34
+ case opts[:key]
35
+ when Range
36
+ range = opts.delete(:key)
37
+ opts.update({:startkey => range.first, :endkey => range.last})
38
+ when Array
39
+ if opts[:key].any?{|v| v.kind_of?(Range) }
40
+ key = opts.delete(:key)
41
+ opts[:startkey] = key.map {|v| v.kind_of?(Range) ? v.first : v }
42
+ opts[:endkey] = key.map {|v| v.kind_of?(Range) ? v.last : v }
43
+ end
44
+ end
45
+ elsif opts[:keys] && opts[:keys].empty?
46
+ opts.delete(:keys)
47
+ end
48
+
49
+ opts
50
+ end
51
+
52
+ def view view_name, opts={}
53
+ opts = parse_opts opts
54
+ return [] unless opts[:key] || opts[:startkey] || opts[:keys] || opts[:all]
55
+ opts.delete(:all)
56
+ database.view("#{design_doc_name}/#{view_name}", opts)['rows']
57
+ end
58
+
59
+ def docs_for view_name, opts={}
60
+ response = view view_name, opts.update({:include_docs => true})
61
+ response.map do |doc|
62
+ model = Exegesis::Document.instantiate doc['doc']
63
+ model.database = database
64
+ model
65
+ end
66
+ end
67
+
68
+ def values_for view_name, opts={}
69
+ response = view view_name, opts
70
+ response.map {|row| row['value'] }
71
+ end
72
+
73
+ def keys_for view_name, opts={}
74
+ response = view view_name, opts
75
+ response.map {|row| row['key'] }
76
+ end
77
+
78
+ def ids_for view_name, opts={}
79
+ response = view view_name, opts
80
+ response.map {|row| row['id'] }
81
+ end
82
+
83
+ end
84
+ end
@@ -0,0 +1,92 @@
1
+ require 'johnson'
2
+ require 'digest/md5'
3
+
4
+ module Exegesis
5
+ class Design
6
+ module DesignDocs
7
+
8
+ def self.included(base)
9
+ base.extend ClassMethods
10
+ end
11
+
12
+ module ClassMethods
13
+ def designs_directory dir=nil
14
+ if dir
15
+ @designs_directory = Pathname.new(dir)
16
+ else
17
+ @designs_directory || Exegesis.designs_directory
18
+ end
19
+ end
20
+
21
+ def design_doc_path
22
+ designs_directory + "#{design_doc_name}.js"
23
+ end
24
+
25
+ def design_doc
26
+ js_doc = Johnson.evaluate("v = #{File.read(design_doc_path)}");
27
+ views = js_doc['views'].entries.inject({}) do |memo, (name, mapreduce)|
28
+ memo[name] = mapreduce.entries.inject({}) do |view, (role, func)|
29
+ view.update role => func.toString
30
+ end
31
+ memo
32
+ end
33
+ composite_views = declared_views.dup.update(views)
34
+ { '_id' => "_design/#{design_doc_name}",
35
+ 'language' => 'javascript',
36
+ 'views' => composite_views
37
+ }
38
+ end
39
+
40
+ def declared_views
41
+ @declared_views ||= {}
42
+ end
43
+
44
+ def view_by *keys
45
+ view_name = "by_#{keys.join('_and_')}"
46
+ doc_keys = keys.map {|k| "doc['#{k}']" }
47
+ declared_views[view_name] = {
48
+ 'map' => %|function(doc) {
49
+ if (doc['.kind'] == '#{name.sub(/Design$/,'')}' && #{doc_keys.join(' && ')}) {
50
+ emit(#{keys.length == 1 ? doc_keys.first : "[#{doc_keys.join(', ')}]" }, null);
51
+ }
52
+ }|
53
+ }
54
+ define_method view_name do |*args|
55
+ docs_for view_name, *args
56
+ end
57
+ end
58
+
59
+ def design_doc_hash
60
+ hash_for_design design_doc
61
+ end
62
+
63
+ def hash_for_design design
64
+ funcs = design['views'].map do |name, view|
65
+ "//view/#{name}/#{view['map']}/#{view['reduce']}"
66
+ end
67
+ Digest::MD5.hexdigest(funcs.sort.join)
68
+ end
69
+
70
+ end
71
+
72
+ def design_doc_hash
73
+ design_doc.nil? ? '' : self.class.hash_for_design(design_doc)
74
+ end
75
+
76
+ def design_doc reload=false
77
+ @design_doc = nil if reload
78
+ @design_doc ||= database.get "_design/#{design_doc_name}" rescue nil
79
+ end
80
+
81
+ def push_design!
82
+ return if design_doc_hash == self.class.design_doc_hash
83
+ if design_doc
84
+ design_doc.update(self.class.design_doc)
85
+ design_doc.save
86
+ else
87
+ database.save_doc(self.class.design_doc)
88
+ end
89
+ end
90
+ end
91
+ end
92
+ end
@@ -0,0 +1,149 @@
1
+ module Exegesis
2
+ class Document < CouchRest::Document
3
+
4
+ def self.inherited subklass
5
+ Exegesis.document_classes[subklass.name] = subklass
6
+ end
7
+
8
+ def self.instantiate hash={}
9
+ Exegesis.document_classes[hash['.kind']].new(hash)
10
+ end
11
+
12
+ def self.expose *attrs
13
+ opts = if attrs.last.is_a?(Hash)
14
+ attrs.pop
15
+ else
16
+ {}
17
+ end
18
+
19
+ [attrs].flatten.each do |attrib|
20
+ attrib = "#{attrib}"
21
+ if opts.has_key?(:writer)
22
+ if opts[:writer]
23
+ define_method("#{attrib}=") {|val| self[attrib] = opts[:writer].call(val) }
24
+ end
25
+ else
26
+ define_method("#{attrib}=") {|val| self[attrib] = val }
27
+ end
28
+ if opts[:as]
29
+ if opts[:as] == :reference
30
+ define_method(attrib) do |*reload|
31
+ reload = false if reload.empty?
32
+ instance_variable_set("@#{attrib}", nil) if reload
33
+ return instance_variable_get("@#{attrib}") if instance_variable_get("@#{attrib}")
34
+ instance_variable_set("@#{attrib}", load_reference(self[attrib]))
35
+ end
36
+ else
37
+ define_method(attrib) do
38
+ self[attrib] = if self[attrib].is_a?(Array)
39
+ self[attrib].map {|val| cast opts[:as], val }.compact
40
+ else
41
+ cast opts[:as], self[attrib]
42
+ end
43
+ end
44
+ end
45
+ else
46
+ define_method(attrib) { self[attrib] }
47
+ end
48
+ end
49
+ end
50
+
51
+ def self.default hash=nil
52
+ if hash
53
+ @default = hash
54
+ else
55
+ @default ||= superclass.respond_to?(:default) ? superclass.default : {}
56
+ end
57
+ end
58
+
59
+ def self.timestamps!
60
+ define_method :set_timestamps do
61
+ self['updated_at'] = Time.now
62
+ self['created_at'] ||= Time.now
63
+ end
64
+ expose 'updated_at', :as => Time, :writer => false
65
+ expose 'created_at', :as => Time, :writer => false
66
+ end
67
+
68
+ def self.unique_id meth
69
+ define_method :set_unique_id do
70
+ self['_id'] = self.send(meth)
71
+ end
72
+ end
73
+
74
+ alias :_rev :rev
75
+ alias_method :document_save, :save
76
+
77
+ def save
78
+ set_timestamps if respond_to?(:set_timestamps)
79
+ if respond_to?(:set_unique_id) && id.nil?
80
+ @unique_id_attempt = 0
81
+ begin
82
+ self['_id'] = set_unique_id
83
+ document_save
84
+ rescue RestClient::RequestFailed => e
85
+ @unique_id_attempt += 1
86
+ retry
87
+ end
88
+ else
89
+ document_save
90
+ end
91
+ end
92
+
93
+ def initialize keys={}
94
+ apply_default
95
+ super keys
96
+ self['.kind'] ||= self.class.to_s
97
+ end
98
+
99
+ def update_attributes attrs={}
100
+ raise ArgumentError, 'must include a matching _rev attribute' unless rev == attrs.delete('_rev')
101
+ attrs.each_pair do |key, value|
102
+ self.send("#{key}=", value) rescue nil
103
+ attrs.delete(key)
104
+ end
105
+ save
106
+ end
107
+
108
+ private
109
+
110
+ def apply_default
111
+ self.class.default.each do |key, value|
112
+ self[key] = value
113
+ end
114
+ end
115
+
116
+ def cast as, value
117
+ return nil if value.nil?
118
+ klass = if as == :given
119
+ if value.is_a?(Hash)
120
+ Exegesis.document_classes[value['.kind']]
121
+ else
122
+ nil #nfi what do to in this case; maybe just have it be a doc hash?
123
+ end
124
+ elsif as.is_a?(Class)
125
+ as
126
+ else
127
+ Exegesis.document_classes[nil] # whatever the default is? Hell idk.
128
+ end
129
+
130
+ with = klass == Time ? :parse : :new
131
+ casted = klass.send with, value
132
+ casted
133
+ end
134
+
135
+ def load_reference ids
136
+ raise ArgumentError, "a database is required for loading a reference" unless database
137
+ if ids.is_a?(Array)
138
+ ids.map {|val| Exegesis::Document.instantiate(database.get(val)) }
139
+ else
140
+ Exegesis::Document.instantiate(database.get(ids))
141
+ end
142
+ end
143
+
144
+ end
145
+ end
146
+
147
+ # $:.unshift File.dirname(__FILE__)
148
+ # require 'document/annotated_reference'
149
+ # require 'document/referencing'
@@ -0,0 +1,120 @@
1
+ require File.join(File.dirname(__FILE__), 'test_helper.rb')
2
+
3
+ class FoosDesign < Exegesis::Design
4
+ view_by :foo
5
+ view_by :foo, :bar
6
+ end
7
+ class CustomDesignDirDesign < Exegesis::Design
8
+ designs_directory File.join(File.dirname(__FILE__), 'fixtures')
9
+ end
10
+
11
+
12
+ class ComposingDesignDocTest < Test::Unit::TestCase
13
+ before(:all) { Exegesis.designs_directory = fixtures_path('designs') }
14
+
15
+ context "setting a custom designs directory" do
16
+ before do
17
+ @custom_design_dir = File.join(File.dirname(__FILE__), 'fixtures')
18
+ end
19
+
20
+ expect { CustomDesignDirDesign.designs_directory.to_s.will == @custom_design_dir }
21
+
22
+ end
23
+
24
+ context "composing design docs from local sources" do
25
+ before do
26
+ @design = FoosDesign.design_doc
27
+ @file = File.read(fixtures_path('designs/foos.js'))
28
+ @jsdoc = Johnson.evaluate("v=#{@file}")
29
+ end
30
+
31
+ expect { @design.has_key?('_id').will be(true) }
32
+ expect { @design['_id'].will == '_design/foos' }
33
+ expect { @design['views']['by_bar']['map'].will == @jsdoc['views']['by_bar']['map'].toString }
34
+ end
35
+
36
+ context "composing a design doc from view_by declarations" do
37
+ before do
38
+ @design = FoosDesign.design_doc
39
+ @by_foo = @design['views']['by_foo']['map']
40
+ @by_foo_and_bar = @design['views']['by_foo_and_bar']['map']
41
+ end
42
+
43
+ expect { @by_foo.will include("if (doc['.kind'] == 'Foos' && doc['foo'])") }
44
+ expect { @by_foo.will include("emit(doc['foo'], null);") }
45
+
46
+ expect { @by_foo_and_bar.will include("if (doc['.kind'] == 'Foos' && doc['foo'] && doc['bar'])") }
47
+ expect { @by_foo_and_bar.will include("emit([doc['foo'], doc['bar']], null)") }
48
+ end
49
+
50
+ context "building a hash a design doc" do
51
+ before do
52
+ @design = {
53
+ 'views' => {
54
+ 'a' => {'map' => 'some value', 'reduce' => 'some value'},
55
+ 'b' => {'map' => 'some value'}
56
+ }
57
+ }
58
+ funcs = @design['views'].map{|name, view| "//view/#{name}/#{view['map']}/#{view['reduce']}" }.sort
59
+ @hashed = Digest::MD5.hexdigest(funcs.join)
60
+ end
61
+ expect { FoosDesign.hash_for_design(@design).will == @hashed }
62
+ end
63
+
64
+ context "syncronising with a databae" do
65
+ before do
66
+ reset_db
67
+ end
68
+
69
+ context "pushing design doc when it doesn't exist yet" do
70
+ before do
71
+ foo = FoosDesign.new(@db)
72
+ foo.push_design!
73
+ @get_design = lambda { @db.get('_design/foos') }
74
+ @design = @get_design.call rescue nil
75
+ end
76
+
77
+ expect { @get_design.wont raise_error }
78
+ expect { @design['_rev'].will =~ /^[0-9]+$/ }
79
+ expect { @design['language'].will == 'javascript' }
80
+ expect { @design['views'].has_key?('by_bar').will be(true) }
81
+ end
82
+
83
+ context "reading the existing design document" do
84
+ before do
85
+ @db.save_doc(FoosDesign.design_doc)
86
+ @foo = FoosDesign.new(@db)
87
+ end
88
+
89
+ expect { lambda {@foo.design_doc}.wont raise_error }
90
+ expect { @foo.design_doc['_rev'].will =~ /^[0-9]+$/ }
91
+ expect { @foo.design_doc['views'].keys.will == FoosDesign.design_doc['views'].keys }
92
+ end
93
+
94
+ context "updating an existing doc" do
95
+
96
+ context "when it hasn't changed" do
97
+ before do
98
+ @db.save_doc(FoosDesign.design_doc)
99
+ @foo = FoosDesign.new(@db)
100
+ end
101
+
102
+ expect { lambda { @foo.push_design! }.wont change { @db.get('_design/foos')['_rev'] } }
103
+ end
104
+
105
+ context "when it has chagned" do
106
+ before do
107
+ @db.save_doc({'_id' => '_design/foos',
108
+ 'views' => {'a' => {'map' => 'function(doc) { emit(true, null)}'}}
109
+ })
110
+ @foo = FoosDesign.new(@db)
111
+ @pushing = lambda{ @foo.push_design! }
112
+ end
113
+
114
+ expect { @pushing.will change { @db.get('_design/foos')['_rev'] } }
115
+ expect { @pushing.call; @foo.design_doc_hash.will == FoosDesign.design_doc_hash }
116
+ end
117
+ end
118
+
119
+ end
120
+ end
@@ -0,0 +1,113 @@
1
+ require File.join(File.dirname(__FILE__), 'test_helper.rb')
2
+
3
+ class FoosDesign < Exegesis::Design; end
4
+ class BarsDesign < Exegesis::Design
5
+ use_design_doc_name :something_else
6
+ end
7
+
8
+ class TestForDesign < Exegesis::Document; end
9
+
10
+ class ExegesisDesignTest < Test::Unit::TestCase
11
+
12
+ before do
13
+ reset_db
14
+ @doc = FoosDesign.new(@db)
15
+ end
16
+
17
+ expect { @doc.database.will == @db }
18
+ expect { @doc.design_doc_name.will == "foos" }
19
+
20
+ expect { FoosDesign.design_doc_name.will == "foos" }
21
+ expect { BarsDesign.design_doc_name.will == "something_else" }
22
+
23
+ context "retrieving documents with #get" do
24
+ before do
25
+ @db.save_doc '_id' => 'foo', 'foo' => 'bar', '.kind' => 'TestForDesign'
26
+ @obj = @doc.get('foo')
27
+ end
28
+
29
+ expect { @obj.will be_kind_of(TestForDesign) }
30
+ expect { @obj['foo'].will == 'bar' }
31
+ end
32
+
33
+ context "retreiving views" do
34
+ before do
35
+ @raw_docs = [
36
+ {'_id' => 'bar', 'foo' => 'bar', 'bar' => 'bar', '.kind' => 'TestForDesign'},
37
+ {'_id' => 'baz', 'foo' => 'baz', 'bar' => 'baz', '.kind' => 'TestForDesign'},
38
+ {'_id' => 'foo', 'foo' => 'foo', 'bar' => 'foo', '.kind' => 'TestForDesign'}
39
+ ]
40
+ @db.bulk_save @raw_docs
41
+ @db.save_doc({
42
+ '_id' => '_design/foos',
43
+ 'views' => {
44
+ 'test' => { 'map'=>'function(doc) {emit(doc.foo, doc.bar);}' },
45
+ }
46
+ })
47
+ end
48
+
49
+ context "parsing options" do
50
+ context "when the key is a range" do
51
+ before { @opts = @doc.parse_opts(:key => 'bar'..'baz') }
52
+
53
+ expect { @opts[:key].will == nil }
54
+ expect { @opts[:startkey].will == 'bar' }
55
+ expect { @opts[:endkey].will == 'baz' }
56
+ end
57
+
58
+ context "when the key is an array with a range in it" do
59
+ before { @opts = @doc.parse_opts(:key => ['published', '2008'..'2008/13']) }
60
+
61
+ expect { @opts[:key].will be(nil) }
62
+ expect { @opts[:startkey].will == ['published', '2008'] }
63
+ expect { @opts[:endkey].will == ['published', '2008/13'] }
64
+ end
65
+
66
+ context "when a keys option is empty" do
67
+ before { @opts = @doc.parse_opts(:keys => []) }
68
+
69
+ expect { @opts[:keys].will be(nil) }
70
+ end
71
+ end
72
+
73
+ context "when no key, keys, startkey or all option is present" do
74
+ before { @response = @doc.view :test }
75
+
76
+ expect { @response.will == [] }
77
+ end
78
+
79
+ context "with an all key" do
80
+ before { @response = @doc.view :test, :all => true }
81
+
82
+ expect { @response.will == @raw_docs.map{|d| {'id' => d['_id'], 'key' => d['foo'], 'value' => d['bar']} } }
83
+ end
84
+
85
+ context "with docs" do
86
+ before { @response = @doc.docs_for :test, :key => 'foo' }
87
+
88
+ expect { @response.will be_kind_of(Array) }
89
+ expect { @response.size.will == 1 }
90
+ expect { @response.first.will be_kind_of(TestForDesign) }
91
+ expect { @response.first['foo'].will == 'foo' }
92
+ end
93
+
94
+ context "for the view's data" do
95
+ before { @response = @doc.values_for :test, :all => true }
96
+
97
+ expect { @response.will == %w(bar baz foo) }
98
+ end
99
+
100
+ context "for the view's matching keys" do
101
+ before { @response = @doc.keys_for :test, :key => 'bar'..'baz' }
102
+
103
+ expect { @response.will == %w(bar baz) }
104
+ end
105
+
106
+ context "for the view's matching ids" do
107
+ before { @response = @doc.ids_for :test, :key => 'bar'..'foo'}
108
+
109
+ expect { @response.will == %w(bar baz foo) }
110
+ end
111
+ end
112
+
113
+ end
@@ -0,0 +1,267 @@
1
+ require File.join(File.dirname(__FILE__), 'test_helper.rb')
2
+
3
+ class Foo < Exegesis::Document; end
4
+ class Bar < Exegesis::Document; end
5
+
6
+ class WithDefault < Exegesis::Document
7
+ default :foo => 'bar'
8
+ end
9
+
10
+ class Exposer < Exegesis::Document
11
+ expose :foo, :bar
12
+ expose :read_only, :writer => false
13
+ expose :custom_writer, :writer => lambda {|val| {'value' => val} }
14
+ expose :castee, :castees, :as => :given
15
+ expose :time, :times, :as => Time
16
+ expose :regex, :regexen, :as => Regexp
17
+ expose :other_doc, :other_docs, :as => :reference
18
+ end
19
+
20
+ class Timestamper < Exegesis::Document
21
+ timestamps!
22
+ end
23
+
24
+ class UniqueSnowflake < Exegesis::Document
25
+ unique_id :set_id
26
+ def set_id
27
+ @unique_id_attempt.zero? ? "snowflake" : "snowflake-#{@unique_id_attempt}"
28
+ end
29
+ end
30
+
31
+ class ExegesisDocumentClassDefinitionsTest < Test::Unit::TestCase
32
+
33
+ context "a bare Exegesis::Document" do
34
+ before do
35
+ reset_db
36
+ @obj = Foo.new
37
+ @obj.database = @db
38
+ @obj.save
39
+ end
40
+
41
+ expect { @obj['.kind'].will == "Foo" }
42
+ expect { @obj['foo'].will == nil }
43
+ expect { @obj.will_not respond_to(:foo) }
44
+ expect { @obj['created_at'].will == nil }
45
+ expect { @obj['updated_at'].will == nil }
46
+ end
47
+
48
+ context "instantiating" do
49
+ expect { Exegesis::Document.instantiate({'.kind' => 'Foo'}).will be_kind_of(Foo) }
50
+
51
+ context "transitions" do
52
+ before do
53
+ @foo = Foo.new
54
+ @foo['.kind'] = 'Bar'
55
+ @bar = Exegesis::Document.instantiate(@foo)
56
+ end
57
+
58
+ expect { @bar.will be_kind_of(Bar) }
59
+ end
60
+ end
61
+
62
+ context "default objects" do
63
+ expect { WithDefault.new['foo'].will == 'bar' }
64
+ expect { WithDefault.new({'foo' => 'baz'})['foo'].will == 'baz' }
65
+ end
66
+
67
+ context "exposing keys" do
68
+ context "regular declarations" do
69
+ before do
70
+ @obj = Exposer.new(:foo => 'bar', :bar => 'foo')
71
+ @writing = lambda { @obj.bar = "bee" }
72
+ end
73
+ expect { @obj.foo.will == 'bar' }
74
+ expect { @obj.bar.will == 'foo' }
75
+ expect { @writing.wont raise_error }
76
+ expect { @writing.call; @obj.bar.will == "bee" }
77
+ end
78
+
79
+ context "with a custom writer" do
80
+ context "when false" do
81
+ before { @obj = Exposer.new(:read_only => 'value') }
82
+ expect { @obj.read_only.will == 'value' }
83
+ expect { lambda{@obj.read_only = "other value"}.will raise_error(NoMethodError) }
84
+ end
85
+
86
+ context "when lambda" do
87
+ before do
88
+ @obj = Exposer.new(:custom_writer => 'value')
89
+ @obj.custom_writer = 'other value'
90
+ @expected = {'value' => 'other value'}
91
+ end
92
+ expect { @obj.custom_writer.will == @expected }
93
+ end
94
+ end
95
+
96
+ context "when casting a value" do
97
+ context "when as given" do
98
+ before do
99
+ @obj = Exposer.new({
100
+ :castee => {'foo' => 'foo', '.kind' => 'Foo'},
101
+ :castees => [{'foo' => 'foo', '.kind' => 'Foo'}, {'foo' => 'bar', '.kind' => 'Bar'}]
102
+ })
103
+ end
104
+
105
+ expect { @obj.castee.class.will == Foo }
106
+ expect { @obj.castee['foo'].will == 'foo' }
107
+
108
+ expect { @obj.castees.class.will == Array }
109
+ expect { @obj.castees[0].class.will == Foo }
110
+ expect { @obj.castees[0]['foo'].will == 'foo' }
111
+ expect { @obj.castees[1].class.will == Bar }
112
+ expect { @obj.castees[1]['foo'].will == 'bar' }
113
+ end
114
+
115
+ context "when as time" do
116
+ before do
117
+ @obj = Exposer.new({:time => Time.now.to_json, :times => [Time.local(2009,3,1).to_json, Time.local(2009,2,1).to_json]})
118
+ end
119
+
120
+ expect { @obj.time.class.will == Time }
121
+ expect { @obj.time.to_f.will be_close(Time.now.to_f, 1) }
122
+
123
+ expect { @obj.times.class.will == Array }
124
+ expect { @obj.times[0].class.will == Time }
125
+ expect { @obj.times[0].will == Time.local(2009,3,1) }
126
+ expect { @obj.times[1].class.will == Time }
127
+ expect { @obj.times[1].will == Time.local(2009,2,1) }
128
+ end
129
+
130
+ context "when as non document class" do
131
+ before do
132
+ @obj = Exposer.new({
133
+ :regex => 'foo',
134
+ :regexen => ['foo', 'bar']
135
+ })
136
+ end
137
+
138
+ expect { @obj.regex.will == /foo/ }
139
+
140
+ expect { @obj.regexen.class.will == Array }
141
+ expect { @obj.regexen[0].will == /foo/ }
142
+ expect { @obj.regexen[1].will == /bar/ }
143
+ end
144
+
145
+ context "when as reference" do
146
+ context "with a database present" do
147
+ before do
148
+ reset_db
149
+ @obj = Exposer.new(:other_doc => "other_doc", :other_docs => ["other_docs_1", "other_docs_2"])
150
+ @obj.database = @db
151
+ end
152
+ context "when the document exists" do
153
+ before do
154
+ @db.bulk_save([
155
+ {'.kind' => 'Foo', '_id' => 'other_doc'},
156
+ {'.kind' => 'Foo', '_id' => 'other_docs_1'},
157
+ {'.kind' => 'Foo', '_id' => 'other_docs_2'}
158
+ ])
159
+ end
160
+
161
+ expect { @obj.other_doc.rev.will == @db.get('other_doc')['_rev'] }
162
+ expect { @obj.other_doc.class.will == Foo }
163
+ expect { @obj.other_docs.class.will == Array }
164
+ expect { @obj.other_docs[0].rev.will == @db.get('other_docs_1')['_rev'] }
165
+ expect { @obj.other_docs[0].class.will == Foo }
166
+ expect { @obj.other_docs[1].rev.will == @db.get('other_docs_2')['_rev'] }
167
+ expect { @obj.other_docs[1].class.will == Foo }
168
+
169
+ context "caching" do
170
+ before do
171
+ @obj.other_doc
172
+ doc = @db.get('other_doc')
173
+ doc['foo'] = "updated"
174
+ doc.save
175
+ end
176
+
177
+ expect { @obj.other_doc['foo'].will be(nil) }
178
+ expect { @obj.other_doc(true)['foo'].will == 'updated' }
179
+ end
180
+ end
181
+
182
+ context "when the document is missing" do
183
+ expect { lambda{@obj.other_doc}.will raise_error(RestClient::ResourceNotFound) }
184
+ expect { lambda{@obj.other_docs}.will raise_error(RestClient::ResourceNotFound) }
185
+ end
186
+ end
187
+
188
+ context "without a database present" do
189
+ before { @obj = Exposer.new(:other_doc => "some_doc_id") }
190
+ expect { lambda{@obj.other_doc}.will raise_error(ArgumentError) }
191
+ end
192
+ end
193
+
194
+ context "when the value is nil" do
195
+ before do
196
+ @obj = Exposer.new({:castee => nil, :castees => nil, :regexen => ['foo', nil]})
197
+ end
198
+ expect { @obj.castee.will be(nil) }
199
+ expect { @obj.castees.will be(nil) }
200
+ expect { @obj.regexen.will == [/foo/] }
201
+ end
202
+ end
203
+ end
204
+
205
+ context "with timestamps" do
206
+ before do
207
+ reset_db
208
+ @obj = Timestamper.new
209
+ @obj.database = @db
210
+ @obj.save
211
+ @obj = Timestamper.new(@db.get(@obj.id))
212
+ end
213
+
214
+ context "initial save" do
215
+ expect { @obj.created_at.to_f.will be_close(Time.now.to_f, 1) }
216
+ expect { @obj.updated_at.to_f.will be_close(Time.now.to_f, 1) }
217
+ end
218
+
219
+ context "when created_at already exists" do
220
+ before do
221
+ @obj.database = @db
222
+ @obj['created_at'] = Time.now
223
+ @obj.save
224
+ @obj = Timestamper.new(@db.get(@obj.id))
225
+ end
226
+
227
+ expect { @obj.created_at.to_f.will be_close(Time.now.to_f, 1) }
228
+ expect { @obj.updated_at.to_f.will be_close(Time.now.to_f, 1) }
229
+ end
230
+
231
+ end
232
+
233
+ context "with a custom unique_id setter" do
234
+ before do
235
+ reset_db
236
+ @obj = UniqueSnowflake.new
237
+ @obj.database = @db
238
+ end
239
+
240
+ context "when the id isn't in use yet" do
241
+ before do
242
+ @obj.save
243
+ end
244
+
245
+ expect { @obj.id.will == "snowflake" }
246
+ end
247
+
248
+ context "when there is an id in place already" do
249
+ before do
250
+ @obj['_id'] = 'foo'
251
+ @obj.save
252
+ end
253
+
254
+ expect { @obj.id.will == "foo" }
255
+ end
256
+
257
+ context "when the desired id is already in use" do
258
+ before do
259
+ @db.save_doc({'_id' => 'snowflake', 'foo' => 'bar'})
260
+ @obj.save
261
+ end
262
+
263
+ expect { @obj.id.will == 'snowflake-1' }
264
+ end
265
+ end
266
+
267
+ end
@@ -0,0 +1,40 @@
1
+ require File.join(File.dirname(__FILE__), 'test_helper.rb')
2
+
3
+ class DocInstance < Exegesis::Document
4
+ expose :foo
5
+ end
6
+
7
+ class ExegesisDocumentInstanceMethodsTest < Test::Unit::TestCase
8
+
9
+ context "update_attributes" do
10
+ before do
11
+ reset_db
12
+ end
13
+
14
+ context "an existing doc" do
15
+ before do
16
+ @doc = DocInstance.new({'foo' => 'bar'})
17
+ @doc.database = @db
18
+ @doc.save
19
+ @old_rev = @doc.rev
20
+ end
21
+
22
+ context "without a matching rev" do
23
+ expect { lambda {@doc.update_attributes({'foo' => 'bee'})}.will raise_error(ArgumentError) }
24
+ expect { lambda {@doc.update_attributes({'foo' => 'bee', '_rev' => 'z'})}.will raise_error(ArgumentError) }
25
+ end
26
+
27
+ context "with a matching rev" do
28
+ before do
29
+ @doc.update_attributes({'_rev' => @doc.rev, 'foo' => 'bee', 'bar' => 'boo'})
30
+ end
31
+
32
+ expect { @doc['foo'].will == 'bee' }
33
+ expect { @doc['bar'].will be(nil) }
34
+ expect { @doc.rev.wont == @old_rev }
35
+ end
36
+ end
37
+
38
+ end
39
+
40
+ end
@@ -0,0 +1,28 @@
1
+ require File.join(File.dirname(__FILE__), 'test_helper.rb')
2
+
3
+ class ExegesisTest < Test::Unit::TestCase
4
+
5
+ context "designs directory" do
6
+ context "setting custom" do
7
+ before do
8
+ @custom_design_dir = File.join(File.dirname(__FILE__), 'fixtures', 'designs')
9
+ Exegesis.designs_directory = @custom_design_dir
10
+ end
11
+
12
+ expect { Exegesis.designs_directory.to_s.will == @custom_design_dir }
13
+ end
14
+
15
+
16
+ end
17
+
18
+ context "database template" do
19
+ before do
20
+ @template_string = "http://localhost:5984/appname-%s"
21
+ @account = "foo"
22
+ Exegesis.database_template = @template_string
23
+ end
24
+
25
+ expect { Exegesis.database_for(@account).will == @template_string % @account }
26
+ end
27
+
28
+ end
@@ -0,0 +1,12 @@
1
+ {
2
+ views: {
3
+ by_bar: {
4
+ map: function(doc) {
5
+ emit(doc.bar, null)
6
+ },
7
+ reduce: function(keys, values, rereduce) {
8
+ return(sum(values))
9
+ }
10
+ },
11
+ }
12
+ }
@@ -0,0 +1,39 @@
1
+ require 'rubygems'
2
+ require 'test/unit'
3
+
4
+ require 'context' # with github: gem install jeremymcanally-context
5
+ require 'matchy' # best bet for now: clone from github and build yourself; when jeremy fixes matchy, jeremymcanally-matchy
6
+ require 'zebra' # until jeremy updates matchy, download and build yourself, after that, with github: gem install giraffesoft-zebra
7
+
8
+ begin
9
+ require 'ruby-debug'
10
+ Debugger.start
11
+ rescue
12
+ puts "no ruby-debug installed? REAlLY? ok, if that's how you roll..."
13
+ end
14
+
15
+ $LOAD_PATH.unshift(File.dirname(__FILE__))
16
+ require 'lib/exegesis'
17
+
18
+ class Test::Unit::TestCase
19
+
20
+ def fixtures_path fixtures
21
+ File.join(File.dirname(__FILE__), 'fixtures', fixtures)
22
+ end
23
+
24
+ # todo: extract to some helper methods to include ala RR, etc
25
+ def reset_db(name=nil)
26
+ @db = CouchRest.database db(name) rescue nil
27
+ @db.delete! rescue nil
28
+ @db = CouchRest.database! db(name)
29
+ end
30
+
31
+ def teardown_db
32
+ @db.delete! rescue nil
33
+ end
34
+
35
+ def db(name)
36
+ "http://localhost:5984/exegesis-test#{name.nil? ? '' : "-#{name}"}"
37
+ end
38
+
39
+ end
metadata ADDED
@@ -0,0 +1,79 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: mattly-exegesis
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.6
5
+ platform: ruby
6
+ authors:
7
+ - Matt Lyon
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+
12
+ date: 2009-03-01 00:00:00 -08:00
13
+ default_executable:
14
+ dependencies:
15
+ - !ruby/object:Gem::Dependency
16
+ name: jchris-couchrest
17
+ type: :runtime
18
+ version_requirement:
19
+ version_requirements: !ruby/object:Gem::Requirement
20
+ requirements:
21
+ - - "="
22
+ - !ruby/object:Gem::Version
23
+ version: 0.12.6
24
+ version:
25
+ description: TODO
26
+ email: matt@flowerpowered.com
27
+ executables: []
28
+
29
+ extensions: []
30
+
31
+ extra_rdoc_files: []
32
+
33
+ files:
34
+ - README.rdoc
35
+ - VERSION.yml
36
+ - lib/exegesis
37
+ - lib/exegesis/design
38
+ - lib/exegesis/design/design_docs.rb
39
+ - lib/exegesis/design.rb
40
+ - lib/exegesis/document.rb
41
+ - lib/exegesis.rb
42
+ - test/design_doc_test.rb
43
+ - test/design_test.rb
44
+ - test/document_class_definitions_test.rb
45
+ - test/document_instance_methods_test.rb
46
+ - test/exegesis_test.rb
47
+ - test/fixtures
48
+ - test/fixtures/designs
49
+ - test/fixtures/designs/foos.js
50
+ - test/test_helper.rb
51
+ has_rdoc: true
52
+ homepage: http://github.com/mattly/exegesis
53
+ post_install_message:
54
+ rdoc_options:
55
+ - --inline-source
56
+ - --charset=UTF-8
57
+ require_paths:
58
+ - lib
59
+ required_ruby_version: !ruby/object:Gem::Requirement
60
+ requirements:
61
+ - - ">="
62
+ - !ruby/object:Gem::Version
63
+ version: "0"
64
+ version:
65
+ required_rubygems_version: !ruby/object:Gem::Requirement
66
+ requirements:
67
+ - - ">="
68
+ - !ruby/object:Gem::Version
69
+ version: "0"
70
+ version:
71
+ requirements: []
72
+
73
+ rubyforge_project:
74
+ rubygems_version: 1.2.0
75
+ signing_key:
76
+ specification_version: 2
77
+ summary: TODO
78
+ test_files: []
79
+