ad_gear_client 0.3.8

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 (49) hide show
  1. data/.gitignore +6 -0
  2. data/LICENSE +21 -0
  3. data/README.rdoc +55 -0
  4. data/Rakefile +132 -0
  5. data/VERSION +1 -0
  6. data/ad_gear_client.gemspec +114 -0
  7. data/examples/.gitignore +1 -0
  8. data/examples/ad_gear.yml.sample +8 -0
  9. data/examples/create_campaign.rb +23 -0
  10. data/examples/create_placement.rb +57 -0
  11. data/examples/prelude.rb +27 -0
  12. data/examples/read_write_site.rb +36 -0
  13. data/examples/upload_file_to_ad_unit.rb +49 -0
  14. data/lib/ad_gear/ad_spot.rb +7 -0
  15. data/lib/ad_gear/ad_spot_membership.rb +5 -0
  16. data/lib/ad_gear/ad_unit.rb +17 -0
  17. data/lib/ad_gear/ad_unit_click.rb +5 -0
  18. data/lib/ad_gear/ad_unit_file.rb +6 -0
  19. data/lib/ad_gear/ad_unit_interaction.rb +5 -0
  20. data/lib/ad_gear/ad_unit_variable.rb +5 -0
  21. data/lib/ad_gear/advertiser.rb +4 -0
  22. data/lib/ad_gear/base.rb +169 -0
  23. data/lib/ad_gear/click.rb +4 -0
  24. data/lib/ad_gear/config.rb +143 -0
  25. data/lib/ad_gear/core_ext.rb +18 -0
  26. data/lib/ad_gear/file.rb +4 -0
  27. data/lib/ad_gear/format.rb +4 -0
  28. data/lib/ad_gear/has_many_array.rb +84 -0
  29. data/lib/ad_gear/interaction.rb +4 -0
  30. data/lib/ad_gear/placement_membership.rb +5 -0
  31. data/lib/ad_gear/placement_rule.rb +5 -0
  32. data/lib/ad_gear/publisher.rb +7 -0
  33. data/lib/ad_gear/site.rb +6 -0
  34. data/lib/ad_gear/template.rb +4 -0
  35. data/lib/ad_gear/upload.rb +46 -0
  36. data/lib/ad_gear/variable.rb +4 -0
  37. data/lib/ad_gear/web_campaign.rb +6 -0
  38. data/lib/ad_gear/web_placement.rb +26 -0
  39. data/lib/ad_gear/xml_format.rb +35 -0
  40. data/lib/ad_gear.rb +108 -0
  41. data/test/ad_gear/ad_spot_test.rb +43 -0
  42. data/test/ad_gear/config_test.rb +114 -0
  43. data/test/ad_gear/placement_rule_test.rb +22 -0
  44. data/test/ad_gear/site_test.rb +69 -0
  45. data/test/ad_gear/upload_test.rb +58 -0
  46. data/test/ad_gear_test.rb +23 -0
  47. data/test/fixtures/access-denied-no-auth.txt +13 -0
  48. data/test/test_helper.rb +33 -0
  49. metadata +163 -0
