Skip to content

Commit 5a30170

Browse files
Implementation of the Start.io adapter
1 parent e0b7ed3 commit 5a30170

15 files changed

+2446
-0
lines changed

adapters/startio/startio.go

+179
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,179 @@
1+
package startio
2+
3+
import (
4+
"fmt"
5+
"net/http"
6+
"net/url"
7+
"slices"
8+
9+
"github.com/prebid/openrtb/v20/openrtb2"
10+
"github.com/prebid/prebid-server/v3/adapters"
11+
"github.com/prebid/prebid-server/v3/config"
12+
"github.com/prebid/prebid-server/v3/errortypes"
13+
"github.com/prebid/prebid-server/v3/openrtb_ext"
14+
"github.com/prebid/prebid-server/v3/util/jsonutil"
15+
)
16+
17+
type StartioAdapter struct {
18+
endpoint string
19+
}
20+
21+
func Builder(bidderName openrtb_ext.BidderName, config config.Adapter, server config.Server) (adapters.Bidder, error) {
22+
uri, err := url.ParseRequestURI(config.Endpoint)
23+
if err != nil {
24+
return nil, err
25+
}
26+
27+
bidder := &StartioAdapter{
28+
endpoint: uri.String(),
29+
}
30+
31+
return bidder, nil
32+
}
33+
34+
func (adapter *StartioAdapter) MakeRequests(request *openrtb2.BidRequest, reqInfo *adapters.ExtraRequestInfo) ([]*adapters.RequestData, []error) {
35+
var requests []*adapters.RequestData
36+
var errors []error
37+
requestCopy := *request
38+
39+
if err := validateRequest(requestCopy); err != nil {
40+
return nil, []error{err}
41+
}
42+
43+
validImpressions, err := getValidImpressions(requestCopy.Imp)
44+
if err != nil {
45+
return nil, []error{err}
46+
}
47+
for i := range validImpressions {
48+
requestCopy.Imp = []openrtb2.Imp{validImpressions[i]}
49+
50+
requestBody, err := jsonutil.Marshal(requestCopy)
51+
if err != nil {
52+
errors = append(errors, fmt.Errorf("imp[%d]: failed to marshal request: %w", i, err))
53+
continue
54+
}
55+
56+
requestData := &adapters.RequestData{
57+
Method: http.MethodPost,
58+
Uri: adapter.endpoint,
59+
Body: requestBody,
60+
Headers: buildRequestHeaders(),
61+
ImpIDs: []string{validImpressions[i].ID},
62+
}
63+
64+
requests = append(requests, requestData)
65+
}
66+
67+
return requests, errors
68+
}
69+
70+
func (adapter *StartioAdapter) MakeBids(internalRequest *openrtb2.BidRequest, externalRequest *adapters.RequestData, response *adapters.ResponseData) (*adapters.BidderResponse, []error) {
71+
if response.StatusCode == http.StatusNoContent {
72+
return nil, nil
73+
} else if response.StatusCode != http.StatusOK {
74+
return nil, []error{wrapReqError(fmt.Sprintf("Unexpected status code: %d.", response.StatusCode))}
75+
}
76+
77+
if err := adapters.CheckResponseStatusCodeForErrors(response); err != nil {
78+
return nil, []error{err}
79+
}
80+
81+
var bidResponse openrtb2.BidResponse
82+
if err := jsonutil.Unmarshal(response.Body, &bidResponse); err != nil {
83+
return nil, []error{wrapReqError(fmt.Sprintf("failed to unmarshal response body: %v", err))}
84+
}
85+
86+
var errs []error
87+
bidderResponse := adapters.NewBidderResponseWithBidsCapacity(len(bidResponse.SeatBid))
88+
89+
for i := range bidResponse.SeatBid {
90+
for j := range bidResponse.SeatBid[i].Bid {
91+
bid := &bidResponse.SeatBid[i].Bid[j]
92+
bidType, err := getMediaTypeForBid(*bid)
93+
94+
if err != nil {
95+
errs = append(errs, err)
96+
} else {
97+
bidderResponse.Bids = append(bidderResponse.Bids, &adapters.TypedBid{
98+
Bid: bid,
99+
BidType: bidType,
100+
})
101+
}
102+
}
103+
}
104+
105+
return bidderResponse, errs
106+
}
107+
108+
func validateRequest(request openrtb2.BidRequest) error {
109+
if !isSupportedCurrency(request.Cur) {
110+
return wrapReqError("unsupported currency: only USD is accepted")
111+
}
112+
113+
if !hasSiteOrAppID(request) {
114+
return wrapReqError("request must contain either site.id or app.id")
115+
}
116+
117+
return nil
118+
}
119+
120+
func getValidImpressions(imps []openrtb2.Imp) ([]openrtb2.Imp, error) {
121+
var validImpressions []openrtb2.Imp
122+
123+
for _, imp := range imps {
124+
imp.Audio = nil
125+
hasValidMedia := imp.Banner != nil || imp.Video != nil || imp.Native != nil
126+
if hasValidMedia {
127+
validImpressions = append(validImpressions, imp)
128+
}
129+
}
130+
131+
if len(validImpressions) == 0 {
132+
return nil, wrapReqError("invalid bid request: at least one banner, video, or native impression is required.")
133+
}
134+
135+
return validImpressions, nil
136+
}
137+
138+
func hasSiteOrAppID(req openrtb2.BidRequest) bool {
139+
return (req.Site != nil && req.Site.ID != "") || (req.App != nil && req.App.ID != "")
140+
}
141+
142+
func buildRequestHeaders() http.Header {
143+
headers := http.Header{}
144+
headers.Add("Content-Type", "application/json;charset=utf-8")
145+
headers.Add("Accept", "application/json")
146+
headers.Add("X-Openrtb-Version", "2.5")
147+
148+
return headers
149+
}
150+
151+
func isSupportedCurrency(currencies []string) bool {
152+
return len(currencies) == 0 || slices.Contains(currencies, "USD")
153+
}
154+
155+
func getMediaTypeForBid(bid openrtb2.Bid) (openrtb_ext.BidType, error) {
156+
157+
if bid.Ext != nil {
158+
var bidExt openrtb_ext.ExtBid
159+
err := jsonutil.Unmarshal(bid.Ext, &bidExt)
160+
if err == nil && bidExt.Prebid != nil {
161+
switch bidExt.Prebid.Type {
162+
case "banner":
163+
return openrtb_ext.BidTypeBanner, nil
164+
case "video":
165+
return openrtb_ext.BidTypeVideo, nil
166+
case "native":
167+
return openrtb_ext.BidTypeNative, nil
168+
}
169+
}
170+
}
171+
172+
return "", &errortypes.BadServerResponse{
173+
Message: fmt.Sprintf("Failed to parse bid media type for impression %s.", bid.ImpID),
174+
}
175+
}
176+
177+
func wrapReqError(errorStr string) *errortypes.BadInput {
178+
return &errortypes.BadInput{Message: errorStr}
179+
}

0 commit comments

Comments
 (0)