zuck 0.0.4
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.
- data/.rvmrc +1 -0
- data/.travis.yml +7 -0
- data/.yardopts +4 -0
- data/CHANGELOG.markdown +4 -0
- data/Gemfile +35 -0
- data/Gemfile.lock +110 -0
- data/Guardfile.dist +45 -0
- data/LICENSE.txt +20 -0
- data/README.markdown +138 -0
- data/Rakefile +39 -0
- data/VERSION +1 -0
- data/console +26 -0
- data/lib/zuck/facebook/ad_account.rb +40 -0
- data/lib/zuck/facebook/ad_campaign.rb +24 -0
- data/lib/zuck/facebook/ad_creative.rb +30 -0
- data/lib/zuck/facebook/ad_group.rb +39 -0
- data/lib/zuck/facebook/targeting_spec.rb +200 -0
- data/lib/zuck/fb_object/dsl.rb +110 -0
- data/lib/zuck/fb_object/error.rb +8 -0
- data/lib/zuck/fb_object/hash_delegator.rb +111 -0
- data/lib/zuck/fb_object/helpers.rb +57 -0
- data/lib/zuck/fb_object/read.rb +147 -0
- data/lib/zuck/fb_object/read_only.rb +0 -0
- data/lib/zuck/fb_object/write.rb +75 -0
- data/lib/zuck/fb_object.rb +53 -0
- data/lib/zuck/koala/koala_methods.rb +27 -0
- data/lib/zuck.rb +9 -0
- data/spec/fixtures/a_single_account.yml +75 -0
- data/spec/fixtures/a_single_campaign.yml +48 -0
- data/spec/fixtures/create_ad_campaign.yml +49 -0
- data/spec/fixtures/create_ad_group.yml +47 -0
- data/spec/fixtures/delete_ad_group.yml +50 -0
- data/spec/fixtures/find_a_single_campaign_and_update_it.yml +247 -0
- data/spec/fixtures/list_of_ad_accounts.yml +75 -0
- data/spec/fixtures/list_of_ad_campaigns.yml +76 -0
- data/spec/fixtures/list_of_ad_creatives.yml +51 -0
- data/spec/fixtures/list_of_ad_groups.yml +49 -0
- data/spec/fixtures/list_of_all_ad_creatives_of_account.yml +86 -0
- data/spec/fixtures/reach_for_invalid_keyword.yml +95 -0
- data/spec/fixtures/reach_for_valid_keywords.yml +93 -0
- data/spec/fixtures/reach_for_valid_keywords_male_young.yml +93 -0
- data/spec/lib/zuck/facebook/ad_account_spec.rb +26 -0
- data/spec/lib/zuck/facebook/ad_campaign_spec.rb +4 -0
- data/spec/lib/zuck/facebook/targeting_spec_spec.rb +174 -0
- data/spec/lib/zuck/fb_object/helpers_spec.rb +67 -0
- data/spec/lib/zuck/koala/koala_methods_spec.rb +30 -0
- data/spec/lib/zuck/util/hash_delegator_spec.rb +54 -0
- data/spec/lib/zuck_spec.rb +165 -0
- data/spec/spec_helper.rb +47 -0
- data/spec/vcr_setup.rb +15 -0
- data/zuck.gemspec +141 -0
- metadata +389 -0
@@ -0,0 +1,200 @@
|
|
1
|
+
module Zuck
|
2
|
+
|
3
|
+
# Thrown when a keyword, country, gender etc. is not valid
|
4
|
+
class InvalidSpecError < RuntimeError; end
|
5
|
+
class InvalidKeywordError < InvalidSpecError; end
|
6
|
+
class InvalidCountryError < InvalidSpecError; end
|
7
|
+
class InvalidGenderError < InvalidSpecError; end
|
8
|
+
class ParamsMissingError < InvalidSpecError; end
|
9
|
+
|
10
|
+
|
11
|
+
|
12
|
+
#
|
13
|
+
# Some helpers around https://developers.facebook.com/docs/reference/ads-api/targeting-specs/
|
14
|
+
# Use like this:
|
15
|
+
#
|
16
|
+
# > ts = Facebook::TargetingSpec.new(graph, ad_account, keyword: 'foo', countries: ['US'])
|
17
|
+
# > ts.spec
|
18
|
+
# => {
|
19
|
+
# :countries => [
|
20
|
+
# [0] "US"
|
21
|
+
# ],
|
22
|
+
# :keywords => [
|
23
|
+
# [0] "foo"
|
24
|
+
# ]
|
25
|
+
# }
|
26
|
+
# > ts.fetch_reach
|
27
|
+
# => 12345
|
28
|
+
#
|
29
|
+
class TargetingSpec
|
30
|
+
attr_reader :spec, :graph
|
31
|
+
|
32
|
+
# @param graph [Koala::Facebook::API] The koala graph object to use
|
33
|
+
# @param ad_account [String] The ad account you want to use to query the facebook api
|
34
|
+
# @param spec [Hash] The targeting spec. Supported keys:
|
35
|
+
#
|
36
|
+
# - `:countries`: Array of uppercase two letter country codes
|
37
|
+
# - `:genders` (optional): Can be an array with 2 (female) and 1 (male)
|
38
|
+
# - `:gender` (optional): Set it to 'male' or 'female' to autoconfigure the genders array
|
39
|
+
# - `:age_min` (optional): In years
|
40
|
+
# - `:age_max` (optional): In years
|
41
|
+
# - `:age_class` (optional): Set it to `young` or `old` to autoconfigure `age_min` and `age_max`
|
42
|
+
# for people older or younger than 25
|
43
|
+
# - `:locales` (optional): [disabled] Array of integers, valid keys are here https://graph.facebook.com/search?type=adlocale&q=en
|
44
|
+
# - `:keywords`: Array of strings with keywords
|
45
|
+
#
|
46
|
+
def initialize(graph, ad_account, spec = nil)
|
47
|
+
@validated_keywords = {}
|
48
|
+
@graph = graph
|
49
|
+
@ad_account = "act_#{ad_account}".gsub('act_act_', 'act_')
|
50
|
+
self.spec = spec
|
51
|
+
end
|
52
|
+
|
53
|
+
# @param spec [Hash] See {#initialize}
|
54
|
+
def spec=(spec)
|
55
|
+
@spec = spec || {}
|
56
|
+
build_spec
|
57
|
+
end
|
58
|
+
|
59
|
+
# @return [Hash] The reach for the options given in {#initialize}, see
|
60
|
+
# https://developers.facebook.com/docs/reference/ads-api/reachestimate/
|
61
|
+
def fetch_reach
|
62
|
+
validate_spec
|
63
|
+
json = @spec.to_json
|
64
|
+
o = "#{@ad_account}/reachestimate"
|
65
|
+
result = graph.get_object(o, targeting_spec: json)
|
66
|
+
return false unless result
|
67
|
+
result.with_indifferent_access
|
68
|
+
end
|
69
|
+
|
70
|
+
def validate_keywords
|
71
|
+
@spec[:keywords].each do |w|
|
72
|
+
raise(InvalidKeywordError, w) unless validate_keyword(w)
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
# Validates a single keyword from the cache or calls
|
77
|
+
# {TargetingSpec.validate_keywords}.to validate the keywords via
|
78
|
+
# facebook's api.
|
79
|
+
# @param keyword [String] A single keyword (will be downcased)
|
80
|
+
# @return boolean
|
81
|
+
def validate_keyword(keyword)
|
82
|
+
if @validated_keywords[keyword] == nil
|
83
|
+
keywords = normalize_array([@spec[:keywords]] + [keyword])
|
84
|
+
@validated_keywords = self.class.validate_keywords(@graph, keywords)
|
85
|
+
end
|
86
|
+
@validated_keywords[keyword] == true
|
87
|
+
end
|
88
|
+
|
89
|
+
# Checks the ad api to see if the given keywords are valid
|
90
|
+
# @return [Hash] The keys are the (lowercased) keywords and the values their validity
|
91
|
+
def self.validate_keywords(graph, keywords)
|
92
|
+
keywords = normalize_array(keywords).map{|k| k.gsub(',', '%2C')}
|
93
|
+
search = graph.search(nil, type: 'adkeywordvalid', keyword_list: keywords.join(","))
|
94
|
+
results = {}
|
95
|
+
search.each do |r|
|
96
|
+
results[r['name'].downcase] = r['valid']
|
97
|
+
end
|
98
|
+
results
|
99
|
+
end
|
100
|
+
|
101
|
+
# Fetches a bunch of reach estimates from facebook at once.
|
102
|
+
# @param graph Koala graph instance
|
103
|
+
# @param specs [Array<Hash>] An array of specs as you would pass to {#initialize}
|
104
|
+
# @return [Array<Hash>] Each spec you passed in as the requests parameter with
|
105
|
+
# the [:success] set to true/false and [:reach]/[:error] are filled respectively
|
106
|
+
def self.batch_reaches(graph, ad_account, specs)
|
107
|
+
|
108
|
+
# Make all requests
|
109
|
+
reaches = []
|
110
|
+
specs.each_slice(50) do |specs_slice|
|
111
|
+
reaches += graph.batch do |batch_api|
|
112
|
+
specs_slice.each do |spec|
|
113
|
+
targeting_spec = Zuck::TargetingSpec.new(batch_api, ad_account, spec)
|
114
|
+
targeting_spec.fetch_reach
|
115
|
+
end
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
# Structure results
|
120
|
+
result = []
|
121
|
+
reaches.each_with_index do |res, i|
|
122
|
+
result[i] = specs[i].dup
|
123
|
+
if res.class < StandardError
|
124
|
+
result[i][:success] = false
|
125
|
+
result[i][:error] = res
|
126
|
+
else
|
127
|
+
result[i][:success] = true
|
128
|
+
result[i][:reach] = res.with_indifferent_access
|
129
|
+
end
|
130
|
+
end
|
131
|
+
result
|
132
|
+
end
|
133
|
+
|
134
|
+
# Convenience method, parameters are the same as in {#initialize}
|
135
|
+
# @return (see #initialize)
|
136
|
+
def self.fetch_reach(graph, ad_account, options)
|
137
|
+
ts = Zuck::TargetingSpec.new(graph, ad_account, options)
|
138
|
+
ts.fetch_reach
|
139
|
+
end
|
140
|
+
|
141
|
+
private
|
142
|
+
|
143
|
+
def self.normalize_array(arr)
|
144
|
+
[arr].flatten.compact.map(&:to_s).map(&:downcase).uniq.sort
|
145
|
+
end
|
146
|
+
|
147
|
+
def self.normalize_countries(countries)
|
148
|
+
normalize_array(countries).map(&:upcase)
|
149
|
+
end
|
150
|
+
|
151
|
+
def normalize_array(arr)
|
152
|
+
self.class.normalize_array(arr)
|
153
|
+
end
|
154
|
+
|
155
|
+
def normalize_countries(countries)
|
156
|
+
self.class.normalize_countries(countries)
|
157
|
+
end
|
158
|
+
|
159
|
+
def validate_spec
|
160
|
+
@spec[:countries] = normalize_countries(@spec[:countries])
|
161
|
+
@spec[:keywords] = normalize_array(@spec[:keywords])
|
162
|
+
@spec[:broad_age] ||= false
|
163
|
+
raise(InvalidCountryError, "Need to set :countries") unless @spec[:countries].present?
|
164
|
+
unless @spec[:keywords].present? or @spec[:connections].present?
|
165
|
+
raise(ParamsMissingError, "Need to set :keywords or :connections")
|
166
|
+
end
|
167
|
+
end
|
168
|
+
|
169
|
+
def build_spec
|
170
|
+
return unless @spec
|
171
|
+
age = @spec.delete(:age_class)
|
172
|
+
if age.to_s == 'young'
|
173
|
+
@spec[:age_min] = 13
|
174
|
+
@spec[:age_max] = 24
|
175
|
+
elsif age.to_s == 'old'
|
176
|
+
@spec[:age_min] = 25
|
177
|
+
else
|
178
|
+
@spec[:age_min] = 13
|
179
|
+
end
|
180
|
+
|
181
|
+
gender = spec.delete(:gender)
|
182
|
+
if gender and !['male', 'female'].include?(gender.to_s)
|
183
|
+
raise(InvalidGenderError, "Gender can only be male or female")
|
184
|
+
end
|
185
|
+
@spec[:genders] = [1] if gender.to_s == 'male'
|
186
|
+
@spec[:genders] = [2] if gender.to_s == 'female'
|
187
|
+
|
188
|
+
keyword = spec.delete(:keyword)
|
189
|
+
@spec[:keywords] = normalize_array([keyword, @spec[:keywords]])
|
190
|
+
|
191
|
+
country = spec.delete(:country)
|
192
|
+
@spec[:countries] = normalize_countries([country, @spec[:countries]])
|
193
|
+
|
194
|
+
connections = spec.delete(:connections)
|
195
|
+
@spec[:connections] = normalize_array([connections, @spec[:connections]])
|
196
|
+
|
197
|
+
end
|
198
|
+
|
199
|
+
end
|
200
|
+
end
|
@@ -0,0 +1,110 @@
|
|
1
|
+
require_relative 'error'
|
2
|
+
|
3
|
+
module Zuck
|
4
|
+
module FbObject
|
5
|
+
module DSL
|
6
|
+
|
7
|
+
def self.included(base)
|
8
|
+
base.extend(ClassMethods)
|
9
|
+
end
|
10
|
+
|
11
|
+
# @return [String] Most facebook objects will need to return their
|
12
|
+
# id property here, so that's the default. Overwrite if necessary
|
13
|
+
def path
|
14
|
+
self[:id] or raise "Can't find a path unless I have an id #{self.inspect}"
|
15
|
+
end
|
16
|
+
|
17
|
+
module ClassMethods
|
18
|
+
|
19
|
+
# Don't allow create/update/delete
|
20
|
+
def read_only
|
21
|
+
@read_only = true
|
22
|
+
end
|
23
|
+
|
24
|
+
def read_only?
|
25
|
+
!!@read_only
|
26
|
+
end
|
27
|
+
|
28
|
+
# Part of our little DSL, sets the part of the path that fetches the
|
29
|
+
# list of objects from facebook.
|
30
|
+
#
|
31
|
+
# class Foo < FbObject
|
32
|
+
# ...
|
33
|
+
# list_path :foos
|
34
|
+
# end
|
35
|
+
#
|
36
|
+
# {FbObject} uses this to construct a path together with this class'
|
37
|
+
# parent object's path method (which is usually just it's ID
|
38
|
+
# property)
|
39
|
+
#
|
40
|
+
# @param path [String, Symbol] Pass a value if you want to set the
|
41
|
+
# list_path for this object.
|
42
|
+
# @return The object's `list_path`
|
43
|
+
def list_path(path = nil)
|
44
|
+
@list_path = path if path
|
45
|
+
@list_path
|
46
|
+
end
|
47
|
+
|
48
|
+
# Pretty much like a `belongs_to`, but is used to construct paths to
|
49
|
+
# access the facebook api.
|
50
|
+
#
|
51
|
+
# It also defines a getter method. Look
|
52
|
+
#
|
53
|
+
# class AdCampaign < FbObject
|
54
|
+
# ...
|
55
|
+
# parent_object :ad_account
|
56
|
+
# end
|
57
|
+
#
|
58
|
+
# Now on instances you can call `my_campaign.ad_account` to fetch
|
59
|
+
# the ad account your campaign is part of.
|
60
|
+
#
|
61
|
+
# @param type [Symbol] Pass an underscored symbol here, for example
|
62
|
+
# `ad_account`
|
63
|
+
def parent_object(type)
|
64
|
+
@parent_object_type = type.to_s
|
65
|
+
define_method(type) do
|
66
|
+
@parent_object
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
# Defines which other classes might have this one as their parent.
|
71
|
+
#
|
72
|
+
# If you do something like
|
73
|
+
#
|
74
|
+
# ```ruby
|
75
|
+
# class Foo < RawFbObject
|
76
|
+
# ...
|
77
|
+
# connections :dings, :dongs
|
78
|
+
# end
|
79
|
+
# ```
|
80
|
+
#
|
81
|
+
# then your `Foo` instances will have a `#dings` and `#dongs` methods,
|
82
|
+
# which will call `Ding.all` and `Dong.call` on the appropriate graph
|
83
|
+
# object.
|
84
|
+
#
|
85
|
+
# Also, you will get a `#create_ding` and `#create_dong` methods that
|
86
|
+
# forward to `Ding.new` and `Dong.new`.
|
87
|
+
def connections(*args)
|
88
|
+
args.each do |c|
|
89
|
+
|
90
|
+
# Why a lambda? Because it gets evaluated on runtime, not now. This is a
|
91
|
+
# good thing because it allows for randomly loading files with classes
|
92
|
+
# that inherit from FbObject.
|
93
|
+
class_resolver = lambda{"Zuck::#{c.to_s.singularize.camelize}".constantize}
|
94
|
+
|
95
|
+
# Define getter for connections
|
96
|
+
define_method(c.to_s.pluralize) do
|
97
|
+
class_resolver.call.all(graph, self)
|
98
|
+
end
|
99
|
+
|
100
|
+
# Define create method for connections
|
101
|
+
define_method("create_#{c.to_s.singularize}") do |data|
|
102
|
+
class_resolver.call.create(graph, data, self)
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
@@ -0,0 +1,111 @@
|
|
1
|
+
module Zuck
|
2
|
+
|
3
|
+
# Including this module does three things:
|
4
|
+
#
|
5
|
+
# 1. Lets you use `x[:foo]` to access keys of the
|
6
|
+
# underlying Hash
|
7
|
+
# 2. Lets you use `x[:foo] = :bar` to set values in
|
8
|
+
# the underlying Hash
|
9
|
+
# 3. Lets you define which keys are to be expected in
|
10
|
+
# the underlying hash. These keys will become methods
|
11
|
+
#
|
12
|
+
# Here's an example:
|
13
|
+
#
|
14
|
+
# class MyObjectWithHash
|
15
|
+
#
|
16
|
+
# include Zuck::HashDelegator
|
17
|
+
#
|
18
|
+
# known_keys :foo, :bar
|
19
|
+
#
|
20
|
+
# def initialize(initial_data)
|
21
|
+
# set_data(initial_data)
|
22
|
+
# end
|
23
|
+
# end
|
24
|
+
#
|
25
|
+
# > x = MyObjectWithHash.new(foo: :foo)
|
26
|
+
# > x.foo
|
27
|
+
# => :foo
|
28
|
+
# > x.bar
|
29
|
+
# => nil
|
30
|
+
# > x['bar'] = :everything_is_a_symbol
|
31
|
+
# > x[:bar]
|
32
|
+
# => :everything_is_a_symbol
|
33
|
+
# > x['bar']
|
34
|
+
# => :everything_is_a_symbol
|
35
|
+
# > x.foo
|
36
|
+
# => :everything_is_a_symbol
|
37
|
+
# > x.foo = :you_also_have_setters
|
38
|
+
# => :you_also_have_setters
|
39
|
+
#
|
40
|
+
# As you can see, all string keys become symbols and the
|
41
|
+
# foo and bar methods were added because they are known keys
|
42
|
+
#
|
43
|
+
module HashDelegator
|
44
|
+
|
45
|
+
def self.included(base)
|
46
|
+
base.extend(ClassMethods)
|
47
|
+
end
|
48
|
+
|
49
|
+
module ClassMethods
|
50
|
+
def known_keys(*args)
|
51
|
+
args.each do |key|
|
52
|
+
|
53
|
+
# Define getter
|
54
|
+
self.send(:define_method, key) do
|
55
|
+
init_hash
|
56
|
+
@hash_delegator_hash[key]
|
57
|
+
end
|
58
|
+
|
59
|
+
# Define setter
|
60
|
+
self.send(:define_method, "#{key}=") do |val|
|
61
|
+
init_hash
|
62
|
+
@hash_delegator_hash[key] = val
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
def set_data(d)
|
69
|
+
e = "You can only assign a Hash to #{self.class}, not a #{d.class}"
|
70
|
+
raise e unless d.is_a? Hash
|
71
|
+
hash = Hash.new
|
72
|
+
d.each do |key, value|
|
73
|
+
hash[(key.to_sym rescue key) || key] = value
|
74
|
+
end
|
75
|
+
@hash_delegator_hash = hash
|
76
|
+
end
|
77
|
+
|
78
|
+
def data=(d)
|
79
|
+
set_data(d)
|
80
|
+
end
|
81
|
+
|
82
|
+
def data
|
83
|
+
@hash_delegator_hash
|
84
|
+
end
|
85
|
+
|
86
|
+
def [](key)
|
87
|
+
init_hash
|
88
|
+
@hash_delegator_hash[key.to_sym]
|
89
|
+
end
|
90
|
+
|
91
|
+
def []=(key, value)
|
92
|
+
init_hash
|
93
|
+
@hash_delegator_hash[key.to_sym] = value
|
94
|
+
end
|
95
|
+
|
96
|
+
def to_s
|
97
|
+
init_hash
|
98
|
+
vars = @hash_delegator_hash.map do |k, v|
|
99
|
+
"#{k}: #{v.to_json}"
|
100
|
+
end.join(", ")
|
101
|
+
"#<#{self.class} #{vars}>"
|
102
|
+
end
|
103
|
+
|
104
|
+
private
|
105
|
+
|
106
|
+
def init_hash
|
107
|
+
@hash_delegator_hash ||= {}
|
108
|
+
end
|
109
|
+
|
110
|
+
end
|
111
|
+
end
|
@@ -0,0 +1,57 @@
|
|
1
|
+
module Zuck
|
2
|
+
module FbObject
|
3
|
+
module Helpers
|
4
|
+
|
5
|
+
private
|
6
|
+
|
7
|
+
def get(graph, path)
|
8
|
+
begin
|
9
|
+
graph.get_object(path)
|
10
|
+
rescue => e
|
11
|
+
puts "#{e} graph.get_object(#{path.to_json})" if in_irb?
|
12
|
+
raise e
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
def create_connection(graph, parent, connection, args = {}, opts = {})
|
17
|
+
begin
|
18
|
+
graph.put_connections(parent, connection, args, opts)
|
19
|
+
rescue => e
|
20
|
+
msg = "#{e} graph.put_connections(#{parent.to_json}, #{connection.to_json}, #{args.to_json}, #{opts.to_json})"
|
21
|
+
puts msg if in_irb?
|
22
|
+
raise e
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def post(graph, path, data, opts = {})
|
27
|
+
begin
|
28
|
+
graph.graph_call(path.to_s, data, "post", opts)
|
29
|
+
rescue => e
|
30
|
+
msg = "#{e} graph.graph_call(#{path.to_json}, #{data.to_json}, \"post\", #{opts.to_json})"
|
31
|
+
puts msg if in_irb?
|
32
|
+
raise e
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
def delete(graph, path)
|
37
|
+
begin
|
38
|
+
graph.delete_object(path)
|
39
|
+
rescue => e
|
40
|
+
puts "#{e} graph.delete(#{path.to_json})" if in_irb?
|
41
|
+
raise e
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
def path_with_parent(parent=nil)
|
46
|
+
paths = []
|
47
|
+
paths << parent.path if parent
|
48
|
+
paths << list_path
|
49
|
+
paths.join('/')
|
50
|
+
end
|
51
|
+
|
52
|
+
def in_irb?
|
53
|
+
defined?(IRB)
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
@@ -0,0 +1,147 @@
|
|
1
|
+
module Zuck
|
2
|
+
module FbObject
|
3
|
+
module Read
|
4
|
+
def self.included(base)
|
5
|
+
base.extend(ClassMethods)
|
6
|
+
end
|
7
|
+
|
8
|
+
# @param graph [Koala::Facebook::API] A graph with access_token
|
9
|
+
# @param data [Hash] The properties you want to assign, this is what
|
10
|
+
# facebook gave us (see known_keys).
|
11
|
+
# @param parent [<FbObject] A parent context for this class, must
|
12
|
+
# inherit from {Zuck::FbObject}
|
13
|
+
def initialize(graph, data = {}, parent=nil)
|
14
|
+
self.graph = graph
|
15
|
+
set_data(data)
|
16
|
+
|
17
|
+
# If the parent is an {AdAccount} we only want to set it as this
|
18
|
+
# object's direct parent when this object is an {AdCampaign}.
|
19
|
+
if !parent.is_a?(AdAccount) or parent.is_a?(AdAccount) and self.is_a?(AdCampaign)
|
20
|
+
set_parent(parent)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
# Refetches the data from façeboko
|
25
|
+
def reload
|
26
|
+
data = get(graph, path)
|
27
|
+
validate_data(data)
|
28
|
+
set_data(data)
|
29
|
+
self
|
30
|
+
end
|
31
|
+
|
32
|
+
private
|
33
|
+
|
34
|
+
# Sets the parent of this instance
|
35
|
+
#
|
36
|
+
# @param parent [FbObject] Has to be of the same class type you defined
|
37
|
+
# using {FbObject.parent_object}
|
38
|
+
def set_parent(parent)
|
39
|
+
return unless parent
|
40
|
+
self.class.validate_parent_object_class(parent)
|
41
|
+
@parent_object = parent
|
42
|
+
end
|
43
|
+
|
44
|
+
# Makes sure that the data passed comes from a facebook
|
45
|
+
# object of the same type. We check this by comparing
|
46
|
+
# the 'group_id' value or the 'ad_group_id' value
|
47
|
+
# with the 'id' value, when this
|
48
|
+
# is called on a {Zuck::AdGroup} for example.
|
49
|
+
#
|
50
|
+
# Facebook omits the "ad" prefix sometimes, so we check
|
51
|
+
# for both.
|
52
|
+
def validate_data(data)
|
53
|
+
singular_list_path = self.class.list_path.to_s.singularize
|
54
|
+
|
55
|
+
# This is a special case for ad accounts (they have weird ids
|
56
|
+
# that begin with act: "act_12345" instead of "12345"
|
57
|
+
return if data['account_id'] and "act_#{data['account_id']}" == data['id'].to_s
|
58
|
+
|
59
|
+
# This is the case for all other objects
|
60
|
+
long_id_key = "#{singular_list_path}_id"
|
61
|
+
short_id_key = "#{singular_list_path[2..-1]}_id"
|
62
|
+
return if data[long_id_key] and data[long_id_key].to_s == data["id"].to_s
|
63
|
+
return if data[short_id_key] and data[short_id_key].to_s == data["id"].to_s
|
64
|
+
|
65
|
+
# Something went wrong. Either the data provided by the user is
|
66
|
+
# not consistent, or a wrong object type was belongs to this id on facebook
|
67
|
+
# (an ad group instead of an ad campaign, for example).
|
68
|
+
#
|
69
|
+
# Maybe we can make somebody's life easier by raising a verbose exception.
|
70
|
+
error = "Invalid type.\n\nExpected data['id']=#{data['id'].inspect} to be equal to one of these:\n"
|
71
|
+
error += " * data['account_id']=#{data['account_id'].inspect}\n"
|
72
|
+
error += " * data['#{short_id_key}']=#{data[short_id_key].inspect}\n"
|
73
|
+
error += " * data['#{long_id_key}']=#{data[long_id_key].inspect}\n"
|
74
|
+
|
75
|
+
raise error
|
76
|
+
end
|
77
|
+
|
78
|
+
module ClassMethods
|
79
|
+
|
80
|
+
# Finds by object id and checks type
|
81
|
+
def find(id, graph = Zuck.graph)
|
82
|
+
new(graph, id: id).reload
|
83
|
+
end
|
84
|
+
|
85
|
+
# Automatique all getter.
|
86
|
+
#
|
87
|
+
# Let's say you want to fetch all campaigns
|
88
|
+
# from facebook. This can happen in the context of an ad
|
89
|
+
# account. In this gem, that context is called a parent. This method
|
90
|
+
# would only be called on objects that inherit from {FbObject}.
|
91
|
+
# It asks the `parent` for it's path (if it is given), and appends
|
92
|
+
# it's own `list_path` property that you have defined (see
|
93
|
+
# list_path)
|
94
|
+
#
|
95
|
+
# If, however, you want to fetch all ad creatives, regardless of
|
96
|
+
# which ad group is their parent, you can omit the `parent`
|
97
|
+
# parameter. The creatives returned by `Zuck::AdCreative.all` will
|
98
|
+
# return `nil` when you call `#ad_group` on them, though, because facebook
|
99
|
+
# will not return this information. So if you can, try to fetch
|
100
|
+
# objects through their direct parent, e.g.
|
101
|
+
# `my_ad_group.ad_creatives`.
|
102
|
+
#
|
103
|
+
# @param graph [Koala::Facebook::API] A graph with access_token
|
104
|
+
# @param parent [<FbObject] A parent object to scope
|
105
|
+
def all(graph = Zuck.graph, parent = nil)
|
106
|
+
parent ||= parent_ad_account_fallback
|
107
|
+
r = get(graph, path_with_parent(parent))
|
108
|
+
r.map do |c|
|
109
|
+
new(graph, c, parent)
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
# Makes sure the given parent matches what you defined
|
114
|
+
# in {FbObject.parent_object}
|
115
|
+
def validate_parent_object_class(parent)
|
116
|
+
resolve_parent_object_class
|
117
|
+
e = "Invalid parent_object: #{parent.class} is not a #{@parent_object_class}"
|
118
|
+
raise e if @parent_object_class and !parent.is_a?(@parent_object_class)
|
119
|
+
end
|
120
|
+
|
121
|
+
private
|
122
|
+
|
123
|
+
|
124
|
+
# Attempts to resolve the {FbObject.parent_object} to a class at runtime
|
125
|
+
# so we can load files in any random order...
|
126
|
+
def resolve_parent_object_class
|
127
|
+
return if @parent_object_class
|
128
|
+
class_s = "Zuck::#{@parent_object_type.camelcase}"
|
129
|
+
@parent_object_class = class_s.constantize
|
130
|
+
end
|
131
|
+
|
132
|
+
# Some objects can be fetched "per account" or "per parent
|
133
|
+
# object", e.g. you can fetch all ad creatives for your account
|
134
|
+
# or only for a special ad group.
|
135
|
+
#
|
136
|
+
# @return [nil, Zuck::FbObject] Returns the current ad account
|
137
|
+
# unless you're calling `Zuck::AdAccount.all`. Then we return
|
138
|
+
# nil because the ad account needs no parent.
|
139
|
+
def parent_ad_account_fallback
|
140
|
+
return nil if self == Zuck::AdAccount
|
141
|
+
Zuck::AdAccount.all.first
|
142
|
+
end
|
143
|
+
|
144
|
+
end
|
145
|
+
end
|
146
|
+
end
|
147
|
+
end
|
File without changes
|