zuck 0.0.4

Sign up to get free protection for your applications and to get access to all the features.
Files changed (52) hide show
  1. data/.rvmrc +1 -0
  2. data/.travis.yml +7 -0
  3. data/.yardopts +4 -0
  4. data/CHANGELOG.markdown +4 -0
  5. data/Gemfile +35 -0
  6. data/Gemfile.lock +110 -0
  7. data/Guardfile.dist +45 -0
  8. data/LICENSE.txt +20 -0
  9. data/README.markdown +138 -0
  10. data/Rakefile +39 -0
  11. data/VERSION +1 -0
  12. data/console +26 -0
  13. data/lib/zuck/facebook/ad_account.rb +40 -0
  14. data/lib/zuck/facebook/ad_campaign.rb +24 -0
  15. data/lib/zuck/facebook/ad_creative.rb +30 -0
  16. data/lib/zuck/facebook/ad_group.rb +39 -0
  17. data/lib/zuck/facebook/targeting_spec.rb +200 -0
  18. data/lib/zuck/fb_object/dsl.rb +110 -0
  19. data/lib/zuck/fb_object/error.rb +8 -0
  20. data/lib/zuck/fb_object/hash_delegator.rb +111 -0
  21. data/lib/zuck/fb_object/helpers.rb +57 -0
  22. data/lib/zuck/fb_object/read.rb +147 -0
  23. data/lib/zuck/fb_object/read_only.rb +0 -0
  24. data/lib/zuck/fb_object/write.rb +75 -0
  25. data/lib/zuck/fb_object.rb +53 -0
  26. data/lib/zuck/koala/koala_methods.rb +27 -0
  27. data/lib/zuck.rb +9 -0
  28. data/spec/fixtures/a_single_account.yml +75 -0
  29. data/spec/fixtures/a_single_campaign.yml +48 -0
  30. data/spec/fixtures/create_ad_campaign.yml +49 -0
  31. data/spec/fixtures/create_ad_group.yml +47 -0
  32. data/spec/fixtures/delete_ad_group.yml +50 -0
  33. data/spec/fixtures/find_a_single_campaign_and_update_it.yml +247 -0
  34. data/spec/fixtures/list_of_ad_accounts.yml +75 -0
  35. data/spec/fixtures/list_of_ad_campaigns.yml +76 -0
  36. data/spec/fixtures/list_of_ad_creatives.yml +51 -0
  37. data/spec/fixtures/list_of_ad_groups.yml +49 -0
  38. data/spec/fixtures/list_of_all_ad_creatives_of_account.yml +86 -0
  39. data/spec/fixtures/reach_for_invalid_keyword.yml +95 -0
  40. data/spec/fixtures/reach_for_valid_keywords.yml +93 -0
  41. data/spec/fixtures/reach_for_valid_keywords_male_young.yml +93 -0
  42. data/spec/lib/zuck/facebook/ad_account_spec.rb +26 -0
  43. data/spec/lib/zuck/facebook/ad_campaign_spec.rb +4 -0
  44. data/spec/lib/zuck/facebook/targeting_spec_spec.rb +174 -0
  45. data/spec/lib/zuck/fb_object/helpers_spec.rb +67 -0
  46. data/spec/lib/zuck/koala/koala_methods_spec.rb +30 -0
  47. data/spec/lib/zuck/util/hash_delegator_spec.rb +54 -0
  48. data/spec/lib/zuck_spec.rb +165 -0
  49. data/spec/spec_helper.rb +47 -0
  50. data/spec/vcr_setup.rb +15 -0
  51. data/zuck.gemspec +141 -0
  52. 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,8 @@
1
+ module Zuck
2
+ class ZuckError < StandardError; end
3
+ module Error
4
+
5
+ class ReadOnly < ::Zuck::ZuckError; end
6
+
7
+ end
8
+ end
@@ -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