node package manager

pcf-tools-experimental

PCF Tools

Assemble API Blueprint specs with concatenation, templating, and inheritance. Check out how to write Blueprints here.

Installation

npm install -g pcf-tools-experimental

build command

pcf build path/to/specs path/to/output.md

Loads spec.json at the root of the folder, which looks like this:

{
  "name": "Prolific Store",
  "inherit": "node_modules/pcf-specs",
  "version": "1.1.0",
  "features": [
    "overview",
    "endpoints/products",
    "endpoints/cart",
    "endpoints/checkout",
    "models/product",
    "models/cartItem" 
  ],
  "excludeEndpoints": {
    "/products/{id}/reviews": [
      "POST"
    ],
    "/checkout/gift-cards": true
  }
}

A rundown of each option:

name - The name of the API. inherit (optional) - Path to specs to inherit from. See "Spec Inheritance" to learn how specs are merged. version - Semantic versioning of the specs (not the API). features - Concatenates each markdown file at the given path within the spec folder (with .md omitted). excludeEndpoints (optional) - A map of endpoint paths to omit. If the value is an array, it will only omit those methods. If it is true, it will omit all methods for that endpoint.

docs command

pcf docs path/to/specs path/to/output.html

Loads spec.json at the root of the folder, and generates an html version of the spec.

Setting Up The File Structure

First we'll lay out a basic example, then explain the purpose of each directory and file.

spec/
  spec.json
  package.json
  Makefile
  overview.md
  node_modules/
  endpoints/
    products.md
  examples/
    product.json
  schemas/
    product.json
  models/
    product.md
  headers/
    session.js

package.json

If you plan on extending a public base spec, like pcf-specs, initiate an npm package.json file:

npm init

Then install the spec:

npm install pcf-specs

This will allow you to lock your spec to a specific version of the base spec using the package.json file.

overview.md

The introductory part of the spec, including title:

# Google Maps API
 
This is the Google Maps API. Enjoy!

endpoints/

This holds the markdown files that will be concatenated together. They also allow you to use Handlebars templating, providing several helpers:

# Group Products
These are the endpoints regarding products.
 
## Product [/products/{id}]
 
### Get Product [GET]
Gets the full product model for the given id.
 
+ Parameters
 
  + id (required, string, `12345`) ... The id of the product.
 
+ Response 200 (application/json; charset=utf-8)
 
  + Body
 
            {{example 'product'}}
 
  + Schema
 
            {{schema 'product'}}

The example helper inserts the JSON from examples/product.json, while schema inserts the JSON from schemas/product.json.

examples/

Holds example JSON bodies to be returned by endpoints. Here is an example that extends a parent one. Notice you can also use templating within a JSON file as well. One caveat: if your example is using an array containing objects, you need to make sure that you specify the objects in the same order as in the base spec for the two examples to properly merge together :

{
  "__exclude": ["rating"],
  "reviews": "{{example 'reviewCollection'}}",
  "materials": [
    "cotton",
    "polyester"
  ]
}

See JSON File Features to learn about __exclude and other functions.

schemas/

Includes body schemas to be included in endpoint files.

models/

Includes markdown files that describe each model. These are typically appended at the end of the blueprint.

# Group Product Model
 
Description of the product model.
 
{{schema 'product'}}

Headers

Includes flat JSON files representing names and example values of headers.

{
  "sessionId": "a8d8f9ea108382374"
}

JSON File Features

You can manipulate JSON structures using special keys that act as functions.

  • "__exclude": ["key1", "key2"] - Omits specified keys. Used to suppress inherited keys from base spec. Can be at any level in the JSON object. Must be nested within the object whose key you want to remove.

    Example

    {
      "name": "string",
      "DOB": "string",
      "price": "integer",
      "some_key": {
        "__extends": "schemas/parentObject",
        "key_in_parentObject": {
          "__exclude" : "key in this object level",
          "subkey_in_parentObject": {
            "__exclude": [ "some_key_in_this", "another key at this sub level" ]
          }
        }
      }
    }
     
  • "__include": ["key3", "key4"] - Only uses specified keys. Can be used anywhere in JSON object. Takes precedence over __exclude.

  • "__inherits" / "__extends": "examples/shortProduct" - Merges this object into specified JSON object file.

  • "key": "{{example 'addressCollection'}}" - Imports specified JSON object. Both the base and the child spec will be checked for the existence of the file, and then be merged into the resulting object. Also works for header, model, and schema. See below for how the schema version of this helper works.

  • {{ schema [schemaName] [required] [description] }}

    • Pulls in JSON files from the schemas directory.

    • schemaName: Single quoted string. The name of the schema file you want to load.

    • required: Boolean value. Indicates a required value in the schema. Defaults to true.

    • description: Sinlge quoted string. A description of the schema. It will override any description specified in the schema file.

      Example

       "carMake": "{{ schema 'carMake' false 'The make of a car' }}"
  • {{ example [exampleName] [description] }}

    • This works similarly to the schema helper above, but pulls in files from the examples directory.
  • {{ header [headerName] [description] }}

  • .. pulls in files from the headers directory.

