active_force 0.6.1 → 0.7.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (36) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +7 -0
  3. data/README.md +43 -17
  4. data/lib/active_attr/dirty.rb +3 -0
  5. data/lib/active_force/active_query.rb +32 -2
  6. data/lib/active_force/association.rb +14 -10
  7. data/lib/active_force/association/association.rb +26 -11
  8. data/lib/active_force/association/belongs_to_association.rb +1 -4
  9. data/lib/active_force/association/eager_load_projection_builder.rb +60 -0
  10. data/lib/active_force/association/has_many_association.rb +15 -5
  11. data/lib/active_force/association/relation_model_builder.rb +70 -0
  12. data/lib/active_force/attribute.rb +30 -0
  13. data/lib/active_force/mapping.rb +78 -0
  14. data/lib/active_force/query.rb +2 -8
  15. data/lib/active_force/sobject.rb +79 -95
  16. data/lib/active_force/table.rb +6 -2
  17. data/lib/active_force/version.rb +1 -1
  18. data/lib/generators/active_force/model/model_generator.rb +1 -0
  19. data/lib/generators/active_force/model/templates/model.rb.erb +0 -2
  20. data/spec/active_force/active_query_spec.rb +39 -12
  21. data/spec/active_force/association/relation_model_builder_spec.rb +62 -0
  22. data/spec/active_force/association_spec.rb +53 -88
  23. data/spec/active_force/attribute_spec.rb +27 -0
  24. data/spec/active_force/callbacks_spec.rb +1 -23
  25. data/spec/active_force/mapping_spec.rb +18 -0
  26. data/spec/active_force/query_spec.rb +32 -54
  27. data/spec/active_force/sobject/includes_spec.rb +290 -0
  28. data/spec/active_force/sobject/table_name_spec.rb +0 -21
  29. data/spec/active_force/sobject_spec.rb +212 -29
  30. data/spec/active_force/table_spec.rb +0 -3
  31. data/spec/fixtures/sobject/single_sobject_hash.yml +2 -0
  32. data/spec/spec_helper.rb +10 -4
  33. data/spec/support/restforce_factories.rb +9 -0
  34. data/spec/support/sobjects.rb +97 -0
  35. data/spec/support/whizbang.rb +25 -7
  36. metadata +18 -2
