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

std::variant as vocabulary type #257

Closed
rbroggi opened this issue Jul 4, 2020 · 3 comments
Closed

std::variant as vocabulary type #257

rbroggi opened this issue Jul 4, 2020 · 3 comments

Comments

@rbroggi
Copy link

rbroggi commented Jul 4, 2020

Dear Daniel,

While using the library I've realized that it seems the lib does not have built-in support for std::variant (as it has for std::optional for example). I think it could be a handy feature to support something like the piece of code below:

class ItemTypeOne {
 public:
  const std::string& GetId() const {
    return _id;
  }
  const std::string& GetContent() const {
    return _content;
  }

 private:
  JSONCONS_TYPE_TRAITS_FRIEND;
  std::string _id;
  std::string _content;
};

class ItemTypeTwo {
 public:
  const std::string& GetName() const {
    return _name;
  }

 private:
  JSONCONS_TYPE_TRAITS_FRIEND;
  std::string _name;
};

class Request {
 public:
  const std::optional<std::string>& GetDomain() const {
    return _domain;
  }
  const std::vector<std::variant<acv::ItemTypeOne, acv::ItemTypeTwo>>& GetItems() const {
    return _items;
  }

 private:
  JSONCONS_TYPE_TRAITS_FRIEND;
  std::optional<std::string> _domain;
  std::vector<std::variant<acv::ItemTypeOne, acv::ItemTypeTwo>> _items;
};

JSONCONS_ALL_MEMBER_NAME_TRAITS(acv::ItemTypeOne, (_id, "id"), (_content, "content"));
JSONCONS_ALL_MEMBER_NAME_TRAITS(acv::ItemTypeTwo, (_name, "name"));
JSONCONS_ALL_MEMBER_NAME_TRAITS(acv::Request, (_items, "item"), (_domain, "domain") );

Having std::variant instead of 'polimorphism' has several advantages and I would love seeing jsoncons integrating it as a vocabulary type.

Thank you once again for the amazing product and job,

Rodrigo

@danielaparker
Copy link
Owner

danielaparker commented Jul 7, 2020

Could you provide some sample JSON which match your classes, that I can use for a test case?

Thanks,
Daniel

@rbroggi
Copy link
Author

rbroggi commented Jul 8, 2020

Hi Daniel, sure!

So let's assume first this cpp structure:

enum class Color { YELLOW, RED, GREEN, BLUE };
JSONCONS_ENUM_NAME_TRAITS(Color, (YELLOW, "YELLOW"), (RED, "RED"), (GREEN, "GREEN"), (BLUE, "BLUE"));

class Fruit {
 private:
  JSONCONS_TYPE_TRAITS_FRIEND;
  std::string name;
  Color color;
};
JSONCONS_ALL_MEMBER_NAME_TRAITS(Fruit,
                                (name, "name"),
                                (color, "color"));

class Indument {
 private:
  JSONCONS_TYPE_TRAITS_FRIEND;
  int size;
  std::string material;
};
JSONCONS_ALL_MEMBER_NAME_TRAITS(Indument,
                                (size, "size"),
                                (material, "material"));

class Basket {
 private:
  JSONCONS_TYPE_TRAITS_FRIEND;
  std::string owner;
  std::vector<std::variant<Fruit, Basket>> items;
};
JSONCONS_ALL_MEMBER_NAME_TRAITS(Basket,
                                (owner, "owner"),
                                (items, "items"));

I would expect the following json to be compliant (deserializable):

{
  "owner": "Rodrigo",
  "items": [
    {
      "name": "banana",
      "color": "YELLOW"
    },
    {
      "size": 40,
      "material": "wool"
    },
    {
      "name": "apple",
      "color": "RED"
    },
    {
      "size": 40,
      "material": "cotton"
    }
  ]
}

and the following json to be non-compliant:

{
  "owner": "Rodrigo",
  "items": [
    {
      "name": "banana",
      "color": "YELLOW"
    },
    {
      "size": 40,
      "color": "RED"
    },
    {
      "tittle": "what a feature!"
    },
    {
      "size": 40,
      "material": "cotton"
    }
  ]
}

