Skip to content

Commit af8ba6b

Browse files
authored
Merge pull request #326 from apasel422/tidy-http
2 parents 2192b96 + 5eb044f commit af8ba6b

File tree

3 files changed

+130
-118
lines changed

3 files changed

+130
-118
lines changed

api.bs

Lines changed: 40 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1922,13 +1922,19 @@ if the user has opted out of collection of diagnostic data.
19221922
set on a response requesting that the user agent invoke the
19231923
<a method for=Attribution>saveImpression()</a> API.
19241924

1925-
<pre class=example id=ex-save-impression-header>
1926-
Save-Impression: conversion-sites=("advertiser.example"), conversion-callers=("intermediary.example"), histogram-index=2, match-value=12, lifetime-days=7
1927-
</pre>
1925+
<div class=example id=ex-save-impression-header>
1926+
This is the HTTP equivalent of <a href=#ex-save-impression>the JavaScript `saveImpression` example</a>:
1927+
<xmp highlight=http>
1928+
Save-Impression: histogram-index=3, match-value=2, conversion-sites=("advertiser.example"), lifetime-days=7
1929+
</xmp>
1930+
</div>
19281931

19291932
The following keys are defined, corresponding to the members of
19301933
the {{AttributionImpressionOptions}} dictionary passed to
1931-
<a method for=Attribution>saveImpression()</a>.
1934+
<a method for=Attribution>saveImpression()</a>. Default values for omitted
1935+
optional keys are treated the same way as the corresponding
1936+
{{AttributionImpressionOptions}} field.
1937+
19321938

19331939
<dl dfn-for=save-impression>
19341940
<dt><dfn noexport><code>conversion-sites</code></dfn></dt>
@@ -1937,15 +1943,15 @@ the {{AttributionImpressionOptions}} dictionary passed to
19371943
an [=structured header/inner list=] containing [=structured header/string|strings=].
19381944
Each string value includes a domain name using A-labels only;
19391945
[[RFC5890|Internationalized Domain Names]] therefore need to use [[RFC3492|punycode]].
1940-
This key is optional. If not supplied, an empty set is saved for [=impression/Conversion Sites=].
1946+
This key is optional.
19411947
</dd>
19421948
<dt><dfn noexport><code>conversion-callers</code></dfn></dt>
19431949
<dd>
19441950
Value of <a dict-member for=AttributionImpressionOptions>conversionCallers</a>,
19451951
an [=structured header/inner list=] containing [=structured header/string|strings=].
19461952
Each string value includes a domain name using A-labels only;
19471953
[[RFC5890|Internationalized Domain Names]] therefore need to use [[RFC3492|punycode]].
1948-
This key is optional. If not supplied, an empty set is saved for [=impression/Conversion Callers=].
1954+
This key is optional.
19491955
</dd>
19501956
<dt><dfn noexport><code>histogram-index</code></dfn></dt>
19511957
<dd>
@@ -1961,13 +1967,11 @@ the {{AttributionImpressionOptions}} dictionary passed to
19611967
<dd>
19621968
Value of <a dict-member for=AttributionImpressionOptions>matchValue</a>,
19631969
an [=structured header/integer=] in the [=32-bit unsigned integer=] range. This key is optional.
1964-
If not supplied, a value of 0 is saved for [=impression/Match Value=].
19651970
</dd>
19661971
<dt><dfn noexport><code>lifetime-days</code></dfn></dt>
19671972
<dd>
19681973
Value of <a dict-member for=AttributionImpressionOptions>lifetimeDays</a>,
19691974
a positive [=structured header/integer=]. This key is optional.
1970-
If not supplied, 30 days is saved for [=impression/Lifetime=].
19711975
</dd>
19721976
</dl>
19731977

