dolly 0.0.1

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 (49) hide show
  1. checksums.yaml +7 -0
  2. data/Rakefile +11 -0
  3. data/lib/dolly.rb +51 -0
  4. data/lib/dolly/collection.rb +57 -0
  5. data/lib/dolly/connection.rb +15 -0
  6. data/lib/dolly/document.rb +114 -0
  7. data/lib/dolly/property.rb +67 -0
  8. data/lib/dolly/representations/collection_representation.rb +15 -0
  9. data/lib/dolly/representations/document_representation.rb +19 -0
  10. data/lib/dolly/request.rb +58 -0
  11. data/lib/dolly/version.rb +3 -0
  12. data/lib/exceptions/dolly.rb +12 -0
  13. data/test/dolly_test.rb +147 -0
  14. data/test/dummy/README.rdoc +28 -0
  15. data/test/dummy/Rakefile +6 -0
  16. data/test/dummy/app/assets/javascripts/application.js +13 -0
  17. data/test/dummy/app/assets/stylesheets/application.css +13 -0
  18. data/test/dummy/app/controllers/application_controller.rb +5 -0
  19. data/test/dummy/app/helpers/application_helper.rb +2 -0
  20. data/test/dummy/app/views/layouts/application.html.erb +14 -0
  21. data/test/dummy/bin/bundle +3 -0
  22. data/test/dummy/bin/rails +4 -0
  23. data/test/dummy/bin/rake +4 -0
  24. data/test/dummy/config.ru +4 -0
  25. data/test/dummy/config/application.rb +27 -0
  26. data/test/dummy/config/boot.rb +5 -0
  27. data/test/dummy/config/couchdb.yml +15 -0
  28. data/test/dummy/config/environment.rb +5 -0
  29. data/test/dummy/config/environments/development.rb +29 -0
  30. data/test/dummy/config/environments/production.rb +80 -0
  31. data/test/dummy/config/environments/test.rb +36 -0
  32. data/test/dummy/config/initializers/backtrace_silencers.rb +7 -0
  33. data/test/dummy/config/initializers/filter_parameter_logging.rb +4 -0
  34. data/test/dummy/config/initializers/inflections.rb +16 -0
  35. data/test/dummy/config/initializers/mime_types.rb +5 -0
  36. data/test/dummy/config/initializers/secret_token.rb +12 -0
  37. data/test/dummy/config/initializers/session_store.rb +3 -0
  38. data/test/dummy/config/initializers/wrap_parameters.rb +14 -0
  39. data/test/dummy/config/locales/en.yml +23 -0
  40. data/test/dummy/config/routes.rb +56 -0
  41. data/test/dummy/lib/couch_rest_adapter/railtie.rb +10 -0
  42. data/test/dummy/log/test.log +12660 -0
  43. data/test/dummy/public/404.html +58 -0
  44. data/test/dummy/public/422.html +58 -0
  45. data/test/dummy/public/500.html +57 -0
  46. data/test/dummy/public/favicon.ico +0 -0
  47. data/test/factories/factories.rb +8 -0
  48. data/test/test_helper.rb +37 -0
  49. metadata +239 -0
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: ea68df6004ba83c19215952ce9a3ce9e19169ec3
4
+ data.tar.gz: 835ecc53373d834ff7a8c05173200911490d7e4e
5
+ SHA512:
6
+ metadata.gz: 40468de07bee1d6412e8dcae7b96f06d85ab9a7c8e1c7a41158a648f922cbb08b0b9c7ac3e54b30bf98a23647597f30d1c603dafabe784db61997323f5e12a9c
7
+ data.tar.gz: 30ddf8c41e68c48645f899d145aa439f6c4bde64218f82e048ba3af8aa85dc614fe7c18eeda6973faee88345cea4932c1ad07b4d4671c068d71c5416a4aa7f18
@@ -0,0 +1,11 @@
1
+ require "bundler/gem_tasks"
2
+ require 'rake/testtask'
3
+
4
+ Rake::TestTask.new(:test) do |t|
5
+ t.libs << 'lib'
6
+ t.libs << 'test'
7
+ t.pattern = 'test/**/*_test.rb'
8
+ t.verbose = false
9
+ end
10
+
11
+ task default: :test
@@ -0,0 +1,51 @@
1
+ require "dolly/version"
2
+ require "dolly/document"
3
+ require "dolly/property"
4
+
5
+ module Dolly
6
+
7
+ class Base
8
+ include Dolly::Document
9
+ attr_accessor :rows, :doc, :key
10
+
11
+ def rows= col
12
+ col.each{ |r| @doc = r['doc'] }
13
+ _properties.each do |p|
14
+ self.send "#{p.name}=", doc[p.name]
15
+ end
16
+ @rows = col
17
+ end
18
+
19
+ def self.property *ary
20
+ options = ary.pop if ary.last.kind_of? Hash
21
+ options ||= {}
22
+ @properties ||= []
23
+
24
+ @properties += ary.map do |name|
25
+ options.merge!({name: name})
26
+ property = Property.new(options)
27
+
28
+ define_method(name) do
29
+ property.value = @doc[name.to_s]
30
+ property.value
31
+ end
32
+
33
+ define_method("#{name}=") do |val|
34
+ @doc ||={}
35
+ @doc[name.to_s] = val
36
+ end
37
+
38
+ define_method(:"#{name}?") { send name } if property.boolean?
39
+ define_method("[]") {|n| send n.to_sym}
40
+
41
+ property
42
+ end
43
+ end
44
+
45
+ private
46
+ def _properties
47
+ self.class.properties
48
+ end
49
+ end
50
+
51
+ end
@@ -0,0 +1,57 @@
1
+ require 'dolly/representations/collection_representation'
2
+
3
+ module Dolly
4
+ class Collection
5
+ extend Forwardable
6
+ attr_accessor :rows
7
+ attr_writer :json, :docs_class
8
+
9
+ def_delegators :@set, :clear, :empty?, :length, :+, :-
10
+
11
+ def initialize str, docs_class
12
+ @set = Set.new
13
+ @docs_class = docs_class
14
+ @json = str
15
+ end
16
+
17
+ def map &block
18
+ load if empty?
19
+ @set.collect &block
20
+ end
21
+
22
+ def each &block
23
+ load if empty?
24
+ @set.each &block
25
+ #TODO: returning nil to avoid extra time serializing set.
26
+ nil
27
+ end
28
+
29
+ def to_a
30
+ load if empty?
31
+ @set.to_a
32
+ end
33
+
34
+ def count
35
+ load if empty?
36
+ length
37
+ end
38
+
39
+ def load
40
+ @set = self.extend(representation).from_json(json).rows
41
+ end
42
+
43
+ private
44
+ def representation
45
+ Representations::CollectionRepresentation.config(docs_class)
46
+ end
47
+
48
+ def docs_class
49
+ @docs_class
50
+ end
51
+
52
+ def json
53
+ @json
54
+ end
55
+
56
+ end
57
+ end
@@ -0,0 +1,15 @@
1
+ require "dolly/request"
2
+
3
+ module Dolly
4
+ module Connection
5
+
6
+ def database
7
+ @database ||= Request.new(database_name: @@database_name)
8
+ end
9
+
10
+ def database_name value
11
+ @@database_name ||= value
12
+ end
13
+
14
+ end
15
+ end
@@ -0,0 +1,114 @@
1
+ require "active_model/naming"
2
+ require "dolly/connection"
3
+ require "dolly/collection"
4
+ require "dolly/representations/document_representation"
5
+ require "dolly/representations/collection_representation"
6
+ require "exceptions/dolly"
7
+
8
+ module Dolly
9
+ module Document
10
+
11
+ def id
12
+ doc['_id']
13
+ end
14
+
15
+ def rev
16
+ doc['_rev']
17
+ end
18
+
19
+ def save
20
+ response = database.put(id_as_resource, self.doc.to_json)
21
+ obj = JSON::parse response.parsed_response
22
+ doc['_rev'] = obj['rev'] if obj['rev']
23
+ obj['ok']
24
+ end
25
+
26
+ def save!
27
+ #TODO: decide how to handle validations...
28
+ save
29
+ end
30
+
31
+ def from_json string
32
+ self.class.new.extend(representation).from_json( string )
33
+ end
34
+
35
+ def database
36
+ self.class.database
37
+ end
38
+
39
+ def id_as_resource
40
+ CGI::escape id
41
+ end
42
+
43
+ def representation
44
+ Representations::DocumentRepresentation.config(self.class.properties)
45
+ end
46
+
47
+ module ClassMethods
48
+ include ActiveModel::Naming
49
+ include Dolly::Connection
50
+ attr_accessor :properties
51
+
52
+ DESIGN_DOC = "dolly"
53
+
54
+ def find *ids
55
+ response = default_view(keys: ids.map{|id| namespace(id)}).parsed_response
56
+ ids.count > 1 ? Collection.new(response, name.constantize) : self.new.from_json(response)
57
+ rescue NoMethodError => err
58
+ if err.message == "undefined method `[]' for nil:NilClass"
59
+ raise Dolly::ResourceNotFound
60
+ else
61
+ raise
62
+ end
63
+ end
64
+
65
+ def all
66
+ Collection.new default_view.parsed_response, name.constantize
67
+ end
68
+
69
+ def default_view options = {}
70
+ view default_doc, options
71
+ end
72
+
73
+ def view doc, options = {}
74
+ options.merge! include_docs: true
75
+ database.get doc, options
76
+ end
77
+
78
+ def default_doc
79
+ "#{design_doc}/_view/#{name_paramitized}"
80
+ end
81
+
82
+ def design_doc
83
+ "_design/#{@@design_doc || DESIGN_DOC}"
84
+ end
85
+
86
+ def set_design_doc value
87
+ @@design_doc = value
88
+ end
89
+
90
+ def name_paramitized
91
+ model_name.param_key
92
+ end
93
+
94
+ def namespace id
95
+ return id if id =~ /^#{name_paramitized}/
96
+ "#{name_paramitized}/#{id}"
97
+ end
98
+
99
+ def timestamps!
100
+ %i/created_at updated_at/.each do |method|
101
+ define_method(method){ @doc[method.to_s] ||= DateTime.now }
102
+ define_method(:"[]"){|m| self.send(m.to_sym) }
103
+ define_method(:"[]="){|m, v| self.send(:"#{m}=", v) }
104
+ define_method(:"#{method}="){|val| @doc[method.to_s] = val }
105
+ end
106
+ end
107
+
108
+ end
109
+
110
+ def self.included(base)
111
+ base.extend ClassMethods
112
+ end
113
+ end
114
+ end
@@ -0,0 +1,67 @@
1
+ module Dolly
2
+ class Property
3
+ attr_writer :value
4
+ attr_accessor :name
5
+
6
+ def initialize opts = {}
7
+ @class_name = opts.delete(:class_name) if opts.present?
8
+ @name = opts.delete(:name).to_s
9
+ @default = opts.delete(:default)
10
+ warn 'There are some unprocesed options' if opts.present?
11
+ end
12
+
13
+ def value
14
+ return @default if @value.nil?
15
+ return @value unless self_klass
16
+
17
+ klass_sym = :"#{self_klass.name.underscore}_#{__method__}"
18
+
19
+ return self_klass.new @value unless self.respond_to?(klass_sym)
20
+
21
+ self.send klass_sym
22
+ end
23
+
24
+ def array_value
25
+ @value.to_a
26
+ end
27
+
28
+ def hash_value
29
+ @value.to_h
30
+ end
31
+
32
+ def string_value
33
+ @value.to_s
34
+ end
35
+
36
+ def integer_value
37
+ @value.to_i
38
+ end
39
+
40
+ def float_value
41
+ @value.to_f
42
+ end
43
+
44
+ def true_class_value
45
+ truthy_value?
46
+ end
47
+
48
+ def false_class_value
49
+ truthy_value?
50
+ end
51
+
52
+ def boolean?
53
+ self_klass == TrueClass || self_klass == FalseClass
54
+ end
55
+
56
+ private
57
+ def truthy_value?
58
+ @value =~ /true/ || @value === true
59
+ end
60
+
61
+ def self_klass
62
+ return unless @class_name
63
+ @class_name.is_a?(Class)? @class_name : @class_name.constantize
64
+ end
65
+
66
+ end
67
+ end
@@ -0,0 +1,15 @@
1
+ require 'representable/json'
2
+
3
+ module Dolly
4
+ module Representations
5
+ module CollectionRepresentation
6
+ include Representable::JSON
7
+
8
+ def self.config docs_class
9
+ self.collection :rows, extend: DocumentRepresentation, class: docs_class
10
+ self
11
+ end
12
+ end
13
+ end
14
+ end
15
+
@@ -0,0 +1,19 @@
1
+ require 'representable/json'
2
+
3
+ module Dolly
4
+ module Representations
5
+ module DocumentRepresentation
6
+ include Representable::JSON
7
+
8
+ property :rows
9
+ property :doc
10
+
11
+ def self.config properties
12
+ properties.each do |p|
13
+ self.property p.name
14
+ end
15
+ self
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,58 @@
1
+ require "httparty"
2
+
3
+ module Dolly
4
+
5
+ class Request
6
+ include HTTParty
7
+ DEFAULT_HOST = 'localhost'
8
+ DEFAULT_PORT = '5984'
9
+
10
+ attr_accessor :database_name, :host, :port
11
+
12
+ def initialize options = {}
13
+ @host = options[:host] || DEFAULT_HOST
14
+ @port = options[:port] || DEFAULT_PORT
15
+ @database_name = options[:database_name]
16
+
17
+ self.class.base_uri "#{protocol}://#{host}:#{port}"
18
+ end
19
+
20
+ def get resource, data
21
+ request :get, resource, {query: values_to_json(data)}
22
+ end
23
+
24
+ def put resource, data
25
+ request :put, resource, {body: data}
26
+ end
27
+
28
+ def delete resource
29
+ request :delete, resource, {}
30
+ end
31
+
32
+ def protocol
33
+ @protocol || 'http'
34
+ end
35
+
36
+ private
37
+ def values_to_json hash
38
+ hash.reduce({}){|h, v| h[v.first] = v.last.to_json; h}
39
+ end
40
+
41
+ def full_path resource
42
+ "/#{database_name}/#{resource}"
43
+ end
44
+
45
+ def request method, resource, data = nil
46
+ headers = { 'Content-Type' => 'application/json' }
47
+ response = self.class.send method, full_path(resource), data.merge(headers: headers)
48
+ if response.code == 404
49
+ raise Dolly::ResourceNotFound
50
+ elsif (500..600).include? response.code
51
+ raise Dolly::ServerError
52
+ else
53
+ response
54
+ end
55
+ end
56
+ end
57
+
58
+ end
@@ -0,0 +1,3 @@
1
+ module Dolly
2
+ VERSION = "0.0.1"
3
+ end
@@ -0,0 +1,12 @@
1
+ module Dolly
2
+ class ResourceNotFound < RuntimeError
3
+ def to_s
4
+ 'The document was not found.'
5
+ end
6
+ end
7
+ class ServerError < RuntimeError
8
+ def to_s
9
+ 'There has been an error on the couchdb server. Please review your couch logs.'
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,147 @@
1
+ require 'test_helper'
2
+
3
+ class FooBar < Dolly::Base
4
+ database_name 'test'
5
+ set_design_doc 'test'
6
+
7
+ property :foo, :bar
8
+ property :with_default, default: 1
9
+ property :boolean, class_name: TrueClass, default: true
10
+
11
+ timestamps!
12
+ end
13
+
14
+ class DollyTest < ActiveSupport::TestCase
15
+
16
+ def setup
17
+ data = {foo: 'Foo', bar: 'Bar', type: 'foo_bar'}
18
+ all_docs = [ {foo: 'Foo B', bar: 'Bar B', type: 'foo_bar'}, {foo: 'Foo A', bar: 'Bar A', type: 'foo_bar'}]
19
+
20
+ view_resp = build_view_response [data]
21
+ empty_resp = build_view_response []
22
+ @multi_resp = build_view_response all_docs
23
+
24
+ build_request ["foo_bar/1"], view_resp
25
+ build_request ["foo_bar/2"], empty_resp
26
+ build_request ["foo_bar/1","foo_bar/2"], @multi_resp
27
+ build_request [], @multi_resp
28
+ end
29
+
30
+ test 'with timestamps!' do
31
+ later = DateTime.new 1963, 1, 1
32
+ now = DateTime.now
33
+ DateTime.stubs(:now).returns(now)
34
+ foo = FooBar.find "1"
35
+ assert_equal now, foo.created_at
36
+ assert_equal now, foo['created_at']
37
+ assert_equal now, foo.updated_at
38
+ assert foo.updated_at = later
39
+ assert_equal later, foo.updated_at
40
+ assert foo['created_at'] = later
41
+ assert_equal later, foo['created_at']
42
+ assert_equal foo['created_at'], foo.created_at
43
+ end
44
+
45
+ test 'empty find should raise error' do
46
+ assert_raise Dolly::ResourceNotFound do
47
+ FakeWeb.register_uri :get, "http://localhost:5984/test/_design/test/_view/foo_bar?keys=%5B%5D&include_docs=true", :status => ["404", "Not Found"]
48
+ foo = FooBar.find
49
+ end
50
+ end
51
+
52
+ test 'error on server raises Dolly::ServerError' do
53
+ assert_raise Dolly::ServerError do
54
+ FakeWeb.register_uri :get, "http://localhost:5984/test/_design/test/_view/foo_bar?keys=%5B%5D&include_docs=true", :status => ["500", "Error"]
55
+ foo = FooBar.find
56
+ end
57
+ end
58
+
59
+ test 'will have object with boolean? method' do
60
+ foo = FooBar.find "1"
61
+ assert_equal true, foo.boolean?
62
+ foo.boolean = false
63
+ assert_equal false, foo['boolean']
64
+ assert_equal false, foo.boolean?
65
+ end
66
+
67
+ test 'find will get a FooBar document' do
68
+ foo = FooBar.find "1"
69
+ assert_equal true, foo.kind_of?(FooBar)
70
+ end
71
+
72
+ test 'will have key properties' do
73
+ foo = FooBar.find "1"
74
+ assert_equal 'Foo', foo['foo']
75
+ assert_equal 'Bar', foo['bar']
76
+ end
77
+
78
+ test 'will have only set properties' do
79
+ foo = FooBar.find "1"
80
+ assert_equal 'Foo', foo.foo
81
+ assert_equal 'Bar', foo.bar
82
+ assert_equal false, foo.respond_to?(:type)
83
+ end
84
+
85
+ test 'with default will return default value on nil' do
86
+ foo = FooBar.find "1"
87
+ assert_equal 1, foo.with_default
88
+ end
89
+
90
+ test 'default will be avoerwriten' do
91
+ foo = FooBar.find "1"
92
+ assert_equal 1, foo.with_default
93
+ assert foo.with_default = 30
94
+ assert_equal 30, foo.with_default
95
+ end
96
+
97
+ test 'getting not found document' do
98
+ assert_raise Dolly::ResourceNotFound do
99
+ missing = FooBar.find "2"
100
+ end
101
+ end
102
+
103
+ test 'find with multiple ids will return Collection' do
104
+ many = FooBar.find "1", "2"
105
+ assert_equal true, many.kind_of?(Dolly::Collection)
106
+ end
107
+
108
+ test 'all will get 2 docs' do
109
+ all = FooBar.all
110
+ assert_equal 2, all.count
111
+ end
112
+
113
+ test 'all will get FooBar documents' do
114
+ all = FooBar.all
115
+ all.each{ |d| assert_equal true, d.kind_of?(FooBar) }
116
+ end
117
+
118
+ test 'multi response with right data' do
119
+ all = FooBar.all
120
+ ids = @multi_resp[:rows].map{|d| d[:id]}
121
+ foos = @multi_resp[:rows].map{|d| d[:doc][:foo]}
122
+ bars = @multi_resp[:rows].map{|d| d[:doc][:bar]}
123
+ all.each do |d|
124
+ assert foos.include?(d.foo)
125
+ end
126
+ end
127
+
128
+ private
129
+ def build_view_response properties
130
+ rows = properties.map.with_index do |v, i|
131
+ {
132
+ id: "foo_bar/#{i}",
133
+ key: "foo_bar",
134
+ value: 1,
135
+ doc: {_id: "foo_bar/#{i}", _rev: SecureRandom.hex}.merge!(v)
136
+ }
137
+ end
138
+ {total_rows: properties.count, offset:0, rows: rows}
139
+ end
140
+
141
+ def build_request keys, body, view_name = 'foo_bar'
142
+ base_url = "http://localhost:5984/test/_design/test/_view/foo_bar"
143
+ query = "keys=#{CGI::escape keys.to_s.gsub(' ','')}&" unless keys.blank?
144
+ FakeWeb.register_uri :get, "#{base_url}?#{query.to_s}include_docs=true", body: body.to_json
145
+ end
146
+
147
+ end