simplemapper 0.0.4 → 0.0.5

Sign up to get free protection for your applications and to get access to all the features.
Files changed (151) hide show
  1. data/Rakefile +1 -1
  2. data/lib/simple_mapper/adapters/http_adapter.rb +36 -5
  3. data/lib/simple_mapper/base.rb +5 -175
  4. data/lib/simple_mapper/default_plugins/associations.rb +374 -0
  5. data/lib/simple_mapper/default_plugins/properties.rb +70 -0
  6. data/lib/simple_mapper/persistence.rb +169 -0
  7. data/lib/simple_mapper/plugin_support.rb +19 -0
  8. data/lib/simple_mapper/support.rb +2 -2
  9. data/lib/simple_mapper/support/bliss_serializer.rb +43 -12
  10. data/lib/simple_mapper/support/core_ext.rb +23 -2
  11. metadata +6 -179
  12. data/doc/classes/Array.html +0 -158
  13. data/doc/classes/Array.src/M000005.html +0 -18
  14. data/doc/classes/Array.src/M000006.html +0 -34
  15. data/doc/classes/Enumerable.html +0 -131
  16. data/doc/classes/Enumerable.src/M000040.html +0 -21
  17. data/doc/classes/Hash.html +0 -173
  18. data/doc/classes/Hash.src/M000002.html +0 -20
  19. data/doc/classes/Hash.src/M000003.html +0 -18
  20. data/doc/classes/Hash.src/M000004.html +0 -34
  21. data/doc/classes/Inflector.html +0 -516
  22. data/doc/classes/Inflector.src/M000020.html +0 -22
  23. data/doc/classes/Inflector.src/M000021.html +0 -25
  24. data/doc/classes/Inflector.src/M000022.html +0 -25
  25. data/doc/classes/Inflector.src/M000023.html +0 -22
  26. data/doc/classes/Inflector.src/M000024.html +0 -18
  27. data/doc/classes/Inflector.src/M000025.html +0 -22
  28. data/doc/classes/Inflector.src/M000026.html +0 -18
  29. data/doc/classes/Inflector.src/M000027.html +0 -18
  30. data/doc/classes/Inflector.src/M000028.html +0 -18
  31. data/doc/classes/Inflector.src/M000029.html +0 -18
  32. data/doc/classes/Inflector.src/M000030.html +0 -19
  33. data/doc/classes/Inflector.src/M000031.html +0 -18
  34. data/doc/classes/Inflector.src/M000032.html +0 -22
  35. data/doc/classes/Inflector.src/M000033.html +0 -27
  36. data/doc/classes/Inflector/Inflections.html +0 -323
  37. data/doc/classes/Inflector/Inflections.src/M000034.html +0 -18
  38. data/doc/classes/Inflector/Inflections.src/M000035.html +0 -18
  39. data/doc/classes/Inflector/Inflections.src/M000036.html +0 -18
  40. data/doc/classes/Inflector/Inflections.src/M000037.html +0 -19
  41. data/doc/classes/Inflector/Inflections.src/M000038.html +0 -18
  42. data/doc/classes/Inflector/Inflections.src/M000039.html +0 -23
  43. data/doc/classes/Merb.html +0 -111
  44. data/doc/classes/Merb/Request.html +0 -139
  45. data/doc/classes/Merb/Request.src/M000041.html +0 -22
  46. data/doc/classes/OAuth.html +0 -112
  47. data/doc/classes/OAuth/RequestProxy.html +0 -111
  48. data/doc/classes/OAuth/RequestProxy/Base.html +0 -139
  49. data/doc/classes/OAuth/RequestProxy/Base.src/M000015.html +0 -18
  50. data/doc/classes/OAuth/Signature.html +0 -111
  51. data/doc/classes/OAuth/Signature/Base.html +0 -139
  52. data/doc/classes/OAuth/Signature/Base.src/M000014.html +0 -25
  53. data/doc/classes/OAuthController.html +0 -243
  54. data/doc/classes/OAuthController.src/M000008.html +0 -22
  55. data/doc/classes/OAuthController.src/M000009.html +0 -18
  56. data/doc/classes/OAuthController.src/M000010.html +0 -18
  57. data/doc/classes/OAuthController.src/M000011.html +0 -25
  58. data/doc/classes/OAuthController.src/M000012.html +0 -19
  59. data/doc/classes/Object.html +0 -154
  60. data/doc/classes/Object.src/M000013.html +0 -19
  61. data/doc/classes/Proc.html +0 -143
  62. data/doc/classes/Proc.src/M000007.html +0 -18
  63. data/doc/classes/Serialize.html +0 -189
  64. data/doc/classes/Serialize.src/M000016.html +0 -21
  65. data/doc/classes/Serialize.src/M000017.html +0 -39
  66. data/doc/classes/Serialize.src/M000018.html +0 -16
  67. data/doc/classes/Serialize.src/M000019.html +0 -18
  68. data/doc/classes/SimpleMapper.html +0 -151
  69. data/doc/classes/SimpleMapper/Base.html +0 -621
  70. data/doc/classes/SimpleMapper/Base.src/M000067.html +0 -16
  71. data/doc/classes/SimpleMapper/Base.src/M000068.html +0 -16
  72. data/doc/classes/SimpleMapper/Base.src/M000069.html +0 -18
  73. data/doc/classes/SimpleMapper/Base.src/M000070.html +0 -29
  74. data/doc/classes/SimpleMapper/Base.src/M000071.html +0 -18
  75. data/doc/classes/SimpleMapper/Base.src/M000072.html +0 -21
  76. data/doc/classes/SimpleMapper/Base.src/M000073.html +0 -18
  77. data/doc/classes/SimpleMapper/Base.src/M000074.html +0 -27
  78. data/doc/classes/SimpleMapper/Base.src/M000075.html +0 -21
  79. data/doc/classes/SimpleMapper/Base.src/M000076.html +0 -18
  80. data/doc/classes/SimpleMapper/Base.src/M000077.html +0 -18
  81. data/doc/classes/SimpleMapper/Base.src/M000078.html +0 -19
  82. data/doc/classes/SimpleMapper/Base.src/M000079.html +0 -23
  83. data/doc/classes/SimpleMapper/Base.src/M000080.html +0 -21
  84. data/doc/classes/SimpleMapper/Base.src/M000081.html +0 -18
  85. data/doc/classes/SimpleMapper/Base.src/M000082.html +0 -19
  86. data/doc/classes/SimpleMapper/Base.src/M000083.html +0 -18
  87. data/doc/classes/SimpleMapper/Base.src/M000085.html +0 -18
  88. data/doc/classes/SimpleMapper/Base.src/M000086.html +0 -19
  89. data/doc/classes/SimpleMapper/Base.src/M000087.html +0 -18
  90. data/doc/classes/SimpleMapper/Base.src/M000088.html +0 -18
  91. data/doc/classes/SimpleMapper/Base.src/M000089.html +0 -18
  92. data/doc/classes/SimpleMapper/Base.src/M000090.html +0 -19
  93. data/doc/classes/SimpleMapper/Base.src/M000091.html +0 -20
  94. data/doc/classes/SimpleMapper/Base.src/M000092.html +0 -24
  95. data/doc/classes/SimpleMapper/Base.src/M000093.html +0 -18
  96. data/doc/classes/SimpleMapper/CallbacksExtension.html +0 -161
  97. data/doc/classes/SimpleMapper/CallbacksExtension.src/M000056.html +0 -18
  98. data/doc/classes/SimpleMapper/CallbacksExtension.src/M000057.html +0 -18
  99. data/doc/classes/SimpleMapper/CallbacksExtension.src/M000058.html +0 -19
  100. data/doc/classes/SimpleMapper/HttpAdapter.html +0 -289
  101. data/doc/classes/SimpleMapper/HttpAdapter.src/M000059.html +0 -18
  102. data/doc/classes/SimpleMapper/HttpAdapter.src/M000060.html +0 -18
  103. data/doc/classes/SimpleMapper/HttpAdapter.src/M000061.html +0 -19
  104. data/doc/classes/SimpleMapper/HttpAdapter.src/M000063.html +0 -19
  105. data/doc/classes/SimpleMapper/HttpAdapter.src/M000064.html +0 -18
  106. data/doc/classes/SimpleMapper/HttpAdapter.src/M000065.html +0 -18
  107. data/doc/classes/SimpleMapper/HttpAdapter.src/M000066.html +0 -18
  108. data/doc/classes/SimpleMapper/HttpOAuthExtension.html +0 -188
  109. data/doc/classes/SimpleMapper/HttpOAuthExtension.src/M000048.html +0 -47
  110. data/doc/classes/SimpleMapper/HttpOAuthExtension.src/M000049.html +0 -21
  111. data/doc/classes/SimpleMapper/HttpOAuthExtension.src/M000050.html +0 -18
  112. data/doc/classes/SimpleMapper/HttpOAuthExtension.src/M000051.html +0 -24
  113. data/doc/classes/SimpleMapper/SimpleModel.html +0 -184
  114. data/doc/classes/SimpleMapper/SimpleModel.src/M000042.html +0 -18
  115. data/doc/classes/SimpleMapper/SimpleModel.src/M000043.html +0 -18
  116. data/doc/classes/SimpleMapper/SimpleModel.src/M000044.html +0 -18
  117. data/doc/classes/SimpleMapper/SimpleModel.src/M000045.html +0 -24
  118. data/doc/classes/SimpleMapper/SimpleModel/ClassMethods.html +0 -146
  119. data/doc/classes/SimpleMapper/SimpleModel/ClassMethods.src/M000046.html +0 -19
  120. data/doc/classes/SimpleMapper/SimpleModel/ClassMethods.src/M000047.html +0 -19
  121. data/doc/classes/SimpleMapper/XmlFormat.html +0 -169
  122. data/doc/classes/SimpleMapper/XmlFormat.src/M000052.html +0 -18
  123. data/doc/classes/SimpleMapper/XmlFormat.src/M000053.html +0 -18
  124. data/doc/classes/SimpleMapper/XmlFormat.src/M000054.html +0 -20
  125. data/doc/classes/SimpleMapper/XmlFormat/ClassMethods.html +0 -152
  126. data/doc/classes/SimpleMapper/XmlFormat/ClassMethods.src/M000055.html +0 -32
  127. data/doc/classes/String.html +0 -120
  128. data/doc/classes/Time.html +0 -139
  129. data/doc/classes/Time.src/M000001.html +0 -18
  130. data/doc/created.rid +0 -1
  131. data/doc/files/LICENSE.html +0 -129
  132. data/doc/files/README.html +0 -130
  133. data/doc/files/lib/simple_mapper/adapters/http_adapter_rb.html +0 -111
  134. data/doc/files/lib/simple_mapper/base_rb.html +0 -108
  135. data/doc/files/lib/simple_mapper/default_plugins/callbacks_rb.html +0 -101
  136. data/doc/files/lib/simple_mapper/default_plugins/oauth_rb.html +0 -110
  137. data/doc/files/lib/simple_mapper/default_plugins/options_to_query_rb.html +0 -116
  138. data/doc/files/lib/simple_mapper/default_plugins/simple_model_rb.html +0 -101
  139. data/doc/files/lib/simple_mapper/formats/xml_format_rb.html +0 -110
  140. data/doc/files/lib/simple_mapper/support/bliss_serializer_rb.html +0 -110
  141. data/doc/files/lib/simple_mapper/support/core_ext_rb.html +0 -108
  142. data/doc/files/lib/simple_mapper/support/inflections_rb.html +0 -101
  143. data/doc/files/lib/simple_mapper/support/inflector_rb.html +0 -108
  144. data/doc/files/lib/simple_mapper/support_rb.html +0 -109
  145. data/doc/files/lib/simple_mapper_rb.html +0 -137
  146. data/doc/fr_class_index.html +0 -53
  147. data/doc/fr_file_index.html +0 -41
  148. data/doc/fr_method_index.html +0 -119
  149. data/doc/index.html +0 -24
  150. data/doc/rdoc-style.css +0 -208
  151. data/lib/simple_mapper/default_plugins/simple_model.rb +0 -36
data/Rakefile CHANGED
@@ -6,7 +6,7 @@ require 'rake/gempackagetask'
6
6
  require 'rake/contrib/rubyforgepublisher'
7
7
 
8
8
  PKG_NAME = 'simplemapper'
9
- PKG_VERSION = "0.0.4"
9
+ PKG_VERSION = "0.0.5"
10
10
 
11
11
  PKG_FILES = FileList[
12
12
  "lib/**/*", "rspec/**/*", "[A-Z]*", "Rakefile", "doc/**/*"
@@ -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
- query = find_options.empty? ? '' : ('?' + find_options.to_query)
31
- http.request(request('get', base_uri.path + query)).body
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
- http.request(request('put', URI.parse(identifier).path, data)).body
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
- http.request(request('post', base_uri.path, data)).body
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
- http.request(request('delete', URI.parse(identifier).path)).body
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
@@ -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
- class << self
6
- attr_reader :format
7
- def debug?; @debug end
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