@@ -0,0 +1,169 @@
1
+ module AdGear
2
+ class Base < ActiveResource::Base
3
+ def initialize(params={})
4
+ super({})
5
+ params.each do |key, value|
6
+ send("#{key}=", value)
7
+ end
8
+ end
9
+
10
+ # Declares a relationship where we store the ID, but accept and/or return the full object.
11
+ #
12
+ # @example
13
+ # class AdGear::AdSpot < AdGear::Base
14
+ # belongs_to :format
15
+ # belongs_to :bookable, :polymorphic => true
16
+ # belongs_to :web_campaign, :as => :campaign
17
+ # end
18
+ #
19
+ # include AdGear
20
+ # AdSpot.new(:format => Format.find(1), :bookable => Publisher.find(2), :web_campaign => WebCampaign.find(3)).attributes
21
+ # #=> {"format_id" => 1, "bookable_id" => 2, "bookable_type" => "Publisher", "campaign_id" => 3}
22
+ def self.belongs_to(*attributes)
23
+ options = attributes.extract_options!
24
+ attributes.each do |attribute|
25
+ attribute_name = attribute.to_s
26
+ belongs_to_associations[attribute_name] = options.reverse_merge(:as => attribute_name)
27
+
28
+ if options[:polymorphic] then
29
+ polymorphic_belongs_to_attribute_writer_for(attribute_name)
30
+ polymorphic_belongs_to_attribute_reader_for(attribute_name)
31
+ else
32
+ belongs_to_attribute_writer_for(attribute_name)
33
+ belongs_to_attribute_reader_for(attribute_name)
34
+ end
35
+ end
36
+ end
37
+
38
+ def self.belongs_to_associations #:nodoc:
39
+ @belongs_to_associations ||= {}
40
+ end
41
+
42
+ # Defines a managed sub-collection. Elements managed through a #has_many are
43
+ # ready for use by AttributeFu or ActiveRecord's 2.3 nested_attributes.
44
+ #
45
+ # @example
46
+ # class AdGear::AdUnit < AdGear::Base
47
+ # has_many :ad_unit_files, :ad_unit_clicks
48
+ # has_many :ad_unit_interactions, :ad_unit_variables
49
+ # end
50
+ #
51
+ # AdGear::AdUnit.new(:ad_unit_files => [AdUnitFile.new(...)]).to_xml
52
+ # <ad-unit>
53
+ # <ad-unit-file-attributes>
54
+ # <new>
55
+ # <n0>
56
+ # ...
57
+ # </n0>
58
+ # </new>
59
+ # <!-- If there were "old" records, they'd be here
60
+ # </ad-unit-file-attributes>
61
+ # </ad-unit>
62
+ def self.has_many(*collections)
63
+ collections.flatten.compact.each do |collection|
64
+ collection_name = collection.to_s
65
+
66
+ # Remember what collections we are managing, for use in #to_xml
67
+ has_many_collections << collection_name
68
+
69
+ # Define a getter for the collection that transforms a plain Array into
70
+ # a HasManyArray which knows about new and old records
71
+ define_method(collection_name) do
72
+ arr = @attributes[collection_name]
73
+ return arr if arr.kind_of?(AdGear::HasManyArray)
74
+ @attributes[collection_name] = AdGear::HasManyArray.new(collection_name.to_sym, AdGear.const_get(collection_name.classify), arr)
75
+ end
76
+
77
+ define_method("#{collection_name}=") do |arr|
78
+ @attributes[collection_name] = if arr.kind_of?(AdGear::HasManyArray) then
79
+ arr
80
+ else
81
+ AdGear::HasManyArray.new(collection_name.to_sym, AdGear.const_get(collection_name.classify), arr)
82
+ end
83
+ end
84
+ end
85
+ end
86
+
87
+ def self.has_many_collections #:nodoc:
88
+ @has_many_collections ||= []
89
+ end
90
+
91
+ # Defines a list of attributes that should be ignored and not sent back to the server.
92
+ #
93
+ # @example
94
+ # class AdGear::Site < AdGear::Base
95
+ # ignorable_attributes :embed_code
96
+ # end
97
+ #
98
+ # AdGear::Site.find(13).to_xml
99
+ # <site>
100
+ # ...
101
+ # <!-- no embed-code element -->
102
+ # </site>
103
+ def self.ignorable_attributes(*attributes)
104
+ @ignorable_attributes ||= []
105
+ @ignorable_attributes += attributes.flatten.compact.map {|name| name.to_s}
106
+ @ignorable_attributes
107
+ end
108
+
109
+ def to_xml(options={})
110
+ self.class.has_many_collections.each do |name|
111
+ send(name) # convert the @attributes value into a HasManyArray
112
+ end
113
+
114
+ hash = if self.class.ignorable_attributes.empty?
115
+ @attributes
116
+ else
117
+ returning(@attributes.dup) do |hash|
118
+ self.class.ignorable_attributes.each do |attr_name|
119
+ hash.delete(attr_name)
120
+ end
121
+ end
122
+ end
123
+ hash.to_xml({:root => self.class.element_name}.merge(options))
124
+ end
125
+
126
+ # Delegates to #to_xml, as this is the simplest thing that could possibly work
127
+ def encode(options={}) #:nodoc:
128
+ self.to_xml
129
+ end
130
+
131
+ private
132
+
133
+ def self.polymorphic_belongs_to_attribute_writer_for(attribute_name) #:nodoc:
134
+ define_method("#{attribute_name}=") do |value|
135
+ @attributes[self.class.belongs_to_attribute_name_for_id(attribute_name)] = value ? value.id : nil
136
+ @attributes[self.class.belongs_to_attribute_name_for_class(attribute_name)] = value ? value.class.name.demodulize : nil
137
+ end
138
+ end
139
+
140
+ def self.polymorphic_belongs_to_attribute_reader_for(attribute_name) #:nodoc:
141
+ define_method(attribute_name) do
142
+ klass = @attributes[self.class.belongs_to_attribute_name_for_class(attribute_name)]
143
+ id = @attributes[self.class.belongs_to_attribute_name_for_id(attribute_name)]
144
+ id.blank? ? nil : AdGear.const_get(AdGear.const_get(klass)).find(id)
145
+ end
146
+ end
147
+
148
+ def self.belongs_to_attribute_writer_for(attribute_name) #:nodoc:
149
+ define_method("#{attribute_name}=") do |value|
150
+ @attributes[self.class.belongs_to_attribute_name_for_id(attribute_name)] = value ? value.id : nil
151
+ end
152
+ end
153
+
154
+ def self.belongs_to_attribute_reader_for(attribute_name) #:nodoc:
155
+ define_method(attribute_name) do
156
+ id = @attributes[self.class.belongs_to_attribute_name_for_id(attribute_name)]
157
+ id.blank? ? nil : AdGear.const_get(attribute_name.classify).find(id)
158
+ end
159
+ end
160
+
161
+ def self.belongs_to_attribute_name_for_class(association_name)
162
+ "%s_type" % belongs_to_associations[association_name][:as]
163
+ end
164
+
165
+ def self.belongs_to_attribute_name_for_id(association_name)
166
+ "%s_id" % belongs_to_associations[association_name][:as]
167
+ end
168
+ end
169
+ end
@@ -0,0 +1,4 @@
1
+ module AdGear
2
+ class Click < AdGear::Base
3
+ end
4
+ end
@@ -0,0 +1,143 @@
1
+ require "erb"
2
+ require "pathname"
3
+
4
+ module AdGear
5
+ class Config
6
+ # Raised when the specified environment could not be found in the YAML configuration file.
7
+ class MissingEnvironmentSpecification < RuntimeError; end
8
+
9
+ # Reads in a YAML file containing configuration parameters for the AdGear::Client. The file
10
+ # can contain sections relating to the environment the client is running within.
11
+ #
12
+ # The YAML configuration file *must* use strings as keys.
13
+ #
14
+ # @example A sample YAML configuration file:
15
+ # development:
16
+ # site: http://localhost:3000/api
17
+ # user: dummy
18
+ # password: whatever
19
+ # logger: <%= Rails.root %>/path/to/log/file.log
20
+ # production:
21
+ # site: http://admin.adgear.com/api
22
+ # user: your_real_login
23
+ # password: your_real_digest_password
24
+ # logger: /var/log/ad_gear.log
25
+ #
26
+ # @example Reading this sample file:
27
+ # AdGear.config = AdGear::Config.new("path/to/config/file", "development")
28
+ #
29
+ # @example Reading from +config/initializers/ad_gear.rb+ in a Rails environment
30
+ # AdGear.config = AdGear::Config.new(Rails.root + "config/ad_gear.yml", Rails.env)
31
+ #
32
+ # @param obj [#read, Hash, String] When the object responds to #read, reads in the IO-like
33
+ # object and parses it as a YAML stream. If the object is a
34
+ # Hash, use it as-is. Else, treat the object as the path to a
35
+ # YAML file containing the configuration.
36
+ # @param environment [String, Symbol, nil] If this is +nil+, then do no environment unpacking, else
37
+ # find the environment that matches this string. If none
38
+ # matches, a MissingEnvironmentSpecification exception will
39
+ # be raised.
40
+ #
41
+ # @raise MissingEnvironmentSpecification When the +environment+ parameter specifies a missing environment declaration.
42
+ def initialize(obj={}, environment=nil)
43
+ @config = Hash.new
44
+ @logger = nil
45
+
46
+ config = if obj.kind_of?(Hash) then
47
+ obj
48
+ elsif obj.respond_to?(:read) then
49
+ YAML.load(ERB.new(obj.read).result)
50
+ else
51
+ YAML.load(ERB.new(IO.read(obj)).result)
52
+ end
53
+ if environment then
54
+ raise MissingEnvironmentSpecification, "Could not find #{environment.to_s.inspect} in #{config.inspect} read from #{obj.inspect}. Add the missing environment declaration, or change the parameter to this method." unless config.respond_to?(:has_key?) && config.has_key?(environment.to_s)
55
+ config = config[environment.to_s]
56
+ end
57
+
58
+ config.each_pair do |key, value|
59
+ send("#{key}=", value)
60
+ end
61
+ end
62
+
63
+ # Returns a Hash of the currect configuration.
64
+ def to_hash
65
+ @config.dup
66
+ end
67
+
68
+ def site=(value)
69
+ @config["site"] = value
70
+ end
71
+
72
+ # Returns the configured site, which is the root of the API.
73
+ def site
74
+ @config["site"]
75
+ end
76
+
77
+ def user=(value)
78
+ @config["user"] = value
79
+ end
80
+
81
+ # Returns the configured username / login.
82
+ def user
83
+ @config["user"]
84
+ end
85
+
86
+ def password=(value)
87
+ @config["password"] = value
88
+ end
89
+
90
+ # Returns the configured password.
91
+ def password
92
+ @config["password"]
93
+ end
94
+
95
+ # Denies or allows using Basic authentication method. This will have an effect only if you are using http://github.com/francois/rails/ar_basic/activeresource
96
+ def use_basic_authentication
97
+ @config["use_basic_authentication"]
98
+ end
99
+
100
+ def use_basic_authentication=(value)
101
+ @config["use_basic_authentication"] = value
102
+ end
103
+
104
+ # Denies or allows using Digest authentication method. This will have an effect only if you are using http://github.com/francois/rails/ar_digest/activeresource
105
+ def use_digest_authentication
106
+ @config["use_digest_authentication"]
107
+ end
108
+
109
+ def use_digest_authentication=(value)
110
+ @config["use_digest_authentication"] = value
111
+ end
112
+
113
+ # Returns a format object suitable for use by ActiveResource.
114
+ def format
115
+ AdGear::XmlFormat
116
+ end
117
+
118
+ # Returns the Logger instance that was configured in #logger=.
119
+ def logger
120
+ @logger
121
+ end
122
+
123
+ # Register a Logger for use by AdGear's client.
124
+ # @param value [nil, String] When nil, no Logger is assigned.
125
+ # When the values STDERR or STDOUT are used, logs to the specified stream.
126
+ # Else, it is taken as the path to a log file.
127
+ def logger=(value)
128
+ @config["logger"] = value
129
+ @logger = case value
130
+ when nil, ""
131
+ # No logger
132
+ nil
133
+ when /^std(err|out)$/i
134
+ # Existing stream
135
+ Logger.new(Object.const_get(value.upcase))
136
+ else
137
+ warn("WARNING: path to logger in AdGear::Client configuration is relative: CWD is #{Dir.pwd.inspect}") unless Pathname.new(value).absolute?
138
+ # Path to a file
139
+ Logger.new(value)
140
+ end
141
+ end
142
+ end
143
+ end
@@ -0,0 +1,18 @@
1
+ #
2
+ # ActiveSupport's Array#to_xml will build an XML with root set to the full class name including
3
+ # module(s) it may be nested in - we don't want that, we want it to be *just* the class name,
4
+ # make it so with this monkey patch...
5
+ #
6
+ # Note this is loaded last from AdGear.config= after ActiveSupport has already been loaded. We
7
+ # cannot simply override because ActiveSupport does lazy loading.
8
+ #
9
+ class Array
10
+ def to_xml_with_module_prefixing(options = {})
11
+ raise "Not all elements respond to to_xml in #{self.inspect}" unless all? { |e| e.respond_to? :to_xml }
12
+ root = all? { |e| e.is_a?(first.class) && first.class.to_s != "Hash" } ?
13
+ ActiveSupport::Inflector.pluralize(ActiveSupport::Inflector.underscore(first.class.to_s.split("::").last)) : "records"
14
+ to_xml_without_module_prefixing({ :root => root }.merge(options))
15
+ end
16
+
17
+ alias_method_chain :to_xml, :module_prefixing
18
+ end
@@ -0,0 +1,4 @@
1
+ module AdGear
2
+ class File < AdGear::Base
3
+ end
4
+ end
@@ -0,0 +1,4 @@
1
+ module AdGear
2
+ class Format < AdGear::Base
3
+ end
4
+ end
@@ -0,0 +1,84 @@
1
+ module AdGear
2
+ class HasManyArray
3
+ include Enumerable
4
+
5
+ def initialize(name, klass, saved=nil)
6
+ @name = name.to_s.singularize.dasherize + "-attributes"
7
+ @klass = klass
8
+ @new = []
9
+ @saved = []
10
+ saved.each do |obj|
11
+ self << obj
12
+ end if saved
13
+ end
14
+
15
+ def <<(object)
16
+ root = if object.kind_of?(Hash) then
17
+ if object.has_key?("id") || object.has_key?(:id) then
18
+ object = @klass.new(object)
19
+ @saved
20
+ else
21
+ # Instantiate an object from the Hash, which will obviously be a new object
22
+ object = @klass.new(object)
23
+ @new
24
+ end
25
+ elsif object.respond_to?(:new_record?) && object.new_record?
26
+ @new
27
+ else
28
+ @saved
29
+ end
30
+ root << object
31
+ end
32
+
33
+ def each(&block)
34
+ @saved.each(&block)
35
+ @new.each(&block)
36
+ end
37
+
38
+ def length
39
+ @saved.length + @new.length
40
+ end
41
+
42
+ alias_method :size, :length
43
+
44
+ def empty?
45
+ combined.empty?
46
+ end
47
+
48
+ def [](*args)
49
+ combined[*args]
50
+ end
51
+
52
+ def first
53
+ combined.first
54
+ end
55
+
56
+ def last
57
+ combined.last
58
+ end
59
+
60
+ def to_xml(options={})
61
+ xml = options[:builder] || Builder::XmlMarkup.new
62
+ xml.__send__(@name) do
63
+ unless @new.empty?
64
+ xml.tag!(:new) do
65
+ @new.each_with_index do |obj, index|
66
+ obj.to_xml(options.merge(:root => "n" << index.to_s))
67
+ end
68
+ end
69
+ end
70
+
71
+ unless @saved.empty?
72
+ @saved.each do |obj|
73
+ obj.to_xml(options.merge(:root => "n" << obj.id.to_s))
74
+ end
75
+ end
76
+ end
77
+ end
78
+
79
+ private
80
+ def combined
81
+ @new + @saved
82
+ end
83
+ end
84
+ end
@@ -0,0 +1,4 @@
1
+ module AdGear
2
+ class Interaction < AdGear::Base
3
+ end
4
+ end
@@ -0,0 +1,5 @@
1
+ module AdGear
2
+ class PlacementMembership < AdGear::Base
3
+ belongs_to :ad_unit
4
+ end
5
+ end
@@ -0,0 +1,5 @@
1
+ module AdGear
2
+ class PlacementRule < AdGear::Base
3
+ belongs_to :bookable, :polymorphic => true
4
+ end
5
+ end
@@ -0,0 +1,7 @@
1
+ module AdGear
2
+ class Publisher < AdGear::Base
3
+ def active?
4
+ state == "active"
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,6 @@
1
+ module AdGear
2
+ class Site < AdGear::Base
3
+ has_many :ad_spots
4
+ ignorable_attributes :embed_code
5
+ end
6
+ end
@@ -0,0 +1,4 @@
1
+ module AdGear
2
+ class Template < AdGear::Base
3
+ end
4
+ end
@@ -0,0 +1,46 @@
1
+ begin
2
+ gem "francois-rest-client", ">= 1.1.5"
3
+ rescue
4
+ # Ignored. We hope we got what we wanted
5
+ warn "Could not ensure francois-rest-client 1.1.5 is loaded. If you have problems uploading files, make sure you have that gem installed and that the AdGear::Client uses that version."
6
+ end
7
+ require "restclient"
8
+
9
+ module AdGear
10
+ class Upload < AdGear::Base
11
+ def save
12
+ retried = false
13
+ headers = Hash.new
14
+ begin
15
+ uri = self.class.site.merge(URI.parse(self.class.collection_path))
16
+ params = {"upload[uploaded_data]" => ::File.new(attributes["filename"]), "upload[filename]" => ::File.basename(attributes["filename"])}
17
+ out = RestClient.post(uri.to_s, params, headers)
18
+ logger.debug out if logger
19
+ load(connection.format.decode(out))
20
+ rescue RestClient::Unauthorized => e
21
+ # Retry once only
22
+ raise if retried
23
+
24
+ www_authenticate = e.response["www-authenticate"]
25
+ header = ActiveResource::Digest.authenticate(uri, self.class.user, self.class.password, www_authenticate, :post)
26
+ headers["Authorization"] = header
27
+
28
+ retried = true
29
+ retry
30
+ end
31
+ end
32
+
33
+ def update_attribute(*args)
34
+ raise_unsupported_operation
35
+ end
36
+
37
+ def update_attributes(*args)
38
+ raise_unsupported_operation
39
+ end
40
+
41
+ protected
42
+ def raise_unsupported_operation
43
+ raise UnsupportedOperation, "Unsupported operation: uploads are write-once only"
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,4 @@
1
+ module AdGear
2
+ class Variable < AdGear::Base
3
+ end
4
+ end
@@ -0,0 +1,6 @@
1
+ module AdGear
2
+ class WebCampaign < AdGear::Base
3
+ belongs_to :advertiser
4
+ has_many :web_placements
5
+ end
6
+ end
@@ -0,0 +1,26 @@
1
+ module AdGear
2
+ class WebPlacement < AdGear::Base
3
+ belongs_to :campaign, :format
4
+ has_many :placement_rules, :placement_memberships
5
+
6
+ def selection_mechanism
7
+ case value = attributes["selection_mechanism"]
8
+ when "R" ; "rotated"
9
+ when "W" ; "weighted"
10
+ else ; value
11
+ end
12
+ end
13
+
14
+ def selection_mechanism=(value)
15
+ case value
16
+ when /^r/i
17
+ attributes["selection_mechanism"] = "R"
18
+ when /^w/i
19
+ attributes["selection_mechanism"] = "W"
20
+ else
21
+ # Assign whatever we received, and hope for the best
22
+ attributes["selection_mechanism"] = value
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,35 @@
1
+ require 'active_support/core_ext/hash/conversions'
2
+
3
+ module AdGear
4
+ # Copied from ActiveResource's +active_resource/formats/xml_format.rb+. Changed +mime_type+ to return AdGear's expected mime type.
5
+ module XmlFormat
6
+ extend self
7
+
8
+ def extension
9
+ "agml"
10
+ end
11
+
12
+ def mime_type
13
+ "application/vnd.bloom.adgear.v1+xml"
14
+ end
15
+
16
+ def encode(hash, options={})
17
+ hash.to_xml(options)
18
+ end
19
+
20
+ def decode(xml)
21
+ from_xml_data(Hash.from_xml(xml))
22
+ end
23
+
24
+ private
25
+ # Manipulate from_xml Hash, because xml_simple is not exactly what we
26
+ # want for Active Resource.
27
+ def from_xml_data(data)
28
+ if data.is_a?(Hash) && data.keys.size == 1
29
+ data.values.first
30
+ else
31
+ data
32
+ end
33
+ end
34
+ end
35
+ end