addywaddy-couch_surfer 0.0.2 → 0.0.4

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.
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: []