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.
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