Skip to content
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
4 changes: 4 additions & 0 deletions api/codegen.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,10 @@ types:
TextMention: text_mention
CustomEmoji: custom_emoji
DateTime: date_time
RichText:
extra_union_types:
- std::string
- std::vector<std::shared_ptr<RichText>>
Sticker:
fields:
type:
Expand Down
8 changes: 5 additions & 3 deletions api_codegen/generate.py
Original file line number Diff line number Diff line change
Expand Up @@ -307,7 +307,7 @@ def _dependencies(

@staticmethod
def _standard_headers(fields: tuple[_FieldModel, ...], union_members: tuple[str, ...]) -> tuple[str, ...]:
cpp_types = " ".join(field.cpp_type for field in fields)
cpp_types = " ".join([field.cpp_type for field in fields] + list(union_members))
headers = {"memory"}
for cpp_type, header in (
("std::int", "cstdint"),
Expand Down Expand Up @@ -348,13 +348,15 @@ def _union_members(self, name: str, schema: Schema) -> tuple[str, ...]:
if schema.get("properties"):
return ()

return tuple(
members = tuple(
dict.fromkeys(
line.strip()
f"std::shared_ptr<{line.strip()}>"
for line in schema.get("description", "").splitlines()
if line.strip() in self._names and line.strip() != name
)
)
# Append extra representations to preserve existing variant indices and defaults.
return members + tuple(self._type_config.get(name, {}).get("extra_union_types", ()))


class _MethodModelBuilder(_BaseModelBuilder):
Expand Down
2 changes: 1 addition & 1 deletion api_codegen/templates/types.h.j2
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ struct {{ type.name }} {
{% if type.union_members %}
std::variant<
{% for member in type.union_members %}
std::shared_ptr<{{ member }}>{{ "," if not loop.last else "" }}
{{ member }}{{ "," if not loop.last else "" }}
{% endfor %}
>
value;
Expand Down
21 changes: 21 additions & 0 deletions api_codegen/tests/test_generate.py
Original file line number Diff line number Diff line change
Expand Up @@ -290,6 +290,27 @@ def test_union_header_includes_variant() -> None:
assert "variant" in _TypeModelBuilder._standard_headers((), ("Message",))


def test_rich_text_union_appends_plain_text_and_recursive_array() -> None:
components = {
"RichText": {"description": "String or Array of RichText, or:\nRichTextBold\nRichTextItalic"},
"RichTextBold": {},
"RichTextItalic": {},
}
rich_text = next(model for model in _TypeModelBuilder(components).build() if model.name == "RichText")

assert rich_text.union_members == (
"std::shared_ptr<RichTextBold>",
"std::shared_ptr<RichTextItalic>",
"std::string",
"std::vector<std::shared_ptr<RichText>>",
)
assert rich_text.dependencies == ("RichTextBold", "RichTextItalic")
assert {"string", "vector", "variant"} <= set(rich_text.standard_headers)
header = _OpenApiGenerator._render("types.h.j2", types=(rich_text,), methods=())
assert "std::vector<std::shared_ptr<RichText>>" in header
assert "std::shared_ptr<std::string>" not in header


def test_generator_renders_types_methods_and_documentation(
tmp_path: Path,
) -> None:
Expand Down
2 changes: 1 addition & 1 deletion include/tgbot/Types.h
Original file line number Diff line number Diff line change
Expand Up @@ -11576,7 +11576,7 @@ struct RichText {
std::shared_ptr<RichTextBankCardNumber>, std::shared_ptr<RichTextMention>, std::shared_ptr<RichTextHashtag>,
std::shared_ptr<RichTextCashtag>, std::shared_ptr<RichTextBotCommand>, std::shared_ptr<RichTextButton>,
std::shared_ptr<RichTextAnchor>, std::shared_ptr<RichTextAnchorLink>, std::shared_ptr<RichTextReference>,
std::shared_ptr<RichTextReferenceLink>>
std::shared_ptr<RichTextReferenceLink>, std::string, std::vector<std::shared_ptr<RichText>>>
value;
};

Expand Down
47 changes: 47 additions & 0 deletions tests/tgbot/TypesJson.cpp
Original file line number Diff line number Diff line change
@@ -1,11 +1,58 @@
#include <memory>
#include <string>
#include <type_traits>

#include <gtest/gtest.h>
#include <nlohmann/json.hpp>

#include "tgbot/Types.h"

TEST(TypesJson, RichTextPreservesExistingVariantIndicesAndDefault) {
using Value = decltype(TgBot::RichText::value);
static_assert(std::is_same_v<std::variant_alternative_t<0, Value>, std::shared_ptr<TgBot::RichTextBold>>);
static_assert(std::is_same_v<std::variant_alternative_t<25, Value>, std::shared_ptr<TgBot::RichTextReferenceLink>>);
const TgBot::RichText text;
EXPECT_EQ(text.value.index(), 0);
}

TEST(TypesJson, RichTextRoundTripsPlainText) {
for (const auto& text : { std::string(), std::string("plain text") }) {
const nlohmann::json json = text;
const auto richText = json.get<TgBot::RichText>();
ASSERT_TRUE(std::holds_alternative<std::string>(richText.value));
EXPECT_EQ(std::get<std::string>(richText.value), text);
EXPECT_EQ(nlohmann::json(richText), json);
}
}

TEST(TypesJson, RichTextRoundTripsRecursiveMixedArray) {
const auto json = nlohmann::json::parse(R"(["plain text", {"type":"bold","text":"bold text"}, ["nested", []]])");
const auto richText = json.get<TgBot::RichText>();
const auto& items = std::get<std::vector<TgBot::RichText::Ptr>>(richText.value);
ASSERT_EQ(items.size(), 3);
ASSERT_TRUE(items[0]);
EXPECT_EQ(std::get<std::string>(items[0]->value), "plain text");
EXPECT_EQ(nlohmann::json(richText), json);
}

TEST(TypesJson, RichTextRoundTripsFormattedObjectAndParagraph) {
const auto bold = nlohmann::json::parse(R"({"type":"bold","text":"plain text"})");
const auto richText = bold.get<TgBot::RichText>();
EXPECT_TRUE(std::holds_alternative<std::shared_ptr<TgBot::RichTextBold>>(richText.value));
EXPECT_EQ(nlohmann::json(richText), bold);

const auto paragraph = nlohmann::json::parse(R"({"type":"paragraph","text":"plain text"})");
EXPECT_EQ(nlohmann::json(paragraph.get<TgBot::RichBlockParagraph>()), paragraph);
}

TEST(TypesJson, RichTextRejectsInvalidRepresentations) {
for (const auto& input :
{ "null", "42", "true", R"({"type":"unknown"})", "[null]", "[42]", R"({"type":"bold","text":false})" }) {
SCOPED_TRACE(input);
EXPECT_THROW(nlohmann::json::parse(input).get<TgBot::RichText>(), std::invalid_argument);
}
}

TEST(TypesJson, GeneratedDiscriminatorHasLegacyConstantAndDefault) {
TgBot::InlineQueryResultCachedAudio result;

Expand Down
Loading