Skip to content

Commit ab39fb6

Browse files
authored
Merge branch 'main' into docify-bugfix
2 parents a4bf967 + fd6a386 commit ab39fb6

20 files changed

Lines changed: 1020 additions & 109 deletions

calm/draft/2025-03/prototype/anyof/options-prototype.pattern.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -159,7 +159,7 @@
159159
{
160160
"anyOf": [
161161
{
162-
"$ref": "https://calm.finos.org/draft/2025-03/meta/core.json#/defs/option-type",
162+
"$ref": "https://calm.finos.org/draft/2025-03/meta/core.json#/defs/decision",
163163
"type": "object",
164164
"properties": {
165165
"description": {
@@ -178,7 +178,7 @@
178178
}
179179
},
180180
{
181-
"$ref": "https://calm.finos.org/draft/2025-03/meta/core.json#/defs/option-type",
181+
"$ref": "https://calm.finos.org/draft/2025-03/meta/core.json#/defs/decision",
182182
"type": "object",
183183
"properties": {
184184
"description": {

calm/draft/2025-03/prototype/oneof/options-prototype.pattern.json

Lines changed: 50 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -170,60 +170,60 @@
170170
"type": "object",
171171
"properties": {
172172
"options": {
173-
"type": "array",
174-
"minItems": 1,
175-
"maxItems": 1,
176-
"prefixItems": [
177-
{
178-
"oneOf": [
179-
{
180-
"$ref": "https://calm.finos.org/draft/2025-03/meta/core.json#/defs/option-type",
181-
"type": "object",
182-
"properties": {
183-
"description": {
184-
"const": "Application A connects to Application C"
185-
},
186-
"nodes": {
187-
"const": [
188-
"application-a"
189-
]
190-
},
191-
"relationships": {
192-
"const": [
193-
"application-a-to-c"
194-
]
173+
"type": "array",
174+
"minItems": 1,
175+
"maxItems": 1,
176+
"prefixItems": [
177+
{
178+
"oneOf": [
179+
{
180+
"$ref": "https://calm.finos.org/draft/2025-03/meta/core.json#/defs/decision",
181+
"type": "object",
182+
"properties": {
183+
"description": {
184+
"const": "Application A connects to Application C"
185+
},
186+
"nodes": {
187+
"const": [
188+
"application-a"
189+
]
190+
},
191+
"relationships": {
192+
"const": [
193+
"application-a-to-c"
194+
]
195+
}
195196
}
196-
}
197-
},
198-
{
199-
"$ref": "https://calm.finos.org/draft/2025-03/meta/core.json#/defs/option-type",
200-
"type": "object",
201-
"properties": {
202-
"description": {
203-
"const": "Application B connects to Application C"
204-
},
205-
"nodes": {
206-
"const": [
207-
"application-b"
208-
]
209-
},
210-
"relationships": {
211-
"const": [
212-
"application-b-to-c"
213-
]
197+
},
198+
{
199+
"$ref": "https://calm.finos.org/draft/2025-03/meta/core.json#/defs/decision",
200+
"type": "object",
201+
"properties": {
202+
"description": {
203+
"const": "Application B connects to Application C"
204+
},
205+
"nodes": {
206+
"const": [
207+
"application-b"
208+
]
209+
},
210+
"relationships": {
211+
"const": [
212+
"application-b-to-c"
213+
]
214+
}
214215
}
215216
}
216-
}
217-
]
218-
}
219-
]
220-
}
217+
]
218+
}
219+
]
220+
}
221221
}
222-
},
223-
"required": [
224-
"options"
225-
]
226-
}
222+
}
223+
},
224+
"required": [
225+
"options"
226+
]
227227
}
228228
]
229229
}

cli/package.json

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -29,37 +29,36 @@
2929
"author": "",
3030
"license": "ISC",
3131
"dependencies": {
32+
"@apidevtools/json-schema-ref-parser": "^11.9.3",
33+
"@inquirer/prompts": "^7.4.0",
3234
"commander": "^13.0.0",
3335
"copyfiles": "^2.4.1",
34-
"mkdirp": "^3.0.1",
3536
"express-rate-limit": "^7.5.0",
36-
"@apidevtools/json-schema-ref-parser": "^11.9.3",
37+
"mkdirp": "^3.0.1",
3738
"ts-node": "10.9.2"
3839
},
3940
"devDependencies": {
40-
"@types/supertest": "^6.0.2",
41-
"jest-environment-node": "^29.7.0",
42-
"axios": "^1.7.9",
4341
"@jest/globals": "^29.7.0",
4442
"@types/jest": "^29.5.14",
4543
"@types/json-pointer": "^1.0.34",
4644
"@types/junit-report-builder": "^3.0.2",
4745
"@types/lodash": "^4.17.0",
4846
"@types/node": "^22.10.0",
47+
"@types/supertest": "^6.0.2",
4948
"@types/xml2js": "^0.4.14",
5049
"@typescript-eslint/eslint-plugin": "^8.0.0",
5150
"@typescript-eslint/parser": "^8.15.0",
51+
"axios": "^1.7.9",
5252
"chokidar": "^4.0.1",
5353
"eslint": "^9.13.0",
5454
"globals": "^16.0.0",
5555
"jest": "^29.7.0",
56-
"supertest": "^7.0.0",
5756
"link": "^2.1.1",
57+
"supertest": "^7.0.0",
5858
"ts-jest": "^29.2.5",
5959
"tsup": "^8.0.0",
6060
"typescript": "^5.4.3",
6161
"xml2js": "^0.6.2"
62-
6362
},
6463
"overrides": {
6564
"esbuild": "^0.25.0"

cli/src/cli.spec.ts

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
import { Command } from 'commander';
2-
import { setupCLI } from './cli';
32

43
let calmShared: typeof import('@finos/calm-shared');
54
let validateModule: typeof import('./command-helpers/validate');
65
let serverModule: typeof import('./server/cli-server');
76
let templateModule: typeof import('./command-helpers/template');
7+
let fileReaderModule: typeof import('./command-helpers/file-input');
8+
let optionsModule: typeof import ('./command-helpers/generate-options');
9+
let setupCLI: typeof import('./cli').setupCLI;
810

911
describe('CLI Commands', () => {
1012
let program: Command;
@@ -13,10 +15,16 @@ describe('CLI Commands', () => {
1315
vi.resetModules();
1416
vi.clearAllMocks();
1517

18+
fileReaderModule = await import('./command-helpers/file-input');
19+
const loadFileSpy = vi.spyOn(fileReaderModule, 'loadJsonFromFile');
20+
loadFileSpy.mockReset();
21+
loadFileSpy.mockImplementation(() => Promise.resolve({}));
22+
1623
calmShared = await import('@finos/calm-shared');
1724
validateModule = await import('./command-helpers/validate');
1825
serverModule = await import('./server/cli-server');
1926
templateModule = await import('./command-helpers/template');
27+
optionsModule = await import('./command-helpers/generate-options');
2028

2129
vi.spyOn(calmShared, 'runGenerate').mockResolvedValue(undefined);
2230
vi.spyOn(calmShared.TemplateProcessor.prototype, 'processTemplate').mockResolvedValue(undefined);
@@ -28,6 +36,11 @@ describe('CLI Commands', () => {
2836
vi.spyOn(serverModule, 'startServer').mockImplementation(vi.fn());
2937
vi.spyOn(templateModule, 'getUrlToLocalFileMap').mockReturnValue(new Map());
3038

39+
vi.spyOn(optionsModule, 'promptUserForOptions').mockResolvedValue([]);
40+
41+
const cliModule = await import('./cli');
42+
setupCLI = cliModule.setupCLI;
43+
3144
program = new Command();
3245
setupCLI(program);
3346
});
@@ -42,8 +55,11 @@ describe('CLI Commands', () => {
4255
'--schemaDirectory', 'schemas',
4356
]);
4457

58+
expect(fileReaderModule.loadJsonFromFile).toHaveBeenCalledWith('pattern.json', true);
59+
expect(optionsModule.promptUserForOptions).toHaveBeenCalled();
60+
4561
expect(calmShared.runGenerate).toHaveBeenCalledWith(
46-
'pattern.json', 'output.json', true, 'schemas'
62+
{}, 'output.json', true, [], 'schemas'
4763
);
4864
});
4965
});
@@ -79,7 +95,7 @@ describe('CLI Commands', () => {
7995
verbose: true,
8096
});
8197
});
82-
});
98+
});
8399

