dm-is-remixable 0.9.7
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/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
|
+
|