/ src / deserialize.ts
deserialize.ts
  1  import { SchemaError } from "./errors/SchemaError";
  2  import { getModelMetadata } from "./metadata";
  3  import { SchemaType } from "./schema/type";
  4  
  5  interface CompiledField {
  6    defaultValue?: (() => any) | any;
  7    deserializer?: (value: any, self: any) => any;
  8    key: string;
  9    rename?: string;
 10    schema: SchemaType;
 11  }
 12  
 13  interface CompiledModel {
 14    deserializer: (data: any) => any;
 15    fields: Array<CompiledField>;
 16    name: string;
 17    prototype: unknown;
 18  }
 19  
 20  type Ctor<T> = new (...args: any[]) => T;
 21  
 22  const cache = new WeakMap<Ctor<any>, CompiledModel>();
 23  
 24  interface Deserializer {
 25    body: string;
 26    helpers: {
 27      arrays: Record<string, unknown>;
 28      defaults: Record<string, unknown>;
 29      deserializers: Record<string, unknown>;
 30      references: Record<string, unknown>;
 31      SchemaError: typeof SchemaError;
 32    };
 33  }
 34  
 35  /**
 36   * Deserialize an object {@link data} into your a given {@link Model}.
 37   *
 38   * @param Model Destination model you want to deserialize to.
 39   * @param data Original object, could be anything as long as it matches your model schema.
 40   *
 41   * @example
 42   * class MyModel {
 43   *   \@rename("who")
 44   *   hello = t.string()
 45   * }
 46   *
 47   * const model = deserialize(MyModel, {
 48   *   who: "world"
 49   * });
 50   *
 51   * console.log(model.hello) // "world"
 52   */
 53  export function deserialize<T extends new (...args: any[]) => any>(Model: T, data: any): InstanceType<T> {
 54    return getCompiledModel(Model).deserializer(data);
 55  }
 56  
 57  const decodeArray = (
 58    Model: CompiledModel,
 59    field: string,
 60    value: any,
 61    schema: SchemaType
 62  ): any => {
 63    if (!Array.isArray(value)) {
 64      throw new SchemaError(Model.name, field, `expected array but got "${typeof value}"`);
 65    }
 66  
 67    return value.map((item) => {
 68      if (schema.optional && item == null) {
 69        return null;
 70      }
 71  
 72      if (schema.array) {
 73        return decodeArray(Model, field, item, schema.array);
 74      }
 75      else if (schema.reference) {
 76        return deserialize(schema.reference, item);
 77      }
 78  
 79      return item;
 80    });
 81  };
 82  
 83  function generateDeserializerFunction(compiled: CompiledModel): Deserializer {
 84    // mod = model
 85    // proto = prototype
 86    // _     = helpers
 87    // $     = data
 88    // $[i]  = model/data key/value
 89    //
 90    // We're doing this to prevent conflicting names.
 91  
 92    let body = "const mod=Object.create(proto);";
 93  
 94    const _ = {
 95      arrays: {} as Record<string, any>,
 96      defaults: {} as Record<string, any>,
 97      deserializers: {} as Record<string, any>,
 98      references: {} as Record<string, any>,
 99      SchemaError
100    };
101  
102    for (let i = 0; i < compiled.fields.length; i++) {
103      const {
104        defaultValue,
105        deserializer,
106        key: unsafeFieldKey,
107        rename: unsafeRenameFieldKey,
108        schema
109      } = compiled.fields[i];
110  
111      const varn = `$${i}`;
112      const safeFieldKey = JSON.stringify(unsafeFieldKey);
113      body += `let ${varn}=$[${unsafeRenameFieldKey ? JSON.stringify(unsafeRenameFieldKey) : safeFieldKey}];`;
114  
115      if (defaultValue !== void 0) {
116        _.defaults[varn] = defaultValue;
117  
118        if (typeof defaultValue === "function") {
119          body += `${varn}??=_.defaults.${varn}();`;
120        }
121        else {
122          body += `${varn}??=_.defaults.${varn};`;
123        }
124  
125        body += `if(${varn}===null)throw new _.SchemaError("${compiled.name}",${safeFieldKey},"default value cannot be null");`;
126  
127        if (schema.typeof) {
128          body += `if(typeof ${varn}!=="${schema.typeof}")throw new _.SchemaError("${compiled.name}",${safeFieldKey},"default value has incorrect type, got \\""+typeof ${varn}+"\\" and expected \\"${schema.typeof}\\"");`;
129        }
130  
131        if (schema.instanceof) {
132          _.defaults[`${varn}$1`] = schema.instanceof;
133          body += `if(!(${varn} instanceof _.defaults["${varn}$1"]))throw new _.SchemaError("${compiled.name}",${safeFieldKey},"default value is not an instance of \\"${schema.instanceof.name}\\"");`;
134        }
135  
136        if (schema.enum) {
137          _.defaults[`${varn}$2`] = Object.values(schema.enum);
138          body += `if(!_.defaults.${varn}$2.includes(${varn}))throw new _.SchemaError("${compiled.name}",${safeFieldKey},\`default value (\${safe}) does not match any value of provided enum\`);`;
139        }
140  
141        if (schema.reference) {
142          body += `throw new _.SchemaError("${compiled.name}",${safeFieldKey},"default value is not allowed on reference fields (${safeFieldKey})");`;
143        }
144      }
145      else if (!schema.optional) {
146        body += `if(${varn}==null)throw new _.SchemaError("${compiled.name}",${safeFieldKey},\`not optional but got "\${${varn}}"\`);`;
147      }
148      else {
149        body += `if(${varn}==null)${varn}=null;`;
150      }
151  
152      // Handle transformations (only for non-null values)
153      if (deserializer) {
154        _.deserializers[varn] = deserializer;
155        body += `if(${varn}!=null)${varn}=_.deserializers.${varn}(${varn},mod);`;
156      }
157      else if (schema.array) {
158        _.arrays[varn] = (value: any) =>
159          decodeArray(compiled, unsafeFieldKey, value, schema.array!);
160        body += `if(${varn}!=null)${varn}=_.arrays.${varn}(${varn});`;
161      }
162      else if (schema.reference) {
163        _.references[varn] = (value: any) =>
164          deserialize(schema.reference!, value);
165        body += `if(${varn}!=null)${varn}=_.references.${varn}(${varn});`;
166      }
167  
168      body += `mod[${safeFieldKey}]=${varn};`;
169    }
170  
171    body += "return mod";
172    return { body, helpers: _ };
173  }
174  
175  function getCompiledModel<T extends Ctor<any>>(Model: T): CompiledModel {
176    let compiled = cache.get(Model);
177    if (compiled?.deserializer) return compiled;
178  
179    if (!compiled) {
180      const instance = new Model();
181      const metadata = new Map(getModelMetadata(instance).map((m) => [m.key, m] as const));
182  
183      const fields: Array<CompiledField> = [];
184      for (const [key, schema] of Object.entries(instance)) {
185        if (!(schema instanceof SchemaType)) continue;
186  
187        const info = metadata.get(key);
188        fields.push({
189          defaultValue: info?.defaultValue,
190          deserializer: info?.deserializer,
191          key,
192          rename: info?.rename,
193          schema
194        });
195      }
196  
197      compiled = {
198        fields,
199        name: Model.name,
200        prototype: Model.prototype
201      } as CompiledModel;
202    }
203  
204    const { body, helpers } = generateDeserializerFunction(compiled);
205    const fn = new Function("$", "proto", "_", body);
206    compiled.deserializer = (data: unknown) => fn(data, compiled.prototype, helpers);
207    cache.set(Model, compiled);
208    return compiled;
209  }