Skip to content

Commit

Permalink
New Adapter: Alkimi (prebid#3247)
Browse files Browse the repository at this point in the history
co-authored by @kalidas-alkimi  @pro-nsk
  • Loading branch information
kalidas-alkimi authored and svamiftah committed Nov 21, 2023
1 parent 6b2b5ac commit 1cf29d8
Show file tree
Hide file tree
Showing 15 changed files with 1,199 additions and 0 deletions.
192 changes: 192 additions & 0 deletions adapters/alkimi/alkimi.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,192 @@
package alkimi

import (
"encoding/json"
"fmt"
"github.com/prebid/prebid-server/v2/errortypes"
"github.com/prebid/prebid-server/v2/floors"
"net/http"
"net/url"
"strconv"
"strings"

"github.com/prebid/openrtb/v19/openrtb2"
"github.com/prebid/prebid-server/v2/adapters"
"github.com/prebid/prebid-server/v2/config"
"github.com/prebid/prebid-server/v2/openrtb_ext"
)

const price_macro = "${AUCTION_PRICE}"

type adapter struct {
endpoint string
}

type extObj struct {
AlkimiBidderExt openrtb_ext.ExtImpAlkimi `json:"bidder"`
}

// Builder builds a new instance of the Alkimi adapter for the given bidder with the given config.
func Builder(bidderName openrtb_ext.BidderName, config config.Adapter, server config.Server) (adapters.Bidder, error) {
endpointURL, err := url.Parse(config.Endpoint)
if err != nil || len(endpointURL.String()) == 0 {
return nil, fmt.Errorf("invalid endpoint: %v", err)
}

bidder := &adapter{
endpoint: endpointURL.String(),
}
return bidder, nil
}

// MakeRequests creates Alkimi adapter requests
func (adapter *adapter) MakeRequests(request *openrtb2.BidRequest, req *adapters.ExtraRequestInfo) (reqsBidder []*adapters.RequestData, errs []error) {
reqCopy := *request

updated, errs := updateImps(reqCopy)
if len(errs) > 0 || len(reqCopy.Imp) != len(updated) {
return nil, errs
}

reqCopy.Imp = updated
encoded, err := json.Marshal(reqCopy)
if err != nil {
errs = append(errs, err)
} else {
reqBidder := buildBidderRequest(adapter, encoded)
reqsBidder = append(reqsBidder, reqBidder)
}
return
}

func updateImps(bidRequest openrtb2.BidRequest) ([]openrtb2.Imp, []error) {
var errs []error

updatedImps := make([]openrtb2.Imp, 0, len(bidRequest.Imp))
for _, imp := range bidRequest.Imp {

var bidderExt adapters.ExtImpBidder
var extImpAlkimi openrtb_ext.ExtImpAlkimi

if err := json.Unmarshal(imp.Ext, &bidderExt); err != nil {
errs = append(errs, err)
continue
}

if err := json.Unmarshal(bidderExt.Bidder, &extImpAlkimi); err != nil {
errs = append(errs, err)
continue
}

var bidFloorPrice floors.Price
bidFloorPrice.FloorMinCur = imp.BidFloorCur
bidFloorPrice.FloorMin = imp.BidFloor

if len(bidFloorPrice.FloorMinCur) > 0 && bidFloorPrice.FloorMin > 0 {
imp.BidFloor = bidFloorPrice.FloorMin
} else {
imp.BidFloor = extImpAlkimi.BidFloor
}
imp.Instl = extImpAlkimi.Instl
imp.Exp = extImpAlkimi.Exp

temp := extObj{AlkimiBidderExt: extImpAlkimi}
temp.AlkimiBidderExt.AdUnitCode = imp.ID

extJson, err := json.Marshal(temp)
if err != nil {
errs = append(errs, err)
continue
}
imp.Ext = extJson
updatedImps = append(updatedImps, imp)
}

return updatedImps, errs
}

func buildBidderRequest(adapter *adapter, encoded []byte) *adapters.RequestData {
headers := http.Header{}
headers.Add("Content-Type", "application/json;charset=utf-8")
headers.Add("Accept", "application/json")

reqBidder := &adapters.RequestData{
Method: "POST",
Uri: adapter.endpoint,
Body: encoded,
Headers: headers,
}
return reqBidder
}

// MakeBids will parse the bids from the Alkimi server
func (adapter *adapter) MakeBids(request *openrtb2.BidRequest, externalRequest *adapters.RequestData, response *adapters.ResponseData) (*adapters.BidderResponse, []error) {
var errs []error

if adapters.IsResponseStatusCodeNoContent(response) {
return nil, nil
}

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

var bidResp openrtb2.BidResponse
err := json.Unmarshal(response.Body, &bidResp)
if err != nil {
return nil, []error{err}
}

seatBidCount := len(bidResp.SeatBid)
if seatBidCount == 0 {
return nil, []error{&errortypes.BadServerResponse{
Message: "Empty SeatBid array",
}}
}

bidResponse := adapters.NewBidderResponseWithBidsCapacity(len(request.Imp))
for _, seatBid := range bidResp.SeatBid {
for _, bid := range seatBid.Bid {
copyBid := bid
resolveMacros(&copyBid)
impId := copyBid.ImpID
imp := request.Imp
bidType, err := getMediaTypeForImp(impId, imp)
if err != nil {
errs = append(errs, err)
continue
}
bidderBid := &adapters.TypedBid{
Bid: &copyBid,
BidType: bidType,
}
bidResponse.Bids = append(bidResponse.Bids, bidderBid)
}
}
return bidResponse, errs
}

func resolveMacros(bid *openrtb2.Bid) {
strPrice := strconv.FormatFloat(bid.Price, 'f', -1, 64)
bid.NURL = strings.Replace(bid.NURL, price_macro, strPrice, -1)
bid.AdM = strings.Replace(bid.AdM, price_macro, strPrice, -1)
}

func getMediaTypeForImp(impId string, imps []openrtb2.Imp) (openrtb_ext.BidType, error) {
for _, imp := range imps {
if imp.ID == impId {
if imp.Banner != nil {
return openrtb_ext.BidTypeBanner, nil
}
if imp.Video != nil {
return openrtb_ext.BidTypeVideo, nil
}
if imp.Audio != nil {
return openrtb_ext.BidTypeAudio, nil
}
}
}
return "", &errortypes.BadInput{
Message: fmt.Sprintf("Failed to find imp \"%s\"", impId),
}
}
57 changes: 57 additions & 0 deletions adapters/alkimi/alkimi_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
package alkimi

import (
"testing"

"github.com/prebid/prebid-server/v2/adapters"
"github.com/prebid/prebid-server/v2/adapters/adapterstest"
"github.com/prebid/prebid-server/v2/config"
"github.com/prebid/prebid-server/v2/openrtb_ext"
"github.com/stretchr/testify/assert"
)

const (
alkimiTestEndpoint = "https://exchange.alkimi-onboarding.com/server/bid"
)

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

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

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

func TestEndpointEmpty(t *testing.T) {
_, buildErr := Builder(openrtb_ext.BidderAlkimi, config.Adapter{
Endpoint: ""}, config.Server{ExternalUrl: alkimiTestEndpoint, GvlID: 1, DataCenter: "2"})
assert.Error(t, buildErr)
}

func TestEndpointMalformed(t *testing.T) {
_, buildErr := Builder(openrtb_ext.BidderAlkimi, config.Adapter{
Endpoint: " http://leading.space.is.invalid"}, config.Server{ExternalUrl: alkimiTestEndpoint, GvlID: 1, DataCenter: "2"})
assert.Error(t, buildErr)
}

func TestBuilder(t *testing.T) {
bidder, buildErr := buildBidder()
if buildErr != nil {
t.Fatalf("Failed to build bidder: %v", buildErr)
}
assert.NotNil(t, bidder)
}

func buildBidder() (adapters.Bidder, error) {
return Builder(
openrtb_ext.BidderAlkimi,
config.Adapter{Endpoint: alkimiTestEndpoint},
config.Server{ExternalUrl: "http://hosturl.com", GvlID: 1, DataCenter: "2"},
)
}
143 changes: 143 additions & 0 deletions adapters/alkimi/alkimitest/exemplary/simple-audio.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
{
"mockBidRequest": {
"id": "test-request-id",
"device": {
"ip": "123.123.123.123",
"ua": "iPad"
},
"site": {
"domain": "www.example.com",
"page": "http://www.example.com",
"publisher": {
"domain": "example.com"
},
"ext": {
"amp": 0
}
},
"imp": [
{
"id": "test-imp-id",
"tagid": "test",
"audio": {
"mimes": [
"audio/mpeg",
"audio/mp3"
],
"minduration": 5,
"maxduration": 30,
"minbitrate": 32,
"maxbitrate": 128
},
"bidfloor": 0.7,
"bidfloorcur": "USD",
"ext": {
"bidder": {
"token": "XXX",
"bidFloor": 0.5
}
}
}
]
},
"httpCalls": [
{
"expectedRequest": {
"uri": "https://exchange.alkimi-onboarding.com/server/bid",
"body": {
"id": "test-request-id",
"imp": [
{
"id": "test-imp-id",
"tagid": "test",
"audio": {
"mimes": [
"audio/mpeg",
"audio/mp3"
],
"minduration": 5,
"maxduration": 30,
"minbitrate": 32,
"maxbitrate": 128
},
"bidfloor": 0.7,
"bidfloorcur": "USD",
"ext": {
"bidder": {
"token": "XXX",
"bidFloor": 0.5,
"adUnitCode": "test-imp-id",
"exp": 0,
"instl": 0
}
}
}
],
"site": {
"domain": "www.example.com",
"page": "http://www.example.com",
"publisher": {
"domain": "example.com"
},
"ext": {
"amp": 0
}
},
"device": {
"ip": "123.123.123.123",
"ua": "iPad"
}
}
},
"mockResponse": {
"status": 200,
"body": {
"id": "test-request-id",
"seatbid": [
{
"bid": [
{
"id": "test_bid_id",
"impid": "test-imp-id",
"price": 0.9,
"adm": "<?xml version=\"1.0\" encoding=\"UTF-8\" ?><VAST version=\"2.0\"><Ad id=\"128a6.44d74.46b3\"><InLine><Error><![CDATA[http:\/\/example.net\/hbx\/verr?e=]]><\/Error><Impression><![CDATA[http:\/\/example.net\/hbx\/vimp?lid=test&aid=testapp]]><\/Impression><Creatives><Creative sequence=\"1\"><Linear><Duration>00:00:15<\/Duration><TrackingEvents><Tracking event=\"firstQuartile\"><![CDATA[https:\/\/example.com?event=first_quartile]]><\/Tracking><\/TrackingEvents><VideoClicks><ClickThrough><![CDATA[http:\/\/example.com]]><\/ClickThrough><\/VideoClicks><MediaFiles><MediaFile delivery=\"progressive\" width=\"16\" height=\"9\" type=\"audio\/mp3\" bitrate=\"128\"><![CDATA[https:\/\/example.com\/media.mp4]]><\/MediaFile><\/MediaFiles><\/Linear><\/Creative><\/Creatives><\/InLine><\/Ad><\/VAST>",
"cid": "test_cid",
"crid": "test_crid",
"ext": {
"prebid": {
"type": "audio"
}
}
}
],
"seat": "alkimi"
}
],
"cur": "USD"
}
}
}
],
"expectedBidResponses": [
{
"bids": [
{
"bid": {
"id": "test_bid_id",
"impid": "test-imp-id",
"price": 0.9,
"adm": "<?xml version=\"1.0\" encoding=\"UTF-8\" ?><VAST version=\"2.0\"><Ad id=\"128a6.44d74.46b3\"><InLine><Error><![CDATA[http:\/\/example.net\/hbx\/verr?e=]]><\/Error><Impression><![CDATA[http:\/\/example.net\/hbx\/vimp?lid=test&aid=testapp]]><\/Impression><Creatives><Creative sequence=\"1\"><Linear><Duration>00:00:15<\/Duration><TrackingEvents><Tracking event=\"firstQuartile\"><![CDATA[https:\/\/example.com?event=first_quartile]]><\/Tracking><\/TrackingEvents><VideoClicks><ClickThrough><![CDATA[http:\/\/example.com]]><\/ClickThrough><\/VideoClicks><MediaFiles><MediaFile delivery=\"progressive\" width=\"16\" height=\"9\" type=\"audio\/mp3\" bitrate=\"128\"><![CDATA[https:\/\/example.com\/media.mp4]]><\/MediaFile><\/MediaFiles><\/Linear><\/Creative><\/Creatives><\/InLine><\/Ad><\/VAST>",
"cid": "test_cid",
"crid": "test_crid",
"ext": {
"prebid": {
"type": "audio"
}
}
},
"type": "audio"
}
]
}
]
}
Loading

0 comments on commit 1cf29d8

Please sign in to comment.