Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/workflows/scripts/codepath-notification
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,4 @@ adapters/ix|imp_ix|ix.json|ix.yaml: pdu-supply-prebid@indexexchange.com
medianet: prebid@media.net
gumgum: prebid@gumgum.com
kargo: kraken@kargo.com
bidwave: prebid@bidwave.net
216 changes: 216 additions & 0 deletions adapters/bidwave/bidwave.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,216 @@
package bidwave

import (
"encoding/json"
"fmt"
"net/http"
"strings"

"github.com/prebid/openrtb/v20/openrtb2"
"github.com/prebid/prebid-server/v4/adapters"
"github.com/prebid/prebid-server/v4/config"
"github.com/prebid/prebid-server/v4/errortypes"
"github.com/prebid/prebid-server/v4/openrtb_ext"
"github.com/prebid/prebid-server/v4/util/jsonutil"
)

const defaultCurrency = "USD"

type adapter struct {
endpoint string
}

type requestExtBidwave struct {
PID string `json:"pid"`
}

type impGroup struct {
publisherID string
imps []openrtb2.Imp
}

func Builder(_ openrtb_ext.BidderName, config config.Adapter, _ config.Server) (adapters.Bidder, error) {
return &adapter{
endpoint: config.Endpoint,
}, nil
}

func (a *adapter) MakeRequests(request *openrtb2.BidRequest, reqInfo *adapters.ExtraRequestInfo) ([]*adapters.RequestData, []error) {
groups, errs := groupImpsByPublisherID(request.Imp, reqInfo)

requests := make([]*adapters.RequestData, 0, len(groups))
for _, group := range groups {
outgoingRequest := *request
outgoingRequest.Cur = []string{defaultCurrency}
outgoingRequest.Imp = group.imps

ext, err := setBidwaveExt(outgoingRequest.Ext, group.publisherID)
if err != nil {
errs = append(errs, err)
continue
}
outgoingRequest.Ext = ext

requestJSON, err := jsonutil.Marshal(outgoingRequest)
if err != nil {
errs = append(errs, err)
continue
}

headers := http.Header{}
headers.Add("Content-Type", "application/json;charset=utf-8")

requests = append(requests, &adapters.RequestData{
Method: http.MethodPost,
Uri: a.endpoint,
Body: requestJSON,
Headers: headers,
ImpIDs: openrtb_ext.GetImpIDs(group.imps),
})
}

return requests, errs
}

func prepareImpCurrency(imp openrtb2.Imp, reqInfo *adapters.ExtraRequestInfo) (openrtb2.Imp, error) {
if imp.BidFloor > 0 && strings.TrimSpace(imp.BidFloorCur) != "" && !strings.EqualFold(imp.BidFloorCur, defaultCurrency) {
bidFloor, err := reqInfo.ConvertCurrency(imp.BidFloor, imp.BidFloorCur, defaultCurrency)
if err != nil {
return imp, &errortypes.BadInput{
Message: fmt.Sprintf("expected currency %s for bid floor; unable to convert from %s for impression %s: %s", defaultCurrency, imp.BidFloorCur, imp.ID, err.Error()),
}
}

imp.BidFloor = bidFloor
imp.BidFloorCur = defaultCurrency
}

return imp, nil
}

func groupImpsByPublisherID(imps []openrtb2.Imp, reqInfo *adapters.ExtraRequestInfo) ([]impGroup, []error) {
groupIndexes := make(map[string]int)
groups := make([]impGroup, 0, len(imps))
errs := make([]error, 0)

for i := range imps {
publisherID, err := parsePublisherID(imps[i])
if err != nil {
errs = append(errs, err)
continue
}

imp, err := prepareImpCurrency(imps[i], reqInfo)
Comment thread
eng-bidwave marked this conversation as resolved.
if err != nil {
errs = append(errs, err)
continue
}
imp.Ext = nil

if groupIndex, ok := groupIndexes[publisherID]; ok {
groups[groupIndex].imps = append(groups[groupIndex].imps, imp)
continue
}

groupIndexes[publisherID] = len(groups)
groups = append(groups, impGroup{
publisherID: publisherID,
imps: []openrtb2.Imp{imp},
})
}

return groups, errs
}

func parsePublisherID(imp openrtb2.Imp) (string, error) {
var ext adapters.ExtImpBidder
if err := jsonutil.Unmarshal(imp.Ext, &ext); err != nil {
return "", &errortypes.BadInput{
Message: fmt.Sprintf("invalid imp.ext for impression %s: %s", imp.ID, err.Error()),
}
}

var params openrtb_ext.ExtImpBidwave
if err := jsonutil.Unmarshal(ext.Bidder, &params); err != nil {
return "", &errortypes.BadInput{
Message: fmt.Sprintf("invalid bidwave params for impression %s: %s", imp.ID, err.Error()),
}
}

return params.PublisherID, nil
}

func setBidwaveExt(rawExt json.RawMessage, publisherID string) (json.RawMessage, error) {
ext := map[string]json.RawMessage{}
if len(rawExt) > 0 {
if err := jsonutil.Unmarshal(rawExt, &ext); err != nil {
return nil, err
}
}

bidwaveExt, err := jsonutil.Marshal(requestExtBidwave{PID: publisherID})
if err != nil {
return nil, err
}

ext["bidwave"] = bidwaveExt
return jsonutil.Marshal(ext)
}

