ad_gear_client 0.3.8

Sign up to get free protection for your applications and to get access to all the features.
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