Skip to content

Commit

Permalink
[TT-278] Generate spot contract hash (airtasker#281)
Browse files Browse the repository at this point in the history
* [TT-278]
- Implement function to generate hashes based on contracts
- Add command line script to interact with it
  • Loading branch information
medric authored Jun 14, 2019
1 parent 06247e4 commit 7f6bb0d
Show file tree
Hide file tree
Showing 4 changed files with 186 additions and 0 deletions.
20 changes: 20 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -263,5 +263,25 @@ EXAMPLE
$ spot validate api.ts
```

_See code: [build/cli/src/commands/validate.js](https://github.com/airtasker/spot/blob/v0.2.13/build/cli/src/commands/validate.js)_

## `spot tag SPOT_CONTRACT`

Generate a version tag based on a Spot contract

```
USAGE
$ spot tag SPOT_CONTRACT
ARGUMENTS
SPOT_CONTRACT path to Spot contract
OPTIONS
-h, --help show CLI help
EXAMPLE
$ spot tag api.ts
```

_See code: [build/cli/src/commands/validate.js](https://github.com/airtasker/spot/blob/v0.2.13/build/cli/src/commands/validate.js)_
<!-- commandsstop -->
38 changes: 38 additions & 0 deletions cli/src/commands/checksum.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { Command, flags } from "@oclif/command";
import { hashContractDefinition } from "../../../lib/src/tagging/hash";
import { safeParse } from "../common/safe-parse";

const ARG_API = "spot_contract";

/**
* oclif command to generate a tag based on a Spot contract
*/
export default class Checksum extends Command {
static description = "Generate a version tag based on a Spot contract";

static examples = ["$ spot mock api.ts"];

static args = [
{
name: ARG_API,
required: true,
description: "path to Spot contract",
hidden: false
}
];

static flags = {
help: flags.help({ char: "h" })
};

async run() {
const { args } = this.parse(Checksum);
try {
const contract = safeParse.call(this, args[ARG_API]).definition;
const hash = hashContractDefinition(contract);
this.log(hash);
} catch (e) {
this.error(e, { exit: 1 });
}
}
}
110 changes: 110 additions & 0 deletions lib/src/tagging/hash.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
import { cleanse } from "../cleansers/cleanser";
import { EndpointDefinition } from "../models/definitions";
import { TypeKind } from "../models/types/kinds"; //
import { parse } from "../parsers/parser";
import { hashContractDefinition } from "./hash";

describe("Hash", () => {
describe("hashContractDefinition", () => {
it("returns a consistent hash", () => {
const result = parse("./lib/src/test/examples/contract.ts");
const contractDefinition = cleanse(result);
const hash0 = hashContractDefinition(contractDefinition);
const hash1 = hashContractDefinition(contractDefinition);
expect(hash0).toEqual(hash1);
});

it("returns a new hash when a new endpoint is added", () => {
const result = parse("./lib/src/test/examples/contract.ts");
const contractDefinition = cleanse(result);

const hash0 = hashContractDefinition(contractDefinition);

const endpointDefinition: EndpointDefinition = {
name: "testEndpoint",
description: "test endpoint",
tags: ["test"],
method: "GET",
path: "/test",
request: {
headers: [],
pathParams: [],
queryParams: [],
body: {
description: "test response body",
type: {
kind: TypeKind.STRING
}
}
},
responses: [],
tests: []
};

contractDefinition.endpoints.push(endpointDefinition);

const hash1 = hashContractDefinition(contractDefinition);

expect(hash0).not.toEqual(hash1);
});

it("returns a new hash when an endpoint request body gets udpated", () => {
const result = parse("./lib/src/test/examples/contract.ts");
const contractDefinition = cleanse(result);

const hash0 = hashContractDefinition(contractDefinition);

contractDefinition.endpoints[0].request.body = {
type: {
kind: TypeKind.STRING
}
};

const hash1 = hashContractDefinition(contractDefinition);

expect(hash0).not.toEqual(hash1);
});

it("returns a new hash when an endpoint response body gets udpated", () => {
const result = parse("./lib/src/test/examples/contract.ts");
const contractDefinition = cleanse(result);

const hash0 = hashContractDefinition(contractDefinition);

contractDefinition.endpoints[0].responses[0].description =
"test response";

const hash1 = hashContractDefinition(contractDefinition);

expect(hash0).not.toEqual(hash1);
});

it("returns a new hash when an endpoint request header gets udpated", () => {
const result = parse("./lib/src/test/examples/contract.ts");
const contractDefinition = cleanse(result);

const hash0 = hashContractDefinition(contractDefinition);

contractDefinition.endpoints[0].request.headers[0].description =
"test request header";

const hash1 = hashContractDefinition(contractDefinition);

expect(hash0).not.toEqual(hash1);
});

it("returns a new hash when an endpoint request parameter gets udpated", () => {
const result = parse("./lib/src/test/examples/contract.ts");
const contractDefinition = cleanse(result);

const hash0 = hashContractDefinition(contractDefinition);

contractDefinition.endpoints[0].request.pathParams[0].description =
"test path parameter";

const hash1 = hashContractDefinition(contractDefinition);

expect(hash0).not.toEqual(hash1);
});
});
});
18 changes: 18 additions & 0 deletions lib/src/tagging/hash.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { createHash } from "crypto";
import { ContractDefinition } from "../models/definitions";

export function hashContractDefinition(
contractDefinition: ContractDefinition
): string {
// Remove unnecessary whitespace and aim to make the output JSON-formatting independent.
const contractDefinitionString = JSON.stringify(contractDefinition).replace(
/\s/g,
""
);

const hash = createHash("sha1")
.update(contractDefinitionString)
.digest("hex");

return hash;
}

0 comments on commit 7f6bb0d

Please sign in to comment.