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..62da91f 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,7 +1,7 @@ language: go -install: make test +sudo: false go: - - 1.2 - - 1.3 - - 1.4 - + - 1.6.4 + - 1.7.4 +install: + - go get -u -t ./... diff --git a/Makefile b/Makefile index 2583a86..7e42be8 100644 --- a/Makefile +++ b/Makefile @@ -1,7 +1,10 @@ -default: test +default: vet test -deps: - go get -t ./... +test: + go test -a ./... -test: deps - go test ./... +bench: + go test ./... -bench=. -run=NONE + +vet: + go vet ./... diff --git a/README.md b/README.md index c781bb5..04d30a2 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# Go OpenRTB 2.2 +# Go OpenRTB v2.x [![Build Status](https://travis-ci.org/bsm/openrtb.svg?branch=master)](https://travis-ci.org/bsm/openrtb) @@ -8,39 +8,42 @@ OpenRTB implementation for Go To install, use `go get`: - go get github.com/bsm/openrtb +```shell +go get github.com/bsm/openrtb +``` -To update, use `go get -u`: - - go get -u github.com/bsm/openrtb +## Usage Import the package: - package main - - import ( - "github.com/bsm/openrtb" - ) +```go +package main -## Example +import ( + "log" + "github.com/bsm/openrtb" +) - // Handle a HTTP request - http.HandleFunc("/bid", func(w http.ResponseWriter, r *http.Request) { - defer r.Body().Close() +func main() { + file, err := os.Open("stored.json") + if err != nil { + log.Fatal(err) + } + defer file.Close() - 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) - } + var req *openrtb.BidRequest + err = json.NewDecoder(file).Decode(&req) + if err != nil { + log.Fatal(err) + } - 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 +64,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/audio.go b/audio.go new file mode 100644 index 0000000..5dd8a03 --- /dev/null +++ b/audio.go @@ -0,0 +1,68 @@ +package openrtb + +import ( + "encoding/json" + "errors" +) + +// Validation errors +var ( + ErrInvalidAudioNoMimes = errors.New("openrtb: audio has no mimes") +) + +// The "audio" object must be included directly in the impression object +type Audio struct { + Mimes []string `json:"mimes"` // Content MIME types supported. + MinDuration int `json:"minduration,omitempty"` // Minimum video ad duration in seconds + MaxDuration int `json:"maxduration,omitempty"` // Maximum video ad duration in seconds + Protocols []int `json:"protocols,omitempty"` // Video bid response protocols + StartDelay int `json:"startdelay,omitempty"` // Indicates the start delay in seconds + Sequence int `json:"sequence,omitempty"` // Default: 1 + BAttr []int `json:"battr,omitempty"` // Blocked creative attributes + MaxExtended int `json:"maxextended,omitempty"` // Maximum extended video ad duration + MinBitrate int `json:"minbitrate,omitempty"` // Minimum bit rate in Kbps + MaxBitrate int `json:"maxbitrate,omitempty"` // Maximum bit rate in Kbps + Delivery []int `json:"delivery,omitempty"` // List of supported delivery methods + CompanionAd []Banner `json:"companionad,omitempty"` + API []int `json:"api,omitempty"` + CompanionType []int `json:"companiontype,omitempty"` + MaxSequence int `json:"maxseq,omitempty"` // The maximumnumber of ads that canbe played in an ad pod. + Feed int `json:"feed,omitempty"` // Type of audio feed. + Stitched int `json:"stitched,omitempty"` // Indicates if the ad is stitched with audio content or delivered independently + NVol int `json:"nvol,omitempty"` // Volume normalization mode. + Ext Extension `json:"ext,omitempty"` +} + +type jsonAudio Audio + +// Validates the object +func (a *Audio) Validate() error { + if len(a.Mimes) == 0 { + return ErrInvalidAudioNoMimes + } + return nil +} + +// MarshalJSON custom marshalling with normalization +func (a *Audio) MarshalJSON() ([]byte, error) { + a.normalize() + return json.Marshal((*jsonAudio)(a)) +} + +// UnmarshalJSON custom unmarshalling with normalization +func (a *Audio) UnmarshalJSON(data []byte) error { + var h jsonAudio + if err := json.Unmarshal(data, &h); err != nil { + return err + } + + *a = (Audio)(h) + a.normalize() + return nil +} + +func (a *Audio) normalize() { + if a.Sequence == 0 { + a.Sequence = 1 + } +} diff --git a/audio_test.go b/audio_test.go new file mode 100644 index 0000000..fe09cea --- /dev/null +++ b/audio_test.go @@ -0,0 +1,58 @@ +package openrtb + +import ( + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +var _ = Describe("Audio", func() { + var subject *Audio + + BeforeEach(func() { + err := fixture("audio", &subject) + Expect(err).NotTo(HaveOccurred()) + }) + + It("should parse correctly", func() { + Expect(subject).To(Equal(&Audio{ + Mimes: []string{ + "audio/mp4", + }, + MinDuration: 5, + MaxDuration: 30, + Protocols: []int{AudioProtocolDAAST1, AudioProtocolDAAST1Wrapper}, + Sequence: 1, + BAttr: []int{13, 14}, + MaxExtended: 30, + MinBitrate: 300, + MaxBitrate: 1500, + Delivery: []int{2}, + CompanionAd: []Banner{ + {W: 300, H: 250, ID: "1234567893-1", Pos: 1, BAttr: []int{13, 14}, ExpDir: []int{ExpDirRight, ExpDirDown}}, + {W: 728, H: 90, ID: "1234567893-2", Pos: 1, BAttr: []int{13, 14}}, + }, + API: []int{1, 2}, + CompanionType: []int{1, 2}, + })) + }) + + It("should validate", func() { + Expect((&Audio{ + MinDuration: 5, + MaxDuration: 30, + Protocols: []int{AudioProtocolDAAST1, AudioProtocolDAAST1Wrapper}, + Sequence: 1, + BAttr: []int{13, 14}, + MaxExtended: 30, + MinBitrate: 300, + MaxBitrate: 1500, + Delivery: []int{2}, + CompanionAd: []Banner{ + {W: 300, H: 250, ID: "1234567893-1", Pos: 1, BAttr: []int{13, 14}, ExpDir: []int{ExpDirRight, ExpDirDown}}, + {W: 728, H: 90, ID: "1234567893-2", Pos: 1, BAttr: []int{13, 14}}, + }, + CompanionType: []int{1, 2}, + }).Validate()).To(Equal(ErrInvalidAudioNoMimes)) + }) + +}) diff --git a/banner.go b/banner.go index 644cf73..e4b2a03 100644 --- a/banner.go +++ b/banner.go @@ -7,48 +7,20 @@ 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 + Format []Format `json:"format,omitempty"` //Array of format objects representing the banner sizes permitted. + WMax int `json:"wmax,omitempty"` // Width maximum DEPRECATED + HMax int `json:"hmax,omitempty"` // Height maximum DEPRECATED + WMin int `json:"wmin,omitempty"` // Width minimum DEPRECATED + HMin int `json:"hmin,omitempty"` // Height minimum DEPRECATED + ID string `json:"id,omitempty"` // A unique identifier + BType []int `json:"btype,omitempty"` // Blocked creative types + BAttr []int `json:"battr,omitempty"` // Blocked creative attributes + Pos int `json:"pos,omitempty"` // Ad Position + 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 Extension `json:"ext,omitempty"` } diff --git a/banner_test.go b/banner_test.go index 66a68b0..a8f8787 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{BannerTypeFrame}, + BAttr: []int{CreativeAttributeWindowsDialogOrAlert}, + Api: []int{APIFrameworkMRAID1}, + })) }) - It("should have defaults", func() { - subject.WithDefaults() - Expect(*subject.Topframe).To(Equal(0)) - Expect(*subject.Pos).To(Equal(AD_POS_UNKNOWN)) - }) }) diff --git a/bench_test.go b/bench_test.go new file mode 100644 index 0000000..c90b2dd --- /dev/null +++ b/bench_test.go @@ -0,0 +1,42 @@ +package openrtb + +import ( + "encoding/json" + "io/ioutil" + "path/filepath" + "testing" +) + +func BenchmarkBidRequest_Unmarshal(b *testing.B) { + data, err := ioutil.ReadFile(filepath.Join("testdata", "breq.video.json")) + if err != nil { + b.Fatal(err.Error()) + } + + b.ResetTimer() + for i := 0; i < b.N; i++ { + var req *BidRequest + if err := json.Unmarshal(data, &req); err != nil { + b.Fatal(err.Error()) + } + } +} + +func BenchmarkBidRequest_Marshal(b *testing.B) { + data, err := ioutil.ReadFile(filepath.Join("testdata", "breq.video.json")) + if err != nil { + b.Fatal(err.Error()) + } + + var req *BidRequest + if err := json.Unmarshal(data, &req); err != nil { + b.Fatal(err.Error()) + } + + b.ResetTimer() + for i := 0; i < b.N; i++ { + if _, err := json.Marshal(req); err != nil { + b.Fatal(err.Error()) + } + } +} diff --git a/bid.go b/bid.go index b21c007..e9a7226 100644 --- a/bid.go +++ b/bid.go @@ -1,10 +1,14 @@ package openrtb -import ( - "errors" +import "errors" + +// 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. +// 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,65 +16,36 @@ 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. - Attr []int `json:"attr,omitempty"` // Array of creative attributes. - DealId *string `json:"deal_id,omitempty"` // DealID extension of private marketplace deals - Ext Extensions `json:"ext,omitempty"` + 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. + Bundle string `json:"bundle,omitempty"` // A platform-specific application identifier intended to be unique to the app and independent of the exchange. + 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. + API int `json:"api,omitempty"` // API required by the markup if applicable + Protocol int `json:"protocol,omitempty"` // Video response protocol of the markup if applicable + QAGMediaRating int `json:"qagmediarating,omitempty"` // Creative media rating per IQG guidelines. + DealID string `json:"dealid,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. + Exp int `json:"exp,omitempty"` // Advisory as to the number of seconds the bidder is willing to wait between the auction and the actual impression. + Ext Extension `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 +// Validate required attributes +func (bid *Bid) Validate() error { + if bid.ID == "" { + return ErrInvalidBidNoID + } else if bid.ImpID == "" { + return ErrInvalidBidNoImpID } - return true, nil -} - -// Set the ID -func (bid *Bid) SetID(id string) *Bid { - if bid.Id == nil { - bid.Id = new(string) - } - *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..369d2bd 100644 --- a/bid_test.go +++ b/bid_test.go @@ -9,21 +9,31 @@ 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", + DealID: "example_deal", + 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..46e1c8b --- /dev/null +++ b/bidrequest.go @@ -0,0 +1,54 @@ +package openrtb + +import "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"` + Test int `json:"test,omitempty"` // Indicator of test mode in which auctions are not billable, where 0 = live mode, 1 = test mode + 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 + BApp []string `json:"bapp,omitempty"` // Block list of applications by their platform-specific exchange-independent application identifiers. On Android, these should be bundle or package names (e.g., com.foo.mygame). On iOS, these are numeric IDs. + Regs *Regulations `json:"regs,omitempty"` + Ext Extension `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..fae9988 --- /dev/null +++ b/bidrequest_test.go @@ -0,0 +1,79 @@ +package openrtb + +import ( + "encoding/json" + . "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", + Secure: json.Number("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", + BuyerUID: "5df678asd8987656asdf78987654", + }, + Test: 1, + 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", 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..a759e2a --- /dev/null +++ b/bidresponse.go @@ -0,0 +1,40 @@ +package openrtb + +import "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 Extension `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: "