Skip to content

Commit

Permalink
Add Adoppler bidder support. (#1186)
Browse files Browse the repository at this point in the history
* Add Adoppler bidder support.

* Address code review comments. Use JSON-templates for testing.

* Fix misprint; Add url.PathEscape call for adunit URL parameter.
  • Loading branch information
vchimishuk committed Feb 21, 2020
1 parent 7762c0c commit 8e382e7
Show file tree
Hide file tree
Showing 18 changed files with 485 additions and 0 deletions.
210 changes: 210 additions & 0 deletions adapters/adoppler/adoppler.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,210 @@
package adoppler

import (
"encoding/json"
"errors"
"fmt"
"net/http"
"net/url"

"github.com/mxmCherry/openrtb"
"github.com/prebid/prebid-server/adapters"
"github.com/prebid/prebid-server/errortypes"
"github.com/prebid/prebid-server/openrtb_ext"
)

var bidHeaders http.Header = map[string][]string{
"Accept": {"application/json"},
"Content-Type": {"application/json;charset=utf-8"},
"X-OpenRTB-Version": {"2.5"},
}

type adsVideoExt struct {
Duration int `json:"duration"`
}

type adsImpExt struct {
Video *adsVideoExt `json:"video"`
}

type AdopplerAdapter struct {
endpoint string
}

func NewAdopplerBidder(endpoint string) *AdopplerAdapter {
return &AdopplerAdapter{endpoint}
}

func (ads *AdopplerAdapter) MakeRequests(
req *openrtb.BidRequest,
info *adapters.ExtraRequestInfo,
) (
[]*adapters.RequestData,
[]error,
) {
if len(req.Imp) == 0 {
return nil, nil
}

var datas []*adapters.RequestData
var errs []error
for _, imp := range req.Imp {
ext, err := unmarshalExt(imp.Ext)
if err != nil {
errs = append(errs, &errortypes.BadInput{err.Error()})
continue
}

var r openrtb.BidRequest = *req
r.ID = req.ID + "-" + ext.AdUnit
r.Imp = []openrtb.Imp{imp}

body, err := json.Marshal(r)
if err != nil {
errs = append(errs, err)
continue
}

uri := fmt.Sprintf("%s/processHeaderBid/%s",
ads.endpoint, url.PathEscape(ext.AdUnit))
data := &adapters.RequestData{
Method: "POST",
Uri: uri,
Body: body,
Headers: bidHeaders,
}
datas = append(datas, data)
}

return datas, errs
}

func (ads *AdopplerAdapter) MakeBids(
intReq *openrtb.BidRequest,
extReq *adapters.RequestData,
resp *adapters.ResponseData,
) (
*adapters.BidderResponse,
[]error,
) {
if resp.StatusCode == http.StatusNoContent {
return nil, nil
}
if resp.StatusCode == http.StatusBadRequest {
return nil, []error{&errortypes.BadInput{"bad request"}}
}
if resp.StatusCode != http.StatusOK {
err := &errortypes.BadServerResponse{
fmt.Sprintf("unexpected status: %d", resp.StatusCode),
}
return nil, []error{err}
}

var bidResp openrtb.BidResponse
err := json.Unmarshal(resp.Body, &bidResp)
if err != nil {
err := &errortypes.BadServerResponse{
fmt.Sprintf("invalid body: %s", err.Error()),
}
return nil, []error{err}
}

impTypes := make(map[string]openrtb_ext.BidType)
for _, imp := range intReq.Imp {
if _, ok := impTypes[imp.ID]; ok {
return nil, []error{&errortypes.BadInput{
fmt.Sprintf("duplicate $.imp.id %s", imp.ID),
}}
}
if imp.Banner != nil {
impTypes[imp.ID] = openrtb_ext.BidTypeBanner
} else if imp.Video != nil {
impTypes[imp.ID] = openrtb_ext.BidTypeVideo
} else if imp.Audio != nil {
impTypes[imp.ID] = openrtb_ext.BidTypeAudio
} else if imp.Native != nil {
impTypes[imp.ID] = openrtb_ext.BidTypeNative
} else {
return nil, []error{&errortypes.BadInput{
"one of $.imp.banner, $.imp.video, $.imp.audio and $.imp.native field required",
}}
}
}

var bids []*adapters.TypedBid
for _, seatBid := range bidResp.SeatBid {
for _, bid := range seatBid.Bid {
tp, ok := impTypes[bid.ImpID]
if !ok {
err := &errortypes.BadServerResponse{
fmt.Sprintf("unknown impid: %s", bid.ImpID),
}
return nil, []error{err}
}

var bidVideo *openrtb_ext.ExtBidPrebidVideo
if tp == openrtb_ext.BidTypeVideo {
adsExt, err := unmarshalAdsExt(bid.Ext)
if err != nil {
return nil, []error{&errortypes.BadServerResponse{err.Error()}}
}
if adsExt == nil || adsExt.Video == nil {
return nil, []error{&errortypes.BadServerResponse{
"$.seatbid.bid.ext.ads.video required",
}}
}
bidVideo = &openrtb_ext.ExtBidPrebidVideo{
Duration: adsExt.Video.Duration,
PrimaryCategory: head(bid.Cat),
}
}
bids = append(bids, &adapters.TypedBid{
Bid: &bid,
BidType: tp,
BidVideo: bidVideo,
})
}
}

adsResp := adapters.NewBidderResponseWithBidsCapacity(len(bids))
adsResp.Bids = bids

return adsResp, nil
}

func unmarshalExt(ext json.RawMessage) (*openrtb_ext.ExtImpAdoppler, error) {
var bext adapters.ExtImpBidder
err := json.Unmarshal(ext, &bext)
if err != nil {
return nil, err
}

var adsExt openrtb_ext.ExtImpAdoppler
err = json.Unmarshal(bext.Bidder, &adsExt)
if err != nil {
return nil, err
}

if adsExt.AdUnit == "" {
return nil, errors.New("$.imp.ext.adoppler.adunit required")
}

return &adsExt, nil
}

func unmarshalAdsExt(ext json.RawMessage) (*adsImpExt, error) {
var e struct {
Ads *adsImpExt `json:"ads"`
}
err := json.Unmarshal(ext, &e)

return e.Ads, err
}

func head(s []string) string {
if len(s) == 0 {
return ""
}

return s[0]
}
12 changes: 12 additions & 0 deletions adapters/adoppler/adoppler_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package adoppler

import (
"testing"

"github.com/prebid/prebid-server/adapters/adapterstest"
)

func TestJsonSamples(t *testing.T) {
bidder := NewAdopplerBidder("http://adoppler.com")
adapterstest.RunJSONBidderTest(t, "adopplertest", bidder)
}
60 changes: 60 additions & 0 deletions adapters/adoppler/adopplertest/exemplary/multibid.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
{"mockBidRequest": {"id": "req1",
"imp":[{"id": "imp1",
"banner": {"w": 100,
"h": 200},
"ext": {"bidder": {"adunit": "unit1"}}},
{"id": "imp2",
"video": {"minduration": 120,
"mimes": ["video/mp4"]},
"ext": {"bidder": {"adunit": "unit2"}}},
{"id": "imp3",
"native": {"request": "{}"},
"ext": {"bidder": {"adunit": "unit3"}}}]},
"httpCalls": [{"expectedRequest": {"uri": "http://adoppler.com/processHeaderBid/unit1",
"body": {"id": "req1-unit1",
"imp": [{"id": "imp1",
"banner": {"w": 100, "h": 200},
"ext": {"bidder": {"adunit": "unit1"}}}]}},
"mockResponse": {"status": 200,
"body": {"id": "req1-imp1-resp1",
"seatbid": [{"bid": [{"id": "req1-imp1-bid1",
"impid": "imp1",
"price": 0.12,
"adm": "<b>a banner</b>"}]}],
"cur": "USD"}}},
{"expectedRequest": {"uri": "http://adoppler.com/processHeaderBid/unit2",
"body": {"id": "req1-unit2",
"imp": [{"id": "imp2",
"video": {"minduration": 120,
"mimes": ["video/mp4"]},
"ext": {"bidder": {"adunit": "unit2"}}}]}},
"mockResponse": {"status": 200,
"body": {"id": "req1-imp2-resp2",
"seatbid": [{"bid": [{"id": "req1-imp2-bid1",
"impid": "imp2",
"price": 0.24,
"adm": "<VAST />",
"cat": ["IAB1", "IAB2"],
"ext": {"ads": {"video": {"duration": 121}}}}]}],
"cur": "USD"}}},
{"expectedRequest": {"uri": "http://adoppler.com/processHeaderBid/unit3",
"body": {"id": "req1-unit3",
"imp": [{"id": "imp3",
"native": {"request": "{}"},
"ext": {"bidder": {"adunit": "unit3"}}}]}},
"mockResponse": {"status": 204,
"body": ""}}],
"expectedBidResponses": [{"currency": "USD",
"bids": [{"bid": {"id": "req1-imp1-bid1",
"impid": "imp1",
"price": 0.12,
"adm": "<b>a banner</b>"},
"type": "banner"}]},
{"currency": "USD",
"bids": [{"bid": {"id": "req1-imp2-bid1",
"impid": "imp2",
"price": 0.24,
"adm": "<VAST />",
"cat": ["IAB1", "IAB2"],
"ext": {"ads": {"video": {"duration": 121}}}},
"type": "video"}]}]}
13 changes: 13 additions & 0 deletions adapters/adoppler/adopplertest/exemplary/no-bid.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{"mockBidRequest": {"id": "req1",
"imp":[{"id": "imp1",
"banner": {"w": 100,
"h": 200},
"ext": {"bidder": {"adunit": "unit1"}}}]},
"httpCalls": [{"expectedRequest": {"uri": "http://adoppler.com/processHeaderBid/unit1",
"body": {"id": "req1-unit1",
"imp": [{"id": "imp1",
"banner": {"w": 100, "h": 200},
"ext": {"bidder": {"adunit": "unit1"}}}]}},
"mockResponse": {"status": 204,
"body": ""}}],
"expectedBidResponses": []}
15 changes: 15 additions & 0 deletions adapters/adoppler/adopplertest/supplemental/bad-request.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{"mockBidRequest": {"id": "req1",
"imp":[{"id": "imp1",
"banner": {"w": 100,
"h": 200},
"ext": {"bidder": {"adunit": "unit1"}}}]},
"httpCalls": [{"expectedRequest": {"uri": "http://adoppler.com/processHeaderBid/unit1",
"body": {"id": "req1-unit1",
"imp": [{"id": "imp1",
"banner": {"w": 100, "h": 200},
"ext": {"bidder": {"adunit": "unit1"}}}]}},
"mockResponse": {"status": 400,
"body": ""}}],
"expectedBidResponses": [],
"expectedMakeBidsErrors": [{"value": "bad request",
"comparison": "literal"}]}
38 changes: 38 additions & 0 deletions adapters/adoppler/adopplertest/supplemental/duplicate-imp.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
{"mockBidRequest": {"id": "req1",
"imp":[{"id": "imp1",
"banner": {"w": 100,
"h": 200},
"ext": {"bidder": {"adunit": "unit1"}}},
{"id": "imp1",
"banner": {"w": 100,
"h": 200},
"ext": {"bidder": {"adunit": "unit2"}}}]},
"httpCalls": [{"expectedRequest": {"uri": "http://adoppler.com/processHeaderBid/unit1",
"body": {"id": "req1-unit1",
"imp": [{"id": "imp1",
"banner": {"w": 100, "h": 200},
"ext": {"bidder": {"adunit": "unit1"}}}]}},
"mockResponse": {"status": 200,
"body": {"id": "req1-imp1-resp1",
"seatbid": [{"bid": [{"id": "req1-imp1-bid1",
"impid": "imp1",
"price": 0.12,
"adm": "<b>a banner</b>"}]}],
"cur": "USD"}}},
{"expectedRequest": {"uri": "http://adoppler.com/processHeaderBid/unit2",
"body": {"id": "req1-unit2",
"imp": [{"id": "imp1",
"banner": {"w": 100, "h": 200},
"ext": {"bidder": {"adunit": "unit2"}}}]}},
"mockResponse": {"status": 200,
"body": {"id": "req1-imp1-resp1",
"seatbid": [{"bid": [{"id": "req1-imp1-bid1",
"impid": "imp1",
"price": 0.12,
"adm": "<b>a banner</b>"}]}],
"cur": "USD"}}}],
"expectedBidResponses": [],
"expectedMakeBidsErrors": [{"value": "duplicate $.imp.id imp1",
"comparison": "literal"},
{"value": "duplicate $.imp.id imp1",
"comparison": "literal"}]}
20 changes: 20 additions & 0 deletions adapters/adoppler/adopplertest/supplemental/invalid-impid.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
{"mockBidRequest": {"id": "req1",
"imp":[{"id": "imp1",
"banner": {"w": 100,
"h": 200},
"ext": {"bidder": {"adunit": "unit1"}}}]},
"httpCalls": [{"expectedRequest": {"uri": "http://adoppler.com/processHeaderBid/unit1",
"body": {"id": "req1-unit1",
"imp": [{"id": "imp1",
"banner": {"w": 100, "h": 200},
"ext": {"bidder": {"adunit": "unit1"}}}]}},
"mockResponse": {"status": 200,
"body": {"id": "req1-imp1-resp1",
"seatbid": [{"bid": [{"id": "req1-imp1-bid1",
"impid": "invalid",
"price": 0.12,
"adm": "<b>a banner</b>"}]}],
"cur": "USD"}}}],
"expectedBidResponses": [],
"expectedMakeBidsErrors": [{"value": "unknown impid: invalid",
"comparison": "literal"}]}
15 changes: 15 additions & 0 deletions adapters/adoppler/adopplertest/supplemental/invalid-response.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{"mockBidRequest": {"id": "req1",
"imp":[{"id": "imp1",
"banner": {"w": 100,
"h": 200},
"ext": {"bidder": {"adunit": "unit1"}}}]},
"httpCalls": [{"expectedRequest": {"uri": "http://adoppler.com/processHeaderBid/unit1",
"body": {"id": "req1-unit1",
"imp": [{"id": "imp1",
"banner": {"w": 100, "h": 200},
"ext": {"bidder": {"adunit": "unit1"}}}]}},
"mockResponse": {"status": 200,
"body": "invalid-json"}}],
"expectedBidResponses": [],
"expectedMakeBidsErrors": [{"value": "invalid body: json: cannot unmarshal string into Go value of type openrtb.BidResponse",
"comparison": "literal"}]}
Loading

0 comments on commit 8e382e7

Please sign in to comment.