CPPKiteConnect
utils.hpp
1 /*
2  * Licensed under the MIT License <http://opensource.org/licenses/MIT>.
3  * SPDX-License-Identifier: MIT
4  *
5  * Copyright (c) 2020-2022 Bhumit Attarde
6  *
7  * Permission is hereby granted, free of charge, to any person obtaining a
8  * copy of this software and associated documentation files (the "Software"),
9  * to deal in the Software without restriction, including without limitation
10  * the rights to use, copy, modify, merge, publish, distribute, sublicense,
11  * and/or sell copies of the Software, and to permit persons to whom the
12  * Software is furnished to do so, subject to the following conditions:
13  *
14  * The above copyright notice and this permission notice shall be included in
15  * all copies or substantial portions of the Software.
16  *
17  * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
18  * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
19  * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN
20  * NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
21  * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT
22  * OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
23  * USE OR OTHER DEALINGS IN THE SOFTWARE.
24  */
25 
26 #pragma once
27 
28 #include <cstdint>
29 #include <functional>
30 #include <optional>
31 #include <string>
32 #include <type_traits>
33 #include <vector>
34 
35 #include "exceptions.hpp"
36 
37 #include "cpp-httplib/httplib.h"
38 #define FMT_HEADER_ONLY 1
39 #include "fmt/include/fmt/args.h"
40 #include "fmt/include/fmt/format.h"
41 #include "rapidcsv/src/rapidcsv.h"
42 #include "rapidjson/include/rapidjson/document.h"
43 #include "rapidjson/include/rapidjson/encodings.h"
44 #include "rapidjson/include/rapidjson/rapidjson.h"
45 #include "rapidjson/include/rapidjson/stringbuffer.h"
46 #include "rapidjson/include/rapidjson/writer.h"
47 
48 // Check endieness of platform
49 #if defined(_WIN32)
50 // Do nothing (Assuming all modern Windows machines are little endian)
51 #else // Windows check
52 #ifdef __BIG_ENDIAN__
53 #define WORDS_BIGENDIAN 1
54 #else /* __BIG_ENDIAN__ */
55 #ifdef __LITTLE_ENDIAN__
56 #undef WORDS_BIGENDIAN
57 #else
58 #ifdef BSD
59 #include <sys/endian.h>
60 #else
61 #include <endian.h>
62 #endif
63 #if __BYTE_ORDER == __BIG_ENDIAN
64 #define WORDS_BIGENDIAN 1
65 #elif __BYTE_ORDER == __LITTLE_ENDIAN
66 #undef WORDS_BIGENDIAN
67 #else
68 #error "unable to determine endianess!"
69 #endif /* __BYTE_ORDER */
70 #endif /* __LITTLE_ENDIAN__ */
71 #endif /* __BIG_ENDIAN__ */
72 #endif // Windows check
73 
74 // NOLINTNEXTLINE(google-global-names-in-headers, misc-unused-using-decls)
75 using fmt::literals::operator""_a;
76 // NOLINTNEXTLINE(cppcoreguidelines-macro-usage)
77 #define FMT fmt::format
78 
79 // NOLINTNEXTLINE(cppcoreguidelines-macro-usage)
80 #define GENERATE_FLUENT_METHOD(returnType, fieldType, fieldName, methodName) \
81  returnType& methodName(fieldType arg) { \
82  (fieldName) = arg; \
83  return *this; \
84  };
85 
86 namespace kiteconnect::internal::utils {
87 using std::string;
88 namespace rj = rapidjson;
89 
91 using FmtArgs = std::vector<string>;
92 template <typename>
93 struct isOptional : std::false_type {};
94 template <typename T>
95 struct isOptional<std::optional<T>> : std::true_type {};
96 
97 template <typename>
98 struct isVector : std::false_type {};
99 template <typename T>
100 struct isVector<std::vector<T>> : std::true_type {};
101 
102 constexpr uint16_t MILLISECONDS_IN_A_SECOND = 1000;
103 
104 namespace json {
105 
106 using JsonObject = rj::GenericValue<rj::UTF8<>>::Object;
107 using JsonArray = rj::GenericValue<rj::UTF8<>>::Array;
108 template <class Res>
109 using CustomObjectParser = std::function<Res(JsonObject&)>;
110 template <class Res>
111 using CustomArrayParser = std::function<Res(JsonArray&)>;
112 template <class Res, class Data, bool UseCustomParser>
113 using CustomParser = std::conditional_t<std::is_same_v<Data, JsonObject>,
114  const CustomObjectParser<Res>&, const CustomArrayParser<Res>&>;
115 template <class T>
116 using JsonEncoder = std::function<void(const T&, rj::Value&)>;
117 
118 // FIXME templatize extract* methods
119 inline JsonObject extractObject(rj::Document& doc) {
120  try {
121  return doc["data"].GetObject();
122  } catch (const std::exception& ex) { throw libException("invalid body"); }
123 }
124 
125 inline JsonArray extractArray(rj::Document& doc) {
126  try {
127  return doc["data"].GetArray();
128  } catch (const std::exception& ex) { throw libException("invalid body"); }
129 }
130 
131 inline bool extractBool(rj::Document& doc) {
132  try {
133  return doc["data"].GetBool();
134  } catch (const std::exception& ex) { throw libException("invalid body"); }
135 }
136 
137 inline string extractString(rj::Document& doc) {
138  try {
139  return doc["data"].GetString();
140  } catch (const std::exception& ex) { throw libException("invalid body"); }
141 }
142 
143 template <class Output, class Document = rj::Value::Object>
144 // NOLINTNEXTLINE(readability-function-cognitive-complexity)
145 inline Output get(const Document& val, const char* name) {
146  static const auto exceptionString = [&name](const string& type) {
147  return FMT("type of {0} not is not {1}", name, type);
148  };
149 
150  auto it = val.FindMember(name);
151  if (it != val.MemberEnd()) {
152  if constexpr (!isVector<Output>::value) {
153  if constexpr (std::is_same_v<std::decay_t<Output>, string>) {
154  if (it->value.IsString()) { return it->value.GetString(); };
155  if (it->value.IsNull()) { return ""; };
156  throw libException(exceptionString("string"));
157  } else if constexpr (std::is_same_v<std::decay_t<Output>, double>) {
158  if (it->value.IsDouble()) { return it->value.GetDouble(); };
159  // if the sent value doesn't have a floating point (this time),
160  // GetDouble() will throw error
161  if (it->value.IsInt()) { return it->value.GetInt(); };
162  throw libException(exceptionString("double"));
163  } else if constexpr (std::is_same_v<std::decay_t<Output>, int>) {
164  if (it->value.IsInt()) { return it->value.GetInt(); };
165  throw libException(exceptionString("int"));
166  } else if constexpr (std::is_same_v<std::decay_t<Output>,
167  uint32_t>) {
168  if (it->value.IsUint()) { return it->value.GetUint(); };
169  throw libException(exceptionString("uint32_t"));
170  } else if constexpr (std::is_same_v<std::decay_t<Output>,
171  int64_t>) {
172  if (it->value.IsInt64()) { return it->value.GetInt64(); };
173  throw libException(exceptionString("int64_t"));
174  } else if constexpr (std::is_same_v<std::decay_t<Output>, bool>) {
175  if (it->value.IsBool()) { return it->value.GetBool(); };
176  throw libException(exceptionString("bool"));
177  } else {
178  throw libException("type not supported");
179  }
180  } else {
181  if (it->value.IsArray()) {
182  Output out;
183  for (const auto& v : it->value.GetArray()) {
184  if constexpr (std::is_same_v<
185  std::decay_t<typename Output::value_type>,
186  string>) {
187  (v.IsString()) ?
188  out.emplace_back(v.GetString()) :
189  throw libException(exceptionString("string"));
190  } else if constexpr (std::is_same_v<
191  std::decay_t<
192  typename Output::value_type>,
193  double>) {
194  if (v.IsDouble()) {
195  out.emplace_back(v.GetDouble());
196  continue;
197  };
198  // if the sent value doesn't have a floating point (this
199  // time), GetDouble() will throw error
200  if (v.IsInt()) {
201  out.emplace_back(v.GetInt());
202  continue;
203  };
204  throw libException(exceptionString("array of doubles"));
205  } else {
206  throw libException("type not supported");
207  }
208  }
209  return out;
210  };
211  throw libException(exceptionString("array"));
212  }
213  } else {
214  return {};
215  }
216 };
217 
218 template <class Val, class Output>
219 Output get(const rj::Value::Object& val, const char* name) {
220  static const auto exceptionString = [&name](const string& type) {
221  return FMT("type of {0} not is not {1}", name, type);
222  };
223 
224  auto it = val.FindMember(name);
225  if constexpr (std::is_same_v<Val, JsonObject>) {
226  static_assert(std::is_constructible_v<Output, rj::Value::Object>);
227  rj::Value out(rj::kObjectType);
228  if (it != val.MemberEnd()) {
229  if (it->value.IsObject()) { return Output(it->value.GetObject()); };
230  throw libException(exceptionString("object"));
231  };
232  return {};
233  } else if constexpr (std::is_same_v<Val, JsonArray>) {
234  static_assert(std::is_constructible_v<Output, rj::Value::Array>);
235  rj::Value out(rj::kArrayType);
236  if (it != val.MemberEnd()) {
237  if (it->value.IsArray()) { return Output(it->value.GetArray()); };
238  throw libException(exceptionString("array"));
239  };
240  return {};
241  };
242  return {};
243 };
244 
245 template <class Val>
246 bool get(const rj::Value::Object& val, rj::Value& out, const char* name) {
247  static const auto exceptionString = [&name](const string& type) {
248  return FMT("type of {0} not is not {1}", name, type);
249  };
250 
251  auto it = val.FindMember(name);
252  if constexpr (std::is_same_v<Val, JsonObject>) {
253  if (it != val.MemberEnd()) {
254  if (it->value.IsObject()) {
255  out = it->value.GetObject();
256  return true;
257  };
258  throw libException(exceptionString("object"));
259  };
260  return false;
261  } else if constexpr (std::is_same_v<Val, JsonArray>) {
262  if (it != val.MemberEnd()) {
263  if (it->value.IsArray()) {
264  out = it->value.GetArray();
265  return true;
266  };
267  throw libException(exceptionString("array"));
268  };
269  return false;
270  };
271  return false;
272 };
273 
274 template <class Res, class Data, bool UseCustomParser>
275 Res parse(
276  rj::Document& doc, CustomParser<Res, Data, UseCustomParser> customParser) {
277  if constexpr (std::is_same_v<Data, JsonObject>) {
278  auto object = extractObject(doc);
279  if constexpr (UseCustomParser) {
280  return customParser(object);
281  } else {
282  static_assert(std::is_constructible_v<Res, JsonObject>,
283  "Res should be constructable using JsonObject");
284  return Res(object);
285  }
286  } else if constexpr (std::is_same_v<Data, JsonArray>) {
287  auto array = extractArray(doc);
288  if constexpr (UseCustomParser) {
289  return customParser(array);
290  } else {
291  static_assert(std::is_constructible_v<Res, JsonArray>,
292  "Res should be constructable using JsonArray");
293  return Res(array);
294  }
295  } else if constexpr (std::is_same_v<Data, bool>) {
296  static_assert(
297  std::is_same_v<Res, bool>, "Res needs to be bool if Data is bool");
298  return extractBool(doc);
299  }
300 }
301 
302 inline bool parse(rj::Document& dom, const string& str) {
303  rj::ParseResult result = dom.Parse(str.c_str());
304  if (result == nullptr) {
305  throw libException(FMT("failed to parse json string: {0}", str));
306  };
307  return true;
308 };
309 
310 inline string serialize(rj::Document& dom) {
311  rj::StringBuffer buffer;
312  rj::Writer<rj::StringBuffer> writer(buffer);
313  dom.Accept(writer);
314  return buffer.GetString();
315 }
316 
317 template <class T>
318 class json {
319  public:
320  json() {
321  if constexpr (std::is_same_v<T, JsonObject>) {
322  dom.SetObject();
323  } else {
324  dom.StartArray();
325  }
326  };
327 
328  template <class Value>
329  void field(const string& name, const Value& value,
330  rj::Value* docOverride = nullptr) {
331  auto& allocater = dom.GetAllocator();
332 
333  if constexpr (std::is_same_v<std::decay_t<Value>, string>) {
334  buffer.SetString(value.c_str(), value.size(), allocater);
335  } else if constexpr (std::is_integral_v<std::decay_t<Value>>) {
336  buffer.SetInt64(value);
337  } else if constexpr (std::is_floating_point_v<std::decay_t<Value>>) {
338  buffer.SetDouble(value);
339  };
340 
341  if (docOverride == nullptr) {
342  dom.AddMember(
343  rj::Value(name.c_str(), allocater).Move(), buffer, allocater);
344  } else {
345  docOverride->AddMember(
346  rj::Value(name.c_str(), allocater).Move(), buffer, allocater);
347  }
348  }
349 
350  template <class Value>
351  void field(const string& name, const std::vector<Value>& values,
352  const JsonEncoder<Value>& encode = {}) {
353  auto& allocater = dom.GetAllocator();
354  rj::Value arrayBuffer(rj::kArrayType);
355  for (const auto& i : values) {
356  if constexpr (std::is_fundamental_v<std::decay_t<Value>>) {
357  arrayBuffer.PushBack(i, allocater);
358  } else {
359  rj::Value objectBuffer(rj::kObjectType);
360  encode(i, objectBuffer);
361  arrayBuffer.PushBack(objectBuffer, allocater);
362  }
363  };
364 
365  if constexpr (std::is_same_v<T, JsonObject>) {
366  dom.AddMember(rj::Value(name.c_str(), dom.GetAllocator()).Move(),
367  arrayBuffer, allocater);
368  } else {
369  dom.Swap(arrayBuffer);
370  }
371  }
372 
373  template <class Value>
374  void array(const std::vector<Value>& values,
375  const JsonEncoder<Value>& encode = {}) {
376  field("", values, encode);
377  }
378 
379  string serialize() {
380  rj::StringBuffer strBuffer;
381  rj::Writer<rj::StringBuffer> writer(strBuffer);
382  dom.Accept(writer);
383  return strBuffer.GetString();
384  }
385 
386  private:
387  rj::Document dom;
388  rj::Value buffer;
389 };
390 } // namespace json
391 
392 namespace http {
393 
394 using Params = httplib::Params;
395 
396 namespace code {
397 constexpr uint16_t OK = 200;
398 } // namespace code
399 
400 enum class METHOD : uint8_t
401 {
402  GET,
403  POST,
404  PUT,
405  DEL,
406  HEAD
407 };
408 
409 enum class CONTENT_TYPE : uint8_t
410 {
411  JSON,
412  NON_JSON
413 };
414 
415 struct endpoint {
416  bool operator==(const endpoint& lhs) const {
417  return lhs.method == this->method && lhs.Path.Path == this->Path.Path &&
418  lhs.contentType == this->contentType;
419  }
420 
421  METHOD method = METHOD::GET;
422  struct path {
423 
424  string operator()(const FmtArgs& fmtArgs = {}) const {
425  if (!fmtArgs.empty()) {
426  fmt::dynamic_format_arg_store<fmt::format_context> store;
427  for (const auto& arg : fmtArgs) { store.push_back(arg); };
428  return fmt::vformat(Path, store);
429  };
430  return Path;
431  };
432 
433  string Path;
434  } Path;
435  CONTENT_TYPE contentType = CONTENT_TYPE::NON_JSON;
436  CONTENT_TYPE responseType = CONTENT_TYPE::JSON;
437 };
438 
439 class response {
440  public:
441  response(uint16_t Code, const string& body, bool json = true): code(Code) {
442  parse(Code, body, json);
443  };
444 
445  explicit operator bool() const { return !error; };
446 
447  uint16_t code = 0;
448  bool error = false;
449  rj::Document data;
450  string errorType =
451  "NoException";
452  string message;
454  string rawBody;
456 
457  private:
458  void parse(uint16_t code, const string& body, bool json) {
459  if (json) {
460  json::parse(data, body);
461  if (code != static_cast<uint16_t>(code::OK)) {
462  string status;
463  status = utils::json::get<string, rj::Document>(data, "status");
464  errorType =
465  utils::json::get<string, rj::Document>(data, "error_type");
466  message =
467  utils::json::get<string, rj::Document>(data, "message");
468  if (status != "success") { error = true; };
469  };
470  } else {
471  if (code != static_cast<uint16_t>(code::OK)) { error = true; };
472  rawBody = body;
473  }
474  };
475 };
476 
477 struct request {
478 
479  // NOLINTNEXTLINE(readability-function-cognitive-complexity)
480  response send(httplib::Client& client) const {
481  const httplib::Headers headers = { { "Authorization", authToken } };
482  uint16_t code = 0;
483  string data;
484 
485  // httplib::Result doesn't have a default constructor and using a
486  // pointer causes segfault
487  switch (method) {
488  case utils::http::METHOD::GET:
489  if (auto res = client.Get(path, headers)) {
490  code = res->status;
491  data = res->body;
492  } else {
493  throw libException(FMT("request failed ({0})",
494  httplib::to_string(res.error())));
495  }
496  break;
497  case utils::http::METHOD::POST:
498  if (contentType != CONTENT_TYPE::JSON) {
499  if (auto res = client.Post(path, headers, body)) {
500  code = res->status;
501  data = res->body;
502  } else {
503  throw libException(FMT("request failed ({0})",
504  httplib::to_string(res.error())));
505  }
506  } else {
507  if (auto res = client.Post(path, headers, serializedBody,
508  "application/json")) {
509  code = res->status;
510  data = res->body;
511  } else {
512  throw libException(FMT("request failed({0})",
513  httplib::to_string(res.error())));
514  }
515  };
516  break;
517  case utils::http::METHOD::PUT:
518  if (contentType != CONTENT_TYPE::JSON) {
519  if (auto res = client.Put(path, headers, body)) {
520  code = res->status;
521  data = res->body;
522  } else {
523  throw libException(FMT("request failed ({0})",
524  httplib::to_string(res.error())));
525  }
526  } else {
527  if (auto res = client.Put(path, headers, serializedBody,
528  "application/json")) {
529  code = res->status;
530  data = res->body;
531  } else {
532  throw libException(FMT("request failed({0})",
533  httplib::to_string(res.error())));
534  }
535  }
536  break;
537  case utils::http::METHOD::DEL:
538  if (auto res = client.Delete(path, headers)) {
539  code = res->status;
540  data = res->body;
541  } else {
542  throw libException(FMT("request failed ({0})",
543  httplib::to_string(res.error())));
544  }
545  break;
546  default: throw libException("unsupported http method");
547  };
548 
549  return { code, data, responseType == CONTENT_TYPE::JSON };
550  };
551 
552  utils::http::METHOD method;
553  string path;
554  string authToken;
555  Params body;
556  CONTENT_TYPE contentType = CONTENT_TYPE::NON_JSON;
557  CONTENT_TYPE responseType = CONTENT_TYPE::JSON;
558  string serializedBody;
559 };
560 } // namespace http
561 
562 namespace ws::ERROR_CODE {
563 const unsigned int NORMAL_CLOSURE = 1000;
564 const unsigned int NO_REASON = 1006;
565 } // namespace ws::ERROR_CODE
566 
567 template <class Param>
568 void addParam(http::Params& bodyParams, Param& param, const string& fieldName) {
569  static_assert(
570  isOptional<std::decay_t<Param>>::value, "Param must be std::optional");
571  if (param.has_value()) {
572  string fieldValue;
573  if constexpr (!std::is_same_v<typename Param::value_type, string>) {
574  fieldValue = std::to_string(param.value());
575  } else {
576  fieldValue = param.value();
577  }
578  if (param.has_value()) { bodyParams.emplace(fieldName, fieldValue); }
579  }
580 };
581 
582 template <class Instrument>
583 inline std::vector<Instrument> parseInstruments(const std::string& data) {
584  static_assert(std::is_constructible_v<Instrument, std::vector<string>>,
585  "Instrument must have a constructor that accepts vector of strings");
586 
587  std::stringstream sstream(data);
588  rapidcsv::Document csv(sstream, rapidcsv::LabelParams(0, -1));
589  const size_t numberOfRows = csv.GetRowCount();
590 
591  std::vector<Instrument> instruments;
592  for (size_t row = 0; row < numberOfRows; row++) {
593  instruments.emplace_back(csv.GetRow<string>(row));
594  }
595  return instruments;
596 };
597 
598 } // namespace kiteconnect::internal::utils