<repo_name>pisa-engine/wapopp<file_sep>/src/wapopp.cpp
#include <wapopp/detail.hpp>
#include <iostream>

using namespace wapopp;

template <class C, class Fn>
void append_content(nlohmann::json const &node, std::vector<Content> &contents, Fn read_content)
{
    auto content = read_content(node);
    if (detail::holds<C>(content)) {
        contents.push_back(detail::take<C>(std::move(content)));
    }
}

[[nodiscard]] auto Record::read(std::istream &is) -> Result
{
    std::string line;
    std::getline(is, line);
    try {
        nlohmann::json data = nlohmann::json::parse(line);
        std::vector<Content> contents;
        for (auto &&content_entry : data["contents"]) {
            if (auto type_pos = content_entry.find("type"); type_pos != content_entry.end()) {
                auto type = type_pos->get<std::string>();
                if (type == "kicker") {
                    append_content<Kicker>(
                        content_entry, contents, detail::read_simple_content<Kicker>);
                } else if (type == "title") {
                    append_content<Title>(
                        content_entry, contents, detail::read_simple_content<Title>);
                } else if (type == "byline") {
                    append_content<Byline>(
                        content_entry, contents, detail::read_simple_content<Byline>);
                } else if (type == "sanitized_html") {
                    append_content<Text>(
                        content_entry, contents, detail::read_simple_content<Text>);
                } else if (type == "date") {
                    append_content<Date>(content_entry, contents, detail::read_date);
                } else if (type == "author_info") {
                    append_content<AuthorInfo>(content_entry, contents, detail::read_author_info);
                } else if (type == "image") {
                    append_content<Image>(content_entry, contents, detail::read_image);
                }
            }
        }
        return Record{data["id"],
                      detail::read_field_or<std::string>(data, "article_url", ""),
                      detail::read_field_or<std::string>(data, "title", ""),
                      detail::read_field_or<std::string>(data, "author", ""),
                      detail::read_field_or<std::string>(data, "type", ""),
                      detail::read_field_or<std::string>(data, "source", ""),
                      detail::read_field_or<std::uint64_t>(data, "published_date", 0u),
                      std::move(contents)};
    } catch (nlohmann::detail::exception const& error) {
        return Error{error.what(), line};
    }
}
<file_sep>/CMakeLists.txt
cmake_minimum_required(VERSION 3.0)
project(wapopp VERSION 1.0 LANGUAGES CXX)

if (CMAKE_BUILD_TYPE)
    message(STATUS "CMAKE_BUILD_TYPE: ${CMAKE_BUILD_TYPE}")
endif()
include(cmake/verify_version.cmake)

#
# CONFIGURATION
#
set(WAPOPP_TARGET_NAME ${PROJECT_NAME})
set(WAPOPP_CONFIG_INSTALL_DIR "lib/cmake/${PROJECT_NAME}" CACHE INTERNAL "")
set(WAPOPP_INCLUDE_BUILD_DIR "${PROJECT_SOURCE_DIR}/include")
set(WAPOPP_INCLUDE_INSTALL_DIR "include")
set(WAPOPP_TARGETS_EXPORT_NAME "${PROJECT_NAME}Targets")
set(WAPOPP_CMAKE_CONFIG_TEMPLATE "cmake/config.cmake.in")
set(WAPOPP_CMAKE_CONFIG_DIR "${CMAKE_CURRENT_BINARY_DIR}")
set(WAPOPP_CMAKE_VERSION_CONFIG_FILE
    "${WAPOPP_CMAKE_CONFIG_DIR}/${PROJECT_NAME}ConfigVersion.cmake")
set(WAPOPP_CMAKE_PROJECT_CONFIG_FILE
    "${WAPOPP_CMAKE_CONFIG_DIR}/${PROJECT_NAME}Config.cmake")
set(WAPOPP_CMAKE_PROJECT_TARGETS_FILE
    "${WAPOPP_CMAKE_CONFIG_DIR}/${PROJECT_NAME}Targets.cmake")
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_EXTENSIONS OFF)

#
# OPTIONS
#
option(WAPOPP_ENABLE_TESTING "Enable testing of the library." ON)

#
# DEPENDENCIES
#
add_subdirectory(external)

#
# ADD LIBRARY
#
add_library(${WAPOPP_TARGET_NAME}
    "${PROJECT_SOURCE_DIR}/include/wapopp/wapopp.hpp"
    "${PROJECT_SOURCE_DIR}/src/wapopp.cpp")
