paritytech / capi

[WIP] A framework for crafting interactions with Substrate chains
https://docs.capi.dev
Apache License 2.0
104 stars 10 forks source link

Higher-level Visitor for JS-aware Types #185

Closed harrysolovay closed 2 years ago

harrysolovay commented 2 years ago

Something that could be used both by codec derivation and (TODO) typegen:

import * as $ from "../_deps/scale.ts";
import * as M from "./Metadata.ts";
import { Visitor } from "./TyVisitor.ts";

export const $null = $.dummy(null);

export type DeriveCodec = (tyI: number) => $.Codec<any>;

export class DeriveCodecVisitor extends Visitor<$.Codec<any>> {
  emptyTuple = $null;
  unit = $null;
  emptyStruct = $null;
  emptyUnion = $.never as unknown as $.Codec<unknown>;
  compact = $.compact;
  bitSequence = $.bitSequence;
  u8a = $.uint8array;

  singleElTuple = (field: number) => {
    return this.visit(field);
  };

  multipleElTuple = (fields: number[]) => {
    return $.tuple(...fields.map(this.visit));
  };

  tupleStruct = (ty: M.Ty & M.StructTyDef) => {
    return $.tuple(...ty.fields.map((field) => this.visit(field.ty)));
  };

  objectStruct = (ty: M.Ty & M.StructTyDef) => {
    return $.object(...ty.fields.map((field): $.Field => [field.name!, this.visit(field.ty)]));
  };

  option = (ty: M.Ty & M.UnionTyDef) => {
    return $.option(this.visit(ty.params[0]!.ty!));
  };

  result = (ty: M.Ty & M.UnionTyDef) => {
    return $.result(
      this.visit(ty.params[0]!.ty!),
      $.instance(ChainError, ["value", this.visit(ty.params[1]!.ty!)]),
    );
  };

  allMembersEmptyUnionMember = (member: M.UnionTyDefMember) => {
    return $.dummy(member.name);
  };

  emptyUnionMember = (member: M.UnionTyDefMember) => {
    return $.dummy({ type: member.name });
  };

  tupleUnionMember = (member: M.UnionTyDefMember) => {
    const $value = this.tupleFromFields(member.fields.map((f) => f.ty));
    return $.transform(
      $value,
      ({ value }: { value: unknown }) => value,
      (value) => ({ type: member.name, value }),
    );
  };

  objectUnionMember = (member: M.UnionTyDefMember) => {
    const memberFields = member.fields.map((field, i) => {
      return [
        field.name || i,
        $.deferred(() => this.visit(field.ty)),
      ] as [
        string,
        $.Codec<unknown>,
      ];
    });
    return $.object(["type", $.dummy(member.name)], ...memberFields);
  };

  union = (ty: M.Ty & M.UnionTyDef, ...members: $.Codec<unknown>[]) => {
    const memberIByTag: Record<string, number> = {};
    const memberIByDiscriminant: Record<number, number> = {};
    ty.members.forEach((member, i) => {
      memberIByTag[member.name] = member.i;
      memberIByDiscriminant[member.i] = i;
    });
    return union(
      (member) => {
        const tag = typeof member === "string" ? member : (member as any).type;
        const discriminant = memberIByTag[tag];
        if (discriminant === undefined) {
          throw new Error(
            `Invalid tag ${JSON.stringify(tag)}, expected one of ${
              JSON.stringify(Object.keys(memberIByTag))
            }`,
          );
        }
        return discriminant;
      },
      (discriminant) => {
        const i = memberIByDiscriminant[discriminant];
        if (i === undefined) {
          throw new Error(
            `Invalid discriminant ${discriminant}, expected one of ${
              JSON.stringify(Object.keys(memberIByDiscriminant))
            }`,
          );
        }
        return i;
      },
      ...members,
    ) as unknown as $.Codec<any>;
  };

  array = (ty: M.Ty & M.SequenceTyDef) => {
    return $.array(this.visit(ty.typeParam));
  };

  sizedU8a = (len: number) => {
    return $.sizedUint8array(len);
  };

  sizedArray = (ty: M.Ty & M.SizedArrayTyDef) => {
    return $.sizedArray(this.visit(ty.typeParam), ty.len);
  };

  cycle = (tyI: number) => {
    return $.deferred(() => this.visit(tyI));
  };

  getPrimitive = (ty: M.Ty & M.PrimitiveTyDef) => {
    if (ty.kind === "char") return $.str;
    return $[ty.kind];
  };
}

export function DeriveCodec(metadata: M.Metadata) {
  return new DeriveCodecVisitor(metadata).visit;
}

export class ChainError extends Error {
  constructor(readonly value: unknown) {
    super();
  }
}

type NativeUnion<MemberCodecs extends $.Codec<any>[]> = $.Native<MemberCodecs[number]>;

// TODO: get rid of this upon fixing in SCALE impl
function union<Members extends $.Codec<any>[]>(
  discriminate: (value: NativeUnion<Members>) => number,
  getIndexOfDiscriminant: (discriminant: number) => number,
  ...members: [...Members]
): $.Codec<NativeUnion<Members>> {
  return $.createCodec({
    _metadata: [union, discriminate, getIndexOfDiscriminant, ...members],
    _staticSize: 1 + Math.max(...members.map((x) => x._staticSize)),
    _encode(buffer, value) {
      const discriminant = discriminate(value);
      buffer.array[buffer.index++] = discriminant;
      const $member = members[discriminant]!;
      $member._encode(buffer, value);
    },
    _decode(buffer) {
      const discriminant = buffer.array[buffer.index++]!;
      const indexOfDiscriminant = getIndexOfDiscriminant(discriminant);
      const $member = members[indexOfDiscriminant];
      if (!$member) {
        throw new Error(`No such member codec matching the discriminant \`${discriminant}\``);
      }
      return $member._decode(buffer);
    },
  });
}
tjjfvi commented 2 years ago

Fixed by #195