Skip to content

Commit

Permalink
Merge pull request #232 from cmars/feat/auto-discover-vervet-resource…
Browse files Browse the repository at this point in the history
…s-dir

feat: automatically discover default vervet resources directory
  • Loading branch information
cmars authored Apr 7, 2022
2 parents c7d5b93 + 5264d5e commit 5d744c8
Show file tree
Hide file tree
Showing 7 changed files with 714 additions and 25 deletions.
4 changes: 4 additions & 0 deletions end-end-tests/workflows/.vervet.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,9 @@
"@useoptic/openapi-io",
"@useoptic/openapi-utilities"
],
"resolutions": {
"urijs": "^1.19.11"
},
"jest": {
"testPathIgnorePatterns": [
"build",
Expand Down
23 changes: 14 additions & 9 deletions src/workflows/file-resolvers.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,23 @@
import findParentDir from "find-parent-dir";
import fs from "fs-extra";
import path from "path";
import { OpenAPIV3 } from "@useoptic/openapi-utilities";
import { loadVervetResoucePaths } from "./vervet-resolver";

export async function resolveResourcesDirectory(
workingDirectory: string = getSweaterCombWorkingDirectory(),
): Promise<string | undefined> {
return new Promise((resolve) => {
findParentDir(workingDirectory, "resources", function (err, dir) {
if (err || !dir) {
resolve(undefined);
} else resolve(path.join(dir, "resources"));
});
});
const vervetPaths = await loadVervetResoucePaths(workingDirectory);
if (!vervetPaths) {
return undefined;
}
const resolvedWorkingDirectory = path.resolve(workingDirectory);
for (const rcPath of vervetPaths.resourcesPaths.values()) {
const absRcPath = path.resolve(path.join(vervetPaths.projectRoot, rcPath));
if (resolvedWorkingDirectory.startsWith(absRcPath)) {
return absRcPath;
}
}
return path.join(vervetPaths.projectRoot, vervetPaths.defaultResourcesPath);
}

export type ResourceVersionLookupResults =
Expand Down Expand Up @@ -53,7 +58,7 @@ export async function resolveResourceVersion(
const resourceNameLowerCase = resourceName.toLowerCase();

const resourceNames = (await fs.readdir(resources)).filter((maybeResource) =>
fs.lstatSync(path.join(resources, maybeResource)).isDirectory(),
fs.lstatSync(path.join(resources!, maybeResource)).isDirectory(),
);

if (!resourceNames.includes(resourceNameLowerCase)) {
Expand Down
15 changes: 14 additions & 1 deletion src/workflows/tests/file-resolver.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,11 @@ import {
const testResourcesExamples = path.resolve(
path.join(__dirname, "../../../end-end-tests/workflows/resources"),
);
const nonVervetProjectPath = path.resolve(
path.join(__dirname, "../../../end-end-tests/simple-scenario"),
);

describe("vervet resolver", () => {
describe("file resolver", () => {
it("can resolve when there is a resources dir", async () => {
const resources = await resolveResourcesDirectory(testResourcesExamples);
expect(
Expand Down Expand Up @@ -43,6 +46,16 @@ describe("vervet resolver", () => {
const result = await tryLookup("issues", "2020-01-01");
expect("failed" in result).toBeTruthy();
});

it("will not resolve outside of a vervet project", async () => {
const resources = await resolveResourcesDirectory(nonVervetProjectPath);
expect(resources).toBeFalsy();
});

it("will not resolve a non-existent path", async () => {
const resources = await resolveResourcesDirectory("/no/such/place");
expect(resources).toBeFalsy();
});
});

async function tryLookup(resourceName: string, version: string) {
Expand Down
106 changes: 106 additions & 0 deletions src/workflows/tests/vervet-resolver.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
import * as fs from "fs";
import * as path from "path";
import * as os from "os";
import { loadVervetResoucePaths } from "../vervet-resolver";

describe("vervet resolver", () => {
const originalwd = process.cwd();
let tmpDir = "";

beforeEach(() => {
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "foo-"));
fs.writeFileSync(
path.join(tmpDir, ".vervet.yaml"),
`
apis:
testapi:
resources:
- path: 'src/testapi/something/something/resources'
- path: 'src/testapi/something-else/something-else/resources'
testapi2:
resources:
- path: 'src/testapi2/resources'
`,
);
});

afterEach(() => {
process.chdir(originalwd);
if (tmpDir !== "") {
fs.rmSync(tmpDir, { force: true, recursive: true });
}
});

it("chooses the first resource path in the first API", async () => {
// arrange
process.chdir(tmpDir);
// act
const vervetPaths = await loadVervetResoucePaths();
// assert
expect(vervetPaths?.defaultResourcesPath).toMatch(
new RegExp(".*src/testapi/something/something/resources$"),
);
});

it("works from a subdirectory in the same project", async () => {
// arrange
const otherDir = path.join(tmpDir, "other-place");
fs.mkdirSync(otherDir);
process.chdir(otherDir);
// act
const vervetPaths = await loadVervetResoucePaths();
// assert
expect(vervetPaths?.defaultResourcesPath).toMatch(
new RegExp(".*src/testapi/something/something/resources$"),
);
});

it("requires a vervet config", async () => {
// arrange
process.chdir(tmpDir);
fs.unlinkSync(".vervet.yaml");
// act
const vervetPaths = await loadVervetResoucePaths();
// assert
expect(vervetPaths).toBeFalsy();
});

it("requires an api to be defined", async () => {
// arrange
process.chdir(tmpDir);
fs.writeFileSync(".vervet.yaml", "apis: {}");
// act
const vervetPaths = await loadVervetResoucePaths();
// assert
expect(vervetPaths).toBeFalsy();
});

it("requires a valid vervet config", async () => {
// arrange
process.chdir(tmpDir);
fs.writeFileSync(".vervet.yaml", "}}}bad wolf{{{");
// act
const vervetPaths = await loadVervetResoucePaths();
// assert
expect(vervetPaths).toBeFalsy();
});

it("requires target directory to exist", async () => {
// arrange
process.chdir(tmpDir);
// act
const vervetPaths = await loadVervetResoucePaths("/does-not-exist");
// assert
expect(vervetPaths).toBeFalsy();
});

it("requires apis to declare resources", async () => {
// arrange
process.chdir(tmpDir);
fs.writeFileSync(".vervet.yaml", "apis: {foo: {}}");
// act
const vervetPaths = await loadVervetResoucePaths("/does-not-exist");
// assert
expect(vervetPaths).toBeFalsy();
});
});
71 changes: 71 additions & 0 deletions src/workflows/vervet-resolver.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import findParentDir from "find-parent-dir";
import * as path from "path";
import * as fs from "fs/promises";
import * as util from "util";
import { loadYaml } from "@useoptic/openapi-io";

const findParentDirAsync = util.promisify(findParentDir);

/**
* VervetResourcePaths models the resource paths defined in a Vervet project
* configuration file.
*
* For more information, see https://pkg.go.dev/github.com/snyk/vervet/v4/config#Project
*/
export class VervetResourcePaths {
public readonly defaultResourcesPath: string;
public readonly resourcesPaths: Set<string>;
public readonly projectRoot: string;

/**
* Create a new instance given the projectRoot directory and Vervet
* configuration document object.
*/
constructor(projectRoot: string, doc: any) {
this.projectRoot = projectRoot;
const apis = doc?.apis;
if (!apis) {
throw new InvalidVervetConfig();
}
const resourcesPaths: string[] = [];
for (const apiName in apis) {
const resources: Array<{ path: string }> = apis[apiName]?.resources ?? [];
resources
.filter((rc) => rc && rc.path && rc.path !== "")
.forEach((rc) => {
resourcesPaths.push(rc.path);
});
}
if (resourcesPaths.length == 0) {
throw new InvalidVervetConfig();
}
this.defaultResourcesPath = resourcesPaths[0];
this.resourcesPaths = new Set(resourcesPaths);
}
}

class InvalidVervetConfig extends Error {}

export const loadVervetResoucePaths = async (
fromDir = ".",
): Promise<VervetResourcePaths | null> => {
const vervetConfDir = await findParentDirAsync(
path.resolve(fromDir),
".vervet.yaml",
);
if (!vervetConfDir) {
return null;
}
const vervetConfPath = path.join(vervetConfDir, ".vervet.yaml");
if (!vervetConfPath) {
return null;
}
try {
const vervetConfYaml = await fs.readFile(vervetConfPath);
const vervetDoc = loadYaml(vervetConfYaml.toString());
const vervetConfig = new VervetResourcePaths(vervetConfDir, vervetDoc);
return vervetConfig;
} catch (_err) {
return null;
}
};
Loading

0 comments on commit 5d744c8

Please sign in to comment.