target_link_libraries(${WAPOPP_TARGET_NAME} PRIVATE nlohmann_json::nlohmann_json)
target_include_directories(${WAPOPP_TARGET_NAME} PUBLIC
    $<BUILD_INTERFACE:${WAPOPP_INCLUDE_BUILD_DIR}>
    $<INSTALL_INTERFACE:${WAPOPP_INCLUDE_INSTALL_DIR}>
)

#
# TESTING
#
if (WAPOPP_ENABLE_TESTING AND BUILD_TESTING)
    enable_testing()
    add_subdirectory(test)
endif()

#
# INSTALL install header files, generate and install cmake config files for
# find_package()
#
include(CMakePackageConfigHelpers)
write_basic_package_version_file(${WAPOPP_CMAKE_VERSION_CONFIG_FILE}
                                 COMPATIBILITY SameMajorVersion)
configure_file(${WAPOPP_CMAKE_CONFIG_TEMPLATE} ${WAPOPP_CMAKE_PROJECT_CONFIG_FILE}
               @ONLY)

install(FILES "${WAPOPP_INCLUDE_BUILD_DIR}/${PROJECT_NAME}/wapopp.hpp"
        DESTINATION ${WAPOPP_INCLUDE_INSTALL_DIR})
install(FILES
        ${WAPOPP_CMAKE_PROJECT_CONFIG_FILE}
        ${WAPOPP_CMAKE_VERSION_CONFIG_FILE}
        DESTINATION ${WAPOPP_CONFIG_INSTALL_DIR})
export(TARGETS ${WAPOPP_TARGET_NAME}
       NAMESPACE ${PROJECT_NAME}::
       FILE ${WAPOPP_CMAKE_PROJECT_TARGETS_FILE})
install(TARGETS ${WAPOPP_TARGET_NAME}
        EXPORT ${WAPOPP_TARGETS_EXPORT_NAME}
        INCLUDES
        DESTINATION ${WAPOPP_INCLUDE_INSTALL_DIR}
        ARCHIVE DESTINATION lib
        LIBRARY DESTINATION lib
        COMPONENT library)
install(EXPORT ${WAPOPP_TARGETS_EXPORT_NAME}
        NAMESPACE ${PROJECT_NAME}::
        DESTINATION ${WAPOPP_CONFIG_INSTALL_DIR})
<file_sep>/external/CMakeLists.txt
EXECUTE_PROCESS(COMMAND git submodule update --init
                WORKING_DIRECTORY ${CMAKE_CURRENT_LIST_DIR}/..
                OUTPUT_QUIET
        )

# Add JSON for Modern C++
if(NOT TARGET nlohmann_json)
    set(JSON_BuildTests OFF CACHE INTERNAL "")
    add_subdirectory(json EXCLUDE_FROM_ALL)
endif()

# Add CLI11
if(NOT TARGET CLI11)
    set(CLI11_TESTING OFF CACHE INTERNAL "")
    add_subdirectory(CLI11)
endif()

# Add Catch
if(NOT TARGET Catch2)
    add_subdirectory(Catch2)
endif()
<file_sep>/test/CMakeLists.txt
add_executable(test_detail test_detail.cpp)
target_include_directories(test_detail PRIVATE "${PROJECT_SOURCE_DIR}/include/")
target_link_libraries(test_detail
    nlohmann_json
    Catch2
)
add_test(test_detail test_detail)

add_executable(test_wapopp test_wapopp.cpp)
target_link_libraries(test_wapopp
    wapopp
    nlohmann_json
    Catch2
)
add_test(test_wapopp test_wapopp)
<file_sep>/cmake/config.cmake.in
include(FindPackageHandleStandardArgs)
set(${CMAKE_FIND_PACKAGE_NAME}_CONFIG ${CMAKE_CURRENT_LIST_FILE})
find_package_handle_standard_args(@PROJECT_NAME@ CONFIG_MODE)

if(NOT TARGET wapopp::wapopp)
  include("${CMAKE_CURRENT_LIST_DIR}/wapoppTargets.cmake")
endif()
<file_sep>/include/wapopp/detail.hpp
#pragma once

#include <istream>
#include <ostream>
#include <sstream>
#include <string>
#include <variant>

#include <nlohmann/json.hpp>

#include "wapopp.hpp"

namespace wapopp {
namespace detail {

