salesforce_record 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.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 6ad16d2a059b16aff6a15262ab0552d83d3d12f9
4
+ data.tar.gz: 48123ec4187661eae7f4cd93c20bebbd9e5c4ead
5
+ SHA512:
6
+ metadata.gz: feea79ba0c8a43da14fa76a52c2cfc253357be6ab6d480f5b3373cea59fa7f99a4f24979e244687a693e1435cbdad23a31cbadf087929f53a182e2015f29677b
7
+ data.tar.gz: 577b8ab8cec74c6834298afdb5173443d3b8c3d4f9f03dda8f1d44ca58623eb18163bba24ca5e52a9f5711777aa82ff415b2d8360f2caf990e67bc3d6884d998
@@ -0,0 +1,69 @@
1
+ # SalesforceRecord
2
+
3
+ [![Build Status](https://travis-ci.org/mru2/salesforce_record.png?branch=master)](https://travis-ci.org/mru2/salesforce_record) [![Coverage Status](https://coveralls.io/repos/mru2/salesforce_record/badge.png)](https://coveralls.io/r/mru2/salesforce_record) [![Code Climate](https://codeclimate.com/github/mru2/salesforce_record.png)](https://codeclimate.com/github/mru2/salesforce_record)
4
+
5
+ ActiveRecord-like mixin for querying, fetching and updating Salesforce models
6
+
7
+ ## Installation
8
+
9
+ Add this line to your application's Gemfile:
10
+
11
+ gem 'salesforce_record'
12
+
13
+ And then execute:
14
+
15
+ $ bundle
16
+
17
+ Or install it yourself as:
18
+
19
+ $ gem install salesforce_record
20
+
21
+ ## Usage
22
+
23
+ ```
24
+ require 'salesforce_adapter'
25
+ require 'salesforce_record'
26
+
27
+ $salesforce = SalesforceAdapter::Base.new(
28
+ :url => 'https://test.salesforce.com/services/Soap/u/16.0',
29
+ :login => 'your_salesforce_login',
30
+ :password => 'your_salesforce_password'
31
+ )
32
+
33
+ class MyLeadModel
34
+ include SalesforceRecord
35
+
36
+ is_salesforce_model :Lead
37
+ sf_adapter $salesforce
38
+ sf_attributes :Company, :FirstName, :LastName
39
+ end
40
+
41
+ # Finders
42
+ > lead = MyLeadModel.find('00Qg000000ORDER66')
43
+ => #<MyLeadModel:0x007fe47983b700 @Id="00Qg000000ORDER66", @Company="JEDI inc", @FirstName="Mace", @LastName="Windu">
44
+
45
+ # Attribute getters
46
+ > lead.Company
47
+ => "JEDI inc"
48
+
49
+ # Remote setters
50
+ > lead.update_fields(:Company => 'One-arm support group')
51
+ => true
52
+
53
+ # Record creation
54
+ > new_lead = MyLeadModel.create(:LastName => 'Binks', :FirstName => 'Jar-Jar', :Company => 'Lobby for a new Autocracy')
55
+ => #<MyLeadModel:0x007fe4798e1970 @Id="00Qg0000002DUMBASS", @Company="Unwilling Dictatorship Lobby", @FirstName="Jar-Jar", @LastName="Binks">
56
+
57
+ # Queries
58
+ > results = MyLeadModel.where(:LastName => 'Windu')
59
+ => [#<MyLeadModel:0x007fe47991bee0 @Id="00Qg000000ORDER66", @Company="One-arm support group", @FirstName="Mace", @LastName="Windu">]
60
+ ```
61
+
62
+
63
+ ## Contributing
64
+
65
+ 1. Fork it ( http://github.com/<my-github-username>/salesforce_record/fork )
66
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
67
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
68
+ 4. Push to the branch (`git push origin my-new-feature`)
69
+ 5. Create new Pull Request
@@ -0,0 +1,34 @@
1
+ # Module allowing a class to wrap a salesforce object
2
+ # Define accessors and finder methods
3
+
4
+ # Usage :
5
+ # class Lead
6
+ # is_salesforce_model :Lead
7
+ # include SalesforceRecord
8
+ # sf_adapter salesforce_adapter
9
+ # sf_attributes :ConvertedAccountId, :Street
10
+ # sf_attribute :owner_email, :from => 'Owner.Email'
11
+ # sf_attribute :Date_saisie_CB__c, :type => :date
12
+ # end
13
+ #
14
+ # lead = Lead.find('my_lead_id', rforce_salesforce_adapter)
15
+ # lead.owner_email # => "my@email.tld"
16
+
17
+ require 'salesforce_record/version'
18
+
19
+ require 'salesforce_record/base'
20
+ require 'salesforce_record/attributes'
21
+ require 'salesforce_record/finder'
22
+ require 'salesforce_record/persistence'
23
+
24
+ module SalesforceRecord
25
+
26
+ def self.included(base)
27
+ # Include dependencies
28
+ base.send :include, SalesforceRecord::Base
29
+ base.send :include, SalesforceRecord::Attributes
30
+ base.send :include, SalesforceRecord::Finder
31
+ base.send :include, SalesforceRecord::Persistence
32
+ end
33
+
34
+ end
@@ -0,0 +1,152 @@
1
+ # Attributes handling
2
+ # defines #new and #attributes
3
+ # also class methods for parsing / encoding attributes hash for salesforce
4
+
5
+ require 'salesforce_record/fields'
6
+
7
+ module SalesforceRecord
8
+ module Attributes
9
+
10
+ def self.included(base)
11
+ base.send(:extend, ClassMethods)
12
+
13
+ base.instance_eval do
14
+ # Store the attributes and aliases at the class level
15
+ @salesforce_fields = {} # name => field instance
16
+
17
+ # The base attributes
18
+ sf_attribute :Id, :type => :id
19
+ end
20
+ end
21
+
22
+
23
+ module ClassMethods
24
+
25
+ # The salesforce fields and options
26
+ def salesforce_fields ; @salesforce_fields ; end
27
+
28
+ # The attributes for this model
29
+ def salesforce_attributes ; @salesforce_fields.keys ; end
30
+
31
+ # Add salesforce attributes to the model
32
+ def sf_attributes(*attrs)
33
+ attrs.each do |attr|
34
+ sf_attribute attr
35
+ end
36
+ end
37
+
38
+ # Add a salesforce attribute to the model
39
+ # Possible options :
40
+ # :from => the remote key to fetch to populate the field
41
+ # :type => one of :date, :float, :integer, :boolean, :id
42
+ def sf_attribute(attribute, opts={})
43
+
44
+ case opts[:type]
45
+ when :date
46
+ @salesforce_fields[attribute] = Fields::DateField.new(attribute, opts)
47
+ when :float
48
+ @salesforce_fields[attribute] = Fields::FloatField.new(attribute, opts)
49
+ when :integer
50
+ @salesforce_fields[attribute] = Fields::IntegerField.new(attribute, opts)
51
+ else
52
+ @salesforce_fields[attribute] = Fields::BaseField.new(attribute, opts)
53
+ end
54
+
55
+ # Set its accessor
56
+ attr_reader attribute
57
+ end
58
+
59
+
60
+ # Create an imported record from salesforce
61
+ def from_salesforce(fields)
62
+ new parse_salesforce_fields(fields)
63
+ end
64
+
65
+ # Parse fields coming from salesforce
66
+ def parse_salesforce_fields(sf_fields)
67
+ {}.tap do |parsed_fields|
68
+ sf_fields.each do |name, value|
69
+ parsed_field = parse_salesforce_field(name, value) unless value.nil?
70
+ parsed_fields.merge! parsed_field if !parsed_field.nil?
71
+ end
72
+ end
73
+ end
74
+
75
+ # Encode fields for salesforce
76
+ def encode_salesforce_fields(fields)
77
+ {}.tap do |encoded_fields|
78
+ fields.each do |name, value|
79
+ encoded_field = encode_salesforce_field(name, value)
80
+ encoded_fields.merge! encoded_field if !encoded_field.nil?
81
+ end
82
+ end
83
+ end
84
+
85
+ # Parse a field coming from salesforce (type)
86
+ def parse_salesforce_field(name, value)
87
+ # If existing, parse it depending on type
88
+ if (field = get_field name)
89
+ { field.local_name => field.parse(value) }
90
+
91
+ # Else, try to find if it is an alias
92
+ else
93
+ find_matching_alias(name => value)
94
+ end
95
+ end
96
+
97
+ # Encode a field from salesforce
98
+ def encode_salesforce_field(name, value)
99
+ # If existing, encode it depending on type and alias
100
+ if (field = get_field name) && (!field.alias?)
101
+ { field.remote_name => field.encode(value) }
102
+ end
103
+ end
104
+
105
+ def get_field(name)
106
+ self.salesforce_fields[name]
107
+ end
108
+
109
+ def has_field?(name)
110
+ !!get_field(name)
111
+ end
112
+
113
+ def field_type(name)
114
+ has_field?(name) && self.salesforce_fields[name].type
115
+ end
116
+
117
+ def field_remote_name(name)
118
+ has_field?(name) && self.salesforce_fields[name].remote_name
119
+ end
120
+
121
+
122
+ # Tries to find a salesforce alias matching a nested hash. Returns the alias name and corresponding value if found
123
+ def find_matching_alias(nested_hash)
124
+ self.salesforce_fields.each do |name, field|
125
+ if (value = field.is_alias_of(nested_hash))
126
+ return parse_salesforce_field(name, value)
127
+ end
128
+ end
129
+ return nil
130
+ end
131
+
132
+ end
133
+
134
+
135
+ # Constructor
136
+ def initialize(attributes = {})
137
+ # Iterate over the salesforce attributes and sets them
138
+ attributes.each do |name, value|
139
+ instance_variable_set :"@#{name}", value
140
+ end
141
+ end
142
+
143
+
144
+ # The attributes hash : name => parsed value
145
+ def attributes
146
+ Hash[*self.class.salesforce_attributes.map{|attr|[attr, self.send(attr)]}.flatten]
147
+ end
148
+
149
+
150
+
151
+ end
152
+ end
@@ -0,0 +1,36 @@
1
+ # Base dependencies for the salesforce models
2
+ # Handles the attribute list, the salesforce adapter, ...
3
+
4
+ module SalesforceRecord
5
+ module Base
6
+
7
+ # Bulding the class
8
+ def self.included(base)
9
+ base.send(:extend, ClassMethods)
10
+
11
+ base.instance_eval do
12
+ @salesforce_table_name = nil
13
+ @salesforce_adapter = nil
14
+ end
15
+ end
16
+
17
+
18
+ module ClassMethods
19
+
20
+ # Defines the salesforce table name
21
+ def is_salesforce_model(table_name)
22
+ @salesforce_table_name = table_name
23
+ end
24
+ def salesforce_table_name ; @salesforce_table_name ; end
25
+
26
+
27
+ # Defines the salesforce adapter
28
+ def sf_adapter(adapter)
29
+ @salesforce_adapter = adapter
30
+ end
31
+ def salesforce_adapter ; @salesforce_adapter ; end
32
+
33
+ end
34
+
35
+ end
36
+ end
@@ -0,0 +1,80 @@
1
+ # All the possible salesforce fields
2
+ # Responsible for parsing / encoding the values
3
+ # Also handle local and remote name, if different
4
+
5
+ module SalesforceRecord
6
+ module Fields
7
+
8
+ # The base class for the fields. No values parsing or encoding
9
+ class BaseField
10
+
11
+ attr_reader :local_name
12
+
13
+ def initialize(name, opts)
14
+ @local_name = name
15
+ @remote_name = opts[:from]
16
+ end
17
+
18
+ # Parse : SF => local
19
+ def parse(value)
20
+ value
21
+ end
22
+
23
+ # Encode : local => SF
24
+ def encode(value)
25
+ value
26
+ end
27
+
28
+ def remote_name
29
+ @remote_name || @local_name
30
+ end
31
+
32
+ def alias?
33
+ !!@remote_name
34
+ end
35
+
36
+ # Check if matching nested hash
37
+ def is_alias_of(nested_hash)
38
+ return false unless @remote_name
39
+ deep_fetch(nested_hash, remote_name.split('.').map(&:to_sym))
40
+ end
41
+
42
+ private
43
+
44
+ # Helper : fetch deep value from a hash
45
+ def deep_fetch(hash, keys)
46
+ keys.inject(hash){|subhash, key| subhash.is_a?(Hash) && subhash[key] }
47
+ end
48
+ end
49
+
50
+
51
+ # A date. YYYY-MM-DD on salesforce
52
+ class DateField < BaseField
53
+ def encode(value)
54
+ value.strftime("%Y-%m-%d")
55
+ end
56
+
57
+ def parse(value)
58
+ Date.parse(value)
59
+ end
60
+ end
61
+
62
+
63
+ # A float
64
+ class FloatField < BaseField
65
+ def parse(value)
66
+ value.to_f
67
+ end
68
+ end
69
+
70
+
71
+ # An integer
72
+ class IntegerField < BaseField
73
+ def parse(value)
74
+ value.to_i
75
+ end
76
+ end
77
+
78
+
79
+ end
80
+ end
@@ -0,0 +1,101 @@
1
+ # Finder methods
2
+ # defines #find and #where
3
+
4
+ module SalesforceRecord
5
+ module Finder
6
+
7
+ def self.included(base)
8
+ base.send(:extend, ClassMethods)
9
+ end
10
+
11
+ module ClassMethods
12
+
13
+ # Returns the object matching a salesforce id
14
+ def find(id)
15
+ where(:Id => id).first
16
+ end
17
+
18
+
19
+ # Returns a list of object matching a specific query
20
+ def where(query)
21
+
22
+ # Build the matching query string
23
+ query_string = query_string(query)
24
+
25
+ # Run the query and get the results (always an array)
26
+ sf_results = salesforce_adapter.query(query_string)
27
+
28
+ # Create new objets with the results and return them
29
+ sf_results.map{|sf_attributes| from_salesforce(sf_attributes)}
30
+
31
+ end
32
+
33
+
34
+ # Creates an soql query for the resource. The query matchers are the argument
35
+ def query_string(query)
36
+
37
+ # The first letter of the model used
38
+ # i.e : Lead => l
39
+ # Used in the query : Select l.Type_de_paiement__c from Lead l where Id='id-goes-here'
40
+ key =
41
+
42
+ # Build the base query : select all the attributes in the model table (except "type" : returned but should not be queried)
43
+ query_string = "Select "
44
+ query_string << (salesforce_attributes - [:type]).map{|attribute| remote_attribute_name(attribute)}.join(", ")
45
+ query_string << " from #{salesforce_table_name} #{salesforce_query_key}"
46
+
47
+ # Add the selectors
48
+ if query.is_a? String
49
+ soql_selector = query
50
+ else
51
+ soql_selector = query.map{|attribute, value| format_where_clause(attribute, value)}.join(' AND ')
52
+ end
53
+
54
+ query_string << " WHERE #{soql_selector}"
55
+
56
+ query_string
57
+
58
+ end
59
+
60
+
61
+ # The key used to namespace the salesforce queries : first character of the table name
62
+ # i.e : Lead => l
63
+ # Used in : Select l.Type_de_paiement__c from Lead l where Id='id-goes-here'
64
+ def salesforce_query_key
65
+ self.salesforce_table_name.to_s[0, 1].downcase
66
+ end
67
+
68
+ # Aliases : the remote name for an attribute
69
+ def remote_attribute_name(attribute)
70
+ if (field = self.get_field attribute)
71
+ name_without_alias = field.remote_name
72
+ else
73
+ name_without_alias = attribute
74
+ end
75
+ "#{salesforce_query_key}.#{name_without_alias}"
76
+ end
77
+
78
+
79
+
80
+ # Create the query string for an attribute/value pair
81
+ # Returns something along the lines of "key.attribute='value'", to be injected in a WHERE clause
82
+ def format_where_clause(attribute, value)
83
+ q = "#{remote_attribute_name(attribute)}=" #exact match
84
+
85
+ # No escaping boolean values
86
+ if value.is_a?(TrueClass) || value.is_a?(FalseClass)
87
+ q << value.to_s
88
+ elsif value.is_a? Date
89
+ q << value.strftime("%Y-%m-%d")
90
+ elsif value.nil?
91
+ q << "NULL"
92
+ else
93
+ q << "'#{value}'"
94
+ end
95
+
96
+ q
97
+ end
98
+ end
99
+
100
+ end
101
+ end
@@ -0,0 +1,79 @@
1
+ # Persisting the model to salesforce
2
+ # defines #update_fields and #create methods
3
+
4
+ require 'salesforce_adapter'
5
+
6
+ module SalesforceRecord
7
+ module Persistence
8
+
9
+ def self.included(base)
10
+ base.send(:extend, ClassMethods)
11
+ end
12
+
13
+
14
+ # Pushes new field values to salesforce and updates them locally
15
+ def update_fields(fields)
16
+
17
+ update_fields = self.class.encode_attributes(fields).merge({
18
+ :Id => self.Id,
19
+ :type => self.class.salesforce_table_name.to_s
20
+ })
21
+
22
+ begin
23
+ self.class.salesforce_adapter.update( self.class.salesforce_table_name, update_fields )
24
+ saved = true
25
+ rescue SalesforceAdapter::SalesforceFailedUpdate => e
26
+ # No check of the error code necessary here
27
+ saved = false
28
+ end
29
+
30
+ # Update the fields locally if the request passed
31
+ if saved
32
+ fields.each do |attribute, value|
33
+ self.instance_variable_set(:"@#{attribute}", value)
34
+ end
35
+ end
36
+
37
+ return saved
38
+ end
39
+
40
+
41
+
42
+ module ClassMethods
43
+
44
+ # Tries to create an instance, given a set of attributes
45
+ def create(attributes)
46
+
47
+ fields = encode_attributes(attributes).merge(:type => self.salesforce_table_name.to_s)
48
+
49
+ # Tries to create it remotely
50
+ begin
51
+ id = self.salesforce_adapter.create( self.salesforce_table_name, fields )
52
+
53
+ # If successful, return an instance with the given Id
54
+ return from_salesforce(fields.merge(:Id => id))
55
+
56
+ # TODO : have a real handling of failures
57
+ rescue => e
58
+ puts e.message
59
+ return nil
60
+ end
61
+
62
+ end
63
+
64
+
65
+ def encode_attributes(attributes)
66
+ {}.tap do |encoded_attributes|
67
+ attributes.each do |name, value|
68
+ if (field = self.get_field(name))
69
+ encoded_attributes[name] = field.encode value
70
+ else
71
+ encoded_attributes[name] = value
72
+ end
73
+ end
74
+ end
75
+ end
76
+ end
77
+
78
+ end
79
+ end
@@ -0,0 +1,3 @@
1
+ module SalesforceRecord
2
+ VERSION = "1.0.0"
3
+ end
metadata ADDED
@@ -0,0 +1,95 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: salesforce_record
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0
5
+ platform: ruby
6
+ authors:
7
+ - ClicRDV
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2014-04-28 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: salesforce_adapter
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ~>
18
+ - !ruby/object:Gem::Version
19
+ version: 1.0.0
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ~>
25
+ - !ruby/object:Gem::Version
26
+ version: 1.0.0
27
+ - !ruby/object:Gem::Dependency
28
+ name: bundler
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ~>
32
+ - !ruby/object:Gem::Version
33
+ version: '1.5'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ~>
39
+ - !ruby/object:Gem::Version
40
+ version: '1.5'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rake
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - '>='
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - '>='
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ description:
56
+ email:
57
+ - david.ruyer@clicrdv.com
58
+ executables: []
59
+ extensions: []
60
+ extra_rdoc_files: []
61
+ files:
62
+ - README.md
63
+ - lib/salesforce_record.rb
64
+ - lib/salesforce_record/attributes.rb
65
+ - lib/salesforce_record/base.rb
66
+ - lib/salesforce_record/fields.rb
67
+ - lib/salesforce_record/finder.rb
68
+ - lib/salesforce_record/persistence.rb
69
+ - lib/salesforce_record/version.rb
70
+ homepage: ''
71
+ licenses:
72
+ - MIT
73
+ metadata: {}
74
+ post_install_message:
75
+ rdoc_options: []
76
+ require_paths:
77
+ - lib
78
+ required_ruby_version: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - '>='
81
+ - !ruby/object:Gem::Version
82
+ version: '0'
83
+ required_rubygems_version: !ruby/object:Gem::Requirement
84
+ requirements:
85
+ - - '>='
86
+ - !ruby/object:Gem::Version
87
+ version: '0'
88
+ requirements: []
89
+ rubyforge_project:
90
+ rubygems_version: 2.2.1
91
+ signing_key:
92
+ specification_version: 4
93
+ summary: ActiveRecord-like mixin for querying, fetching and updating Salesforce models
94
+ test_files: []
95
+ has_rdoc: