Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
*.json -diff
2 changes: 1 addition & 1 deletion lib/data/endpoint-response.json

Large diffs are not rendered by default.

146 changes: 146 additions & 0 deletions lib/data/generate-endpoint-response-json.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
#!/bin/sh #/* jshint ignore:start */
':' //; exec "$(command -v nodejs || command -v node)" "$0" "$@"
/* jshint ignore:end */
// ^^^ Lovely polyglot script to permit usage via node _or_ via bash: see
// http://unix.stackexchange.com/questions/65235/universal-node-js-shebang

/**
* To avoid requiring that auto-discovery be utilized every time the API client
* is initialized, this library ships with a built-in route definition from a
* vanilla WordPress REST API installation. That file may be updated by
* installing the API plugin on a clean WP development instance, with no other
* plugins running, and downloading the JSON output from `yourwpsite.com/wp-json/`
* into the "endpoint-response.json" file in this directory.
*
* That file can also be generated by running this script against the same live
* WP REST API instance to download that same file, the difference being that,
* if the `endpoint-response.json` file is downloaded through this script, it
* will be run through the `simplifyObject` utility to cut out about 1/3 of the
* bytes of the response by removing properties that do not effect route generation.
*
* This script is NOT intended to be a dependency of any part of wp.js, and is
* provided purely as a utility for upgrading the built-in copy of the endpoint
* response JSON file that is used to bootstrap the default route handlers.
*
* @example
*
* # Invoke directly, run against default endpoint (details below)
* ./generate-endpoint-response-json.js
*
* # Invoke with `node` CLI, run against default endpoint
* node ./generate-endpoint-response-json --endpoint=http://my-site.com/wp-json
*
* This script runs against http://wpapi.loc/wp-json by default, but it can be
* run against an arbitrary WordPress REST API endpoint by passing the --endpoint
* argument on the CLI:
*
* @example
*
* # Invoke directly, run against an arbitrary WordPress API root
* ./generate-endpoint-response-json.js --endpoint=http://my-site.com/wp-json
*
* # Invoke with `node` CLI, run against an arbitrary WordPress API root
* node ./generate-endpoint-response-json --endpoint=http://my-site.com/wp-json
*
* Either form will update the `endpoint-response.json` file in this directory,
* providing that the endpoint data is downloaded successfully.
*
* This script also has some utility for downloading a custom JSON file for your
* own WP REST API-enabled site, so that you can bootstrap your own routes without
* incurring an HTTP request. To output to a different directory than the default
* (which is this directory, `lib/data/`), pass an --output argument on the CLI:
*
* @example
*
* # Output to your current working directory
* ./path/to/this/dir/generate-endpoint-response-json.js --output=.
*
* # Output to an arbitrary absolute path
* ./path/to/this/dir/generate-endpoint-response-json.js --output=/home/mordor/output.json
*
* These command-line flags may be combined, and you will usually want to use
* --endpoint alongside --output to download your own JSON into your own directory.
*/
'use strict';

var agent = require( 'superagent' );
var fs = require( 'fs' );
var path = require( 'path' );
var simplifyObject = require( './simplify-object' );

// Parse the arguments object
var argv = require( 'minimist' )( process.argv.slice( 2 ) );

// The output directory defaults to this directory. To customize it specify your
// own directory with --output=your/output/directory (relative or absolute)
var outputPath = argv.output ?
// Nested ternary, don't try this at home: this is to support absolute paths
argv.output[ 0 ] === '/' ? argv.output : path.join( process.cwd(), argv.output ) :
path.dirname( __filename );

// Specify your own API endpoint with --endpoint=http://your-endpoint.com/wp-json
var endpoint = argv.endpoint || 'http://wpapi.loc/wp-json';