    // Due to AppleClang's lack of support
    template <class T, class... Types>
    [[nodiscard]] auto take(std::variant<Types...> &&v) -> T &&
    {
        if (auto ptr = std::get_if<T>(&v); ptr != nullptr) {
            return std::move(*ptr);
        } else {
            throw std::invalid_argument("Couldn't access required type");
        }
    }

    // Due to AppleClang's lack of support
    template <class T, class... Types>
    [[nodiscard]] auto holds(std::variant<Types...> const &v) -> bool
    {
        return std::get_if<T>(&v) != nullptr;
    }

    template <class T>
    [[nodiscard]] auto read_field_or(nlohmann::json const &node,
                                     std::string const &field,
                                     T default_value) -> T
    {
        if (auto pos = node.find(field); pos != node.end()) {
            try {
                return pos->get<T>();
            } catch (std::exception const &) {
                return default_value;
            }
        } else {
            return default_value;
        }
    }

    template <class T>
    [[nodiscard]] auto read_mandatory_field(nlohmann::json const &node, std::string const &field)
        -> std::variant<T, Error>
    {
        if (auto pos = node.find(field); pos != node.end()) {
            try {
                return pos->get<T>();
            } catch (std::exception const &) {
                std::ostringstream os;
                os << "Failed to parse " << field << " (" << *pos << ") to a requested type";
                return Error{os.str(), node.dump()};
            }
        } else {
            return Error{std::string("Missing ") + field, node.dump()};
        }
    }

    template <class T>
    [[nodiscard]] auto read_simple_content(nlohmann::json const &node) -> std::variant<T, Error>
    {
        auto content = read_mandatory_field<std::string>(node, "content");
        if (holds<Error>(content)) {
            return take<Error>(std::move(content));
        }
        auto mime = read_mandatory_field<std::string>(node, "mime");
        if (holds<Error>(mime)) {
            return take<Error>(std::move(mime));
        }
        return T{take<std::string>(std::move(content)), take<std::string>(std::move(mime))};
    }

    [[nodiscard]] auto read_date(nlohmann::json const &node) -> std::variant<Date, Error>
    {
        auto date = read_mandatory_field<std::uint64_t>(node, "content");
        if (holds<Error>(date)) {
            return take<Error>(std::move(date));
        }
        return Date{take<std::uint64_t>(std::move(date))};
    }

    [[nodiscard]] auto read_author_info(nlohmann::json const &node)
        -> std::variant<AuthorInfo, Error>
    {
        auto role = read_mandatory_field<std::string>(node, "role");
        if (holds<Error>(role)) {
            return take<Error>(std::move(role));
        }
        auto name = read_mandatory_field<std::string>(node, "name");
        if (holds<Error>(name)) {
            return take<Error>(std::move(name));
        }
        auto bio = read_mandatory_field<std::string>(node, "bio");
        if (holds<Error>(bio)) {
            return take<Error>(std::move(bio));
        }
        return AuthorInfo{take<std::string>(std::move(role)),
                          take<std::string>(std::move(name)),
                          take<std::string>(std::move(bio))};
    }

    [[nodiscard]] auto read_image(nlohmann::json const &node)
        -> std::variant<Image, Error>
    {
        auto caption = read_mandatory_field<std::string>(node, "fullcaption");
        if (holds<Error>(caption)) {
            return take<Error>(std::move(caption));
        }
        auto blurb = read_mandatory_field<std::string>(node, "blurb");
        if (holds<Error>(blurb)) {
            return take<Error>(std::move(blurb));
        }
        auto url = read_mandatory_field<std::string>(node, "imageURL");
        if (holds<Error>(url)) {
            return take<Error>(std::move(url));
        }
        auto height = read_mandatory_field<int>(node, "imageHeight");
        if (holds<Error>(height)) {
            return take<Error>(std::move(height));
        }
        auto width = read_mandatory_field<int>(node, "imageWidth");
        if (holds<Error>(width)) {
            return take<Error>(std::move(width));
        }
        return Image{take<std::string>(std::move(caption)),
                     take<std::string>(std::move(blurb)),
                     take<std::string>(std::move(url)),
                     take<int>(std::move(height)),
                     take<int>(std::move(width))};
    }

} // namespace detail

} // namespace wapopp
<file_sep>/include/wapopp/wapopp.hpp
#pragma once

#include <istream>
#include <ostream>
#include <string>
#include <variant>

