Skip to content

Commit

Permalink
feat: timestamp serializing and de-serializing (#216)
Browse files Browse the repository at this point in the history
  • Loading branch information
srchase committed Apr 16, 2019
1 parent 51eb26f commit 0556c99
Show file tree
Hide file tree
Showing 26 changed files with 752 additions and 58 deletions.
22 changes: 21 additions & 1 deletion packages/json-builder/src/index.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@ import {
listOfStringsShape,
mapOfStringsToIntegersShape,
stringShape,
timestampShape
timestampShape,
timestampShapeCustom
} from './shapes.fixtures';

describe('JsonBuilder', () => {
Expand Down Expand Up @@ -220,13 +221,22 @@ describe('JsonBuilder', () => {
members: {
timestamp: {
shape: {...timestampShape},
},
timestampCustom: {
shape: {...timestampShapeCustom},
},
timestampMember: {
shape: {...timestampShape},
timestampFormat: 'rfc822',
}
}
}
}
};
const date = new Date('2017-05-22T19:33:14.175Z');
const timestamp = 1495481594;
const timestampCustom = '2017-05-22T19:33:14Z';
const timestampMember = 'Mon, 22 May 2017 19:33:14 GMT';
const jsonBody = new JsonBuilder(jest.fn(), jest.fn());

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

it('should format dates using timestampFormat trait of shape', () => {
expect(jsonBody.build({operation, input: {timestampCustom: date}}))
.toBe(JSON.stringify({timestampCustom}));
});

it('should format dates using timestampFormat trait of member', () => {
expect(jsonBody.build({operation, input: {timestampMember: date}}))
.toBe(JSON.stringify({timestampMember}));
})

it('should throw if a value that cannot be converted to a time object is provided', () => {
for (let nonTime of [[], {}, true, new ArrayBuffer(0)]) {
expect(() => jsonBody.build({operation, input: {timestamp: nonTime}}))
Expand Down
24 changes: 12 additions & 12 deletions packages/json-builder/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import {isArrayBuffer} from '@aws-sdk/is-array-buffer';
import {epoch} from "@aws-sdk/protocol-timestamp";
import {formatTimestamp} from "@aws-sdk/protocol-timestamp";
import {isIterable} from "@aws-sdk/is-iterable";
import {
BodySerializer,
Expand All @@ -8,7 +8,8 @@ import {
Encoder,
OperationModel,
SerializationModel,
Structure as StructureShape
Structure as StructureShape,
Member
} from "@aws-sdk/types";

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

private format(shape: SerializationModel, input: any): JsonValue {
private format(member: Member, input: any): JsonValue {
const inputType = typeof input;
const shape = member.shape;
if (shape.type === 'structure') {
if (inputType !== 'object' || input === null) {
throw new Error(
Expand All @@ -59,10 +60,9 @@ export class JsonBuilder implements BodySerializer {
const {
location,
locationName = key,
shape: memberShape
} = shape.members[key];
if (!location) {
data[locationName] = this.format(memberShape, input[key]);
data[locationName] = this.format(shape.members[key], input[key]);
}
}

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

return data;
Expand All @@ -86,7 +86,7 @@ export class JsonBuilder implements BodySerializer {
// A map input is should be a [key, value] iterable...
if (isIterable(input)) {
for (let [key, value] of input) {
data[key] = this.format(shape.value.shape, value);
data[key] = this.format(shape.value, value);
}
return data;
}
Expand All @@ -100,7 +100,7 @@ export class JsonBuilder implements BodySerializer {
}

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

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

return input;
}
}
}
5 changes: 5 additions & 0 deletions packages/json-builder/src/shapes.fixtures.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,11 @@ export const timestampShape: Timestamp = {
type: 'timestamp'
};

export const timestampShapeCustom: Timestamp = {
type: 'timestamp',
timestampFormat: 'iso8601'
}

export const listOfStringsShape: List = {
type: 'list',
member: {
Expand Down
22 changes: 17 additions & 5 deletions packages/json-parser/src/index.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -126,16 +126,28 @@ describe('JsonParser', () => {

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

it('should convert timestamps to date objects', () => {
expect(jsonBody.parse(timestampShape, timestamp.toString(10)))
it('should convert unixTimestamps to date objects', () => {
expect(jsonBody.parse(timestampShape, unixTimestamp.toString(10)))
.toEqual(date);
});

it('should return undefined if the input is not a number', () => {
it('should convert rfc822 timeStamps to date objects', () => {
expect(jsonBody.parse(timestampShape, JSON.stringify(rfc822Timestamp)))
.toEqual(date);
});

it('should convert iso8601 timeStamps to date objects', () => {
expect(jsonBody.parse(timestampShape, JSON.stringify(isoTimestamp)))
.toEqual(date);
});

it('should return undefined if the input is not a timestamp', () => {
expect(jsonBody.parse(timestampShape, JSON.stringify('foo')))
.toBeUndefined();
});
Expand Down
11 changes: 7 additions & 4 deletions packages/json-parser/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,11 +68,14 @@ export class JsonParser implements BodyParser {
}

if (shape.type === 'timestamp') {
if (typeof input !== 'number') {
return undefined;
if (typeof input === 'string' || typeof input === 'number') {
let date = toDate(input);
if(date.toString() === 'Invalid Date') {
return undefined;
}
return date;
}

return toDate(input);
return undefined;
}

if (shape.type === 'blob') {
Expand Down
17 changes: 14 additions & 3 deletions packages/protocol-rest/src/RestSerializer.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -316,7 +316,7 @@ describe('RestMarshaller', () => {
expect(serialized.headers['x-amz-bool']).toBe('false');
});

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

it('always populates headers from timestamps using rfc822', () => {
it('populates headers from timestamps using specified format', () => {
const toSerialize = {
Bucket: 'bucket',
Key: 'key',
HeaderTimestampOverride: new Date(0)
};

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

it('populates headers preferring timestampFormat on member over shape', () => {
const toSerialize = {
Bucket: 'bucket',
Key: 'key',
HeaderTimestampMemberOverride: new Date(0)
}

const serialized = restMarshaller.serialize(complexGetOperation, toSerialize);
expect(serialized.headers['x-amz-timestamp-member-ovr']).toBe('0');
})

it('populates blobs', () => {
const base64Encoder = jest.fn(() => 'base64');
const utf8Decoder = jest.fn();
Expand Down
25 changes: 13 additions & 12 deletions packages/protocol-rest/src/RestSerializer.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
import {
rfc822
} from '@aws-sdk/protocol-timestamp';
import {formatTimestamp} from '@aws-sdk/protocol-timestamp';
import {
BodySerializer,
Decoder,
Expand Down Expand Up @@ -133,23 +131,23 @@ export class RestSerializer<StreamType> implements
const member = members[memberName];
const {
location,
locationName = memberName,
shape: memberShape
locationName = memberName
} = member;

if (location === 'header' || location === 'headers') {
this.populateHeader(headers, memberShape, locationName, inputValue);
this.populateHeader(headers, member, locationName, inputValue);
} else if (location === 'uri') {
uri = this.populateUri(uri, locationName, inputValue);
} else if (location === 'querystring') {
this.populateQuery(query, memberShape, locationName, inputValue);
this.populateQuery(query, member, locationName, inputValue);
}
}

return {headers, query, uri};
}

private populateQuery(query: QueryParameterBag, shape: SerializationModel, name: string, input: any) {
private populateQuery(query: QueryParameterBag, member: Member, name: string, input: any) {
const shape = member.shape;
if (shape.type === 'list') {
const values = [];
if (isIterable(input)) {
Expand All @@ -166,14 +164,16 @@ export class RestSerializer<StreamType> implements
} else if (shape.type === 'map') {
if (isIterable(input)) {
for (let [inputKey, inputValue] of input) {
this.populateQuery(query, shape.value.shape, inputKey, inputValue);
this.populateQuery(query, shape.value, inputKey, inputValue);
}
} else if (typeof input === 'object' && input !== null) {
for (let inputKey of Object.keys(input)) {
const inputValue = input[inputKey];
this.populateQuery(query, shape.value.shape, inputKey, inputValue);
this.populateQuery(query, shape.value, inputKey, inputValue);
}
}
} else if (shape.type === 'timestamp') {
query[name] = encodeURIComponent(String(formatTimestamp(input, member.timestampFormat || shape.timestampFormat || 'iso8601')));
} else {
query[name] = String(input);
}
Expand All @@ -192,7 +192,8 @@ export class RestSerializer<StreamType> implements
}
return uri;
}
private populateHeader(headers: HeaderBag, shape: SerializationModel, name: string, input: any): void {
private populateHeader(headers: HeaderBag, member: Member, name: string, input: any): void {
const shape = member.shape;
if (shape.type === 'map') {
if (isIterable(input)) {
for (let [inputKey, inputValue] of input) {
Expand All @@ -206,7 +207,7 @@ export class RestSerializer<StreamType> implements
} else {
switch (shape.type) {
case 'timestamp':
headers[name] = rfc822(input);
headers[name] = String(formatTimestamp(input, member.timestampFormat || shape.timestampFormat || 'rfc822'));
break;
case 'string':
headers[name] = shape.jsonValue ?
Expand Down
9 changes: 9 additions & 0 deletions packages/protocol-rest/src/operations.fixture.ts
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,15 @@ export const complexGetOperation: OperationModel = {
location: 'header',
locationName: 'x-amz-timestamp-ovr'
},
HeaderTimestampMemberOverride: {
shape: {
type:'timestamp',
timestampFormat: 'iso8601'
},
location: 'header',
locationName: 'x-amz-timestamp-member-ovr',
timestampFormat: 'unixTimestamp'
},
QueryList: {
shape: {
type: 'list',
Expand Down
54 changes: 54 additions & 0 deletions packages/protocol-timestamp/src/index.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import {
iso8601,
rfc822,
toDate,
formatTimestamp,
} from "./";

const toIsoString = '2017-05-22T19:33:14.175Z';
Expand Down Expand Up @@ -89,3 +90,56 @@ describe('toDate', () => {
expect(date.valueOf()).toBe(epochTs * 1000);
});
});

describe('formatTimestamp', () => {
it('should throw error for invalid format', () => {
expect(
() => formatTimestamp(epochTs, 'badFormat')
).toThrowError('Invalid TimestampFormat: badFormat');
});

it('should format epoch timestamps to RFC 822 strings', () => {
const date = formatTimestamp(epochTs, 'rfc822');
expect(date.valueOf()).toBe(rfc822String);
});

it('should format epoch timestamps to ISO-8601', () => {
const date = formatTimestamp(epochTs, 'iso8601')
expect(date.valueOf()).toBe(iso8601String);
});

it('should format epoch timestamps to unixTimestamp', () => {
const date = formatTimestamp(epochTs, 'unixTimestamp')
expect(date.valueOf()).toBe(epochTs);
});

it('should format ISO-8601 timestamps to RFC 822 strings', () => {
const date = formatTimestamp(iso8601String, 'rfc822');
expect(date.valueOf()).toBe(rfc822String);
});

it('should format ISO-8601 timestamps to ISO-8601', () => {
const date = formatTimestamp(iso8601String, 'iso8601')
expect(date.valueOf()).toBe(iso8601String);
});

it('should format ISO-8601 timestamps to unixTimestamp', () => {
const date = formatTimestamp(iso8601String, 'unixTimestamp')
expect(date.valueOf()).toBe(epochTs);
});

it('should format RFC 822 timestamps to RFC 822 strings', () => {
const date = formatTimestamp(rfc822String, 'rfc822');
expect(date.valueOf()).toBe(rfc822String);
});

it('should format RFC 822 timestamps to ISO-8601', () => {
const date = formatTimestamp(rfc822String, 'iso8601')
expect(date.valueOf()).toBe(iso8601String);
});

it('should format RFC 822 timestamps to unixTimestamp', () => {
const date = formatTimestamp(rfc822String, 'unixTimestamp')
expect(date.valueOf()).toBe(epochTs);
});
});

0 comments on commit 0556c99

Please sign in to comment.