@@ -0,0 +1,70 @@
1
+ module ActiveForce
2
+ module Association
3
+ class RelationModelBuilder
4
+ class << self
5
+ def build(association, value)
6
+ new(association, value).build_relation_model
7
+ end
8
+ end
9
+
10
+ def initialize(association, value)
11
+ @association = association
12
+ @value = value
13
+ end
14
+
15
+ def build_relation_model
16
+ klass = resolve_class
17
+ klass.new(@association, @value).call
18
+ end
19
+
20
+ private
21
+
22
+ def resolve_class
23
+ association_builder = @value.class.name.gsub('::', '_')
24
+ ActiveForce::Association.const_get "BuildFrom#{association_builder}"
25
+ rescue NameError
26
+ raise "Don't know how to build relation from #{@value.class.name}"
27
+ end
28
+ end
29
+
30
+ class AbstractBuildFrom
31
+ attr_reader :association, :value
32
+
33
+ def initialize(association, value)
34
+ @association = association
35
+ @value = value
36
+ end
37
+
38
+ def call
39
+ raise "Must implement #{self.class.name}#call"
40
+ end
41
+ end
42
+
43
+ class BuildFromHash < AbstractBuildFrom
44
+ def call
45
+ association.build value
46
+ end
47
+ end
48
+
49
+ class BuildFromArray < AbstractBuildFrom
50
+ def call
51
+ value.map { |mash| association.build mash }
52
+ end
53
+ end
54
+
55
+ class BuildFromNilClass < AbstractBuildFrom
56
+ def call
57
+ association.is_a?(BelongsToAssociation) ? nil : []
58
+ end
59
+ end
60
+
61
+ class BuildFromRestforce_SObject < BuildFromHash
62
+ end
63
+
64
+ class BuildFromRestforce_Mash < BuildFromHash
65
+ end
66
+
67
+ class BuildFromRestforce_Collection < BuildFromArray
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,30 @@
1
+ module ActiveForce
2
+ class Attribute
3
+
4
+ attr_accessor :local_name, :sfdc_name, :as
5
+
6
+ def initialize name, options = {}
7
+ self.local_name = name
8
+ self.sfdc_name = options[:sfdc_name] || options[:from] || default_api_name
9
+ self.as = options[:as] || :string
10
+ end
11
+
12
+ def value_for_hash value
13
+ case as
14
+ when :multi_picklist
15
+ value.reject(&:empty?).join(';')
16
+ else
17
+ value
18
+ end
19
+ end
20
+
21
+ private
22
+
23
+ ###
24
+ # Transforms +attribute+ to the conventional Salesforce API name.
25
+ #
26
+ def default_api_name
27
+ local_name.to_s.split('_').map(&:capitalize).join('_') << '__c'
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,78 @@
1
+ require 'active_force/attribute'
2
+ require 'active_force/table'
3
+ require 'forwardable'
4
+
5
+ module ActiveForce
6
+ class Mapping
7
+ extend Forwardable
8
+
9
+ STRINGLIKE_TYPES = [
10
+ nil, :string, :base64, :byte, :ID, :reference, :currency, :textarea,
11
+ :phone, :url, :email, :combobox, :picklist, :multipicklist, :anyType,
12
+ :location
13
+ ]
14
+
15
+ def_delegators :table, :custom_table?, :table_name
16
+
17
+ def initialize model
18
+ @model = model
19
+ end
20
+
21
+ def mappings
22
+ Hash[fields.map { |field, attr| [field, attr.sfdc_name] }]
23
+ end
24
+
25
+ def sfdc_names
26
+ mappings.values
27
+ end
28
+
29
+ def field name, options
30
+ fields.merge!({ name => ActiveForce::Attribute.new(name, options) })
31
+ end
32
+
33
+ def table
34
+ @table ||= ActiveForce::Table.new @model
35
+ end
36
+
37
+ def translate_to_sf attributes
38
+ attrs = attributes.map do |attribute, value|
39
+ attr = fields[attribute.to_sym]
40
+ [attr.sfdc_name, attr.value_for_hash(value)]
41
+ end
42
+ Hash[attrs]
43
+ end
44
+
45
+ def translate_value value, field_name
46
+ return value unless !!field_name
47
+ typecast_value value.to_s, fields[field_name].as
48
+ end
49
+
50
+
51
+ private
52
+
53
+ def fields
54
+ @fields ||= {}
55
+ end
56
+
57
+ # Handles Salesforce FieldTypes as described here:
58
+ # http://www.salesforce.com/us/developer/docs/api/Content/sforce_api_calls_describesobjects_describesobjectresult.htm#i1427700
59
+ def typecast_value value, type
60
+ case type
61
+ when *STRINGLIKE_TYPES
62
+ value
63
+ when :boolean
64
+ !['false','0','f'].include? value.downcase
65
+ when :int
66
+ value.to_i
67
+ when :double, :percent
68
+ value.to_f
69
+ when :date
70
+ Date.parse value
71
+ when :datetime
72
+ DateTime.parse value
73
+ else
74
+ value
75
+ end
76
+ end
77
+ end
78
+ end
@@ -86,19 +86,13 @@ module ActiveForce
86
86
  self
87
87
  end
88
88
 
89
- def options args
90
- where args[:where]
91
- limit args[:limit]
92
- order args[:order]
93
- end
94
-
95
89
  protected
96
90
  def build_select
97
- @query_fields.uniq.join(', ')
91
+ @query_fields.compact.uniq.join(', ')
98
92
  end
99
93
 
100
94
  def build_where
101
- "WHERE #{ @conditions.join(' AND ') }" unless @conditions.empty?
95
+ "WHERE (#{ @conditions.join(') AND (') })" unless @conditions.empty?
102
96
  end
103
97
 
104
98
  def build_limit
@@ -3,17 +3,19 @@ require 'active_attr'
3
3
  require 'active_attr/dirty'
4
4
  require 'active_force/active_query'
5
5
  require 'active_force/association'
6
- require 'active_force/table'
6
+ require 'active_force/mapping'
7
7
  require 'yaml'
8
8
  require 'forwardable'
9
9
  require 'logger'
10
10
  require 'restforce'
11
11
 
12
12
  module ActiveForce
13
+ class RecordInvalid < StandardError;end
14
+
13
15
  class SObject
