dolly 0.9.0 → 1.0.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: c0ed634c57601285c5f226c689864684e556edbe
4
- data.tar.gz: 343cf2e87366a95ca10a22cd76a7e9b014fe8eeb
3
+ metadata.gz: 1952fc2428d46ea5090b64c1785e63c2665196e1
4
+ data.tar.gz: 365cf7c705ebd578fe5116721a94653a28cca9fc
5
5
  SHA512:
6
- metadata.gz: 46eb110e75de2a3511cd5682bc5f8a4caddf11c4c8b41c2af20dd09b29e5413c9380385981c4b957655ac163f24b7a17beb6b43dac012695eaf6d1b2a2794275
7
- data.tar.gz: 4cab1eb32ab93835832897fda1ed3a300063df79e1e6f6895024355939843608ba1c3a3168635d63d4a339cc39f4f06eb05322a673e91f03babee3146f180bbb
6
+ metadata.gz: c043052831b7bc395ca53d49771026d1450e409a1bd92dcc2efc45377d4ae63d1cf81ec95ebed62717998308cadfa8f4a12085c43f1bd2e77fc66631d500e7da
7
+ data.tar.gz: 0a90e6732e7125a4f835d320c6f6902bff4327f37afd63794789fc4fb8ec0687e30ef638bc1b98dfac1a9ac38fe3388908c90070158cd918b52d275ab8f07d63
@@ -10,6 +10,7 @@ module Dolly
10
10
 
11
11
  attr_accessor :rows, :doc, :key
12
12
  class_attribute :properties
13
+ cattr_accessor :timestamps
13
14
 
14
15
  def initialize options = {}
15
16
  @doc ||= {}
@@ -50,11 +51,12 @@ module Dolly
50
51
  doc['_rev'] = value
51
52
  end
52
53
 
53
- def save
54
+ def save options = {}
55
+ return false unless options[:validate] == false || valid?
54
56
  self.doc['_id'] = self.id if self.id.present?
55
57
  self.doc['_id'] = self.class.next_id if self.doc['_id'].blank?
56
- set_created_at if respond_to? :created_at
57
- set_updated_at if respond_to? :updated_at
58
+ set_created_at if timestamps
59
+ set_updated_at if timestamps
58
60
  response = database.put(id_as_resource, self.doc.to_json)
59
61
  obj = JSON::parse response.parsed_response
60
62
  doc['_rev'] = obj['rev'] if obj['rev']
@@ -62,7 +64,7 @@ module Dolly
62
64
  end
63
65
 
64
66
  def save!
65
- #TODO: decide how to handle validations...
67
+ raise DocumentInvalidError unless valid?
66
68
  save
67
69
  end
68
70
 
@@ -96,6 +98,18 @@ module Dolly
96
98
  CGI::escape id
97
99
  end
98
100
 
101
+ def attach_file! file_name, mime_type, body, opts={}
102
+ if opts[:inline]
103
+ attachment_data = { file_name.to_s => { 'content_type' => mime_type,
104
+ 'data' => Base64.encode64(body)} }
105
+ doc['_attachments'] ||= {}
106
+ doc['_attachments'].merge! attachment_data
107
+ save
108
+ else
109
+ database.attach id_as_resource, CGI.escape(file_name), body, { 'Content-Type' => mime_type }
110
+ end
111
+ end
112
+
99
113
  def self.create options = {}
100
114
  obj = new options
101
115
  obj.save
@@ -124,11 +138,15 @@ module Dolly
124
138
  end
125
139
 
126
140
  def write_property name, value
127
- @doc[name.to_s] = self.properties[name].value = value
141
+ instance_variable_set(:"@#{name}", value)
142
+ @doc[name.to_s] = value
128
143
  end
129
144
 
130
145
  def read_property name
131
- doc[name.to_s] || self.properties[name].value
146
+ if instance_variable_get(:"@#{name}").nil?
147
+ write_property name, (doc[name.to_s] || self.properties[name].value)
148
+ end
149
+ instance_variable_get(:"@#{name}")
132
150
  end
133
151
 
134
152
  def _properties
@@ -146,7 +164,11 @@ module Dolly
146
164
  end
147
165
 
148
166
  def initialize_default_properties options
