From e7de1f25ada3e3bbcfad6ce1688c3a901815a606 Mon Sep 17 00:00:00 2001 From: "K. Adam White" Date: Sat, 30 Jan 2016 18:17:24 +0000 Subject: [PATCH] Add initial support for querying custom API endpoints This begins to address #140, by implementing an .endpoint method that will create and return a factory method which can create CollectionRequest instances against the provided endpoint. Downsides to this approach: - Somewhat duplicative w/r/t .root() - Presumes that only resource(/:id) format collections will be used - Actually supporting an arbitrary structure of URL will eventually be necessary in order to support discovery-based generation; however, at present we haven't worked out a solid approach for parsing PCRE named groups. Upsides: - Provides a vector to reintroduce .registerType very cleanly The current plan is to merge this, get feedback, and iterate prior to making a formal decision on supporting this or using an alternative approach prior to releasing v0.7. --- lib/media.js | 7 +---- lib/pages.js | 8 +---- lib/path/numeric-id.js | 17 +++++++++++ lib/posts.js | 7 +---- lib/users.js | 7 +---- tests/unit/wp.js | 68 ++++++++++++++++++++++++++++++++++++++++++ wp.js | 41 +++++++++++++++++++++++++ 7 files changed, 130 insertions(+), 25 deletions(-) create mode 100644 lib/path/numeric-id.js diff --git a/lib/media.js b/lib/media.js index 110465b3..1d37fa6e 100644 --- a/lib/media.js +++ b/lib/media.js @@ -114,11 +114,6 @@ MediaRequest.prototype._pathValidators = { * @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; -}; +MediaRequest.prototype.id = require( './path/numeric-id' ); module.exports = MediaRequest; diff --git a/lib/pages.js b/lib/pages.js index 8f591e1f..f1e77521 100644 --- a/lib/pages.js +++ b/lib/pages.js @@ -127,13 +127,7 @@ PagesRequest.prototype._pathValidators = { * @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; -}; +PagesRequest.prototype.id = require( './path/numeric-id' ); /** * Specify that we are getting the comments for a specific page diff --git a/lib/path/numeric-id.js b/lib/path/numeric-id.js new file mode 100644 index 00000000..5236425d --- /dev/null +++ b/lib/path/numeric-id.js @@ -0,0 +1,17 @@ +'use strict'; + +/** + * Request constructor mixin to specify a resource ID to query + * + * @mixin + * @chainable + * @param {Number} id The (numeric) ID of a resource to retrieve + * @return The request instance (for chaining) + */ +module.exports = function( id ) { + /* jshint validthis:true */ + this._path.id = parseInt( id, 10 ); + this._supportedMethods = [ 'head', 'get', 'put', 'post', 'delete' ]; + + return this; +}; diff --git a/lib/posts.js b/lib/posts.js index 62900ab5..451c5484 100644 --- a/lib/posts.js +++ b/lib/posts.js @@ -124,12 +124,7 @@ PostsRequest.prototype._pathValidators = { * @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; -}; +PostsRequest.prototype.id = require( './path/numeric-id' ); /** * Specify that we are retrieving Post Meta (forces basic auth) diff --git a/lib/users.js b/lib/users.js index cd2ca054..fa3697ac 100644 --- a/lib/users.js +++ b/lib/users.js @@ -121,11 +121,6 @@ UsersRequest.prototype.me = function() { * @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; -}; +UsersRequest.prototype.id = require( './path/numeric-id' ); module.exports = UsersRequest; diff --git a/tests/unit/wp.js b/tests/unit/wp.js index 5662cd6d..8bc8cddb 100644 --- a/tests/unit/wp.js +++ b/tests/unit/wp.js @@ -200,4 +200,72 @@ describe( 'wp', function() { }); + describe( 'endpoint()', function() { + + it( 'is a function', function() { + expect( site ).to.have.property( 'endpoint' ); + expect( site.endpoint ).to.be.a( 'function' ); + }); + + it( 'returns an endpoint factory function', function() { + var endpoint = site.endpoint({ + base: 'resources', + namespace: 'ns/v1' + }); + expect( endpoint ).to.be.a( 'function' ); + }); + + it( 'supports a string shorthand', function() { + var endpoint = site.endpoint( 'ns/v1/resources' ); + expect( endpoint ).to.be.a( 'function' ); + }); + + it( 'requires a base property', function() { + expect(function() { + site.endpoint({ + namespace: 'no/base' + }); + }).to.throw; + }); + + describe( 'factory method', function() { + + it( 'returns CollectionRequest instances', function() { + var pathRequest = site.endpoint({ + base: 'resources', + namespace: 'ns/v1' + })(); + expect( pathRequest instanceof CollectionRequest ).to.be.true; + }); + + it( 'returns a correctly-configured request instance', function() { + var endpoint = site.endpoint({ + base: 'resources', + namespace: 'ns/v1' + }); + expect( endpoint()._renderURI() ).to.equal( 'endpoint/url/ns/v1/resources' ); + }); + + it( 'returns a correctly-configured request instance when using the string shorthand', function() { + var endpoint = site.endpoint( 'ns/v1/resources' ); + expect( endpoint()._renderURI() ).to.equal( 'endpoint/url/ns/v1/resources' ); + }); + + it( 'permits accessing sub-resources by ID', function() { + var endpoint = site.endpoint({ + base: 'resources', + namespace: 'ns/v1' + }); + expect( endpoint().id( 2501 )._renderURI() ).to.equal( 'endpoint/url/ns/v1/resources/2501' ); + }); + + it( 'permits accessing sub-resources by ID when using the string shorthand', function() { + var endpoint = site.endpoint( 'ns/v1/resources' ); + expect( endpoint().id( 2501 )._renderURI() ).to.equal( 'endpoint/url/ns/v1/resources/2501' ); + }); + + }); + + }); + }); diff --git a/wp.js b/wp.js index 530ae439..5c2cb751 100644 --- a/wp.js +++ b/wp.js @@ -82,6 +82,47 @@ WP.site = function( endpoint ) { return new WP({ endpoint: endpoint }); }; +/** + * Create a factory method for requests against an arbitrary endpoint base + * + * @example + * + * wp.myPluginResources = wp.endpoint({ + * base: 'resources', + * namespace: 'myplugin/v2' + * }); + * wp.myPluginResources().get().then( // ... + * wp.myPluginResources().id( 7 ).then( // ... + * + * @method endpoint + * @param {Object} [options] An options hash for a new MediaRequest + * @return {MediaRequest} A MediaRequest instance + */ +WP.prototype.endpoint = function( options ) { + var base = typeof options === 'string' ? options : options && options.base || ''; + var namespace = options && options.namespace; + var instance = this; + + if ( ! base || typeof base !== 'string' ) { + throw new Error( 'options hash must contain an endpoint base string' ); + } + + // Return a factory method that will create a configured CollectionRequest + return function( options ) { + options = options || {}; + options = extend( options, instance._options ); + + var collectionRequest = new CollectionRequest( options ); + + // Naively assume that any post endpoint will take an ID property to retrieve + // a specific resource from what we are assuming to be a collection + collectionRequest._template = base + '(/:id)'; + collectionRequest.id = require( './lib/path/numeric-id' ); + + return collectionRequest.namespace( namespace ); + }; +}; + /** * Start a request against the `/media` endpoint *