@@ -1979,35 +1983,36 @@ To <dfn noexport>parse a `Save-Impression` header</dfn> given a [=header value=]
19791983
with <var ignore>input_bytes</var> set to |input| and
19801984
<var ignore>field_type</var> set to "`dictionary`".
19811985
1. If parsing failed, return an error.
1982-
1. If |dict|["<code>[=save-impression/histogram-index=]</code>"] does not [=map/exist=] or
1983-
is not an [=structured header/integer=] in the [=32-bit unsigned integer=] range, return an error.
1984-
1. Let |histogramIndex| be |dict|["<code>[=save-impression/histogram-index=]</code>"].
1985-
1. Let |conversionSites| be |dict|["<code>[=save-impression/conversion-sites=]</code>"]
1986-
[=map/with default=] an empty [=structured header/inner list=].
1987-
1. If |conversionSites| is not an [=structured header/inner list=], or if any of |conversionSites|' [=list/items=] is not a [=structured header/string=], return an error.
1988-
1. Let |conversionCallers| be |dict|["<code>[=save-impression/conversion-callers=]</code>"]
1989-
[=map/with default=] an empty [=structured header/inner list=].
1990-
1. If |conversionCallers| is not an [=structured header/inner list=], or if any of |conversionCallers|' [=list/items=] is not a [=structured header/string=], return an error.
1991-
1. Let |matchValue| be |dict|["<code>[=save-impression/match-value=]</code>"] [=map/with default=] 0.
1992-
1. If |matchValue| is not an [=structured header/integer=] in the [=32-bit unsigned integer=] range, return an error.
1993-
1. Let |lifetimeDays| be |dict|["<code>[=save-impression/lifetime-days=]</code>"] [=map/with default=] 30.
1994-
1. If |lifetimeDays| is not a positive [=structured header/integer=], return an error.
1995-
1. Clamp |lifetimeDays| to the [=32-bit unsigned integer=] range.
1996-
1. Let |priority| be |dict|["<code>[=save-impression/priority=]</code>"] [=map/with default=] 0.
1997-
1. If |priority| is not an [=structured header/integer=] in the [=32-bit signed integer=] range, return an error.
1998-
1. Return a new {{AttributionImpressionOptions}} with the following items:
1986+
1. Let |histogramIndex| be |dict|["<code>[=save-impression/histogram-index=]</code>"] [=map/with default=] `undefined`.
1987+
1. If |histogramIndex| is not an [=structured header/integer=] in the [=32-bit unsigned integer=] range, return an error.
1988+
1. Let |opts| be a new {{AttributionImpressionOptions}} with the following items:
19991989
: {{AttributionImpressionOptions/histogramIndex}}
20001990
:: |histogramIndex|
2001-
: {{AttributionImpressionOptions/matchValue}}
2002-
:: |matchValue|
2003-
: {{AttributionImpressionOptions/conversionSites}}
2004-
:: |conversionSites|
2005-
: {{AttributionImpressionOptions/conversionCallers}}
2006-
:: |conversionCallers|
2007-
: {{AttributionImpressionOptions/lifetimeDays}}
2008-
:: |lifetimeDays|
2009-
: {{AttributionImpressionOptions/priority}}
2010-
:: |priority|
1991+
1. If |dict|["<code>[=save-impression/conversion-sites=]</code>"] [=map/exists=]:
1992+
1. Let |conversionSites| be its [=map/value=].
1993+
1. If |conversionSites| is not an [=structured header/inner list=], or if any of
1994+
|conversionSites|' [=list/items=] is not a [=structured header/string=],
1995+
return an error.
1996+
1. Set |opts|.{{AttributionImpressionOptions/conversionSites}} to |conversionSites|.
1997+
1. If |dict|["<code>[=save-impression/conversion-callers=]</code>"] [=map/exists=]:
1998+
1. Let |conversionCallers| be its [=map/value=].
1999+
1. If |conversionCallers| is not an [=structured header/inner list=], or if any of
2000+
|conversionCallers|' [=list/items=] is not a [=structured header/string=],
2001+
return an error.
2002+
1. Set |opts|.{{AttributionImpressionOptions/conversionCallers}} to |conversionCallers|.
2003+
1. If |dict|["<code>[=save-impression/match-value=]</code>"] [=map/exists=]:
2004+
1. Let |matchValue| be its [=map/value=].
2005+
1. If |matchValue| is not an [=structured header/integer=] in the [=32-bit unsigned integer=] range, return an error.
2006+
1. Set |opts|.{{AttributionImpressionOptions/matchValue}} to |matchValue|.
2007+
1. If |dict|["<code>[=save-impression/lifetime-days=]</code>"] [=map/exists=]:
2008+
1. Let |lifetimeDays| be its [=map/value=].
2009+
1. If |lifetimeDays| is not a positive [=structured header/integer=], return an error.
2010+
1. Set |opts|.{{AttributionImpressionOptions/lifetimeDays}} to |lifetimeDays|.
2011+
1. If |dict|["<code>[=save-impression/priority=]</code>"] [=map/exists=]:
2012+
1. Let |priority| be its [=map/value=].
2013+
1. If |priority| is not an [=structured header/integer=] in the [=32-bit signed integer=] range, return an error.
2014+
1. Set |opts|.{{AttributionImpressionOptions/priority}} to |priority|.
2015+
1. Return |opts|.
20112016

