/* eslint-disable no-use-before-define */ import { Interface, ToInterface, Cast, ToConstructor, HasTail, Head, Tail, ListSpec, ToType, DefaultFieldConstructor } from './Types'; import { Doc, FieldType } from './Doc'; import { ObjectField } from './ObjectField'; import { RefField } from './RefField'; import { SelfProxy } from './DocSymbols'; import { List } from './List'; type AllToInterface = { 1: ToInterface> & AllToInterface>; 0: ToInterface>; }[HasTail extends true ? 1 : 0]; export const emptySchema = createSchema({}); export const Document = makeInterface(emptySchema); export type Document = makeInterface<[typeof emptySchema]>; export interface InterfaceFunc { (docs: Doc[]): makeInterface[]; (): makeInterface; (doc: Doc): makeInterface; } export type makeInterface = AllToInterface & Doc & { proto: Doc | undefined }; export function makeInterface(...schemas: T): InterfaceFunc { const schema: Interface = {}; for (const s of schemas) { for (const key in s) { schema[key] = s[key]; } } const proto = new Proxy( {}, { get(target: unknown, prop, receiver) { const field = receiver.doc?.[prop]; if (prop in schema) { // eslint-disable-next-line @typescript-eslint/no-explicit-any const desc = prop === 'proto' ? Doc : (schema as any)[prop]; // bcz: proto doesn't appear in schemas ... maybe it should? if (typeof desc === 'object' && 'defaultVal' in desc && 'type' in desc) { // defaultSpec return Cast(field, desc.type, desc.defaultVal); } // eslint-disable-next-line no-prototype-builtins if (typeof desc === 'function' && !ObjectField.isPrototypeOf(desc) && !RefField.isPrototypeOf(desc)) { const doc = Cast(field, Doc); if (doc === undefined) { return undefined; } if (doc instanceof Doc) { return desc(doc); } return doc.then(d => d && desc(d)); } return Cast(field, desc); } return field; }, set(target: unknown, prop, value, receiver) { receiver.doc && (receiver.doc[prop] = value); // receiver.doc may be undefined as the result of a change in acls return true; }, } ); // !(doc instanceof Doc) && (throw new Error("Currently wrapping a schema in another schema isn't supported")); const fn = (doc: Doc) => Object.create(proto, { doc: { value: doc[SelfProxy], writable: false } }); return ((doc?: Doc | Doc[]) => (doc instanceof List ? doc : undefined)?.map?.(fn) ?? fn((doc as Doc) ?? new Doc())) as InterfaceFunc; } export type makeStrictInterface = Partial>; export function makeStrictInterface(schema: T): (doc: Doc) => makeStrictInterface { const proto = {}; for (const key in schema) { const type = schema[key]; Object.defineProperty(proto, key, { get() { return Cast(this.__doc[key], type as never); }, set(setValue) { const value = Cast(setValue, type as never); if (value !== undefined) { this.__doc[key] = value; return; } throw new TypeError('Expected type ' + type); }, }); } return function (doc: unknown) { if (!(doc instanceof Doc)) { throw new Error("Currently wrapping a schema in another schema isn't supported"); } const obj = Object.create(proto); obj.__doc = doc; return obj; }; } // eslint-disable-next-line @typescript-eslint/no-unused-vars export function createSchema(schema: T): T & { proto: ToConstructor } { return undefined as never; // (schema as any).proto = Doc; // return schema as any; } export function listSpec>(type: U): ListSpec> { return { List: type as never }; // TODO Types } export function defaultSpec>(type: T, defaultVal: ToType): DefaultFieldConstructor> { return { type: type as never, defaultVal, }; }