Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adding generic version entities for SFS API Responses #91

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 25 additions & 17 deletions client/src/SFSClient.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

#include "SFSClient.h"

#include "details/ContentUtil.h"
#include "details/ErrorHandling.h"
#include "details/ReportingHandler.h"
#include "details/SFSClientImpl.h"
Expand All @@ -11,6 +12,25 @@

using namespace SFS;
using namespace SFS::details;
using namespace SFS::details::contentutil;

namespace
{
void ValidateRequestParams(const RequestParams& requestParams)
{
THROW_CODE_IF(InvalidArg, requestParams.productRequests.empty(), "productRequests cannot be empty");

// TODO #78: Add support for multiple product requests
THROW_CODE_IF(NotImpl,
requestParams.productRequests.size() > 1,
"There cannot be more than 1 productRequest at the moment");

for (const auto& [product, _] : requestParams.productRequests)
{
THROW_CODE_IF(InvalidArg, product.empty(), "product cannot be empty");
}
}
} // namespace

// Defining the constructor and destructor here allows us to use a unique_ptr to SFSClientImpl in the header file
SFSClient::SFSClient() noexcept = default;
Expand Down Expand Up @@ -42,22 +62,7 @@ Result SFSClient::GetLatestDownloadInfo(const RequestParams& requestParams,
std::unique_ptr<Content>& content) const noexcept
try
{
if (requestParams.productRequests.empty())
{
return Result(Result::InvalidArg, "productRequests cannot be empty");
}

// TODO #78: Add support for multiple product requests
if (requestParams.productRequests.size() > 1)
{
return Result(Result::NotImpl, "There cannot be more than 1 productRequest at the moment");
}

const auto& [product, targetingAttributes] = requestParams.productRequests[0];
if (product.empty())
{
return Result(Result::InvalidArg, "product cannot be empty");
}
ValidateRequestParams(requestParams);

// TODO #50: Adapt retrieval to storeapps flow with pre-requisites once that is implemented server-side

arthuraraujo-msft marked this conversation as resolved.
Show resolved Hide resolved
arthuraraujo-msft marked this conversation as resolved.
Show resolved Hide resolved
Expand All @@ -66,7 +71,10 @@ try
connectionConfig.baseCV = requestParams.baseCV;
const auto connection = m_impl->MakeConnection(connectionConfig);

auto contentId = m_impl->GetLatestVersion(requestParams.productRequests[0], *connection);
auto versionEntity = m_impl->GetLatestVersion(requestParams.productRequests[0], *connection);
arthuraraujo-msft marked this conversation as resolved.
Show resolved Hide resolved
auto contentId = GenericVersionEntityToContentId(std::move(*versionEntity), m_impl->GetReportingHandler());

const auto& product = requestParams.productRequests[0].product;
arthuraraujo-msft marked this conversation as resolved.
Show resolved Hide resolved
auto files = m_impl->GetDownloadInfo(product, contentId->GetVersion(), *connection);

std::unique_ptr<Content> tmp;
Expand Down
50 changes: 44 additions & 6 deletions client/src/details/ContentUtil.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -37,27 +37,65 @@ void ThrowInvalidResponseIfFalse(bool condition, const std::string& message, con
{
THROW_CODE_IF_LOG(ServiceInvalidResponse, !condition, handler, message);
}

void ValidateContentType(const VersionEntity& versionEntity, ContentType expectedType, const ReportingHandler& handler)
{
THROW_CODE_IF_LOG(Result::ServiceInvalidResponse,
versionEntity.GetContentType() != expectedType,
handler,
"Unexpected content type returned by the service");
}
} // namespace

std::unique_ptr<ContentId> contentutil::ContentIdJsonToObj(const json& contentId, const ReportingHandler& handler)
std::unique_ptr<VersionEntity> contentutil::ParseJsonToVersionEntity(const nlohmann::json& data,
const ReportingHandler& handler)
{
// Expected format for a generic version entity:
// {
// "ContentId": {
// "Namespace": <ns>,
// "Name": <name>,
// "Version": <version>
// }
// }
//

// TODO #50: Use a different entity once the service supports app content.

std::unique_ptr<GenericVersionEntity> tmp = std::make_unique<GenericVersionEntity>();

ThrowInvalidResponseIfFalse(data.is_object(), "Response is not a JSON object", handler);
ThrowInvalidResponseIfFalse(data.contains("ContentId"), "Missing ContentId in response", handler);

const auto& contentId = data["ContentId"];
ThrowInvalidResponseIfFalse(contentId.is_object(), "ContentId is not a JSON object", handler);

ThrowInvalidResponseIfFalse(contentId.contains("Namespace"), "Missing ContentId.Namespace in response", handler);
ThrowInvalidResponseIfFalse(contentId["Namespace"].is_string(), "ContentId.Namespace is not a string", handler);
std::string nameSpace = contentId["Namespace"];
tmp->contentId.nameSpace = contentId["Namespace"];

ThrowInvalidResponseIfFalse(contentId.contains("Name"), "Missing ContentId.Name in response", handler);
ThrowInvalidResponseIfFalse(contentId["Name"].is_string(), "ContentId.Name is not a string", handler);
std::string name = contentId["Name"];
tmp->contentId.name = contentId["Name"];

ThrowInvalidResponseIfFalse(contentId.contains("Version"), "Missing ContentId.Version in response", handler);
ThrowInvalidResponseIfFalse(contentId["Version"].is_string(), "ContentId.Version is not a string", handler);
std::string version = contentId["Version"];
tmp->contentId.version = contentId["Version"];

std::unique_ptr<ContentId> tmp;
THROW_IF_FAILED_LOG(ContentId::Make(std::move(nameSpace), std::move(name), std::move(version), tmp), handler);
return tmp;
}

std::unique_ptr<ContentId> contentutil::GenericVersionEntityToContentId(VersionEntity&& entity,
const ReportingHandler& handler)
{
ValidateContentType(entity, ContentType::Generic, handler);

std::unique_ptr<ContentId> tmp;
THROW_IF_FAILED_LOG(ContentId::Make(std::move(entity.contentId.nameSpace),
std::move(entity.contentId.name),
std::move(entity.contentId.version),
tmp),
handler);
return tmp;
}

Expand Down
5 changes: 4 additions & 1 deletion client/src/details/ContentUtil.h
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
#pragma once

#include "Content.h"
#include "SFSEntities.h"

#include <nlohmann/json_fwd.hpp>

Expand All @@ -19,7 +20,9 @@ namespace contentutil
// JSON conversion utilities
//

std::unique_ptr<ContentId> ContentIdJsonToObj(const nlohmann::json& contentId, const ReportingHandler& handler);
std::unique_ptr<VersionEntity> ParseJsonToVersionEntity(const nlohmann::json& data, const ReportingHandler& handler);
std::unique_ptr<ContentId> GenericVersionEntityToContentId(VersionEntity&& entity, const ReportingHandler& handler);

std::unique_ptr<File> FileJsonToObj(const nlohmann::json& file, const ReportingHandler& handler);

//
Expand Down
133 changes: 64 additions & 69 deletions client/src/details/SFSClientImpl.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -60,26 +60,7 @@ json ParseServerMethodStringToJson(const std::string& data, const std::string& m
}
}

