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
65 changes: 65 additions & 0 deletions adapters/vdoai/params_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
package vdoai

import (
"encoding/json"
"testing"

"github.com/prebid/prebid-server/v4/openrtb_ext"
)

// This file tests static/bidder-params/vdoai.json
// These also validate the format of the external API: request.imp[i].ext.prebid.bidder.vdoai

func TestValidParams(t *testing.T) {
validator, err := openrtb_ext.NewBidderParamsValidator("../../static/bidder-params")
if err != nil {
t.Fatalf("Failed to fetch the json-schemas. %v", err)
}

for _, validParam := range validParams {
if err := validator.Validate(openrtb_ext.BidderVdoai, json.RawMessage(validParam)); err != nil {
t.Errorf("Schema rejected vdoai params: %s", validParam)
}
}
}

func TestInvalidParams(t *testing.T) {
validator, err := openrtb_ext.NewBidderParamsValidator("../../static/bidder-params")
if err != nil {
t.Fatalf("Failed to fetch the json-schemas. %v", err)
}

for _, invalidParam := range invalidParams {
if err := validator.Validate(openrtb_ext.BidderVdoai, json.RawMessage(invalidParam)); err == nil {
t.Errorf("Schema allowed unexpected params: %s", invalidParam)
}
}
}

var validParams = []string{
`{"adUnitId":123456,"adUnitType":"banner","host":"exchange.ortb.net","publisherId":"pub-abc"}`,
`{"adUnitId":123456,"adUnitType":"video","host":"ads.vdo.ai","publisherId":"pub-xyz"}`,
`{"adUnitId":123456,"adUnitType":"banner","host":"exchange.ortb.net","publisherId":"pub-abc","bidfloor":1.5}`,
`{"adUnitId":123456,"adUnitType":"banner","host":"exchange.ortb.net","publisherId":"pub-abc","custom1":"val1","custom2":"val2","custom3":"val3","custom4":"val4","custom5":"val5"}`,
`{"adUnitId":123456,"adUnitType":"video","host":"ads.vdo.ai","publisherId":"pub-xyz","bidfloor":2.0,"custom1":"c1"}`,
`{"adUnitId":"123456","adUnitType":"banner","host":"exchange.ortb.net","publisherId":"pub-abc"}`,
`{"adUnitId":"vdo-123456","adUnitType":"banner","host":"exchange.ortb.net","publisherId":"pub-abc"}`,
}

var invalidParams = []string{
``,
`null`,
`true`,
`5`,
`4.2`,
`[]`,
`{}`,
`{"adUnitId":"123456"}`,
`{"adUnitType":"banner"}`,
`{"adUnitId":123456,"adUnitType":"banner"}`,
`{"adUnitId":123456,"adUnitType":"video"}`,
`{"adUnitId":123456,"adUnitType":"banner","host":"exchange.ortb.net"}`,
`{"adUnitId":123456,"adUnitType":"banner","publisherId":"pub-abc"}`,
`{"adUnitId":"123456","adUnitType":"banner"}`,
`{"adUnitId":"123456","adUnitType":"banner","host":"exchange.ortb.net","publisherId":"pub-abc","bidfloor":"notanumber"}`,
}
195 changes: 195 additions & 0 deletions adapters/vdoai/vdoai.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,195 @@
package vdoai

import (
"encoding/json"
"fmt"
"net/http"
"text/template"

"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/macros"
"github.com/prebid/prebid-server/v4/openrtb_ext"
"github.com/prebid/prebid-server/v4/util/jsonutil"
)

type adapter struct {
endpointTemplate *template.Template
}

// Builder builds a new instance of the vdoai adapter for the given bidder with the given config.
func Builder(bidderName openrtb_ext.BidderName, config config.Adapter, server config.Server) (adapters.Bidder, error) {
template, err := template.New("endpointTemplate").Parse(config.Endpoint)
if err != nil {
return nil, fmt.Errorf("unable to parse endpoint url template: %v", err)
}

bidder := &adapter{
endpointTemplate: template,
}
return bidder, nil
}

