Node.js skeleton for Booking Server API v3
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..76bbbeb
--- /dev/null
+++ b/README.md
@@ -0,0 +1,75 @@
+# Booking Server Skeleton for Node.js
+
+This is a reference implementation of
+[API v3 Booking Server](https://developers.google.com/maps-booking/guides/partner-implementing-booking-server-1)
+based on Node.js
+
+### Prerequisites
+
+Requires an installation of
+
+* [Node.js](https://nodejs.org/)
+
+## Getting Started
+
+Booking Server is implemented using standard Node.js without any additional
+libraries or frameworks for the simplicity of illustration purposes. If you are
+using any other frameworks you could easily change this implementation to
+Express.js, or MEAN.js, or any other Node.js-based framework of your choice.
+
+The implementation is also not using protocol buffer libraries, but instead
+relies on simple JSON serialization and its JSON.parse() and JSON.stringify()
+methods.
+
+To download the project execute the following command:
+
+ git clone https://maps-booking.googlesource.com/js-maps-booking-rest-server-v3-skeleton
+
+The entire code base consists of only two JavaScript files: - bookingserver.js -
+HTTP server and requests handling logic, including authentication -
+apiv3methods.js - methods implementing API v3 interface
+
+After you downloaded the files you can start the Booking Server by running the
+command:
+
+ node bookingserver.js
+
+The skeleton writes all incoming and outgoing requests to console so you can
+monitor its execution for tracing purposes.
+
+Shoud you need an IDE for code changes or debugging you can use
+[Visual Studio Code](https://code.visualstudio.com/) or any other editor of your
+choice. Debug the project by starting bookingserver.js in Node.js environment
+and set breakpoints where needed.
+
+## Testing your Booking Server
+
+Download
+[Booking test utility](https://maps-booking.googlesource.com/maps-booking-v3/).
+To install it, follow the provided installation instructions in its README page.
+
+For the tests, you need to create a text file to store your credentials.
+Put your username and password there in one line, e.g. cred.txt file:
+
+username:password
+
+You also need an availability feed for your test merchants. In our sample
+commands below, we saved it as avail.json filename.
+
+Now, you can test your Booking Server with these commands:
+
+* Test calls to HealthCheck method:
+
+ bin/bookingClient -server_addr="localhost:8080" -health_check_test=true -credentials_file="./cred.txt"
+
+* Test calls to CheckAvailability method:
+
+ bin/bookingClient -server_addr="localhost:8080" -check_availability_test=true -availability_feed="./avail.json" -credentials_file="./cred.txt"
+
+* Test calls to CreateBooking and UpdateBooking methods:
+
+ bin/bookingClient -server_addr="localhost:8080" -booking_test=true -availability_feed="./avail.json" -credentials_file="./cred.txt"
+
+As you are working on implementing your own Booking Server, you may need to
+run additional tests against it (e.g. list_bookings_test, rescheduling_test,
+etc) with the goal of eventually passing all tests (-all_tests=true).
diff --git a/apiv3methods.js b/apiv3methods.js
new file mode 100644
index 0000000..9c96027
--- /dev/null
+++ b/apiv3methods.js
@@ -0,0 +1,165 @@
+/*
+ * Copyright 2018, Google Inc.
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above
+ * copyright notice, this list of conditions and the following disclaimer
+ * in the documentation and/or other materials provided with the
+ * distribution.
+ * * Neither the name of Google Inc. nor the names of its
+ * contributors may be used to endorse or promote products derived from
+ * this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+/**
+ * HealthCheck method
+ * https://developers.google.com/maps-booking/reference/rest-api-v3/healthcheck-method
+ * @param {string} requestBody - HTTP request body
+ * @return {string} HTTP response body
+ */
+function HealthCheck(requestBody) {
+ // TO-DO: add any additional server checks, e.g. database status
+ // ...
+ // Return a response similar to gRPC Health Check
+ // https://github.com/grpc/grpc/blob/master/doc/health-checking.md
+ var res = {status: 'SERVING'};
+ const responseBody = JSON.stringify(res);
+ return responseBody;
+}
+
+/**
+ * CheckAvailability method
+ * https://developers.google.com/maps-booking/reference/rest-api-v3/checkavailability-method
+ * @param {string} requestBody - HTTP request body
+ * @return {string} HTTP response body
+ */
+function CheckAvailability(requestBody) {
+ // CheckAvailabilityRequest
+ const req = JSON.parse(requestBody);
+ // TO-DO: validate req, e.g.
+ // (req.slot !== null && req.slot.merchant_id !== null)
+ // TO-DO: add code to verify the provided slot availability
+ // ...
+ // CheckAvailabilityResponse
+ var resp = {
+ slot: req.slot,
+ count_available: 1,
+ duration_requirement: 'DURATION_REQUIREMENT_UNSPECIFIED'
+ // TO-DO: populate proper values and other fields, such as
+ // availability_update
+ };
+ const responseBody = JSON.stringify(resp);
+ return responseBody;
+}
+
+/**
+ * CreateBooking method
+ * https://developers.google.com/maps-booking/reference/rest-api-v3/createbooking-method
+ * @param {string} requestBody - HTTP request body
+ * @return {string} HTTP response body
+ */
+function CreateBooking(requestBody) {
+ // CreateBookingRequest
+ const req = JSON.parse(requestBody);
+ // TO-DO: validate req, e.g. (req.user_information !== null)
+ // TO-DO: add code to create a booking
+ // ...
+ // CreateBookingResponse
+ var resp = {
+ booking: {
+ booking_id: '1234',
+ slot: req.slot,
+ user_information: {user_id: req.user_information.user_id},
+ payment_information: req.payment_information,
+ status: 'CONFIRMED'
+ }
+ };
+ const responseBody = JSON.stringify(resp);
+ return responseBody;
+}
+
+/**
+ * UpdateBooking method
+ * https://developers.google.com/maps-booking/reference/rest-api-v3/updatebooking-method
+ * @param {string} requestBody - HTTP request body
+ * @return {string} HTTP response body
+ */
+function UpdateBooking(requestBody) {
+ // UpdateBookingRequest
+ const req = JSON.parse(requestBody);
+ // TO-DO: validate req, e.g.
+ // (req.booking !== null && req.booking.booking_id !== null)
+ // TO-DO: add code to update the provided booking
+ // ...
+ // UpdateBookingResponse
+ var resp = {
+ booking: {booking_id: req.booking.booking_id, status: req.booking.status}
+ };
+ const responseBody = JSON.stringify(resp);
+ return responseBody;
+}
+
+/**
+ * GetBookingStatus method
+ * https://developers.google.com/maps-booking/reference/rest-api-v3/getbookingstatus-method
+ * @param {string} requestBody - HTTP request body
+ * @return {string} HTTP response body
+ */
+function GetBookingStatus(requestBody) {
+ // GetBookingStatusRequest
+ const req = JSON.parse(requestBody);
+ // TO-DO: validate req, e.g. (req.booking_id !== null)
+ // TO-DO: add code to retrieve the booking status
+ // ...
+ // GetBookingStatusResponse
+ var resp = {
+ booking_id: req.booking_id,
+ booking_status: 'BOOKING_STATUS_UNSPECIFIED'
+ };
+ const responseBody = JSON.stringify(resp);
+ return responseBody;
+}
+
+/**
+ * ListBookings method
+ * https://developers.google.com/maps-booking/reference/rest-api-v3/listbookings-method
+ * @param {string} requestBody - HTTP request body
+ * @return {string} HTTP response body
+ */
+function ListBookings(requestBody) {
+ // ListBookingsRequest
+ const req = JSON.parse(requestBody);
+ console.log(`ListBookings() for user_id: ${req.user_id}`);
+ // TO-DO: validate req, e.g. (req.user_id !== null)
+ // TO-DO: add code to fetch all bookings for the user_id
+ // ...
+ // ListBookingsResponse
+ var resp = {bookings: {}};
+ const responseBody = JSON.stringify(resp);
+ return responseBody;
+}
+
+module.exports.HealthCheck = HealthCheck;
+module.exports.CheckAvailability = CheckAvailability;
+module.exports.CreateBooking = CreateBooking;
+module.exports.UpdateBooking = UpdateBooking;
+module.exports.GetBookingStatus = GetBookingStatus;
+module.exports.ListBookings = ListBookings;
diff --git a/bookingserver.js b/bookingserver.js
new file mode 100644
index 0000000..849a6de
--- /dev/null
+++ b/bookingserver.js
@@ -0,0 +1,186 @@
+/*
+ * Copyright 2018, Google Inc.
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above
+ * copyright notice, this list of conditions and the following disclaimer
+ * in the documentation and/or other materials provided with the
+ * distribution.
+ * * Neither the name of Google Inc. nor the names of its
+ * contributors may be used to endorse or promote products derived from
+ * this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+const hostname = '127.0.0.1';
+const port = 8080;
+const usernamePassword = 'username:password';
+
+const http = require('http');
+const apiv3 = require('./apiv3methods.js');
+
+// TO-DO: implement SSL server using https module
+// for more info: https://nodejs.org/api/https.html
+// const https = require('https');
+// const fs = require('fs');
+// const options = {
+// key: fs.readFileSync('./keys/booking-server-key.pem'),
+// cert: fs.readFileSync('./keys/booking-server-cert.pem')
+// };
+// const server = https.createServer(options, (request, response) => {...
+
+const server = http.createServer((request, response) => {
+ const {headers, method, url} = request;
+
+ // Parsing basic authentication to extract base64 encoded username:password
+ // Authorization:Basic dXNlcm5hbWU6cGFzc3dvcmQ=
+ var decodedString = '';
+ const authorization = headers['authorization'];
+ if (authorization) {
+ const encodedString = authorization.replace('Basic ', '');
+ const decodedBuffer = new Buffer(encodedString, 'base64');
+ decodedString = decodedBuffer.toString(); // "username:password"
+ }
+
+ if (decodedString !== usernamePassword) {
+ response.statusCode = 401; // Unauthorized
+ response.setHeader('Content-Type', 'text/plain');
+ response.end('Unauthorized Request');
+ return;
+ }
+
+ // convert url to lower case and remove trailing '/' if there's one
+ // you can also remove prefixed URL, if your server is hosted on
+ // server/somepath/
+ var path =
+ url.endsWith('/') ? url.slice(0, -1).toLowerCase() : url.toLowerCase();
+
+ console.log(`HTTP Request ${method} ${path}`);
+
+ // retrieving request body and processing it
+ // for more info:
+ // https://nodejs.org/en/docs/guides/anatomy-of-an-http-transaction/
+ let requestBody = [];
+ request
+ .on('error',
+ (err) => {
+ console.error(err);
+ })
+ .on('data',
+ (chunk) => {
+ // when a large request come in chunks
+ requestBody.push(chunk);
+ })
+ .on('end', () => {
+ // reconstructing entire request body in the string requestBody
+ requestBody = Buffer.concat(requestBody).toString();
+
+ // if there is an error return one of the HTTP error codes
+ // https://developers.google.com/maps-booking/reference/rest-api-v3/status_codes
+ var httpCode = 200; // OK
+ var responseBody = '';
+ var contentType = 'application/json';
+
+ if (method === 'GET') {
+ // GET /v3/HealthCheck/
+ if (path === '/v3/healthcheck') {
+ try {
+ responseBody = apiv3.HealthCheck(requestBody);
+ } catch (e) {
+ // TO-DO: add a specific error handling if necessary
+ httpCode = 500; // Internal Server Error
+ console.log(`Error: ${e}`);
+ }
+ } else // some unknown request
+ {
+ httpCode = 400; // Bad Request
+ contentType = 'text/plain';
+ responseBody = 'Request Not Supported';
+ }
+ } else if (method === 'POST') {
+ switch (path) {
+ // POST /v3/CheckAvailability/
+ case '/v3/checkavailability':
+ try {
+ responseBody = apiv3.CheckAvailability(requestBody);
+ } catch (e) {
+ // TO-DO: add a specific error handling if necessary
+ httpCode = 500; // Internal Server Error
+ console.log(`Error: ${e}`);
+ }
+ break;
+ // POST /v3/CreateBooking/
+ case '/v3/createbooking':
+ try {
+ responseBody = apiv3.CreateBooking(requestBody);
+ } catch (e) {
+ // TO-DO: add a specific error handling if necessary
+ httpCode = 500; // Internal Server Error
+ console.log(`Error: ${e}`);
+ }
+ break;
+ // POST /v3/UpdateBooking/
+ case '/v3/updatebooking':
+ try {
+ responseBody = apiv3.UpdateBooking(requestBody);
+ } catch (e) {
+ // TO-DO: add a specific error handling if necessary
+ httpCode = 500; // Internal Server Error
+ console.log(`Error: ${e}`);
+ }
+ break;
+ // POST /v3/GetBookingStatus/
+ case '/v3/getbookingstatus':
+ try {
+ responseBody = apiv3.GetBookingStatus(requestBody);
+ } catch (e) {
+ // TO-DO: add a specific error handling if necessary
+ httpCode = 500; // Internal Server Error
+ console.log(`Error: ${e}`);
+ }
+ break;
+ // POST /v3/ListBookings/
+ case '/v3/listbookings':
+ try {
+ responseBody = apiv3.ListBookings(requestBody);
+ } catch (e) {
+ // TO-DO: add a specific error handling if necessary
+ httpCode = 500; // Internal Server Error
+ console.log(`Error: ${e}`);
+ }
+ break;
+ // some unknown request
+ default:
+ httpCode = 400; // Bad Request
+ contentType = 'text/plain';
+ responseBody = 'Request Not Supported';
+ }
+ }
+
+ console.log(`HTTP Response ${httpCode} ${responseBody}`);
+ response.statusCode = httpCode;
+ response.setHeader('Content-Type', contentType);
+ response.end(responseBody);
+ });
+});
+
+server.listen(port, hostname, () => {
+ console.log(`Booking Server is running at ${hostname}:${port}`);
+});