addywaddy-couch_surfer 0.0.2 → 0.0.4

Sign up to get free protection for your applications and to get access to all the features.
data/CHANGELOG.md CHANGED
@@ -1,7 +1,19 @@
1
+ 0.0.4 (2009-02-04)
2
+ ------------------
3
+ - `has_many :through`
4
+ - `has_many :inline`
5
+ - `create` was in 0.0.2 but forgot to mention it :)
6
+ - `all` now emits (id, null) so you can use `:keys => [1,2,3,4]` in your query
7
+ - `format_utc_offset` helper (zdzolton)
8
+ - Syntax change:
9
+ - OLD: `:view => {:name => :by_this_and_that, :query => lambda { {:key => [this_id, id]} }}`
10
+ - NEW: `:view => :by_this_and_that, :query => lambda { {:key => [this_id, id]} }`
11
+
1
12
  0.0.2 (2009-01-24)
2
13
  ------------------
3
14
  - `timestamps!` class method now adds instance methods
4
15
  - `created_at` and `updated_at` always return an instance of `Time`
16
+
5
17
  0.0.1 (2009-01-21)
6
18
  ------------------
7
- - Initial Release
19
+ - Initial Release
data/README.md CHANGED
@@ -1,49 +1,114 @@
1
- CouchSurfer
2
- ===========
3
-
1
+ CouchSurfer - CouchDB ORM
2
+ =========================
3
+ ---
4
4
  Description
5
5
  -----------
