diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..0157694 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,12 @@ +root = true + +[*] +indent_style = tab +end_of_line = lf +charset = utf-8 +trim_trailing_whitespace = true +insert_final_newline = true + +[*.{json,rb,md,yml,yaml,feature}] +indent_style = space +indent_size = 2 diff --git a/.travis.yml b/.travis.yml index 98f31ec..bb5eeee 100644 --- a/.travis.yml +++ b/.travis.yml @@ -4,4 +4,4 @@ go: - 1.2 - 1.3 - 1.4 - + - 1.5 diff --git a/README.md b/README.md index 2f685bf..712e435 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# Go OpenRTB 2.3 +# Go OpenRTB v2.x [![Build Status](https://travis-ci.org/bsm/openrtb.svg?branch=master)](https://travis-ci.org/bsm/openrtb) @@ -8,39 +8,40 @@ OpenRTB implementation for Go To install, use `go get`: - go get github.com/bsm/openrtb - -To update, use `go get -u`: - - go get -u github.com/bsm/openrtb +```shell +go get github.com/bsm/openrtb +``` Import the package: - package main +```go +package main - import ( - "github.com/bsm/openrtb" - ) +import ( + "log" + "github.com/bsm/openrtb" +) -## Example +func main() { + file, err := os.Open("stored.json") + if err != nil { + log.Fatal(err) + } + defer file.Close() - // Handle a HTTP request - http.HandleFunc("/bid", func(w http.ResponseWriter, r *http.Request) { - defer r.Body().Close() + var req *openrtb.BidRequest + err = json.NewDecoder(file).Decode(&req) + if err != nil { + log.Fatal(err) + } - req, err := openrtb.ParseRequest(r.Body(), true) - if err != nil { - log.Println("ERROR %s", err.Error()) - } else { - log.Println("INFO Received bid request %s", *req.Id) - } - - w.WriteHeader(204) // respond with 'no bid' - }) + log.Printf("%+v\n", req) +} +``` ## Licence - Copyright (c) 2014 Black Square Media Ltd. All rights reserved. + Copyright (c) 2015 Black Square Media Ltd. All rights reserved. (The MIT License) Permission is hereby granted, free of charge, to any person obtaining @@ -61,3 +62,6 @@ Import the package: CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + Some test examples were taken from: + https://code.google.com/p/openrtb/wiki/OpenRTB_Examples diff --git a/app.go b/app.go deleted file mode 100644 index 2fe05d3..0000000 --- a/app.go +++ /dev/null @@ -1,51 +0,0 @@ -package openrtb - -// An "app" object should be included if the ad supported content is part of a mobile application -// (as opposed to a mobile website). A bid request must not contain both an "app" object and a -// "site" object. -type App struct { - Id *string `json:"id,omitempty"` // App ID on the exchange - Name *string `json:"name,omitempty"` // App name - Domain *string `json:"domain,omitempty"` - Cat []string `json:"cat,omitempty"` // Array of IAB content categories - Sectioncat []string `json:"sectioncat,omitempty"` // Array of IAB content categories for subsection - Pagecat []string `json:"pagecat,omitempty"` // Array of IAB content categories for page - Ver *string `json:"ver,omitempty"` // App version - Bundle *string `json:"bundle,omitempty"` // App bundle or package name - Privacypolicy *int `json:"privacypolicy,omitempty"` // Default: 1 ("1": site has a privacy policy) - Paid *int `json:"paid,omitempty"` // "1": Paid, "2": Free - Publisher *Publisher `json:"publisher,omitempty"` - // Content Content - Keywords *string `json:"keywords,omitempty"` - Storeurl *string `json:"storeurl,omitempty"` // App store URL for an installed app - Ext Extensions `json:"ext,omitempty"` -} - -// Returns the privacy policy status, with default fallback -func (a *App) IsPrivacyPolicy() bool { - if a.Privacypolicy != nil { - return *a.Privacypolicy == 1 - } - return false -} - -// Returns the paid status, with default fallback -func (a *App) IsPaid() bool { - if a.Paid != nil { - return *a.Paid == 1 - } - return false -} - -// Applies defaults -func (a *App) WithDefaults() *App { - if a.Privacypolicy == nil { - a.Privacypolicy = new(int) - *a.Privacypolicy = 0 - } - if a.Paid == nil { - a.Paid = new(int) - *a.Paid = 0 - } - return a -} diff --git a/app_test.go b/app_test.go deleted file mode 100644 index a9a8399..0000000 --- a/app_test.go +++ /dev/null @@ -1,25 +0,0 @@ -package openrtb - -import ( - . "github.com/onsi/ginkgo" - . "github.com/onsi/gomega" -) - -var _ = Describe("App", func() { - var subject *App - - BeforeEach(func() { - subject = new(App) - }) - - It("should have accessors", func() { - Expect(subject.IsPrivacyPolicy()).To(BeFalse()) - Expect(subject.IsPaid()).To(BeFalse()) - }) - - It("should have defaults", func() { - subject.WithDefaults() - Expect(*subject.Privacypolicy).To(Equal(0)) - Expect(*subject.Paid).To(Equal(0)) - }) -}) diff --git a/asset.go b/asset.go deleted file mode 100644 index 6f55548..0000000 --- a/asset.go +++ /dev/null @@ -1,44 +0,0 @@ -package openrtb - -// TODO: Add description, use cases and other details -type Asset struct { - Id *int `json:"id"` // Unique asset ID, assigned by exchange - Required *int `json:"required,omitempty"` // Set to 1 if asset is required - Title *Title `json:"title,omitempty"` // Title object for title assets - Img *Image `json:"img,omitempty"` // Image object for image assets - Video *NativeVideo `json:"video,omitempty"` // Video object for video assets - Data *NativeData `json:"data,omitempty"` // Data object for ratings, price, etc. - Ext Extensions `json:"ext,omitempty"` -} - -type Title struct { - Len *int `json:"len"` // Maximum length of the text in the title element - Ext Extensions `json:"ext,omitempty"` -} - -type Image struct { - Type *int `json:"type,omitempty"` // Type ID of the image element supported by the publisher - W *int `json:"w,omitempty"` // Width - H *int `json:"h,omitempty"` // Height - Wmax *int `json:"wmax,omitempty"` // Width maximum - Hmax *int `json:"hmax,omitempty"` // Height maximum - Wmin *int `json:"wmin,omitempty"` // Width minimum - Hmin *int `json:"hmin,omitempty"` // Height minimum - Mimes []string `json:"mimes,omitempty"` // Whitelist of content MIME types supported - Ext Extensions `json:"ext,omitempty"` -} - -type NativeData struct { - Type *int `json:"type"` // Type ID of the image element supported by the publisher - Len *int `json:"len,omitempty"` // Maximum length of the text in the element’s response - Ext Extensions `json:"ext,omitempty"` -} - -// Applies defaults -func (a *Asset) WithDefaults() *Asset { - if a.Required == nil { - a.Required = new(int) - *a.Required = 0 - } - return a -} diff --git a/asset_test.go b/asset_test.go deleted file mode 100644 index 6dc795b..0000000 --- a/asset_test.go +++ /dev/null @@ -1,19 +0,0 @@ -package openrtb - -import ( - . "github.com/onsi/ginkgo" - . "github.com/onsi/gomega" -) - -var _ = Describe("Asset", func() { - var subject *Asset - - BeforeEach(func() { - subject = new(Asset) - }) - - It("should have defaults", func() { - subject.WithDefaults() - Expect(*subject.Required).To(Equal(0)) - }) -}) diff --git a/banner.go b/banner.go index 644cf73..7e679d1 100644 --- a/banner.go +++ b/banner.go @@ -1,5 +1,7 @@ package openrtb +import "encoding/json" + // The "banner" object must be included directly in the impression object if the impression offered // for auction is display or rich media, or it may be optionally embedded in the video object to // describe the companion banners available for the linear or non-linear video ad. The banner @@ -7,48 +9,19 @@ package openrtb // VAST response to dictate placement of the companion creatives when multiple companion ad // opportunities of the same size are available on a page. type Banner struct { - W *int `json:"w,omitempty"` // Width - H *int `json:"h,omitempty"` // Height - Wmax *int `json:"wmax,omitempty"` // Width maximum - Hmax *int `json:"hmax,omitempty"` // Height maximum - Wmin *int `json:"wmin,omitempty"` // Width minimum - Hmin *int `json:"hmin,omitempty"` // Height minimum - Id *string `json:"id,omitempty"` // A unique identifier - Pos *int `json:"pos,omitempty"` // Ad Position - Btype []int `json:"btype,omitempty"` // Blocked creative types - Battr []int `json:"battr,omitempty"` // Blocked creative attributes - Mimes []string `json:"mimes,omitempty"` // Whitelist of content MIME types supported - Topframe *int `json:"topframe,omitempty"` // Default: 0 ("1": Delivered in top frame, "0": Elsewhere) - Expdir []int `json:"expdir,omitempty"` // Specify properties for an expandable ad - Api []int `json:"api,omitempty"` // List of supported API frameworks - Ext Extensions `json:"ext,omitempty"` -} - -// Returns topframe status, with default fallback -func (b *Banner) IsTopFrame() bool { - if b.Topframe != nil { - return *b.Topframe == 1 - } - return false -} - -// Returns the position, with default fallback -func (b *Banner) Position() int { - if b.Pos != nil { - return *b.Pos - } - return AD_POS_UNKNOWN -} - -// Applies defaults -func (b *Banner) WithDefaults() *Banner { - if b.Topframe == nil { - b.Topframe = new(int) - *b.Topframe = 0 - } - if b.Pos == nil { - b.Pos = new(int) - *b.Pos = AD_POS_UNKNOWN - } - return b + W int `json:"w,omitempty"` // Width + H int `json:"h,omitempty"` // Height + WMax int `json:"wmax,omitempty"` // Width maximum + HMax int `json:"hmax,omitempty"` // Height maximum + WMin int `json:"wmin,omitempty"` // Width minimum + HMin int `json:"hmin,omitempty"` // Height minimum + ID string `json:"id,omitempty"` // A unique identifier + Pos int `json:"pos,omitempty"` // Ad Position + BType []int `json:"btype,omitempty"` // Blocked creative types + BAttr []int `json:"battr,omitempty"` // Blocked creative attributes + Mimes []string `json:"mimes,omitempty"` // Whitelist of content MIME types supported + TopFrame int `json:"topframe,omitempty"` // Default: 0 ("1": Delivered in top frame, "0": Elsewhere) + ExpDir []int `json:"expdir,omitempty"` // Specify properties for an expandable ad + Api []int `json:"api,omitempty"` // List of supported API frameworks + Ext json.RawMessage `json:"ext,omitempty"` } diff --git a/banner_test.go b/banner_test.go index 66a68b0..906b8c6 100644 --- a/banner_test.go +++ b/banner_test.go @@ -9,17 +9,19 @@ var _ = Describe("Banner", func() { var subject *Banner BeforeEach(func() { - subject = new(Banner) + err := fixture("banner", &subject) + Expect(err).NotTo(HaveOccurred()) }) - It("should have accessors", func() { - Expect(subject.IsTopFrame()).To(BeFalse()) - Expect(subject.Position()).To(Equal(0)) + It("should parse correctly", func() { + Expect(subject).To(Equal(&Banner{ + W: 728, + H: 90, + Pos: AdPosAboveFold, + BType: []int{4}, + BAttr: []int{14}, + Api: []int{3}, + })) }) - It("should have defaults", func() { - subject.WithDefaults() - Expect(*subject.Topframe).To(Equal(0)) - Expect(*subject.Pos).To(Equal(AD_POS_UNKNOWN)) - }) }) diff --git a/bid.go b/bid.go index c9a2134..543489a 100644 --- a/bid.go +++ b/bid.go @@ -1,10 +1,17 @@ package openrtb import ( + "encoding/json" "errors" ) -// ID, Impid and Price are required; all other optional. +// Validation errors +var ( + ErrInvalidBidNoID = errors.New("openrtb: bid is missing ID") + ErrInvalidBidNoImpID = errors.New("openrtb: bid is missing impression ID") +) + +// ID, ImpID and Price are required; all other optional. // If the bidder wins the impression, the exchange calls notice URL (nurl) // a) to inform the bidder of the win; // b) to convey certain information using substitution macros. @@ -12,68 +19,31 @@ import ( // Cid can be used to block ads that were previously identified as inappropriate. // Substitution macros may allow a bidder to use a static notice URL for all of its bids. type Bid struct { - Id *string `json:"id"` - Impid *string `json:"impid"` // Required string ID of the impression object to which this bid applies. - Price *float32 `json:"price"` // Bid price in CPM. Suggests using integer math for accounting to avoid rounding errors. - Adid *string `json:"adid,omitempty"` // References the ad to be served if the bid wins. - Nurl *string `json:"nurl,omitempty"` // Win notice URL. - Adm *string `json:"adm,omitempty"` // Actual ad markup. XHTML if a response to a banner object, or VAST XML if a response to a video object. - Adomain []string `json:"adomain,omitempty"` // Advertiser’s primary or top-level domain for advertiser checking; or multiple if imp rotating. - Iurl *string `json:"iurl,omitempty"` // Sample image URL. - Cid *string `json:"cid,omitempty"` // Campaign ID that appears with the Ad markup. - Crid *string `json:"crid,omitempty"` // Creative ID for reporting content issues or defects. This could also be used as a reference to a creative ID that is posted with an exchange. - Cat []string `json:"cat,omitempty"` // IAB content categories of the creative. Refer to List 5.1 - Attr []int `json:"attr,omitempty"` // Array of creative attributes. - DealId *string `json:"deal_id,omitempty"` // DealID extension of private marketplace deals - H *int `json:"h,omitempty"` // Height of the ad in pixels. - W *int `json:"w,omitempty"` // Width of the ad in pixels. - Ext Extensions `json:"ext,omitempty"` -} - -// Validation errors -var ( - ErrInvalidBidID = errors.New("openrtb response: bid is missing ID") - ErrInvalidBidImpID = errors.New("openrtb response: bid is missing impression ID") - ErrInvalidBidPrice = errors.New("openrtb response: bid is missing price") -) - -// Validate Bid required attributes -func (bid *Bid) Valid() (bool, error) { - - if bid.Id == nil { - return false, ErrInvalidBidID - } else if bid.Impid == nil { - return false, ErrInvalidBidImpID - } else if bid.Price == nil { - return false, ErrInvalidBidPrice - } - - return true, nil + ID string `json:"id"` + ImpID string `json:"impid"` // Required string ID of the impression object to which this bid applies. + Price float64 `json:"price"` // Bid price in CPM. Suggests using integer math for accounting to avoid rounding errors. + AdID string `json:"adid,omitempty"` // References the ad to be served if the bid wins. + NURL string `json:"nurl,omitempty"` // Win notice URL. + AdMarkup string `json:"adm,omitempty"` // Actual ad markup. XHTML if a response to a banner object, or VAST XML if a response to a video object. + AdvDomain []string `json:"adomain,omitempty"` // Advertiser’s primary or top-level domain for advertiser checking; or multiple if imp rotating. + IURL string `json:"iurl,omitempty"` // Sample image URL. + CampaignID string `json:"cid,omitempty"` // Campaign ID that appears with the Ad markup. + CreativeID string `json:"crid,omitempty"` // Creative ID for reporting content issues or defects. This could also be used as a reference to a creative ID that is posted with an exchange. + Cat []string `json:"cat,omitempty"` // IAB content categories of the creative. Refer to List 5.1 + Attr []int `json:"attr,omitempty"` // Array of creative attributes. + DealID string `json:"deal_id,omitempty"` // DealID extension of private marketplace deals + H int `json:"h,omitempty"` // Height of the ad in pixels. + W int `json:"w,omitempty"` // Width of the ad in pixels. + Ext json.RawMessage `json:"ext,omitempty"` } -// Set the ID -func (bid *Bid) SetID(id string) *Bid { - if bid.Id == nil { - bid.Id = new(string) +// Validate required attributes +func (bid *Bid) Validate() error { + if bid.ID == "" { + return ErrInvalidBidNoID + } else if bid.ImpID == "" { + return ErrInvalidBidNoImpID } - *bid.Id = id - return bid -} -// Set the ImpID -func (bid *Bid) SetImpID(id string) *Bid { - if bid.Impid == nil { - bid.Impid = new(string) - } - *bid.Impid = id - return bid -} - -// Set the Price -func (bid *Bid) SetPrice(p float32) *Bid { - if bid.Price == nil { - bid.Price = new(float32) - } - *bid.Price = p - return bid + return nil } diff --git a/bid_test.go b/bid_test.go index 4173baa..f330d72 100644 --- a/bid_test.go +++ b/bid_test.go @@ -9,21 +9,30 @@ var _ = Describe("Bid", func() { var subject *Bid BeforeEach(func() { - subject = new(Bid) + err := fixture("bid", &subject) + Expect(err).NotTo(HaveOccurred()) }) - It("should have validation", func() { - ok, err := subject.Valid() - Expect(err).To(HaveOccurred()) - Expect(ok).To(BeFalse()) - - subject.SetID("BIDID") - subject.SetImpID("IMPID") - subject.SetPrice(0.0) + It("should parse correctly", func() { + Expect(subject).To(Equal(&Bid{ + ID: "1", + ImpID: "1", + Price: 0.751371, + AdID: "52a5516d29e435137c6f6e74", + NURL: "http://ads.com/win/112770_1386565997?won=${AUCTION_PRICE}", + AdMarkup: "
", + AdvDomain: []string{"ads.com"}, + IURL: "http://ads.com/112770_1386565997.jpeg", + CampaignID: "52a5516d29e435137c6f6e74", + CreativeID: "52a5516d29e435137c6f6e74_1386565997", + Attr: []int{}, + })) + }) - ok, err = subject.Valid() - Expect(err).NotTo(HaveOccurred()) - Expect(ok).To(BeTrue()) + It("should validate", func() { + Expect((&Bid{}).Validate()).To(Equal(ErrInvalidBidNoID)) + Expect((&Bid{ID: "BIDID"}).Validate()).To(Equal(ErrInvalidBidNoImpID)) + Expect(subject.Validate()).NotTo(HaveOccurred()) }) }) diff --git a/bidrequest.go b/bidrequest.go new file mode 100644 index 0000000..e1a74ab --- /dev/null +++ b/bidrequest.go @@ -0,0 +1,55 @@ +package openrtb + +import ( + "encoding/json" + "errors" +) + +// Validation errors +var ( + ErrInvalidReqNoID = errors.New("openrtb: request ID missing") + ErrInvalidReqNoImps = errors.New("openrtb: request has no impressions") + ErrInvalidReqMultiInv = errors.New("openrtb: request has multiple inventory sources") // has site and app +) + +// The top-level bid request object contains a globally unique bid request or auction ID. This "id" +// attribute is required as is at least one "imp" (i.e., impression) object. Other attributes are +// optional since an exchange may establish default values. +type BidRequest struct { + ID string `json:"id"` // Unique ID of the bid request + Imp []Impression `json:"imp,omitempty"` + Site *Site `json:"site,omitempty"` + App *App `json:"app,omitempty"` + Device *Device `json:"device,omitempty"` + User *User `json:"user,omitempty"` + AuctionType int `json:"at"` // Auction type, where 1 = First Price, 2 = Second Price Plus. Exchange-specific auction types can be defined using values greater than 500. + TMax int `json:"tmax,omitempty"` // Maximum amount of time in milliseconds to submit a bid + WSeat []string `json:"wseat,omitempty"` // Array of buyer seats allowed to bid on this auction + AllImps int `json:"allimps,omitempty"` // Flag to indicate whether exchange can verify that all impressions offered represent all of the impressions available in context, Default: 0 + Cur []string `json:"cur,omitempty"` // Array of allowed currencies + Bcat []string `json:"bcat,omitempty"` // Blocked Advertiser Categories. + BAdv []string `json:"badv,omitempty"` // Array of strings of blocked toplevel domains of advertisers + Regs *Regulations `json:"regs,omitempty"` + Ext json.RawMessage `json:"ext,omitempty"` + + Pmp *Pmp `json:"pmp,omitempty"` // DEPRECATED: kept for backwards compatibility +} + +// Validates the request +func (req *BidRequest) Validate() error { + if req.ID == "" { + return ErrInvalidReqNoID + } else if len(req.Imp) == 0 { + return ErrInvalidReqNoImps + } else if req.Site != nil && req.App != nil { + return ErrInvalidReqMultiInv + } + + for _, imp := range req.Imp { + if err := (&imp).Validate(); err != nil { + return err + } + } + + return nil +} diff --git a/bidrequest_test.go b/bidrequest_test.go new file mode 100644 index 0000000..10da7eb --- /dev/null +++ b/bidrequest_test.go @@ -0,0 +1,76 @@ +package openrtb + +import ( + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +var _ = Describe("BidRequest", func() { + var subject *BidRequest + + BeforeEach(func() { + err := fixture("breq.banner", &subject) + Expect(err).NotTo(HaveOccurred()) + }) + + It("should parse complex requests", func() { + for _, kind := range []string{"exp", "video", "native"} { + var req *BidRequest + err := fixture("breq."+kind, &req) + Expect(err).NotTo(HaveOccurred(), "for %s", kind) + Expect(req.Validate()).NotTo(HaveOccurred(), "for %s", kind) + } + }) + + It("should parse correctly", func() { + Expect(subject).To(Equal(&BidRequest{ + ID: "1234534625254", + Imp: []Impression{ + { + ID: "1", + Banner: &Banner{W: 300, H: 250, Pos: AdPosAboveFold, BAttr: []int{13}}, + }, + }, + Site: &Site{ + Inventory: Inventory{ + ID: "234563", + Name: "Site ABCD", + Domain: "siteabcd.com", + Cat: []string{"IAB2-1", "IAB2-2"}, + Publisher: &Publisher{ID: "pub12345", Name: "Publisher A"}, + Content: &Content{ + Keywords: "keyword a,keyword b,keyword c", + }, + }, + Page: "http://siteabcd.com/page.htm", + Ref: "http://referringsite.com/referringpage.htm", + }, + Device: &Device{ + UA: "Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10.6; en-US; rv:1.9.2.16) Gecko/20110319 Firefox/3.6.16", + IP: "64.124.253.1", + OS: "OS X", + JS: 1, + FlashVer: "10.1", + }, + User: &User{ + ID: "45asdf987656789adfad4678rew656789", + }, + AuctionType: 2, + TMax: 120, + BAdv: []string{"company1.com", "company2.com"}, + })) + }) + + It("should validate", func() { + Expect((&BidRequest{}).Validate()).To(Equal(ErrInvalidReqNoID)) + Expect((&BidRequest{ID: "A"}).Validate()).To(Equal(ErrInvalidReqNoImps)) + Expect((&BidRequest{ID: "A", Imp: []Impression{{ID: "1"}}, Site: &Site{}, App: &App{}}).Validate()).To(Equal(ErrInvalidReqMultiInv)) + Expect((&BidRequest{ID: "A", Imp: []Impression{{ID: "1"}}}).Validate()).To(Equal(ErrInvalidImpNoAssets)) + + Expect((&BidRequest{ID: "A", Imp: []Impression{{ID: "1", Banner: &Banner{}}}}).Validate()).NotTo(HaveOccurred()) + Expect((&BidRequest{ID: "A", Imp: []Impression{{ID: "1", Banner: &Banner{}}}, Site: &Site{}}).Validate()).NotTo(HaveOccurred()) + Expect((&BidRequest{ID: "A", Imp: []Impression{{ID: "1", Banner: &Banner{}}}, App: &App{}}).Validate()).NotTo(HaveOccurred()) + Expect(subject.Validate()).NotTo(HaveOccurred()) + }) + +}) diff --git a/bidresponse.go b/bidresponse.go new file mode 100644 index 0000000..31d07af --- /dev/null +++ b/bidresponse.go @@ -0,0 +1,43 @@ +package openrtb + +import ( + "encoding/json" + "errors" +) + +// Validation errors +var ( + ErrInvalidRespNoID = errors.New("openrtb: response missing ID") + ErrInvalidRespNoSeatBids = errors.New("openrtb: response missing seatbids") +) + +// ID and at least one "seatbid” object is required, which contains a bid on at least one impression. +// Other attributes are optional since an exchange may establish default values. +// No-Bids on all impressions should be indicated as a HTTP 204 response. +// For no-bids on specific impressions, the bidder should omit these from the bid response. +type BidResponse struct { + ID string `json:"id"` // Reflection of the bid request ID for logging purposes + SeatBid []SeatBid `json:"seatbid"` // Array of seatbid objects + BidID string `json:"bidid,omitempty"` // Optional response tracking ID for bidders + Currency string `json:"cur,omitempty"` // Bid currency + CustomData string `json:"customdata,omitempty"` // Encoded user features + NBR int `json:"nbr,omitempty"` // Reason for not bidding, where 0 = unknown error, 1 = technical error, 2 = invalid request, 3 = known web spider, 4 = suspected Non-Human Traffic, 5 = cloud, data center, or proxy IP, 6 = unsupported device, 7 = blocked publisher or site, 8 = unmatched user + Ext json.RawMessage `json:"ext,omitempty"` // Custom specifications in JSon +} + +// Validate required attributes +func (res *BidResponse) Validate() error { + if res.ID == "" { + return ErrInvalidRespNoID + } else if len(res.SeatBid) == 0 { + return ErrInvalidRespNoSeatBids + } + + for _, sb := range res.SeatBid { + if err := sb.Validate(); err != nil { + return err + } + } + + return nil +} diff --git a/bidresponse_test.go b/bidresponse_test.go new file mode 100644 index 0000000..8f81f17 --- /dev/null +++ b/bidresponse_test.go @@ -0,0 +1,57 @@ +package openrtb + +import ( + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +var _ = Describe("BidResponse", func() { + var subject *BidResponse + + BeforeEach(func() { + err := fixture("bres.single", &subject) + Expect(err).NotTo(HaveOccurred()) + }) + + It("should parse complex responses", func() { + for _, kind := range []string{"multi", "pmp", "vast"} { + var req *BidResponse + err := fixture("bres."+kind, &req) + Expect(err).NotTo(HaveOccurred(), "for %s", kind) + Expect(req.Validate()).NotTo(HaveOccurred(), "for %s", kind) + } + }) + + It("should parse responses", func() { + Expect(subject).To(Equal(&BidResponse{ + ID: "BID-4-ZIMP-4b309eae-504a-4252-a8a8-4c8ceee9791a", + SeatBid: []SeatBid{ + { + Bid: []Bid{ + { + ID: "32a69c6ba388f110487f9d1e63f77b22d86e916b", + ImpID: "32a69c6ba388f110487f9d1e63f77b22d86e916b", + Price: 0.065445, + AdID: "529833ce55314b19e8796116", + NURL: "http://ads.com/win/529833ce55314b19e8796116?won=${auction_price}", + AdMarkup: "