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}`);
+});