From d285baa654d9eb97835e8fd38a15af7fa2d0221a Mon Sep 17 00:00:00 2001 From: Rebekah Davis Date: Wed, 19 Feb 2025 13:45:15 -0500 Subject: [PATCH 1/8] Add class RestProfile for local maintenance of users' REST parameters. --- tiledb/CMakeLists.txt | 4 +- tiledb/common/CMakeLists.txt | 3 +- tiledb/common/filesystem/CMakeLists.txt | 33 ++ tiledb/common/filesystem/home_directory.cc | 75 +++++ tiledb/common/filesystem/home_directory.h | 74 +++++ .../test/compile_home_directory_main.cc | 36 ++ tiledb/sm/rest/CMakeLists.txt | 16 +- tiledb/sm/rest/rest_profile.cc | 230 +++++++++++++ tiledb/sm/rest/rest_profile.h | 219 +++++++++++++ tiledb/sm/rest/test/CMakeLists.txt | 32 ++ .../sm/rest/test/compile_rest_profile_main.cc | 34 ++ tiledb/sm/rest/test/main.cc | 34 ++ tiledb/sm/rest/test/unit_rest_profile.cc | 309 ++++++++++++++++++ 13 files changed, 1094 insertions(+), 5 deletions(-) create mode 100644 tiledb/common/filesystem/CMakeLists.txt create mode 100644 tiledb/common/filesystem/home_directory.cc create mode 100644 tiledb/common/filesystem/home_directory.h create mode 100644 tiledb/common/filesystem/test/compile_home_directory_main.cc create mode 100644 tiledb/sm/rest/rest_profile.cc create mode 100644 tiledb/sm/rest/rest_profile.h create mode 100644 tiledb/sm/rest/test/CMakeLists.txt create mode 100644 tiledb/sm/rest/test/compile_rest_profile_main.cc create mode 100644 tiledb/sm/rest/test/main.cc create mode 100644 tiledb/sm/rest/test/unit_rest_profile.cc diff --git a/tiledb/CMakeLists.txt b/tiledb/CMakeLists.txt index f8ed77d2059..e736cb489e1 100644 --- a/tiledb/CMakeLists.txt +++ b/tiledb/CMakeLists.txt @@ -4,7 +4,7 @@ # # The MIT License # -# Copyright (c) 2017-2024 TileDB, Inc. +# Copyright (c) 2017-2025 TileDB, Inc. # Copyright (c) 2016 MIT and Intel Corporation # # Permission is hereby granted, free of charge, to any person obtaining a copy @@ -133,6 +133,7 @@ endif() set(TILEDB_CORE_SOURCES ${TILEDB_CORE_INCLUDE_DIR}/tiledb/common/memory.cc ${TILEDB_CORE_INCLUDE_DIR}/tiledb/common/stdx_string.cc + ${TILEDB_CORE_INCLUDE_DIR}/tiledb/common/filesystem/home_directory.cc ${TILEDB_CORE_INCLUDE_DIR}/tiledb/common/interval/interval.cc ${TILEDB_CORE_INCLUDE_DIR}/tiledb/common/types/dynamic_typed_datum.cc ${TILEDB_CORE_INCLUDE_DIR}/tiledb/platform/cert_file.cc @@ -279,6 +280,7 @@ set(TILEDB_CORE_SOURCES ${TILEDB_CORE_INCLUDE_DIR}/tiledb/sm/query/writers/writer_base.cc ${TILEDB_CORE_INCLUDE_DIR}/tiledb/sm/query_plan/query_plan.cc ${TILEDB_CORE_INCLUDE_DIR}/tiledb/sm/rest/rest_client.cc + ${TILEDB_CORE_INCLUDE_DIR}/tiledb/sm/rest/rest_profile.cc ${TILEDB_CORE_INCLUDE_DIR}/tiledb/sm/rtree/rtree.cc ${TILEDB_CORE_INCLUDE_DIR}/tiledb/sm/serialization/array.cc ${TILEDB_CORE_INCLUDE_DIR}/tiledb/sm/serialization/array_directory.cc diff --git a/tiledb/common/CMakeLists.txt b/tiledb/common/CMakeLists.txt index e67cb2b4331..2c4358e6cad 100644 --- a/tiledb/common/CMakeLists.txt +++ b/tiledb/common/CMakeLists.txt @@ -3,7 +3,7 @@ # # The MIT License # -# Copyright (c) 2021-2024 TileDB, Inc. +# Copyright (c) 2021-2025 TileDB, Inc. # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal @@ -43,6 +43,7 @@ add_subdirectory(algorithm) add_subdirectory(dynamic_memory) add_subdirectory(evaluator) add_subdirectory(exception) +add_subdirectory(filesystem) add_subdirectory(governor) add_subdirectory(interval) add_subdirectory(random) diff --git a/tiledb/common/filesystem/CMakeLists.txt b/tiledb/common/filesystem/CMakeLists.txt new file mode 100644 index 00000000000..6bf072ed8a8 --- /dev/null +++ b/tiledb/common/filesystem/CMakeLists.txt @@ -0,0 +1,33 @@ +# +# tiledb/common/filesystem/CMakeLists.txt +# +# The MIT License +# +# Copyright (c) 2025 TileDB, Inc. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +include(common NO_POLICY_SCOPE) +include(object_library) + +commence(object_library home_directory) + this_target_sources(home_directory.cc) + this_target_link_libraries(export) + this_target_object_libraries(baseline) +conclude(object_library) diff --git a/tiledb/common/filesystem/home_directory.cc b/tiledb/common/filesystem/home_directory.cc new file mode 100644 index 00000000000..4d0daad4eb8 --- /dev/null +++ b/tiledb/common/filesystem/home_directory.cc @@ -0,0 +1,75 @@ +/** + * @file home_directory.cc + * + * @section LICENSE + * + * The MIT License + * + * @copyright Copyright (c) 2025 TileDB, Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * @section DESCRIPTION + * + * This file implements class HomeDirectory and function home_directory(). + */ + +#include "home_directory.h" + +#include +#include + +namespace tiledb::common::filesystem { + +/* ********************************* */ +/* API */ +/* ********************************* */ + +std::optional home_directory() { + return HomeDirectory().path(); +} + +/* ********************************* */ +/* HomeDirectory */ +/* ********************************* */ + +HomeDirectory::HomeDirectory() + : path_(std::nullopt) { +#ifdef _WIN32 + wchar_t home[MAX_PATH]; + if (SHGetFolderPathW(NULL, CSIDL_PROFILE, NULL, 0, home) == S_OK) { + path_ = home; + } +#else + const char* home = std::getenv("HOME"); + if (home != nullptr) { + path_ = home; + } +#endif + // Remove trailing slash, if exists. + if (path_.has_value() && path_.value().back() == '/') { + path_.value().pop_back(); + } +} + +std::optional HomeDirectory::path() { + return path_; +} + +} // namespace tiledb::common::filesystem diff --git a/tiledb/common/filesystem/home_directory.h b/tiledb/common/filesystem/home_directory.h new file mode 100644 index 00000000000..ef17434eda4 --- /dev/null +++ b/tiledb/common/filesystem/home_directory.h @@ -0,0 +1,74 @@ +/** + * @file home_directory.h + * + * @section LICENSE + * + * The MIT License + * + * @copyright Copyright (c) 2025 TileDB, Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * @section DESCRIPTION + * + * This file defines class HomeDirectory and function home_directory(). + */ + +#ifndef TILEDB_HOME_DIRECTORY_H +#define TILEDB_HOME_DIRECTORY_H + +#include +#include + +namespace tiledb::common::filesystem { + +/** Standalone function which returns the path to user's home directory. */ +std::optional home_directory(); + +class HomeDirectory { + public: + /* ********************************* */ + /* CONSTRUCTORS & DESTRUCTORS */ + /* ********************************* */ + + /** Constructor. */ + HomeDirectory(); + + /** Destructor. */ + ~HomeDirectory() = default; + + /* ********************************* */ + /* API */ + /* ********************************* */ + + /** Return the path of the home directory. */ + std::optional path(); + + private: + /* ********************************* */ + /* PRIVATE ATTRIBUTES */ + /* ********************************* */ + + /** The path of the home directory. */ + std::optional path_; +}; + +} // namespace tiledb::common::filesystem + +#endif // TILEDB_HOME_DIRECTORY_H diff --git a/tiledb/common/filesystem/test/compile_home_directory_main.cc b/tiledb/common/filesystem/test/compile_home_directory_main.cc new file mode 100644 index 00000000000..0a1b1cb02f9 --- /dev/null +++ b/tiledb/common/filesystem/test/compile_home_directory_main.cc @@ -0,0 +1,36 @@ +/** + * @file compile_home_directory_main.cc + * + * @section LICENSE + * + * The MIT License + * + * @copyright Copyright (c) 2025 TileDB, Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#include "../home_directory.h" + +using namespace tiledb::common::filesystem; + +int main() { + (void)home_directory(); + return 0; +} diff --git a/tiledb/sm/rest/CMakeLists.txt b/tiledb/sm/rest/CMakeLists.txt index c830d7c1fda..2f0d544f251 100644 --- a/tiledb/sm/rest/CMakeLists.txt +++ b/tiledb/sm/rest/CMakeLists.txt @@ -3,7 +3,7 @@ # # The MIT License # -# Copyright (c) 2024 TileDB, Inc. +# Copyright (c) 2024-2025 TileDB, Inc. # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal @@ -26,12 +26,20 @@ include(common NO_POLICY_SCOPE) include(object_library) +# +# Object library `rest_profile` +# +commence(object_library rest_profile) + this_target_sources(rest_profile.cc) + this_target_object_libraries(config home_directory seedable_global_PRNG) +conclude(object_library) + # # Object library `rest_client` # commence(object_library rest_client) this_target_sources(rest_client.cc) - this_target_object_libraries(config) + this_target_object_libraries(rest_profile) conclude(object_library) # @@ -39,4 +47,6 @@ conclude(object_library) # # This object library does not link standalone at present. As long as the # cyclic dependencies in `class RestClient` persist, that won't be possible. -# \ No newline at end of file +# + +add_test_subdirectory() diff --git a/tiledb/sm/rest/rest_profile.cc b/tiledb/sm/rest/rest_profile.cc new file mode 100644 index 00000000000..6488530cb6f --- /dev/null +++ b/tiledb/sm/rest/rest_profile.cc @@ -0,0 +1,230 @@ +/** + * @file rest_profile.cc + * + * @section LICENSE + * + * The MIT License + * + * @copyright Copyright (c) 2025 TileDB, Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * @section DESCRIPTION + * + * This file implements class RestProfile. + */ + +#include + +#include "rest_profile.h" +#include "tiledb/common/random/random_label.h" +#include "tiledb/sm/misc/constants.h" + +using namespace tiledb::common; +using namespace tiledb::common::filesystem; + +namespace tiledb::sm { + +/* ****************************** */ +/* PARAMETER DEFAULTS */ +/* ****************************** */ + +const std::string RestProfile::DEFAULT_NAME{"default"}; +const std::string RestProfile::DEFAULT_PASSWORD{""}; +const std::string RestProfile::DEFAULT_PAYER_NAMESPACE{""}; +const std::string RestProfile::DEFAULT_TOKEN{""}; +const std::string RestProfile::DEFAULT_SERVER_ADDRESS{"https://api.tiledb.com"}; +const std::string RestProfile::DEFAULT_USERNAME{""}; + +const std::map default_values = { + std::make_pair("rest.password", RestProfile::DEFAULT_PASSWORD), + std::make_pair( + "rest.payer_namespace", RestProfile::DEFAULT_PAYER_NAMESPACE), + std::make_pair("rest.token", RestProfile::DEFAULT_TOKEN), + std::make_pair("rest.server_address", RestProfile::DEFAULT_SERVER_ADDRESS), + std::make_pair("rest.username", RestProfile::DEFAULT_USERNAME)}; + +/* ****************************** */ +/* CONSTRUCTORS & DESTRUCTORS */ +/* ****************************** */ + +RestProfile::RestProfile(std::string name) + : version_(1) + , name_(name) + , homedir_(home_directory().has_value() ? home_directory().value() : "") + , filepath_(homedir_ + "/.tiledb/profiles.json") + , old_filepath_(homedir_ + "/.tiledb/cloud.json") + , param_values_(default_values) { + // Ensure the user's $HOME is found + if (homedir_.empty()) { + throw RestProfileException( + "Failed to create RestProfile; $HOME is not set."); + } + + // Fstream cannot create directories. If `homedir_/.tiledb/` DNE, create it. + if (!std::filesystem::exists(homedir_ + "/.tiledb")) { + std::filesystem::create_directory(homedir_ + "/.tiledb"); + } + + // If the local file exists, load the profile with the given name. + if (std::filesystem::exists(filepath_)) { + load_from_json_file(filepath_); + } else { + // If the old version of the file exists, load the profile from there + if (std::filesystem::exists(old_filepath_)) { + load_from_json_file(old_filepath_); + } + } +} + +/* ****************************** */ +/* API */ +/* ****************************** */ + +void RestProfile::set(const std::string& param, const std::string& value) { + // Validate incoming parameter name + if (default_values.count(param) == 0) { + throw RestProfileException( + "Failed to set parameter of invalid name \'" + param + "\'"); + } + param_values_[param] = value; +} + +std::string RestProfile::get(const std::string& param) const { + auto it = param_values_.find(param); + if (it == param_values_.end()) { + throw RestProfileException( + "Failed to retrieve parameter \'" + param + "\'"); + } + return it->second; +} + +/** + * @note The `version_` will always be listed toward the end of the local file. + * `nlohmann::json` does not preserve the structure of the original, top-level + * json object, but rather sorts its elements alphabetically. + * See issue [#727](https://github.com/nlohmann/json/issues/727) for details. + */ +void RestProfile::save() { + // Validate that the profile is complete (if username is set, so is password) + if ((param_values_["rest.username"] != RestProfile::DEFAULT_USERNAME && + param_values_["rest.password"] == RestProfile::DEFAULT_PASSWORD) || + (param_values_["rest.username"] == RestProfile::DEFAULT_USERNAME && + param_values_["rest.password"] != RestProfile::DEFAULT_PASSWORD)) { + throw RestProfileException( + "Failed to save RestProfile; invalid username/password pairing."); + } + + // If the file already exists, load it into a json object. + json data; + std::fstream file; + std::string original_filepath = filepath_; + if (std::filesystem::exists(filepath_)) { + // Temporarily append filename with a random label to guarantee atomicity. + filepath_ = filepath_ + random_label(); + if (std::rename(original_filepath.c_str(), filepath_.c_str()) != 0) { + throw RestProfileException( + "Failed to save RestProfile due to an internal error."); + } + + // Read the file into the json object. + file.open(filepath_, std::ofstream::in); + file >> data; + file.close(); + + // If the file is outdated, throw an error. This behavior will evolve. + if (data["version"] < version_) { + throw RestProfileException( + "The version of your local profile.json file is out of date."); + } + + // If a profile of the given name already exists, remove it. + if (data.contains(name_)) { + data.erase(name_); + } + } else { + // Write the version number iff this is the first time opening the file. + data.push_back(json::object_t::value_type("version", version_)); + } + + // Add this profile to the json object. + data.push_back(json::object_t::value_type(name_, to_json())); + + // Write to the file, which will be created if it does not yet exist. + file.open(filepath_, std::ofstream::out); + file << std::setw(2) << data << std::endl; + file.close(); + + // Remove the random label from the filename, if applicable. + if (strcmp(filepath_.c_str(), original_filepath.c_str()) != 0) { + if (std::rename(filepath_.c_str(), original_filepath.c_str()) != 0) { + throw RestProfileException( + "Failed to save RestProfile due to an internal error."); + } + } +} + +void RestProfile::remove() { + std::string original_filepath = filepath_; + if (std::filesystem::exists(filepath_)) { + // Temporarily append filename with a random label to guarantee atomicity. + filepath_ = filepath_ + random_label(); + if (std::rename(original_filepath.c_str(), filepath_.c_str()) != 0) { + throw RestProfileException( + "Failed to remove RestProfile due to an internal error."); + } + + // Read the file into a json object. + json data; + std::fstream file; + file.open(filepath_, std::ofstream::in); + file >> data; + file.close(); + + // If a profile of the given name exists, remove it. + data.erase(data.find(name_)); + + // Write the json back to the file. + file.open(filepath_, std::ofstream::out); + file << std::setw(2) << data << std::endl; + file.close(); + } + + // Remove the random label from the filename, if applicable. + if (strcmp(filepath_.c_str(), original_filepath.c_str()) != 0) { + if (std::rename(filepath_.c_str(), original_filepath.c_str()) != 0) { + throw RestProfileException( + "Failed to remove RestProfile due to an internal error."); + } + } +} + +json RestProfile::to_json() { + json j; + for (const auto& param : param_values_) { + j[param.first] = param.second; + } + return j; +} + +std::string RestProfile::dump() { + return json{{name_, to_json()}}.dump(2); +} + +} // namespace tiledb::sm diff --git a/tiledb/sm/rest/rest_profile.h b/tiledb/sm/rest/rest_profile.h new file mode 100644 index 00000000000..a6983c9549b --- /dev/null +++ b/tiledb/sm/rest/rest_profile.h @@ -0,0 +1,219 @@ +/** + * @file rest_profile.h + * + * @section LICENSE + * + * The MIT License + * + * @copyright Copyright (c) 2025 TileDB, Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * @section DESCRIPTION + * + * This file defines class RestProfile. + */ + +#ifndef TILEDB_REST_PROFILE_H +#define TILEDB_REST_PROFILE_H + +#include +#include +#include +#include + +#include "external/include/nlohmann/json.hpp" +#include "tiledb/common/exception/exception.h" +#include "tiledb/common/filesystem/home_directory.h" + +using json = nlohmann::json; +using namespace tiledb::common; + +namespace tiledb::sm { + +class RestProfileException : public StatusException { + public: + explicit RestProfileException(const std::string& message) + : StatusException("RestProfile", message) { + } +}; + +class RestProfile { + /** Make the internals of class RestProfile available to class Config. */ + friend class Config; + + public: + /* ****************************** */ + /* PARAMETER DEFAULTS */ + /* ****************************** */ + + /** The default name of a RestProfile. */ + static const std::string DEFAULT_NAME; + + /** The user's REST password. */ + static const std::string DEFAULT_PASSWORD; + + /** The namespace that should be charged for the request. */ + static const std::string DEFAULT_PAYER_NAMESPACE; + + /** The user's REST token. */ + static const std::string DEFAULT_TOKEN; + + /** The default address for REST server. */ + static const std::string DEFAULT_SERVER_ADDRESS; + + /** The user's REST username. */ + static const std::string DEFAULT_USERNAME; + + /* ********************************* */ + /* CONSTRUCTORS & DESTRUCTORS */ + /* ********************************* */ + + /** + * Constructor. + * + * @param name The name of the RestProfile. Defaulted to "default". + */ + RestProfile(std::string name = RestProfile::DEFAULT_NAME); + + /** Destructor. */ + ~RestProfile() = default; + + /* ****************************** */ + /* API */ + /* ****************************** */ + + inline std::string name() { + return name_; + } + + /** + * Sets the given parameter to the given value. + * + * @param param The parameter to set. + * @param value The value to set on the given parameter. + */ + void set(const std::string& param, const std::string& value); + + /** + * Retrieves the value of the given parameter. + * + * @param param The parameter to fetch. + * @return The value of the given parameter. + */ + std::string get(const std::string& param) const; + + /** Saves this profile to the local file. */ + void save(); + + /** Removes this profile from the local file. */ + void remove(); + + /** + * Exports this profile's parameters and their values to a json object. + * + * @return A json object of this RestProfile's parameter : value mapping. + */ + json to_json(); + + /** + * Dumps the parameter : value mapping in json object format. + * + * @return This RestProfile's parameter : value mapping in json object format. + */ + std::string dump(); + + private: + /* ********************************* */ + /* PRIVATE API */ + /* ********************************* */ + + /** Loads the profile parameters from the local json file, if present. */ + inline void load_from_json_file(const std::string& filename) { + if (filename.empty() || + (strcmp(filename.c_str(), filepath_.c_str()) != 0 && + strcmp(filename.c_str(), old_filepath_.c_str()) != 0)) { + throw RestProfileException("Cannot load from file; invalid filename."); + } + + if (!std::filesystem::exists(filename)) { + throw RestProfileException("Cannot load from file; file does not exist."); + } + + // Load the file into a json object. + std::ifstream file(filename); + json data; + try { + file >> data; + } catch (...) { + throw RestProfileException("Error parsing json file."); + } + + // If possible, load (overwrite) the parameters from the local file + if (strcmp(filename.c_str(), old_filepath_.c_str()) == 0) { + // Parse the old file and load the parameters + if (data.contains("api_key") && + data["api_key"].contains("X-TILEDB-REST-API-KEY")) { + param_values_["rest.token"] = data["api_key"]["X-TILEDB-REST-API-KEY"]; + } + if (data.contains("host")) { + param_values_["rest.server_address"] = data["host"]; + } + if (data.contains("password")) { + param_values_["rest.password"] = data["password"]; + } + if (data.contains("username")) { + param_values_["rest.username"] = data["username"]; + } + } else { + json profile = data[name_]; + if (!profile.is_null()) { + for (auto it = profile.begin(); it != profile.end(); ++it) { + param_values_[it.key()] = profile[it.key()]; + } + } + } + } + + /* ********************************* */ + /* PRIVATE ATTRIBUTES */ + /* ********************************* */ + + /** The version of this class. */ + int version_; + + /** The name of this RestProfile. */ + std::string name_; + + /** The path to the local $HOME directory. */ + std::string homedir_; + + /** The path to the local file which stores all profiles. */ + std::string filepath_; + + /** The path to the old local file which previously stored a profile. */ + std::string old_filepath_; + + /** Stores a map of for the set-parameters. */ + std::map param_values_; +}; + +} // namespace tiledb::sm + +#endif // TILEDB_REST_PROFILE_H diff --git a/tiledb/sm/rest/test/CMakeLists.txt b/tiledb/sm/rest/test/CMakeLists.txt new file mode 100644 index 00000000000..2a6acce5be2 --- /dev/null +++ b/tiledb/sm/rest/test/CMakeLists.txt @@ -0,0 +1,32 @@ +# +# tiledb/sm/rest/test/CMakeLists.txt +# +# The MIT License +# +# Copyright (c) 2025 TileDB, Inc. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +# + +include(unit_test) + +commence(unit_test rest_profile) + this_target_sources(main.cc unit_rest_profile.cc) + this_target_link_libraries(rest_profile tiledb_test_support_lib) +conclude(unit_test) diff --git a/tiledb/sm/rest/test/compile_rest_profile_main.cc b/tiledb/sm/rest/test/compile_rest_profile_main.cc new file mode 100644 index 00000000000..c0911156a83 --- /dev/null +++ b/tiledb/sm/rest/test/compile_rest_profile_main.cc @@ -0,0 +1,34 @@ +/** + * @file tiledb/sm/rest/test/compile_rest_profile_main.cc + * + * @section LICENSE + * + * The MIT License + * + * @copyright Copyright (c) 2025 TileDB, Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#include "../rest_profile.h" + +int main() { + [[maybe_unused]] tiledb::sm::RestProfile x{}; + return 0; +} diff --git a/tiledb/sm/rest/test/main.cc b/tiledb/sm/rest/test/main.cc new file mode 100644 index 00000000000..21ecba0ab39 --- /dev/null +++ b/tiledb/sm/rest/test/main.cc @@ -0,0 +1,34 @@ +/** + * @file tiledb/sm/rest/test/main.cc + * + * @section LICENSE + * + * The MIT License + * + * @copyright Copyright (c) 2025 TileDB, Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * @section DESCRIPTION + * + * This file defines a test `main()` + */ + +#define CATCH_CONFIG_MAIN +#include diff --git a/tiledb/sm/rest/test/unit_rest_profile.cc b/tiledb/sm/rest/test/unit_rest_profile.cc new file mode 100644 index 00000000000..5ac7243651e --- /dev/null +++ b/tiledb/sm/rest/test/unit_rest_profile.cc @@ -0,0 +1,309 @@ +/** + * @file unit_rest_profile.cc + * + * @section LICENSE + * + * The MIT License + * + * @copyright Copyright (c) 2025 TileDB, Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * @section DESCRIPTION + * + * Tests the `RestProfile` class. + */ + +#include "test/support/src/temporary_local_directory.h" +#include "tiledb/sm/rest/rest_profile.h" + +#include +#include + +using namespace tiledb::common; +using namespace tiledb::sm; + +std::string cloudtoken_ = "abc123def456"; // Token used by in-test cloud.json. + +/* Tracks the expected name and parameter values for a RestProfile. */ +struct expected_values_t { + std::string name = RestProfile::DEFAULT_NAME; + std::string password = RestProfile::DEFAULT_PASSWORD; + std::string payer_namespace = RestProfile::DEFAULT_PAYER_NAMESPACE; + std::string token = cloudtoken_; + std::string server_address = RestProfile::DEFAULT_SERVER_ADDRESS; + std::string username = RestProfile::DEFAULT_USERNAME; +}; + +struct RestProfileFx { + public: + std::string homedir_; // The user's $HOME directory. + TemporaryLocalDirectory tempdir_; // A temporary working directory. + std::string cloudpath_; // The in-test path to the cloud.json file. + + /** + * Constructor which changes the user's `$HOME` to a `TemporaryLocalDirectory` + * for the duration of the test. This ensures that no in-test changes are + * made to the local, user-sensitive files `profiles.json` and `cloud.json`. + */ + RestProfileFx() + : homedir_(tiledb::common::filesystem::home_directory().value()) + , tempdir_(TemporaryLocalDirectory("unit_rest_profile")) + , cloudpath_(tempdir_.path() + ".tiledb/cloud.json") { + if (setenv("HOME", tempdir_.path().c_str(), 1) != 0) { + std::cerr << "[ERROR] Failed to set HOME environment variable." + << std::endl; + return; + } + + // Fstream cannot create directories, only files, so create the .tiledb dir. + std::filesystem::create_directory(tempdir_.path() + ".tiledb"); + + // Write to the cloudpath_. + json j = { + {"api_key", + {json::object_t::value_type("X-TILEDB-REST-API-KEY", cloudtoken_)}}, + {"host", RestProfile::DEFAULT_SERVER_ADDRESS}, + {"username", RestProfile::DEFAULT_USERNAME}, + {"password", RestProfile::DEFAULT_PASSWORD}, + {"verify_ssl", "true"}}; + std::ofstream file(cloudpath_); + file << std::setw(2) << j << std::endl; + file.close(); + } + + /** Destructor. */ + ~RestProfileFx() { + // Reset $HOME env variable back to its original value + if (setenv("HOME", homedir_.c_str(), 1) != 0) { + std::cerr << "[ERROR] Failed to reset HOME environment variable." + << std::endl; + return; + } + } + + /** Returns true iff the profile's parameter values match the expected. */ + bool is_valid(RestProfile p, expected_values_t e) { + if (p.name() != e.name) + return false; + if (p.get("rest.password") != e.password) + return false; + if (p.get("rest.payer_namespace") != e.payer_namespace) + return false; + if (p.get("rest.token") != e.token) + return false; + if (p.get("rest.server_address") != e.server_address) + return false; + if (p.get("rest.username") != e.username) + return false; + return true; + } + + /** + * Returns the RestProfile at the given name from the local file, + * as a json object. + */ + json profile_from_file_to_json(std::string name) { + json data; + std::string filepath = tempdir_.path() + ".tiledb/profiles.json"; + if (std::filesystem::exists(filepath)) { + std::ifstream file(filepath); + file >> data; + file.close(); + return data[name]; + } else { + return data; + } + } +}; + +TEST_CASE_METHOD( + RestProfileFx, "REST Profile: Default profile", "[rest_profile][default]") { + // Remove cloud.json to ensure the RestProfile constructor doesn't inherit it. + std::filesystem::remove(cloudpath_); + + // Create and validate a default RestProfile. + RestProfile profile; + profile.save(); + + // Set the expected token value; expected_values_t uses cloudpath_ by default. + expected_values_t expected; + expected.token = RestProfile::DEFAULT_TOKEN; + CHECK(is_valid(profile, expected)); +} + +TEST_CASE_METHOD( + RestProfileFx, + "REST Profile: Default profile inherited from cloudpath", + "[rest_profile][default][inherited]") { + // Create and validate a default RestProfile. + RestProfile profile; + profile.save(); + expected_values_t expected; + CHECK(is_valid(profile, expected)); +} + +TEST_CASE_METHOD( + RestProfileFx, + "REST Profile: Save/Remove", + "[rest_profile][save][remove]") { + // Create a default RestProfile. + RestProfile p; + CHECK(profile_from_file_to_json(p.name()).empty()); + + // Save and validate. + p.save(); + expected_values_t e; + CHECK(is_valid(p, e)); + CHECK(!profile_from_file_to_json(p.name()).empty()); + + // Remove the profile and validate that the local json object is removed. + p.remove(); + CHECK(profile_from_file_to_json(p.name()).empty()); +} + +TEST_CASE_METHOD( + RestProfileFx, + "REST Profile: Non-default profile", + "[rest_profile][non-default]") { + expected_values_t e{ + "non-default", + "password", + "payer_namespace", + "token", + "server_address", + "username"}; + + // Set and validate non-default parameters. + RestProfile p(e.name); + p.set("rest.password", e.password); + p.set("rest.payer_namespace", e.payer_namespace); + p.set("rest.token", e.token); + p.set("rest.server_address", e.server_address); + p.set("rest.username", e.username); + p.save(); + CHECK(is_valid(p, e)); +} + +TEST_CASE_METHOD( + RestProfileFx, "REST Profile: to_json", "[rest_profile][to_json]") { + // Create a default RestProfile. + RestProfile p; + p.save(); + + // Validate. + expected_values_t e; + json j = p.to_json(); + CHECK(j["rest.password"] == e.password); + CHECK(j["rest.payer_namespace"] == e.payer_namespace); + CHECK(j["rest.token"] == e.token); + CHECK(j["rest.server_address"] == e.server_address); + CHECK(j["rest.username"] == e.username); +} + +TEST_CASE_METHOD( + RestProfileFx, + "REST Profile: Get/Set invalid parameters", + "[rest_profile][get_set_invalid]") { + RestProfile p; + + // Try to get a parameter with an invalid name. + REQUIRE_THROWS_WITH( + p.get("username"), + Catch::Matchers::ContainsSubstring("Failed to retrieve parameter")); + + // Try to set a parameter with an invalid name. + REQUIRE_THROWS_WITH( + p.set("username", "failed_username"), + Catch::Matchers::ContainsSubstring( + "Failed to set parameter of invalid name")); + + // Set username and try to save without setting password. + p.set("rest.username", "username"); + REQUIRE_THROWS_WITH( + p.save(), + Catch::Matchers::ContainsSubstring("invalid username/password pairing")); + // Set password and save valid profile + p.set("rest.password", "password"); + p.save(); + + // Validate. + expected_values_t e; + e.username = "username"; + e.password = "password"; + CHECK(is_valid(p, e)); +} + +TEST_CASE_METHOD( + RestProfileFx, + "REST Profile: Multiple profiles, same name", + "[rest_profile][multiple][same_name]") { + std::string payer_namespace = "payer_namespace"; + std::string token = "token"; + + // Create and validate a RestProfile with default name. + RestProfile p; + p.set("rest.payer_namespace", payer_namespace); + p.save(); + expected_values_t e; + e.payer_namespace = payer_namespace; + CHECK(is_valid(p, e)); + + // Create a second profile, ensuring the payer_namespace is inherited. + RestProfile p2; + CHECK(p2.get("rest.payer_namespace") == payer_namespace); + + // Set a non-default token on the second profile. + p2.set("rest.token", token); + p2.save(); + e.token = token; + CHECK(is_valid(p2, e)); + + // Ensure the first profile is now out of date. + CHECK(p.get("rest.token") == cloudtoken_); +} + +TEST_CASE_METHOD( + RestProfileFx, + "REST Profile: Multiple profiles, different name", + "[rest_profile][multiple][different_name]") { + std::string name = "non-default"; + std::string payer_namespace = "payer_namespace"; + + // Create and validate a RestProfile with default name. + RestProfile p; + p.set("rest.payer_namespace", payer_namespace); + p.save(); + expected_values_t e; + e.payer_namespace = payer_namespace; + CHECK(is_valid(p, e)); + + // Create a second profile with non-default name and ensure the + // payer_namespace and cloudtoken_ are NOT inherited. + RestProfile p2(name); + CHECK(p2.get("rest.payer_namespace") != payer_namespace); + p2.save(); + e.name = name; + e.payer_namespace = RestProfile::DEFAULT_PAYER_NAMESPACE; + e.token = RestProfile::DEFAULT_TOKEN; + CHECK(is_valid(p2, e)); + + // Ensure the default profile is unchanged. + CHECK(p.name() == RestProfile::DEFAULT_NAME); +} From 625ffd0b1015bd33ca294717c8fcb29012e1c371 Mon Sep 17 00:00:00 2001 From: Theodore Tsirpanis Date: Thu, 20 Feb 2025 23:51:30 +0200 Subject: [PATCH 2/8] Use `SHGetKnownFolderPath`. --- tiledb/common/filesystem/home_directory.cc | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/tiledb/common/filesystem/home_directory.cc b/tiledb/common/filesystem/home_directory.cc index 4d0daad4eb8..24d290c8ebd 100644 --- a/tiledb/common/filesystem/home_directory.cc +++ b/tiledb/common/filesystem/home_directory.cc @@ -30,8 +30,13 @@ * This file implements class HomeDirectory and function home_directory(). */ +#include +#include + #include "home_directory.h" +#include "tiledb/common/scoped_executor.h" +#include #include #include @@ -52,9 +57,13 @@ std::optional home_directory() { HomeDirectory::HomeDirectory() : path_(std::nullopt) { #ifdef _WIN32 - wchar_t home[MAX_PATH]; - if (SHGetFolderPathW(NULL, CSIDL_PROFILE, NULL, 0, home) == S_OK) { - path_ = home; + wchar_t* home; + auto _ = ScopedExecutor([&]() { + if (home) + CoTaskMemFree(home); + }); + if (SHGetKnownFolderPath(FOLDERID_Profile, 0, NULL, &home) == S_OK) { + path_ = std::wstring_convert>{}.to_bytes(home); } #else const char* home = std::getenv("HOME"); From 0c97dc7529f15dee0af77398ba738f04eff14ec9 Mon Sep 17 00:00:00 2001 From: Theodore Tsirpanis Date: Thu, 20 Feb 2025 23:57:37 +0200 Subject: [PATCH 3/8] Add missing ifdefs. --- tiledb/common/filesystem/home_directory.cc | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/tiledb/common/filesystem/home_directory.cc b/tiledb/common/filesystem/home_directory.cc index 24d290c8ebd..c597eaf3f21 100644 --- a/tiledb/common/filesystem/home_directory.cc +++ b/tiledb/common/filesystem/home_directory.cc @@ -30,13 +30,16 @@ * This file implements class HomeDirectory and function home_directory(). */ +#ifdef _WIN32 #include #include +#include +#endif + #include "home_directory.h" #include "tiledb/common/scoped_executor.h" -#include #include #include From 1facd99b020a070dea670d5edfe645b8713a40ae Mon Sep 17 00:00:00 2001 From: Rebekah Davis Date: Thu, 20 Feb 2025 17:21:35 -0500 Subject: [PATCH 4/8] Address comments. --- tiledb/common/filesystem/home_directory.h | 7 +- tiledb/sm/misc/constants.cc | 5 +- tiledb/sm/misc/constants.h | 5 +- tiledb/sm/rest/rest_profile.cc | 107 ++++++++++++++++------ tiledb/sm/rest/rest_profile.h | 50 +--------- tiledb/sm/rest/test/unit_rest_profile.cc | 20 ++-- 6 files changed, 105 insertions(+), 89 deletions(-) diff --git a/tiledb/common/filesystem/home_directory.h b/tiledb/common/filesystem/home_directory.h index ef17434eda4..8f1a1c4669f 100644 --- a/tiledb/common/filesystem/home_directory.h +++ b/tiledb/common/filesystem/home_directory.h @@ -38,7 +38,12 @@ namespace tiledb::common::filesystem { -/** Standalone function which returns the path to user's home directory. */ +/** + * Standalone function which returns the path to user's home directory. + * + * @invariant `sudo` does not always preserve the path to `$HOME`. Rather than + * throw if the path does not exist, this API will return `std::nullopt`. + */ std::optional home_directory(); class HomeDirectory { diff --git a/tiledb/sm/misc/constants.cc b/tiledb/sm/misc/constants.cc index 57917dc30c3..7d8b7561728 100644 --- a/tiledb/sm/misc/constants.cc +++ b/tiledb/sm/misc/constants.cc @@ -5,7 +5,7 @@ * * The MIT License * - * @copyright Copyright (c) 2017-2024 TileDB, Inc. + * @copyright Copyright (c) 2017-2025 TileDB, Inc. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -775,6 +775,9 @@ extern const std::string redirection_header_key = "location"; /** The config key prefix for REST custom headers. */ const std::string rest_header_prefix = "rest.custom_headers."; +/** The current RestProfile API version. */ +const format_version_t rest_profile_version = 1; + /** String describing MIME_AUTODETECT. */ const std::string mime_autodetect_str = "AUTODETECT"; diff --git a/tiledb/sm/misc/constants.h b/tiledb/sm/misc/constants.h index 5a854d404b8..aac847b29cd 100644 --- a/tiledb/sm/misc/constants.h +++ b/tiledb/sm/misc/constants.h @@ -5,7 +5,7 @@ * * The MIT License * - * @copyright Copyright (c) 2017-2024 TileDB, Inc. + * @copyright Copyright (c) 2017-2025 TileDB, Inc. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -763,6 +763,9 @@ extern const std::string redirection_header_key; /** The REST custom headers config key prefix. */ extern const std::string rest_header_prefix; +/** The current RestProfile API version. */ +extern const format_version_t rest_profile_version; + /** Delimiter for lists passed as config parameter */ extern const std::string config_delimiter; diff --git a/tiledb/sm/rest/rest_profile.cc b/tiledb/sm/rest/rest_profile.cc index 6488530cb6f..1c990301b04 100644 --- a/tiledb/sm/rest/rest_profile.cc +++ b/tiledb/sm/rest/rest_profile.cc @@ -64,23 +64,29 @@ const std::map default_values = { /* CONSTRUCTORS & DESTRUCTORS */ /* ****************************** */ -RestProfile::RestProfile(std::string name) - : version_(1) +RestProfile::RestProfile(const std::string& name) + : version_(constants::rest_profile_version) , name_(name) , homedir_(home_directory().has_value() ? home_directory().value() : "") , filepath_(homedir_ + "/.tiledb/profiles.json") , old_filepath_(homedir_ + "/.tiledb/cloud.json") , param_values_(default_values) { - // Ensure the user's $HOME is found + /** + * Ensure the user's $HOME is found. + * There's an edge case in which `sudo` does not always preserve the path to + * `$HOME`. In this case, the home_directory() API does not throw, but instead + * returns `std::nullopt`. As such, we can check for a value in the returned + * `std::optional` path of the home_directory and throw an error to the user + * accordingly, so they may decide the proper course of action: set the + * $HOME path, or perhaps stop using `sudo`. + */ if (homedir_.empty()) { throw RestProfileException( "Failed to create RestProfile; $HOME is not set."); } // Fstream cannot create directories. If `homedir_/.tiledb/` DNE, create it. - if (!std::filesystem::exists(homedir_ + "/.tiledb")) { - std::filesystem::create_directory(homedir_ + "/.tiledb"); - } + std::filesystem::create_directory(homedir_ + "/.tiledb"); // If the local file exists, load the profile with the given name. if (std::filesystem::exists(filepath_)) { @@ -123,17 +129,14 @@ std::string RestProfile::get(const std::string& param) const { */ void RestProfile::save() { // Validate that the profile is complete (if username is set, so is password) - if ((param_values_["rest.username"] != RestProfile::DEFAULT_USERNAME && - param_values_["rest.password"] == RestProfile::DEFAULT_PASSWORD) || - (param_values_["rest.username"] == RestProfile::DEFAULT_USERNAME && - param_values_["rest.password"] != RestProfile::DEFAULT_PASSWORD)) { + if ((param_values_["rest.username"] == RestProfile::DEFAULT_USERNAME) != + (param_values_["rest.password"] == RestProfile::DEFAULT_PASSWORD)) { throw RestProfileException( "Failed to save RestProfile; invalid username/password pairing."); } // If the file already exists, load it into a json object. json data; - std::fstream file; std::string original_filepath = filepath_; if (std::filesystem::exists(filepath_)) { // Temporarily append filename with a random label to guarantee atomicity. @@ -144,9 +147,10 @@ void RestProfile::save() { } // Read the file into the json object. - file.open(filepath_, std::ofstream::in); - file >> data; - file.close(); + { + std::ifstream file(filepath_); + file >> data; + } // If the file is outdated, throw an error. This behavior will evolve. if (data["version"] < version_) { @@ -167,12 +171,13 @@ void RestProfile::save() { data.push_back(json::object_t::value_type(name_, to_json())); // Write to the file, which will be created if it does not yet exist. - file.open(filepath_, std::ofstream::out); - file << std::setw(2) << data << std::endl; - file.close(); + { + std::ofstream file(filepath_); + file << std::setw(2) << data << std::endl; + } // Remove the random label from the filename, if applicable. - if (strcmp(filepath_.c_str(), original_filepath.c_str()) != 0) { + if (filepath_ != original_filepath) { if (std::rename(filepath_.c_str(), original_filepath.c_str()) != 0) { throw RestProfileException( "Failed to save RestProfile due to an internal error."); @@ -192,22 +197,23 @@ void RestProfile::remove() { // Read the file into a json object. json data; - std::fstream file; - file.open(filepath_, std::ofstream::in); - file >> data; - file.close(); + { + std::ifstream file(filepath_); + file >> data; + } // If a profile of the given name exists, remove it. data.erase(data.find(name_)); // Write the json back to the file. - file.open(filepath_, std::ofstream::out); - file << std::setw(2) << data << std::endl; - file.close(); + { + std::ofstream file(filepath_); + file << std::setw(2) << data << std::endl; + } } // Remove the random label from the filename, if applicable. - if (strcmp(filepath_.c_str(), original_filepath.c_str()) != 0) { + if (filepath_ != original_filepath) { if (std::rename(filepath_.c_str(), original_filepath.c_str()) != 0) { throw RestProfileException( "Failed to remove RestProfile due to an internal error."); @@ -227,4 +233,53 @@ std::string RestProfile::dump() { return json{{name_, to_json()}}.dump(2); } +/* ****************************** */ +/* PRIVATE API */ +/* ****************************** */ + +void RestProfile::load_from_json_file(const std::string& filename) { + if (filename.empty() || + (filename != filepath_ && filename != old_filepath_)) { + throw RestProfileException("Cannot load from file; invalid filename."); + } + + if (!std::filesystem::exists(filename)) { + throw RestProfileException("Cannot load from file; file does not exist."); + } + + // Load the file into a json object. + std::ifstream file(filename); + json data; + try { + file >> data; + } catch (...) { + throw RestProfileException("Error parsing json file."); + } + + // If possible, load (overwrite) the parameters from the local file + if (filename.c_str() == old_filepath_.c_str()) { + // Parse the old file and load the parameters + if (data.contains("api_key") && + data["api_key"].contains("X-TILEDB-REST-API-KEY")) { + param_values_["rest.token"] = data["api_key"]["X-TILEDB-REST-API-KEY"]; + } + if (data.contains("host")) { + param_values_["rest.server_address"] = data["host"]; + } + if (data.contains("password")) { + param_values_["rest.password"] = data["password"]; + } + if (data.contains("username")) { + param_values_["rest.username"] = data["username"]; + } + } else { + json profile = data[name_]; + if (!profile.is_null()) { + for (auto it = profile.begin(); it != profile.end(); ++it) { + param_values_[it.key()] = profile[it.key()]; + } + } + } +} + } // namespace tiledb::sm diff --git a/tiledb/sm/rest/rest_profile.h b/tiledb/sm/rest/rest_profile.h index a6983c9549b..696719920af 100644 --- a/tiledb/sm/rest/rest_profile.h +++ b/tiledb/sm/rest/rest_profile.h @@ -90,7 +90,7 @@ class RestProfile { * * @param name The name of the RestProfile. Defaulted to "default". */ - RestProfile(std::string name = RestProfile::DEFAULT_NAME); + RestProfile(const std::string& name = RestProfile::DEFAULT_NAME); /** Destructor. */ ~RestProfile() = default; @@ -145,58 +145,14 @@ class RestProfile { /* ********************************* */ /** Loads the profile parameters from the local json file, if present. */ - inline void load_from_json_file(const std::string& filename) { - if (filename.empty() || - (strcmp(filename.c_str(), filepath_.c_str()) != 0 && - strcmp(filename.c_str(), old_filepath_.c_str()) != 0)) { - throw RestProfileException("Cannot load from file; invalid filename."); - } - - if (!std::filesystem::exists(filename)) { - throw RestProfileException("Cannot load from file; file does not exist."); - } - - // Load the file into a json object. - std::ifstream file(filename); - json data; - try { - file >> data; - } catch (...) { - throw RestProfileException("Error parsing json file."); - } - - // If possible, load (overwrite) the parameters from the local file - if (strcmp(filename.c_str(), old_filepath_.c_str()) == 0) { - // Parse the old file and load the parameters - if (data.contains("api_key") && - data["api_key"].contains("X-TILEDB-REST-API-KEY")) { - param_values_["rest.token"] = data["api_key"]["X-TILEDB-REST-API-KEY"]; - } - if (data.contains("host")) { - param_values_["rest.server_address"] = data["host"]; - } - if (data.contains("password")) { - param_values_["rest.password"] = data["password"]; - } - if (data.contains("username")) { - param_values_["rest.username"] = data["username"]; - } - } else { - json profile = data[name_]; - if (!profile.is_null()) { - for (auto it = profile.begin(); it != profile.end(); ++it) { - param_values_[it.key()] = profile[it.key()]; - } - } - } - } + void load_from_json_file(const std::string& filename); /* ********************************* */ /* PRIVATE ATTRIBUTES */ /* ********************************* */ /** The version of this class. */ - int version_; + format_version_t version_; /** The name of this RestProfile. */ std::string name_; diff --git a/tiledb/sm/rest/test/unit_rest_profile.cc b/tiledb/sm/rest/test/unit_rest_profile.cc index 5ac7243651e..ce27ab87eb8 100644 --- a/tiledb/sm/rest/test/unit_rest_profile.cc +++ b/tiledb/sm/rest/test/unit_rest_profile.cc @@ -100,19 +100,13 @@ struct RestProfileFx { /** Returns true iff the profile's parameter values match the expected. */ bool is_valid(RestProfile p, expected_values_t e) { - if (p.name() != e.name) - return false; - if (p.get("rest.password") != e.password) - return false; - if (p.get("rest.payer_namespace") != e.payer_namespace) - return false; - if (p.get("rest.token") != e.token) - return false; - if (p.get("rest.server_address") != e.server_address) - return false; - if (p.get("rest.username") != e.username) - return false; - return true; + if (p.name() == e.name && p.get("rest.password") == e.password && + p.get("rest.payer_namespace") == e.payer_namespace && + p.get("rest.token") == e.token && + p.get("rest.server_address") == e.server_address && + p.get("rest.username") == e.username) + return true; + return false; } /** From 70a2ccfb24cff05bfcf882c9366b54fab9727c15 Mon Sep 17 00:00:00 2001 From: Rebekah Davis Date: Mon, 24 Feb 2025 14:33:20 -0500 Subject: [PATCH 5/8] Add constructor overload to take in homedir. --- tiledb/common/filesystem/home_directory.cc | 4 -- tiledb/sm/rest/rest_profile.cc | 18 ++++-- tiledb/sm/rest/rest_profile.h | 12 +++- tiledb/sm/rest/test/unit_rest_profile.cc | 74 ++++++++-------------- 4 files changed, 48 insertions(+), 60 deletions(-) diff --git a/tiledb/common/filesystem/home_directory.cc b/tiledb/common/filesystem/home_directory.cc index c597eaf3f21..950b1071d30 100644 --- a/tiledb/common/filesystem/home_directory.cc +++ b/tiledb/common/filesystem/home_directory.cc @@ -74,10 +74,6 @@ HomeDirectory::HomeDirectory() path_ = home; } #endif - // Remove trailing slash, if exists. - if (path_.has_value() && path_.value().back() == '/') { - path_.value().pop_back(); - } } std::optional HomeDirectory::path() { diff --git a/tiledb/sm/rest/rest_profile.cc b/tiledb/sm/rest/rest_profile.cc index 1c990301b04..201ba907c92 100644 --- a/tiledb/sm/rest/rest_profile.cc +++ b/tiledb/sm/rest/rest_profile.cc @@ -64,12 +64,11 @@ const std::map default_values = { /* CONSTRUCTORS & DESTRUCTORS */ /* ****************************** */ -RestProfile::RestProfile(const std::string& name) +RestProfile::RestProfile(const std::string& name, const std::string& homedir) : version_(constants::rest_profile_version) , name_(name) - , homedir_(home_directory().has_value() ? home_directory().value() : "") - , filepath_(homedir_ + "/.tiledb/profiles.json") - , old_filepath_(homedir_ + "/.tiledb/cloud.json") + , filepath_(homedir + ".tiledb/profiles.json") + , old_filepath_(homedir + ".tiledb/cloud.json") , param_values_(default_values) { /** * Ensure the user's $HOME is found. @@ -80,13 +79,13 @@ RestProfile::RestProfile(const std::string& name) * accordingly, so they may decide the proper course of action: set the * $HOME path, or perhaps stop using `sudo`. */ - if (homedir_.empty()) { + if (homedir.empty()) { throw RestProfileException( "Failed to create RestProfile; $HOME is not set."); } - // Fstream cannot create directories. If `homedir_/.tiledb/` DNE, create it. - std::filesystem::create_directory(homedir_ + "/.tiledb"); + // Fstream cannot create directories. If `homedir/.tiledb/` DNE, create it. + std::filesystem::create_directories(homedir + ".tiledb"); // If the local file exists, load the profile with the given name. if (std::filesystem::exists(filepath_)) { @@ -99,6 +98,11 @@ RestProfile::RestProfile(const std::string& name) } } +RestProfile::RestProfile(const std::string& name) + : RestProfile( + name, home_directory().has_value() ? home_directory().value() : "") { +} + /* ****************************** */ /* API */ /* ****************************** */ diff --git a/tiledb/sm/rest/rest_profile.h b/tiledb/sm/rest/rest_profile.h index 696719920af..6fdb3841cd7 100644 --- a/tiledb/sm/rest/rest_profile.h +++ b/tiledb/sm/rest/rest_profile.h @@ -92,6 +92,15 @@ class RestProfile { */ RestProfile(const std::string& name = RestProfile::DEFAULT_NAME); + /** + * Constructor. Intended for testing purposes only, to preserve the user's + * $HOME path and their profiles from in-test changes. + * + * @param name The name of the RestProfile. + * @param homedir The user's $HOME directory, or desired in-test path. + */ + RestProfile(const std::string& name, const std::string& homedir); + /** Destructor. */ ~RestProfile() = default; @@ -157,9 +166,6 @@ class RestProfile { /** The name of this RestProfile. */ std::string name_; - /** The path to the local $HOME directory. */ - std::string homedir_; - /** The path to the local file which stores all profiles. */ std::string filepath_; diff --git a/tiledb/sm/rest/test/unit_rest_profile.cc b/tiledb/sm/rest/test/unit_rest_profile.cc index ce27ab87eb8..d72e9a27b67 100644 --- a/tiledb/sm/rest/test/unit_rest_profile.cc +++ b/tiledb/sm/rest/test/unit_rest_profile.cc @@ -53,29 +53,16 @@ struct expected_values_t { struct RestProfileFx { public: - std::string homedir_; // The user's $HOME directory. - TemporaryLocalDirectory tempdir_; // A temporary working directory. - std::string cloudpath_; // The in-test path to the cloud.json file. + std::string homedir_; // The temporary in-test $HOME directory. + std::string cloudpath_; // The in-test path to the cloud.json file. - /** - * Constructor which changes the user's `$HOME` to a `TemporaryLocalDirectory` - * for the duration of the test. This ensures that no in-test changes are - * made to the local, user-sensitive files `profiles.json` and `cloud.json`. - */ RestProfileFx() - : homedir_(tiledb::common::filesystem::home_directory().value()) - , tempdir_(TemporaryLocalDirectory("unit_rest_profile")) - , cloudpath_(tempdir_.path() + ".tiledb/cloud.json") { - if (setenv("HOME", tempdir_.path().c_str(), 1) != 0) { - std::cerr << "[ERROR] Failed to set HOME environment variable." - << std::endl; - return; - } - + : homedir_(TemporaryLocalDirectory("unit_rest_profile").path()) + , cloudpath_(homedir_ + ".tiledb/cloud.json") { // Fstream cannot create directories, only files, so create the .tiledb dir. - std::filesystem::create_directory(tempdir_.path() + ".tiledb"); + std::filesystem::create_directories(homedir_ + ".tiledb"); - // Write to the cloudpath_. + // Write to the cloud path. json j = { {"api_key", {json::object_t::value_type("X-TILEDB-REST-API-KEY", cloudtoken_)}}, @@ -83,21 +70,15 @@ struct RestProfileFx { {"username", RestProfile::DEFAULT_USERNAME}, {"password", RestProfile::DEFAULT_PASSWORD}, {"verify_ssl", "true"}}; - std::ofstream file(cloudpath_); - file << std::setw(2) << j << std::endl; - file.close(); - } - /** Destructor. */ - ~RestProfileFx() { - // Reset $HOME env variable back to its original value - if (setenv("HOME", homedir_.c_str(), 1) != 0) { - std::cerr << "[ERROR] Failed to reset HOME environment variable." - << std::endl; - return; + { + std::ofstream file(cloudpath_); + file << std::setw(2) << j << std::endl; } } + ~RestProfileFx() = default; + /** Returns true iff the profile's parameter values match the expected. */ bool is_valid(RestProfile p, expected_values_t e) { if (p.name() == e.name && p.get("rest.password") == e.password && @@ -113,9 +94,8 @@ struct RestProfileFx { * Returns the RestProfile at the given name from the local file, * as a json object. */ - json profile_from_file_to_json(std::string name) { + json profile_from_file_to_json(std::string filepath, std::string name) { json data; - std::string filepath = tempdir_.path() + ".tiledb/profiles.json"; if (std::filesystem::exists(filepath)) { std::ifstream file(filepath); file >> data; @@ -133,10 +113,11 @@ TEST_CASE_METHOD( std::filesystem::remove(cloudpath_); // Create and validate a default RestProfile. - RestProfile profile; + RestProfile profile(RestProfile::DEFAULT_NAME, homedir_); profile.save(); - // Set the expected token value; expected_values_t uses cloudpath_ by default. + // Set the expected token value; expected_values_t uses cloudtoken_ by + // default. expected_values_t expected; expected.token = RestProfile::DEFAULT_TOKEN; CHECK(is_valid(profile, expected)); @@ -147,7 +128,7 @@ TEST_CASE_METHOD( "REST Profile: Default profile inherited from cloudpath", "[rest_profile][default][inherited]") { // Create and validate a default RestProfile. - RestProfile profile; + RestProfile profile(RestProfile::DEFAULT_NAME, homedir_); profile.save(); expected_values_t expected; CHECK(is_valid(profile, expected)); @@ -158,18 +139,19 @@ TEST_CASE_METHOD( "REST Profile: Save/Remove", "[rest_profile][save][remove]") { // Create a default RestProfile. - RestProfile p; - CHECK(profile_from_file_to_json(p.name()).empty()); + RestProfile p(RestProfile::DEFAULT_NAME, homedir_); + std::string filepath = homedir_ + ".tiledb/profiles.json"; + CHECK(profile_from_file_to_json(filepath, p.name()).empty()); // Save and validate. p.save(); expected_values_t e; CHECK(is_valid(p, e)); - CHECK(!profile_from_file_to_json(p.name()).empty()); + CHECK(!profile_from_file_to_json(filepath, p.name()).empty()); // Remove the profile and validate that the local json object is removed. p.remove(); - CHECK(profile_from_file_to_json(p.name()).empty()); + CHECK(profile_from_file_to_json(filepath, p.name()).empty()); } TEST_CASE_METHOD( @@ -185,7 +167,7 @@ TEST_CASE_METHOD( "username"}; // Set and validate non-default parameters. - RestProfile p(e.name); + RestProfile p(e.name, homedir_); p.set("rest.password", e.password); p.set("rest.payer_namespace", e.payer_namespace); p.set("rest.token", e.token); @@ -198,7 +180,7 @@ TEST_CASE_METHOD( TEST_CASE_METHOD( RestProfileFx, "REST Profile: to_json", "[rest_profile][to_json]") { // Create a default RestProfile. - RestProfile p; + RestProfile p(RestProfile::DEFAULT_NAME, homedir_); p.save(); // Validate. @@ -215,7 +197,7 @@ TEST_CASE_METHOD( RestProfileFx, "REST Profile: Get/Set invalid parameters", "[rest_profile][get_set_invalid]") { - RestProfile p; + RestProfile p(RestProfile::DEFAULT_NAME, homedir_); // Try to get a parameter with an invalid name. REQUIRE_THROWS_WITH( @@ -252,7 +234,7 @@ TEST_CASE_METHOD( std::string token = "token"; // Create and validate a RestProfile with default name. - RestProfile p; + RestProfile p(RestProfile::DEFAULT_NAME, homedir_); p.set("rest.payer_namespace", payer_namespace); p.save(); expected_values_t e; @@ -260,7 +242,7 @@ TEST_CASE_METHOD( CHECK(is_valid(p, e)); // Create a second profile, ensuring the payer_namespace is inherited. - RestProfile p2; + RestProfile p2(RestProfile::DEFAULT_NAME, homedir_); CHECK(p2.get("rest.payer_namespace") == payer_namespace); // Set a non-default token on the second profile. @@ -281,7 +263,7 @@ TEST_CASE_METHOD( std::string payer_namespace = "payer_namespace"; // Create and validate a RestProfile with default name. - RestProfile p; + RestProfile p(RestProfile::DEFAULT_NAME, homedir_); p.set("rest.payer_namespace", payer_namespace); p.save(); expected_values_t e; @@ -290,7 +272,7 @@ TEST_CASE_METHOD( // Create a second profile with non-default name and ensure the // payer_namespace and cloudtoken_ are NOT inherited. - RestProfile p2(name); + RestProfile p2(name, homedir_); CHECK(p2.get("rest.payer_namespace") != payer_namespace); p2.save(); e.name = name; From c6aca03ffd2ea345801d6b2db809907938fc475d Mon Sep 17 00:00:00 2001 From: Rebekah Davis Date: Mon, 24 Feb 2025 15:52:25 -0500 Subject: [PATCH 6/8] Address comment. --- tiledb/sm/rest/test/unit_rest_profile.cc | 34 +++++++++++++++--------- 1 file changed, 21 insertions(+), 13 deletions(-) diff --git a/tiledb/sm/rest/test/unit_rest_profile.cc b/tiledb/sm/rest/test/unit_rest_profile.cc index d72e9a27b67..b953ad54b5d 100644 --- a/tiledb/sm/rest/test/unit_rest_profile.cc +++ b/tiledb/sm/rest/test/unit_rest_profile.cc @@ -53,11 +53,13 @@ struct expected_values_t { struct RestProfileFx { public: - std::string homedir_; // The temporary in-test $HOME directory. - std::string cloudpath_; // The in-test path to the cloud.json file. + TemporaryLocalDirectory tempdir_; // A temporary working directory. + std::string homedir_; // The temporary in-test $HOME directory. + std::string cloudpath_; // The in-test path to the cloud.json file. RestProfileFx() - : homedir_(TemporaryLocalDirectory("unit_rest_profile").path()) + : tempdir_(TemporaryLocalDirectory("unit_rest_profile")) + , homedir_(tempdir_.path()) , cloudpath_(homedir_ + ".tiledb/cloud.json") { // Fstream cannot create directories, only files, so create the .tiledb dir. std::filesystem::create_directories(homedir_ + ".tiledb"); @@ -79,6 +81,12 @@ struct RestProfileFx { ~RestProfileFx() = default; + /** Returns a new RestProfile with the given name, at the in-test $HOME. */ + RestProfile create_profile( + const std::string& name = RestProfile::DEFAULT_NAME) { + return RestProfile(name, homedir_); + } + /** Returns true iff the profile's parameter values match the expected. */ bool is_valid(RestProfile p, expected_values_t e) { if (p.name() == e.name && p.get("rest.password") == e.password && @@ -113,7 +121,7 @@ TEST_CASE_METHOD( std::filesystem::remove(cloudpath_); // Create and validate a default RestProfile. - RestProfile profile(RestProfile::DEFAULT_NAME, homedir_); + RestProfile profile(create_profile()); profile.save(); // Set the expected token value; expected_values_t uses cloudtoken_ by @@ -128,7 +136,7 @@ TEST_CASE_METHOD( "REST Profile: Default profile inherited from cloudpath", "[rest_profile][default][inherited]") { // Create and validate a default RestProfile. - RestProfile profile(RestProfile::DEFAULT_NAME, homedir_); + RestProfile profile(create_profile()); profile.save(); expected_values_t expected; CHECK(is_valid(profile, expected)); @@ -139,7 +147,7 @@ TEST_CASE_METHOD( "REST Profile: Save/Remove", "[rest_profile][save][remove]") { // Create a default RestProfile. - RestProfile p(RestProfile::DEFAULT_NAME, homedir_); + RestProfile p(create_profile()); std::string filepath = homedir_ + ".tiledb/profiles.json"; CHECK(profile_from_file_to_json(filepath, p.name()).empty()); @@ -167,7 +175,7 @@ TEST_CASE_METHOD( "username"}; // Set and validate non-default parameters. - RestProfile p(e.name, homedir_); + RestProfile p(create_profile(e.name)); p.set("rest.password", e.password); p.set("rest.payer_namespace", e.payer_namespace); p.set("rest.token", e.token); @@ -180,7 +188,7 @@ TEST_CASE_METHOD( TEST_CASE_METHOD( RestProfileFx, "REST Profile: to_json", "[rest_profile][to_json]") { // Create a default RestProfile. - RestProfile p(RestProfile::DEFAULT_NAME, homedir_); + RestProfile p(create_profile()); p.save(); // Validate. @@ -197,7 +205,7 @@ TEST_CASE_METHOD( RestProfileFx, "REST Profile: Get/Set invalid parameters", "[rest_profile][get_set_invalid]") { - RestProfile p(RestProfile::DEFAULT_NAME, homedir_); + RestProfile p(create_profile()); // Try to get a parameter with an invalid name. REQUIRE_THROWS_WITH( @@ -234,7 +242,7 @@ TEST_CASE_METHOD( std::string token = "token"; // Create and validate a RestProfile with default name. - RestProfile p(RestProfile::DEFAULT_NAME, homedir_); + RestProfile p(create_profile()); p.set("rest.payer_namespace", payer_namespace); p.save(); expected_values_t e; @@ -242,7 +250,7 @@ TEST_CASE_METHOD( CHECK(is_valid(p, e)); // Create a second profile, ensuring the payer_namespace is inherited. - RestProfile p2(RestProfile::DEFAULT_NAME, homedir_); + RestProfile p2(create_profile()); CHECK(p2.get("rest.payer_namespace") == payer_namespace); // Set a non-default token on the second profile. @@ -263,7 +271,7 @@ TEST_CASE_METHOD( std::string payer_namespace = "payer_namespace"; // Create and validate a RestProfile with default name. - RestProfile p(RestProfile::DEFAULT_NAME, homedir_); + RestProfile p(create_profile()); p.set("rest.payer_namespace", payer_namespace); p.save(); expected_values_t e; @@ -272,7 +280,7 @@ TEST_CASE_METHOD( // Create a second profile with non-default name and ensure the // payer_namespace and cloudtoken_ are NOT inherited. - RestProfile p2(name, homedir_); + RestProfile p2(create_profile(name)); CHECK(p2.get("rest.payer_namespace") != payer_namespace); p2.save(); e.name = name; From f4f192a46c5a404c87270acdecc4c8de516dee44 Mon Sep 17 00:00:00 2001 From: Rebekah Davis Date: Wed, 26 Feb 2025 16:06:35 -0500 Subject: [PATCH 7/8] Address most comments, still locally investigating some. --- tiledb/sm/misc/constants.cc | 6 ++ tiledb/sm/misc/constants.h | 6 ++ tiledb/sm/rest/rest_profile.cc | 77 +++++++++--------------- tiledb/sm/rest/rest_profile.h | 29 +++++---- tiledb/sm/rest/test/unit_rest_profile.cc | 64 ++++++++++---------- 5 files changed, 92 insertions(+), 90 deletions(-) diff --git a/tiledb/sm/misc/constants.cc b/tiledb/sm/misc/constants.cc index 7d8b7561728..07d4f296869 100644 --- a/tiledb/sm/misc/constants.cc +++ b/tiledb/sm/misc/constants.cc @@ -778,6 +778,12 @@ const std::string rest_header_prefix = "rest.custom_headers."; /** The current RestProfile API version. */ const format_version_t rest_profile_version = 1; +/** Filepath for the special local cloud profile files used in TileDB. */ +const std::string cloud_profile_filepath = ".tiledb/cloud.json"; + +/** Filepath for the special local RestProfile files used in TileDB. */ +const std::string rest_profile_filepath = ".tiledb/profiles.json"; + /** String describing MIME_AUTODETECT. */ const std::string mime_autodetect_str = "AUTODETECT"; diff --git a/tiledb/sm/misc/constants.h b/tiledb/sm/misc/constants.h index aac847b29cd..f931f68cb4a 100644 --- a/tiledb/sm/misc/constants.h +++ b/tiledb/sm/misc/constants.h @@ -766,6 +766,12 @@ extern const std::string rest_header_prefix; /** The current RestProfile API version. */ extern const format_version_t rest_profile_version; +/** Filepath for the special local cloud profile files used in TileDB. */ +extern const std::string cloud_profile_filepath; + +/** Filepath for the special local RestProfile files used in TileDB. */ +extern const std::string rest_profile_filepath; + /** Delimiter for lists passed as config parameter */ extern const std::string config_delimiter; diff --git a/tiledb/sm/rest/rest_profile.cc b/tiledb/sm/rest/rest_profile.cc index 201ba907c92..cc386a3e15d 100644 --- a/tiledb/sm/rest/rest_profile.cc +++ b/tiledb/sm/rest/rest_profile.cc @@ -41,25 +41,6 @@ using namespace tiledb::common::filesystem; namespace tiledb::sm { -/* ****************************** */ -/* PARAMETER DEFAULTS */ -/* ****************************** */ - -const std::string RestProfile::DEFAULT_NAME{"default"}; -const std::string RestProfile::DEFAULT_PASSWORD{""}; -const std::string RestProfile::DEFAULT_PAYER_NAMESPACE{""}; -const std::string RestProfile::DEFAULT_TOKEN{""}; -const std::string RestProfile::DEFAULT_SERVER_ADDRESS{"https://api.tiledb.com"}; -const std::string RestProfile::DEFAULT_USERNAME{""}; - -const std::map default_values = { - std::make_pair("rest.password", RestProfile::DEFAULT_PASSWORD), - std::make_pair( - "rest.payer_namespace", RestProfile::DEFAULT_PAYER_NAMESPACE), - std::make_pair("rest.token", RestProfile::DEFAULT_TOKEN), - std::make_pair("rest.server_address", RestProfile::DEFAULT_SERVER_ADDRESS), - std::make_pair("rest.username", RestProfile::DEFAULT_USERNAME)}; - /* ****************************** */ /* CONSTRUCTORS & DESTRUCTORS */ /* ****************************** */ @@ -67,23 +48,8 @@ const std::map default_values = { RestProfile::RestProfile(const std::string& name, const std::string& homedir) : version_(constants::rest_profile_version) , name_(name) - , filepath_(homedir + ".tiledb/profiles.json") - , old_filepath_(homedir + ".tiledb/cloud.json") - , param_values_(default_values) { - /** - * Ensure the user's $HOME is found. - * There's an edge case in which `sudo` does not always preserve the path to - * `$HOME`. In this case, the home_directory() API does not throw, but instead - * returns `std::nullopt`. As such, we can check for a value in the returned - * `std::optional` path of the home_directory and throw an error to the user - * accordingly, so they may decide the proper course of action: set the - * $HOME path, or perhaps stop using `sudo`. - */ - if (homedir.empty()) { - throw RestProfileException( - "Failed to create RestProfile; $HOME is not set."); - } - + , filepath_(homedir + constants::rest_profile_filepath) + , old_filepath_(homedir + constants::cloud_profile_filepath) { // Fstream cannot create directories. If `homedir/.tiledb/` DNE, create it. std::filesystem::create_directories(homedir + ".tiledb"); @@ -98,25 +64,40 @@ RestProfile::RestProfile(const std::string& name, const std::string& homedir) } } -RestProfile::RestProfile(const std::string& name) - : RestProfile( - name, home_directory().has_value() ? home_directory().value() : "") { +RestProfile::RestProfile(const std::string& name) { + /** + * Ensure the user's $HOME is found. + * There's an edge case in which `sudo` does not always preserve the path to + * `$HOME`. In this case, the home_directory() API does not throw, but instead + * returns `std::nullopt`. As such, we can check for a value in the returned + * `std::optional` path of the home_directory and throw an error to the user + * accordingly, so they may decide the proper course of action: set the + * $HOME path, or perhaps stop using `sudo`. + */ + auto homedir = home_directory().has_value() ? home_directory().value() : ""; + if (homedir.empty()) { + throw RestProfileException( + "Failed to create RestProfile; $HOME is not set."); + } + + RestProfile(name, homedir); } /* ****************************** */ /* API */ /* ****************************** */ -void RestProfile::set(const std::string& param, const std::string& value) { +void RestProfile::set_param( + const std::string& param, const std::string& value) { // Validate incoming parameter name - if (default_values.count(param) == 0) { + if (param_values_.count(param) == 0) { throw RestProfileException( "Failed to set parameter of invalid name \'" + param + "\'"); } param_values_[param] = value; } -std::string RestProfile::get(const std::string& param) const { +std::string RestProfile::get_param(const std::string& param) const { auto it = param_values_.find(param); if (it == param_values_.end()) { throw RestProfileException( @@ -131,7 +112,7 @@ std::string RestProfile::get(const std::string& param) const { * json object, but rather sorts its elements alphabetically. * See issue [#727](https://github.com/nlohmann/json/issues/727) for details. */ -void RestProfile::save() { +void RestProfile::save_to_file() { // Validate that the profile is complete (if username is set, so is password) if ((param_values_["rest.username"] == RestProfile::DEFAULT_USERNAME) != (param_values_["rest.password"] == RestProfile::DEFAULT_PASSWORD)) { @@ -189,7 +170,7 @@ void RestProfile::save() { } } -void RestProfile::remove() { +void RestProfile::remove_from_file() { std::string original_filepath = filepath_; if (std::filesystem::exists(filepath_)) { // Temporarily append filename with a random label to guarantee atomicity. @@ -244,11 +225,13 @@ std::string RestProfile::dump() { void RestProfile::load_from_json_file(const std::string& filename) { if (filename.empty() || (filename != filepath_ && filename != old_filepath_)) { - throw RestProfileException("Cannot load from file; invalid filename."); + throw RestProfileException( + "Cannot load from \'" + filename + "\'; invalid filename."); } if (!std::filesystem::exists(filename)) { - throw RestProfileException("Cannot load from file; file does not exist."); + throw RestProfileException( + "Cannot load from \'" + filename + "\'; file does not exist."); } // Load the file into a json object. @@ -257,7 +240,7 @@ void RestProfile::load_from_json_file(const std::string& filename) { try { file >> data; } catch (...) { - throw RestProfileException("Error parsing json file."); + throw RestProfileException("Error parsing json file \'" + filename + "\'."); } // If possible, load (overwrite) the parameters from the local file diff --git a/tiledb/sm/rest/rest_profile.h b/tiledb/sm/rest/rest_profile.h index 6fdb3841cd7..1678feefb74 100644 --- a/tiledb/sm/rest/rest_profile.h +++ b/tiledb/sm/rest/rest_profile.h @@ -64,22 +64,22 @@ class RestProfile { /* ****************************** */ /** The default name of a RestProfile. */ - static const std::string DEFAULT_NAME; + static constexpr std::string DEFAULT_NAME{"default"}; /** The user's REST password. */ - static const std::string DEFAULT_PASSWORD; + static constexpr std::string DEFAULT_PASSWORD{""}; /** The namespace that should be charged for the request. */ - static const std::string DEFAULT_PAYER_NAMESPACE; + static constexpr std::string DEFAULT_PAYER_NAMESPACE{""}; /** The user's REST token. */ - static const std::string DEFAULT_TOKEN; + static constexpr std::string DEFAULT_TOKEN{""}; /** The default address for REST server. */ - static const std::string DEFAULT_SERVER_ADDRESS; + static constexpr std::string DEFAULT_SERVER_ADDRESS{"https://api.tiledb.com"}; /** The user's REST username. */ - static const std::string DEFAULT_USERNAME; + static constexpr std::string DEFAULT_USERNAME{""}; /* ********************************* */ /* CONSTRUCTORS & DESTRUCTORS */ @@ -118,7 +118,7 @@ class RestProfile { * @param param The parameter to set. * @param value The value to set on the given parameter. */ - void set(const std::string& param, const std::string& value); + void set_param(const std::string& param, const std::string& value); /** * Retrieves the value of the given parameter. @@ -126,13 +126,13 @@ class RestProfile { * @param param The parameter to fetch. * @return The value of the given parameter. */ - std::string get(const std::string& param) const; + std::string get_param(const std::string& param) const; /** Saves this profile to the local file. */ - void save(); + void save_to_file(); /** Removes this profile from the local file. */ - void remove(); + void remove_from_file(); /** * Exports this profile's parameters and their values to a json object. @@ -173,7 +173,14 @@ class RestProfile { std::string old_filepath_; /** Stores a map of for the set-parameters. */ - std::map param_values_; + std::map param_values_ = { + std::make_pair("rest.password", RestProfile::DEFAULT_PASSWORD), + std::make_pair( + "rest.payer_namespace", RestProfile::DEFAULT_PAYER_NAMESPACE), + std::make_pair("rest.token", RestProfile::DEFAULT_TOKEN), + std::make_pair( + "rest.server_address", RestProfile::DEFAULT_SERVER_ADDRESS), + std::make_pair("rest.username", RestProfile::DEFAULT_USERNAME)}; }; } // namespace tiledb::sm diff --git a/tiledb/sm/rest/test/unit_rest_profile.cc b/tiledb/sm/rest/test/unit_rest_profile.cc index b953ad54b5d..f72d232d18c 100644 --- a/tiledb/sm/rest/test/unit_rest_profile.cc +++ b/tiledb/sm/rest/test/unit_rest_profile.cc @@ -89,11 +89,11 @@ struct RestProfileFx { /** Returns true iff the profile's parameter values match the expected. */ bool is_valid(RestProfile p, expected_values_t e) { - if (p.name() == e.name && p.get("rest.password") == e.password && - p.get("rest.payer_namespace") == e.payer_namespace && - p.get("rest.token") == e.token && - p.get("rest.server_address") == e.server_address && - p.get("rest.username") == e.username) + if (p.name() == e.name && p.get_param("rest.password") == e.password && + p.get_param("rest.payer_namespace") == e.payer_namespace && + p.get_param("rest.token") == e.token && + p.get_param("rest.server_address") == e.server_address && + p.get_param("rest.username") == e.username) return true; return false; } @@ -122,7 +122,7 @@ TEST_CASE_METHOD( // Create and validate a default RestProfile. RestProfile profile(create_profile()); - profile.save(); + profile.save_to_file(); // Set the expected token value; expected_values_t uses cloudtoken_ by // default. @@ -137,7 +137,7 @@ TEST_CASE_METHOD( "[rest_profile][default][inherited]") { // Create and validate a default RestProfile. RestProfile profile(create_profile()); - profile.save(); + profile.save_to_file(); expected_values_t expected; CHECK(is_valid(profile, expected)); } @@ -152,13 +152,13 @@ TEST_CASE_METHOD( CHECK(profile_from_file_to_json(filepath, p.name()).empty()); // Save and validate. - p.save(); + p.save_to_file(); expected_values_t e; CHECK(is_valid(p, e)); CHECK(!profile_from_file_to_json(filepath, p.name()).empty()); // Remove the profile and validate that the local json object is removed. - p.remove(); + p.remove_from_file(); CHECK(profile_from_file_to_json(filepath, p.name()).empty()); } @@ -176,12 +176,12 @@ TEST_CASE_METHOD( // Set and validate non-default parameters. RestProfile p(create_profile(e.name)); - p.set("rest.password", e.password); - p.set("rest.payer_namespace", e.payer_namespace); - p.set("rest.token", e.token); - p.set("rest.server_address", e.server_address); - p.set("rest.username", e.username); - p.save(); + p.set_param("rest.password", e.password); + p.set_param("rest.payer_namespace", e.payer_namespace); + p.set_param("rest.token", e.token); + p.set_param("rest.server_address", e.server_address); + p.set_param("rest.username", e.username); + p.save_to_file(); CHECK(is_valid(p, e)); } @@ -189,7 +189,7 @@ TEST_CASE_METHOD( RestProfileFx, "REST Profile: to_json", "[rest_profile][to_json]") { // Create a default RestProfile. RestProfile p(create_profile()); - p.save(); + p.save_to_file(); // Validate. expected_values_t e; @@ -209,23 +209,23 @@ TEST_CASE_METHOD( // Try to get a parameter with an invalid name. REQUIRE_THROWS_WITH( - p.get("username"), + p.get_param("username"), Catch::Matchers::ContainsSubstring("Failed to retrieve parameter")); // Try to set a parameter with an invalid name. REQUIRE_THROWS_WITH( - p.set("username", "failed_username"), + p.set_param("username", "failed_username"), Catch::Matchers::ContainsSubstring( "Failed to set parameter of invalid name")); // Set username and try to save without setting password. - p.set("rest.username", "username"); + p.set_param("rest.username", "username"); REQUIRE_THROWS_WITH( - p.save(), + p.save_to_file(), Catch::Matchers::ContainsSubstring("invalid username/password pairing")); // Set password and save valid profile - p.set("rest.password", "password"); - p.save(); + p.set_param("rest.password", "password"); + p.save_to_file(); // Validate. expected_values_t e; @@ -243,24 +243,24 @@ TEST_CASE_METHOD( // Create and validate a RestProfile with default name. RestProfile p(create_profile()); - p.set("rest.payer_namespace", payer_namespace); - p.save(); + p.set_param("rest.payer_namespace", payer_namespace); + p.save_to_file(); expected_values_t e; e.payer_namespace = payer_namespace; CHECK(is_valid(p, e)); // Create a second profile, ensuring the payer_namespace is inherited. RestProfile p2(create_profile()); - CHECK(p2.get("rest.payer_namespace") == payer_namespace); + CHECK(p2.get_param("rest.payer_namespace") == payer_namespace); // Set a non-default token on the second profile. - p2.set("rest.token", token); - p2.save(); + p2.set_param("rest.token", token); + p2.save_to_file(); e.token = token; CHECK(is_valid(p2, e)); // Ensure the first profile is now out of date. - CHECK(p.get("rest.token") == cloudtoken_); + CHECK(p.get_param("rest.token") == cloudtoken_); } TEST_CASE_METHOD( @@ -272,8 +272,8 @@ TEST_CASE_METHOD( // Create and validate a RestProfile with default name. RestProfile p(create_profile()); - p.set("rest.payer_namespace", payer_namespace); - p.save(); + p.set_param("rest.payer_namespace", payer_namespace); + p.save_to_file(); expected_values_t e; e.payer_namespace = payer_namespace; CHECK(is_valid(p, e)); @@ -281,8 +281,8 @@ TEST_CASE_METHOD( // Create a second profile with non-default name and ensure the // payer_namespace and cloudtoken_ are NOT inherited. RestProfile p2(create_profile(name)); - CHECK(p2.get("rest.payer_namespace") != payer_namespace); - p2.save(); + CHECK(p2.get_param("rest.payer_namespace") != payer_namespace); + p2.save_to_file(); e.name = name; e.payer_namespace = RestProfile::DEFAULT_PAYER_NAMESPACE; e.token = RestProfile::DEFAULT_TOKEN; From 04de8302417649318c7c08f4f6da6c202af567f7 Mon Sep 17 00:00:00 2001 From: Rebekah Davis Date: Thu, 27 Feb 2025 10:47:20 -0500 Subject: [PATCH 8/8] Revert minor change, remove HomeDirectory class and optional behavior. --- tiledb/common/filesystem/home_directory.cc | 27 ++++------------- tiledb/common/filesystem/home_directory.h | 35 ++-------------------- tiledb/sm/rest/rest_profile.cc | 13 +++++++- tiledb/sm/rest/rest_profile.h | 12 ++++---- 4 files changed, 27 insertions(+), 60 deletions(-) diff --git a/tiledb/common/filesystem/home_directory.cc b/tiledb/common/filesystem/home_directory.cc index 950b1071d30..06e2ab61c33 100644 --- a/tiledb/common/filesystem/home_directory.cc +++ b/tiledb/common/filesystem/home_directory.cc @@ -27,7 +27,7 @@ * * @section DESCRIPTION * - * This file implements class HomeDirectory and function home_directory(). + * This file implements standalone function home_directory(). */ #ifdef _WIN32 @@ -45,20 +45,8 @@ namespace tiledb::common::filesystem { -/* ********************************* */ -/* API */ -/* ********************************* */ - -std::optional home_directory() { - return HomeDirectory().path(); -} - -/* ********************************* */ -/* HomeDirectory */ -/* ********************************* */ - -HomeDirectory::HomeDirectory() - : path_(std::nullopt) { +std::string home_directory() { + std::string path = ""; #ifdef _WIN32 wchar_t* home; auto _ = ScopedExecutor([&]() { @@ -66,18 +54,15 @@ HomeDirectory::HomeDirectory() CoTaskMemFree(home); }); if (SHGetKnownFolderPath(FOLDERID_Profile, 0, NULL, &home) == S_OK) { - path_ = std::wstring_convert>{}.to_bytes(home); + path = std::wstring_convert>{}.to_bytes(home); } #else const char* home = std::getenv("HOME"); if (home != nullptr) { - path_ = home; + path = home; } #endif -} - -std::optional HomeDirectory::path() { - return path_; + return path; } } // namespace tiledb::common::filesystem diff --git a/tiledb/common/filesystem/home_directory.h b/tiledb/common/filesystem/home_directory.h index 8f1a1c4669f..6d9ec602c91 100644 --- a/tiledb/common/filesystem/home_directory.h +++ b/tiledb/common/filesystem/home_directory.h @@ -27,13 +27,12 @@ * * @section DESCRIPTION * - * This file defines class HomeDirectory and function home_directory(). + * This file defines standalone function home_directory(). */ #ifndef TILEDB_HOME_DIRECTORY_H #define TILEDB_HOME_DIRECTORY_H -#include #include namespace tiledb::common::filesystem { @@ -42,37 +41,9 @@ namespace tiledb::common::filesystem { * Standalone function which returns the path to user's home directory. * * @invariant `sudo` does not always preserve the path to `$HOME`. Rather than - * throw if the path does not exist, this API will return `std::nullopt`. + * throw if the path does not exist, this API will return an empty string. */ -std::optional home_directory(); - -class HomeDirectory { - public: - /* ********************************* */ - /* CONSTRUCTORS & DESTRUCTORS */ - /* ********************************* */ - - /** Constructor. */ - HomeDirectory(); - - /** Destructor. */ - ~HomeDirectory() = default; - - /* ********************************* */ - /* API */ - /* ********************************* */ - - /** Return the path of the home directory. */ - std::optional path(); - - private: - /* ********************************* */ - /* PRIVATE ATTRIBUTES */ - /* ********************************* */ - - /** The path of the home directory. */ - std::optional path_; -}; +std::string home_directory(); } // namespace tiledb::common::filesystem diff --git a/tiledb/sm/rest/rest_profile.cc b/tiledb/sm/rest/rest_profile.cc index cc386a3e15d..dfc1765d626 100644 --- a/tiledb/sm/rest/rest_profile.cc +++ b/tiledb/sm/rest/rest_profile.cc @@ -41,6 +41,17 @@ using namespace tiledb::common::filesystem; namespace tiledb::sm { +/* ****************************** */ +/* PARAMETER DEFAULTS */ +/* ****************************** */ + +const std::string RestProfile::DEFAULT_NAME{"default"}; +const std::string RestProfile::DEFAULT_PASSWORD{""}; +const std::string RestProfile::DEFAULT_PAYER_NAMESPACE{""}; +const std::string RestProfile::DEFAULT_TOKEN{""}; +const std::string RestProfile::DEFAULT_SERVER_ADDRESS{"https://api.tiledb.com"}; +const std::string RestProfile::DEFAULT_USERNAME{""}; + /* ****************************** */ /* CONSTRUCTORS & DESTRUCTORS */ /* ****************************** */ @@ -74,7 +85,7 @@ RestProfile::RestProfile(const std::string& name) { * accordingly, so they may decide the proper course of action: set the * $HOME path, or perhaps stop using `sudo`. */ - auto homedir = home_directory().has_value() ? home_directory().value() : ""; + auto homedir = home_directory(); if (homedir.empty()) { throw RestProfileException( "Failed to create RestProfile; $HOME is not set."); diff --git a/tiledb/sm/rest/rest_profile.h b/tiledb/sm/rest/rest_profile.h index 1678feefb74..f4801c8a409 100644 --- a/tiledb/sm/rest/rest_profile.h +++ b/tiledb/sm/rest/rest_profile.h @@ -64,22 +64,22 @@ class RestProfile { /* ****************************** */ /** The default name of a RestProfile. */ - static constexpr std::string DEFAULT_NAME{"default"}; + static const std::string DEFAULT_NAME; /** The user's REST password. */ - static constexpr std::string DEFAULT_PASSWORD{""}; + static const std::string DEFAULT_PASSWORD; /** The namespace that should be charged for the request. */ - static constexpr std::string DEFAULT_PAYER_NAMESPACE{""}; + static const std::string DEFAULT_PAYER_NAMESPACE; /** The user's REST token. */ - static constexpr std::string DEFAULT_TOKEN{""}; + static const std::string DEFAULT_TOKEN; /** The default address for REST server. */ - static constexpr std::string DEFAULT_SERVER_ADDRESS{"https://api.tiledb.com"}; + static const std::string DEFAULT_SERVER_ADDRESS; /** The user's REST username. */ - static constexpr std::string DEFAULT_USERNAME{""}; + static const std::string DEFAULT_USERNAME; /* ********************************* */ /* CONSTRUCTORS & DESTRUCTORS */