Skip to content

Commit b3f2b56

Browse files
authored
feat: add new context command to make it easier to work with multiple files (#14)
1 parent 0391300 commit b3f2b56

19 files changed

+728
-25
lines changed

README.md

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,9 +23,19 @@ Usage
2323
Commands
2424
validate
2525
Options
26-
-c --context Your AsyncAPI specification
26+
-c --context context-name saved in the store
2727
-w --watch Enable watchMode (not implemented yet)
28+
29+
context
30+
current show the current set context
31+
list show the list of all stored contexts
32+
remove <context-name> remove a context from the store
33+
use <context-name> set any context from store as current
34+
add <context-name> <filepath> add/update new context
2835
2936
Examples
30-
$ asyncapi validate --context=specification.yml
37+
$ asyncapi context add dummy ./asyncapi.yml
38+
$ asyncapi validate --context=dummy
3139
```
40+
41+
> For now --context flag is requried to run validate command

src/CliModels.ts

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11

22
export type Command = string;
33
export type HelpMessage = string;
4+
export type Arguments = string[];
45

56
export interface Options {
67
context: string,
@@ -10,12 +11,12 @@ export interface Options {
1011
export class CliInput {
1112
private readonly _command: Command
1213
private readonly _options: Options
13-
private readonly _helpMessage: HelpMessage
14+
private readonly _arguments: Arguments
1415

15-
private constructor(command: Command, options: Options, helpMessage: HelpMessage) {
16+
private constructor(command: Command, options: Options, args: Arguments) {
1617
this._command = command;
1718
this._options = options;
18-
this._helpMessage = helpMessage;
19+
this._arguments = args;
1920
}
2021

2122
get command(): Command {
@@ -26,14 +27,20 @@ export class CliInput {
2627
return this._options;
2728
}
2829

29-
get helpMessage(): HelpMessage {
30-
return this._helpMessage;
30+
31+
get arguments(): Arguments {
32+
return this._arguments;
3133
}
3234

3335
static createFromMeow(meowOutput: any): CliInput {
34-
const [command] = meowOutput.input;
35-
const help = meowOutput.help;
36+
const [command, ...args] = meowOutput.input;
3637
const { context, watch } = meowOutput.flags;
37-
return new CliInput(command || 'help', { context, watch }, help);
38+
return new CliInput(command || 'help', { context, watch }, args);
39+
}
40+
41+
static createSubCommand(cliInput: CliInput): CliInput {
42+
const [command, ...args] = cliInput.arguments;
43+
return new CliInput(command || 'help', cliInput.options, args);
3844
}
3945
}
46+

src/CommandsRouter.spec.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ describe('CommandsRouter should', () => {
2323

2424
setTimeout(() => {
2525
expect(lastFrame()).toBe(chalk.green(`File: ${file.getSpecificationName()} successfully validated!`));
26-
done();
2726
}, 200);
27+
done();
2828
});
2929
});

src/CommandsRouter.tsx

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,16 @@
11
import React from "react";
22
import Validate from "./components/Validate/Validate";
3+
import { contextRouter } from './components/Context';
34
import { CliInput } from "./CliModels";
45

5-
66
const commandsDictionary = (cliInput: CliInput) => ({
7-
['validate']: <Validate options={cliInput.options}/>,
7+
['validate']: <Validate options={cliInput.options} />,
8+
['context']: contextRouter(cliInput)
89
});
910

1011
export const commandsRouter = (cli: any) => {
1112
const cliInput = CliInput.createFromMeow(cli);
12-
// @ts-ignore
13+
14+
//@ts-ignore
1315
return commandsDictionary(cliInput)[cliInput.command];
1416
};

src/cli.ts

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,17 +12,24 @@ const cli = meow(`
1212
Commands
1313
validate
1414
Options
15-
-c --context Your AsyncAPI specification
15+
-c --context context-name saved in the store
1616
-w --watch Enable watchMode (not implemented yet)
17+
context
18+
current show the current set context
19+
list show the list of all stored context
20+
remove <context-name> remove a context from the store
21+
use <context-name> set any context as current
22+
add <context-name> <filepath> add/update new context
1723
1824
Examples
19-
$ asyncapi validate --context=specification.yml
25+
$ asyncapi context add dummy ./asyncapi.yml
26+
$ asyncapi validate --context=dummy
2027
`, {
2128
flags: {
2229
context: {
2330
alias: 'c',
2431
type: 'string',
25-
isRequired: true
32+
isRequired: false
2633
},
2734
watch: {
2835
alias: 'w',

src/components/Context/Context.tsx

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
import { useContextFile } from '../../hooks/context';
2+
import React, { FunctionComponent } from 'react';
3+
import { Box, Text } from 'ink';
4+
import ContextError from './contexterror';
5+
import { SpecificationFile } from '../../hooks/validation';
6+
7+
export const ListContexts: FunctionComponent = () => {
8+
const { response, error } = useContextFile().list();
9+
10+
if (error) {
11+
return <ContextError error={error} />
12+
}
13+
14+
if (response) {
15+
return <Box flexDirection="column">
16+
{response.map((context: any) => <Text key={context.key}>{context.key} : {context.path}</Text>)}
17+
</Box>
18+
}
19+
20+
return <></>
21+
}
22+
23+
export const ShowCurrentContext: FunctionComponent = () => {
24+
const { response, error } = useContextFile().current();
25+
26+
if (error) {
27+
return <ContextError error={error} />
28+
}
29+
30+
if (response) {
31+
return <Text>{response.key} : {response.path}</Text>
32+
}
33+
34+
return <></>
35+
}
36+
37+
export const AddContext: FunctionComponent<{ options: any, args: string[] }> = ({ args }) => {
38+
const [key, path] = args
39+
40+
if (!key || !path) {
41+
return <ContextError error={new Error("missing arguments")} />
42+
}
43+
44+
const { response, error } = useContextFile().addContext(key, new SpecificationFile(path));
45+
46+
if (error) {
47+
return <ContextError error={error} />
48+
}
49+
50+
return <Text>{response}</Text>
51+
}
52+
53+
export const SetCurrent: FunctionComponent<{ options: any, args: string[] }> = ({ args }) => {
54+
const [key,] = args;
55+
56+
if (!key) {
57+
return <ContextError error={new Error("missing arguments")} />
58+
}
59+
60+
const { response, error } = useContextFile().setCurrent(key);
61+
62+
if (error) {
63+
return <ContextError error={error} />
64+
}
65+
66+
if (response) {
67+
return <Text>{response.key} : {response.path}</Text>
68+
}
69+
70+
return <></>
71+
}
72+
73+
export const RemoveContext: FunctionComponent<{ options: any, args: string[] }> = ({ args }) => {
74+
const [key] = args;
75+
76+
if (!key) {
77+
return <ContextError error={new Error("missing arguments")} />
78+
}
79+
80+
const { response, error } = useContextFile().deleteContext(key);
81+
82+
if (error) {
83+
return <ContextError error={error} />
84+
}
85+
86+
return <Text>{response}</Text>
87+
}
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
import React from 'react';
2+
import { render } from 'ink-testing-library';
3+
import { ListContexts, ShowCurrentContext, AddContext, SetCurrent } from './Context';
4+
import { ContextTestingHelper } from '../../constants';
5+
6+
let testing = new ContextTestingHelper();
7+
8+
describe('listing contexts', () => {
9+
test("should render error when no context file found", () => {
10+
testing.deleteDummyContextFile();
11+
let { lastFrame } = render(<ListContexts />);
12+
expect(lastFrame()).toMatch("No contexts saved yet.");
13+
})
14+
15+
test("Should render the context list", () => {
16+
testing.createDummyContextFile()
17+
let { lastFrame } = render(<ListContexts />);
18+
expect(lastFrame()).toMatch(
19+
`home : ${testing.context.store["home"]}\n` +
20+
`code : ${testing.context.store["code"]}`
21+
);
22+
})
23+
})
24+
25+
describe('rendering current context', () => {
26+
test('showing error if now current context is found', () => {
27+
testing.deleteDummyContextFile();
28+
let { lastFrame } = render(<ShowCurrentContext />);
29+
expect(lastFrame()).toMatch('No contexts saved yet.');
30+
})
31+
32+
test('showing current context ', () => {
33+
testing.createDummyContextFile();
34+
let { lastFrame } = render(<ShowCurrentContext />);
35+
expect(lastFrame()).toMatch(`home : ${testing.context.store["home"]}`);
36+
})
37+
})
38+
39+
describe('AddContext ', () => {
40+
test("should return message", () => {
41+
testing.createDummyContextFile();
42+
let { lastFrame } = render(<AddContext options={{}} args={['home', './test/specification.yml']} />);
43+
expect(lastFrame()).toMatch('New context added');
44+
})
45+
})
46+
47+
describe('SetContext ', () => {
48+
49+
test('Should render error message is key is not in store', () => {
50+
testing.createDummyContextFile();
51+
let { lastFrame } = render(<SetCurrent args={['name']} options={{}} />)
52+
expect(lastFrame()).toMatch('The context you are trying to use is not present');
53+
});
54+
55+
test('Should render the update context', () => {
56+
testing.createDummyContextFile();
57+
let { lastFrame } = render(<SetCurrent args={['code']} options={{}} />)
58+
expect(lastFrame()).toMatch(`code : ${testing.context.store['code']}`);
59+
});
60+
})
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import React, { FunctionComponent } from 'react';
2+
import { Text } from 'ink';
3+
import { ContextFileNotFoundError, DeletingCurrentContextError, KeyNotFoundError } from '../../hooks/context';
4+
5+
const ContextError: FunctionComponent<{ error: Error }> = ({ error }) => {
6+
7+
if (error instanceof ContextFileNotFoundError) {
8+
return <Text>No contexts saved yet.</Text>
9+
}
10+
11+
if(error instanceof KeyNotFoundError){
12+
return <Text>The context you are trying to use is not present</Text>
13+
}
14+
15+
if(error instanceof DeletingCurrentContextError) {
16+
return <Text>You are trying to delete a context that is set as current.</Text>
17+
}
18+
19+
return <Text>{error.message}</Text>
20+
};
21+
22+
export default ContextError;

src/components/Context/index.tsx

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import React from 'react';
2+
import { CliInput } from '../../CliModels';
3+
import { ListContexts,AddContext,RemoveContext,ShowCurrentContext,SetCurrent } from './Context';
4+
5+
//@ts-ignore
6+
const commandDictionary = (cliInput: CliInput) => ({
7+
['list']: <ListContexts />,
8+
['current']: <ShowCurrentContext />,
9+
['use']: <SetCurrent options={cliInput.options} args={cliInput.arguments} />,
10+
['add']: <AddContext options={cliInput.options} args={cliInput.arguments} />,
11+
['remove']: <RemoveContext options={cliInput.options} args={cliInput.arguments} />
12+
})
13+
14+
export const contextRouter = (cliInput: CliInput) => {
15+
let subCommand = CliInput.createSubCommand(cliInput);
16+
//@ts-ignore
17+
return commandDictionary(subCommand)[subCommand.command];
18+
}

src/components/Validate/Validate.spec.tsx

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ import { Options } from "../../CliModels";
55
import { UseValidateResponse } from "../../hooks/validation/models";
66
import chalk from "chalk";
77
import * as validationHook from '../../hooks/validation';
8+
import { ContextTestingHelper } from '../../constants';
9+
810

911

1012
function makeUseValidateReturn(response: UseValidateResponse) {
@@ -14,13 +16,16 @@ function makeUseValidateReturn(response: UseValidateResponse) {
1416
}
1517

1618
function renderValidationComponentWith(options: Options) {
17-
return render(<Validate options={options}/>);
19+
return render(<Validate options={options} />);
1820
}
1921

22+
const testing = new ContextTestingHelper();
23+
2024
describe('Validate component should', () => {
2125
test('render the success message in green color when the specification is correct', (done) => {
26+
testing.createDummyContextFile();
2227
const options: Options = {
23-
context: 'oneFile.yml',
28+
context: 'home',
2429
watch: false,
2530
};
2631

@@ -35,8 +40,9 @@ describe('Validate component should', () => {
3540
});
3641

3742
test('render the single error message in red color when the file is not valid', (done) => {
43+
testing.createDummyContextFile();
3844
const options: Options = {
39-
context: 'oneFile.yml',
45+
context: 'home',
4046
watch: false,
4147
};
4248

@@ -51,8 +57,9 @@ describe('Validate component should', () => {
5157
});
5258

5359
test('render the different error messages in red color when the validation fails', (done) => {
60+
testing.createDummyContextFile();
5461
const options: Options = {
55-
context: 'oneFile.yml',
62+
context: 'home',
5663
watch: false,
5764
};
5865

0 commit comments

Comments
 (0)