Skip to content

Commit

Permalink
feat: add function serialization to actor input (apify#169)
Browse files Browse the repository at this point in the history
  • Loading branch information
mnmkng authored Apr 20, 2021
1 parent a488bd8 commit 59d6e80
Show file tree
Hide file tree
Showing 5 changed files with 100 additions and 1 deletion.
39 changes: 38 additions & 1 deletion src/interceptors.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
const axios = require('axios');
const contentTypeParser = require('content-type');
const { maybeParseBody } = require('./body_parser');
const {
isNode,
Expand Down Expand Up @@ -28,10 +29,46 @@ class InvalidResponseBodyError extends Error {
*/
function serializeRequest(config) {
const [defaultTransform] = axios.defaults.transformRequest;
config.data = defaultTransform(config.data, config.headers);

// The function not only serializes data, but it also adds correct headers.
const data = defaultTransform(config.data, config.headers);

// Actor inputs can include functions and we don't want to omit those,
// because it's convenient for users. JSON.stringify removes them.
// It's a bit inefficient that we serialize the JSON twice, but I feel
// it's a small price to pay. The axios default transform does a lot
// of body type checks and we would have to copy all of them to the resource clients.
if (config.stringifyFunctions) {
const contentTypeHeader = config.headers['Content-Type'] || config.headers['content-type'];
try {
const { type } = contentTypeParser.parse(contentTypeHeader);
if (type === 'application/json') {
config.data = stringifyWithFunctions(config.data);
} else {
config.data = data;
}
} catch (err) {
config.data = data;
}
} else {
config.data = data;
}

return config;
}

/**
* JSON.stringify() that serializes functions to string instead
* of replacing them with null or removing them.
* @param {object} obj
* @return {string}
*/
function stringifyWithFunctions(obj) {
return JSON.stringify(obj, (key, value) => {
return typeof value === 'function' ? value.toString() : value;
});
}

/**
* @param {object} config
* @return {Promise<object>}
Expand Down
3 changes: 3 additions & 0 deletions src/resource_clients/actor.js
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,9 @@ class ActorClient extends ResourceClient {
method: 'POST',
data: input,
params: this._params(params),
// Apify internal property. Tells the request serialization interceptor
// to stringify functions to JSON, instead of omitting them.
stringifyFunctions: true,
};
if (options.contentType) {
request.headers = {
Expand Down
3 changes: 3 additions & 0 deletions src/resource_clients/task.js
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,9 @@ class TaskClient extends ResourceClient {
method: 'POST',
data: input,
params: this._params(params),
// Apify internal property. Tells the request serialization interceptor
// to stringify functions to JSON, instead of omitting them.
stringifyFunctions: true,
};

const response = await this.httpClient.call(request);
Expand Down
28 changes: 28 additions & 0 deletions test/actors.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,34 @@ describe('Actor methods', () => {
validateRequest(query, { actorId }, { some: 'body' }, { 'content-type': contentType });
});

test('start() works with functions in input', async () => {
const actorId = 'some-id';
const input = {
foo: 'bar',
fn: async (a, b) => a + b,
};

const expectedRequestProps = [
{},
{ actorId },
{ foo: 'bar', fn: input.fn.toString() },
{ 'content-type': 'application/json;charset=utf-8' },
];

const res = await client.actor(actorId).start(input);
expect(res.id).toEqual('run-actor');
validateRequest(...expectedRequestProps);

const browserRes = await page.evaluate((id) => {
return client.actor(id).start({
foo: 'bar',
fn: async (a, b) => a + b,
});
}, actorId);
expect(browserRes).toEqual(res);
validateRequest(...expectedRequestProps);
});

test('start() with webhook works', async () => {
const actorId = 'some-id';
const webhooks = [
Expand Down
28 changes: 28 additions & 0 deletions test/tasks.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,34 @@ describe('Task methods', () => {
validateRequest(query, { taskId }, input);
});

test('start() works with functions in input', async () => {
const taskId = 'some-id';
const input = {
foo: 'bar',
fn: async (a, b) => a + b,
};

const expectedRequestProps = [
{},
{ taskId },
{ foo: 'bar', fn: input.fn.toString() },
{ 'content-type': 'application/json;charset=utf-8' },
];

const res = await client.task(taskId).start(input);
expect(res.id).toEqual('run-task');
validateRequest(...expectedRequestProps);

const browserRes = await page.evaluate((id) => {
return client.task(id).start({
foo: 'bar',
fn: async (a, b) => a + b,
});
}, taskId);
expect(browserRes).toEqual(res);
validateRequest(...expectedRequestProps);
});

test('start() works with webhooks', async () => {
const taskId = 'some-id';
const webhooks = [
Expand Down

0 comments on commit 59d6e80

Please sign in to comment.