JSPM

  • Created
  • Published
  • Downloads 5
  • Score
    100M100P100Q70790F
  • License MIT

Generate React prop types, Flow types, or TypeScript interfaces from JSON schema files.

Package Exports

  • shapeshifter
  • shapeshifter/lib/Transpiler

This package does not declare an exports field, so the exports above have been automatically detected and optimized by JSPM instead. If any package subpath is missing, it is recommended to post an issue to the original package (shapeshifter) to support the "exports" field. If that is not possible, create a JSPM override to customize the exports field for this package.

Readme

Shapeshifter v2.2.0

Build Status

Shapeshifter is a command line tool for generating ES2015 compatible files that export React prop types, Flow type aliases, or TypeScript interfaces from JSON schema files. Schemas can represent database tables, API endpoints, data structures, resources, internal shapes, and more.

Take this user schema for example.

{
    "name": "User",
    "attributes": {
        "id": "number",
        "username": "string",
        "email": {
            "type": "string",
            "required": true
        },
        "location": {
            "type": "shape",
            "attributes": {
                "lat": "number",
                "long": "number"
            }
        }
    }
}

Which transpiles down to the following React prop types.

import { PropTypes } from 'react';

export const UserShape = PropTypes.shape({
    id: PropTypes.number,
    username: PropTypes.string,
    email: PropTypes.string.isRequired,
    location: PropTypes.shape({
        lat: PropTypes.number,
        long: PropTypes.number,
    }),
});

Or the following Flow type aliases.

// @flow

export type UserType = {
    id: number,
    username: string,
    email: string,
    location: {
        lat: number,
        long: number,
    },
};

Or lastly, TypeScript interfaces.

export interface UserInterface {
    id?: number;
    username?: string;
    email: string;
    location?: {
        lat?: number;
        long?: number;
    };
}

Requirements

  • ES2015
  • Node 4+
  • React 0.14+ / Flow 0.20+ / TypeScript 1.6+

Installation

npm install shapeshifter --save-dev

Usage

Shapeshifter is provided as a binary which can be executed like so.

shapeshifter [options] [input] > [output]

The binary input accepts either a single schema file or a directory of schema files. If a directory is provided, they will be combined into a single output.

By default, the binary will send output to stdout, which can then be redirected to a destination of your choosing, otherwise the output will be sent to the console.

Options

--help, -h - Displays a help menu.

--nullable, -n - Marks all attributes as nullable by default. Defaults to false. (Flow only)

--required, -r - Marks all attributes as required by default. Defaults to false. (React and TypeScript only)

--indent - Defines the indentation characters to use in the generated output. Defaults to 2 spaces.

--format - The format to output to. Accepts "react", "flow", or "typescript". Defaults to "react".

--schemas - Include schema class exports in the output. Defaults to "false".

--attributes - Include an attribute list in the schema class export. Defaults to "false".

--types - Include type definition exports in the output. Defaults to "false".

Documentation

JSON Structure

A schema can either be a JSON file, or a JavaScript file that exports an object (Node.js compatible). JSON is preferred as it's a consumable format across many languages.

Every schema requires a name and attributes property. The name is used to denote the name of the export found in the ES2015 transpiled output file, while the attributes denote the available fields in the current schema.

{
    "name": "Users",
    "attributes": {}
}

Furthermore, a schema supports the following optional properties: imports, constants, and subsets. Continue reading for more information on all the supported schema properties.

Attributes

The attributes object represents a mapping of field names to type definitions. Attributes are usually a one-to-one mapping of database table columns or data model attributes.

The value of a field, the type definition, represents what type of data this value expects to be. This definition can either be a string of the name of a primitive type, or an object with a required type property, which can be the name of any type.

Depending on the type used, additional properties may be required.

"attributes": {
    "primitiveField": "string",
    "compoundField": {
        "type": "enum",
        "valueType": "number",
        "values": [1, 2, 3]
    }
}
Modifiers

All attribute type definitions support two modifier properties, required and null. Both of these values accept a boolean value.

When required is used by React, it will append isRequired to all prop types. When used by TypeScript, it will omit ? on every property name. Required attributes are not supported by Flow, instead it supports null. Nullable fields will prepend ? to every type alias.

"field": {
    "type": "string",
    "required": true,
    "null": false
}

Subsets

The subsets object allows for smaller sets of attributes to be defined and exported in the ES2015 output. Each key in the object represents a unique name for the subset, while the value of each property can either be an array of attribute names, or an object of attributes, required (optional), and null (optional) properties.

Unlike required and null properties found on type definitions, these properties represent a mapping of attributes in the current subset to boolean values, which enable or disable the modifier.

"subsets": {
    "SetA": ["foo", "bar"],
    "SetB": {
        "attributes": ["foo", "qux"],
        "null": {
            "foo": true
        }
    },
    "SetC": {
        "attributes": ["bar", "baz"],
        "required": {
            "bar": true,
            "baz": false
        }
    }
},
"attributes": {
    "foo": "number",
    "bar": "bool",
    "baz": {
        "type": "string",
        "required": true
    },
    "qux": {
        "type": "string",
        "null": true
    }
}

