facebook_ads 0.1.5 → 0.1.7
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.
- checksums.yaml +4 -4
- data/.gitignore +2 -0
- data/.rubocop.yml +23 -0
- data/.travis.yml +1 -1
- data/Gemfile +4 -5
- data/Gemfile.lock +34 -82
- data/{README.markdown → README.md} +43 -10
- data/Rakefile +6 -0
- data/bin/console +15 -1
- data/facebook_ads.gemspec +11 -13
- data/lib/facebook_ads.rb +11 -7
- data/lib/facebook_ads/ad.rb +6 -8
- data/lib/facebook_ads/ad_account.rb +65 -54
- data/lib/facebook_ads/ad_audience.rb +22 -0
- data/lib/facebook_ads/ad_campaign.rb +21 -17
- data/lib/facebook_ads/ad_creative.rb +7 -9
- data/lib/facebook_ads/ad_image.rb +3 -5
- data/lib/facebook_ads/ad_insight.rb +4 -5
- data/lib/facebook_ads/ad_product.rb +6 -0
- data/lib/facebook_ads/ad_product_catalog.rb +60 -0
- data/lib/facebook_ads/ad_product_feed.rb +6 -0
- data/lib/facebook_ads/ad_product_set.rb +6 -0
- data/lib/facebook_ads/ad_set.rb +9 -11
- data/lib/facebook_ads/ad_targeting.rb +19 -21
- data/lib/facebook_ads/base.rb +107 -62
- data/test/ad_account_test.rb +27 -0
- data/test/ad_campaign_test.rb +32 -0
- data/test/ad_creative_test.rb +7 -0
- data/test/ad_image_test.rb +27 -0
- data/test/ad_insight_test.rb +7 -0
- data/test/ad_product_catalog_test.rb +28 -0
- data/test/ad_product_feed_test.rb +7 -0
- data/test/ad_product_set_test.rb +7 -0
- data/test/ad_product_test.rb +22 -0
- data/test/ad_set_test.rb +7 -0
- data/test/ad_targeting_test.rb +7 -0
- data/test/ad_test.rb +7 -0
- data/test/facebook_ads_test.rb +8 -0
- data/test/test_helper.rb +64 -0
- data/test/vcr_cassettes/AdAccountTest-test_all.yml +70 -0
- data/test/vcr_cassettes/AdAccountTest-test_applications.yml +130 -0
- data/test/vcr_cassettes/AdAccountTest-test_find_by.yml +71 -0
- data/test/vcr_cassettes/AdCampaignTest-test_create.yml +295 -0
- data/test/vcr_cassettes/AdCampaignTest-test_list.yml +133 -0
- data/test/vcr_cassettes/AdImageTest-test_create.yml +2963 -0
- data/test/vcr_cassettes/AdImageTest-test_list.yml +137 -0
- data/test/vcr_cassettes/AdProductCatalogTest-test_all.yml +60 -0
- data/test/vcr_cassettes/AdProductCatalogTest-test_create.yml +256 -0
- data/test/vcr_cassettes/AdProductTest-test_list.yml +130 -0
- metadata +44 -49
- data/spec/ad_account_spec.rb +0 -78
- data/spec/ad_campaign_spec.rb +0 -13
- data/spec/ad_creative_spec.rb +0 -14
- data/spec/ad_image_spec.rb +0 -11
- data/spec/ad_insight_spec.rb +0 -11
- data/spec/ad_set_spec.rb +0 -13
- data/spec/ad_spec.rb +0 -13
- data/spec/ad_targeting_spec.rb +0 -4
- data/spec/spec_helper.rb +0 -15
- data/spec/support/fixtures.sh +0 -17
- data/spec/support/fixtures/6057330925170.json +0 -1
- data/spec/support/fixtures/6057810634370.json +0 -1
- data/spec/support/fixtures/6057810946970.json +0 -1
- data/spec/support/fixtures/6057824295570.json +0 -1
- data/spec/support/fixtures/act_861827983860489.json +0 -1
- data/spec/support/fixtures/act_861827983860489/adcreatives.json +0 -1
- data/spec/support/fixtures/act_861827983860489/adimages.json +0 -1
- data/spec/support/fixtures/act_861827983860489/ads.json +0 -1
- data/spec/support/fixtures/act_861827983860489/adsets.json +0 -1
- data/spec/support/fixtures/act_861827983860489/campaigns.json +0 -1
- data/spec/support/fixtures/me/adaccounts.json +0 -1
- data/spec/support/rack_facebook.rb +0 -22
@@ -2,23 +2,22 @@ module FacebookAds
|
|
2
2
|
# Ad insights exist for ad accounts, ad campaigns, ad sets, and ads.
|
3
3
|
# A lot more can be done here.
|
4
4
|
# https://developers.facebook.com/docs/marketing-api/insights/overview
|
5
|
+
# https://developers.facebook.com/docs/marketing-api/insights/fields/v2.8
|
5
6
|
class AdInsight < Base
|
6
|
-
|
7
|
-
FIELDS = %w(ad_id objective impressions unique_actions cost_per_unique_action_type clicks cpc cpm ctr spend)
|
7
|
+
FIELDS = %w(account_id campaign_id adset_id ad_id objective impressions unique_actions cost_per_unique_action_type clicks cpc cpm cpp ctr spend reach relevance_score).freeze
|
8
8
|
|
9
9
|
class << self
|
10
|
-
def find(
|
10
|
+
def find(_id)
|
11
11
|
raise Exception, 'NOT IMPLEMENTED'
|
12
12
|
end
|
13
13
|
end
|
14
14
|
|
15
|
-
def update(
|
15
|
+
def update(_data)
|
16
16
|
raise Exception, 'NOT IMPLEMENTED'
|
17
17
|
end
|
18
18
|
|
19
19
|
def destroy
|
20
20
|
raise Exception, 'NOT IMPLEMENTED'
|
21
21
|
end
|
22
|
-
|
23
22
|
end
|
24
23
|
end
|
@@ -0,0 +1,60 @@
|
|
1
|
+
module FacebookAds
|
2
|
+
# https://developers.facebook.com/docs/marketing-api/reference/product-catalog
|
3
|
+
class AdProductCatalog < Base
|
4
|
+
FIELDS = %w(id name vertical product_count feed_count).freeze
|
5
|
+
|
6
|
+
class << self
|
7
|
+
def all
|
8
|
+
get("/#{FacebookAds.business_id}/product_catalogs", objectify: true)
|
9
|
+
end
|
10
|
+
|
11
|
+
def find_by(conditions)
|
12
|
+
all.detect do |object|
|
13
|
+
conditions.all? do |key, value|
|
14
|
+
object.send(key) == value
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
def create(name:)
|
20
|
+
catalog = post("/#{FacebookAds.business_id}/product_catalogs", query: { name: name }, objectify: true)
|
21
|
+
find(catalog.id)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
# has_many ad_product_feeds
|
26
|
+
|
27
|
+
def ad_product_feeds
|
28
|
+
AdProductFeed.paginate("/#{id}/product_feeds")
|
29
|
+
end
|
30
|
+
|
31
|
+
# catalog.create_ad_product_feed(name: 'Test', schedule: { url: 'https://tophatter.com/admin/ad_automation/ad_product_feeds/1.csv', interval: 'HOURLY' })
|
32
|
+
def create_ad_product_feed(name:, schedule:)
|
33
|
+
feed = AdProductCatalog.post("/#{id}/product_feeds", query: { name: name, schedule: schedule }, objectify: true)
|
34
|
+
AdProductFeed.find(feed.id)
|
35
|
+
end
|
36
|
+
|
37
|
+
# has_many product_groups
|
38
|
+
|
39
|
+
# def ad_product_groups
|
40
|
+
# AdProductGroup.paginate("/#{id}/product_groups")
|
41
|
+
# end
|
42
|
+
|
43
|
+
# has_many product_sets
|
44
|
+
|
45
|
+
# def ad_product_sets
|
46
|
+
# AdProductSet.paginate("/#{id}/product_sets")
|
47
|
+
# end
|
48
|
+
|
49
|
+
# has_many ad_products
|
50
|
+
|
51
|
+
def ad_products
|
52
|
+
AdProduct.paginate("/#{id}/products")
|
53
|
+
end
|
54
|
+
|
55
|
+
def create_ad_product(data)
|
56
|
+
product = AdProductCatalog.post("/#{id}/products", query: data, objectify: true)
|
57
|
+
AdProduct.find(product.id)
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
@@ -0,0 +1,6 @@
|
|
1
|
+
module FacebookAds
|
2
|
+
# https://developers.facebook.com/docs/marketing-api/reference/product-feed
|
3
|
+
class AdProductFeed < Base
|
4
|
+
FIELDS = %w(id country created_time default_currency deletion_enabled delimiter encoding file_name latest_upload name product_count quoted_fields_mode schedule).freeze
|
5
|
+
end
|
6
|
+
end
|
data/lib/facebook_ads/ad_set.rb
CHANGED
@@ -2,34 +2,32 @@ module FacebookAds
|
|
2
2
|
# An ad set belongs to a campaign and has many ads.
|
3
3
|
# https://developers.facebook.com/docs/marketing-api/reference/ad-campaign
|
4
4
|
class AdSet < Base
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
OPTIMIZATION_GOALS = %w(APP_INSTALLS OFFSITE_CONVERSIONS) # %w(NONE BRAND_AWARENESS CLICKS ENGAGED_USERS EXTERNAL EVENT_RESPONSES IMPRESSIONS LEAD_GENERATION LINK_CLICKS OFFER_CLAIMS PAGE_ENGAGEMENT PAGE_LIKES POST_ENGAGEMENT REACH SOCIAL_IMPRESSIONS VIDEO_VIEWS)
|
5
|
+
FIELDS = %w(id account_id campaign_id adlabels adset_schedule bid_amount bid_info billing_event configured_status created_time creative_sequence effective_status end_time frequency_cap frequency_cap_reset_period frequency_control_specs is_autobid lifetime_frequency_cap lifetime_imps name optimization_goal promoted_object rf_prediction_id rtb_flag start_time targeting updated_time use_new_app_click pacing_type budget_remaining daily_budget lifetime_budget).freeze
|
6
|
+
STATUSES = %w(ACTIVE PAUSED DELETED PENDING_REVIEW DISAPPROVED PREAPPROVED PENDING_BILLING_INFO CAMPAIGN_PAUSED ARCHIVED ADSET_PAUSED).freeze
|
7
|
+
BILLING_EVENTS = %w(APP_INSTALLS IMPRESSIONS).freeze
|
8
|
+
OPTIMIZATION_GOALS = %w(APP_INSTALLS OFFSITE_CONVERSIONS).freeze
|
10
9
|
|
11
10
|
# belongs_to ad_account
|
12
11
|
|
13
12
|
def ad_account
|
14
|
-
@
|
13
|
+
@ad_account ||= AdAccount.find("act_#{account_id}")
|
15
14
|
end
|
16
15
|
|
17
16
|
# belongs_to ad_campaign
|
18
17
|
|
19
18
|
def ad_campaign
|
20
|
-
@campaign ||=
|
19
|
+
@campaign ||= AdCampaign.find(campaign_id)
|
21
20
|
end
|
22
21
|
|
23
22
|
# has_many ads
|
24
23
|
|
25
24
|
def ads(effective_status: ['ACTIVE'], limit: 100)
|
26
|
-
|
25
|
+
Ad.paginate("/#{id}/ads", query: { effective_status: effective_status, limit: limit })
|
27
26
|
end
|
28
27
|
|
29
28
|
def create_ad(name:, creative_id:)
|
30
|
-
ad =
|
31
|
-
|
29
|
+
ad = Ad.post("/act_#{account_id}/ads", query: { name: name, adset_id: id, creative: { creative_id: creative_id }.to_json }, objectify: true) # Returns an Ad instance.
|
30
|
+
Ad.find(ad.id)
|
32
31
|
end
|
33
|
-
|
34
32
|
end
|
35
33
|
end
|
@@ -1,35 +1,34 @@
|
|
1
|
-
# https://developers.facebook.com/docs/marketing-api/targeting-specs
|
2
1
|
module FacebookAds
|
2
|
+
# https://developers.facebook.com/docs/marketing-api/targeting-specs
|
3
3
|
class AdTargeting
|
4
|
-
|
5
4
|
MEN = 1
|
6
5
|
WOMEN = 2
|
7
|
-
GENDERS = [MEN, WOMEN]
|
8
|
-
ANDROID_OS = 'Android'
|
9
|
-
APPLE_OS = 'iOS'
|
10
|
-
OSES = [ANDROID_OS, APPLE_OS]
|
11
|
-
ANDROID_DEVICES = %w(Android_Smartphone Android_Tablet)
|
12
|
-
APPLE_DEVICES = %w(iPhone iPad iPod)
|
6
|
+
GENDERS = [MEN, WOMEN].freeze
|
7
|
+
ANDROID_OS = 'Android'.freeze
|
8
|
+
APPLE_OS = 'iOS'.freeze
|
9
|
+
OSES = [ANDROID_OS, APPLE_OS].freeze
|
10
|
+
ANDROID_DEVICES = %w(Android_Smartphone Android_Tablet).freeze
|
11
|
+
APPLE_DEVICES = %w(iPhone iPad iPod).freeze
|
13
12
|
DEVICES = ANDROID_DEVICES + APPLE_DEVICES
|
14
|
-
INSTALLED = 'installed'
|
15
|
-
NOT_INSTALLED = 'not_installed'
|
16
|
-
APP_INSTALL_STATES = [INSTALLED, NOT_INSTALLED]
|
13
|
+
INSTALLED = 'installed'.freeze
|
14
|
+
NOT_INSTALLED = 'not_installed'.freeze
|
15
|
+
APP_INSTALL_STATES = [INSTALLED, NOT_INSTALLED].freeze
|
17
16
|
|
18
17
|
attr_accessor :genders, :age_min, :age_max, :countries, :user_os, :user_device, :app_install_state
|
19
18
|
|
20
19
|
def initialize
|
21
|
-
# self.genders
|
22
|
-
# self.age_min
|
23
|
-
# self.age_max
|
24
|
-
self.countries
|
25
|
-
# self.user_os
|
26
|
-
# self.user_device
|
20
|
+
# self.genders = [WOMEN] # If nil, defaults to all genders.
|
21
|
+
# self.age_min = 18 # If nil, defaults to 18.
|
22
|
+
# self.age_max = 65 # If nil, defaults to 65+.
|
23
|
+
self.countries = ['US']
|
24
|
+
# self.user_os = [ANDROID_OS]
|
25
|
+
# self.user_device = ANDROID_DEVICES
|
27
26
|
# self.app_install_state = NOT_INSTALLED
|
28
27
|
end
|
29
28
|
|
30
29
|
def validate!
|
31
30
|
{ gender: genders, countries: countries, user_os: user_os, user_device: user_device }.each_pair do |key, array|
|
32
|
-
if array.
|
31
|
+
if !array.nil? && !array.is_a?(Array)
|
33
32
|
raise Exception, "#{self.class.name}: #{key} must be an array"
|
34
33
|
end
|
35
34
|
end
|
@@ -37,8 +36,8 @@ module FacebookAds
|
|
37
36
|
{ genders: [genders, GENDERS], user_os: [user_os, OSES], user_device: [user_device, DEVICES] }.each_pair do |key, provided_and_acceptable|
|
38
37
|
provided, acceptable = provided_and_acceptable
|
39
38
|
|
40
|
-
if provided.
|
41
|
-
raise Exception, "#{self.class.name}: #{
|
39
|
+
if !provided.nil? && !(invalid = provided.detect { |value| !acceptable.include?(value) }).nil?
|
40
|
+
raise Exception, "#{self.class.name}: #{invalid} is an invalid #{key}"
|
42
41
|
end
|
43
42
|
end
|
44
43
|
|
@@ -56,6 +55,5 @@ module FacebookAds
|
|
56
55
|
app_install_state: app_install_state
|
57
56
|
}.compact
|
58
57
|
end
|
59
|
-
|
60
58
|
end
|
61
59
|
end
|
data/lib/facebook_ads/base.rb
CHANGED
@@ -1,47 +1,70 @@
|
|
1
1
|
module FacebookAds
|
2
|
+
# The base class for all ads objects.
|
2
3
|
class Base < Hashie::Mash
|
3
|
-
|
4
4
|
class << self
|
5
|
-
|
6
5
|
def find(id)
|
7
6
|
get("/#{id}", objectify: true)
|
8
7
|
end
|
9
8
|
|
10
9
|
def get(path, query: {}, objectify:)
|
11
10
|
query = pack(query, objectify) # Adds access token, fields, etc.
|
12
|
-
|
13
|
-
|
14
|
-
response =
|
11
|
+
uri = "#{FacebookAds.base_uri}#{path}?" + build_nested_query(query)
|
12
|
+
FacebookAds.logger.debug "GET #{uri}"
|
13
|
+
response = begin
|
14
|
+
RestClient.get(uri)
|
15
|
+
rescue RestClient::Exception => e
|
16
|
+
exception(:get, path, e)
|
17
|
+
end
|
18
|
+
unpack(response, objectify: objectify)
|
15
19
|
end
|
16
20
|
|
17
21
|
def post(path, query: {}, objectify:)
|
18
22
|
query = pack(query, objectify)
|
19
|
-
|
20
|
-
|
21
|
-
response =
|
23
|
+
uri = "#{FacebookAds.base_uri}#{path}"
|
24
|
+
FacebookAds.logger.debug "POST #{uri} #{query}"
|
25
|
+
response = begin
|
26
|
+
RestClient.post(uri, query)
|
27
|
+
rescue RestClient::Exception => e
|
28
|
+
exception(:post, path, e)
|
29
|
+
end
|
30
|
+
unpack(response, objectify: objectify)
|
22
31
|
end
|
23
32
|
|
24
33
|
def delete(path, query: {})
|
25
34
|
query = pack(query, false)
|
26
|
-
|
27
|
-
|
28
|
-
response =
|
35
|
+
uri = "#{FacebookAds.base_uri}#{path}?" + build_nested_query(query)
|
36
|
+
FacebookAds.logger.debug "DELETE #{uri}"
|
37
|
+
response = begin
|
38
|
+
RestClient.delete(uri)
|
39
|
+
rescue RestClient::Exception => e
|
40
|
+
exception(:delete, path, e)
|
41
|
+
end
|
42
|
+
unpack(response, objectify: false)
|
29
43
|
end
|
30
44
|
|
31
45
|
def paginate(path, query: {})
|
46
|
+
query[:limit] ||= 100
|
47
|
+
limit = query[:limit]
|
32
48
|
response = get(path, query: query.merge(fields: self::FIELDS.join(',')), objectify: false)
|
33
|
-
data = response['data'].
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
49
|
+
data = response['data'].nil? ? [] : response['data']
|
50
|
+
|
51
|
+
if data.length == limit
|
52
|
+
while !(paging = response['paging']).nil? && !(url = paging['next']).nil?
|
53
|
+
FacebookAds.logger.debug "GET #{url}"
|
54
|
+
response = begin
|
55
|
+
RestClient.get(url)
|
56
|
+
rescue RestClient::Exception => e
|
57
|
+
exception(:get, url, e)
|
58
|
+
end
|
59
|
+
response = unpack(response, objectify: false)
|
60
|
+
data += response['data'] unless response['data'].nil?
|
61
|
+
end
|
39
62
|
end
|
40
63
|
|
41
|
-
if data.
|
42
|
-
data.map { |hash| instantiate(hash) }
|
43
|
-
else
|
64
|
+
if data.nil?
|
44
65
|
[]
|
66
|
+
else
|
67
|
+
data.map { |hash| instantiate(hash) }
|
45
68
|
end
|
46
69
|
end
|
47
70
|
|
@@ -61,75 +84,98 @@ module FacebookAds
|
|
61
84
|
def pack(hash, objectify)
|
62
85
|
hash = hash.merge(access_token: FacebookAds.access_token)
|
63
86
|
hash = hash.merge(fields: self::FIELDS.join(',')) if objectify
|
64
|
-
hash.
|
87
|
+
hash.delete_if { |_k, v| v.nil? }
|
65
88
|
end
|
66
89
|
|
67
90
|
def unpack(response, objectify:)
|
68
|
-
|
69
|
-
|
91
|
+
raise Exception, 'Invalid nil response' if response.nil?
|
92
|
+
response = response.body if response.is_a?(RestClient::Response)
|
93
|
+
|
94
|
+
if response.is_a?(String)
|
95
|
+
response = begin
|
96
|
+
JSON.parse(response)
|
97
|
+
rescue JSON::ParserError
|
98
|
+
raise Exception, "Invalid JSON response: #{response.inspect}"
|
99
|
+
end
|
70
100
|
end
|
71
101
|
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
end
|
102
|
+
raise Exception, "Invalid response: #{response.inspect}" unless response.is_a?(Hash)
|
103
|
+
raise Exception, "[#{response['error']['code']}] #{response['error']['message']} - raw response: #{response.inspect}" unless response['error'].nil?
|
104
|
+
return response unless objectify
|
76
105
|
|
77
|
-
if
|
78
|
-
|
79
|
-
data.map { |hash| instantiate(hash) }
|
80
|
-
else
|
81
|
-
instantiate(response)
|
82
|
-
end
|
106
|
+
if response.key?('data') && (data = response['data']).is_a?(Array)
|
107
|
+
data.map { |hash| instantiate(hash) }
|
83
108
|
else
|
84
|
-
response
|
109
|
+
instantiate(response)
|
85
110
|
end
|
86
111
|
end
|
87
112
|
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
if data.present?
|
92
|
-
response = self.class.post("/#{id}", query: data, objectify: false)
|
113
|
+
def escape(s)
|
114
|
+
URI.encode_www_form_component(s)
|
115
|
+
end
|
93
116
|
|
94
|
-
|
95
|
-
|
117
|
+
# https://github.com/rack/rack/blob/master/lib/rack/utils.rb
|
118
|
+
def build_nested_query(value, prefix = nil)
|
119
|
+
case value
|
120
|
+
when Array
|
121
|
+
value.map { |v| build_nested_query(v, "#{prefix}[]") }.join('&')
|
122
|
+
when Hash
|
123
|
+
value.map { |k, v| build_nested_query(v, prefix ? "#{prefix}[#{escape(k)}]" : escape(k)) }.reject(&:empty?).join('&')
|
124
|
+
when nil
|
125
|
+
prefix
|
96
126
|
else
|
97
|
-
raise
|
127
|
+
raise ArgumentError, 'value must be a Hash' if prefix.nil?
|
128
|
+
"#{prefix}=#{escape(value)}"
|
98
129
|
end
|
99
|
-
else
|
100
|
-
false
|
101
130
|
end
|
102
|
-
end
|
103
131
|
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
132
|
+
def exception(verb, path, e)
|
133
|
+
if e.response.is_a?(String)
|
134
|
+
begin
|
135
|
+
hash = JSON.parse(e.response)
|
136
|
+
error = hash['error']
|
137
|
+
message = error.nil? ? hash.inspect : "#{error['type']} code=#{error['code']} message=#{error['message']}"
|
138
|
+
rescue JSON::ParserError
|
139
|
+
message = e.response.first(100)
|
140
|
+
end
|
111
141
|
else
|
112
|
-
|
142
|
+
message = e.response.inspect
|
113
143
|
end
|
114
|
-
|
115
|
-
|
144
|
+
|
145
|
+
FacebookAds.logger.error "#{verb.upcase} #{path} [#{e.message}] #{message}"
|
146
|
+
raise e
|
116
147
|
end
|
117
148
|
end
|
118
149
|
|
150
|
+
def update(data)
|
151
|
+
return false if data.nil?
|
152
|
+
response = self.class.post("/#{id}", query: data, objectify: false)
|
153
|
+
raise Exception, "Invalid response from update: #{response.inspect}" unless @response.is_a?(Hash) && response.key?('success')
|
154
|
+
response['success']
|
155
|
+
end
|
156
|
+
|
157
|
+
def save
|
158
|
+
return nil if changes.nil? || changes.length.zero?
|
159
|
+
data = {}
|
160
|
+
changes.keys.each { |key| data[key] = self[key] }
|
161
|
+
return nil unless update(data)
|
162
|
+
self.class.find(id)
|
163
|
+
end
|
164
|
+
|
119
165
|
def destroy(path: nil, query: {})
|
120
166
|
response = self.class.delete(path || "/#{id}", query: query)
|
121
|
-
|
122
|
-
|
123
|
-
response['success']
|
124
|
-
else
|
125
|
-
raise Exception, "Invalid response from destroy: #{response.inspect}"
|
126
|
-
end
|
167
|
+
raise Exception, "Invalid response from destroy: #{response.inspect}" unless response.key?('success')
|
168
|
+
response['success']
|
127
169
|
end
|
128
170
|
|
129
171
|
protected
|
130
172
|
|
131
173
|
attr_accessor :changes
|
132
174
|
|
175
|
+
def persisted?
|
176
|
+
!id.nil?
|
177
|
+
end
|
178
|
+
|
133
179
|
private
|
134
180
|
|
135
181
|
def []=(key, value)
|
@@ -146,6 +192,5 @@ module FacebookAds
|
|
146
192
|
|
147
193
|
new_values
|
148
194
|
end
|
149
|
-
|
150
195
|
end
|
151
196
|
end
|