Skip to content

Commit 09ba123

Browse files
Merge branch 'main' into add-init-ai-invalid-provider
2 parents f928733 + 1808b57 commit 09ba123

File tree

5 files changed

+263
-134
lines changed

5 files changed

+263
-134
lines changed

cli/src/command-helpers/validate.spec.ts

Lines changed: 1 addition & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,10 @@
11
import { Command } from 'commander';
22
import { Mock } from 'vitest';
33
import { getFormattedOutput, validate, exitBasedOffOfValidationOutcome } from '@finos/calm-shared';
4-
import { CALM_HUB_PROTO } from '@finos/calm-shared/dist/document-loader/document-loader';
54
import { mkdirp } from 'mkdirp';
65
import { writeFileSync } from 'fs';
76
import path from 'path';
8-
import { runValidate, writeOutputFile, checkValidateOptions, ValidateOptions, resolveSchemaRef, __test__ } from './validate';
7+
import { runValidate, writeOutputFile, checkValidateOptions, ValidateOptions, __test__ } from './validate';
98

109

1110
const dummyArch = { dummy: 'arch' };
@@ -297,65 +296,6 @@ describe('checkValidateOptions', () => {
297296
});
298297
});
299298

300-
describe('resolveSchemaRef', () => {
301-
const mockLogger = { debug: vi.fn(), error: vi.fn(), info: vi.fn(), warn: vi.fn() };
302-
303-
beforeEach(() => {
304-
vi.resetAllMocks();
305-
});
306-
307-
it('should return http URLs unchanged', () => {
308-
const result = resolveSchemaRef('http://example.com/schema.json', '/path/to/arch.json', mockLogger);
309-
expect(result).toBe('http://example.com/schema.json');
310-
});
311-
312-
it('should return https URLs unchanged', () => {
313-
const result = resolveSchemaRef('https://calm.finos.org/schema.json', '/path/to/arch.json', mockLogger);
314-
expect(result).toBe('https://calm.finos.org/schema.json');
315-
});
316-
317-
it(`should return ${CALM_HUB_PROTO} protocol URLs unchanged`, () => {
318-
const result = resolveSchemaRef(`${CALM_HUB_PROTO}//namespace/schema`, '/path/to/arch.json', mockLogger);
319-
expect(result).toBe(`${CALM_HUB_PROTO}//namespace/schema`);
320-
});
321-
322-
it('should return file:// URLs unchanged', () => {
323-
const result = resolveSchemaRef('file:///absolute/path/schema.json', '/path/to/arch.json', mockLogger);
324-
expect(result).toBe('file:///absolute/path/schema.json');
325-
});
326-
327-
it('should return absolute file paths unchanged', () => {
328-
const result = resolveSchemaRef('/absolute/path/schema.json', '/path/to/arch.json', mockLogger);
329-
expect(result).toBe('/absolute/path/schema.json');
330-
});
331-
332-
it('should resolve relative paths against architecture file directory', () => {
333-
const result = resolveSchemaRef('../schemas/custom.json', '/project/architectures/arch.json', mockLogger);
334-
expect(result).toBe('/project/schemas/custom.json');
335-
});
336-
337-
it('should resolve sibling relative paths against architecture file directory', () => {
338-
const result = resolveSchemaRef('./schema.json', '/project/architectures/arch.json', mockLogger);
339-
expect(result).toBe('/project/architectures/schema.json');
340-
});
341-
342-
it('should resolve simple filename against architecture file directory', () => {
343-
const result = resolveSchemaRef('schema.json', '/project/architectures/arch.json', mockLogger);
344-
expect(result).toBe('/project/architectures/schema.json');
345-
});
346-
347-
it('should return schemaRef unchanged when architecturePath is empty', () => {
348-
const result = resolveSchemaRef('../schemas/custom.json', '', mockLogger);
349-
expect(result).toBe('../schemas/custom.json');
350-
});
351-
352-
it('should log debug message when resolving relative path', () => {
353-
resolveSchemaRef('../schemas/custom.json', '/project/architectures/arch.json', mockLogger);
354-
expect(mockLogger.debug).toHaveBeenCalledWith(
355-
expect.stringContaining('Resolved relative $schema path')
356-
);
357-
});
358-
});
359299