EXAMPLES

In the following example we're going to demonstrate the usage of a base spec, and a child spec, and see how we can use this tool to generate a new API spec by combining the two in a few different ways. We can extend the base spec with new endpoints and schemas, modify existing schemas by adding and removing properties, and we can reuse schemas in different contexts.

For the examples we will be using the following files:

SPEC FILES

Spec files contain the complete map of your API, If you want a file to be available when compiling your Blueprint, you must specify it here. We organize our files into endpoints, schemas, examples, and headers, each in their respective folders.

Base ./node_modules/pcf-specs/spec.json

{
  "name":              "e_commerce_base_spec",
  "version":           "1.0.0",
  "features":          [
    "overview",
 
    "endpoints/cart",
    "endpoints/product"
  ]
}

Child ./spec.json

We specify the spec to inherit from by passing the directory containing the spec with the "inherit" key. We're assuming here that the base spec has been installed as a dependency of the child spec in its package.json file, but that's not a requirement. A spec.json file must exist in this directory. You could easily change this if you wanted to write your own base spec as well. You should also note that the base spec only has endpoints for cart and product, while the child has endpoints for user. The final generated spec will have all three endpoints.

{
  "inherit":           "node_modules/pcf-specs",
  "name":              "e_commerce_subspec",
  "version":           "1.0.0",
  "features":          [
    "overview",
 
    "endpoints/user"
  ]
}

ENDPOINTS

These are the individual endpoints to the API. They are Markdown files written according to the Blueprint guidelines. Check out how to write Blueprints here.

Cart Endpoint (base) ./node_modules/pcf-specs/endpoints/cart.md

# Group Cart
This handles all of the cart related endpoints.  Including the checkout process, and cart
finalization.
 
## Cart [/cart]
These endpoints handle viewing and editing the user's cart.
 
### Get Cart [GET]
Returns the cart for the current user.
 
+ Request (application/json; charset=utf-8)
 
  + Headers
 
            {{header 'session'}}
 
+ Response 200 (application/json; charset=utf-8)
 
  + Body
 
            {{example 'cart'}}
 
  + Schema
 
            {{schema 'cart'}}

Product Endpoint (base) ./node_modules/pcf-specs/endpoints/products.md

# Group Products
These are the endpoints regarding products.  They are used for browsing all of the products offered.
They provide endpoints for getting lists of products through search, as well as get details for a
specific product.
 
## Product [/products/{id}]
 
### Get Product [GET]
Gets the full product model for the given id.
 
+ Parameters
 
  + id (required, string, `12345`) ... The id of the product.
 
+ Response 200 (application/json; charset=utf-8)
 
  + Body
 
            {{example 'product'}}
 
  + Schema
 
            {{schema 'product'}}

User Endpoint (child) ./endpoints/users.md

# Group User
 
Used to get user and profile information, as well as create, login, and logout accounts.
 
## User [/user]
 
### Get User [GET]
Gets the current users basic information.
 
+ Request (application/json; charset=utf-8)
 
  + Headers
 
            {{headers 'session'}}
 
+ Response 200 (application/json; charset=utf-8)
 
  + Body
 
            {{example 'user'}}
 
  + Schema
 
            {{schema 'user'}}

SCHEMAS

Cart Schema

Base ./node_modules/pcf-specs/schemas/cart.json

{
  "title": "Cart",
  "type": "object",
  "description": "The full cart, detailing all information known about a cart.",
  "required": true,
  "properties": {
    "tax": {
      "type": ["number", "null"],
      "required": true,
      "description": "Tax on the cart."
    },
    "total": {
      "type": "number",
      "required": true,
      "description": "Total cost of the cart."
    },
    "shippingWeight": {
      "type": "number",
      "required": true,
      "description": "Total weight of items in the cart"
    }
  }
}

Child ./schemas/cart.json

{
  "properties": {
    "__exclude": ["shippingWeight"],
    "tariff": {
      "type": "number",
      "required": true,
      "description": "Extra tax calculated on exports"
    }
  }
}

Editing the base spec via exclusion

