@@ -17,140 +17,129 @@ function get(dict: Dictionary, key: string): BareItem | Item[] | undefined {
1717 return value ;
1818}
1919
20- function getInteger ( dict : Dictionary , key : string ) : number | undefined {
20+ function optional < T > (
21+ dict : Dictionary ,
22+ key : string ,
23+ f : ( value : BareItem | Item [ ] , errPrefix : string ) => T ,
24+ ) : T | undefined {
2125 const value = get ( dict , key ) ;
22- if ( value === undefined ) {
23- return value ;
24- }
25-
26- if ( typeof value !== "number" || ! Number . isInteger ( value ) ) {
27- throw new TypeError ( `${ key } must be an integer` ) ;
28- }
29-
30- return value ;
26+ return value === undefined ? value : f ( value , key ) ;
3127}
3228
33- function get32BitUnsignedInteger (
29+ function required < T > (
3430 dict : Dictionary ,
3531 key : string ,
36- ) : number | undefined {
37- const value = getInteger ( dict , key ) ;
32+ f : ( value : BareItem | Item [ ] , errPrefix : string ) => T ,
33+ ) : T {
34+ const value = get ( dict , key ) ;
3835 if ( value === undefined ) {
39- return value ;
36+ throw new TypeError ( ` ${ key } is required` ) ;
4037 }
38+ return f ( value , key ) ;
39+ }
4140
42- if ( value < 0 || value > MAX_UINT32 ) {
43- throw new RangeError ( `${ key } must be in the 32-bit unsigned range` ) ;
41+ function asInteger ( value : BareItem | Item [ ] , errPrefix : string ) : number {
42+ if ( typeof value !== "number" || ! Number . isInteger ( value ) ) {
43+ throw new TypeError ( `${ errPrefix } be an integer` ) ;
4444 }
45-
4645 return value ;
4746}
4847
49- function getPositive32BitUnsignedInteger (
50- dict : Dictionary ,
51- key : string ,
52- ) : number | undefined {
53- const value = get32BitUnsignedInteger ( dict , key ) ;
54- if ( value === undefined ) {
55- return value ;
56- }
57-
58- if ( value === 0 ) {
59- throw new RangeError ( `${ key } must be positive` ) ;
48+ function asDecimalOrInteger (
49+ value : BareItem | Item [ ] ,
50+ errPrefix : string ,
51+ ) : number {
52+ if ( typeof value !== "number" ) {
53+ throw new TypeError ( `${ errPrefix } must be a decimal or an integer` ) ;
6054 }
61-
6255 return value ;
6356}
6457
65- function parseInnerList < T > (
66- dict : Dictionary ,
67- key : string ,
68- parseItem : ( i : number , value : BareItem ) => T ,
69- ) : T [ ] | undefined {
70- const values = get ( dict , key ) ;
71- if ( values === undefined ) {
72- return values ;
58+ function as32BitUnsignedInteger (
59+ value : BareItem | Item [ ] ,
60+ errPrefix : string ,
61+ ) : number {
62+ const integer = asInteger ( value , errPrefix ) ;
63+ if ( integer < 0 || integer > MAX_UINT32 ) {
64+ throw new RangeError ( `${ errPrefix } must be in the 32-bit unsigned range` ) ;
7365 }
66+ return integer ;
67+ }
7468
75- if ( ! Array . isArray ( values ) ) {
76- throw new TypeError ( `${ key } must be an inner list` ) ;
69+ function as32BitSignedInteger (
70+ value : BareItem | Item [ ] ,
71+ errPrefix : string ,
72+ ) : number {
73+ const integer = asInteger ( value , errPrefix ) ;
74+ if ( integer < MIN_INT32 || integer > MAX_INT32 ) {
75+ throw new RangeError ( `${ errPrefix } must be in the 32-bit signed range` ) ;
7776 }
77+ return integer ;
78+ }
7879
79- const result = [ ] ;
80- for ( const [ i , [ value ] ] of values . entries ( ) ) {
81- result . push ( parseItem ( i , value ) ) ;
80+ function asPositive ( value : number , errPrefix : string ) : number {
81+ if ( value <= 0 ) {
82+ throw new TypeError ( ` ${ errPrefix } be positive` ) ;
8283 }
83- return result ;
84+ return value ;
8485}
8586
86- function parseInnerListOfSites (
87- dict : Dictionary ,
88- key : string ,
89- ) : string [ ] | undefined {
90- return parseInnerList ( dict , key , ( i , value ) => {
91- if ( typeof value !== "string" ) {
92- throw new TypeError ( `${ key } [${ i } ] must be a string` ) ;
93- }
94- return value ;
95- } ) ;
87+ function asPositiveInteger (
88+ value : BareItem | Item [ ] ,
89+ errPrefix : string ,
90+ ) : number {
91+ return asPositive ( asInteger ( value , errPrefix ) , errPrefix ) ;
9692}
9793
98- function validate32BitUnsignedInteger (
99- value : BareItem | Item [ ] | undefined ,
94+ function asPositive32BitUnsignedInteger (
95+ value : BareItem | Item [ ] ,
10096 errPrefix : string ,
101- ) : asserts value is number {
102- if (
103- typeof value !== "number" ||
104- ! Number . isInteger ( value ) ||
105- value < 0 ||
106- value > MAX_UINT32
107- ) {
108- throw new RangeError (
109- `${ errPrefix } must be an integer in the 32-bit unsigned range` ,
110- ) ;
97+ ) : number {
98+ return asPositive ( as32BitUnsignedInteger ( value , errPrefix ) , errPrefix ) ;
99+ }
100+
101+ function asString ( value : BareItem | Item [ ] , errPrefix : string ) : string {
102+ if ( typeof value !== "string" ) {
103+ throw new TypeError ( `${ errPrefix } must be a string` ) ;
111104 }
105+ return value ;
112106}
113107
114- function validatePositiveInteger (
115- value : BareItem | Item [ ] | undefined ,
108+ function asInnerList < T > (
109+ values : BareItem | Item [ ] ,
116110 errPrefix : string ,
117- ) : asserts value is number {
118- if ( typeof value !== "number" || ! Number . isInteger ( value ) || value <= 0 ) {
119- throw new RangeError ( `${ errPrefix } must be a positive integer` ) ;
111+ parseItem : ( value : BareItem , errPrefix : string ) => T ,
112+ ) : T [ ] {
113+ if ( ! Array . isArray ( values ) ) {
114+ throw new TypeError ( `${ errPrefix } must be an inner list` ) ;
120115 }
116+ return values . map ( ( [ value ] , i ) => parseItem ( value , `${ errPrefix } [${ i } ]` ) ) ;
117+ }
118+
119+ function asInnerListOfStrings (
120+ values : BareItem | Item [ ] ,
121+ errPrefix : string ,
122+ ) : string [ ] {
123+ return asInnerList ( values , errPrefix , asString ) ;
121124}
122125
123126export function parseSaveImpressionHeader (
124127 input : string ,
125128) : AttributionImpressionOptions {
126129 const dict = parseDictionary ( input ) ;
127130
128- const histogramIndex = get32BitUnsignedInteger ( dict , "histogram-index" ) ;
129- if ( histogramIndex === undefined ) {
130- throw new TypeError ( "histogram-index is required" ) ;
131- }
132-
133- const opts : AttributionImpressionOptions = { histogramIndex } ;
134-
135- opts . conversionSites = parseInnerListOfSites ( dict , "conversion-sites" ) ;
136- opts . conversionCallers = parseInnerListOfSites ( dict , "conversion-callers" ) ;
137-
138- opts . matchValue = get32BitUnsignedInteger ( dict , "match-value" ) ;
139-
140- opts . lifetimeDays = getInteger ( dict , "lifetime-days" ) ;
141- if ( opts . lifetimeDays !== undefined && opts . lifetimeDays <= 0 ) {
142- throw new RangeError ( "lifetime-days must be positive" ) ;
143- }
144-
145- opts . priority = getInteger ( dict , "priority" ) ;
146- if (
147- opts . priority !== undefined &&
148- ( opts . priority < MIN_INT32 || opts . priority > MAX_INT32 )
149- ) {
150- throw new RangeError ( "priority must be in the 32-bit signed range" ) ;
151- }
152-
153- return opts ;
131+ return {
132+ histogramIndex : required ( dict , "histogram-index" , as32BitUnsignedInteger ) ,
133+ conversionSites : optional ( dict , "conversion-sites" , asInnerListOfStrings ) ,
134+ conversionCallers : optional (
135+ dict ,
136+ "conversion-callers" ,
137+ asInnerListOfStrings ,
138+ ) ,
139+ matchValue : optional ( dict , "match-value" , as32BitUnsignedInteger ) ,
140+ lifetimeDays : optional ( dict , "lifetime-days" , asPositiveInteger ) ,
141+ priority : optional ( dict , "priority" , as32BitSignedInteger ) ,
142+ } ;
154143}
155144
156145export type ParsedMeasureConversionHeader = [
@@ -164,68 +153,41 @@ export function parseMeasureConversionHeader(
164153) : ParsedMeasureConversionHeader {
165154 const dict = parseDictionary ( input ) ;
166155
167- const aggregationService = get ( dict , "aggregation-service" ) ;
168- if ( aggregationService === undefined ) {
169- throw new TypeError ( "aggregation-service is required" ) ;
170- }
171- if ( typeof aggregationService !== "string" ) {
172- throw new TypeError ( "aggregation-service must be a string" ) ;
173- }
174-
175- const histogramSize = getPositive32BitUnsignedInteger ( dict , "histogram-size" ) ;
176- if ( histogramSize === undefined ) {
177- throw new TypeError ( "histogram-size is required" ) ;
178- }
179-
180- const reportUrlString = get ( dict , "report-url" ) ;
181- if ( reportUrlString === undefined ) {
182- throw new TypeError ( "report-url is required" ) ;
183- }
184- if ( typeof reportUrlString !== "string" ) {
185- throw new TypeError ( "report-url must be a string" ) ;
186- }
187- const reportUrl = new URL ( reportUrlString , baseUrl ) ;
188- // The specification requires reportUrl to be potentially trustworthy, but
189- // there is no direct analogue of this in JS, so for now we let the protocol
190- // check below suffice.
191- if ( reportUrl . protocol !== "https:" ) {
192- throw new TypeError ( "report-url's scheme must be https" ) ;
193- }
194-
195- const opts : AttributionConversionOptions = {
196- aggregationService,
197- histogramSize,
198- } ;
199-
200- const epsilon = get ( dict , "epsilon" ) ;
201- if ( epsilon !== undefined && typeof epsilon !== "number" ) {
202- throw new TypeError ( "epsilon must be a decimal or an integer" ) ;
203- }
204- opts . epsilon = epsilon ;
205-
206- const lookbackDays = get ( dict , "lookback-days" ) ;
207- if ( lookbackDays !== undefined ) {
208- validatePositiveInteger ( lookbackDays , "lookback-days" ) ;
209- }
210- opts . lookbackDays = lookbackDays ;
211-
212- opts . matchValues = parseInnerList ( dict , "match-values" , ( i , value ) => {
213- validate32BitUnsignedInteger ( value , `match-values[${ i } ]` ) ;
214- return value ;
215- } ) ;
216-
217- opts . impressionSites = parseInnerListOfSites ( dict , "impression-sites" ) ;
218- opts . impressionCallers = parseInnerListOfSites ( dict , "impression-callers" ) ;
219-
220- opts . credit = parseInnerList ( dict , "credit" , ( i , value ) => {
221- if ( typeof value !== "number" ) {
222- throw new RangeError ( `credit[${ i } ] must be a decimal or an integer` ) ;
156+ const reportUrl = required ( dict , "report-url" , ( value , errPrefix ) => {
157+ const url = new URL ( asString ( value , errPrefix ) , baseUrl ) ;
158+ // The specification requires reportUrl to be potentially trustworthy, but
159+ // there is no direct analogue of this in JS, so for now we let the protocol
160+ // check below suffice.
161+ if ( url . protocol !== "https:" ) {
162+ throw new TypeError ( `${ errPrefix } 's scheme must be https` ) ;
223163 }
224- return value ;
164+ return url ;
225165 } ) ;
226166
227- opts . value = getPositive32BitUnsignedInteger ( dict , "value" ) ;
228- opts . maxValue = getPositive32BitUnsignedInteger ( dict , "max-value" ) ;
167+ const opts = {
168+ aggregationService : required ( dict , "aggregation-service" , asString ) ,
169+ histogramSize : required (
170+ dict ,
171+ "histogram-size" ,
172+ asPositive32BitUnsignedInteger ,
173+ ) ,
174+ epsilon : optional ( dict , "epsilon" , asDecimalOrInteger ) ,
175+ lookbackDays : optional ( dict , "lookback-days" , asPositiveInteger ) ,
176+ matchValues : optional ( dict , "match-values" , ( values , errPrefix ) =>
177+ asInnerList ( values , errPrefix , as32BitUnsignedInteger ) ,
178+ ) ,
179+ impressionSites : optional ( dict , "impression-sites" , asInnerListOfStrings ) ,
180+ impressionCallers : optional (
181+ dict ,
182+ "impression-callers" ,
183+ asInnerListOfStrings ,
184+ ) ,
185+ credit : optional ( dict , "credit" , ( values , errPrefix ) =>
186+ asInnerList ( values , errPrefix , asDecimalOrInteger ) ,
187+ ) ,
188+ value : optional ( dict , "value" , asPositive32BitUnsignedInteger ) ,
189+ maxValue : optional ( dict , "max-value" , asPositive32BitUnsignedInteger ) ,
190+ } ;
229191
230192 return [ opts , reportUrl ] ;
231193}
0 commit comments