149
- _properties.reject { |property| options.keys.include? property.name }.each { |property| self.doc[property.name] ||= property.default }
167
+ _properties.reject { |property| options.keys.include? property.name }.each do |property|
168
+ property_value = property.default.clone unless Dolly::Property::CANT_CLONE.any? { |klass| property.default.is_a? klass }
169
+ property_value ||= property.default
170
+ self.doc[property.name] ||= property_value
171
+ end
150
172
  end
151
173
 
152
174
  def init_doc options
@@ -163,5 +185,7 @@ module Dolly
163
185
  def properties_include? property
164
186
  _properties.map(&:name).include? property
165
187
  end
188
+
189
+ def valid?; true; end
166
190
  end
167
191
  end
@@ -4,10 +4,13 @@ module Dolly
4
4
  attr_accessor :name
5
5
  attr_reader :class_name, :default
6
6
 
7
+ CANT_CLONE = [NilClass, TrueClass, FalseClass, Fixnum].freeze
8
+
7
9
  def initialize opts = {}
8
10
  @class_name = opts.delete(:class_name) if opts.present?
9
11
  @name = opts.delete(:name).to_s
10
12
  @default = opts.delete(:default)
13
+ @default = @default.clone if @default && CANT_CLONE.none? { |klass| @default.is_a? klass }
11
14
  @value = @default if @default
12
15
  warn 'There are some unprocessed options!' if opts.present?
13
16
  end
data/lib/dolly/request.rb CHANGED
@@ -40,6 +40,11 @@ module Dolly
40
40
  request :delete, full_path(resource), {}
41
41
  end
42
42
 
43
+ def attach resource, attachment_name, data, headers = {}
44
+ data = StringIO.new(data) if data.is_a?(String)
45
+ request :put, attachment_path(resource, attachment_name), {body: data, headers: headers}
46
+ end
47
+
43
48
  def protocol
44
49
  @protocol || 'http'
45
50
  end
@@ -61,8 +66,8 @@ module Dolly
61
66
  response = self.class.send method, resource, data.merge(headers: headers)
62
67
  if response.code == 404
63
68
  raise Dolly::ResourceNotFound
64
- elsif (500..600).include? response.code
65
- raise Dolly::ServerError
69
+ elsif (400..600).include? response.code
70
+ raise Dolly::ServerError.new( response )
66
71
  else
67
72
  response
68
73
  end
@@ -88,6 +93,10 @@ module Dolly
88
93
  def full_path resource
89
94
  "/#{database_name}/#{resource}"
90
95
  end
96
+
97
+ def attachment_path resource, attachment_name
98
+ "#{full_path(resource)}/#{attachment_name}"
99
+ end
91
100
  end
92
101
 
93
102
  end
@@ -4,13 +4,16 @@ module Dolly
4
4
  def timestamps!
5
5
  property :created_at, :updated_at, class_name: DateTime
6
6
 
7
+ self.class_variable_set :@@timestamps, true
8
+
7
9
  Dolly::Document.class_eval do
10
+
8
11
  def set_created_at
9
- self.created_at ||= DateTime.now
12
+ doc['created_at'] ||= DateTime.now
10
13
  end
11
14
 
12
15
  def set_updated_at
13
- self.updated_at = DateTime.now
16
+ doc['updated_at'] = DateTime.now
14
17
  end
15
18
  end
16
19
  end
data/lib/dolly/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module Dolly
2
- VERSION = "0.9.0"
2
+ VERSION = "1.0.0"
3
3
  end
@@ -5,8 +5,12 @@ module Dolly
5
5
  end
6
6
  end
7
7
  class ServerError < RuntimeError
8
+ def initialize msg
9
+ @msg = msg
10
+ end
11
+
8
12
  def to_s
9
- 'There has been an error on the couchdb server. Please review your couch logs.'
13
+ "There has been an error on the couchdb server: #{@msg.inspect}"
10
14
  end
11
15
  end
12
16
  class MissingDesignError < RuntimeError
@@ -29,4 +33,5 @@ module Dolly
29
33
  "Trying to set an undefined property."
30
34
  end
31
35
  end
36
+ class DocumentInvalidError < RuntimeError; end
32
37
  end
@@ -1,6 +1,8 @@
1
1
  require 'test_helper'
2
2
 
3
- class FooBar < Dolly::Document
3
+ class BaseDolly < Dolly::Document; end
4
+
5
+ class FooBar < BaseDolly
4
6
  property :foo, :bar
5
7
  end
6
8
 
@@ -1,12 +1,19 @@
1
1
  require 'test_helper'
2
2
 
