Skip to content

Commit b29fba5

Browse files
committed
Parsing helpers
1 parent c22f1a2 commit b29fba5

File tree

1 file changed

+117
-155
lines changed

1 file changed

+117
-155
lines changed

impl/src/http.ts

Lines changed: 117 additions & 155 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 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

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)