Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,8 @@ npm install @google-cloud/functions-framework
};
```

Option 1:

1. Run the following command:

```sh
Expand All @@ -76,6 +78,33 @@ npm install @google-cloud/functions-framework

1. Open http://localhost:8080/ in your browser and see _Hello, World_.

Option 2:

1. Create a `main.js` file with the following contents:

```js
import { run } from '@google-cloud/functions-framework';
import { helloWorld } from './hello.js';

run(helloWorld);
```

1. Run `node main.js` to start the local development server to serve the function:

```
Serving function...
Function: function
Signature type: http
URL: http://localhost:8080/
```

1. Send requests to this function using `curl` from another terminal window:

```sh
curl localhost:8080
# Output: Hello, World
```

### Quickstart: Set up a new project

1. Create a `package.json` file using `npm init`:
Expand Down
65 changes: 65 additions & 0 deletions docs/generated/api.json
Original file line number Diff line number Diff line change
Expand Up @@ -1589,6 +1589,71 @@
"endIndex": 2
}
]
},
{
"kind": "Function",
"canonicalReference": "@google-cloud/functions-framework!run_2:function(1)",
"docComment": "/**\n * Main entrypoint for the functions framework that loads the user's function and starts the HTTP server.\n *\n * @param code - A function to be executed.\n */\n",
"excerptTokens": [
{
"kind": "Content",
"text": "main: (code?: "
},
{
"kind": "Reference",
"text": "HttpFunction",
"canonicalReference": "@google-cloud/functions-framework!HttpFunction:interface"
},
{
"kind": "Content",
"text": " | "
},
{
"kind": "Reference",
"text": "EventFunction",
"canonicalReference": "@google-cloud/functions-framework!EventFunction:interface"
},
{
"kind": "Content",
"text": " | "
},
{
"kind": "Reference",
"text": "CloudEventFunction",
"canonicalReference": "@google-cloud/functions-framework!CloudEventFunction:interface"
},
{
"kind": "Content",
"text": ") => "
},
{
"kind": "Reference",
"text": "Promise",
"canonicalReference": "!Promise:interface"
},
{
"kind": "Content",
"text": "<void>"
}
],
"fileUrlPath": "src/main.ts",
"returnTypeTokenRange": {
"startIndex": 7,
"endIndex": 9
},
"releaseTag": "Public",
"overloadIndex": 1,
"parameters": [
{
"parameterName": "code",
"parameterTypeTokenRange": {
"startIndex": 1,
"endIndex": 6
},
"isOptional": true
}
],
"name": "run_2"
}
]
}
Expand Down
4 changes: 4 additions & 0 deletions docs/generated/api.md.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,10 @@ export { Request_2 as Request }

export { Response_2 as Response }

// @public
const run_2: (code?: HttpFunction | EventFunction | CloudEventFunction) => Promise<void>;
export { run_2 as run }

// (No @packageDocumentation comment for this package)

