Skip to content

Commit

Permalink
feat: Use the native ffi bindings for the Verifier instead of the rub…
Browse files Browse the repository at this point in the history
…y bindings
  • Loading branch information
TimothyJones committed Aug 19, 2021
1 parent fc99e86 commit 119c3ce
Show file tree
Hide file tree
Showing 17 changed files with 1,157 additions and 923 deletions.
1,628 changes: 728 additions & 900 deletions package-lock.json

Large diffs are not rendered by default.

6 changes: 4 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@
"chalk": "2.3.1",
"check-types": "7.3.0",
"cross-spawn": "^7.0.1",
"ffi-napi": "^4.0.3",
"libnpmconfig": "^1.2.1",
"mkdirp": "1.0.0",
"needle": "^2.6.0",
Expand All @@ -80,6 +81,7 @@
"@types/cross-spawn": "^6.0.1",
"@types/decompress": "^4.2.3",
"@types/express": "4.11.1",
"@types/ffi-napi": "^2.4.3",
"@types/jest": "^25.2.3",
"@types/mkdirp": "^0.5.2",
"@types/mocha": "2.2.48",
Expand All @@ -92,8 +94,8 @@
"@types/unixify": "^1.0.0",
"@types/unzipper": "^0.10.2",
"@types/url-join": "^4.0.0",
"@typescript-eslint/eslint-plugin": "^2.3.2",
"@typescript-eslint/parser": "^2.3.2",
"@typescript-eslint/eslint-plugin": "^4.28.0",
"@typescript-eslint/parser": "^4.28.0",
"basic-auth": "2.0.0",
"body-parser": "1.18.2",
"chai": "4.1.2",
Expand Down
57 changes: 57 additions & 0 deletions src/ffi/ffi.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
// https://github.com/node-ffi/node-ffi/wiki/Node-FFI-Tutorial
import ffi = require('ffi-napi');

// I am so so sorry about these types. They exist
// to infer the returned type of the library
// using the object that we pass in to describe the functions
type AsyncFfiCall<Args extends unknown[], ReturnType> = {
async: (
...args: [...Args, (err: Error, ret: ReturnType) => void]
) => ReturnType;
};

type FfiFunction<T> = T extends (...a: infer Args) => infer ReturnType
? T & AsyncFfiCall<Args, ReturnType>
: never;

type StringType = 'string' | 'void' | 'int' | 'double' | 'float';

type ActualType<T> = [T] extends ['string']
? string
: [T] extends ['void']
? void
: [T] extends ['int' | 'double' | 'float']
? number
: never;

type ArrayActualType<Tuple extends [...Array<unknown>]> = {
[Index in keyof Tuple]: ActualType<Tuple[Index]>;
} & { length: Tuple['length'] };

type TupleType = [StringType, Array<StringType>];

type FunctionFromArray<A extends TupleType> = A extends [
r: infer ReturnTypeString,
args: [...infer ArgArrayType]
]
? (...args: ArrayActualType<ArgArrayType>) => ActualType<ReturnTypeString>
: never;

type LibDescription<Functions extends string> = {
[k in Functions]: [StringType, Array<StringType>];
};

type FfiBinding<T> = T extends LibDescription<string>
? {
[Key in keyof T]: FfiFunction<FunctionFromArray<T[Key]>>;
}
: never;

// This function exists to wrap the untyped ffi lib, and return
// the typed version
export const initialiseFfi = <T>(
binaryPath: string,
description: T
): FfiBinding<T> =>
// eslint-disable-next-line @typescript-eslint/no-explicit-any
ffi.Library(binaryPath, description as { [k: string]: any });
2 changes: 1 addition & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import pact from './pact';
module.exports = exports = pact;
export default pact;

export * from './verifier';
export * from './verifier/verifier';

export * from './server';