14
16
  include ActiveAttr::Model
15
17
  include ActiveAttr::Dirty
16
- include ActiveForce::Association
18
+ extend ActiveForce::Association
17
19
  extend ActiveModel::Callbacks
18
20
 
19
21
  define_model_callbacks :save, :create, :update
@@ -22,21 +24,11 @@ module ActiveForce
22
24
 
23
25
  class << self
24
26
  extend Forwardable
25
- def_delegators :query, :where, :first, :last, :all, :find, :find_by, :count
26
- def_delegators :table, :custom_table_name?
27
+ def_delegators :query, :where, :first, :last, :all, :find, :find_by, :count, :includes, :limit, :order, :select
28
+ def_delegators :mapping, :table, :table_name, :custom_table?, :mappings
27
29
 
28
30
  private
29
31
 
30
- ###
31
- # Transforms +attribute+ to the conventional Salesforce API name.
32
- #
33
- # Example:
34
- # > default_api_name :some_attribute
35
- # => "Some_Attribute__c"
36
- def default_api_name(attribute)
37
- String(attribute).split('_').map(&:capitalize).join('_') << '__c'
38
- end
39
-
40
32
  ###
41
33
  # Provide each subclass with a default id field. Can be overridden
42
34
  # in the subclass if needed
@@ -45,29 +37,23 @@ module ActiveForce
45
37
  end
46
38
  end
47
39
 
48
- # The table name to used to make queries.
49
- # It is derived from the class name adding the "__c" when needed.
50
- def self.table_name
51
- table.name
52
- end
53
-
54
- def self.table
55
- @table ||= ActiveForce::Table.new name
40
+ def self.mapping
41
+ @mapping ||= ActiveForce::Mapping.new name
56
42
  end
57
43
 
58
44
  def self.fields
59
- mappings.values
45
+ mapping.sfdc_names
60
46
  end
61
47
 
62
48
  def self.query
63
49
  ActiveForce::ActiveQuery.new self
64
50
  end
65
51
 
66
- def self.build sf_table_description
67
- return unless sf_table_description
52
+ def self.build mash
53
+ return unless mash
68
54
  sobject = new
69
- mappings.each do |attr, sf_field|
70
- sobject[attr] = sf_table_description[sf_field]
55
+ mash.each do |column, sf_value|
56
+ sobject.write_value column, sf_value
71
57
  end
72
58
  sobject.changed_attributes.clear
73
59
  sobject
@@ -75,35 +61,42 @@ module ActiveForce
75
61
 
76
62
  def update_attributes! attributes = {}
77
63
  assign_attributes attributes
78
- return false unless valid?
79
- sfdc_client.update! table_name, attributes_for_sfdb
80
- changed_attributes.clear
81
- self
64
+ validate!
65
+ run_callbacks :save do
66
+ run_callbacks :update do
67
+ sfdc_client.update! table_name, attributes_for_sfdb
68
+ changed_attributes.clear
69
+ end
70
+ end
71
+ true
82
72
  end
83
73
 
74
+ alias_method :update!, :update_attributes!
75
+
84
76
  def update_attributes attributes = {}
85
- run_callbacks :update do
86
- update_attributes! attributes
87
- end
88
- rescue Faraday::Error::ClientError => error
89
- logger_output __method__
77
+ update_attributes! attributes
78
+ rescue Faraday::Error::ClientError, RecordInvalid => error
79
+ handle_save_error error
90
80
  end
91
81
 
92
82
  alias_method :update, :update_attributes
93
83
 
94
84
  def create!
95
- return false unless valid?
96
- self.id = sfdc_client.create! table_name, attributes_for_sfdb
97
- changed_attributes.clear
85
+ validate!
86
+ run_callbacks :save do
87
+ run_callbacks :create do
88
+ self.id = sfdc_client.create! table_name, attributes_for_sfdb
89
+ changed_attributes.clear
90
+ end
91
+ end
98
92
  self
99
93
  end
100
94
 
101
95
  def create
102
- run_callbacks :create do
103
- create!
104
- end
105
- rescue Faraday::Error::ClientError => error
106
- logger_output __method__
96
+ create!
97
+ rescue Faraday::Error::ClientError, RecordInvalid => error
98
+ handle_save_error error
99
+ self
107
100
  end
108
101
 