Basically I would be able to have either Indument or Fruit in my array.
Of course the idea would be to have variant working with:

  • the other std::containers as well e.g. (std::unordered_map<std::string, std::variant<int, double>> ).

  • standalone (like optional)

does that help?

Thank you,
Rodrigo

@danielaparker
Copy link
Owner

danielaparker commented Jul 8, 2020

This feature is now supported on master provided that C++17 is detected.

#include <jsoncons/json.hpp>

namespace ns {

    enum class Color {yellow, red, green, blue};

    inline
    std::ostream& operator<<(std::ostream& os, Color val)
    {
        switch (val)
        {
            case Color::yellow: os << "yellow"; break;
            case Color::red: os << "red"; break;
            case Color::green: os << "green"; break;
            case Color::blue: os << "blue"; break;
        }
        return os;
    }

    class Fruit 
    {
    private:
        JSONCONS_TYPE_TRAITS_FRIEND
        std::string name_;
        Color color_;
    public:
        friend std::ostream& operator<<(std::ostream& os, const Fruit& val)
        {
            os << "name: " << val.name_ << ", color: " << val.color_ << "\n";
            return os;
        }
    };

    class Fabric 
    {
    private:
        JSONCONS_TYPE_TRAITS_FRIEND
        int size_;
        std::string material_;
    public:
        friend std::ostream& operator<<(std::ostream& os, const Fabric& val)
        {
            os << "size: " << val.size_ << ", material: " << val.material_ << "\n";
            return os;
        }
    };

    class Basket 
    {
    private:
        JSONCONS_TYPE_TRAITS_FRIEND
        std::string owner_;
        std::vector<std::variant<Fruit, Fabric>> items_;

    public:
        std::string owner() const
        {
            return owner_;
        }

        std::vector<std::variant<Fruit, Fabric>> items() const
        {
            return items_;
        }
    };

} // ns

JSONCONS_ENUM_NAME_TRAITS(ns::Color, (yellow, "YELLOW"), (red, "RED"), (green, "GREEN"), (blue, "BLUE"))

JSONCONS_ALL_MEMBER_NAME_TRAITS(ns::Fruit,
                                (name_, "name"),
                                (color_, "color"))
JSONCONS_ALL_MEMBER_NAME_TRAITS(ns::Fabric,
                                (size_, "size"),
                                (material_, "material"))
JSONCONS_ALL_MEMBER_NAME_TRAITS(ns::Basket,
                                (owner_, "owner"),
                                (items_, "items"))

int main()
{
    std::string input = R"(
{
  "owner": "Rodrigo",
  "items": [
    {
      "name": "banana",
      "color": "YELLOW"
    },
    {
      "size": 40,
      "material": "wool"
    },
    {
      "name": "apple",
      "color": "RED"
    },
    {
      "size": 40,
      "material": "cotton"
    }
  ]
}
    )";

    ns::Basket basket = jsoncons::decode_json<ns::Basket>(input);
    std::cout << basket.owner() << "\n\n";

    std::cout << "(1)\n";
    for (const auto& var : basket.items()) 
    {
        std::visit([](auto&& arg) {
            using T = std::decay_t<decltype(arg)>;
            if constexpr (std::is_same_v<T, ns::Fruit>)
                std::cout << "Fruit " << arg << '\n';
            else if constexpr (std::is_same_v<T, ns::Fabric>)
                std::cout << "Fabric " << arg << '\n';
        }, var);
    }

    std::string output;
    jsoncons::encode_json(basket, output, jsoncons::indenting::indent);
    std::cout << "(2)\n" << output << "\n\n";
}

Output:

Rodrigo

(1)
Fruit name: banana, color: yellow

Fabric size: 28, material: wool

Fruit name: apple, color: red

Fabric size: 28, material: cotton

