fix(common): legacy .kpj schema

Fixes #9140.
Fixes #9148.

Keyman Developer 9.0 .kpj files included a lot of additional state
metadata. We need a schema which validates these files, as they are
otherwise valid to load (we'll never save them any more). Rather than
add all the extra metadata to what is otherwise a fairly clean schema,
we'll provide a legacy .schema.json.

In the future, we may be able to merge these schemas, as we move towards
the .kpj 2.0 format which doesn't list files. Ideally, all three formats
(Keyman Developer 9.0 schema, call it legacy, 1.0 schema for Keyman
Developer 10.0+ which has Options and Files listed, 2.0 schema for
capturing just project settings for a folder) will be supported by a
single schema file.
This commit is contained in:
Marc Durdin 2023-07-03 08:59:45 +07:00
parent f85478ac6f
commit 33affc660f
11 changed files with 171 additions and 9 deletions

View file

@ -0,0 +1,11 @@
# kpj-9.0.schema.json
**Note:** `KeymanDeveloperProject.Options.Version` is currently implicitly
always '1.0'. It will be required for version 2.0 and later of the format.
This is a schema for for supporting legacy versions of .kpj, from Keyman
Developer 9.0 and earlier, is now available. Note that this schema is not fully
validating on the legacy fields, but just sufficient to pass a valid
file.
You should be using kpj.schema.json for most purposes

View file

@ -0,0 +1,134 @@
{
"title": "kpj-9.0.xsd",
"$schema": "http://json-schema.org/schema#",
"type": "object",
"properties": {
"KeymanDeveloperProject": {
"properties": {
"Options": {
"$ref": "#/definitions/Options"
},
"Files": {
"$ref": "#/definitions/Files"
},
"templatepath": { "type":"string", "$comment": "Keyman Developer 9, no longer used in modern .kpj files" },
"stringspath": { "type":"string", "$comment": "Keyman Developer 9, no longer used in modern .kpj files" },
"state": { "type": "string", "$comment": "Keyman Developer 9, no longer used in modern .kpj files" },
"Modules": { "oneOf": [{"type": "object"}, {"type": "string"}], "$comment": "Keyman Developer 9, no longer used in modern .kpj files" },
"MRU": { "type": "object", "$comment": "Keyman Developer 9, no longer used in modern .kpj files" }
},
"required": [
],
"additionalProperties": false,
"type": "object"
}
},
"required": [
"KeymanDeveloperProject"
],
"additionalProperties": false,
"definitions": {
"Options": {
"type": "object",
"properties": {
"BuildPath": {
"type": "string"
},
"SourcePath": {
"type": "string"
},
"CompilerWarningsAsErrors": {
"type": "string",
"pattern": "^(True|False)$"
},
"WarnDeprecatedCode": {
"type": "string",
"pattern": "^(True|False)$"
},
"CheckFilenameConventions": {
"type": "string",
"pattern": "^(True|False)$"
},
"ProjectType": {
"type": "string",
"pattern": "^(keyboard|lexicalmodel)$"
},
"Version": {
"type": "string",
"pattern": "^(1\\.0|2\\.0)$"
}
},
"required": [
],
"additionalProperties": false
},
"Files": {
"type": "object",
"properties": {
"File": {
"type": "array",
"items": {
"$ref": "#/definitions/File"
}
}
},
"additionalProperties": false,
"required": [
"File"
]
},
"File": {
"type": "object",
"properties": {
"ID": {
"type": "string"
},
"Filename": {
"type": "string"
},
"Filepath": {
"type": "string"
},
"FileVersion": {
"type": "string"
},
"FileType": {
"type": "string"
},
"Details": {
"$ref": "#/definitions/FileDetails"
},
"ParentFileID": {
"type": "string"
},
"FullPath": { "type":"string", "$comment": "Keyman Developer 9, no longer used in modern .kpj files" },
"IDEState": { "type":"object", "$comment": "Keyman Developer 9, no longer used in modern .kpj files" }
},
"required": [
"Filename"
],
"additionalProperties": false
},
"FileDetails": {
"type": "object",
"properties": {
"Name": {
"type": "string"
},
"Copyright": {
"type": "string"
},
"Message": {
"type": "string"
},
"Version": {
"type": "string"
},
"Debug": { "type":"string", "$comment": "Keyman Developer 9, no longer used in modern .kpj files" }
},
"required": [
],
"additionalProperties": false
}
}
}

View file

@ -3,6 +3,9 @@
**Note:** `KeymanDeveloperProject.Options.Version` is currently implicitly
always '1.0'. It will be required for version 2.0 and later of the format.
**Note:** An additional schema file, kpj-9.0.schema.json, for supporting legacy
versions of .kpj, from Keyman Developer 9.0 and earlier, is now available.
## 2023-02-27 2.0
* Version 2.0 makes 'Files' optional (internally, Files/File will be ignored,
deleted on load and populated from folder structure). Adds Options/SourcePath,

View file

@ -45,6 +45,7 @@ function copy_schemas() {
cp "$KEYMAN_ROOT/resources/standards-data/ldml-keyboards/techpreview/ldml-keyboardtest.schema.json" "$THIS_SCRIPT_PATH/build/src/"
cp "$KEYMAN_ROOT/common/schemas/kvks/kvks.schema.json" "$THIS_SCRIPT_PATH/build/src/"
cp "$KEYMAN_ROOT/common/schemas/kpj/kpj.schema.json" "$THIS_SCRIPT_PATH/build/src/"
cp "$KEYMAN_ROOT/common/schemas/kpj-9.0/kpj-9.0.schema.json" "$THIS_SCRIPT_PATH/build/src/"
cp "$KEYMAN_ROOT/common/schemas/keyman-touch-layout/keyman-touch-layout.clean.spec.json" "$THIS_SCRIPT_PATH/build/src/"
cp "$KEYMAN_ROOT/common/schemas/displaymap/displaymap.schema.json" "$THIS_SCRIPT_PATH/build/src/"
# Store CLDR imports

View file

@ -23,21 +23,27 @@ export class KPJFileReader {
});
parser.parseString(file, (e: unknown, r: unknown) => { data = r as KPJFile });
data = this.boxArrays(data);
for(let file of data.KeymanDeveloperProject?.Files?.File) {
// xml2js imports <Details/> as '' so we will just delete the empty string
if(typeof file.Details == 'string') {
delete file.Details;
}
}
data = this.boxArrays(data);
return data as KPJFile;
}
public validate(source: KPJFile, schemaBuffer: Uint8Array): void {
public validate(source: KPJFile, schemaBuffer: Uint8Array, legacySchemaBuffer: Uint8Array): void {
const schema = JSON.parse(new TextDecoder().decode(schemaBuffer));
const ajv = new Ajv();
if(!ajv.validate(schema, source)) {
throw new Error(ajv.errorsText());
const ajvLegacy = new Ajv();
const legacySchema = JSON.parse(new TextDecoder().decode(legacySchemaBuffer));
if(!ajvLegacy.validate(legacySchema, source)) {
// If the legacy schema also does not validate, then we will only report
// the errors against the modern schema
throw new Error(ajv.errorsText());
}
}
}

View file

@ -116,6 +116,7 @@ export type CompilerSchema =
'ldml-keyboardtest' |
'kvks' |
'kpj' |
'kpj-9.0' |
'displaymap';
// | 'keyman-touch-layout.clean'; TODO this has the wrong name pattern, .spec.json instead of .schema.json

View file

@ -12,15 +12,15 @@ export class TestCompilerCallbacks implements CompilerCallbacks {
loadSchema(schema: CompilerSchema): Buffer {
switch (schema) {
case 'kpj':
throw new Error('loadKpjJsonSchema not implemented.'); // not needed for this test
case 'kpj-9.0':
case 'kvks':
throw new Error('loadKvksJsonSchema not implemented.');
case 'displaymap':
// listing all unimplemented schemas rather than using 'default'
throw new Error(`loadSchema(${schema}) not implemented.`); // not needed for this test
case 'ldml-keyboard':
return loadSchema(schema);
case 'ldml-keyboardtest':
return loadSchema(schema);
case 'displaymap':
throw new Error('loadDisplayMapSchema not implemented.'); // not needed for this test
}
}
clear() {

View file

@ -16,7 +16,7 @@ describe('kpj-file-reader', function () {
const reader = new KPJFileReader(callbacks);
const kpj = reader.read(input);
assert.doesNotThrow(() => {
reader.validate(kpj, loadSchema('kpj'));
reader.validate(kpj, loadSchema('kpj'), loadSchema('kpj-9.0'));
});
assert.equal(kpj.KeymanDeveloperProject.Options.BuildPath, '$PROJECTPATH\\build');
assert.equal(kpj.KeymanDeveloperProject.Options.CheckFilenameConventions, 'False');

View file

@ -45,6 +45,9 @@ fi
SCHEMAS_COPIED=false
copy_schemas() {
# TODO: why do we need a copy of the schemas here? kmc already has a copy of
# them; are they used only by tests, in which case, can't we use them in
# their source location?
if $SCHEMAS_COPIED; then
return 0
fi
@ -55,6 +58,7 @@ copy_schemas() {
cp "$KEYMAN_ROOT/resources/standards-data/ldml-keyboards/techpreview/ldml-keyboardtest.schema.json" "$THIS_SCRIPT_PATH/build/src/"
cp "$KEYMAN_ROOT/common/schemas/kvks/kvks.schema.json" "$THIS_SCRIPT_PATH/build/src/"
cp "$KEYMAN_ROOT/common/schemas/kpj/kpj.schema.json" "$THIS_SCRIPT_PATH/build/src/"
cp "$KEYMAN_ROOT/common/schemas/kpj-9.0/kpj-9.0.schema.json" "$THIS_SCRIPT_PATH/build/src/"
}
#-------------------------------------------------------------------------------------------------------------------

View file

@ -61,6 +61,7 @@ function copy_schemas() {
cp "$KEYMAN_ROOT/resources/standards-data/ldml-keyboards/techpreview/ldml-keyboardtest.schema.json" "$THIS_SCRIPT_PATH/build/src/util/"
cp "$KEYMAN_ROOT/common/schemas/kvks/kvks.schema.json" "$THIS_SCRIPT_PATH/build/src/util/"
cp "$KEYMAN_ROOT/common/schemas/kpj/kpj.schema.json" "$THIS_SCRIPT_PATH/build/src/util/"
cp "$KEYMAN_ROOT/common/schemas/kpj-9.0/kpj-9.0.schema.json" "$THIS_SCRIPT_PATH/build/src/util/"
cp "$KEYMAN_ROOT/common/schemas/displaymap/displaymap.schema.json" "$THIS_SCRIPT_PATH/build/src/util/"
}

View file

@ -89,8 +89,9 @@ class ProjectBuilder {
const reader = new KPJFileReader(this.callbacks);
const kpj = reader.read(kpjData);
const schema = this.callbacks.loadSchema('kpj');
const legacySchema = this.callbacks.loadSchema('kpj-9.0');
try {
reader.validate(kpj, schema);
reader.validate(kpj, schema, legacySchema);
} catch(e) {
this.callbacks.reportMessage(InfrastructureMessages.Error_InvalidProjectFile({message: (e??'').toString()}));
return null;