// This directory will be called to kick off the JSON download: it uses
// superagent internally for HTTP transport that respects HTTP redirects.
function getJSON( cbFn ) {
agent
.get( endpoint )
.set( 'Accept', 'application/json' )
.end(function( err, res ) {
// Inspect the error and then the response to infer various error states
if ( err ) {
console.error( '\nSomething went wrong! Could not download endpoint JSON.' );
if ( err.status ) {
console.error( 'Error ' + err.status );
}
if ( err.response && err.response.error ) {
console.error( err.response.error );
}
return process.exit( 1 );
}

if ( res.type !== 'application/json' ) {
console.error( '\nError: expected response type "application/json", got ' + res.type );
console.error( 'Could not save endpoint-response.json' );
return process.exit( 1 );
}

cbFn( res );
});
}

// The only assumption we want to make about the URL is that it should be a web
// URL of _some_ sort, which generally means it has "http" in it somewhere. We
// can't assume much else due to how customizable the location of API root is
// within your WP install.
if ( ! /http/i.test( endpoint ) ) {
console.error( '\nError: ' + endpoint );
console.error( 'This does not appear to be a valid URL. Please double-check the URL format\n' +
'(should be e.g. "http://your-domain.com/wp-json") and try again.' );
process.exit( 1 );
}

fs.stat( outputPath, function( err, stats ) {
if ( err || ! stats.isDirectory() ) {
console.error( '\nError: ' + outputPath );
console.error( 'This is not a valid directory. Please double-check the path and try again.' );
process.exit( 1 );
}

// If we made it this far, our arguments look good! Carry on.
getJSON(function( response ) {
// Extract the JSON
var endpointJSON = JSON.parse( JSON.stringify( response.body ) );
var slimJSON = simplifyObject( endpointJSON );

var outputFilePath = path.join( outputPath, 'endpoint-response.json' );
fs.writeFile( outputFilePath, JSON.stringify( slimJSON ), function( err ) {
if ( err ) {
console.error( '\nSomething went wrong! Could not save ' + outputFilePath );
return process.exit( 1 );
}
console.log( '\nSuccessfully saved ' + outputFilePath );
});
});
});
55 changes: 55 additions & 0 deletions lib/data/simplify-object.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
'use strict';

/**
* Walk through the keys and values of a provided object, removing any properties
* which would be inessential to the generation of the route tree used to deduce
* route handlers from a `wp-json/` root API endpoint. This module is not used by
* the wordpress-rest-api module itself, but is rather a dependency of the script
* that is used to create the `endpoint-response.json` file that is shipped along
* with this module for use in generating the "default" routes.
*
* @param {*} obj An arbitrary JS value, probably an object
* @returns {*} The passed-in value, with non-essential args properties and all
* _links properties removes.
*/
function simplifyObject( obj ) {
// Pass through falsy values, Dates and RegExp values without modification
if ( ! obj || obj instanceof Date || obj instanceof RegExp ) {
return obj;
}

// Map arrays through simplifyObject
if ( Array.isArray( obj ) ) {
return obj.map( simplifyObject );
}

// Reduce through objects to run each property through simplifyObject
if ( typeof obj === 'object' ) {
return Object.keys( obj ).reduce(function( newObj, key ) {
// Omit _links objects entirely
if ( key === '_links' ) {
return newObj;
}

// If the key is "args", omit all keys of second-level descendants
// other than "required"
if ( key === 'args' ) {
newObj.args = Object.keys( obj.args ).reduce(function( slimArgs, arg ) {
slimArgs[ arg ] = {
required: obj.args[ arg ].required
};
return slimArgs;
}, {});
} else {
// Pass all other objects through simplifyObject
newObj[ key ] = simplifyObject( obj[ key ] );
}
return newObj;
}, {});
}

// All other types pass through without modification
return obj;
}

