From 208cf69da355f977f712e6525a530651136586f7 Mon Sep 17 00:00:00 2001
From: Jeff Koch <4649003+jrkoch@users.noreply.github.com>
Date: Tue, 21 Nov 2017 15:22:05 -0800
Subject: [PATCH] feat(driver-run-trip): Add support for Drivers, Runs, Trips.
(#28)
* Add Driver and DriversContext resources
* Add trip resource
* Add Run resource
---
src/examples/get_drivers.test.js | 48 ++++++++++++++++++
src/examples/get_run.test.js | 27 ++++++++++
src/examples/get_trip.test.js | 27 ++++++++++
src/mocks.js | 76 ++++++++++++++++++++++++++++
src/resources/Customer.js | 39 ++++++++++++++
src/resources/Customer.test.js | 8 +++
src/resources/Driver.js | 58 +++++++++++++++++++++
src/resources/Driver.test.js | 52 +++++++++++++++++++
src/resources/DriversContext.js | 48 ++++++++++++++++++
src/resources/DriversContext.test.js | 34 +++++++++++++
src/resources/Run.js | 64 +++++++++++++++++++++++
src/resources/Run.test.js | 50 ++++++++++++++++++
src/resources/Trip.js | 64 +++++++++++++++++++++++
src/resources/Trip.test.js | 50 ++++++++++++++++++
14 files changed, 645 insertions(+)
create mode 100644 src/examples/get_drivers.test.js
create mode 100644 src/examples/get_run.test.js
create mode 100644 src/examples/get_trip.test.js
create mode 100644 src/resources/Driver.js
create mode 100644 src/resources/Driver.test.js
create mode 100644 src/resources/DriversContext.js
create mode 100644 src/resources/DriversContext.test.js
create mode 100644 src/resources/Run.js
create mode 100644 src/resources/Run.test.js
create mode 100644 src/resources/Trip.js
create mode 100644 src/resources/Trip.test.js
diff --git a/src/examples/get_drivers.test.js b/src/examples/get_drivers.test.js
new file mode 100644
index 0000000..b818123
--- /dev/null
+++ b/src/examples/get_drivers.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, drivers as mockDrivers } from '../mocks';
+
+chai.should();
+chai.use(chaiAsPromised);
+
+describe('When searching for drivers by name', () => {
+ const api = new Track({ autoRenew: false });
+
+ beforeEach(() => charlie.setUpSuccessfulMock(api.client));
+ beforeEach(() => mockDrivers.setUpSuccessfulMock(api.client));
+ beforeEach(() => fetchMock.catch(503));
+ afterEach(fetchMock.restore);
+
+ it('should get a list of drivers', () => {
+ api.logIn({ username: 'charlie@example.com', password: 'securepassword' });
+
+ const driversPromise = api.customer('SYNC').drivers()
+ .withQuery('charlie') // drivers containing 'charlie' in their name
+ .getPage()
+ .then(page => page.list)
+ .then(drivers => drivers); // Do things with list of drivers
+
+ return driversPromise;
+ });
+});
+
+describe('When retrieving a driver by ID', () => {
+ const api = new Track({ autoRenew: false });
+
+ beforeEach(() => charlie.setUpSuccessfulMock(api.client));
+ beforeEach(() => mockDrivers.setUpSuccessfulMock(api.client));
+ beforeEach(() => fetchMock.catch(503));
+ afterEach(fetchMock.restore);
+
+ it('should get a driver', () => {
+ api.logIn({ username: 'charlie@example.com', password: 'securepassword' });
+
+ const driverPromise = api.customer('SYNC').driver(1)
+ .fetch()
+ .then(driver => driver); // Do things with driver
+
+ return driverPromise;
+ });
+});
diff --git a/src/examples/get_run.test.js b/src/examples/get_run.test.js
new file mode 100644
index 0000000..a554b5f
--- /dev/null
+++ b/src/examples/get_run.test.js
@@ -0,0 +1,27 @@
+import chai from 'chai';
+import chaiAsPromised from 'chai-as-promised';
+import fetchMock from 'fetch-mock';
+import Track from '../index';
+import { charlie, runs as mockRuns } from '../mocks';
+
+chai.should();
+chai.use(chaiAsPromised);
+
+describe('When retrieving a run by ID', () => {
+ const api = new Track({ autoRenew: false });
+
+ beforeEach(() => charlie.setUpSuccessfulMock(api.client));
+ beforeEach(() => mockRuns.setUpSuccessfulMock(api.client));
+ beforeEach(() => fetchMock.catch(503));
+ afterEach(fetchMock.restore);
+
+ it('should get a run', () => {
+ api.logIn({ username: 'charlie@example.com', password: 'securepassword' });
+
+ const runPromise = api.customer('SYNC').run(1)
+ .fetch()
+ .then(run => run); // Do things with run
+
+ return runPromise;
+ });
+});
diff --git a/src/examples/get_trip.test.js b/src/examples/get_trip.test.js
new file mode 100644
index 0000000..a9d1b8d
--- /dev/null
+++ b/src/examples/get_trip.test.js
@@ -0,0 +1,27 @@
+import chai from 'chai';
+import chaiAsPromised from 'chai-as-promised';
+import fetchMock from 'fetch-mock';
+import Track from '../index';
+import { charlie, trips as mockTrips } from '../mocks';
+
+chai.should();
+chai.use(chaiAsPromised);
+
+describe('When retrieving a trip by ID', () => {
+ const api = new Track({ autoRenew: false });
+
+ beforeEach(() => charlie.setUpSuccessfulMock(api.client));
+ beforeEach(() => mockTrips.setUpSuccessfulMock(api.client));
+ beforeEach(() => fetchMock.catch(503));
+ afterEach(fetchMock.restore);
+
+ it('should get a trip', () => {
+ api.logIn({ username: 'charlie@example.com', password: 'securepassword' });
+
+ const tripPromise = api.customer('SYNC').trip(3)
+ .fetch()
+ .then(trip => trip); // Do things with trip
+
+ return tripPromise;
+ });
+});
diff --git a/src/mocks.js b/src/mocks.js
index f73e309..97a8a0e 100644
--- a/src/mocks.js
+++ b/src/mocks.js
@@ -103,6 +103,31 @@ export const agencies = {
}],
};
+export const drivers = {
+ setUpSuccessfulMock: (client) => {
+ const listResponse = () => new Response(
+ Client.toBlob(drivers.list), {
+ headers: {
+ Link: '1/SYNC/drivers?page=1&per_page=10&q=charlie&sort=>; rel="next", 1/SYNC/drivers?page=1&per_page=10&q=charlie&sort=>; rel="last"',
+ },
+ });
+ const singleResponse = () => new Response(Client.toBlob(drivers.getById(1)));
+
+ fetchMock
+ .get(client.resolve('/1/SYNC/drivers?page=1&per_page=10&sort='), listResponse)
+ .get(client.resolve('/1/SYNC/drivers?page=1&per_page=10&q=charlie&sort='), listResponse)
+ .get(client.resolve('/1/SYNC/drivers/1'), singleResponse);
+ },
+ getById: id => drivers.list.find(v => v.id === id),
+ list: [{
+ href: '/1/SYNC/drivers/1',
+ id: 1,
+ customer_driver_id: '0001',
+ first_name: 'Charlie',
+ last_name: 'Singh',
+ }],
+};
+
export const externalApis = {
setUpSuccessfulMock: (client) => {
const listResponse = () => new Response(
@@ -353,6 +378,29 @@ export const patterns = {
],
};
+export const runs = {
+ setUpSuccessfulMock: (client) => {
+ const singleResponse = () => new Response(Client.toBlob(runs.getById(1)));
+
+ fetchMock
+ .get(client.resolve('/1/SYNC/runs/1'), singleResponse);
+ },
+ getById: id => runs.list.find(v => v.id === id),
+ list: [{
+ href: '/1/SYNC/runs/1',
+ id: 1,
+ name: 'Run 1',
+ short_name: 'R01',
+ service: {
+ href: '/1/SYNC/services/1',
+ },
+ trips: [
+ { href: '/1/SYNC/trips/1' },
+ { href: '/1/SYNC/trips/2' },
+ ],
+ }],
+};
+
export const signs = {
setUpSuccessfulMock: (client) => {
const listResponse = () => new Response(
@@ -447,6 +495,34 @@ export const tags = {
}],
};
+export const trips = {
+ setUpSuccessfulMock: (client) => {
+ const singleResponse = () => new Response(Client.toBlob(trips.getById(3)));
+
+ fetchMock
+ .get(client.resolve('/1/SYNC/trips/3'), singleResponse);
+ },
+ getById: id => trips.list.find(v => v.id === id),
+ list: [{
+ href: '/1/SYNC/trips/3',
+ id: 3,
+ name: 'T03',
+ order: 1,
+ pattern: {
+ href: '/1/SYNC/patterns/1',
+ },
+ service: {
+ href: '/1/SYNC/services/1',
+ },
+ block: {
+ href: '/1/SYNC/blocks/1',
+ },
+ runs: [{
+ href: '/1/SYNC/runs/1',
+ }],
+ }],
+};
+
export const users = {
setUpSuccessfulMock: (client) => {
const listResponse = () => new Response(
diff --git a/src/resources/Customer.js b/src/resources/Customer.js
index 69dfa39..c6878c8 100644
--- a/src/resources/Customer.js
+++ b/src/resources/Customer.js
@@ -1,6 +1,8 @@
import RealTimeContextFactory from './RealTimeContextFactory';
import Resource from './Resource';
import Agency from './Agency';
+import Driver from './Driver';
+import DriversContext from './DriversContext';
import ExternalApi from './ExternalApi';
import ExternalApisContext from './ExternalApisContext';
import MessageTemplate from './MessageTemplate';
@@ -9,12 +11,14 @@ import Pattern from './Pattern';
import PatternsContext from './PatternsContext';
import Route from './Route';
import RoutesContext from './RoutesContext';
+import Run from './Run';
import Sign from './Sign';
import SignsContext from './SignsContext';
import Stop from './Stop';
import StopsContext from './StopsContext';
import Tag from './Tag';
import TagsContext from './TagsContext';
+import Trip from './Trip';
import Vehicle from './Vehicle';
import VehiclesContext from './VehiclesContext';
@@ -58,6 +62,23 @@ class Customer extends Resource {
return this.resource(Agency, Agency.makeHref(this.code), payload);
}
+ /**
+ * Gets a context for querying this customer's drivers
+ * @returns {DriversContext} Context for querying this customer's drivers
+ */
+ drivers() {
+ return this.resource(DriversContext, this.code);
+ }
+
+ /**
+ * Gets a driver resource by id
+ * @param {Number} id Identity of the driver
+ * @returns {Driver} Driver resource
+ */
+ driver(id) {
+ return this.resource(Driver, Driver.makeHref(this.code, id));
+ }
+
/**
* Gets a context for querying this customer's external APIs
* @returns {ExternalApisContext} Context for querying this customer's external APIs
@@ -129,6 +150,15 @@ class Customer extends Resource {
return this.resource(Pattern, Pattern.makeHref(this.code, id));
}
+ /**
+ * Gets a Run resource by ID
+ * @param {Number} id Identity of the run
+ * @returns {Run} Run resource
+ */
+ run(id) {
+ return this.resource(Run, Run.makeHref(this.code, id));
+ }
+
/**
* Gets a context for querying this customer's signs
* @returns {SignsContext} Context for querying this customer's signs
@@ -183,6 +213,15 @@ class Customer extends Resource {
return this.resource(Tag, { code: this.code, ...id });
}
+ /**
+ * Gets a trip resource by id
+ * @param {Number} id Identity of the trip
+ * @returns {Trip} Trip resource
+ */
+ trip(id) {
+ return this.resource(Trip, Trip.makeHref(this.code, id));
+ }
+
/**
* Gets a context for querying this customer's vehicles
* @returns {VehiclesContext} Context for querying this customer's vehicles
diff --git a/src/resources/Customer.test.js b/src/resources/Customer.test.js
index 1634732..e497db8 100644
--- a/src/resources/Customer.test.js
+++ b/src/resources/Customer.test.js
@@ -4,6 +4,8 @@ import Client from '../Client';
import Customer from './Customer';
import RealTimeClient from '../RealTimeClient';
import Agency from './Agency';
+import Driver from './Driver';
+import DriversContext from './DriversContext';
import ExternalApi from './ExternalApi';
import ExternalApisContext from './ExternalApisContext';
import MessageTemplate from './MessageTemplate';
@@ -12,11 +14,13 @@ import Pattern from './Pattern';
import PatternsContext from './PatternsContext';
import Route from './Route';
import RoutesContext from './RoutesContext';
+import Run from './Run';
import Sign from './Sign';
import SignsContext from './SignsContext';
import Stop from './Stop';
import StopsContext from './StopsContext';
import Tag from './Tag';
+import Trip from './Trip';
import TagsContext from './TagsContext';
import Vehicle from './Vehicle';
import VehiclesContext from './VehiclesContext';
@@ -30,6 +34,8 @@ describe('When getting resources related to a customer', () => {
const customer = new Customer(client, realTimeClient, 'SYNC');
it('should allow the agency record to be retrieved', () => customer.agency().should.be.instanceOf(Agency));
+ it('should allow drivers to be searched', () => customer.drivers().should.be.instanceOf(DriversContext));
+ it('should allow a driver to be retrieved', () => customer.driver().should.be.instanceOf(Driver));
it('should allow external apis to be searched', () => customer.externalApis().should.be.instanceOf(ExternalApisContext));
it('should allow an external api to be retrieved', () => customer.externalApi().should.be.instanceOf(ExternalApi));
it('should allow message templates to be searched', () => customer.messageTemplates().should.be.instanceof(MessageTemplatesContext));
@@ -38,12 +44,14 @@ describe('When getting resources related to a customer', () => {
it('should allow a pattern to be retrieved', () => customer.pattern().should.be.instanceof(Pattern));
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 a run to be retrieved', () => customer.run().should.be.instanceof(Run));
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 stops to be searched', () => customer.stops().should.be.instanceof(StopsContext));
it('should allow a stop to be retrieved', () => customer.stop().should.be.instanceof(Stop));
it('should allow tags to be searched', () => customer.tags().should.be.instanceof(TagsContext));
it('should allow a tag to be retrieved', () => customer.tag().should.be.instanceof(Tag));
+ it('should allow a trip to be retrieved', () => customer.trip().should.be.instanceof(Trip));
it('should allow vehicles to be searched', () => customer.vehicles().should.be.instanceof(VehiclesContext));
it('should allow a vehicle to be retrieved', () => customer.vehicle().should.be.instanceof(Vehicle));
});
diff --git a/src/resources/Driver.js b/src/resources/Driver.js
new file mode 100644
index 0000000..7b26778
--- /dev/null
+++ b/src/resources/Driver.js
@@ -0,0 +1,58 @@
+import Resource from './Resource';
+
+/**
+ * Driver resource
+ */
+class Driver extends Resource {
+
+ /**
+ * Creates a new driver.
+ *
+ * Will populate itself with the values given to it after the client parameter
+ * @example
Assigning partial driver data to a new instance
+ * const client = new Client();
+ * const partialDriverData = {
+ * href: '/1/SYNC/drivers/1',
+ * id: 1,
+ * customer_driver_id: '0001',
+ * first_name: 'Charlie',
+ * last_name: 'Singh',
+ * };
+ * const driver = new Driver(client, partialDriverData);
+ *
+ * driver.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');
+ Object.assign(this, newProperties, { hydrated });
+ }
+
+ /**
+ * Makes a href for a given customer code and DI
+ * @param {string} customerCode Customer code
+ * @param {number} id Driver ID
+ * @returns {{href: string}} URI to instance of driver
+ */
+ static makeHref(customerCode, id) {
+ return {
+ href: `/1/${customerCode}/drivers/${id}`,
+ };
+ }
+
+ /**
+ * Fetches the data for this driver via the client
+ * @returns {Promise} If successful, a hydrated instance of this driver
+ */
+ fetch() {
+ return this.client.get(this.href)
+ .then(response => response.json())
+ .then(driver => new Driver(this.client, this, driver));
+ }
+}
+
+export default Driver;
diff --git a/src/resources/Driver.test.js b/src/resources/Driver.test.js
new file mode 100644
index 0000000..db79a8b
--- /dev/null
+++ b/src/resources/Driver.test.js
@@ -0,0 +1,52 @@
+import chai from 'chai';
+import chaiAsPromised from 'chai-as-promised';
+import fetchMock from 'fetch-mock';
+import Client from '../Client';
+import Driver from './Driver';
+import { drivers as mockDrivers } from '../mocks';
+
+chai.should();
+chai.use(chaiAsPromised);
+
+describe('When instantiating a driver based on customer and ID', () => {
+ const client = new Client();
+ const driver = new Driver(client, Driver.makeHref('SYNC', 1));
+
+ it('should set the href', () => driver.href.should.equal('/1/SYNC/drivers/1'));
+ it('should not be hydrated', () => driver.hydrated.should.equal(false));
+});
+
+describe('When instantiating a driver based on an object', () => {
+ const client = new Client();
+ const driver = new Driver(client, mockDrivers.getById(1));
+
+ it('should set the ID', () => driver.id.should.equal(1));
+ it('should set the href', () => driver.href.should.equal('/1/SYNC/drivers/1'));
+ it('should be hydrated', () => driver.hydrated.should.equal(true));
+ it('should have the expected customer driver id', () =>
+ driver.customer_driver_id.should.equal('0001'));
+ it('should have the expected first name', () => driver.first_name.should.equal('Charlie'));
+ it('should have the expected last name', () => driver.last_name.should.equal('Singh'));
+});
+
+describe('When fetching a driver based on customer and ID', () => {
+ const client = new Client();
+
+ beforeEach(() => mockDrivers.setUpSuccessfulMock(client));
+ beforeEach(() => fetchMock.catch(503));
+ afterEach(fetchMock.restore);
+
+ let promise;
+ beforeEach(() => {
+ promise = new Driver(client, Driver.makeHref('SYNC', 1)).fetch();
+ });
+
+ it('should resolve the promise', () => promise.should.be.fulfilled);
+ it('should set the ID', () => promise.then(p => p.id).should.eventually.equal(1));
+ it('should set the href', () => promise.then(p => p.href).should.eventually.equal('/1/SYNC/drivers/1'));
+ it('should be hydrated', () => promise.then(p => p.hydrated).should.eventually.equal(true));
+ it('should have the expected customer driver id', () => promise.then(p => p.customer_driver_id)
+ .should.eventually.equal('0001'));
+ it('should have the expected first name', () => promise.then(p => p.first_name).should.eventually.equal('Charlie'));
+ it('should have the expected last name', () => promise.then(p => p.last_name).should.eventually.equal('Singh'));
+});
diff --git a/src/resources/DriversContext.js b/src/resources/DriversContext.js
new file mode 100644
index 0000000..b978573
--- /dev/null
+++ b/src/resources/DriversContext.js
@@ -0,0 +1,48 @@
+import 'isomorphic-fetch';
+import PagedContext from './PagedContext';
+import Driver from './Driver';
+
+/**
+ * Driver querying context
+ *
+ * This is used to query the list of drivers for a customer
+ */
+class DriversContext extends PagedContext {
+ /**
+ * Creates a new drivers 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 RUL
+ */
+ constructor(client, customerCode, params) {
+ super(client, { ...params });
+ this.code = customerCode;
+ }
+
+ /**
+ * Sets the query term for the context
+ * @example
+ * const drivers = new DriversContext(...);
+ * patterns
+ * .withQuery('blue')
+ * .getPage()
+ * .then(page => ...);
+ * @param {string} term Query term to search for
+ * @returns {DriversContext} 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 Driver objects
+ * @see Driver
+ */
+ getPage() {
+ return this.page(Driver, `/1/${this.code}/drivers`);
+ }
+}
+
+export default DriversContext;
diff --git a/src/resources/DriversContext.test.js b/src/resources/DriversContext.test.js
new file mode 100644
index 0000000..18975e5
--- /dev/null
+++ b/src/resources/DriversContext.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 DriversContext from './DriversContext';
+import { drivers as mockDrivers } from '../mocks';
+
+chai.should();
+chai.use(chaiAsPromised);
+
+describe('When building a query for drivers', () => {
+ const client = new Client();
+ client.setAuthenticated();
+
+ beforeEach(() => fetchMock
+ .get(client.resolve('/1/SYNC/drivers?page=9&per_page=27&q=charlie&sort=first_name asc,last_name desc'), mockDrivers.list)
+ .catch(503));
+ afterEach(fetchMock.restore);
+
+ let promise;
+ beforeEach(() => {
+ const drivers = new DriversContext(client, 'SYNC');
+ promise = drivers
+ .withPage(9)
+ .withPerPage(27)
+ .withQuery('charlie')
+ .sortedBy('ignored', 'desc')
+ .sortedBy('first_name')
+ .thenBy('last_name', 'desc')
+ .getPage();
+ });
+
+ it('should make the expected request', () => promise.should.be.fulfilled);
+});
diff --git a/src/resources/Run.js b/src/resources/Run.js
new file mode 100644
index 0000000..9f76ac3
--- /dev/null
+++ b/src/resources/Run.js
@@ -0,0 +1,64 @@
+import Resource from './Resource';
+import Trip from './Trip';
+
+/**
+ * Run resource
+ */
+class Run extends Resource {
+ /**
+ * Creates a new Run resource object
+ *
+ * Will populate itself with the values given to it after the client parameter
+ * @example Assigning partial run data to a new instance
+ * const client = new Client();
+ * const partialRunData = {
+ * href: '/1/SYNC/runs/1',
+ * name: 'Scheduled Run',
+ * short_name: 'R01',
+ * };
+ * const run = new Run(client, partialRunData);
+ *
+ * run.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 = {
+ trips: newProperties.trips && newProperties.trips.map(t => new Trip(this.client, t)),
+ };
+
+ Object.assign(this, newProperties, {
+ hydrated,
+ ...references,
+ });
+ }
+
+ /**
+ * Makes a href for a given customer code and ID
+ * @param {string} customerCode Customer code
+ * @param {Number} id Run ID
+ * @returns {{href: string}} URI to instance of run
+ */
+ static makeHref(customerCode, id) {
+ return {
+ href: `/1/${customerCode}/runs/${id}`,
+ };
+ }
+
+ /**
+ * Fetches the data for this run via the client
+ * @returns {Promise} If successful, a hydrated instance of this run
+ */
+ fetch() {
+ return this.client.get(this.href)
+ .then(response => response.json())
+ .then(run => new Run(this.client, this, run));
+ }
+}
+
+export default Run;
diff --git a/src/resources/Run.test.js b/src/resources/Run.test.js
new file mode 100644
index 0000000..06b6cf3
--- /dev/null
+++ b/src/resources/Run.test.js
@@ -0,0 +1,50 @@
+import chai from 'chai';
+import chaiAsPromised from 'chai-as-promised';
+import fetchMock from 'fetch-mock';
+import Client from '../Client';
+import Run from './Run';
+import { runs as mockRuns } from '../mocks';
+
+chai.should();
+chai.use(chaiAsPromised);
+
+describe('When instantiating a run based on customer and ID', () => {
+ const client = new Client();
+ const run = new Run(client, Run.makeHref('SYNC', 1));
+
+ it('should set the href', () => run.href.should.equal('/1/SYNC/runs/1'));
+ it('should not be hydrated', () => run.hydrated.should.equal(false));
+});
+
+describe('When instantiating a run based on an object', () => {
+ const client = new Client();
+ const run = new Run(client, mockRuns.getById(1));
+
+ it('should set the ID', () => run.id.should.equal(1));
+ it('should set the href', () => run.href.should.equal('/1/SYNC/runs/1'));
+ it('should be hydrated', () => run.hydrated.should.equal(true));
+ it('should set the name', () => run.name.should.equal('Run 1'));
+ it('should set the short name', () => run.short_name.should.equal('R01'));
+ it('should map every trip', () => run.trips.length.should.equal(2));
+});
+
+describe('When fetching a trip based on customer and ID', () => {
+ const client = new Client();
+
+ beforeEach(() => mockRuns.setUpSuccessfulMock(client));
+ beforeEach(() => fetchMock.catch(503));
+ afterEach(fetchMock.restore);
+
+ let promise;
+ beforeEach(() => {
+ promise = new Run(client, Run.makeHref('SYNC', 1)).fetch();
+ });
+
+ it('should resolve the promise', () => promise.should.be.fulfilled);
+ it('should set the ID', () => promise.then(p => p.id).should.eventually.equal(1));
+ it('should set the href', () => promise.then(p => p.href).should.eventually.equal('/1/SYNC/runs/1'));
+ it('should be hydrated', () => promise.then(p => p.hydrated).should.eventually.equal(true));
+ it('should set the name', () => promise.then(p => p.name).should.eventually.equal('Run 1'));
+ it('should set the short name', () => promise.then(p => p.short_name).should.eventually.equal('R01'));
+ it('should map every trip', () => promise.then(p => p.trips.length).should.eventually.equal(2));
+});
diff --git a/src/resources/Trip.js b/src/resources/Trip.js
new file mode 100644
index 0000000..8909652
--- /dev/null
+++ b/src/resources/Trip.js
@@ -0,0 +1,64 @@
+import Resource from './Resource';
+import Pattern from './Pattern';
+
+/**
+ * Trip resource
+ */
+class Trip extends Resource {
+ /**
+ * Creates a new trip.
+ *
+ * Will populate itself with the values given to it after the client parameter
+ * @example Assigning partial trip data to a new instance
+ * const client = new Client();
+ * const partialTripData = {
+ * href: '/1/SYNC/trips/1',
+ * id: 1,
+ * name: 'T01',
+ * order: 1,
+ * };
+ * const trip = new Trip(client, partialTripData);
+ *
+ * trip.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 = {
+ pattern: newProperties.pattern && new Pattern(this.client, newProperties.pattern),
+ };
+
+ Object.assign(this, newProperties, {
+ hydrated,
+ ...references,
+ });
+ }
+
+ /**
+ * Makes a href for a given customer code and ID
+ * @param {string} customerCode Customer code
+ * @param {Number} id Trip ID
+ * @returns {{href: string}} URI to instance of trip
+ */
+ static makeHref(customerCode, id) {
+ return {
+ href: `/1/${customerCode}/trips/${id}`,
+ };
+ }
+
+ /**
+ * Fetches the data for this trip via the client
+ * @returns {Promise} If successful, a hydrated instance of this trip
+ */
+ fetch() {
+ return this.client.get(this.href)
+ .then(response => response.json())
+ .then(trip => new Trip(this.client, this, trip));
+ }
+}
+
+export default Trip;
diff --git a/src/resources/Trip.test.js b/src/resources/Trip.test.js
new file mode 100644
index 0000000..bb27a03
--- /dev/null
+++ b/src/resources/Trip.test.js
@@ -0,0 +1,50 @@
+import chai from 'chai';
+import chaiAsPromised from 'chai-as-promised';
+import fetchMock from 'fetch-mock';
+import Client from '../Client';
+import Trip from './Trip';
+import { trips as mockTrips } from '../mocks';
+
+chai.should();
+chai.use(chaiAsPromised);
+
+describe('When instantiating a trip based on customer and ID', () => {
+ const client = new Client();
+ const trip = new Trip(client, Trip.makeHref('SYNC', 1));
+
+ it('should set the href', () => trip.href.should.equal('/1/SYNC/trips/1'));
+ it('should not be hydrated', () => trip.hydrated.should.equal(false));
+});
+
+describe('When instantiating a trip based on an object', () => {
+ const client = new Client();
+ const trip = new Trip(client, mockTrips.getById(3));
+
+ it('should set the ID', () => trip.id.should.equal(3));
+ it('should set the href', () => trip.href.should.equal('/1/SYNC/trips/3'));
+ it('should be hydrated', () => trip.hydrated.should.equal(true));
+ it('should set the name', () => trip.name.should.equal('T03'));
+ it('should set the order', () => trip.order.should.equal(1));
+ it('should have the expected pattern', () => trip.pattern.href.should.equal('/1/SYNC/patterns/1'));
+});
+
+describe('When fetching a trip based on customer and ID', () => {
+ const client = new Client();
+
+ beforeEach(() => mockTrips.setUpSuccessfulMock(client));
+ beforeEach(() => fetchMock.catch(503));
+ afterEach(fetchMock.restore);
+
+ let promise;
+ beforeEach(() => {
+ promise = new Trip(client, Trip.makeHref('SYNC', 3)).fetch();
+ });
+
+ it('should resolve the promise', () => promise.should.be.fulfilled);
+ it('should set the ID', () => promise.then(p => p.id).should.eventually.equal(3));
+ it('should set the href', () => promise.then(p => p.href).should.eventually.equal('/1/SYNC/trips/3'));
+ it('should be hydrated', () => promise.then(p => p.hydrated).should.eventually.equal(true));
+ it('should set the name', () => promise.then(p => p.name).should.eventually.equal('T03'));
+ it('should set the order', () => promise.then(p => p.order).should.eventually.equal(1));
+ it('should have the expected pattern', () => promise.then(p => p.pattern.href).should.eventually.equal('/1/SYNC/patterns/1'));
+});