diff --git a/lib/comments.js b/lib/comments.js index 0b3c8630..5ecb9c38 100644 --- a/lib/comments.js +++ b/lib/comments.js @@ -5,8 +5,12 @@ * @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 * @@ -73,6 +77,12 @@ function CommentsRequest( options ) { // 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 * @@ -143,40 +153,4 @@ CommentsRequest.prototype.term = function( term ) { return this; }; -/** - * Search for hierarchical taxonomy terms that are children of the parent term - * indicated by the provided term ID - * - * @example - * - * wp.categories().parent( 42 ).then(function( categories ) { - * console.log( 'all of these categories are sub-items of cat ID#42:' ); - * console.log( categories ); - * }); - * - * @method parent - * @chainable - * @param {Number} parentId The ID of a (hierarchical) taxonomy term - * @return {CommentsRequest} The CommentsRequest instance (for chaining) - */ -CommentsRequest.prototype.parent = function( parentId ) { - this.param( 'parent', parentId, true ); - - return this; -}; - -/** - * Specify the post for which to retrieve terms - * - * @method forPost - * @chainable - * @param {String|Number} post The ID of the post for which to retrieve terms - * @return {CommentsRequest} The CommentsRequest instance (for chaining) - */ -CommentsRequest.prototype.forPost = function( postId ) { - this.param( 'post', postId ); - - return this; -}; - module.exports = CommentsRequest; diff --git a/lib/shared/alphanumeric-sort.js b/lib/lib/alphanumeric-sort.js similarity index 100% rename from lib/shared/alphanumeric-sort.js rename to lib/lib/alphanumeric-sort.js diff --git a/lib/media.js b/lib/media.js index 110465b3..6df9a575 100644 --- a/lib/media.js +++ b/lib/media.js @@ -28,24 +28,6 @@ function MediaRequest( options ) { */ 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 * diff --git a/lib/mixins/filters.js b/lib/mixins/filters.js new file mode 100644 index 00000000..0e785da8 --- /dev/null +++ b/lib/mixins/filters.js @@ -0,0 +1,218 @@ +'use strict'; +/** + * Filter methods that can be mixed in to a request constructor's prototype to + * allow that request to take advantage of the `?filter[]=` aliases for WP_Query + * parameters for collection endpoints. These are most relevant to posts, pages + * and CPTs. + * + * @module filters + */ +var _ = require( 'lodash' ); +var extend = require( 'node.extend' ); +var alphaNumericSort = require( '../lib/alphanumeric-sort' ); + +var filterMixins = {}; + +// Filter Methods +// ============== + +/** + * Specify key-value pairs by which to filter the API results (commonly used + * to retrieve only posts meeting certain criteria, such as posts within a + * particular category or by a particular author). + * + * @example + * // Set a single property: + * wp.filter( 'post_type', 'cpt_event' )... + * + * // Set multiple properties at once: + * wp.filter({ + * post_status: 'publish', + * category_name: 'news' + * })... + * + * // Chain calls to .filter(): + * wp.filter( 'post_status', 'publish' ).filter( 'category_name', 'news' )... + * + * @method filter + * @chainable + * @param {String|Object} props A filter property name string, or object of name/value pairs + * @param {String|Number|Array} [value] The value(s) corresponding to the provided filter property + * @return The request instance (for chaining) + */ +filterMixins.filter = function( props, value ) { + /* jshint validthis:true */ + + if ( ! props || _.isString( props ) && typeof value === 'undefined' ) { + // We have no filter to set, or no value to set for that filter + return this; + } + + // convert the property name string `props` and value `value` into an object + if ( _.isString( props ) ) { + props = _.zipObject([[ props, value ]]); + } + + this._filters = extend( this._filters, props ); + + return this; +}; + +/** + * Restrict the query results to posts matching one or more taxonomy terms. + * + * @method taxonomy + * @chainable + * @param {String} taxonomy The name of the taxonomy to filter by + * @param {String|Number|Array} term A string or integer, or array thereof, representing terms + * @return The request instance (for chaining) + */ +filterMixins.taxonomy = function( taxonomy, term ) { + /* jshint validthis:true */ + var termIsArray = _.isArray( term ); + var termIsNumber = termIsArray ? + term.reduce(function( allAreNumbers, term ) { + return allAreNumbers && _.isNumber( term ); + }, true ) : + _.isNumber( term ); + var termIsString = termIsArray ? + term.reduce(function( allAreStrings, term ) { + return allAreStrings && _.isString( term ); + }, true ) : + _.isString( term ); + var taxonomyTerms; + + if ( ! termIsString && ! termIsNumber ) { + throw new Error( 'term must be a number, string, or array of numbers or strings' ); + } + + if ( taxonomy === 'category' ) { + if ( termIsString ) { + // Query param for filtering by category slug is "category_name" + taxonomy = 'category_name'; + } else { + // The boolean check above ensures that if taxonomy === 'category' and + // term is not a string, then term must be a number and therefore an ID: + // Query param for filtering by category ID is "cat" + taxonomy = 'cat'; + } + } else if ( taxonomy === 'post_tag' ) { + // tag is used in place of post_tag in the public query variables + taxonomy = 'tag'; + } + + // Ensure the taxonomy filters object is available + this._taxonomyFilters = this._taxonomyFilters || {}; + + // Ensure there's an array of terms available for this taxonomy + taxonomyTerms = ( this._taxonomyFilters[ taxonomy ] || [] ) + // Insert the provided terms into the specified taxonomy's terms array + .concat( term ) + // Sort array + .sort( alphaNumericSort ); + + // De-dupe + this._taxonomyFilters[ taxonomy ] = _.unique( taxonomyTerms, true ); + + return this; +}; + +/** + * Convenience wrapper for `.taxonomy( 'category', ... )`. + * + * @method category + * @chainable + * @param {String|Number|Array} category A string or integer, or array thereof, representing terms + * @return The request instance (for chaining) + */ +filterMixins.category = function( category ) { + /* jshint validthis:true */ + return filterMixins.taxonomy.call( this, 'category', category ); +}; + +/** + * Convenience wrapper for `.taxonomy( 'tag', ... )`. + * + * @method tag + * @chainable + * @param {String|Number|Array} tag A tag term string or array of tag term strings + * @return The request instance (for chaining) + */ +filterMixins.tag = function( tag ) { + /* jshint validthis:true */ + return filterMixins.taxonomy.call( this, 'tag', tag ); +}; + +/** + * Query for posts published in a given year. + * + * @method year + * @chainable + * @param {Number} year integer representation of year requested + * @returns The request instance (for chaining) + */ +filterMixins.year = function( year ) { + /* jshint validthis:true */ + return filterMixins.filter.call( this, 'year', year ); +}; + +/** + * Query for posts published in a given month, either by providing the number + * of the requested month (e.g. 3), or the month's name as a string (e.g. "March") + * + * @method month + * @chainable + * @param {Number|String} month Integer for month (1) or month string ("January") + * @returns The request instance (for chaining) + */ +filterMixins.month = function( month ) { + /* jshint validthis:true */ + var monthDate; + if ( _.isString( month ) ) { + // Append a arbitrary day and year to the month to parse the string into a Date + monthDate = new Date( Date.parse( month + ' 1, 2012' ) ); + + // If the generated Date is NaN, then the passed string is not a valid month + if ( isNaN( monthDate ) ) { + return this; + } + + // JS Dates are 0 indexed, but the WP API requires a 1-indexed integer + month = monthDate.getMonth() + 1; + } + + // If month is a Number, add the monthnum filter to the request + if ( _.isNumber( month ) ) { + return filterMixins.filter.call( this, 'monthnum', month ); + } + + return this; +}; + +/** + * Add the day filter into the request to retrieve posts for a given day + * + * @method day + * @chainable + * @param {Number} day Integer representation of the day requested + * @returns The request instance (for chaining) + */ +filterMixins.day = function( day ) { + /* jshint validthis:true */ + return filterMixins.filter.call( this, 'day', day ); +}; + +/** + * Specify that we are requesting a page by its path (specific to Page resources) + * + * @method path + * @chainable + * @param {String} path The root-relative URL path for a page + * @returns The request instance (for chaining) + */ +filterMixins.path = function( path ) { + /* jshint validthis:true */ + return filterMixins.filter.call( this, 'pagename', path ); +}; + +module.exports = filterMixins; diff --git a/lib/mixins/parameters.js b/lib/mixins/parameters.js new file mode 100644 index 00000000..c6d2ac0b --- /dev/null +++ b/lib/mixins/parameters.js @@ -0,0 +1,166 @@ +'use strict'; +/*jshint -W106 */// Disable underscore_case warnings in this file b/c WP uses them +/** + * Filter methods that can be mixed in to a request constructor's prototype to + * allow that request to take advantage of top-level query parameters for + * collection endpoints. These are most relevant to posts, pages and CPTs, but + * pagination helpers are applicable to any collection. + * + * @module filters + */ +var _ = require( 'lodash' ); + +var parameterMixins = {}; + +// Needed for .author mixin, as author by ID is a parameter and by Name is a filter +var filter = require( './filters' ).filter; + +// Pagination Methods +// ================== + +/** + * Set the pagination of a request. Use in conjunction with `.perPage()` for explicit + * pagination handling. (The number of pages in a response can be retrieved from the + * response's `_paging.totalPages` property.) + * + * @method page + * @chainable + * @param {Number} pageNumber The page number of results to retrieve + * @return The request instance (for chaining) + */ +parameterMixins.page = function( pageNumber ) { + /* jshint validthis:true */ + return this.param( 'page', pageNumber ); +}; + +/** + * Set the number of items to be returned in a page of responses. + * + * @method perPage + * @chainable + * @param {Number} itemsPerPage The number of items to return in one page of results + * @return The request instance (for chaining) + */ +parameterMixins.perPage = function( itemsPerPage ) { + /* jshint validthis:true */ + return this.param( 'per_page', itemsPerPage ); +}; + +// Parameter Methods +// ================= + +/** + * Query a collection for members with a specific slug. + * + * @method slug + * @chainable + * @param {String} slug A post slug (slug), e.g. "hello-world" + * @return The request instance (for chaining) + */ +parameterMixins.slug = function( slug ) { + /* jshint validthis:true */ + return this.param( 'slug', slug ); +}; + +/** + * Alias for .slug() + * + * @method name + * @alias slug + * @chainable + * @param {String} slug A post name (slug), e.g. "hello-world" + * @return The request instance (for chaining) + */ +parameterMixins.name = function( slug ) { + /* jshint validthis:true */ + return parameterMixins.slug.call( this, slug ); +}; + +/** + * Filter results to those matching the specified search terms. + * + * @method search + * @chainable + * @param {String} searchString A string to search for within post content + * @return The request instance (for chaining) + */ +parameterMixins.search = function( searchString ) { + /* jshint validthis:true */ + return this.param( 'search', searchString ); +}; + +/** + * Query for posts by a specific author. + * This method will replace any previous 'author' query parameters that had been set. + * + * Note that this method will either set the "author" top-level query parameter, + * or else the "author_name" filter parameter: this is irregular as most parameter + * helper methods either set a top level parameter or a filter, not both. + * + * @method author + * @chainable + * @param {String|Number} author The nicename or ID for a particular author + * @return The request instance (for chaining) + */ +parameterMixins.author = function( author ) { + /* jshint validthis:true */ + if ( typeof author === 'undefined' ) { + return this; + } + if ( _.isString( author ) ) { + this.param( 'author', null ); + return filter.call( this, 'author_name', author ); + } + if ( _.isNumber( author ) ) { + filter.call( this, 'author_name', null ); + return this.param( 'author', author ); + } + if ( author === null ) { + filter.call( this, 'author_name', null ); + return this.param( 'author', null ); + } + throw new Error( 'author must be either a nicename string or numeric ID' ); +}; + +/** + * Search for hierarchical taxonomy terms that are children of the parent term + * indicated by the provided term ID + * + * @example + * + * wp.pages().parent( 3 ).then(function( pages ) { + * // console.log( 'all of these pages are nested below page ID#3:' ); + * // console.log( pages ); + * }); + * + * wp.categories().parent( 42 ).then(function( categories ) { + * console.log( 'all of these categories are sub-items of cat ID#42:' ); + * console.log( categories ); + * }); + * + * @method parent + * @chainable + * @param {Number} parentId The ID of a (hierarchical) taxonomy term + * @return The request instance (for chaining) + */ +parameterMixins.parent = function( parentId ) { + /* jshint validthis:true */ + return this.param( 'parent', parentId, true ); +}; + +/** + * Specify the post for which to retrieve terms (relevant for *e.g.* taxonomy + * and comment collection endpoints). `forPost` is used to avoid conflicting + * with the `.post()` method, which corresponds to the HTTP POST action. + * + * @method forPost + * @chainable + * @param {String|Number} post The ID of the post for which to retrieve terms + * @return The request instance (for chaining) + */ +parameterMixins.forPost = function( postId ) { + /* jshint validthis:true */ + return this.param( 'post', postId ); +}; + +module.exports = parameterMixins; diff --git a/lib/pages.js b/lib/pages.js index 8f591e1f..f870d19d 100644 --- a/lib/pages.js +++ b/lib/pages.js @@ -5,8 +5,12 @@ * @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 * @@ -28,24 +32,6 @@ function PagesRequest( options ) { */ 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 * @@ -90,6 +76,12 @@ function PagesRequest( options ) { 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 * @@ -180,18 +172,4 @@ PagesRequest.prototype.revisions = function() { return this.auth(); }; -/** - * Specify that we are requesting a page by its path - * - * @method path - * @chainable - * @param {String} path The root-relative URL path for a page - * @return {PagesRequest} The PagesRequest instance (for chaining) - */ -PagesRequest.prototype.path = function( path ) { - return this.filter({ - pagename: path - }); -}; - module.exports = PagesRequest; diff --git a/lib/posts.js b/lib/posts.js index 62900ab5..b0d75cb4 100644 --- a/lib/posts.js +++ b/lib/posts.js @@ -28,24 +28,6 @@ function PostsRequest( options ) { */ 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 * diff --git a/lib/shared/collection-request.js b/lib/shared/collection-request.js index 9dc89e58..f839582f 100644 --- a/lib/shared/collection-request.js +++ b/lib/shared/collection-request.js @@ -1,16 +1,16 @@ 'use strict'; -/*jshint -W106 */// Disable underscore_case warnings in this file b/c WP uses them /** * @module WP * @submodule CollectionRequest * @beta */ var WPRequest = require( './wp-request' ); -var _ = require( 'lodash' ); +var pick = require( 'lodash' ).pick; var extend = require( 'node.extend' ); var inherit = require( 'util' ).inherits; -var alphaNumericSort = require( './alphanumeric-sort' ); +var filters = require( '../mixins/filters' ); +var parameters = require( '../mixins/parameters' ); /** * CollectionRequest extends WPRequest with properties & methods for filtering collections @@ -100,276 +100,29 @@ function CollectionRequest( options ) { inherit( CollectionRequest, WPRequest ); -// Private helper methods -// ====================== - -/** - * Utility function for sorting arrays of numbers or strings. - * - * @param {String|Number} a The first comparator operand - * @param {String|Number} a The second comparator operand - * @return -1 if the values are backwards, 1 if they're ordered, and 0 if they're the same - */ -function alphaNumericSort( a, b ) { - if ( a > b ) { - return 1; - } - if ( a < b ) { - return -1; - } - return 0; -} - -// Prototype Methods -// ================= - -/** - * Set the pagination of a request. Use in conjunction with `.perPage()` for explicit - * pagination handling. (The number of pages in a response can be retrieved from the - * response's `_paging.totalPages` property.) - * - * @method page - * @chainable - * @param {Number} pageNumber The page number of results to retrieve - * @return {CollectionRequest} The CollectionRequest instance (for chaining) - */ -CollectionRequest.prototype.page = function( pageNumber ) { - return this.param( 'page', pageNumber ); -}; - -/** - * Set the number of items to be returned in a page of responses. - * - * @method perPage - * @chainable - * @param {Number} itemsPerPage The number of items to return in one page of results - * @return {CollectionRequest} The CollectionRequest instance (for chaining) - */ -CollectionRequest.prototype.perPage = function( itemsPerPage ) { - return this.param( 'per_page', itemsPerPage ); -}; - -/** - * Specify key-value pairs by which to filter the API results (commonly used - * to retrieve only posts meeting certain criteria, such as posts within a - * particular category or by a particular author). - * - * @example - * // Set a single property: - * wp.filter( 'post_type', 'cpt_event' )... - * - * // Set multiple properties at once: - * wp.filter({ - * post_status: 'publish', - * category_name: 'news' - * })... - * - * // Chain calls to .filter(): - * wp.filter( 'post_status', 'publish' ).filter( 'category_name', 'news' )... - * - * @method filter - * @chainable - * @param {String|Object} props A filter property name string, or object of name/value pairs - * @param {String|Number|Array} [value] The value(s) corresponding to the provided filter property - * @return {CollectionRequest} The CollectionRequest instance (for chaining) - */ -CollectionRequest.prototype.filter = function( props, value ) { - // convert the property name string `props` and value `value` into an object - if ( _.isString( props ) && value ) { - props = _.zipObject([[ props, value ]]); - } - - this._filters = extend( this._filters, props ); - - return this; -}; - -/** - * Restrict the query results to posts matching one or more taxonomy terms. - * - * @method taxonomy - * @chainable - * @param {String} taxonomy The name of the taxonomy to filter by - * @param {String|Number|Array} term A string or integer, or array thereof, representing terms - * @return {CollectionRequest} The CollectionRequest instance (for chaining) - */ -CollectionRequest.prototype.taxonomy = function( taxonomy, term ) { - var termIsArray = _.isArray( term ); - var termIsNumber = termIsArray ? _.isNumber( term[ 0 ] ) : _.isNumber( term ); - var termIsString = termIsArray ? _.isString( term[ 0 ] ) : _.isString( term ); - var taxonomyTerms; - - if ( ! termIsString && ! termIsNumber ) { - throw new Error( 'term must be a number, string, or array of numbers or strings' ); - } - - if ( taxonomy === 'category' ) { - if ( termIsString ) { - // Query param for filtering by category slug is category_name - taxonomy = 'category_name'; - } else if ( termIsNumber ) { - // Query param for filtering by category slug is category_name - taxonomy = 'cat'; - } - } else if ( taxonomy === 'post_tag' ) { - // tag is used in place of post_tag in the public query variables - taxonomy = 'tag'; - } - - // Ensure there's an array of terms available for this taxonomy - taxonomyTerms = this._taxonomyFilters[ taxonomy ] || []; - - // Insert the provided terms into the specified taxonomy's terms array - if ( termIsArray ) { - taxonomyTerms = taxonomyTerms.concat( term ); - } else { - taxonomyTerms.push( term ); - } - - // Sort array - taxonomyTerms.sort( alphaNumericSort ); - - // De-dupe - this._taxonomyFilters[ taxonomy ] = _.unique( taxonomyTerms, true ); - - return this; -}; - -/** - * Convenience wrapper for `.taxonomy( 'category', ... )`. - * - * @method category - * @chainable - * @param {String|Number|Array} category A string or integer, or array thereof, representing terms - * @return {CollectionRequest} The CollectionRequest instance (for chaining) - */ -CollectionRequest.prototype.category = function( category ) { - return this.taxonomy( 'category', category ); -}; - -/** - * Convenience wrapper for `.taxonomy( 'tag', ... )`. - * - * @method tag - * @chainable - * @param {String|Number|Array} tag A tag term string or array of tag term strings - * @return {CollectionRequest} The CollectionRequest instance (for chaining) - */ -CollectionRequest.prototype.tag = function( tag ) { - return this.taxonomy( 'tag', tag ); -}; - -/** - * Filter results to those matching the specified search terms. - * - * @method search - * @param {String} searchString A string to search for within post content - * @return {CollectionRequest} The CollectionRequest instance (for chaining) - */ -CollectionRequest.prototype.search = function( searchString ) { - return this.param( 'search', searchString ); -}; - -/** - * Query for posts by a specific author. - * This method will replace any previous 'author' query parameters that had been set. - * - * @method author - * @chainable - * @param {String|Number} author The nicename or ID for a particular author - * @return {CollectionRequest} The CollectionRequest instance (for chaining) - */ -CollectionRequest.prototype.author = function( author ) { - if ( _.isString( author ) ) { - delete this._params.author; - return this.filter( 'author_name', author ); - } - if ( _.isNumber( author ) ) { - delete this._filters.author_name; - return this.param( 'author', author ); - } - throw new Error( 'author must be either a nicename string or numeric ID' ); -}; - -/** - * Query a collection for members with a specific slug. - * - * @method slug - * @chainable - * @param {String} slug A post slug (slug), e.g. "hello-world" - * @return {CollectionRequest} The CollectionRequest instance (for chaining) - */ -CollectionRequest.prototype.slug = function( slug ) { - return this.param( 'slug', slug ); -}; - -/** - * Alias for .slug() - * - * @method name - * @alias slug - * @chainable - * @param {String} slug A post name (slug), e.g. "hello-world" - * @return {CollectionRequest} The CollectionRequest instance (for chaining) - */ -CollectionRequest.prototype.name = function( slug ) { - return this.slug( slug ); -}; - -/** - * Query for posts published in a given year. - * - * @method year - * @chainable - * @param {Number} year integer representation of year requested - * @returns {CollectionRequest} The CollectionRequest instance (for chaining) - */ -CollectionRequest.prototype.year = function( year ) { - return this.filter( 'year', year ); -}; - -/** - * Query for posts published in a given month, either by providing the number - * of the requested month (e.g. 3), or the month's name as a string (e.g. "March") - * - * @method month - * @chainable - * @param {Number|String} month Integer for month (1) or month string ("January") - * @returns {CollectionRequest} The PostsRequest instance (for chaining) - */ -CollectionRequest.prototype.month = function( month ) { - var monthDate; - if ( _.isString( month ) ) { - // Append a arbitrary day and year to the month to parse the string into a Date - monthDate = new Date( Date.parse( month + ' 1, 2012' ) ); - - // If the generated Date is NaN, then the passed string is not a valid month - if ( isNaN( monthDate ) ) { - return this; - } - - // JS Dates are 0 indexed, but the WP API requires a 1-indexed integer - return this.filter( 'monthnum', monthDate.getMonth() + 1 ); - } - - // If month is a Number, add the monthnum filter to the request - if ( _.isNumber( month ) ) { - return this.filter( 'monthnum', month ); - } - - return this; -}; - -/** - * Add the day filter into the request to retrieve posts for a given day - * - * @method day - * @chainable - * @param {Number} day Integer representation of the day requested - * @returns {CollectionRequest} The CollectionRequest instance (for chaining) - */ -CollectionRequest.prototype.day = function( day ) { - return this.filter( 'day', day ); -}; +// 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/shared/wp-request.js b/lib/shared/wp-request.js index 0c4c0b8d..8acdfead 100644 --- a/lib/shared/wp-request.js +++ b/lib/shared/wp-request.js @@ -16,7 +16,7 @@ var _ = require( 'lodash' ); var extend = require( 'node.extend' ); // TODO: reorganize library so that this has a better home -var alphaNumericSort = require( './alphanumeric-sort' ); +var alphaNumericSort = require( '../lib/alphanumeric-sort' ); /** * WPRequest is the base API request object constructor @@ -394,14 +394,20 @@ WPRequest.prototype._renderQuery = function() { WPRequest.prototype.param = function( props, value, merge ) { merge = merge || false; - // We can use the same iterator function below to handle explicit key-value pairs if we - // convert them into to an object we can iterate over: - if ( _.isString( props ) && typeof value !== 'undefined' ) { + if ( ! props || _.isString( props ) && typeof value === 'undefined' ) { + // We have no property to set, or no value to set for that property + return this; + } + + // We can use the same iterator function below to handle explicit key-value + // pairs if we convert them into to an object we can iterate over: + if ( _.isString( props ) ) { props = _.zipObject([[ props, value ]]); } // Iterate through the properties - _.each( props, function( value, key ) { + Object.keys( props ).forEach(function( key ) { + var value = props[ key ]; var currentVal = this._params[ key ]; // Simple case: setting for the first time, or not merging diff --git a/lib/taxonomies.js b/lib/taxonomies.js index ccdace4d..850d7d52 100644 --- a/lib/taxonomies.js +++ b/lib/taxonomies.js @@ -5,7 +5,10 @@ * @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 @@ -28,15 +31,6 @@ function TaxonomiesRequest( options ) { */ 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 non-filter query parameters * @@ -126,40 +120,9 @@ TaxonomiesRequest.prototype.term = function( term ) { return this; }; -/** - * Search for hierarchical taxonomy terms that are children of the parent term - * indicated by the provided term ID - * - * @example - * - * wp.categories().parent( 42 ).then(function( categories ) { - * console.log( 'all of these categories are sub-items of cat ID#42:' ); - * console.log( categories ); - * }); - * - * @method parent - * @chainable - * @param {Number} parentId The ID of a (hierarchical) taxonomy term - * @return {TaxonomiesRequest} The TaxonomiesRequest instance (for chaining) - */ -TaxonomiesRequest.prototype.parent = function( parentId ) { - this.param( 'parent', parentId, true ); - - return this; -}; - -/** - * Specify the post for which to retrieve terms - * - * @method forPost - * @chainable - * @param {String|Number} post The ID of the post for which to retrieve terms - * @return {TaxonomiesRequest} The TaxonomiesRequest instance (for chaining) - */ -TaxonomiesRequest.prototype.forPost = function( postId ) { - this.param( 'post', postId ); - - return this; -}; +extend( TaxonomiesRequest.prototype, pick( parameters, [ + 'parent', + 'forPost' +] ) ); module.exports = TaxonomiesRequest; diff --git a/lib/types.js b/lib/types.js index b5a0ab17..0ac658ca 100644 --- a/lib/types.js +++ b/lib/types.js @@ -28,15 +28,6 @@ function TypesRequest( options ) { */ 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 non-filter query parameters * diff --git a/lib/users.js b/lib/users.js index cd2ca054..f5116f6d 100644 --- a/lib/users.js +++ b/lib/users.js @@ -29,15 +29,6 @@ function UsersRequest( options ) { */ 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 non-filter query parameters * diff --git a/tests/integration/pages.js b/tests/integration/pages.js new file mode 100644 index 00000000..9b7fd038 --- /dev/null +++ b/tests/integration/pages.js @@ -0,0 +1,199 @@ +'use strict'; +var chai = require( 'chai' ); +// Variable to use as our "success token" in promise assertions +var SUCCESS = 'success'; +// Chai-as-promised and the `expect( prom ).to.eventually.equal( SUCCESS ) is +// used to ensure that the assertions running within the promise chains are +// actually run. +chai.use( require( 'chai-as-promised' ) ); +var expect = chai.expect; + +var WP = require( '../../' ); +var WPRequest = require( '../../lib/shared/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) +var expectedResults = { + titles: { + page1: [ + 'Page Markup And Formatting', + 'Page Image Alignment', + 'Level 3b', + 'Level 3a', + 'Level 2b', + 'Level 2a', + 'Page B', + 'Page A', + 'Blog', + 'Front Page' + ], + page2: [ + 'Clearing Floats', + 'About The Tests', + 'Level 1', + 'Level 2', + 'Level 3', + 'Page with comments disabled', + 'Page with comments', + 'Lorem Ipsum' + ] + } +}; + +// Inspecting the titles of the returned pages arrays is an easy way to +// validate that the right page of results was returned +function getTitles( pages ) { + return pages.map(function( post ) { + return post.title.rendered; + }); +} + +describe( 'integration: pages()', function() { + var wp; + + beforeEach(function() { + wp = new WP({ + endpoint: 'http://wpapi.loc/wp-json' + }); + }); + + it( 'can be used to retrieve a list of recent pages', function() { + var prom = wp.pages().get().then(function( pages ) { + expect( pages ).to.be.an( 'array' ); + expect( pages.length ).to.equal( 10 ); + return SUCCESS; + }); + return expect( prom ).to.eventually.equal( SUCCESS ); + }); + + it( 'fetches the 10 most recent pages by default', function() { + var prom = wp.pages().get().then(function( pages ) { + expect( getTitles( pages ) ).to.deep.equal( expectedResults.titles.page1 ); + return SUCCESS; + }); + return expect( prom ).to.eventually.equal( SUCCESS ); + }); + + describe( 'paging properties', function() { + + it( 'are exposed as _paging on the response array', function() { + var prom = wp.pages().get().then(function( pages ) { + expect( pages ).to.have.property( '_paging' ); + expect( pages._paging ).to.be.an( 'object' ); + return SUCCESS; + }); + return expect( prom ).to.eventually.equal( SUCCESS ); + }); + + it( 'include the total number of pages', function() { + var prom = wp.pages().get().then(function( pages ) { + expect( pages._paging ).to.have.property( 'total' ); + expect( pages._paging.total ).to.equal( '18' ); + return SUCCESS; + }); + return expect( prom ).to.eventually.equal( SUCCESS ); + }); + + it( 'include the total number of pages available', function() { + var prom = wp.pages().get().then(function( pages ) { + expect( pages._paging ).to.have.property( 'totalPages' ); + expect( pages._paging.totalPages ).to.equal( '2' ); + return SUCCESS; + }); + return expect( prom ).to.eventually.equal( SUCCESS ); + }); + + it( 'provides a bound WPRequest for the next page as .next', function() { + var prom = wp.pages().get().then(function( pages ) { + expect( pages._paging ).to.have.property( 'next' ); + expect( pages._paging.next ).to.be.an( 'object' ); + expect( pages._paging.next ).to.be.an.instanceOf( WPRequest ); + expect( pages._paging.next._options.endpoint ).to + .equal( 'http://wpapi.loc/wp-json/wp/v2/pages?page=2' ); + // Get last page & ensure "next" no longer appears + return wp.pages().page( pages._paging.totalPages ).get().then(function( pages ) { + expect( pages._paging ).not.to.have.property( 'next' ); + expect( getTitles( pages ) ).to.deep.equal( expectedResults.titles.page2 ); + return SUCCESS; + }); + }); + return expect( prom ).to.eventually.equal( SUCCESS ); + }); + + it( 'allows access to the next page of results via .next', function() { + var prom = wp.pages().get().then(function( pages ) { + return pages._paging.next.get().then(function( pages ) { + expect( pages ).to.be.an( 'array' ); + expect( pages.length ).to.equal( 8 ); + expect( getTitles( pages ) ).to.deep.equal( expectedResults.titles.page2 ); + return SUCCESS; + }); + }); + return expect( prom ).to.eventually.equal( SUCCESS ); + }); + + it( 'provides a bound WPRequest for the previous page as .prev', function() { + var prom = wp.pages().get().then(function( pages ) { + expect( pages._paging ).not.to.have.property( 'prev' ); + return pages._paging.next.get().then(function( pages ) { + expect( pages._paging ).to.have.property( 'prev' ); + expect( pages._paging.prev ).to.be.an( 'object' ); + expect( pages._paging.prev ).to.be.an.instanceOf( WPRequest ); + expect( pages._paging.prev._options.endpoint ).to + .equal( 'http://wpapi.loc/wp-json/wp/v2/pages?page=1' ); + return SUCCESS; + }); + }); + return expect( prom ).to.eventually.equal( SUCCESS ); + }); + + it( 'allows access to the previous page of results via .prev', function() { + var prom = wp.pages().page( 2 ).get().then(function( pages ) { + expect( getTitles( pages ) ).to.deep.equal( expectedResults.titles.page2 ); + return pages._paging.prev.get().then(function( pages ) { + expect( pages ).to.be.an( 'array' ); + expect( pages.length ).to.equal( 10 ); + expect( getTitles( pages ) ).to.deep.equal( expectedResults.titles.page1 ); + return SUCCESS; + }); + }); + return expect( prom ).to.eventually.equal( SUCCESS ); + }); + + }); + + describe( 'filter methods', function() { + + describe( 'slug', function() { + + it( 'can be used to return only pages with the specified slug', function() { + var prom = wp.pages().slug( 'clearing-floats' ).get().then(function( pages ) { + expect( pages.length ).to.equal( 1 ); + expect( getTitles( pages ) ).to.deep.equal([ + 'Clearing Floats' + ]); + return SUCCESS; + }); + return expect( prom ).to.eventually.equal( SUCCESS ); + }); + + }); + + describe( 'path', function() { + + it( 'can be used to return only pages with the specified URL path', function() { + var prom = wp.pages().path( 'level-1/level-2/level-3a' ).get().then(function( pages ) { + expect( pages.length ).to.equal( 1 ); + expect( getTitles( pages ) ).to.deep.equal([ + 'Level 3a' + ]); + return SUCCESS; + }); + return expect( prom ).to.eventually.equal( SUCCESS ); + }); + + }); + + }); + +}); diff --git a/tests/integration/posts.js b/tests/integration/posts.js index 5fefa29f..b885991b 100644 --- a/tests/integration/posts.js +++ b/tests/integration/posts.js @@ -180,6 +180,21 @@ describe( 'integration: posts()', function() { describe( 'filter methods', function() { + describe( 'slug', function() { + + it( 'can be used to return only posts with the specified slug', function() { + var prom = wp.posts().slug( 'template-excerpt-generated' ).get().then(function( posts ) { + expect( posts.length ).to.equal( 1 ); + expect( getTitles( posts ) ).to.deep.equal([ + 'Template: Excerpt (Generated)' + ]); + return SUCCESS; + }); + return expect( prom ).to.eventually.equal( SUCCESS ); + }); + + }); + describe( 'tag', function() { it( 'can be used to return only posts with a provided tag', function() { diff --git a/tests/unit/lib/media.js b/tests/unit/lib/media.js index ff26dd2c..22a3f635 100644 --- a/tests/unit/lib/media.js +++ b/tests/unit/lib/media.js @@ -33,8 +33,6 @@ describe( 'wp.media', function() { }); it( 'should intitialize instance properties', function() { - expect( media._filters ).to.deep.equal( {} ); - expect( media._taxonomyFilters ).to.deep.equal( {} ); expect( media._path ).to.deep.equal( {} ); expect( media._params ).to.deep.equal( {} ); expect( media._template ).to.equal( 'media(/:id)' ); diff --git a/tests/unit/lib/mixins/filters.js b/tests/unit/lib/mixins/filters.js new file mode 100644 index 00000000..c5beb8c8 --- /dev/null +++ b/tests/unit/lib/mixins/filters.js @@ -0,0 +1,436 @@ +'use strict'; +var expect = require( 'chai' ).expect; + +var inherit = require( 'util' ).inherits; + +var filterMixins = require( '../../../../lib/mixins/filters' ); +var WPRequest = require( '../../../../lib/shared/wp-request' ); + +describe( 'mixins: filter', function() { + var Req; + var req; + var getQueryStr; + + beforeEach(function() { + Req = function() { + WPRequest.apply( this, arguments ); + }; + inherit( Req, WPRequest ); + + req = new Req(); + + getQueryStr = function( req ) { + var query = req + ._renderQuery() + .replace( /^\?/, '' ); + return decodeURIComponent( query ); + }; + }); + + describe( '.filter()', function() { + + beforeEach(function() { + Req.prototype.filter = filterMixins.filter; + }); + + it( 'mixin method is defined', function() { + expect( filterMixins ).to.have.property( 'filter' ); + }); + + it( 'is a function', function() { + expect( filterMixins.filter ).to.be.a( 'function' ); + }); + + it( 'supports chaining', function() { + expect( req.filter() ).to.equal( req ); + }); + + it( 'will nave no effect if called with no filter value', function() { + var result = req.filter( 'a' ); + expect( getQueryStr( result ) ).to.equal( '' ); + }); + + it( 'sets the filter query parameter on a request instance', function() { + var result = req.filter( 'a', 'b' ); + expect( getQueryStr( result ) ).to.equal( 'filter[a]=b' ); + }); + + it( 'can set multiple filters on the request', function() { + var result = req.filter( 'a', 'b' ).filter( 'c', 'd' ); + expect( getQueryStr( result ) ).to.equal( 'filter[a]=b&filter[c]=d' ); + }); + + it( 'will overwrite previously-set filter values', function() { + var result = req.filter( 'a', 'b' ).filter( 'a', 'd' ); + expect( getQueryStr( result ) ).to.equal( 'filter[a]=d' ); + }); + + it( 'will unset a filter if called with an empty string', function() { + var result = req.filter( 'a', 'b' ).filter( 'a', '' ); + expect( getQueryStr( result ) ).to.equal( '' ); + }); + + it( 'will unset a filter if called with null', function() { + var result = req.filter( 'a', 'b' ).filter( 'a', null ); + expect( getQueryStr( result ) ).to.equal( '' ); + }); + + it( 'can set multiple filters in one call when passed an object', function() { + var result = req.filter({ + a: 'b', + c: 'd', + e: 'f' + }); + expect( getQueryStr( result ) ).to.equal( 'filter[a]=b&filter[c]=d&filter[e]=f' ); + }); + + it( 'can set multiple filters on the request when passed an object', function() { + var result = req + .filter({ + a: 'b', + c: 'd' + }) + .filter({ + e: 'f' + }); + expect( getQueryStr( result ) ).to.equal( 'filter[a]=b&filter[c]=d&filter[e]=f' ); + }); + + it( 'will overwrite multiple previously-set filter values when passed an object', function() { + var result = req + .filter({ + a: 'b', + c: 'd', + e: 'f' + }) + .filter({ + a: 'g', + c: 'h', + i: 'j' + }); + expect( getQueryStr( result ) ).to.equal( 'filter[a]=g&filter[c]=h&filter[e]=f&filter[i]=j' ); + }); + + }); + + describe( 'taxonomy()', function() { + + beforeEach(function() { + Req.prototype.taxonomy = filterMixins.taxonomy; + }); + + it( 'mixin is defined', function() { + expect( filterMixins ).to.have.property( 'taxonomy' ); + }); + + it( 'is a function', function() { + expect( filterMixins.taxonomy ).to.be.a( 'function' ); + }); + + it( 'supports chaining', function() { + expect( req.taxonomy( 'tag', 'foo' ) ).to.equal( req ); + }); + + describe( 'argument type check errors', function() { + + it( 'errors if no term is provided', function() { + expect(function() { req.taxonomy( 'tag' ); }).to.throw(); + }); + + it( 'does not error if the term is a string', function() { + expect(function() { req.taxonomy( 'tag', 'cat' ); }).not.to.throw(); + }); + + it( 'does not error if the term is an array of strings', function() { + expect(function() { req.taxonomy( 'tag', [ 'cat', 'dog' ] ); }).not.to.throw(); + }); + + it( 'does not error if term is a number', function() { + expect(function() { req.taxonomy( 'cat', 7 ); }).not.to.throw(); + }); + + it( 'does not error if term is an array of numbers', function() { + expect(function() { req.taxonomy( 'cat', [ 7, 11 ] ); }).not.to.throw(); + }); + + it( 'errors if the term is null', function() { + expect(function() { req.taxonomy( 'tag', null ); }).to.throw(); + }); + + it( 'errors if the term is a boolean', function() { + expect(function() { req.taxonomy( 'tag', true ); }).to.throw(); + expect(function() { req.taxonomy( 'tag', false ); }).to.throw(); + }); + + it( 'errors if the term is a Date', function() { + expect(function() { req.taxonomy( 'tag', new Date() ); }).to.throw(); + }); + + it( 'errors if the term is an object', function() { + expect(function() { req.taxonomy( 'tag', {} ); }).to.throw(); + }); + + it( 'errors if the term is an array of types other than strings or numbers', function() { + expect(function() { req.taxonomy( 'tag', [ null ] ); }).to.throw(); + }); + + it( 'errors if the term is not all strings or numbers', function() { + expect(function() { req.taxonomy( 'tag', [ 'cat', null ] ); }).to.throw(); + expect(function() { req.taxonomy( 'cat', [ 7, null ] ); }).to.throw(); + expect(function() { req.taxonomy( 'cat', [ 'foo', 7 ] ); }).to.throw(); + }); + + }); + + describe( 'filter name aliasing behavior', function() { + + it( 'sets the "category_name" filter for categories where the term is a string', function() { + var result = req.taxonomy( 'category', 'str' ); + expect( getQueryStr( result ) ).to.equal( 'filter[category_name]=str' ); + }); + + it( 'sets the "cat" filter for categories where the term is a number', function() { + var result = req.taxonomy( 'category', 7 ); + expect( getQueryStr( result ) ).to.equal( 'filter[cat]=7' ); + }); + + it( 'sets the "tag" filter if the taxonomy is "post_tag"', function() { + var result = req.taxonomy( 'post_tag', 'sometag' ); + expect( getQueryStr( result ) ).to.equal( 'filter[tag]=sometag' ); + }); + + }); + + describe( 'filter value setting behavior', function() { + + it( 'de-duplicates taxonomy terms (will only set a term once)', function() { + var result = req.taxonomy( 'tag', 'cat' ).taxonomy( 'tag', 'cat' ); + expect( getQueryStr( result ) ).to.equal( 'filter[tag]=cat' ); + }); + + it( 'supports setting an array of string terms', function() { + // TODO: Multiple terms may be deprecated by API! + var result = req.taxonomy( 'tag', [ 'a', 'b' ] ); + expect( getQueryStr( result ) ).to.equal( 'filter[tag]=a+b' ); + }); + + it( 'supports setting an array of numeric terms', function() { + // TODO: Multiple terms may be deprecated by API! + var result = req.taxonomy( 'tag', [ 1, 2 ] ); + expect( getQueryStr( result ) ).to.equal( 'filter[tag]=1+2' ); + }); + + it( 'does not overwrite previously-specified terms on subsequent calls', function() { + // TODO: Multiple terms may be deprecated by API! + var result = req.taxonomy( 'tag', 'a' ).taxonomy( 'tag', [ 'b' ] ); + expect( getQueryStr( result ) ).to.equal( 'filter[tag]=a+b' ); + }); + + it( 'sorts provided terms', function() { + var result = req.taxonomy( 'tag', 'z' ).taxonomy( 'tag', 'a' ); + expect( getQueryStr( result ) ).to.equal( 'filter[tag]=a+z' ); + }); + + }); + + }); + + describe( 'category()', function() { + + beforeEach(function() { + // By only applying .category and not .taxonomy, we implicitly test that + // .category does not depend on the .taxonomy mixin having been added + Req.prototype.category = filterMixins.category; + }); + + it( 'mixin is defined', function() { + expect( filterMixins ).to.have.property( 'category' ); + }); + + it( 'is a function', function() { + expect( filterMixins.category ).to.be.a( 'function' ); + }); + + it( 'supports chaining', function() { + expect( req.category( 'foo' ) ).to.equal( req ); + }); + + it( 'is an alias for .taxonomy( "category", ... )', function() { + Req.prototype.taxonomy = filterMixins.taxonomy; + [ 'str', 7, [ 'a', 'b' ] ].forEach(function( term ) { + var result1 = ( new Req() ).category( term ); + var result2 = ( new Req() ).taxonomy( 'category', term ); + expect( getQueryStr( result1 ) ).to.equal( getQueryStr( result2 ) ); + }); + }); + + it( 'sets the "category_name" filter for categories where the term is a string', function() { + var result = req.category( 'str' ); + expect( getQueryStr( result ) ).to.equal( 'filter[category_name]=str' ); + }); + + it( 'sets the "cat" filter for categories where the term is a number', function() { + var result = req.category( 7 ); + expect( getQueryStr( result ) ).to.equal( 'filter[cat]=7' ); + }); + + }); + + describe( 'tag()', function() { + + beforeEach(function() { + // By only applying .tag and not .taxonomy, we implicitly test that + // .tag does not depend on the .taxonomy mixin having been added + Req.prototype.tag = filterMixins.tag; + }); + + it( 'mixin is defined', function() { + expect( filterMixins ).to.have.property( 'tag' ); + }); + + it( 'is a function', function() { + expect( filterMixins.tag ).to.be.a( 'function' ); + }); + + it( 'supports chaining', function() { + expect( req.tag( 'foo' ) ).to.equal( req ); + }); + + it( 'is an alias for .taxonomy( "tag", ... )', function() { + Req.prototype.taxonomy = filterMixins.taxonomy; + [ 'str', 7, [ 'a', 'b' ] ].forEach(function( term ) { + var result1 = ( new Req() ).tag( term ); + var result2 = ( new Req() ).taxonomy( 'tag', term ); + expect( getQueryStr( result1 ) ).to.equal( getQueryStr( result2 ) ); + }); + }); + + it( 'sets the "tag" filter', function() { + var result = req.tag( 'str' ); + expect( getQueryStr( result ) ).to.equal( 'filter[tag]=str' ); + }); + + }); + + describe( 'date filters', function() { + + describe( 'year()', function() { + + beforeEach(function() { + // By only applying .year and not .filter, we implicitly test that + // .year does not depend on the .filter mixin having been added + Req.prototype.year = filterMixins.year; + }); + + it( 'mixin is defined', function() { + expect( filterMixins ).to.have.property( 'year' ); + }); + + it( 'is a function', function() { + expect( filterMixins.year ).to.be.a( 'function' ); + }); + + it( 'supports chaining', function() { + expect( req.year( 'foo' ) ).to.equal( req ); + }); + + it( 'is an alias for .filter( "year", ... )', function() { + Req.prototype.filter = filterMixins.filter; + var result1 = ( new Req() ).year( '2015' ); + var result2 = ( new Req() ).filter( 'year', '2015' ); + expect( getQueryStr( result1 ) ).to.equal( getQueryStr( result2 ) ); + }); + + it( 'sets the "year" filter', function() { + var result = req.year( 'str' ); + expect( getQueryStr( result ) ).to.equal( 'filter[year]=str' ); + }); + + }); + + describe( 'month()', function() { + + beforeEach(function() { + // By only applying .month and not .filter, we implicitly test that + // .month does not depend on the .filter mixin having been added + Req.prototype.month = filterMixins.month; + }); + + it( 'mixin is defined', function() { + expect( filterMixins ).to.have.property( 'month' ); + }); + + it( 'is a function', function() { + expect( filterMixins.month ).to.be.a( 'function' ); + }); + + it( 'supports chaining', function() { + expect( req.month( 'foo' ) ).to.equal( req ); + }); + + it( 'is an alias for .filter( "monthnum", ... )', function() { + Req.prototype.filter = filterMixins.filter; + var result1 = ( new Req() ).month( 7 ); + var result2 = ( new Req() ).filter( 'monthnum', 7 ); + expect( getQueryStr( result1 ) ).to.equal( getQueryStr( result2 ) ); + }); + + it( 'sets the "monthnum" filter', function() { + var result = req.month( 1 ); + expect( getQueryStr( result ) ).to.equal( 'filter[monthnum]=1' ); + }); + + it( 'converts named months into their numeric monthnum equivalent', function() { + var result = req.month( 'March' ); + expect( getQueryStr( result ) ).to.equal( 'filter[monthnum]=3' ); + }); + + it( 'returns without setting any filter if an invalid month string is provided', function() { + var result = req.month( 'Not a month' )._renderURI(); + expect( result.match( /filter/ ) ).to.equal( null ); + }); + + it( 'returns without setting any filter if an invalid argument is provided', function() { + var result = req.month( [ 'arrrr', 'i', 'be', 'an', 'array!' ] )._renderURI(); + expect( result.match( /filter/ ) ).to.equal( null ); + }); + + }); + + describe( 'day()', function() { + + beforeEach(function() { + // By only applying .day and not .filter, we implicitly test that + // .day does not depend on the .filter mixin having been added + Req.prototype.day = filterMixins.day; + }); + + it( 'mixin is defined', function() { + expect( filterMixins ).to.have.property( 'day' ); + }); + + it( 'is a function', function() { + expect( filterMixins.day ).to.be.a( 'function' ); + }); + + it( 'supports chaining', function() { + expect( req.day( 'foo' ) ).to.equal( req ); + }); + + it( 'is an alias for .filter( "day", ... )', function() { + Req.prototype.filter = filterMixins.filter; + var result1 = ( new Req() ).day( '2015' ); + var result2 = ( new Req() ).filter( 'day', '2015' ); + expect( getQueryStr( result1 ) ).to.equal( getQueryStr( result2 ) ); + }); + + it( 'sets the "day" filter', function() { + var result = req.day( 'str' ); + expect( getQueryStr( result ) ).to.equal( 'filter[day]=str' ); + }); + + }); + + }); + +}); diff --git a/tests/unit/lib/mixins/parameters.js b/tests/unit/lib/mixins/parameters.js new file mode 100644 index 00000000..49a822ec --- /dev/null +++ b/tests/unit/lib/mixins/parameters.js @@ -0,0 +1,330 @@ +'use strict'; +var expect = require( 'chai' ).expect; + +var inherit = require( 'util' ).inherits; + +var parameterMixins = require( '../../../../lib/mixins/parameters' ); +var WPRequest = require( '../../../../lib/shared/wp-request' ); + +describe( 'mixins: parameters', function() { + var Req; + var req; + var getQueryStr; + + beforeEach(function() { + Req = function() { + WPRequest.apply( this, arguments ); + }; + inherit( Req, WPRequest ); + + req = new Req(); + + getQueryStr = function( req ) { + var query = req + ._renderQuery() + .replace( /^\?/, '' ); + return decodeURIComponent( query ); + }; + }); + + describe( 'pagination parameters', function() { + + describe( '.page()', function() { + + beforeEach(function() { + Req.prototype.page = parameterMixins.page; + }); + + it( 'mixin method is defined', function() { + expect( parameterMixins ).to.have.property( 'page' ); + }); + + it( 'is a function', function() { + expect( parameterMixins.page ).to.be.a( 'function' ); + }); + + it( 'supports chaining', function() { + expect( req.page() ).to.equal( req ); + }); + + it( 'has no effect when called with no argument', function() { + var result = req.page(); + expect( getQueryStr( result ) ).to.equal( '' ); + }); + + it( 'sets the "page" query parameter when provided a value', function() { + var result = req.page( 7 ); + expect( getQueryStr( result ) ).to.equal( 'page=7' ); + }); + + }); + + describe( '.perPage()', function() { + + beforeEach(function() { + Req.prototype.perPage = parameterMixins.perPage; + }); + + it( 'mixin method is defined', function() { + expect( parameterMixins ).to.have.property( 'perPage' ); + }); + + it( 'is a function', function() { + expect( parameterMixins.perPage ).to.be.a( 'function' ); + }); + + it( 'supports chaining', function() { + expect( req.perPage() ).to.equal( req ); + }); + + it( 'has no effect when called with no argument', function() { + var result = req.perPage(); + expect( getQueryStr( result ) ).to.equal( '' ); + }); + + it( 'sets the "per_page" query parameter when provided a value', function() { + var result = req.perPage( 7 ); + expect( getQueryStr( result ) ).to.equal( 'per_page=7' ); + }); + + }); + + }); + + describe( 'name parameters', function() { + + describe( '.slug()', function() { + + beforeEach(function() { + Req.prototype.slug = parameterMixins.slug; + }); + + it( 'mixin method is defined', function() { + expect( parameterMixins ).to.have.property( 'slug' ); + }); + + it( 'is a function', function() { + expect( parameterMixins.slug ).to.be.a( 'function' ); + }); + + it( 'supports chaining', function() { + expect( req.slug() ).to.equal( req ); + }); + + it( 'has no effect when called with no argument', function() { + var result = req.slug(); + expect( getQueryStr( result ) ).to.equal( '' ); + }); + + it( 'sets the "slug" query parameter when provided a value', function() { + var result = req.slug( 'my-slug' ); + expect( getQueryStr( result ) ).to.equal( 'slug=my-slug' ); + }); + + }); + + describe( '.name()', function() { + + beforeEach(function() { + Req.prototype.name = parameterMixins.name; + }); + + it( 'mixin method is defined', function() { + expect( parameterMixins ).to.have.property( 'name' ); + }); + + it( 'is a function', function() { + expect( parameterMixins.name ).to.be.a( 'function' ); + }); + + it( 'supports chaining', function() { + expect( req.name() ).to.equal( req ); + }); + + it( 'has no effect when called with no argument', function() { + var result = req.name(); + expect( getQueryStr( result ) ).to.equal( '' ); + }); + + it( 'is equivalent to .slug', function() { + Req.prototype.slug = parameterMixins.slug; + var result1 = ( new Req() ).name( 'myname' ); + var result2 = ( new Req() ).slug( 'myname' ); + expect( getQueryStr( result1 ) ).to.equal( getQueryStr( result2 ) ); + }); + + it( 'sets the "slug" query parameter when provided a value', function() { + var result = req.name( 7 ); + expect( getQueryStr( result ) ).to.equal( 'slug=7' ); + }); + + }); + + }); + + describe( 'search', function() { + + beforeEach(function() { + Req.prototype.search = parameterMixins.search; + }); + + it( 'mixin method is defined', function() { + expect( parameterMixins ).to.have.property( 'search' ); + }); + + it( 'is a function', function() { + expect( parameterMixins.search ).to.be.a( 'function' ); + }); + + it( 'supports chaining', function() { + expect( req.search() ).to.equal( req ); + }); + + it( 'has no effect when called with no argument', function() { + var result = req.search(); + expect( getQueryStr( result ) ).to.equal( '' ); + }); + + it( 'sets the "search" query parameter when provided a value', function() { + var result = req.search( 'my search string' ); + expect( getQueryStr( result ) ).to.equal( 'search=my search string' ); + }); + + it( 'overwrites previously-set values on subsequent calls', function() { + var result = req.search( 'query' ).search( 'newquery' ); + expect( getQueryStr( result ) ).to.equal( 'search=newquery' ); + }); + + }); + + describe( 'author', function() { + + beforeEach(function() { + Req.prototype.author = parameterMixins.author; + }); + + it( 'mixin method is defined', function() { + expect( parameterMixins ).to.have.property( 'author' ); + }); + + it( 'is a function', function() { + expect( parameterMixins.author ).to.be.a( 'function' ); + }); + + it( 'supports chaining', function() { + expect( req.author( 1 ) ).to.equal( req ); + }); + + it( 'has no effect when called with no argument', function() { + var result = req.author(); + expect( getQueryStr( result ) ).to.equal( '' ); + }); + + it( 'throws an error when called with a non-string, non-numeric value', function() { + expect(function() { req.author({}); }).to.throw(); + }); + + it( 'sets the "author" query parameter when provided a numeric value', function() { + var result = req.author( 1138 ); + expect( getQueryStr( result ) ).to.equal( 'author=1138' ); + }); + + 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' ); + }); + + it( 'unsets author when called with an empty string', function() { + var result = req.author( 'jorge-luis-borges' ).author( '' ); + expect( getQueryStr( result ) ).to.equal( '' ); + }); + + it( 'unsets author when called with null', function() { + var result = req.author( 7 ).author( null ); + expect( getQueryStr( result ) ).to.equal( '' ); + }); + + it( 'unsets author parameter when called with author name string', function() { + var result = req.author( 7 ).author( 'haruki-murakami' ); + expect( getQueryStr( result ) ).to.equal( 'filter[author_name]=haruki-murakami' ); + }); + + it( 'unsets author name filter when called with numeric author id', function() { + var result = req.author( 'haruki-murakami' ).author( 7 ); + expect( getQueryStr( result ) ).to.equal( 'author=7' ); + }); + + }); + + describe( 'parent', function() { + + beforeEach(function() { + Req.prototype.parent = parameterMixins.parent; + }); + + it( 'mixin method is defined', function() { + expect( parameterMixins ).to.have.property( 'parent' ); + }); + + it( 'is a function', function() { + expect( parameterMixins.parent ).to.be.a( 'function' ); + }); + + it( 'supports chaining', function() { + expect( req.parent() ).to.equal( req ); + }); + + it( 'has no effect when called with no argument', function() { + var result = req.parent(); + expect( getQueryStr( result ) ).to.equal( '' ); + }); + + it( 'sets the "parent" query parameter when provided a value', function() { + var result = req.parent( 42 ); + expect( getQueryStr( result ) ).to.equal( 'parent=42' ); + }); + + it( 'merges values on subsequent calls', function() { + // TODO: Is this how the API actually functions? + var result = req.parent( 42 ).parent( 2501 ); + expect( getQueryStr( result ) ).to.equal( 'parent[]=2501&parent[]=42' ); + }); + + }); + + describe( 'forPost', function() { + + beforeEach(function() { + Req.prototype.forPost = parameterMixins.forPost; + }); + + it( 'mixin method is defined', function() { + expect( parameterMixins ).to.have.property( 'forPost' ); + }); + + it( 'is a function', function() { + expect( parameterMixins.forPost ).to.be.a( 'function' ); + }); + + it( 'supports chaining', function() { + expect( req.forPost() ).to.equal( req ); + }); + + it( 'has no effect when called with no argument', function() { + var result = req.forPost(); + expect( getQueryStr( result ) ).to.equal( '' ); + }); + + it( 'sets the "post" query parameter when provided a value', function() { + var result = req.forPost( 3263827 ); + expect( getQueryStr( result ) ).to.equal( 'post=3263827' ); + }); + + it( 'overwrites previously-set values on subsequent calls', function() { + var result = req.forPost( 1138 ).forPost( 2501 ); + expect( getQueryStr( result ) ).to.equal( 'post=2501' ); + }); + + }); + +}); diff --git a/tests/unit/lib/pages.js b/tests/unit/lib/pages.js index fb7916f0..625137dd 100644 --- a/tests/unit/lib/pages.js +++ b/tests/unit/lib/pages.js @@ -33,8 +33,6 @@ describe( 'wp.pages', function() { }); it( 'should intitialize instance properties', function() { - expect( pages._filters ).to.deep.equal( {} ); - expect( pages._taxonomyFilters ).to.deep.equal( {} ); expect( pages._path ).to.deep.equal( {} ); expect( pages._params ).to.deep.equal( {} ); expect( pages._template ).to.equal( 'pages(/:id)(/:action)(/:commentId)' ); diff --git a/tests/unit/lib/posts.js b/tests/unit/lib/posts.js index f98ac415..cd431078 100644 --- a/tests/unit/lib/posts.js +++ b/tests/unit/lib/posts.js @@ -33,8 +33,6 @@ describe( 'wp.posts', function() { }); it( 'should intitialize instance properties', function() { - expect( posts._filters ).to.deep.equal( {} ); - expect( posts._taxonomyFilters ).to.deep.equal( {} ); expect( posts._path ).to.deep.equal( {} ); expect( posts._template ).to.equal( 'posts(/:id)(/:action)(/:actionId)' ); var _supportedMethods = posts._supportedMethods.sort().join( '|' ); diff --git a/tests/unit/lib/shared/collection-request.js b/tests/unit/lib/shared/collection-request.js index ee291401..2180ca1e 100644 --- a/tests/unit/lib/shared/collection-request.js +++ b/tests/unit/lib/shared/collection-request.js @@ -7,6 +7,7 @@ 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() { @@ -302,11 +303,11 @@ describe( 'CollectionRequest', function() { describe( 'category()', function() { - it( 'delegates to taxonomy()', function() { - sinon.stub( request, 'taxonomy' ); + it( 'delegates to taxonomy() mixin', function() { + sinon.stub( filterMixins, 'taxonomy' ); request.category( 'news' ); - expect( request.taxonomy ).to.have.been.calledWith( 'category', 'news' ); - request.taxonomy.restore(); + expect( filterMixins.taxonomy ).to.have.been.calledWith( 'category', 'news' ); + filterMixins.taxonomy.restore(); }); it( 'should be chainable, and accumulates values', function() { @@ -320,11 +321,11 @@ describe( 'CollectionRequest', function() { describe( 'tag()', function() { - it( 'delegates to taxonomy()', function() { - sinon.stub( request, 'taxonomy' ); + it( 'delegates to taxonomy() mixin', function() { + sinon.stub( filterMixins, 'taxonomy' ); request.tag( 'the-good-life' ); - expect( request.taxonomy ).to.have.been.calledWith( 'tag', 'the-good-life' ); - request.taxonomy.restore(); + expect( filterMixins.taxonomy ).to.have.been.calledWith( 'tag', 'the-good-life' ); + filterMixins.taxonomy.restore(); }); it( 'should be chainable, and accumulates values', function() { diff --git a/tests/unit/lib/shared/wp-request.js b/tests/unit/lib/shared/wp-request.js index 53e52cce..8dc23578 100644 --- a/tests/unit/lib/shared/wp-request.js +++ b/tests/unit/lib/shared/wp-request.js @@ -98,6 +98,11 @@ describe( 'WPRequest', function() { expect( request.param ).to.be.a( 'function' ); }); + it( 'will have no effect if called without any arguments', function() { + request.param(); + expect( request._renderQuery() ).to.equal( '' ); + }); + it( 'will set a query parameter value', function() { request.param( 'key', 'value' ); expect( request._renderQuery() ).to.equal( '?key=value' ); @@ -119,6 +124,16 @@ describe( 'WPRequest', function() { expect( request._renderQuery() ).to.equal( '' ); }); + it( 'will have no effect if called with no value', function() { + request.param( 'key' ); + expect( request._renderQuery() ).to.equal( '' ); + }); + + it( 'will have no effect if called with an empty object', function() { + request.param({}); + expect( request._renderQuery() ).to.equal( '' ); + }); + it( 'should set the internal _params hash', function() { request.param( 'type', 'some_cpt' ); expect( request._renderQuery() ).to.equal( '?type=some_cpt' ); diff --git a/tests/unit/lib/taxonomies.js b/tests/unit/lib/taxonomies.js index 2eb5f0c9..5e836e07 100644 --- a/tests/unit/lib/taxonomies.js +++ b/tests/unit/lib/taxonomies.js @@ -34,7 +34,6 @@ describe( 'wp.taxonomies', function() { it( 'should intitialize instance properties', function() { var _supportedMethods = taxonomies._supportedMethods.sort().join( '|' ); - expect( taxonomies._filters ).to.deep.equal( {} ); expect( taxonomies._path ).to.deep.equal({ collection: 'taxonomies' }); expect( taxonomies._params ).to.deep.equal( {} ); expect( taxonomies._template ).to.equal( '(:collection)(/:term)' ); diff --git a/tests/unit/lib/types.js b/tests/unit/lib/types.js index 6345512c..e717ed6d 100644 --- a/tests/unit/lib/types.js +++ b/tests/unit/lib/types.js @@ -34,7 +34,6 @@ describe( 'wp.types', function() { it( 'should intitialize instance properties', function() { var _supportedMethods = types._supportedMethods.sort().join( '|' ); - expect( types._filters ).to.deep.equal( {} ); expect( types._path ).to.deep.equal( {} ); expect( types._params ).to.deep.equal( {} ); expect( types._template ).to.equal( 'types(/:type)' ); diff --git a/tests/unit/lib/users.js b/tests/unit/lib/users.js index 0f72b7e0..c7337e07 100644 --- a/tests/unit/lib/users.js +++ b/tests/unit/lib/users.js @@ -38,7 +38,6 @@ describe( 'wp.users', function() { }); it( 'should initialize instance properties', function() { - expect( users._filters ).to.deep.equal( {} ); expect( users._path ).to.deep.equal( {} ); expect( users._params ).to.deep.equal( {} ); var _supportedMethods = users._supportedMethods.sort().join( '|' );