module.exports = simplifyObject;
4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,11 @@
"wordpress"
],
"scripts": {
"download-endpoint-json": "node ./lib/data/generate-endpoint-response-json",
"docs": "grunt yuidoc",
"jshint": "jshint --reporter=node_modules/jshint-stylish/index.js Gruntfile.js wp.js lib tests",
"jscs": "jscs Gruntfile.js wp.js lib tests --reporter node_modules/jscs-stylish/jscs-stylish.js",
"lint": "npm run jshint && npm run jscs",
"lint": "npm run jshint && npm run jscs || true",
"mocha": "_mocha tests --recursive --reporter=nyan",
"watch": "grunt watch",
"test:all": "_mocha tests --recursive --reporter=nyan",
Expand Down Expand Up @@ -60,6 +61,7 @@
"jshint": "^2.9.2",
"jshint-stylish": "^2.2.0",
"load-grunt-tasks": "^0.6.0",
"minimist": "^1.2.0",
"mocha": "^1.21.5",
"sandboxed-module": "^1.0.3",
"sinon": "^1.17.2",
Expand Down
1 change: 1 addition & 0 deletions tests/unit/lib/data/posts-collection-route-definition.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"namespace":"wp/v2","methods":["GET","POST"],"endpoints":[{"methods":["GET"],"args":{"context":{"required":false,"default":"view","enum":["view","embed","edit"],"description":"Scope under which the request is made; determines fields present in response."},"page":{"required":false,"default":1,"description":"Current page of the collection."},"per_page":{"required":false,"default":10,"description":"Maximum number of items to be returned in result set."},"search":{"required":false,"description":"Limit results to those matching a string."},"after":{"required":false,"description":"Limit response to resources published after a given ISO8601 compliant date."},"author":{"required":false,"default":[],"description":"Limit result set to posts assigned to specific authors."},"author_exclude":{"required":false,"default":[],"description":"Ensure result set excludes posts assigned to specific authors."},"before":{"required":false,"description":"Limit response to resources published before a given ISO8601 compliant date."},"exclude":{"required":false,"default":[],"description":"Ensure result set excludes specific ids."},"include":{"required":false,"default":[],"description":"Limit result set to specific ids."},"offset":{"required":false,"description":"Offset the result set by a specific number of items."},"order":{"required":false,"default":"desc","enum":["asc","desc"],"description":"Order sort attribute ascending or descending."},"orderby":{"required":false,"default":"date","enum":["date","id","include","title","slug"],"description":"Sort collection by object attribute."},"slug":{"required":false,"description":"Limit result set to posts with a specific slug."},"status":{"required":false,"default":"publish","description":"Limit result set to posts assigned a specific status."},"filter":{"required":false,"description":"Use WP Query arguments to modify the response; private query vars require appropriate authorization."},"categories":{"required":false,"default":[],"description":"Limit result set to all items that have the specified term assigned in the categories taxonomy."},"tags":{"required":false,"default":[],"description":"Limit result set to all items that have the specified term assigned in the tags taxonomy."}}},{"methods":["POST"],"args":{"date":{"required":false,"description":"The date the object was published, in the site's timezone."},"date_gmt":{"required":false,"description":"The date the object was published, as GMT."},"password":{"required":false,"description":"A password to protect access to the post."},"slug":{"required":false,"description":"An alphanumeric identifier for the object unique to its type."},"status":{"required":false,"enum":["publish","future","draft","pending","private"],"description":"A named status for the object."},"title":{"required":false,"description":"The title for the object."},"content":{"required":false,"description":"The content for the object."},"author":{"required":false,"description":"The id for the author of the object."},"excerpt":{"required":false,"description":"The excerpt for the object."},"featured_media":{"required":false,"description":"The id of the featured media for the object."},"comment_status":{"required":false,"enum":["open","closed"],"description":"Whether or not comments are open on the object."},"ping_status":{"required":false,"enum":["open","closed"],"description":"Whether or not the object can be pinged."},"format":{"required":false,"enum":["standard","aside","chat","gallery","link","image","quote","status","video","audio"],"description":"The format for the object."},"sticky":{"required":false,"description":"Whether or not the object should be treated as sticky."},"categories":{"required":false,"description":"The terms assigned to the object in the category taxonomy."},"tags":{"required":false,"description":"The terms assigned to the object in the post_tag taxonomy."}}}],"_links":{"self":"http://wpapi.loc/wp-json/wp/v2/posts"}}
137 changes: 137 additions & 0 deletions tests/unit/lib/data/simplify-object.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
'use strict';
var expect = require( 'chai' ).expect;