func (a *adapter) MakeBids(request *openrtb2.BidRequest, _ *adapters.RequestData, response *adapters.ResponseData) (*adapters.BidderResponse, []error) {
if adapters.IsResponseStatusCodeNoContent(response) {
return nil, nil
}

if err := adapters.CheckResponseStatusCodeForErrors(response); err != nil {
return nil, []error{err}
}

var bidResponse openrtb2.BidResponse
if err := jsonutil.Unmarshal(response.Body, &bidResponse); err != nil {
return nil, []error{&errortypes.BadServerResponse{
Message: fmt.Sprintf("unable to unmarshal response: %s", err.Error()),
}}
}

result := adapters.NewBidderResponseWithBidsCapacity(len(request.Imp))
if bidResponse.Cur != "" {
result.Currency = bidResponse.Cur
}

var errs []error
for seatBidIndex := range bidResponse.SeatBid {
for bidIndex := range bidResponse.SeatBid[seatBidIndex].Bid {
bid := &bidResponse.SeatBid[seatBidIndex].Bid[bidIndex]
bidType, err := getBidType(bid)
if err != nil {
errs = append(errs, err)
continue
}

result.Bids = append(result.Bids, &adapters.TypedBid{
Bid: bid,
BidType: bidType,
})
}
}

return result, errs
}

func getBidType(bid *openrtb2.Bid) (openrtb_ext.BidType, error) {
switch bid.MType {
case openrtb2.MarkupBanner:
return openrtb_ext.BidTypeBanner, nil
case openrtb2.MarkupVideo:
return openrtb_ext.BidTypeVideo, nil
case 0:
return "", &errortypes.BadServerResponse{
Message: fmt.Sprintf("bid must have non-zero mtype for impression with ID: \"%s\"", bid.ImpID),
}
default:
return "", &errortypes.BadServerResponse{
Message: fmt.Sprintf("unsupported mtype %d for impression with ID: \"%s\"", bid.MType, bid.ImpID),
}
}
}
103 changes: 103 additions & 0 deletions adapters/bidwave/bidwave_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
package bidwave

import (
"testing"

"github.com/prebid/openrtb/v20/openrtb2"
"github.com/prebid/prebid-server/v4/adapters"
"github.com/prebid/prebid-server/v4/adapters/adapterstest"
"github.com/prebid/prebid-server/v4/config"
"github.com/prebid/prebid-server/v4/currency"
"github.com/prebid/prebid-server/v4/openrtb_ext"
"github.com/stretchr/testify/assert"
)

func TestJsonSamples(t *testing.T) {
bidder, buildErr := Builder(openrtb_ext.BidderBidwave, config.Adapter{
Endpoint: "https://rtb.bidwave.net/rtb/v1/bid",
}, config.Server{})
if buildErr != nil {
t.Fatalf("Builder returned unexpected error %v", buildErr)
}

adapterstest.RunJSONBidderTest(t, "bidwavetest", bidder)
}

func TestPrepareImpCurrencyConvertsToUSD(t *testing.T) {
reqInfo := adapters.NewExtraRequestInfo(currency.NewRates(map[string]map[string]float64{
"EUR": {
"USD": 2,
},
}))
imp := openrtb2.Imp{
ID: "imp-1",
BidFloor: 1.5,
BidFloorCur: "EUR",
}

preparedImp, err := prepareImpCurrency(imp, &reqInfo)

assert.NoError(t, err)
assert.Equal(t, 3.0, preparedImp.BidFloor)
assert.Equal(t, "USD", preparedImp.BidFloorCur)
}

func TestPrepareImpCurrencyReturnsErrorWhenBidFloorCurrencyCannotBeConverted(t *testing.T) {
reqInfo := adapters.NewExtraRequestInfo(currency.NewRates(map[string]map[string]float64{}))
imp := openrtb2.Imp{
ID: "imp-1",
BidFloor: 1.5,
BidFloorCur: "EUR",
}

preparedImp, err := prepareImpCurrency(imp, &reqInfo)

assert.Equal(t, imp, preparedImp)
assert.Error(t, err)
assert.Contains(t, err.Error(), "expected currency USD for bid floor; unable to convert from EUR for impression imp-1")
}

func TestGetBidType(t *testing.T) {
testCases := []struct {
name string
bid openrtb2.Bid
expected openrtb_ext.BidType
expectedErr string
}{
{
name: "banner mtype",
bid: openrtb2.Bid{ImpID: "imp-1", MType: openrtb2.MarkupBanner},
expected: openrtb_ext.BidTypeBanner,
},
{
name: "video mtype",
bid: openrtb2.Bid{ImpID: "imp-1", MType: openrtb2.MarkupVideo},
expected: openrtb_ext.BidTypeVideo,
},
{
name: "missing mtype",
bid: openrtb2.Bid{ImpID: "imp-1"},
expectedErr: "bid must have non-zero mtype for impression with ID: \"imp-1\"",
},
{
name: "unsupported mtype",
bid: openrtb2.Bid{ImpID: "imp-1", MType: openrtb2.MarkupNative},
expectedErr: "unsupported mtype 4 for impression with ID: \"imp-1\"",
},
}

for _, testCase := range testCases {
t.Run(testCase.name, func(t *testing.T) {
bidType, err := getBidType(&testCase.bid)

if testCase.expectedErr != "" {
assert.EqualError(t, err, testCase.expectedErr)
assert.Empty(t, bidType)
return
}

assert.NoError(t, err)
assert.Equal(t, testCase.expected, bidType)
})
}
}
Loading
Loading