active_force 0.6.1 → 0.7.0

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