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: '; rel="next", ; 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')); +});