diff --git a/docs/endpoints/openrtb2/auction.md b/docs/endpoints/openrtb2/auction.md index 579ec197e2b..81c8c03d371 100644 --- a/docs/endpoints/openrtb2/auction.md +++ b/docs/endpoints/openrtb2/auction.md @@ -303,12 +303,16 @@ Bids can be temporarily cached on the server by sending the following data as `r ``` { - "bids": {} + "bids": {}, + "vastxml": {} } ``` -This property has no effect unless `request.ext.prebid.targeting` is also set in the request. -If present, Prebid Server will make a _best effort_ to include these extra `bid.ext.prebid.targeting` keys: +Both `bids` and `vastxml` are optional, but one of the two is required. Thils property will have no effect +unless `request.ext.prebid.targeting` is also set in the request. + +If `bids` is present, Prebid Server will make a _best effort_ to include these extra +`bid.ext.prebid.targeting` keys: - `hb_cache_id`: On the highest overall Bid in each Imp. - `hb_cache_id_{bidderName}`: On the highest Bid from {bidderName} in each Imp. @@ -317,7 +321,11 @@ Clients _should not assume_ that these keys will exist, just because they were r If they exist, the value will be a UUID which can be used to fetch Bid JSON from [Prebid Cache](https://github.com/prebid/prebid-cache). They may not exist if the host company's cache is full, having connection problems, or other issues like that. -This is mainly intended for certain limited Prebid Mobile setups, where bids cannot be cached client-side. +If `vastxml` is present, PBS will try to add analogous keys `hb_uuid` and `hb_uuid_{bidderName}`. +In addition to the caveats above, these will exist _only if the relevant Bids are for Video_. +If they exist, the values can be used to fetch the bid's VAST XML from Prebid Cache directly. + +These options are mainly intended for certain limited Prebid Mobile setups, where bids cannot be cached client-side. #### GDPR diff --git a/endpoints/openrtb2/amp_auction.go b/endpoints/openrtb2/amp_auction.go index 876303c36b3..1115b38e9b9 100644 --- a/endpoints/openrtb2/amp_auction.go +++ b/endpoints/openrtb2/amp_auction.go @@ -427,11 +427,14 @@ func defaultRequestExt(req *openrtb.BidRequest) (errs []error) { PriceGranularity: openrtb_ext.PriceGranularityFromString("med"), } } - if extRequest.Prebid.Cache == nil || extRequest.Prebid.Cache.Bids == nil { + if extRequest.Prebid.Cache == nil { setDefaults = true extRequest.Prebid.Cache = &openrtb_ext.ExtRequestPrebidCache{ Bids: &openrtb_ext.ExtRequestPrebidCacheBids{}, } + } else if extRequest.Prebid.Cache.Bids == nil { + setDefaults = true + extRequest.Prebid.Cache.Bids = &openrtb_ext.ExtRequestPrebidCacheBids{} } if setDefaults { newExt, err := json.Marshal(extRequest) diff --git a/endpoints/openrtb2/sample-requests/invalid-whole/cache-no-bids.json b/endpoints/openrtb2/sample-requests/invalid-whole/cache-nothing.json similarity index 100% rename from endpoints/openrtb2/sample-requests/invalid-whole/cache-no-bids.json rename to endpoints/openrtb2/sample-requests/invalid-whole/cache-nothing.json diff --git a/endpoints/openrtb2/sample-requests/valid-whole/supplementary/cache-bids.json b/endpoints/openrtb2/sample-requests/valid-whole/supplementary/cache-bids.json new file mode 100644 index 00000000000..884ec898dab --- /dev/null +++ b/endpoints/openrtb2/sample-requests/valid-whole/supplementary/cache-bids.json @@ -0,0 +1,29 @@ +{ + "id": "some-request-id", + "site": { + "page": "test.somepage.com" + }, + "imp": [ + { + "id": "my-imp-id", + "video": { + "mimes": [ + "video/mp4" + ] + }, + "ext": { + "appnexus": { + "placementId": 10433394 + } + } + } + ], + "ext": { + "prebid": { + "cache": { + "bids": {} + }, + "targeting": {} + } + } +} diff --git a/endpoints/openrtb2/sample-requests/valid-whole/supplementary/cache-vast.json b/endpoints/openrtb2/sample-requests/valid-whole/supplementary/cache-vast.json new file mode 100644 index 00000000000..7878767af78 --- /dev/null +++ b/endpoints/openrtb2/sample-requests/valid-whole/supplementary/cache-vast.json @@ -0,0 +1,28 @@ +{ + "id": "some-request-id", + "site": { + "page": "test.somepage.com" + }, + "imp": [ + { + "id": "my-imp-id", + "video": { + "mimes": [ + "video/mp4" + ] + }, + "ext": { + "appnexus": { + "placementId": 10433394 + } + } + } + ], + "ext": { + "prebid": { + "cache": { + "vastxml": {} + } + } + } +} diff --git a/exchange/auction.go b/exchange/auction.go index 92a728a6c90..cffadd70d88 100644 --- a/exchange/auction.go +++ b/exchange/auction.go @@ -2,6 +2,7 @@ package exchange import ( "context" + "encoding/json" "github.com/golang/glog" "github.com/mxmCherry/openrtb" @@ -54,16 +55,86 @@ func (a *auction) setRoundedPrices(priceGranularity openrtb_ext.PriceGranularity a.roundedPrices = roundedPrices } -func (a *auction) doCache(ctx context.Context, cache prebid_cache_client.Client) { - toCache := make([]*openrtb.Bid, 0, len(a.roundedPrices)) +func (a *auction) doCache(ctx context.Context, cache prebid_cache_client.Client, bids bool, vast bool) { + if !bids && !vast { + return + } + + expectNumBids := valOrZero(bids, len(a.roundedPrices)) + expectNumVast := valOrZero(vast, len(a.roundedPrices)) + bidIndices := make(map[int]*openrtb.Bid, expectNumBids) + vastIndices := make(map[int]*openrtb.Bid, expectNumVast) + toCache := make([]prebid_cache_client.Cacheable, 0, expectNumBids+expectNumVast) for _, topBidsPerImp := range a.winningBidsByBidder { for _, topBidPerBidder := range topBidsPerImp { - toCache = append(toCache, topBidPerBidder.bid) + if bids { + if jsonBytes, err := json.Marshal(topBidPerBidder.bid); err == nil { + toCache = append(toCache, prebid_cache_client.Cacheable{ + Type: prebid_cache_client.TypeJSON, + Data: jsonBytes, + }) + bidIndices[len(toCache)-1] = topBidPerBidder.bid + } + } + if vast && topBidPerBidder.bidType == openrtb_ext.BidTypeVideo { + vast := makeVAST(topBidPerBidder.bid) + if jsonBytes, err := json.Marshal(vast); err == nil { + toCache = append(toCache, prebid_cache_client.Cacheable{ + Type: prebid_cache_client.TypeXML, + Data: jsonBytes, + }) + vastIndices[len(toCache)-1] = topBidPerBidder.bid + } + } + } + } + + ids := cache.PutJson(ctx, toCache) + + if bids { + a.cacheIds = make(map[*openrtb.Bid]string, len(bidIndices)) + for index, bid := range bidIndices { + if ids[index] != "" { + a.cacheIds[bid] = ids[index] + } } } + if vast { + a.vastCacheIds = make(map[*openrtb.Bid]string, len(vastIndices)) + for index, bid := range vastIndices { + if ids[index] != "" { + a.vastCacheIds[bid] = ids[index] + } + } + } +} - a.cacheIds = cacheBids(ctx, cache, toCache) +// makeVAST returns some VAST XML for the given bid. If AdM is defined, +// it takes precedence. Otherwise the Nurl will be wrapped in a redirect tag. +func makeVAST(bid *openrtb.Bid) string { + if bid.AdM == "" { + return `` + + `prebid.org wrapper` + + `` + + `` + + `` + } + return bid.AdM +} + +func valOrZero(useVal bool, val int) int { + if useVal { + return val + } + return 0 +} + +func maybeMake(shouldMake bool, capacity int) []prebid_cache_client.Cacheable { + if shouldMake { + return make([]prebid_cache_client.Cacheable, 0, capacity) + } + return nil } type auction struct { @@ -73,6 +144,8 @@ type auction struct { winningBidsByBidder map[string]map[openrtb_ext.BidderName]*pbsOrtbBid // roundedPrices stores the price strings rounded for each bid according to the price granularity. roundedPrices map[*pbsOrtbBid]string - // cacheIds stores the UUIDs from Prebid Cache for each bid. + // cacheIds stores the UUIDs from Prebid Cache for fetching the full bid JSON. cacheIds map[*openrtb.Bid]string + // vastCacheIds stores UUIDS from Prebid cache for fetching the VAST markup to video bids. + vastCacheIds map[*openrtb.Bid]string } diff --git a/exchange/auction_test.go b/exchange/auction_test.go new file mode 100644 index 00000000000..26f5ece14e9 --- /dev/null +++ b/exchange/auction_test.go @@ -0,0 +1,31 @@ +package exchange + +import ( + "testing" + + "github.com/mxmCherry/openrtb" + "github.com/stretchr/testify/assert" +) + +func TestMakeVASTGiven(t *testing.T) { + const expect = `` + bid := &openrtb.Bid{ + AdM: expect, + } + vast := makeVAST(bid) + assert.Equal(t, expect, vast) +} + +func TestMakeVASTNurl(t *testing.T) { + const url = "http://domain.com/win-notify/1" + const expect = `` + + `prebid.org wrapper` + + `` + + `` + + `` + bid := &openrtb.Bid{ + NURL: url, + } + vast := makeVAST(bid) + assert.Equal(t, expect, vast) +} diff --git a/exchange/cache.go b/exchange/cache.go deleted file mode 100644 index 067a4c10c43..00000000000 --- a/exchange/cache.go +++ /dev/null @@ -1,34 +0,0 @@ -package exchange - -import ( - "context" - "encoding/json" - - "github.com/golang/glog" - "github.com/mxmCherry/openrtb" - "github.com/prebid/prebid-server/prebid_cache_client" -) - -func cacheBids(ctx context.Context, cache prebid_cache_client.Client, bids []*openrtb.Bid) map[*openrtb.Bid]string { - // Marshal the bids into JSON payloads. If any errors occur during marshalling, eject that bid from the array. - // After this block, we expect "bids" and "jsonValues" to have the same number of elements in the same order. - jsonValues := make([]json.RawMessage, 0, len(bids)) - for i := 0; i < len(bids); i++ { - if jsonBytes, err := json.Marshal(bids[i]); err != nil { - glog.Errorf("Error marshalling OpenRTB Bid for Prebid Cache: %v", err) - bids = append(bids[:i], bids[i+1:]...) - i-- - } else { - jsonValues = append(jsonValues, jsonBytes) - } - } - - ids := cache.PutJson(ctx, jsonValues) - toReturn := make(map[*openrtb.Bid]string, len(bids)) - for i := 0; i < len(bids); i++ { - if ids[i] != "" { - toReturn[bids[i]] = ids[i] - } - } - return toReturn -} diff --git a/exchange/cache_test.go b/exchange/cache_test.go deleted file mode 100644 index 0ad1f74f267..00000000000 --- a/exchange/cache_test.go +++ /dev/null @@ -1,117 +0,0 @@ -package exchange - -import ( - "context" - "encoding/json" - "testing" - - "github.com/evanphx/json-patch" - "github.com/mxmCherry/openrtb" -) - -func TestBidSerialization(t *testing.T) { - winningBid := &openrtb.Bid{ - ID: "bar", - ImpID: "a", - Price: 1.5, - } - otherBid := &openrtb.Bid{ - ID: "foo", - ImpID: "a", - Price: 0.5, - } - - mockClient := &mockCacheClient{ - mockReturns: map[*openrtb.Bid]string{ - winningBid: "0", - otherBid: "1", - }, - } - - bidMap := cacheBids(context.Background(), mockClient, []*openrtb.Bid{winningBid, otherBid}) - - assertStringValue(t, `bid "bar"`, "0", bidMap[winningBid]) - assertStringValue(t, `bid "foo"`, "1", bidMap[otherBid]) -} - -func TestCacheFailures(t *testing.T) { - winningBid := &openrtb.Bid{ - ID: "bar", - ImpID: "a", - Price: 1.5, - } - otherBid := &openrtb.Bid{ - ID: "foo", - ImpID: "a", - Price: 0.5, - } - - mockClient := &mockCacheClient{ - mockReturns: map[*openrtb.Bid]string{ - winningBid: "", - otherBid: "1", - }, - } - bidMap := cacheBids(context.Background(), mockClient, []*openrtb.Bid{winningBid, otherBid}) - - assertStringValue(t, `bid "foo"`, "1", bidMap[otherBid]) - if _, ok := bidMap[winningBid]; ok { - t.Error("If the cache call fails, no ID should exist for that bid.") - } -} - -func TestMarshalFailure(t *testing.T) { - badBid := &openrtb.Bid{ - ImpID: "foo", - Price: 1, - Ext: openrtb.RawJSON("{"), - } - goodBid := &openrtb.Bid{ - ImpID: "bar", - Price: 2, - } - - mockClient := &mockCacheClient{ - mockReturns: map[*openrtb.Bid]string{ - badBid: "0", - goodBid: "1", - }, - } - - bidMap := cacheBids(context.Background(), mockClient, []*openrtb.Bid{goodBid, badBid}) - if _, ok := bidMap[badBid]; ok { - t.Errorf("bids with malformed JSON should not be cached.") - } - if id, ok := bidMap[goodBid]; ok { - if id != "1" { - t.Errorf("Wrong id for good bid. Expected 1, got %s", id) - } - } else { - t.Errorf("bids with malformed JSON should not prevent other bids from being cached.") - } -} - -type mockCacheClient struct { - mockReturns map[*openrtb.Bid]string -} - -func (c *mockCacheClient) PutJson(ctx context.Context, values []json.RawMessage) []string { - returns := make([]string, len(values)) - for i, value := range values { - for bid, id := range c.mockReturns { - bidBytes, _ := json.Marshal(bid) - if jsonpatch.Equal(bidBytes, value) { - returns[i] = id - break - } - } - } - return returns -} - -func assertStringValue(t *testing.T, object string, expect string, value string) { - t.Helper() - if expect != value { - t.Errorf("Wrong value for %s, expected \"%s\", got \"%s\"", object, expect, value) - } -} diff --git a/exchange/exchange.go b/exchange/exchange.go index 1fe805b8f3c..5e7681eb4f8 100644 --- a/exchange/exchange.go +++ b/exchange/exchange.go @@ -86,6 +86,7 @@ func (e *exchange) HoldAuction(ctx context.Context, bidRequest *openrtb.BidReque // Process the request to check for targeting parameters. var targData *targetData shouldCacheBids := false + shouldCacheVAST := false var bidAdjustmentFactors map[string]float64 if len(bidRequest.Ext) > 0 { var requestExt openrtb_ext.ExtRequest @@ -94,7 +95,10 @@ func (e *exchange) HoldAuction(ctx context.Context, bidRequest *openrtb.BidReque return nil, fmt.Errorf("Error decoding Request.ext : %s", err.Error()) } bidAdjustmentFactors = requestExt.Prebid.BidAdjustmentFactors - shouldCacheBids = requestExt.Prebid.Cache != nil && requestExt.Prebid.Cache.Bids != nil + if requestExt.Prebid.Cache != nil { + shouldCacheBids = requestExt.Prebid.Cache.Bids != nil + shouldCacheVAST = requestExt.Prebid.Cache.VastXML != nil + } if requestExt.Prebid.Targeting != nil { targData = &targetData{ @@ -103,7 +107,10 @@ func (e *exchange) HoldAuction(ctx context.Context, bidRequest *openrtb.BidReque includeBidderKeys: requestExt.Prebid.Targeting.IncludeBidderKeys, } if shouldCacheBids { - targData.includeCache = true + targData.includeCacheBids = true + } + if shouldCacheVAST { + targData.includeCacheVast = true } } } @@ -117,9 +124,7 @@ func (e *exchange) HoldAuction(ctx context.Context, bidRequest *openrtb.BidReque auc := newAuction(adapterBids, len(bidRequest.Imp)) if targData != nil { auc.setRoundedPrices(targData.priceGranularity) - if targData.includeCache { - auc.doCache(ctx, e.cache) - } + auc.doCache(ctx, e.cache, targData.includeCacheBids, targData.includeCacheVast) targData.setTargeting(auc, bidRequest.App != nil) } // Build the response diff --git a/exchange/exchange_test.go b/exchange/exchange_test.go index ea988565b0a..93a3d6c0fd4 100644 --- a/exchange/exchange_test.go +++ b/exchange/exchange_test.go @@ -13,6 +13,8 @@ import ( "testing" "time" + "github.com/prebid/prebid-server/prebid_cache_client" + "github.com/buger/jsonparser" "github.com/mxmCherry/openrtb" "github.com/prebid/prebid-server/config" @@ -511,7 +513,7 @@ func mockHandler(statusCode int, getBody string, postBody string) http.Handler { type wellBehavedCache struct{} -func (c *wellBehavedCache) PutJson(ctx context.Context, values []json.RawMessage) []string { +func (c *wellBehavedCache) PutJson(ctx context.Context, values []prebid_cache_client.Cacheable) []string { ids := make([]string, len(values)) for i := 0; i < len(values); i++ { ids[i] = strconv.Itoa(i) diff --git a/exchange/exchangetest/targeting-cache-vast-banner.json b/exchange/exchangetest/targeting-cache-vast-banner.json new file mode 100644 index 00000000000..a2cddfe0cbf --- /dev/null +++ b/exchange/exchangetest/targeting-cache-vast-banner.json @@ -0,0 +1,85 @@ +{ + "incomingRequest": { + "ortbRequest": { + "id": "some-request-id", + "site": { + "page": "test.somepage.com" + }, + "imp": [ + { + "id": "my-imp-id", + "banner": { + "w": 300, + "h": 250 + }, + "ext": { + "appnexus": { + "placementId": 1 + } + } + } + ], + "ext": { + "prebid": { + "cache": { + "vastxml": {} + }, + "targeting": {} + } + } + } + }, + "outgoingRequests": { + "appnexus": { + "mockResponse": { + "pbsSeatBid": { + "pbsBids": [ + { + "ortbBid": { + "id": "winning-bid", + "impid": "my-imp-id", + "price": 0.01, + "w": 200, + "h": 250, + "crid": "creative-1" + }, + "bidType": "banner" + } + ] + } + } + } + }, + "response": { + "bids": { + "id": "some-request-id", + "seatbid": [ + { + "seat": "appnexus", + "bid": [{ + "id": "winning-bid", + "impid": "my-imp-id", + "price": 0.01, + "w": 200, + "h": 250, + "crid": "creative-1", + "ext": { + "prebid": { + "type": "banner", + "targeting": { + "hb_bidder": "appnexus", + "hb_bidder_appnexus": "appnexus", + "hb_pb": "0.00", + "hb_pb_appnexus": "0.00", + "hb_size": "200x250", + "hb_size_appnexus": "200x250", + "hb_creative_loadtype": "html" + } + } + } + }] + } + ] + } + } +} diff --git a/exchange/exchangetest/targeting-cache-vast.json b/exchange/exchangetest/targeting-cache-vast.json new file mode 100644 index 00000000000..a7bb4b46482 --- /dev/null +++ b/exchange/exchangetest/targeting-cache-vast.json @@ -0,0 +1,86 @@ +{ + "incomingRequest": { + "ortbRequest": { + "id": "some-request-id", + "site": { + "page": "test.somepage.com" + }, + "imp": [ + { + "id": "my-imp-id", + "video": { + "mimes": ["video/mp4"] + }, + "ext": { + "appnexus": { + "placementId": 1 + } + } + } + ], + "ext": { + "prebid": { + "cache": { + "vastxml": {} + }, + "targeting": {} + } + } + } + }, + "outgoingRequests": { + "appnexus": { + "mockResponse": { + "pbsSeatBid": { + "pbsBids": [ + { + "ortbBid": { + "id": "winning-bid", + "impid": "my-imp-id", + "price": 0.01, + "w": 200, + "h": 250, + "crid": "creative-1" + }, + "bidType": "video" + } + ] + } + } + } + }, + "response": { + "bids": { + "id": "some-request-id", + "seatbid": [ + { + "seat": "appnexus", + "bid": [{ + "id": "winning-bid", + "impid": "my-imp-id", + "price": 0.01, + "w": 200, + "h": 250, + "crid": "creative-1", + "ext": { + "prebid": { + "type": "video", + "targeting": { + "hb_bidder": "appnexus", + "hb_bidder_appnexus": "appnexus", + "hb_uuid": "0", + "hb_uuid_appnexus": "0", + "hb_pb": "0.00", + "hb_pb_appnexus": "0.00", + "hb_size": "200x250", + "hb_size_appnexus": "200x250", + "hb_creative_loadtype": "html" + } + } + } + }] + } + ] + } + } +} diff --git a/exchange/exchangetest/targeting-cache-zero.json b/exchange/exchangetest/targeting-cache-zero.json index 5c21f914eda..004611f4615 100644 --- a/exchange/exchangetest/targeting-cache-zero.json +++ b/exchange/exchangetest/targeting-cache-zero.json @@ -23,7 +23,8 @@ "ext": { "prebid": { "cache": { - "bids": {} + "bids": {}, + "vastxml": {} }, "targeting": {} } @@ -72,6 +73,8 @@ "hb_bidder_appnexus": "appnexus", "hb_cache_id": "0", "hb_cache_id_appnexus": "0", + "hb_uuid": "1", + "hb_uuid_appnexus": "1", "hb_pb": "0.00", "hb_pb_appnexus": "0.00", "hb_size": "200x250", diff --git a/exchange/targeting.go b/exchange/targeting.go index 8f66d6fec90..e74d4499cc4 100644 --- a/exchange/targeting.go +++ b/exchange/targeting.go @@ -20,7 +20,8 @@ type targetData struct { priceGranularity openrtb_ext.PriceGranularity includeWinners bool includeBidderKeys bool - includeCache bool + includeCacheBids bool + includeCacheVast bool } // setTargeting writes all the targeting params into the bids. @@ -43,8 +44,11 @@ func (targData *targetData) setTargeting(auc *auction, isApp bool) { if hbSize := makeHbSize(topBidPerBidder.bid); hbSize != "" { targData.addKeys(targets, openrtb_ext.HbSizeConstantKey, hbSize, bidderName, isOverallWinner) } - if cacheId, ok := auc.cacheIds[topBidPerBidder.bid]; ok { - targData.addKeys(targets, openrtb_ext.HbCacheKey, cacheId, bidderName, isOverallWinner) + if cacheID, ok := auc.cacheIds[topBidPerBidder.bid]; ok { + targData.addKeys(targets, openrtb_ext.HbCacheKey, cacheID, bidderName, isOverallWinner) + } + if vastID, ok := auc.vastCacheIds[topBidPerBidder.bid]; ok { + targData.addKeys(targets, openrtb_ext.HbVastCacheKey, vastID, bidderName, isOverallWinner) } if deal := topBidPerBidder.bid.DealID; len(deal) > 0 { targData.addKeys(targets, openrtb_ext.HbDealIdConstantKey, deal, bidderName, isOverallWinner) diff --git a/openrtb_ext/bid.go b/openrtb_ext/bid.go index 84bb02bfefe..453f8aa2a4e 100644 --- a/openrtb_ext/bid.go +++ b/openrtb_ext/bid.go @@ -87,9 +87,14 @@ const ( // Other demand sources are happy to let Prebid Mobile use a Webview. HbCreativeLoadMethodConstantKey TargetingKey = "hb_creative_loadtype" HbDealIdConstantKey TargetingKey = "hb_deal" - // HbCacheKey stores the UUID which can be used to fetch the bid data from prebid cache. - // Callers should *never* assume that this exists, since the call to the cache may always fail. - HbCacheKey TargetingKey = "hb_cache_id" + + // HbCacheKey and HbVastCacheKey store UUIDs which can be used to fetch things from prebid cache. + // Callers should *never* assume that either of these exist, since the call to the cache may always fail. + // + // HbVastCacheKey's UUID will fetch the entire bid JSON, while HbVastCacheKey will fetch just the VAST XML. + // HbVastCacheKey will only ever exist for Video bids. + HbCacheKey TargetingKey = "hb_cache_id" + HbVastCacheKey TargetingKey = "hb_uuid" // These are not keys, but values used by hbCreativeLoadMethodConstantKey HbCreativeLoadMethodHTML string = "html" diff --git a/openrtb_ext/request.go b/openrtb_ext/request.go index 0e8fffcf43a..c0175364f16 100644 --- a/openrtb_ext/request.go +++ b/openrtb_ext/request.go @@ -21,7 +21,8 @@ type ExtRequestPrebid struct { // ExtRequestPrebidCache defines the contract for bidrequest.ext.prebid.cache type ExtRequestPrebidCache struct { - Bids *ExtRequestPrebidCacheBids `json:"bids"` + Bids *ExtRequestPrebidCacheBids `json:"bids"` + VastXML *ExtRequestPrebidCacheVAST `json:"vastxml"` } // UnmarshalJSON prevents nil bids arguments. @@ -32,8 +33,8 @@ func (ert *ExtRequestPrebidCache) UnmarshalJSON(b []byte) error { return err } - if proxy.Bids == nil { - return errors.New(`request.ext.prebid.cache missing required property "bids"`) + if proxy.Bids == nil && proxy.VastXML == nil { + return errors.New(`request.ext.prebid.cache requires one of "bids" or "vastml" to be defined`) } *ert = ExtRequestPrebidCache(proxy) @@ -43,6 +44,9 @@ func (ert *ExtRequestPrebidCache) UnmarshalJSON(b []byte) error { // ExtRequestPrebidCacheBids defines the contract for bidrequest.ext.prebid.cache.bids type ExtRequestPrebidCacheBids struct{} +// ExtRequestPrebidCacheVAST defines the contract for bidrequest.ext.prebid.cache.vastxml +type ExtRequestPrebidCacheVAST struct{} + // ExtRequestTargeting defines the contract for bidrequest.ext.prebid.targeting type ExtRequestTargeting struct { PriceGranularity PriceGranularity `json:"pricegranularity"` diff --git a/openrtb_ext/request_test.go b/openrtb_ext/request_test.go index 7dac44729e7..860334af98f 100644 --- a/openrtb_ext/request_test.go +++ b/openrtb_ext/request_test.go @@ -4,6 +4,8 @@ import ( "encoding/json" "reflect" "testing" + + "github.com/stretchr/testify/assert" ) // Test the unmashalling of the prebid extensions and setting default Price Granularity @@ -80,14 +82,23 @@ func TestCacheIllegal(t *testing.T) { } } -func TestCacheLegal(t *testing.T) { +func TestCacheBids(t *testing.T) { var bids ExtRequestPrebidCache - if err := json.Unmarshal([]byte(`{"bids":{}}`), &bids); err != nil { - t.Error("Unmarshal should succeed when cache.bids is defined.") - } - if bids.Bids == nil { - t.Error("bids.Bids should not be nil.") - } + assert.NoError(t, json.Unmarshal([]byte(`{"bids":{}}`), &bids)) + assert.NotNil(t, bids.Bids) + assert.Nil(t, bids.VastXML) +} + +func TestCacheVast(t *testing.T) { + var bids ExtRequestPrebidCache + assert.NoError(t, json.Unmarshal([]byte(`{"vastxml":{}}`), &bids)) + assert.Nil(t, bids.Bids) + assert.NotNil(t, bids.VastXML) +} + +func TestCacheNothing(t *testing.T) { + var bids ExtRequestPrebidCache + assert.Error(t, json.Unmarshal([]byte(`{}`), &bids)) } type granularityTestData struct { diff --git a/prebid_cache_client/client.go b/prebid_cache_client/client.go index 15561e81f58..da08c1bd979 100644 --- a/prebid_cache_client/client.go +++ b/prebid_cache_client/client.go @@ -4,12 +4,13 @@ import ( "bytes" "context" "encoding/json" + "io/ioutil" + "net/http" + "github.com/buger/jsonparser" "github.com/golang/glog" "github.com/prebid/prebid-server/config" "golang.org/x/net/context/ctxhttp" - "io/ioutil" - "net/http" ) // Client stores values in Prebid Cache. For more info, see https://github.com/prebid/prebid-cache @@ -19,7 +20,19 @@ type Client interface { // The returned string slice will always have the same number of elements as the values argument. If a // value could not be saved, the element will be an empty string. Implementations are responsible for // logging any relevant errors to the app logs - PutJson(ctx context.Context, values []json.RawMessage) []string + PutJson(ctx context.Context, values []Cacheable) []string +} + +type PayloadType string + +const ( + TypeJSON PayloadType = "json" + TypeXML PayloadType = "xml" +) + +type Cacheable struct { + Type PayloadType + Data json.RawMessage } func NewClient(conf *config.Cache) Client { @@ -39,7 +52,7 @@ type clientImpl struct { putUrl string } -func (c *clientImpl) PutJson(ctx context.Context, values []json.RawMessage) (uuids []string) { +func (c *clientImpl) PutJson(ctx context.Context, values []Cacheable) (uuids []string) { if len(values) < 1 { return nil } @@ -95,7 +108,7 @@ func (c *clientImpl) PutJson(ctx context.Context, values []json.RawMessage) (uui return uuidsToReturn } -func encodeValues(values []json.RawMessage) ([]byte, error) { +func encodeValues(values []Cacheable) ([]byte, error) { // This function assumes that m is non-nil and has at least one element. // clientImp.PutBids should respect this. var buf bytes.Buffer @@ -109,18 +122,15 @@ func encodeValues(values []json.RawMessage) ([]byte, error) { return buf.Bytes(), nil } -func encodeValueToBuffer(value json.RawMessage, leadingComma bool, buffer *bytes.Buffer) error { +func encodeValueToBuffer(value Cacheable, leadingComma bool, buffer *bytes.Buffer) error { if leadingComma { buffer.WriteByte(',') } - encodedBytes, err := json.Marshal(value) - if err != nil { - return err - } else { - buffer.WriteString(`{"type":"json","value":`) - buffer.Write(encodedBytes) - buffer.WriteByte('}') - } + buffer.WriteString(`{"type":"`) + buffer.WriteString(string(value.Type)) + buffer.WriteString(`","value":`) + buffer.Write(value.Data) + buffer.WriteByte('}') return nil } diff --git a/prebid_cache_client/client_test.go b/prebid_cache_client/client_test.go index 4fd2a27a5b5..f72e2d08d96 100644 --- a/prebid_cache_client/client_test.go +++ b/prebid_cache_client/client_test.go @@ -23,7 +23,7 @@ func TestEmptyPut(t *testing.T) { } ids := client.PutJson(context.Background(), nil) assertIntEqual(t, len(ids), 0) - ids = client.PutJson(context.Background(), []json.RawMessage{}) + ids = client.PutJson(context.Background(), []Cacheable{}) assertIntEqual(t, len(ids), 0) } @@ -38,7 +38,15 @@ func TestBadResponse(t *testing.T) { httpClient: server.Client(), putUrl: server.URL, } - ids := client.PutJson(context.Background(), []json.RawMessage{json.RawMessage("true"), json.RawMessage("false")}) + ids := client.PutJson(context.Background(), []Cacheable{ + Cacheable{ + Type: TypeJSON, + Data: json.RawMessage("true"), + }, Cacheable{ + Type: TypeJSON, + Data: json.RawMessage("false"), + }, + }) assertIntEqual(t, len(ids), 2) assertStringEqual(t, ids[0], "") assertStringEqual(t, ids[1], "") @@ -58,7 +66,11 @@ func TestCancelledContext(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) cancel() - ids := client.PutJson(ctx, []json.RawMessage{json.RawMessage("true")}) + ids := client.PutJson(ctx, []Cacheable{Cacheable{ + Type: TypeJSON, + Data: json.RawMessage("true"), + }, + }) assertIntEqual(t, len(ids), 1) assertStringEqual(t, ids[0], "") } @@ -72,7 +84,15 @@ func TestSuccessfulPut(t *testing.T) { putUrl: server.URL, } - ids := client.PutJson(context.Background(), []json.RawMessage{json.RawMessage("true"), json.RawMessage("false")}) + ids := client.PutJson(context.Background(), []Cacheable{ + Cacheable{ + Type: TypeJSON, + Data: json.RawMessage("true"), + }, Cacheable{ + Type: TypeJSON, + Data: json.RawMessage("false"), + }, + }) assertIntEqual(t, len(ids), 2) assertStringEqual(t, ids[0], "0") assertStringEqual(t, ids[1], "1")