openstax_active_force 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (49) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +17 -0
  3. data/.mailmap +3 -0
  4. data/.rspec +2 -0
  5. data/.travis.yml +3 -0
  6. data/CHANGELOG.md +98 -0
  7. data/Gemfile +4 -0
  8. data/LICENSE.txt +22 -0
  9. data/README.md +174 -0
  10. data/Rakefile +6 -0
  11. data/active_force.gemspec +30 -0
  12. data/lib/active_attr/dirty.rb +33 -0
  13. data/lib/active_force/active_query.rb +155 -0
  14. data/lib/active_force/association/association.rb +50 -0
  15. data/lib/active_force/association/belongs_to_association.rb +26 -0
  16. data/lib/active_force/association/eager_load_projection_builder.rb +60 -0
  17. data/lib/active_force/association/has_many_association.rb +33 -0
  18. data/lib/active_force/association/relation_model_builder.rb +70 -0
  19. data/lib/active_force/association.rb +28 -0
  20. data/lib/active_force/attribute.rb +30 -0
  21. data/lib/active_force/mapping.rb +78 -0
  22. data/lib/active_force/query.rb +110 -0
  23. data/lib/active_force/sobject.rb +210 -0
  24. data/lib/active_force/standard_types.rb +357 -0
  25. data/lib/active_force/table.rb +37 -0
  26. data/lib/active_force/version.rb +3 -0
  27. data/lib/active_force.rb +13 -0
  28. data/lib/generators/active_force/model/USAGE +8 -0
  29. data/lib/generators/active_force/model/model_generator.rb +62 -0
  30. data/lib/generators/active_force/model/templates/model.rb.erb +5 -0
  31. data/spec/active_force/active_query_spec.rb +178 -0
  32. data/spec/active_force/association/relation_model_builder_spec.rb +62 -0
  33. data/spec/active_force/association_spec.rb +157 -0
  34. data/spec/active_force/attribute_spec.rb +27 -0
  35. data/spec/active_force/callbacks_spec.rb +20 -0
  36. data/spec/active_force/mapping_spec.rb +18 -0
  37. data/spec/active_force/query_spec.rb +126 -0
  38. data/spec/active_force/sobject/includes_spec.rb +290 -0
  39. data/spec/active_force/sobject/table_name_spec.rb +27 -0
  40. data/spec/active_force/sobject_spec.rb +398 -0
  41. data/spec/active_force/table_spec.rb +25 -0
  42. data/spec/active_force_spec.rb +7 -0
  43. data/spec/fixtures/sobject/single_sobject_hash.yml +26 -0
  44. data/spec/spec_helper.rb +16 -0
  45. data/spec/support/fixture_helpers.rb +45 -0
  46. data/spec/support/restforce_factories.rb +9 -0
  47. data/spec/support/sobjects.rb +97 -0
  48. data/spec/support/whizbang.rb +30 -0
  49. metadata +196 -0