std::unique_ptr<ContentId> ConvertSingleProductVersionResponseToContentId(const json& data,
const ReportingHandler& handler)
{
// Expected format:
// {
// "ContentId": {
// "Namespace": <ns>,
// "Name": <name>,
// "Version": <version>
// }
// }
//

ThrowInvalidResponseIfFalse(data.is_object(), "Response is not a JSON object", handler);
ThrowInvalidResponseIfFalse(data.contains("ContentId"), "Missing ContentId in response", handler);

return ContentIdJsonToObj(data["ContentId"], handler);
}

std::vector<ContentId> ConvertLatestVersionBatchResponseToContentIds(const json& data, const ReportingHandler& handler)
VersionEntities ConvertLatestVersionBatchResponseToVersionEntities(const json& data, const ReportingHandler& handler)
{
// Expected format:
// [
Expand All @@ -97,15 +78,13 @@ std::vector<ContentId> ConvertLatestVersionBatchResponseToContentIds(const json&
ThrowInvalidResponseIfFalse(data.is_array(), "Response is not a JSON array", handler);
ThrowInvalidResponseIfFalse(data.size() > 0, "Response does not have the expected size", handler);

std::vector<ContentId> contentIds;
VersionEntities entities;
for (const auto& obj : data)
{
ThrowInvalidResponseIfFalse(obj.is_object(), "Array element is not a JSON object", handler);
ThrowInvalidResponseIfFalse(obj.contains("ContentId"), "Missing ContentId in response", handler);
contentIds.push_back(std::move(*ContentIdJsonToObj(obj["ContentId"], handler)));
entities.push_back(std::move(ParseJsonToVersionEntity(obj, handler)));
arthuraraujo-msft marked this conversation as resolved.
Show resolved Hide resolved
}

return contentIds;
return entities;
}

std::vector<File> ConvertDownloadInfoResponseToFileVector(const json& data, const ReportingHandler& handler)
Expand Down Expand Up @@ -144,9 +123,47 @@ std::vector<File> ConvertDownloadInfoResponseToFileVector(const json& data, cons
return tmp;
}

bool VerifyVersionResponseMatchesProduct(const ContentId& contentId, std::string_view nameSpace, std::string_view name)
bool VerifyVersionResponseMatchesProduct(const ContentIdEntity& contentId,
arthuraraujo-msft marked this conversation as resolved.
Show resolved Hide resolved
std::string_view nameSpace,
std::string_view name)
{
return contentId.nameSpace == nameSpace && contentId.name == name;
}

void ValidateVersionEntity(const VersionEntity& versionEntity,
const std::string& nameSpace,
const std::string& product,
const ReportingHandler& handler)
{
THROW_CODE_IF_LOG(ServiceInvalidResponse,
!VerifyVersionResponseMatchesProduct(versionEntity.contentId, nameSpace, product),
arthuraraujo-msft marked this conversation as resolved.
Show resolved Hide resolved
handler,
"Response does not match the requested product");
}

void ValidateBatchVersionEntity(const VersionEntities& versionEntities,
const std::string& nameSpace,
const std::unordered_set<std::string>& requestedProducts,
const ReportingHandler& handler)
{
return contentId.GetNameSpace() == nameSpace && contentId.GetName() == name;
for (const auto& entity : versionEntities)
{
THROW_CODE_IF_LOG(ServiceInvalidResponse,
requestedProducts.count(entity->contentId.name) == 0,
handler,
"Received product [" + entity->contentId.name +
"] which is not one of the requested products");
THROW_CODE_IF_LOG(ServiceInvalidResponse,
AreNotEqualI(entity->contentId.nameSpace, nameSpace),
handler,
"Received product [" + entity->contentId.name + "] with a namespace [" +
entity->contentId.nameSpace + "] that does not match the requested namespace");

LOG_INFO(handler,
arthuraraujo-msft marked this conversation as resolved.
Show resolved Hide resolved
"Received a response for product [%s] with version %s",
entity->contentId.name.c_str(),
arthuraraujo-msft marked this conversation as resolved.
Show resolved Hide resolved
entity->contentId.version.c_str());
}
}
} // namespace

