simplemapper 0.0.4 → 0.0.5
Sign up to get free protection for your applications and to get access to all the features.
- data/Rakefile +1 -1
- data/lib/simple_mapper/adapters/http_adapter.rb +36 -5
- data/lib/simple_mapper/base.rb +5 -175
- data/lib/simple_mapper/default_plugins/associations.rb +374 -0
- data/lib/simple_mapper/default_plugins/properties.rb +70 -0
- data/lib/simple_mapper/persistence.rb +169 -0
- data/lib/simple_mapper/plugin_support.rb +19 -0
- data/lib/simple_mapper/support.rb +2 -2
- data/lib/simple_mapper/support/bliss_serializer.rb +43 -12
- data/lib/simple_mapper/support/core_ext.rb +23 -2
- metadata +6 -179
- data/doc/classes/Array.html +0 -158
- data/doc/classes/Array.src/M000005.html +0 -18
- data/doc/classes/Array.src/M000006.html +0 -34
- data/doc/classes/Enumerable.html +0 -131
- data/doc/classes/Enumerable.src/M000040.html +0 -21
- data/doc/classes/Hash.html +0 -173
- data/doc/classes/Hash.src/M000002.html +0 -20
- data/doc/classes/Hash.src/M000003.html +0 -18
- data/doc/classes/Hash.src/M000004.html +0 -34
- data/doc/classes/Inflector.html +0 -516
- data/doc/classes/Inflector.src/M000020.html +0 -22
- data/doc/classes/Inflector.src/M000021.html +0 -25
- data/doc/classes/Inflector.src/M000022.html +0 -25
- data/doc/classes/Inflector.src/M000023.html +0 -22
- data/doc/classes/Inflector.src/M000024.html +0 -18
- data/doc/classes/Inflector.src/M000025.html +0 -22
- data/doc/classes/Inflector.src/M000026.html +0 -18
- data/doc/classes/Inflector.src/M000027.html +0 -18
- data/doc/classes/Inflector.src/M000028.html +0 -18
- data/doc/classes/Inflector.src/M000029.html +0 -18
- data/doc/classes/Inflector.src/M000030.html +0 -19
- data/doc/classes/Inflector.src/M000031.html +0 -18
- data/doc/classes/Inflector.src/M000032.html +0 -22
- data/doc/classes/Inflector.src/M000033.html +0 -27
- data/doc/classes/Inflector/Inflections.html +0 -323
- data/doc/classes/Inflector/Inflections.src/M000034.html +0 -18
- data/doc/classes/Inflector/Inflections.src/M000035.html +0 -18
- data/doc/classes/Inflector/Inflections.src/M000036.html +0 -18
- data/doc/classes/Inflector/Inflections.src/M000037.html +0 -19
- data/doc/classes/Inflector/Inflections.src/M000038.html +0 -18
- data/doc/classes/Inflector/Inflections.src/M000039.html +0 -23
- data/doc/classes/Merb.html +0 -111
- data/doc/classes/Merb/Request.html +0 -139
- data/doc/classes/Merb/Request.src/M000041.html +0 -22
- data/doc/classes/OAuth.html +0 -112
- data/doc/classes/OAuth/RequestProxy.html +0 -111
- data/doc/classes/OAuth/RequestProxy/Base.html +0 -139
- data/doc/classes/OAuth/RequestProxy/Base.src/M000015.html +0 -18
- data/doc/classes/OAuth/Signature.html +0 -111
- data/doc/classes/OAuth/Signature/Base.html +0 -139
- data/doc/classes/OAuth/Signature/Base.src/M000014.html +0 -25
- data/doc/classes/OAuthController.html +0 -243
- data/doc/classes/OAuthController.src/M000008.html +0 -22
- data/doc/classes/OAuthController.src/M000009.html +0 -18
- data/doc/classes/OAuthController.src/M000010.html +0 -18
- data/doc/classes/OAuthController.src/M000011.html +0 -25
- data/doc/classes/OAuthController.src/M000012.html +0 -19
- data/doc/classes/Object.html +0 -154
- data/doc/classes/Object.src/M000013.html +0 -19
- data/doc/classes/Proc.html +0 -143
- data/doc/classes/Proc.src/M000007.html +0 -18
- data/doc/classes/Serialize.html +0 -189
- data/doc/classes/Serialize.src/M000016.html +0 -21
- data/doc/classes/Serialize.src/M000017.html +0 -39
- data/doc/classes/Serialize.src/M000018.html +0 -16
- data/doc/classes/Serialize.src/M000019.html +0 -18
- data/doc/classes/SimpleMapper.html +0 -151
- data/doc/classes/SimpleMapper/Base.html +0 -621
- data/doc/classes/SimpleMapper/Base.src/M000067.html +0 -16
- data/doc/classes/SimpleMapper/Base.src/M000068.html +0 -16
- data/doc/classes/SimpleMapper/Base.src/M000069.html +0 -18
- data/doc/classes/SimpleMapper/Base.src/M000070.html +0 -29
- data/doc/classes/SimpleMapper/Base.src/M000071.html +0 -18
- data/doc/classes/SimpleMapper/Base.src/M000072.html +0 -21
- data/doc/classes/SimpleMapper/Base.src/M000073.html +0 -18
- data/doc/classes/SimpleMapper/Base.src/M000074.html +0 -27
- data/doc/classes/SimpleMapper/Base.src/M000075.html +0 -21
- data/doc/classes/SimpleMapper/Base.src/M000076.html +0 -18
- data/doc/classes/SimpleMapper/Base.src/M000077.html +0 -18
- data/doc/classes/SimpleMapper/Base.src/M000078.html +0 -19
- data/doc/classes/SimpleMapper/Base.src/M000079.html +0 -23
- data/doc/classes/SimpleMapper/Base.src/M000080.html +0 -21
- data/doc/classes/SimpleMapper/Base.src/M000081.html +0 -18
- data/doc/classes/SimpleMapper/Base.src/M000082.html +0 -19
- data/doc/classes/SimpleMapper/Base.src/M000083.html +0 -18
- data/doc/classes/SimpleMapper/Base.src/M000085.html +0 -18
- data/doc/classes/SimpleMapper/Base.src/M000086.html +0 -19
- data/doc/classes/SimpleMapper/Base.src/M000087.html +0 -18
- data/doc/classes/SimpleMapper/Base.src/M000088.html +0 -18
- data/doc/classes/SimpleMapper/Base.src/M000089.html +0 -18
- data/doc/classes/SimpleMapper/Base.src/M000090.html +0 -19
- data/doc/classes/SimpleMapper/Base.src/M000091.html +0 -20
- data/doc/classes/SimpleMapper/Base.src/M000092.html +0 -24
- data/doc/classes/SimpleMapper/Base.src/M000093.html +0 -18
- data/doc/classes/SimpleMapper/CallbacksExtension.html +0 -161
- data/doc/classes/SimpleMapper/CallbacksExtension.src/M000056.html +0 -18
- data/doc/classes/SimpleMapper/CallbacksExtension.src/M000057.html +0 -18
- data/doc/classes/SimpleMapper/CallbacksExtension.src/M000058.html +0 -19
- data/doc/classes/SimpleMapper/HttpAdapter.html +0 -289
- data/doc/classes/SimpleMapper/HttpAdapter.src/M000059.html +0 -18
- data/doc/classes/SimpleMapper/HttpAdapter.src/M000060.html +0 -18
- data/doc/classes/SimpleMapper/HttpAdapter.src/M000061.html +0 -19
- data/doc/classes/SimpleMapper/HttpAdapter.src/M000063.html +0 -19
- data/doc/classes/SimpleMapper/HttpAdapter.src/M000064.html +0 -18
- data/doc/classes/SimpleMapper/HttpAdapter.src/M000065.html +0 -18
- data/doc/classes/SimpleMapper/HttpAdapter.src/M000066.html +0 -18
- data/doc/classes/SimpleMapper/HttpOAuthExtension.html +0 -188
- data/doc/classes/SimpleMapper/HttpOAuthExtension.src/M000048.html +0 -47
- data/doc/classes/SimpleMapper/HttpOAuthExtension.src/M000049.html +0 -21
- data/doc/classes/SimpleMapper/HttpOAuthExtension.src/M000050.html +0 -18
- data/doc/classes/SimpleMapper/HttpOAuthExtension.src/M000051.html +0 -24
- data/doc/classes/SimpleMapper/SimpleModel.html +0 -184
- data/doc/classes/SimpleMapper/SimpleModel.src/M000042.html +0 -18
- data/doc/classes/SimpleMapper/SimpleModel.src/M000043.html +0 -18
- data/doc/classes/SimpleMapper/SimpleModel.src/M000044.html +0 -18
- data/doc/classes/SimpleMapper/SimpleModel.src/M000045.html +0 -24
- data/doc/classes/SimpleMapper/SimpleModel/ClassMethods.html +0 -146
- data/doc/classes/SimpleMapper/SimpleModel/ClassMethods.src/M000046.html +0 -19
- data/doc/classes/SimpleMapper/SimpleModel/ClassMethods.src/M000047.html +0 -19
- data/doc/classes/SimpleMapper/XmlFormat.html +0 -169
- data/doc/classes/SimpleMapper/XmlFormat.src/M000052.html +0 -18
- data/doc/classes/SimpleMapper/XmlFormat.src/M000053.html +0 -18
- data/doc/classes/SimpleMapper/XmlFormat.src/M000054.html +0 -20
- data/doc/classes/SimpleMapper/XmlFormat/ClassMethods.html +0 -152
- data/doc/classes/SimpleMapper/XmlFormat/ClassMethods.src/M000055.html +0 -32
- data/doc/classes/String.html +0 -120
- data/doc/classes/Time.html +0 -139
- data/doc/classes/Time.src/M000001.html +0 -18
- data/doc/created.rid +0 -1
- data/doc/files/LICENSE.html +0 -129
- data/doc/files/README.html +0 -130
- data/doc/files/lib/simple_mapper/adapters/http_adapter_rb.html +0 -111
- data/doc/files/lib/simple_mapper/base_rb.html +0 -108
- data/doc/files/lib/simple_mapper/default_plugins/callbacks_rb.html +0 -101
- data/doc/files/lib/simple_mapper/default_plugins/oauth_rb.html +0 -110
- data/doc/files/lib/simple_mapper/default_plugins/options_to_query_rb.html +0 -116
- data/doc/files/lib/simple_mapper/default_plugins/simple_model_rb.html +0 -101
- data/doc/files/lib/simple_mapper/formats/xml_format_rb.html +0 -110
- data/doc/files/lib/simple_mapper/support/bliss_serializer_rb.html +0 -110
- data/doc/files/lib/simple_mapper/support/core_ext_rb.html +0 -108
- data/doc/files/lib/simple_mapper/support/inflections_rb.html +0 -101
- data/doc/files/lib/simple_mapper/support/inflector_rb.html +0 -108
- data/doc/files/lib/simple_mapper/support_rb.html +0 -109
- data/doc/files/lib/simple_mapper_rb.html +0 -137
- data/doc/fr_class_index.html +0 -53
- data/doc/fr_file_index.html +0 -41
- data/doc/fr_method_index.html +0 -119
- data/doc/index.html +0 -24
- data/doc/rdoc-style.css +0 -208
- data/lib/simple_mapper/default_plugins/simple_model.rb +0 -36
data/Rakefile
CHANGED
@@ -12,6 +12,8 @@ require 'simple_mapper/default_plugins/options_to_query'
|
|
12
12
|
module SimpleMapper
|
13
13
|
class HttpAdapter
|
14
14
|
attr_accessor :base_url
|
15
|
+
attr_accessor :raise_http_errors
|
16
|
+
|
15
17
|
alias :set_base_url :base_url=
|
16
18
|
def base_uri
|
17
19
|
URI.parse(base_url)
|
@@ -26,22 +28,51 @@ module SimpleMapper
|
|
26
28
|
end
|
27
29
|
alias :set_headers :headers=
|
28
30
|
|
31
|
+
def finder_options
|
32
|
+
@finder_options ||= {}
|
33
|
+
end
|
34
|
+
def finder_options=(options)
|
35
|
+
raise TypeError, "options must be a hash!" unless options.is_a?(Hash)
|
36
|
+
@finder_options = options
|
37
|
+
end
|
38
|
+
|
29
39
|
def get(find_options={})
|
30
|
-
|
31
|
-
|
40
|
+
options = finder_options.merge(find_options)
|
41
|
+
query = options.empty? ? '' : ('?' + options.to_query)
|
42
|
+
begin
|
43
|
+
http.request(request('get', base_uri.path + query)).body
|
44
|
+
rescue => e
|
45
|
+
raise e if !!raise_http_errors
|
46
|
+
nil
|
47
|
+
end
|
32
48
|
end
|
33
49
|
|
34
50
|
def put(identifier,data)
|
35
|
-
|
51
|
+
begin
|
52
|
+
http.request(request('put', URI.parse(identifier).path, data)).body
|
53
|
+
rescue => e
|
54
|
+
raise e if !!raise_http_errors
|
55
|
+
nil
|
56
|
+
end
|
36
57
|
end
|
37
58
|
|
38
59
|
def post(data)
|
39
|
-
|
60
|
+
begin
|
61
|
+
http.request(request('post', base_uri.path, data)).body
|
62
|
+
rescue => e
|
63
|
+
raise e if !!raise_http_errors
|
64
|
+
nil
|
65
|
+
end
|
40
66
|
end
|
41
67
|
|
42
68
|
# In the http adapter, the identifier is a url.
|
43
69
|
def delete(identifier)
|
44
|
-
|
70
|
+
begin
|
71
|
+
http.request(request('delete', URI.parse(identifier).path)).body
|
72
|
+
rescue => e
|
73
|
+
raise e if !!raise_http_errors
|
74
|
+
nil
|
75
|
+
end
|
45
76
|
end
|
46
77
|
|
47
78
|
private
|
data/lib/simple_mapper/base.rb
CHANGED
@@ -1,181 +1,11 @@
|
|
1
|
-
require 'simple_mapper/support'
|
1
|
+
require File.expand_path('simple_mapper/support')
|
2
|
+
require File.expand_path('simple_mapper/plugin_support')
|
2
3
|
|
3
4
|
module SimpleMapper
|
4
5
|
class Base
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
def debug!; @debug = true end
|
9
|
-
|
10
|
-
def connection_adapters
|
11
|
-
@connection_adapters ||= Hash.new {|h,k| h[k] = {}}
|
12
|
-
end
|
13
|
-
|
14
|
-
def add_connection_adapter(name_or_adapter,adapter=nil,&block)
|
15
|
-
if adapter.nil?
|
16
|
-
adapter = name_or_adapter
|
17
|
-
name = :default
|
18
|
-
else
|
19
|
-
name = name_or_adapter.to_sym
|
20
|
-
end
|
21
|
-
# Should complain if the adapter doesn't exist.
|
22
|
-
connection_adapters[name][:adapter] = adapter
|
23
|
-
require "#{File.dirname(__FILE__)}/adapters/#{adapter}_adapter"
|
24
|
-
connection_adapters[name][:init_block] = block if block_given?
|
25
|
-
connection_adapters[name][:debug] = @debug
|
26
|
-
[name, adapter]
|
27
|
-
end
|
28
|
-
|
29
|
-
# This is only here to show you in the docs how to clone a connection
|
30
|
-
# connection_adapters[adapter_name] = connection_adapters[adapter_name]
|
31
|
-
# connections[adapter_name] = klass.connection(adapter_name)
|
32
|
-
def clone_connection(klass,adapter_name=nil)
|
33
|
-
raise 'Not Implemented!'
|
34
|
-
end
|
35
|
-
|
36
|
-
# set_format :xml
|
37
|
-
# self.format = :json
|
38
|
-
def format=(format)
|
39
|
-
@format_name = format.to_s
|
40
|
-
require "#{File.dirname(__FILE__)}/formats/#{@format_name}_format"
|
41
|
-
@format = Object.module_eval("::SimpleMapper::#{@format_name.camelize}Format", __FILE__, __LINE__)
|
42
|
-
include @format
|
43
|
-
end
|
44
|
-
alias :set_format :format=
|
45
|
-
attr_reader :format_name
|
46
|
-
|
47
|
-
def connections
|
48
|
-
@connections ||= {}
|
49
|
-
end
|
50
|
-
def connection(name=:default,refresh=false)
|
51
|
-
connections[name] = begin
|
52
|
-
# Initialize the connection with the connection adapter.
|
53
|
-
raise ArgumentError, "Must include :adapter!" unless connection_adapters[name][:adapter].to_s.camelize.length > 0
|
54
|
-
adapter = Object.module_eval("::SimpleMapper::#{connection_adapters[name][:adapter].to_s.camelize}Adapter", __FILE__, __LINE__).new
|
55
|
-
connection_adapters[name][:init_block].in_context(adapter).call if connection_adapters[name][:init_block].is_a?(Proc)
|
56
|
-
adapter.set_headers format.mime_type_headers
|
57
|
-
adapter.debug! if connection_adapters[name][:debug]
|
58
|
-
adapter
|
59
|
-
end if !connections[name] || refresh
|
60
|
-
connections[name]
|
61
|
-
end
|
62
|
-
|
63
|
-
# get
|
64
|
-
def get(*args)
|
65
|
-
adapter = adapter_from_args(*args)
|
66
|
-
objs = extract_from(connection(adapter || :default).get(*args))
|
67
|
-
objs.is_a?(Array) ? objs.each {|e| e.instance_variable_set(:@adapter, adapter)} : objs.instance_variable_set(:@adapter, adapter) if adapter
|
68
|
-
objs
|
69
|
-
end
|
70
|
-
|
71
|
-
# new.save
|
72
|
-
def create(*args)
|
73
|
-
new(*args).save
|
74
|
-
end
|
75
|
-
|
76
|
-
def persistent?
|
77
|
-
true
|
78
|
-
end
|
79
|
-
|
80
|
-
def extract_from(formatted_data)
|
81
|
-
objs = send("from_#{format_name}".to_sym, formatted_data)
|
82
|
-
objs.is_a?(Array) ? objs.collect {|e| e.extended {@persisted = true}} : objs.extended {@persisted = true}
|
83
|
-
end
|
84
|
-
|
85
|
-
def extract_one(formatted_data, identifier=nil)
|
86
|
-
objs = extract_from(formatted_data)
|
87
|
-
if objs.is_a?(Array)
|
88
|
-
identifier.nil? ? objs.first : objs.reject {|e| e.identifier != identifier}[0]
|
89
|
-
else
|
90
|
-
identifier.nil? ? objs : (objs.identifier == identifier ? objs : nil)
|
91
|
-
end
|
92
|
-
end
|
93
|
-
|
94
|
-
def load(data=nil)
|
95
|
-
obj = allocate
|
96
|
-
obj.original_data = data
|
97
|
-
obj.send(:initialize, data)
|
98
|
-
obj
|
99
|
-
end
|
100
|
-
|
101
|
-
private
|
102
|
-
def adapter_from_args(*args)
|
103
|
-
adapter = nil
|
104
|
-
adapter = args.first.delete(:adapter) if args.first.is_a?(Hash)
|
105
|
-
adapter
|
106
|
-
end
|
107
|
-
end
|
108
|
-
|
109
|
-
def initialize(data=nil)
|
110
|
-
self.data = data unless data.nil?
|
111
|
-
end
|
112
|
-
attr_reader :identifier
|
113
|
-
|
114
|
-
def original_data=(data)
|
115
|
-
@original_data = data.freeze
|
116
|
-
@original_attributes = data.keys
|
117
|
-
end
|
118
|
-
attr_reader :original_data
|
119
|
-
attr_reader :original_attributes
|
120
|
-
|
121
|
-
def data=(data)
|
122
|
-
instantiate(data)
|
123
|
-
end
|
124
|
-
alias :update_data :data=
|
125
|
-
def data
|
126
|
-
to_hash
|
127
|
-
end
|
128
|
-
|
129
|
-
# Sets the data into the object. This is provided as a default method, but your model can overwrite it any
|
130
|
-
# way you want. For example, you could set the data to some other object type, or to a Marshalled storage.
|
131
|
-
# The type of data you receive will depend on the format and parser you use. Of course you could make up
|
132
|
-
# your own spin-off of one of those, too.
|
133
|
-
def instantiate(data)
|
134
|
-
raise TypeError, "data must be a hash" unless data.is_a?(Hash)
|
135
|
-
data.each {|k,v| instance_variable_set("@#{k}".to_sym, v)}
|
136
|
-
end
|
137
|
-
|
138
|
-
# Reads the data from the object for saving back to the persisted store. This is provided as a default
|
139
|
-
# method, but you can overwrite it in your model.
|
140
|
-
def formatted_data
|
141
|
-
send("to_#{self.class.format_name}".to_sym)
|
142
|
-
end
|
143
|
-
|
144
|
-
def dirty?
|
145
|
-
data != original_data
|
146
|
-
end
|
147
|
-
|
148
|
-
# persisted? ? put : post
|
149
|
-
def save
|
150
|
-
persisted? ? put : post
|
151
|
-
end
|
152
|
-
|
153
|
-
# sends a put request with self.data
|
154
|
-
def put(*args)
|
155
|
-
self.data = self.class.extract_one(self.class.connection(@adapter || :default).put(identifier, formatted_data), identifier).to_hash
|
156
|
-
self
|
157
|
-
end
|
158
|
-
|
159
|
-
# sends a post request with self.data
|
160
|
-
def post(*args)
|
161
|
-
self.data = self.class.extract_one(self.class.connection(@adapter || :default).post(formatted_data)).to_hash
|
162
|
-
@persisted = true
|
163
|
-
self
|
164
|
-
end
|
165
|
-
|
166
|
-
# delete
|
167
|
-
def delete
|
168
|
-
if self.class.connection(@adapter || :default).delete(identifier)
|
169
|
-
@persisted = false
|
170
|
-
instance_variable_set('@'+self.class.identifier, nil)
|
171
|
-
true
|
172
|
-
else
|
173
|
-
false
|
174
|
-
end
|
175
|
-
end
|
176
|
-
|
177
|
-
def persisted?
|
178
|
-
!!@persisted && (self.class.identifier.nil? || !instance_variable_get('@'+self.class.identifier).nil?)
|
6
|
+
def self.inherited(klass)
|
7
|
+
klass.send(:include, SimpleMapper::Persistence)
|
8
|
+
SimpleMapper::Persistence::prepare_for_inheritance(klass)
|
179
9
|
end
|
180
10
|
end
|
181
11
|
end
|
@@ -0,0 +1,374 @@
|
|
1
|
+
require File.expand_path(File.dirname(__FILE__) + '/../support/inflector')
|
2
|
+
|
3
|
+
module SimpleMapper
|
4
|
+
module Associations
|
5
|
+
# Returns an Association::Instance or Association::Set object (depending on the type of association, :single or :many),
|
6
|
+
# with the association applied to the current object. Note that the association definition object is duplicated when it
|
7
|
+
# is applied to an object -- this allows for them to be modified _after_ they are applied to an object without modifying
|
8
|
+
# the class association template.
|
9
|
+
def association_set(name,options={})
|
10
|
+
@association_sets ||= {}
|
11
|
+
names = name.is_a?(Array) ? name : [name]
|
12
|
+
@association_sets[names.flatten.sort.join('&')] ||= names.inject(associations[names.shift]) {|a,n| a.merge(n)}.set(self)
|
13
|
+
end
|
14
|
+
|
15
|
+
# Accesses name in assocations hash
|
16
|
+
def association(name)
|
17
|
+
associations[name]
|
18
|
+
end
|
19
|
+
|
20
|
+
# Generates (and caches) a hash of the associations for this object's class -- but duplicates that are applied to this object.
|
21
|
+
def associations
|
22
|
+
@associations || begin
|
23
|
+
@associations = {}
|
24
|
+
self.class.associations.keys.each {|k| @associations[k] = self.class.associations[k].apply_to(self)}
|
25
|
+
obj = self
|
26
|
+
(class << @associations; self end).send(:define_method, :[]) do |k|
|
27
|
+
obj.class.associations[k].apply_to(obj)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
@associations
|
31
|
+
end
|
32
|
+
|
33
|
+
def self.included(base)
|
34
|
+
base.extend(AssociationMacros)
|
35
|
+
end
|
36
|
+
|
37
|
+
# AssociationMacros
|
38
|
+
#
|
39
|
+
# This class is automatically extended into your model class when you include SimpleMapper::Associations, and therefore
|
40
|
+
# gives all its methods to your model class.
|
41
|
+
#
|
42
|
+
# There are currently only two types of associations: :single and :many. These can be customized by applying
|
43
|
+
# primary or foreign association options (see Association). Different from ActiveRecord or DataMapper,
|
44
|
+
# these methods ONLY define an association mapping between models, they don't rely on an accessor method.
|
45
|
+
# By default, association accessor methods are created, but this can be disabled by specifying :accessor => false,
|
46
|
+
# or simply by overwriting the accessor method after creating the association.
|
47
|
+
# For :single type associations, a setter method is created, of the style 'association='. For example, if a
|
48
|
+
# Person.has_many :pets, then a pet would by default have a person= method.
|
49
|
+
#
|
50
|
+
# These macros are the only place that assume a method 'key' in your model class in order to automatically assume
|
51
|
+
# assocation attributes. For example, if a Door.has_many :doorknobs and Door.key == :id, then Doorknob will be
|
52
|
+
# assumed to have methods 'door_id' and 'door_id='. To change that up a little, if a Door.has_many(:doorknobs).as(:owner) and
|
53
|
+
# Door.key == :name, then Doorknob will be assumed to have methods 'owner_name' and 'owner_name='.
|
54
|
+
#
|
55
|
+
# See Association for more information on assumptions that are made about your model class.
|
56
|
+
module AssociationMacros
|
57
|
+
def associations
|
58
|
+
@associations ||= {}
|
59
|
+
end
|
60
|
+
def association(name, type, options={})
|
61
|
+
accessor = options.has_key?(:accessor) ? options.delete(:accessor) : true
|
62
|
+
raise ArgumentError, "type must be :single or :many" unless [:single, :many].include?(type)
|
63
|
+
associations[name] = Association.new(self, type, {:class_name => name}.merge(options))
|
64
|
+
define_method(name.to_s+'=') do |object|
|
65
|
+
association_set(name).associate(object)
|
66
|
+
end if type == :single
|
67
|
+
define_method(name) do
|
68
|
+
association_set(name)
|
69
|
+
end if accessor
|
70
|
+
associations[name]
|
71
|
+
end
|
72
|
+
|
73
|
+
def has_one(name, options={})
|
74
|
+
assoc = association(name, :single, options)
|
75
|
+
assoc.dynamic {|assoc,obj| assoc.foreign(Inflector.underscore(assoc.act_as.to_s) + '_' + self.key.to_s => self.key) if assoc.act_as} unless self.name.underscore == assoc.instance_variable_get(:@options)[:class_name].to_s
|
76
|
+
assoc
|
77
|
+
end
|
78
|
+
|
79
|
+
def belongs_to(name, options={})
|
80
|
+
assoc = association(name, :single, options)
|
81
|
+
assoc.dynamic {|assoc,obj| assoc.primary(assoc.associated_klass.key => Inflector.underscore(name.to_s) + '_' + assoc.associated_klass.key.to_s)} unless self.name.underscore == assoc.instance_variable_get(:@options)[:class_name].to_s
|
82
|
+
assoc
|
83
|
+
end
|
84
|
+
|
85
|
+
def has_many(name, options={})
|
86
|
+
assoc = association(name, :many, {:class_name => Inflector.singularize(name)}.merge(options))
|
87
|
+
assoc.dynamic {|assoc,obj| assoc.foreign(Inflector.underscore(assoc.act_as.to_s) + '_' + self.key.to_s => self.key) if assoc.act_as} unless self.name.underscore == assoc.instance_variable_get(:@options)[:class_name].to_s
|
88
|
+
assoc
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
# Association is a completely independent class that is designed to use standard methods in your model
|
93
|
+
# in order to bring associations to that model class. The following assumptions are made about the logic
|
94
|
+
# of the concept of associations:
|
95
|
+
#
|
96
|
+
# 1. object -> associated_objects: associations are ALWAYS managed on a basis of one object being associated with other objects. This means that you can specify association attributes that are based on the primary object, the associated (foreign) object, or both; but you can only 'scope' the finding of associated objects based on foreign attributes.
|
97
|
+
# 2. primary, foreign, and scope: primary refers to the object in hand, foreign refers to an associated object, and scope refers to a subset of the associated objects. Primary options govern the association-related attributes on the primary object, Foreign options govern the association-related attributes on the foreign object, and Scope options are simply extra unrelated attributes used to find a smaller group within the otherwise associatable objects.
|
98
|
+
# 3. Model.all: the 'all' method is called on your Model in order to find associated records, and is passed the aggregated finder_options.
|
99
|
+
#
|
100
|
+
# Todo: Add in the amazing :through option
|
101
|
+
class Association
|
102
|
+
OPTIONS = [:class_name]
|
103
|
+
|
104
|
+
attr_reader :type
|
105
|
+
|
106
|
+
def initialize(klass, type, options={}, object=nil)
|
107
|
+
@klass = klass
|
108
|
+
@type = type
|
109
|
+
@options = options
|
110
|
+
@object = object; @procs_run = 0
|
111
|
+
end
|
112
|
+
|
113
|
+
# # # # # # # # # # # # # # # # # # # # # # # # # #
|
114
|
+
begin # Chainable methods that all return the association object. Use these to form your associations.
|
115
|
+
# If the association has already been tied to an object, a modified duplicate will be returned instead.
|
116
|
+
|
117
|
+
# Sets association attributes that are based on the primary object. These are used both for finding
|
118
|
+
# associated objects and for creating a new association.
|
119
|
+
def primary(options={})
|
120
|
+
@primary_options = primary_options(false).merge(options)
|
121
|
+
@procs_run = 0
|
122
|
+
self
|
123
|
+
end
|
124
|
+
|
125
|
+
# Sets association attributes that are based on the foreign object. These are used both for finding
|
126
|
+
# associated objects and for creating a new association.
|
127
|
+
def foreign(options={})
|
128
|
+
@foreign_options = foreign_options(false).merge(options)
|
129
|
+
@procs_run = 0
|
130
|
+
self
|
131
|
+
end
|
132
|
+
|
133
|
+
# Sets finder_options that are based on the foreign object. These are used only for finding associated objects.
|
134
|
+
def scope(options={})
|
135
|
+
@foreign_scope = foreign_scope(false).merge(options)
|
136
|
+
@procs_run = 0
|
137
|
+
self
|
138
|
+
end
|
139
|
+
|
140
|
+
# Add a proc to be run before using the association on an object. The proc is called with two arguments:
|
141
|
+
# the first argument is this association, the second argument is the object the association is being called on.
|
142
|
+
def dynamic(&block)
|
143
|
+
dynamic_procs << block
|
144
|
+
@procs_run = 0
|
145
|
+
self
|
146
|
+
end
|
147
|
+
|
148
|
+
# Sets the association to 'act as' a different name. For association macros such as has_one and has_many, it will
|
149
|
+
# affect the name of the foreign_key. If there is no mirrored association specified, this name is also used as
|
150
|
+
# the mirrored association name.
|
151
|
+
def as(association_name)
|
152
|
+
@act_as = association_name
|
153
|
+
@procs_run = 0
|
154
|
+
self
|
155
|
+
end
|
156
|
+
|
157
|
+
# Sets the association to mirror another named association in the associated class.
|
158
|
+
def mirrors(association_name)
|
159
|
+
@mirrored_association_name = association_name
|
160
|
+
@procs_run = 0
|
161
|
+
self
|
162
|
+
end
|
163
|
+
end
|
164
|
+
# # # # # # # # # # # # # # # # # # # # # # # # # #
|
165
|
+
|
166
|
+
# # # # # # # # # # # # # # # # # # # # # # # # # #
|
167
|
+
# Accessor Methods
|
168
|
+
|
169
|
+
def inspect # :nodoc:
|
170
|
+
run_dynamic_procs
|
171
|
+
super
|
172
|
+
end
|
173
|
+
|
174
|
+
def primary_options(run_dynamic=true)
|
175
|
+
@primary_options ||= {}
|
176
|
+
run_dynamic_procs if run_dynamic
|
177
|
+
@primary_options
|
178
|
+
end
|
179
|
+
def foreign_options(run_dynamic=true)
|
180
|
+
@foreign_options ||= {}
|
181
|
+
run_dynamic_procs if run_dynamic
|
182
|
+
@foreign_options
|
183
|
+
end
|
184
|
+
def foreign_scope(run_dynamic=true)
|
185
|
+
@foreign_scope ||= {}
|
186
|
+
run_dynamic_procs if run_dynamic
|
187
|
+
@foreign_scope
|
188
|
+
end
|
189
|
+
|
190
|
+
# Returns the class associated, if it is found.
|
191
|
+
def associated_klass
|
192
|
+
@associated_klass ||= Object.module_eval("::#{Inflector.camelize(@options[:class_name].to_s)}", __FILE__, __LINE__)
|
193
|
+
end
|
194
|
+
|
195
|
+
# This is a very key method that creates an association set based on an object and the association.
|
196
|
+
# If you call it with an instance, it will dup the association for the set; If this association is
|
197
|
+
# already applied to an object, it creates an association set.
|
198
|
+
def set(instance=nil)
|
199
|
+
if instance
|
200
|
+
apply_to(instance).set
|
201
|
+
elsif @object
|
202
|
+
@type == :many ? Set.new(@object, self) : Instance.new(@object, self)
|
203
|
+
else
|
204
|
+
raise ArgumentError, "must include instance"
|
205
|
+
end
|
206
|
+
end
|
207
|
+
|
208
|
+
def mirrored_association_name
|
209
|
+
@mirrored_association_name || @act_as
|
210
|
+
end
|
211
|
+
|
212
|
+
def mirrored_association
|
213
|
+
return nil unless @mirrored_association_name || @act_as
|
214
|
+
@mirrored_association || begin
|
215
|
+
@procs_run = 0 # Just another place to reset this -- a dynamic proc could rely on a mirrored_association, after all...
|
216
|
+
@mirrored_association = associated_klass.associations[@mirrored_association_name || @act_as]
|
217
|
+
end
|
218
|
+
end
|
219
|
+
|
220
|
+
def act_as
|
221
|
+
@act_as || @mirrored_association_name
|
222
|
+
end
|
223
|
+
|
224
|
+
def primary?
|
225
|
+
!primary_options.empty?
|
226
|
+
end
|
227
|
+
def foreign?
|
228
|
+
!foreign_options.empty?
|
229
|
+
end
|
230
|
+
|
231
|
+
# The current finder_options used for finding associated objects. It simply combines primary, foreign, and scope.
|
232
|
+
# This is public only for debugging purposes.
|
233
|
+
def finder_options(run_dynamic=true)
|
234
|
+
primary_options(run_dynamic).merge(foreign_options(run_dynamic)).merge(foreign_scope(run_dynamic)).inject({}) do |h,(k,v)|
|
235
|
+
h[k] = (@object && v.is_a?(Symbol) && @object.respond_to?(v)) ? @object.send(v) : v
|
236
|
+
h
|
237
|
+
end
|
238
|
+
end
|
239
|
+
# # # # # # # # # # # # # # # # # # # # # # # # # #
|
240
|
+
|
241
|
+
# Ties the association definition with a primary object.
|
242
|
+
# This will allow for any options based on the object itself to be determined before running the query for associated objects.
|
243
|
+
def apply_to(object)
|
244
|
+
applied = dup
|
245
|
+
applied.instance_variable_set(:@object, object)
|
246
|
+
applied.instance_variable_set(:@procs_run, 0)
|
247
|
+
applied
|
248
|
+
end
|
249
|
+
|
250
|
+
# This is a powerful method that merges two associations into one new association.
|
251
|
+
def merge(other)
|
252
|
+
raise ArgumentError, "must be an association definition object" unless other.is_a?(SimpleMapper::Associations::Association)
|
253
|
+
# self.class.new(@klass, :many)
|
254
|
+
other
|
255
|
+
end
|
256
|
+
|
257
|
+
private
|
258
|
+
def run_dynamic_procs
|
259
|
+
!@object || @procs_run == dynamic_procs.length || begin
|
260
|
+
pre_find = finder_options(false)
|
261
|
+
dynamic_procs.each {|p| p.call(self,@object)}
|
262
|
+
post_find = finder_options(false)
|
263
|
+
@procs_run = dynamic_procs.length if !post_find.empty? && pre_find == post_find
|
264
|
+
end
|
265
|
+
end
|
266
|
+
def dynamic_procs
|
267
|
+
@dynamic_procs = (@dynamic_procs || []).reject {|p| !p.is_a?(Proc)}
|
268
|
+
end
|
269
|
+
|
270
|
+
public
|
271
|
+
class Instance
|
272
|
+
attr_accessor :association
|
273
|
+
|
274
|
+
def initialize(instance, association)
|
275
|
+
@instance = instance
|
276
|
+
@association = association
|
277
|
+
@items = nil
|
278
|
+
@loaded = false
|
279
|
+
end
|
280
|
+
|
281
|
+
def method_missing(method, *args)
|
282
|
+
(item || items).send(method, *args)
|
283
|
+
end
|
284
|
+
|
285
|
+
def dirty?
|
286
|
+
@items && @items.any? { |item| item.dirty? }
|
287
|
+
end
|
288
|
+
|
289
|
+
def associate!(object)
|
290
|
+
associate(object)
|
291
|
+
object.save
|
292
|
+
end
|
293
|
+
def associate(object,associate_other=true)
|
294
|
+
# puts "Associating #{@instance.inspect} << #{object.inspect} (#{@association.primary? ? 'Primary' : ''} / #{@association.foreign? ? 'Foreign' : ''})\n#{@association.inspect}"
|
295
|
+
# 1) Set the @association.primary attributes to @instance
|
296
|
+
if @association.primary?
|
297
|
+
# puts "Primary options: #{@association.primary_options.inspect}"
|
298
|
+
@association.primary_options.each do |atr,k|
|
299
|
+
# puts "\tSetting instance##{k} = #{object.respond_to?(atr) ? object.send(atr) : atr}"
|
300
|
+
@instance.send("#{k}=", object.send(atr)) if @instance.respond_to?("#{k}=") && object.respond_to?(atr)
|
301
|
+
end
|
302
|
+
end
|
303
|
+
# 2) Set the @association.foreign attributes to object
|
304
|
+
if @association.foreign?
|
305
|
+
# puts "Foreign options: #{@association.foreign_options.inspect}"
|
306
|
+
@association.foreign_options.each do |k,atr|
|
307
|
+
# puts "\tSetting object##{k} = #{@instance.respond_to?(atr) ? @instance.send(atr) : atr}"
|
308
|
+
object.send("#{k}=", @instance.respond_to?(atr) ? @instance.send(atr) : atr) if object.respond_to?("#{k}=")
|
309
|
+
end
|
310
|
+
end
|
311
|
+
# 3) If there is a @association.mirrored_association, call object.association_set(@association.mirrored_association_name).associate(@instance,false) on it.
|
312
|
+
if associate_other && @association.mirrored_association
|
313
|
+
object.association_set(@association.mirrored_association_name).associate(@instance,false)
|
314
|
+
end
|
315
|
+
(@items ||= []) << object
|
316
|
+
end
|
317
|
+
# REWRITE
|
318
|
+
# def disassociate(item=nil)
|
319
|
+
# dis_items = item || items
|
320
|
+
# (dis_items.is_a?(Array) ? dis_items : [dis_items]).each do |item|
|
321
|
+
# item.instance_variable_set("@#{@association.foreign_key}", nil) unless item.new_record? || @association.foreign_key == :none
|
322
|
+
# @items = @items - [item]
|
323
|
+
# end
|
324
|
+
# item || items
|
325
|
+
# end
|
326
|
+
def build(options)
|
327
|
+
associate(@association.associated_klass.new)
|
328
|
+
end
|
329
|
+
def create(options)
|
330
|
+
object = @association.associated_klass.new
|
331
|
+
associate(object)
|
332
|
+
object.save
|
333
|
+
end
|
334
|
+
def reload!
|
335
|
+
@items = nil
|
336
|
+
@loaded = false
|
337
|
+
true
|
338
|
+
end
|
339
|
+
|
340
|
+
def respond_to?(symbol)
|
341
|
+
(item || items).respond_to?(symbol) || super
|
342
|
+
end
|
343
|
+
|
344
|
+
def items
|
345
|
+
# This will look more like it was made for set
|
346
|
+
@items || begin
|
347
|
+
@items = @association.associated_klass.all(@association.finder_options)
|
348
|
+
@loaded = true
|
349
|
+
end
|
350
|
+
@items
|
351
|
+
end
|
352
|
+
def item
|
353
|
+
items.length == 1 ? items[0] : nil
|
354
|
+
end
|
355
|
+
|
356
|
+
def inspect
|
357
|
+
(item || items).inspect
|
358
|
+
end
|
359
|
+
end
|
360
|
+
|
361
|
+
class Set < Instance
|
362
|
+
include Enumerable
|
363
|
+
|
364
|
+
def each
|
365
|
+
items.each { |item| yield item }
|
366
|
+
end
|
367
|
+
|
368
|
+
def <<(object)
|
369
|
+
(@items ||= []) << associate(object)
|
370
|
+
end
|
371
|
+
end
|
372
|
+
end
|
373
|
+
end
|
374
|
+
end
|