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