couchmodel 0.1.0.beta2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (45) hide show
  1. data/LICENSE +20 -0
  2. data/README.rdoc +156 -0
  3. data/Rakefile +20 -0
  4. data/lib/core_extension/array.rb +14 -0
  5. data/lib/core_extension/string.rb +12 -0
  6. data/lib/couch_model/active_model.rb +86 -0
  7. data/lib/couch_model/base/accessor.rb +39 -0
  8. data/lib/couch_model/base/association.rb +63 -0
  9. data/lib/couch_model/base/finder.rb +28 -0
  10. data/lib/couch_model/base/setup.rb +88 -0
  11. data/lib/couch_model/base.rb +117 -0
  12. data/lib/couch_model/collection.rb +84 -0
  13. data/lib/couch_model/configuration.rb +68 -0
  14. data/lib/couch_model/database.rb +64 -0
  15. data/lib/couch_model/design.rb +92 -0
  16. data/lib/couch_model/server.rb +44 -0
  17. data/lib/couch_model/transport.rb +68 -0
  18. data/lib/couch_model/view.rb +52 -0
  19. data/lib/couch_model.rb +15 -0
  20. data/spec/fake_transport.yml +202 -0
  21. data/spec/fake_transport_helper.rb +27 -0
  22. data/spec/integration/basic_spec.rb +125 -0
  23. data/spec/integration/design/membership.design +5 -0
  24. data/spec/integration/design/user.design +2 -0
  25. data/spec/lib/core_extension/array_spec.rb +24 -0
  26. data/spec/lib/core_extension/string_spec.rb +22 -0
  27. data/spec/lib/couch_model/active_model_spec.rb +228 -0
  28. data/spec/lib/couch_model/base_spec.rb +169 -0
  29. data/spec/lib/couch_model/collection_spec.rb +100 -0
  30. data/spec/lib/couch_model/configuration_spec.rb +117 -0
  31. data/spec/lib/couch_model/core/accessor_spec.rb +59 -0
  32. data/spec/lib/couch_model/core/association_spec.rb +114 -0
  33. data/spec/lib/couch_model/core/finder_spec.rb +24 -0
  34. data/spec/lib/couch_model/core/setup_spec.rb +88 -0
  35. data/spec/lib/couch_model/database_spec.rb +165 -0
  36. data/spec/lib/couch_model/design/association_test_model_one.design +5 -0
  37. data/spec/lib/couch_model/design/base_test_model.design +10 -0
  38. data/spec/lib/couch_model/design/setup_test_model.design +10 -0
  39. data/spec/lib/couch_model/design_spec.rb +144 -0
  40. data/spec/lib/couch_model/server_spec.rb +64 -0
  41. data/spec/lib/couch_model/transport_spec.rb +44 -0
  42. data/spec/lib/couch_model/view_spec.rb +166 -0
  43. data/spec/lib/couch_model_spec.rb +3 -0
  44. data/spec/spec_helper.rb +27 -0
  45. metadata +128 -0
