simplemapper 0.0.4 → 0.0.5
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/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
|