GithubHelp home page GithubHelp logo

succulent's Introduction

Succulent

Powerful and easy runtime type checking

Motivation

What if you could just write TypeScript, and get runtime validation for free?

Basically, a lot of equivalent libraries have weird naming and syntax. We already know TypeScript, and that knowledge already does so much for us, but to take the concept a little bit further, and extend our type checking to the runtime, it kind of feels like having to learn another dialect, with all of its subtle differences. Succulent's main goal is to make it feel like you're just writing TypeScript, and for the necessary differences to feel obvious quickly.

Some examples...

  • the type string is represented by the schema $string
  • the type bigint is represented by the schema $bigint
  • the type Date is represented by the schema $Date
  • the type ArrayBuffer is represented by the schema $ArrayBuffer

Getting more complex...

  • the type T[]/Array<T> could be represented by the schema $Array($T) (assuming $T is a schema)
  • the type Map<K, V> could be represented by the schema $Map($K, $V) (assuming $K and $V are schemas)
  • the types any and never can be represented by the schemas $any and $never respectively

Examples

import { guard, $string } from "succulent";

/**
 * Takes untrusted user input!
 */
export default function (x: unknown): string {
	guard(x, $string);

	// x now has type `string` and can be treated as such
	return x;
}

More complicated...

import {
	guard,
	hasMaxLength,
	matches,
	Type,
	$Array,
	$Date,
	$interface,
	$optional,
	$string,
} from "succulent";

// Use your schema definition to automatically generate TypeScript types.
// Either one of the following should work. The choice is mostly a matter of style.
export type User = Type<typeof $User>;
export interface User extends Type<typeof $User> {}

// Easily define a reuseable way to validate input from untrusted sources
// By convention, schemas are named after the type they represent, prefixed with `$`.
export const $User = $interface({
	id: $string.that(matches(/[A-Za-z0-9_-]{24}/)),
	name: $string.that(hasMaxLength(50)),
	emailAddresses: $Array($string.that(matches(/[A-Za-z0-9_-]{1,}\@hey\.com/))),
	meta: $optional(
		$interface({
			lastSeen: $optional($Date),
		}),
	),
});

export default function (user: unknown) {
	// You can specify a compatible generic type to use instead of the generated type!
	// Mostly helpful for getting nicer editor hints
	guard<User>(x, $User);

	// x now has type `User`
	// ...
}

Even more complicated...

import { guard, inRange, lazy, Schema, $Array, $int, $interface, $string } from "succulent";

type Friend = {
	name: string;
	happiness: number;
	friends: Friend[];
};

// Specifying `Friend` here as a generic ensures that our $Friend schema is
// compatible with the `Friend` type. If they get out of sync, TypeScript will throw
// a compilation error to let you know.
const $Friend: Schema<Friend> = $interface({
	name: $string,
	happiness: $int.that(inRange(0, 10)),
	// We need to use `lazy` here because $Friend is not yet defined. A little unfortunate,
	// but there isn't really a be better way to do this. (unless you know of one, then tell me!)
	friends: $Array(lazy(() => $Friend)),
});

export default function (person: unknown) {
	try {
		guard(person, $Friend);

		// person has type `Friend` now!
		// ...
	} catch (error) {
		// Do something with the error, like probe the heirarchy of where errors came from!
	}
}

succulent's People

Contributors

aslilac avatar

Stargazers

Anne Thorpe avatar Caleb Comstock avatar Bruno Quaresma avatar Stephen Kirby avatar Jon Ayers avatar Nikita avatar John Björk avatar Thiago Delgado Pinto avatar Jayden Seric avatar Toni Villena avatar Jan Bergeson avatar Andrejs Agejevs avatar Danielle Maywood avatar Tynan Beatty avatar  avatar Juan Felix avatar Alex Manning avatar Martijn Gribnau avatar  avatar Andrew Chou avatar Louis Pilfold avatar  avatar Roman avatar James Hay avatar Masanori Ogino avatar Yoshiya Hinosawa avatar Cain avatar Isabella Skořepová avatar Al Murray avatar Hayleigh Thompson avatar  avatar

Watchers

 avatar  avatar

Forkers

jaydenseric

succulent's Issues

Display the values of unexpected properties in `$Exact` errors

It would be very useful to see the values of unexpected properties in $Exact errors, as they provide clues about what went wrong with the object.

For example:

import { $Exact, $literal, guard } from "succulent";

const result = { a: 1, b: 2, c: 3 };

guard(
  result,
  $Exact({
    a: $literal(1),
  })
);

Currently outputs just the unexpected property names, without their values:

file://[redacted]/node_modules/succulent/build/index.mjs:169
      throw new TypeError(trace(errorMessages_exports.invalidValue(x, this), error));
            ^