var simplifyObject = require( '../../../../lib/data/simplify-object' );

var fullPostsCollectionRouteDefinition = require( './posts-collection-route-definition.json' );

describe( 'simplifyObject', function() {

it( 'is a function', function() {
expect( simplifyObject ).to.be.a( 'function' );
});

it( 'passes through strings without modification', function() {
expect( simplifyObject( 'foo' ) ).to.be.a( 'string' );
expect( simplifyObject( 'foo' ) ).to.equal( 'foo' );
});

it( 'passes through numbers without modification', function() {
expect( simplifyObject( 7 ) ).to.be.a( 'number' );
expect( simplifyObject( 7 ) ).to.equal( 7 );
});

it( 'passes through booleans without modification', function() {
expect( simplifyObject( true ) ).to.be.a( 'boolean' );
expect( simplifyObject( true ) ).to.equal( true );
});

it( 'passes through arrays of simple values without modification', function() {
expect( simplifyObject([]) ).to.be.an( 'array' );
expect( simplifyObject([ 1, 2, 3 ]) ).to.deep.equal([ 1, 2, 3 ]);
expect( simplifyObject([ 'a', 'b', 'c' ]) ).to.deep.equal([ 'a', 'b', 'c' ]);
expect( simplifyObject([ true, false ]) ).to.deep.equal([ true, false ]);
});

it( 'passes through most objects without modification', function() {
expect( simplifyObject({
some: 'set',
of: 'basic',
nested: {
properties: [ 'of', 'no', {
particular: 'consequence'
} ],
nr: 7
}
}) ).to.deep.equal({
some: 'set',
of: 'basic',
nested: {
properties: [ 'of', 'no', {
particular: 'consequence'
} ],
nr: 7
}
});
});

it( 'strips out _links properties', function() {
expect( simplifyObject({
some: 'object with a',
_links: {
prop: 'within it'
}
}) ).to.deep.equal({
some: 'object with a'
});
});

it( 'removes non-`.required` keys from children of .args objects', function() {
expect( simplifyObject({
args: {
context: {
required: false,
other: 'properties',
go: 'here'
}
}
}) ).to.deep.equal({
args: {
context: {
required: false
}
}
});
});

it( 'properly transforms a full route definition object', function() {
expect( simplifyObject( fullPostsCollectionRouteDefinition ) ).to.deep.equal({
namespace: 'wp/v2',
methods: [ 'GET', 'POST' ],
endpoints: [ {
methods: [ 'GET' ],
args: {
context: { required: false },
page: { required: false },
per_page: { required: false },
search: { required: false },
after: { required: false },
author: { required: false },
author_exclude: { required: false },
before: { required: false },
exclude: { required: false },
include: { required: false },
offset: { required: false },
order: { required: false },
orderby: { required: false },
slug: { required: false },
status: { required: false },
filter: { required: false },
categories: { required: false },
tags: { required: false }
}
}, {
methods: [ 'POST' ],
args: {
date: { required: false },
date_gmt: { required: false },
password: { required: false },
slug: { required: false },
status: { required: false },
title: { required: false },
content: { required: false },
author: { required: false },
excerpt: { required: false },
featured_media: { required: false },
comment_status: { required: false },
ping_status: { required: false },
format: { required: false },
sticky: { required: false },
categories: { required: false },
tags: { required: false }
}
} ]
});
});

});