109
102
  def destroy
@@ -111,27 +104,27 @@ module ActiveForce
111
104
  end
112
105
 
113
106
  def self.create args
114
- new(args).save
107
+ new(args).create
115
108
  end
116
109
 
117
110
  def self.create! args
118
- new(args).save!
111
+ new(args).create!
119
112
  end
120
113
 
121
- def save
114
+ def save!
122
115
  run_callbacks :save do
123
116
  if persisted?
124
- update
117
+ !!update!
125
118
  else
126
- create
119
+ !!create!
127
120
  end
128
121
  end
129
122
  end
130
123
 
131
- def save!
132
- save
133
- rescue Faraday::Error::ClientError => error
134
- logger_output __method__
124
+ def save
125
+ save!
126
+ rescue Faraday::Error::ClientError, RecordInvalid => error
127
+ handle_save_error error
135
128
  end
136
129
 
137
130
  def to_param
@@ -143,14 +136,8 @@ module ActiveForce
143
136
  end
144
137
 
145
138
  def self.field field_name, args = {}
146
- args[:from] ||= default_api_name(field_name)
147
- args[:as] ||= :string
148
- mappings[field_name] = args[:from]
149
- attribute field_name, sf_type: args[:as]
150
- end
151
-
152
- def self.mappings
153
- @mappings ||= {}
139
+ mapping.field field_name, args
140
+ attribute field_name
154
141
  end
155
142
 
156
143
  def reload
@@ -161,50 +148,47 @@ module ActiveForce
161
148
  self
162
149
  end
163
150
 
164
- private
165
-
166
- def association_cache
167
- @association_cache ||= {}
151
+ def write_value column, value
152
+ if association = self.class.find_association(column)
153
+ field = association.relation_name
154
+ value = Association::RelationModelBuilder.build(association, value)
155
+ else
156
+ field = mappings.invert[column]
157
+ value = self.class.mapping.translate_value value, field unless value.nil?
158
+ end
159
+ send "#{field}=", value if field
168
160
  end
169
161
 
170
- def logger_output action
171
- logger = Logger.new(STDOUT)
172
- logger.info("[SFDC] [#{self.class.model_name}] [#{self.class.table_name}] Error while #{ action }, params: #{hash}, error: #{error.inspect}")
173
- errors[:base] << error.message
174
- false
175
- end
162
+ private
176
163
 
177
- def attributes_for_sfdb
178
- if persisted?
179
- attrs = changed_mappings.map do |attr, sf_field|
180
- value = read_value(attr)
181
- [sf_field, value]
182
- end
183
- attrs << ['Id', id]
184
- else
185
- attrs = mappings.map do |attr, sf_field|
186
- value = read_value(attr)
187
- [sf_field, value] unless value.nil?
188
- end
164
+ def validate!
165
+ unless valid?
166
+ raise RecordInvalid.new(
167
+ "Validation failed: #{errors.full_messages.join(', ')}"
168
+ )
189
169
  end
190
- Hash[attrs.compact]
191
170
  end
192
171
 
193
- def changed_mappings
194
- mappings.select { |attr, sf_field| changed.include? attr.to_s}
172
+ def handle_save_error error
173
+ return false if error.class == RecordInvalid
174
+ logger_output __method__, error, attributes
195
175
  end
196
176
 
197
- def read_value field
198
- case sf_field_type field
199
- when :multi_picklist
200
- attribute(field.to_s).reject(&:empty?).join(';')
201
- else
202
- attribute(field.to_s)
203
- end
177
+ def association_cache
178
+ @association_cache ||= {}
204
179
  end
205
180
 
206
- def sf_field_type field
207
- self.class.attributes[field][:sf_type]
181
+ def logger_output action, exception, params = {}
182
+ logger = Logger.new(STDOUT)
183
+ logger.info("[SFDC] [#{self.class.model_name}] [#{self.class.table_name}] Error while #{ action }, params: #{params}, error: #{exception.inspect}")
184
+ errors[:base] << exception.message
185
+ false
186
+ end
187
+
188
+ def attributes_for_sfdb
189
+ attrs = self.class.mapping.translate_to_sf(attributes_and_changes)
190
+ attrs.merge!({'Id' => id }) if persisted?
191
+ attrs
208
192
  end
209
193
 
210
194
  def self.picklist field