Skip to content

Commit 1d0b2d9

Browse files
committed
stash: i hate ts
1 parent 9a26bf0 commit 1d0b2d9

File tree

8 files changed

+366
-147
lines changed

8 files changed

+366
-147
lines changed

source/api.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
import type {
55
ResponseObject,
66
HeadersObject,
7-
RateLimitInfo,
7+
ParsedRateLimit,
88
ParserOptions,
99
} from './types'
1010
import { getHeaderObject, getNonStandardHeaders } from './headers.js'
@@ -18,12 +18,12 @@ import { rateLimitSorter } from './utilities.js'
1818
* @param input {ResponseObject | HeadersObject} - The node/fetch-style response/headers object.
1919
* @param passedOptions {Partial<ParserOptions> | undefined} - The configuration for the parser.
2020
*
21-
* @returns {RateLimitInfo | undefined} - The rate limit information parsed from the headers.
21+
* @returns {ParsedRateLimit | undefined} - The rate limit information parsed from the headers.
2222
*/
2323
export const getRateLimit = (
2424
input: ResponseObject | HeadersObject,
2525
passedOptions?: Partial<ParserOptions>,
26-
): RateLimitInfo | undefined => {
26+
): ParsedRateLimit | undefined => {
2727
const rateLimits = getRateLimits(input, passedOptions)
2828
return rateLimits.length === 0 ? undefined : rateLimits[0]
2929
}
@@ -35,12 +35,12 @@ export const getRateLimit = (
3535
* @param input {ResponseObject | HeadersObject} - The node/fetch-style response/headers object.
3636
* @param passedOptions {Partial<ParserOptions> | undefined} - The configuration for the parser.
3737
*
38-
* @returns {RateLimitInfo[]} - The rate limit information parsed from the headers.
38+
* @returns {ParsedRateLimit[]} - The rate limit information parsed from the headers.
3939
*/
4040
export const getRateLimits = (
4141
input: ResponseObject | HeadersObject,
4242
passedOptions?: Partial<ParserOptions>,
43-
): RateLimitInfo[] => {
43+
): ParsedRateLimit[] => {
4444
// Default to no configuration, and get the headers object from the input.
4545
const options = passedOptions ?? {}
4646
const headers = getHeaderObject(input)

source/parsers/ietf-draft.ts

Lines changed: 211 additions & 76 deletions
Original file line numberDiff line numberDiff line change
@@ -1,103 +1,238 @@
11
// /source/parser/ietf-draft.ts
2-
// The parser for headers following the IETF draft specification (v6+).
2+
// The parser for headers following the IETF draft specification (v7+).
33

44
import { parseList, parseDictionary as parseDict } from 'structured-headers'
5-
import type { RateLimitInfo, HeadersObject, ParserOptions } from '../types'
5+
import type {
6+
ParsedRateLimit,
7+
RateLimitPolicy,
8+
HeadersObject,
9+
ParserOptions,
10+
} from '../types'
611
import { getHeader } from '../headers.js'
12+
import { toIntOrUndefined, constructParsedRateLimit } from '../utilities.js'
713
import { parse as parseResetTime } from './reset.js'
814

15+
type QuotaUnit = RateLimitPolicy['quota']['unit']
16+
17+
// Utility type for the parsed structured header policy.
18+
type ParsedValue = string | number | undefined
19+
type ParsedParameters = Map<string, ParsedValue[]>
20+
type ParsedHeaderPolicy = {
21+
value: string | number
22+
params: ParsedParameters
23+
}
24+
25+
// Utility type for the parsed structured header entries.
26+
type ParsedDraft7Header = ParsedParameters
27+
type ParsedDraft8Header = Array<[string, ParsedParameters]>
28+
929
/**
10-
* Parses standard headers as per the IETF draft specification.
30+
* Attempts to parse header using the 7th draft of the specification.
1131
*
12-
* @param headers {HeadersObject} - The object containing headers to parse.
32+
* @param header {string} - The `RateLimit` header's contents.
1333
* @param options {Partial<ParserOptions>} - The parser's configuration.
14-
* @param prefix {string} - The vendor-specific prefix that each header contains.
1534
*
16-
* @returns {RateLimitInfo | undefined} - The normalized rate limit information, if any header is found.
35+
* @returns {ParsedDraft7Header | undefined} - The parsed header, if the contents are valid.
1736
*/
18-
export const parse = (
19-
headers: HeadersObject,
37+
const tryParsingDraft7 = (
38+
header: string,
2039
options: Partial<ParserOptions>,
21-
): RateLimitInfo[] => {
22-
const header = getHeader(headers, 'ratelimit')
23-
const policyHeader = getHeader(headers, 'ratelimit-policy')
40+
): ParsedDraft7Header | undefined => {
41+
try {
42+
return parseDict(header) as ParsedDraft7Header
43+
} catch {
44+
return undefined
45+
}
46+
}
47+
48+
/**
49+
* Attempts to parse header using the 8th draft of the specification.
50+
*
51+
* @param header {string} - The `RateLimit` header's contents.
52+
* @param options {Partial<ParserOptions>} - The parser's configuration.
53+
*
54+
* @returns {ParsedDraft8Header | undefined} - The parsed header, if the contents are valid.
55+
*/
56+
const tryParsingDraft8 = (
57+
header: string,
58+
options: Partial<ParserOptions>,
59+
): ParsedDraft8Header | undefined => {
60+
try {
61+
return parseList(header) as ParsedDraft8Header
62+
} catch {
63+
return undefined
64+
}
65+
}
2466

25-
// The rate limit information is provided in a manner similar to non-standard
26-
// headers according to the 6th draft, so we do not need to handle it here
27-
// separately.
67+
/**
68+
* Attempts to parse policies from the `RateLimit-Policy` header for either the
69+
* 7th or 8th draft of the spec.
70+
*
71+
* @param header {string} - The `RateLimit-Policy` header's contents.
72+
*
73+
* @returns {ParsedHeaderPolicy[]} - The list of policies specified in the header.
74+
*/
75+
function parsePolicies(header?: string): ParsedHeaderPolicy[] {
76+
if (!header) return []
2877

29-
// The value of the `RateLimit` header in the 7th draft is a dictionary, while
30-
// in the 8th draft, it is a list.
31-
let draft7, draft8
3278
try {
33-
draft7 = parseDict(header)
79+
return parseList(header).map((policy) => ({
80+
value: policy[0],
81+
params: policy[1],
82+
})) as ParsedHeaderPolicy[]
3483
} catch {
35-
try {
36-
draft8 = parseList(header)
37-
} catch (error) {
38-
if (options?.strict)
39-
throw Error(
40-
`Failed to parse 'RateLimit' header as per IETF standard drafts: ${error.message}`,
41-
)
42-
}
84+
return []
4385
}
86+
}
4487

45-
const rateLimits = []
46-
const policies =
47-
policyHeader !== undefined
48-
? parseList(policyHeader).map((policy) => {
49-
return {
50-
value: policy[0],
51-
params: policy[1],
52-
}
53-
})
54-
: []
55-
56-
if (draft7 !== undefined) {
57-
const limit = draft7.get('limit')[0]
58-
const info = {
59-
used: limit - draft7.get('remaining')[0],
60-
remaining: draft7.get('remaining')[0],
61-
reset: draft7.get('reset')[0],
88+
/**
89+
* Processes rate limit information from the header as per the 7th draft.
90+
*
91+
* @param draft7 {ParsedDraft7Header} - The parsed `RateLimit` structured header.
92+
* @param policies {ParsedHeaderPolicy[]} - The parsed `RateLimit-Policy` structured header.
93+
* @param options {Partial<ParserOptions>} - The parser's configuration.
94+
*
95+
* @return {ParsedRateLimit[]} - The parsed rate limit information.
96+
*/
97+
function processDraft7Limits(
98+
draft7: ParsedDraft7Header,
99+
policies: ParsedHeaderPolicy[],
100+
options: Partial<ParserOptions>,
101+
): ParsedRateLimit[] {
102+
const limit = toIntOrUndefined(draft7.get('limit')?.[0])
103+
const remaining = toIntOrUndefined(draft7.get('remaining')?.[0])
104+
const reset = parseResetTime(draft7.get('reset')?.[0] as ParsedValue, options)
105+
const info = { limit, remaining, reset }
106+
107+
// Make sure limit exists in strict mode, since it is required by the draft.
108+
if (limit === undefined && options?.strict) {
109+
throw new Error(
110+
`Failed to parse 'RateLimit' header: 'limit' is not a number.`,
111+
)
112+
}
113+
114+
// If no policies exist, create a single rate limit with the policy straight
115+
// from the `RateLimit` header.
116+
if (policies.length === 0 && limit !== undefined) {
117+
return [constructParsedRateLimit({ info })]
118+
}
119+
120+
// Map policies to rate limits
121+
return policies.map((parsedPolicy) => {
122+
const policy = {
123+
quota: {
124+
value: parsedPolicy.value as number,
125+
unit: 'requests' as QuotaUnit,
126+
},
127+
window: toIntOrUndefined(parsedPolicy.params.get('w')),
62128
}
63129

64-
for (const policy of policies)
65-
rateLimits.push({
66-
info: limit === policy.value ? info : undefined,
67-
policy: {
68-
quota: { value: policy.value, unit: 'requests' },
69-
window: policy.params.get('w'),
130+
return constructParsedRateLimit({
131+
info: limit === parsedPolicy.value ? info : undefined,
132+
policy,
133+
})
134+
})
135+
}
136+
137+
/**
138+
* Processes rate limit information from the header as per the 8th draft.
139+
*
140+
* @param draft8 {ParsedDraft8Header} - The parsed `RateLimit` structured header.
141+
* @param policies {ParsedHeaderPolicy[]} - The parsed `RateLimit-Policy` structured header.
142+
* @param options {Partial<ParserOptions>} - The parser's configuration.
143+
*
144+
* @return {ParsedRateLimit[]} - The parsed rate limit information.
145+
*/
146+
function processDraft8Limits(
147+
draft8: ParsedDraft8Header,
148+
policies: ParsedHeaderPolicy[],
149+
options: Partial<ParserOptions>,
150+
): ParsedRateLimit[] {
151+
// If no policies exist, create rate limits and their policies directly from
152+
// the `RateLimit` header.
153+
if (policies.length === 0) {
154+
return draft8.map(([name, parameters]) => {
155+
// There is no `q` in the draft, but if there is no policy header, maybe
156+
// the server has sent it along with the `RateLimit` header? If not,
157+
// default to the value of `remaining` as the limit.
158+
const limit = toIntOrUndefined(parameters.get('q') ?? parameters.get('r'))
159+
const remaining = toIntOrUndefined(parameters.get('r'))
160+
const reset = parseResetTime(parameters.get('t') as ParsedValue, options)
161+
162+
const info = { limit, remaining, reset }
163+
const policy = {
164+
name,
165+
quota: {
166+
value: limit,
167+
unit: 'requests' as QuotaUnit,
70168
},
71-
})
169+
}
170+
171+
return constructParsedRateLimit({ info, policy })
172+
})
72173
}
73174

74-
if (draft8 !== undefined) {
75-
for (const header of draft8) {
76-
const name = header[0]
77-
const info = {
78-
remaining: header[1].get('r'),
79-
reset: parseResetTime(header[1].get('t'), options),
80-
}
175+
// If an accompanying policy header was found, map each policy to the
176+
// corresponding info.
177+
return draft8.flatMap(([name, headerParameters]) => {
178+
const reset = parseResetTime(
179+
headerParameters.get('t') as ParsedValue,
180+
options,
181+
)
182+
183+
return policies.map(({ value, params: policyParameters }) => {
184+
// Give precedence to the value of partition key in the `RateLimit` header
185+
// over the `RateLimit-Policy` header, even though the both should be same.
186+
const partition = (headerParameters.get('pk')?.value ??
187+
policyParameters.get('pk')?.value) as string | undefined
188+
const remaining = toIntOrUndefined(headerParameters.get('r'))
189+
const window = toIntOrUndefined(policyParameters.get('w'))
190+
const limit = toIntOrUndefined(policyParameters.get('q'))
191+
const unit = (policyParameters.get('qu') ?? 'requests') as QuotaUnit
81192

82-
for (const policy of policies) {
83-
const params = policy.params
84-
info.used = policy.params.get('q') - info.remaining
85-
86-
rateLimits.push({
87-
info: name === policy.value ? info : undefined,
88-
policy: {
89-
name,
90-
quota: {
91-
value: params.get('q'),
92-
unit: params.get('qu') ?? 'requests',
93-
},
94-
window: params.get('w'),
95-
partition: header[1].get('pk')?.value ?? params.get('pk')?.value,
96-
},
97-
})
193+
const info = { limit, remaining, reset }
194+
const policy = {
195+
name,
196+
quota: {
197+
value: limit,
198+
unit,
199+
},
200+
window,
201+
partition,
98202
}
99-
}
100-
}
101203

102-
return rateLimits
204+
return constructParsedRateLimit({ info, policy })
205+
})
206+
})
207+
}
208+
209+
/**
210+
* Parses standard headers as per the IETF draft specification.
211+
*
212+
* @param headers - The object containing headers to parse.
213+
* @param options - The parser's configuration.
214+
* @returns An array of parsed rate limit information.
215+
*/
216+
export const parse = (
217+
headers: HeadersObject,
218+
options: Partial<ParserOptions> = {},
219+
): ParsedRateLimit[] => {
220+
const header = getHeader(headers, 'ratelimit')
221+
const policyHeader = getHeader(headers, 'ratelimit-policy')
222+
223+
// If no rate limit header is present, return an empty array.
224+
if (!header) return []
225+
226+
// Attempt to parse headers with different draft specifications.
227+
const draft7 = tryParsingDraft7(header, options)
228+
const draft8 = tryParsingDraft8(header, options)
229+
230+
// Parse policies if the policy header exists.
231+
const policies = parsePolicies(policyHeader)
232+
233+
// Combine the parsed rate limits with the corresponding policies.
234+
return [
235+
...(draft7 ? processDraft7Limits(draft7, policies, options) : []),
236+
...(draft8 ? processDraft8Limits(draft8, policies, options) : []),
237+
]
103238
}

source/parsers/non-standard.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,10 @@
33

44
// @ts-expect-error no type definitions
55
import { parseItem } from 'structured-headers'
6-
import { parse as parseResetTime } from './reset.js'
7-
import type { HeadersObject, RateLimitInfo, ParserOptions } from '../types'
6+
import type { HeadersObject, ParsedRateLimit, ParserOptions } from '../types'
87
import { getHeader } from '../headers.js'
98
import { toInt } from '../utilities.js'
9+
import { parse as parseResetTime } from './reset.js'
1010

1111
/**
1212
* Parses non standard headers as per their documentation.
@@ -15,13 +15,13 @@ import { toInt } from '../utilities.js'
1515
* @param options {Partial<ParserOptions>} - The parser's configuration.
1616
* @param prefix {string} - The vendor-specific prefix that each header contains.
1717
*
18-
* @returns {RateLimitInfo | undefined} - The normalized rate limit information, if any header is found.
18+
* @returns {ParsedRateLimit | undefined} - The normalized rate limit information, if any header is found.
1919
*/
2020
export const parse = (
2121
headers: HeadersObject,
2222
options: Partial<ParserOptions>,
2323
prefix: string,
24-
): RateLimitInfo | undefined => {
24+
): ParsedRateLimit | undefined => {
2525
// Note that `||` is valid in the following lines because used should always
2626
// be at least 1, and `||` handles NaN correctly, whereas `??` doesn't.
2727
/* eslint-disable @typescript-eslint/prefer-nullish-coalescing */

0 commit comments

Comments
 (0)