3
- class FooBar < Dolly::Document
3
+ class BaseDolly < Dolly::Document; end
4
+
5
+ class BarFoo < BaseDolly
6
+ property :a, :b, :c, :d, :e, :f, :g, :h, :i, :j, :k, :l, :m, :n, :persist
7
+ end
8
+
9
+ class FooBar < BaseDolly
4
10
  property :foo, :bar
5
11
  property :with_default, default: 1
6
12
  property :boolean, class_name: TrueClass, default: true
7
13
  property :date, class_name: Date
8
14
  property :time, class_name: Time
9
15
  property :datetime, class_name: DateTime
16
+ property :is_nil, class_name: NilClass, default: nil
10
17
 
11
18
  timestamps!
12
19
  end
@@ -22,10 +29,26 @@ class FooBaz < Dolly::Document
22
29
  end
23
30
  end
24
31
 
32
+ class WithTime < Dolly::Document
33
+ property :created_at
34
+ end
35
+
25
36
  class TestFoo < Dolly::Document
26
37
  property :default_test_property, class_name: String, default: 'FOO'
27
38
  end
28
39
 
40
+ class DocumentWithValidMethod < Dolly::Document
41
+ property :foo
42
+
43
+ def valid?
44
+ foo.present?
45
+ end
46
+ end
47
+
48
+ class DocWithSameDefaults < Dolly::Document
49
+ property :foo, :bar, class_name: Array, default: []
50
+ end
51
+
29
52
  class DocumentTest < ActiveSupport::TestCase
30
53
  DB_BASE_PATH = "http://localhost:5984/test".freeze
31
54
 
@@ -55,6 +78,7 @@ class DocumentTest < ActiveSupport::TestCase
55
78
  FakeWeb.register_uri :get, "#{query_base_path}?keys=%5B%22foo_bar%2Ferror%22%5D&include_docs=true", body: 'error', status: ["500", "Error"]
56
79
  FakeWeb.register_uri :get, "#{query_base_path}?keys=%5B%22foo_bar%2F1%22%2C%22foo_bar%2F2%22%5D&include_docs=true", body: @multi_resp.to_json
57
80
  FakeWeb.register_uri :get, "#{query_base_path}?keys=%5B%22foo_bar%2F2%22%5D&include_docs=true", body: not_found_resp.to_json
81
+ FakeWeb.register_uri :get, "#{query_base_path}?keys=%5B%22foo_bar%2Fbig_doc%22%5D&include_docs=true", body: build_view_response([data.merge(other_property: 'other')]).to_json
58
82
  end
59
83
 
60
84
  test 'new in memory document' do
@@ -284,7 +308,7 @@ class DocumentTest < ActiveSupport::TestCase
284
308
  test 'trying to update invalid property' do
285
309
  foo = FooBar.new 'id' => 'a', foo: 'ab', bar: 'ba'
286
310
  assert_raise Dolly::InvalidProperty do
287
- foo.update_properties key_to_success: false
311
+ foo.update_properties key_to_success: false
288
312
  end
289
313
  end
290
314
 
@@ -348,6 +372,101 @@ class DocumentTest < ActiveSupport::TestCase
348
372
  assert_equal 'bar', test_foo.doc['default_test_property']
349
373
  end
350
374
 
