Skip to content

Commit

Permalink
Merge pull request #67 from nitrictech/feature/json-response
Browse files Browse the repository at this point in the history
feat: add json response helper
  • Loading branch information
jyecusch authored Oct 7, 2021
2 parents 08262dd + 23de3fd commit efc414a
Show file tree
Hide file tree
Showing 5 changed files with 134 additions and 80 deletions.
58 changes: 29 additions & 29 deletions licenseconfig.json
Original file line number Diff line number Diff line change
@@ -1,31 +1,31 @@
{
"ignoreFile": ".gitignore",
"ignore": [
"README",
".*",
"**/*.config.*",
"**/.gitignore",
"lib/**/*",
"contracts/**/*",
".yarn/",
".github/",
"**/*.md",
"*.lock",
"*.html",
"**/.eslintignore"
],
"license": "./assets/license_header.txt",
"licenseFormats": {
"js|ts": {
"eachLine": {
"prepend": "// "
}
},
"dotfile|^Dockerfile": {
"eachLine": {
"prepend": "# "
}
}
"ignoreFile": ".gitignore",
"ignore": [
"README",
".*",
"**/*.config.*",
"**/.gitignore",
"lib/**/*",
"contracts/**/*",
".yarn/",
".github/",
"**/*.md",
"*.lock",
"*.html",
"**/.eslintignore"
],
"license": "./assets/license_header.txt",
"licenseFormats": {
"js|ts": {
"eachLine": {
"prepend": "// "
}
},
"trailingWhitespace": "TRIM"
}
"dotfile|^Dockerfile": {
"eachLine": {
"prepend": "# "
}
}
},
"trailingWhitespace": "TRIM"
}
20 changes: 15 additions & 5 deletions src/faas/v0/context.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,8 @@ describe('NitricTrigger.fromGrpcTriggerRequest', () => {
const testHeader2 = new HeaderValue();
testHeader2.addValue('test2.1');
testHeader2.addValue('test2.2');
ctx.getHeadersMap().set('test', testHeader);
ctx.getHeadersMap().set('test2', testHeader2);
ctx.getHeadersMap().set('test', testHeader);
ctx.getHeadersMap().set('test2', testHeader2);
ctx.getQueryParamsMap().set('test', 'test');
const request = new TriggerRequest();
request.setData('Hello World');
Expand All @@ -49,7 +49,6 @@ describe('NitricTrigger.fromGrpcTriggerRequest', () => {
it('should have HTTP context', () => {
expect(trigger.http).not.toBeUndefined();
});


it('should have the triggers HTTP Method', () => {
expect(trigger.http.req.method).toBe('GET');
Expand All @@ -67,6 +66,17 @@ describe('NitricTrigger.fromGrpcTriggerRequest', () => {
it('should have the provided query params', () => {
expect(trigger.http.req.query['test']).toBe('test');
});

it('should allow json response', () => {
const ctx = trigger.http.res.json({ message: 'success' });
expect(ctx.res.headers).toEqual({ 'Content-Type': ['application/json'] });

expect(
JSON.parse(new TextDecoder('utf-8').decode(ctx.res.body as Uint8Array))
).toEqual({
message: 'success',
});
});
});

// XXX: Remove once the deprecated old headers value is removed.
Expand All @@ -77,8 +87,8 @@ describe('NitricTrigger.fromGrpcTriggerRequest', () => {
const ctx = new GrpcHttpTriggerRequest();
ctx.setMethod('GET');
ctx.setPath('/test/');
ctx.getHeadersOldMap().set('test', 'test');
ctx.getHeadersOldMap().set('test2', 'test2');
ctx.getHeadersOldMap().set('test', 'test');
ctx.getHeadersOldMap().set('test2', 'test2');
const request = new TriggerRequest();
request.setData('Hello World');
request.setHttp(ctx);
Expand Down
115 changes: 72 additions & 43 deletions src/faas/v0/context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,20 +18,24 @@ import {
HttpResponseContext,
HeaderValue,
} from '@nitric/api/proto/faas/v1/faas_pb';
import { jsonResponse } from './json';

export abstract class TriggerContext<Req extends AbstractRequest = AbstractRequest, Resp extends Record<string, any> = any> {
export abstract class TriggerContext<
Req extends AbstractRequest = AbstractRequest,
Resp extends Record<string, any> = any
> {
protected request: Req;
protected response: Resp;

/**
*
*
*/
public get http(): HttpContext | undefined {
return undefined
return undefined;
}

/**
*
*
*/
public get event(): EventContext | undefined {
return undefined;
Expand All @@ -45,7 +49,7 @@ export abstract class TriggerContext<Req extends AbstractRequest = AbstractReque
}

/**
*
*
*/
get res(): Resp {
return this.response;
Expand All @@ -61,19 +65,17 @@ export abstract class TriggerContext<Req extends AbstractRequest = AbstractReque
} else if (trigger.hasTopic()) {
return EventContext.fromGrpcTriggerRequest(trigger);
}
throw new Error("Unsupported trigger request type");
throw new Error('Unsupported trigger request type');
}

static toGrpcTriggerResponse(
ctx: TriggerContext
): TriggerResponse {
static toGrpcTriggerResponse(ctx: TriggerContext): TriggerResponse {
if (ctx.http) {
return HttpContext.toGrpcTriggerResponse(ctx);
} else if (ctx.event) {
return EventContext.toGrpcTriggerResponse(ctx);
}

throw new Error("Unsupported trigger context type");
throw new Error('Unsupported trigger context type');
}
}

Expand All @@ -85,20 +87,14 @@ export abstract class AbstractRequest {
}
}

interface HttpResponse {
status: number;
headers: Record<string, string[]>;
body: string | Uint8Array;
}

interface EventResponse {
success: boolean;
}

type Method = 'GET' | 'POST' | 'DELETE' | 'PATCH' | 'PUT' | 'HEAD';

interface HttpRequestArgs {
data: string | Uint8Array,
data: string | Uint8Array;
method: Method | string;
path: string;
query: Record<string, string>;
Expand All @@ -109,7 +105,7 @@ export class HttpRequest extends AbstractRequest {
public readonly method: Method | string;
public readonly path: string;
public readonly query: Record<string, string>;
public readonly headers: Record<string, (string[] | string)>;
public readonly headers: Record<string, string[] | string>;

constructor({ data, method, path, query, headers }: HttpRequestArgs) {
super(data);
Expand All @@ -120,9 +116,38 @@ export class HttpRequest extends AbstractRequest {
}
}

interface HttpResponseArgs {
body: string | Uint8Array;
status: number;
headers: Record<string, string[]>;
ctx: HttpContext;
}

export class HttpResponse {
public status: number;
public body: string | Uint8Array;
public headers: Record<string, string[]>;
private ctx: HttpContext;

constructor({ status, headers, body, ctx }: HttpResponseArgs) {
this.status = status;
this.headers = headers;
this.body = body;
this.ctx = ctx;
}

/**
* Helper method to encode to JSON string for JSON http responses
* @returns HttpContext with body property set with an encoded JSON string and json headers set.
*/
get json() {
return jsonResponse(this.ctx);
}
}

export class EventRequest extends AbstractRequest {
public readonly topic: string;

constructor(data: string | Uint8Array, topic: string) {
super(data);
this.topic = topic;
Expand All @@ -142,43 +167,50 @@ export class HttpContext extends TriggerContext<HttpRequest, HttpResponse> {
const http = trigger.getHttp();
const ctx = new HttpContext();

const headers = (http
const headers = ((http
.getHeadersMap()
// XXX: getEntryList claims to return [string, faas.HeaderValue][], but really returns [string, string[][]][]
// we force the type to match the real return type.
.getEntryList() as unknown as [string, string[][]][])
.reduce((acc, [key, [val]]) => ({
...acc, [key.toLowerCase()]: val.length === 1 ? val[0] : val,
}), {});

.getEntryList() as unknown) as [string, string[][]][]).reduce(
(acc, [key, [val]]) => ({
...acc,
[key.toLowerCase()]: val.length === 1 ? val[0] : val,
}),
{}
);

const oldHeaders = http
.getHeadersOldMap()
.toArray()
.reduce((acc, [key, val]) => ({
...acc,
[key.toLowerCase()]: val
}), {});
.reduce(
(acc, [key, val]) => ({
...acc,
[key.toLowerCase()]: val,
}),
{}
);

ctx.request = new HttpRequest({
data: trigger.getData(),
path: http.getPath(),
query: http
.getQueryParamsMap()
.toArray()
.reduce((acc, [key, val]) => ({ ...acc, [key]: val }), {}),
.getQueryParamsMap()
.toArray()
.reduce((acc, [key, val]) => ({ ...acc, [key]: val }), {}),
// TODO: remove after 1.0
// check for old headers if new headers is unpopulated. This is for backwards compatibility.
headers: Object.keys(headers).length ? headers : oldHeaders,
method: http.getMethod(),
});

ctx.response = {
ctx.response = new HttpResponse({
status: 200,
headers: {},
body: '',
};
ctx,
});

if(!ctx) {
if (!ctx) {
throw new Error('failed to create context');
}

Expand All @@ -195,8 +227,8 @@ export class HttpContext extends TriggerContext<HttpRequest, HttpResponse> {

Object.entries(httpCtx.response.headers).forEach(([k, v]) => {
const headerVal = new HeaderValue();
headerVal.setValueList(v)
resp.getHttp().getHeadersMap().set(k, headerVal)
headerVal.setValueList(v);
resp.getHttp().getHeadersMap().set(k, headerVal);
resp.getHttp().getHeadersOldMap().set(k, v[0]);
});

Expand All @@ -213,10 +245,7 @@ export class EventContext extends TriggerContext<EventRequest, EventResponse> {
const topic = trigger.getTopic();
const ctx = new EventContext();

ctx.request = new EventRequest(
trigger.getData_asU8(),
topic.getTopic(),
);
ctx.request = new EventRequest(trigger.getData_asU8(), topic.getTopic());

ctx.response = {
success: true,
Expand All @@ -233,4 +262,4 @@ export class EventContext extends TriggerContext<EventRequest, EventResponse> {
triggerResponse.setTopic(topicResponse);
return triggerResponse;
}
}
}
1 change: 1 addition & 0 deletions src/faas/v0/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,4 @@
export * from './handler';
export * from './context';
export * from './start';
export * from './json';
20 changes: 17 additions & 3 deletions src/faas/v0/json.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import { HttpContext, HttpMiddleware } from ".";
import { HttpContext, HttpMiddleware } from '.';

const decodeData = (data: string | Uint8Array): string => {
if (typeof data !== 'string') {
Expand All @@ -25,7 +25,21 @@ const decodeData = (data: string | Uint8Array): string => {
* @param ctx HttpContext containing the raw request data.
* @returns HttpContext with body property added containing a decoded JSON object from the req data.
*/
export const json: HttpMiddleware = (ctx: HttpContext) => {
export const json = (): HttpMiddleware => (ctx: HttpContext, next) => {
(ctx.req as any).body = JSON.parse(decodeData(ctx.req.data));
return next(ctx);
};

/**
* Helper method to encode to JSON string for JSON http responses
* @param ctx HttpContext
* @returns HttpContext with body property set with an encoded JSON string and json headers set.
*/
export const jsonResponse = (ctx: HttpContext) => (
data: string | number | boolean | Record<string, any>
) => {
ctx.res.body = new TextEncoder().encode(JSON.stringify(data));
ctx.res.headers['Content-Type'] = ['application/json'];

return ctx;
};
};

0 comments on commit efc414a

Please sign in to comment.