Skip to content

Commit

Permalink
Merge pull request #59 from emyann/feat/add-json-schema
Browse files Browse the repository at this point in the history
feat: add json schema
  • Loading branch information
emyann authored Mar 8, 2020
2 parents a62262a + ad6b1cf commit ae6dac2
Show file tree
Hide file tree
Showing 7 changed files with 202 additions and 118 deletions.
7 changes: 5 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,13 @@
],
"scripts": {
"start": "tsnd --respawn ./src/cli/index.ts",
"build": "run-p build:js build:types",
"build": "yarn generate-json-schema && run-p build:js build:types",
"build:js": "TS_NODE_PROJECT=\"tsconfig.webpack.json\" webpack --mode=production",
"build:types": "tsc -p tsconfig.prod.json --emitDeclarationOnly",
"local": "yarn build && npm link && rm -rf package-lock.json",
"test": "jest",
"semantic-release": "semantic-release"
"semantic-release": "semantic-release",
"generate-json-schema": "ts-json-schema-generator -p ./src/core/parser/parser.types.ts -t MatronDocument -o ./src/core/parser/schema.json"
},
"release": {
"branches": [
Expand Down Expand Up @@ -58,6 +59,7 @@
"npm-run-all": "^4.1.5",
"semantic-release": "^17.0.4",
"source-map-loader": "^0.2.4",
"ts-json-schema-generator": "^0.65.0",
"ts-node-dev": "^1.0.0-pre.44",
"typescript": "^3.7.5",
"webpack": "^4.41.6",
Expand All @@ -66,6 +68,7 @@
},
"dependencies": {
"@reduxjs/toolkit": "^1.2.4",
"ajv": "^6.12.0",
"chalk": "^3.0.0",
"cross-spawn": "^7.0.1",
"deepmerge": "^4.2.2",
Expand Down
16 changes: 15 additions & 1 deletion src/core/parser/parser.spec.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { parser, evaluate, Command, toSpawnCommand } from './parser';
import { loadMatronDocument, parser, lint, evaluate, toSpawnCommand } from './parser';
import { Command } from './parser.types';

describe('parser', () => {
it('should parse a YAML file to a command', () => {
Expand All @@ -20,6 +21,19 @@ describe('parser', () => {
expect(jobs.length).toEqual(1);
expect(jobs[0]).toEqual({ key, name, steps: [{ cmd, args }] });
});

it('should throw when yaml linter fails', () => {
const yaml = `
jobs:
a-job:
steps:
- UNKNOWN_COMMAND: 12
`;

const doc = loadMatronDocument(yaml);
const fn = () => lint(doc);
expect(fn).toThrow();
});
});

describe('evaluate', () => {
Expand Down
83 changes: 48 additions & 35 deletions src/core/parser/parser.ts
Original file line number Diff line number Diff line change
@@ -1,56 +1,69 @@
import yaml from 'js-yaml';
import { readFileSync } from 'fs';
import { morphism, createSchema } from 'morphism';

export interface Command {
cmd: keyof typeof CommandType;
args: string[];
}

export enum CommandType {
COPY = 'COPY',
RUN = 'RUN',
WORKDIR = 'WORKDIR',
MERGE_JSON = 'MERGE_JSON'
}
import Ajv, { ValidationError } from 'ajv';
import fse from 'fs-extra';
import path from 'path';
import jsonSchema from './schema.json';
import { Command, CommandType, Job, MatronDocumentJobStep, MatronDocument } from './parser.types.js';

export function loadFile(path: string) {
return Promise.resolve(readFileSync(path, 'utf8'));
}

export interface Job {
key: string;
name?: string;
steps: Command[];
}

type YamlDocStep = { [key: string]: string };
interface YamlDoc {
jobs: { [key: string]: { name?: string; steps: YamlDocStep[] } };
}

const toCommand = morphism(
createSchema<Command, YamlDocStep>({
createSchema<Command, MatronDocumentJobStep>({
cmd: step => {
return Object.keys(step)[0] as CommandType;
},
args: step => Object.values(step)[0].split(' ')
args: step => {
const argsStr = Object.values(step)[0];
if (argsStr) {
const args = argsStr.split(' ');
return args;
} else {
return [];
}
}
})
);
export function parser(content: string): Job[] {

export function loadMatronDocument(content: string): MatronDocument {
try {
const doc: YamlDoc = yaml.safeLoad(content);
return Object.entries(doc.jobs).map(([key, job]) => {
return {
key,
name: job.name,
steps: toCommand(job.steps)
};
});
return yaml.safeLoad(content);
} catch (error) {
throw error;
}
}
export function parser(content: string): Job[] {
const doc = loadMatronDocument(content);
lint(doc);
return Object.entries(doc.jobs).map(([key, job]) => {
return {
key,
name: job.name,
steps: toCommand(job.steps)
};
});
}

export function lint(doc: MatronDocument) {
const ajv = new Ajv();
const validate = ajv.compile(jsonSchema);
const isValid = validate(doc);
if (!isValid) {
throw new LinterError(validate.errors!);
}
}

class LinterError extends Error {
constructor(errors: Ajv.ErrorObject[]) {
const message = `
Unable to validate the Matron file.
Errors: ${JSON.stringify(errors, null, 2)}
`;
super(message);
}
}

function getCommandParameters(commandString: string) {
const regex = /\${(?<param>\w+)}/gm;
Expand Down
27 changes: 27 additions & 0 deletions src/core/parser/parser.types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
export interface Job {
key: string;
name?: string;
steps: Command[];
}

export interface Command {
cmd: keyof typeof CommandType;
args: string[];
}

export enum CommandType {
COPY = 'COPY',
RUN = 'RUN',
WORKDIR = 'WORKDIR',
MERGE_JSON = 'MERGE_JSON'
}

export type MatronDocumentJobStep = { [key in CommandType]?: string };
export interface MatronDocumentJob {
name?: string;
steps: MatronDocumentJobStep[];
}
export type MatronDocumentJobs = { [key: string]: MatronDocumentJob };
export interface MatronDocument {
jobs: MatronDocumentJobs;
}
60 changes: 60 additions & 0 deletions src/core/parser/schema.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
{
"$ref": "#/definitions/MatronDocument",
"$schema": "http://json-schema.org/draft-07/schema#",
"definitions": {
"MatronDocument": {
"additionalProperties": false,
"properties": {
"jobs": {
"$ref": "#/definitions/MatronDocumentJobs"
}
},
"required": [
"jobs"
],
"type": "object"
},
"MatronDocumentJob": {
"additionalProperties": false,
"properties": {
"name": {
"type": "string"
},
"steps": {
"items": {
"$ref": "#/definitions/MatronDocumentJobStep"
},
"type": "array"
}
},
"required": [
"steps"
],
"type": "object"
},
"MatronDocumentJobStep": {
"additionalProperties": false,
"properties": {
"COPY": {
"type": "string"
},
"MERGE_JSON": {
"type": "string"
},
"RUN": {
"type": "string"
},
"WORKDIR": {
"type": "string"
}
},
"type": "object"
},
"MatronDocumentJobs": {
"additionalProperties": {
"$ref": "#/definitions/MatronDocumentJob"
},
"type": "object"
}
}
}
3 changes: 2 additions & 1 deletion src/core/stateMachine.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import { interpret, createMachine, assign, actions } from 'xstate';
import { createAction } from '@reduxjs/toolkit';
import { Command, CommandType, parser, loadFile, evaluate, toSpawnCommand } from './parser/parser';
import { parser, loadFile, evaluate, toSpawnCommand } from './parser/parser';
import spawn from 'cross-spawn';
import uuidv4 from 'uuid/v4';
import merge from 'deepmerge';
import path from 'path';
import fs from 'fs';
import fse from 'fs-extra';
import { Command, CommandType } from './parser/parser.types';

const { log } = actions;

Expand Down
Loading

0 comments on commit ae6dac2

Please sign in to comment.