diff --git a/README.md b/README.md index 8d9a5a799..c745b958b 100644 --- a/README.md +++ b/README.md @@ -233,6 +233,28 @@ The core of the Semaphore protocol is in the [circuit logic](/packages/circuits/ + + + + @semaphore-protocol/utils + + + (docs) + + + + + + NPM version + + + + + + Downloads + + + diff --git a/packages/utils/LICENSE b/packages/utils/LICENSE new file mode 100644 index 000000000..8ef16f7a5 --- /dev/null +++ b/packages/utils/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2024 Ethereum Foundation + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/packages/utils/README.md b/packages/utils/README.md new file mode 100644 index 000000000..77e86e326 --- /dev/null +++ b/packages/utils/README.md @@ -0,0 +1,80 @@ +

+

+ Semaphore utils +

+

A library to provide utility functions to the other Semaphore packages.

+

+ +

+ + + + + NPM license + + + NPM version + + + Downloads + + + Documentation typedoc + + + Linter eslint + + + Code style prettier + +

+ +
+

+ + 👥 Contributing + +   |   + + 🤝 Code of conduct + +   |   + + 🔎 Issues + +   |   + + 🗣️ Chat & Support + +

+
+ +> [!NOTE] +> Please, for more information on the modules provided by this library, see its code documentation [here](https://js.semaphore.pse.dev/modules/_semaphore_protocol_utils). + +## 🛠 Install + +### npm or yarn + +Install the `@semaphore-protocol/utils` package with npm: + +```bash +npm i @semaphore-protocol/utils +``` + +or yarn: + +```bash +yarn add @semaphore-protocol/utils +``` + +## 📜 Usage + +```typescript +// You can import modules from the main bundle. +import { errors, types } from "@semaphore-protocol/utils" + +// Or by using conditional exports. +import { requireNumber } from "@semaphore-protocol/utils/errors" +import { isNumber } from "@semaphore-protocol/utils/types" +``` diff --git a/packages/utils/build.tsconfig.json b/packages/utils/build.tsconfig.json new file mode 100644 index 000000000..2d4a1d6da --- /dev/null +++ b/packages/utils/build.tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "baseUrl": ".", + "declarationDir": "dist/types" + }, + "include": ["src"] +} diff --git a/packages/utils/package.json b/packages/utils/package.json new file mode 100644 index 000000000..bf837b6cd --- /dev/null +++ b/packages/utils/package.json @@ -0,0 +1,48 @@ +{ + "name": "@semaphore-protocol/utils", + "version": "4.0.0-alpha.8", + "description": "A library to provide utility functions to the other Semaphore packages.", + "type": "module", + "license": "MIT", + "main": "dist/index.js", + "types": "dist/types/index.d.ts", + "exports": { + ".": { + "types": "./dist/types/index.d.ts", + "require": "./dist/index.cjs", + "default": "./dist/index.js" + }, + "./errors": { + "types": "./dist/types/errors.d.ts", + "require": "./dist/lib.commonjs/errors.cjs", + "default": "./dist/lib.esm/errors.js" + }, + "./types": { + "types": "./dist/types/types.d.ts", + "require": "./dist/lib.commonjs/types.cjs", + "default": "./dist/lib.esm/types.js" + } + }, + "files": [ + "dist/", + "src/", + "LICENSE", + "README.md" + ], + "repository": "https://github.com/semaphore-protocol/semaphore", + "homepage": "https://github.com/semaphore-protocol/semaphore/tree/main/packages/utils", + "bugs": { + "url": "https://github.com/semaphore-protocol/semaphore.git/issues" + }, + "scripts": { + "build": "rimraf dist && rollup -c rollup.config.ts --configPlugin typescript", + "prepublishOnly": "yarn build" + }, + "publishConfig": { + "access": "public" + }, + "devDependencies": { + "rollup-plugin-cleanup": "^3.2.1", + "rollup-plugin-typescript2": "^0.36.0" + } +} diff --git a/packages/utils/rollup.config.ts b/packages/utils/rollup.config.ts new file mode 100644 index 000000000..535b0adbd --- /dev/null +++ b/packages/utils/rollup.config.ts @@ -0,0 +1,30 @@ +import * as fs from "fs" +import cleanup from "rollup-plugin-cleanup" +import typescript from "rollup-plugin-typescript2" + +const pkg = JSON.parse(fs.readFileSync("./package.json", "utf-8")) +const banner = `/** + * @module ${pkg.name} + * @version ${pkg.version} + * @file ${pkg.description} + * @copyright Ethereum Foundation 2024 + * @license ${pkg.license} + * @see [Github]{@link ${pkg.homepage}} +*/` + +export default { + input: "src/index.ts", + output: [ + { file: pkg.exports["."].require, format: "cjs", banner, exports: "auto" }, + { file: pkg.exports["."].default, format: "es", banner }, + { dir: "./dist/lib.commonjs", format: "cjs", banner, preserveModules: true }, + { dir: "./dist/lib.esm", format: "es", banner, preserveModules: true } + ], + plugins: [ + typescript({ + tsconfig: "./build.tsconfig.json", + useTsconfigDeclarationDir: true + }), + cleanup({ comments: "jsdoc" }) + ] +} diff --git a/packages/utils/src/errors.ts b/packages/utils/src/errors.ts new file mode 100644 index 000000000..842bd479a --- /dev/null +++ b/packages/utils/src/errors.ts @@ -0,0 +1,108 @@ +/** + * @module Errors + * This module is designed to provide utility functions for validating + * function parameters. It includes functions that throw type errors if + * the parameters do not meet specified criteria, such as being defined, + * a number, a string, a function, or an array. This module helps ensure + * that functions receive the correct types of inputs, enhancing code + * reliability and reducing runtime errors. + */ + +import { + SupportedType, + isArray, + isDefined, + isFunction, + isNumber, + isString, + isSupportedType, + isType, + isUint8Array +} from "./types" + +/** + * It throws a type error if the parameter value has not been defined. + * @param parameterValue The parameter value. + * @param parameterName The parameter name. + */ +export function requireDefined(parameterValue: any, parameterName: string) { + if (!isDefined(parameterValue)) { + throw new TypeError(`Parameter '${parameterName}' is not defined`) + } +} + +/** + * It throws a type error if the parameter value is not a number. + * @param parameterValue The parameter value. + * @param parameterName The parameter name. + */ +export function requireNumber(parameterValue: number, parameterName: string) { + if (!isNumber(parameterValue)) { + throw new TypeError(`Parameter '${parameterName}' is not a number`) + } +} + +/** + * It throws a type error if the parameter value is not a string. + * @param parameterValue The parameter value. + * @param parameterName The parameter name. + */ +export function requireString(parameterValue: string, parameterName: string) { + if (!isString(parameterValue)) { + throw new TypeError(`Parameter '${parameterName}' is not a string`) + } +} + +/** + * It throws a type error if the parameter value is not a function. + * @param parameterValue The parameter value. + * @param parameterName The parameter name. + */ +export function requireFunction(parameterValue: Function, parameterName: string) { + if (!isFunction(parameterValue)) { + throw new TypeError(`Parameter '${parameterName}' is not a function`) + } +} + +/** + * It throws a type error if the parameter value is not an array. + * @param parameterValue The parameter value. + * @param parameterName The parameter name. + */ +export function requireArray(parameterValue: any[], parameterName: string) { + if (!isArray(parameterValue)) { + throw new TypeError(`Parameter '${parameterName}' is not an array`) + } +} + +/** + * It throws a type error if the parameter value is not a uint8array. + * @param parameterValue The parameter value. + * @param parameterName The parameter name. + */ +export function requireUint8Array(parameterValue: Uint8Array, parameterName: string) { + if (!isUint8Array(parameterValue)) { + throw new TypeError(`Parameter '${parameterName}' is not a Uint8Array`) + } +} + +/** + * It throws a type error if the parameter value type is not part of the list of types. + * @param parameterValue The parameter value. + * @param parameterName The parameter name. + */ +export function requireTypes(parameterValue: any, parameterName: string, types: SupportedType[]) { + for (const type of types) { + if (!isSupportedType(type)) { + throw new Error(`Type '${type}' is not supported`) + } + } + + for (const type of types) { + if (isType(parameterValue, type)) { + return + } + } + + throw new TypeError(`Parameter '${parameterName}' is none of the following types: ${types.join(", ")}`) +} diff --git a/packages/utils/src/index.ts b/packages/utils/src/index.ts new file mode 100644 index 000000000..a76016484 --- /dev/null +++ b/packages/utils/src/index.ts @@ -0,0 +1,4 @@ +import * as errors from "./errors" +import * as types from "./types" + +export { errors, types } diff --git a/packages/utils/src/types.ts b/packages/utils/src/types.ts new file mode 100644 index 000000000..be6944e4f --- /dev/null +++ b/packages/utils/src/types.ts @@ -0,0 +1,101 @@ +/** + * @module Types + * This module provides utility functions to check data types. + * It defines a set of supported types and includes functions to check if + * a value is defined and if it matches a supported type. These functions + * are useful for type checking and validation in the other libraries, + * enhancing code robustness and reliability. + */ + +// The list of types supported by this utility functions. +const supportedTypes = ["number", "string", "function", "array", "uint8array"] as const + +// Type extracted from the list above. +export type SupportedType = (typeof supportedTypes)[number] + +/** + * It returns true if the value is defined, false otherwise. + * @param value The value to be checked. + * @returns True or false. + */ +export function isDefined(value: any): boolean { + return typeof value !== "undefined" +} + +/** + * It returns true if the value is a number, false otherwise. + * @param value The value to be checked. + * @returns True or false. + */ +export function isNumber(value: any): boolean { + return typeof value === "number" +} + +/** + * It returns true if the value is a string, false otherwise. + * @param value The value to be checked. + * @returns True or false. + */ +export function isString(value: any): boolean { + return typeof value === "string" +} + +/** + * It returns true if the value is a function, false otherwise. + * @param value The value to be checked. + * @returns True or false. + */ +export function isFunction(value: any): boolean { + return typeof value === "function" +} + +/** + * It returns true if the value is an array, false otherwise. + * @param value The value to be checked. + * @returns True or false. + */ +export function isArray(value: any): boolean { + return typeof value === "object" && Array.isArray(value) +} + +/** + * It returns true if the value is a uint8array, false otherwise. + * @param value The value to be checked. + * @returns True or false. + */ +export function isUint8Array(value: any): boolean { + return value instanceof Uint8Array +} + +/** + * It returns true if the value type is the same as the type passed + * as the second parameter, false otherwise. + * @param type The expected type. + * @returns True or false. + */ +export function isType(value: any, type: SupportedType): boolean { + switch (type) { + case "number": + return isNumber(value) + case "string": + return isString(value) + case "function": + return isFunction(value) + case "array": + return isArray(value) + case "uint8array": + return isUint8Array(value) + default: + return false + } +} + +/** + * Return true if the type is being supported by this utility + * functions, false otherwise. + * @param type The type to be checked. + * @returns True or false + */ +export function isSupportedType(type: string): type is SupportedType { + return (supportedTypes as readonly string[]).includes(type) +} diff --git a/packages/utils/tests/index.test.ts b/packages/utils/tests/index.test.ts new file mode 100644 index 000000000..6366fdc28 --- /dev/null +++ b/packages/utils/tests/index.test.ts @@ -0,0 +1,162 @@ +import { errors, types } from "../src" + +describe("Utils", () => { + describe("# types", () => { + it("Should return true if the value is a number", () => { + expect(types.isNumber(1)).toBeTruthy() + }) + + it("Should return false if the value is not a number", () => { + expect(types.isNumber("string")).toBeFalsy() + }) + + it("Should return true if the value is a string", () => { + expect(types.isString("string")).toBeTruthy() + }) + + it("Should return false if the value is not a string", () => { + expect(types.isString(1)).toBeFalsy() + }) + + it("Should return true if the value is a function", () => { + expect(types.isFunction(() => true)).toBeTruthy() + }) + + it("Should return false if the value is not a function", () => { + expect(types.isFunction(1)).toBeFalsy() + }) + + it("Should return true if the value is an array", () => { + expect(types.isArray([])).toBeTruthy() + }) + + it("Should return false if the value is not an array", () => { + expect(types.isArray(1)).toBeFalsy() + }) + + it("Should return true if the value is an uint8array", () => { + expect(types.isUint8Array(new Uint8Array([]))).toBeTruthy() + }) + + it("Should return false if the value is not an uint8array", () => { + expect(types.isArray(1)).toBeFalsy() + }) + + it("Should return true if the value type is the one expected", () => { + expect(types.isType(1, "number")).toBeTruthy() + expect(types.isType("string", "string")).toBeTruthy() + expect(types.isType(() => true, "function")).toBeTruthy() + expect(types.isType([], "array")).toBeTruthy() + expect(types.isType(new Uint8Array([]), "uint8array")).toBeTruthy() + }) + + it("Should return false if the value type is not the one expected or is not supported", () => { + expect(types.isType("string", "number")).toBeFalsy() + expect(types.isType(1, "string")).toBeFalsy() + expect(types.isType(1, "function")).toBeFalsy() + expect(types.isType(1, "array")).toBeFalsy() + expect(types.isType(1, "uint8array")).toBeFalsy() + expect(types.isType(1, "type" as any)).toBeFalsy() + }) + + it("Should return true if the type is supported", () => { + expect(types.isSupportedType("number")).toBeTruthy() + }) + + it("Should return false if the type is not supported", () => { + expect(types.isSupportedType("type")).toBeFalsy() + }) + }) + + describe("# errors", () => { + it("Should throw an error if the parameter is not defined", () => { + const fun = () => errors.requireDefined(undefined as any, "parameter") + + expect(fun).toThrow("Parameter 'parameter' is not defined") + }) + + it("Should not throw an error if the parameter is defined", () => { + const fun = () => errors.requireDefined(1, "parameter") + + expect(fun).not.toThrow() + }) + + it("Should throw an error if the parameter is not a number", () => { + const fun = () => errors.requireNumber("euo" as any, "parameter") + + expect(fun).toThrow("Parameter 'parameter' is not a number") + }) + + it("Should not throw an error if the parameter is a number", () => { + const fun = () => errors.requireNumber(1, "parameter") + + expect(fun).not.toThrow() + }) + + it("Should throw an error if the parameter is not a string", () => { + const fun = () => errors.requireString(1 as any, "parameter") + + expect(fun).toThrow("Parameter 'parameter' is not a string") + }) + + it("Should not throw an error if the parameter is a string", () => { + const fun = () => errors.requireString("string", "parameter") + + expect(fun).not.toThrow() + }) + + it("Should throw an error if the parameter is not an array", () => { + const fun = () => errors.requireArray(1 as any, "parameter") + + expect(fun).toThrow("Parameter 'parameter' is not an array") + }) + + it("Should not throw an error if the parameter is an array", () => { + const fun = () => errors.requireArray([], "parameter") + + expect(fun).not.toThrow() + }) + + it("Should throw an error if the parameter is not a uint8array", () => { + const fun = () => errors.requireUint8Array([] as any, "parameter") + + expect(fun).toThrow("Parameter 'parameter' is not a Uint8Array") + }) + + it("Should not throw an error if the parameter is a uint8array", () => { + const fun = () => errors.requireUint8Array(new Uint8Array([]), "parameter") + + expect(fun).not.toThrow() + }) + + it("Should throw an error if the parameter is not a function", () => { + const fun = () => errors.requireFunction(1 as any, "parameter") + + expect(fun).toThrow("Parameter 'parameter' is not a function") + }) + + it("Should not throw an error if the parameter is a function", () => { + const fun = () => errors.requireFunction(() => true, "parameter") + + expect(fun).not.toThrow() + }) + + it("Should throw an error if the parameter is neither a function nor a number", () => { + const fun = () => errors.requireTypes("string", "parameter", ["function", "number"]) + + expect(fun).toThrow("Parameter 'parameter' is none of the following types: function, number") + }) + + it("Should not throw an error if the parameter is either a string or an array", () => { + const fun = () => errors.requireTypes("string", "parameter", ["string", "array"]) + + expect(fun).not.toThrow() + }) + + it("Should throw an error if the parameter types are not supported", () => { + const fun = () => errors.requireTypes("string", "parameter", ["string", "type" as any]) + + expect(fun).toThrow("Type 'type' is not supported") + }) + }) +}) diff --git a/packages/utils/tsconfig.json b/packages/utils/tsconfig.json new file mode 100644 index 000000000..71510a096 --- /dev/null +++ b/packages/utils/tsconfig.json @@ -0,0 +1,4 @@ +{ + "extends": "../../tsconfig.json", + "include": ["src", "tests", "rollup.config.ts"] +} diff --git a/packages/utils/typedoc.json b/packages/utils/typedoc.json new file mode 100644 index 000000000..77a471c91 --- /dev/null +++ b/packages/utils/typedoc.json @@ -0,0 +1,3 @@ +{ + "entryPoints": ["src/index.ts"] +} diff --git a/yarn.lock b/yarn.lock index cf953157d..17cc334b6 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8533,6 +8533,7 @@ __metadata: dependencies: "@rollup/plugin-alias": ^5.1.0 "@rollup/plugin-json": ^6.1.0 + "@semaphore-protocol/utils": 4.0.0-alpha.8 "@types/download": ^8.0.5 "@types/snarkjs": 0.7.8 "@types/tmp": ^0.2.6 @@ -8549,6 +8550,15 @@ __metadata: languageName: unknown linkType: soft +"@semaphore-protocol/utils@4.0.0-alpha.8, @semaphore-protocol/utils@workspace:packages/utils": + version: 0.0.0-use.local + resolution: "@semaphore-protocol/utils@workspace:packages/utils" + dependencies: + rollup-plugin-cleanup: ^3.2.1 + rollup-plugin-typescript2: ^0.36.0 + languageName: unknown + linkType: soft + "@sentry/core@npm:5.30.0": version: 5.30.0 resolution: "@sentry/core@npm:5.30.0"