diff --git a/src/examples/get_routes.test.js b/src/examples/get_routes.test.js
new file mode 100644
index 0000000..968490d
--- /dev/null
+++ b/src/examples/get_routes.test.js
@@ -0,0 +1,48 @@
+import chai from 'chai';
+import chaiAsPromised from 'chai-as-promised';
+import fetchMock from 'fetch-mock';
+import Track from '../index';
+import { charlie, routes as mockRoutes } from '../mocks';
+
+chai.should();
+chai.use(chaiAsPromised);
+
+describe('When searching for routes by name', () => {
+ const api = new Track({ autoRenew: false });
+
+ beforeEach(() => charlie.setUpSuccessfulMock(api.client));
+ beforeEach(() => mockRoutes.setUpSuccessfulMock(api.client));
+ beforeEach(() => fetchMock.catch(503));
+ afterEach(fetchMock.restore);
+
+ it('should get a list of routes', () => {
+ api.logIn({ username: 'charlie@example.com', password: 'securepassword' });
+
+ const routesPromise = api.customer('SYNC').routes()
+ .withQuery('blue') // Routes containing "blue" in their name
+ .getPage()
+ .then(page => page.list)
+ .then(routes => routes); // Do things with list of routes
+
+ return routesPromise;
+ });
+});
+
+describe('When retrieving a route by ID', () => {
+ const api = new Track({ autoRenew: false });
+
+ beforeEach(() => charlie.setUpSuccessfulMock(api.client));
+ beforeEach(() => mockRoutes.setUpSuccessfulMock(api.client));
+ beforeEach(() => fetchMock.catch(503));
+ afterEach(fetchMock.restore);
+
+ it('should get a route', () => {
+ api.logIn({ username: 'charlie@example.com', password: 'securepassword' });
+
+ const routesPromise = api.customer('SYNC').route(1)
+ .fetch()
+ .then(route => route); // Do things with route
+
+ return routesPromise;
+ });
+});
diff --git a/src/mocks.js b/src/mocks.js
index 52159d8..d89a385 100644
--- a/src/mocks.js
+++ b/src/mocks.js
@@ -72,6 +72,41 @@ export const charlie = {
},
};
+export const routes = {
+ setUpSuccessfulMock: (client) => {
+ const listResponse = () => new Response(
+ toBlob(routes.list),
+ {
+ headers: {
+ Link: '1/SYNC/routes?page=1&perPage=10&q=blue&sort=>; rel="next", 1/SYNC/routes?page=1&perPage=10&q=blue&sort=>; rel="last"',
+ },
+ });
+ const singleResponse = () => new Response(toBlob(routes.getById(1)));
+
+ fetchMock
+ .get(client.resolve('/1/SYNC/routes?page=1&perPage=10&q=blue&sort='), listResponse)
+ .get(client.resolve('/1/SYNC/routes/1'), singleResponse);
+ },
+ getById: id => routes.list.find(v => v.id === id),
+ list: [
+ {
+ href: '/1/SYNC/routes/1',
+ id: 1,
+ name: 'Blue Line',
+ short_name: 'Blue',
+ description: 'Servicing the Townsville community',
+ is_public: true,
+ color: '#0000FF',
+ text_color: '#FFFFFF',
+ patterns: [
+ {
+ href: '/1/SYNC/patterns/1',
+ },
+ ],
+ },
+ ],
+};
+
export const signs = {
setUpSuccessfulMock: (client) => {
const listResponse = () => new Response(
diff --git a/src/resources/Customer.js b/src/resources/Customer.js
index ca87da2..fba563f 100644
--- a/src/resources/Customer.js
+++ b/src/resources/Customer.js
@@ -1,4 +1,6 @@
import Resource from './Resource';
+import Route from './Route';
+import RoutesContext from './RoutesContext';
import Sign from './Sign';
import SignsContext from './SignsContext';
import Vehicle from './Vehicle';
@@ -25,9 +27,26 @@ class Customer extends Resource {
this.code = customerCode;
}
+ /**
+ * Gets a context for querying this customer's routes
+ * @returns {RoutesContext} Context for querying this customer's routes
+ */
+ routes() {
+ return this.resource(RoutesContext, this.code);
+ }
+
+ /**
+ * Gets a route resource by id
+ * @param {Number} id Identity of the route
+ * @returns {Route} Route resource
+ */
+ route(id) {
+ return this.resource(Route, Route.makeHref(this.code, id));
+ }
+
/**
* Gets a context for querying this customer's signs
- * @returns {SignContext} Context for querying this customer's signs
+ * @returns {SignsContext} Context for querying this customer's signs
*/
signs() {
return this.resource(SignsContext, this.code);
@@ -44,7 +63,7 @@ class Customer extends Resource {
/**
* Gets a context for querying this customer's vehicles
- * @returns {VehicleContext} Context for querying this customer's vehicles
+ * @returns {VehiclesContext} Context for querying this customer's vehicles
*/
vehicles() {
return this.resource(VehiclesContext, this.code);
diff --git a/src/resources/Customer.test.js b/src/resources/Customer.test.js
index 1364eb0..d06a16b 100644
--- a/src/resources/Customer.test.js
+++ b/src/resources/Customer.test.js
@@ -2,6 +2,8 @@ import chai from 'chai';
import chaiAsPromised from 'chai-as-promised';
import Client from '../Client';
import Customer from './Customer';
+import Route from './Route';
+import RoutesContext from './RoutesContext';
import Sign from './Sign';
import SignsContext from './SignsContext';
import Vehicle from './Vehicle';
@@ -14,6 +16,8 @@ describe('When getting resources related to a customer', () => {
const client = new Client();
const customer = new Customer(client, 'SYNC');
+ it('should allow routes to be searched', () => customer.routes().should.be.instanceof(RoutesContext));
+ it('should allow a route to be retrieved', () => customer.route().should.be.instanceof(Route));
it('should allow signs to be searched', () => customer.signs().should.be.instanceof(SignsContext));
it('should allow a sign to be retrieved', () => customer.sign().should.be.instanceof(Sign));
it('should allow vehicles to be searched', () => customer.vehicles().should.be.instanceof(VehiclesContext));
diff --git a/src/resources/Route.js b/src/resources/Route.js
new file mode 100644
index 0000000..66d6e26
--- /dev/null
+++ b/src/resources/Route.js
@@ -0,0 +1,62 @@
+import Resource from './Resource';
+import Assignment from './Assignment';
+
+/**
+ * Route resource
+ */
+class Route extends Resource {
+ /**
+ * Creates a new route
+ *
+ * Will populate itself with the values given to it after the client parameter
+ * @example
Assigning partial route data to a new instance
+ * const client = new Client();
+ * const partialRouteData = {
+ * href: '/1/SYNC/routes/2',
+ * name: '9876',
+ * };
+ * const route = new Route(client, partialRouteData);
+ *
+ * route.hydrated == true;
+ * @param {Client} client Instance of pre-configured client
+ * @param {Array} rest Remaining arguments to use in assigning values to this instance
+ */
+ constructor(client, ...rest) {
+ super(client);
+
+ const newProperties = Object.assign({}, ...rest);
+ const hydrated = !Object.keys(newProperties).every(k => k === 'href');
+ const references = {
+ assignment: newProperties.assignment && new Assignment(this.client, newProperties.assignment),
+ };
+
+ Object.assign(this, newProperties, {
+ hydrated,
+ ...references,
+ });
+ }
+
+ /**
+ * Makes a href for a given customer code and ID
+ * @param {string} customerCode Customer code
+ * @param {Number} id Route ID
+ * @returns {string} URI to instance of route
+ */
+ static makeHref(customerCode, id) {
+ return {
+ href: `/1/${customerCode}/routes/${id}`,
+ };
+ }
+
+ /**
+ * Fetches the data for this route via the client
+ * @returns {Promise} If successful, a hydrated instance of this route
+ */
+ fetch() {
+ return this.client.get(this.href)
+ .then(response => response.json())
+ .then(route => new Route(this.client, this, route));
+ }
+}
+
+export default Route;
diff --git a/src/resources/Route.test.js b/src/resources/Route.test.js
new file mode 100644
index 0000000..95df3e7
--- /dev/null
+++ b/src/resources/Route.test.js
@@ -0,0 +1,48 @@
+import chai from 'chai';
+import chaiAsPromised from 'chai-as-promised';
+import fetchMock from 'fetch-mock';
+import Client from '../Client';
+import Route from './Route';
+import { routes as mockRoutes } from '../mocks';
+
+chai.should();
+chai.use(chaiAsPromised);
+
+describe('When instantiating a route based on customer and ID', () => {
+ const client = new Client();
+ const route = new Route(client, Route.makeHref('SYNC', 1));
+
+ it('should set the href', () => route.href.should.equal('/1/SYNC/routes/1'));
+ it('should not be hydrated', () => route.hydrated.should.equal(false));
+});
+
+describe('When instantiating a route based on an object', () => {
+ const client = new Client();
+ const route = new Route(client, mockRoutes.getById(1));
+
+ it('should set the ID', () => route.id.should.equal(1));
+ it('should set the href', () => route.href.should.equal('/1/SYNC/routes/1'));
+ it('should be hydrated', () => route.hydrated.should.equal(true));
+ it('should have one pattern', () => route.patterns.length.should.equal(1));
+ it('should have the expected pattern', () => route.patterns[0].href.should.equal('/1/SYNC/patterns/1'));
+});
+
+describe('When fetching a route based on customer and ID', () => {
+ const client = new Client();
+
+ beforeEach(() => mockRoutes.setUpSuccessfulMock(client));
+ beforeEach(() => fetchMock.catch(503));
+ afterEach(fetchMock.restore);
+
+ let promise;
+ beforeEach(() => {
+ promise = new Route(client, Route.makeHref('SYNC', 1)).fetch();
+ });
+
+ it('should resolve the promise', () => promise.should.be.fulfilled);
+ it('should set the ID', () => promise.then(r => r.id).should.eventually.equal(1));
+ it('should set the href', () => promise.then(r => r.href).should.eventually.equal('/1/SYNC/routes/1'));
+ it('should be hydrated', () => promise.then(r => r.hydrated).should.eventually.equal(true));
+ it('should have one pattern', () => promise.then(r => r.patterns.length).should.eventually.equal(1));
+ it('should have the expected pattern', () => promise.then(r => r.patterns[0].href).should.eventually.equal('/1/SYNC/patterns/1'));
+});
diff --git a/src/resources/RoutesContext.js b/src/resources/RoutesContext.js
new file mode 100644
index 0000000..980b088
--- /dev/null
+++ b/src/resources/RoutesContext.js
@@ -0,0 +1,48 @@
+import 'isomorphic-fetch';
+import PagedContext from './PagedContext';
+import Route from './Route';
+
+/**
+ * Route querying context
+ *
+ * This is used to query the list of routes for a customer
+ */
+class RoutesContext extends PagedContext {
+ /**
+ * Creates a new route context
+ * @param {Client} client Instance of pre-configured client
+ * @param {string} customerCode Customer code
+ * @param {Object} params Object of querystring parameters to append to the URL
+ */
+ constructor(client, customerCode, params) {
+ super(client, { ...params });
+ this.code = customerCode;
+ }
+
+ /**
+ * Sets the query term for the context
+ * @example
+ * const routes = new RoutesContext(...);
+ * routes
+ * .withQuery('blue')
+ * .getPage()
+ * .then(page => ...);
+ * @param {string} term Query term to search for
+ * @returns {RoutesContext} Returns itself
+ */
+ withQuery(term) {
+ this.params.q = term;
+ return this;
+ }
+
+ /**
+ * Gets the first page of results for this context
+ * @returns {Promise} If successful, a page of Route objects
+ * @see Route
+ */
+ getPage() {
+ return this.page(Route, `/1/${this.code}/routes`);
+ }
+}
+
+export default RoutesContext;
diff --git a/src/resources/RoutesContext.test.js b/src/resources/RoutesContext.test.js
new file mode 100644
index 0000000..7945fb4
--- /dev/null
+++ b/src/resources/RoutesContext.test.js
@@ -0,0 +1,34 @@
+import chai from 'chai';
+import chaiAsPromised from 'chai-as-promised';
+import fetchMock from 'fetch-mock';
+import Client from '../Client';
+import RoutesContext from './RoutesContext';
+import { routes as mockRoutes } from '../mocks';
+
+chai.should();
+chai.use(chaiAsPromised);
+
+describe('When building a query for routes', () => {
+ const client = new Client();
+ client.setAuthenticated();
+
+ beforeEach(() => fetchMock
+ .get(client.resolve('/1/SYNC/routes?page=9&perPage=27&q=valid&sort=first_valid asc,second_valid desc'), mockRoutes.list)
+ .catch(503));
+ afterEach(fetchMock.restore);
+
+ let promise;
+ beforeEach(() => {
+ const routes = new RoutesContext(client, 'SYNC');
+ promise = routes
+ .withPage(9)
+ .withPerPage(27)
+ .withQuery('valid')
+ .sortedBy('ignored', 'desc')
+ .sortedBy('first_valid')
+ .thenBy('second_valid', 'desc')
+ .getPage();
+ });
+
+ it('should make the expected request', () => promise.should.be.fulfilled);
+});
diff --git a/src/resources/VehiclesContext.js b/src/resources/VehiclesContext.js
index a526fe0..1bc3ebf 100644
--- a/src/resources/VehiclesContext.js
+++ b/src/resources/VehiclesContext.js
@@ -22,7 +22,7 @@ class VehiclesContext extends PagedContext {
/**
* Sets the query term for the context
* @example
- * const vehicles = new VehicleContext(...);
+ * const vehicles = new VehiclesContext(...);
* vehicles
* .withQuery('12')
* .getPage()