Skip to content

Commit

Permalink
createType: support indexed access (#122)
Browse files Browse the repository at this point in the history
* Fix validateMapping and add test

* Work out type indexedAccess for getTypeKeys

* Add src-runtime/resolveType.js (with spec)

* add more resolveType tests

* Add createTypeFromKeyof

* Fix createTypeFromKeyof stuff

* Fix registerTypedef types

* Add src-runtime/createType.js and src-runtime/createTypeFromIndexedAccess.js with some repl/spec files
Other fixes

* Add src-runtime/validateIndexedAccess.js (only for `IndexedAccess` typedef right now)

* validate-mapping-indexed-access.mjs - just some special case typedef for testing

* Fix lint
  • Loading branch information
kungfooman authored Jan 17, 2024
1 parent 85de72c commit 8213566
Show file tree
Hide file tree
Showing 20 changed files with 430 additions and 10 deletions.
1 change: 1 addition & 0 deletions .eslintignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
*.repl.js
25 changes: 25 additions & 0 deletions src-runtime/createType.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import {resolveType } from "./resolveType.js";
import {createTypeFromIndexedAccess} from "./createTypeFromIndexedAccess.js";
import {createTypeFromKeyof } from "./createTypeFromKeyof.js";
import {createTypeFromMapping } from "./createTypeFromMapping.js";
/**
* @param {string|import('./validateType.mjs').Type} expect - The supposed type information of said value.
* @param {console["warn"]} warn - Function to warn with.
* @returns {import('./validateType.mjs').Type|undefined} - New type that can be used for validatoin
*/
function createType(expect, warn) {
const mapping = resolveType(expect, 'mapping', warn);
if (mapping) {
return createTypeFromMapping(mapping, warn);
}
const indexedAccess = resolveType(expect, 'indexedAccess', warn);
if (indexedAccess) {
return createTypeFromIndexedAccess(indexedAccess, warn);
}
const keyof = resolveType(expect, 'keyof', warn);
if (keyof) {
return createTypeFromKeyof(keyof, warn);
}
return expect;
}
export {createType};
62 changes: 62 additions & 0 deletions src-runtime/createType.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import {createType } from './createType.js';
import {registerTypedef} from './registerTypedef.mjs';
function prepare() {
/**
* @typedef {{a: 'aa', b: 'bb', c: 'cc'}} Obj
* @typedef {Obj[keyof Obj]} ObjValues
* @typedef {ObjValues} ObjValuesTypedef
* @typedef {ObjValuesTypedef} ObjValuesTypedef2
*/
registerTypedef('Obj', {
"type": "object",
"properties": {
"a": "'aa'",
"b": "\"bb\"",
"c": "'cc'"
}
});
registerTypedef('ObjValues', {
"type": "indexedAccess",
"index": {
"type": "keyof",
"argument": "Obj"
},
"object": "Obj"
});
registerTypedef('ObjValuesTypedef', "ObjValues");
registerTypedef('ObjValuesTypedef2', "ObjValuesTypedef");
}
function test1() {
prepare();
/** @type {import('./validateUnion.mjs').Union} */
// @ts-ignore
const t = createType("ObjValuesTypedef2", console.warn);
if (!t) {
console.warn('createType: t is not defined.');
return false;
}
if (t.type !== 'union') {
console.warn('t is not an union.', t);
return false;
}
if (t.members.length !== 3) {
console.warn('t should have three union members.');
return false;
}
if (t.members[0] !== 'aa') {
console.warn(`Value for t.members[0] should be 'aa', but got '${t.members[0]}'.`);
return false;
}
if (t.members[1] !== 'bb') {
console.warn(`Value for t.members[1] should be 'aa', but got '${t.members[1]}'.`);
return false;
}
if (t.members[2] !== 'cc') {
console.warn(`Value for t.members[2] should be 'aa', but got '${t.members[2]}'.`);
return false;
}
return true;
}
export const tests = [
test1,
];
47 changes: 47 additions & 0 deletions src-runtime/createTypeFromIndexedAccess.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
//import {createType } from "./createType.js";
import {resolveType } from "./resolveType.js";
//import {replaceType } from "./replaceType.js";
import {getTypeKeys } from "./validateKeyof.mjs";
/**
* @param {import('./validateIndexedAccess.js').IndexedAccess} expect - The supposed type information of said value.
* @param {console["warn"]} warn - Function to warn with.
* @returns {import('./validateType.mjs').TypeObject|undefined} - New type that can be used for validation.
*/
function createTypeFromIndexedAccess(expect, warn) {
const {object, index} = expect;
const resolvedObject = resolveType(object, 'object', warn);
if (resolvedObject) {
// const indexType = createType(index, warn);
const indexKeys = getTypeKeys(index, warn);
//console.log("createTypeFromIndexedAccess", {resolvedObject, object, index, indexType, indexKeys});
if (!indexKeys) {
warn('createTypeFromIndexedAccess: missing indexKeys');
return;
}
/** @type {import('./validateType.mjs').Type[]} */
const members = [];
for (const indexKey of indexKeys) {
let prop = resolvedObject.properties[indexKey];
if (!prop) {
console.warn(`Missing prop for ${indexKey}`, {indexKey});
}
//console.log("asd", indexKey, prop);
if (typeof prop === 'string') {
// Remove ' from string literal
if (prop[0] === "'" && prop[prop.length - 1] === "'") {
prop = prop.slice(1, -1);
}
// Remove " from string literal
if (prop[0] === '"' && prop[prop.length - 1] === '"') {
prop = prop.slice(1, -1);
}
}
members.push(prop);
//const cloneResult = structuredClone(result);
//replaceType(cloneResult, element, typeKey, warn);
//properties[typeKey] = cloneResult;
}
return {type: 'union', members, optional: false};
}
}
export {createTypeFromIndexedAccess};
32 changes: 32 additions & 0 deletions src-runtime/createTypeFromIndexedAccess.repl.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
/**
* @typedef {{a: 'aa', b: "bb", c: 'cc'}} Obj
* @typedef {{a: 0, b: '', c: undefined, d: null}} ObjTestcase
* @typedef {Obj[keyof Obj]} ObjValues
* @typedef {ObjTestcase[keyof ObjTestcase]} ObjValuesTestcase - Should be "" | 0 | null | undefined
*/
const t = createType('ObjValues', console.log);
console.log(t);
/**
* @param {ObjTest1} x - First argument.
* @returns {ObjTest1} - Return value.
*/
function identity1(x) {
return x;
}
identity1({
aa: {testkey: 'aa'},
bb: {testkey: 'bb'},
cc: {testkey: 'cc'},
});
/**
* @param {ObjTest2} x - First argument.
* @returns {ObjTest2} - Return value.
*/
function identity2(x) {
return x;
}
identity2({
aa: {testkey: 'aa'},
bb: {testkey: 'bb'},
cc: {testkey: 'cc'},
});
18 changes: 18 additions & 0 deletions src-runtime/createTypeFromKeyof.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import {resolveType} from "./resolveType.js";
/**
* @todo reuse this function in createTypeFromMapping/getTypeKeys
* @param {import('./validateKeyof.mjs').Keyof} expect - The supposed type information of said value.
* @param {console["warn"]} warn - Function to warn with.
* @returns {import('./validateType.mjs').TypeObject|undefined} - New type that can be used for validatoin
*/
function createTypeFromKeyof(expect, warn) {
const {argument} = expect;
const object = resolveType(argument, 'object', warn);
if (object) {
const optional = false;
const members = Object.keys(object.properties);
return {type: 'union', optional, members};
}
warn('createTypeFromKeyof: unhandled type.', {expect});
}
export {createTypeFromKeyof};
9 changes: 9 additions & 0 deletions src-runtime/createTypeFromKeyof.repl.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
/**
* @typedef {{a: 'aa', b: 'bb', c: 'cc'}} Obj
* @typedef {keyof Obj} ObjKeys
* @typedef {ObjKeys} ObjKeysTypedef
*/
console.log('Obj', typedefs.Obj);
console.log('ObjKeys', typedefs.ObjKeys);
const newType = createType("ObjKeysTypedef", console.warn);
console.log('newType', newType);
61 changes: 61 additions & 0 deletions src-runtime/createTypeFromKeyof.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import {createType } from './createType.js';
import {registerTypedef} from './registerTypedef.mjs';
function prepare() {
/**
* @template T
* @typedef {{[K in keyof T]: T[K] extends object ? Unpack<T[K]> : T[K]}} Unpack
*/
/**
* @typedef {{a: 'aa', b: 'bb', c: 'cc'}} Obj
* @typedef {keyof Obj} ObjKeys
* @typedef {ObjKeys} ObjKeysTypedef
* @typedef {Unpack<ObjKeys>} ObjKeysUnpacked
*/
registerTypedef('Obj', {
"type": "object",
"properties": {
"a": "'aa'",
"b": "'bb'",
"c": "'cc'"
}
});
registerTypedef('ObjKeys', {
"type": "keyof",
"argument": "Obj"
});
registerTypedef('ObjKeysTypedef', "ObjKeys");
}
function test1() {
prepare();
/** @type {import('./validateUnion.mjs').Union} */
const newType = createType('ObjKeysTypedef', console.warn);
if (!newType) {
console.warn('t is not defined.');
return false;
}
const {type, members} = newType;
if (type !== 'union') {
console.warn('t is not an union.');
return false;
}
if (members.length !== 3) {
console.warn('t should have three union members.');
return false;
}
if (members[0] !== 'a') {
console.warn("t.elements[0] should be 'a'");
return false;
}
if (members[1] !== 'b') {
console.warn("t.elements[1] should be 'b'");
return false;
}
if (members[2] !== 'c') {
console.warn("t.elements[2] should be 'c'");
return false;
}
return true;
}
export const tests = [
test1,
];
9 changes: 7 additions & 2 deletions src-runtime/createTypeFromMapping.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,16 @@
import {replaceType } from "./replaceType.js";
import {getTypeKeys } from "./validateKeyof.mjs";
import {typedefs } from "./registerTypedef.mjs";
/**
* @param {import('./validateMapping.mjs').Mapping} expect - The supposed type information of said value.
* @param {string|import('./validateMapping.mjs').Mapping} expect - The supposed type information of said value.
* @param {console["warn"]} warn - Function to warn with.
* @returns {import('./validateType.mjs').TypeObject|undefined} - New type that can be used for validatoin
* @returns {import('./validateType.mjs').TypeObject|undefined} - New type that can be used for validation.
*/
function createTypeFromMapping(expect, warn) {
/** @todo some kind of resolveType(expect, 'mapping', depth = 0) function */
if (typeof expect === 'string' && typedefs[expect]) {
expect = typedefs[expect];
}
const {iterable, element, result} = expect;
const typeKeys = getTypeKeys(iterable, warn);
if (!typeKeys) {
Expand Down
14 changes: 14 additions & 0 deletions src-runtime/getTypeKeys.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,20 @@ function getTypeKeys(expect, warn) {
const {argument} = expect;
//console.log("want key for", argument);
return getTypeKeys(argument, warn);
} else if (type === 'indexedAccess') {
const {index} = expect;
let {object} = expect;
// todo replace with resolveType
if (typeof object === 'string' && typedefs[object]) {
object = typedefs[object];
}
const indexKeys = getTypeKeys(index, warn);
const arr = [];
for (const indexKey of indexKeys) {
arr.push(object.properties[indexKey]);
}
// console.log({object, index, indexKeys, arr});
return arr;
}
warn(`Couldn't get keys for type`, expect);
}
Expand Down
4 changes: 4 additions & 0 deletions src-runtime/index.mjs
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
export * from './assertMode.js';
export * from './createDiv.mjs';
export * from './createType.js';
export * from './createTypeFromIndexedAccess.js';
export * from './createTypeFromKeyof.js';
export * from './createTypeFromMapping.js';
export * from './customTypes.mjs';
export * from './customValidations.mjs';
Expand All @@ -14,6 +17,7 @@ export * from './registerClass.mjs';
export * from './registerTypedef.mjs';
export * from './registerVariable.js';
export * from './replaceType.js';
export * from './resolveType.js';
export * from './validateArray.mjs';
export * from './validateDivision.mjs';
export * from './validateIntersection.js';
Expand Down
4 changes: 2 additions & 2 deletions src-runtime/registerTypedef.mjs
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
/**
* @type {Record<string, object>}
* @type {Record<string, string|object>}
*/
const typedefs = {};
/**
* @param {string} name - Name of typedef.
* @param {Record<string, object>} typedef - The parsed typedef from source.
* @param {string|object} typedef - The parsed typedef from source.
*/
function registerTypedef(name, typedef) {
// If it already exists as object but now it's a string
Expand Down
23 changes: 23 additions & 0 deletions src-runtime/resolveType.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import {typedefs} from "./registerTypedef.mjs";
/**
* @param {any} type - The type.
* @param {string} as - What to resolve as.
* @param {console["warn"]} warn - Function to warn with.
* @returns {any} - Resolved type.
*/
function resolveType(type, as, warn) {
for (let depth = 0; depth < 20; depth++) {
if (typeof type === 'string' && typedefs[type]) {
type = typedefs[type];
}
if (type.type) {
if (type.type === as) {
return type;
}
// If we already resolved as a structured type, it can't be a typedef any longer.
return;
}
}
warn('resolveType: exceeded allowed depth');
}
export {resolveType};
Loading

0 comments on commit 8213566

Please sign in to comment.