@hyurl/schematize v1.1.1
Schematize
Utilities to ensure object structures and perform pattern matching.
For many reasons, an API may not return the data to what we want them to be. For
example, we expect a JSON response should have a foo property of number type,
however, the server provides it as a numeric string, or even worse it
doesn't exist at all, or is set null, which would cause the client to crash
if the exception is not handled well.
That's why schematize comes on stage. It ensures the input or output data must be of a certain structure based on the schema, which provides the ability to auto-cast compatible values and provides default values when they're missing.
Install
npm i @hyurl/schematizeExample
schematize
import schematize, { Optional } from "@hyurl/schematize";
import * as express from "express";
let AuthorSchema = {
uid: Number,
username: String,
birthday: Optional(Date), // use 'Optional' to define an optional property
isPopular: Boolean,
contactInfo: { // child schema is supported and unlimited
mobile: String,
email: {
personal: String,
work: String
}
}
};
// Client-side example
(async () => {
let res = await fetch("https://localhost/author/12345");
if (res.ok) {
// 'author' will be well-typed in TypeScript.
let author = schematize(await res.json(), authorSchema);
console.log(
`${author.username} (${author.uid}) ${author.isPopular ? "is" : "isn't"} a popular writer.`);
if (author.birthday) { // 'birthday' is optional, it could be missing
console.log(`His birthday is ${author.birthday}.`);
}
}
})();
// Server-side example
(async () => {
let app = express();
app.get("/author/:uid", (req, res) => {
// assume db is defined
let author = await db.findOne({ uid: Number(req.params.uid) });
if (author) {
// At this point, we don't know what fields does the 'author' has,
// but that's no problem, `schematize()` will make sure that all the
// fields we expected are presented in the outgoing response.
res.send(schematize(author, AuthorSchema));
}
});
app.listen(80);
})();match
import { match, Optional } from "@hyurl/schematize";
let userSchema = { uid: Number, username: String, birthday: Optional(String) };
let res = await fetch(someLink);
if (res.ok) {
let data = await res.json();
// Assume the structure of the returning data from fetch(someLink) could be
// in multiple forms, we could use `match()` to do pattern-matching.
if (match(data, userSchema)) {
// 'data' will be well-typed in TypeScript in the conditional block
console.log(`${data.username} (${data.uid}), born on: ${data.birthday || "unknown"}`);
} else if (match(data, { code: Number, data: userSchema })) {
let _data = data.data;
console.log(`${_data.username} (${_data.uid}), born on: ${_data.birthday || "unknown"}`);
} else if (match(data, { code: Number, reason: String })) {
console.log(`Failed to fetch user: ${data.reason} (${data.code})`);
}
}API
schematize
/**
* Ensures the input object is restraint with the types defined in the schema
* and automatically fills any property that is missing.
* @param omitUntyped If set, those properties that are not specified in the
* schema will be removed.
*/
function schematize<T>(
obj: any,
schema: T,
omitUntyped?: boolean
): Structured<T>;
/**
* Ensures the input array of objects is restraint with the types defined in
* the schema and automatically fills any property that is missing.
* @param schema For array of objects, the schema must be defined as an array
* with one element which sets the types for all objects in the input array.
* @param omitUntyped If set, those properties that are not specified in the
* schema will be removed.
*/
function schematize<T>(
arr: any[],
schema: [T],
omitUntyped?: boolean
): Structured<T>[];match
/**
* Performs a pattern matching on an object according to the given schema.
* @param exactMatch If set, the object must only contain the properties that
* are presented in the schema.
*/
function match<T extends object>(
obj: object,
schema: T,
exactMatch?: boolean
): obj is Structured<T>;
/**
* Performs a pattern matching on an array according to the given schema.
* @param schema For array of objects, the schema must be defined as an array
* with one element which sets the types for all objects in the input array.
* @param exactMatch If set, the objects in the input array must only contain
* the properties that are presented in the schema.
*/
export default function match<T>(
arr: any[],
schema: [T],
exactMatch?: boolean
): arr is Structured<T>[];For more details about types, please check the type definition.
More On Schematize
Default Values
For the schematize() function, by default, if you provide a type constructor
in the schema, when the specified property is missing, it will create a default
value to make sure the property always available (expect using
Optional wrapper). The default values of each types are:
String=>''(empty string)Number=>0BigInt=>0nBoolean=>falseObject=>{}(empty plain object for plain object constructor)Array=>[]Date=>new Date()(The current date)Map=>new Map([])(empty map)Set=>new Set([])(empty set)Buffer=>Buffer.from([])(empty buffer)
Other type constructors (include Symbol and user-defined classes) are all set
null, if the relevant property is missing.
Other than these, you can always by providing an instance value to the schema, and it will be used as the default value of the property automatically.
(NOTE: you can also set an instance value in the schema of match() function,
but it will be used for full-equality comparison.)
Auto-cast Types
When a property in the input data of the schematize() function isn't of the
type that defined in the schema, however, it is compatible to the type, or can
generate a similar representation, the value will be automatically casted into
an instance of the defined type.
For example:
let schema = { url: URL }; // 'url' is defined as a URL
let data = schematize({
url: "https://example.com" // but provided a string
}, schema);
assert(data.url instanceof URL); // 'data.url' will be auto-casted to URLThe current casting rule is as the following:
StringDateto ISO stringObjectorArrayto JSON stringSymbolin global registry, to string- any other types that are not functions or classes can be casted to strings
NumberBigIntto its number equivalentsStringthat is numericDateto Unix timestamp in millisecondstrueto1falseto0
BigIntNumberto its bigint equivalents- other types are similar to
Number
BooleanNumber/BigInt:0/0ntofalse, others to1String/^([Tt]rue|[Yy]es|[Oo]n|1)$/casted totrue/^([Ff]alse|[Nn]o|[Oo]ff|0)$/casted tofalse
SymbolStringmatches/Symbol\(.*?\)/or not, casted to global-scoped symbol
ObjectObjectderivatives, create a shallow copyStringstarts with{and ends with}will try to be parsed as JSON
ArrayString- starts with
[and ends with]will try to be parsed as JSON - others will be split by
,
- starts with
DateNumberused as Unix timestampStringused as date-time string
URLStringa valid URL string
RegExpStringa valid RegExp literal
MapArraya valid Map entry
SetArray
BufferUint8ArrayArrayBufferArraythat only contains numbers ranged from0-255
TypedArray- any iterable object with numbers will try to be casted
ErrorStringused as error messageObjectwith signature{ name: string, message: string, stack?: string }
(NOTE: match() function doesn't support auto-casting.)