20122017
</div>
20132018

impl/src/http.test.ts

Lines changed: 31 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -34,11 +34,11 @@ runTests([
3434
input: "histogram-index=123",
3535
expected: {
3636
histogramIndex: 123,
37-
matchValue: 0,
38-
conversionSites: [],
39-
conversionCallers: [],
40-
lifetimeDays: 30,
41-
priority: 0,
37+
matchValue: undefined,
38+
conversionSites: undefined,
39+
conversionCallers: undefined,
40+
lifetimeDays: undefined,
41+
priority: undefined,
4242
},
4343
},
4444

@@ -60,11 +60,11 @@ runTests([
6060
input: "histogram-index=1, conversion-sites=(), conversion-callers=()",
6161
expected: {
6262
histogramIndex: 1,
63-
matchValue: 0,
63+
matchValue: undefined,
6464
conversionSites: [],
6565
conversionCallers: [],
66-
lifetimeDays: 30,
67-
priority: 0,
66+
lifetimeDays: undefined,
67+
priority: undefined,
6868
},
6969
},
7070

@@ -78,11 +78,11 @@ runTests([
7878
input: "histogram-index=4294967295",
7979
expected: {
8080
histogramIndex: 4294967295,
81-
matchValue: 0,
82-
conversionSites: [],
83-
conversionCallers: [],
84-
lifetimeDays: 30,
85-
priority: 0,
81+
matchValue: undefined,
82+
conversionSites: undefined,
83+
conversionCallers: undefined,
84+
lifetimeDays: undefined,
85+
priority: undefined,
8686
},
8787
},
8888
{
@@ -120,10 +120,10 @@ runTests([
120120
expected: {
121121
histogramIndex: 1,
122122
matchValue: 4294967295,
123-
conversionSites: [],
124-
conversionCallers: [],
125-
lifetimeDays: 30,
126-
priority: 0,
123+
conversionSites: undefined,
124+
conversionCallers: undefined,
125+
lifetimeDays: undefined,
126+
priority: undefined,
127127
},
128128
},
129129
{
@@ -145,15 +145,15 @@ runTests([
145145
},
146146
{ name: "lifetime-days-zero", input: "lifetime-days=0, histogram-index=1" },
147147
{
148-
name: "valid-lifetime-days-maximal-clamped",
148+
name: "valid-lifetime-days-maximal",
149149
input: "lifetime-days=999999999999999, histogram-index=1",
150150
expected: {
151151
histogramIndex: 1,
152-
matchValue: 0,
153-
conversionSites: [],
154-
conversionCallers: [],
155-
lifetimeDays: 4294967295,
156-
priority: 0,
152+
matchValue: undefined,
153+
conversionSites: undefined,
154+
conversionCallers: undefined,
155+
lifetimeDays: 999999999999999,
156+
priority: undefined,
157157
},
158158
},
159159

@@ -164,10 +164,10 @@ runTests([
164164
input: "priority=2147483647, histogram-index=1",
165165
expected: {
166166
histogramIndex: 1,
167-
matchValue: 0,
168-
conversionSites: [],
169-
conversionCallers: [],
170-
lifetimeDays: 30,
167+
matchValue: undefined,
168+
conversionSites: undefined,
169+
conversionCallers: undefined,
170+
lifetimeDays: undefined,
171171
priority: 2147483647,
172172
},
173173
},
@@ -176,10 +176,10 @@ runTests([
176176
input: "priority=-2147483648, histogram-index=1",
177177
expected: {
178178
histogramIndex: 1,
179-
matchValue: 0,
180-
conversionSites: [],
181-
conversionCallers: [],
182-
lifetimeDays: 30,
179+
matchValue: undefined,
180+
conversionSites: undefined,
181+
conversionCallers: undefined,
182+
lifetimeDays: undefined,
183183
priority: -2147483648,
184184
},
185185
},

impl/src/http.ts

Lines changed: 59 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import type { AttributionImpressionOptions } from "./index";
22

3-
import type { Dictionary } from "structured-headers";
3+
import type { BareItem, Dictionary, Item } from "structured-headers";
44

55
import { parseDictionary } from "structured-headers";
66

@@ -9,8 +9,49 @@ const MAX_UINT32: number = 4294967295;
99
const MIN_INT32: number = -2147483648;
1010
const MAX_INT32: number = 2147483647;
1111

12-
function parseInnerListOfSites(dict: Dictionary, key: string): string[] {
13-
const [values] = dict.get(key) ?? [[]];
12+
function get(dict: Dictionary, key: string): BareItem | Item[] | undefined {
13+
const [value] = dict.get(key) ?? [undefined];
14+
return value;
15+
}
16+
17+
function getInteger(dict: Dictionary, key: string): number | undefined {
18+
const value = get(dict, key);
19+
if (value === undefined) {
20+
return value;
21+
}
22+
23+
if (typeof value !== "number" || !Number.isInteger(value)) {
24+
throw new TypeError(`${key} must be an integer`);
25+
}
26+
27+
return value;
28+
}
29+
30+
function get32BitUnsignedInteger(
31+
dict: Dictionary,
32+
key: string,
33+
): number | undefined {
34+
const value = getInteger(dict, key);
35+
if (value === undefined) {
36+
return value;
37+
}
38+
39+
if (value < 0 || value > MAX_UINT32) {
40+
throw new RangeError(`${key} must be in the 32-bit unsigned range`);
41+
}
42+
43+
return value;
44+
}
45+
46+
function parseInnerListOfSites(
47+
dict: Dictionary,
48+
key: string,
49+
): string[] | undefined {
50+
const values = get(dict, key);
51+
if (values === undefined) {
52+
return values;
53+
}
54+
1455
if (!Array.isArray(values)) {
1556
throw new TypeError(`${key} must be an inner list`);
1657
}
@@ -30,64 +71,30 @@ export function parseSaveImpressionHeader(
3071
): AttributionImpressionOptions {
3172
const dict = parseDictionary(input);
3273

33-
const [histogramIndex] = dict.get("histogram-index") ?? [undefined];
34-
if (
35-
typeof histogramIndex !== "number" ||
36-
!Number.isInteger(histogramIndex) ||
37-
histogramIndex < 0 ||
38-
histogramIndex > MAX_UINT32
39-
) {
40-
throw new RangeError(
41-
"histogram-index must be an integer in the 32-bit unsigned range",
42-
);
74+
const histogramIndex = get32BitUnsignedInteger(dict, "histogram-index");
75+
if (histogramIndex === undefined) {
76+
throw new TypeError("histogram-index is required");
4377
}
4478

45-
const conversionSites = parseInnerListOfSites(dict, "conversion-sites");
46-
const conversionCallers = parseInnerListOfSites(dict, "conversion-callers");
79+
const opts: AttributionImpressionOptions = { histogramIndex };
4780

48-
const [matchValue] = dict.get("match-value") ?? [0];
49-
if (
50-
typeof matchValue !== "number" ||
51-
!Number.isInteger(matchValue) ||
52-
matchValue < 0 ||
53-
matchValue > MAX_UINT32
54-
) {
55-
throw new RangeError(
56-
"match-value must be an integer in the 32-bit unsigned range",
57-
);
58-
}
81+
opts.conversionSites = parseInnerListOfSites(dict, "conversion-sites");
82+
opts.conversionCallers = parseInnerListOfSites(dict, "conversion-callers");
5983

60-
let [lifetimeDays] = dict.get("lifetime-days") ?? [30];
61-
if (
62-
typeof lifetimeDays !== "number" ||
63-
!Number.isInteger(lifetimeDays) ||
64-
lifetimeDays <= 0
65-
) {
66-
throw new RangeError("lifetime-days must be a positive integer");
67-
}
84+
opts.matchValue = get32BitUnsignedInteger(dict, "match-value");
6885

69-
if (lifetimeDays > MAX_UINT32) {
70-
lifetimeDays = MAX_UINT32;
86+
opts.lifetimeDays = getInteger(dict, "lifetime-days");
87+
if (opts.lifetimeDays !== undefined && opts.lifetimeDays <= 0) {
88+
throw new RangeError("lifetime-days must be positive");
7189
}
7290

73-
const [priority] = dict.get("priority") ?? [0];
91+
opts.priority = getInteger(dict, "priority");
7492
if (
75-
typeof priority !== "number" ||
76-
!Number.isInteger(priority) ||
77-
priority < MIN_INT32 ||
78-
priority > MAX_INT32
93+
opts.priority !== undefined &&
94+
(opts.priority < MIN_INT32 || opts.priority > MAX_INT32)
7995
) {
80-
throw new RangeError(
81-
"priority must be an integer in the 32-bit signed range",
82-
);
96+
throw new RangeError("priority must be in the 32-bit signed range");
8397
}
8498

85-
return {
86-
histogramIndex,
87-
matchValue,
88-
conversionSites,
89-
conversionCallers,
90-
lifetimeDays,
91-
priority,
92-
};
99+
return opts;
93100
}

0 commit comments

Comments
 (0)