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.
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