In this cart schema example, the shippingWeight property in the base spec is being excluded, via the __exclude JSON helper, and a new property, tariff, is being added. All the keys in both the parent and child specs will be merged, and then any JSON helpers will be run. this will result in the following object:

{
  "title": "Cart",
  "type": "object",
  "description": "The full cart, detailing all information known about a cart.",
  "required": true,
  "properties": {
    "tax": {
      "type": ["number", "null"],
      "required": true,
      "description": "Tax on the cart."
    },
    "total": {
      "type": "number",
      "required": true,
      "description": "Total cost of the cart."
    },
    "tariff": {
      "type": "number",
      "required": true,
      "description": "Extra tax calculated on exports"
    }
  }
}

This is one of the way we can use inheritance and exclusion to our advantage.

Product Schema

Base ./node_modules/pcf-specs/schemas/product.json

{
  "title": "Product",
  "type": "object",
  "required": true,
  "description": "Describes a full product.",
  "properties": {
    "name": {
      "type": "string",
      "required": true,
      "description": "Name of the product"
    },
    "price": {
      "type": "number",
      "required": true,
      "description": "Price of the product"
    }
  }
}

Child ./schemas/product.json

No file. There are no changes from what's in the base spec.

User Schema

Base

This file does not exist in the base spec.

Child ./schemas/user.json

{
  "title":       "User",
  "type":        "object",
  "description": "Describes a user.",
  "required":    true,
  "properties":  {
    "firstName": {
      "type":        "string",
      "required":    true,
      "description": "User's first name."
    },
    "lastName":  {
      "type":        "string",
      "required":    true,
      "description": "User's last name."
    },
    "email":  {
      "type":        "string",
      "required":    true,
      "description": "User's email."
    }
  }
}

Address Schema

Base

This file does not exist in the base spec.

Child ./schemas/address.json

{
  "title": "Address",
  "type": "object",
  "description": "Contains information pertaining to an address.",
  "required": true,
  "properties": {
    "address1": {
      "type": "string",
      "required": true,
      "description": "First street address field."
    },
    "city": {
      "type": "string",
      "required": true,
      "description": "City on address."
    },
    "state": {
      "type": ["string", "null"],
      "required": true,
      "description": "State (or region/province) on address. The recommendation for this value is for it to be a standard state/region/province abbreviation so that it can be used with the 'countries' endpoint, but implementations may vary depending on requirements."
    },
    "country": {
      "type": "string",
      "required": true,
      "description": "Country on address. The recommendation for this value is for it to be a standard country abbreviation so that it can be used with the 'countries' endpoint, but implementations may vary depending on requirements."
    },
    "zip": {
      "type": "string",
      "required": true,
      "description": "Postal code on address."
    }
  }
}

Including an object

Now let's say we wanted to have our user schema also have an address as one of it's properties. The quickest way to do this would be to just have it reference the address schema. Here's how we do that, using one of the JSON helpers:

{
  "title":       "User",
  "type":        "object",
  "description": "Describes a user.",
  "required":    true,
  "properties":  {
    "firstName": {
      "type":        "string",
      "required":    true,
      "description": "User's first name."
    },
    "lastName":  {
      "type":        "string",
      "required":    true,
      "description": "User's last name."
    },
    "email":  {
      "type":        "string",
      "required":    true,
      "description": "User's email."
    },
    "address": "{{schema 'address'}}"
  }
}

Just by using our JSON helpers, we've grafted one schema onto another, with minimal effort.

Extending an object

Extending a pre-existing object is also simple. Let's say we wanted to create a new type of product called giftcard. It would need all the existing properties of a product, but also a few new ones. Here's how we can accomplish that:

Child ./schemas/giftCard.json

{
  "__extends": "schemas/product",
  "properties": {
      "startingValue": {
      "type": "number",
      "required": "true",
      "description": "Value of the card"
  },
    "activationStatus": {
      "type": "boolean",
      "required": "true",
      "description": "Wether the card has been activated or not"
    }
  }
}

This will result in the following object for the giftCard schema:

{
  "title": "Product",
  "type": "object",
  "required": true,
  "description": "Describes a full product.",
  "properties": {
    "name": {
      "type": "string",
      "required": true,
      "description": "Name of the product"
    },
    "price": {
      "type": "number",
      "required": true,
      "description": "Price of the product"
    },
    "startingValue": {
      "type": "number",
      "required": "true",
      "description": "Value of the card"
    },
    "activationStatus": {
      "type": "boolean",
      "required": "true",
      "description": "Whether the card has been activated or not"
    }
  } 
}