Imports

The imports array provides a mechanism for defining ES2015 imports, which in turn allow re-use of application level structures.

An import object requires a path property that maps to a file in the application relative to the transpiled schema. Default imports can be defined with the default property, while named imports can be defined with named (an array).

"imports": [
    { "path": "./foo.js", "default": "FooClass" },
    { "path": "../bar.js", "named": ["funcName", "constName"] },
    { "path": "../baz/qux.js", "default": "BarClass", "named": ["className"] }
]

Constants

The constants object is a mapping of a constant to a primitive value or an array of primitive values. Constants are transpiled down to exported ES2015 consts, allowing easy re-use of values.

The primary use case of this feature is to provide constants from a backend model layer that can easily be used on the frontend, without introducing duplication.

"constants": {
    "STATUS_PENDING": 0,
    "STATUS_ACTIVE": 1,
    "STATUSES": [0, 1],
    "ADMIN_FLAG": "admin"
}

Metadata

The meta object allows arbitrary metadata to be defined. Only two fields are supported currently, primaryKey and resourceName, both of which are used when generating Schema classes using --schemas. The primaryKey defines the unique identifier / primary key of the record, usually "id" (default), while resourceName is the unique name found in a URL path. For example, in the URL "/api/users/123", the "users" path part would be the resource name, and "123" would be the primary key.

"meta": {
    "primaryKey": "id",
    "resourceName": "users"
}

Attribute Types

For every attribute defined in a JSON schema, a type definition is required. Types will be generated when the --types CLI option is passed. The following types are supported.

Primitives

There are 3 primitive types, all of which map to native JavaScript primitives. They are string, number, and boolean. Primitives are the only types that support shorthand notation.

"name": "string",
"status": "number",
"active": "boolean",

As well as the expanded standard notation.

"name": {
    "type": "string"
},
"status": {
    "type": "number"
},
"active": {
    "type": "boolean"
},

This transpiles down to:

// React
name: PropTypes.string,
status: PropTypes.number,
active: PropTypes.bool,

// Flow
name: string,
status: number,
active: boolean,

// TypeScript
name?: string;
status?: number;
active?: boolean;

Alias names: str, num, int, integer, float, bool, binary

Arrays

An array is a dynamic list of values, with the type of the value defined by the valueType property.

"messages": {
    "type": "array",
    "valueType": {
        "type": "string"
    }
}

This transpiles down to:

// React
messages: PropTypes.arrayOf(PropTypes.string),

// Flow
messages: string[],

// TypeScript
messages?: string[];

Alias names: list

Objects

An object maps key-value pairs through the keyType and valueType properties -- both of which are type definitions. When transpiling down to React, the keyType is not required.

This is equivalent to generics from other languages: Object<T1, T2>.

"costs": {
    "type": "object",
    "keyType": "string",
    "valueType": {
        "type": "number"
    }
}

This transpiles down to:

// React
costs: PropTypes.objectOf(PropTypes.number),

// Flow
costs: { [key: string]: number },

// TypeScript
costs?: { [key: string]: number };

Alias names: obj, map

Enums

An enum is a fixed list of values, with both the values and the value type being defined through the values and valueType properties respectively.

"words": {
    "type": "enum",
    "valueType": "string",
    "values": ["foo", "bar", "baz"]
}

This transpiles down to:

// React
words: PropTypes.oneOf(['foo', 'bar', 'baz']),

// Flow
words: 'foo' | 'bar' | 'baz',

// TypeScript
export enum SchemaWordsEnum {
  foo = 0,
  bar = 1,
  baz = 2
}

words?: SchemaWordsEnum;

Shapes

A shape is a key-value object with its own set of attributes and types. A shape differs from an object in that an object defines the type for all keys and values, while a shape defines individual attributes. This provides nested structures within a schema.

A shape is similar to a struct found in the C language.

"location": {
    "type": "shape",
    "attributes": {
        "lat": "number",
        "long": "number",
        "name": {
            "type": "string",
            "required": true
        }
    }
}

This transpiles to:

// React
location: PropTypes.shape({
    lat: PropTypes.number,
    long: PropTypes.number,
    name: PropTypes.string.isRequired,
}),

// Flow
location: {
    lat: number,
    long: number,
    name: string,
},

// TypeScript
location?: {
    lat?: number,
    long?: number,
    name: string,
},

Alias names: struct

Unions

The union type provides a logical OR operation against a list of type definitions defined at valueTypes. Any value passed through this attribute must match one of the types in the list.

"error": {
    "type": "union",
    "valueTypes": [
        "string",
        { "type": "number" },
        {
            "type": "instance",
            "contract": "Error"
        }
    ]
}

This transpiles to:

// React
error: PropTypes.oneOfType([
    PropTypes.string,
    PropTypes.number,
    PropTypes.instanceOf(Error),
]),

// Flow
error: string | number | Error,