namespace wapopp {

struct Error {
    std::string msg{};
    std::string json{};
};
struct Record;
using Result = std::variant<Record, Error>;

struct SimpleContent {
    std::string content;
    std::string mime;
};
struct Kicker : public SimpleContent {
};
struct Title : public SimpleContent {
};
struct Byline : public SimpleContent {
};
struct Text : public SimpleContent {
};
struct Date {
    std::uint64_t timestamp;
};
struct AuthorInfo {
    std::string role;
    std::string name;
    std::string bio;
};
struct Image {
    std::string caption;
    std::string blurb;
    std::string url;
    int height;
    int width;
};
using Content = std::variant<Kicker, Title, Byline, Text, Date, AuthorInfo, Image>;

struct Record {
    std::string id;
    std::string url;
    std::string title;
    std::string author;
    std::string type;
    std::string source;
    std::uint64_t published;
    std::vector<Content> contents;
    [[nodiscard]] static auto read(std::istream &is) -> Result;
};

} // namespace wapopp
<file_sep>/test/test_wapopp.cpp
#define CATCH_CONFIG_MAIN
#include <catch2/catch.hpp>

#include <nlohmann/json.hpp>

#include "wapopp/wapopp.hpp"

using namespace wapopp;

TEST_CASE("Read record", "[unit]")
{
    auto r =
        R"({"id": "b2e89334-33f9-11e1-825f-dabc29fd7071",)"
        R"("article_url": "https://www.washingtonpost.com/truncated.html",)"
        R"("title": "<NAME>, <NAME> are a perfect 1-2 punch for Virginia Tech",)"
        R"("author": null,)"
        R"("published_date": 1325376562000,)"
        R"("contents": [)"
        R"({)"
        R"(    "content":"Colleges",)"
        R"(    "mime":"text/plain",)"
        R"(    "type":"kicker")"
        R"(},)"
        R"({)"
        R"(    "content":"<NAME>, <NAME> are a perfect 1-2 punch for Virginia Tech",)"
        R"(    "mime":"text/plain",)"
        R"(    "type":"title")"
        R"(},)"
        R"({)"
        R"(    "fullcaption":"Virginia Tech wide receiver Danny Coale",)"
        R"(    "imageURL":"https://img.washingtonpost.com/somepic.jpg",)"
        R"(    "mime":"image/jpeg",)"
        R"(    "imageHeight":2260,)"
        R"(    "imageWidth":2775,)"
        R"(    "type":"image",)"
        R"(    "blurb":"Virginia Tech Blurb")"
        R"(},)"
        R"({)"
        R"(    "content":"By <NAME>",)"
        R"(    "mime":"text/plain",)"
        R"(    "type":"byline")"
        R"(},)"
        R"({)"
        R"(    "content":1325376562000,)"
        R"(    "mime":"text/plain",)"
        R"(    "type":"date")"
        R"(},)"
        R"({)"
        R"(    "content":"<span class=\"dateline\">NEW ORLEANS</span>.",)"
        R"(    "subtype":"paragraph",)"
        R"(    "type":"sanitized_html",)"
        R"(    "mime":"text/html")"
        R"(},)"
        R"({)"
        R"(    "corrupted":"content",)"
        R"(    "subtype":"paragraph",)"
        R"(    "type":"sanitized_html",)"
        R"(    "mime":"text/html")"
        R"(},)"
        R"({)"
        R"(    "type":"unknown")"
        R"(},)"
        R"({)"
        R"(    "role":"Reporter",)"
        R"(    "type":"author_info",)"
        R"(    "name":"<NAME>",)"
        R"(    "bio":"<NAME> bio")"
        R"(})"
        R"(],)"
        R"("type": "article",)"
        R"("source": "The Washington Post")"
        "}";
    std::istringstream is(r);
    auto result = Record::read(is);
    REQUIRE(std::get_if<Record>(&result) != nullptr);
    auto record = *std::get_if<Record>(&result);

    auto j = nlohmann::json::parse(r);
    REQUIRE(record.id == j["id"]);
    REQUIRE(record.url == j["article_url"]);
    REQUIRE(record.title == j["title"]);
    REQUIRE(record.author == "");
    REQUIRE(record.published == j["published_date"]);
    REQUIRE(record.type == j["type"]);
    REQUIRE(record.source == j["source"]);

    {
        REQUIRE(std::get_if<Kicker>(&record.contents[0]) != nullptr);
        auto kicker = *std::get_if<Kicker>(&record.contents[0]);
        REQUIRE(kicker.content == "Colleges");
        REQUIRE(kicker.mime == "text/plain");
    }
    {
        REQUIRE(std::get_if<Title>(&record.contents[1]) != nullptr);
        auto title = *std::get_if<Title>(&record.contents[1]);
        REQUIRE(title.content ==
                "<NAME>, <NAME> are a perfect 1-2 punch for Virginia Tech");
        REQUIRE(title.mime == "text/plain");
    }
    {
        REQUIRE(std::get_if<Image>(&record.contents[2]) != nullptr);
        auto image = *std::get_if<Image>(&record.contents[2]);
        REQUIRE(image.caption == "Virginia Tech wide receiver <NAME>");
        REQUIRE(image.url == "https://img.washingtonpost.com/somepic.jpg");
        REQUIRE(image.height == 2260);
        REQUIRE(image.width == 2775);
        REQUIRE(image.blurb == "Virginia Tech Blurb");
    }
    {
        REQUIRE(std::get_if<Byline>(&record.contents[3]) != nullptr);
        auto byline = *std::get_if<Byline>(&record.contents[3]);
        REQUIRE(byline.content == "By <NAME>");
        REQUIRE(byline.mime == "text/plain");
    }
    {
        REQUIRE(std::get_if<Date>(&record.contents[4]) != nullptr);
        auto date = *std::get_if<Date>(&record.contents[4]);
        REQUIRE(date.timestamp == 1325376562000);
    }
    {
        REQUIRE(std::get_if<Text>(&record.contents[5]) != nullptr);
        auto text = *std::get_if<Text>(&record.contents[5]);
        REQUIRE(text.content == "<span class=\"dateline\">NEW ORLEANS</span>.");
        REQUIRE(text.mime == "text/html");
    }
    {
        REQUIRE(std::get_if<AuthorInfo>(&record.contents[6]) != nullptr);
        auto author_info = *std::get_if<AuthorInfo>(&record.contents[6]);
        REQUIRE(author_info.role == "Reporter");
        REQUIRE(author_info.name == "<NAME>");
        REQUIRE(author_info.bio == "<NAME>'s bio");
    }
}
<file_sep>/test/test_detail.cpp
#define CATCH_CONFIG_MAIN
#include <catch2/catch.hpp>