(2)
{
    "items": [
        {
            "color": "YELLOW",
            "name": "banana"
        },
        {
            "material": "wool",
            "size": 40
        },
        {
            "color": "RED",
            "name": "apple"
        },
        {
            "material": "cotton",
            "size": 40
        }
    ],
    "owner": "Rodrigo"
}

For classes supported through the convenience macros, e.g. Fruit and Fabric, the type selection strategy is the same as for polymorphic types, and is based on the presence of mandatory members in the classes. More generally, the type selection strategy is based on the json_type_traits<Json,T>::is(const Json& j) function, checking each type in the variant from left to right, and stopping when json_type_traits<Json,T>::is(j) returns true.

Now consider

int main()
{
    using variant_type  = std::variant<int, double, bool, std::string, ns::Color>;

    variant_type var1(100);
    variant_type var2(10.1);
    variant_type var3(false);
    variant_type var4(std::string("Hello World"));
    variant_type var5(ns::Color::yellow);

    std::string buffer1;
    jsoncons::encode_json(var1,buffer1);
    std::string buffer2;
    jsoncons::encode_json(var2,buffer2);
    std::string buffer3;
    jsoncons::encode_json(var3,buffer3);
    std::string buffer4;
    jsoncons::encode_json(var4,buffer4);
    std::string buffer5;
    jsoncons::encode_json(var5,buffer5);

    std::cout << "(1) " << buffer1 << "\n";
    std::cout << "(2) " << buffer2 << "\n";
    std::cout << "(3) " << buffer3 << "\n";
    std::cout << "(4) " << buffer4 << "\n";
    std::cout << "(5) " << buffer5 << "\n";

    auto v1 = jsoncons::decode_json<variant_type>(buffer1);
    auto v2 = jsoncons::decode_json<variant_type>(buffer2);
    auto v3 = jsoncons::decode_json<variant_type>(buffer3);
    auto v4 = jsoncons::decode_json<variant_type>(buffer4);
    auto v5 = jsoncons::decode_json<variant_type>(buffer5);

    auto visitor = [](auto&& arg) {
            using T = std::decay_t<decltype(arg)>;
            if constexpr (std::is_same_v<T, int>)
                std::cout << "int " << arg << '\n';
            else if constexpr (std::is_same_v<T, double>)
                std::cout << "double " << arg << '\n';
            else if constexpr (std::is_same_v<T, bool>)
                std::cout << "bool " << arg << '\n';
            else if constexpr (std::is_same_v<T, std::string>)
                std::cout << "std::string " << arg << '\n';
            else if constexpr (std::is_same_v<T, ns::Color>)
                std::cout << "ns::Color " << arg << '\n';
        };

    std::cout << "\n";
    std::cout << "(6) ";
    std::visit(visitor, v1);
    std::cout << "(7) ";
    std::visit(visitor, v2);
    std::cout << "(8) ";
    std::visit(visitor, v3);
    std::cout << "(9) ";
    std::visit(visitor, v4);
    std::cout << "(10) ";
    std::visit(visitor, v5);
    std::cout << "\n\n";
}

Output:

(1) 100
(2) 10.1
(3) false
(4) "Hello World"
(5) "YELLOW"

(6) int 100
(7) double 10.1
(8) bool false
(9) std::string Hello World
(10) std::string YELLOW

Encode is fine. But when decoding, jsoncons checks if the JSON string "YELLOW" is a std::string before it checks whether it is an ns::Color, and since the answer is yes, it goes into the variant as a std::string.

But if we switch the order of ns::Color and std::string in the variant definition, viz.

 using variant_type  = std::variant<int, double, bool, ns::Color, std::string>;

strings containing the text "YELLOW", "RED", "GREEN", or "BLUE" are detected to be ns::Color,and the others std::string.

And the output becomes

(1) 100
(2) 10.1
(3) false
(4) "Hello World"
(5) "YELLOW"

(6) int 100
(7) double 10.1
(8) bool false
(9) std::string Hello World
(10) ns::Color yellow

So: types that are more constrained should appear to the left of types that are less constrained.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

2 participants