mattly-exegesis 0.0.10 → 0.2.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.
@@ -1,153 +1,93 @@
1
1
  module Exegesis
2
- class Document < CouchRest::Document
2
+ module Document
3
3
 
4
- def self.inherited subklass
5
- Exegesis.document_classes[subklass.name] = subklass
4
+ def self.included base
5
+ base.send :include, Exegesis::Model
6
+ base.extend ClassMethods
7
+ base.send :include, InstanceMethods
8
+ base.send :attr_accessor, :database
6
9
  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 }
10
+
11
+ module ClassMethods
12
+ def timestamps!
13
+ define_method :set_timestamps do
14
+ @attributes['updated_at'] = Time.now
15
+ @attributes['created_at'] ||= Time.now
27
16
  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
17
+ expose 'updated_at', :as => Time, :writer => false
18
+ expose 'created_at', :as => Time, :writer => false
19
+ end
20
+
21
+ def unique_id meth=nil, &block
22
+ if block
23
+ @unique_id_method = block
24
+ elsif meth
25
+ @unique_id_method = meth
45
26
  else
46
- define_method(attrib) { self[attrib] }
27
+ @unique_id_method ||= nil
47
28
  end
48
29
  end
49
30
  end
50
31
 
51
- def self.default hash=nil
52
- if hash
53
- @default = hash
54
- else
55
- @default ||= superclass.respond_to?(:default) ? superclass.default : {}
32
+ module InstanceMethods
33
+ def initialize hash={}, db=nil
34
+ super hash
35
+ @database = db
56
36
  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
37
+
38
+ def == other
39
+ self.id == other.id
63
40
  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)
41
+
42
+ def id
43
+ @attributes['_id']
71
44
  end
72
- end
73
-
74
- alias :_rev :rev
75
- alias_method :document_save, :save
76
-
77
- attr_accessor :parent
78
-
79
- def save
80
- raise ChildError, "cannot save if a parent is set" if parent
81
- set_timestamps if respond_to?(:set_timestamps)
82
- if respond_to?(:set_unique_id) && id.nil?
83
- @unique_id_attempt = 0
84
- begin
85
- self['_id'] = set_unique_id
86
- document_save
87
- rescue RestClient::RequestFailed => e
88
- @unique_id_attempt += 1
89
- retry
45
+
46
+ def rev
47
+ @attributes['_rev']
48
+ end
49
+
50
+ def save
51
+ set_timestamps if respond_to?(:set_timestamps)
52
+ if self.class.unique_id && id.nil?
53
+ save_with_custom_unique_id
54
+ else
55
+ save_document
90
56
  end
91
- else
92
- document_save
93
57
  end
94
- end
95
-
96
- def initialize keys={}
97
- apply_default
98
- super keys
99
- self['.kind'] ||= self.class.to_s
100
- end
101
-
102
- def update_attributes attrs={}
103
- raise ArgumentError, 'must include a matching _rev attribute' unless rev == attrs.delete('_rev')
104
- attrs.each_pair do |key, value|
105
- self.send("#{key}=", value) rescue nil
106
- attrs.delete(key)
58
+
59
+ def update_attributes attrs={}
60
+ raise ArgumentError, 'must include a matching _rev attribute' unless rev == attrs.delete('_rev')
61
+ super attrs
62
+ save
107
63
  end
108
- save
109
- end
110
-
111
- private
112
-
113
- def apply_default
114
- self.class.default.each do |key, value|
115
- self[key] = value
64
+
65
+ private
66
+
67
+ def save_document
68
+ raise ArgumentError, "canont save without a database" unless database
69
+ database.save self.attributes
116
70
  end
117
- end
118
-
119
- def cast as, value
120
- return nil if value.nil?
121
- klass = if as == :given
122
- if value.is_a?(Hash)
123
- Exegesis.document_classes[value['.kind']]
124
- else
125
- nil #nfi what do to in this case; maybe just have it be a doc hash?
71
+
72
+ def save_with_custom_unique_id
73
+ attempt = 0
74
+ value = ''
75
+ begin
76
+ @attributes['_id'] = if self.class.unique_id.is_a?(Proc)
77
+ self.class.unique_id.call(self, attempt)
78
+ else
79
+ self.send(self.class.unique_id, attempt)
80
+ end
81
+ save_document
82
+ rescue RestClient::RequestFailed => e
83
+ oldvalue = value
84
+ value = @attributes['_id']
85
+ raise RestClient::RequestFailed if oldvalue == value || attempt > 100
86
+ attempt += 1
87
+ retry
126
88
  end
