Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Loyal: New Adapter (#3140) #3183

Merged
merged 26 commits into from
Jun 3, 2024
Merged
Show file tree
Hide file tree
Changes from 22 commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
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
171 changes: 171 additions & 0 deletions src/main/java/org/prebid/server/bidder/loyal/LoyalBidder.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
package org.prebid.server.bidder.loyal;

import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.iab.openrtb.request.BidRequest;
import com.iab.openrtb.request.Imp;
import com.iab.openrtb.response.Bid;
import com.iab.openrtb.response.BidResponse;
import com.iab.openrtb.response.SeatBid;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.lang3.StringUtils;
import org.prebid.server.bidder.Bidder;
import org.prebid.server.bidder.loyal.proto.LoyalImpExt;
import org.prebid.server.bidder.model.BidderBid;
import org.prebid.server.bidder.model.BidderCall;
import org.prebid.server.bidder.model.BidderError;
import org.prebid.server.bidder.model.HttpRequest;
import org.prebid.server.bidder.model.Result;
import org.prebid.server.exception.PreBidException;
import org.prebid.server.json.DecodeException;
import org.prebid.server.json.JacksonMapper;
import org.prebid.server.proto.openrtb.ext.ExtPrebid;
import org.prebid.server.proto.openrtb.ext.request.loyal.ExtImpLoyal;
import org.prebid.server.proto.openrtb.ext.response.BidType;
import org.prebid.server.util.BidderUtil;
import org.prebid.server.util.HttpUtil;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import java.util.Optional;

public class LoyalBidder implements Bidder<BidRequest> {

private static final TypeReference<ExtPrebid<?, ExtImpLoyal>> LOYAL_EXT_TYPE_REFERENCE =
new TypeReference<>() {
};

private static final String PUBLISHER_PROPERTY = "publisher";
private static final String NETWORK_PROPERTY = "network";
private static final String BIDDER_PROPERTY = "bidder";

private final String endpointUrl;
private final JacksonMapper mapper;

public LoyalBidder(String endpointUrl, JacksonMapper mapper) {
this.endpointUrl = HttpUtil.validateUrl(Objects.requireNonNull(endpointUrl));
this.mapper = Objects.requireNonNull(mapper);
}

@Override
public Result<List<HttpRequest<BidRequest>>> makeHttpRequests(BidRequest request) {
final List<BidderError> errors = new ArrayList<>();
final List<HttpRequest<BidRequest>> httpRequests = new ArrayList<>();

for (Imp imp : request.getImp()) {
try {
final ExtImpLoyal extImpLoyal = parseExtImp(imp);
if (isValidImp(extImpLoyal)) {
Copy link
Collaborator

Choose a reason for hiding this comment

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

no sure we need this check, one of those field are required by the json scheme

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

You said:
AntoxaAntoxic 2 weeks ago
the following logic is missing

if len(adapterRequests) == 0 {
	errs = append(errs, errors.New("found no valid impressions"))
	return nil, errs
}

and this is part of this

Copy link
Collaborator

Choose a reason for hiding this comment

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

and you've implemented it already a few lines below

        if (httpRequests.isEmpty()) {
            return Result.withError(BidderError.badInput("found no valid impressions"));
        }

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

when I have deleted it "invalidImp goes to the results". We have to seperate invalidImp
image

Copy link
Collaborator

Choose a reason for hiding this comment

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

so this is the Go code

if loyalExt.PlacementID != "" {
			impExt.LoyalBidderExt.PlacementID = loyalExt.PlacementID
			impExt.LoyalBidderExt.Type = "publisher"
		} else if loyalExt.EndpointID != "" {
			impExt.LoyalBidderExt.EndpointID = loyalExt.EndpointID
			impExt.LoyalBidderExt.Type = "network"
		}

as you see the imp that doesn't have the placementId and endpointId fields won't be treated as invalid, but you've added the unit test that defines invalid bid as the following:

private static Imp givenBadImp(UnaryOperator<Imp.ImpBuilder> impCustomizer) {
        return impCustomizer.apply(Imp.builder()
                        .id("invalidImp")
                        .ext(mapper.valueToTree(ExtPrebid.of(null,
                                ExtImpLoyal.of(null, null)))))
                .build();
    }

So my conclusion the logic of your version of the bidder differs from the GO's version

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

okey, now its clear. I hade made wrong badImp, I was doing it in wrong way.

final Imp modifiedImp = modifyImp(imp, extImpLoyal);
httpRequests.add(makeHttpRequest(request, modifiedImp));
} else {
errors.add(BidderError.badInput("Invalid imp: " + imp.getId()));
}
} catch (PreBidException e) {
errors.add(BidderError.badInput(e.getMessage()));
}
}

if (httpRequests.isEmpty()) {
return Result.withError(BidderError.badInput("found no valid impressions"));
}

return Result.of(httpRequests, errors);
}

private ExtImpLoyal parseExtImp(Imp imp) {
try {
return mapper.mapper().convertValue(imp.getExt(), LOYAL_EXT_TYPE_REFERENCE).getBidder();
} catch (IllegalArgumentException e) {
throw new PreBidException("Cannot deserialize ExtImpLoyal: " + e.getMessage());
}
}

private boolean isValidImp(ExtImpLoyal extImpLoyal) {
return StringUtils.isNotEmpty(extImpLoyal.getPlacementId())
|| StringUtils.isNotEmpty(extImpLoyal.getEndpointId());
}

private Imp modifyImp(Imp imp, ExtImpLoyal extImpLoyal) {
final LoyalImpExt impExtLoyalWithType = resolveImpExt(extImpLoyal);
final ObjectNode modifiedImpExtBidder = mapper.mapper().createObjectNode();
modifiedImpExtBidder.set(BIDDER_PROPERTY, mapper.mapper().valueToTree(impExtLoyalWithType));

return imp.toBuilder().ext(modifiedImpExtBidder).build();
}

private LoyalImpExt resolveImpExt(ExtImpLoyal extImpLoyal) {
final LoyalImpExt.LoyalImpExtBuilder builder = LoyalImpExt.builder();

if (StringUtils.isNotEmpty(extImpLoyal.getPlacementId())) {
builder.type(PUBLISHER_PROPERTY).placementId(extImpLoyal.getPlacementId());
} else if (StringUtils.isNotEmpty(extImpLoyal.getEndpointId())) {
builder.type(NETWORK_PROPERTY).endpointId(extImpLoyal.getEndpointId());
}

return builder.build();
}

private HttpRequest<BidRequest> makeHttpRequest(BidRequest request, Imp imp) {
final BidRequest outgoingRequest = request.toBuilder().imp(List.of(imp)).build();

return BidderUtil.defaultRequest(outgoingRequest, endpointUrl, mapper);
}

@Override
public Result<List<BidderBid>> makeBids(BidderCall<BidRequest> httpCall, BidRequest bidRequest) {
try {
final BidResponse bidResponse = mapper.decodeValue(httpCall.getResponse().getBody(), BidResponse.class);
final List<BidderBid> bids = extractBids(httpCall.getRequest().getPayload(), bidResponse);
return Result.withValues(bids);
} catch (DecodeException | PreBidException e) {
return Result.withError(BidderError.badServerResponse(e.getMessage()));
}
}

private List<BidderBid> extractBids(BidRequest bidRequest, BidResponse bidResponse) {
if (bidResponse == null || CollectionUtils.isEmpty(bidResponse.getSeatbid())) {
return Collections.emptyList();
}

return bidResponse.getSeatbid().stream()
.filter(Objects::nonNull)
.map(SeatBid::getBid)
.filter(Objects::nonNull)
.flatMap(Collection::stream)
.filter(Objects::nonNull)
.map(bid -> BidderBid.of(bid, getBidType(bid), bidResponse.getCur()))
.toList();
}

private BidType getBidType(Bid bid) {
final JsonNode typeNode = Optional.ofNullable(bid.getExt())
.map(extNode -> extNode.get("prebid"))
.map(extPrebidNode -> extPrebidNode.get("type"))
.orElse(null);

final BidType bidType;
try {
bidType = mapper.mapper().convertValue(typeNode, BidType.class);
} catch (IllegalArgumentException e) {
throw new PreBidException("Failed to parse bid.ext.prebid.type for bid.id: '%s'"
.formatted(bid.getId()));
}

if (bidType == null) {
throw new PreBidException("bid.ext.prebid.type is not present for bid.id: '%s'"
.formatted(bid.getId()));
}

return switch (bidType) {
case banner, video, xNative -> bidType;
default -> throw new PreBidException("Unsupported BidType: "
+ bidType.getName() + " for bid.id: '" + bid.getId() + "'");
};
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package org.prebid.server.bidder.loyal.proto;

import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Builder;
import lombok.Value;

@Builder
@Value
public class LoyalImpExt {

@JsonProperty("type")
String type;

@JsonProperty("placementId")
String placementId;

@JsonProperty("endpointId")
String endpointId;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package org.prebid.server.proto.openrtb.ext.request.loyal;

import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Value;

@Value(staticConstructor = "of")
public class ExtImpLoyal {

@JsonProperty("placementId")
String placementId;

@JsonProperty("endpointId")
String endpointId;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package org.prebid.server.spring.config.bidder;

import org.prebid.server.bidder.BidderDeps;
import org.prebid.server.bidder.loyal.LoyalBidder;
import org.prebid.server.json.JacksonMapper;
import org.prebid.server.spring.config.bidder.model.BidderConfigurationProperties;
import org.prebid.server.spring.config.bidder.util.BidderDepsAssembler;
import org.prebid.server.spring.config.bidder.util.UsersyncerCreator;
import org.prebid.server.spring.env.YamlPropertySourceFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.PropertySource;

import jakarta.validation.constraints.NotBlank;

@Configuration
@PropertySource(value = "classpath:/bidder-config/loyal.yaml", factory = YamlPropertySourceFactory.class)
public class LoyalConfiguration {

private static final String BIDDER_NAME = "loyal";

@Bean("loyalConfigurationProperties")
@ConfigurationProperties("adapters.loyal")
BidderConfigurationProperties configurationProperties() {
return new BidderConfigurationProperties();
}

@Bean
BidderDeps loaylBidderDeps(BidderConfigurationProperties loyalConfigurationProperties,
@NotBlank @Value("${external-url}") String externalUrl,
JacksonMapper mapper) {

return BidderDepsAssembler.forBidder(BIDDER_NAME)
.withConfig(loyalConfigurationProperties)
.usersyncerCreator(UsersyncerCreator.create(externalUrl))
.bidderCreator(config -> new LoyalBidder(config.getEndpoint(), mapper))
.assemble();
}
}
17 changes: 17 additions & 0 deletions src/main/resources/bidder-config/loyal.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
adapters:
loyal:
endpoint: "https://us-east-1.loyal.app/pserver"
geoscope:
- USA
meta-info:
maintainer-email: "hello@loyal.app"
app-media-types:
- banner
- video
- native
site-media-types:
- banner
- video
- native
supported-vendors: []
vendor-id: 0
30 changes: 30 additions & 0 deletions src/main/resources/static/bidder-params/loyal.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
{
"$schema": "http://json-schema.org/draft-04/schema#",
"title": "Loyal Adapter Params",
"description": "A schema which validates params accepted by the Loyal adapter",
"type": "object",
"properties": {
"placementId": {
"type": "string",
"minLength": 1,
"description": "Placement ID"
},
"endpointId": {
"type": "string",
"minLength": 1,
"description": "Endpoint ID"
}
},
"oneOf": [
{
"required": [
"placementId"
]
},
{
"required": [
"endpointId"
]
}
]
}
Loading
Loading