@@ -0,0 +1,60 @@
1
+ module ActiveForce
2
+ module Association
3
+ class EagerLoadProjectionBuilder
4
+ class << self
5
+ def build(association)
6
+ new(association).projections
7
+ end
8
+ end
9
+
10
+ attr_reader :association
11
+
12
+ def initialize(association)
13
+ @association = association
14
+ end
15
+
16
+ def projections
17
+ klass = association.class.name.split('::').last
18
+ builder_class = ActiveForce::Association.const_get "#{klass}ProjectionBuilder"
19
+ builder_class.new(association).projections
20
+ rescue NameError
21
+ raise "Don't know how to build projections for #{klass}"
22
+ end
23
+ end
24
+
25
+ class AbstractProjectionBuilder
26
+ attr_reader :association
27
+
28
+ def initialize(association)
29
+ @association = association
30
+ end
31
+
32
+ def projections
33
+ raise "Must define #{self.class.name}#projections"
34
+ end
35
+ end
36
+
37
+ class HasManyAssociationProjectionBuilder < AbstractProjectionBuilder
38
+ ###
39
+ # Use ActiveForce::Query to build a subquery for the SFDC
40
+ # relationship name. Per SFDC convention, the name needs
41
+ # to be pluralized
42
+ def projections
43
+ match = association.sfdc_association_field.match /__r\z/
44
+ # pluralize the table name, and append '__r' if it was there to begin with
45
+ relationship_name = association.sfdc_association_field.sub(match.to_s, '').pluralize + match.to_s
46
+ query = Query.new relationship_name
47
+ query.fields association.relation_model.fields
48
+ ["(#{query.to_s})"]
49
+ end
50
+ end
51
+
52
+ class BelongsToAssociationProjectionBuilder < AbstractProjectionBuilder
53
+ def projections
54
+ association.relation_model.fields.map do |field|
55
+ "#{ association.sfdc_association_field }.#{ field }"
56
+ end
57
+ end
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,33 @@
1
+ module ActiveForce
2
+ module Association
3
+ class HasManyAssociation < Association
4
+ private
5
+
6
+ def default_foreign_key
7
+ infer_foreign_key_from_model @parent
8
+ end
9
+
10
+ def define_relation_method
11
+ association = self
12
+ _method = @relation_name
13
+ @parent.send :define_method, _method do
14
+ association_cache.fetch _method do
15
+ query = association.relation_model.query
16
+ if scope = association.options[:scoped_as]
17
+ if scope.arity > 0
18
+ query.instance_exec self, &scope
19
+ else
20
+ query.instance_exec &scope
21
+ end
22
+ end
23
+ association_cache[_method] = query.where association.foreign_key => self.id
24
+ end
25
+ end
26
+
27
+ @parent.send :define_method, "#{_method}=" do |associated|
28
+ association_cache[_method] = associated
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
@@ -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,28 @@
1
+ require 'active_force/association/association'
2
+ require 'active_force/association/eager_load_projection_builder'
3
+ require 'active_force/association/relation_model_builder'
4
+ require 'active_force/association/has_many_association'
5
+ require 'active_force/association/belongs_to_association'
6
+
7
+ module ActiveForce
8
+ module Association
9
+ def associations
10
+ @associations ||= {}
11
+ end
12
+
13
+ # i.e name = 'Quota__r'
14
+ def find_association name
15
+ associations.values.detect do |association|
16
+ association.represents_sfdc_table? name
17
+ end
18
+ end
19
+
20
+ def has_many relation_name, options = {}
21
+ associations[relation_name] = HasManyAssociation.new(self, relation_name, options)
22
+ end
23
+
24
+ def belongs_to relation_name, options = {}
25
+ associations[relation_name] = BelongsToAssociation.new(self, relation_name, options)
26
+ end
27
+ end
28
+ 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, 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.to_s
63
+ when :boolean
64
+ !['false','0','f'].include? value.to_s.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
@@ -0,0 +1,110 @@
1
+ module ActiveForce
2
+ class Query
3
+ attr_reader :table
4
+
5
+ def initialize table
6
+ @table = table
7
+ @conditions = []
8
+ @table_id = 'Id'
9
+ @query_fields = [@table_id]
10
+ end
11
+
12
+ def fields fields_collection = []
13
+ @query_fields += fields_collection.to_a
14
+ end
15
+
16
+ def all
17
+ self
18
+ end
19
+
20
+ def to_s
21
+ <<-SOQL.gsub(/\s+/, " ").strip
22
+ SELECT
23
+ #{ build_select }
24
+ FROM
25
+ #{ @table }
26
+ #{ build_where }
27
+ #{ build_order }
28
+ #{ build_limit }
29
+ #{ build_offset }
30
+ SOQL
31
+ end
32
+
33
+ def select *columns
34
+ @query_fields = columns
35
+ self
36
+ end
37
+
38
+ def where condition
39
+ @conditions << condition if condition
40
+ self
41
+ end
42
+
43
+ def order order
44
+ @order = order if order
45
+ self
46
+ end
47
+
48
+ def limit size
49
+ @size = size if size
50
+ self
51
+ end
52
+
53
+ def limit_value
54
+ @size
55
+ end
56
+
57
+ def offset offset
58
+ @offset = offset
59
+ self
60
+ end
61
+
62
+ def offset_value
63
+ @offset
64
+ end
65
+
66
+ def find id
67
+ where "#{ @table_id } = '#{ id }'"
68
+ limit 1
69
+ end
70
+
71
+ def first
72
+ limit 1
73
+ end
74
+
75
+ def last
76
+ order("Id DESC").limit(1)
77
+ end
78
+
79
+ def join object_query
80
+ fields ["(#{ object_query.to_s })"]
81
+ self
82
+ end
83
+
84
+ def count
85
+ @query_fields = ["count(Id)"]
86
+ self
87
+ end
88
+
89
+ protected
90
+ def build_select
91
+ @query_fields.compact.uniq.join(', ')
92
+ end
93
+
94
+ def build_where
95
+ "WHERE (#{ @conditions.join(') AND (') })" unless @conditions.empty?
96
+ end
97
+
98
+ def build_limit
99
+ "LIMIT #{ @size }" if @size
100
+ end
101
+
102
+ def build_order
103
+ "ORDER BY #{ @order }" if @order
104
+ end
105
+
106
+ def build_offset
107
+ "OFFSET #{ @offset }" if @offset
108
+ end
109
+ end
110
+ end
@@ -0,0 +1,210 @@
1
+ require 'active_model'
2
+ require 'active_attr'
3
+ require 'active_attr/dirty'
4
+ require 'active_force/active_query'
5
+ require 'active_force/association'
6
+ require 'active_force/mapping'
7
+ require 'yaml'
8
+ require 'forwardable'
9
+ require 'logger'
10
+ require 'restforce'
11
+
12
+ module ActiveForce
13
+ class RecordInvalid < StandardError;end
14
+
15
+ class SObject
16
+ include ActiveAttr::Model
17
+ include ActiveAttr::Dirty
18
+ extend ActiveForce::Association
19
+ extend ActiveModel::Callbacks
20
+
21
+ define_model_callbacks :save, :create, :update
22
+
23
+ class_attribute :mappings, :table_name
24
+
25
+ class << self
26
+ extend Forwardable
27
+ def_delegators :query, :where, :first, :last, :all, :find, :find_by, :count, :includes, :limit, :order, :select, :none
28
+ def_delegators :mapping, :table, :table_name, :custom_table?, :mappings
29
+
30
+ private
31
+
32
+ ###
33
+ # Provide each subclass with a default id field. Can be overridden
34
+ # in the subclass if needed
35
+ def inherited(subclass)
36
+ subclass.field :id, from: 'Id'
37
+ end
38
+ end
39
+
40
+ def self.mapping
41
+ @mapping ||= ActiveForce::Mapping.new name
42
+ end
43
+
44
+ def self.fields
45
+ mapping.sfdc_names
46
+ end
47
+
48
+ def self.query
49
+ ActiveForce::ActiveQuery.new self
50
+ end
51
+
52
+ def self.build mash
53
+ return unless mash
54
+ sobject = new
55
+ mash.each do |column, sf_value|
56
+ sobject.write_value column, sf_value
57
+ end
58
+ sobject.clear_changes_information
59
+ sobject
60
+ end
61
+
62
+ def update_attributes! attributes = {}
63
+ assign_attributes attributes
64
+ validate!
65
+ run_callbacks :save do
66
+ run_callbacks :update do
67
+ sfdc_client.update! table_name, attributes_for_sfdb
68
+ changes_applied
69
+ end
70
+ end
71
+ true
72
+ end
73
+
74
+ alias_method :update!, :update_attributes!
75
+
76
+ def update_attributes attributes = {}
77
+ update_attributes! attributes
78
+ rescue Faraday::Error::ClientError, RecordInvalid => error
79
+ handle_save_error error
80
+ end
81
+
82
+ alias_method :update, :update_attributes
83
+
84
+ def create!
85
+ validate!
86
+ run_callbacks :save do
87
+ run_callbacks :create do
88
+ self.id = sfdc_client.create! table_name, attributes_for_sfdb
89
+ changes_applied
90
+ end
91
+ end
92
+ self
93
+ end
94
+
95
+ def create
96
+ create!
97
+ rescue Faraday::Error::ClientError, RecordInvalid => error
98
+ handle_save_error error
99
+ self
100
+ end
101
+
102
+ def destroy
103
+ sfdc_client.destroy! self.class.table_name, id
104
+ end
105
+
106
+ def self.create args
107
+ new(args).create
108
+ end
109
+
110
+ def self.create! args
111
+ new(args).create!
112
+ end
113
+
114
+ def save!
115
+ run_callbacks :save do
116
+ if persisted?
117
+ !!update!
118
+ else
119
+ !!create!
120
+ end
121
+ end
122
+ end
123
+
124
+ def save
125
+ save!
126
+ rescue Faraday::Error::ClientError, RecordInvalid => error
127
+ handle_save_error error
128
+ end
129
+
130
+ def to_param
131
+ id
132
+ end
133
+
134
+ def persisted?
135
+ !!id
136
+ end
137
+
138
+ def self.field field_name, args = {}
139
+ mapping.field field_name, args
140
+ attribute field_name
141
+ end
142
+
143
+ def reload
144
+ association_cache.clear
145
+ reloaded = self.class.find(id)
146
+ self.attributes = reloaded.attributes
147
+ clear_changes_information
148
+ self
149
+ end
150
+
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
160
+ end
161
+
162
+ private
163
+
164
+ def validate!
165
+ unless valid?
166
+ raise RecordInvalid.new(
167
+ "Validation failed: #{errors.full_messages.join(', ')}"
168
+ )
169
+ end
170
+ end
171
+
172
+ def handle_save_error error
173
+ return false if error.class == RecordInvalid
174
+ logger_output __method__, error, attributes
175
+ end
176
+
177
+ def association_cache
178
+ @association_cache ||= {}
179
+ end
180
+
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
192
+ end
193
+
194
+ def self.picklist field
195
+ picks = sfdc_client.picklist_values(table_name, mappings[field])
196
+ picks.map do |value|
197
+ [value[:label], value[:value]]
198
+ end
199
+ end
200
+
201
+ def self.sfdc_client
202
+ ActiveForce.sfdc_client
203
+ end
204
+
205
+ def sfdc_client
206
+ self.class.sfdc_client
207
+ end
208
+ end
209
+
210
+ end