Expand All @@ -170,8 +187,8 @@ SFSClientImpl<ConnectionManagerT>::SFSClientImpl(ClientConfig&& config)
}

template <typename ConnectionManagerT>
std::unique_ptr<ContentId> SFSClientImpl<ConnectionManagerT>::GetLatestVersion(const ProductRequest& productRequest,
Connection& connection) const
std::unique_ptr<VersionEntity> SFSClientImpl<ConnectionManagerT>::GetLatestVersion(const ProductRequest& productRequest,
Connection& connection) const
try
{
const auto& [product, attributes] = productRequest;
Expand All @@ -185,18 +202,17 @@ try
const std::string postResponse{connection.Post(url, body.dump())};
const json versionResponse = ParseServerMethodStringToJson(postResponse, "GetLatestVersion", m_reportingHandler);

auto contentId = ConvertSingleProductVersionResponseToContentId(versionResponse, m_reportingHandler);
THROW_CODE_IF_LOG(ServiceInvalidResponse,
!VerifyVersionResponseMatchesProduct(*contentId, m_nameSpace, product),
m_reportingHandler,
"(GetLatestVersion) Response does not match the requested product");
auto versionEntity = ParseJsonToVersionEntity(versionResponse, m_reportingHandler);
ValidateVersionEntity(*versionEntity, m_nameSpace, product, m_reportingHandler);

SFS_INFO("Received a response with version %s", versionEntity->contentId.version.c_str());

return contentId;
return versionEntity;
}
SFS_CATCH_LOG_RETHROW(m_reportingHandler)

