All files / services/validation preset-shape-writer.ts

94.28% Statements 33/35
80% Branches 16/20
100% Functions 4/4
93.93% Lines 31/33

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132                                                                                5x   5x 1x     4x   4x 1x     3x 3x   3x 5x   5x 3x   3x       3x 2x     3x                                 3x 4x 4x 4x   4x 2x     2x   1x     1x         1x             1x                     4x 4x   2x   2x      
import * as vscode from 'vscode';
import { WorkspaceUri } from '@src/providers/workspace-uri';
import { getShapesFolder } from '@src/utilities/vscode/config';
import { VALIDATION_PRESETS } from './preset-definitions';
import { getPresetShapeSource } from './presets';
 
/**
 * The result of writing a preset shape graph into the workspace.
 */
export interface WrittenPresetShapes {
	/**
	 * The canonical `workspace:///` URI of the written shape file, to store in a
	 * profile's `shapes[]`.
	 */
	uri: string;
 
	/**
	 * `true` when an existing, identical file was reused instead of writing a new one.
	 */
	reused: boolean;
}
 
/**
 * Writes a copy of a built-in preset shape graph into the workspace so a profile
 * created from a preset validates against a frozen, version-controlled copy
 * rather than the bundled graph (which may change in a future Mentor release).
 *
 * The file is written to `<shapesFolder>/<presetId>-<version>.ttl` (see the
 * `mentor.shacl.shapesFolder` setting), mirroring the bundled source's file name —
 * the version in the name ties the frozen copy to the shape graph release it was
 * taken from. When a file already exists at that path, an identical one is reused.
 * A different one (edited, or written by an older Mentor release) is never
 * overwritten: the user is asked whether to reuse the existing copy or keep both,
 * in which case a numeric suffix (`-2`, `-3`, …) is chosen for the new file.
 *
 * @param presetId The built-in preset id (e.g. `'ontology'`).
 * @returns The canonical workspace URI of the shape file and whether it was reused.
 * @throws When the preset is unknown or no workspace folder is open.
 */
export async function writePresetShapes(presetId: string): Promise<WrittenPresetShapes> {
	const source = getPresetShapeSource(presetId);
 
	if (source === undefined) {
		throw new Error(`Unknown validation preset: ${presetId}`);
	}
 
	const root = WorkspaceUri.getEffectiveRootUri();
 
	if (!root) {
		throw new Error('Open a workspace folder to copy the shapes into it.');
	}
 
	const folder = getShapesFolder();
	const bytes = new TextEncoder().encode(source);
 
	const version = VALIDATION_PRESETS.find(p => p.id === presetId)?.version;
	const baseName = version ? `${presetId}-${version}` : presetId;
 
	const target = await resolveTargetUri(root, folder, presetId, baseName, source);
	const workspaceUri = WorkspaceUri.toWorkspaceUri(target.uri);
 
	Iif (!workspaceUri) {
		throw new Error('Could not resolve a workspace URI for the shapes file.');
	}
 
	if (!target.reused) {
		await vscode.workspace.fs.writeFile(target.uri, bytes);
	}
 
	return { uri: workspaceUri.toString(), reused: target.reused };
}
 
/**
 * Picks the file URI to write the shapes to under `<folder>`: the plain
 * `<baseName>.ttl` when free or already holding identical content (reused).
 * When it holds different content (edited, or from an older Mentor release), the
 * user chooses between reusing that copy and keeping both; keeping both picks
 * the first free `<baseName>-N.ttl`.
 */
async function resolveTargetUri(
	root: vscode.Uri,
	folder: string,
	presetId: string,
	baseName: string,
	source: string
): Promise<{ uri: vscode.Uri; reused: boolean }> {
	for (let index = 1; ; index++) {
		const name = index === 1 ? `${baseName}.ttl` : `${baseName}-${index}.ttl`;
		const uri = vscode.Uri.joinPath(root, folder, name);
		const existing = await readIfExists(uri);
 
		if (existing === undefined) {
			return { uri, reused: false };
		}
 
		if (existing === source) {
			// An identical copy already exists — reuse it rather than duplicating.
			return { uri, reused: true };
		}
 
		Eif (index === 1) {
			// The primary copy differs from the current built-in shapes — it was
			// either edited or written by an older Mentor release. Never overwrite
			// it; ask whether to validate against it or write a fresh copy next to
			// it. Dismissing the prompt keeps both (the non-destructive default).
			const action = await vscode.window.showWarningMessage(
				`A copy of the "${presetId}" shapes already exists in the workspace (${folder}/${name}) but differs from the current built-in version — `
				+ 'it may have been edited, or come from an older Mentor release.',
				'Use Existing Copy',
				'Keep Both'
			);
 
			Iif (action === 'Use Existing Copy') {
				return { uri, reused: true };
			}
		}
	}
}
 
/**
 * Reads a file as UTF-8 text, returning `undefined` when it does not exist.
 */
async function readIfExists(uri: vscode.Uri): Promise<string | undefined> {
	try {
		const bytes = await vscode.workspace.fs.readFile(uri);
 
		return new TextDecoder().decode(bytes);
	} catch {
		return undefined;
	}
}