127
- elsif as.is_a?(Class)
128
- as
129
- else
130
- Exegesis.document_classes[nil] # whatever the default is? Hell idk.
131
- end
132
-
133
- with = klass == Time ? :parse : :new
134
- casted = klass.send with, value
135
- casted.parent = self if casted.respond_to?(:parent)
136
- casted
137
- end
138
-
139
- def load_reference ids
140
- raise ArgumentError, "a database is required for loading a reference" unless database || (parent && parent.database)
141
- if ids.is_a?(Array)
142
- ids.map {|val| Exegesis::Document.instantiate((database || parent && parent.database).get(val)) }
143
- else
144
- Exegesis::Document.instantiate((database || parent && parent.database).get(ids))
145
89
  end
146
90
  end
147
91
 
148
92
  end
149
- end
150
-
151
- # $:.unshift File.dirname(__FILE__)
152
- # require 'document/annotated_reference'
153
- # require 'document/referencing'
93
+ end
@@ -0,0 +1,142 @@
1
+ module Exegesis
2
+ module Model
3
+
4
+ def self.included base
5
+ base.extend ClassMethods
6
+ base.send :include, InstanceMethods
7
+ Exegesis.model_classes[base.name] = base
8
+ base.send :attr_accessor, :attributes, :references, :parent
9
+ end
10
+
11
+ module ClassMethods
12
+ def expose *attrs
13
+ opts = attrs.last.is_a?(Hash) ? attrs.pop : {}
14
+ [attrs].flatten.each do |attrib|
15
+ attrib = attrib.to_s
16
+ if opts[:writer]
17
+ define_writer(attrib) {|val| @attributes[attrib] = opts[:writer].call(val) }
18
+ elsif !opts.has_key?(:writer)
19
+ define_writer(attrib) {|val| @attributes[attrib] = val }
20
+ end
21
+ if opts[:as] == :reference
22
+ define_reference attrib
23
+ elsif opts[:as]
24
+ define_caster attrib, opts[:as]
25
+ else
26
+ define_method(attrib) { @attributes[attrib] }
27
+ end
28
+ end
29
+ end
30
+
31
+ # sets a default hash object for the attributes of the instances of the class if an argument given,
32
+ # else retrieves the default
33
+ def default hash=nil
34
+ if hash
35
+ @default = {}
36
+ hash.each {|key, value| @default[key.to_s] = value }
37
+ else
38
+ @default ||= {}
39
+ end
40
+ end
41
+
42
+ private
43
+ def define_writer attrib, &block
44
+ define_method("#{attrib}=", block)
45
+ end
46
+
47
+ def define_reference attrib
48
+ define_method(attrib) do |*reload|
49
+ reload = false if reload.empty?
50
+ @references ||= {}
51
+ @references[attrib] = nil if reload
52
+ @references[attrib] ||= load_reference(@attributes[attrib])
53
+ end
54
+ end
55
+
56
+ def define_caster attrib, as
57
+ define_method(attrib) do
58
+ @attributes[attrib] = if @attributes[attrib].is_a?(Array)
59
+ @attributes[attrib].map {|val| cast as, val }.compact
60
+ else
61
+ cast as, @attributes[attrib]
62
+ end
63
+ end
64
+ end
65
+
66
+ end
67
+
68
+ module InstanceMethods
69
+ def initialize hash={}
70
+ apply_default
71
+ hash.each {|key, value| @attributes[key.to_s] = value }
72
+ @attributes['class'] = self.class.name
73
+ end
74
+
75
+ # works like Hash#update on the attributes hash, bypassing any writers
76
+ def update hash={}
77
+ hash.each {|key, value| @attributes[key.to_s] = value }
78
+ end
79
+
80
+ # update the attributes in the model using writers. If no writer is defined for a given
81
+ # key it will raise NoMethodError
82
+ def update_attributes hash={}
83
+ hash.each do |key, value|
84
+ self.send("#{key}=", value)
85
+ end
86
+ end
87
+
88
+ # retrieves the attribte
89
+ def [] key
90
+ @attributes[key]
91
+ end
92
+
93
+ # directly sets the attribute, avoiding any writers or lack thereof.
94
+ def []= key, value
95
+ @attributes[key] = value
96
+ end
97
+
98
+ # returns the instance's database, or its parents database if it has a parent.
99
+ # If neither, returns false.
100
+ # This is overwritten in classes including Exegesis::Document by an attr_accessor.
101
+ def database
102
+ parent && parent.database
103
+ end
104
+
105
+ private
106
+ def apply_default
107
+ @attributes = self.class.default.dup
108
+ end
109
+
110
+ def cast as, value
111
+ return nil if value.nil?
112
+ klass = if as == :given && value.is_a?(Hash)
113
+ Exegesis.model_classes[value['class']]
114
+ elsif as.is_a?(Class)
115
+ as
116
+ else
117
+ nil
118
+ end
119
+
120
+ casted = if klass.nil?
121
+ value # if no class, just return the value
122
+ elsif klass == Time # Time is a special case; the ONLY special case.
123
+ Time.parse value
124
+ else
125
+ klass.new value
126
+ end
127
+ casted.parent = self if casted.respond_to?(:parent)
128
+ casted
129
+ end
130
+
131
+ def load_reference ids
132
+ raise ArgumentError, "a database is required for loading a reference" unless database
133
+ if ids.is_a?(Array)
134
+ ids.map {|val| database.get(val) }
135
+ else
136
+ database.get(ids)
137
+ end
138
+ end
139
+ end
140
+
141
+ end
142
+ end
@@ -0,0 +1,28 @@
1
+ module Exegesis
2
+ class Server
3
+ include Exegesis::Http
4
+
5
+ attr_accessor :uri, :version
6
+
7
+ # Creates a new instance of Exegesis::Server. Defaults to http://localhost:5984 and
8
+ # verifies the existance of the database.
9
+ def initialize address='http://localhost:5984'
10
+ @uri = address
11
+ @version = get(@uri)['version']
12
+ end
13
+
14
+ # returns an array of all the databases on the server
15
+ def databases
16
+ get "#{@uri}/_all_dbs"
17
+ end
18
+
19
+ # creates a database with the given name on the server
20
+ def create_database name
21
+ put "#{@uri}/#{name}"
22
+ end
23
+
24
+ def inspect
25
+ "#<Exegesis::Server #{@uri}>"
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,38 @@
1
+ require 'cgi'
2
+ module Exegesis
3
+ module Http
4
+ extend self
5
+
6
+ def format_url url, params={}
7
+ if params && !params.empty?
8
+ query = params.map do |key, value|
9
+ value = value.to_json if [:key, :startkey, :endkey, :keys].include?(key)
10
+ "#{key}=#{CGI.escape(value.to_s)}"
11
+ end.join('&')
12
+ url = "#{url}?#{query}"
13
+ end
14
+ url
15
+ end
16
+
17
+ def escape_id id
18
+ /^_design\/(.*)/ =~ id ? "_design/#{CGI.escape($1)}" : CGI.escape(id)
19
+ end
20
+
21
+ def get url
22
+ JSON.parse(RestClient.get(url), :max_nesting => false)
23
+ end
24
+
25
+ def post url, body=''
26
+ JSON.parse(RestClient.post(url, (body || '').to_json))
27
+ end
28
+
29
+ def put url, body=''
30
+ JSON.parse(RestClient.put(url, (body || '').to_json))
31
+ end
32
+
33
+ def delete url
34
+ JSON.parse(RestClient.delete(url))
35
+ end
36
+
37
+ end
38
+ end
@@ -0,0 +1,5 @@
1
+ class Time
2
+ def to_json(options = nil)
3
+ %("#{self.dup.utc.strftime("%Y/%m/%d %H:%M:%S +0000")}")
4
+ end
5
+ end
@@ -0,0 +1,161 @@
1
+ require File.join(File.dirname(__FILE__), 'test_helper.rb')
2
+
3
+ class DatabaseTest
4
+ include Exegesis::Database
5
+ end
6
+ class CustomDesignDirDatabaseTest
7
+ include Exegesis::Database
8
+ designs_directory 'app/designs'
9
+ end
10
+ class DatabaseTestDocument
11
+ include Exegesis::Document
12
+ end
13
+
14
+ class ExegesisDatabaseTest < Test::Unit::TestCase
15
+ before(:all) do
16
+ @server = Exegesis::Server.new('http://localhost:5984')
17
+ RestClient.delete("#{@server.uri}/exegesis-test") rescue nil
18
+ RestClient.delete("#{@server.uri}/exegesis-test-nonexistent") rescue nil
19
+ RestClient.put("#{@server.uri}/exegesis-test", '')
20
+ end
21
+
22
+ context "initializing" do
23
+ context "with server and name arguments" do
24
+ before do
25
+ @db =DatabaseTest.new(@server, 'exegesis-test')
26
+ end
27
+
28
+ expect { @db.is_a?(DatabaseTest).will == true }
29
+ expect { @db.uri.will == "#{@server.uri}/exegesis-test"}
30
+
31
+ context "when the database does not exist" do
32
+ before do
33
+ @action = lambda { DatabaseTest.new(@server, 'exegesis-test-nonexistent') }
34
+ end
35
+
36
+ expect { @action.will raise_error(RestClient::ResourceNotFound) }
37
+ end
38
+ end
39
+
40
+ context "with a url argument" do
41
+ before do
42
+ @db = DatabaseTest.new('http://localhost:5984/exegesis-test')
43
+ end
44
+
45
+ expect { @db.is_a?(DatabaseTest).will == true }
46
+ expect { @db.uri.will == 'http://localhost:5984/exegesis-test' }
47
+ end
48
+
49
+ context "with a name argument" do
50
+ before do
51
+ @db = DatabaseTest.new('exegesis-test')
52
+ end
53
+
54
+ expect { @db.is_a?(DatabaseTest).will == true }
55
+ expect { @db.uri.will == "http://localhost:5984/exegesis-test" }
56
+ end
57
+ end
58
+
59
+ context "retrieving documents by id" do
60
+ before do
61
+ @db = DatabaseTest.new @server, 'exegesis-test'
62
+ RestClient.put "#{@db.uri}/test-document", {'key'=>'value', 'class'=>'DatabaseTestDocument'}.to_json rescue nil
63
+ @doc = @db.get('test-document')
64
+ end
65
+
66
+ after do
67
+ RestClient.delete("#{@db.uri}/test-document?rev=#{@doc['_rev']}") rescue nil
68
+ end
69
+
70
+ expect { @doc.is_a?(DatabaseTestDocument).will == true }
71
+ expect { @doc.id.will == 'test-document' }
72
+ expect { @doc['key'].will == 'value' }
73
+
74
+ context "retrieving the raw document" do
75
+ before do
76
+ @doc = @db.raw_get('test-document')
77
+ end
78
+
79
+ expect { @doc.is_a?(Hash).will == true }
80
+ expect { @doc['_id'].will == 'test-document' }
81
+ expect { @doc['key'].will == 'value' }
82
+ expect { @doc['class'].will == 'DatabaseTestDocument' }
83
+ end
84
+ end
85
+
86
+ context "saving docs" do
87
+ before do
88
+ reset_db
89
+ @db = DatabaseTest.new('exegesis-test')
90
+ end
91
+
92
+ context "a single doc" do
93
+ before { @doc = {'foo' => 'bar'} }
94
+
95
+ context "without an id" do
96
+ before do
97
+ @db.save(@doc)
98
+ @rdoc = @db.get(@doc['_id'])
99
+ end
100
+ expect { @doc['_rev'].will == @rdoc['_rev'] }
101
+ expect { @rdoc['foo'].will == @doc['foo'] }
102
+ end
103
+
104
+ context "with an id" do
105
+ before { @doc['_id'] = 'test-document' }
106
+
107
+ context "when the document doesn't exist yet" do
108
+ before do
109
+ @db.save(@doc)
110
+ @rdoc = @db.get('test-document')
111
+ end
112
+ expect { @doc['_rev'].will == @rdoc['_rev'] }
113
+ expect { @rdoc['foo'].will == @doc['foo'] }
114
+ end
115
+
116
+ context "when the document exists already" do
117
+ before do
118
+ response = @db.post(@doc)
119
+ @doc['_id'] = response['id']
120
+ @doc['_rev'] = response['rev']
121
+ @doc['foo'] = 'bee'
122
+ end
123
+
124
+ expect { lambda { @db.save(@doc) }.wont raise_error }
125
+
126
+ context "without a valid rev" do
127
+ before { @doc.delete('_rev') }
128
+ expect { lambda { @db.save(@doc) }.will raise_error }
129
+ end
130
+ end
131
+ end
132
+ end
133
+
134
+ context "multiple docs" do
135
+ before do
136
+ @updated = @db.post({'_id' => 'updated', 'key' => 'original'})
137
+ @deleted = @db.post({'_id' => 'deleted', 'key' => 'original'})
138
+ @saving = lambda {
139
+ @db.save([
140
+ {'_id' => 'new', 'key' => 'new'},
141
+ {'_id' => 'updated', 'key' => 'new', '_rev' => @updated['rev']},
142
+ {'_id' => 'deleted', '_rev' => @deleted['rev'], '_deleted' => true }
143
+ ])
144
+ }
145
+ end
146
+
147
+ context "without conflicts" do
148
+ before { @saving.call }
149
+ expect { @db.get('new')['key'].will == 'new' }
150
+ expect { @db.get('updated')['key'].will == 'new' }
151
+ expect { lambda {@db.get('deleted')}.will raise_error(RestClient::ResourceNotFound) }
152
+ end
153
+ end
154
+ end
155
+
156
+ context "setting the designs directory" do
157
+ expect { DatabaseTest.designs_directory.will == Pathname.new('designs') }
158
+ expect { CustomDesignDirDatabaseTest.designs_directory.will == Pathname.new('app/designs') }
159
+ end
160
+
161
+ end