360300
describe('rewritePathWithIds', () => {
361301
const { rewritePathWithIds } = __test__;

cli/src/command-helpers/validate.ts

Lines changed: 2 additions & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
1-
import { getFormattedOutput, validate, exitBasedOffOfValidationOutcome, SchemaDirectory, ValidationFormattingOptions, ValidationOutcome } from '@finos/calm-shared';
1+
import { getFormattedOutput, validate, exitBasedOffOfValidationOutcome, ValidationFormattingOptions, ValidationOutcome, loadArchitectureAndPattern } from '@finos/calm-shared';
22
import { initLogger } from '@finos/calm-shared';
33
import path from 'path';
44
import { mkdirp } from 'mkdirp';
55
import { readFileSync, writeFileSync } from 'fs';
66
import { Command } from 'commander';
77
import { ValidateOutputFormat } from '@finos/calm-shared/dist/commands/validate/validate';
88
import { buildSchemaDirectory, parseDocumentLoaderConfig } from '../cli';
9-
import { buildDocumentLoader, DocumentLoader, CALM_HUB_PROTO } from '@finos/calm-shared/dist/document-loader/document-loader';
9+
import { buildDocumentLoader, DocumentLoader } from '@finos/calm-shared/dist/document-loader/document-loader';
1010
import { Logger } from '@finos/calm-shared/dist/logger';
1111
import { getLocationForJsonPath, parseWithPointers } from '@stoplight/json';
1212

@@ -54,78 +54,7 @@ export async function runValidate(options: ValidateOptions) {
5454
}
5555
}
5656

57-
// Update loadArchitectureAndPattern and helpers to use DocumentLoader type
58-
async function loadArchitectureAndPattern(architecturePath: string, patternPath: string, docLoader: DocumentLoader, schemaDirectory: SchemaDirectory, logger: Logger): Promise<{ architecture: object, pattern: object }> {
59-
const architecture = await loadArchitecture(architecturePath, docLoader, logger);
60-
if (!architecture) {
61-
// we have already validated that at least one of the options is provided, so pattern must be set
62-
const pattern = await loadPattern(patternPath, docLoader, logger);
63-
return { architecture: undefined, pattern };
64-
}
65-
if (patternPath) {
66-
// both options set
67-
const pattern = await loadPattern(patternPath, docLoader, logger);
68-
return { architecture, pattern };
69-
}
70-
// architecture is set, but pattern is not; try to load pattern from architecture if present
71-
return { architecture, pattern: await loadPatternFromArchitectureIfPresent(architecture, architecturePath, docLoader, schemaDirectory, logger) };
72-
}
73-
74-
export function resolveSchemaRef(schemaRef: string, architecturePath: string, logger: Logger): string {
75-
// If it's an absolute URL (http, https, file) or calm: protocol, use as-is
76-
if (schemaRef.startsWith('http://') || schemaRef.startsWith('https://') || schemaRef.startsWith('file://') || schemaRef.startsWith(CALM_HUB_PROTO)) {
77-
return schemaRef;
78-
}
79-
// If it's an absolute file path, use as-is
80-
if (path.isAbsolute(schemaRef)) {
81-
return schemaRef;
82-
}
83-
// It's a relative path - resolve it relative to the architecture file's directory
84-
if (architecturePath) {
85-
const archDir = path.dirname(path.resolve(architecturePath));
86-
const resolved = path.resolve(archDir, schemaRef);
87-
logger.debug(`Resolved relative $schema path '${schemaRef}' to: ${resolved}`);
88-
return resolved;
89-
}
90-
logger.warn(`Could not resolve relative $schema path '${schemaRef}' because architecturePath is missing or falsy. Returning unresolved relative path.`);
91-
return schemaRef;
92-
}
93-
94-
async function loadPatternFromArchitectureIfPresent(architecture: object, architecturePath: string, docLoader: DocumentLoader, schemaDirectory: SchemaDirectory, logger: Logger): Promise<object> {
95-
if (!architecture || !architecture['$schema']) {
96-
return;
97-
}
98-
const schemaRef = resolveSchemaRef(architecture['$schema'], architecturePath, logger);
99-
try {
100-
const schema = schemaDirectory.getSchema(schemaRef);
101-
logger.debug(`Loaded schema from architecture: ${schemaRef}`);
102-
return schema;
103-
}
104-
catch (_) {
105-
logger.debug(`Trying to load pattern from architecture schema: ${schemaRef}`);
106-
}
107-
const pattern = docLoader.loadMissingDocument(schemaRef, 'pattern');
108-
logger.debug(`Loaded pattern from architecture schema: ${schemaRef}`);
109-
return pattern;
110-
}
11157

112-
async function loadPattern(patternPath: string, docLoader: DocumentLoader, logger: Logger): Promise<object> {
113-
if (!patternPath) {
114-
return undefined;
115-
}
116-
const pattern = docLoader.loadMissingDocument(patternPath, 'pattern');
117-
logger.debug(`Loaded pattern from ${patternPath}`);
118-
return pattern;
119-
}
120-
121-
async function loadArchitecture(architecturePath: string, docLoader: DocumentLoader, logger: Logger): Promise<object> {
122-
if (!architecturePath) {
123-
return undefined;
124-
}
125-
const arch = docLoader.loadMissingDocument(architecturePath, 'architecture');
126-
logger.debug(`Loaded architecture from ${architecturePath}`);
127-
return arch;
128-
}
12958

13059

13160
export function writeOutputFile(output: string, validationsOutput: string) {
Lines changed: 183 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,183 @@
1+
import { describe, it, expect, vi, beforeEach } from 'vitest';
2+
import { resolveSchemaRef, loadArchitectureAndPattern, loadArchitecture, loadPattern, loadPatternFromArchitectureIfPresent } from './loading-helpers';
3+
import { CALM_HUB_PROTO, DocumentLoader } from './document-loader';
4+
import { SchemaDirectory } from '../schema-directory';
5+
import { Logger } from '../logger';
6+
7+
describe('resolveSchemaRef', () => {
8+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
9+
const mockLogger: any = { debug: vi.fn(), error: vi.fn(), info: vi.fn(), warn: vi.fn() };
10+
11+
beforeEach(() => {
12+
vi.resetAllMocks();
13+
});
14+
15+
it('should return http URLs unchanged', () => {
16+
const result = resolveSchemaRef('http://example.com/schema.json', '/path/to/arch.json', mockLogger);
17+
expect(result).toBe('http://example.com/schema.json');
18+
});
19+
20+
it('should return https URLs unchanged', () => {
21+
const result = resolveSchemaRef('https://calm.finos.org/schema.json', '/path/to/arch.json', mockLogger);
22+
expect(result).toBe('https://calm.finos.org/schema.json');
23+
});
24+
25+
it(`should return ${CALM_HUB_PROTO} protocol URLs unchanged`, () => {
26+
const result = resolveSchemaRef(`${CALM_HUB_PROTO}//namespace/schema`, '/path/to/arch.json', mockLogger);
27+
expect(result).toBe(`${CALM_HUB_PROTO}//namespace/schema`);
28+
});
29+
30+
it('should return file:// URLs unchanged', () => {
31+
const result = resolveSchemaRef('file:///absolute/path/schema.json', '/path/to/arch.json', mockLogger);
32+
expect(result).toBe('file:///absolute/path/schema.json');
33+
});
34+
35+
it('should return absolute file paths unchanged', () => {
36+
const result = resolveSchemaRef('/absolute/path/schema.json', '/path/to/arch.json', mockLogger);
37+
expect(result).toBe('/absolute/path/schema.json');
38+
});
39+
40+
it('should resolve relative paths against architecture file directory', () => {
41+
const result = resolveSchemaRef('../schemas/custom.json', '/project/architectures/arch.json', mockLogger);
42+
expect(result).toBe('/project/schemas/custom.json');
43+
});
44+
45+
it('should resolve sibling relative paths against architecture file directory', () => {
46+
const result = resolveSchemaRef('./schema.json', '/project/architectures/arch.json', mockLogger);
47+
expect(result).toBe('/project/architectures/schema.json');
48+
});
49+
50+
it('should resolve simple filename against architecture file directory', () => {
51+
const result = resolveSchemaRef('schema.json', '/project/architectures/arch.json', mockLogger);
52+
expect(result).toBe('/project/architectures/schema.json');
53+
});
54+
55+
it('should return schemaRef unchanged when architecturePath is empty', () => {
56+
const result = resolveSchemaRef('../schemas/custom.json', '', mockLogger);
57+
expect(result).toBe('../schemas/custom.json');
58+
});
59+
60+
it('should log debug message when resolving relative path', () => {
61+
resolveSchemaRef('../schemas/custom.json', '/project/architectures/arch.json', mockLogger);
62+
expect(mockLogger.debug).toHaveBeenCalledWith(
63+
expect.stringContaining('Resolved relative $schema path')
64+
);
65+
});
66+
});
67+
68+
describe('loading helpers', () => {
69+
const mockLogger = { debug: vi.fn(), warn: vi.fn(), error: vi.fn(), log: vi.fn(), info: vi.fn() } as unknown as Logger;
70+
const mockDocLoader = {
71+
loadMissingDocument: vi.fn(),
72+
initialise: vi.fn(),
73+
resolvePath: vi.fn()
74+
} as unknown as DocumentLoader;
75+
const mockSchemaDirectory = {
76+
getSchema: vi.fn()
77+
} as unknown as SchemaDirectory;
78+
79+
beforeEach(() => {
80+
vi.resetAllMocks();
81+
});
82+
83+
describe('loadArchitecture', () => {
84+
it('should return undefined if architecturePath is not provided', async () => {
85+
const result = await loadArchitecture(undefined, mockDocLoader, mockLogger);
86+
expect(result).toBeUndefined();
87+
});
88+
89+
it('should load architecture from path', async () => {
90+
const arch = { kind: 'architecture' };
91+
vi.mocked(mockDocLoader.loadMissingDocument).mockResolvedValue(arch);
92+
const result = await loadArchitecture('path/to/arch.json', mockDocLoader, mockLogger);
93+
expect(mockDocLoader.loadMissingDocument).toHaveBeenCalledWith('path/to/arch.json', 'architecture');
94+
expect(result).toBe(arch);
95+
});
96+
});
97+
98+
describe('loadPattern', () => {
99+
it('should return undefined if patternPath is not provided', async () => {
100+
const result = await loadPattern(undefined, mockDocLoader, mockLogger);
101+
expect(result).toBeUndefined();
102+
});
103+
104+
it('should load pattern from path', async () => {
105+
const pattern = { kind: 'pattern' };
106+
vi.mocked(mockDocLoader.loadMissingDocument).mockResolvedValue(pattern);
107+
const result = await loadPattern('path/to/pattern.json', mockDocLoader, mockLogger);
108+
expect(mockDocLoader.loadMissingDocument).toHaveBeenCalledWith('path/to/pattern.json', 'pattern');
109+
expect(result).toBe(pattern);
110+
});
111+
});
112+
113+
describe('loadPatternFromArchitectureIfPresent', () => {
114+
it('should return undefined if architecture is missing', async () => {
115+
const result = await loadPatternFromArchitectureIfPresent(undefined, 'arch.json', mockDocLoader, mockSchemaDirectory, mockLogger);
116+
expect(result).toBeUndefined();
117+
});
118+
119+
it('should return undefined if architecture has no $schema', async () => {
120+
const result = await loadPatternFromArchitectureIfPresent({}, 'arch.json', mockDocLoader, mockSchemaDirectory, mockLogger);
121+
expect(result).toBeUndefined();
122+
});
123+
124+
it('should load schema from SchemaDirectory if available', async () => {
125+
const schema = { kind: 'pattern' };
126+
const arch = { '$schema': 'pattern.json' };
127+
vi.mocked(mockSchemaDirectory.getSchema).mockResolvedValue(schema);
128+
129+
const result = await loadPatternFromArchitectureIfPresent(arch, '/path/arch.json', mockDocLoader, mockSchemaDirectory, mockLogger);
130+
131+
expect(mockSchemaDirectory.getSchema).toHaveBeenCalledWith('/path/pattern.json');
132+
expect(result).toBe(schema);
133+
});
134+
135+
it('should fall back to docLoader if SchemaDirectory throws', async () => {
136+
const pattern = { kind: 'pattern' };
137+
const arch = { '$schema': 'pattern.json' };
138+
vi.mocked(mockSchemaDirectory.getSchema).mockRejectedValue(new Error('not found'));
139+
vi.mocked(mockDocLoader.loadMissingDocument).mockResolvedValue(pattern);
140+
141+
const result = await loadPatternFromArchitectureIfPresent(arch, '/path/arch.json', mockDocLoader, mockSchemaDirectory, mockLogger);
142+
143+
expect(mockDocLoader.loadMissingDocument).toHaveBeenCalledWith('/path/pattern.json', 'pattern');
144+
expect(result).toBe(pattern);
145+
});
146+
});
147+
148+
describe('loadArchitectureAndPattern', () => {
149+
it('should load architecture and pattern when both paths provided', async () => {
150+
const arch = { kind: 'architecture' };
151+
const pattern = { kind: 'pattern' };
152+
vi.mocked(mockDocLoader.loadMissingDocument)
153+
.mockResolvedValueOnce(arch)
154+
.mockResolvedValueOnce(pattern);
155+
156+
const result = await loadArchitectureAndPattern('arch.json', 'pattern.json', mockDocLoader, mockSchemaDirectory, mockLogger);
157+
158+
expect(result).toEqual({ architecture: arch, pattern });
159+
});
160+
161+
it('should load pattern only if architecture fails to load', async () => {
162+
const pattern = { kind: 'pattern' };
163+
vi.mocked(mockDocLoader.loadMissingDocument)
164+
.mockResolvedValueOnce(undefined) // architecture
165+
.mockResolvedValueOnce(pattern); // pattern
166+
167+
const result = await loadArchitectureAndPattern('arch.json', 'pattern.json', mockDocLoader, mockSchemaDirectory, mockLogger);
168+
169+
expect(result).toEqual({ architecture: undefined, pattern });
170+
});
171+
172+
it('should load pattern from architecture if patternPath missing', async () => {
173+
const arch = { kind: 'architecture', '$schema': 'pattern.json' };
174+
const pattern = { kind: 'pattern' };
175+
vi.mocked(mockDocLoader.loadMissingDocument).mockResolvedValueOnce(arch);
176+
vi.mocked(mockSchemaDirectory.getSchema).mockResolvedValue(pattern);
177+
178+
const result = await loadArchitectureAndPattern('arch.json', undefined, mockDocLoader, mockSchemaDirectory, mockLogger);
179+
180+
expect(result).toEqual({ architecture: arch, pattern });
181+
});
182+
});
183+
});

0 commit comments

Comments
 (0)