// TypeScript
error?: string | number | Error;

References

The final type, reference, is the most powerful and versatile type. A reference provides a link to an external schema file, permitting easy re-use and extensibility, while avoiding schema structure duplication across files.

To make use of references, a references map must be defined in the root of the schema. Each value in the map is a relative path to an external schema file.

{
    "name": "Users",
    "references": {
        "profile": "./profile.json"
    },
    "attributes": {}
}

Once the reference map exists, we can define the attribute type, which requires the reference property to point to a key found in the references map. An optional subset property can be defined that points to a subset found in the reference schema file.

"profile": {
    "type": "reference",
    "reference": "profile",
    "subset": ""
}

This transpiles to:

// React
profile: ProfileShape,

// Flow
profile: ProfileType,

// TypeScript
profile?: ProfileInterface;

Alias names: ref

Self References

It's possible to create recursive structures using the self property, which refers to the current schema in which it was defined. When using self, the reference property and the references map is not required.

"node": {
    "type": "reference",
    "self": true
}
Exported Schemas

When generating schema classes using the --schemas CLI option, all references defined in a schema are considered a relation (ORM style), and in turn, will generate schemas as well. To disable this export from occurring, set export to false.

"node": {
    "type": "reference",
    "reference": "field",
    "export": false
}
Relation Type

When generating schema classes, like above, a reference attribute can define the type of relation it has with the parent schema, using ORM styled terminology, and the relation property.

The following relation types are supported, which are based on the Schema class constants: hasOne, hasMany, belongsTo, and belongsToMany (many to many).

"node": {
    "type": "reference",
    "reference": "field",
    "relation": "belongsTo"
}

Instance Ofs

The instance type provides a mechanism for comparing a value to an object (class, function, etc) found in JavaScript. While this doesn't necessarily map to a database table, it does provide an easy way to map to something like a model in the application.

The instance type requires a contract property, which is the name of the object.

"model": {
    "type": "instance",
    "contract": "UserModel"
}

For the most part, this feature must be used in unison with imports, as to pull the object into scope.

"imports": [
    { "default": "UserModel", "path": "../models/UserModel" }
]

This transpiles down to:

import UserModel from '../models/UserModel';

// React
model: PropTypes.instanceOf(UserModel),

// Flow
model: UserModel,

// TypeScript
model: UserModel;

Alias names: inst

Schema Classes

Schema classes are ES2015 based classes that are generated and included in the output when --schemas is passed to the command line. These schemas provide basic attribute and relational support, which in turn can be used by consuming libraries through exports.

Using our users example in the intro, and the --schemas --attributes CLI options, we would get the following output.

export const UserSchema = new Schema('users', 'id');

The following properties are available on the Schema class instance.

resourceName (string) - The resource name of the schema, passed as the first argument to the constructor. This field is based on meta.resourceName in the JSON schema file.

primaryKey (string) - The name of the primary key in the current schema, passed as the second argument to the constructor. This field is based on meta.primaryKey in the JSON schema file. Defaults to "id".

attributes (string[]) - List of attribute names in the current schema.

relations (object[]) - List of relational objects that map specific attributes to externally referenced schemas. The relational object follows this structure:

{
    attribute: 'foo',           // Field name
    schema: new Schema(),       // Reference schema class
    relation: Schema.HAS_ONE,   // Relation type
    collection: false,          // Is it an array?
}

relationTypes (object) - Maps attribute names to relation types. A relation type is one of the following constants found on the Schema class: HAS_ONE, HAS_MANY, BELONGS_TO, BELONGS_TO_MANY.

Including Attributes

By default, attributes are excluded from the output unless the --attributes CLI option is passed. One passed, the are defined as a list of strings using addAttributes().

Continuing with our previous example, the output will be.

export const UserSchema = new Schema('users', 'id');

UserSchema
    .addAttributes(['id', 'username', 'email', 'location']);
Including Relations

Unlike attributes, relations are always included in the output, as relations between entities (via schemas) are highly informational. Relations are divided into 4 categories: has one, has many, belongs to, and belongs to many (many to many).

Relations are generated based on references found within the current schema. Assume we add a has many posts relation, and a has one country relation to the current users schema, we would generate the following output.

export const CountrySchema = new Schema('countries');

export const PostSchema = new Schema('posts');

export const UserSchema = new Schema('users');

PostSchema
    .belongsTo({
        user: UserSchema,
    });

UserSchema
    .hasOne({
        country: CountrySchema,
    })
    .hasMany({
        posts: PostSchema,
    });

FAQ

Why arrayOf, objectOf over array, object React prop types?

I chose arrayOf and objectOf because they provide type safety and the assurance of the values found within the collection. Using non-type safe features would defeat the purpose of this library.

What about node, element, and func React prop types?

The node and element types represent DOM elements or React structures found within the application. These types don't really map to database tables or data structures very well, if at all.

The same could be said for func -- however, that is supported. I've simply opted out in mentioning it in the documentation, as I'm not too sure on its use case. Kind of a hidden feature really.