Expand Down
3 changes: 2 additions & 1 deletion src/pact.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import * as path from 'path';
import serverFactory, { Server, ServerOptions } from './server';
import stubFactory, { Stub, StubOptions } from './stub';
import verifierFactory, { VerifierOptions } from './verifier';
import verifierFactory from './verifier/verifier';
import { VerifierOptions } from './verifier/types';
import messageFactory, { MessageOptions } from './message';
import publisherFactory, { PublisherOptions } from './publisher';
import canDeployFactory, {
Expand Down
40 changes: 40 additions & 0 deletions src/verifier/argumentMapper/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { ArgMapping } from './types';
import logger from '../../logger';

export const argumentMapper = <PactOptions>(
argMapping: ArgMapping<PactOptions>,
options: PactOptions
): string[] =>
Object.keys(options)
.map((key: string) => {
if (!argMapping[key]) {
logger.error(`No argument mapping exists for '${key}'`);
return [];
}
if (argMapping[key].warningMessage) {
logger.warn(argMapping[key].warningMessage);
return [];
}
if (argMapping[key].arg) {
switch (argMapping[key].mapper) {
case 'string':
return [argMapping[key].arg, options[key]];
case 'flag':
return options[key] ? [argMapping[key].arg] : [];
default:
logger.error(
`Argument mapper for '${key}' maps to '${argMapping[key].arg}' with unknown mapper type '${argMapping[key].mapper}'`
);
return [];
}
}
if (typeof argMapping[key] === 'function') {
return argMapping[key](options[key]);
}
logger.error(
`The argument mapper completely failed to find a mapping for '${key}'. This is a bug in pact-js-core`
);
return [];
})
// This can be replaced with .flat() when node 10 is EOL
.reduce((acc: string[], current: string[]) => [...acc, ...current], []);
11 changes: 11 additions & 0 deletions src/verifier/argumentMapper/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
export type ArgMapping<PactOptions> = {
[Key in keyof PactOptions]:
| {
arg: string;
mapper: 'string' | 'flag';
}
| {
warningMessage: string;
}
| ((arg: NonNullable<PactOptions[Key]>) => string[]);
};
105 changes: 105 additions & 0 deletions src/verifier/arguments.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
import { LogLevel } from '../service';
import { ArgMapping } from './argumentMapper/types';
import { VerifierOptions } from './types';

import path = require('path');
import url = require('url');
import fs = require('fs');

type UriType = 'URL' | 'DIRECTORY' | 'FILE' | 'FILE_NOT_FOUND';

// Todo: Extract this, and possibly rename
const fileType = (uri: string): UriType => {
if (/https?:/.test(url.parse(uri).protocol || '')) {
return 'URL';
}
try {
if (fs.statSync(path.normalize(uri)).isDirectory()) {
return 'DIRECTORY';
} else {
return 'FILE';
}
} catch (e) {
throw new Error(`Pact file or directory '${uri}' doesn't exist`);
}
};

export const argMapping: ArgMapping<VerifierOptions> = {
providerBaseUrl: (providerBaseUrl: string) => {
const u = url.parse(providerBaseUrl);
return u && u.port && u.hostname
? ['--port', u.port, '--hostname', u.hostname]
: [];
},
logLevel: (logLevel: LogLevel) => ['--loglevel', logLevel],
provider: { arg: '--provider-name', mapper: 'string' },
pactUrls: (pactUrls: string[]) =>
pactUrls.reduce<Array<string>>((acc: Array<string>, uri: string) => {
switch (fileType(uri)) {
case 'URL':
return [...acc, '--url', uri];
case 'DIRECTORY':
return [...acc, '--dir', uri];
case 'FILE':
return [...acc, '--file', uri];
case 'FILE_NOT_FOUND':
throw new Error(`Pact file or directory '${uri}' doesn't exist`);
default:
return acc;
}
}, []),
pactBrokerUrl: { arg: '--broker-url', mapper: 'string' },
pactBrokerUsername: { arg: '--user', mapper: 'string' },
pactBrokerPassword: { arg: '--password', mapper: 'string' },
pactBrokerToken: { arg: '--broker-token', mapper: 'string' },
consumerVersionTags: (tags: string | string[]) => [
'--consumer-version-tags',
Array.isArray(tags) ? tags.join(',') : tags,
],
providerVersionTags: (tags: string | string[]) => [
'--provider-version-tags',
Array.isArray(tags) ? tags.join(',') : tags,
],
providerStatesSetupUrl: { arg: '--state-change-url', mapper: 'string' },

providerVersion: { arg: '--provider-version', mapper: 'string' },

// Todo in FFI
includeWipPactsSince: { arg: '--include-wip-pacts-since', mapper: 'string' },
consumerVersionSelectors: () => {
throw new Error('Consumer version selectors are not yet implemented');
},
publishVerificationResult: { arg: '--publish', mapper: 'flag' },
enablePending: { arg: '--enable-pending', mapper: 'flag' },

// Todo in Rust
customProviderHeaders: () => {
throw new Error('customProviderHeaders are not yet implemented');
},
timeout: {
warningMessage: 'Timeout currently has no effect on the rust binary',
},
// We should support these, I think
format: {
warningMessage:
"All output is currently on standard out, setting 'format' has no effect",
},
out: {
warningMessage:
"All output is currently on standard out, setting 'out' has no effect",
},

// Deprecate
logDir: {
warningMessage:
'Setting logDir is deprecated as all logs are now on standard out.',
},
verbose: {
warningMessage:
"Verbose mode is deprecated and has no effect, please use logLevel: 'DEBUG' instead",
},
monkeypatch: {
warningMessage:
'The undocumented feature monkeypatch is no more, please file an issue if you were using it and need this functionality',
},
};
28 changes: 28 additions & 0 deletions src/verifier/ffiVerifier.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import path = require('path');
import { initialiseFfi } from '../ffi/ffi';

// We have to declare this twice because typescript can't figure it out
// There's a workaround here we could employ:
// https://gist.github.com/jcalz/381562d282ebaa9b41217d1b31e2c211
type FfiVerifierType = {
init: ['string', ['string']];
version: ['string', []];
free_string: ['void', ['string']];
verify: ['int', ['string']];
};

const description: FfiVerifierType = {
init: ['string', ['string']],
version: ['string', []],
free_string: ['void', ['string']],
verify: ['int', ['string']],
};

// TODO: make this dynamic and select the right one for the architecture
const dll = path.resolve(
process.cwd(),
'ffi',
'libpact_verifier_ffi-osx-x86_64.dylib'
);

export const verifierLib = initialiseFfi(dll, description);
Empty file added src/verifier/index.ts
Empty file.
88 changes: 88 additions & 0 deletions src/verifier/nativeVerifier.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import { VerifierOptions } from './types';
import { verifierLib } from './ffiVerifier';
import logger from '../logger';
import { argMapping } from './arguments';
import { argumentMapper } from './argumentMapper';

const VERIFICATION_SUCCESSFUL = 0;
const VERIFICATION_FAILED = 1;
// 2 - null string passed
// 3 - method panicked
const INVALID_ARGUMENTS = 4;

const pactCrashMessage = (
extraMessage: string
) => `!!!!!!!!! PACT CRASHED !!!!!!!!!
${extraMessage}
This is almost certainly a bug in pact-js-core. It would be great if you could
open a bug report at: https://github.com/pact-foundation/pact-js-core/issues
so that we can fix it.
There is additional debugging information above. If you open a bug report,
please rerun with logLevel: 'debug' set in the VerifierOptions, and include the
full output.
SECURITY WARNING: Before including your log in the issue tracker, make sure you
have removed sensitive info such as login credentials and urls that you don't want
to share with the world.
Lastly, we're sorry about this!
`;

export const verify = (opts: VerifierOptions): Promise<string> => {
// Todo: probably separate out the sections of this logic into separate promises
return new Promise<string>((resolve, reject) => {
// Todo: Does this need to be a specific log level?
// PACT_LOG_LEVEL
// LOG_LEVEL
// < .. >
verifierLib.init('LOG_LEVEL');

const request = argumentMapper(argMapping, opts).join('\n');

logger.debug('sending arguments to FFI:');
logger.debug(request);

verifierLib.verify.async(request, (err: Error, res: number) => {
logger.debug(`response from verifier: ${err}, ${res}`);
if (err) {
logger.error(err);
logger.error(
pactCrashMessage(
'The underlying pact core returned an error through the ffi interface'
)
);
reject(err);
} else {
switch (res) {
case VERIFICATION_SUCCESSFUL:
logger.info('Verification successful');
resolve(`finished: ${res}`);
break;
case VERIFICATION_FAILED:
logger.error('Verification unsuccessful');
reject(new Error('Verfication failed'));
break;
case INVALID_ARGUMENTS:
logger.error(
pactCrashMessage(
'The underlying pact core was invoked incorrectly.'
)
);
reject(new Error('Verification was unable to run'));
break;
default:
logger.error(
pactCrashMessage(
'The underlying pact core crashed in an unexpected way.'
)
);
reject(new Error('Pact core crashed'));
break;
}
}
});
});
};
Loading

0 comments on commit 119c3ce

Please sign in to comment.