375
+ test 'doc and method and instance var are the same' do
376
+ test_foo = FooBar.new
377
+ test_foo.foo = 'test_value'
378
+ assert_equal 'test_value', test_foo.foo
379
+ assert_equal 'test_value', test_foo.doc['foo']
380
+ assert_equal 'test_value', test_foo.instance_variable_get(:@foo)
381
+ end
382
+
383
+ test 'created at is current time' do
384
+ resp = {ok: true, id: "with_time/timed", rev: "FF0000"}
385
+ FakeWeb.register_uri :put, /http:\/\/localhost:5984\/test\/with_time%2F.+/, body: resp.to_json
386
+ test = WithTime.new id: "timed"
387
+ assert test.respond_to?(:created_at)
388
+ assert test.save
389
+ assert test.created_at
390
+ end
391
+
392
+ test 'nil default' do
393
+ properties = {foo: nil, is_nil: nil}
394
+ resp = {ok: true, id: "foo_bar/1", rev: "FF0000"}
395
+ FakeWeb.register_uri :put, /http:\/\/localhost:5984\/test\/foo_bar%2F.+/, body: resp.to_json
396
+ foo = FooBar.new properties
397
+ foo.save
398
+ properties.each do |k, v|
399
+ assert_equal v, foo[k]
400
+ end
401
+ end
402
+
403
+ test 'setting on instance value does set it for other instances' do
404
+ foo = FooBar.new
405
+ foo.bar = 'I belong to the foo, not the bar'
406
+ bar = FooBar.new
407
+ assert_not_equal foo.bar, bar.bar
408
+ end
409
+
410
+ test 'subclass raises DocumentInvalidError if valid? fails' do
411
+ foo = DocumentWithValidMethod.new
412
+ assert_raise Dolly::DocumentInvalidError do
413
+ foo.save!
414
+ end
415
+ end
416
+
417
+ test 'save returns false for invalid document on save' do
418
+ foo = DocumentWithValidMethod.new
419
+ assert_not foo.save
420
+ end
421
+
422
+ test 'save succeeds for invalid document if skipping validations' do
423
+ resp = {ok: true, id: "document_with_valid_method/1", rev: "FF0000"}
424
+ FakeWeb.register_uri :put, /http:\/\/localhost:5984\/test\/document_with_valid_method%2F.+/, body: resp.to_json
425
+ foo = DocumentWithValidMethod.new
426
+ assert foo.save(validate: false)
427
+ end
428
+
429
+ test 'default objects are not the same in memory' do
430
+ doc_with_same_default = DocWithSameDefaults.new
431
+ assert_not doc_with_same_default.foo.equal? doc_with_same_default.bar
432
+ doc_with_same_default.foo.push 'foo'
433
+ assert doc_with_same_default.bar == []
434
+ end
435
+
436
+ test 'default properties do not update the class default properties' do
437
+ doc = DocWithSameDefaults.new
438
+ assert_equal [], DocWithSameDefaults.properties[:foo].default
439
+ assert doc.foo.push 'foo'
440
+ assert_equal ['foo'], doc.foo
441
+ assert_equal [], DocWithSameDefaults.properties[:foo].default
442
+ assert second_doc = DocWithSameDefaults.new
443
+ assert_equal [], second_doc.foo
444
+ end
445
+
446
+ test 'attach_file! will add a standalone attachment to the document' do
447
+ assert save_response = {ok: true, id: "base_dolly/79178957-96ff-40d9-9ecb-217fa35bdea7", rev: "1"}
448
+ assert FakeWeb.register_uri :put, /http:\/\/localhost:5984\/test\/base_dolly%2F.+/, body: save_response.to_json
449
+ assert doc = BaseDolly.new
450
+ assert doc.save
451
+ assert resp = {ok: true, id: '79178957-96ff-40d9-9ecb-217fa35bdea7', rev: '2'}
452
+ assert FakeWeb.register_uri :put, /http:\/\/localhost:5984\/test\/base_dolly\/79178957-96ff-40d9-9ecb-217fa35bdea7\/test.txt/, body: resp.to_json
453
+ assert data = File.open("#{FileUtils.pwd}/test/support/test.txt").read
454
+ assert doc.attach_file! 'test.txt', 'text/plain', data
455
+ end
456
+
457
+ test 'attach_file! will add an inline attachment if specified' do
458
+ assert save_response = {ok: true, id: "base_dolly/79178957-96ff-40d9-9ecb-217fa35bdea7", rev: "1"}
459
+ assert FakeWeb.register_uri :put, /http:\/\/localhost:5984\/test\/base_dolly%2F.+/, body: save_response.to_json
460
+ assert doc = BaseDolly.new
461
+ assert doc.save
462
+ assert resp = {ok: true, id: '79178957-96ff-40d9-9ecb-217fa35bdea7', rev: '2'}
463
+ assert FakeWeb.register_uri :put, /http:\/\/localhost:5984\/test\/base_dolly\/79178957-96ff-40d9-9ecb-217fa35bdea7\/test.txt/, body: resp.to_json
464
+ assert data = File.open("#{FileUtils.pwd}/test/support/test.txt").read
465
+ assert doc.attach_file! 'test.txt', 'text/plain', data, inline: true
466
+ assert doc.doc['_attachments']['test.txt'].present?
467
+ assert_equal Base64.encode64(data), doc.doc['_attachments']['test.txt']['data']
468
+ end
469
+
351
470
  private
352
471
  def generic_response rows, count = 1
353
472
  {total_rows: count, offset:0, rows: rows}