loose_change 0.3.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.
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