Skip to content

Commit 0556c99

Browse files
authored
feat: timestamp serializing and de-serializing (#216)
1 parent 51eb26f commit 0556c99

File tree

26 files changed

+752
-58
lines changed

26 files changed

+752
-58
lines changed

Diff for: packages/json-builder/src/index.spec.ts

+21-1
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,8 @@ import {
1010
listOfStringsShape,
1111
mapOfStringsToIntegersShape,
1212
stringShape,
13-
timestampShape
13+
timestampShape,
14+
timestampShapeCustom
1415
} from './shapes.fixtures';
1516

1617
describe('JsonBuilder', () => {
@@ -220,13 +221,22 @@ describe('JsonBuilder', () => {
220221
members: {
221222
timestamp: {
222223
shape: {...timestampShape},
224+
},
225+
timestampCustom: {
226+
shape: {...timestampShapeCustom},
227+
},
228+
timestampMember: {
229+
shape: {...timestampShape},
230+
timestampFormat: 'rfc822',
223231
}
224232
}
225233
}
226234
}
227235
};
228236
const date = new Date('2017-05-22T19:33:14.175Z');
229237
const timestamp = 1495481594;
238+
const timestampCustom = '2017-05-22T19:33:14Z';
239+
const timestampMember = 'Mon, 22 May 2017 19:33:14 GMT';
230240
const jsonBody = new JsonBuilder(jest.fn(), jest.fn());
231241

232242
it('should convert Date objects to epoch timestamps', () => {
@@ -244,6 +254,16 @@ describe('JsonBuilder', () => {
244254
.toBe(JSON.stringify({timestamp}));
245255
});
246256

257+
it('should format dates using timestampFormat trait of shape', () => {
258+
expect(jsonBody.build({operation, input: {timestampCustom: date}}))
259+
.toBe(JSON.stringify({timestampCustom}));
260+
});
261+
262+
it('should format dates using timestampFormat trait of member', () => {
263+
expect(jsonBody.build({operation, input: {timestampMember: date}}))
264+
.toBe(JSON.stringify({timestampMember}));
265+
})
266+
247267
it('should throw if a value that cannot be converted to a time object is provided', () => {
248268
for (let nonTime of [[], {}, true, new ArrayBuffer(0)]) {
249269
expect(() => jsonBody.build({operation, input: {timestamp: nonTime}}))

Diff for: packages/json-builder/src/index.ts

+12-12
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import {isArrayBuffer} from '@aws-sdk/is-array-buffer';
2-
import {epoch} from "@aws-sdk/protocol-timestamp";
2+
import {formatTimestamp} from "@aws-sdk/protocol-timestamp";
33
import {isIterable} from "@aws-sdk/is-iterable";
44
import {
55
BodySerializer,
@@ -8,7 +8,8 @@ import {
88
Encoder,
99
OperationModel,
1010
SerializationModel,
11-
Structure as StructureShape
11+
Structure as StructureShape,
12+
Member
1213
} from "@aws-sdk/types";
1314

1415
type Scalar = string|number|boolean|null;
@@ -32,12 +33,12 @@ export class JsonBuilder implements BodySerializer {
3233
member = operation.input,
3334
input
3435
}: BodySerializerBuildOptions): string {
35-
let shape = member.shape as StructureShape;
36-
return JSON.stringify(this.format(shape, input));
36+
return JSON.stringify(this.format(member, input));
3737
}
3838

39-
private format(shape: SerializationModel, input: any): JsonValue {
39+
private format(member: Member, input: any): JsonValue {
4040
const inputType = typeof input;
41+
const shape = member.shape;
4142
if (shape.type === 'structure') {
4243
if (inputType !== 'object' || input === null) {
4344
throw new Error(
@@ -59,10 +60,9 @@ export class JsonBuilder implements BodySerializer {
5960
const {
6061
location,
6162
locationName = key,
62-
shape: memberShape
6363
} = shape.members[key];
6464
if (!location) {
65-
data[locationName] = this.format(memberShape, input[key]);
65+
data[locationName] = this.format(shape.members[key], input[key]);
6666
}
6767
}
6868

@@ -71,7 +71,7 @@ export class JsonBuilder implements BodySerializer {
7171
if (Array.isArray(input) || isIterable(input)) {
7272
const data: JsonArray = [];
7373
for (let element of input) {
74-
data.push(this.format(shape.member.shape, element));
74+
data.push(this.format(shape.member, element));
7575
}
7676

7777
return data;
@@ -86,7 +86,7 @@ export class JsonBuilder implements BodySerializer {
8686
// A map input is should be a [key, value] iterable...
8787
if (isIterable(input)) {
8888
for (let [key, value] of input) {
89-
data[key] = this.format(shape.value.shape, value);
89+
data[key] = this.format(shape.value, value);
9090
}
9191
return data;
9292
}
@@ -100,7 +100,7 @@ export class JsonBuilder implements BodySerializer {
100100
}
101101

102102
for (let key of Object.keys(input)) {
103-
data[key] = this.format(shape.value.shape, input[key]);
103+
data[key] = this.format(shape.value, input[key]);
104104
}
105105
return data;
106106
} else if (shape.type === 'blob') {
@@ -127,7 +127,7 @@ export class JsonBuilder implements BodySerializer {
127127
['number', 'string'].indexOf(typeof input) > -1
128128
|| Object.prototype.toString.call(input) === '[object Date]'
129129
) {
130-
return epoch(input);
130+
return formatTimestamp(input, member.timestampFormat || shape.timestampFormat || 'unixTimestamp');
131131
}
132132

133133
throw new Error(
@@ -138,4 +138,4 @@ export class JsonBuilder implements BodySerializer {
138138

139139
return input;
140140
}
141-
}
141+
}

Diff for: packages/json-builder/src/shapes.fixtures.ts

+5
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,11 @@ export const timestampShape: Timestamp = {
3232
type: 'timestamp'
3333
};
3434

35+
export const timestampShapeCustom: Timestamp = {
36+
type: 'timestamp',
37+
timestampFormat: 'iso8601'
38+
}
39+
3540
export const listOfStringsShape: List = {
3641
type: 'list',
3742
member: {

Diff for: packages/json-parser/src/index.spec.ts

+17-5
Original file line numberDiff line numberDiff line change
@@ -126,16 +126,28 @@ describe('JsonParser', () => {
126126

127127
describe('timestamps', () => {
128128
const timestampShape: Member = {shape: {type: "timestamp"}};
129-
const date = new Date('2017-05-22T19:33:14.000Z');
130-
const timestamp = 1495481594;
129+
const unixTimestamp = 1495481594;
130+
const rfc822Timestamp = 'Mon, May 22, 2017 19:33:14 GMT'
131+
const isoTimestamp = '2017-05-22T19:33:14.000Z';
132+
const date = new Date(isoTimestamp);
131133
const jsonBody = new JsonParser(jest.fn());
132134

133-
it('should convert timestamps to date objects', () => {
134-
expect(jsonBody.parse(timestampShape, timestamp.toString(10)))
135+
it('should convert unixTimestamps to date objects', () => {
136+
expect(jsonBody.parse(timestampShape, unixTimestamp.toString(10)))
135137
.toEqual(date);
136138
});
137139

138-
it('should return undefined if the input is not a number', () => {
140+
it('should convert rfc822 timeStamps to date objects', () => {
141+
expect(jsonBody.parse(timestampShape, JSON.stringify(rfc822Timestamp)))
142+
.toEqual(date);
143+
});
144+
145+
it('should convert iso8601 timeStamps to date objects', () => {
146+
expect(jsonBody.parse(timestampShape, JSON.stringify(isoTimestamp)))
147+
.toEqual(date);
148+
});
149+
150+
it('should return undefined if the input is not a timestamp', () => {
139151
expect(jsonBody.parse(timestampShape, JSON.stringify('foo')))
140152
.toBeUndefined();
141153
});

Diff for: packages/json-parser/src/index.ts

+7-4
Original file line numberDiff line numberDiff line change
@@ -68,11 +68,14 @@ export class JsonParser implements BodyParser {
6868
}
6969

7070
if (shape.type === 'timestamp') {
71-
if (typeof input !== 'number') {
72-
return undefined;
71+
if (typeof input === 'string' || typeof input === 'number') {
72+
let date = toDate(input);
73+
if(date.toString() === 'Invalid Date') {
74+
return undefined;
75+
}
76+
return date;
7377
}
74-
75-
return toDate(input);
78+
return undefined;
7679
}
7780

7881
if (shape.type === 'blob') {

Diff for: packages/protocol-rest/src/RestSerializer.spec.ts

+14-3
Original file line numberDiff line numberDiff line change
@@ -316,7 +316,7 @@ describe('RestMarshaller', () => {
316316
expect(serialized.headers['x-amz-bool']).toBe('false');
317317
});
318318

319-
it('populates headers from timestamps', () => {
319+
it('populates headers from timestamps using rfc822 by default', () => {
320320
const toSerialize = {
321321
Bucket: 'bucket',
322322
Key: 'key',
@@ -327,17 +327,28 @@ describe('RestMarshaller', () => {
327327
expect(serialized.headers['x-amz-timestamp']).toBe('Thu, 01 Jan 1970 00:00:00 GMT');
328328
});
329329

330-
it('always populates headers from timestamps using rfc822', () => {
330+
it('populates headers from timestamps using specified format', () => {
331331
const toSerialize = {
332332
Bucket: 'bucket',
333333
Key: 'key',
334334
HeaderTimestampOverride: new Date(0)
335335
};
336336

337337
const serialized = restMarshaller.serialize(complexGetOperation, toSerialize);
338-
expect(serialized.headers['x-amz-timestamp-ovr']).toBe('Thu, 01 Jan 1970 00:00:00 GMT');
338+
expect(serialized.headers['x-amz-timestamp-ovr']).toBe('1970-01-01T00:00:00Z');
339339
});
340340

341+
it('populates headers preferring timestampFormat on member over shape', () => {
342+
const toSerialize = {
343+
Bucket: 'bucket',
344+
Key: 'key',
345+
HeaderTimestampMemberOverride: new Date(0)
346+
}
347+
348+
const serialized = restMarshaller.serialize(complexGetOperation, toSerialize);
349+
expect(serialized.headers['x-amz-timestamp-member-ovr']).toBe('0');
350+
})
351+
341352
it('populates blobs', () => {
342353
const base64Encoder = jest.fn(() => 'base64');
343354
const utf8Decoder = jest.fn();

Diff for: packages/protocol-rest/src/RestSerializer.ts

+13-12
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,4 @@
1-
import {
2-
rfc822
3-
} from '@aws-sdk/protocol-timestamp';
1+
import {formatTimestamp} from '@aws-sdk/protocol-timestamp';
42
import {
53
BodySerializer,
64
Decoder,
@@ -133,23 +131,23 @@ export class RestSerializer<StreamType> implements
133131
const member = members[memberName];
134132
const {
135133
location,
136-
locationName = memberName,
137-
shape: memberShape
134+
locationName = memberName
138135
} = member;
139136

140137
if (location === 'header' || location === 'headers') {
141-
this.populateHeader(headers, memberShape, locationName, inputValue);
138+
this.populateHeader(headers, member, locationName, inputValue);
142139
} else if (location === 'uri') {
143140
uri = this.populateUri(uri, locationName, inputValue);
144141
} else if (location === 'querystring') {
145-
this.populateQuery(query, memberShape, locationName, inputValue);
142+
this.populateQuery(query, member, locationName, inputValue);
146143
}
147144
}
148145

149146
return {headers, query, uri};
150147
}
151148

152-
private populateQuery(query: QueryParameterBag, shape: SerializationModel, name: string, input: any) {
149+
private populateQuery(query: QueryParameterBag, member: Member, name: string, input: any) {
150+
const shape = member.shape;
153151
if (shape.type === 'list') {
154152
const values = [];
155153
if (isIterable(input)) {
@@ -166,14 +164,16 @@ export class RestSerializer<StreamType> implements
166164
} else if (shape.type === 'map') {
167165
if (isIterable(input)) {
168166
for (let [inputKey, inputValue] of input) {
169-
this.populateQuery(query, shape.value.shape, inputKey, inputValue);
167+
this.populateQuery(query, shape.value, inputKey, inputValue);
170168
}
171169
} else if (typeof input === 'object' && input !== null) {
172170
for (let inputKey of Object.keys(input)) {
173171
const inputValue = input[inputKey];
174-
this.populateQuery(query, shape.value.shape, inputKey, inputValue);
172+
this.populateQuery(query, shape.value, inputKey, inputValue);
175173
}
176174
}
175+
} else if (shape.type === 'timestamp') {
176+
query[name] = encodeURIComponent(String(formatTimestamp(input, member.timestampFormat || shape.timestampFormat || 'iso8601')));
177177
} else {
178178
query[name] = String(input);
179179
}
@@ -192,7 +192,8 @@ export class RestSerializer<StreamType> implements
192192
}
193193
return uri;
194194
}
195-
private populateHeader(headers: HeaderBag, shape: SerializationModel, name: string, input: any): void {
195+
private populateHeader(headers: HeaderBag, member: Member, name: string, input: any): void {
196+
const shape = member.shape;
196197
if (shape.type === 'map') {
197198
if (isIterable(input)) {
198199
for (let [inputKey, inputValue] of input) {
@@ -206,7 +207,7 @@ export class RestSerializer<StreamType> implements
206207
} else {
207208
switch (shape.type) {
208209
case 'timestamp':
209-
headers[name] = rfc822(input);
210+
headers[name] = String(formatTimestamp(input, member.timestampFormat || shape.timestampFormat || 'rfc822'));
210211
break;
211212
case 'string':
212213
headers[name] = shape.jsonValue ?

Diff for: packages/protocol-rest/src/operations.fixture.ts

+9
Original file line numberDiff line numberDiff line change
@@ -195,6 +195,15 @@ export const complexGetOperation: OperationModel = {
195195
location: 'header',
196196
locationName: 'x-amz-timestamp-ovr'
197197
},
198+
HeaderTimestampMemberOverride: {
199+
shape: {
200+
type:'timestamp',
201+
timestampFormat: 'iso8601'
202+
},
203+
location: 'header',
204+
locationName: 'x-amz-timestamp-member-ovr',
205+
timestampFormat: 'unixTimestamp'
206+
},
198207
QueryList: {
199208
shape: {
200209
type: 'list',

Diff for: packages/protocol-timestamp/src/index.spec.ts

+54
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import {
33
iso8601,
44
rfc822,
55
toDate,
6+
formatTimestamp,
67
} from "./";
78

89
const toIsoString = '2017-05-22T19:33:14.175Z';
@@ -89,3 +90,56 @@ describe('toDate', () => {
8990
expect(date.valueOf()).toBe(epochTs * 1000);
9091
});
9192
});
93+
94+
describe('formatTimestamp', () => {
95+
it('should throw error for invalid format', () => {
96+
expect(
97+
() => formatTimestamp(epochTs, 'badFormat')
98+
).toThrowError('Invalid TimestampFormat: badFormat');
99+
});
100+
101+
it('should format epoch timestamps to RFC 822 strings', () => {
102+
const date = formatTimestamp(epochTs, 'rfc822');
103+
expect(date.valueOf()).toBe(rfc822String);
104+
});
105+
106+
it('should format epoch timestamps to ISO-8601', () => {
107+
const date = formatTimestamp(epochTs, 'iso8601')
108+
expect(date.valueOf()).toBe(iso8601String);
109+
});
110+
111+
it('should format epoch timestamps to unixTimestamp', () => {
112+
const date = formatTimestamp(epochTs, 'unixTimestamp')
113+
expect(date.valueOf()).toBe(epochTs);
114+
});
115+
116+
it('should format ISO-8601 timestamps to RFC 822 strings', () => {
117+
const date = formatTimestamp(iso8601String, 'rfc822');
118+
expect(date.valueOf()).toBe(rfc822String);
119+
});
120+
121+
it('should format ISO-8601 timestamps to ISO-8601', () => {
122+
const date = formatTimestamp(iso8601String, 'iso8601')
123+
expect(date.valueOf()).toBe(iso8601String);
124+
});
125+
126+
it('should format ISO-8601 timestamps to unixTimestamp', () => {
127+
const date = formatTimestamp(iso8601String, 'unixTimestamp')
128+
expect(date.valueOf()).toBe(epochTs);
129+
});
130+
131+
it('should format RFC 822 timestamps to RFC 822 strings', () => {
132+
const date = formatTimestamp(rfc822String, 'rfc822');
133+
expect(date.valueOf()).toBe(rfc822String);
134+
});
135+
136+
it('should format RFC 822 timestamps to ISO-8601', () => {
137+
const date = formatTimestamp(rfc822String, 'iso8601')
138+
expect(date.valueOf()).toBe(iso8601String);
139+
});
140+
141+
it('should format RFC 822 timestamps to unixTimestamp', () => {
142+
const date = formatTimestamp(rfc822String, 'unixTimestamp')
143+
expect(date.valueOf()).toBe(epochTs);
144+
});
145+
});

0 commit comments

Comments
 (0)