Skip to content

Commit c1a8ad2

Browse files
committed
Parsing helpers
1 parent c22f1a2 commit c1a8ad2

File tree

1 file changed

+116
-154
lines changed

1 file changed

+116
-154
lines changed

impl/src/http.ts

Lines changed: 116 additions & 154 deletions
Original file line numberDiff line numberDiff line change
@@ -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 asString(value: BareItem | Item[], errPrefix: string): string {
42+
if (typeof value !== "string") {
43+
throw new TypeError(`${errPrefix} must be a string`);
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;
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`);
5654
}
55+
return value;
56+
}
5757

58-
if (value === 0) {
59-
throw new RangeError(`${key} must be positive`);
58+
function asInteger(value: BareItem | Item[], errPrefix: string): number {
59+
if (typeof value !== "number" || !Number.isInteger(value)) {
60+
throw new TypeError(`${errPrefix} be an integer`);
6061
}
61-
6262
return value;
6363
}
6464

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;
65+
function asPositive(value: number, errPrefix: string): number {
66+
if (value <= 0) {
67+
throw new TypeError(`${errPrefix} be positive`);
7368
}
69+
return value;
70+
}
7471

75-
if (!Array.isArray(values)) {
76-
throw new TypeError(`${key} must be an inner list`);
77-
}
72+
function asPositiveInteger(
73+
value: BareItem | Item[],
74+
errPrefix: string,
75+
): number {
76+
return asPositive(asInteger(value, errPrefix), errPrefix);
77+
}
7878

79-
const result = [];
80-
for (const [i, [value]] of values.entries()) {
81-
result.push(parseItem(i, value));
79+
function as32BitUnsignedInteger(
80+
value: BareItem | Item[],
81+
errPrefix: string,
82+
): number {
83+
const integer = asInteger(value, errPrefix);
84+
if (integer < 0 || integer > MAX_UINT32) {
85+
throw new RangeError(`${errPrefix} must be in the 32-bit unsigned range`);
8286
}
83-
return result;
87+
return integer;
8488
}
8589

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-
});
90+
function asPositive32BitUnsignedInteger(
91+
value: BareItem | Item[],
92+
errPrefix: string,
93+
): number {
94+
return asPositive(as32BitUnsignedInteger(value, errPrefix), errPrefix);
9695
}
9796

98-
function validate32BitUnsignedInteger(
99-
value: BareItem | Item[] | undefined,
97+
function as32BitSignedInteger(
98+
value: BareItem | Item[],
10099
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-
);
100+
): number {
101+
const integer = asInteger(value, errPrefix);
102+
if (integer < MIN_INT32 || integer > MAX_INT32) {
103+
throw new RangeError(`${errPrefix} must be in the 32-bit signed range`);
111104
}
105+
return integer;
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

123126
export 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

156145
export 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

Comments
 (0)