From 0da12bec0f2772e9373e8e6db26c24311ef23fac Mon Sep 17 00:00:00 2001 From: emeric Date: Sun, 1 Oct 2023 16:04:28 +0200 Subject: [PATCH] extracted album handling --- src/libs/subsonic/CMakeLists.txt | 3 + src/libs/subsonic/impl/SubsonicResource.cpp | 119 +---- src/libs/subsonic/impl/responses/Album.cpp | 102 +++++ src/libs/subsonic/impl/responses/Album.hpp | 34 ++ src/libs/subsonic/impl/responses/Artist.cpp | 46 ++ src/libs/subsonic/impl/responses/Artist.hpp | 39 ++ src/libs/utils/impl/String.cpp | 480 ++++++++++---------- src/libs/utils/include/utils/String.hpp | 131 +++--- src/libs/utils/test/String.cpp | 15 + 9 files changed, 541 insertions(+), 428 deletions(-) create mode 100644 src/libs/subsonic/impl/responses/Album.cpp create mode 100644 src/libs/subsonic/impl/responses/Album.hpp create mode 100644 src/libs/subsonic/impl/responses/Artist.cpp create mode 100644 src/libs/subsonic/impl/responses/Artist.hpp diff --git a/src/libs/subsonic/CMakeLists.txt b/src/libs/subsonic/CMakeLists.txt index d4f4b985..95b7ccbb 100644 --- a/src/libs/subsonic/CMakeLists.txt +++ b/src/libs/subsonic/CMakeLists.txt @@ -1,5 +1,7 @@ add_library(lmssubsonic SHARED + impl/responses/Album.cpp + impl/responses/Artist.cpp impl/ProtocolVersion.cpp impl/Scan.cpp impl/Stream.cpp @@ -13,6 +15,7 @@ target_include_directories(lmssubsonic INTERFACE ) target_include_directories(lmssubsonic PRIVATE + impl include ) diff --git a/src/libs/subsonic/impl/SubsonicResource.cpp b/src/libs/subsonic/impl/SubsonicResource.cpp index ed1c6685..53967d0a 100644 --- a/src/libs/subsonic/impl/SubsonicResource.cpp +++ b/src/libs/subsonic/impl/SubsonicResource.cpp @@ -21,7 +21,6 @@ #include #include -#include #include #include @@ -55,6 +54,9 @@ #include "SubsonicId.hpp" #include "SubsonicResponse.hpp" +#include "responses/Artist.hpp" +#include "responses/Album.hpp" + using namespace Database; static const std::string_view genreClusterName {"GENRE"}; @@ -183,25 +185,6 @@ checkUserTypeIsAllowed(RequestContext& context, EnumSet allo throw UserNotAuthorizedError {}; } -static -std::string -getArtistNames(const std::vector& artists) -{ - if (artists.size() == 1) - return artists.front()->getName(); - - std::vector names; - names.resize(artists.size()); - - std::transform(std::cbegin(artists), std::cend(artists), std::begin(names), - [](const Artist::pointer& artist) - { - return artist->getName(); - }); - - return StringUtils::joinStrings(names, ", "); -} - static std::string getTrackPath(const Track::pointer& track) @@ -254,17 +237,6 @@ formatToSuffix(AudioFormat format) return ""; } -static -std::string -dateTimeToCreatedString(const Wt::WDateTime& dateTime) -{ - const std::time_t t {dateTime.toTime_t()}; - std::tm gmTime; - std::ostringstream oss; - oss << std::put_time(::gmtime_r(&t, &gmTime), "%FT%T"); - return oss.str(); -} - static Response::Node trackToResponseNode(const Track::pointer& track, Session& dbSession, const User::pointer& user) @@ -303,7 +275,7 @@ trackToResponseNode(const Track::pointer& track, Session& dbSession, const User: const std::vector& artists {track->getArtists({TrackArtistLinkType::Artist})}; if (!artists.empty()) { - trackResponse.setAttribute("artist", getArtistNames(artists)); + trackResponse.setAttribute("artist", utils::joinArtistNames(artists)); if (artists.size() == 1) trackResponse.setAttribute("artistId", idToString(artists.front()->getId())); @@ -318,7 +290,7 @@ trackToResponseNode(const Track::pointer& track, Session& dbSession, const User: trackResponse.setAttribute("duration", std::chrono::duration_cast(track->getDuration()).count()); trackResponse.setAttribute("type", "music"); - trackResponse.setAttribute("created", dateTimeToCreatedString(track->getLastWritten())); + trackResponse.setAttribute("created", StringUtils::toISO8601String(track->getLastWritten())); if (Service::get()->isStarred(user->getId(), track->getId())) trackResponse.setAttribute("starred", reportedStarredDate); @@ -351,71 +323,6 @@ trackBookmarkToResponseNode(const TrackBookmark::pointer& trackBookmark) return trackBookmarkNode; } -static -Response::Node -releaseToResponseNode(const Release::pointer& release, Session& dbSession, const User::pointer& user, bool id3) -{ - Response::Node albumNode; - - if (id3) - { - albumNode.setAttribute("name", release->getName()); - albumNode.setAttribute("songCount", release->getTracksCount()); - albumNode.setAttribute("duration", std::chrono::duration_cast(release->getDuration()).count()); - } - else - { - albumNode.setAttribute("title", release->getName()); - albumNode.setAttribute("isDir", true); - } - - albumNode.setAttribute("created", dateTimeToCreatedString(release->getLastWritten())); - albumNode.setAttribute("id", idToString(release->getId())); - albumNode.setAttribute("coverArt", idToString(release->getId())); - if (const Wt::WDate releaseDate {release->getReleaseDate()}; releaseDate.isValid()) - albumNode.setAttribute("year", releaseDate.year()); - - auto artists {release->getReleaseArtists()}; - if (artists.empty()) - artists = release->getArtists(); - - if (artists.empty() && !id3) - { - albumNode.setAttribute("parent", idToString(RootId {})); - } - else if (!artists.empty()) - { - albumNode.setAttribute("artist", getArtistNames(artists)); - - if (artists.size() == 1) - { - albumNode.setAttribute(id3 ? "artistId" : "parent", idToString(artists.front()->getId())); - } - else - { - if (!id3) - albumNode.setAttribute("parent", idToString(RootId {})); - } - } - - if (id3) - { - // Report the first GENRE for this track - ClusterType::pointer clusterType{ClusterType::find(dbSession, genreClusterName)}; - if (clusterType) - { - auto clusters{release->getClusterGroups({clusterType}, 1)}; - if (!clusters.empty() && !clusters.front().empty()) - albumNode.setAttribute("genre", clusters.front().front()->getName()); - } - } - - if (Service::get()->isStarred(user->getId(), release->getId())) - albumNode.setAttribute("starred", reportedStarredDate); - - return albumNode; -} - static Response::Node artistToResponseNode(const Artist::pointer& artist, Session& session, const User::pointer& user, bool id3) @@ -816,7 +723,7 @@ handleGetAlbumListRequestCommon(const RequestContext& context, bool id3) for (const ReleaseId releaseId : releases.results) { const Release::pointer release {Release::find(context.dbSession, releaseId)}; - albumListNode.addArrayChild("album", releaseToResponseNode(release, context.dbSession, user, id3)); + albumListNode.addArrayChild("album", createAlbumNode(release, context.dbSession, user, id3)); } return response; @@ -854,16 +761,16 @@ handleGetAlbumRequest(RequestContext& context) throw UserNotAuthorizedError {}; Response response {Response::createOkResponse(context.serverProtocolVersion)}; - Response::Node releaseNode {releaseToResponseNode(release, context.dbSession, user, true /* id3 */)}; + Response::Node albumNode {createAlbumNode(release, context.dbSession, user, true /* id3 */)}; const auto tracks {Track::find(context.dbSession, Track::FindParameters {}.setRelease(id).setSortMethod(TrackSortMethod::Release))}; for (const TrackId trackId : tracks.results) { const Track::pointer track {Track::find(context.dbSession, trackId)}; - releaseNode.addArrayChild("song", trackToResponseNode(track, context.dbSession, user)); + albumNode.addArrayChild("song", trackToResponseNode(track, context.dbSession, user)); } - response.addNode("album", std::move(releaseNode)); + response.addNode("album", std::move(albumNode)); return response; } @@ -915,7 +822,7 @@ handleGetArtistRequest(RequestContext& context) for (const ReleaseId releaseId : releases.results) { const Release::pointer release {Release::find(context.dbSession, releaseId)}; - artistNode.addArrayChild("album", releaseToResponseNode(release, context.dbSession, user, true /* id3 */)); + artistNode.addArrayChild("album", createAlbumNode(release, context.dbSession, user, true /* id3 */)); } response.addNode("artist", std::move(artistNode)); @@ -1029,7 +936,7 @@ handleGetMusicDirectoryRequest(RequestContext& context) for (const ReleaseId artistReleaseId : artistReleases.results) { const Release::pointer release {Release::find(context.dbSession, artistReleaseId)}; - directoryNode.addArrayChild("child", releaseToResponseNode(release, context.dbSession, user, false /* no id3 */)); + directoryNode.addArrayChild("child", createAlbumNode(release, context.dbSession, user, false /* no id3 */)); } } else if (releaseId) @@ -1311,7 +1218,7 @@ handleGetStarredRequestCommon(RequestContext& context, bool id3) for (const ReleaseId releaseId : scrobbling.getStarredReleases(context.userId, {} /* clusters */, Range {}).results) { if (auto release {Release::find(context.dbSession, releaseId)}) - starredNode.addArrayChild("album", releaseToResponseNode(release, context.dbSession, user, id3)); + starredNode.addArrayChild("album", createAlbumNode(release, context.dbSession, user, id3)); } for (const TrackId trackId : scrobbling.getStarredTracks(context.userId, {} /* clusters */, Range {}).results) @@ -1547,7 +1454,7 @@ handleSearchRequestCommon(RequestContext& context, bool id3) for (const ReleaseId releaseId : releaseIds.results) { const auto release {Release::find(context.dbSession, releaseId)}; - searchResult2Node.addArrayChild("album", releaseToResponseNode(release, context.dbSession, user, id3)); + searchResult2Node.addArrayChild("album", createAlbumNode(release, context.dbSession, user, id3)); } } diff --git a/src/libs/subsonic/impl/responses/Album.cpp b/src/libs/subsonic/impl/responses/Album.cpp new file mode 100644 index 00000000..76e00a42 --- /dev/null +++ b/src/libs/subsonic/impl/responses/Album.cpp @@ -0,0 +1,102 @@ +/* + * Copyright (C) 2023 Emeric Poupon + * + * This file is part of LMS. + * + * LMS is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * LMS is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with LMS. If not, see . + */ + +#include "services/database/Cluster.hpp" +#include "services/database/Artist.hpp" +#include "services/database/Release.hpp" +#include "services/database/User.hpp" +#include "services/scrobbling/IScrobblingService.hpp" +#include "utils/Service.hpp" +#include "utils/String.hpp" + +#include "responses/Album.hpp" +#include "responses/Artist.hpp" +#include "SubsonicId.hpp" + +namespace API::Subsonic +{ + static const std::string_view reportedDummyStarredDate {"2000-01-01T00:00:00"}; + + using namespace Database; + + Response::Node createAlbumNode(const Release::pointer& release, Session& dbSession, const User::pointer& user, bool id3) + { + Response::Node albumNode; + + if (id3) { + albumNode.setAttribute("name", release->getName()); + albumNode.setAttribute("songCount", release->getTracksCount()); + albumNode.setAttribute( + "duration", std::chrono::duration_cast( + release->getDuration()) + .count()); + } + else + { + albumNode.setAttribute("title", release->getName()); + albumNode.setAttribute("isDir", true); + } + + albumNode.setAttribute("created", StringUtils::toISO8601String(release->getLastWritten())); + albumNode.setAttribute("id", idToString(release->getId())); + albumNode.setAttribute("coverArt", idToString(release->getId())); + if (const Wt::WDate releaseDate{ release->getReleaseDate() }; releaseDate.isValid()) + albumNode.setAttribute("year", releaseDate.year()); + + auto artists{ release->getReleaseArtists() }; + if (artists.empty()) + artists = release->getArtists(); + + if (artists.empty() && !id3) + { + albumNode.setAttribute("parent", idToString(RootId{})); + } + else if (!artists.empty()) + { + albumNode.setAttribute("artist", utils::joinArtistNames(artists)); + + if (artists.size() == 1) + { + albumNode.setAttribute(id3 ? "artistId" : "parent", idToString(artists.front()->getId())); + } + else + { + if (!id3) + albumNode.setAttribute("parent", idToString(RootId{})); + } + } + + if (id3) + { + // Report the first GENRE for this track + ClusterType::pointer clusterType{ ClusterType::find(dbSession, "GENRE") }; + if (clusterType) + { + auto clusters{ release->getClusterGroups({clusterType}, 1) }; + if (!clusters.empty() && !clusters.front().empty()) + albumNode.setAttribute("genre", clusters.front().front()->getName()); + } + } + + if (Service::get()->isStarred(user->getId(), release->getId())) + albumNode.setAttribute("starred", reportedDummyStarredDate); // TODO report correct date/time + + return albumNode; + } +} \ No newline at end of file diff --git a/src/libs/subsonic/impl/responses/Album.hpp b/src/libs/subsonic/impl/responses/Album.hpp new file mode 100644 index 00000000..8002333c --- /dev/null +++ b/src/libs/subsonic/impl/responses/Album.hpp @@ -0,0 +1,34 @@ +/* + * Copyright (C) 2023 Emeric Poupon + * + * This file is part of LMS. + * + * LMS is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * LMS is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with LMS. If not, see . + */ + +#pragma once + +#include "services/database/Object.hpp" +#include "SubsonicResponse.hpp" + +namespace Database +{ + class Release; + class User; +} + +namespace API::Subsonic +{ + Response::Node createAlbumNode(const Database::ObjectPtr& release, Database::Session& dbSession, const Database::ObjectPtr& user, bool id3); +} \ No newline at end of file diff --git a/src/libs/subsonic/impl/responses/Artist.cpp b/src/libs/subsonic/impl/responses/Artist.cpp new file mode 100644 index 00000000..60f435b8 --- /dev/null +++ b/src/libs/subsonic/impl/responses/Artist.cpp @@ -0,0 +1,46 @@ +/* + * Copyright (C) 2023 Emeric Poupon + * + * This file is part of LMS. + * + * LMS is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * LMS is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with LMS. If not, see . + */ + +#include "responses/Artist.hpp" +#include "services/database/Artist.hpp" + +namespace API::Subsonic +{ + using namespace Database; + + namespace utils + { + std::string joinArtistNames(const std::vector& artists) + { + if (artists.size() == 1) + return artists.front()->getName(); + + std::vector names; + names.resize(artists.size()); + + std::transform(std::cbegin(artists), std::cend(artists), std::begin(names), + [](const Artist::pointer& artist) + { + return artist->getName(); + }); + + return StringUtils::joinStrings(names, ", "); + } + } +} \ No newline at end of file diff --git a/src/libs/subsonic/impl/responses/Artist.hpp b/src/libs/subsonic/impl/responses/Artist.hpp new file mode 100644 index 00000000..014e9a04 --- /dev/null +++ b/src/libs/subsonic/impl/responses/Artist.hpp @@ -0,0 +1,39 @@ +/* + * Copyright (C) 2023 Emeric Poupon + * + * This file is part of LMS. + * + * LMS is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * LMS is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with LMS. If not, see . + */ + +#pragma once + +#include +#include +#include "services/database/Object.hpp" +#include "SubsonicResponse.hpp" + +namespace Database +{ + class Artist; + class User; +} + +namespace API::Subsonic +{ + namespace utils + { + std::string joinArtistNames(const std::vector>& artists); + } +} \ No newline at end of file diff --git a/src/libs/utils/impl/String.cpp b/src/libs/utils/impl/String.cpp index 89c58edf..f214dcb2 100644 --- a/src/libs/utils/impl/String.cpp +++ b/src/libs/utils/impl/String.cpp @@ -27,279 +27,273 @@ #include #include -namespace StringUtils { +#include +#include -bool -readList(const std::string& str, const std::string& separators, std::list& results) +namespace StringUtils { - std::string curStr; - for (char c : str) + bool readList(const std::string& str, const std::string& separators, std::list& results) { - if (separators.find(c) != std::string::npos) { - if (!curStr.empty()) { - results.push_back(curStr); - curStr.clear(); + std::string curStr; + + for (char c : str) + { + if (separators.find(c) != std::string::npos) { + if (!curStr.empty()) { + results.push_back(curStr); + curStr.clear(); + } + } + else { + if (curStr.empty() && std::isspace(c)) + continue; + + curStr.push_back(c); } } - else { - if (curStr.empty() && std::isspace(c)) + + if (!curStr.empty()) + results.push_back(curStr); + + return !str.empty(); + } + + template<> + std::optional readAs(std::string_view str) + { + return std::string{ str }; + } + + template<> + std::optional readAs(std::string_view str) + { + return str; + } + + template<> + std::optional readAs(std::string_view str) + { + if (str == "1" || str == "true") + return true; + else if (str == "0" || str == "false") + return false; + + return std::nullopt; + } + + std::vector splitStringCopy(std::string_view string, std::string_view separators) + { + std::string str{ stringTrim(string, separators) }; + + std::vector res; + boost::algorithm::split(res, str, boost::is_any_of(separators), boost::token_compress_on); + + return res; + } + + std::vector splitString(std::string_view str, std::string_view separators) + { + std::vector res; + + std::string_view::size_type strBegin{}; + + while ((strBegin = str.find_first_not_of(separators, strBegin)) != std::string_view::npos) + { + auto strEnd{ str.find_first_of(separators, strBegin + 1) }; + if (strEnd == std::string_view::npos) + { + res.push_back(str.substr(strBegin, str.size() - strBegin)); + break; + } + + res.push_back(str.substr(strBegin, strEnd - strBegin)); + strBegin = strEnd + 1; + } + + return res; + } + + std::string joinStrings(const std::vector& strings, const std::string& delimiter) + { + return boost::algorithm::join(strings, delimiter); + } + + std::string_view stringTrim(std::string_view str, std::string_view whitespaces) + { + std::string_view res; + + const auto strBegin = str.find_first_not_of(whitespaces); + if (strBegin != std::string_view::npos) + { + const auto strEnd{ str.find_last_not_of(whitespaces) }; + const auto strRange{ strEnd - strBegin + 1 }; + + res = str.substr(strBegin, strRange); + } + + return res; + } + + std::string_view stringTrimEnd(std::string_view str, std::string_view whitespaces) + { + return str.substr(0, str.find_last_not_of(whitespaces) + 1); + } + + std::string stringToLower(std::string_view str) + { + std::string res; + res.reserve(str.size()); + + std::transform(std::cbegin(str), std::cend(str), std::back_inserter(res), [](unsigned char c) { return std::tolower(c);}); + + return res; + } + + void stringToLower(std::string& str) + { + std::transform(std::cbegin(str), std::cend(str), std::begin(str), [](unsigned char c) { return std::tolower(c);}); + } + + std::string stringToUpper(const std::string& str) + { + std::string res; + res.reserve(str.size()); + + std::transform(std::cbegin(str), std::cend(str), std::back_inserter(res), [](char c) { return std::toupper(c);}); + + return res; + } + + std::string bufferToString(const std::vector& data) + { + std::ostringstream oss; + + for (unsigned char c : data) + { + oss << std::setw(2) << std::setfill('0') << std::hex << (int)c; + } + + return oss.str(); + } + + void capitalize(std::string& str) + { + for (auto it{ std::begin(str) }; it != std::end(str); ++it) + { + if (std::isspace(*it)) continue; - curStr.push_back(c); - } - } + if (std::isalpha(*it)) + *it = std::toupper(*it); - if (!curStr.empty()) - results.push_back(curStr); - - return !str.empty(); -} - -template<> -std::optional -readAs(std::string_view str) -{ - return std::string {str}; -} - -template<> -std::optional -readAs(std::string_view str) -{ - return str; -} - -template<> -std::optional -readAs(std::string_view str) -{ - if (str == "1" || str == "true") - return true; - else if (str == "0" || str == "false") - return false; - - return std::nullopt; -} - -std::vector -splitStringCopy(std::string_view string, std::string_view separators) -{ - std::string str {stringTrim(string, separators)}; - - std::vector res; - boost::algorithm::split(res, str, boost::is_any_of(separators), boost::token_compress_on); - - return res; -} - -std::vector -splitString(std::string_view str, std::string_view separators) -{ - std::vector res; - - std::string_view::size_type strBegin {}; - - while ((strBegin = str.find_first_not_of(separators, strBegin)) != std::string_view::npos) - { - auto strEnd {str.find_first_of(separators, strBegin + 1)}; - if (strEnd == std::string_view::npos) - { - res.push_back(str.substr(strBegin, str.size() - strBegin)); break; } - - res.push_back(str.substr(strBegin, strEnd - strBegin)); - strBegin = strEnd + 1; } - return res; -} - -std::string -joinStrings(const std::vector& strings, const std::string& delimiter) -{ - return boost::algorithm::join(strings, delimiter); -} - -std::string_view -stringTrim(std::string_view str, std::string_view whitespaces) -{ - std::string_view res; - - const auto strBegin = str.find_first_not_of(whitespaces); - if (strBegin != std::string_view::npos) + std::string replaceInString(std::string_view str, const std::string& from, const std::string& to) { - const auto strEnd {str.find_last_not_of(whitespaces)}; - const auto strRange {strEnd - strBegin + 1}; + std::string res{ str }; + size_t pos = 0; - res = str.substr(strBegin, strRange); - } - - return res; -} - -std::string_view -stringTrimEnd(std::string_view str, std::string_view whitespaces) -{ - return str.substr(0, str.find_last_not_of(whitespaces) + 1); -} - -std::string -stringToLower(std::string_view str) -{ - std::string res; - res.reserve(str.size()); - - std::transform(std::cbegin(str), std::cend(str), std::back_inserter(res), [](unsigned char c) { return std::tolower(c);}); - - return res; -} - -void -stringToLower(std::string& str) -{ - std::transform(std::cbegin(str), std::cend(str), std::begin(str), [](unsigned char c) { return std::tolower(c);}); -} - -std::string -stringToUpper(const std::string& str) -{ - std::string res; - res.reserve(str.size()); - - std::transform(std::cbegin(str), std::cend(str), std::back_inserter(res), [](char c) { return std::toupper(c);}); - - return res; -} - -std::string -bufferToString(const std::vector& data) -{ - std::ostringstream oss; - - for (unsigned char c : data) - { - oss << std::setw(2) << std::setfill('0') << std::hex << (int)c; - } - - return oss.str(); -} - -void -capitalize(std::string& str) -{ - for (auto it {std::begin(str)}; it != std::end(str); ++it) - { - if (std::isspace(*it)) - continue; - - if (std::isalpha(*it)) - *it = std::toupper(*it); - - break; - } -} - -std::string -replaceInString(std::string_view str, const std::string& from, const std::string& to) -{ - std::string res {str}; - size_t pos = 0; - - while ((pos = res.find(from, pos)) != std::string::npos) - { - res.replace(pos, from.length(), to); - pos += to.length(); - } - - return res; -} - -std::string -jsEscape(const std::string& str) -{ - static const std::unordered_map escapeMap - { - { '\\', "\\\\" }, - { '\n', "\\n" }, - { '\r', "\\r" }, - { '\t', "\\t" }, - { '"', "\\\"" }, - { '\'', "\\\'" }, - }; - - std::string escaped; - escaped.reserve(str.length()); - - for (const char c : str) - { - auto it {escapeMap.find(c)}; - if (it == std::cend(escapeMap)) + while ((pos = res.find(from, pos)) != std::string::npos) { - escaped += c; - continue; + res.replace(pos, from.length(), to); + pos += to.length(); } - escaped += it->second; + return res; } - return escaped; -} - -std::string -escapeString(std::string_view str, std::string_view charsToEscape, char escapeChar) -{ - std::string res; - res.reserve(str.size()); - - for (const char c : str) + std::string jsEscape(const std::string& str) { - if (std::any_of(std::cbegin(charsToEscape), std::cend(charsToEscape), [c](char charToEscape) { return c == charToEscape; })) - res += escapeChar; + static const std::unordered_map escapeMap + { + { '\\', "\\\\" }, + { '\n', "\\n" }, + { '\r', "\\r" }, + { '\t', "\\t" }, + { '"', "\\\"" }, + { '\'', "\\\'" }, + }; - res += c; + std::string escaped; + escaped.reserve(str.length()); + + for (const char c : str) + { + auto it{ escapeMap.find(c) }; + if (it == std::cend(escapeMap)) + { + escaped += c; + continue; + } + + escaped += it->second; + } + + return escaped; } - return res; -} - -bool -stringEndsWith(const std::string& str, const std::string& ending) -{ - return boost::algorithm::ends_with(str, ending); -} - -std::optional -stringFromHex(const std::string& str) -{ - static const char lut[] {"0123456789ABCDEF"}; - - if (str.length() % 2 != 0) - return std::nullopt; - - std::string res; - res.reserve(str.length() / 2); - - auto it {std::cbegin(str)}; - while (it != std::cend(str)) + std::string escapeString(std::string_view str, std::string_view charsToEscape, char escapeChar) { - unsigned val {}; + std::string res; + res.reserve(str.size()); - auto itHigh {std::lower_bound(std::cbegin(lut), std::cend(lut), std::toupper(*(it++)))}; - auto itLow {std::lower_bound(std::cbegin(lut), std::cend(lut), std::toupper(*(it++)))}; + for (const char c : str) + { + if (std::any_of(std::cbegin(charsToEscape), std::cend(charsToEscape), [c](char charToEscape) { return c == charToEscape; })) + res += escapeChar; - if (itHigh == std::cend(lut) || itLow == std::cend(lut)) - return {}; + res += c; + } - val = std::distance(std::cbegin(lut), itHigh) << 4; - val += std::distance(std::cbegin(lut), itLow ); - - res.push_back(static_cast(val)); + return res; } - return res; -} + bool stringEndsWith(const std::string& str, const std::string& ending) + { + return boost::algorithm::ends_with(str, ending); + } + std::optional stringFromHex(const std::string& str) + { + static const char lut[]{ "0123456789ABCDEF" }; + + if (str.length() % 2 != 0) + return std::nullopt; + + std::string res; + res.reserve(str.length() / 2); + + auto it{ std::cbegin(str) }; + while (it != std::cend(str)) + { + unsigned val{}; + + auto itHigh{ std::lower_bound(std::cbegin(lut), std::cend(lut), std::toupper(*(it++))) }; + auto itLow{ std::lower_bound(std::cbegin(lut), std::cend(lut), std::toupper(*(it++))) }; + + if (itHigh == std::cend(lut) || itLow == std::cend(lut)) + return {}; + + val = std::distance(std::cbegin(lut), itHigh) << 4; + val += std::distance(std::cbegin(lut), itLow); + + res.push_back(static_cast(val)); + } + + return res; + } + + std::string toISO8601String(const Wt::WDateTime& dateTime) + { + return dateTime.toString("yyyy-MM-ddThh:mm:ss.zzz", false).toUTF8(); + } + + std::string toISO8601String(const Wt::WDate& date) + { + return date.toString("yyyy-MM-dd").toUTF8(); + } } // StringUtils diff --git a/src/libs/utils/include/utils/String.hpp b/src/libs/utils/include/utils/String.hpp index dff67ca0..c5741866 100644 --- a/src/libs/utils/include/utils/String.hpp +++ b/src/libs/utils/include/utils/String.hpp @@ -29,94 +29,67 @@ #define QUOTEME(x) QUOTEME_1(x) #define QUOTEME_1(x) #x -namespace StringUtils { - -[[nodiscard]] -std::vector -splitStringCopy(std::string_view string, std::string_view separators); - -[[nodiscard]] -std::vector -splitString(std::string_view string, std::string_view separators); - -[[nodiscard]] -std::string -joinStrings(const std::vector& strings, const std::string& delimiter); - -[[nodiscard]] -std::string_view -stringTrim(std::string_view str, std::string_view whitespaces = " \t"); - -[[nodiscard]] -std::string_view -stringTrimEnd(std::string_view str, std::string_view whitespaces = " \t"); - -[[nodiscard]] -std::string -stringToLower(std::string_view str); - -void -stringToLower(std::string& str); - -[[nodiscard]] -std::string -stringToUpper(const std::string& str); - -[[nodiscard]] -std::string -bufferToString(const std::vector& data); - -void -capitalize(std::string& str); - -template -[[nodiscard]] -std::optional readAs(std::string_view str) +namespace Wt { - T res; - - std::istringstream iss {std::string {str}}; - iss >> res; - if (iss.fail()) - return std::nullopt; - - return res; + class WDate; + class WDateTime; } -template<> -[[nodiscard]] -std::optional -readAs(std::string_view str); +namespace StringUtils { -template<> -[[nodiscard]] -std::optional -readAs(std::string_view str); + [[nodiscard]] std::vector splitStringCopy(std::string_view string, std::string_view separators); -template<> -[[nodiscard]] -std::optional -readAs(std::string_view str); + [[nodiscard]] std::vector splitString(std::string_view string, std::string_view separators); -[[nodiscard]] -std::string -replaceInString(std::string_view str, const std::string& from, const std::string& to); + [[nodiscard]] std::string joinStrings(const std::vector& strings, const std::string& delimiter); -[[nodiscard]] -std::string -jsEscape(const std::string& str); + [[nodiscard]] std::string_view stringTrim(std::string_view str, std::string_view whitespaces = " \t"); -[[nodiscard]] -std::string -escapeString(std::string_view str, std::string_view charsToEscape, char escapeChar); + [[nodiscard]] std::string_view stringTrimEnd(std::string_view str, std::string_view whitespaces = " \t"); -[[nodiscard]] -bool -stringEndsWith(const std::string& str, const std::string& ending); + [[nodiscard]] std::string stringToLower(std::string_view str); -[[nodiscard]] -std::optional -stringFromHex(const std::string& str); + void stringToLower(std::string& str); -} // StringUtils + [[nodiscard]] std::string stringToUpper(const std::string& str); + [[nodiscard]] std::string bufferToString(const std::vector& data); + + void capitalize(std::string& str); + + template + [[nodiscard]] std::optional readAs(std::string_view str) + { + T res; + + std::istringstream iss{ std::string {str} }; + iss >> res; + if (iss.fail()) + return std::nullopt; + + return res; + } + + template<> + [[nodiscard]] std::optional readAs(std::string_view str); + + template<> + [[nodiscard]] std::optional readAs(std::string_view str); + + template<> + [[nodiscard]] std::optional readAs(std::string_view str); + + [[nodiscard]] std::string replaceInString(std::string_view str, const std::string& from, const std::string& to); + + [[nodiscard]] std::string jsEscape(const std::string& str); + + [[nodiscard]] std::string escapeString(std::string_view str, std::string_view charsToEscape, char escapeChar); + + [[nodiscard]] bool stringEndsWith(const std::string& str, const std::string& ending); + + [[nodiscard]] std::optional stringFromHex(const std::string& str); + + [[nodiscard]] std::string toISO8601String(const Wt::WDateTime& dateTime); + [[nodiscard]] std::string toISO8601String(const Wt::WDate& date); + +} // StringUtils \ No newline at end of file diff --git a/src/libs/utils/test/String.cpp b/src/libs/utils/test/String.cpp index ccf02829..eb5281f6 100644 --- a/src/libs/utils/test/String.cpp +++ b/src/libs/utils/test/String.cpp @@ -19,6 +19,9 @@ #include +#include +#include +#include #include "utils/String.hpp" TEST(StringUtils, splitString) @@ -147,3 +150,15 @@ TEST(StringUtils, capitalize) EXPECT_EQ(str, test.expectedOutput) << " str was '" << test.input << "'"; } } + +TEST(Stringutils, date) +{ + const Wt::WDate date{ 2020, 01, 03 }; + EXPECT_EQ(StringUtils::toISO8601String(date), "2020-01-03"); +} + +TEST(Stringutils, dateTime) +{ + const Wt::WDateTime dateTime{ Wt::WDate {2020, 01, 03 }, Wt::WTime{9, 8, 11, 75} }; + EXPECT_EQ(StringUtils::toISO8601String(dateTime), "2020-01-03T09:08:11.075"); +} \ No newline at end of file