```
2 changes: 1 addition & 1 deletion src/function_registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ const registrationContainer = new Map<string, RegisteredFunction<any>>();
/**
* Helper method to store a registered function in the registration container
*/
const register = <T = unknown, U = unknown>(
const register = <T = unknown>(
functionName: string,
signatureType: SignatureType,
userFunction: HandlerFunction<T>,
Expand Down
5 changes: 5 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,3 +21,8 @@ export * from './functions';
* @public
*/
export {http, cloudEvent} from './function_registry';

/**
* @public
*/
export {main as run} from './main';
38 changes: 30 additions & 8 deletions src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,13 +21,22 @@ import {getUserFunction} from './loader';
import {ErrorHandler} from './invoker';
import {getServer} from './server';
import {parseOptions, helpText, OptionsError} from './options';
import {
HttpFunction,
EventFunction,
CloudEventFunction,
HandlerFunction,
} from './functions';
import {loggingHandlerAddExecutionContext} from './logger';

/**
* Main entrypoint for the functions framework that loads the user's function
* and starts the HTTP server.
* @param code - A function to be executed.
*/
export const main = async () => {
export const main = async (
code?: HttpFunction | EventFunction | CloudEventFunction,
) => {
try {
const options = parseOptions();

Expand All @@ -40,11 +49,21 @@ export const main = async () => {
loggingHandlerAddExecutionContext();
}

const loadedFunction = await getUserFunction(
options.sourceLocation,
options.target,
options.signatureType,
);
let loadedFunction;
// If a function is provided directly, use it.
if (code) {
loadedFunction = {
userFunction: code,
signatureType: options.signatureType || 'http',
};
} else {
// Otherwise, load the function from file.
loadedFunction = await getUserFunction(
options.sourceLocation,
options.target,
options.signatureType,
);
}
if (!loadedFunction) {
console.error('Could not load the function, shutting down.');
// eslint-disable-next-line no-process-exit
Expand All @@ -55,7 +74,7 @@ export const main = async () => {
// It is possible to overwrite the configured signature type in code so we
// reset it here based on what we loaded.
options.signatureType = signatureType;
const server = getServer(userFunction!, options);
const server = getServer(userFunction as HandlerFunction, options);
const errorHandler = new ErrorHandler(server);
server
.listen(options.port, () => {
Expand All @@ -79,4 +98,7 @@ export const main = async () => {
};

// Call the main method to load the user code and start the http server.
void main();
// Only call main if the module is not being required.
if (require.main === module) {
void main();
}
109 changes: 109 additions & 0 deletions test/integration/programmatic.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
// Copyright 2021 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// 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 * as assert from 'assert';
import * as sinon from 'sinon';
import * as supertest from 'supertest';
import {main} from '../../src/main';
import * as server from '../../src/server';
import {HttpFunction, CloudEventFunction} from '../../src/functions';
import {Server} from 'http';

describe('programmatic functions', () => {
let exitStub: sinon.SinonStub;
let errorStub: sinon.SinonStub;
let getServerStub: sinon.SinonStub;

beforeEach(() => {
exitStub = sinon.stub(process, 'exit');
errorStub = sinon.stub(console, 'error');
});

afterEach(() => {
exitStub.restore();
errorStub.restore();
if (getServerStub) {
getServerStub.restore();
}
});

it('should run an HTTP function', async () => {
const httpFunc: HttpFunction = (req, res) => {
res.send('hello');
};

let capturedServer: Server | null = null;
let listenStub: sinon.SinonStub;
const originalGetServer = server.getServer;
getServerStub = sinon.stub(server, 'getServer').callsFake((fn, opts) => {
const s = originalGetServer(fn, opts);
capturedServer = s;
listenStub = sinon.stub(s, 'listen').returns(s);
return s;
});

await main(httpFunc);

listenStub!.restore();

assert.ok(capturedServer);
const st = supertest(capturedServer!);
const response = await st.get('/');
assert.strictEqual(response.status, 200);
assert.strictEqual(response.text, 'hello');
});

it('should run a CloudEvent function', async () => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
let receivedEvent: any = null;
const cloudEventFunc: CloudEventFunction = cloudEvent => {
receivedEvent = cloudEvent;
};

let capturedServer: Server | null = null;
let listenStub: sinon.SinonStub;
const originalGetServer = server.getServer;
getServerStub = sinon.stub(server, 'getServer').callsFake((fn, opts) => {
const s = originalGetServer(fn, opts);
capturedServer = s;
listenStub = sinon.stub(s, 'listen').returns(s);
return s;
});

const argv = process.argv;
process.argv = ['node', 'index.js', '--signature-type=cloudevent'];
await main(cloudEventFunc);
process.argv = argv;

listenStub!.restore();

assert.ok(capturedServer);
const st = supertest(capturedServer!);
const event = {
specversion: '1.0',
type: 'com.google.cloud.storage',
source: 'test',
id: 'test',
data: 'hello',
};
const response = await st
.post('/')
.send(event)
.set('Content-Type', 'application/cloudevents+json');

assert.strictEqual(response.status, 204);
assert.ok(receivedEvent);
assert.strictEqual(receivedEvent.data, 'hello');
});
});
Loading