openstax_active_force 1.0.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 (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