#include "wapopp/detail.hpp"

using namespace wapopp;
using namespace wapopp::detail;

TEST_CASE("Read simple content", "[unit]")
{
    SECTION("Missing content")
    {
        auto j = R"({
            "mime": "text/plain",
            "type": "kicker"
        })"_json;
        auto content = read_simple_content<Kicker>(j);
        REQUIRE(std::get_if<Error>(&content) != nullptr);
        REQUIRE(std::get_if<Error>(&content)->msg == "Missing content");
    }
    SECTION("Missing mime")
    {
        auto j = R"({
            "content": "Colleges",
            "type": "kicker"
        })"_json;
        auto content = read_simple_content<Kicker>(j);
        REQUIRE(std::get_if<Error>(&content) != nullptr);
        REQUIRE(std::get_if<Error>(&content)->msg == "Missing mime");
    }
    SECTION("Kicker")
    {
        auto j = R"({
            "content": "Colleges",
            "mime": "text/plain",
            "type": "kicker"
        })"_json;
        auto content = read_simple_content<Kicker>(j);
        REQUIRE(std::get_if<Kicker>(&content) != nullptr);
        REQUIRE(std::get_if<Kicker>(&content)->content == j["content"]);
        REQUIRE(std::get_if<Kicker>(&content)->mime == j["mime"]);
    }
    SECTION("Title")
    {
        auto j = R"({
            "content": "<NAME>, <NAME> are a perfect 1-2 punch for Virginia Tech",
            "mime": "text/plain",
            "type": "title"
        })"_json;
        auto content = read_simple_content<Title>(j);
        REQUIRE(std::get_if<Title>(&content) != nullptr);
        REQUIRE(std::get_if<Title>(&content)->content == j["content"]);
        REQUIRE(std::get_if<Title>(&content)->mime == j["mime"]);
    }
    SECTION("Byline")
    {
        auto j = R"({
            "content": "By <NAME>",
            "mime": "text/plain",
            "type": "title"
        })"_json;
        auto content = read_simple_content<Byline>(j);
        REQUIRE(std::get_if<Byline>(&content) != nullptr);
        REQUIRE(std::get_if<Byline>(&content)->content == j["content"]);
        REQUIRE(std::get_if<Byline>(&content)->mime == j["mime"]);
    }
    SECTION("Text")
    {
        auto j = R"({
            "content": "<span>paragraph</span>",
            "subtype":"paragraph",
            "mime": "text/html",
            "type": "sanitized_html"
        })"_json;
        auto content = read_simple_content<Text>(j);
        REQUIRE(std::get_if<Text>(&content) != nullptr);
        REQUIRE(std::get_if<Text>(&content)->content == j["content"]);
        REQUIRE(std::get_if<Text>(&content)->mime == j["mime"]);
    }
}