84100
describe('Template Command', () => {
85101
it('should instantiate TemplateProcessor and call processTemplate', async () => {

cli/src/cli.ts

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
1-
import {CALM_META_SCHEMA_DIRECTORY} from '@finos/calm-shared';
1+
import {CALM_META_SCHEMA_DIRECTORY, runGenerate} from '@finos/calm-shared';
22
import { Option, Command } from 'commander';
33
import { version } from '../package.json';
4+
import { loadJsonFromFile } from './command-helpers/file-input';
5+
import { promptUserForOptions } from './command-helpers/generate-options';
6+
import { CalmChoice } from '@finos/calm-shared/dist/commands/generate/components/options';
47

58
const FORMAT_OPTION = '-f, --format <format>';
69
const ARCHITECTURE_OPTION = '-a, --architecture <file>';
@@ -24,8 +27,9 @@ export function setupCLI(program: Command) {
2427
.option(SCHEMAS_OPTION, 'Path to the directory containing the meta schemas to use.', CALM_META_SCHEMA_DIRECTORY)
2528
.option(VERBOSE_OPTION, 'Enable verbose logging.', false)
2629
.action(async (options) => {
27-
const { runGenerate } = await import('@finos/calm-shared');
28-
await runGenerate(options.pattern, options.output, !!options.verbose, options.schemaDirectory);
30+
const pattern: object = await loadJsonFromFile(options.pattern, options.verbose);
31+
const choices: CalmChoice[] = await promptUserForOptions(pattern, options.verbose);
32+
await runGenerate(pattern, options.output, !!options.verbose, choices, options.schemaDirectory);
2933
});
3034

3135
program
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import { loadJsonFromFile } from './file-input';
2+
3+
const mocks = vi.hoisted(() => {
4+
return {
5+
readFile: vi.fn()
6+
};
7+
});
8+
9+
vi.mock('node:fs/promises', async () => {
10+
return {
11+
readFile: mocks.readFile
12+
};
13+
});
14+
15+
describe('fileInput', () => {
16+
it('should read a file and return its content as an object', async () => {
17+
mocks.readFile.mockReturnValue(JSON.stringify({ key: 'value' }));
18+
19+
await expect(loadJsonFromFile('test.json', false)).resolves.toEqual({ key: 'value' });
20+
expect(mocks.readFile).toHaveBeenCalledWith('test.json', 'utf-8');
21+
});
22+
23+
it('should pass along error if a random error is thrown', async () => {
24+
mocks.readFile.mockImplementation(() => {
25+
throw new Error('Random error');
26+
});
27+
28+
await expect(loadJsonFromFile('error.json', false)).rejects.toThrow('Random error');
29+
expect(mocks.readFile).toHaveBeenCalledWith('error.json', 'utf-8');
30+
});
31+
});
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import * as fs from 'node:fs/promises';
2+
import { initLogger } from '@finos/calm-shared';
3+
4+
export async function loadJsonFromFile(path: string, debug: boolean): Promise<object> {
5+
const logger = initLogger(debug, 'file-input');
6+
try {
7+
logger.info('Loading json from file: ' + path);
8+
const raw = await fs.readFile(path, 'utf-8');
9+
10+
logger.debug('Attempting to load json file: ' + raw);
11+
const pattern = JSON.parse(raw);
12+
13+
logger.debug('Loaded json file.');
14+
return pattern;
15+
} catch (err) {
16+
if (err.code === 'ENOENT') {
17+
logger.error('File not found!');
18+
} else {
19+
logger.error(err);
20+
}
21+
throw new Error(err);
22+
}
23+
}
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
import { CalmChoice } from '@finos/calm-shared/dist/commands/generate/components/options';
2+
import { promptUserForOptions } from './generate-options';
3+
4+
const mocks = vi.hoisted(() => {
5+
return {
6+
extractOptions: vi.fn(),
7+
select: vi.fn(),
8+
checkbox: vi.fn()
9+
};
10+
});
11+
12+
vi.mock('@finos/calm-shared/dist/commands/generate/components/options', () => ({
13+
extractOptions: mocks.extractOptions
14+
}));
15+
16+
vi.mock('@inquirer/prompts', () => ({
17+
select: mocks.select,
18+
checkbox: mocks.checkbox
19+
}));
20+
21+
const choice1 = { description: 'Option 1', nodes: ['option1'], relationships: ['relationship1'] };
22+
const choice2 = { description: 'Option 2', nodes: ['option2'], relationships: ['relationship2'] };
23+
24+
const choiceA = { description: 'Option A', nodes: ['optionA'], relationships: ['relationshipA'] };
25+
const choiceB = { description: 'Option B', nodes: ['optionB'], relationships: ['relationshipB'] };
26+
27+
const pattern = {}; // pattern doesn't matter since we're mocking the extractOptions function
28+
29+
describe('promptUserForOptions', () => {
30+
it('should prompt user for options and return selected choices', async () => {
31+
mocks.extractOptions.mockReturnValue([
32+
{
33+
optionType: 'oneOf',
34+
prompt: 'Choose an option:',
35+
choices: [choice1, choice2]
36+
},
37+
{
38+
optionType: 'anyOf',
39+
prompt: 'Select any of these options:',
40+
choices: [choiceA, choiceB]
41+
}
42+
]);
43+
mocks.select.mockReturnValue(Promise.resolve('Option 1'));
44+
mocks.checkbox.mockReturnValue(Promise.resolve(['Option A']));
45+
46+
const expectedChoices: CalmChoice[] = [choice1, choiceA];
47+
await expect(promptUserForOptions(pattern)).resolves.toEqual(expectedChoices);
48+
});
49+
50+
it('should return empty list when user selects nothing', async () => {
51+
mocks.extractOptions.mockReturnValue([{
52+
optionType: 'anyOf',
53+
prompt: 'Select any of these options:',
54+
choices: [choiceA, choiceB]
55+
}]);
56+
mocks.checkbox.mockReturnValue(Promise.resolve([])); // user selects nothing
57+
58+
await expect(promptUserForOptions(pattern)).resolves.toEqual([]);
59+
});
60+
61+
it('should throw an error when user selects an option thats not in the pattern', async () => {
62+
mocks.extractOptions.mockReturnValue([{
63+
optionType: 'oneOf',
64+
prompt: 'Choose an option:',
65+
choices: [choice1, choice2]
66+
}]);
67+
mocks.select.mockReturnValue(Promise.resolve('Invalid Option'));
68+
69+
await expect(promptUserForOptions(pattern)).rejects.toThrow('The choice of [Invalid Option] is not a valid choice in the pattern.');
70+
});
71+
});

0 commit comments

Comments
 (0)