From f39814048dcad170f07a7800557d21654e68e2f9 Mon Sep 17 00:00:00 2001 From: Irakli Gozalishvili Date: Mon, 3 Apr 2023 10:33:35 -0700 Subject: [PATCH] First draft of IPLD based schema --- packages/schema/package.json | 107 +++++++++++++ packages/schema/src/schema.ts | 280 ++++++++++++++++++++++++++++++++++ packages/schema/tsconfig.json | 103 +++++++++++++ 3 files changed, 490 insertions(+) create mode 100644 packages/schema/package.json create mode 100644 packages/schema/src/schema.ts create mode 100644 packages/schema/tsconfig.json diff --git a/packages/schema/package.json b/packages/schema/package.json new file mode 100644 index 00000000..cce2371f --- /dev/null +++ b/packages/schema/package.json @@ -0,0 +1,107 @@ +{ + "name": "@ucanto/schema", + "description": "ucanto schema", + "version": "5.2.0", + "keywords": [ + "UCAN", + "RPC", + "IPLD", + "JWT", + "multicodec", + "codec", + "invocation" + ], + "files": [ + "src", + "dist/src" + ], + "repository": { + "type": "git", + "url": "https://github.com/web3-storage/ucanto.git" + }, + "homepage": "https://github.com/web3-storage/ucanto", + "scripts": { + "test:web": "playwright-test test/*.spec.js --cov && nyc report", + "test:node": "c8 --check-coverage --branches 100 --functions 100 --lines 100 mocha test/*.spec.js", + "test": "npm run test:node", + "coverage": "c8 --reporter=html mocha test/*.spec.js && npm_config_yes=true npx st -d coverage -p 8080", + "check": "tsc --build", + "build": "tsc --build" + }, + "dependencies": { + "@ipld/car": "^5.1.0", + "@ipld/dag-cbor": "^9.0.0", + "@ipld/dag-ucan": "^3.3.2", + "@ucanto/interface": "workspace:^", + "multiformats": "^11.0.0" + }, + "devDependencies": { + "@types/chai": "^4.3.3", + "@types/mocha": "^10.0.1", + "@ucanto/principal": "workspace:^", + "c8": "^7.13.0", + "chai": "^4.3.6", + "mocha": "^10.1.0", + "nyc": "^15.1.0", + "playwright-test": "^8.2.0", + "typescript": "^4.9.5" + }, + "type": "module", + "main": "src/lib.js", + "types": "./dist/src/lib.d.ts", + "typesVersions": { + "*": { + "*": [ + "dist/src/*" + ], + "dist/src/lib.d.ts": [ + "dist/src/lib.d.ts" + ] + } + }, + "exports": { + ".": { + "types": "./dist/src/lib.d.ts", + "import": "./src/lib.js" + }, + "./src/lib.js": { + "types": "./dist/src/lib.d.ts", + "import": "./src/lib.js" + }, + "./link": { + "types": "./dist/src/link.d.ts", + "import": "./src/link.js" + }, + "./delegation": { + "types": "./dist/src/delegation.d.ts", + "import": "./src/delegation.js" + }, + "./receipt": { + "types": "./dist/src/receipt.d.ts", + "import": "./src/receipt.js" + }, + "./cbor": { + "types": "./dist/src/cbor.d.ts", + "import": "./src/cbor.js" + }, + "./car": { + "types": "./dist/src/car.d.ts", + "import": "./src/car.js" + }, + "./dag": { + "types": "./dist/src/dag.d.ts", + "import": "./src/dag.js" + }, + "./result": { + "types": "./dist/src/result.d.ts", + "import": "./src/result.js" + } + }, + "c8": { + "exclude": [ + "test/**", + "dist/**" + ] + }, + "license": "(Apache-2.0 AND MIT)" +} diff --git a/packages/schema/src/schema.ts b/packages/schema/src/schema.ts new file mode 100644 index 00000000..45b2f890 --- /dev/null +++ b/packages/schema/src/schema.ts @@ -0,0 +1,280 @@ +declare function struct(schema: T): StructSchema + +export interface Unit {} + +declare function string(): Schema<{ string: Unit }> +declare function boolean(): Schema<{ boolean: Unit }> +declare function int(): Schema<{ int: Unit }> +declare function float(): Schema<{ float: Unit }> +declare function bytes(): Schema<{ bytes: Unit }> +declare function unit(): Schema<{ unit: Unit }> + +type SchemaType = Variant<{ + string: Unit + boolean: Unit + int: Unit + float: Unit + unit: Unit + bytes: Unit + struct: StructType> +}> + +const Point = struct({ + x: int().optional().rename('hello'), + y: int(), +}) + +Point.__type.y + +const PointStr = Point.representation.stringPairs({ + innerDelim: '=', + entryDelim: '&', +}) + +PointStr.__type +PointStr.__representation + +const PointList = Point.representation.list() + +interface StructFields { + [key: string]: Field +} + +interface Field< + T extends SchemaType = SchemaType, + Options extends FieldOptions> = {} +> { + type: T + field: Options + + rename(name: Name): Field +} + +interface FieldOptions { + optional?: boolean + nullable?: boolean + rename?: string + implicit?: T +} + +interface Schema { + type: Type + field: {} + + conforms(value: unknown): value is Infer + + decode(data: Uint8Array): Infer + + // implicit>( + // value: Implicit + // ): ImplicitField + optional(): Field + rename(name: Name): Field + + __type: Infer +} + +interface StructSchema< + T extends StructFields, + R extends StructRepresentation = { + map: DeriveStructMapRepresentation + } +> extends Schema<{ struct: StructType }> { + representation: { + stringPairs(options: { + innerDelim: I + entryDelim: E + }): StructSchema + + list(): StructSchema + } + + __representation: R +} + +type InferStructField = T extends Field ? Infer : never + +interface StructType< + Fields extends StructFields, + Representation extends StructRepresentation = { + map: DeriveStructMapRepresentation + } +> { + fields: Fields + representation: Representation +} + +type StructRepresentation = Variant<{ + map: StructRepresentationMap + stringPairs: StructRepresentationStringPairs + list: StructRepresentationList +}> + +interface StructRepresentationMap { + fields?: { + [K in keyof Fields]?: FieldDetails + } +} + +interface StructRepresentationStringPairs< + I extends string = string, + E extends string = string +> { + innerDelim: I + entryDelim: E +} + +interface StructRepresentationList {} + +interface FieldDetails { + rename?: string + implicit?: {} +} + +type DeriveStructMapRepresentation = + // OmitUnitFields<{ + // fields?: OmitUnitFields<{ + // [K in keyof Fields]: (Fields[K]['fieldName'] extends string + // ? { rename: Fields[K]['fieldName'] } + // : {}) & + // (Fields[K]['implicitValue'] extends {} + // ? { implicit: Fields[K]['implicitValue'] } + // : {}) + // }> + // }> + { + fields?: { + [K in keyof Fields]: Fields[K] extends Field + ? Options + : never + } + } + +type OmitUnitFields> = { + [K in keyof T]: keyof T[K] extends never ? never : T[K] +} + +// INFER + +type Infer = T extends { type: { string: Unit } } + ? string + : T extends { boolean: Unit } + ? boolean + : T extends { int: Unit } + ? number + : T extends { float: Unit } + ? number + : T extends { unit: Unit } + ? Unit + : T extends { bytes: Unit } + ? Uint8Array + : T extends { struct: StructType } + ? InferStruct + : never + +type InferStruct< + T extends StructFields, + R extends StructRepresentation +> = R extends { map: infer U } + ? InferStructMap + : R extends { stringPairs: infer U } + ? InferStructStringPairs + : R extends { list: infer U } + ? InferStructList + : never + +type InferStructMap< + T extends StructFields, + R extends StructRepresentation +> = { + [K in keyof T]: InferStructField +} + +type InferStructStringPairs< + T extends StructFields, + R extends StructRepresentationStringPairs +> = JoinTulpe< + UnionToTuple< + { + [K in keyof T]: `${K & string}${R['innerDelim']}${InferStructField & + (string | number | boolean | null)}` + }[keyof T] + >, + R['entryDelim'] +> + +type InferStructList = { + [K in keyof T]: [K, InferStructField] +}[keyof T] + +type UnionToTuple = ( + (T extends any ? (t: T) => T : never) extends infer U + ? (U extends any ? (u: U) => any : never) extends (v: infer V) => any + ? V + : never + : never +) extends (_: any) => infer W + ? [...UnionToTuple>, W] + : [] + +type JoinTulpe = T extends [] + ? '' + : T extends [infer First] + ? First + : T extends [infer First, infer Second, ...infer Rest] + ? `${First & string}${delimiter}${JoinTulpe<[Second, ...Rest], delimiter> & + string}` + : never + +// UTILS + +/** + * Defines result type as per invocation spec + * + * @see https://github.com/ucan-wg/invocation/#6-result + */ + +export type Result = Variant<{ + ok: T + error: X +}> + +/** + * Utility type for defining a [keyed union] type as in IPLD Schema. In practice + * this just works around typescript limitation that requires discriminant field + * on all variants. + * + * ```ts + * type Result = + * | { ok: T } + * | { error: X } + * + * const demo = (result: Result) => { + * if (result.ok) { + * // ^^^^^^^^^ Property 'ok' does not exist on type '{ error: Error; }` + * } + * } + * ``` + * + * Using `Variant` type we can define same union type that works as expected: + * + * ```ts + * type Result = Variant<{ + * ok: T + * error: X + * }> + * + * const demo = (result: Result) => { + * if (result.ok) { + * result.ok.toUpperCase() + * } + * } + * ``` + * + * [keyed union]:https://ipld.io/docs/schemas/features/representation-strategies/#union-keyed-representation + */ +export type Variant> = { + [Key in keyof U]: { [K in Exclude]?: never } & { + [K in Key]: U[Key] + } +}[keyof U] diff --git a/packages/schema/tsconfig.json b/packages/schema/tsconfig.json new file mode 100644 index 00000000..3ccd24b2 --- /dev/null +++ b/packages/schema/tsconfig.json @@ -0,0 +1,103 @@ +{ + "compilerOptions": { + /* Visit https://aka.ms/tsconfig.json to read more about this file */ + + /* Projects */ + "incremental": true /* Enable incremental compilation */, + "composite": true /* Enable constraints that allow a TypeScript project to be used with project references. */, + // "tsBuildInfoFile": "./dist", /* Specify the folder for .tsbuildinfo incremental compilation files. */ + // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects */ + // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */ + // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */ + + /* Language and Environment */ + "target": "ES2020" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */, + // "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */ + // "jsx": "preserve", /* Specify what JSX code is generated. */ + // "experimentalDecorators": true, /* Enable experimental support for TC39 stage 2 draft decorators. */ + // "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */ + // "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h' */ + // "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */ + // "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using `jsx: react-jsx*`.` */ + // "reactNamespace": "", /* Specify the object invoked for `createElement`. This only applies when targeting `react` JSX emit. */ + // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */ + // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */ + + /* Modules */ + "module": "ES2020" /* Specify what module code is generated. */, + // "rootDir": "./", /* Specify the root folder within your source files. */ + "moduleResolution": "node" /* Specify how TypeScript looks up a file from a given module specifier. */, + "baseUrl": "./" /* Specify the base directory to resolve non-relative module names. */, + // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */ + // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */ + // "typeRoots": [], /* Specify multiple folders that act like `./node_modules/@types`. */ + // "types": [], /* Specify type package names to be included without being referenced in a source file. */ + // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ + // "resolveJsonModule": true, /* Enable importing .json files */ + // "noResolve": true, /* Disallow `import`s, `require`s or ``s from expanding the number of files TypeScript should add to a project. */ + + /* JavaScript Support */ + "allowJs": true /* Allow JavaScript files to be a part of your program. Use the `checkJS` option to get errors from these files. */, + "checkJs": true /* Enable error reporting in type-checked JavaScript files. */, + // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from `node_modules`. Only applicable with `allowJs`. */ + + /* Emit */ + // "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */ + "declarationMap": true /* Create sourcemaps for d.ts files. */, + "emitDeclarationOnly": true /* Only output d.ts files and not JavaScript files. */, + // "sourceMap": true, /* Create source map files for emitted JavaScript files. */ + // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If `declaration` is true, also designates a file that bundles all .d.ts output. */ + "outDir": "./dist/" /* Specify an output folder for all emitted files. */, + // "removeComments": true, /* Disable emitting comments. */ + // "noEmit": true, /* Disable emitting files from a compilation. */ + // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */ + // "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types */ + // "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */ + // "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */ + // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ + // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */ + // "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */ + // "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */ + // "newLine": "crlf", /* Set the newline character for emitting files. */ + // "stripInternal": true, /* Disable emitting declarations that have `@internal` in their JSDoc comments. */ + // "noEmitHelpers": true, /* Disable generating custom helper functions like `__extends` in compiled output. */ + // "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */ + // "preserveConstEnums": true, /* Disable erasing `const enum` declarations in generated code. */ + // "declarationDir": "./", /* Specify the output directory for generated declaration files. */ + // "preserveValueImports": true, /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */ + + /* Interop Constraints */ + // "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */ + // "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */ + "esModuleInterop": true /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables `allowSyntheticDefaultImports` for type compatibility. */, + // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */ + "forceConsistentCasingInFileNames": true /* Ensure that casing is correct in imports. */, + + /* Type Checking */ + "strict": true /* Enable all strict type-checking options. */, + // "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied `any` type.. */ + // "strictNullChecks": true, /* When type checking, take into account `null` and `undefined`. */ + // "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */ + // "strictBindCallApply": true, /* Check that the arguments for `bind`, `call`, and `apply` methods match the original function. */ + // "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */ + // "noImplicitThis": true, /* Enable error reporting when `this` is given the type `any`. */ + // "useUnknownInCatchVariables": true, /* Type catch clause variables as 'unknown' instead of 'any'. */ + // "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */ + // "noUnusedLocals": true, /* Enable error reporting when a local variables aren't read. */ + // "noUnusedParameters": true, /* Raise an error when a function parameter isn't read */ + // "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */ + // "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */ + // "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */ + // "noUncheckedIndexedAccess": true, /* Include 'undefined' in index signature results */ + // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */ + // "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type */ + // "allowUnusedLabels": true, /* Disable error reporting for unused labels. */ + // "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */ + + /* Completeness */ + // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ + "skipLibCheck": true /* Skip type checking all .d.ts files. */ + }, + "include": ["src", "test"], + "references": [{ "path": "../interface" }, { "path": "../principal" }] +}