Skip to content

Commit 8070000

Browse files
authored
Rules Engine Module: Dynamic ruleset from YAML geoscopes (#4509)
1 parent 366b715 commit 8070000

18 files changed

+2656
-312
lines changed

config/bidderinfo.go

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ type BidderInfo struct {
3232
Capabilities *CapabilitiesInfo `yaml:"capabilities" mapstructure:"capabilities"`
3333
ModifyingVastXmlAllowed bool `yaml:"modifyingVastXmlAllowed" mapstructure:"modifyingVastXmlAllowed"`
3434
Debug *DebugInfo `yaml:"debug" mapstructure:"debug"`
35+
Geoscope []string `yaml:"geoscope" mapstructure:"geoscope"`
3536
GVLVendorID uint16 `yaml:"gvlVendorID" mapstructure:"gvlVendorID"`
3637

3738
Syncer *Syncer `yaml:"userSync" mapstructure:"userSync"`
@@ -475,6 +476,9 @@ func validateInfo(bidder BidderInfo, infos BidderInfos, bidderName string) error
475476
if err := validateMaintainer(bidder.Maintainer, bidderName); err != nil {
476477
return err
477478
}
479+
if err := validateGeoscope(bidder.Geoscope, bidderName); err != nil {
480+
return err
481+
}
478482
if err := validateCapabilities(bidder.Capabilities, bidderName); err != nil {
479483
return err
480484
}
@@ -591,6 +595,38 @@ func validatePlatformInfo(info *PlatformInfo) error {
591595
return nil
592596
}
593597

598+
func validateGeoscope(geoscope []string, bidderName string) error {
599+
// ISO 3166-1 alpha-3 country codes are uppercase 3-letter codes
600+
for i, code := range geoscope {
601+
code = strings.ToUpper(strings.TrimSpace(code))
602+
603+
if code == "GLOBAL" || code == "EEA" {
604+
continue
605+
}
606+
607+
// Handle exclusion pattern with "!" prefix
608+
exclusion := ""
609+
if strings.HasPrefix(code, "!") {
610+
exclusion = "!"
611+
code = code[1:]
612+
}
613+
614+
if len(code) != 3 {
615+
return fmt.Errorf("invalid geoscope entry at index %d: %s for adapter: %s%s - must be a 3-letter ISO 3166-1 alpha-3 country code",
616+
i, code, exclusion, bidderName)
617+
}
618+
619+
for _, char := range code {
620+
if char < 'A' || char > 'Z' {
621+
return fmt.Errorf("invalid geoscope entry at index %d: %s for adapter: %s%s - must contain only uppercase letters A-Z",
622+
i, code, exclusion, bidderName)
623+
}
624+
}
625+
}
626+
627+
return nil
628+
}
629+
594630
func validateSyncer(bidderInfo BidderInfo) error {
595631
if bidderInfo.Syncer == nil {
596632
return nil

config/bidderinfo_test.go

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2008,3 +2008,97 @@ func TestReadFullYamlBidderConfig(t *testing.T) {
20082008
}
20092009
assert.Equalf(t, expectedBidderInfo, actualBidderInfo, "Bidder info objects aren't matching")
20102010
}
2011+
2012+
func TestValidateGeoscope(t *testing.T) {
2013+
testCases := []struct {
2014+
name string
2015+
geoscope []string
2016+
bidderName string
2017+
expectErr bool
2018+
errMsg string
2019+
}{
2020+
{
2021+
name: "nil",
2022+
geoscope: nil,
2023+
bidderName: "testBidder",
2024+
expectErr: false,
2025+
},
2026+
{
2027+
name: "empty",
2028+
geoscope: []string{},
2029+
bidderName: "testBidder",
2030+
expectErr: false,
2031+
},
2032+
{
2033+
name: "valid-iso-code",
2034+
geoscope: []string{"USA"},
2035+
bidderName: "testBidder",
2036+
expectErr: false,
2037+
},
2038+
{
2039+
name: "valid-with-global-and-eea",
2040+
geoscope: []string{"USA", "GLOBAL", "EEA"},
2041+
bidderName: "testBidder",
2042+
expectErr: false,
2043+
},
2044+
{
2045+
name: "valid-with-exclusion",
2046+
geoscope: []string{"!USA"},
2047+
bidderName: "testBidder",
2048+
expectErr: false,
2049+
},
2050+
{
2051+
name: "mixed-case-valid",
2052+
geoscope: []string{"UsA", "can", "GbR"},
2053+
bidderName: "testBidder",
2054+
expectErr: false,
2055+
},
2056+
{
2057+
name: "invalid-length",
2058+
geoscope: []string{"USAA"},
2059+
bidderName: "testBidder",
2060+
expectErr: true,
2061+
errMsg: "invalid geoscope entry at index 0: USAA for adapter: testBidder - must be a 3-letter ISO 3166-1 alpha-3 country code",
2062+
},
2063+
{
2064+
name: "invalid-exclusion-length",
2065+
geoscope: []string{"!USAA"},
2066+
bidderName: "testBidder",
2067+
expectErr: true,
2068+
errMsg: "invalid geoscope entry at index 0: USAA for adapter: !testBidder - must be a 3-letter ISO 3166-1 alpha-3 country code",
2069+
},
2070+
{
2071+
name: "non-letter-characters",
2072+
geoscope: []string{"US1"},
2073+
bidderName: "testBidder",
2074+
expectErr: true,
2075+
errMsg: "invalid geoscope entry at index 0: US1 for adapter: testBidder - must contain only uppercase letters A-Z",
2076+
},
2077+
{
2078+
name: "too-short-code",
2079+
geoscope: []string{"US"},
2080+
bidderName: "testBidder",
2081+
expectErr: true,
2082+
errMsg: "invalid geoscope entry at index 0: US for adapter: testBidder - must be a 3-letter ISO 3166-1 alpha-3 country code",
2083+
},
2084+
{
2085+
name: "whitespace-and-trimming",
2086+
geoscope: []string{" USA "},
2087+
bidderName: "testBidder",
2088+
expectErr: false,
2089+
},
2090+
}
2091+
2092+
for _, tc := range testCases {
2093+
t.Run(tc.name, func(t *testing.T) {
2094+
err := validateGeoscope(tc.geoscope, tc.bidderName)
2095+
2096+
if tc.expectErr {
2097+
assert.Error(t, err)
2098+
assert.Contains(t, err.Error(), tc.errMsg)
2099+
} else {
2100+
assert.NoError(t, err)
2101+
}
2102+
})
2103+
}
2104+
}

modules/moduledeps/deps.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,4 +11,5 @@ import (
1111
type ModuleDeps struct {
1212
HTTPClient *http.Client
1313
RateConvertor *currency.RateConverter
14+
Geoscope map[string][]string
1415
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
package rulesengine
2+
3+
import (
4+
"github.com/prebid/prebid-server/v3/rules"
5+
)
6+
7+
// buildBidderConfigRuleSet builds a dynamic ruleset based on the geoscope annotations in the
8+
// static bidder-info bidder YAML files
9+
func buildBidderConfigRuleSet(geoscopes map[string][]string, setDefinitions map[string][]string) ([]cacheRuleSet[RequestWrapper, ProcessedAuctionHookResult], error) {
10+
crs := cacheRuleSet[RequestWrapper, ProcessedAuctionHookResult]{
11+
name: "Dynamic ruleset from geoscopes",
12+
modelGroups: []cacheModelGroup[RequestWrapper, ProcessedAuctionHookResult]{
13+
{
14+
weight: 100,
15+
version: "1.0",
16+
analyticsKey: "bidderConfig",
17+
},
18+
},
19+
}
20+
21+
builder := NewBidderConfigRuleSetBuilder[RequestWrapper, ProcessedAuctionHookResult](geoscopes, setDefinitions)
22+
23+
tree, err := rules.NewTree[RequestWrapper, ProcessedAuctionHookResult](builder)
24+
if err != nil {
25+
return nil, err
26+
}
27+
crs.modelGroups[0].tree = *tree
28+
29+
return []cacheRuleSet[RequestWrapper, ProcessedAuctionHookResult]{crs}, nil
30+
}

0 commit comments

Comments
 (0)