Customize your SDK with hooks
This tutorial includes the following SDK languages and versions:
TypeScript v1 TypeScript v2 Java Python v1 Python v2 C# Go PHP ✅ ✅ ✅ ✅ ✅ ✅ ✅ ✅
Hooks are a powerful way to customize your SDK. They allow you to hook into the API calls and modify the request or response. You can also use them to add custom functionality to your SDK.
Some examples of ways you can use hooks:
- Add custom headers to all requests
- Add an API version as a header or a query string parameter
- Customize authentication, such as copying an API key from a header to a URL parameter
- Add telemetry
- Add logging to API errors
In this tutorial, we'll show you how to use hooks to add API versions to your SDK using the Accept-version
header.
Prerequisites
This tutorial assumes you already have:
- The liblab CLI installed and you are logged in
- An API spec
- A liblab config file created using
liblab init
Steps
In this tutorial you'll:
Create the hooks
First we'll create the hooks code. Run the following command in the same folder as your config file:
liblab hooks add
This will create a hooks
folder with hooks for each SDK language defined in your config file:
hooks
├── csharp
├── go
├── java
├── php
├── python
└── typescript
You may see a different set of hooks folders depending on the languages you have defined in your config file.
For each language, you may need to install some necessary dependencies.
- TypeScript v1
- TypeScript v2
- Python v1
- Python v2
- Java
- C#
- Go
- PHP
To install the TypeScript dependencies, run the following command from the hooks/typescript
folder:
npm install
To install the TypeScript dependencies, run the following command from the hooks/typescript
folder:
npm install
The hooks folder includes a requirements.txt
file with the relevant dependencies for the hooks code. You should install this in a virtual environment.
-
Create the virtual environment in the
hooks/python
folder:Terminalpython -m venv .venv
Use
python3
if you have both Python 2 and Python 3 installed. -
Activate the virtual environment. On linux or macOS run:
Terminalsource .venv/bin/activate
On Windows run:
cmd.venv\Scripts\activate.bat
-
Install the requirements:
Terminalpip install -r requirements.txt
When you first create hooks this file is empty. When you add dependencies to your hooks, they need to be added here to be picked up by the SDK generation code and added to your SDK.
The hooks folder includes a requirements.txt
file with the relevant dependencies for the hooks code. You should install this in a virtual environment.
-
Create the virtual environment in the
hooks/python
folder:Terminalpython -m venv .venv
Use
python3
if you have both Python 2 and Python 3 installed. -
Activate the virtual environment. On linux or macOS run:
Terminalsource .venv/bin/activate
On Windows run:
cmd.venv\Scripts\activate.bat
-
Install the requirements:
Terminalpip install -r requirements.txt
When you first create hooks this file is empty. When you add dependencies to your hooks, they need to be added here to be picked up by the SDK generation code and added to your SDK.
The Java hooks project is set up to use Maven. You can install the dependencies with:
mvn install
The C# hooks code has no additional dependencies.
The Go hooks code has no additional dependencies.
The hooks folder includes a composer.json
file with the relevant dependencies for the hooks code.
-
Install the dependencies:
Terminalcomposer install
Add the hook code
In the hooks
folder, you will find a project for each of the languages you have generated. The API version will be added to a header in the before request hook.
Locate the before request hook
Open the relevant file and inspect the hook code.
- TypeScript v1
- TypeScript v2
- Python v1
- Python v2
- Java
- C#
- Go
- PHP
The beforeRequest
method is implemented in the CustomHook
class in the src/index.ts
file:
export default class CustomHook implements Hook {
async beforeRequest(request: Request): Promise<void> {
// Your code goes here
}
}
The beforeRequest
method is implemented in the CustomHook
class in the src/index.ts
file:
export default class CustomHook implements Hook {
async beforeRequest(request: Request): Promise<void> {
// Your code goes here
}
}
The before_request
method is implemented in the CustomHook
class in the src/hook.py
file:
class CustomHook:
def before_request(self, request: Request, **kwargs):
# Your code goes here
The before_request
method is implemented in the CustomHook
class in the src/hook.py
file:
class CustomHook:
def before_request(self, request: Request, **kwargs):
# Your code goes here
The beforeRequest
method is implemented in the CustomHook
class in the src/main/<namespace>/hook/CustomHook.java
file:
public class CustomHook implements Hook {
@Override
public void beforeRequest(Request request) {
// Your code goes here
}
}
The BeforeRequestAsync
method is implemented in the CustomHook
class in the CustomHook.cs
file:
public async Task<HttpRequestMessage> BeforeRequestAsync(HttpRequestMessage request)
{
// Your code goes here
}
The BeforeRequest
method is implemented in the hooks
package in the custom_hook.go
file:
type CustomHook struct{}
func (h *CustomHook) BeforeRequest(req Request) Request {
// Your code goes here
return req
}
The beforeRequest
method is implemented in the CustomHook
class in the CustomHook.php
file:
class CustomHook implements HookInterface
{
public function beforeRequest(RequestInterface &$request): void
{
// Your code goes here
}
}
Each request object has a headers property, which is a dictionary of headers. We can add the API version to this dictionary.
- TypeScript v1
- TypeScript v2
- Python v1
- Python v2
- Java
- C#
- Go
- PHP
The Request
class lives in the src/index.ts
file:
export interface Request {
method: string;
url: string;
input?: object;
headers: object;
}
The Request
class lives in the src/index.ts
file:
export interface Request {
method: string;
url: string;
input?: object;
headers: object;
}
The Request
class lives in the src/hook.py
file:
class Request:
def __init__(self, method, url, headers, body=""):
self.method = method
self.url = url
self.headers = headers
self.body = body
The Request
class lives in the src/hook.py
file:
class Request:
def __init__(self, method, url, headers, body=""):
self.method = method
self.url = url
self.headers = headers
self.body = body
The Request
class lives in the src/main/<namespace>/hook/model/Request.java
file:
public class Request {
public Request(String method, String url, String body, Map<String, String> headers) {}
public String getMethod() {}
public void setMethod(String method) {}
public String getUrl() {}
public void setUrl(String url) {}
public String getBody() {}
public void setBody(String body) {}
public Map<String, String> getHeaders() {}
public void setHeaders(Map<String, String> headers) {}
}
The request
type used is an instance of System.Net.Http.HttpRequestMessage
.
The Request
type lives in the hooks/hook.go
file:
type Request interface {
GetMethod() string
SetMethod(method string)
GetBaseUrl() string
SetBaseUrl(baseUrl string)
GetPath() string
SetPath(path string)
GetHeader(header string) string
SetHeader(header string, value string)
GetPathParam(param string) string
SetPathParam(param string, value any)
GetQueryParam(param string) string
SetQueryParam(param string, value any)
GetBody() any
SetBody(body any)
}
The request
type used is an instance of Psr\Http\Message\RequestInterface
.
These request objects are mutable, and any changes you make will be used by the SDK when making the API call.
Add the version header
To add the version header, make the following code changes to the before request hooks:
- TypeScript v1
- TypeScript v2
- Python v1
- Python v2
- Java
- C#
- Go
- PHP
async beforeRequest(request: Request): Promise<void> {
// Add the Accept-version header to the request with version 1.0
request.headers = {
...request.headers,
"Accept-version": "1.0"
};
}
async beforeRequest(request: Request): Promise<void> {
// Add the Accept-version header to the request with version 1.0
request.headers = {
...request.headers,
"Accept-version": "1.0"
};
}
def before_request(self, request: Request, **kwargs):
# Add the Accept-version header to the request with version 1.0
request.headers["Accept-version"] = "1.0"
def before_request(self, request: Request, **kwargs):
# Add the Accept-version header to the request with version 1.0
request.headers["Accept-version"] = "1.0"
public void beforeRequest(Request request) {
// Add the Accept-version header to the request with version 1.0
request.getHeaders().put("Accept-version", "1.0");
}
public async Task<HttpRequestMessage> BeforeRequestAsync(HttpRequestMessage request)
{
request.Headers.Add("Accept-version", "1.0");
return request;
}
func (h *CustomHook) BeforeRequest(req Request) Request {
req.SetHeader("X-Request-Id", "123")
return req
}
public function beforeRequest(RequestInterface &$request): void
{
// Add the Accept-version header to the request with version 1.0
$request = $request->withHeader('Accept-version', '1.0');
}
This will insert the new header into the headers dictionary, and this will then be used by the SDK when making the API call.
Add tests
The hook code includes a test project for you to build unit tests against your hook code.
- TypeScript v1
- TypeScript v2
- Python v1
- Python v2
- Java
- C#
- Go
- PHP
The tests live in the tests
folder. An example test is included in the hook.spec.ts
file.
Rename the test from example passing test
to given the custom hook, when before request is called, then the API version header is added
:
it('given the custom hook, when before request is called, then the API version header is added', async () => {
Add the following assertion to the end of the test code, below the existing assertion:
// Assert the version header is added
assert(request.headers['Accept-version'] === '1.0');
Expand to see the complete test code
import { beforeEach } from 'mocha';
import { expect } from 'chai';
import CustomHook, { Request } from "../src/index";
import * as dotenv from 'dotenv';
import { assert } from 'console';
dotenv.config();
describe('test custom hook', () => {
let hook = new CustomHook();
beforeEach(() => {
hook = new CustomHook();
});
it('given the custom hook, when before request is called, then the API version header is added', async () => {
const request: Request = {
method: 'GET',
url: 'https://api.example.com',
input: {},
headers: {
'Content-Type': 'application/json',
},
};
await hook.beforeRequest(request);
console.log(request.headers)
// Assert the headers are correct
assert(request.headers['Content-Type'] === 'application/json');
// Assert the version header is added
assert(request.headers['Accept-version'] === '1.0');
});
});
Run the tests:
npm test
You will see the test pass:
test custom hook
{ 'Content-Type': 'application/json', 'Accept-version': '1.0' }
✓ empty passing test
1 passing (3ms)
The tests live in the tests
folder. An example test is included in the hook.spec.ts
file.
Rename the test from example passing test
to given the custom hook, when before request is called, then the API version header is added
:
it('given the custom hook, when before request is called, then the API version header is added', async () => {
Add the following assertion to the end of the test code, below the existing assertion:
// Assert the version header is added
assert(request.headers['Accept-version'] === '1.0');
Expand to see the complete test code
import { beforeEach } from 'mocha';
import { expect } from 'chai';
import CustomHook, { Request } from "../src/index";
import * as dotenv from 'dotenv';
import { assert } from 'console';
dotenv.config();
describe('test custom hook', () => {
let hook = new CustomHook();
beforeEach(() => {
hook = new CustomHook();
});
it('given the custom hook, when before request is called, then the API version header is added', async () => {
const request: Request = {
method: 'GET',
url: 'https://api.example.com',
input: {},
headers: {
'Content-Type': 'application/json',
},
};
await hook.beforeRequest(request);
console.log(request.headers)
// Assert the headers are correct
assert(request.headers['Content-Type'] === 'application/json');
// Assert the version header is added
assert(request.headers['Accept-version'] === '1.0');
});
});
Run the tests:
npm test
You will see the test pass:
test custom hook
{ 'Content-Type': 'application/json', 'Accept-version': '1.0' }
✓ empty passing test
1 passing (3ms)
The tests live in the test
folder. An example test is included in the test_hook.py
file.
Rename the test method from test_before_request
to test_given_the_custom_hook_when_before_request_is_called_then_the_API_version_header_is_added
:
def test_given_the_custom_hook_when_before_request_is_called_then_the_API_version_header_is_added(self):
Add the following assertions to the end of the test code, below the existing assertions:
# Assert the API version header is set
self.assertTrue(request.headers["Accept-version"] is not None)
self.assertEqual(request.headers["Accept-version"], "1.0")
Expand to see the complete test code
import unittest
from src.hook import CustomHook, Request
class TestCustomHook(unittest.TestCase):
@classmethod
def setUpClass(cls):
pass
def test_given_the_custom_hook_when_before_request_is_called_then_the_API_version_header_is_added(self):
hook = CustomHook()
request = Request("GET", "https://api.example.com", {
"Content-Type": "application/json",
})
hook.before_request(request)
# Assert the header is set
self.assertTrue(request.headers["Content-Type"] is not None)
self.assertEqual(request.headers["Content-Type"], "application/json")
# Assert the API version header is set
self.assertTrue(request.headers["Accept-version"] is not None)
self.assertEqual(request.headers["Accept-version"], "1.0")
if __name__ == '__main__':
unittest.main()
Run the tests:
pytest
You will see the test pass:
collected 1 item
test/test_hook.py . [100%]
=============================== 1 passed in 0.01s ===============================
The tests live in the test
folder. An example test is included in the test_hook.py
file.
Rename the test method from test_before_request
to test_given_the_custom_hook_when_before_request_is_called_then_the_API_version_header_is_added
:
def test_given_the_custom_hook_when_before_request_is_called_then_the_API_version_header_is_added(self):
Add the following assertions to the end of the test code, below the existing assertions:
# Assert the API version header is set
self.assertTrue(request.headers["Accept-version"] is not None)
self.assertEqual(request.headers["Accept-version"], "1.0")
Expand to see the complete test code
import unittest
from src.hook import CustomHook, Request
class TestCustomHook(unittest.TestCase):
@classmethod
def setUpClass(cls):
pass
def test_given_the_custom_hook_when_before_request_is_called_then_the_API_version_header_is_added(self):
hook = CustomHook()
request = Request("GET", "https://api.example.com", {
"Content-Type": "application/json",
})
hook.before_request(request)
# Assert the header is set
self.assertTrue(request.headers["Content-Type"] is not None)
self.assertEqual(request.headers["Content-Type"], "application/json")
# Assert the API version header is set
self.assertTrue(request.headers["Accept-version"] is not None)
self.assertEqual(request.headers["Accept-version"], "1.0")
if __name__ == '__main__':
unittest.main()
Run the tests:
pytest
You will see the test pass:
collected 1 item
test/test_hook.py . [100%]
=============================== 1 passed in 0.01s ===============================
The tests live in the src/test
folder. An example test is included in the CustomHookTest.java
file.
Rename the test method from testBeforeRequest
to testGivenTheCustomHookWhenBeforeRequestIsCalledThenTheAPIVersionHeaderIsAdded
:
public void testGivenTheCustomHookWhenBeforeRequestIsCalledThenTheAPIVersionHeaderIsAdded() {
Add the following assertions to the end of the test code, below the existing assertions:
// Assert the API version header is present
assertTrue(request.getHeaders().containsKey("Accept-version"));
assertEquals(request.getHeaders().get("Accept-version"), "1.0");
Expand to see the complete test code
package com.liblab.hook;
import com.liblab.hook.model.Request;
import java.util.HashMap;
import org.junit.Before;
import org.junit.Test;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.assertEquals;;
public class CustomHookTest {
@Before
public void setUp() {
}
@Test
public void testGivenTheCustomHookWhenBeforeRequestIsCalledThenTheAPIVersionHeaderIsAdded() {
CustomHook hook = new CustomHook();
Request request = new Request("GET", "https://api.example.com", "", new HashMap<String, String>() {{
put("Content-Type", "application/json");
}});
hook.beforeRequest(request);
System.out.println(request.getHeaders());
// Assert the header is present
assertTrue(request.getHeaders().containsKey("Content-Type"));
assertEquals(request.getHeaders().get("Content-Type"), "application/json");
// Assert the API version header is present
assertTrue(request.getHeaders().containsKey("Accept-version"));
assertEquals(request.getHeaders().get("Accept-version"), "1.0");
}
}
Run the tests:
mvn test
You will see the test pass:
[INFO] Results:
[INFO]
[INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0
[INFO]
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 1.096 s
[INFO] Finished at: 2023-09-08T20:47:41Z
[INFO] ------------------------------------------------------------------------
This feature is not currently supported during the C# beta.
This feature is not currently supported during the Go beta.
This feature is not currently supported during the PHP beta.
Build the SDK
Now that your hook code is ready, you can build the SDK with the following command:
liblab build
Packaging hooks...
✓ C# built
✓ Go built
✓ Java built
✓ Python built
✓ TypeScript built
Successfully generated SDKs downloaded. You can find them inside the "output" folder
This will send your hook code along with your spec and config to liblab to use in the SDK generation. You can see the results in the SDKs that are generated and downloaded to the output
folder.
- TypeScript v1
- TypeScript v2
- Python v1
- Python v2
- Java
- C#
- Go
- PHP
The hooks can be found in the src/hooks
folder:
hooks
├── CustomHook.ts
└── Hook.ts
Your hook code is in the Hook.ts
file.
The hooks can be found in the src/hooks
folder:
hooks
├── CustomHook.ts
└── Hook.ts
Your hook code is in the Hook.ts
file.
The hooks can be found in the src/<project>/hooks
folder:
hooks
├── __init__.py
└── hook.py
Your hook code is in the hook.py
file.
The hooks can be found in the src/<project>/hooks
folder:
hooks
├── __init__.py
└── hook.py
Your hook code is in the hook.py
file.
The hooks can be found in the src/main/<namespace>/hooks
folder:
hooks
├── CustomHook.java
└── model
├── Hook.java
├── Request.java
└── Response.java
Your hook code is in the CustomHook.java
file.
The hooks can be found in the <namespace>/Hooks
folder:
Hooks
├── CustomHook.cs
└── IHook.cs
Your hook code is in the CustomHook.cs
file.
The hooks can be found in the internal/clients/rest/hooks
folder:
Hooks
├── custom_hook.go
└── hook.go
Your hook code is in the custom_hook.go
file.
The hooks can be found in the src/hooks
folder:
hooks
├── CustomHook.php
└── HookInterface.php
Your hook code is in the CustomHook.php
file.