template <typename ConnectionManagerT>
std::vector<ContentId> SFSClientImpl<ConnectionManagerT>::GetLatestVersionBatch(
VersionEntities SFSClientImpl<ConnectionManagerT>::GetLatestVersionBatch(
const std::vector<ProductRequest>& productRequests,
Connection& connection) const
try
Expand All @@ -206,12 +222,12 @@ try
SFS_INFO("Requesting latest version of multiple products from URL [%s]", url.c_str());

// Creating request body
std::unordered_set<std::string> products;
std::unordered_set<std::string> requestedProducts;
json body = json::array();
for (const auto& [product, attributes] : productRequests)
{
SFS_INFO("Product #%zu: [%s]", body.size() + size_t{1}, product.c_str());
products.insert(product);
requestedProducts.insert(product);

body.push_back({{"TargetingAttributes", attributes}, {"Product", product}});
}
Expand All @@ -223,35 +239,17 @@ try
const json versionResponse =
ParseServerMethodStringToJson(postResponse, "GetLatestVersionBatch", m_reportingHandler);

auto contentIds = ConvertLatestVersionBatchResponseToContentIds(versionResponse, m_reportingHandler);
auto entities = ConvertLatestVersionBatchResponseToVersionEntities(versionResponse, m_reportingHandler);
ValidateBatchVersionEntity(entities, m_nameSpace, requestedProducts, m_reportingHandler);

// Validating responses
for (const auto& contentId : contentIds)
{
THROW_CODE_IF_LOG(ServiceInvalidResponse,
products.count(contentId.GetName()) == 0,
m_reportingHandler,
"(GetLatestVersionBatch) Response of product [" + contentId.GetName() +
"] does not match the requested products");
THROW_CODE_IF_LOG(ServiceInvalidResponse,
AreNotEqualI(contentId.GetNameSpace(), m_nameSpace),
m_reportingHandler,
"(GetLatestVersionBatch) Response of product [" + contentId.GetName() +
"] does not match the requested namespace");

SFS_INFO("Received a response for product [%s] with version %s",
contentId.GetName().c_str(),
contentId.GetVersion().c_str());
}

return contentIds;
return entities;
}
SFS_CATCH_LOG_RETHROW(m_reportingHandler)

template <typename ConnectionManagerT>
std::unique_ptr<ContentId> SFSClientImpl<ConnectionManagerT>::GetSpecificVersion(const std::string& product,
const std::string& version,
Connection& connection) const
std::unique_ptr<VersionEntity> SFSClientImpl<ConnectionManagerT>::GetSpecificVersion(const std::string& product,
const std::string& version,
Connection& connection) const
try
{
const std::string url{
Expand All @@ -263,15 +261,12 @@ try

const json versionResponse = ParseServerMethodStringToJson(getResponse, "GetSpecificVersion", m_reportingHandler);

auto contentId = ConvertSingleProductVersionResponseToContentId(versionResponse, m_reportingHandler);
THROW_CODE_IF_LOG(ServiceInvalidResponse,
!VerifyVersionResponseMatchesProduct(*contentId, m_nameSpace, product),
m_reportingHandler,
"(GetSpecificVersion) Response does not match the requested product");
auto versionEntity = ParseJsonToVersionEntity(versionResponse, m_reportingHandler);
ValidateVersionEntity(*versionEntity, m_nameSpace, product, m_reportingHandler);

SFS_INFO("Received the expected response with version %s", contentId->GetVersion().c_str());
SFS_INFO("Received the expected response with version %s", versionEntity->contentId.version.c_str());

return contentId;
return versionEntity;
}
SFS_CATCH_LOG_RETHROW(m_reportingHandler)

Expand Down
Loading