-
Notifications
You must be signed in to change notification settings - Fork 208
/
SchemaFileLocater.ts
241 lines (210 loc) · 9.93 KB
/
SchemaFileLocater.ts
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
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
/*---------------------------------------------------------------------------------------------
* Copyright (c) Bentley Systems, Incorporated. All rights reserved.
* See LICENSE.md in the project root for license terms and full copyright notice.
*--------------------------------------------------------------------------------------------*/
/** @packageDocumentation
* @module Locaters
*/
import * as fs from "fs";
import { globSync } from "glob";
import * as path from "path";
import { Schema, SchemaContext, SchemaKey, SchemaMatchType } from "@itwin/ecschema-metadata";
const formatString = (format: string, ...args: string[]) => {
return format.replace(/{(\d+)}/g, (match, theNumber) => {
return typeof args[theNumber] !== "undefined"
? args[theNumber]
: match;
});
};
const padStartEx = (str: string, targetLength: number, padString: string) => {
targetLength = targetLength >> 0; // truncate if number or convert non-number to 0;
padString = String((typeof padString !== "undefined" ? padString : " "));
if (str.length > targetLength) {
return String(str);
} else {
targetLength = targetLength - str.length;
if (targetLength > padString.length) {
padString += padString.repeat(targetLength / padString.length); // append to original to ensure we are longer than needed
}
return padString.slice(0, targetLength) + str;
}
};
/**
* A SchemaKey implementation that aids in identifying Schema files via the
* addition of two properties: fileName and schemaText. The fileName contains the
* full path to the file on disk and schemaText is the full string representation
* of the Schema.
* @beta
*/
export class FileSchemaKey extends SchemaKey {
// The schema file associated with the SchemaKey
public fileName: string;
// The JSON text for the schema loaded
public schemaText?: string;
/**
* Initializes a new FileSchemaKey object.
* @param key The EC SchemaKey identifying the Schema.
* @param fileName The full path to the Schema file.
* @param schemaText The string representation of the Schema
* loaded from disk. Optional.
*/
constructor(key: SchemaKey, fileName: string, schemaJson?: string) {
super(key.name, key.version);
this.fileName = fileName;
this.schemaText = schemaJson;
}
}
/**
* Abstract class to hold common/overlapping functionality between SchemaJsonFileLocater and SchemaXmlFileLocater
* @beta - Needs further testing and possibly moved to a separate package.
*/
export abstract class SchemaFileLocater {
public searchPaths: string[];
constructor() {
this.searchPaths = [];
}
public async readUtf8FileToString(filePath: string): Promise<string | undefined> {
return new Promise<string | undefined>((resolve, reject) => {
fs.readFile(filePath, "utf-8", (err, data) => {
if (err)
reject(err);
else
resolve(data);
});
});
}
public readUtf8FileToStringSync(filePath: string): string | undefined {
return fs.readFileSync(filePath, "utf-8");
}
public async fileExists(filePath: string): Promise<boolean | undefined> {
return new Promise<boolean | undefined>((resolve) => {
fs.access(filePath, fs.constants.F_OK, (err) => {
resolve(err ? false : true);
});
});
}
public fileExistsSync(filePath: string): boolean | undefined {
return fs.existsSync(filePath);
}
/**
* Adds more search paths used by this locator to find the
* Schema files.
* @param schemaPaths An array of search paths to add
*/
public addSchemaSearchPaths(schemaPaths: string[]) {
// If the path is not in the schemaPaths array, add it
for (const schemaPath of schemaPaths)
this.addSchemaSearchPath(schemaPath);
}
/**
* Add one search path used by this locator to find the
* Schema files.
* @param schemaPath A search path to add
*/
public addSchemaSearchPath(schemaPath: string) {
// If the path is not in the schemaPaths array, add it
if (!this.searchPaths.find((entry) => entry === schemaPath))
this.searchPaths.push(schemaPath);
}
protected abstract getSchemaKey(data: string): SchemaKey;
/**
* Adds SchemaKeys to the provided foundFiles collection that match the desired SchemaKey. This method
* only attempts to find schema files that have no version in the file name.
* @param foundFiles The collection of SchemaKeys found in the given directory.
* @param schemaPath The directory in which to search for the Schemas.
* @param schemaName The short name of the Schema (without version).
* @param desiredKey The SchemaKey used to find matching Schema files.
* @param matchType The SchemaMatchType to use when comparing the desiredKey and the keys found during the search.
* @param format The type of file that the schema key refers to. json or xml
*/
private addCandidateNoExtSchemaKey(foundFiles: FileSchemaKey[], schemaPath: string, schemaName: string, desiredKey: Readonly<SchemaKey>, matchType: SchemaMatchType, format: string) {
const fullPath = path.join(schemaPath, `${schemaName}.ecschema.${format}`);
// If the file does not exist, end
if (!fs.existsSync(fullPath))
return;
// Read the file
const file = fs.readFileSync(fullPath);
if (!file)
return;
// Get the schema key
const key = this.getSchemaKey(file.toString());
// If the key matches, put it in foundFiles
if (key.matches(desiredKey, matchType))
foundFiles.push(new FileSchemaKey(key, fullPath, file.toString()));
}
/**
* Adds SchemaKeys to the provided foundFiles collection that match the desired SchemaKey
* @param foundFiles The collection of SchemaKeys found in the given directory
* @param schemaPath The directory in which to search for the Schemas
* @param fileFilter The file filter, potentially with wildcards, used to locate the Schema files.
* @param desiredKey The schemaKey used to find matching Schema files
* @param matchType The SchemaMatchType to use when comparing the desired Key and the keys found during the search.
* @param format The type of file that the schema key refers to. json or xml
*/
private addCandidateSchemaKeys(foundFiles: FileSchemaKey[], schemaPath: string, fileFilter: string, desiredKey: Readonly<SchemaKey>, matchType: SchemaMatchType, format: string) {
const fullPath = path.join(schemaPath, fileFilter);
const result = globSync(fullPath, { windowsPathsNoEscape: true });
for (const match of result) {
let fileName = path.basename(match, (`.ecschema.${format}`));
// TODO: should this be moved or handled elsewhere?
// Handles two version file names - SchemaKey.parseString supports only 3 version names.
if (/[^\d]\.\d?\d\.\d?\d$/.test(fileName)) {
const parts = fileName.split(".");
parts.splice(2, 0, "00");
fileName = parts.join(".");
}
const file = fs.readFileSync(match);
if (!file)
continue;
const schemaKey = SchemaKey.parseString(fileName);
if (schemaKey.matches(desiredKey, matchType))
foundFiles.push(new FileSchemaKey(schemaKey, match, file.toString()));
}
}
/**
* Attempts to find all Schema files in the configurable search paths that match
* the desired SchemaKey.
* @param desiredKey The SchemaKey to match.
* @param matchType The SchemaMatchType.
* @param format The type of file that the schema key refers to. json or xml
*/
protected findEligibleSchemaKeys(desiredKey: Readonly<SchemaKey>, matchType: SchemaMatchType, format: string): FileSchemaKey[] {
const foundFiles = new Array<FileSchemaKey>();
let twoVersionSuffix: string;
let threeVersionSuffix: string;
const readVersion = desiredKey.readVersion.toString();
const writeVersion = desiredKey.writeVersion.toString();
const minorVersion = desiredKey.minorVersion.toString();
if (matchType === SchemaMatchType.Latest) {
twoVersionSuffix = (`.*.*.ecschema.${format}`);
threeVersionSuffix = (`.*.*.*.ecschema.${format}`);
} else if (matchType === SchemaMatchType.LatestWriteCompatible) {
twoVersionSuffix = formatString(`.{0}.*.ecschema.${format}`, padStartEx(readVersion, 2, "0"));
threeVersionSuffix = formatString(`.{0}.{1}.*.ecschema.${format}`, padStartEx(readVersion, 2, "0"), padStartEx(writeVersion, 2, "0"));
} else if (matchType === SchemaMatchType.LatestReadCompatible) {
twoVersionSuffix = formatString(`.{0}.*.ecschema.${format}`, padStartEx(readVersion, 2, "0"));
threeVersionSuffix = formatString(`.{0}.*.*.ecschema.${format}`, padStartEx(readVersion, 2, "0"));
} else {
twoVersionSuffix = formatString(`.{0}.{1}.ecschema.${format}`, padStartEx(readVersion, 2, "0"), padStartEx(writeVersion, 2, "0"));
threeVersionSuffix = formatString(`.{0}.{1}.{2}.ecschema.${format}`, padStartEx(readVersion, 2, "0"), padStartEx(writeVersion, 2, "0"), padStartEx(minorVersion, 2, "0"));
}
const twoVersionExpression = desiredKey.name + twoVersionSuffix;
const threeVersionExpression = desiredKey.name + threeVersionSuffix;
for (const searchPath of this.searchPaths) {
this.addCandidateNoExtSchemaKey(foundFiles, searchPath, desiredKey.name, desiredKey, matchType, format);
this.addCandidateSchemaKeys(foundFiles, searchPath, twoVersionExpression, desiredKey, matchType, format);
this.addCandidateSchemaKeys(foundFiles, searchPath, threeVersionExpression, desiredKey, matchType, format);
}
return foundFiles;
}
public abstract getSchema<T extends Schema>(key: SchemaKey, matchType: SchemaMatchType, context: SchemaContext): Promise<T | undefined>;
/**
* Compares two Schema versions. If the left-hand version is greater, 1 is returned. If the
* left-hand version is less, -1 us returned. If the versions are an exact match, 0 is returned.
* @param lhs The 'left-hand' FileSchemaKey.
* @param rhs The 'right-hand' FileSchemaKey.
*/
public compareSchemaKeyByVersion = (lhs: FileSchemaKey, rhs: FileSchemaKey): number => {
return lhs.compareByVersion(rhs);
};
}