From 08b92aac2e3dcef14670410e188389585b800409 Mon Sep 17 00:00:00 2001 From: Sean Morris <640101+seanmorris@users.noreply.github.com> Date: Wed, 4 Sep 2024 21:16:38 -0400 Subject: [PATCH] Documenting schema. --- README.md | 239 ++++++++++++++++++++++++++++++++++++++++++- Schema.mjs | 5 +- index.mjs | 1 + package.json | 1 + test/schema.test.mjs | 4 +- 5 files changed, 244 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index dbb959f..70a3f36 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # libtuple -[![Test](https://github.com/seanmorris/libtuple/actions/workflows/test.yaml/badge.svg)](https://github.com/seanmorris/libtuple/actions/workflows/test.yaml) *Memory-efficient tuple implementation in 6.4kB* +[![Test](https://github.com/seanmorris/libtuple/actions/workflows/test.yaml/badge.svg)](https://github.com/seanmorris/libtuple/actions/workflows/test.yaml) *Memory-efficient immutables in 10.4kB* ### Install & Use @@ -81,6 +81,243 @@ Dict({a, b, c}) === Dict({a, b, c}); // true Dict({a, b, c}) === Dict({c, b, a}); // false ``` +### Schema + +A `Schema` allows you to define a complex structure for your immutables. It is defined by one or more SchemaMappers, which take a value and either return it, or throw an error: + +```javascript +import { Schema } from 'libtuple'; + +const boolSchema = s.boolean(); + +boolSchema(true); // returns true +boolSchema(false); // returns false +boolSchema(123); // throws an error +``` + +You can create schemas for Tuples, Groups, Records, and Dicts: + +```javascript +const userSchema = s.record({ + id: s.number(), + email: s.string(), +}); + +const users = [ + {id: 1, email: "fake@example.com"}, + {id: 2, email: "another@example.com"}, + {id: 3, email: "and_a_third@example.com"}, +] + +const userRecord = userSchema(users[0]); + +const userListSchema = s.nTuple(userSchema); + +const userListTuple = userListSchema(users); +``` +##### Schema.boolean(options) + +* options.map - Callback to transform the value after its been validated. + +##### Schema.number(options) + +* options.min - Min value +* options.max - Max value +* options.map - Callback to transform the value after its been validated. +* options.check - Throw a TypeError if this returns false. + +##### Schema.string(options) + +* options.min - Min length +* options.max - Max length +* options.map - Callback to transform the value after its been validated. +* options.match - Throw a TypeError if this does NOT match +* options.noMatch - Throw a TypeError if this DOES match +* options.check - Throw a TypeError if this returns false. + +##### Schema.array(options) + +* options.min - Min length +* options.max - Max length +* options.map - Callback to transform the value after its been validated. +* options.each - Callback to transform each element. +* options.check - Throw a TypeError if this returns false. + +##### Schema.object(options) + +* options.class - Throw a TypeError if the class does not match. +* options.map - Callback to transform the value after its been validated. +* options.each - Callback to transform each element. +* options.check - Throw a TypeError if this returns false. + +##### Schema.function(options) + +* options.map - Callback to transform the value after its been validated. +* options.check - Throw a TypeError if this returns false. + +##### Schema.symbol(options) + +* options.map - Callback to transform the value after its been validated. +* options.check - Throw a TypeError if this returns false. + +##### Schema.null(options) + +* options.map - Callback to transform the value after its been validated. + +##### Schema.undefined(options) + +* options.map - Callback to transform the value after its been validated. + +##### Schema.value(options) + +* options.map - Callback to transform the value after its been validated. +* options.check - Throw a TypeError if this returns false. + +##### Schema.drop() + +Drop the value (always maps to `undefined`) + +##### Schema.or() + +Map the value with the first matching SchemaMapper + +```javascript +import { Schema as s } from 'libtuple'; + +const dateSchema = s.or( + s.string({match: /\d\d \w+ \d\d\d\d \d\d:\d\d:\d\d \w+?/, map: s => new Date(s)}) + , s.object({class: Date}) +); + +console.log( dateSchema('04 Apr 1995 00:12:00 GMT') ); +console.log( dateSchema(new Date) ); +``` + +##### Schema.repeat(, schemaMapper) + +Repeat a SchemaMapper n times + +##### Schema.tuple(...values) + +Map one or more values to a Tuple. + +```javascript +import { Schema as s } from 'libtuple'; + +const pointSchema = s.tuple(s.number(), s.number()); + +const point = pointSchema([5, 10]); +``` + +##### Schema.group(...values) + +Map one or more values to a Group. + +##### Schema.record(properties) + +Map one or more properties to a Record. + +```javascript +const companySchema = s.sDict({ + name: s.string(), + phone: s.string(), + address: s.string(), +}); + +const company = companySchema({ + name: 'Acme Corporation', + phone: '+1-000-555-1234', + address: '123 Fake St, Anytown, USA', +}); +``` + +##### Schema.dict(properties) + +Map one or more values to a Dict. + +##### Schema.nTuple() + +Map n values to a Tuple. Will append each value in the input to the Tuple using the same mapper. + +##### Schema.nGroup() + +Map n values to a Group. Will append each value in the input to the Group using the same mapper. + +##### Schema.nRecord() + +Map n properties to a Record. Will append additional properties without mapping or validation, if present. + +```javascript +const companySchema = s.sDict({ + name: s.string(), + phone: s.string(), + address: s.string(), +}); + +const company = companySchema({ + name: 'Acme Corporation', + phone: '+1-000-555-1234', + address: '123 Fake St, Anytown, USA', + openHours: "9AM-7PM", +}); +``` + +##### Schema.nDict() + +Map n properties to a Dict. Will append additional properties without mapping or validation, if present. + +##### Schema.sTuple() + +Strictly map values to a Tuple. Will throw an error if the number of values does not match. + +```javascript +import { Schema as s } from 'libtuple'; + +const pointSchema = s.sTuple(s.number(), s.number()); + +const pointA = pointSchema([5, 10]); +const pointB = pointSchema([5, 10, 1]); // ERROR! +``` + +##### Schema.sGroup() + +Strictly map values to a Group. Will throw an error if the number of values does not match. + +##### Schema.sRecord() + +Strictly map values to a Record. Will throw an error if the number of values does not match. + +##### Schema.sDict() + +Strictly map values to a Dict. Will throw an error if the number of values does not match. + +##### Schema.xTuple() + +Exclusively map values to a Tuple. Will drop any keys not present in the schema. + +```javascript +import { Schema as s } from 'libtuple'; + +const pointSchema = s.sTuple(s.number(), s.number()); + +const pointA = pointSchema([5, 10]); // [5, 10] +const pointB = pointSchema([5, 10, 1]); // Also [5, 10] + +console.log(pointB[2]); //undefined +``` + +##### Schema.xGroup() + +Exclusively map values to a Group. Will drop any keys not present in the schema. + +##### Schema.xRecord() + +Exclusively map values to a Record. Will drop any keys not present in the schema. + +##### Schema.xDict() + +Exclusively map values to a Dict. Will drop any keys not present in the schema. + ## Gotchas In JavaScript, object comparisons are based on reference, not on the actual content of the objects. This means that even if two objects have the same properties and values, they are considered different if they do not reference the same memory location. diff --git a/Schema.mjs b/Schema.mjs index a3a9618..172b7b5 100644 --- a/Schema.mjs +++ b/Schema.mjs @@ -118,7 +118,6 @@ const Schema = { /** * Map n keys to a Record. - * Will append each value in the input to the Record using the same mapper. * @param {Object.} schema - An Object holding SchemaMappers */ nRecord(schema) @@ -133,7 +132,6 @@ const Schema = { /** * Map n keys to a Dict. - * Will append each value in the input to the Dict using the same mapper. * @param {Object.} schema - An Object holding SchemaMappers */ nDict(schema) @@ -305,7 +303,8 @@ const Schema = { /** * Validate a boolean - * @param {*} options + * @param {Object} options + * @param {function(any):any} options.map Transform the value after its been validated. */ boolean(options = {}) { diff --git a/index.mjs b/index.mjs index c93679c..b2a1c00 100644 --- a/index.mjs +++ b/index.mjs @@ -2,3 +2,4 @@ export { default as Tuple } from "./Tuple.mjs"; export { default as Group } from "./Group.mjs"; export { default as Record } from "./Record.mjs"; export { default as Dict } from "./Dict.mjs"; +export { default as Schema } from "./Schema.mjs"; diff --git a/package.json b/package.json index 39a1879..d123eca 100644 --- a/package.json +++ b/package.json @@ -15,6 +15,7 @@ "Group.mjs", "Record.mjs", "Dict.mjs", + "Schema.mjs", "index.mjs", "README.md", "LICENSE", diff --git a/test/schema.test.mjs b/test/schema.test.mjs index 62c2aac..398c3c1 100644 --- a/test/schema.test.mjs +++ b/test/schema.test.mjs @@ -132,8 +132,8 @@ test('s.or param test', t => { }); test('usersSchema test', t => { - const usersSchema = s.sTuple( - ...s.repeat(10, s.sRecord({ + const usersSchema = s.nTuple( + (s.sRecord({ id: s.number({}), name: s.string({ map: s => Tuple(...s.split(' '))