dm-is-remixable 0.9.7
Sign up to get free protection for your applications and to get access to all the features.
- data/History.txt +5 -0
- data/LICENSE +20 -0
- data/Manifest.txt +24 -0
- data/README.txt +127 -0
- data/Rakefile +51 -0
- data/TODO +30 -0
- data/lib/dm-is-remixable/is/remixable.rb +402 -0
- data/lib/dm-is-remixable/is/version.rb +7 -0
- data/lib/dm-is-remixable.rb +19 -0
- data/spec/data/addressable.rb +15 -0
- data/spec/data/article.rb +51 -0
- data/spec/data/billable.rb +12 -0
- data/spec/data/bot.rb +21 -0
- data/spec/data/commentable.rb +10 -0
- data/spec/data/image.rb +9 -0
- data/spec/data/rating.rb +37 -0
- data/spec/data/tag.rb +6 -0
- data/spec/data/taggable.rb +24 -0
- data/spec/data/topic.rb +15 -0
- data/spec/data/user.rb +34 -0
- data/spec/data/viewable.rb +11 -0
- data/spec/integration/remixable_spec.rb +239 -0
- data/spec/spec.opts +2 -0
- data/spec/spec_helper.rb +28 -0
- metadata +99 -0
data/History.txt
ADDED
data/LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) 2008 Cory ODaniel
|
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/Manifest.txt
ADDED
@@ -0,0 +1,24 @@
|
|
1
|
+
History.txt
|
2
|
+
LICENSE
|
3
|
+
Manifest.txt
|
4
|
+
README.txt
|
5
|
+
Rakefile
|
6
|
+
TODO
|
7
|
+
lib/dm-is-remixable.rb
|
8
|
+
lib/dm-is-remixable/is/remixable.rb
|
9
|
+
lib/dm-is-remixable/is/version.rb
|
10
|
+
spec/data/addressable.rb
|
11
|
+
spec/data/article.rb
|
12
|
+
spec/data/billable.rb
|
13
|
+
spec/data/bot.rb
|
14
|
+
spec/data/commentable.rb
|
15
|
+
spec/data/image.rb
|
16
|
+
spec/data/rating.rb
|
17
|
+
spec/data/tag.rb
|
18
|
+
spec/data/taggable.rb
|
19
|
+
spec/data/topic.rb
|
20
|
+
spec/data/user.rb
|
21
|
+
spec/data/viewable.rb
|
22
|
+
spec/integration/remixable_spec.rb
|
23
|
+
spec/spec.opts
|
24
|
+
spec/spec_helper.rb
|
data/README.txt
ADDED
@@ -0,0 +1,127 @@
|
|
1
|
+
= dm-is-remixable
|
2
|
+
|
3
|
+
DataMapper::Is::Remixable allows you to create re-usable chunks of relational data, its kind of like multiple
|
4
|
+
inheritance for models.
|
5
|
+
|
6
|
+
|
7
|
+
For example:
|
8
|
+
#Comments are everywhere, why define them over and over?
|
9
|
+
module Comment
|
10
|
+
include DataMapper::Resource
|
11
|
+
is :remixable
|
12
|
+
|
13
|
+
property :id, Integer, :key => true, :serial => true
|
14
|
+
property :body, String
|
15
|
+
property :created_at, DateTime
|
16
|
+
end
|
17
|
+
|
18
|
+
#Lots of things can be addressable; people, buildings
|
19
|
+
module Addressable
|
20
|
+
include DataMapper::Resource
|
21
|
+
|
22
|
+
is :remixable,
|
23
|
+
:suffix => "address" #Default suffix is module name pluralized
|
24
|
+
|
25
|
+
property :id, Integer, :key => true, :serial => true
|
26
|
+
|
27
|
+
property :label, String #home, work, etc...
|
28
|
+
|
29
|
+
property :address1, String, :length => 255
|
30
|
+
property :address2, String, :length => 255
|
31
|
+
|
32
|
+
property :city, String, :length => 128
|
33
|
+
property :state, String, :length => 2
|
34
|
+
property :zip, String, :length => 5..10
|
35
|
+
end
|
36
|
+
|
37
|
+
module Vote
|
38
|
+
include DataMapper::Resource
|
39
|
+
|
40
|
+
is :remixable
|
41
|
+
|
42
|
+
property :id, Integer, :key => true, :serial => true
|
43
|
+
property :opinion, Enum.new("good","bad")
|
44
|
+
|
45
|
+
end
|
46
|
+
|
47
|
+
class Location
|
48
|
+
include DataMapper::Resource
|
49
|
+
|
50
|
+
#Location can have 1 address
|
51
|
+
remix 1, :addressables
|
52
|
+
|
53
|
+
# This does the following:
|
54
|
+
# - creates a class called LocationAddress
|
55
|
+
(default name would be LocationAddressable, but Addressable#suffix was specified)
|
56
|
+
# - duplicates the properties of Addressable within LocationAddress
|
57
|
+
# - a table called location_addresses
|
58
|
+
# - creates Location#location_addresses accessor
|
59
|
+
|
60
|
+
#... methods, properties, etc ...#
|
61
|
+
end
|
62
|
+
|
63
|
+
|
64
|
+
class User
|
65
|
+
include DataMapper::Resource
|
66
|
+
|
67
|
+
#User can have many addresses
|
68
|
+
remix n, :addressables, :as => "addresses"
|
69
|
+
# - creates a class called UserAddress
|
70
|
+
(default name would be UserAddressable, but Addressable#suffix was specified)
|
71
|
+
# - duplicates the properties of Addressable within UserAddress
|
72
|
+
# - a table called user_addresses
|
73
|
+
# - creates User#user_addresses accessor
|
74
|
+
# - creates an accessor alias User#addresses
|
75
|
+
|
76
|
+
enhance :addressables do
|
77
|
+
storage_names[:default] = "a_different_table_name"
|
78
|
+
property :label, Enum.new("work","home")
|
79
|
+
|
80
|
+
#This adds a column to user_addresses to store an address label
|
81
|
+
end
|
82
|
+
|
83
|
+
#... methods, properties, etc ...#
|
84
|
+
end
|
85
|
+
|
86
|
+
class Article
|
87
|
+
include DataMapper::Resource
|
88
|
+
|
89
|
+
remix n, :comments, :for => "User"
|
90
|
+
# - creates a class called ArticleComment
|
91
|
+
# - duplicates the properties of Comment within ArticleComment
|
92
|
+
# - a table called article_comments
|
93
|
+
# - creates Article#article_comments
|
94
|
+
# - creates User#article_comments
|
95
|
+
|
96
|
+
#... methods, properties, etc ...#
|
97
|
+
|
98
|
+
end
|
99
|
+
|
100
|
+
class Video
|
101
|
+
include DataMapper::Resource
|
102
|
+
|
103
|
+
remix n, :comments, :for => "User", :as => "comments"
|
104
|
+
# - creates a class called VideoComment
|
105
|
+
# - duplicates the properties of Comment within VideoComment
|
106
|
+
# - a table called video_comments
|
107
|
+
# - creates Video#video_comments
|
108
|
+
# - creates User#video_comments
|
109
|
+
# - create Video#comments
|
110
|
+
|
111
|
+
enhance :comments do
|
112
|
+
# VideoComment now has the method #reverse
|
113
|
+
def reverse
|
114
|
+
return self.body.reverse
|
115
|
+
end
|
116
|
+
|
117
|
+
#I like YouTubes ability for users to vote comments up and down
|
118
|
+
remix 1, :votes, :for => "User"
|
119
|
+
# - creates a class called VideoCommentVote
|
120
|
+
# - duplicates the properties of Vote within VideoCommentVote
|
121
|
+
# - a table called video_comment_votes
|
122
|
+
# - creates Video#video_comments#votes
|
123
|
+
|
124
|
+
end
|
125
|
+
|
126
|
+
#... methods, properties, etc ...#
|
127
|
+
end
|
data/Rakefile
ADDED
@@ -0,0 +1,51 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'spec'
|
3
|
+
require 'spec/rake/spectask'
|
4
|
+
require 'pathname'
|
5
|
+
|
6
|
+
ROOT = Pathname(__FILE__).dirname.expand_path
|
7
|
+
require ROOT + 'lib/dm-is-remixable/is/version'
|
8
|
+
|
9
|
+
AUTHOR = "Cory O'Daniel"
|
10
|
+
EMAIL = "dm-is-remixable [a] coryodaniel [d] com"
|
11
|
+
GEM_NAME = "dm-is-remixable"
|
12
|
+
GEM_VERSION = DataMapper::Is::Remixable::VERSION
|
13
|
+
GEM_DEPENDENCIES = [["dm-core", GEM_VERSION]]
|
14
|
+
GEM_CLEAN = ["log", "pkg"]
|
15
|
+
GEM_EXTRAS = { :has_rdoc => true, :extra_rdoc_files => %w[ README.txt LICENSE TODO ] }
|
16
|
+
|
17
|
+
PROJECT_NAME = "datamapper"
|
18
|
+
PROJECT_URL = "http://github.com/sam/dm-more/tree/master/dm-remixes"
|
19
|
+
PROJECT_DESCRIPTION = PROJECT_SUMMARY = "dm-is-remixable allow you to create reusable data functionality"
|
20
|
+
|
21
|
+
require ROOT.parent + 'tasks/hoe'
|
22
|
+
|
23
|
+
task :default => [ :spec ]
|
24
|
+
|
25
|
+
WIN32 = (RUBY_PLATFORM =~ /win32|mingw|cygwin/) rescue nil
|
26
|
+
SUDO = WIN32 ? '' : ('sudo' unless ENV['SUDOLESS'])
|
27
|
+
|
28
|
+
desc "Install #{GEM_NAME} #{GEM_VERSION}"
|
29
|
+
task :install => [ :package ] do
|
30
|
+
sh "#{SUDO} gem install --local pkg/#{GEM_NAME}-#{GEM_VERSION} --no-ri --no-rdoc --no-update-sources", :verbose => false
|
31
|
+
end
|
32
|
+
|
33
|
+
desc "Uninstall #{GEM_NAME} #{GEM_VERSION} (default ruby)"
|
34
|
+
task :uninstall => [ :clobber ] do
|
35
|
+
sh "#{SUDO} gem uninstall #{GEM_NAME} -v#{GEM_VERSION} -I -x", :verbose => false
|
36
|
+
end
|
37
|
+
|
38
|
+
desc 'Run specifications'
|
39
|
+
Spec::Rake::SpecTask.new(:spec) do |t|
|
40
|
+
t.spec_opts << '--options' << 'spec/spec.opts' if File.exists?('spec/spec.opts')
|
41
|
+
t.spec_files = Pathname.glob((ROOT + 'spec/**/*_spec.rb').to_s)
|
42
|
+
|
43
|
+
begin
|
44
|
+
t.rcov = ENV.has_key?('NO_RCOV') ? ENV['NO_RCOV'] != 'true' : true
|
45
|
+
t.rcov_opts << '--exclude' << 'spec'
|
46
|
+
t.rcov_opts << '--text-summary'
|
47
|
+
t.rcov_opts << '--sort' << 'coverage' << '--sort-reverse'
|
48
|
+
rescue Exception
|
49
|
+
# rcov not installed
|
50
|
+
end
|
51
|
+
end
|
data/TODO
ADDED
@@ -0,0 +1,30 @@
|
|
1
|
+
FIXME
|
2
|
+
====
|
3
|
+
Add ability to have a unique relationship when joining through remixable
|
4
|
+
This is stubbed at remixable.rb @~ line 223; waiting on stategic eager loading
|
5
|
+
M:M Video.remix n, :commentables, :as => "comments", :for => "User", :unique => false
|
6
|
+
M:M Article.remix n, :views, :for => "User", :unique => true
|
7
|
+
in the generated table article_views article_id and user_id would be a unique key,
|
8
|
+
only counting a users viewing of an article once
|
9
|
+
|
10
|
+
TODO
|
11
|
+
====
|
12
|
+
- Add ability to set primary keys and indexes when remixing
|
13
|
+
|
14
|
+
- Test nested remixing User remixes Photogenic; Photogenic Remixes comments
|
15
|
+
- Test double+ remixing. User remixes Commentable; enhance Commentable remix Commentable
|
16
|
+
|
17
|
+
- Harvest Class methods (including it into Remixed Model gets the instance methods, but not class methods...)
|
18
|
+
|
19
|
+
- Squash protection;
|
20
|
+
IF ClassA => remix ModuleB, :table_name => "squashme"
|
21
|
+
AND ClassC => remix ModuleB, :table_name => "squashme" #SQUASHED THAT TABLE
|
22
|
+
|
23
|
+
- Remixable.related(*remixed_models)
|
24
|
+
Taggable.related(Article, JobPostings)
|
25
|
+
|
26
|
+
|
27
|
+
CONSIDERATIONS
|
28
|
+
==============
|
29
|
+
- Customizing Assocations (http://datamapper.org/docs/associations.html)
|
30
|
+
- Adding Conditions to Associations (http://datamapper.org/docs/associations.html)
|
@@ -0,0 +1,402 @@
|
|
1
|
+
# reopen sam/extlib/lib/extlib/object.rb
|
2
|
+
class Object
|
3
|
+
|
4
|
+
def full_const_defined?(name)
|
5
|
+
!!full_const_get(name) rescue false
|
6
|
+
end
|
7
|
+
|
8
|
+
end
|
9
|
+
|
10
|
+
Extlib::Inflection.rule 'ess', 'esses'
|
11
|
+
|
12
|
+
module DataMapper
|
13
|
+
module Is
|
14
|
+
module Remixable
|
15
|
+
|
16
|
+
#==============================INCLUSION METHODS==============================#
|
17
|
+
|
18
|
+
# Adds remixer methods to DataMapper::Resource
|
19
|
+
def self.included(base)
|
20
|
+
base.send(:include,RemixerClassMethods)
|
21
|
+
base.send(:include,RemixerInstanceMethods)
|
22
|
+
end
|
23
|
+
|
24
|
+
# - is_remixable
|
25
|
+
# ==== Description
|
26
|
+
# Adds RemixeeClassMethods and RemixeeInstanceMethods to any model that is: remixable
|
27
|
+
# ==== Examples
|
28
|
+
# class User #Remixer
|
29
|
+
# remixes Commentable
|
30
|
+
# remixes Vote
|
31
|
+
# end
|
32
|
+
#
|
33
|
+
# module Commentable #Remixable
|
34
|
+
# include DataMapper::Resource
|
35
|
+
#
|
36
|
+
# is :remixable,
|
37
|
+
# :suffix => "comment"
|
38
|
+
# end
|
39
|
+
#
|
40
|
+
# module Vote #Remixable
|
41
|
+
# include DataMapper::Resource
|
42
|
+
#
|
43
|
+
# is :remixable
|
44
|
+
#
|
45
|
+
# ==== Notes
|
46
|
+
# These options are just available for whatever reason your Remixable Module name
|
47
|
+
# might not be what you'd like to see the table name and property accessor named.
|
48
|
+
# These are just configurable defaults, upon remixing the class_name and accessor there
|
49
|
+
# take precedence over the defaults set here
|
50
|
+
# ==== Options
|
51
|
+
# :suffix <String>
|
52
|
+
# Table suffix, defaults to YourModule.name.downcase.singular
|
53
|
+
# Yields table name of remixer_suffix; ie user_comments, user_votes
|
54
|
+
def is_remixable(options={})
|
55
|
+
extend DataMapper::Is::Remixable::RemixeeClassMethods
|
56
|
+
include DataMapper::Is::Remixable::RemixeeInstanceMethods
|
57
|
+
@is_remixable = true
|
58
|
+
# support clean suffixes for nested modules
|
59
|
+
default_suffix = Extlib::Inflection.demodulize(self.name).singular.snake_case
|
60
|
+
suffix(options.delete(:suffix) || default_suffix)
|
61
|
+
end
|
62
|
+
|
63
|
+
|
64
|
+
#==============================CLASS METHODS==============================#
|
65
|
+
|
66
|
+
# - RemixerClassMethods
|
67
|
+
# ==== Description
|
68
|
+
# Methods available to all DataMapper::Resources
|
69
|
+
module RemixerClassMethods
|
70
|
+
def self.included(base);end;
|
71
|
+
|
72
|
+
def is_remixable?
|
73
|
+
@is_remixable ||= false
|
74
|
+
end
|
75
|
+
|
76
|
+
# - remixables
|
77
|
+
# ==== Description
|
78
|
+
# Returns a hash of the remixables used by this class
|
79
|
+
# ==== Returns
|
80
|
+
# <Hash> Remixable Class Name => Remixed Class Name
|
81
|
+
def remixables
|
82
|
+
@remixables
|
83
|
+
end
|
84
|
+
|
85
|
+
# - remix
|
86
|
+
# ==== Description
|
87
|
+
# Remixes a Remixable Module
|
88
|
+
# ==== Parameters
|
89
|
+
# cardinality <~Fixnum> 1, n, x ...
|
90
|
+
# remixable <Symbol> plural of remixable; i.e. Comment => :comments
|
91
|
+
# options <Hash> options hash
|
92
|
+
# :class_name <String> Remixed Model name (Also creates a storage_name as tableize(:class_name))
|
93
|
+
# This is the class that will be created from the Remixable Module
|
94
|
+
# The storage_name can be changed via 'enhance' in the class that is remixing
|
95
|
+
# Default: self.name.downcase + "_" + remixable.suffix.pluralize
|
96
|
+
# :as <String> Alias to access associated data
|
97
|
+
# Default: tableize(:class_name)
|
98
|
+
# :for|:on <String> Class name to join to through Remixable
|
99
|
+
# This will create a M:M relationship THROUGH the remixable, rather than
|
100
|
+
# a 1:M with the remixable
|
101
|
+
# :via <String> changes the name of the second id in a unary relationship
|
102
|
+
# see example below; only used when remixing a module between the same class twice
|
103
|
+
# ie: self.class.to_s == options[:for||:on]
|
104
|
+
# :unique <Boolean> Only works with :for|:on; creates a unique composite key
|
105
|
+
# over the two table id's
|
106
|
+
# ==== Examples
|
107
|
+
# Given: User (Class), Addressable (Module)
|
108
|
+
#
|
109
|
+
# One-To-Many; Class-To-Remixable
|
110
|
+
#
|
111
|
+
# remix n, :addressables,
|
112
|
+
# :class_name => "UserAddress",
|
113
|
+
# :as => "addresses"
|
114
|
+
#
|
115
|
+
# Tables: users, user_addresses
|
116
|
+
# Classes: User, UserAddress
|
117
|
+
# User.user_addresses << UserAddress.new
|
118
|
+
# User.addresses << UserAddress.new
|
119
|
+
# --------------------------------------------
|
120
|
+
# --------------------------------------------
|
121
|
+
#
|
122
|
+
# Given: User (Class), Video (Class), Commentable (Module)
|
123
|
+
#
|
124
|
+
# Many-To-Many; Class-To-Class through RemixableIntermediate (Video allows Commentable for User)
|
125
|
+
#
|
126
|
+
# Video.remix n, :commentables
|
127
|
+
# :for => 'User' #:for & :on have same effect, just a choice of wording...
|
128
|
+
# --------------------------------------------
|
129
|
+
# --------------------------------------------
|
130
|
+
#
|
131
|
+
# Given: User (Class), User (Class), Commentable (Module)
|
132
|
+
#
|
133
|
+
# Many-To-Many Unary relationship between User & User through comments
|
134
|
+
# User.remix n, :commentables, :as => "comments", :for => 'User', :via => "commentor"
|
135
|
+
# => This would create user_id and commentor_id as the
|
136
|
+
#
|
137
|
+
def remix(cardinality, remixable, options={})
|
138
|
+
#A map for remixable names to Remixed Models
|
139
|
+
@remixables = {} if @remixables.nil?
|
140
|
+
|
141
|
+
# Allow nested modules to be remixable to better support using dm-is-remixable in gems
|
142
|
+
# Example (from my upcoming dm-is-rateable gem)
|
143
|
+
# remix n, "DataMapper::Is::Rateable::Rating", :as => :ratings
|
144
|
+
remixable_module = Object.full_const_get(Extlib::Inflection.classify(remixable))
|
145
|
+
|
146
|
+
unless remixable_module.is_remixable?
|
147
|
+
raise Exception, "#{remixable_module} is not remixable"
|
148
|
+
end
|
149
|
+
|
150
|
+
#Merge defaults/options
|
151
|
+
options = {
|
152
|
+
:as => nil,
|
153
|
+
:class_name => Extlib::Inflection.classify(self.name.snake_case + "_" + remixable_module.suffix.pluralize),
|
154
|
+
:for => nil,
|
155
|
+
:on => nil,
|
156
|
+
:unique => false,
|
157
|
+
:via => nil
|
158
|
+
}.merge(options)
|
159
|
+
|
160
|
+
#Make sure the class hasn't been remixed already
|
161
|
+
unless Object.full_const_defined?(Extlib::Inflection.classify(options[:class_name]))
|
162
|
+
|
163
|
+
#Storage name of our remixed model
|
164
|
+
options[:table_name] = Extlib::Inflection.tableize(options[:class_name])
|
165
|
+
|
166
|
+
#Other model to mix with in case of M:M through Remixable
|
167
|
+
options[:other_model] = options[:for] || options[:on]
|
168
|
+
|
169
|
+
DataMapper.logger.info "Generating Remixed Model: #{options[:class_name]}"
|
170
|
+
model = generate_remixed_model(remixable_module, options)
|
171
|
+
|
172
|
+
# map the remixable to the remixed model
|
173
|
+
# since this will be used from 'enhance api' i think it makes perfect sense to
|
174
|
+
# always refer to a remixable by its demodulized snake_cased constant name
|
175
|
+
remixable_key = Extlib::Inflection.demodulize(remixable_module.name).snake_case.to_sym
|
176
|
+
populate_remixables_mapping(model, options.merge(:remixable_key => remixable_key))
|
177
|
+
|
178
|
+
#Create relationships between Remixer and remixed class
|
179
|
+
if options[:other_model]
|
180
|
+
# M:M Class-To-Class w/ Remixable Module as intermediate table
|
181
|
+
# has n and belongs_to (or One-To-Many)
|
182
|
+
remix_many_to_many cardinality, model, options
|
183
|
+
else
|
184
|
+
# 1:M Class-To-Remixable
|
185
|
+
# has n and belongs_to (or One-To-Many)
|
186
|
+
remix_one_to_many cardinality, model, options
|
187
|
+
end
|
188
|
+
|
189
|
+
#Add accessor alias
|
190
|
+
attach_accessor(options) unless options[:as].nil?
|
191
|
+
else
|
192
|
+
DataMapper.logger.warn "#{__FILE__}:#{__LINE__} warning: already remixed constant #{options[:class_name]}"
|
193
|
+
end
|
194
|
+
end
|
195
|
+
|
196
|
+
# - enhance
|
197
|
+
# ==== Description
|
198
|
+
# Enhance a remix; allows nesting remixables, adding columns & functions to a remixed resource
|
199
|
+
# ==== Parameters
|
200
|
+
# remixable <Symbol> Name of remixable to enhance (plural or singular name of is :remixable module)
|
201
|
+
# model_class <Class, symbol, String> Name of the remixable generated Model Class.
|
202
|
+
# block <Proc> Enhancements to perform
|
203
|
+
# ==== Examples
|
204
|
+
# When you have one remixable:
|
205
|
+
#
|
206
|
+
# class Video
|
207
|
+
# include DataMapper::Resource
|
208
|
+
# remix Comment
|
209
|
+
#
|
210
|
+
# enhance :comments do
|
211
|
+
# remix n, :votes #This would result in something like YouTubes Voting comments up/down
|
212
|
+
#
|
213
|
+
# property :updated_at, DateTime
|
214
|
+
#
|
215
|
+
# def backwards; self.test.reverse; end;
|
216
|
+
# end
|
217
|
+
#
|
218
|
+
# When you remixe the same remixable modules twice:
|
219
|
+
#
|
220
|
+
# class Article
|
221
|
+
# include DataMapper::Resource
|
222
|
+
# remix n, :taggings, :for => User, :class_name => "UserArticleTagging"
|
223
|
+
# remix n, :taggings, :for => Bot, :class_name => "BotArticleTagging"
|
224
|
+
#
|
225
|
+
# enhance :taggings, "UserArticleTagging" do
|
226
|
+
# property :updated_at, DateTime
|
227
|
+
# belongs_to :user
|
228
|
+
# belongs_to :tag
|
229
|
+
# end
|
230
|
+
#
|
231
|
+
# enhance :taggings, "BotArticleTagging" do
|
232
|
+
# belongs_to :bot
|
233
|
+
# belongs_to :tag
|
234
|
+
# end
|
235
|
+
|
236
|
+
def enhance(remixable,remixable_model=nil, &block)
|
237
|
+
# always use innermost singular snake_cased constant name
|
238
|
+
remixable_name = remixable.to_s.singular.snake_case.to_sym
|
239
|
+
class_name = if remixable_model.nil?
|
240
|
+
@remixables[remixable_name].keys.first
|
241
|
+
else
|
242
|
+
Extlib::Inflection.demodulize(remixable_model.to_s).snake_case.to_sym
|
243
|
+
end
|
244
|
+
|
245
|
+
model = @remixables[remixable_name][class_name][:model] unless @remixables[remixable_name][class_name].nil?
|
246
|
+
|
247
|
+
unless model.nil?
|
248
|
+
model.class_eval &block
|
249
|
+
else
|
250
|
+
raise Exception, "#{remixable} must be remixed with :class_name option as #{remixable_model} before it can be enhanced"
|
251
|
+
end
|
252
|
+
end
|
253
|
+
|
254
|
+
private
|
255
|
+
|
256
|
+
# - attach_accessor
|
257
|
+
# ==== Description
|
258
|
+
# Creates additional alias for r/w accessor
|
259
|
+
# ==== Parameters
|
260
|
+
# options <Hash> options hash
|
261
|
+
def attach_accessor(options)
|
262
|
+
self.class_eval(<<-EOS, __FILE__, __LINE__ + 1)
|
263
|
+
alias #{options[:as].to_sym} #{options[:table_name].to_sym}
|
264
|
+
alias #{options[:as].to_sym}= #{options[:table_name].to_sym}=
|
265
|
+
EOS
|
266
|
+
end
|
267
|
+
|
268
|
+
# - populate_remixables_mapping
|
269
|
+
# ==== Description
|
270
|
+
# Populates the Hash of remixables with information about the remixable
|
271
|
+
# ==== Parameters
|
272
|
+
# remixable
|
273
|
+
# options <Hash> options hash
|
274
|
+
def populate_remixables_mapping(remixable_model, options)
|
275
|
+
key = options[:remixable_key]
|
276
|
+
accessor_name = options[:as] ? options[:as] : options[:table_name]
|
277
|
+
@remixables[key] ||= {}
|
278
|
+
model_key = Extlib::Inflection.demodulize(remixable_model.to_s).snake_case.to_sym
|
279
|
+
@remixables[key][model_key] ||= {}
|
280
|
+
@remixables[key][model_key][:reader] ||= accessor_name.to_sym
|
281
|
+
@remixables[key][model_key][:writer] ||= "#{accessor_name}=".to_sym
|
282
|
+
@remixables[key][model_key][:model] ||= remixable_model
|
283
|
+
end
|
284
|
+
|
285
|
+
# - remix_one_to_many
|
286
|
+
# ==== Description
|
287
|
+
# creates a one to many relationship Class has many of remixed model
|
288
|
+
# ==== Parameters
|
289
|
+
# cardinality <Fixnum> cardinality of relationship
|
290
|
+
# model <Class> remixed model that 'self' is relating to
|
291
|
+
# options <Hash> options hash
|
292
|
+
def remix_one_to_many(cardinality, model, options)
|
293
|
+
self.has cardinality, options[:table_name].intern
|
294
|
+
model.property Extlib::Inflection.foreign_key(self.name).intern, Integer, :nullable => false
|
295
|
+
model.belongs_to Extlib::Inflection.tableize(self.name).intern
|
296
|
+
end
|
297
|
+
|
298
|
+
# - remix_many_to_many
|
299
|
+
# ==== Description
|
300
|
+
# creates a many to many relationship between two DataMapper models THROUGH a Remixable module
|
301
|
+
# ==== Parameters
|
302
|
+
# cardinality <Fixnum> cardinality of relationship
|
303
|
+
# model <Class> remixed model that 'self' is relating through
|
304
|
+
# options <Hash> options hash
|
305
|
+
def remix_many_to_many(cardinality, model, options)
|
306
|
+
options[:other_model] = Object.full_const_get(Extlib::Inflection.classify(options[:other_model]))
|
307
|
+
|
308
|
+
#TODO if options[:unique] the two *_id's need to be a unique composite key, maybe even
|
309
|
+
# attach a validates_is_unique if the validator is included.
|
310
|
+
puts " ~ options[:unique] is not yet supported" if options[:unique]
|
311
|
+
|
312
|
+
# Is M:M between two different classes or the same class
|
313
|
+
unless self.name == options[:other_model].name
|
314
|
+
self.has cardinality, options[:table_name].intern
|
315
|
+
options[:other_model].has cardinality, options[:table_name].intern
|
316
|
+
|
317
|
+
model.belongs_to Extlib::Inflection.tableize(self.name).intern
|
318
|
+
model.belongs_to Extlib::Inflection.tableize(options[:other_model].name).intern
|
319
|
+
else
|
320
|
+
raise Exception, "options[:via] must be specified when Remixing a module between two of the same class" unless options[:via]
|
321
|
+
|
322
|
+
self.has cardinality, options[:table_name].intern
|
323
|
+
model.belongs_to Extlib::Inflection.tableize(self.name).intern
|
324
|
+
model.belongs_to options[:via].intern, :class_name => options[:other_model].name, :child_key => ["#{options[:via]}_id".intern]
|
325
|
+
end
|
326
|
+
end
|
327
|
+
|
328
|
+
# - generate_remixed_model
|
329
|
+
# ==== Description
|
330
|
+
# Generates a Remixed Model Class from a Remixable Module and options
|
331
|
+
# ==== Parameters
|
332
|
+
# remixable <Module> module that is being remixed
|
333
|
+
# options <Hash> options hash
|
334
|
+
# ==== Returns
|
335
|
+
# <Class> remixed model
|
336
|
+
def generate_remixed_model(remixable,options)
|
337
|
+
#Create Remixed Model
|
338
|
+
klass = Class.new Object do
|
339
|
+
include DataMapper::Resource
|
340
|
+
end
|
341
|
+
|
342
|
+
#Give remixed model a name and create its constant
|
343
|
+
model = Object.full_const_set(options[:class_name], klass)
|
344
|
+
|
345
|
+
#Get instance methods & validators
|
346
|
+
model.send(:include,remixable)
|
347
|
+
|
348
|
+
#port the properties over...
|
349
|
+
remixable.properties.each do |prop|
|
350
|
+
model.property(prop.name, prop.type, prop.options)
|
351
|
+
end
|
352
|
+
|
353
|
+
model
|
354
|
+
end
|
355
|
+
|
356
|
+
end # RemixerClassMethods
|
357
|
+
|
358
|
+
# - RemixeeClassMethods
|
359
|
+
# ==== Description
|
360
|
+
# Methods available to any model that is :remixable
|
361
|
+
module RemixeeClassMethods
|
362
|
+
# - suffix
|
363
|
+
# ==== Description
|
364
|
+
# modifies the storage name suffix, which is by default based on the Remixable Module name
|
365
|
+
# ==== Parameters
|
366
|
+
# suffix <String> storage name suffix to use (singular)
|
367
|
+
def suffix(sfx=nil)
|
368
|
+
@suffix = sfx unless sfx.nil?
|
369
|
+
@suffix
|
370
|
+
end
|
371
|
+
|
372
|
+
# Squash auto_migrate!
|
373
|
+
# model.auto_migrate! never gets called directly from dm-core/auto_migrations.rb
|
374
|
+
# The models are explicitly migrated down and up again.
|
375
|
+
def auto_migrate_up!(args=nil)
|
376
|
+
DataMapper.logger.warn("Skipping auto_migrate_up! for remixable module (#{self.name})")
|
377
|
+
end
|
378
|
+
|
379
|
+
def auto_migrate_down!(args=nil)
|
380
|
+
DataMapper.logger.warn("Skipping auto_migrate_down! for remixable module (#{self.name})")
|
381
|
+
end
|
382
|
+
|
383
|
+
#Squash auto_upgrade!
|
384
|
+
def auto_upgrade!(args=nil)
|
385
|
+
DataMapper.logger.warn("Skipping auto_upgrade! for remixable module (#{self.name})")
|
386
|
+
end
|
387
|
+
end # RemixeeClassMethods
|
388
|
+
|
389
|
+
|
390
|
+
#==============================INSTANCE METHODS==============================#
|
391
|
+
|
392
|
+
module RemixeeInstanceMethods
|
393
|
+
def self.included(base);end;
|
394
|
+
end # RemixeeInstanceMethods
|
395
|
+
|
396
|
+
module RemixerInstanceMethods
|
397
|
+
def self.included(base);end;
|
398
|
+
end # RemixerInstanceMethods
|
399
|
+
|
400
|
+
end # Example
|
401
|
+
end # Is
|
402
|
+
end # DataMapper
|
@@ -0,0 +1,19 @@
|
|
1
|
+
# Needed to import datamapper and other gems
|
2
|
+
require 'rubygems'
|
3
|
+
require 'pathname'
|
4
|
+
|
5
|
+
# Add all external dependencies for the plugin here
|
6
|
+
gem 'dm-core', '~>0.9.7'
|
7
|
+
require 'dm-core'
|
8
|
+
|
9
|
+
# Require plugin-files
|
10
|
+
require Pathname(__FILE__).dirname.expand_path / 'dm-is-remixable' / 'is' / 'remixable.rb'
|
11
|
+
|
12
|
+
# Include the plugin in Resource
|
13
|
+
module DataMapper
|
14
|
+
module Resource
|
15
|
+
module ClassMethods
|
16
|
+
include DataMapper::Is::Remixable
|
17
|
+
end # module ClassMethods
|
18
|
+
end # module Resource
|
19
|
+
end # module DataMapper
|
@@ -0,0 +1,15 @@
|
|
1
|
+
module Addressable
|
2
|
+
include DataMapper::Resource
|
3
|
+
|
4
|
+
is :remixable,
|
5
|
+
:suffix => "address"
|
6
|
+
|
7
|
+
property :id, Integer, :key => true, :serial => true
|
8
|
+
|
9
|
+
property :address1, String, :length => 255
|
10
|
+
property :address2, String, :length => 255
|
11
|
+
|
12
|
+
property :city, String, :length => 128
|
13
|
+
property :state, String, :length => 2
|
14
|
+
property :zip, String, :length => 5..10
|
15
|
+
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
require Pathname(__FILE__).dirname / "image"
|
2
|
+
require Pathname(__FILE__).dirname / "commentable"
|
3
|
+
require Pathname(__FILE__).dirname / "viewable"
|
4
|
+
require Pathname(__FILE__).dirname / "taggable"
|
5
|
+
require Pathname(__FILE__).dirname / "user"
|
6
|
+
require Pathname(__FILE__).dirname / "bot"
|
7
|
+
require Pathname(__FILE__).dirname / "tag"
|
8
|
+
|
9
|
+
class Article
|
10
|
+
include DataMapper::Resource
|
11
|
+
|
12
|
+
property :id, Integer, :key => true, :serial => true
|
13
|
+
property :title, String
|
14
|
+
property :url, String
|
15
|
+
|
16
|
+
|
17
|
+
remix 1, :images, :as => "pics"
|
18
|
+
|
19
|
+
remix n, :viewables, :as => "views"
|
20
|
+
|
21
|
+
remix n, :commentables, :as => "comments", :for => "User"
|
22
|
+
|
23
|
+
remix n, "My::Nested::Remixable::Rating", :as => :ratings
|
24
|
+
|
25
|
+
remix n, :taggable, :as => "user_taggings", :for => "User", :class_name => "UserTagging"
|
26
|
+
|
27
|
+
remix n, :taggable, :as => "bot_taggings", :for => "Bot", :class_name => "BotTagging"
|
28
|
+
|
29
|
+
enhance :viewables do
|
30
|
+
belongs_to :user
|
31
|
+
end
|
32
|
+
|
33
|
+
enhance :taggable, "UserTagging" do
|
34
|
+
belongs_to :user
|
35
|
+
belongs_to :tag
|
36
|
+
end
|
37
|
+
|
38
|
+
enhance :taggable, "BotTagging" do
|
39
|
+
belongs_to :bot
|
40
|
+
belongs_to :tag
|
41
|
+
end
|
42
|
+
|
43
|
+
def viewed_by(usr)
|
44
|
+
art_view = ArticleView.new
|
45
|
+
art_view.ip = "127.0.0.1"
|
46
|
+
art_view.user_id = usr.id
|
47
|
+
|
48
|
+
self.views << art_view
|
49
|
+
end
|
50
|
+
|
51
|
+
end
|
@@ -0,0 +1,12 @@
|
|
1
|
+
module Billable
|
2
|
+
include DataMapper::Resource
|
3
|
+
|
4
|
+
is :remixable,
|
5
|
+
:suffix => "billing_account"
|
6
|
+
|
7
|
+
property :id, Integer, :key => true, :serial => true
|
8
|
+
|
9
|
+
property :cc_type, Enum.new("mastercard","amex","visa")
|
10
|
+
property :cc_num, String, :length => 12..20
|
11
|
+
property :expiration, Date
|
12
|
+
end
|
data/spec/data/bot.rb
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
require Pathname(__FILE__).dirname / "viewable"
|
2
|
+
require Pathname(__FILE__).dirname / "billable"
|
3
|
+
require Pathname(__FILE__).dirname / "addressable"
|
4
|
+
require Pathname(__FILE__).dirname / "rating"
|
5
|
+
|
6
|
+
class Bot
|
7
|
+
include DataMapper::Resource
|
8
|
+
|
9
|
+
property :id, Integer,
|
10
|
+
:key => true,
|
11
|
+
:serial => true
|
12
|
+
|
13
|
+
property :bot_name, String,
|
14
|
+
:nullable => false,
|
15
|
+
:length => 2..50
|
16
|
+
|
17
|
+
property :bot_version, String,
|
18
|
+
:nullable => false,
|
19
|
+
:length => 2..50
|
20
|
+
|
21
|
+
end
|
data/spec/data/image.rb
ADDED
data/spec/data/rating.rb
ADDED
@@ -0,0 +1,37 @@
|
|
1
|
+
module My
|
2
|
+
module Nested
|
3
|
+
module Remixable
|
4
|
+
|
5
|
+
module Rating
|
6
|
+
|
7
|
+
def self.included(base)
|
8
|
+
base.extend ClassMethods
|
9
|
+
end
|
10
|
+
|
11
|
+
include DataMapper::Resource
|
12
|
+
|
13
|
+
is :remixable
|
14
|
+
|
15
|
+
# properties
|
16
|
+
|
17
|
+
property :id, Integer, :serial => true
|
18
|
+
|
19
|
+
property :user_id, Integer, :nullable => false
|
20
|
+
property :rating, Integer, :nullable => false, :default => 0
|
21
|
+
|
22
|
+
module ClassMethods
|
23
|
+
|
24
|
+
# total rating for all rateable instances of this type
|
25
|
+
def total_rating
|
26
|
+
rating_sum = self.sum(:rating).to_f
|
27
|
+
rating_count = self.count.to_f
|
28
|
+
rating_count > 0 ? rating_sum / rating_count : 0
|
29
|
+
end
|
30
|
+
|
31
|
+
end
|
32
|
+
|
33
|
+
end
|
34
|
+
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
data/spec/data/tag.rb
ADDED
@@ -0,0 +1,24 @@
|
|
1
|
+
module Taggable
|
2
|
+
def self.included(base)
|
3
|
+
base.extend Taggable::ClassMethods
|
4
|
+
end
|
5
|
+
|
6
|
+
include DataMapper::Resource
|
7
|
+
|
8
|
+
is :remixable
|
9
|
+
|
10
|
+
property :id, Integer, :key => true, :serial => true
|
11
|
+
property :tag_id, Integer
|
12
|
+
|
13
|
+
|
14
|
+
|
15
|
+
|
16
|
+
module ClassMethods
|
17
|
+
|
18
|
+
def related_tags
|
19
|
+
puts "should work"
|
20
|
+
end
|
21
|
+
|
22
|
+
end
|
23
|
+
|
24
|
+
end
|
data/spec/data/topic.rb
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
require Pathname(__FILE__).dirname / "rating"
|
2
|
+
|
3
|
+
class Topic
|
4
|
+
include DataMapper::Resource
|
5
|
+
|
6
|
+
property :id, Integer, :key => true, :serial => true
|
7
|
+
|
8
|
+
property :name, String
|
9
|
+
property :description, String
|
10
|
+
|
11
|
+
remix n, My::Nested::Remixable::Rating,
|
12
|
+
:as => :ratings_for_topic,
|
13
|
+
:class_name => "Rating"
|
14
|
+
|
15
|
+
end
|
data/spec/data/user.rb
ADDED
@@ -0,0 +1,34 @@
|
|
1
|
+
require Pathname(__FILE__).dirname / "viewable"
|
2
|
+
require Pathname(__FILE__).dirname / "billable"
|
3
|
+
require Pathname(__FILE__).dirname / "addressable"
|
4
|
+
require Pathname(__FILE__).dirname / "rating"
|
5
|
+
|
6
|
+
class User
|
7
|
+
include DataMapper::Resource
|
8
|
+
|
9
|
+
property :id, Integer,
|
10
|
+
:key => true,
|
11
|
+
:serial => true
|
12
|
+
|
13
|
+
property :first_name, String,
|
14
|
+
:nullable => false,
|
15
|
+
:length => 2..50
|
16
|
+
|
17
|
+
property :last_name, String,
|
18
|
+
:nullable => false,
|
19
|
+
:length => 2..50
|
20
|
+
|
21
|
+
remix n, :viewables
|
22
|
+
|
23
|
+
remix n, :billables, :class_name => "Account"
|
24
|
+
|
25
|
+
remix n, :addressables
|
26
|
+
|
27
|
+
remix n, :commentables, :as => "comments", :for => "User", :via => "commentor"
|
28
|
+
|
29
|
+
remix n, "My::Nested::Remixable::Rating"
|
30
|
+
|
31
|
+
enhance :addressables do
|
32
|
+
property :label, Enum.new('home','work')
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,239 @@
|
|
1
|
+
require 'pathname'
|
2
|
+
require Pathname(__FILE__).dirname.expand_path.parent + 'spec_helper'
|
3
|
+
|
4
|
+
require "dm-types"
|
5
|
+
require Pathname(__FILE__).dirname.expand_path.parent / 'data' / 'addressable'
|
6
|
+
require Pathname(__FILE__).dirname.expand_path.parent / 'data' / 'billable'
|
7
|
+
require Pathname(__FILE__).dirname.expand_path.parent / 'data' / 'commentable'
|
8
|
+
require Pathname(__FILE__).dirname.expand_path.parent / 'data' / 'article'
|
9
|
+
require Pathname(__FILE__).dirname.expand_path.parent / 'data' / 'image'
|
10
|
+
require Pathname(__FILE__).dirname.expand_path.parent / 'data' / 'user'
|
11
|
+
require Pathname(__FILE__).dirname.expand_path.parent / 'data' / 'viewable'
|
12
|
+
require Pathname(__FILE__).dirname.expand_path.parent / 'data' / 'topic'
|
13
|
+
require Pathname(__FILE__).dirname.expand_path.parent / 'data' / 'rating'
|
14
|
+
require Pathname(__FILE__).dirname.expand_path.parent / 'data' / 'taggable'
|
15
|
+
require Pathname(__FILE__).dirname.expand_path.parent / 'data' / 'bot'
|
16
|
+
require Pathname(__FILE__).dirname.expand_path.parent / 'data' / 'tag'
|
17
|
+
DataMapper.auto_migrate!
|
18
|
+
|
19
|
+
if HAS_SQLITE3 || HAS_MYSQL || HAS_POSTGRES
|
20
|
+
describe 'DataMapper::Is::Remixable' do
|
21
|
+
describe 'DataMapper::Resource' do
|
22
|
+
it "should know if it is remixable" do
|
23
|
+
User.is_remixable?.should be(false)
|
24
|
+
Image.is_remixable?.should be(true)
|
25
|
+
Article.is_remixable?.should be(false)
|
26
|
+
Commentable.is_remixable?.should be(true)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
it "should only allow remixables to be remixed" do
|
31
|
+
lambda { User.remix 1, :articles }.should raise_error(Exception)
|
32
|
+
end
|
33
|
+
|
34
|
+
it "should not allow enhancements of modules that aren't remixed" do
|
35
|
+
lambda {
|
36
|
+
User.enhance Image
|
37
|
+
}.should raise_error
|
38
|
+
end
|
39
|
+
|
40
|
+
it "should provide a default suffix values for models that do 'is :remixable'" do
|
41
|
+
Image.suffix.should == "image"
|
42
|
+
end
|
43
|
+
|
44
|
+
it "should allow enhancing a model that is remixed" do
|
45
|
+
Article.enhance :images do
|
46
|
+
def self.test_enhance
|
47
|
+
true
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
ArticleImage.should respond_to("test_enhance")
|
52
|
+
end
|
53
|
+
|
54
|
+
it "should allow enhancing a model that was remixed from a nested module" do
|
55
|
+
Article.enhance :ratings do
|
56
|
+
def self.test_enhance
|
57
|
+
true
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
ArticleRating.should respond_to("test_enhance")
|
62
|
+
ArticleRating.should respond_to("total_rating")
|
63
|
+
ArticleRating.new.should respond_to("user_id")
|
64
|
+
ArticleRating.new.should respond_to("rating")
|
65
|
+
end
|
66
|
+
|
67
|
+
it "should allow enhancing the same remixable twice with different class_name attributes" do
|
68
|
+
Article.enhance :taggable, "UserTagging" do
|
69
|
+
def self.test_enhance
|
70
|
+
true
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
UserTagging.should respond_to("test_enhance")
|
75
|
+
UserTagging.should respond_to("related_tags")
|
76
|
+
UserTagging.new.should respond_to("user_id")
|
77
|
+
UserTagging.new.should respond_to("tag")
|
78
|
+
|
79
|
+
Article.enhance :taggable, "BotTagging" do
|
80
|
+
def self.test_enhance_2
|
81
|
+
true
|
82
|
+
end
|
83
|
+
end
|
84
|
+
BotTagging.should respond_to("test_enhance_2")
|
85
|
+
BotTagging.should respond_to("related_tags")
|
86
|
+
BotTagging.new.should respond_to("bot_id")
|
87
|
+
BotTagging.new.should respond_to("tag")
|
88
|
+
end
|
89
|
+
|
90
|
+
it "should through exception when enhancing an unknown class" do
|
91
|
+
lambda {
|
92
|
+
Article.enhance :taggable, "NonExistentClass"
|
93
|
+
}.should raise_error
|
94
|
+
end
|
95
|
+
|
96
|
+
it "should provided a map of Remixable Modules to Remixed Models names" do
|
97
|
+
User.remixables.should_not be(nil)
|
98
|
+
end
|
99
|
+
|
100
|
+
it "should store the remixed model in the map of Remixable Modules to Remixed Models" do
|
101
|
+
User.remixables[:billable][:account][:model].should == Account
|
102
|
+
# nested remixables
|
103
|
+
User.remixables[:rating][:user_rating][:model].should == UserRating
|
104
|
+
Article.remixables[:rating][:article_rating][:model].should == ArticleRating
|
105
|
+
Topic.remixables[:rating][:rating][:model].should == Rating
|
106
|
+
end
|
107
|
+
|
108
|
+
it "should store the remixee reader name in the map of Remixable Modules to Remixed Models" do
|
109
|
+
User.remixables[:billable][:account][:reader].should == :accounts
|
110
|
+
# nested remixables
|
111
|
+
User.remixables[:rating][:user_rating][:reader].should == :user_ratings
|
112
|
+
Article.remixables[:rating][:article_rating][:reader].should == :ratings
|
113
|
+
Topic.remixables[:rating][:rating][:reader].should == :ratings_for_topic
|
114
|
+
end
|
115
|
+
|
116
|
+
it "should store the remixee writer name in the map of Remixable Modules to Remixed Models" do
|
117
|
+
User.remixables[:billable][:account][:writer].should == :accounts=
|
118
|
+
# nested remixables
|
119
|
+
User.remixables[:rating][:user_rating][:writer].should == :user_ratings=
|
120
|
+
Article.remixables[:rating][:article_rating][:writer].should == :ratings=
|
121
|
+
Topic.remixables[:rating][:rating][:writer].should == :ratings_for_topic=
|
122
|
+
end
|
123
|
+
|
124
|
+
it "should allow specifying an alternate class name" do
|
125
|
+
User.remixables[:billable][:account][:model].name.should_not == "UserBillable"
|
126
|
+
User.remixables[:billable][:account][:model].name.should == "Account"
|
127
|
+
end
|
128
|
+
|
129
|
+
it "should create a storage name based on the class name" do
|
130
|
+
|
131
|
+
Article.remixables[:image][:article_image][:model].storage_names[:default].should == "article_images"
|
132
|
+
User.remixables[:billable][:account][:model].storage_names[:default].should == "accounts"
|
133
|
+
end
|
134
|
+
|
135
|
+
it "should allow creating an accessor alias" do
|
136
|
+
article = Article.new
|
137
|
+
article.should respond_to("pics")
|
138
|
+
article.should respond_to("article_images")
|
139
|
+
end
|
140
|
+
|
141
|
+
it "should copy properties from the Remixable Module to the Remixed Model" do
|
142
|
+
#Billabe => Account
|
143
|
+
account = Account.new
|
144
|
+
|
145
|
+
account.should respond_to("cc_num")
|
146
|
+
account.should respond_to("cc_type")
|
147
|
+
account.should respond_to("expiration")
|
148
|
+
end
|
149
|
+
|
150
|
+
it "should allow 1:M relationships with the Remixable Module" do
|
151
|
+
user = User.new
|
152
|
+
addy = UserAddress.new
|
153
|
+
addy2 = UserAddress.new
|
154
|
+
|
155
|
+
user.first_name = "Jack"
|
156
|
+
user.last_name = "Tester"
|
157
|
+
|
158
|
+
addy.address1 = "888 West Whatnot Ave."
|
159
|
+
addy.city = "Los Angeles"
|
160
|
+
addy.state = "CA"
|
161
|
+
addy.zip = "90230"
|
162
|
+
|
163
|
+
addy2.address1 = "325 East NoWhere Lane"
|
164
|
+
addy2.city = "Fort Myers"
|
165
|
+
addy2.state = "FL"
|
166
|
+
addy2.zip = "33971"
|
167
|
+
|
168
|
+
user.user_addresses << addy
|
169
|
+
user.user_addresses << addy2
|
170
|
+
|
171
|
+
user.user_addresses.length.should be(2)
|
172
|
+
end
|
173
|
+
|
174
|
+
it "should allow 1:1 relationships with the Remixable Module" do
|
175
|
+
article = Article.new
|
176
|
+
image1 = ArticleImage.new
|
177
|
+
image2 = ArticleImage.new
|
178
|
+
|
179
|
+
article.title = "Really important news!"
|
180
|
+
article.url = "http://example.com/index.html"
|
181
|
+
|
182
|
+
image1.description = "Shocking and horrific photo!"
|
183
|
+
image1.path = "~/pictures/shocking.jpg"
|
184
|
+
|
185
|
+
image2.description = "Other photo"
|
186
|
+
image2.path = "~/pictures/mom_naked.yipes"
|
187
|
+
|
188
|
+
begin
|
189
|
+
article.pics << image1
|
190
|
+
false
|
191
|
+
rescue Exception => e
|
192
|
+
e.class.should be(NoMethodError)
|
193
|
+
end
|
194
|
+
|
195
|
+
article.pics = image2
|
196
|
+
article.pics.path.should == image2.path
|
197
|
+
end
|
198
|
+
|
199
|
+
it "should allow M:M unary relationships through the Remixable Module" do
|
200
|
+
#User => Commentable => User
|
201
|
+
user = User.new
|
202
|
+
user.first_name = "Tester"
|
203
|
+
user2 = User.new
|
204
|
+
user2.first_name = "Testy"
|
205
|
+
|
206
|
+
comment = UserComment.new
|
207
|
+
comment.comment = "YOU SUCK!"
|
208
|
+
comment.commentor = user2
|
209
|
+
user.user_comments << comment
|
210
|
+
|
211
|
+
user2.user_comments.length.should be(0)
|
212
|
+
|
213
|
+
comment.commentor.first_name.should == "Testy"
|
214
|
+
user.user_comments.length.should be(1)
|
215
|
+
end
|
216
|
+
|
217
|
+
it "should allow M:M relationships through the Remixable Module" do
|
218
|
+
#Article => Commentable => User
|
219
|
+
user = User.new
|
220
|
+
article = Article.new
|
221
|
+
|
222
|
+
ac = ArticleComment.new
|
223
|
+
|
224
|
+
user.first_name = "Talker"
|
225
|
+
user.last_name = "OnTheInternetz"
|
226
|
+
|
227
|
+
article.url = "Http://example.com/"
|
228
|
+
article.title = "Important internet thingz, lol"
|
229
|
+
|
230
|
+
ac.comment = "This article sux!"
|
231
|
+
|
232
|
+
article.comments << ac
|
233
|
+
user.article_comments << ac
|
234
|
+
|
235
|
+
article.comments.first.should be(ac)
|
236
|
+
user.article_comments.first.should be(ac)
|
237
|
+
end
|
238
|
+
end
|
239
|
+
end
|
data/spec/spec.opts
ADDED
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,28 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
gem 'rspec', '>=1.1.3'
|
3
|
+
require 'spec'
|
4
|
+
require 'pathname'
|
5
|
+
require Pathname(__FILE__).dirname.expand_path.parent + 'lib/dm-is-remixable'
|
6
|
+
require "ruby-debug"
|
7
|
+
def load_driver(name, default_uri)
|
8
|
+
return false if ENV['ADAPTER'] != name.to_s
|
9
|
+
|
10
|
+
lib = "do_#{name}"
|
11
|
+
|
12
|
+
begin
|
13
|
+
gem lib, '~>0.9.7'
|
14
|
+
require lib
|
15
|
+
DataMapper.setup(name, ENV["#{name.to_s.upcase}_SPEC_URI"] || default_uri)
|
16
|
+
DataMapper::Repository.adapters[:default] = DataMapper::Repository.adapters[name]
|
17
|
+
true
|
18
|
+
rescue Gem::LoadError => e
|
19
|
+
warn "Could not load #{lib}: #{e}"
|
20
|
+
false
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
ENV['ADAPTER'] ||= 'sqlite3'
|
25
|
+
|
26
|
+
HAS_SQLITE3 = load_driver(:sqlite3, 'sqlite3::memory:')
|
27
|
+
HAS_MYSQL = load_driver(:mysql, 'mysql://localhost/dm_core_test')
|
28
|
+
HAS_POSTGRES = load_driver(:postgres, 'postgres://postgres@localhost/dm_core_test')
|
metadata
ADDED
@@ -0,0 +1,99 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: dm-is-remixable
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.9.7
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Cory O'Daniel
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
|
12
|
+
date: 2008-11-18 00:00:00 -08:00
|
13
|
+
default_executable:
|
14
|
+
dependencies:
|
15
|
+
- !ruby/object:Gem::Dependency
|
16
|
+
name: dm-core
|
17
|
+
type: :runtime
|
18
|
+
version_requirement:
|
19
|
+
version_requirements: !ruby/object:Gem::Requirement
|
20
|
+
requirements:
|
21
|
+
- - "="
|
22
|
+
- !ruby/object:Gem::Version
|
23
|
+
version: 0.9.7
|
24
|
+
version:
|
25
|
+
- !ruby/object:Gem::Dependency
|
26
|
+
name: hoe
|
27
|
+
type: :development
|
28
|
+
version_requirement:
|
29
|
+
version_requirements: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ">="
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: 1.8.2
|
34
|
+
version:
|
35
|
+
description: dm-is-remixable allow you to create reusable data functionality
|
36
|
+
email:
|
37
|
+
- dm-is-remixable [a] coryodaniel [d] com
|
38
|
+
executables: []
|
39
|
+
|
40
|
+
extensions: []
|
41
|
+
|
42
|
+
extra_rdoc_files:
|
43
|
+
- README.txt
|
44
|
+
- LICENSE
|
45
|
+
- TODO
|
46
|
+
files:
|
47
|
+
- History.txt
|
48
|
+
- LICENSE
|
49
|
+
- Manifest.txt
|
50
|
+
- README.txt
|
51
|
+
- Rakefile
|
52
|
+
- TODO
|
53
|
+
- lib/dm-is-remixable.rb
|
54
|
+
- lib/dm-is-remixable/is/remixable.rb
|
55
|
+
- lib/dm-is-remixable/is/version.rb
|
56
|
+
- spec/data/addressable.rb
|
57
|
+
- spec/data/article.rb
|
58
|
+
- spec/data/billable.rb
|
59
|
+
- spec/data/bot.rb
|
60
|
+
- spec/data/commentable.rb
|
61
|
+
- spec/data/image.rb
|
62
|
+
- spec/data/rating.rb
|
63
|
+
- spec/data/tag.rb
|
64
|
+
- spec/data/taggable.rb
|
65
|
+
- spec/data/topic.rb
|
66
|
+
- spec/data/user.rb
|
67
|
+
- spec/data/viewable.rb
|
68
|
+
- spec/integration/remixable_spec.rb
|
69
|
+
- spec/spec.opts
|
70
|
+
- spec/spec_helper.rb
|
71
|
+
has_rdoc: true
|
72
|
+
homepage: http://github.com/sam/dm-more/tree/master/dm-remixes
|
73
|
+
post_install_message:
|
74
|
+
rdoc_options:
|
75
|
+
- --main
|
76
|
+
- README.txt
|
77
|
+
require_paths:
|
78
|
+
- lib
|
79
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
80
|
+
requirements:
|
81
|
+
- - ">="
|
82
|
+
- !ruby/object:Gem::Version
|
83
|
+
version: "0"
|
84
|
+
version:
|
85
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - ">="
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: "0"
|
90
|
+
version:
|
91
|
+
requirements: []
|
92
|
+
|
93
|
+
rubyforge_project: datamapper
|
94
|
+
rubygems_version: 1.3.1
|
95
|
+
signing_key:
|
96
|
+
specification_version: 2
|
97
|
+
summary: dm-is-remixable allow you to create reusable data functionality
|
98
|
+
test_files: []
|
99
|
+
|