func (a *adapter) MakeRequests(request *openrtb2.BidRequest, requestInfo *adapters.ExtraRequestInfo) ([]*adapters.RequestData, []error) {
var errors []error

// Group imps by endpoint URL
impsByEndpoint := make(map[string][]openrtb2.Imp)

for i := range request.Imp {
imp := request.Imp[i]

var bidderExt adapters.ExtImpBidder
if err := jsonutil.Unmarshal(imp.Ext, &bidderExt); err != nil {
errors = append(errors, &errortypes.BadInput{
Message: fmt.Sprintf("ignoring imp id=%s, error while decoding extImpBidder, err: %s", imp.ID, err),
})
continue
}

var impExt openrtb_ext.ImpExtVdoai
if err := jsonutil.Unmarshal(bidderExt.Bidder, &impExt); err != nil {
errors = append(errors, &errortypes.BadInput{
Message: fmt.Sprintf("ignoring imp id=%s, error while decoding impExt, err: %s", imp.ID, err),
})
continue
}

// Inject bidfloor from bidder ext into the imp if not already set
if impExt.BidFloor > 0 && imp.BidFloor == 0 {
imp.BidFloor = impExt.BidFloor
}

endpointParams := macros.EndpointTemplateParams{
Host: impExt.Host,
PublisherID: impExt.PublisherId,
}

endpointURL, err := macros.ResolveMacros(a.endpointTemplate, endpointParams)
if err != nil {
errors = append(errors, err)
continue
}

impsByEndpoint[endpointURL] = append(impsByEndpoint[endpointURL], imp)
}

if len(impsByEndpoint) == 0 {
return nil, errors
}

var requests []*adapters.RequestData
for endpoint, imps := range impsByEndpoint {
requestCopy := *request
requestCopy.Imp = imps

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

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

requestData := &adapters.RequestData{
Method: "POST",
Uri: endpoint,
Body: requestJSON,
Headers: headers,
ImpIDs: openrtb_ext.GetImpIDs(imps),
}

requests = append(requests, requestData)
}

return requests, errors
}

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

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

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

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

var errors []error
for _, seatBid := range response.SeatBid {
for i, bid := range seatBid.Bid {
bidType, err := getMediaTypeForBid(bid, request.Imp)
if err != nil {
errors = append(errors, err)
continue
}

bidResponse.Bids = append(bidResponse.Bids, &adapters.TypedBid{
Bid: &seatBid.Bid[i],
BidType: bidType,
})
}
}
return bidResponse, errors
}

// getMediaTypeForBid determines the media type for a bid.
// It first tries bid.ext.prebid.type (set by the server), then falls back to
// detecting from the matching impression's media type objects.
func getMediaTypeForBid(bid openrtb2.Bid, imps []openrtb2.Imp) (openrtb_ext.BidType, error) {
// Try bid.ext.prebid.type first
if bid.Ext != nil {
var bidExt openrtb_ext.ExtBid
if err := jsonutil.Unmarshal(bid.Ext, &bidExt); err == nil && bidExt.Prebid != nil {
bidType, err := openrtb_ext.ParseBidType(string(bidExt.Prebid.Type))
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider this as a suggestion. Prebid server expects the media type to be explicitly set in the adapter response. Therefore, recommends implementing a pattern where the adapter server sets the MType field in the response to accurately determine the media type for the impression.

if err == nil {
return bidType, nil
}
}
}

// Try bid.MType (OpenRTB 2.6)
switch bid.MType {
case openrtb2.MarkupBanner:
return openrtb_ext.BidTypeBanner, nil
case openrtb2.MarkupVideo:
return openrtb_ext.BidTypeVideo, nil
}

// Fallback: detect from the matching impression
for _, imp := range imps {
if imp.ID == bid.ImpID {
if imp.Video != nil {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider this as a suggestion. The current implementation follows an anti-pattern, assumes that if there is a multi-format request, the media type defaults to openrtb_ext.BidTypeVideo, nil. Prebid server expects the media type to be explicitly set in the adapter response. Therefore, we strongly recommend implementing a pattern where the adapter server sets the MType field in the response to accurately determine the media type for the impression.

return openrtb_ext.BidTypeVideo, nil
}
if imp.Banner != nil {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider this as a suggestion. The current implementation follows an anti-pattern, assumes that if there is a multi-format request, the media type defaults to openrtb_ext.BidTypeBanner, nil. Prebid server expects the media type to be explicitly set in the adapter response. Therefore, we strongly recommend implementing a pattern where the adapter server sets the MType field in the response to accurately determine the media type for the impression.

return openrtb_ext.BidTypeBanner, nil
}
break
}
}

return "", &errortypes.BadServerResponse{
Message: fmt.Sprintf("failed to determine media type for bid with imp id \"%s\"", bid.ImpID),
}
}

// vdoaiImpExt is used to re-marshal the imp ext with the vdoai-specific fields
// preserved under the "bidder" key.
type vdoaiImpExt struct {
Bidder json.RawMessage `json:"bidder"`
}
23 changes: 23 additions & 0 deletions adapters/vdoai/vdoai_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package vdoai

import (
"testing"

"github.com/prebid/prebid-server/v4/adapters/adapterstest"
"github.com/prebid/prebid-server/v4/config"
"github.com/prebid/prebid-server/v4/openrtb_ext"
)

func TestJsonSamples(t *testing.T) {
bidder, buildErr := Builder(
openrtb_ext.BidderVdoai,
config.Adapter{Endpoint: "https://{{.Host}}/{{.PublisherID}}"},
config.Server{ExternalUrl: "http://hosturl.com", GvlID: 1, DataCenter: "2"},
)

if buildErr != nil {
t.Fatalf("Builder returned unexpected error %v", buildErr)
}

adapterstest.RunJSONBidderTest(t, "vdoaitest", bidder)
}
Loading
Loading