6
- CouchSurfer is an extraction of CouchRest::Model from the excellent [CouchRest](http://github.com/jchris/couchrest/ "CouchRest") gem by J. Chris Anderson.
6
+ CouchSurfer is an extraction of CouchRest::Model from the excellent [CouchRest](http://github.com/jchris/couchrest/ "CouchRest") gem by J. Chris Anderson. In addition, it provides association and validation methods.
7
+
8
+ Associations
9
+ ------------
10
+ CouchSurfer provides the following 4 association kinds:
11
+
12
+ - `belongs_to`
13
+ - `has_many`
14
+ - `has_many :inline`; and
15
+ - `has_many :through`
16
+
17
+ All association kinds take an optional `:class_name` option should you want your association to be named differently to the associated model.
18
+
19
+ class Page
20
+
21
+ belongs_to :owner, :class_name => :user
22
+
23
+ end
24
+
25
+ page = Page.create(…)
26
+ page.owner # returns an instance of user
27
+
28
+ The `belongs_to` associations accept two additional options - `:view` and `query`, enabling you to customise your associations to fit your needs. You must explicitly declare the view on the child model for associations to work
29
+
30
+ **Example 1: basic**
7
31
 
8
- Features
9
- --------
10
- - ORM (Extracted from CouchRest::Model)
11
- - Associations
12
- - `has_ many`
13
- - `belongs_to`
32
+ class User
33
+
34
+ has_many :pages
35
+
36
+ end
37
+
38
+ class Page
39
+
40
+ view_by :user_id
41
+
42
+ end
43
+
44
+ user = User.create(…)
45
+ 10.times {Page.create(…, :user_id => user.id)}
46
+ user.pages
47
+
14
48
 
15
- - Validations
16
- - All validations from the [Validatable](http://github.com/jrun/validatable/ "Validatable") gem
17
- - `validates_uniqueness_of`
49
+ **Example 2: with options**
18
50
 
19
- Examples
20
- --------
21
51
  class Account
22
- include CouchSurfer::Model
23
- include CouchSurfer::Associations
24
-
25
- key_accessor :name
26
-
27
- # Will use the Project.by_account_id view with {:key => account_instance.id}
28
- has_many :projects
29
-
30
- # Uses a custom view and key
31
- has_many :employees, :view => {:name => :by_account_id_and_email,
32
- :query => lambda{ {:startkey => [id, nil], :endkey => [id, {}]} }}
52
+
53
+ has_many :employees,
54
+ :class_name, :user,
55
+ :view => :by_account_id_and_email,
56
+ :query => lambda { {:startkey => [account_id, nil], :endkey => [account_id, {}]} }
57
+
58
+ end
33
59
 
60
+ class User
61
+
62
+ view_by :account_id, :email # see validation examples below
63
+
34
64
  end
35
65
 
36
- class Project
37
- include CouchSurfer::Model
38
- include CouchSurfer::Associations
39
-
40
- key_accessor :name, :account_id
41
-
42
- belongs_to :account
43
-
66
+ account = Acccount.create(…)
67
+ 10.times {User.create(…, :account_id => acount.id)}
68
+ account.employees
69
+
70
+ **Example 2: :through**
71
+
72
+ class Account
73
+
74
+ has_many :projects,
75
+ :through => :memberships
76
+
77
+ end
78
+
79
+ class Membership
80
+
44
81
  view_by :account_id
82
+
83
+ end
84
+
85
+ class Project
86
+
87
+ view_by :account_id, :email # see validation examples below
88
+
45
89
  end
46
90
 
91
+ account = Acccount.create(…)
92
+ 10.times do
93
+ p = Project.create(…)
94
+ Membership.create(…, :account_id => account.id, :project_id => p.id)
95
+ end
96
+ account.projects
97
+
98
+ **Note on HasManyThrough**
99
+
100
+ With reference to the above example, HasManyThrough works by retrieving all memberships associated with the account, collecting their ids and running a [bulk retrieval](http://wiki.apache.org/couchdb/HTTP_view_API "Query Options") on the Project.all view, which is implicitly created for all models and, as of 0.0.4, emits it's id (see CHANGELOG).
101
+
102
+ *Caveats*
103
+
104
+ - Sorting needs to be done client side
105
+ - The results do not contain any extra information that may be present on the ':through' model.
106
+
107
+ Validations
108
+ -----------
109
+ Validations, with the exception of `validates_uniqueness_of`, have been implemented using the [Validatable](http://github.com/jrun/validatable/ "Validatable") gem.
110
+
111
+ **Example**
47
112
 
48
113
  class Employee
49
114
  include CouchSurfer::Model
@@ -62,12 +127,21 @@ Examples
62
127
  validates_length_of :postcode, :is => 7, :message => "not the correct length"
63
128
 
64
129
  # Will use the Employee.by_name view with {:key => employee_instance.name}
65
- validates_uniqueness_of :name, :message => "No two Beatles have the same name"
130
+ validates_uniqueness_of :name
66
131
 
67
- # Uses a custom view and key
68
- validates_uniqueness_of :email, :view => {:name => :by_account_id_and_email,
69
- :query => lambda{ {:key => [account_id, email]} }}, :message => "Already taken!"
132
+ # Uses a custom view and key for uniqueness within a specific scope
133
+ validates_uniqueness_of :email,
134
+ :view => :by_account_id_and_email,
135
+ :query => lambda{ {:key => [account_id, email]} },
136
+ :message => "The email address for this account is taken"
70
137
 
71
138
  end
72
-
73
- Please check out the specs as well :)
139
+
140
+
141
+ Please check out the specs as well :)
142
+
143
+ Next
144
+ ----
145
+ - Error handling
146
+ - association methods with arguments:
147
+ `@account.projects(:limit => 2, :offset => 1)`
data/Rakefile CHANGED
@@ -19,7 +19,7 @@ spec = Gem::Specification.new do |s|
19
19
  s.version = CouchSurfer::VERSION
20
20
  s.date = "2009-01-22"
21
21
  s.summary = "ORM based on CouchRest::Model"
22
- s.email = "jchris@apache.org"
22
+ s.email = "adam.groves@gmail.com"
23
23
  s.homepage = "http://github.com/addywaddy/couchsurfer"
24
24
  s.description = "CouchSurfer provides an ORM for CouchDB, as well as supporting association and validation declarations."
25
25
  s.has_rdoc = true
@@ -1,40 +1,56 @@
1
1
  require 'rubygems'
2
2
  require 'extlib'
3
-
3
+ module CouchSurfer
4
+ class InlineCollection < Array
5
+ def << child
6
+ child = child.kind_of?( CouchSurfer::Model) ? child.attributes : child
7
+ super(child)
8
+ end
9
+
10
+ def delete(child)
11
+ child = child.kind_of?( CouchSurfer::Model) ? child.attributes : child
12
+ super(child)
13
+ end
14
+ end
15
+ end
4
16
  module CouchSurfer
5
17
  module Associations
6
18
  module ClassMethods
7
19
  def has_many *args
8
- options = args.last.is_a?(Hash) ? args.pop : {}
20
+ options = extract_options!(args)
9
21
  children = args.first
10
- define_method children do |*args|
11
- query_params = args.last.is_a?(Hash) ? args.pop : nil
22
+ if options[:inline]
12
23
  name = ::Extlib::Inflection.camelize(children.to_s.singular)
13
- klass = ::Extlib::Inflection.constantize(name)
14
- if options[:view].is_a?(Hash)
15
- view_name = options[:view][:name]
16
- query = options[:view][:query].is_a?(Proc) ? self.instance_eval(&options[:view][:query]) : nil
24
+ cast children, :as => [name]
25
+ before(:save) do
26
+ if self[children.to_s]
27
+ self[children.to_s].map!{|child| child.kind_of?( CouchSurfer::Model) ? child.attributes : child}
28
+ end
17
29
  end
18
- view_name ||= "by_#{self.class.name.downcase}_id"
19
- query ||= {:key => self.id}
20
- klass.send(view_name, query)
30
+ define_method children do |*args|
31
+ self[children.to_s] ||= CouchSurfer::InlineCollection.new
32
+ end
33
+ return
34
+ end
35
+ if options[:through]
36
+ define_method_for_children(options[:through], options)
37
+ define_method children do
38
+ name = options[:class_name] || children.to_s.singular
39
+ class_name = ::Extlib::Inflection.camelize(name)
40
+ klass = ::Extlib::Inflection.constantize(class_name)
41
+ through_items = self.send("#{options[:through]}")
42
+ query ||= {:keys => through_items.map{|child| child.send("#{name}_id")}}
43
+ view_name ||= "by_#{self.class.name.downcase}_id"
44
+ klass.send("all", query)
45
+ end
46
+ return
21
47
  end
48
+ define_method_for_children(children, options, options[:class_name])
22
49
  end
23
50
 
24
51
  def belongs_to *args
25
- options = args.last.is_a?(Hash) ? args.pop : {}
52
+ options = extract_options!(args)
26
53
  parent = args.first
27
- # view_key = "#{parent}_id".to_sym
28
- # if options[:identifiers]
29
- # if options[:prepend]
30
- # view_key = options[:identifiers] << view_key
31
- # else
32
- # view_key = options[:identifiers].unshift(view_key)
33
- # end
34
- # end
35
- # class_eval do
36
- # view_by *view_key
37
- # end
38
54
  define_method parent do
39
55
  name = ::Extlib::Inflection.camelize(parent.to_s)
40
56
  klass = ::Extlib::Inflection.constantize(name)
@@ -48,6 +64,28 @@ module CouchSurfer
48
64
  self["#{parent_obj.class.name.downcase}_id"] = parent_obj.id
49
65
  end
50
66
  end
67
+
68
+ private
69
+
70
+ def extract_options!(args)
71
+ args.last.is_a?(Hash) ? args.pop : {}
72
+ end
73
+
74
+ def define_method_for_children(children, options, name = nil)
75
+ class_name = ::Extlib::Inflection.camelize(name || children.to_s.singular)
76
+ define_method children do
77
+ klass = ::Extlib::Inflection.constantize(class_name)
78
+ if options[:view]
79
+ view_name = options[:view]
80
+ end
81
+ if options[:query].is_a?(Proc)
82
+ query = self.instance_eval(&options[:query])
83
+ end
84
+ view_name ||= "by_#{self.class.name.downcase}_id"
85
+ query ||= {:key => self.id}
86
+ klass.send(view_name, query)
87
+ end
88
+ end
51
89
  end
52
90
 
53
91
  def self.included(receiver)
@@ -15,6 +15,15 @@ module CouchSurfer
15
15
  @database = db
16
16
  end
17
17
 
18
+ # Adapted from ActiveSupport Time#formatted_offset
19
+ def self.format_utc_offset(time)
20
+ seconds_offset_from_utc = time.utc_offset
21
+ sign = (seconds_offset_from_utc < 0 ? -1 : 1)
22
+ hours = seconds_offset_from_utc.abs / 3600
23
+ minutes = (seconds_offset_from_utc.abs % 3600) / 60
24
+ "%+03d%02d" % [ hours * sign, minutes ]
25
+ end
26
+
18
27
  module ClassMethods
19
28
  # override the CouchSurfer::Model-wide default_database
20
29
  def use_database db
@@ -154,8 +163,8 @@ module CouchSurfer
154
163
  end
155
164
  before(:save) do
156
165
  time = Time.now
157
- usec = time.usec
158
- self['updated_at'] = time.strftime("%Y/%m/%d %H:%M:%S.#{time.usec} %z")
166
+ utc_offset = CouchSurfer::Model.format_utc_offset(time)
167
+ self['updated_at'] = time.strftime("%Y/%m/%d %H:%M:%S.#{time.usec} #{utc_offset}")
159
168
  self['created_at'] = self['updated_at'] if new_document?
160
169
  end
161
170
  end
@@ -290,7 +299,7 @@ module CouchSurfer
290
299
  # Deletes any non-current design docs that were created by this class.
291
300
  # Running this when you're deployed version of your application is steadily
292
301
  # and consistently using the latest code, is the way to clear out old design
293
- # docs. Running it to early could mean that live code has to regenerate
302
+ # docs. Running it too early could mean that live code has to regenerate
294
303
  # potentially large indexes.
295
304
  def cleanup_design_docs!
296
305
  ddocs = all_design_doc_versions
@@ -359,7 +368,7 @@ module CouchSurfer
359
368
  'all' => {
360
369
  'map' => "function(doc) {
361
370
  if (doc['couchrest-type'] == '#{self.to_s}') {
362
- emit(null,null);
371
+ emit(doc['_id'],null);
363
372
  }
364
373
  }"
365
374
  }
@@ -385,6 +394,7 @@ module CouchSurfer
385
394
  end
386
395
  self.design_doc_fresh = true
387
396
  end
397
+
388
398
  end
389
399
 
390
400
  module InstanceMethods
@@ -18,9 +18,9 @@ module CouchSurfer
18
18
 
19
19
  module InstanceMethods
20
20
  def is_unique?(field, options)
21
- if options[:view].is_a?(Hash)
22
- view_name = options[:view][:name]
23
- query = options[:view][:query].is_a?(Proc) ? self.instance_eval(&options[:view][:query]) : nil
21
+ if options[:view]
22
+ view_name = options[:view]
23
+ query = options[:query].is_a?(Proc) ? self.instance_eval(&options[:query]) : nil
24
24
  end
25
25
  view_name ||= "by_#{field}"
26
26
  query ||= {:key => self.send(field)}
data/lib/couch_surfer.rb CHANGED
@@ -2,7 +2,7 @@ $:.unshift(File.dirname(__FILE__)) unless
2
2
  $:.include?(File.dirname(__FILE__)) || $:.include?(File.expand_path(File.dirname(__FILE__)))
3
3
 
4
4
  module CouchSurfer
5
- VERSION = '0.0.2'
5
+ VERSION = '0.0.4'
6
6
  autoload :Model, 'couch_surfer/model'
7
7
  autoload :Validations, 'couch_surfer/validations'
8
8
  autoload :Associations, 'couch_surfer/associations'
@@ -3,10 +3,17 @@ require File.dirname(__FILE__) + '/../spec_helper.rb'
3
3
  class Account
4
4
  include CouchSurfer::Model
5
5
  include CouchSurfer::Associations
6
-
6
+
7
7
  key_accessor :name
8
-
9
- has_many :employees, :view => {:name => :by_account_id_and_email, :query => lambda{ {:startkey => [id, nil], :endkey => [id, {}]} }}
8
+
9
+ has_many :employees,
10
+ :view => :by_account_id_and_role,
11
+ :query => lambda{ {:startkey => [id, nil], :endkey => [id, {}]} }
12
+
13
+ has_many :programmers, :class_name => :employee,
14
+ :view => :by_account_id_and_role,
15
+ :query => lambda{ {:startkey => [id, "Programmer"], :endkey => [id, "Programmer"]} }
16
+
10
17
  has_many :projects
11
18
 
12
19
  end
@@ -18,8 +25,22 @@ class Project
18
25
  key_accessor :name, :account_id
19
26
 
20
27
  belongs_to :account
28
+ has_many :members, :through => :memberships, :class_name => :employee
21
29
 
22
30
  view_by :account_id
31
+ view_by :id
32
+ end
33
+
34
+ class Membership
35
+ include CouchSurfer::Model
36
+ include CouchSurfer::Associations
37
+
38
+ key_accessor :project_id, :employee_id
39
+ belongs_to :project
40
+ belongs_to :employee
41
+
42
+ view_by :project_id
43
+ view_by :employee_id
23
44
  end
24
45
 
25
46
 
@@ -27,48 +48,117 @@ class Employee
27
48
  include CouchSurfer::Model
28
49
  include CouchSurfer::Associations
29
50
 
30
- key_accessor :email, :account_id
51
+ key_accessor :email, :account_id, :role
52
+
31
53
  belongs_to :account
54
+ has_many :shirts, :inline => true
55
+ has_many :projects, :through => :memberships
32
56
 
33
- view_by :account_id, :email
57
+ view_by :account_id, :role
34
58
 
35
59
  end
36
60
 
61
+ class Shirt
62
+ include CouchSurfer::Model
63
+ include CouchSurfer::Associations
64
+
65
+ key_accessor :color
66
+ end
67
+
37
68
  describe CouchSurfer::Associations do
38
69
  before(:all) do
39
70
  db = CouchRest.database!('couch_surfer-test')
40
71
  db.delete!
41
72
  CouchSurfer::Model.default_database = CouchRest.database!('http://127.0.0.1:5984/couch_surfer-test')
42
73
  @account = Account.create(:name => "My Account")
43
- 5.times do |i|
44
- Employee.create(:email => "foo#{i}@bar.com", :account_id => @account.id)
74
+ 2.times do |i|
75
+ Employee.create(:email => "foo#{i+1}@bar.com", :account_id => @account.id, :role => "Programmer")
76
+ Employee.create(:email => "foo#{i+3}@bar.com", :account_id => @account.id, :role => "Engineer")
45
77
  Project.create(:name => "Project No. #{i}", :account_id => @account.id)
46
78
  end
79
+ @employee = @account.employees.first
80
+ @project = @account.projects.first
47
81
  end
48
82
 
49
- describe "An employee" do
50
- it "should return it's users" do
51
- @other_employee = Employee.create(:email => "woo@war.com", :account_id => "ANOTHER_ACCOUNT_ID")
52
- @account.employees.length.should == 5
53
- @account.employees.should_not include(@other_employee)
54
- end
55
-
56
- it "should return it's parent account" do
83
+ describe "belongs_to" do
84
+ it "should return it's parent" do
57
85
  @employee = @account.employees.first
58
86
  @employee.account.should == @account
59
87
  end
60
88
  end
61
89
 
62
- describe "A project" do
63
- it "should return it's projects" do
64
- @other_project = Project.create(:name => "Another Project", :account_id => "ANOTHER_ACCOUNT_ID")
65
- @account.projects.length.should == 5
66
- @account.projects.should_not include(@other_project)
90
+ describe "has_many" do
91
+ describe "vanilla" do
92
+ it "should return it's children" do
93
+ @account.employees.length.should == 4
94
+ @account.employees.map{|employee| employee.email}.sort.should == ["foo1@bar.com", "foo2@bar.com", "foo3@bar.com", "foo4@bar.com"]
95
+ end
96
+ end
97
+
98
+ describe ":class_name" do
99
+ it "should return it's children" do
100
+ @account.programmers.length.should == 2
101
+ end
102
+ end
103
+ describe ":inline" do
104
+ before(:all) do
105
+ @employee.shirts.clear
106
+ @blue_shirt = Shirt.create(:color => "White")
107
+ @pink_shirt = Shirt.create(:color => "Pink")
108
+ @employee.shirts << {:color => "Blue"}
109
+ @employee.shirts << @blue_shirt
110
+ @employee.save
111
+ @employee = Employee.get(@employee.id)
112
+ @employee.shirts << @pink_shirt
113
+ @employee.save
114
+ end
115
+ it "should return it's children" do
116
+ employee = Employee.get(@employee.id)
117
+ employee.shirts.each do |shirt|
118
+ shirt.should be_kind_of(Shirt)
119
+ end
120
+ employee.shirts.map{|shirt| shirt.color}.should == %w(Blue White Pink)
121
+ end
122
+
123
+ it "should have a delete method" do
124
+ employee = Employee.get(@employee.id)
125
+ employee.shirts.delete(@pink_shirt)
126
+ employee.save
127
+ employee = Employee.get(@employee.id)
128
+ employee.shirts.map{|shirt| shirt.color}.should == %w(Blue White)
129
+ end
130
+
131
+ it "should have an append method" do
132
+ employee = Employee.get(@employee.id)
133
+ employee.shirts << {:color => "Black"}
134
+ employee.save
135
+ employee = Employee.get(@employee.id)
136
+ employee.shirts.map{|shirt| shirt.color}.should == %w(Blue White Black)
137
+ end
138
+
67
139
  end
68
140
 
69
- it "should return it's parent account" do
70
- @project = @account.projects.first
71
- @project.account.should == @account
141
+ describe ":through" do
142
+ it "should return it's 'through' children" do
143
+ @employee.memberships.should be_empty
144
+ end
145
+
146
+ it "should return it's children" do
147
+ 3.times do |i|
148
+ p = Project.create(:name => "Project with Invitations No. #{i}", :account_id => @account.id)
149
+ Membership.create(:employee_id => @employee.id, :project_id => p.id)
150
+ end
151
+
152
+ 7.times do |i|
153
+ e = Employee.create(:email => "bar#{i}@bar.com", :account_id => @account.id)
154
+ Membership.create(:employee_id => e.id, :project_id => @project.id)
155
+ end
156
+
157
+ @employee.memberships.size.should == 3
158
+ @employee.projects.size.should == 3
159
+ @project.memberships.size.should == 7
160
+ @project.members.size.should == 7
161
+ end
72
162
  end
73
163
  end
74
164
  end
@@ -72,7 +72,6 @@ class Question
72
72
  include CouchSurfer::Model
73
73
 
74
74
  key_accessor :q, :a
75
- couchrest_type = 'Question'
76
75
  end
77
76
 
78
77
  class Person
@@ -351,7 +350,7 @@ describe CouchSurfer::Model do
351
350
  @event = Event.get e['id']
352
351
  end
353
352
  it "should cast created_at to Time" do
354
- @event['occurs_at'].should be_an_instance_of(Time)
353
+ @event.occurs_at.should be_an_instance_of(Time)
355
354
  end
356
355
  end
357
356
 
@@ -534,6 +533,7 @@ describe CouchSurfer::Model do
534
533
  foundart.created_at.should == foundart.updated_at
535
534
  end
536
535
  it "should set the time on update" do
536
+ sleep 1 # HACK!! Sometimes takes less than a second to call save the second time. Really should mock this!
537
537
  @art.save
538
538
  @art.created_at.should < @art.updated_at
539
539
  end
@@ -15,7 +15,7 @@ class User
15
15
  validates_format_of :email, :with => /\A([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})\Z/i
16
16
  validates_length_of :postcode, :is => 7, :message => "not the correct length"
17
17
  validates_uniqueness_of :name, :message => "No two Beatles have the same name"
18
- validates_uniqueness_of :email, :view => {:name => :by_account_id_and_email, :query => lambda{ {:key => [account_id, email]} }}, :message => "Already taken!"
18
+ validates_uniqueness_of :email, :view => :by_account_id_and_email, :query => lambda{ {:key => [account_id, email]} }, :message => "Already taken!"
19
19
  end
20
20
 
21
21
 
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: addywaddy-couch_surfer
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.2
4
+ version: 0.0.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - Adam Groves
@@ -40,7 +40,7 @@ dependencies:
40
40
  version: 0.12.2
41
41
  version:
42
42
  description: CouchSurfer provides an ORM for CouchDB, as well as supporting association and validation declarations.
43
- email: jchris@apache.org
43
+ email: adam.groves@gmail.com
44
44
  executables: []
45
45
 
46
46
  extensions: []