couchmodel 0.1.0.beta2

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.
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"