Add unknown ranges and open object values
This commit is contained in:
@@ -8,22 +8,25 @@ copyFileSync(resolve(packageDir, '../../LICENSE-APACHE'), resolve(packageDir, 'L
|
||||
|
||||
const declarationPath = resolve(packageDir, 'decodal_wasm.d.ts');
|
||||
let declarations = readFileSync(declarationPath, 'utf8');
|
||||
const hostTypes = `
|
||||
/** A concrete JavaScript value or Decodal schema descriptor supplied by the host. */
|
||||
export type DecodalHostValue =
|
||||
const valueTypes = `
|
||||
/** A concrete JavaScript value or Decodal range descriptor. */
|
||||
export type DecodalValue =
|
||||
| string
|
||||
| number
|
||||
| boolean
|
||||
| DecodalHostValue[]
|
||||
| { [field: string]: DecodalHostValue }
|
||||
| DecodalValue[]
|
||||
| { [field: string]: DecodalValue }
|
||||
| DecodalPrimitiveDescriptor
|
||||
| DecodalUnknownDescriptor
|
||||
| DecodalArrayDescriptor
|
||||
| DecodalMapDescriptor
|
||||
| DecodalAbstractDescriptor;
|
||||
| DecodalObjectDescriptor
|
||||
| DecodalRangeDescriptor;
|
||||
|
||||
export type DecodalPrimitiveType = 'String' | 'Int' | 'Float' | 'Bool';
|
||||
|
||||
export type DecodalConstraint =
|
||||
| { kind: 'unknown' }
|
||||
| { kind: 'type'; value: DecodalPrimitiveType }
|
||||
| { kind: 'compare'; op: '>' | '>=' | '<' | '<='; value: string | number | boolean }
|
||||
| { kind: 'regex'; value: string }
|
||||
@@ -32,32 +35,44 @@ export type DecodalConstraint =
|
||||
export interface DecodalPrimitiveDescriptor {
|
||||
$decodal: DecodalPrimitiveType;
|
||||
constraints?: DecodalConstraint[];
|
||||
default?: DecodalHostValue;
|
||||
default?: DecodalValue;
|
||||
}
|
||||
|
||||
export interface DecodalUnknownDescriptor {
|
||||
$decodal: 'Unknown';
|
||||
constraints?: DecodalConstraint[];
|
||||
default?: DecodalValue;
|
||||
}
|
||||
|
||||
export interface DecodalArrayDescriptor {
|
||||
$decodal: 'Array';
|
||||
item: DecodalHostValue;
|
||||
item: DecodalValue;
|
||||
constraints?: DecodalConstraint[];
|
||||
default?: DecodalHostValue;
|
||||
default?: DecodalValue;
|
||||
}
|
||||
|
||||
export interface DecodalMapDescriptor {
|
||||
$decodal: 'Map';
|
||||
value: DecodalHostValue;
|
||||
value: DecodalValue;
|
||||
constraints?: DecodalConstraint[];
|
||||
default?: DecodalHostValue;
|
||||
default?: DecodalValue;
|
||||
}
|
||||
|
||||
export interface DecodalAbstractDescriptor {
|
||||
$decodal: 'Abstract';
|
||||
export interface DecodalObjectDescriptor {
|
||||
$decodal: 'Object';
|
||||
fields: Record<string, DecodalValue>;
|
||||
rest: DecodalValue;
|
||||
}
|
||||
|
||||
export interface DecodalRangeDescriptor {
|
||||
$decodal: 'Range';
|
||||
constraints?: DecodalConstraint[];
|
||||
default?: DecodalHostValue;
|
||||
default?: DecodalValue;
|
||||
}
|
||||
|
||||
export type DecodalLoadedImport =
|
||||
| { kind: 'source'; key: string; name?: string; source: string }
|
||||
| { kind: 'value'; key: string; value: DecodalHostValue };
|
||||
| { kind: 'value'; key: string; value: DecodalValue };
|
||||
|
||||
export type DecodalImportCandidate =
|
||||
| string
|
||||
@@ -65,7 +80,7 @@ export type DecodalImportCandidate =
|
||||
|
||||
/** Host-owned environment shared by evaluation and language tooling. */
|
||||
export interface DecodalEnvironment {
|
||||
globals?: Record<string, DecodalHostValue>;
|
||||
globals?: Record<string, DecodalValue>;
|
||||
/** Synchronous import callback. Preload or cache asynchronous resources first. */
|
||||
loadImport?: (currentKey: string | null, specifier: string) => DecodalLoadedImport;
|
||||
/** Synchronous import completion callback. */
|
||||
@@ -73,7 +88,7 @@ export interface DecodalEnvironment {
|
||||
}
|
||||
`;
|
||||
if (!declarations.includes('export interface DecodalEnvironment')) {
|
||||
declarations = declarations.replace('/* eslint-disable */\n', `/* eslint-disable */\n${hostTypes}`);
|
||||
declarations = declarations.replace('/* eslint-disable */\n', `/* eslint-disable */\n${valueTypes}`);
|
||||
}
|
||||
declarations = declarations.replace(
|
||||
'constructor(options: any);',
|
||||
@@ -175,7 +190,11 @@ console.log(JSON.parse(completion));
|
||||
|
||||
\`globals\`, \`loadImport\`, and \`completeImport\` are host-owned. The package does not assume a filesystem or virtual project model. Import callbacks are synchronous; preload or cache remote content before evaluation.
|
||||
|
||||
Plain strings, numbers, booleans, arrays, and objects are concrete host values. Use \`{ $decodal: 'String' | 'Int' | 'Float' | 'Bool' }\` for primitive schemas, \`{ $decodal: 'Array', item: value }\` for array schemas, and \`{ $decodal: 'Map', value: value }\` for associative-array schemas. Descriptors also accept \`constraints\` and \`default\`.
|
||||
Plain strings, numbers, booleans, arrays, and objects are concrete values. Use \`{ $decodal: 'String' | 'Int' | 'Float' | 'Bool' }\` for primitive ranges, \`{ $decodal: 'Unknown' }\` for the top range, \`{ $decodal: 'Array', item: value }\` for array ranges, and \`{ $decodal: 'Map', value: value }\` for associative-array ranges. Primitive, unknown, array, and map descriptors also accept \`constraints\` and \`default\`.
|
||||
|
||||
Use \`{ $decodal: 'Range', constraints, default }\` when constructing a general range directly.
|
||||
|
||||
An object with named fields and an open rest range uses \`{ $decodal: 'Object', fields, rest }\`. The \`fields\` property is a record of values and \`rest\` is the range applied to every other field.
|
||||
|
||||
Methods return JSON strings so callers can handle diagnostics and successful results without depending on Rust data structures.
|
||||
`,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
const DECODAL_KEYWORDS = new Set(['let', 'in', 'fn', 'match', 'import', 'default']);
|
||||
const DECODAL_TYPES = new Set(['String', 'Int', 'Float', 'Bool']);
|
||||
const DECODAL_TYPES = new Set(['String', 'Int', 'Float', 'Bool', 'Unknown']);
|
||||
const DECODAL_LITERALS = new Set(['true', 'false']);
|
||||
|
||||
const HTML_ESCAPE = {
|
||||
@@ -41,7 +41,7 @@ export function highlightDecodalTokens(source, tokens) {
|
||||
|
||||
function tokenClass(kind, text) {
|
||||
if (['let', 'in', 'fn', 'match', 'import', 'default'].includes(kind)) return 'tok-keyword';
|
||||
if (kind === 'ident' && ['String', 'Int', 'Float', 'Bool'].includes(text)) return 'tok-type';
|
||||
if (kind === 'ident' && ['String', 'Int', 'Float', 'Bool', 'Unknown'].includes(text)) return 'tok-type';
|
||||
if (kind === 'ident') return '';
|
||||
if (['true', 'false'].includes(kind)) return 'tok-literal';
|
||||
if (['int', 'float'].includes(kind)) return 'tok-number';
|
||||
|
||||
@@ -22,6 +22,13 @@ test('injects one JavaScript host environment into evaluation and completion', a
|
||||
active: { $decodal: 'Bool', default: true },
|
||||
},
|
||||
},
|
||||
OpenConfig: {
|
||||
$decodal: 'Object',
|
||||
fields: {
|
||||
enabled: { $decodal: 'Bool', default: true },
|
||||
},
|
||||
rest: { $decodal: 'Unknown' },
|
||||
},
|
||||
},
|
||||
},
|
||||
loadImport(_currentKey, specifier) {
|
||||
@@ -46,11 +53,12 @@ test('injects one JavaScript host environment into evaluation and completion', a
|
||||
const evaluated = JSON.parse(service.evaluate(
|
||||
'main.dcdl',
|
||||
'main.dcdl',
|
||||
'let s = import "./schema.dcdl"; in { server = { port = 8080; } as s.Server; services = { api = { port = 8081; }; } as App.Services; enabled = App.enabled; post = import "./post.md" as { frontmatter = { draft = Bool; }; body = String; }; }',
|
||||
'let s = import "./schema.dcdl"; in { server = { port = 8080; } as s.Server; services = { api = { port = 8081; }; } as App.Services; open = { enabled = false; plugin = { name = "cache"; }; } as App.OpenConfig; enabled = App.enabled; post = import "./post.md" as { frontmatter = { draft = Bool; }; body = String; }; }',
|
||||
));
|
||||
assert.equal(evaluated.ok, true, evaluated.error);
|
||||
assert.match(evaluated.output, /"port": 8080/);
|
||||
assert.match(evaluated.output, /"active": true/);
|
||||
assert.match(evaluated.output, /"plugin": \{/);
|
||||
assert.match(evaluated.output, /"body": "# Hello"/);
|
||||
|
||||
const member = JSON.parse(service.complete('main.dcdl', 'App.en', 6, false));
|
||||
@@ -61,5 +69,9 @@ test('injects one JavaScript host environment into evaluation and completion', a
|
||||
assert.equal(imported.ok, true, imported.error);
|
||||
assert.ok(imported.completion.options.some((item) => item.label === './schema.dcdl'));
|
||||
|
||||
const unknown = JSON.parse(service.complete('main.dcdl', 'Unk', 3, false));
|
||||
assert.equal(unknown.ok, true, unknown.error);
|
||||
assert.ok(unknown.completion.options.some((item) => item.label === 'Unknown'));
|
||||
|
||||
service.free();
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user