Skip to content

Commit d0e1f84

Browse files
authored
Merge pull request #245 from Ozmah/feat/parse-dates-option
feat: add parseDates option to disable automatic date string parsing
2 parents b4faf0d + bcbe823 commit d0e1f84

File tree

5 files changed

+300
-24
lines changed

5 files changed

+300
-24
lines changed

src/treaty2/index.ts

Lines changed: 30 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -124,7 +124,10 @@ const processHeaders = async (
124124
}
125125
}
126126

127-
function parseSSEBlock(block: string): Record<string, unknown> | null {
127+
function parseSSEBlock(
128+
block: string,
129+
options?: { parseDates?: boolean }
130+
): Record<string, unknown> | null {
128131
const lines = block.split('\n')
129132
const result: Record<string, unknown> = {}
130133

@@ -137,7 +140,7 @@ function parseSSEBlock(block: string): Record<string, unknown> | null {
137140
// Per SSE spec, strip single leading space if present
138141
const value = line.slice(colonIndex + 1).replace(/^ /, '')
139142
// Preserve empty strings per SSE spec (e.g. "data:" with no value)
140-
result[key] = value ? parseStringifiedValue(value) : value
143+
result[key] = value ? parseStringifiedValue(value, options) : value
141144
}
142145
}
143146

@@ -148,22 +151,28 @@ function parseSSEBlock(block: string): Record<string, unknown> | null {
148151
* Extracts complete SSE events from buffer, yielding parsed events.
149152
* Mutates bufferRef.value to remove consumed events.
150153
*/
151-
function* extractEvents(bufferRef: {
152-
value: string
153-
}): Generator<Record<string, unknown>> {
154+
function* extractEvents(
155+
bufferRef: {
156+
value: string
157+
},
158+
options?: { parseDates?: boolean }
159+
): Generator<Record<string, unknown>> {
154160
let eventEnd: number
155161
while ((eventEnd = bufferRef.value.indexOf('\n\n')) !== -1) {
156162
const eventBlock = bufferRef.value.slice(0, eventEnd)
157163
bufferRef.value = bufferRef.value.slice(eventEnd + 2)
158164

159165
if (eventBlock.trim()) {
160-
const parsed = parseSSEBlock(eventBlock)
166+
const parsed = parseSSEBlock(eventBlock, options)
161167
if (parsed) yield parsed
162168
}
163169
}
164170
}
165171

166-
export async function* streamResponse(response: Response) {
172+
export async function* streamResponse(
173+
response: Response,
174+
options?: { parseDates?: boolean }
175+
) {
167176
const body = response.body
168177

169178
if (!body) return
@@ -184,18 +193,18 @@ export async function* streamResponse(response: Response) {
184193

185194
bufferRef.value += chunk
186195

187-
yield* extractEvents(bufferRef)
196+
yield* extractEvents(bufferRef, options)
188197
}
189198

190199
const remaining = decoder.decode()
191200
if (remaining) {
192201
bufferRef.value += remaining
193202
}
194203

195-
yield* extractEvents(bufferRef)
204+
yield* extractEvents(bufferRef, options)
196205

197206
if (bufferRef.value.trim()) {
198-
const parsed = parseSSEBlock(bufferRef.value)
207+
const parsed = parseSSEBlock(bufferRef.value, options)
199208
if (parsed) {
200209
yield parsed
201210
}
@@ -572,14 +581,18 @@ const createProxy = (
572581
response.headers.get('Content-Type')?.split(';')[0]
573582
) {
574583
case 'text/event-stream':
575-
data = streamResponse(response)
584+
data = streamResponse(response, {
585+
parseDates: config.parseDates
586+
})
576587
break
577588

578589
case 'application/json':
579590
data = JSON.parse(await response.text(), (k, v) => {
580591
if (typeof v !== 'string') return v
581592

582-
const date = parseStringifiedDate(v)
593+
const date = parseStringifiedDate(v, {
594+
parseDates: config.parseDates
595+
})
583596
if (date) return date
584597

585598
return v
@@ -602,9 +615,11 @@ const createProxy = (
602615
break
603616

604617
default:
605-
data = await response
606-
.text()
607-
.then(parseStringifiedValue)
618+
data = await response.text().then((text) =>
619+
parseStringifiedValue(text, {
620+
parseDates: config.parseDates
621+
})
622+
)
608623
}
609624

610625
if (response.status >= 300 || response.status < 200) {

src/treaty2/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -246,6 +246,7 @@ export namespace Treaty {
246246
>
247247
onResponse?: MaybeArray<(response: Response) => MaybePromise<unknown>>
248248
keepDomain?: boolean
249+
parseDates?: boolean
249250
throwHttpErrors?: ThrowHttpErrors
250251
}
251252

src/utils/parsingUtils.ts

Lines changed: 24 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,12 @@ const isShortenDate =
88
export const isNumericString = (message: string) =>
99
message.trim().length !== 0 && !Number.isNaN(Number(message))
1010

11-
export const parseStringifiedDate = (value: any) => {
11+
export const parseStringifiedDate = (
12+
value: any,
13+
options?: { parseDates?: boolean }
14+
) => {
1215
if (typeof value !== 'string') return null
16+
if (options?.parseDates === false) return null
1317

1418
// Remove quote from stringified date
1519
const temp = value.replace(/"/g, '')
@@ -34,9 +38,12 @@ export const isStringifiedObject = (value: string) => {
3438
return (start === 123 && end === 125) || (start === 91 && end === 93)
3539
}
3640

37-
export const parseStringifiedObject = (data: string) =>
41+
export const parseStringifiedObject = (
42+
data: string,
43+
options?: { parseDates?: boolean }
44+
) =>
3845
JSON.parse(data, (_, value) => {
39-
const date = parseStringifiedDate(value)
46+
const date = parseStringifiedDate(value, options)
4047

4148
if (date) {
4249
return date
@@ -45,28 +52,36 @@ export const parseStringifiedObject = (data: string) =>
4552
return value
4653
})
4754

48-
export const parseStringifiedValue = (value: string) => {
55+
export const parseStringifiedValue = (
56+
value: string,
57+
options?: { parseDates?: boolean }
58+
) => {
4959
if (!value) return value
5060
if (isNumericString(value)) return +value
5161
if (value === 'true') return true
5262
if (value === 'false') return false
5363

54-
const date = parseStringifiedDate(value)
55-
if (date) return date
64+
if (options?.parseDates !== false) {
65+
const date = parseStringifiedDate(value, options)
66+
if (date) return date
67+
}
5668

5769
if (isStringifiedObject(value)) {
5870
try {
59-
return parseStringifiedObject(value)
71+
return parseStringifiedObject(value, options)
6072
} catch {}
6173
}
6274

6375
return value
6476
}
6577

66-
export const parseMessageEvent = (event: MessageEvent) => {
78+
export const parseMessageEvent = (
79+
event: MessageEvent,
80+
options?: { parseDates?: boolean }
81+
) => {
6782
const messageString = event.data.toString()
6883

6984
return messageString === 'null'
7085
? null
71-
: parseStringifiedValue(messageString)
86+
: parseStringifiedValue(messageString, options)
7287
}

test/parsingUtils.test.ts

Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
import { describe, expect, it } from 'bun:test'
2+
import {
3+
parseStringifiedDate,
4+
parseStringifiedValue,
5+
parseStringifiedObject
6+
} from '../src/utils/parsingUtils'
7+
8+
describe('parseStringifiedDate', () => {
9+
const isoDate = '2024-01-15T10:30:00.000Z'
10+
const shortDate = '01/05/2026'
11+
const formalDate =
12+
'Mon Jan 15 2024 10:30:00 GMT+0000 (Coordinated Universal Time)'
13+
14+
it('should parse ISO8601 date by default', () => {
15+
const result = parseStringifiedDate(isoDate)
16+
expect(result).toBeInstanceOf(Date)
17+
})
18+
19+
it('should parse short date format by default', () => {
20+
const result = parseStringifiedDate(shortDate)
21+
expect(result).toBeInstanceOf(Date)
22+
})
23+
24+
it('should parse formal date format by default', () => {
25+
const result = parseStringifiedDate(formalDate)
26+
expect(result).toBeInstanceOf(Date)
27+
})
28+
29+
it('should parse date when parseDates is true', () => {
30+
const result = parseStringifiedDate(isoDate, { parseDates: true })
31+
expect(result).toBeInstanceOf(Date)
32+
})
33+
34+
it('should NOT parse date when parseDates is false', () => {
35+
const result = parseStringifiedDate(isoDate, { parseDates: false })
36+
expect(result).toBeNull()
37+
})
38+
39+
it('should NOT parse short date when parseDates is false', () => {
40+
const result = parseStringifiedDate(shortDate, { parseDates: false })
41+
expect(result).toBeNull()
42+
})
43+
44+
it('should parse date when parseDates is undefined', () => {
45+
const result = parseStringifiedDate(isoDate, { parseDates: undefined })
46+
expect(result).toBeInstanceOf(Date)
47+
})
48+
49+
it('should return null for non-string values', () => {
50+
expect(parseStringifiedDate(123)).toBeNull()
51+
expect(parseStringifiedDate(null)).toBeNull()
52+
expect(parseStringifiedDate(undefined)).toBeNull()
53+
})
54+
55+
it('should return null for non-date strings', () => {
56+
expect(parseStringifiedDate('hello')).toBeNull()
57+
expect(parseStringifiedDate('123456789')).toBeNull()
58+
})
59+
})
60+
61+
describe('parseStringifiedValue', () => {
62+
const isoDate = '2024-01-15T10:30:00.000Z'
63+
64+
it('should parse date strings by default', () => {
65+
const result = parseStringifiedValue(isoDate)
66+
expect(result).toBeInstanceOf(Date)
67+
})
68+
69+
it('should parse date strings when parseDates is true', () => {
70+
const result = parseStringifiedValue(isoDate, { parseDates: true })
71+
expect(result).toBeInstanceOf(Date)
72+
})
73+
74+
it('should NOT parse date strings when parseDates is false', () => {
75+
const result = parseStringifiedValue(isoDate, { parseDates: false })
76+
expect(result).toBe(isoDate)
77+
})
78+
79+
it('should still parse numbers when parseDates is false', () => {
80+
expect(parseStringifiedValue('123', { parseDates: false })).toBe(123)
81+
expect(parseStringifiedValue('45.67', { parseDates: false })).toBe(
82+
45.67
83+
)
84+
})
85+
86+
it('should still parse booleans when parseDates is false', () => {
87+
expect(parseStringifiedValue('true', { parseDates: false })).toBe(true)
88+
expect(parseStringifiedValue('false', { parseDates: false })).toBe(
89+
false
90+
)
91+
})
92+
93+
it('should return empty string as is', () => {
94+
expect(parseStringifiedValue('')).toBe('')
95+
})
96+
97+
it('should parse numbers by default', () => {
98+
expect(parseStringifiedValue('42')).toBe(42)
99+
})
100+
101+
it('should parse booleans by default', () => {
102+
expect(parseStringifiedValue('true')).toBe(true)
103+
expect(parseStringifiedValue('false')).toBe(false)
104+
})
105+
})
106+
107+
describe('parseStringifiedObject', () => {
108+
it('should parse dates inside objects by default', () => {
109+
const json = '{"date":"2024-01-15T10:30:00.000Z","name":"test"}'
110+
const result = parseStringifiedObject(json)
111+
112+
expect(result.date).toBeInstanceOf(Date)
113+
expect(result.name).toBe('test')
114+
})
115+
116+
it('should parse dates inside objects when parseDates is true', () => {
117+
const json = '{"date":"2024-01-15T10:30:00.000Z"}'
118+
const result = parseStringifiedObject(json, { parseDates: true })
119+
120+
expect(result.date).toBeInstanceOf(Date)
121+
})
122+
123+
it('should NOT parse dates inside objects when parseDates is false', () => {
124+
const json = '{"date":"2024-01-15T10:30:00.000Z","name":"test"}'
125+
const result = parseStringifiedObject(json, { parseDates: false })
126+
127+
expect(result.date).toBe('2024-01-15T10:30:00.000Z')
128+
expect(result.name).toBe('test')
129+
})
130+
131+
it('should handle nested objects with dates', () => {
132+
const json = '{"user":{"createdAt":"2024-01-15T10:30:00.000Z"}}'
133+
134+
const withParsing = parseStringifiedObject(json, { parseDates: true })
135+
const withoutParsing = parseStringifiedObject(json, {
136+
parseDates: false
137+
})
138+
139+
expect(withParsing.user.createdAt).toBeInstanceOf(Date)
140+
expect(withoutParsing.user.createdAt).toBe('2024-01-15T10:30:00.000Z')
141+
})
142+
143+
it('should handle arrays with dates', () => {
144+
const json =
145+
'{"dates":["2024-01-15T10:30:00.000Z","2024-02-20T15:00:00.000Z"]}'
146+
147+
const withParsing = parseStringifiedObject(json, { parseDates: true })
148+
const withoutParsing = parseStringifiedObject(json, {
149+
parseDates: false
150+
})
151+
152+
expect(withParsing.dates[0]).toBeInstanceOf(Date)
153+
expect(withParsing.dates[1]).toBeInstanceOf(Date)
154+
expect(withoutParsing.dates[0]).toBe('2024-01-15T10:30:00.000Z')
155+
expect(withoutParsing.dates[1]).toBe('2024-02-20T15:00:00.000Z')
156+
})
157+
})

0 commit comments

Comments
 (0)