TEST_CASE("Read date content", "[unit]")
{
    SECTION("Missing date field")
    {
        auto j = R"({
            "mime": "text/plain",
            "type": "date"
        })"_json;
        auto content = read_date(j);
        REQUIRE(std::get_if<Error>(&content) != nullptr);
        REQUIRE(std::get_if<Error>(&content)->msg == "Missing content");
    }
    SECTION("Correct date")
    {
        auto j = R"({
            "mime": "text/plain",
            "content": 1325376562000,
            "type": "date"
        })"_json;
        auto content = read_date(j);
        REQUIRE(std::get_if<Date>(&content) != nullptr);
        REQUIRE(std::get_if<Date>(&content)->timestamp == j["content"]);
    }
    SECTION("Corrupted date")
    {
        auto j = R"({
            "mime": "text/plain",
            "content": "oops",
            "type": "date"
        })"_json;
        auto content = read_date(j);
        REQUIRE(std::get_if<Error>(&content) != nullptr);
        REQUIRE(std::get_if<Error>(&content)->msg ==
                "Failed to parse content (\"oops\") to a requested type");
    }
}

TEST_CASE("Read author info", "[unit]")
{
    SECTION("Correct author info")
    {
        auto j = R"({
            "role":"Reporter",
            "type":"author_info",
            "name":"<NAME>",
            "bio":"<NAME> is a Montgomery County native ..."
        })"_json;
        auto content = read_author_info(j);
        REQUIRE(std::get_if<AuthorInfo>(&content) != nullptr);
        REQUIRE(std::get_if<AuthorInfo>(&content)->role == "Reporter");
        REQUIRE(std::get_if<AuthorInfo>(&content)->name == "<NAME>");
        REQUIRE(std::get_if<AuthorInfo>(&content)->bio ==
                "<NAME> is a Montgomery County native ...");
    }
    SECTION("Missing role")
    {
        auto j = R"({
            "type":"author_info",
            "name":"<NAME>",
            "bio":"<NAME> is a Montgomery County native ..."
        })"_json;
        auto content = read_author_info(j);
        REQUIRE(std::get_if<Error>(&content) != nullptr);
        REQUIRE(std::get_if<Error>(&content)->msg == "Missing role");
    }
    SECTION("Missing name")
    {
        auto j = R"({
            "role":"Reporter",
            "type":"author_info",
            "bio":"<NAME> is a Montgomery County native ..."
        })"_json;
        auto content = read_author_info(j);
        REQUIRE(std::get_if<Error>(&content) != nullptr);
        REQUIRE(std::get_if<Error>(&content)->msg == "Missing name");
    }
    SECTION("Missing bio")
    {
        auto j = R"({
            "role": "Reporter",
            "type": "author_info",
            "name": "<NAME>"
        })"_json;
        auto content = read_author_info(j);
        REQUIRE(std::get_if<Error>(&content) != nullptr);
        REQUIRE(std::get_if<Error>(&content)->msg == "Missing bio");
    }
}

TEST_CASE("Read image", "[unit]")
{
    SECTION("Correct image")
    {
        auto j = R"({
            "fullcaption": "Full Caption",
            "imageURL": "https://img.washingtonpost.com/somepic.jpg",
            "mime": "image/jpeg",
            "imageHeight": 2260,
            "imageWidth": 2775,
            "type": "image",
            "blurb": "Virginia Tech wide receiver Danny Coale..."
        })"_json;
        auto content = read_image(j);
        if (holds<Error>(content)) {
            std::cerr << take<Error>(std::move(content)).msg << std::endl;
            FAIL("");
        }
        REQUIRE(std::get_if<Image>(&content) != nullptr);
        REQUIRE(std::get_if<Image>(&content)->caption == "Full Caption");
        REQUIRE(std::get_if<Image>(&content)->url == "https://img.washingtonpost.com/somepic.jpg");
        REQUIRE(std::get_if<Image>(&content)->blurb ==
                "Virginia Tech wide receiver Danny Coale...");
        REQUIRE(std::get_if<Image>(&content)->height == 2260);
        REQUIRE(std::get_if<Image>(&content)->width == 2775);
    }
}