@@ -0,0 +1,84 @@
1
+ require File.join(File.dirname(__FILE__), "configuration")
2
+ require File.join(File.dirname(__FILE__), "transport")
3
+ require 'json'
4
+
5
+ module CouchModel
6
+
7
+ class Collection
8
+
9
+ REQUEST_PARAMETER_KEYS = [
10
+ :key, :startkey, :startkey_docid, :endkey, :endkey_docid,
11
+ :limit, :stale, :descending, :skip, :group, :group_level,
12
+ :reduce, :inclusive_end
13
+ ].freeze unless defined?(REQUEST_PARAMETER_KEYS)
14
+
15
+ ARRAY_METHOD_NAMES = [
16
+ :[], :at, :collect, :compact, :count, :cycle, :each, :each_index,
17
+ :empty?, :fetch, :index, :first, :flatten, :include?, :join, :last,
18
+ :length, :map, :pack, :reject, :reverse, :reverse_each, :rindex,
19
+ :sample, :shuffle, :size, :slice, :sort, :take, :to_a, :to_ary,
20
+ :values_at, :zip
21
+ ].freeze unless defined?(ARRAY_METHOD_NAMES)
22
+
23
+ attr_reader :url
24
+ attr_reader :options
25
+
26
+ def initialize(url, options = { })
27
+ @url, @options = url, options
28
+ end
29
+
30
+ def total_count
31
+ fetch :meta => true unless @total_count
32
+ @total_count
33
+ end
34
+
35
+ def respond_to?(method_name)
36
+ ARRAY_METHOD_NAMES.include?(method_name) || super
37
+ end
38
+
39
+ def method_missing(method_name, *arguments, &block)
40
+ if ARRAY_METHOD_NAMES.include?(method_name)
41
+ fetch
42
+ @entries.send method_name, *arguments, &block
43
+ else
44
+ super
45
+ end
46
+ end
47
+
48
+ private
49
+
50
+ def fetch(options = { })
51
+ meta = options[:meta] || false
52
+
53
+ evaluate Transport.request(
54
+ :get, url,
55
+ :parameters => request_parameters.merge(meta ? { "limit" => "0" } : { }),
56
+ :expected_status_code => 200
57
+ )
58
+
59
+ true
60
+ end
61
+
62
+ def evaluate(response)
63
+ @total_count = response["total_rows"]
64
+ @entries = response["rows"].select do |row|
65
+ row["doc"].has_key?(Configuration::CLASS_KEY) && Object.const_defined?(row["doc"][Configuration::CLASS_KEY])
66
+ end.map do |row|
67
+ model_class = Object.const_get row["doc"][Configuration::CLASS_KEY]
68
+ model = model_class.new
69
+ model.instance_variable_set :@attributes, row["doc"]
70
+ model
71
+ end
72
+ end
73
+
74
+ def request_parameters
75
+ parameters = { "include_docs" => "true" }
76
+ REQUEST_PARAMETER_KEYS.each do |key|
77
+ parameters[ key.to_s ] = @options[key].is_a?(Array) ? JSON.dump(@options[key]) : @options[key].to_s if @options[key]
78
+ end
79
+ parameters
80
+ end
81
+
82
+ end
83
+
84
+ end
@@ -0,0 +1,68 @@
1
+
2
+ module CouchModel
3
+
4
+ class Configuration
5
+
6
+ CLASS_KEY = "model_class".freeze unless defined?(CLASS_KEY)
7
+ CLASS_VIEW_NAME = "all".freeze unless defined?(CLASS_VIEW_NAME)
8
+
9
+ class << self
10
+
11
+ @@fake_transport = false
12
+ @@databases = [ ]
13
+ @@designs = [ ]
14
+
15
+ def fake_transport=(value)
16
+ @@fake_transport = value
17
+ end
18
+
19
+ def fake_transport
20
+ @@fake_transport
21
+ end
22
+
23
+ def design_directory=(value)
24
+ @@design_directory = value
25
+ end
26
+
27
+ def design_directory
28
+ class_variable_defined?(:@@design_directory) ? @@design_directory : ""
29
+ end
30
+
31
+ def register_database(database)
32
+ result = @@databases.select{ |element| element == database }.first
33
+ unless result
34
+ @@databases << database
35
+ result = database
36
+ end
37
+ result
38
+ end
39
+
40
+ def databases
41
+ @@databases
42
+ end
43
+
44
+ def setup_databases(options = { })
45
+ @@databases.each do |database|
46
+ database.setup! options
47
+ end
48
+ end
49
+
50
+ def register_design(design)
51
+ @@designs << design
52
+ end
53
+
54
+ def designs
55
+ @@designs
56
+ end
57
+
58
+ def setup_designs
59
+ @@designs.each do |design|
60
+ design.push
61
+ end
62
+ end
63
+
64
+ end
65
+
66
+ end
67
+
68
+ end
@@ -0,0 +1,64 @@
1
+ require File.join(File.dirname(__FILE__), "transport")
2
+ require File.join(File.dirname(__FILE__), "server")
3
+ require File.join(File.dirname(__FILE__), "collection")
4
+
5
+ module CouchModel
6
+
7
+ class Database
8
+
9
+ class Error < StandardError; end
10
+
11
+ attr_reader :server
12
+ attr_reader :name
13
+
14
+ def initialize(options = { })
15
+ @name = options[:name] || raise(ArgumentError, "no database was given")
16
+ @server = options[:server] || Server.new
17
+ end
18
+
19
+ def ==(other)
20
+ other.is_a?(self.class) && @name == other.name && @server == other.server
21
+ end
22
+
23
+ def ===(other)
24
+ object_id == other.object_id
25
+ end
26
+
27
+ def create!
28
+ Transport.request :put, url, :expected_status_code => 201
29
+ end
30
+
31
+ def delete!
32
+ Transport.request :delete, url, :expected_status_code => 200
33
+ end
34
+
35
+ def setup!(options = { })
36
+ delete_if_exists = options[:delete_if_exists] || false
37
+
38
+ if delete_if_exists
39
+ delete! if exists?
40
+ create!
41
+ else
42
+ create! unless exists?
43
+ end
44
+ end
45
+
46
+ def informations
47
+ Transport.request :get, url, :expected_status_code => 200
48
+ end
49
+
50
+ def exists?
51
+ @server.database_names.include? @name
52
+ end
53
+
54
+ def url
55
+ "#{@server.url}/#{@name}"
56
+ end
57
+
58
+ def documents(options = { })
59
+ Collection.new url + "/_all_docs", options
60
+ end
61
+
62
+ end
63
+
64
+ end
@@ -0,0 +1,92 @@
1
+ require File.join(File.dirname(__FILE__), "configuration")
2
+ require File.join(File.dirname(__FILE__), "transport")
3
+ require File.join(File.dirname(__FILE__), "base")
4
+ require File.join(File.dirname(__FILE__), "view")
5
+ require 'yaml'
6
+
7
+ module CouchModel
8
+
9
+ class Design
10
+
11
+ attr_reader :database
12
+ attr_reader :model_class
13
+ attr_accessor :id
14
+ attr_reader :rev
15
+ attr_accessor :language
16
+ attr_reader :views
17
+
18
+ def initialize(database, model_class, attributes = { })
19
+ @database = database
20
+ @model_class = model_class
21
+ @language = "javascript"
22
+ @views = [ ]
23
+
24
+ load_file
25
+ self.id = attributes[:id] if attributes[:id]
26
+ self.language = attributes[:language] if attributes[:language]
27
+ self.views = attributes[:views] if attributes[:views]
28
+ end
29
+
30
+ def filename
31
+ @filename ||= File.join(CouchModel::Configuration.design_directory, "#{model_class.to_s.underscore}.design")
32
+ end
33
+
34
+ def load_file
35
+ return false unless File.exists?(filename)
36
+ attributes = YAML::load_file filename
37
+ self.id = attributes[:id]
38
+ self.language = attributes[:language]
39
+ self.views = attributes[:views]
40
+ true
41
+ end
42
+
43
+ def views=(view_hash)
44
+ @views = [ ]
45
+ view_hash.each do |view_name, view|
46
+ @views << View.new(self, view.merge(:name => view_name)) if view.is_a?(Hash)
47
+ end if view_hash.is_a?(Hash)
48
+ end
49
+
50
+ def generate_view(name, options = { })
51
+ view = View.new self, options.merge(:name => name)
52
+ @views.insert 0, view
53
+ view
54
+ end
55
+
56
+ def to_hash
57
+ hash = {
58
+ "_id" => "_design/#{self.id}",
59
+ "language" => self.language,
60
+ "views" => { }
61
+ }
62
+ hash.merge! "_rev" => self.rev if self.rev
63
+ @views.each { |view| hash["views"].merge! view.to_hash }
64
+ hash
65
+ end
66
+
67
+ def exists?
68
+ Transport.request :get, self.url, :expected_status_code => 200
69
+ true
70
+ rescue Transport::UnexpectedStatusCodeError
71
+ false
72
+ end
73
+
74
+ def push
75
+ response = Transport.request :get, self.url
76
+ self.rev = response["_rev"] if response["_rev"]
77
+
78
+ Transport.request :put, self.url, :json => self.to_hash, :expected_status_code => 201
79
+ true
80
+ end
81
+
82
+ def url
83
+ "#{@database.url}/_design/#{self.id}"
84
+ end
85
+
86
+ private
87
+
88
+ attr_writer :rev
89
+
90
+ end
91
+
92
+ end
@@ -0,0 +1,44 @@
1
+ require File.join(File.dirname(__FILE__), "transport")
2
+
3
+ module CouchModel
4
+
5
+ class Server
6
+
7
+ class Error < StandardError; end
8
+
9
+ attr_reader :host
10
+ attr_reader :port
11
+
12
+ def initialize(options = { })
13
+ @host = options[:host] || "localhost"
14
+ @port = options[:port] || "5984"
15
+ end
16
+
17
+ def ==(other)
18
+ other.is_a?(self.class) && @host == other.host && @port == other.port
19
+ end
20
+
21
+ def informations
22
+ Transport.request :get, url + "/", :expected_status_code => 200
23
+ end
24
+
25
+ def statistics
26
+ Transport.request :get, url + "/_stats", :expected_status_code => 200
27
+ end
28
+
29
+ def database_names
30
+ Transport.request :get, url + "/_all_dbs", :expected_status_code => 200
31
+ end
32
+
33
+ def uuids(count = 1)
34
+ response = Transport.request :get, url + "/_uuids", :expected_status_code => 200, :parameters => { :count => count }
35
+ response["uuids"]
36
+ end
37
+
38
+ def url
39
+ "http://#{@host}:#{@port}"
40
+ end
41
+
42
+ end
43
+
44
+ end
@@ -0,0 +1,68 @@
1
+ require 'uri'
2
+ require 'net/http'
3
+ require 'json'
4
+
5
+ module CouchModel
6
+
7
+ module Transport
8
+
9
+ class Error < StandardError; end
10
+
11
+ class UnexpectedStatusCodeError < StandardError
12
+
13
+ attr_reader :status_code
14
+
15
+ def initialize(status_code)
16
+ @status_code = status_code
17
+ end
18
+
19
+ def to_s
20
+ "#{super} received status code #{self.status_code}"
21
+ end
22
+
23
+ end
24
+
25
+ class << self
26
+
27
+ def request(http_method, url, options = { })
28
+ expected_status_code = options[:expected_status_code]
29
+
30
+ uri = URI.parse @base_url ? @base_url + url : url
31
+
32
+ request_class = request_class http_method
33
+ request = request_object request_class, uri, options
34
+
35
+ response = Net::HTTP.start(uri.host, uri.port) { |connection| connection.request request }
36
+
37
+ raise UnexpectedStatusCodeError, response.code.to_i if expected_status_code && expected_status_code.to_s != response.code
38
+ JSON.parse response.body
39
+ end
40
+
41
+ private
42
+
43
+ def request_class(http_method)
44
+ Net::HTTP.const_get http_method.capitalize
45
+ end
46
+
47
+ def request_object(request_class, uri, options)
48
+ parameters = options[:parameters] || { }
49
+ json = options[:json]
50
+
51
+ case request_class.to_s
52
+ when "Net::HTTP::Get"
53
+ request_class.new uri.path +
54
+ (parameters.empty? ? "" : "?" + parameters.collect{ |key, value| "#{key}=#{URI.escape(value.to_s)}" }.reverse.join("&"))
55
+ when "Net::HTTP::Post", "Net::HTTP::Put"
56
+ request = request_class.new uri.path, { "Content-Type" => "application/json" }
57
+ request.body = JSON.dump(json) if json
58
+ request
59
+ else
60
+ request_class.new uri.path
61
+ end
62
+ end
63
+
64
+ end
65
+
66
+ end
67
+
68
+ end
@@ -0,0 +1,52 @@
1
+ require File.join(File.dirname(__FILE__), "configuration")
2
+ require File.join(File.dirname(__FILE__), "collection")
3
+
4
+ module CouchModel
5
+
6
+ class View
7
+
8
+ attr_reader :design
9
+ attr_accessor :name
10
+ attr_accessor :map
11
+ attr_accessor :reduce
12
+
13
+ def initialize(design, attributes = { })
14
+ @design = design
15
+ @name = attributes[:name]
16
+
17
+ generate_functions attributes
18
+ @map = attributes[:map] if attributes[:map]
19
+ @reduce = attributes[:reduce] if attributes[:reduce]
20
+ end
21
+
22
+ def collection(options = { })
23
+ Collection.new url, options
24
+ end
25
+
26
+ def to_hash
27
+ { self.name => { "map" => self.map, "reduce" => self.reduce } }
28
+ end
29
+
30
+ def url
31
+ "#{@design.url}/_view/#{@name}"
32
+ end
33
+
34
+ def generate_functions(options = { })
35
+ keys = [ (options[:keys] || "_id") ].flatten
36
+
37
+ emit_values = keys.map{ |key| "document['#{key}']" }
38
+ check_values = emit_values.select{ |value| value != "document['_id']" }
39
+
40
+ @map =
41
+ """function(document) {
42
+ if (document['#{Configuration::CLASS_KEY}'] == '#{@design.model_class.to_s}'#{check_values.empty? ? "" : " && " + check_values.join(" && ")}) {
43
+ emit(#{emit_values.size == 1 ? emit_values.first : "[ " + emit_values.join(", ") + " ]"}, null);
44
+ }
45
+ }
46
+ """
47
+ @reduce = nil
48
+ end
49
+
50
+ end
51
+
52
+ end
@@ -0,0 +1,15 @@
1
+ require 'rubygems'
2
+
3
+ require File.join(File.dirname(__FILE__), "couch_model", "configuration")
4
+ require File.join(File.dirname(__FILE__), "couch_model", "base")
5
+
6
+ begin
7
+ gem 'activemodel'
8
+ require 'active_model'
9
+
10
+ require File.join(File.dirname(__FILE__), "couch_model", "active_model")
11
+
12
+ # ActiveModel support is activated
13
+ rescue Gem::LoadError
14
+ # ActiveModel support is deactivated
15
+ end
@@ -0,0 +1,202 @@
1
+ -
2
+ :http_method: "get"
3
+ :url: "http://localhost:5984/"
4
+ :response:
5
+ :code: "200"
6
+ :json:
7
+ "couchdb": "Welcome"
8
+ "version": "0.10.0"
9
+ -
10
+ :http_method: "get"
11
+ :url: "http://localhost:5984/_stats"
12
+ :response:
13
+ :code: "200"
14
+ :json:
15
+ "httpd_status_codes": "..."
16
+ "httpd_request_methods": "..."
17
+ -
18
+ :http_method: "get"
19
+ :url: "http://localhost:5984/_all_dbs"
20
+ :response:
21
+ :code: "200"
22
+ :json: [ "development", "test" ]
23
+ -
24
+ :http_method: "get"
25
+ :url: "http://localhost:5984/_uuids"
26
+ :parameters:
27
+ :count: 3
28
+ :response:
29
+ :code: "200"
30
+ :json:
31
+ "uuids": [ "uuid_1", "uuid_2", "uuid_3" ]
32
+ -
33
+ :http_method: "get"
34
+ :url: "http://localhost:5984/test"
35
+ :response:
36
+ :code: "200"
37
+ :json:
38
+ "db_name": "test"
39
+ "doc_count": "0"
40
+ -
41
+ :http_method: "get"
42
+ :url: "http://localhost:5984/new_database"
43
+ :response:
44
+ :code: "404"
45
+ :json:
46
+ -
47
+ :http_method: "get"
48
+ :url: "http://localhost:5984/test/test_model_1"
49
+ :response:
50
+ :code: "200"
51
+ :json:
52
+ "_id": "test_model_1"
53
+ "_rev": "0"
54
+ "model_class": "BaseTestModel"
55
+ "name": "phil"
56
+ "related_id": "test_model_2"
57
+ -
58
+ :http_method: "get"
59
+ :url: "http://localhost:5984/test/test_model_2"
60
+ :response:
61
+ :code: "200"
62
+ :json:
63
+ "_id": "test_model_2"
64
+ "_rev": "0"
65
+ "model_class": "BaseTestModel"
66
+ "name": "keppla"
67
+ -
68
+ :http_method: "get"
69
+ :url: "http://localhost:5984/test/invalid"
70
+ :response:
71
+ :code: "404"
72
+ -
73
+ :http_method: "post"
74
+ :url: "http://localhost:5984/test"
75
+ :response:
76
+ :code: "201"
77
+ :json:
78
+ "ok": true
79
+ "id": "test_model_2"
80
+ "rev": "0"
81
+ -
82
+ :http_method: "put"
83
+ :url: "http://localhost:5984/test/test_model_1"
84
+ :response:
85
+ :code: "200"
86
+ :json:
87
+ "ok": true
88
+ "id": "test_model_1"
89
+ "rev": "1"
90
+ -
91
+ :http_method: "delete"
92
+ :url: "http://localhost:5984/test/test_model_1"
93
+ :parameters:
94
+ "rev": "0"
95
+ :response:
96
+ :code: "200"
97
+ :json:
98
+ "ok": true
99
+ "id": "test_model_1"
100
+ "rev": "1"
101
+ -
102
+ :http_method: "get"
103
+ :url: "http://localhost:5984/test/_design/test_design"
104
+ :response:
105
+ :code: "200"
106
+ :json:
107
+ "_id": "_design/test_design"
108
+ "_rev": "0"
109
+ "language": "javascript"
110
+ "views":
111
+ "test_view":
112
+ "map": "function(document) { };"
113
+ "reduce": "function(key, values, rereduce) { };"
114
+ -
115
+ :http_method: "put"
116
+ :url: "http://localhost:5984/test/_design/test_design"
117
+ :response:
118
+ :code: "201"
119
+ :json:
120
+ "ok": true
121
+ "id": "_design/test_design"
122
+ "rev": "1"
123
+ -
124
+ :http_method: "get"
125
+ :url: "http://localhost:5984/test/_all_docs"
126
+ :parameters:
127
+ "include_docs": "true"
128
+ "limit": "1"
129
+ :response:
130
+ :code: "200"
131
+ :json:
132
+ "total_rows": 1
133
+ "offset": 0
134
+ "rows":
135
+ -
136
+ "id": "test_model_1"
137
+ "key": "test_model_1"
138
+ "value":
139
+ "rev": "0"
140
+ "doc":
141
+ "_id": "test_model_1"
142
+ "_rev": "0"
143
+ "model_class": "CollectionTestModel"
144
+ "name": "phil"
145
+ -
146
+ :http_method: "get"
147
+ :url: "http://localhost:5984/test/_all_docs"
148
+ :parameters:
149
+ "include_docs": "true"
150
+ "limit": "0"
151
+ :response:
152
+ :code: "200"
153
+ :json:
154
+ "total_rows": 1
155
+ "offset": 0
156
+ "rows": [ ]
157
+ -
158
+ :http_method: "get"
159
+ :url: "http://localhost:5984/test/_design/association_test_model_one/_view/by_related_id_and_name"
160
+ :parameters:
161
+ "include_docs": "true"
162
+ "startkey": "[\"test_model_2\",null]"
163
+ "endkey": "[\"test_model_2\",{}]"
164
+ :response:
165
+ :code: "200"
166
+ :json:
167
+ "total_rows": 1
168
+ "offset": 0
169
+ "rows":
170
+ -
171
+ "id": "test_model_1"
172
+ "key": "test_model_2"
173
+ "value":
174
+ "rev": "0"
175
+ "doc":
176
+ "_id": "test_model_1"
177
+ "_rev": "0"
178
+ "model_class": "AssociationTestModelOne"
179
+ "name": "phil"
180
+ -
181
+ :http_method: "get"
182
+ :url: "http://localhost:5984/test/_design/association_test_model_one/_view/by_related_id_and_name"
183
+ :parameters:
184
+ "include_docs": "true"
185
+ "startkey": "[\"test_model_2\",\"phil\"]"
186
+ "endkey": "[\"test_model_2\",\"phil\"]"
187
+ :response:
188
+ :code: "200"
189
+ :json:
190
+ "total_rows": 1
191
+ "offset": 0
192
+ "rows":
193
+ -
194
+ "id": "test_model_1"
195
+ "key": "test_model_2"
196
+ "value":
197
+ "rev": "0"
198
+ "doc":
199
+ "_id": "test_model_1"
200
+ "_rev": "0"
201
+ "model_class": "AssociationTestModelOne"
202
+ "name": "phil"