loose_change 0.3.1

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore ADDED
@@ -0,0 +1,2 @@
1
+ .rvmrc
2
+ .bundle
data/Gemfile ADDED
@@ -0,0 +1,17 @@
1
+ source 'http://rubygems.org'
2
+
3
+ gem 'activesupport', '~> 3.0.0'
4
+ gem 'activemodel', '~> 3.0.0'
5
+ gem 'rest-client'
6
+ gem 'json'
7
+ gem 'will_paginate'
8
+
9
+ group :development do
10
+ gem 'jeweler'
11
+ gem 'rake'
12
+ end
13
+
14
+ group :test do
15
+ gem 'shoulda'
16
+ gem 'timecop'
17
+ end
data/Gemfile.lock ADDED
@@ -0,0 +1,41 @@
1
+ GEM
2
+ remote: http://rubygems.org/
3
+ specs:
4
+ activemodel (3.0.0)
5
+ activesupport (= 3.0.0)
6
+ builder (~> 2.1.2)
7
+ i18n (~> 0.4.1)
8
+ activesupport (3.0.0)
9
+ builder (2.1.2)
10
+ gemcutter (0.6.1)
11
+ git (1.2.5)
12
+ i18n (0.4.1)
13
+ jeweler (1.4.0)
14
+ gemcutter (>= 0.1.0)
15
+ git (>= 1.2.5)
16
+ rubyforge (>= 2.0.0)
17
+ json (1.4.6)
18
+ json_pure (1.4.6)
19
+ mime-types (1.16)
20
+ rake (0.8.7)
21
+ rest-client (1.6.1)
22
+ mime-types (>= 1.16)
23
+ rubyforge (2.0.4)
24
+ json_pure (>= 1.1.7)
25
+ shoulda (2.11.3)
26
+ timecop (0.3.5)
27
+ will_paginate (3.0.pre2)
28
+
29
+ PLATFORMS
30
+ ruby
31
+
32
+ DEPENDENCIES
33
+ activemodel (~> 3.0.0)
34
+ activesupport (~> 3.0.0)
35
+ jeweler
36
+ json
37
+ rake
38
+ rest-client
39
+ shoulda
40
+ timecop
41
+ will_paginate
data/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2010 Joshua Miller
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,39 @@
1
+ # Loose Change is a Ruby ORM for CouchDB
2
+
3
+ * where 'ORM' is as accurate as ['Holy Roman
4
+ Empire'](http://en.wikipedia.org/wiki/Holy_Roman_Empire#Analysis)
5
+
6
+ ## Goals and Principles
7
+
8
+ * Take advantage of
9
+ [ActiveModel](http://yehudakatz.com/2010/01/10/activemodel-make-any-ruby-object-feel-like-activerecord/)
10
+ * Make common tasks easy; get out of your way if you need the metal
11
+ * Make working with [GeoCouch](http://github.com/vmx/couchdb) seamless
12
+
13
+ ## Warnings
14
+
15
+ * This is pretty alpha at this point.
16
+ * Only tested on Ruby 1.9.2. I'm not intentionally breaking 1.8.x, but neither do I guarantee anything.
17
+ * The stuff about GeoCouch above was a goal I didn't get to yet.
18
+
19
+ ## Help
20
+
21
+ * Accepted. Fork at
22
+ [Github](http://github.com/joshuamiller/loose_change)
23
+
24
+ ## Shoulders of Giants
25
+
26
+ Inspiration and help from:
27
+
28
+ * [RestClient](http://github.com/archiloque/rest-client), for the
29
+ basic HTTP plumbing.
30
+ * [CouchRest](http://github.com/couchrest/couchrest), for the idea and
31
+ basic structure of using RestClient to talk to CouchDB.
32
+ * [CouchRest-Rails](http://github.com/hpoydar/couchrest-rails), for
33
+ implementation ideas for interacting with Rails.
34
+ * [will_paginate](http://github.com/mislave/will_paginate), for making
35
+ pagination a dead-simple addition.
36
+
37
+ ## License
38
+
39
+ * MIT. See LICENSE for more details.
data/Rakefile ADDED
@@ -0,0 +1,31 @@
1
+ require 'rubygems'
2
+ require 'bundler/setup'
3
+
4
+ require 'rake'
5
+ require 'rake/testtask'
6
+
7
+ Rake::TestTask.new(:test) do |t|
8
+ Bundler.require(:test)
9
+ t.pattern = 'test/**/*_test.rb'
10
+ t.verbose = true
11
+ end
12
+
13
+ begin
14
+ Bundler.require :development
15
+ Jeweler::Tasks.new do |gemspec|
16
+ gemspec.name = "loose_change"
17
+ gemspec.summary = "ActiveModel-compliant CouchDB ORM"
18
+ gemspec.email = "josh@joshinharrisburg.com"
19
+ gemspec.homepage = "http://github.com/joshuamiller/loose_change"
20
+ gemspec.authors = ["Joshua Miller"]
21
+ # Dependencies
22
+ gemspec.add_dependency 'activesupport', '~> 3.0.0'
23
+ gemspec.add_dependency 'activemodel', '~> 3.0.0'
24
+ gemspec.add_dependency 'rest-client', '~> 1.6.0'
25
+ gemspec.add_dependency 'json', '~> 1.4.6'
26
+ end
27
+ Jeweler::GemcutterTasks.new
28
+ rescue LoadError
29
+ puts "Jeweler not available."
30
+ end
31
+
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 0.3.1
@@ -0,0 +1,23 @@
1
+ require 'active_support'
2
+ require 'active_support/core_ext'
3
+ require 'active_model'
4
+ require 'rest-client'
5
+ require 'json'
6
+ require 'will_paginate'
7
+ require 'will_paginate/collection'
8
+
9
+ module LooseChange
10
+ extend ActiveSupport::Autoload
11
+
12
+ autoload :Attributes, File.dirname(__FILE__) + '/loose_change/attributes'
13
+ autoload :Attachments, File.dirname(__FILE__) + '/loose_change/attachments'
14
+ autoload :Errors, File.dirname(__FILE__) + '/loose_change/errors'
15
+ autoload :Naming, File.dirname(__FILE__) + '/loose_change/naming'
16
+ autoload :Base, File.dirname(__FILE__) + '/loose_change/base'
17
+ autoload :Persistence, File.dirname(__FILE__) + '/loose_change/persistence'
18
+ autoload :Database, File.dirname(__FILE__) + '/loose_change/database'
19
+ autoload :Server, File.dirname(__FILE__) + '/loose_change/server'
20
+ autoload :Views, File.dirname(__FILE__) + '/loose_change/views'
21
+ autoload :Pagination, File.dirname(__FILE__) + '/loose_change/pagination'
22
+ autoload :Helpers, File.dirname(__FILE__) + '/loose_change/helpers'
23
+ end
@@ -0,0 +1,60 @@
1
+ module LooseChange
2
+
3
+ module Attachments
4
+ end
5
+
6
+ module AttachmentClassMethods
7
+
8
+ # Attach a file to this model, to be stored inline in
9
+ # CouchDB. Note that the file will not actually be transferred
10
+ # to CouchDB until #save is called. The +name+ parameter will be
11
+ # used on CouchDB and in Loose Change to retrieve the attachment
12
+ # later. If you set the <tt>:content_type</tt> key in the optional +args+
13
+ # hash, that content-type will be set on the attachment in CouchDB
14
+ # and available when the model is retrieved.
15
+ #
16
+ # recipe = Recipe.create!(:name => "Lasagne")
17
+ # recipe.attach(:photo, File.open("lasagne.png"), :content_type
18
+ # => 'image/png'
19
+ # recipe.save
20
+ def attach(name, file, args = {})
21
+ attachment = args.merge :file => file, :dirty => true
22
+ @attachments = (@attachments || {}).merge(name => attachment)
23
+ end
24
+
25
+ # Returns the file identified by +name+ on a Loose Change model
26
+ # instance, whether or not that file has been saved back to
27
+ # CouchDB. Will return nil if no attachment by that name exists.
28
+ def attachment(name)
29
+ return attachments[name][:file] if @attachments.try(:[], :name).try(:[], :file)
30
+ begin
31
+ result = retrieve_attachment(name)
32
+ @attachments = (@attachments || {}).merge(name => {:file => result[:file], :dirty => false, :content_type => result[:content_type]})
33
+ result[:file]
34
+ rescue RestClient::ResourceNotFound
35
+ nil
36
+ end
37
+ end
38
+
39
+ # Returns a hash composed of <tt>file</tt> and <tt>content_type</tt> as
40
+ # returned from CouchDB identified by +name+.
41
+ def retrieve_attachment(name)
42
+ { :file => RestClient.get("#{ uri }/#{ name }"),
43
+ :content_type => JSON.parse(RestClient.get(uri))['_attachments']['name'] }
44
+ end
45
+
46
+ # Explicitly transfers an attachment to CouchDB without saving the
47
+ # rest of the model.
48
+ #
49
+ # recipe.attach(:photo, File.open("lasagne.png"), :content_type
50
+ # => 'image/png'
51
+ # recipe.put_attachment(:photo)
52
+ def put_attachment(name)
53
+ return unless attachments[name]
54
+ result = JSON.parse(RestClient.put("#{ uri }/#{ name }#{ '?rev=' + @_rev if @_rev }", attachments[name][:file], {:content_type => attachments[name][:content_type], :accept => 'text/json'}))
55
+ @_rev = result['rev']
56
+ end
57
+
58
+ end
59
+
60
+ end
@@ -0,0 +1,79 @@
1
+ module LooseChange
2
+
3
+ module Attributes
4
+
5
+ # Set a field to be stored in a CouchDB document. Any
6
+ # JSON-encodable value can be used; there is no explicit typing.
7
+ # The <tt>:default</tt> option can be used to always store a value
8
+ # on documents where this property has not been set.
9
+ #
10
+ # class Recipe < LooseChange::Base
11
+ # property :name
12
+ # property :popularity, :default => 0
13
+ # end
14
+ def property(name, opts = {})
15
+ attr_accessor name.to_sym
16
+ self.properties = ((self.properties || []) << name.to_sym)
17
+ default(name.to_sym, opts[:default]) if opts[:default]
18
+ define_attribute_methods [name]
19
+ end
20
+
21
+ #:nodoc:
22
+ def default(property, value)
23
+ self.defaults ||= {}
24
+ self.defaults[property] = value
25
+ end
26
+
27
+ # Automatically set up <tt>:created_at</tt> and
28
+ # <tt>:updated_at</tt> properties which are set on creation and
29
+ # save, respectively.
30
+ def timestamps!
31
+ property :created_at
32
+ property :updated_at
33
+
34
+ before_create :touch_created_at
35
+ before_create :touch_updated_at
36
+ before_save :touch_updated_at
37
+ end
38
+ end
39
+
40
+ module AttributeClassMethods
41
+
42
+ # Returns a hash of property names and current values.
43
+ def attributes
44
+ (self.class.properties || []).inject({}) {|acc, key| acc[key] = send(key); acc}
45
+ end
46
+
47
+ # Change the value of a property and save the result to CouchDB.
48
+ def update_attribute(name, value)
49
+ send("#{name}=", value)
50
+ save
51
+ end
52
+
53
+ # Change multiple properties at once with a hash of property names
54
+ # and values, then save the result to CouchDB.
55
+ def update_attributes(args = {})
56
+ args.each do |name, value|
57
+ send("#{name}=", value)
58
+ end
59
+ save
60
+ end
61
+
62
+ private
63
+
64
+ def touch_created_at
65
+ self.created_at = Time.now
66
+ end
67
+
68
+ def touch_updated_at
69
+ self.updated_at = Time.now
70
+ end
71
+
72
+ def apply_defaults
73
+ (self.class.defaults || {}).each do |property, value|
74
+ self.send("#{property}=", self.send(property) || value)
75
+ end
76
+ end
77
+ end
78
+
79
+ end
@@ -0,0 +1,71 @@
1
+ module LooseChange
2
+ class Base
3
+
4
+ include ActiveModel::AttributeMethods
5
+ include ActiveModel::Validations
6
+ include ActiveModel::Serialization
7
+ include ActiveModel::Serializers::JSON
8
+ include ActiveModel::Dirty
9
+
10
+ # Can't escape the following when trying to
11
+ # use a Callbacks module:
12
+ # undefined method `extlib_inheritable_reader' for LooseChange::Callbacks:Module
13
+ # so we'll throw it in here
14
+ extend ActiveModel::Callbacks
15
+ define_model_callbacks :create, :save, :destroy
16
+
17
+ extend Attributes
18
+ include AttributeClassMethods
19
+ extend Attachments
20
+ include AttachmentClassMethods
21
+ extend Naming
22
+ include NamingClassMethods
23
+ include Errors
24
+ extend Persistence
25
+ include PersistenceClassMethods
26
+ extend Views
27
+ extend Pagination
28
+ include Helpers
29
+ extend Helpers
30
+
31
+ class_inheritable_accessor :database, :properties, :defaults
32
+ attr_accessor :attachments
33
+
34
+ def to_key
35
+ persisted? ? [id] : nil
36
+ end
37
+
38
+ def to_model
39
+ self
40
+ end
41
+
42
+ def to_param
43
+ (to_key && persisted?) ? to_key.join('-') : nil
44
+ end
45
+
46
+ def ==(other_model)
47
+ return false unless other_model && other_model.is_a?(self.class)
48
+ id == other_model.id &&
49
+ _rev == other_model._rev &&
50
+ !(changed? || other_model.changed?)
51
+ end
52
+
53
+ alias_method :eql?, :==
54
+
55
+ def hash
56
+ id.try(:hex) || super
57
+ end
58
+
59
+ def initialize(args = {})
60
+ @errors = ActiveModel::Errors.new(self)
61
+ @database = self.database
62
+ @new_record = true unless args['_id']
63
+ args.each {|property, value| self.send("#{property}=".to_sym, value)}
64
+ apply_defaults
65
+ self
66
+ end
67
+
68
+ end
69
+ end
70
+
71
+ LooseChange::Base.include_root_in_json = false
@@ -0,0 +1,52 @@
1
+ require 'cgi'
2
+
3
+ module LooseChange
4
+ class Database
5
+ include Helpers
6
+ extend Helpers
7
+
8
+ attr_reader :server, :name
9
+
10
+ # Build a new LooseChange::Database instance with a database named
11
+ # +name+. The +server+ is a URI string identifying the CouchDB
12
+ # server and port. If the database does not exist on the server,
13
+ # it will be created.
14
+ def initialize(name, server)
15
+ @name = name
16
+ @server = server
17
+ @uri = "/#{name.gsub('/','%2F')}"
18
+ create_database_unless_exists
19
+ end
20
+
21
+ def uri
22
+ server.uri + @uri
23
+ end
24
+
25
+ # Delete the database named +name+ on the server +server+.
26
+ def self.delete(name, server = "http://127.0.0.1:5984")
27
+ begin
28
+ RestClient.delete("#{ server }/#{ name }")
29
+ rescue RestClient::ResourceNotFound
30
+ end
31
+ end
32
+
33
+ def self.setup_design(database, model_name)
34
+ begin
35
+ RestClient.get("#{ database.uri }/_design/#{ CGI.escape(model_name) }")
36
+ rescue
37
+ RestClient.put("#{ database.uri }/_design/#{ CGI.escape(model_name) }",
38
+ { '_id' => "_design/#{ CGI.escape(model_name) }",
39
+ 'language' => 'javascript'}.to_json, default_headers)
40
+ end
41
+ end
42
+
43
+ private
44
+
45
+ def create_database_unless_exists
46
+ unless JSON.parse(RestClient.get("#{server.uri}/_all_dbs", default_headers)).include?(name)
47
+ RestClient.put uri, "", default_headers
48
+ end
49
+ end
50
+
51
+ end
52
+ end