From ada36e197bc644d9386a4a92f7b1d63fc00ae9a8 Mon Sep 17 00:00:00 2001 From: Andrew Paseltiner Date: Thu, 13 Jun 2024 14:02:33 -0400 Subject: [PATCH 1/2] Remove Maybe.flatten function (#1337) It was unsound because it could be called with a Maybe but its type parameter T instantiated with Maybe, resulting in an apparent return type of Maybe>, but the instanceof check would cause the original Maybe to be returned. For example, the following code logs "number" despite y's type indicating that its value should be another Maybe: ```ts const x = Maybe.some(5) const y: Maybe> = Maybe.flatten>(x) console.log(typeof y.value) ``` --- ts/src/header-validator/maybe.ts | 17 +++++----- ts/src/header-validator/validate-json.ts | 33 ++++++++++--------- ts/src/header-validator/validate.ts | 42 ++++++++++++++++++++---- 3 files changed, 62 insertions(+), 30 deletions(-) diff --git a/ts/src/header-validator/maybe.ts b/ts/src/header-validator/maybe.ts index d879f631fe..3db94c5595 100644 --- a/ts/src/header-validator/maybe.ts +++ b/ts/src/header-validator/maybe.ts @@ -1,5 +1,3 @@ -export type Maybeable = T | Maybe - export class Maybe { static readonly None = new Maybe() @@ -7,10 +5,6 @@ export class Maybe { return new Maybe(t) } - static flatten(this: void, t: Maybeable): Maybe { - return t instanceof Maybe ? t : Maybe.some(t) - } - private constructor(private readonly t?: T) {} filter( @@ -21,10 +15,17 @@ export class Maybe { } map( - f: (t: T, ...args: C) => Maybeable, + f: (t: T, ...args: C) => U, + ...args: C + ): Maybe { + return this.t === undefined ? Maybe.None : new Maybe(f(this.t, ...args)) + } + + flatMap( + f: (t: T, ...args: C) => Maybe, ...args: C ): Maybe { - return this.t === undefined ? Maybe.None : Maybe.flatten(f(this.t, ...args)) + return this.t === undefined ? Maybe.None : f(this.t, ...args) } peek(f: (t: T, ...args: C) => void, ...args: C): this { diff --git a/ts/src/header-validator/validate-json.ts b/ts/src/header-validator/validate-json.ts index 1e92f084ae..75669cc910 100644 --- a/ts/src/header-validator/validate-json.ts +++ b/ts/src/header-validator/validate-json.ts @@ -64,6 +64,7 @@ class SourceContext extends RegistrationContext { const { exclusive, field, + fieldMaybeDefault, struct: structInternal, } = validate.make( /*getAndDelete=*/ (d, name) => { @@ -86,7 +87,7 @@ function struct( fields: StructFields, warnUnknown: boolean = true ): Maybe { - return object(d, ctx).map(structInternal, ctx, fields, warnUnknown) + return object(d, ctx).flatMap(structInternal, ctx, fields, warnUnknown) } type TypeSwitch = { @@ -147,7 +148,7 @@ function keyValues( f: CtxFunc>, maxKeys: number = Infinity ): Maybe> { - return object(j, ctx).map((d) => { + return object(j, ctx).flatMap((d) => { const entries = Object.entries(d) if (entries.length > maxKeys) { @@ -220,7 +221,7 @@ function destination(j: Json, ctx: Context): Maybe> { return typeSwitch(j, ctx, { string: (j) => suitableSite(j, ctx).map((s) => new Set([s])), list: (j) => - set(j, ctx, (j) => string(j, ctx).map(suitableSite, ctx), { + set(j, ctx, (j) => string(j, ctx).flatMap(suitableSite, ctx), { minLength: 1, maxLength: 3, }), @@ -311,7 +312,7 @@ function eventReportWindows( ctx: SourceContext, expiry: Maybe ): Maybe { - return object(j, ctx).map((j) => { + return object(j, ctx).flatMap((j) => { const startTimeValue = field( 'start_time', (j) => startTime(j, ctx, expiry), @@ -348,7 +349,7 @@ function set( // checks should be performed on the resulting set, not on the list. return list(j, ctx) .filter((js) => isLengthValid(js.length, ctx, opts)) - .map((js) => validate.set(js.entries(), ctx, f, opts?.requireDistinct)) + .flatMap((js) => validate.set(js.entries(), ctx, f, opts?.requireDistinct)) } type ArrayOpts = LengthOpts & { @@ -363,7 +364,9 @@ function array( ): Maybe { return list(j, ctx) .filter((js) => isLengthValid(js.length, ctx, opts)) - .map((js) => validate.array(js.entries(), ctx, f, opts?.itemErrorAction)) + .flatMap((js) => + validate.array(js.entries(), ctx, f, opts?.itemErrorAction) + ) } function filterDataKeyValue( @@ -861,13 +864,13 @@ function triggerSpec( ) return struct(j, ctx, { - eventReportWindows: field( + eventReportWindows: fieldMaybeDefault( 'event_report_windows', (j) => eventReportWindows(j, ctx, deps.expiry), deps.eventReportWindows ), - summaryBuckets: field( + summaryBuckets: fieldMaybeDefault( 'summary_buckets', (j) => summaryBuckets(j, ctx, deps.maxEventLevelReports), defaultSummaryBuckets @@ -952,7 +955,7 @@ function defaultTriggerSpecs( eventReportWindows: Maybe, maxEventLevelReports: Maybe ): Maybe { - return eventReportWindows.map((eventReportWindows) => + return eventReportWindows.flatMap((eventReportWindows) => maxEventLevelReports.map((maxEventLevelReports) => [ { eventReportWindows, @@ -1048,7 +1051,7 @@ export type Source = CommonDebug & function source(j: Json, ctx: SourceContext): Maybe { return object(j, ctx) - .map((j) => { + .flatMap((j) => { const expiryVal = field( 'expiry', expiry, @@ -1095,7 +1098,7 @@ function source(j: Json, ctx: SourceContext): Maybe { )(j, ctx) return struct(j, ctx, { - aggregatableReportWindow: field( + aggregatableReportWindow: fieldMaybeDefault( 'aggregatable_report_window', (j) => singleReportWindow(j, ctx, expiryVal), expiryVal @@ -1258,7 +1261,7 @@ function aggregatableDedupKeys( } function enumerated(j: Json, ctx: Context, e: Record): Maybe { - return string(j, ctx).map(validate.enumerated, ctx, e) + return string(j, ctx).flatMap(validate.enumerated, ctx, e) } export enum AggregatableSourceRegistrationTime { @@ -1348,7 +1351,7 @@ function aggregationCoordinatorOrigin( ctx: RegistrationContext ): Maybe { return string(j, ctx) - .map(suitableOrigin, ctx) + .flatMap(suitableOrigin, ctx) .filter((s) => { if (!ctx.vsv.aggregationCoordinatorOrigins.includes(s)) { const allowed = ctx.vsv.aggregationCoordinatorOrigins.join(', ') @@ -1373,7 +1376,7 @@ export type Trigger = CommonDebug & function trigger(j: Json, ctx: RegistrationContext): Maybe { return object(j, ctx) - .map((j) => { + .flatMap((j) => { const aggregatableSourceRegTimeVal = field( 'aggregatable_source_registration_time', aggregatableSourceRegistrationTime, @@ -1483,7 +1486,7 @@ function reportDestination(j: Json, ctx: Context): Maybe { return typeSwitch(j, ctx, { string: suitableSiteNoExtraneous, list: (j) => - array(j, ctx, (j) => string(j, ctx).map(suitableSiteNoExtraneous), { + array(j, ctx, (j) => string(j, ctx).flatMap(suitableSiteNoExtraneous), { minLength: 2, maxLength: 3, }).filter((v) => { diff --git a/ts/src/header-validator/validate.ts b/ts/src/header-validator/validate.ts index aa62f3d924..c7faa67a9b 100644 --- a/ts/src/header-validator/validate.ts +++ b/ts/src/header-validator/validate.ts @@ -1,6 +1,6 @@ import * as psl from 'psl' import { Context, PathComponent } from './context' -import { Maybe, Maybeable } from './maybe' +import { Maybe } from './maybe' export type CtxFunc = (i: I, ctx: C) => O @@ -52,14 +52,14 @@ type GetAndDeleteFunc = (d: D, name: string) => V | undefined type FieldFunc = ( name: string, f: CtxFunc>, - valueIfAbsent?: Maybeable + valueIfAbsent?: T ) => CtxFunc> function field(getAndDelete: GetAndDeleteFunc): FieldFunc { return ( name: string, f: CtxFunc>, - valueIfAbsent?: Maybeable + valueIfAbsent?: T ) => (d: D, ctx: C): Maybe => ctx.scope(name, () => { @@ -69,7 +69,33 @@ function field(getAndDelete: GetAndDeleteFunc): FieldFunc { ctx.error('required') return Maybe.None } - return Maybe.flatten(valueIfAbsent) + return Maybe.some(valueIfAbsent) + } + return f(v, ctx) + }) +} + +type FieldMaybeDefaultFunc = ( + name: string, + f: CtxFunc>, + valueIfAbsent: Maybe +) => CtxFunc> + +// TODO(apaseltiner): Merge this function back into `field` since it is +// identical other than whether the default value is T or Maybe. +function fieldMaybeDefault( + getAndDelete: GetAndDeleteFunc +): FieldMaybeDefaultFunc { + return ( + name: string, + f: CtxFunc>, + valueIfAbsent: Maybe + ) => + (d: D, ctx: C): Maybe => + ctx.scope(name, () => { + const v = getAndDelete(d, name) + if (v === undefined) { + return valueIfAbsent } return f(v, ctx) }) @@ -81,7 +107,7 @@ export type Exclusive = { type ExclusiveFunc = ( x: Exclusive, - valueIfAbsent: Maybeable + valueIfAbsent: Maybe ) => CtxFunc> function exclusive( @@ -89,7 +115,7 @@ function exclusive( ): ExclusiveFunc { return ( x: Exclusive, - valueIfAbsent: Maybeable + valueIfAbsent: Maybe ) => (d: D, ctx: C): Maybe => { const found: string[] = [] @@ -112,13 +138,14 @@ function exclusive( return Maybe.None } - return Maybe.flatten(valueIfAbsent) + return valueIfAbsent } } type Funcs = { exclusive: ExclusiveFunc field: FieldFunc + fieldMaybeDefault: FieldMaybeDefaultFunc struct: StructFunc } @@ -130,6 +157,7 @@ export function make( return { exclusive: exclusive(getAndDelete), field: field(getAndDelete), + fieldMaybeDefault: fieldMaybeDefault(getAndDelete), struct: struct(unknownKeys, warnUnknownMsg), } } From aa99a0c1efcee85b52a6f67770edbf5d8872f7eb Mon Sep 17 00:00:00 2001 From: Xavier Capaldi <38892330+xcapaldi@users.noreply.github.com> Date: Thu, 13 Jun 2024 16:02:33 -0400 Subject: [PATCH 2/2] Update ara-tester-list.md (#1315) --- ara-tester-list.md | 1 + 1 file changed, 1 insertion(+) diff --git a/ara-tester-list.md b/ara-tester-list.md index 2823005653..db9398f543 100644 --- a/ara-tester-list.md +++ b/ara-tester-list.md @@ -56,6 +56,7 @@ The usefulness of this page depends on testers sharing information and updates; | MiQ | Adtech & Managed service | From 01.01.2024 | coming soon | privacysandbox@miqdigital.com | | KelkooGroup | Adtech services for affiliate marketing | From 01.12.2023 | | privacysandbox@kelkoogroup.com | | Quantcast | Demand-side platform (DSP) | Testing ongoing | | chrome-privacy-sandbox@quantcast.com | +| Optable | Adtech services | Testing ongoing | | privacysandbox@optable.co | ## Table - Publishers and Advertisers Interested in Testing or Early Adoption Companies who may be interested in participating in tests and early adoption opportunities provided by ad tech companies.