diff --git a/lib/comments.js b/lib/comments.js deleted file mode 100644 index 5ecb9c38..00000000 --- a/lib/comments.js +++ /dev/null @@ -1,156 +0,0 @@ -'use strict'; -/** - * @module WP - * @submodule CommentsRequest - * @beta - */ -var CollectionRequest = require( './shared/collection-request' ); -var pick = require( 'lodash' ).pick; -var extend = require( 'node.extend' ); -var inherit = require( 'util' ).inherits; - -var parameters = require( './mixins/parameters' ); - -/** - * CommentsRequest extends CollectionRequest to handle the /comments API endpoint - * - * @class CommentsRequest - * @constructor - * @extends CollectionRequest - * @param {Object} options A hash of options for the CommentsRequest instance - * @param {String} options.endpoint The endpoint URI for the invoking WP instance - * @param {String} [options.username] A username for authenticating API requests - * @param {String} [options.password] A password for authenticating API requests - */ -function CommentsRequest( options ) { - /** - * Configuration options for the request such as the endpoint for the invoking WP instance - * @property _options - * @type Object - * @private - * @default {} - */ - this._options = options || {}; - - /** - * A hash of non-filter query parameters - * - * @property _params - * @type Object - * @private - * @default {} - */ - this._params = {}; - - /** - * A hash of values to assemble into the API request path - * - * @property _path - * @type Object - * @private - * @default {} - */ - this._path = {}; - - /** - * The URL template that will be used to assemble endpoint paths - * - * @property _template - * @type String - * @protected - * @default 'comments(/:id)' - */ - this._template = 'comments(/:id)'; - - /** - * @property _supportedMethods - * @type Array - * @private - * @default [ 'head', 'get', 'post' ] - */ - this._supportedMethods = [ 'head', 'get', 'post' ]; - - // Default all .comments() requests to assume a query against the WP API v2 endpoints - this.namespace( 'wp/v2' ); -} - -// CommentsRequest extends CollectionRequest -inherit( CommentsRequest, CollectionRequest ); - -// Mixins -extend( CommentsRequest.prototype, pick( parameters, [ - 'parent', - 'forPost' -] ) ); - -/** - * A hash table of path keys and regex validators for those path elements - * - * @property _pathValidators - * @type Object - * @private - */ -CommentsRequest.prototype._pathValidators = { - - /** - * ID must be an integer - * - * @property _pathValidators.id - * @type {RegExp} - */ - id: /^\d+$/ -}; - -/** - * Specify a post ID to query - * - * @method id - * @chainable - * @param {Number} id The ID of a post to retrieve - * @return {CommentsRequest} The CommentsRequest instance (for chaining) - */ -CommentsRequest.prototype.id = function( id ) { - this._path.id = parseInt( id, 10 ); - this._supportedMethods = [ 'head', 'get', 'put', 'post', 'delete' ]; - - return this; -}; - -/** - * Specify the name of the taxonomy collection to query - * - * The collections will not be a strict match to defined comments: *e.g.*, to - * get the list of terms for the taxonomy "category," you must specify the - * collection name "categories" (similarly, specify "tags" to get a list of terms - * for the "post_tag" taxonomy). - * - * To get the dictionary of all available comments, specify the collection - * "taxonomy" (slight misnomer: this case will return an object, not the array - * that would usually be expected with a "collection" request). - * - * @method collection - * @chainable - * @param {String} taxonomyCollection The name of the taxonomy collection to query - * @return {CommentsRequest} The CommentsRequest instance (for chaining) - */ -CommentsRequest.prototype.collection = function( taxonomyCollection ) { - this._path.collection = taxonomyCollection; - - return this; -}; - -/** - * Specify a taxonomy term to request - * - * @method term - * @chainable - * @param {String} term The ID or slug of the term to request - * @return {CommentsRequest} The CommentsRequest instance (for chaining) - */ -CommentsRequest.prototype.term = function( term ) { - this._path.term = term; - - return this; -}; - -module.exports = CommentsRequest; diff --git a/lib/shared/wp-request.js b/lib/constructors/wp-request.js similarity index 82% rename from lib/shared/wp-request.js rename to lib/constructors/wp-request.js index 8acdfead..1da54df9 100644 --- a/lib/shared/wp-request.js +++ b/lib/constructors/wp-request.js @@ -8,7 +8,6 @@ /*jshint -W079 */// Suppress warning about redefiniton of `Promise` var Promise = require( 'bluebird' ); var agent = require( 'superagent' ); -var Route = require( 'route-parser' ); var parseLinkHeader = require( 'li' ).parse; var url = require( 'url' ); var qs = require( 'qs' ); @@ -16,7 +15,7 @@ var _ = require( 'lodash' ); var extend = require( 'node.extend' ); // TODO: reorganize library so that this has a better home -var alphaNumericSort = require( '../lib/alphanumeric-sort' ); +var alphaNumericSort = require( '../util/alphanumeric-sort' ); /** * WPRequest is the base API request object constructor @@ -71,17 +70,6 @@ function WPRequest( options ) { * @default {} */ this._path = {}; - - /** - * The URL template that will be used to assemble endpoint paths - * (This will be overwritten by each specific endpoint handler constructor) - * - * @property _template - * @type String - * @private - * @default '' - */ - this._template = ''; } // Private helper methods @@ -158,45 +146,14 @@ function returnHeaders( result ) { return result.headers; } -/** - * Check path parameter values against validation regular expressions - * - * @param {Object} pathValues A hash of path placeholder keys and their corresponding values - * @param {Object} validators A hash of placeholder keys to validation regexes - * @return {Object} Returns pathValues if all validation passes (else will throw) - */ -function validatePath( pathValues, validators ) { - if ( ! validators ) { - return pathValues; - } - for ( var param in pathValues ) { - if ( ! pathValues.hasOwnProperty( param ) ) { - continue; - } - - // No validator, no problem - if ( ! validators[ param ] ) { - continue; - } - - // Convert parameter to a string value and check it against the regex - if ( ! ( pathValues[ param ] + '' ).match( validators[ param ] ) ) { - throw new Error( param + ' does not match ' + validators[ param ] ); - } - } - - // If validation passed, return the pathValues object - return pathValues; -} - /** * Process arrays of taxonomy terms into query parameters. * All terms listed in the arrays will be required (AND behavior). * * This method will not be called with any values unless we are handling - * a CollectionRequest or one of its descendants; however, since parameter - * handling (and therefore `_renderQuery()`) are part of WPRequest itself, - * this helper method lives here alongside the code where it is used. + * an endpoint with the filter mixin; however, since parameter handling + * (and therefore `_renderQuery()`) are part of WPRequest itself, this + * helper method lives here alongside the code where it is used. * * @example * prepareTaxonomies({ @@ -254,6 +211,42 @@ function populated( obj ) { }, {}); } +/** + * Assert whether a provided URL component is "valid" by checking it against + * an array of registered path component validator methods for that level of + * the URL path. + * + * @param {object[]} levelDefinitions An array of Level Definition objects + * @param {string} levelContents The URL path string that has been specified + * for use on the provided level + * @returns {boolean} Whether the provided input matches any of the provided + * level validation functions + */ +function validatePathLevel( levelDefinitions, levelContents ) { + // One "level" may have multiple options, as a route tree is a branching + // structure. We consider a level "valid" if the provided levelContents + // match any of the available validators. + var valid = levelDefinitions.reduce(function( anyOptionValid, levelOption ) { + if ( ! levelOption.validate ) { + // If there is no validator function, the level is implicitly valid + return true; + } + return anyOptionValid || levelOption.validate( levelContents ); + }, false ); + + if ( ! valid ) { + throw new Error([ + 'Invalid path component:', + levelContents, + // awkward pluralization support: + 'does not match' + ( levelDefinitions.length > 1 ? ' any of' : '' ), + levelDefinitions.reduce(function( components, levelOption ) { + return components.concat( levelOption.component ); + }, [] ).join( ', ' ) + ].join( ' ' ) ); + } +} + // Pagination-Related Helpers // ========================== @@ -380,6 +373,83 @@ WPRequest.prototype._renderQuery = function() { return ( queryString === '' ) ? '' : '?' + queryString; }; +/** + * Set a component of the resource URL itself (as opposed to a query parameter) + * + * If a path component has already been set at this level, throw an error: + * requests are meant to be transient, so any re-writing of a previously-set + * path part value is likely to be a mistake. + * + * @method setPathPart + * @chainable + * @param {Number|String} level A "level" of the path to set, e.g. "1" or "2" + * @param {Number|String} val The value to set at that path part level + * @return {WPRequest} The WPRequest instance (for chaining) + */ +WPRequest.prototype.setPathPart = function( level, val ) { + if ( this._path[ level ] ) { + throw new Error( 'Cannot overwrite value ' + this._path[ level ] ); + } + this._path[ level ] = val; + + return this; +}; + +/** + * Validate whether the specified path parts are valid for this endpoint + * + * "Path parts" are non-query-string URL segments, like "some" "path" in the URL + * `mydomain.com/some/path?and=a&query=string&too`. Because a well-formed path + * is necessary to execute a successful API request, we throw an error if the + * user has omitted a value (such as `/some/[missing component]/url`) or has + * provided a path part value that does not match the regular expression the + * API uses to goven that segment. + * + * @method validatePath + * @chainable + * @returns {WPRequest} The WPRequest instance (for chaining), if no errors were found + */ +WPRequest.prototype.validatePath = function() { + // Iterate through all _specified_ levels of this endpoint + var specifiedLevels = Object.keys( this._path ) + .map(function( level ) { + return parseInt( level, 10 ); + }) + .filter(function( pathPartKey ) { + return ! isNaN( pathPartKey ); + }); + + var maxLevel = Math.max.apply( null, specifiedLevels ); + + // Ensure that all necessary levels are specified + var path = []; + var valid = true; + + for ( var level = 0; level <= maxLevel; level++ ) { + + if ( ! this._levels || ! this._levels[ level ] ) { + continue; + } + + if ( this._path[ level ] ) { + // Validate the provided path level against all available path validators + validatePathLevel( this._levels[ level ], this._path[ level ] ); + + // Add the path value to the array + path.push( this._path[ level ] ); + } else { + path.push( ' ??? ' ); + valid = false; + } + } + + if ( ! valid ) { + throw new Error( 'Incomplete URL! Missing component: ' + path.join( '/' ) ); + } + + return this; +}; + /** * Set a parameter to render into the final query URI. * @@ -512,16 +582,30 @@ WPRequest.prototype._checkMethodSupport = function( method ) { * @return {String} The rendered path */ WPRequest.prototype._renderPath = function() { + // Call validatePath: if the provided path components are not well-formed, + // an error will be thrown + this.validatePath(); + + var pathParts = this._path; + var orderedPathParts = Object.keys( pathParts ) + .sort(function( a, b ) { + var intA = parseInt( a, 10 ); + var intB = parseInt( b, 10 ); + if ( isNaN( intA ) && isNaN( intB ) ) { + return intA - intB; + } + }) + .map(function( pathPartKey ) { + return pathParts[ pathPartKey ]; + }); + // Combine all parts of the path together, filtered to omit any components // that are unspecified or empty strings, to create the full path template - var template = [ - this._namespace, - this._template - ].filter( identity ).join( '/' ); - var path = new Route( template ); - var pathValues = validatePath( this._path, this._pathValidators ); - - return path.reverse( pathValues ) || ''; + var path = [ + this._namespace + ].concat( orderedPathParts ).filter( identity ).join( '/' ); + + return path; }; /** diff --git a/lib/data/endpoint-response.json b/lib/data/endpoint-response.json new file mode 100644 index 00000000..d7f28fb1 --- /dev/null +++ b/lib/data/endpoint-response.json @@ -0,0 +1 @@ +{"name":"WP-API Testbed","description":"Just another WordPress site","url":"http://wpapi.loc/wp","home":"http://wpapi.loc","namespaces":["wp/v2","oembed/1.0"],"authentication":{"oauth1":{"request":"http://wpapi.loc/oauth1/request","authorize":"http://wpapi.loc/oauth1/authorize","access":"http://wpapi.loc/oauth1/access","version":"0.1"}},"routes":{"/":{"namespace":"","methods":["GET"],"endpoints":[{"methods":["GET"],"args":{"context":{"required":false,"default":"view"}}}],"_links":{"self":"http://wpapi.loc/wp-json/"}},"/wp/v2":{"namespace":"wp/v2","methods":["GET"],"endpoints":[{"methods":["GET"],"args":{"namespace":{"required":false,"default":"wp/v2"},"context":{"required":false,"default":"view"}}}],"_links":{"self":"http://wpapi.loc/wp-json/wp/v2"}},"/wp/v2/posts":{"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"}},"/wp/v2/posts/(?P[\\d]+)":{"namespace":"wp/v2","methods":["GET","POST","PUT","PATCH","DELETE"],"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."}}},{"methods":["POST","PUT","PATCH"],"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."}}},{"methods":["DELETE"],"args":{"force":{"required":false,"default":false,"description":"Whether to bypass trash and force deletion."}}}]},"/wp/v2/posts/(?P[\\d]+)/revisions":{"namespace":"wp/v2","methods":["GET"],"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."}}}]},"/wp/v2/posts/(?P[\\d]+)/revisions/(?P[\\d]+)":{"namespace":"wp/v2","methods":["GET","DELETE"],"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."}}},{"methods":["DELETE"],"args":[]}]},"/wp/v2/pages":{"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."},"menu_order":{"required":false,"description":"Limit result set to resources with a specific menu_order value."},"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","menu_order"],"description":"Sort collection by object attribute."},"parent":{"required":false,"default":[],"description":"Limit result set to those of particular parent ids."},"parent_exclude":{"required":false,"default":[],"description":"Limit result set to all items except those of a particular parent id."},"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."}}},{"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."},"parent":{"required":false,"description":"The id for the parent of 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."},"menu_order":{"required":false,"description":"The order of the object in relation to other object of its type."},"template":{"required":false,"enum":[],"description":"The theme file to use to display the object."}}}],"_links":{"self":"http://wpapi.loc/wp-json/wp/v2/pages"}},"/wp/v2/pages/(?P[\\d]+)":{"namespace":"wp/v2","methods":["GET","POST","PUT","PATCH","DELETE"],"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."}}},{"methods":["POST","PUT","PATCH"],"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."},"parent":{"required":false,"description":"The id for the parent of 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."},"menu_order":{"required":false,"description":"The order of the object in relation to other object of its type."},"template":{"required":false,"enum":[],"description":"The theme file to use to display the object."}}},{"methods":["DELETE"],"args":{"force":{"required":false,"default":false,"description":"Whether to bypass trash and force deletion."}}}]},"/wp/v2/pages/(?P[\\d]+)/revisions":{"namespace":"wp/v2","methods":["GET"],"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."}}}]},"/wp/v2/pages/(?P[\\d]+)/revisions/(?P[\\d]+)":{"namespace":"wp/v2","methods":["GET","DELETE"],"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."}}},{"methods":["DELETE"],"args":[]}]},"/wp/v2/media":{"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."},"parent":{"required":false,"default":[],"description":"Limit result set to those of particular parent ids."},"parent_exclude":{"required":false,"default":[],"description":"Limit result set to all items except those of a particular parent id."},"slug":{"required":false,"description":"Limit result set to posts with a specific slug."},"status":{"required":false,"default":"inherit","enum":["inherit","private","trash"],"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."},"media_type":{"required":false,"enum":["image","video","text","application","audio"],"description":"Limit result set to attachments of a particular media type."},"mime_type":{"required":false,"description":"Limit result set to attachments of a particular mime type."}}},{"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."},"author":{"required":false,"description":"The id for the author of 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."},"alt_text":{"required":false,"description":"Alternative text to display when resource is not displayed."},"caption":{"required":false,"description":"The caption for the resource."},"description":{"required":false,"description":"The description for the resource."},"post":{"required":false,"description":"The id for the associated post of the resource."}}}],"_links":{"self":"http://wpapi.loc/wp-json/wp/v2/media"}},"/wp/v2/media/(?P[\\d]+)":{"namespace":"wp/v2","methods":["GET","POST","PUT","PATCH","DELETE"],"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."}}},{"methods":["POST","PUT","PATCH"],"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."},"author":{"required":false,"description":"The id for the author of 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."},"alt_text":{"required":false,"description":"Alternative text to display when resource is not displayed."},"caption":{"required":false,"description":"The caption for the resource."},"description":{"required":false,"description":"The description for the resource."},"post":{"required":false,"description":"The id for the associated post of the resource."}}},{"methods":["DELETE"],"args":{"force":{"required":false,"default":false,"description":"Whether to bypass trash and force deletion."}}}]},"/wp/v2/types":{"namespace":"wp/v2","methods":["GET"],"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."}}}],"_links":{"self":"http://wpapi.loc/wp-json/wp/v2/types"}},"/wp/v2/types/(?P[\\w-]+)":{"namespace":"wp/v2","methods":["GET"],"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."}}}]},"/wp/v2/statuses":{"namespace":"wp/v2","methods":["GET"],"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."}}}],"_links":{"self":"http://wpapi.loc/wp-json/wp/v2/statuses"}},"/wp/v2/statuses/(?P[\\w-]+)":{"namespace":"wp/v2","methods":["GET"],"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."}}}]},"/wp/v2/taxonomies":{"namespace":"wp/v2","methods":["GET"],"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."},"type":{"required":false,"description":"Limit results to resources associated with a specific post type."}}}],"_links":{"self":"http://wpapi.loc/wp-json/wp/v2/taxonomies"}},"/wp/v2/taxonomies/(?P[\\w-]+)":{"namespace":"wp/v2","methods":["GET"],"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."}}}]},"/wp/v2/categories":{"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."},"exclude":{"required":false,"default":[],"description":"Ensure result set excludes specific ids."},"include":{"required":false,"default":[],"description":"Limit result set to specific ids."},"order":{"required":false,"default":"asc","enum":["asc","desc"],"description":"Order sort attribute ascending or descending."},"orderby":{"required":false,"default":"name","enum":["id","include","name","slug","term_group","description","count"],"description":"Sort collection by resource attribute."},"hide_empty":{"required":false,"default":false,"description":"Whether to hide resources not assigned to any posts."},"parent":{"required":false,"description":"Limit result set to resources assigned to a specific parent."},"post":{"required":false,"description":"Limit result set to resources assigned to a specific post."},"slug":{"required":false,"description":"Limit result set to resources with a specific slug."}}},{"methods":["POST"],"args":{"description":{"required":false,"description":"HTML description of the resource."},"name":{"required":true,"description":"HTML title for the resource."},"slug":{"required":false,"description":"An alphanumeric identifier for the resource unique to its type."},"parent":{"required":false,"description":"The id for the parent of the resource."}}}],"_links":{"self":"http://wpapi.loc/wp-json/wp/v2/categories"}},"/wp/v2/categories/(?P[\\d]+)":{"namespace":"wp/v2","methods":["GET","POST","PUT","PATCH","DELETE"],"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."}}},{"methods":["POST","PUT","PATCH"],"args":{"description":{"required":false,"description":"HTML description of the resource."},"name":{"required":false,"description":"HTML title for the resource."},"slug":{"required":false,"description":"An alphanumeric identifier for the resource unique to its type."},"parent":{"required":false,"description":"The id for the parent of the resource."}}},{"methods":["DELETE"],"args":{"force":{"required":false,"default":false,"description":"Required to be true, as resource does not support trashing."}}}]},"/wp/v2/tags":{"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."},"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":"asc","enum":["asc","desc"],"description":"Order sort attribute ascending or descending."},"orderby":{"required":false,"default":"name","enum":["id","include","name","slug","term_group","description","count"],"description":"Sort collection by resource attribute."},"hide_empty":{"required":false,"default":false,"description":"Whether to hide resources not assigned to any posts."},"post":{"required":false,"description":"Limit result set to resources assigned to a specific post."},"slug":{"required":false,"description":"Limit result set to resources with a specific slug."}}},{"methods":["POST"],"args":{"description":{"required":false,"description":"HTML description of the resource."},"name":{"required":true,"description":"HTML title for the resource."},"slug":{"required":false,"description":"An alphanumeric identifier for the resource unique to its type."}}}],"_links":{"self":"http://wpapi.loc/wp-json/wp/v2/tags"}},"/wp/v2/tags/(?P[\\d]+)":{"namespace":"wp/v2","methods":["GET","POST","PUT","PATCH","DELETE"],"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."}}},{"methods":["POST","PUT","PATCH"],"args":{"description":{"required":false,"description":"HTML description of the resource."},"name":{"required":false,"description":"HTML title for the resource."},"slug":{"required":false,"description":"An alphanumeric identifier for the resource unique to its type."}}},{"methods":["DELETE"],"args":{"force":{"required":false,"default":false,"description":"Required to be true, as resource does not support trashing."}}}]},"/wp/v2/users":{"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."},"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":"asc","enum":["asc","desc"],"description":"Order sort attribute ascending or descending."},"orderby":{"required":false,"default":"name","enum":["id","include","name","registered_date"],"description":"Sort collection by object attribute."},"slug":{"required":false,"description":"Limit result set to resources with a specific slug."},"roles":{"required":false,"description":"Limit result set to resources matching at least one specific role provided. Accepts csv list or single role."}}},{"methods":["POST"],"args":{"username":{"required":true,"description":"Login name for the resource."},"name":{"required":false,"description":"Display name for the resource."},"first_name":{"required":false,"description":"First name for the resource."},"last_name":{"required":false,"description":"Last name for the resource."},"email":{"required":true,"description":"The email address for the resource."},"url":{"required":false,"description":"URL of the resource."},"description":{"required":false,"description":"Description of the resource."},"nickname":{"required":false,"description":"The nickname for the resource."},"slug":{"required":false,"description":"An alphanumeric identifier for the resource."},"roles":{"required":false,"description":"Roles assigned to the resource."},"password":{"required":true,"description":"Password for the resource (never included)."},"capabilities":{"required":false,"description":"All capabilities assigned to the resource."}}}],"_links":{"self":"http://wpapi.loc/wp-json/wp/v2/users"}},"/wp/v2/users/(?P[\\d]+)":{"namespace":"wp/v2","methods":["GET","POST","PUT","PATCH","DELETE"],"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."}}},{"methods":["POST","PUT","PATCH"],"args":{"username":{"required":false,"description":"Login name for the resource."},"name":{"required":false,"description":"Display name for the resource."},"first_name":{"required":false,"description":"First name for the resource."},"last_name":{"required":false,"description":"Last name for the resource."},"email":{"required":false,"description":"The email address for the resource."},"url":{"required":false,"description":"URL of the resource."},"description":{"required":false,"description":"Description of the resource."},"nickname":{"required":false,"description":"The nickname for the resource."},"slug":{"required":false,"description":"An alphanumeric identifier for the resource."},"roles":{"required":false,"description":"Roles assigned to the resource."},"password":{"required":false,"description":"Password for the resource (never included)."},"capabilities":{"required":false,"description":"All capabilities assigned to the resource."}}},{"methods":["DELETE"],"args":{"force":{"required":false,"default":false,"description":"Required to be true, as resource does not support trashing."},"reassign":{"required":false}}}]},"/wp/v2/users/me":{"namespace":"wp/v2","methods":["GET"],"endpoints":[{"methods":["GET"],"args":{"context":{"required":false}}}],"_links":{"self":"http://wpapi.loc/wp-json/wp/v2/users/me"}},"/wp/v2/comments":{"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,"description":"Limit result set to comments assigned to specific user ids. Requires authorization."},"author_exclude":{"required":false,"description":"Ensure result set excludes comments assigned to specific user ids. Requires authorization."},"author_email":{"required":false,"description":"Limit result set to that from a specific author email. Requires authorization."},"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."},"karma":{"required":false,"description":"Limit result set to that of a particular comment karma. Requires authorization."},"offset":{"required":false,"description":"Offset the result set by a specific number of comments."},"order":{"required":false,"default":"asc","enum":["asc","desc"],"description":"Order sort attribute ascending or descending."},"orderby":{"required":false,"default":"date_gmt","enum":["date","date_gmt","id","include","post","parent","type"],"description":"Sort collection by object attribute."},"parent":{"required":false,"default":[],"description":"Limit result set to resources of specific parent ids."},"parent_exclude":{"required":false,"default":[],"description":"Ensure result set excludes specific parent ids."},"post":{"required":false,"default":[],"description":"Limit result set to resources assigned to specific post ids."},"status":{"required":false,"default":"approve","description":"Limit result set to comments assigned a specific status. Requires authorization."},"type":{"required":false,"default":"comment","description":"Limit result set to comments assigned a specific type. Requires authorization."}}},{"methods":["POST"],"args":{"author":{"required":false,"description":"The id of the user object, if author was a user."},"author_email":{"required":false,"description":"Email address for the object author."},"author_name":{"required":false,"default":"","description":"Display name for the object author."},"author_url":{"required":false,"description":"URL for the object author."},"content":{"required":false,"default":"","description":"The content for the object."},"date":{"required":false,"description":"The date the object was published."},"date_gmt":{"required":false,"description":"The date the object was published as GMT."},"karma":{"required":false,"description":"Karma for the object."},"parent":{"required":false,"default":0,"description":"The id for the parent of the object."},"post":{"required":false,"default":0,"description":"The id of the associated post object."},"status":{"required":false,"description":"State of the object."},"type":{"required":false,"default":"","description":"Type of Comment for the object."}}}],"_links":{"self":"http://wpapi.loc/wp-json/wp/v2/comments"}},"/wp/v2/comments/(?P[\\d]+)":{"namespace":"wp/v2","methods":["GET","POST","PUT","PATCH","DELETE"],"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."}}},{"methods":["POST","PUT","PATCH"],"args":{"author":{"required":false,"description":"The id of the user object, if author was a user."},"author_email":{"required":false,"description":"Email address for the object author."},"author_name":{"required":false,"description":"Display name for the object author."},"author_url":{"required":false,"description":"URL for the object author."},"content":{"required":false,"description":"The content for the object."},"date":{"required":false,"description":"The date the object was published."},"date_gmt":{"required":false,"description":"The date the object was published as GMT."},"karma":{"required":false,"description":"Karma for the object."},"parent":{"required":false,"description":"The id for the parent of the object."},"post":{"required":false,"description":"The id of the associated post object."},"status":{"required":false,"description":"State of the object."},"type":{"required":false,"description":"Type of Comment for the object."}}},{"methods":["DELETE"],"args":{"force":{"required":false,"default":false,"description":"Whether to bypass trash and force deletion."}}}]},"/oembed/1.0":{"namespace":"oembed/1.0","methods":["GET"],"endpoints":[{"methods":["GET"],"args":{"namespace":{"required":false,"default":"oembed/1.0"},"context":{"required":false,"default":"view"}}}],"_links":{"self":"http://wpapi.loc/wp-json/oembed/1.0"}},"/oembed/1.0/embed":{"namespace":"oembed/1.0","methods":["GET"],"endpoints":[{"methods":["GET"],"args":{"url":{"required":true},"format":{"required":false,"default":"json"},"maxwidth":{"required":false,"default":600}}}],"_links":{"self":"http://wpapi.loc/wp-json/oembed/1.0/embed"}}},"_links":{"help":[{"href":"http://v2.wp-api.org/"}]}} diff --git a/lib/endpoint-factories.js b/lib/endpoint-factories.js new file mode 100644 index 00000000..19cc8fd8 --- /dev/null +++ b/lib/endpoint-factories.js @@ -0,0 +1,44 @@ +'use strict'; +/** + * Take a WP route string (with PCRE named capture groups), such as + * @module parseRouteString + */ + +var extend = require( 'node.extend' ); +var createResourceHandlerSpec = require( './resource-handler-spec' ).create; +var createEndpointRequest = require( './endpoint-request' ).create; + +/** + * Given an array of route definitions and a specific namespace for those routes, + * recurse through the node tree representing all possible routes within the + * provided namespace to define path value setters (and corresponding property + * validators) for all possible variants of each resource's API endpoints. + * + * @param {string} namespace The namespace string for these routes + * @param {object} routeDefinitions A dictionary of route definitions from buildRouteTree + * @returns {object} A dictionary of endpoint request handler factories + */ +function generateEndpointFactories( namespace, routeDefinitions ) { + + // Create + return Object.keys( routeDefinitions ).reduce(function( handlers, resource ) { + + var handlerSpec = createResourceHandlerSpec( routeDefinitions[ resource ], resource ); + + var EndpointRequest = createEndpointRequest( handlerSpec, resource, namespace ); + + // "handler" object is now fully prepared; create the factory method that + // will instantiate and return a handler instance + handlers[ resource ] = function( options ) { + options = options || {}; + options = extend( options, this._options ); + return new EndpointRequest( options ); + }; + + return handlers; + }, {} ); +} + +module.exports = { + generate: generateEndpointFactories +}; diff --git a/lib/endpoint-request.js b/lib/endpoint-request.js new file mode 100644 index 00000000..2937b5e2 --- /dev/null +++ b/lib/endpoint-request.js @@ -0,0 +1,56 @@ +'use strict'; + +var inherit = require( 'util' ).inherits; +var WPRequest = require( './constructors/wp-request' ); +var mixins = require( './mixins' ); + +function createEndpointRequest( handlerSpec, resource, namespace ) { + + // Create the constructor function for this endpoint + function EndpointRequest( options ) { + WPRequest.call( this, options ); + + /** + * Semi-private instance property specifying the available URL path options + * for this endpoint request handler, keyed by ascending whole numbers. + * + * @property _levels + * @type {object} + * @private + */ + this._levels = handlerSpec._levels; + + // Configure handler for this endpoint's root URL path & set namespace + this + .setPathPart( 0, resource ) + .namespace( namespace ); + } + + inherit( EndpointRequest, WPRequest ); + + // Mix in all available shortcut methods for GET request query parameters that + // are valid within this endpoint tree + Object.keys( handlerSpec._getArgs ).forEach(function( supportedQueryParam ) { + var mixinsForParam = mixins[ supportedQueryParam ]; + + // Only proceed if there is a mixin available AND the specified mixins will + // not overwrite any previously-set prototype method + if ( mixinsForParam ) { + Object.keys( mixinsForParam ).forEach(function( methodName ) { + if ( ! EndpointRequest.prototype[ methodName ] ) { + EndpointRequest.prototype[ methodName ] = mixinsForParam[ methodName ]; + } + }); + } + }); + + Object.keys( handlerSpec._setters ).forEach(function( setterFnName ) { + EndpointRequest.prototype[ setterFnName ] = handlerSpec._setters[ setterFnName ]; + }); + + return EndpointRequest; +} + +module.exports = { + create: createEndpointRequest +}; diff --git a/lib/media.js b/lib/media.js deleted file mode 100644 index 6df9a575..00000000 --- a/lib/media.js +++ /dev/null @@ -1,106 +0,0 @@ -'use strict'; -/** - * @module WP - * @submodule MediaRequest - * @beta - */ -var CollectionRequest = require( './shared/collection-request' ); -var inherit = require( 'util' ).inherits; - -/** - * MediaRequest extends CollectionRequest to handle the /media API endpoint - * - * @class MediaRequest - * @constructor - * @extends CollectionRequest - * @param {Object} options A hash of options for the MediaRequest instance - * @param {String} options.endpoint The endpoint URI for the invoking WP instance - * @param {String} [options.username] A username for authenticating API requests - * @param {String} [options.password] A password for authenticating API requests - */ -function MediaRequest( options ) { - /** - * Configuration options for the request such as the endpoint for the invoking WP instance - * @property _options - * @type Object - * @private - * @default {} - */ - this._options = options || {}; - - /** - * A hash of non-filter query parameters - * - * @property _params - * @type Object - * @private - * @default {} - */ - this._params = {}; - - /** - * A hash of values to assemble into the API request path - * - * @property _path - * @type Object - * @private - * @default {} - */ - this._path = {}; - - /** - * The URL template that will be used to assemble endpoint paths - * - * @property _template - * @type String - * @protected - * @default 'media(/:id)' - */ - this._template = 'media(/:id)'; - - /** - * @property _supportedMethods - * @type Array - * @private - * @default [ 'head', 'get', 'post' ] - */ - this._supportedMethods = [ 'head', 'get', 'post' ]; - - // Default all .media() requests to assume a query against the WP API v2 endpoints - this.namespace( 'wp/v2' ); -} - -inherit( MediaRequest, CollectionRequest ); - -/** - * A hash table of path keys and regex validators for those path elements - * - * @property _pathValidators - * @type Object - * @private - */ -MediaRequest.prototype._pathValidators = { - - /** - * ID must be an integer or "me" - * - * @property _pathValidators.id - * @type {RegExp} - */ - id: /^\d+$/ -}; - -/** - * @method id - * @chainable - * @param {Number} id The integer ID of a media record - * @return {MediaRequest} The MediaRequest instance (for chaining) - */ -MediaRequest.prototype.id = function( id ) { - this._path.id = parseInt( id, 10 ); - this._supportedMethods = [ 'head', 'get', 'put', 'post', 'delete' ]; - - return this; -}; - -module.exports = MediaRequest; diff --git a/lib/mixins/filters.js b/lib/mixins/filters.js index 0e785da8..318acc6e 100644 --- a/lib/mixins/filters.js +++ b/lib/mixins/filters.js @@ -9,7 +9,7 @@ */ var _ = require( 'lodash' ); var extend = require( 'node.extend' ); -var alphaNumericSort = require( '../lib/alphanumeric-sort' ); +var alphaNumericSort = require( '../util/alphanumeric-sort' ); var filterMixins = {}; diff --git a/lib/mixins/index.js b/lib/mixins/index.js new file mode 100644 index 00000000..807c8868 --- /dev/null +++ b/lib/mixins/index.js @@ -0,0 +1,22 @@ +/** + * This module defines a mapping between supported GET request query parameter + * arguments and their corresponding mixin, if available. + */ +'use strict'; + +var filterMixins = require( './filters' ); +var parameterMixins = require( './parameters' ); + +// `.context`, `.embed`, and `.edit` (a shortcut for `context(edit, true)`) are +// supported by default in WPRequest, as is the base `.param` method. Any GET +// argument parameters not covered here must be set directly by using `.param`. +module.exports = { + author: { author: parameterMixins.author }, + filter: filterMixins, + page: { page: parameterMixins.page }, + parent: { parent: parameterMixins.parent }, + per_page: { perPage: parameterMixins.perPage }, + post: { forPost: parameterMixins.forPost }, + search: { search: parameterMixins.search }, + slug: { slug: parameterMixins.slug, name: parameterMixins.name } +}; diff --git a/lib/pages.js b/lib/pages.js deleted file mode 100644 index f870d19d..00000000 --- a/lib/pages.js +++ /dev/null @@ -1,175 +0,0 @@ -'use strict'; -/** - * @module WP - * @submodule PagesRequest - * @beta - */ -var CollectionRequest = require( './shared/collection-request' ); -var pick = require( 'lodash' ).pick; -var extend = require( 'node.extend' ); -var inherit = require( 'util' ).inherits; - -var filters = require( './mixins/filters' ); - -/** - * PagesRequest extends CollectionRequest to handle the /posts API endpoint - * - * @class PagesRequest - * @constructor - * @extends CollectionRequest - * @param {Object} options A hash of options for the PagesRequest instance - * @param {String} options.endpoint The endpoint URI for the invoking WP instance - * @param {String} [options.username] A username for authenticating API requests - * @param {String} [options.password] A password for authenticating API requests - */ -function PagesRequest( options ) { - /** - * Configuration options for the request such as the endpoint for the invoking WP instance - * @property _options - * @type Object - * @private - * @default {} - */ - this._options = options || {}; - - /** - * A hash of non-filter query parameters - * - * @property _params - * @type Object - * @private - * @default {} - */ - this._params = {}; - - /** - * A hash of values to assemble into the API request path - * - * @property _path - * @type Object - * @private - * @default {} - */ - this._path = {}; - - /** - * The URL template that will be used to assemble endpoint paths - * - * @property _template - * @type String - * @private - * @default 'pages(/:id)(/:action)(/:commentId)' - */ - this._template = 'pages(/:id)(/:action)(/:commentId)'; - - /** - * @property _supportedMethods - * @type Array - * @private - * @default [ 'head', 'get', 'post' ] - */ - this._supportedMethods = [ 'head', 'get', 'post' ]; - - // Default all .pages() requests to assume a query against the WP API v2 endpoints - this.namespace( 'wp/v2' ); -} - -inherit( PagesRequest, CollectionRequest ); - -// Mixins -extend( PagesRequest.prototype, pick( filters, [ - // Specify that we are requesting a page by its path - 'path' -] ) ); - -/** - * A hash table of path keys and regex validators for those path elements - * - * @property _pathValidators - * @type Object - * @private - */ -PagesRequest.prototype._pathValidators = { - - // No validation on "id", since it can be a string path OR a numeric ID - - /** - * Action must be 'comments' or 'revisions' - * - * @property _pathValidators.action - * @type {RegExp} - * @private - */ - action: /(comments|revisions)/, - - /** - * Comment ID must be an integer - * - * @property _pathValidators.commentId - * @type {RegExp} - * @private - */ - commentId: /^\d+$/ -}; - -/** - * Specify a post ID to query - * - * @method id - * @chainable - * @return {PagesRequest} The PagesRequest instance (for chaining) - */ -PagesRequest.prototype.id = function( id ) { - this._path.id = parseInt( id, 10 ); - - this._supportedMethods = [ 'head', 'get', 'put', 'post', 'delete' ]; - - return this; -}; - -/** - * Specify that we are getting the comments for a specific page - * - * @method comments - * @chainable - * @return {PagesRequest} The PagesRequest instance (for chaining) - */ -PagesRequest.prototype.comments = function() { - this._path.action = 'comments'; - this._supportedMethods = [ 'head', 'get' ]; - - return this; -}; - -/** - * Specify a particular comment to retrieve - * (forces action "comments") - * - * @method comment - * @chainable - * @param {Number} id The ID of the comment to retrieve - * @return {PagesRequest} - */ -PagesRequest.prototype.comment = function( id ) { - this._path.action = 'comments'; - this._path.commentId = parseInt( id, 10 ); - this._supportedMethods = [ 'head', 'get', 'delete' ]; - - return this; -}; - -/** - * Specify that we are requesting the revisions for a specific post (forces basic auth) - * - * @method revisions - * @chainable - * @return {PagesRequest} The PagesRequest instance (for chaining) - */ -PagesRequest.prototype.revisions = function() { - this._path.action = 'revisions'; - this._supportedMethods = [ 'head', 'get' ]; - - return this.auth(); -}; - -module.exports = PagesRequest; diff --git a/lib/path-part-setter.js b/lib/path-part-setter.js new file mode 100644 index 00000000..48f4bf1a --- /dev/null +++ b/lib/path-part-setter.js @@ -0,0 +1,85 @@ +'use strict'; + +/** + * Return a function to set part of the request URL path. + * + * Path part setter methods may be either dynamic (*i.e.* may represent a + * "named group") or non-dynamic (representing a static part of the URL, which + * is usually a collection endpoint of some sort). Which type of function is + * returned depends on whether a given route has one or many sub-resources. + * + * @param {[type]} node [description] + * @returns {[type]} [description] + */ +function createPathPartSetter( node ) { + // Local references to `node` properties used by returned functions + var nodeLevel = node.level; + var nodeName = node.names[ 0 ]; + var supportedMethods = node.methods; + var dynamicChildren = node.children ? Object.keys( node.children ) + .map(function( key ) { + return node.children[ key ]; + }) + .filter(function( childNode ) { + return childNode.namedGroup === true; + }) : []; + var dynamicChild = dynamicChildren.length === 1 && dynamicChildren[ 0 ]; + var dynamicChildLevel = dynamicChild && dynamicChild.level; + + if ( node.namedGroup ) { + /** + * Set a dymanic (named-group) path part of a query URL. + * + * @example + * + * // id() is a dynamic path part setter: + * wp.posts().id( 7 ); // Get posts/7 + * + * @chainable + * @param {String|Number} val The path part value to set + * @return {Object} The handler instance (for chaining) + */ + return function( val ) { + /* jshint validthis:true */ + this.setPathPart( nodeLevel, val ); + this._supportedMethods = supportedMethods; + return this; + }; + } else { + /** + * Set a non-dymanic (non-named-group) path part of a query URL, and + * set the value of a subresource if an input value is provided and + * exactly one named-group child node exists. + * + * @example + * + * // revisions() is a non-dynamic path part setter: + * wp.posts().id( 4 ).revisions(); // Get posts/4/revisions + * wp.posts().id( 4 ).revisions( 1372 ); // Get posts/4/revisions/1372 + * + * @chainable + * @param {String|Number} [val] The path part value to set (if provided) + * for a subresource within this resource + * @return {Object} The handler instance (for chaining) + */ + return function( val ) { + /* jshint validthis:true */ + // If the path part is not a namedGroup, it should have exactly one + // entry in the names array: use that as the value for this setter, + // as it will usually correspond to a collection endpoint. + this.setPathPart( nodeLevel, nodeName ); + + // If this node has exactly one dynamic child, this method may act as + // a setter for that child node. `dynamicChildLevel` will be falsy if the + // node does not have a child or has multiple children. + if ( typeof val !== 'undefined' && dynamicChildLevel ) { + this.setPathPart( dynamicChildLevel, val ); + } + return this; + }; + } +} + +module.exports = { + create: createPathPartSetter +}; diff --git a/lib/posts.js b/lib/posts.js deleted file mode 100644 index b0d75cb4..00000000 --- a/lib/posts.js +++ /dev/null @@ -1,153 +0,0 @@ -'use strict'; -/** - * @module WP - * @submodule PostsRequest - * @beta - */ -var CollectionRequest = require( './shared/collection-request' ); -var inherit = require( 'util' ).inherits; - -/** - * PostsRequest extends CollectionRequest to handle the /posts API endpoint - * - * @class PostsRequest - * @constructor - * @extends CollectionRequest - * @param {Object} options A hash of options for the PostsRequest instance - * @param {String} options.endpoint The endpoint URI for the invoking WP instance - * @param {String} [options.username] A username for authenticating API requests - * @param {String} [options.password] A password for authenticating API requests - */ -function PostsRequest( options ) { - /** - * Configuration options for the request such as the endpoint for the invoking WP instance - * @property _options - * @type Object - * @private - * @default {} - */ - this._options = options || {}; - - /** - * A hash of non-filter query parameters - * - * @property _params - * @type Object - * @private - * @default {} - */ - this._params = {}; - - /** - * A hash of values to assemble into the API request path - * - * @property _path - * @type Object - * @private - * @default {} - */ - this._path = {}; - - /** - * The URL template that will be used to assemble endpoint paths - * - * @property _template - * @type String - * @private - * @default 'posts(/:id)(/:action)(/:actionId)' - */ - this._template = 'posts(/:id)(/:action)(/:actionId)'; - - /** - * @property _supportedMethods - * @type Array - * @private - * @default [ 'head', 'get', 'post' ] - */ - this._supportedMethods = [ 'head', 'get', 'post' ]; - - // Default all .posts() requests to assume a query against the WP API v2 endpoints - this.namespace( 'wp/v2' ); -} - -inherit( PostsRequest, CollectionRequest ); - -/** - * A hash table of path keys and regex validators for those path elements - * - * @property _pathValidators - * @type Object - * @private - */ -PostsRequest.prototype._pathValidators = { - - /** - * ID must be an integer - * - * @property _pathValidators.id - * @type {RegExp} - */ - id: /^\d+$/, - - /** - * Action must be one of 'meta' or 'revisions' - * - * @property _pathValidators.action - * @type {RegExp} - */ - action: /(meta|revisions)/ -}; - -/** - * Specify a post ID to query - * - * @method id - * @chainable - * @param {Number} id The ID of a post to retrieve - * @return {PostsRequest} The PostsRequest instance (for chaining) - */ -PostsRequest.prototype.id = function( id ) { - this._path.id = parseInt( id, 10 ); - this._supportedMethods = [ 'head', 'get', 'put', 'post', 'delete' ]; - - return this; -}; - -/** - * Specify that we are retrieving Post Meta (forces basic auth) - * - * Either return a collection of all meta objects for the specified post, - * or (if a meta ID was provided) return the requested meta object. - * - * @method meta - * @chainable - * @param {Number} [metaId] ID of a specific meta property to retrieve - * @return {PostsRequest} The PostsRequest instance (for chainin) - */ -PostsRequest.prototype.meta = function( metaId ) { - this._path.action = 'meta'; - this._supportedMethods = [ 'head', 'get', 'post' ]; - this._path.actionId = parseInt( metaId, 10 ) || null; - - if ( this._path.actionId ) { - this._supportedMethods = [ 'head', 'get', 'put', 'post', 'delete' ]; - } - - return this.auth(); -}; - -/** - * Specify that we are requesting the revisions for a specific post (forces basic auth) - * - * @method revisions - * @chainable - * @return {PostsRequest} The PostsRequest instance (for chaining) - */ -PostsRequest.prototype.revisions = function() { - this._path.action = 'revisions'; - this._supportedMethods = [ 'head', 'get' ]; - - return this.auth(); -}; - -module.exports = PostsRequest; diff --git a/lib/resource-handler-spec.js b/lib/resource-handler-spec.js new file mode 100644 index 00000000..03e8dd5f --- /dev/null +++ b/lib/resource-handler-spec.js @@ -0,0 +1,116 @@ +'use strict'; + +var createPathPartSetter = require( './path-part-setter' ).create; + +function addLevelOption( levelsObj, level, obj ) { + levelsObj[ level ] = levelsObj[ level ] || []; + levelsObj[ level ].push( obj ); +} + +function assignSetterFnForNode( handler, node ) { + var setterFn; + + // For each node, add its handler to the relevant "level" representation + addLevelOption( handler._levels, node.level, { + component: node.component, + validate: node.validate, + methods: node.methods + }); + + // First level is set implicitly, no dedicated setter needed + if ( node.level > 0 ) { + + setterFn = createPathPartSetter( node ); + + node.names.forEach(function( name ) { + // camel-case the setter name + var setterFnName = name + .toLowerCase() + .replace( /_\w/g, function( match ) { + return match.replace( '_', '' ).toUpperCase(); + }); + + // Don't overwrite previously-set methods + if ( ! handler._setters[ setterFnName ] ) { + handler._setters[ setterFnName ] = setterFn; + } + }); + } +} + +/** + * Walk the tree of a specific resource node to create the setter methods + * + * The API we want to produce from the node tree looks like this: + * + * wp.posts(); /wp/v2/posts + * wp.posts().id( 7 ); /wp/v2/posts/7 + * wp.posts().id( 7 ).revisions(); /wp/v2/posts/7/revisions + * wp.posts().id( 7 ).revisions( 8 ); /wp/v2/posts/7/revisions/8 + * + * ^ That last one's the tricky one: we can deduce that this parameter is "id", but + * that param will already be taken by the post ID, so sub-collections have to be + * set up as `.revisions()` to get the collection, and `.revisions( id )` to get a + * specific resource. + * + * @param {Object} node A node object + * @param {Object} [node.children] An object of child nodes + * // @return {isLeaf} A boolean indicating whether the processed node is a leaf + */ +function extractSetterFromNode( handler, node ) { + + assignSetterFnForNode( handler, node ); + + if ( node.children ) { + // Recurse down to this node's children + Object.keys( node.children ).forEach(function( key ) { + extractSetterFromNode( handler, node.children[ key ] ); + }); + } +} + +/** + * Create a node handler specification object from a route definition object + * + * @param {object} routeDefinition A route definition object + * @param {string} resource The string key of the resource for which to create a handler + * @returns {object} A handler spec object with _path, _levels and _setters properties + */ +function createNodeHandlerSpec( routeDefinition, resource ) { + + var handler = { + // A "path" is an ordered set of + _path: { + '0': resource + }, + + // A "level" is a level-keyed object representing the valid options for + // one level of the resource URL + _levels: {}, + + // Objects that hold methods and properties which will be copied to + // instances of this endpoint's handler + _setters: {}, + + // Arguments (query parameters) that may be set in GET requests to endpoints + // nested within this resource route tree, used to determine the mixins to + // add to the request handler + _getArgs: routeDefinition._getArgs + }; + + // Walk the tree + Object.keys( routeDefinition ).forEach(function( routeDefProp ) { + if ( routeDefProp !== '_getArgs' ) { + extractSetterFromNode( handler, routeDefinition[ routeDefProp ] ); + } + }); + + return handler; +} + +module.exports = { + create: createNodeHandlerSpec, + _extractSetterFromNode: extractSetterFromNode, + _assignSetterFnForNode: assignSetterFnForNode, + _addLevelOption: addLevelOption +}; diff --git a/lib/route-tree.js b/lib/route-tree.js new file mode 100644 index 00000000..3e878067 --- /dev/null +++ b/lib/route-tree.js @@ -0,0 +1,183 @@ +'use strict'; + +var namedGroupRegexp = require( './util/named-group-regexp' ); +var ensure = require( './util/ensure' ); + +/** + * Method to use when reducing route components array. + * + * @method _reduceRouteComponents + * @private + * @param {object} routeObj A route definition object (set via .bind partial application) + * @param {object} topLevel The top-level route tree object for this set of routes (set + * via .bind partial application) + * @param {object} parentLevel The memo object, which is mutated as the reducer adds + * a new level handler for each level in the route + * @param {string} component The string defining this route component + * @param {number} idx The index of this component within the components array + * @param {string[]} components The array of all components + * @returns {object} The child object of the level being reduced + */ +function reduceRouteComponents( routeObj, topLevel, parentLevel, component, idx, components ) { + // Check to see if this component is a dynamic URL segment (i.e. defined by + // a named capture group regular expression). namedGroup will be `null` if + // the regexp does not match, or else an array defining the RegExp match, e.g. + // [ + // 'P[\\d]+)', + // 'id', // Name of the group + // '[\\d]+', // regular expression for this URL segment's contents + // index: 15, + // input: '/wp/v2/posts/(?P[\\d]+)' + // ] + var namedGroup = component.match( namedGroupRegexp ); + // Pull out references to the relevant indices of the match, for utility: + // `null` checking is necessary in case the component did not match the RE, + // hence the `namedGroup &&`. + var groupName = namedGroup && namedGroup[ 1 ]; + var groupPattern = namedGroup && namedGroup[ 2 ]; + + // When branching based on a dynamic capture group we used the group's RE + // pattern as the unique identifier: this is done because the same group + // could be assigned different names in different endpoint handlers, e.g. + // "id" for posts/:id vs "parent_id" for posts/:parent_id/revisions. + var levelKey = namedGroup ? groupPattern : component; + + // Level name, on the other hand, would take its value from the group's name + var levelName = namedGroup ? groupName : component; + + // Check whether we have a preexisting node at this level of the tree, and + // create a new level object if not + var currentLevel = parentLevel[ levelKey ] || { + namedGroup: namedGroup ? true : false, + level: idx, + names: [] + }; + + // A level's "name" corresponds to the list of strings which could describe + // an endpoint's component setter functions: "id", "revisions", etc. + if ( currentLevel.names.indexOf( levelName ) < 0 ) { + currentLevel.names.push( levelName ); + } + + // Set the component, so that the validator can throw the appropriate error + currentLevel.component = component; + + // A level's validate method is called to check whether a value being set + // on the request URL is of the proper type for the location in which it + // is specified. If a group pattern was found, the validator checks whether + // the input string exactly matches the group pattern. + var groupPatternRE = new RegExp( groupPattern ? '^' + groupPattern + '$' : component ); + + // Only one validate function is maintained for each node, because each node + // is defined either by a string literal or by a specific regular expression. + currentLevel.validate = function( input ) { + return groupPatternRE.test( input ); + }; + + // Check to see whether to expect more nodes within this branch of the tree, + if ( components[ idx + 1 ] ) { + // and create a "children" object to hold those nodes if necessary + currentLevel.children = currentLevel.children || {}; + } else { + // At leaf nodes, specify the method capabilities of this endpoint + currentLevel.methods = routeObj.methods ? routeObj.methods.map(function( str ) { + return str.toLowerCase(); + }) : []; + // Ensure HEAD is included whenever GET is supported: the API automatically + // adds support for HEAD if you have GET + if ( currentLevel.methods.indexOf( 'get' ) > -1 ) { + currentLevel.methods.push( 'head' ); + } + + // At leaf nodes also flag (at the top level) what arguments are + // available to GET requests, so that we may automatically apply the + // appropriate parameter mixins + if ( routeObj.endpoints ) { + topLevel._getArgs = topLevel._getArgs || {}; + routeObj.endpoints.forEach(function( endpoint ) { + // endpoint.methods will be an array of methods like `[ 'GET' ]`: we + // only care about GET for this exercise. Validating POST and PUT args + // could be useful but is currently deemed to be out-of-scope. + endpoint.methods.forEach(function( method ) { + if ( method.toLowerCase() === 'get' ) { + Object.keys( endpoint.args ).forEach(function( argKey ) { + // For each argument, store whether it is required or not + topLevel._getArgs[ argKey ] = endpoint.args[ argKey ].required; + }); + } + }); + }); + } + + // // Label node with the title of this endpoint's resource, if available + // if ( routeObj.schema && routeObj.schema.title ) { + // currentLevel.title = routeObj.schema.title; + // } + } + + // Return the child node object as the new "level" + parentLevel[ levelKey ] = currentLevel; + return currentLevel.children; +} + +/** + * + * @method _reduceRouteTree + * @private + * @param {object[]} routes An array of route objects (set via .bind partial application) + * @param {object} namespaces The memo object that becomes a dictionary mapping API + * namespaces to an object of the namespace's routes + * @param {string} route The string key of a route in `routes` + * @returns {object} The namespaces dictionary memo object + */ +function reduceRouteTree( routes, namespaces, route ) { + var routeObj = routes[ route ]; + var nsForRoute = routeObj.namespace; + + // Strip the namespace from the route string (all routes should have the + // format `/namespace/other/stuff`) @TODO: Validate this assumption + var routeString = route.replace( '/' + nsForRoute + '/', '' ); + var routeComponents = routeString.split( '/' ); + + // Do not make a namespace group for the API root + // Do not add the namespace root to its own group + // Do not take any action if routeString is empty + if ( ! nsForRoute || '/' + nsForRoute === route || ! routeString ) { + return namespaces; + } + + // Ensure that the namespace object for this namespace exists + ensure( namespaces, nsForRoute, {} ); + + // Get a local reference to namespace object + var ns = namespaces[ nsForRoute ]; + + // The first element of the route tells us what type of resource this route + // is for, e.g. "posts" or "comments": we build one handler per resource + // type, so we group like resource paths together. + var resource = routeComponents[0]; + + // @TODO: This code above currently precludes baseless routes, e.g. + // myplugin/v2/(?P\w+) -- should those be supported? + + // Create an array to represent this resource, and ensure it is assigned + // to the namespace object. The array will structure the "levels" (path + // components and subresource types) of this resource's endpoint handler. + ensure( ns, resource, {} ); + var levels = ns[ resource ]; + + // Recurse through the route components, mutating levels with information about + // each child node encountered while walking through the routes tree and what + // arguments (parameters) are available for GET requests to this endpoint. + routeComponents.reduce( reduceRouteComponents.bind( null, routeObj, levels ), levels ); + + return namespaces; +} + +function buildRouteTree( routes ) { + return Object.keys( routes ).reduce( reduceRouteTree.bind( null, routes ), {} ); +} + +module.exports = { + build: buildRouteTree +}; diff --git a/lib/shared/collection-request.js b/lib/shared/collection-request.js deleted file mode 100644 index f839582f..00000000 --- a/lib/shared/collection-request.js +++ /dev/null @@ -1,128 +0,0 @@ -'use strict'; -/** - * @module WP - * @submodule CollectionRequest - * @beta - */ -var WPRequest = require( './wp-request' ); -var pick = require( 'lodash' ).pick; -var extend = require( 'node.extend' ); -var inherit = require( 'util' ).inherits; - -var filters = require( '../mixins/filters' ); -var parameters = require( '../mixins/parameters' ); - -/** - * CollectionRequest extends WPRequest with properties & methods for filtering collections - * via query parameters. It is the base constructor for most top-level WP instance methods. - * - * @class CollectionRequest - * @constructor - * @extends WPRequest - * @extensionfor PagesRequest - * @extensionfor PostsRequest - * @extensionfor TaxonomiesRequest - * @extensionfor TypesRequest - * @extensionfor UsersRequest - * @param {Object} options A hash of options for the CollectionRequest instance - * @param {String} options.endpoint The endpoint URI for the invoking WP instance - * @param {String} [options.username] A username for authenticating API requests - * @param {String} [options.password] A password for authenticating API requests - */ -function CollectionRequest( options ) { - /** - * Configuration options for the request such as the endpoint for the invoking WP instance - * @property _options - * @type Object - * @private - * @default {} - */ - this._options = options || {}; - - /** - * A hash of filter values to parse into the final request URI - * @property _filters - * @type Object - * @private - * @default {} - */ - this._filters = {}; - - /** - * A hash of taxonomy terms to parse into the final request URI - * @property _taxonomyFilters - * @type Object - * @private - * @default {} - */ - this._taxonomyFilters = {}; - - /** - * A hash of non-filter query parameters - * This is used to store the query values for Type, Page & Context - * - * @property _params - * @type Object - * @private - * @default {} - */ - this._params = {}; - - /** - * A hash of values to assemble into the API request path - * - * @property _path - * @type Object - * @private - * @default {} - */ - this._path = {}; - - /** - * The URL template that will be used to assemble endpoint paths - * - * @property _template - * @type String - * @private - * @default '' - */ - this._template = ''; - - /** - * An array of supported methods; to be overridden by descendent constructors - * @property _supportedMethods - * @type Array - * @private - * @default [ 'head', 'get', 'put', 'post', 'delete' ] - */ - this._supportedMethods = [ 'head', 'get', 'put', 'post', 'delete' ]; -} - -inherit( CollectionRequest, WPRequest ); - -// Mixins -extend( CollectionRequest.prototype, pick( filters, [ - // Dependency of all other filter parameters, as well as parameterMixins.author - 'filter', - // Taxomy handling - 'taxonomy', - 'category', - 'tag', - // Date filter handling - 'year', - 'month', - 'day' -] ) ); - -extend( CollectionRequest.prototype, pick( parameters, [ - // Pagination - 'page', - 'perPage', - // Other query parameters - 'slug', - 'name', - 'search', - 'author' -] ) ); - -module.exports = CollectionRequest; diff --git a/lib/taxonomies.js b/lib/taxonomies.js deleted file mode 100644 index 850d7d52..00000000 --- a/lib/taxonomies.js +++ /dev/null @@ -1,128 +0,0 @@ -'use strict'; -/** - * @module WP - * @submodule TaxonomiesRequest - * @beta - */ -var CollectionRequest = require( './shared/collection-request' ); -var extend = require( 'node.extend' ); -var inherit = require( 'util' ).inherits; -var parameters = require( './mixins/parameters' ); -var pick = require( 'lodash' ).pick; - -/** - * TaxonomiesRequest extends CollectionRequest to handle the /taxonomies API endpoint - * - * @class TaxonomiesRequest - * @constructor - * @extends CollectionRequest - * @param {Object} options A hash of options for the TaxonomiesRequest instance - * @param {String} options.endpoint The endpoint URI for the invoking WP instance - * @param {String} [options.username] A username for authenticating API requests - * @param {String} [options.password] A password for authenticating API requests - */ -function TaxonomiesRequest( options ) { - /** - * Configuration options for the request such as the endpoint for the invoking WP instance - * @property _options - * @type Object - * @private - * @default {} - */ - this._options = options || {}; - - /** - * A hash of non-filter query parameters - * - * @property _params - * @type Object - * @private - * @default {} - */ - this._params = {}; - - /** - * A hash of values to assemble into the API request path - * - * Default to requesting the taxonomies "collection" (dictionary of publicly- - * registered taxonomies) if no other collection is specified - * - * @property _path - * @type Object - * @private - * @default {} - */ - this._path = { collection: 'taxonomies' }; - - /** - * The URL template that will be used to assemble endpoint paths - * - * There is no path validation for taxonomies requests: terms can be numeric - * (categories) or strings (tags), and the list of registered collections is - * not fixed (it can be augmented or modified through plugin and theme behavior). - * - * @property _template - * @type String - * @private - * @default '(:collection)(/:term)' - */ - this._template = '(:collection)(/:term)'; - - /** - * @property _supportedMethods - * @type Array - * @private - * @default [ 'head', 'get' ] - */ - this._supportedMethods = [ 'head', 'get' ]; - - // Default all .taxonomies() requests to assume a query against the WP API v2 endpoints - this.namespace( 'wp/v2' ); -} - -// TaxonomiesRequest extends CollectionRequest -inherit( TaxonomiesRequest, CollectionRequest ); - -/** - * Specify the name of the taxonomy collection to query - * - * The collections will not be a strict match to defined taxonomies: *e.g.*, to - * get the list of terms for the taxonomy "category," you must specify the - * collection name "categories" (similarly, specify "tags" to get a list of terms - * for the "post_tag" taxonomy). - * - * To get the dictionary of all available taxonomies, specify the collection - * "taxonomy" (slight misnomer: this case will return an object, not the array - * that would usually be expected with a "collection" request). - * - * @method collection - * @chainable - * @param {String} taxonomyCollection The name of the taxonomy collection to query - * @return {TaxonomiesRequest} The TaxonomiesRequest instance (for chaining) - */ -TaxonomiesRequest.prototype.collection = function( taxonomyCollection ) { - this._path.collection = taxonomyCollection; - - return this; -}; - -/** - * Specify a taxonomy term to request - * - * @method term - * @chainable - * @param {String} term The ID or slug of the term to request - * @return {TaxonomiesRequest} The TaxonomiesRequest instance (for chaining) - */ -TaxonomiesRequest.prototype.term = function( term ) { - this._path.term = term; - - return this; -}; - -extend( TaxonomiesRequest.prototype, pick( parameters, [ - 'parent', - 'forPost' -] ) ); - -module.exports = TaxonomiesRequest; diff --git a/lib/types.js b/lib/types.js deleted file mode 100644 index 0ac658ca..00000000 --- a/lib/types.js +++ /dev/null @@ -1,90 +0,0 @@ -'use strict'; -/** - * @module WP - * @submodule TypesRequest - * @beta - */ -var CollectionRequest = require( './shared/collection-request' ); -var inherit = require( 'util' ).inherits; - -/** - * TypesRequest extends CollectionRequest to handle the /taxonomies API endpoint - * - * @class TypesRequest - * @constructor - * @extends CollectionRequest - * @param {Object} options A hash of options for the TypesRequest instance - * @param {String} options.endpoint The endpoint URI for the invoking WP instance - * @param {String} [options.username] A username for authenticating API requests - * @param {String} [options.password] A password for authenticating API requests - */ -function TypesRequest( options ) { - /** - * Configuration options for the request such as the endpoint for the invoking WP instance - * @property _options - * @type Object - * @private - * @default {} - */ - this._options = options || {}; - - /** - * A hash of non-filter query parameters - * - * @property _params - * @type Object - * @private - * @default {} - */ - this._params = {}; - - /** - * A hash of values to assemble into the API request path - * - * @property _path - * @type Object - * @private - * @default {} - */ - this._path = {}; - - /** - * The URL template that will be used to assemble request URI paths - * - * @property _template - * @type String - * @private - * @default 'types(/:type)' - */ - this._template = 'types(/:type)'; - - /** - * @property _supportedMethods - * @type Array - * @private - * @default [ 'head', 'get' ] - */ - this._supportedMethods = [ 'head', 'get' ]; - - // Default all .types() requests to assume a query against the WP API v2 endpoints - this.namespace( 'wp/v2' ); -} - -// TypesRequest extends CollectionRequest -inherit( TypesRequest, CollectionRequest ); - -/** - * Specify the name of the type to query - * - * @method type - * @chainable - * @param {String} typeName The name of the type to query - * @return {TypesRequest} The TypesRequest instance (for chaining) - */ -TypesRequest.prototype.type = function( typeName ) { - this._path.type = typeName; - - return this; -}; - -module.exports = TypesRequest; diff --git a/lib/users.js b/lib/users.js deleted file mode 100644 index f5116f6d..00000000 --- a/lib/users.js +++ /dev/null @@ -1,122 +0,0 @@ -'use strict'; -/** - * @module WP - * @submodule UsersRequest - * @beta - */ -var CollectionRequest = require( './shared/collection-request' ); -var inherit = require( 'util' ).inherits; - -/** - * UsersRequest extends CollectionRequest to handle the `/users` API endpoint. The `/users` - * endpoint responds with a 401 error without authentication, so `users()` forces basic auth. - * - * @class UsersRequest - * @constructor - * @extends CollectionRequest - * @param {Object} options A hash of options for the UsersRequest instance - * @param {String} options.endpoint The endpoint URI for the invoking WP instance - * @param {String} [options.username] A username for authenticating API requests - * @param {String} [options.password] A password for authenticating API requests - */ -function UsersRequest( options ) { - /** - * Configuration options for the request such as the endpoint for the invoking WP instance - * @property _options - * @type Object - * @private - * @default {} - */ - this._options = options || {}; - - /** - * A hash of non-filter query parameters - * - * @property _params - * @type Object - * @private - * @default {} - */ - this._params = {}; - - /** - * A hash of values to assemble into the API request path - * - * @property _path - * @type Object - * @private - * @default {} - */ - this._path = {}; - - /** - * The URL template that will be used to assemble endpoint paths - * - * @property _template - * @type String - * @protected - * @default 'users(/:id)' - */ - this._template = 'users(/:id)'; - - /** - * @property _supportedMethods - * @type Array - * @private - * @default [ 'head', 'get', 'post' ] - */ - this._supportedMethods = [ 'head', 'get', 'post' ]; - - // Default all .users() requests to assume a query against the WP API v2 endpoints - this.namespace( 'wp/v2' ); - - // Force authentication on all users requests - return this.auth(); -} - -inherit( UsersRequest, CollectionRequest ); - -/** - * A hash table of path keys and regex validators for those path elements - * - * @property _pathValidators - * @type Object - * @private - */ -UsersRequest.prototype._pathValidators = { - - /** - * ID must be an integer or "me" - * - * @property _pathValidators.id - * @type {RegExp} - */ - id: /(^\d+$|^me$)/ -}; - -/** - * @method me - * @chainable - * @return {UsersRequest} The UsersRequest instance (for chaining) - */ -UsersRequest.prototype.me = function() { - this._path.id = 'me'; - this._supportedMethods = [ 'head', 'get' ]; - - return this; -}; - -/** - * @method id - * @chainable - * @param {Number} id The integer ID of a user record - * @return {UsersRequest} The UsersRequest instance (for chaining) - */ -UsersRequest.prototype.id = function( id ) { - this._path.id = parseInt( id, 10 ); - this._supportedMethods = [ 'head', 'get', 'put', 'post', 'delete' ]; - - return this; -}; - -module.exports = UsersRequest; diff --git a/lib/lib/alphanumeric-sort.js b/lib/util/alphanumeric-sort.js similarity index 100% rename from lib/lib/alphanumeric-sort.js rename to lib/util/alphanumeric-sort.js diff --git a/lib/util/ensure.js b/lib/util/ensure.js new file mode 100644 index 00000000..5c2b3c51 --- /dev/null +++ b/lib/util/ensure.js @@ -0,0 +1,7 @@ +'use strict'; + +module.exports = function( obj, prop, propDefaultValue ) { + if ( obj && typeof obj[ prop ] === 'undefined' ) { + obj[ prop ] = propDefaultValue; + } +}; diff --git a/lib/util/log-obj.js b/lib/util/log-obj.js new file mode 100644 index 00000000..e189bf4f --- /dev/null +++ b/lib/util/log-obj.js @@ -0,0 +1,10 @@ +'use strict'; + +var inspect = require( 'util' ).inspect; + +module.exports = function( obj ) { + console.log( inspect( obj, { + colors: true, + depth: null + }) ); +}; diff --git a/lib/util/named-group-regexp.js b/lib/util/named-group-regexp.js new file mode 100644 index 00000000..39b9e6a6 --- /dev/null +++ b/lib/util/named-group-regexp.js @@ -0,0 +1,25 @@ +'use strict'; + +/** + * Regular Expression to identify a capture group in PCRE formats + * `(?regex)`, `(?'name'regex)` or `(?Pregex)` (see + * regular-expressions.info/refext.html); RegExp is built as a string + * to enable more detailed annotation. + * + * @type {RegExp} + */ +module.exports = new RegExp([ + // Capture group start + '\\(\\?', + // Capture group name begins either `P<`, `<` or `'` + '(?:P<|<|\')', + // Everything up to the next `>`` or `'` (depending) will be the capture group name + '([^>\']+)', + // Capture group end + '[>\']', + // Get everything up to the end of the capture group: this is the RegExp used + // when matching URLs to this route, which we can use for validation purposes. + '([^\\)]+)', + // Capture group end + '\\)' +].join( '' ) ); diff --git a/tests/integration/categories.js b/tests/integration/categories.js index cafb95a2..d7ccca2a 100644 --- a/tests/integration/categories.js +++ b/tests/integration/categories.js @@ -10,7 +10,7 @@ var expect = chai.expect; var _ = require( 'lodash' ); var WP = require( '../../' ); -var WPRequest = require( '../../lib/shared/wp-request.js' ); +var WPRequest = require( '../../lib/constructors/wp-request.js' ); // Define some arrays to use ensuring the returned data is what we expect // it to be (e.g. an array of the names from categories on the first page) @@ -173,7 +173,7 @@ describe( 'integration: categories()', function() { }); - describe( 'term()', function() { + describe( 'id()', function() { it( 'can be used to access an individual category term', function() { var selectedCategory; @@ -181,7 +181,7 @@ describe( 'integration: categories()', function() { // Pick one of the categories selectedCategory = categories[ 3 ]; // Query for that category directly - return wp.categories().term( selectedCategory.id ); + return wp.categories().id( selectedCategory.id ); }).then(function( category ) { expect( category ).to.be.an( 'object' ); expect( category ).to.have.property( 'id' ); diff --git a/tests/integration/comments.js b/tests/integration/comments.js index 5673c990..5b811edb 100644 --- a/tests/integration/comments.js +++ b/tests/integration/comments.js @@ -10,7 +10,7 @@ chai.use( require( 'chai-as-promised' ) ); var expect = chai.expect; var WP = require( '../../' ); -var WPRequest = require( '../../lib/shared/wp-request.js' ); +var WPRequest = require( '../../lib/constructors/wp-request.js' ); // Define some arrays to use ensuring the returned data is what we expect // it to be (e.g. an array of the titles from posts on the first page) diff --git a/tests/integration/pages.js b/tests/integration/pages.js index 9b7fd038..b8cfc818 100644 --- a/tests/integration/pages.js +++ b/tests/integration/pages.js @@ -9,7 +9,7 @@ chai.use( require( 'chai-as-promised' ) ); var expect = chai.expect; var WP = require( '../../' ); -var WPRequest = require( '../../lib/shared/wp-request.js' ); +var WPRequest = require( '../../lib/constructors/wp-request.js' ); // Define some arrays to use ensuring the returned data is what we expect // it to be (e.g. an array of the titles from pages on the first page) diff --git a/tests/integration/posts.js b/tests/integration/posts.js index b885991b..d5e2b481 100644 --- a/tests/integration/posts.js +++ b/tests/integration/posts.js @@ -9,7 +9,7 @@ chai.use( require( 'chai-as-promised' ) ); var expect = chai.expect; var WP = require( '../../' ); -var WPRequest = require( '../../lib/shared/wp-request.js' ); +var WPRequest = require( '../../lib/constructors/wp-request.js' ); // Define some arrays to use ensuring the returned data is what we expect // it to be (e.g. an array of the titles from posts on the first page) diff --git a/tests/integration/tags.js b/tests/integration/tags.js index b3c89415..d2141e92 100644 --- a/tests/integration/tags.js +++ b/tests/integration/tags.js @@ -9,7 +9,7 @@ chai.use( require( 'chai-as-promised' ) ); var expect = chai.expect; var WP = require( '../../' ); -var WPRequest = require( '../../lib/shared/wp-request.js' ); +var WPRequest = require( '../../lib/constructors/wp-request.js' ); // Define some arrays to use ensuring the returned data is what we expect // it to be (e.g. an array of the names from tags on the first page) @@ -177,7 +177,7 @@ describe( 'integration: tags()', function() { }); - describe( 'term()', function() { + describe( 'id()', function() { it( 'can be used to access an individual tag term', function() { var selectedTag; @@ -185,7 +185,7 @@ describe( 'integration: tags()', function() { // Pick one of the tags selectedTag = tags[ 3 ]; // Query for that tag directly - return wp.tags().term( selectedTag.id ); + return wp.tags().id( selectedTag.id ); }).then(function( tag ) { expect( tag ).to.be.an( 'object' ); expect( tag ).to.have.property( 'id' ); diff --git a/tests/integration/taxonomies.js b/tests/integration/taxonomies.js index 67247405..69811d29 100644 --- a/tests/integration/taxonomies.js +++ b/tests/integration/taxonomies.js @@ -30,8 +30,8 @@ describe( 'integration: taxonomies()', function() { return expect( prom ).to.eventually.equal( SUCCESS ); }); - it( 'can be chained with a term() call to fetch the category taxonomy', function() { - var prom = wp.taxonomies().term( 'category' ).get().then(function( category ) { + it( 'can be chained with a taxonomy() call to fetch the category taxonomy', function() { + var prom = wp.taxonomies().taxonomy( 'category' ).get().then(function( category ) { expect( category ).to.be.an( 'object' ); expect( category ).to.have.property( 'slug' ); expect( category.slug ).to.equal( 'category' ); @@ -42,43 +42,8 @@ describe( 'integration: taxonomies()', function() { return expect( prom ).to.eventually.equal( SUCCESS ); }); - it( 'can be chained with a term() call to fetch the post_tag taxonomy', function() { - var prom = wp.taxonomies().term( 'post_tag' ).get().then(function( tag ) { - expect( tag ).to.be.an( 'object' ); - expect( tag ).to.have.property( 'slug' ); - expect( tag.slug ).to.equal( 'post_tag' ); - expect( tag ).to.have.property( 'hierarchical' ); - expect( tag.hierarchical ).to.equal( false ); - return SUCCESS; - }); - return expect( prom ).to.eventually.equal( SUCCESS ); - }); - -}); - -describe( 'integration: taxonomy()', function() { - var wp; - - beforeEach(function() { - wp = new WP({ - endpoint: 'http://wpapi.loc/wp-json' - }); - }); - - it( 'can be used to directly retrieve the category taxonomy object', function() { - var prom = wp.taxonomy( 'category' ).get().then(function( category ) { - expect( category ).to.be.an( 'object' ); - expect( category ).to.have.property( 'slug' ); - expect( category.slug ).to.equal( 'category' ); - expect( category ).to.have.property( 'hierarchical' ); - expect( category.hierarchical ).to.equal( true ); - return SUCCESS; - }); - return expect( prom ).to.eventually.equal( SUCCESS ); - }); - - it( 'can be used to directly retrieve the post_tag taxonomy object', function() { - var prom = wp.taxonomy( 'post_tag' ).get().then(function( tag ) { + it( 'can be chained with a taxonomy() call to fetch the post_tag taxonomy', function() { + var prom = wp.taxonomies().taxonomy( 'post_tag' ).get().then(function( tag ) { expect( tag ).to.be.an( 'object' ); expect( tag ).to.have.property( 'slug' ); expect( tag.slug ).to.equal( 'post_tag' ); diff --git a/tests/unit/lib/comments.js b/tests/unit/lib/comments.js deleted file mode 100644 index be5eb782..00000000 --- a/tests/unit/lib/comments.js +++ /dev/null @@ -1,149 +0,0 @@ -'use strict'; -var expect = require( 'chai' ).expect; - -var CommentsRequest = require( '../../../lib/comments' ); -var CollectionRequest = require( '../../../lib/shared/collection-request' ); -var WPRequest = require( '../../../lib/shared/wp-request' ); - -describe( 'wp.comments', function() { - - describe( 'constructor', function() { - - var comments; - - beforeEach(function() { - comments = new CommentsRequest(); - }); - - it( 'should create a CommentsRequest instance', function() { - expect( comments instanceof CommentsRequest ).to.be.true; - }); - - it( 'should set any passed-in options', function() { - comments = new CommentsRequest({ - booleanProp: true, - strProp: 'Some string' - }); - expect( comments._options.booleanProp ).to.be.true; - expect( comments._options.strProp ).to.equal( 'Some string' ); - }); - - it( 'should default _options to {}', function() { - expect( comments._options ).to.deep.equal( {} ); - }); - - it( 'should intitialize instance properties', function() { - expect( comments._path ).to.deep.equal( {} ); - expect( comments._template ).to.equal( 'comments(/:id)' ); - var _supportedMethods = comments._supportedMethods.sort().join( '|' ); - expect( _supportedMethods ).to.equal( 'get|head|post' ); - }); - - it( 'should inherit CommentsRequest from CollectionRequest', function() { - expect( comments instanceof CollectionRequest ).to.be.true; - expect( comments instanceof WPRequest ).to.be.true; - }); - - it( 'should inherit prototype methods from both ancestors', function() { - // Spot-check from CollectionRequest: - expect( comments ).to.have.property( 'param' ); - expect( comments.param ).to.be.a( 'function' ); - // From WPRequest: - expect( comments ).to.have.property( 'get' ); - expect( comments.get ).to.be.a( 'function' ); - expect( comments ).to.have.property( '_renderURI' ); - expect( comments._renderURI ).to.be.a( 'function' ); - }); - - }); - - describe( '_pathValidators', function() { - - it( 'defines validators for id and action', function() { - var comments = new CommentsRequest(); - expect( comments._pathValidators ).to.deep.equal({ - id: /^\d+$/ - }); - }); - - }); - - describe( 'query methods', function() { - - var comments; - - beforeEach(function() { - comments = new CommentsRequest(); - comments._options = { - endpoint: '/wp-json/' - }; - }); - - it( 'provides a method to set the ID', function() { - expect( comments ).to.have.property( 'id' ); - expect( comments.id ).to.be.a( 'function' ); - comments.id( 314159 ); - expect( comments._path ).to.have.property( 'id' ); - expect( comments._path.id ).to.equal( 314159 ); - }); - - it( 'parses ID parameters into integers', function() { - comments.id( '8' ); - expect( comments._path ).to.have.property( 'id' ); - expect( comments._path.id ).to.equal( 8 ); - comments.id( 4.019 ); - expect( comments._path.id ).to.equal( 4 ); - }); - - it( 'should update the supported methods when setting ID', function() { - comments.id( 8 ); - var _supportedMethods = comments._supportedMethods.sort().join( '|' ); - expect( _supportedMethods ).to.equal( 'delete|get|head|post|put' ); - }); - - }); - - describe( 'URL Generation', function() { - - var comments; - - beforeEach(function() { - comments = new CommentsRequest(); - comments._options = { - endpoint: '/wp-json/' - }; - }); - - it( 'should create the URL for retrieving all comments', function() { - var path = comments._renderURI(); - expect( path ).to.equal( '/wp-json/wp/v2/comments' ); - }); - - it( 'should create the URL for retrieving a specific comment', function() { - var path = comments.id( 1337 )._renderURI(); - expect( path ).to.equal( '/wp-json/wp/v2/comments/1337' ); - }); - - it( 'throws an error if an invalid ID is specified', function() { - expect(function numberPassesValidation() { - comments._path = { id: 8 }; - comments._renderPath(); - }).not.to.throw(); - - expect(function stringFailsValidation() { - comments._path = { id: 'wombat' }; - comments._renderPath(); - }).to.throw(); - }); - - it( 'should restrict template changes to a single instance', function() { - comments._template = 'path/with/comment/nr/:id'; - var newComments = new CommentsRequest(); - newComments._options.endpoint = 'endpoint/url/'; - var path = newComments.id( 3 )._renderURI(); - expect( path ).to.equal( 'endpoint/url/wp/v2/comments/3' ); - }); - - }); - -}); diff --git a/tests/unit/lib/shared/wp-request.js b/tests/unit/lib/constructors/wp-request.js similarity index 85% rename from tests/unit/lib/shared/wp-request.js rename to tests/unit/lib/constructors/wp-request.js index 8dc23578..482c4ee9 100644 --- a/tests/unit/lib/shared/wp-request.js +++ b/tests/unit/lib/constructors/wp-request.js @@ -5,7 +5,8 @@ chai.use( require( 'sinon-chai' ) ); var sinon = require( 'sinon' ); var sandbox = require( 'sandboxed-module' ); -var WPRequest = require( '../../../../lib/shared/wp-request' ); +var WPRequest = require( '../../../../lib/constructors/wp-request' ); +var filterMixins = require( '../../../../lib/mixins/filters' ); describe( 'WPRequest', function() { @@ -37,6 +38,65 @@ describe( 'WPRequest', function() { }); + describe( '_renderQuery()', function() { + + beforeEach(function() { + Object.keys( filterMixins ).forEach(function( mixin ) { + if ( ! request[ mixin ] ) { + request[ mixin ] = filterMixins[ mixin ]; + } + }); + }); + + it( 'properly parses taxonomy filters', function() { + request._taxonomyFilters = { + tag: [ 'clouds ', 'islands' ], + custom_tax: [ 7 ] + }; + var query = request._renderQuery(); + // Filters should be in alpha order, to support caching requests + expect( query ).to + .equal( '?filter%5Bcustom_tax%5D=7&filter%5Btag%5D=clouds%2Bislands' ); + }); + + it( 'lower-cases taxonomy terms', function() { + request._taxonomyFilters = { + tag: [ 'Diamond-Dust' ] + }; + var query = request._renderQuery(); + expect( query ).to.equal( '?filter%5Btag%5D=diamond-dust' ); + }); + + it( 'properly parses regular filters', function() { + request._filters = { + post_status: 'publish', s: 'Some search string' + }; + var query = request._renderQuery(); + expect( query ).to + .equal( '?filter%5Bpost_status%5D=publish&filter%5Bs%5D=Some%20search%20string' ); + }); + + it( 'properly parses array filters', function() { + request._filters = { post__in: [ 0, 1 ] }; + var query = request._renderQuery(); + expect( query ).to + .equal( '?filter%5Bpost__in%5D%5B%5D=0&filter%5Bpost__in%5D%5B%5D=1' ); + }); + + it( 'correctly merges taxonomy and regular filters & renders them in order', function() { + request._taxonomyFilters = { + cat: [ 7, 10 ] + }; + request._filters = { + name: 'some-slug' + }; + var query = request._renderQuery(); + // Filters should be in alpha order, to support caching requests + expect( query ).to.equal( '?filter%5Bcat%5D=7%2B10&filter%5Bname%5D=some-slug' ); + }); + + }); + describe( '_checkMethodSupport', function() { it( 'should return true when called with a supported method', function() { @@ -51,7 +111,7 @@ describe( 'WPRequest', function() { }).to.throw(); }); - }); // constructor + }); describe( 'namespace', function() { @@ -65,28 +125,19 @@ describe( 'WPRequest', function() { expect( request._renderPath() ).to.equal( 'ns' ); }); - it( 'prefixes any provided template', function() { - request._template = 'face'; - request.namespace( 'nose' ); - expect( request._renderPath() ).to.equal( 'nose/face' ); - }); - it( 'can accept & set a namespace in the (:domain/:version) format', function() { - request._template = 'template'; request.namespace( 'ns/v3' ); - expect( request._renderPath() ).to.equal( 'ns/v3/template' ); + expect( request._renderPath() ).to.equal( 'ns/v3' ); }); it( 'can be removed (to use the legacy api v1) with an empty string', function() { - request._template = 'template'; request.namespace( 'windows/xp' ).namespace( '' ); - expect( request._renderPath() ).to.equal( 'template' ); + expect( request._renderPath() ).to.equal( '' ); }); it( 'can be removed (to use the legacy api v1) by omitting arguments', function() { - request._template = 'template'; request.namespace( 'wordpress/95' ).namespace(); - expect( request._renderPath() ).to.equal( 'template' ); + expect( request._renderPath() ).to.equal( '' ); }); }); @@ -164,6 +215,72 @@ describe( 'WPRequest', function() { }); + describe( 'parameter convenience methods', function() { + + describe( 'context', function() { + + beforeEach(function() { + request = new WPRequest({ + endpoint: '/' + }); + }); + + it( 'should be defined', function() { + expect( request ).to.have.property( 'context' ); + expect( request.context ).to.be.a( 'function' ); + }); + + it( 'wraps .param()', function() { + sinon.stub( request, 'param' ); + request.context( 'view' ); + expect( request.param ).to.have.been.calledWith( 'context', 'view' ); + }); + + it( 'should map to the "context=VALUE" query parameter', function() { + var path = request.context( 'edit' )._renderURI(); + expect( path ).to.equal( '/?context=edit' ); + }); + + it( 'should replace values when called multiple times', function() { + var path = request.context( 'edit' ).context( 'view' )._renderURI(); + expect( path ).to.equal( '/?context=view' ); + }); + + it( 'should provide a .edit() shortcut for .context( "edit" )', function() { + sinon.spy( request, 'context' ); + request.edit(); + expect( request.context ).to.have.been.calledWith( 'edit' ); + expect( request._renderURI() ).to.equal( '/?context=edit' ); + }); + + it( 'should force authentication when called with "edit"', function() { + request.edit(); + expect( request._options ).to.have.property( 'auth' ); + expect( request._options.auth ).to.be.true; + }); + + }); + + describe( 'embed()', function() { + + it( 'should be a function', function() { + expect( request ).to.have.property( 'embed' ); + expect( request.embed ).to.be.a( 'function' ); + }); + + it( 'should set the "_embed" parameter', function() { + request.embed(); + expect( request._params._embed ).to.equal( true ); + }); + + it( 'should be chainable', function() { + expect( request.embed() ).to.equal( request ); + }); + + }); + + }); + describe( 'auth', function() { it( 'is defined', function() { @@ -288,7 +405,7 @@ describe( 'WPRequest', function() { beforeEach(function() { mockAgent = new MockAgent(); - SandboxedRequest = sandbox.require( '../../../../lib/shared/wp-request', { + SandboxedRequest = sandbox.require( '../../../../lib/constructors/wp-request', { requires: { 'superagent': mockAgent } diff --git a/tests/unit/lib/media.js b/tests/unit/lib/media.js deleted file mode 100644 index 22a3f635..00000000 --- a/tests/unit/lib/media.js +++ /dev/null @@ -1,125 +0,0 @@ -'use strict'; -var expect = require( 'chai' ).expect; - -var MediaRequest = require( '../../../lib/media' ); -var CollectionRequest = require( '../../../lib/shared/collection-request' ); -var WPRequest = require( '../../../lib/shared/wp-request' ); - -describe( 'wp.media', function() { - - var media; - - beforeEach(function() { - media = new MediaRequest(); - }); - - describe( 'constructor', function() { - - it( 'should create a MediaRequest instance', function() { - expect( media instanceof MediaRequest ).to.be.true; - }); - - it( 'should default _options to {}', function() { - expect( media._options ).to.deep.equal( {} ); - }); - - it( 'should set any passed-in options', function() { - media = new MediaRequest({ - booleanProp: true, - strProp: 'Some string' - }); - expect( media._options.booleanProp ).to.be.true; - expect( media._options.strProp ).to.equal( 'Some string' ); - }); - - it( 'should intitialize instance properties', function() { - expect( media._path ).to.deep.equal( {} ); - expect( media._params ).to.deep.equal( {} ); - expect( media._template ).to.equal( 'media(/:id)' ); - var _supportedMethods = media._supportedMethods.sort().join( '|' ); - expect( _supportedMethods ).to.equal( 'get|head|post' ); - }); - - it( 'should inherit MediaRequest from CollectionRequest', function() { - expect( media instanceof CollectionRequest ).to.be.true; - expect( media instanceof WPRequest ).to.be.true; - }); - - }); - - describe( '_pathValidators', function() { - - it( 'has a validator for the "id" property', function() { - expect( media._pathValidators ).to.deep.equal({ - id: /^\d+$/ - }); - }); - - }); - - describe( '.id()', function() { - - it( 'should be defined', function() { - expect( media ).to.have.property( 'id' ); - expect( media.id ).to.be.a( 'function' ); - }); - - it( 'should set the ID value in the template', function() { - media.id( 8 ); - expect( media._path ).to.have.property( 'id' ); - expect( media._path.id ).to.equal( 8 ); - }); - - it( 'should update the supported methods', function() { - media.id( 8 ); - var _supportedMethods = media._supportedMethods.sort().join( '|' ); - expect( _supportedMethods ).to.equal( 'delete|get|head|post|put' ); - }); - - it( 'replaces values on successive calls', function() { - media.id( 8 ).id( 3 ); - expect( media._path.id ).to.equal( 3 ); - }); - - it( 'causes a validation error when called with a non-number', function() { - expect(function numberPassesValidation() { - media._path = { id: 8 }; - media._renderPath(); - media._path.id = '9'; - media._renderPath(); - }).not.to.throw(); - - expect(function stringFailsValidation() { - media._path = { id: 'wombat' }; - media._renderPath(); - }).to.throw(); - }); - - }); - - describe( 'url generation', function() { - - beforeEach(function() { - media._options = { - endpoint: 'http://some-site.com/wp-json/' - }; - }); - - it( 'should create the URL for the media collection', function() { - var uri = media._renderURI(); - expect( uri ).to.equal( 'http://some-site.com/wp-json/wp/v2/media' ); - }); - - it( 'can paginate the media collection responses', function() { - var uri = media.page( 4 )._renderURI(); - expect( uri ).to.equal( 'http://some-site.com/wp-json/wp/v2/media?page=4' ); - }); - - it( 'should create the URL for a specific media object', function() { - var uri = media.id( 1492 )._renderURI(); - expect( uri ).to.equal( 'http://some-site.com/wp-json/wp/v2/media/1492' ); - }); - - }); - -}); diff --git a/tests/unit/lib/mixins/filters.js b/tests/unit/lib/mixins/filters.js index c5beb8c8..440bedfa 100644 --- a/tests/unit/lib/mixins/filters.js +++ b/tests/unit/lib/mixins/filters.js @@ -4,7 +4,7 @@ var expect = require( 'chai' ).expect; var inherit = require( 'util' ).inherits; var filterMixins = require( '../../../../lib/mixins/filters' ); -var WPRequest = require( '../../../../lib/shared/wp-request' ); +var WPRequest = require( '../../../../lib/constructors/wp-request' ); describe( 'mixins: filter', function() { var Req; @@ -208,6 +208,20 @@ describe( 'mixins: filter', function() { expect( getQueryStr( result ) ).to.equal( 'filter[tag]=cat' ); }); + it( 'de-dupes the taxonomy list when called with an array', function() { + req.taxonomy( 'post_tag', [ + 'disclosure', + 'alunageorge', + 'disclosure', + 'lorde', + 'lorde', + 'clean-bandit' + ]); + expect( req._taxonomyFilters ).to.deep.equal({ + tag: [ 'alunageorge', 'clean-bandit', 'disclosure', 'lorde' ] + }); + }); + it( 'supports setting an array of string terms', function() { // TODO: Multiple terms may be deprecated by API! var result = req.taxonomy( 'tag', [ 'a', 'b' ] ); diff --git a/tests/unit/lib/mixins/parameters.js b/tests/unit/lib/mixins/parameters.js index 49a822ec..171e356c 100644 --- a/tests/unit/lib/mixins/parameters.js +++ b/tests/unit/lib/mixins/parameters.js @@ -4,7 +4,7 @@ var expect = require( 'chai' ).expect; var inherit = require( 'util' ).inherits; var parameterMixins = require( '../../../../lib/mixins/parameters' ); -var WPRequest = require( '../../../../lib/shared/wp-request' ); +var WPRequest = require( '../../../../lib/constructors/wp-request' ); describe( 'mixins: parameters', function() { var Req; @@ -57,6 +57,11 @@ describe( 'mixins: parameters', function() { expect( getQueryStr( result ) ).to.equal( 'page=7' ); }); + it( 'should be chainable and replace values when called multiple times', function() { + var result = req.page( 71 ).page( 2 ); + expect( getQueryStr( result ) ).to.equal( 'page=2' ); + }); + }); describe( '.perPage()', function() { @@ -87,6 +92,11 @@ describe( 'mixins: parameters', function() { expect( getQueryStr( result ) ).to.equal( 'per_page=7' ); }); + it( 'should be chainable and replace values when called multiple times', function() { + var result = req.perPage( 71 ).perPage( 2 ); + expect( getQueryStr( result ) ).to.equal( 'per_page=2' ); + }); + }); }); @@ -230,8 +240,19 @@ describe( 'mixins: parameters', function() { }); it( 'sets the "author_name" filter when provided a string value', function() { - var result = req.author( 'han-solo' ); - expect( getQueryStr( result ) ).to.equal( 'filter[author_name]=han-solo' ); + var result = req.author( 'jamesagarfield' ); + expect( getQueryStr( result ) ).to.equal( 'filter[author_name]=jamesagarfield' ); + }); + + it( 'is chainable, and replaces author_name values on subsequent calls', function() { + var result = req.author( 'fforde' ).author( 'bronte' ); + expect( result ).to.equal( req ); + expect( getQueryStr( result ) ).to.equal( 'filter[author_name]=bronte' ); + }); + + it( 'is chainable, and replaces author ID values on subsequent calls', function() { + var result = req.author( 1847 ); + expect( getQueryStr( result ) ).to.equal( 'author=1847' ); }); it( 'unsets author when called with an empty string', function() { diff --git a/tests/unit/lib/posts.js b/tests/unit/lib/posts.js deleted file mode 100644 index cd431078..00000000 --- a/tests/unit/lib/posts.js +++ /dev/null @@ -1,219 +0,0 @@ -'use strict'; -var expect = require( 'chai' ).expect; - -var PostsRequest = require( '../../../lib/posts' ); -var CollectionRequest = require( '../../../lib/shared/collection-request' ); -var WPRequest = require( '../../../lib/shared/wp-request' ); - -describe( 'wp.posts', function() { - - describe( 'constructor', function() { - - var posts; - - beforeEach(function() { - posts = new PostsRequest(); - }); - - it( 'should create a PostsRequest instance', function() { - expect( posts instanceof PostsRequest ).to.be.true; - }); - - it( 'should set any passed-in options', function() { - posts = new PostsRequest({ - booleanProp: true, - strProp: 'Some string' - }); - expect( posts._options.booleanProp ).to.be.true; - expect( posts._options.strProp ).to.equal( 'Some string' ); - }); - - it( 'should default _options to {}', function() { - expect( posts._options ).to.deep.equal( {} ); - }); - - it( 'should intitialize instance properties', function() { - expect( posts._path ).to.deep.equal( {} ); - expect( posts._template ).to.equal( 'posts(/:id)(/:action)(/:actionId)' ); - var _supportedMethods = posts._supportedMethods.sort().join( '|' ); - expect( _supportedMethods ).to.equal( 'get|head|post' ); - }); - - it( 'should inherit PostsRequest from CollectionRequest', function() { - expect( posts instanceof CollectionRequest ).to.be.true; - expect( posts instanceof WPRequest ).to.be.true; - }); - - it( 'should inherit prototype methods from both ancestors', function() { - // Spot-check from CollectionRequest: - expect( posts ).to.have.property( 'filter' ); - expect( posts.filter ).to.be.a( 'function' ); - expect( posts ).to.have.property( 'param' ); - expect( posts.param ).to.be.a( 'function' ); - // From WPRequest: - expect( posts ).to.have.property( 'get' ); - expect( posts.get ).to.be.a( 'function' ); - expect( posts ).to.have.property( '_renderURI' ); - expect( posts._renderURI ).to.be.a( 'function' ); - }); - - }); - - describe( '_pathValidators', function() { - - it( 'defines validators for id and action', function() { - var posts = new PostsRequest(); - expect( posts._pathValidators ).to.deep.equal({ - id: /^\d+$/, - action: /(meta|revisions)/ - }); - }); - - }); - - describe( 'query methods', function() { - - var posts; - - beforeEach(function() { - posts = new PostsRequest(); - posts._options = { - endpoint: '/wp-json/' - }; - }); - - it( 'provides a method to set the ID', function() { - expect( posts ).to.have.property( 'id' ); - expect( posts.id ).to.be.a( 'function' ); - posts.id( 314159 ); - expect( posts._path ).to.have.property( 'id' ); - expect( posts._path.id ).to.equal( 314159 ); - }); - - it( 'parses ID parameters into integers', function() { - posts.id( '8' ); - expect( posts._path ).to.have.property( 'id' ); - expect( posts._path.id ).to.equal( 8 ); - posts.id( 4.019 ); - expect( posts._path.id ).to.equal( 4 ); - }); - - it( 'should update the supported methods when setting ID', function() { - posts.id( 8 ); - var _supportedMethods = posts._supportedMethods.sort().join( '|' ); - expect( _supportedMethods ).to.equal( 'delete|get|head|post|put' ); - }); - - it( 'provides a method to get the meta values for a post', function() { - expect( posts ).to.have.property( 'meta' ); - expect( posts.meta ).to.be.a( 'function' ); - posts.id( 3 ).meta(); - expect( posts._path ).to.have.property( 'action' ); - expect( posts._path.action ).to.equal( 'meta' ); - }); - - it( 'should force authentication when querying posts/id/meta', function() { - posts.id( 1337 ).meta(); - expect( posts._options ).to.have.property( 'auth' ); - expect( posts._options.auth ).to.be.true; - }); - - it( 'should update the supported methods when querying for meta', function() { - posts.id( 1066 ).meta(); - var _supportedMethods = posts._supportedMethods.sort().join( '|' ); - expect( _supportedMethods ).to.equal( 'get|head|post' ); - }); - - it( 'provides a method to get specific post meta objects by ID', function() { - posts.id( 3 ).meta( 5 ); - expect( posts._path ).to.have.property( 'actionId' ); - expect( posts._path.actionId ).to.equal( 5 ); - }); - - it( 'parses meta ID parameters into integers', function() { - posts.id( 3 ).meta( '4' ); - expect( posts._path ).to.have.property( 'actionId' ); - expect( posts._path.actionId ).to.equal( 4 ); - posts.id( 3 ).meta( 3.14159 ); - expect( posts._path.actionId ).to.equal( 3 ); - }); - - it( 'should force authentication when querying posts/id/meta/:id', function() { - posts.id( 7331 ).meta( 7 ); - expect( posts._options ).to.have.property( 'auth' ); - expect( posts._options.auth ).to.be.true; - }); - - it( 'should update the supported methods when querying for meta', function() { - posts.id( 1066 ).meta( 2501 ); - var _supportedMethods = posts._supportedMethods.sort().join( '|' ); - expect( _supportedMethods ).to.equal( 'delete|get|head|post|put' ); - }); - - }); - - describe( 'URL Generation', function() { - - var posts; - - beforeEach(function() { - posts = new PostsRequest(); - posts._options = { - endpoint: '/wp-json/' - }; - }); - - it( 'should create the URL for retrieving all posts', function() { - var path = posts._renderURI(); - expect( path ).to.equal( '/wp-json/wp/v2/posts' ); - }); - - it( 'should create the URL for retrieving a specific post', function() { - var path = posts.id( 1337 )._renderURI(); - expect( path ).to.equal( '/wp-json/wp/v2/posts/1337' ); - }); - - it( 'throws an error if an invalid ID is specified', function() { - expect(function numberPassesValidation() { - posts._path = { id: 8 }; - posts._renderPath(); - }).not.to.throw(); - - expect(function stringFailsValidation() { - posts._path = { id: 'wombat' }; - posts._renderPath(); - }).to.throw(); - }); - - it( 'should create the URL for retrieving all meta for a specific post', function() { - var path = posts.id( 1337 ).meta()._renderURI(); - expect( path ).to.equal( '/wp-json/wp/v2/posts/1337/meta' ); - }); - - it( 'should create the URL for retrieving a specific meta item', function() { - var path = posts.id( 1337 ).meta( 2001 )._renderURI(); - expect( path ).to.equal( '/wp-json/wp/v2/posts/1337/meta/2001' ); - }); - - it( 'should create the URL for retrieving the revisions for a specific post', function() { - var path = posts.id( 1337 ).revisions()._renderURI(); - expect( path ).to.equal( '/wp-json/wp/v2/posts/1337/revisions' ); - }); - - it( 'should force authentication when querying posts/id/revisions', function() { - posts.id( 1337 ).revisions(); - expect( posts._options ).to.have.property( 'auth' ); - expect( posts._options.auth ).to.be.true; - }); - - it( 'should restrict template changes to a single instance', function() { - posts._template = 'path/with/post/nr/:id'; - var newPosts = new PostsRequest(); - newPosts._options.endpoint = 'endpoint/url/'; - var path = newPosts.id( 3 )._renderURI(); - expect( path ).to.equal( 'endpoint/url/wp/v2/posts/3' ); - }); - - }); - -}); diff --git a/tests/unit/lib/route-tree.js b/tests/unit/lib/route-tree.js new file mode 100644 index 00000000..c0f92fdc --- /dev/null +++ b/tests/unit/lib/route-tree.js @@ -0,0 +1,101 @@ +'use strict'; +var expect = require( 'chai' ).expect; + +var routeTree = require( '../../../lib/route-tree' ); +var endpointResponse = require( '../../../lib/data/endpoint-response.json' ); + +describe( 'route-tree utility', function() { + + describe( '.build()', function() { + var tree; + + beforeEach(function() { + tree = routeTree.build( endpointResponse.routes ); + }); + + it( 'returns an object keyed by API namespace', function() { + var keys = Object.keys( tree ).sort(); + expect( keys.length ).to.equal( 2 ); + expect( keys ).to.deep.equal([ 'oembed/1.0', 'wp/v2' ]); + }); + + it( 'includes objects for all default wp/v2 routes', function() { + var routes = Object.keys( tree[ 'wp/v2' ] ).sort(); + expect( routes ).to.have.length( 10 ); + expect( routes.join( ',' ) ).to + .equal( 'categories,comments,media,pages,posts,statuses,tags,taxonomies,types,users' ); + }); + + it( 'includes objects for all default oembed/1.0 routes', function() { + var routes = Object.keys( tree[ 'oembed/1.0' ] ).sort(); + expect( routes ).to.have.length( 1 ); + expect( routes.join( ',' ) ).to.equal( 'embed' ); + }); + + // Inspect the .posts tree as a smoke test for whether parsing the API + // definition object was successful + describe( 'posts resource tree', function() { + var posts; + + beforeEach(function() { + posts = tree[ 'wp/v2' ].posts; + }); + + it ( 'includes a ._getArgs property', function() { + expect( posts ).to.have.property( '_getArgs' ); + expect( posts._getArgs ).to.be.an( 'object' ); + }); + + it ( '._getArgs specifies a list of supported parameters', function() { + expect( posts ).to.have.property( '_getArgs' ); + expect( posts._getArgs ).to.be.an( 'object' ); + expect( posts._getArgs ).to.deep.equal({ + context: false, + page: false, + per_page: false, + search: false, + after: false, + author: false, + author_exclude: false, + before: false, + exclude: false, + include: false, + offset: false, + order: false, + orderby: false, + slug: false, + status: false, + filter: false, + categories: false, + tags: false + }); + }); + + it ( 'includes a .posts property', function() { + expect( posts ).to.have.property( 'posts' ); + expect( posts.posts ).to.be.an( 'object' ); + }); + + // This is a decidedly incomplete smoke test... + // But if this fails, so will everything else! + it ( '.posts defines the top level of a route tree', function() { + var routeTree = posts.posts; + expect( routeTree ).to.have.property( 'level' ); + expect( routeTree.level ).to.equal( 0 ); + expect( routeTree ).to.have.property( 'methods' ); + expect( routeTree.methods.sort().join( '|' ) ).to.equal( 'get|head|post' ); + expect( routeTree ).to.have.property( 'namedGroup' ); + expect( routeTree.namedGroup ).to.equal( false ); + expect( routeTree ).to.have.property( 'names' ); + expect( routeTree.names ).to.deep.equal([ 'posts' ]); + expect( routeTree ).to.have.property( 'validate' ); + expect( routeTree.validate ).to.be.a( 'function' ); + expect( routeTree ).to.have.property( 'children' ); + expect( routeTree.children ).to.be.an( 'object' ); + }); + + }); + + }); + +}); diff --git a/tests/unit/lib/shared/collection-request.js b/tests/unit/lib/shared/collection-request.js deleted file mode 100644 index 2180ca1e..00000000 --- a/tests/unit/lib/shared/collection-request.js +++ /dev/null @@ -1,565 +0,0 @@ -'use strict'; -/*jshint -W106 */// Disable underscore_case warnings in this file b/c WP uses them -var chai = require( 'chai' ); -var expect = chai.expect; -var sinon = require( 'sinon' ); -chai.use( require( 'sinon-chai' ) ); - -var CollectionRequest = require( '../../../../lib/shared/collection-request' ); -var WPRequest = require( '../../../../lib/shared/wp-request' ); -var filterMixins = require( '../../../../lib/mixins/filters' ); - -describe( 'CollectionRequest', function() { - - var request; - - beforeEach(function() { - request = new CollectionRequest(); - request._options.endpoint = '/'; - }); - - describe( 'constructor', function() { - - it( 'should create a CollectionRequest instance', function() { - expect( request instanceof CollectionRequest ).to.be.true; - }); - - it( 'should inherit from WPRequest', function() { - expect( request instanceof WPRequest ).to.be.true; - }); - - it( 'should intitialize instance properties', function() { - var _supportedMethods = request._supportedMethods.sort().join( '|' ); - expect( _supportedMethods ).to.equal( 'delete|get|head|post|put' ); - expect( request._filters ).to.deep.equal( {} ); - expect( request._taxonomyFilters ).to.deep.equal( {} ); - expect( request._params ).to.deep.equal( {} ); - expect( request._template ).to.equal( '' ); - }); - - it( 'initializes requests with a _params dictionary', function() { - expect( request ).to.have.property( '_params' ); - expect( request._params ).to.deep.equal( {} ); - }); - - }); - - describe( 'parameter convenience methods', function() { - - describe( 'page', function() { - - it( 'should be defined', function() { - expect( request ).to.have.property( 'page' ); - expect( request.page ).to.be.a( 'function' ); - }); - - it( 'wraps .param()', function() { - sinon.stub( request, 'param' ); - request.page( 9 ); - expect( request.param ).to.have.been.calledWith( 'page', 9 ); - }); - - it( 'should set the "page" parameter', function() { - request.page( 2 ); - expect( request._params ).to.have.property( 'page' ); - expect( request._params.page ).to.equal( 2 ); - }); - - it( 'should map to the "page=N" query parameter', function() { - var path = request.page( 71 )._renderURI(); - expect( path ).to.equal( '/?page=71' ); - }); - - it( 'should replace values when called multiple times', function() { - var path = request.page( 71 ).page( 2 )._renderURI(); - expect( path ).to.equal( '/?page=2' ); - }); - - }); - - describe( 'perPage()', function() { - - it( 'function should exist', function() { - expect( request ).to.have.property( 'perPage' ); - expect( request.perPage ).to.be.a( 'function' ); - }); - - it( 'should set the "per_page=N" query parameter', function() { - var path = request.perPage( 6 )._renderURI(); - expect( path ).to.equal( '/?per_page=6' ); - }); - - it( 'should be chainable, and replace values', function() { - expect( request.perPage( 71 ).perPage( 2 ) ).to.equal( request ); - var path = request.perPage( 71 ).perPage( 2 )._renderURI(); - expect( path ).to.equal( '/?per_page=2' ); - }); - - }); - - describe( 'context', function() { - - it( 'should be defined', function() { - expect( request ).to.have.property( 'context' ); - expect( request.context ).to.be.a( 'function' ); - }); - - it( 'wraps .param()', function() { - sinon.stub( request, 'param' ); - request.context( 'view' ); - expect( request.param ).to.have.been.calledWith( 'context', 'view' ); - }); - - it( 'should set the "context" parameter', function() { - request.context( 'edit' ); - expect( request._params ).to.have.property( 'context' ); - expect( request._params.context ).to.equal( 'edit' ); - }); - - it( 'should map to the "context=VALUE" query parameter', function() { - var path = request.context( 'edit' )._renderURI(); - expect( path ).to.equal( '/?context=edit' ); - }); - - it( 'should replace values when called multiple times', function() { - var path = request.context( 'edit' ).context( 'view' )._renderURI(); - expect( path ).to.equal( '/?context=view' ); - }); - - it( 'should provide a .edit() shortcut for .context( "edit" )', function() { - sinon.spy( request, 'context' ); - request.edit(); - expect( request.context ).to.have.been.calledWith( 'edit' ); - expect( request._renderURI() ).to.equal( '/?context=edit' ); - }); - - it( 'should force authentication when called with "edit"', function() { - request.edit(); - expect( request._options ).to.have.property( 'auth' ); - expect( request._options.auth ).to.be.true; - }); - - }); - - }); - - describe( 'embed()', function() { - - it( 'should be a function', function() { - expect( request ).to.have.property( 'embed' ); - expect( request.embed ).to.be.a( 'function' ); - }); - - it( 'should set the "_embed" parameter', function() { - request.embed(); - expect( request._params._embed ).to.equal( true ); - }); - - it( 'should be chainable', function() { - expect( request.embed() ).to.equal( request ); - }); - - }); - - describe( 'filter()', function() { - - it( 'should set the internal _filters hash', function() { - request.filter({ - someFilterProp: 'filter-value', - postsPerPage: 7 - }); - expect( request._filters ).to.deep.equal({ - someFilterProp: 'filter-value', - postsPerPage: 7 - }); - }); - - it( 'should support passing a single filter property as key & value arguments', function() { - request.filter( 'postType', 'page' ); - expect( request._filters ).to.deep.equal({ - postType: 'page' - }); - }); - - it( 'should support redefining filter values', function() { - request.filter( 'postStatus', 'draft' ); - request.filter( 'postStatus', 'publish' ); - expect( request._filters.postStatus ).to.equal( 'publish' ); - }); - - it( 'should support chaining filters', function() { - request.filter({ - someFilterProp: 'filter-value' - }).filter({ - postsPerPage: 7 - }).filter( 'postStatus', 'draft' ); - expect( request._filters ).to.deep.equal({ - someFilterProp: 'filter-value', - postsPerPage: 7, - postStatus: 'draft' - }); - }); - - }); - - describe( 'filtering convenience methods', function() { - - beforeEach(function() { - request._taxonomyFilters = {}; - request._filters = {}; - }); - - describe( 'taxonomy()', function() { - - it( 'should throw if an invalid term argument is provided', function() { - expect(function() { - request.taxonomy( 'tag', 'slug' ); - }).not.to.throw(); - - expect(function() { - request.taxonomy( 'cat', 7 ); - }).not.to.throw(); - - expect(function() { - request.taxonomy( 'category_name', [ 'slug1', 'slug2' ] ); - }).not.to.throw(); - - expect(function() { - request.taxonomy( 'tag', {} ); - }).to.throw(); - }); - - it( 'should store taxonomy terms in a sorted array, keyed by taxonomy', function() { - request.taxonomy( 'some_tax', 'nigel' ); - request.taxonomy( 'some_tax', [ 'tufnel', 'derek', 'smalls' ] ); - expect( request._taxonomyFilters ).to.deep.equal({ - some_tax: [ 'derek', 'nigel', 'smalls', 'tufnel' ] - }); - request - .taxonomy( 'drummers', [ 'stumpy', 'mama' ] ) - .taxonomy( 'drummers', [ 'stumpy-joe', 'james' ] ) - .taxonomy( 'drummers', 'ric' ); - expect( request._taxonomyFilters ).to.deep.equal({ - some_tax: [ 'derek', 'nigel', 'smalls', 'tufnel' ], - drummers: [ 'james', 'mama', 'ric', 'stumpy', 'stumpy-joe' ] - }); - }); - - it( 'should handle numeric terms, for category and taxonomy ID', function() { - request.taxonomy( 'age', [ 42, 2001, 13 ] ); - expect( request._taxonomyFilters ).to.deep.equal({ - age: [ 13, 42, 2001 ] - }); - }); - - it( 'should map "category" to "cat" for numeric terms', function() { - request.taxonomy( 'category', 7 ); - expect( request._taxonomyFilters ).to.deep.equal({ - cat: [ 7 ] - }); - request.taxonomy( 'category', [ 10, 2 ] ); - expect( request._taxonomyFilters ).to.deep.equal({ - cat: [ 2, 7, 10 ] - }); - }); - - it( 'should map "category" to "category_name" for string terms', function() { - request.taxonomy( 'category', 'news' ); - expect( request._taxonomyFilters ).to.deep.equal({ - category_name: [ 'news' ] - }); - request.taxonomy( 'category', [ 'events', 'fluxus-happenings' ] ); - expect( request._taxonomyFilters ).to.deep.equal({ - category_name: [ 'events', 'fluxus-happenings', 'news' ] - }); - }); - - it( 'should map "post_tag" to "tag" for tag terms', function() { - request.taxonomy( 'post_tag', 'disclosure' ); - expect( request._taxonomyFilters ).to.deep.equal({ - tag: [ 'disclosure' ] - }); - request.taxonomy( 'post_tag', [ 'white-noise', 'settle' ] ); - expect( request._taxonomyFilters ).to.deep.equal({ - tag: [ 'disclosure', 'settle', 'white-noise' ] - }); - }); - - it( 'de-dupes the taxonomy list', function() { - request.taxonomy( 'post_tag', [ - 'disclosure', - 'alunageorge', - 'disclosure', - 'lorde', - 'lorde', - 'clean-bandit' - ]); - expect( request._taxonomyFilters ).to.deep.equal({ - tag: [ 'alunageorge', 'clean-bandit', 'disclosure', 'lorde' ] - }); - }); - - }); - - describe( 'category()', function() { - - it( 'delegates to taxonomy() mixin', function() { - sinon.stub( filterMixins, 'taxonomy' ); - request.category( 'news' ); - expect( filterMixins.taxonomy ).to.have.been.calledWith( 'category', 'news' ); - filterMixins.taxonomy.restore(); - }); - - it( 'should be chainable, and accumulates values', function() { - expect( request.category( 'bat-country' ).category( 'bunny' ) ).to.equal( request ); - expect( request._taxonomyFilters ).to.deep.equal({ - category_name: [ 'bat-country', 'bunny' ] - }); - }); - - }); - - describe( 'tag()', function() { - - it( 'delegates to taxonomy() mixin', function() { - sinon.stub( filterMixins, 'taxonomy' ); - request.tag( 'the-good-life' ); - expect( filterMixins.taxonomy ).to.have.been.calledWith( 'tag', 'the-good-life' ); - filterMixins.taxonomy.restore(); - }); - - it( 'should be chainable, and accumulates values', function() { - expect( request.tag( 'drive-by' ).tag( 'jackson-pollock' ) ).to.equal( request ); - expect( request._taxonomyFilters ).to.deep.equal({ - tag: [ 'drive-by', 'jackson-pollock' ] - }); - }); - - }); - - describe( 'search()', function() { - - it( 'should do nothing if no search string is provided', function() { - request.search( '' ); - expect( request._renderQuery() ).to.equal( '' ); - }); - - it( 'should set the "s" filter property on the request object', function() { - request.search( 'Some search string' ); - expect( request._params.search ).to.equal( 'Some search string' ); - }); - - it( 'should be chainable, and replace values', function() { - expect( request.search( 'str1' ).search( 'str2' ) ).to.equal( request ); - expect( request._params.search ).to.equal( 'str2' ); - }); - - }); - - describe( 'author()', function() { - - it( 'should set the "author" filter property for numeric arguments', function() { - request.author( 301 ); - expect( request._params.author ).to.equal( 301 ); - expect( request._filters.author_name ).not.to.exist; - }); - - it( 'should set the "author_name" filter property for string arguments', function() { - request.author( 'jamesagarfield' ); - expect( request._filters.author_name ).to.equal( 'jamesagarfield' ); - expect( request._params.author ).not.to.exist; - }); - - it( 'should throw an error if arguments are neither string nor number', function() { - expect(function() { - request.author({ some: 'object' }); - }).to.throw(); - }); - - it( 'should be chainable, and replace values', function() { - expect( request.author( 'fforde' ).author( 'bronte' ) ).to.equal( request ); - expect( request._filters.author_name ).to.equal( 'bronte' ); - - request.author( 1847 ); - expect( request._filters.author_name ).not.to.exist; - expect( request._params.author ).to.equal( 1847 ); - }); - - }); - - describe( 'slug()', function() { - - it( 'should set the "slug" parameter on the request', function() { - request.slug( 'greatest-post-in-the-world' ); - expect( request._renderURI() ).to.equal( '/?slug=greatest-post-in-the-world' ); - }); - - it( 'should be chainable, and replace values', function() { - expect( request.slug( 'post-slug-1' ).slug( 'hello-world' ) ).to.equal( request ); - expect( request._renderURI() ).to.equal( '/?slug=hello-world' ); - }); - - }); - - describe( 'name()', function() { - - it( 'should alias through to set the "slug" parameter on the request', function() { - request.name( 'greatest-post-in-the-world' ); - expect( request._renderURI() ).to.equal( '/?slug=greatest-post-in-the-world' ); - }); - - it( 'should be chainable, and replace values', function() { - expect( request.name( 'post-slug-1' ).name( 'hello-world' ) ).to.equal( request ); - expect( request._renderURI() ).to.equal( '/?slug=hello-world' ); - }); - - }); - - describe( 'year()', function() { - - it( 'function should exist', function() { - expect( request.year ).to.exist; - expect( request.year ).to.be.a( 'function' ); - }); - - it( 'should set the "year" filter property on the request object', function() { - request.year( 2014 ); - expect( request._filters.year ).to.equal( 2014 ); - }); - - it( 'should accept year numbers as strings', function() { - request.year( '1066' ); - expect( request._filters.year ).to.equal( '1066' ); - }); - - it( 'should be chainable, and replace values', function() { - expect( request.year( 1999 ).year( 2000 ) ).to.equal( request ); - expect( request._filters.year ).to.equal( 2000 ); - }); - - }); - - describe( 'month()', function() { - - it( 'function should exist', function() { - expect( request.month ).to.exist; - expect( request.month ).to.be.a( 'function' ); - }); - - it( 'should set the "monthnum" filter property on the request object', function() { - request.month( 7 ); - expect( request._filters.monthnum ).to.equal( 7 ); - }); - - it( 'should accept month numbers as strings', function() { - request.month( '3' ); - expect( request._filters.monthnum ).to.equal( 3 ); - }); - - it( 'should convert month name strings to month numbers', function() { - request.month( 'March' ); - expect( request._filters.monthnum ).to.equal( 3 ); - request.month( 'november' ); - expect( request._filters.monthnum ).to.equal( 11 ); - request.month( 'Jul' ); - expect( request._filters.monthnum ).to.equal( 7 ); - }); - - it( 'should be chainable, and replace values', function() { - expect( request.month( 2 ).month( 'September' ) ).to.equal( request ); - expect( request._filters.monthnum ).to.equal( 9 ); - }); - - it( 'should not set anything if an invalid string is provided', function() { - request.month( 'The oldest in the family is moving with authority' ); - expect( request._filters.monthnum ).to.be.undefined; - }); - - it( 'should not set anything if a non-number is provided', function() { - request.month({ - wake: 'me up', - when: 'September ends' - }); - expect( request._filters.monthnum ).to.be.undefined; - }); - - }); - - describe( 'day()', function() { - - it( 'function should exist', function() { - expect( request.day ).to.exist; - expect( request.day ).to.be.a( 'function' ); - }); - - it( 'should set the "day" filter property on the request object', function() { - request.day( 7 ); - expect( request._filters.day ).to.equal( 7 ); - }); - - it( 'should accept day numbers as strings', function() { - request.day( '9' ); - expect( request._filters.day ).to.equal( '9' ); - }); - - it( 'should be chainable, and replace values', function() { - expect( request.day( 7 ).day( 22 ) ).to.equal( request ); - expect( request._filters.day ).to.equal( 22 ); - }); - - }); - - }); - - describe( '_renderQuery()', function() { - - it( 'properly parses taxonomy filters', function() { - request._taxonomyFilters = { - tag: [ 'clouds ', 'islands' ], - custom_tax: [ 7 ] - }; - var query = request._renderQuery(); - // Filters should be in alpha order, to support caching requests - expect( query ).to - .equal( '?filter%5Bcustom_tax%5D=7&filter%5Btag%5D=clouds%2Bislands' ); - }); - - it( 'lower-cases taxonomy terms', function() { - request._taxonomyFilters = { - tag: [ 'Diamond-Dust' ] - }; - var query = request._renderQuery(); - expect( query ).to.equal( '?filter%5Btag%5D=diamond-dust' ); - }); - - it( 'properly parses regular filters', function() { - request._filters = { - post_status: 'publish', s: 'Some search string' - }; - var query = request._renderQuery(); - expect( query ).to - .equal( '?filter%5Bpost_status%5D=publish&filter%5Bs%5D=Some%20search%20string' ); - }); - - it( 'properly parses array filters', function() { - request._filters = { post__in: [ 0, 1 ] }; - var query = request._renderQuery(); - expect( query ).to - .equal( '?filter%5Bpost__in%5D%5B%5D=0&filter%5Bpost__in%5D%5B%5D=1' ); - }); - - it( 'correctly merges taxonomy and regular filters & renders them in order', function() { - request._taxonomyFilters = { - cat: [ 7, 10 ] - }; - request._filters = { - name: 'some-slug' - }; - var query = request._renderQuery(); - // Filters should be in alpha order, to support caching requests - expect( query ).to.equal( '?filter%5Bcat%5D=7%2B10&filter%5Bname%5D=some-slug' ); - }); - - }); - -}); diff --git a/tests/unit/lib/taxonomies.js b/tests/unit/lib/taxonomies.js deleted file mode 100644 index 5e836e07..00000000 --- a/tests/unit/lib/taxonomies.js +++ /dev/null @@ -1,94 +0,0 @@ -'use strict'; -var expect = require( 'chai' ).expect; - -var TaxonomiesRequest = require( '../../../lib/taxonomies' ); -var CollectionRequest = require( '../../../lib/shared/collection-request' ); -var WPRequest = require( '../../../lib/shared/wp-request' ); - -describe( 'wp.taxonomies', function() { - - describe( 'constructor', function() { - - var taxonomies; - - beforeEach(function() { - taxonomies = new TaxonomiesRequest(); - }); - - it( 'should create a TaxonomiesRequest instance', function() { - expect( taxonomies instanceof TaxonomiesRequest ).to.be.true; - }); - - it( 'should set any passed-in options', function() { - taxonomies = new TaxonomiesRequest({ - booleanProp: true, - strProp: 'Some string' - }); - expect( taxonomies._options.booleanProp ).to.be.true; - expect( taxonomies._options.strProp ).to.equal( 'Some string' ); - }); - - it( 'should default _options to {}', function() { - expect( taxonomies._options ).to.deep.equal( {} ); - }); - - it( 'should intitialize instance properties', function() { - var _supportedMethods = taxonomies._supportedMethods.sort().join( '|' ); - expect( taxonomies._path ).to.deep.equal({ collection: 'taxonomies' }); - expect( taxonomies._params ).to.deep.equal( {} ); - expect( taxonomies._template ).to.equal( '(:collection)(/:term)' ); - expect( _supportedMethods ).to.equal( 'get|head' ); - }); - - it( 'should inherit PostsRequest from CollectionRequest', function() { - expect( taxonomies instanceof CollectionRequest ).to.be.true; - expect( taxonomies instanceof WPRequest ).to.be.true; - }); - - it( 'should inherit prototype methods from both ancestors', function() { - // Spot-check from CollectionRequest: - expect( taxonomies ).to.have.property( 'param' ); - expect( taxonomies.param ).to.be.a( 'function' ); - // From WPRequest: - expect( taxonomies ).to.have.property( 'get' ); - expect( taxonomies.get ).to.be.a( 'function' ); - expect( taxonomies ).to.have.property( '_renderURI' ); - expect( taxonomies._renderURI ).to.be.a( 'function' ); - }); - - }); - - describe( 'URL Generation', function() { - - var taxonomies; - - beforeEach(function() { - taxonomies = new TaxonomiesRequest(); - taxonomies._options = { - endpoint: '/wp-json/' - }; - }); - - it( 'should create the URL for retrieving a specific collection', function() { - var url = taxonomies.collection( 'taxonomies' )._renderURI(); - expect( url ).to.equal( '/wp-json/wp/v2/taxonomies' ); - }); - - it( 'should create the URL for retrieving a specific taxonomy', function() { - var url = taxonomies.collection( 'taxonomies' ).term( 'my-tax' )._renderURI(); - expect( url ).to.equal( '/wp-json/wp/v2/taxonomies/my-tax' ); - }); - - it( 'should create the URL for retrieving taxonomies with a shared parent', function() { - var url = taxonomies.collection( 'categories' ).parent( 42 )._renderURI(); - expect( url ).to.equal( '/wp-json/wp/v2/categories?parent=42' ); - }); - - it( 'should permit specifying the parent for a collection of terms', function() { - var url = taxonomies.collection( 'categories' ).forPost( 1234 )._renderURI(); - expect( url ).to.equal( '/wp-json/wp/v2/categories?post=1234' ); - }); - - }); - -}); diff --git a/tests/unit/lib/types.js b/tests/unit/lib/types.js deleted file mode 100644 index e717ed6d..00000000 --- a/tests/unit/lib/types.js +++ /dev/null @@ -1,86 +0,0 @@ -'use strict'; -var expect = require( 'chai' ).expect; - -var TypesRequest = require( '../../../lib/types' ); -var CollectionRequest = require( '../../../lib/shared/collection-request' ); -var WPRequest = require( '../../../lib/shared/wp-request' ); - -describe( 'wp.types', function() { - - describe( 'constructor', function() { - - var types; - - beforeEach(function() { - types = new TypesRequest(); - }); - - it( 'should create a TypesRequest instance', function() { - expect( types instanceof TypesRequest ).to.be.true; - }); - - it( 'should set any passed-in options', function() { - types = new TypesRequest({ - booleanProp: true, - strProp: 'Some string' - }); - expect( types._options.booleanProp ).to.be.true; - expect( types._options.strProp ).to.equal( 'Some string' ); - }); - - it( 'should default _options to {}', function() { - expect( types._options ).to.deep.equal( {} ); - }); - - it( 'should intitialize instance properties', function() { - var _supportedMethods = types._supportedMethods.sort().join( '|' ); - expect( types._path ).to.deep.equal( {} ); - expect( types._params ).to.deep.equal( {} ); - expect( types._template ).to.equal( 'types(/:type)' ); - expect( _supportedMethods ).to.equal( 'get|head' ); - }); - - it( 'should inherit PostsRequest from CollectionRequest', function() { - expect( types instanceof CollectionRequest ).to.be.true; - expect( types instanceof WPRequest ).to.be.true; - }); - - it( 'should inherit prototype methods from both ancestors', function() { - // Spot-check from CollectionRequest: - expect( types ).to.have.property( 'filter' ); - expect( types.filter ).to.be.a( 'function' ); - expect( types ).to.have.property( 'param' ); - expect( types.param ).to.be.a( 'function' ); - // From WPRequest: - expect( types ).to.have.property( 'get' ); - expect( types.get ).to.be.a( 'function' ); - expect( types ).to.have.property( '_renderURI' ); - expect( types._renderURI ).to.be.a( 'function' ); - }); - - }); - - describe( 'URL Generation', function() { - - var types; - - beforeEach(function() { - types = new TypesRequest(); - types._options = { - endpoint: '/wp-json/' - }; - }); - - it( 'should create the URL for retrieving all types', function() { - var url = types._renderURI(); - expect( url ).to.equal( '/wp-json/wp/v2/types' ); - }); - - it( 'should create the URL for retrieving a specific term', function() { - var url = types.type( 'some_type' )._renderURI(); - expect( url ).to.equal( '/wp-json/wp/v2/types/some_type' ); - }); - - }); - -}); diff --git a/tests/unit/lib/users.js b/tests/unit/lib/users.js deleted file mode 100644 index c7337e07..00000000 --- a/tests/unit/lib/users.js +++ /dev/null @@ -1,131 +0,0 @@ -'use strict'; -var expect = require( 'chai' ).expect; - -var UsersRequest = require( '../../../lib/users' ); - -describe( 'wp.users', function() { - - var users; - - describe( 'constructor', function() { - - beforeEach(function() { - users = new UsersRequest(); - }); - - it( 'should create a UsersRequest instance', function() { - expect( users instanceof UsersRequest ).to.be.true; - }); - - it( 'should set any passed-in options', function() { - users = new UsersRequest({ - booleanProp: true, - strProp: 'Some string' - }); - expect( users._options.booleanProp ).to.be.true; - expect( users._options.strProp ).to.equal( 'Some string' ); - }); - - it( 'should force authentication', function() { - expect( users._options ).to.have.property( 'auth' ); - expect( users._options.auth ).to.be.true; - }); - - it( 'should default _options to { auth: true }', function() { - expect( users._options ).to.deep.equal({ - auth: true - }); - }); - - it( 'should initialize instance properties', function() { - expect( users._path ).to.deep.equal( {} ); - expect( users._params ).to.deep.equal( {} ); - var _supportedMethods = users._supportedMethods.sort().join( '|' ); - expect( _supportedMethods ).to.equal( 'get|head|post' ); - }); - - it( 'should inherit prototype methods from both ancestors', function() { - // Spot-check from CollectionRequest: - expect( users ).to.have.property( 'param' ); - expect( users.param ).to.be.a( 'function' ); - // From WPRequest: - expect( users ).to.have.property( 'get' ); - expect( users.get ).to.be.a( 'function' ); - expect( users ).to.have.property( '_renderURI' ); - expect( users._renderURI ).to.be.a( 'function' ); - }); - - }); - - describe( '_pathValidators', function() { - - it( 'has a validator for the "id" property', function() { - var users = new UsersRequest(); - expect( users._pathValidators ).to.deep.equal({ - id: /(^\d+$|^me$)/ - }); - }); - - }); - - describe( '.me()', function() { - - it( 'sets the path to users/me', function() { - var users = new UsersRequest(); - users._options = { - endpoint: 'url/endpoint' - }; - users.me(); - expect( users._path ).to.have.property( 'id' ); - expect( users._path.id ).to.equal( 'me' ); - }); - - }); - - describe( '.id()', function() { - - it( 'sets the path ID to the passed-in value', function() { - var users = new UsersRequest(); - users._options = { - endpoint: 'url/endpoint' - }; - users.id( 2501 ); - expect( users._path ).to.have.property( 'id' ); - expect( users._path.id ).to.equal( 2501 ); - }); - - }); - - describe( 'prototype._renderURI', function() { - - var users; - - beforeEach(function() { - users = new UsersRequest(); - users._options = { - endpoint: '/wp-json/' - }; - }); - - it( 'should create the URL for retrieving all users', function() { - var url = users._renderURI(); - expect( url ).to.equal( '/wp-json/wp/v2/users' ); - }); - - it( 'should create the URL for retrieving the current user', function() { - var url = users.me()._renderURI(); - var _supportedMethods = users._supportedMethods.sort().join( '|' ); - expect( url ).to.equal( '/wp-json/wp/v2/users/me' ); - expect( _supportedMethods ).to.equal( 'get|head' ); - }); - - it( 'should create the URL for retrieving a specific user by ID', function() { - var url = users.id( 1337 )._renderURI(); - var _supportedMethods = users._supportedMethods.sort().join( '|' ); - expect( url ).to.equal( '/wp-json/wp/v2/users/1337' ); - expect( _supportedMethods ).to.equal( 'delete|get|head|post|put' ); - }); - - }); - -}); diff --git a/tests/unit/lib/util/ensure.js b/tests/unit/lib/util/ensure.js new file mode 100644 index 00000000..7512ce4c --- /dev/null +++ b/tests/unit/lib/util/ensure.js @@ -0,0 +1,47 @@ +'use strict'; +var expect = require( 'chai' ).expect; + +var ensure = require( '../../../../lib/util/ensure' ); + +describe( 'ensure utility', function() { + var obj; + + beforeEach(function() { + obj = {}; + }); + + it( 'is defined', function() { + expect( ensure ).to.exist; + }); + + it( 'is a function', function() { + expect( ensure ).to.be.a( 'function' ); + }); + + it( 'sets a default property value on an object', function() { + expect( obj ).not.to.have.property( 'foo' ); + ensure( obj, 'foo', 'bar' ); + expect( obj ).to.have.property( 'foo' ); + expect( obj.foo ).to.be.a( 'string' ); + expect( obj.foo ).to.equal( 'bar' ); + }); + + it( 'will not overwrite an existing value on an object', function() { + obj.foo = 'baz'; + expect( obj ).to.have.property( 'foo' ); + ensure( obj, 'foo', 'bar' ); + expect( obj ).to.have.property( 'foo' ); + expect( obj.foo ).to.be.a( 'string' ); + expect( obj.foo ).to.equal( 'baz' ); + }); + + it( 'will not overwrite a falsy value on an object', function() { + obj.foo = 0; + expect( obj ).to.have.property( 'foo' ); + ensure( obj, 'foo', 'bar' ); + expect( obj ).to.have.property( 'foo' ); + expect( obj.foo ).to.be.a( 'number' ); + expect( obj.foo ).to.equal( 0 ); + }); + +}); diff --git a/tests/unit/route-handlers/comments.js b/tests/unit/route-handlers/comments.js new file mode 100644 index 00000000..39f76e16 --- /dev/null +++ b/tests/unit/route-handlers/comments.js @@ -0,0 +1,132 @@ +'use strict'; +var expect = require( 'chai' ).expect; + +var WP = require( '../../../wp' ); +var WPRequest = require( '../../../lib/constructors/wp-request' ); + +describe( 'wp.comments', function() { + var site; + var comments; + + beforeEach(function() { + site = new WP({ + endpoint: '/wp-json', + username: 'foouser', + password: 'barpass' + }); + comments = site.comments(); + }); + + describe( 'constructor', function() { + + it( 'should set any passed-in options', function() { + comments = site.comments({ + booleanProp: true, + strProp: 'Some string' + }); + expect( comments._options.booleanProp ).to.be.true; + expect( comments._options.strProp ).to.equal( 'Some string' ); + }); + + it( 'should initialize _options to the site defaults', function() { + expect( comments._options ).to.deep.equal({ + endpoint: '/wp-json/', + username: 'foouser', + password: 'barpass' + }); + }); + + it( 'should initialize the base path component', function() { + expect( comments._renderURI() ).to.equal( '/wp-json/wp/v2/comments' ); + }); + + it( 'should set a default _supportedMethods array', function() { + expect( comments ).to.have.property( '_supportedMethods' ); + expect( comments._supportedMethods ).to.be.an( 'array' ); + }); + + it( 'should inherit CommentsRequest from WPRequest', function() { + expect( comments instanceof WPRequest ).to.be.true; + }); + + }); + + describe( 'path part setters', function() { + + describe( '.id()', function() { + + it( 'provides a method to set the ID', function() { + expect( comments ).to.have.property( 'id' ); + expect( comments.id ).to.be.a( 'function' ); + }); + + it( 'should set the ID value in the path', function() { + comments.id( 314159 ); + expect( comments._renderURI() ).to.equal( '/wp-json/wp/v2/comments/314159' ); + }); + + it( 'accepts ID parameters as strings', function() { + comments.id( '8' ); + expect( comments._renderURI() ).to.equal( '/wp-json/wp/v2/comments/8' ); + }); + + it( 'should update the supported methods when setting ID', function() { + comments.id( 8 ); + var _supportedMethods = comments._supportedMethods.sort().join( '|' ); + expect( _supportedMethods ).to.equal( 'delete|get|head|patch|post|put' ); + }); + + }); + + }); + + describe( 'URL Generation', function() { + + it( 'should create the URL for retrieving all comments', function() { + var path = comments._renderURI(); + expect( path ).to.equal( '/wp-json/wp/v2/comments' ); + }); + + it( 'should create the URL for retrieving a specific comment', function() { + var path = comments.id( 1337 )._renderURI(); + expect( path ).to.equal( '/wp-json/wp/v2/comments/1337' ); + }); + + it( 'does not throw an error if a valid numeric ID is specified', function() { + expect(function numberPassesValidation() { + comments.id( 8 ); + comments.validatePath(); + }).not.to.throw(); + }); + + it( 'does not throw an error if a valid numeric ID is specified as a string', function() { + expect( function numberAsStringPassesValidation() { + comments.id( '8' ); + comments.validatePath(); + }).not.to.throw(); + }); + + it( 'throws an error if a non-integer numeric string ID is specified', function() { + expect( function nonIntegerNumberAsStringFailsValidation() { + comments.id( 4.019 ); + comments.validatePath(); + }).to.throw(); + }); + + it( 'throws an error if a non-numeric string ID is specified', function() { + expect(function stringFailsValidation() { + comments.id( 'wombat' ); + comments.validatePath(); + }).to.throw(); + }); + + it( 'should restrict path changes to a single instance', function() { + comments.id( 2 ); + var newComments = site.comments().id( 3 ); + expect( comments._renderURI() ).to.equal( '/wp-json/wp/v2/comments/2' ); + expect( newComments._renderURI() ).to.equal( '/wp-json/wp/v2/comments/3' ); + }); + + }); + +}); diff --git a/tests/unit/route-handlers/media.js b/tests/unit/route-handlers/media.js new file mode 100644 index 00000000..816d5f7a --- /dev/null +++ b/tests/unit/route-handlers/media.js @@ -0,0 +1,117 @@ +'use strict'; +var expect = require( 'chai' ).expect; + +var WP = require( '../../../wp' ); +var WPRequest = require( '../../../lib/constructors/wp-request' ); + +describe( 'wp.media', function() { + var site; + var media; + + beforeEach(function() { + site = new WP({ + endpoint: '/wp-json', + username: 'foouser', + password: 'barpass' + }); + media = site.media(); + }); + + describe( 'constructor', function() { + + it( 'should set any passed-in options', function() { + media = site.media({ + booleanProp: true, + strProp: 'Some string' + }); + expect( media._options.booleanProp ).to.be.true; + expect( media._options.strProp ).to.equal( 'Some string' ); + }); + + it( 'should initialize _options to the site defaults', function() { + expect( media._options ).to.deep.equal({ + endpoint: '/wp-json/', + username: 'foouser', + password: 'barpass' + }); + }); + + it( 'should initialize the base path component', function() { + expect( media._renderURI() ).to.equal( '/wp-json/wp/v2/media' ); + }); + + it( 'should set a default _supportedMethods array', function() { + expect( media ).to.have.property( '_supportedMethods' ); + expect( media._supportedMethods ).to.be.an( 'array' ); + }); + + it( 'should inherit MediaRequest from WPRequest', function() { + expect( media instanceof WPRequest ).to.be.true; + }); + + }); + + describe( '.id()', function() { + + it( 'should be defined', function() { + expect( media ).to.have.property( 'id' ); + expect( media.id ).to.be.a( 'function' ); + }); + + it( 'should set the ID value in the path', function() { + media.id( 8 ); + expect( media._renderURI() ).to.equal( '/wp-json/wp/v2/media/8' ); + }); + + it( 'should update the supported methods', function() { + media.id( 8 ); + var _supportedMethods = media._supportedMethods.sort().join( '|' ); + expect( _supportedMethods ).to.equal( 'delete|get|head|patch|post|put' ); + }); + + it( 'throws an error on successive calls', function() { + expect(function successiveCallsThrowsError() { + media.id( 8 ).id( 3 ); + }).to.throw(); + }); + + it( 'passes validation when called with a number', function() { + expect(function numberPassesValidation() { + media.id( 8 )._renderPath(); + }).not.to.throw(); + }); + + it( 'passes validation when called with a number formatted as a string', function() { + expect(function numberAsStringPassesValidation() { + media.id( '9' )._renderPath(); + }).not.to.throw(); + }); + + it( 'causes a validation error when called with a non-number', function() { + expect(function stringFailsValidation() { + media.id( 'wombat' )._renderPath(); + }).to.throw(); + }); + + }); + + describe( 'url generation', function() { + + it( 'should create the URL for the media collection', function() { + var uri = media._renderURI(); + expect( uri ).to.equal( '/wp-json/wp/v2/media' ); + }); + + it( 'can paginate the media collection responses', function() { + var uri = media.page( 4 )._renderURI(); + expect( uri ).to.equal( '/wp-json/wp/v2/media?page=4' ); + }); + + it( 'should create the URL for a specific media object', function() { + var uri = media.id( 1492 )._renderURI(); + expect( uri ).to.equal( '/wp-json/wp/v2/media/1492' ); + }); + + }); + +}); diff --git a/tests/unit/lib/pages.js b/tests/unit/route-handlers/pages.js similarity index 51% rename from tests/unit/lib/pages.js rename to tests/unit/route-handlers/pages.js index 625137dd..63620975 100644 --- a/tests/unit/lib/pages.js +++ b/tests/unit/route-handlers/pages.js @@ -1,26 +1,26 @@ 'use strict'; var expect = require( 'chai' ).expect; -var PagesRequest = require( '../../../lib/pages' ); -var CollectionRequest = require( '../../../lib/shared/collection-request' ); -var WPRequest = require( '../../../lib/shared/wp-request' ); +var WP = require( '../../../wp' ); +var WPRequest = require( '../../../lib/constructors/wp-request' ); describe( 'wp.pages', function() { - - describe( 'constructor', function() { - - var pages; - - beforeEach(function() { - pages = new PagesRequest(); + var site; + var pages; + + beforeEach(function() { + site = new WP({ + endpoint: '/wp-json', + username: 'foouser', + password: 'barpass' }); + pages = site.pages(); + }); - it( 'should create a PagesRequest instance', function() { - expect( pages instanceof PagesRequest ).to.be.true; - }); + describe( 'constructor', function() { it( 'should set any passed-in options', function() { - pages = new PagesRequest({ + pages = site.pages({ booleanProp: true, strProp: 'Some string' }); @@ -28,74 +28,42 @@ describe( 'wp.pages', function() { expect( pages._options.strProp ).to.equal( 'Some string' ); }); - it( 'should default _options to {}', function() { - expect( pages._options ).to.deep.equal( {} ); - }); - - it( 'should intitialize instance properties', function() { - expect( pages._path ).to.deep.equal( {} ); - expect( pages._params ).to.deep.equal( {} ); - expect( pages._template ).to.equal( 'pages(/:id)(/:action)(/:commentId)' ); - var _supportedMethods = pages._supportedMethods.sort().join( '|' ); - expect( _supportedMethods ).to.equal( 'get|head|post' ); + it( 'should initialize _options to the site defaults', function() { + expect( pages._options ).to.deep.equal({ + endpoint: '/wp-json/', + username: 'foouser', + password: 'barpass' + }); }); - it( 'should inherit PagesRequest from CollectionRequest', function() { - expect( pages instanceof CollectionRequest ).to.be.true; - expect( pages instanceof WPRequest ).to.be.true; + it( 'should initialize the base path component', function() { + expect( pages._renderURI() ).to.equal( '/wp-json/wp/v2/pages' ); }); - it( 'should inherit prototype methods from both ancestors', function() { - // Spot-check from CollectionRequest: - expect( pages ).to.have.property( 'filter' ); - expect( pages.filter ).to.be.a( 'function' ); - expect( pages ).to.have.property( 'param' ); - expect( pages.param ).to.be.a( 'function' ); - // From WPRequest: - expect( pages ).to.have.property( 'get' ); - expect( pages.get ).to.be.a( 'function' ); - expect( pages ).to.have.property( '_renderURI' ); - expect( pages._renderURI ).to.be.a( 'function' ); + it( 'should set a default _supportedMethods array', function() { + expect( pages ).to.have.property( '_supportedMethods' ); + expect( pages._supportedMethods ).to.be.an( 'array' ); }); - }); - - describe( '_pathValidators', function() { - - it( 'defines validators for action and commentId', function() { - var pages = new PagesRequest(); - expect( pages._pathValidators ).to.deep.equal({ - action: /(comments|revisions)/, - commentId: /^\d+$/ - }); + it( 'should inherit PagesRequest from WPRequest', function() { + expect( pages instanceof WPRequest ).to.be.true; }); }); describe( 'URL Generation', function() { - var pages; - - beforeEach(function() { - pages = new PagesRequest(); - pages._options = { - endpoint: '/wp-json/' - }; - }); - - it( 'should restrict template changes to a single instance', function() { - pages._template = 'path/with/post/nr/:id'; - var newPages = new PagesRequest(); - newPages._options.endpoint = 'endpoint/url/'; - var path = newPages.id( 3 )._renderURI(); - expect( path ).to.equal( 'endpoint/url/wp/v2/pages/3' ); + it( 'should restrict path changes to a single instance', function() { + pages.id( 2 ); + var newPages = site.pages().id( 3 ).revisions(); + expect( pages._renderURI() ).to.equal( '/wp-json/wp/v2/pages/2' ); + expect( newPages._renderURI() ).to.equal( '/wp-json/wp/v2/pages/3/revisions' ); }); describe( 'page collections', function() { it( 'should create the URL for retrieving all pages', function() { - var path = pages._renderURI(); - expect( path ).to.equal( '/wp-json/wp/v2/pages' ); + expect( pages._renderURI() ).to.equal( '/wp-json/wp/v2/pages' ); }); it( 'should provide filtering methods', function() { @@ -107,34 +75,53 @@ describe( 'wp.pages', function() { }); - describe( 'page resources', function() { + describe( '.id()', function() { + + it( 'should be defined', function() { + expect( pages ).to.have.property( 'id' ); + expect( pages.id ).to.be.a( 'function' ); + }); it( 'should create the URL for retrieving a specific post', function() { var path = pages.id( 1337 )._renderURI(); expect( path ).to.equal( '/wp-json/wp/v2/pages/1337' ); }); + it( 'should update the supported methods when setting ID', function() { + pages.id( 8 ); + var _supportedMethods = pages._supportedMethods.sort().join( '|' ); + expect( _supportedMethods ).to.equal( 'delete|get|head|patch|post|put' ); + }); + + }); + + describe( '.path()', function() { + + it( 'should be defined', function() { + expect( pages ).to.have.property( 'path' ); + expect( pages.path ).to.be.a( 'function' ); + }); + it( 'should create the URL for retrieving a post by path', function() { var path = pages.path( 'nested/page' )._renderURI(); expect( path ).to .equal( '/wp-json/wp/v2/pages?filter%5Bpagename%5D=nested%2Fpage' ); }); - it( 'should update the supported methods when setting ID', function() { - pages.id( 8 ); - var _supportedMethods = pages._supportedMethods.sort().join( '|' ); - expect( _supportedMethods ).to.equal( 'delete|get|head|post|put' ); - }); - it( 'should not update the supported methods when setting Path', function() { pages.path( 'page/path' ); var _supportedMethods = pages._supportedMethods.sort().join( '|' ); - expect( _supportedMethods ).to.equal( 'get|head|post' ); + expect( _supportedMethods ).to.equal( 'delete|get|head|post|put' ); }); }); - describe( 'comments', function() { + describe.skip( 'comments', function() { + + it( 'should be defined', function() { + expect( pages ).to.have.property( 'comments' ); + expect( pages.comments ).to.be.a( 'function' ); + }); it( 'should create the URL for a page\'s comments collection', function() { var path = pages.id( 1337 ).comments()._renderURI(); @@ -165,15 +152,24 @@ describe( 'wp.pages', function() { }); - it( 'should create the URL for retrieving the revisions for a specific post', function() { - var path = pages.id( 1337 ).revisions()._renderURI(); - expect( path ).to.equal( '/wp-json/wp/v2/pages/1337/revisions' ); - }); + describe( '.revisions()', function() { + + it( 'should be defined', function() { + expect( pages ).to.have.property( 'revisions' ); + expect( pages.revisions ).to.be.a( 'function' ); + }); + + it( 'should create the URL for retrieving the revisions for a specific post', function() { + var path = pages.id( 1337 ).revisions()._renderURI(); + expect( path ).to.equal( '/wp-json/wp/v2/pages/1337/revisions' ); + }); + + it.skip( 'should force authentication when querying pages/id/revisions', function() { + pages.id( 1337 ).revisions(); + expect( pages._options ).to.have.property( 'auth' ); + expect( pages._options.auth ).to.be.true; + }); - it( 'should force authentication when querying pages/id/revisions', function() { - pages.id( 1337 ).revisions(); - expect( pages._options ).to.have.property( 'auth' ); - expect( pages._options.auth ).to.be.true; }); }); diff --git a/tests/unit/route-handlers/posts.js b/tests/unit/route-handlers/posts.js new file mode 100644 index 00000000..54efa8f3 --- /dev/null +++ b/tests/unit/route-handlers/posts.js @@ -0,0 +1,195 @@ +'use strict'; +var expect = require( 'chai' ).expect; + +var WP = require( '../../../wp' ); +var WPRequest = require( '../../../lib/constructors/wp-request' ); + +describe( 'wp.posts', function() { + var site; + var posts; + + beforeEach(function() { + site = new WP({ + endpoint: '/wp-json', + username: 'foouser', + password: 'barpass' + }); + posts = site.posts(); + }); + + describe( 'constructor', function() { + + it( 'should set any passed-in options', function() { + posts = site.posts({ + booleanProp: true, + strProp: 'Some string' + }); + expect( posts._options.booleanProp ).to.be.true; + expect( posts._options.strProp ).to.equal( 'Some string' ); + }); + + it( 'should initialize _options to the site defaults', function() { + expect( posts._options ).to.deep.equal({ + endpoint: '/wp-json/', + username: 'foouser', + password: 'barpass' + }); + }); + + it( 'should initialize the base path component', function() { + expect( posts._renderURI() ).to.equal( '/wp-json/wp/v2/posts' ); + }); + + it( 'should set a default _supportedMethods array', function() { + expect( posts ).to.have.property( '_supportedMethods' ); + expect( posts._supportedMethods ).to.be.an( 'array' ); + }); + + it( 'should inherit PostsRequest from WPRequest', function() { + expect( posts instanceof WPRequest ).to.be.true; + }); + + }); + + describe( 'path part setters', function() { + + describe( '.id()', function() { + + it( 'provides a method to set the ID', function() { + expect( posts ).to.have.property( 'id' ); + expect( posts.id ).to.be.a( 'function' ); + }); + + it( 'should set the ID value in the path', function() { + posts.id( 314159 ); + expect( posts._renderURI() ).to.equal( '/wp-json/wp/v2/posts/314159' ); + }); + + it( 'accepts ID parameters as strings', function() { + posts.id( '8' ); + expect( posts._renderURI() ).to.equal( '/wp-json/wp/v2/posts/8' ); + }); + + it( 'should update the supported methods when setting ID', function() { + posts.id( 8 ); + var _supportedMethods = posts._supportedMethods.sort().join( '|' ); + expect( _supportedMethods ).to.equal( 'delete|get|head|patch|post|put' ); + }); + + }); + + describe.skip( '.meta()', function() { + + it( 'is defined', function() { + expect( posts ).to.have.property( 'meta' ); + expect( posts.meta ).to.be.a( 'function' ); + }); + + it( 'provides a method to get the meta values for a post', function() { + posts.id( 3 ).meta(); + expect( posts._renderURI() ).to.equal( '/wp-json/wp/v2/posts/3/meta' ); + }); + + it( 'should force authentication when querying posts/id/meta', function() { + posts.id( 1337 ).meta(); + expect( posts._options ).to.have.property( 'auth' ); + expect( posts._options.auth ).to.be.true; + }); + + it( 'should update the supported methods when querying for meta', function() { + posts.id( 1066 ).meta(); + var _supportedMethods = posts._supportedMethods.sort().join( '|' ); + expect( _supportedMethods ).to.equal( 'get|head|post' ); + }); + + it( 'provides a method to get specific post meta objects by ID', function() { + posts.id( 3 ).meta( 5 ); + expect( posts._renderURI() ).to.equal( '/wp-json/wp/v2/posts/3/meta/5' ); + }); + + it( 'should force authentication when querying posts/id/meta/:id', function() { + posts.id( 7331 ).meta( 7 ); + expect( posts._options ).to.have.property( 'auth' ); + expect( posts._options.auth ).to.be.true; + }); + + it( 'should update the supported methods when querying for meta', function() { + posts.id( 1066 ).meta( 2501 ); + var _supportedMethods = posts._supportedMethods.sort().join( '|' ); + expect( _supportedMethods ).to.equal( 'delete|get|head|post|put' ); + }); + + }); + + }); + + describe( 'URL Generation', function() { + + it( 'should create the URL for retrieving all posts', function() { + var path = posts._renderURI(); + expect( path ).to.equal( '/wp-json/wp/v2/posts' ); + }); + + it( 'should create the URL for retrieving a specific post', function() { + var path = posts.id( 1337 )._renderURI(); + expect( path ).to.equal( '/wp-json/wp/v2/posts/1337' ); + }); + + it( 'does not throw an error if a valid numeric ID is specified', function() { + expect(function numberPassesValidation() { + posts.id( 8 ); + posts.validatePath(); + }).not.to.throw(); + }); + + it( 'does not throw an error if a valid numeric ID is specified as a string', function() { + expect( function numberAsStringPassesValidation() { + posts.id( '8' ); + posts.validatePath(); + }).not.to.throw(); + }); + + it( 'throws an error if a non-integer numeric string ID is specified', function() { + expect( function nonIntegerNumberAsStringFailsValidation() { + posts.id( 4.019 ); + posts.validatePath(); + }).to.throw(); + }); + + it( 'throws an error if a non-numeric string ID is specified', function() { + expect(function stringFailsValidation() { + posts.id( 'wombat' ); + posts.validatePath(); + }).to.throw(); + }); + + it.skip( 'should create the URL for retrieving all meta for a specific post', function() { + var path = posts.id( 1337 ).meta()._renderURI(); + expect( path ).to.equal( '/wp-json/wp/v2/posts/1337/meta' ); + }); + + it.skip( 'should create the URL for retrieving a specific meta item', function() { + var path = posts.id( 1337 ).meta( 2001 )._renderURI(); + expect( path ).to.equal( '/wp-json/wp/v2/posts/1337/meta/2001' ); + }); + + it( 'should create the URL for retrieving the revisions for a specific post', function() { + var path = posts.id( 1337 ).revisions()._renderURI(); + expect( path ).to.equal( '/wp-json/wp/v2/posts/1337/revisions' ); + }); + + it( 'should create the URL for retrieving a specific revision item', function() { + var path = posts.id( 1337 ).revisions( 2001 )._renderURI(); + expect( path ).to.equal( '/wp-json/wp/v2/posts/1337/revisions/2001' ); + }); + + it( 'should restrict path changes to a single instance', function() { + posts.id( 2 ); + var newPosts = site.posts().id( 3 ).revisions(); + expect( posts._renderURI() ).to.equal( '/wp-json/wp/v2/posts/2' ); + expect( newPosts._renderURI() ).to.equal( '/wp-json/wp/v2/posts/3/revisions' ); + }); + + }); + +}); diff --git a/tests/unit/route-handlers/taxonomies.js b/tests/unit/route-handlers/taxonomies.js new file mode 100644 index 00000000..c24d00c8 --- /dev/null +++ b/tests/unit/route-handlers/taxonomies.js @@ -0,0 +1,81 @@ +'use strict'; +var expect = require( 'chai' ).expect; + +var WP = require( '../../../wp' ); +var WPRequest = require( '../../../lib/constructors/wp-request' ); + +describe( 'wp.taxonomies', function() { + var site; + var taxonomies; + + beforeEach(function() { + site = new WP({ + endpoint: '/wp-json', + username: 'foouser', + password: 'barpass' + }); + taxonomies = site.taxonomies(); + }); + + describe( 'constructor', function() { + + it( 'should set any passed-in options', function() { + taxonomies = site.taxonomies({ + booleanProp: true, + strProp: 'Some string' + }); + expect( taxonomies._options.booleanProp ).to.be.true; + expect( taxonomies._options.strProp ).to.equal( 'Some string' ); + }); + + it( 'should initialize _options to the site defaults', function() { + expect( taxonomies._options ).to.deep.equal({ + endpoint: '/wp-json/', + username: 'foouser', + password: 'barpass' + }); + }); + + it( 'should initialize the base path component', function() { + expect( taxonomies._renderURI() ).to.equal( '/wp-json/wp/v2/taxonomies' ); + }); + + it( 'should set a default _supportedMethods array', function() { + expect( taxonomies ).to.have.property( '_supportedMethods' ); + expect( taxonomies._supportedMethods ).to.be.an( 'array' ); + }); + + it( 'should inherit TaxonomiesRequest from WPRequest', function() { + expect( taxonomies instanceof WPRequest ).to.be.true; + }); + + }); + + describe( 'path part setters', function() { + + describe( '.taxonomy()', function() { + + it( 'provides a method to set the taxonomy', function() { + expect( taxonomies ).to.have.property( 'taxonomy' ); + expect( taxonomies.taxonomy ).to.be.a( 'function' ); + }); + + }); + + }); + + describe( 'URL Generation', function() { + + it( 'should create the URL for retrieving all taxonomies', function() { + var url = taxonomies._renderURI(); + expect( url ).to.equal( '/wp-json/wp/v2/taxonomies' ); + }); + + it( 'should create the URL for retrieving a specific taxonomy', function() { + var url = taxonomies.taxonomy( 'category' )._renderURI(); + expect( url ).to.equal( '/wp-json/wp/v2/taxonomies/category' ); + }); + + }); + +}); diff --git a/tests/unit/route-handlers/types.js b/tests/unit/route-handlers/types.js new file mode 100644 index 00000000..2690b0b3 --- /dev/null +++ b/tests/unit/route-handlers/types.js @@ -0,0 +1,68 @@ +'use strict'; +var expect = require( 'chai' ).expect; + +var WP = require( '../../../wp' ); +var WPRequest = require( '../../../lib/constructors/wp-request' ); + +describe( 'wp.types', function() { + var site; + var types; + + beforeEach(function() { + site = new WP({ + endpoint: '/wp-json', + username: 'foouser', + password: 'barpass' + }); + types = site.types(); + }); + + describe( 'constructor', function() { + + it( 'should set any passed-in options', function() { + types = site.types({ + booleanProp: true, + strProp: 'Some string' + }); + expect( types._options.booleanProp ).to.be.true; + expect( types._options.strProp ).to.equal( 'Some string' ); + }); + + it( 'should initialize _options to the site defaults', function() { + expect( types._options ).to.deep.equal({ + endpoint: '/wp-json/', + username: 'foouser', + password: 'barpass' + }); + }); + + it( 'should initialize the base path component', function() { + expect( types._renderURI() ).to.equal( '/wp-json/wp/v2/types' ); + }); + + it( 'should set a default _supportedMethods array', function() { + expect( types ).to.have.property( '_supportedMethods' ); + expect( types._supportedMethods ).to.be.an( 'array' ); + }); + + it( 'should inherit PostsRequest from WPRequest', function() { + expect( types instanceof WPRequest ).to.be.true; + }); + + }); + + describe( 'URL Generation', function() { + + it( 'should create the URL for retrieving all types', function() { + var url = types._renderURI(); + expect( url ).to.equal( '/wp-json/wp/v2/types' ); + }); + + it( 'should create the URL for retrieving a specific term', function() { + var url = types.type( 'some_type' )._renderURI(); + expect( url ).to.equal( '/wp-json/wp/v2/types/some_type' ); + }); + + }); + +}); diff --git a/tests/unit/route-handlers/users.js b/tests/unit/route-handlers/users.js new file mode 100644 index 00000000..facd2263 --- /dev/null +++ b/tests/unit/route-handlers/users.js @@ -0,0 +1,98 @@ +'use strict'; +var expect = require( 'chai' ).expect; + +var WP = require( '../../../wp' ); +var WPRequest = require( '../../../lib/constructors/wp-request' ); + +describe( 'wp.users', function() { + var site; + var users; + + beforeEach(function() { + site = new WP({ + endpoint: '/wp-json', + username: 'foouser', + password: 'barpass' + }); + users = site.users(); + }); + + describe( 'constructor', function() { + + it( 'should set any passed-in options', function() { + users = site.users({ + booleanProp: true, + strProp: 'Some string' + }); + expect( users._options.booleanProp ).to.be.true; + expect( users._options.strProp ).to.equal( 'Some string' ); + }); + + it( 'should initialize _options to the site defaults', function() { + expect( users._options ).to.deep.equal({ + endpoint: '/wp-json/', + username: 'foouser', + password: 'barpass' + }); + }); + + it( 'should initialize the base path component', function() { + expect( users._renderURI() ).to.equal( '/wp-json/wp/v2/users' ); + }); + + it( 'should set a default _supportedMethods array', function() { + expect( users ).to.have.property( '_supportedMethods' ); + expect( users._supportedMethods ).to.be.an( 'array' ); + }); + + it( 'should inherit UsersRequest from WPRequest', function() { + expect( users instanceof WPRequest ).to.be.true; + }); + + }); + + describe( '.me()', function() { + + it( 'sets the path to users/me', function() { + users.me(); + expect( users._renderURI() ).to.equal( '/wp-json/wp/v2/users/me' ); + }); + + }); + + describe( '.id()', function() { + + it( 'should be defined', function() { + expect( users ).to.have.property( 'id' ); + expect( users.id ).to.be.a( 'function' ); + }); + + it( 'sets the path ID to the passed-in value', function() { + users.id( 2501 ); + expect( users._renderURI() ).to.equal( '/wp-json/wp/v2/users/2501' ); + }); + + }); + + describe( 'prototype._renderURI', function() { + + it( 'should create the URL for retrieving all users', function() { + var url = users._renderURI(); + expect( url ).to.equal( '/wp-json/wp/v2/users' ); + }); + + it( 'should create the URL for retrieving the current user', function() { + var url = users.me()._renderURI(); + expect( url ).to.equal( '/wp-json/wp/v2/users/me' ); + }); + + it( 'should create the URL for retrieving a specific user by ID', function() { + var url = users.id( 1337 )._renderURI(); + var _supportedMethods = users._supportedMethods.sort().join( '|' ); + expect( url ).to.equal( '/wp-json/wp/v2/users/1337' ); + expect( _supportedMethods ).to.equal( 'delete|get|head|patch|post|put' ); + }); + + }); + +}); diff --git a/tests/unit/wp.js b/tests/unit/wp.js index 5662cd6d..d14028a7 100644 --- a/tests/unit/wp.js +++ b/tests/unit/wp.js @@ -3,15 +3,8 @@ var expect = require( 'chai' ).expect; var WP = require( '../../' ); -// Other constructors, for use with instanceof checks -var MediaRequest = require( '../../lib/media' ); -var PagesRequest = require( '../../lib/pages' ); -var PostsRequest = require( '../../lib/posts' ); -var TaxonomiesRequest = require( '../../lib/taxonomies' ); -var TypesRequest = require( '../../lib/types' ); -var UsersRequest = require( '../../lib/users' ); -var CollectionRequest = require( '../../lib/shared/collection-request' ); -var WPRequest = require( '../../lib/shared/wp-request' ); +// Constructors, for use with instanceof checks +var WPRequest = require( '../../lib/constructors/wp-request' ); describe( 'wp', function() { @@ -97,35 +90,28 @@ describe( 'wp', function() { describe( '.root()', function() { + beforeEach(function() { + site = new WP({ endpoint: 'http://my.site.com/wp-json' }); + }); + it( 'is defined', function() { expect( site ).to.have.property( 'root' ); expect( site.root ).to.be.a( 'function' ); }); it( 'creates a get request against the root endpoint', function() { - site._options.endpoint = 'http://my.site.com/wp-json/'; var request = site.root(); expect( request._renderURI() ).to.equal( 'http://my.site.com/wp-json/' ); }); - it( 'takes a "path" property to query a root-relative path', function() { - site._options.endpoint = 'http://my.site.com/wp-json/'; + it( 'takes a "path" argument to query a root-relative path', function() { var request = site.root( 'custom/endpoint' ); expect( request._renderURI() ).to.equal( 'http://my.site.com/wp-json/custom/endpoint' ); }); - it( 'creates a basic WPRequest if "collection" is unspecified or "false"', function() { - var pathRequest = site.root( 'some/relative/root' ); - expect( pathRequest._template ).to.equal( 'some/relative/root' ); + it( 'creates a WPRequest object', function() { + var pathRequest = site.root( 'some/collection/endpoint' ); expect( pathRequest instanceof WPRequest ).to.be.true; - expect( pathRequest instanceof CollectionRequest ).to.be.false; - }); - - it( 'creates a CollectionRequest object if "collection" is "true"', function() { - var pathRequest = site.root( 'some/collection/endpoint', true ); - expect( pathRequest._template ).to.equal( 'some/collection/endpoint' ); - expect( pathRequest instanceof WPRequest ).to.be.true; - expect( pathRequest instanceof CollectionRequest ).to.be.true; }); it( 'inherits options from the parent WP instance', function() { @@ -145,57 +131,43 @@ describe( 'wp', function() { describe( 'endpoint accessors', function() { it( 'defines a media endpoint handler', function() { - var media = site.media(); - expect( media instanceof MediaRequest ).to.be.true; + expect( site ).to.have.property( 'media' ); + expect( site.media ).to.be.a( 'function' ); }); it( 'defines a pages endpoint handler', function() { - var posts = site.pages(); - expect( posts instanceof PagesRequest ).to.be.true; + expect( site ).to.have.property( 'pages' ); + expect( site.pages ).to.be.a( 'function' ); }); it( 'defines a posts endpoint handler', function() { - var posts = site.posts(); - expect( posts instanceof PostsRequest ).to.be.true; + expect( site ).to.have.property( 'posts' ); + expect( site.posts ).to.be.a( 'function' ); }); it( 'defines a taxonomies endpoint handler', function() { - var posts = site.taxonomies(); - expect( posts instanceof TaxonomiesRequest ).to.be.true; - }); - - it( 'defines a types endpoint handler', function() { - var posts = site.types(); - expect( posts instanceof TypesRequest ).to.be.true; + expect( site ).to.have.property( 'taxonomies' ); + expect( site.taxonomies ).to.be.a( 'function' ); }); - it( 'defines a users endpoint handler', function() { - var posts = site.users(); - expect( posts instanceof UsersRequest ).to.be.true; + it( 'defines a categories endpoint handler', function() { + expect( site ).to.have.property( 'categories' ); + expect( site.categories ).to.be.a( 'function' ); }); - }); - - describe( 'taxonomy shortcut handlers', function() { - - it( 'defines a .categories() shortcut for the category terms collection', function() { - var categories = site.categories(); - expect( categories instanceof TaxonomiesRequest ).to.be.true; - expect( categories._renderURI() ).to - .equal( 'endpoint/url/wp/v2/categories' ); + it( 'defines a tags endpoint handler', function() { + expect( site ).to.have.property( 'tags' ); + expect( site.tags ).to.be.a( 'function' ); }); - it( 'defines a .tags() shortcut for the tag terms collection', function() { - var tags = site.tags(); - expect( tags instanceof TaxonomiesRequest ).to.be.true; - expect( tags._renderURI() ).to.equal( 'endpoint/url/wp/v2/tags' ); + it( 'defines a types endpoint handler', function() { + expect( site ).to.have.property( 'types' ); + expect( site.types ).to.be.a( 'function' ); }); - it( 'defines a generic .taxonomy() handler for arbitrary taxonomy objects', function() { - var taxRequest = site.taxonomy( 'my_custom_tax' ); - expect( taxRequest instanceof TaxonomiesRequest ).to.be.true; - var uri = taxRequest._renderURI(); - expect( uri ).to.equal( 'endpoint/url/wp/v2/taxonomies/my_custom_tax' ); + it( 'defines a users endpoint handler', function() { + expect( site ).to.have.property( 'users' ); + expect( site.users ).to.be.a( 'function' ); }); }); diff --git a/wp.js b/wp.js index 6558f229..ce5b31bf 100644 --- a/wp.js +++ b/wp.js @@ -1,4 +1,3 @@ -'use strict'; /** * A WP REST API client for Node.js * @@ -15,23 +14,22 @@ * @beta }) */ +'use strict'; + var extend = require( 'node.extend' ); +// All valid routes in API v2 beta 11 +var routes = require( './lib/data/endpoint-response.json' ).routes; +var buildRouteTree = require( './lib/route-tree' ).build; +var generateEndpointFactories = require( './lib/endpoint-factories' ).generate; + var defaults = { username: '', password: '' }; -// Pull in request module constructors -var CommentsRequest = require( './lib/comments' ); -var MediaRequest = require( './lib/media' ); -var PagesRequest = require( './lib/pages' ); -var PostsRequest = require( './lib/posts' ); -var TaxonomiesRequest = require( './lib/taxonomies' ); -var TypesRequest = require( './lib/types' ); -var UsersRequest = require( './lib/users' ); -var CollectionRequest = require( './lib/shared/collection-request' ); -var WPRequest = require( './lib/shared/wp-request' ); +// Pull in base module constructors +var WPRequest = require( './lib/constructors/wp-request' ); /** * The base constructor for the WP API service @@ -65,6 +63,15 @@ function WP( options ) { return this; } +// Auto-generate default endpoint factories +var routesByNamespace = buildRouteTree( routes ); +var endpointFactories = generateEndpointFactories( 'wp/v2', routesByNamespace[ 'wp/v2' ] ); + +// Apply all auto-generated endpoint factories to the WP object prototype +Object.keys( endpointFactories ).forEach(function( methodName ) { + WP.prototype[ methodName ] = endpointFactories[ methodName ]; +}); + /** * Convenience method for making a new WP instance * @@ -83,165 +90,6 @@ WP.site = function( endpoint ) { return new WP({ endpoint: endpoint }); }; -/** - * Start a request against the `/comments` endpoint - * - * @method comments - * @param {Object} [options] An options hash for a new CommentsRequest - * @return {CommentsRequest} A CommentsRequest instance - */ -WP.prototype.comments = function( options ) { - options = options || {}; - options = extend( options, this._options ); - return new CommentsRequest( options ); -}; - -/** - * Start a request against the `/media` endpoint - * - * @method media - * @param {Object} [options] An options hash for a new MediaRequest - * @return {MediaRequest} A MediaRequest instance - */ -WP.prototype.media = function( options ) { - options = options || {}; - options = extend( options, this._options ); - return new MediaRequest( options ); -}; - -/** - * Start a request against the `/pages` endpoint - * - * @method pages - * @param {Object} [options] An options hash for a new PagesRequest - * @return {PagesRequest} A PagesRequest instance - */ -WP.prototype.pages = function( options ) { - options = options || {}; - options = extend( options, this._options ); - return new PagesRequest( options ); -}; - -/** - * Start a request against the `/posts` endpoint - * - * @method posts - * @param {Object} [options] An options hash for a new PostsRequest - * @return {PostsRequest} A PostsRequest instance - */ -WP.prototype.posts = function( options ) { - options = options || {}; - options = extend( options, this._options ); - return new PostsRequest( options ); -}; - -/** - * Start a request for a taxonomy or taxonomy term collection - * - * @method taxonomies - * @param {Object} [options] An options hash for a new TaxonomiesRequest - * @return {TaxonomiesRequest} A TaxonomiesRequest instance - */ -WP.prototype.taxonomies = function( options ) { - options = options || {}; - options = extend( options, this._options ); - return new TaxonomiesRequest( options ); -}; - -/** - * Start a request for a specific taxonomy object - * - * It is slightly unintuitive to consider the name of a taxonomy a "term," as is - * needed in order to retrieve the taxonomy object from the .taxonomies() method. - * This convenience method lets you create a `TaxonomiesRequest` object that is - * bound to the provided taxonomy name, without having to utilize the "term" method. - * - * @example - * If your site uses two custom taxonomies, book_genre and book_publisher, before you would - * have had to request these terms using the verbose form: - * - * wp.taxonomies().term( 'book_genre' ) - * wp.taxonomies().term( 'book_publisher' ) - * - * Using `.taxonomy()`, the same query can be achieved much more succinctly: - * - * wp.taxonomy( 'book_genre' ) - * wp.taxonomy( 'book_publisher' ) - * - * @method taxonomy - * @param {String} taxonomyName The name of the taxonomy to request - * @return {TaxonomiesRequest} A TaxonomiesRequest object bound to the value of taxonomyName - */ -WP.prototype.taxonomy = function( taxonomyName ) { - var options = extend( {}, this._options ); - return new TaxonomiesRequest( options ).term( taxonomyName ); -}; - -/** - * Request a list of category terms - * - * This is a shortcut method to retrieve the terms for the "category" taxonomy - * - * @example - * These are equivalent: - * - * wp.taxonomies().collection( 'categories' ) - * wp.categories() - * - * @method categories - * @return {TaxonomiesRequest} A TaxonomiesRequest object bound to the categories collection - */ -WP.prototype.categories = function() { - var options = extend( {}, this._options ); - return new TaxonomiesRequest( options ).collection( 'categories' ); -}; - -/** - * Request a list of post_tag terms - * - * This is a shortcut method to interact with the collection of terms for the - * "post_tag" taxonomy. - * - * @example - * These are equivalent: - * - * wp.taxonomies().collection( 'tags' ) - * wp.tags() - * - * @method tags - * @return {TaxonomiesRequest} A TaxonomiesRequest object bound to the tags collection - */ -WP.prototype.tags = function() { - var options = extend( {}, this._options ); - return new TaxonomiesRequest( options ).collection( 'tags' ); -}; - -/** - * Start a request against the `/types` endpoint - * - * @method types - * @param {Object} [options] An options hash for a new TypesRequest - * @return {TypesRequest} A TypesRequest instance - */ -WP.prototype.types = function( options ) { - options = options || {}; - options = extend( options, this._options ); - return new TypesRequest( options ); -}; - -/** - * Start a request against the `/users` endpoint - * - * @method users - * @param {Object} [options] An options hash for a new UsersRequest - * @return {UsersRequest} A UsersRequest instance - */ -WP.prototype.users = function( options ) { - options = options || {}; - options = extend( options, this._options ); - return new UsersRequest( options ); -}; - /** * Generate a request against a completely arbitrary endpoint, with no assumptions about * or mutation of path, filtering, or query parameters. This request is not restricted to @@ -269,18 +117,16 @@ WP.prototype.url = function( url ) { * * @method root * @param {String} [relativePath] An endpoint-relative path to which to bind the request - * @param {Boolean} [collection] Whether to return a CollectionRequest or a vanilla WPRequest - * @return {CollectionRequest|WPRequest} A request object + * @return {WPRequest} A request object */ -WP.prototype.root = function( relativePath, collection ) { +WP.prototype.root = function( relativePath ) { relativePath = relativePath || ''; - collection = collection || false; var options = extend( {}, this._options ); // Request should be - var request = collection ? new CollectionRequest( options ) : new WPRequest( options ); + var request = new WPRequest( options ); // Set the path template to the string passed in - request._template = relativePath; + request._path = { '0': relativePath }; return request; };