TypeError: Expected {|a: 1|}, got [object Object]
  Unexpected property b
  Unexpected property c
    at Schema.check (file://[redacted]/node_modules/succulent/build/index.mjs:169:13)
    at Schema.check (file://[redacted]/node_modules/succulent/build/index.mjs:112:19)
    at check (file://[redacted]/node_modules/succulent/build/index.mjs:186:15)
    at file://[redacted]/demo.mjs:4:1
    at ModuleJob.run (node:internal/modules/esm/module_job:192:25)

Node.js v20.2.0

A use case is when you use succulent to assert a data result resolved from GraphQL execution, if the result unexpectedly contains an errors field that contains the reasons why the data field is missing or incomplete, the error messages in the errors field is very useful to see in the guard error message.

Support shorthand Array syntax

i.e.

is([1,2,3,4,5], [$number])

[$number] here should be interpreted as $array($number)

Implementation attempts so far have shown that this is...tricky to get right. Also slightly concerned about confusion/conflict with single element tuples. Tuples with only one item are kind of pointless, and very uncommon, but I think we'd want to avoid ever adding a shorthand syntax for tuples in general, as [$number] meaning "Array of any length" along side something like [$number, $number] meaning "Array of exactly 2 length" would be confusing

Prevent infinite loops

Pretty sure you could easily get stuck in an infinite loop when validating a type that allows for recursion.

const $Loop = $object({
  me: $Loop,
});

const loop = {};
loop.me = loop;

is(loop, $Loop);

Maybe not infinite exactly because you'd bottom out the call stack at some point, but still. This simplest case should be easy to detect and break out of. I think the current architecture might make it a bit tricky though. The way we compose Schema objects means that you can never really know if the current validation is part of some larger validation. Maybe that needs to change.

As a side note, I think I'd also expect is(loop, $Loop) in this example to return true.

Bug: Property required when using $optional

const $Foo = $interface({ url: $optional($string) });
type Foo = Type<typeof $Foo> & { [index: string]: unknown; };
const data: Foo = { notUrl: "any" }; // <- Error: Property 'url' is missing

Using TypeScript v5.1.3 currently.

Partial `$record` option

Should not require all keys of iterable keySchema, and should assert an appropriate type.

Would Partial<Record<K, V>> be right?

Deno support

Should be pretty easy? Maybe needs adding .js extensions to imports, .ts would break Node unfortunately

Support for key types

const $Hi = $interface({
  type: 'hi'
});

const $Hey = $interface({
  type: 'hey'
});

const $Greeting = union(hi, hey);

// Desired result:
is('hi', $Greeting['type']); // true
is('hey', $Greeting['type']); // true

Support input objects without an `Object` prototype

With this in demo.mts:

import { $Exact, $literal, guard } from "succulent";

const result = Object.assign(Object.create(null), { a: 1 });

guard(
  result,
  $Exact({
    a: $literal(2),
  })
);

Running node demo.mjs reveals an problem with the generated error:

file://[redacted]/node_modules/succulent/build/index.mjs:58
      return String(x);
             ^

TypeError: Cannot convert object to primitive value
    at String (<anonymous>)
    at toDisplayString (file://[redacted]/node_modules/succulent/build/index.mjs:58:14)
    at Object.invalidValue (file://[redacted]/node_modules/succulent/build/index.mjs:69:24)
    at Schema.check (file://[redacted]/node_modules/succulent/build/index.mjs:169:55)
    at Schema.check (file://[redacted]/node_modules/succulent/build/index.mjs:112:19)
    at check (file://[redacted]/node_modules/succulent/build/index.mjs:186:15)
    at file://[redacted]/demo.mjs:3:1
    at ModuleJob.run (node:internal/modules/esm/module_job:192:25)

Node.js v20.2.0

Wheres, with this in demo.mts:

import { $Exact, $literal, guard } from "succulent";

const result = { a: 1 };

guard(
  result,
  $Exact({
    a: $literal(2),
  })
);

Running node demo.mjs displays how such an error should look:

file://[redacted]/node_modules/succulent/build/index.mjs:169
      throw new TypeError(trace(errorMessages_exports.invalidValue(x, this), error));
            ^

TypeError: Expected {|a: 2|}, got [object Object]
  Expected property a to have type 2
    Expected 2, got 1
    at Schema.check (file://[redacted]/node_modules/succulent/build/index.mjs:169:13)
    at Schema.check (file://[redacted]/node_modules/succulent/build/index.mjs:112:19)
    at check (file://[redacted]/node_modules/succulent/build/index.mjs:186:15)
    at file://[redacted]/demo.mjs:3:1
    at ModuleJob.run (node:internal/modules/esm/module_job:192:25)

Node.js v20.2.0

Supporting input objects without an Object prototype is really important because libraries like graphql deliberately resolve result objects containing query data like that to ensure that people don't rely on using Object prototype properties and methods that could be overridden by keys in the queried data (see graphql/graphql-js#484 (comment)).

My use case for succulent is asserting GraphQL execution results in unit tests for our GraphQL API, but unfortunately this issue makes the error messages useless if the test encounters an incorrect result.

Hopefully it wouldn't be too hard to add support for these kinds of objects?

JSDoc missing in published modules

The source code has nice JSDoc descriptions for things that are exported:

/**
* Checks if the value is a primitive `string`.
* @param x The value to check
*/

But they seem to be striped from the published modules:

https://unpkg.com/browse/[email protected]/build/types/string.js

And the published type definitions:

https://unpkg.com/browse/[email protected]/build/types/string.d.ts

So unfortunately you don't get the JSDoc descriptions in editor intellisense:

Screenshot 2023-06-09 at 8 51 23 am

Can the build be configured to preserve these comments?

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.