diff --git a/cpp-generator/experimental/lidl_gen_cdylib.cpp b/cpp-generator/experimental/lidl_gen_cdylib.cpp index 05f2fd3..7e00fe3 100644 --- a/cpp-generator/experimental/lidl_gen_cdylib.cpp +++ b/cpp-generator/experimental/lidl_gen_cdylib.cpp @@ -97,13 +97,14 @@ QString jsonArgToStd(const TypeExpr& te, const QString& expr, const QString& pat const std::set& recs) { if (te.kind == TypeExpr::Primitive) { - if (te.name == "bstr") return "lidlBytesFromJson(" + expr + ")"; + if (te.name == "bstr") + return "logos::bytesFromJsonLenient(" + expr + ", \"" + path + "\")"; if (te.name == "any") return expr; } const QString cpp = lidlTypeToStdCdylib(te, recs); if (cpp == "LogosMap" || cpp == "LogosList") return expr; // untyped JSON passes through, as it always has - return "logos_gen::Codec<" + cpp + ">::from(" + expr + ", \"" + path + "\")"; + return "logos::fromJson<" + cpp + ">(" + expr + ", \"" + path + "\")"; } // std-typed return variable -> json expression @@ -133,7 +134,7 @@ QString stdReturnToJson(const MethodDecl& md, const QString& var, return var; // `nlohmann::json(v)` would serialize a vector as a plain number // array and a record not at all; the codec keeps bytes tagged at depth. - return "logos_gen::Codec<" + cppRet + ">::to(" + var + ")"; + return "logos::toJson<" + cppRet + ">(" + var + ")"; } // Qt-free spelling of a LIDL type. lidlTypeToStd() falls back to Qt containers @@ -193,131 +194,36 @@ QString lidlTypeToStdCdylib(const TypeExpr& te, const std::set& rec // // The primary template is intentionally left UNDEFINED: an unsupported T is a // compile error naming the type, never a silent default-constructed value. -void emitGeneratedCodec(QTextStream& s, const ModuleDecl& module, - const std::set& recs) +// Emits ONE specialization per record the module declares — and nothing else. +// +// The generic half (scalars, bstr, the vector/map composition, the error paths) +// used to be emitted here too, ~186 lines of C++-emitting-C++ that mirrored +// logos-protocol's logos_codec.h by hand. It no longer is: logos_json.h stopped +// defining byte helpers that collided with that header, so a module TU can now +// include the canonical codec directly. +// +// That duplication was not free. The two copies had drifted (the emitted integer +// decode gated on is_number() where the canonical one checked +// is_number_integer() || is_number_unsigned()), they disagreed on padded base64, +// and every codec fix had to be written twice or it silently only half-applied. +// +// What remains is irreducible: a LIDL `type` is a per-contract struct whose field +// names and member types exist only in this module's header, and C++17 has no +// field reflection. Nesting composes for free — Codec> and +// deeper come from the shared generic half once Codec<::Blob> exists. +void emitRecordCodecs(QTextStream& s, const ModuleDecl& module, + const std::set& recs) { - s << "namespace logos_gen {\n\n"; - s << "// Codec::to / ::from — the one place a value's wire form is decided.\n"; - s << "// The primary template is undefined on purpose: an unsupported T is a\n"; - s << "// compile error naming the type, not a silent default.\n"; - s << "template struct Codec;\n\n"; - - s << "[[noreturn]] inline void lidlTypeError(const char* want, const std::string& path,\n"; - s << " const nlohmann::json& got)\n{\n"; - s << " throw std::runtime_error(std::string(\"expected \") + want + \" at \" + path\n"; - s << " + \", got \" + std::string(got.type_name()));\n}\n\n"; - - // Scalars that need no more than a category check. - struct Scalar { const char* cpp; const char* want; const char* check; const char* get; }; - const Scalar scalars[] = { - {"std::string", "string", "is_string()", "get()"}, - {"double", "number", "is_number()", "get()"}, - {"bool", "boolean", "is_boolean()", "get()"}, - }; - for (const Scalar& sc : scalars) { - s << "template <> struct Codec<" << sc.cpp << "> {\n"; - s << " static nlohmann::json to(const " << sc.cpp << "& v) { return nlohmann::json(v); }\n"; - s << " static " << sc.cpp << " from(const nlohmann::json& j, const std::string& path) {\n"; - s << " if (!j." << sc.check << ") lidlTypeError(\"" << sc.want << "\", path, j);\n"; - s << " return j." << sc.get << ";\n }\n};\n\n"; - } - - // The integers are spelled out rather than driven from the table above, - // because a category check is not enough for them and the shortcuts are - // silent rather than loud: - // - // is_number() admits a FLOAT, and .get() TRUNCATES it — 3.7 - // arrived as 3 instead of being rejected. - // is_number() admits a NEGATIVE, and .get() WRAPS it — -1 - // arrived as 18446744073709551615, a sign flip on a nominal value. - // - // Both used to be pinned as conformance expectations, which made the C++ - // provider disagree with the Rust one (which rejects) on a contract they - // share. Rejecting is the correct half of that disagreement: a value the - // declared type cannot represent must not reach the author wearing another. - s << "template <> struct Codec {\n"; - s << " static nlohmann::json to(const int64_t& v) { return nlohmann::json(v); }\n"; - s << " static int64_t from(const nlohmann::json& j, const std::string& path) {\n"; - s << " if (j.is_number_float()) {\n"; - s << " const double d = j.get();\n"; - s << " double ip = 0.0;\n"; - s << " if (std::modf(d, &ip) != 0.0)\n"; - s << " lidlTypeError(\"integer\", path, j);\n"; - s << " if (d < -9223372036854775808.0 || d >= 9223372036854775808.0)\n"; - s << " lidlTypeError(\"signed integer in range\", path, j);\n"; - s << " return static_cast(d);\n"; - s << " }\n"; - s << " if (!j.is_number_integer() && !j.is_number_unsigned())\n"; - s << " lidlTypeError(\"integer\", path, j);\n"; - s << " if (j.is_number_unsigned()\n"; - s << " && j.get() > uint64_t(std::numeric_limits::max()))\n"; - s << " lidlTypeError(\"signed integer in range\", path, j);\n"; - s << " return j.get();\n }\n};\n\n"; - - s << "template <> struct Codec {\n"; - s << " static nlohmann::json to(const uint64_t& v) { return nlohmann::json(v); }\n"; - s << " static uint64_t from(const nlohmann::json& j, const std::string& path) {\n"; - s << " if (j.is_number_float()) {\n"; - s << " const double d = j.get();\n"; - s << " double ip = 0.0;\n"; - s << " if (std::modf(d, &ip) != 0.0)\n"; - s << " lidlTypeError(\"integer\", path, j);\n"; - s << " if (d < 0.0 || d >= 18446744073709551616.0)\n"; - s << " lidlTypeError(\"unsigned integer in range\", path, j);\n"; - s << " return static_cast(d);\n"; - s << " }\n"; - s << " if (!j.is_number_integer() && !j.is_number_unsigned())\n"; - s << " lidlTypeError(\"integer\", path, j);\n"; - s << " if (!j.is_number_unsigned() && j.get() < 0)\n"; - s << " lidlTypeError(\"unsigned integer\", path, j);\n"; - s << " return j.get();\n }\n};\n\n"; - - // bstr. The FULL specialization wins over the generic vector rule below, - // which is what keeps bytes tagged at every depth instead of being - // serialized as a plain array of numbers. - s << "template <> struct Codec> {\n"; - s << " static nlohmann::json to(const std::vector& v) { return logos::bytesToJson(v); }\n"; - s << " static std::vector from(const nlohmann::json& j, const std::string& path) {\n"; - s << " if (j.is_object() && j.size() == 1 && j.contains(\"_bytes\")\n"; - s << " && j.at(\"_bytes\").is_string())\n"; - s << " return logos::jsonToBytes(j);\n"; - s << " lidlTypeError(\"bytes\", path, j);\n }\n};\n\n"; - - // Untyped JSON passes through unchanged — `any`, and the LogosMap/LogosList - // aliases, are all nlohmann::json. - s << "template <> struct Codec {\n"; - s << " static nlohmann::json to(const nlohmann::json& v) { return v; }\n"; - s << " static nlohmann::json from(const nlohmann::json& j, const std::string&) { return j; }\n"; - s << "};\n\n"; - - s << "template struct Codec> {\n"; - s << " static nlohmann::json to(const std::vector& v) {\n"; - s << " nlohmann::json out = nlohmann::json::array();\n"; - s << " for (const T& e : v) out.push_back(Codec::to(e));\n"; - s << " return out;\n }\n"; - s << " static std::vector from(const nlohmann::json& j, const std::string& path) {\n"; - s << " if (!j.is_array()) lidlTypeError(\"array\", path, j);\n"; - s << " std::vector out;\n out.reserve(j.size());\n"; - s << " for (size_t i = 0; i < j.size(); ++i)\n"; - s << " out.push_back(Codec::from(j.at(i), path + \"[\" + std::to_string(i) + \"]\"));\n"; - s << " return out;\n }\n};\n\n"; - - s << "template struct Codec> {\n"; - s << " static nlohmann::json to(const std::map& v) {\n"; - s << " nlohmann::json out = nlohmann::json::object();\n"; - s << " for (const auto& kv : v) out[kv.first] = Codec::to(kv.second);\n"; - s << " return out;\n }\n"; - s << " static std::map from(const nlohmann::json& j, const std::string& path) {\n"; - s << " if (!j.is_object()) lidlTypeError(\"object\", path, j);\n"; - s << " std::map out;\n"; - s << " for (auto it = j.begin(); it != j.end(); ++it)\n"; - s << " out.emplace(it.key(), Codec::from(it.value(), path + \".\" + it.key()));\n"; - s << " return out;\n }\n};\n\n"; - + if (module.types.empty()) return; + // Reopened so the specializations land beside the primary template they + // specialize. `::Name` because the author's record types are at global + // scope, while this is namespace logos::detail — without the qualifier the + // name would resolve inside logos::. + s << "namespace logos { namespace detail {\n\n"; // One specialization per declared record. Field order follows the contract. for (const TypeDecl& t : module.types) { const QString name = qs(t.name); - s << "template <> struct Codec<" << name << "> {\n"; + s << "template <> struct Codec<::" << name << ", void> {\n"; s << " static nlohmann::json to(const " << name << "& v) {\n"; s << " nlohmann::json out = nlohmann::json::object();\n"; for (const FieldDecl& f : t.fields) { @@ -327,7 +233,7 @@ void emitGeneratedCodec(QTextStream& s, const ModuleDecl& module, } s << " return out;\n }\n"; s << " static " << name << " from(const nlohmann::json& j, const std::string& path) {\n"; - s << " if (!j.is_object()) lidlTypeError(\"object\", path, j);\n"; + s << " if (!j.is_object()) detail::typeError(path, \"object\", j);\n"; s << " " << name << " out;\n"; for (const FieldDecl& f : t.fields) { const QString ft = lidlTypeToStdCdylib(f.type, recs); @@ -342,7 +248,7 @@ void emitGeneratedCodec(QTextStream& s, const ModuleDecl& module, } s << " return out;\n }\n};\n\n"; } - s << "} // namespace logos_gen\n\n"; + s << "}} // namespace logos::detail\n\n"; } bool hasBytesEventParam(const ModuleDecl& module) @@ -394,7 +300,7 @@ bool hasJsonEventParam(const ModuleDecl& module) } // The SCALAR tagged-bytes helpers. A `[bstr]` (and bytes at any deeper -// nesting) rides logos_gen::Codec instead: its full specialization for +// nesting) rides logos::Codec instead: its full specialization for // std::vector beats the generic vector rule, so one mechanism covers // [bstr], [[bstr]] and {tstr: [bstr]} alike. #111 emitted a dedicated depth-1 // list codec here; the generic one subsumes it, and keeping both left an @@ -542,12 +448,10 @@ QString lidlMakeTypesHeaderCdylib(const ModuleDecl& module) s << "//\n"; s << "// rather than picking fields out of a LogosMap.\n"; s << "#pragma once\n"; - s << "#include \n"; + s << "#include \n"; // LogosMap / LogosList aliases + s << "#include \n"; // logos::Codec — the ONE definition s << "#include \n"; - s << "#include \n"; // the integer codecs accept whole-valued floats - s << "#include \n"; // ...and range-check s << "#include \n"; - s << "#include \n"; s << "#include \n"; s << "#include \n\n"; @@ -561,7 +465,7 @@ QString lidlMakeTypesHeaderCdylib(const ModuleDecl& module) s << "\n"; } - emitGeneratedCodec(s, module, recs); + emitRecordCodecs(s, module, recs); return c; } @@ -899,7 +803,7 @@ QString lidlMakeEventsSourceCdylib(const ModuleDecl& module, if (evStd != "LogosMap" && evStd != "LogosList" && (isRecord(pd.type, recsEv) || pd.type.kind == TypeExpr::Array || pd.type.kind == TypeExpr::Map)) { - s << " args.push_back(logos_gen::Codec<" << evStd << ">::to(" + s << " args.push_back(logos::toJson<" << evStd << ">(" << pd.name << "));\n"; continue; } diff --git a/cpp/logos_json.h b/cpp/logos_json.h index 90bdb3b..abfc2b0 100644 --- a/cpp/logos_json.h +++ b/cpp/logos_json.h @@ -1,91 +1,30 @@ #pragma once #include -#include -#include -#include -#include - // Semantic aliases for nlohmann::json used in universal module impl classes. // The code generator recognizes these names and emits QVariantMap / QVariantList // conversions in the Qt glue layer, so impl classes remain Qt-free. +// +// ALIASES ONLY, on purpose. This header also used to define b64UrlEncode, +// b64UrlDecode, bytesToJson and jsonToBytes — second copies of functions +// logos-protocol's logos_codec.h already owned. That was not merely duplication: +// the two sets had the same mangled names with weak linkage and DIFFERENT bodies, +// and both reached one program, because module TUs compiled these while +// liblogos_protocol.a carries TUs that included logos_codec.h. Which body won was +// down to link order. +// +// It also made logos_codec.h unincludable from any TU that wanted LogosMap — a +// redefinition error, since `inline` allows one definition per translation unit, +// not two. That is why the cdylib generator had to emit its own copy of the +// entire codec, and why every codec fix had to be written twice. +// +// The byte helpers now live where they belong: the canonical ones in +// logos-protocol's logos_codec.h, and the lenient `lp`-path jsonToBytes beside +// its sibling jsonToStringVec in logos_lp_client.h. +// +// Keeping this header dependency-free (nlohmann only) is deliberate. Some thirty +// alias-only include sites across the module repos would otherwise inherit an +// include path they have no use for, and logos-cpp-sdkConfig.cmake's "its only +// dependency is nlohmann_json" would stop being true. using LogosMap = nlohmann::json; using LogosList = nlohmann::json; - -namespace logos { - -// The canonical tagged form for binary payloads on the wire: -// -// {"_bytes": ""} -// -// It is what logos-protocol emits and expects (logos_json_convert.cpp, -// implementations/plain/json_mapping.cpp), and it is lossless for arbitrary -// bytes — including embedded NULs, which a plain JSON string would not survive. -// The Qt side reaches this form through QByteArray::toBase64/fromBase64 with -// Base64UrlEncoding | OmitTrailingEquals; these are the Qt-free equivalents, -// used by the generated `lp` wrappers and by universal (Qt-free) module code. - -inline std::string b64UrlEncode(const std::vector& bytes) -{ - static const char* alpha = - "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_"; - std::string out; - size_t i = 0; - while (i + 3 <= bytes.size()) { - uint32_t n = (uint32_t(bytes[i]) << 16) | (uint32_t(bytes[i + 1]) << 8) - | uint32_t(bytes[i + 2]); - out += alpha[(n >> 18) & 0x3f]; out += alpha[(n >> 12) & 0x3f]; - out += alpha[(n >> 6) & 0x3f]; out += alpha[n & 0x3f]; - i += 3; - } - if (i < bytes.size()) { - uint32_t n = uint32_t(bytes[i]) << 16; - if (i + 1 < bytes.size()) n |= uint32_t(bytes[i + 1]) << 8; - out += alpha[(n >> 18) & 0x3f]; out += alpha[(n >> 12) & 0x3f]; - if (i + 1 < bytes.size()) out += alpha[(n >> 6) & 0x3f]; - } - return out; -} - -inline std::vector b64UrlDecode(const std::string& in) -{ - auto idx = [](char ch) -> int { - if (ch >= 'A' && ch <= 'Z') return ch - 'A'; - if (ch >= 'a' && ch <= 'z') return ch - 'a' + 26; - if (ch >= '0' && ch <= '9') return ch - '0' + 52; - if (ch == '-') return 62; - if (ch == '_') return 63; - return -1; // skips '=' padding and any stray character - }; - std::vector out; - uint32_t buf = 0; - int bits = 0; - for (char ch : in) { - const int v = idx(ch); - if (v < 0) continue; - buf = (buf << 6) | static_cast(v); - bits += 6; - if (bits >= 8) { - bits -= 8; - out.push_back(static_cast((buf >> bits) & 0xff)); - } - } - return out; -} - -// Bytes -> the tagged JSON object. -inline nlohmann::json bytesToJson(const std::vector& bytes) -{ - return nlohmann::json{{"_bytes", b64UrlEncode(bytes)}}; -} - -// The tagged JSON object -> bytes. Lenient, like the rest of the `lp` decode -// path: anything that is not a well-formed tagged-bytes object yields empty. -inline std::vector jsonToBytes(const nlohmann::json& j) -{ - if (!j.is_object() || !j.contains("_bytes") || !j["_bytes"].is_string()) - return {}; - return b64UrlDecode(j["_bytes"].get()); -} - -} // namespace logos diff --git a/cpp/logos_lp_client.h b/cpp/logos_lp_client.h index 6e15d20..c9c3190 100644 --- a/cpp/logos_lp_client.h +++ b/cpp/logos_lp_client.h @@ -26,7 +26,8 @@ #include "logos_protocol.h" // lp_* C ABI #include "logos_call_error.h" // logos::CallError -#include "logos_json.h" // logos::bytesToJson / logos::jsonToBytes +#include "logos_json.h" // LogosMap / LogosList aliases +#include "logos_codec.h" // logos::bytesToJson, b64UrlDecode, isTaggedBytes #include "logos_result.h" // StdLogosResult namespace logos { @@ -43,8 +44,17 @@ inline std::vector jsonToStringVec(const nlohmann::json& j) { } // Binary payloads travel in the canonical tagged form -// {"_bytes": ""}; logos::bytesToJson / logos::jsonToBytes -// live in logos_json.h and are used by the generated wrappers on both sides. +// {"_bytes": ""}. Encoding is logos::bytesToJson from +// logos_codec.h — the one canonical definition. Decoding on this path is NOT +// the codec's: bytesFromJson throws and bytesFromJsonLenient also accepts a +// plain string, a number and an int array, whereas every `lp` decoder here is +// documented to yield the default-constructed value on a mismatch. So the lp +// decode keeps its own deliberately-narrow spelling, next to jsonToStringVec +// which has exactly the same contract. +inline std::vector jsonToBytes(const nlohmann::json& j) { + if (!isTaggedBytes(j)) return {}; + return b64UrlDecode(j["_bytes"].get()); +} inline StdLogosResult jsonToStdResult(const nlohmann::json& j) { StdLogosResult r; diff --git a/flake.lock b/flake.lock index 4b0e5b7..a8e0c88 100644 --- a/flake.lock +++ b/flake.lock @@ -55,11 +55,11 @@ ] }, "locked": { - "lastModified": 1785336618, - "narHash": "sha256-s+3ZB2cVUJe1+dEZzpQlD46w2xZWXu5L4K1v15HNhBk=", + "lastModified": 1785350411, + "narHash": "sha256-SH3BdM6Z6mW0M5DCyIGz+oo8IWq13qSQAdZuLMi916g=", "owner": "logos-co", "repo": "logos-protocol", - "rev": "c0df466172497741dfaa25d2e74a98947df60ada", + "rev": "43595575a3f94b07f1a33deb161ace1f62c37e3b", "type": "github" }, "original": { diff --git a/tests/experimental/test_lidl_gen_cdylib.cpp b/tests/experimental/test_lidl_gen_cdylib.cpp index 6fb13dc..4e5b3c0 100644 --- a/tests/experimental/test_lidl_gen_cdylib.cpp +++ b/tests/experimental/test_lidl_gen_cdylib.cpp @@ -162,7 +162,7 @@ TEST(LidlGenCdylib, ArrayOfBytesEventParamIsEligibleAndTagsEachElement) // Spelled Qt-free and encoded through the codec, so each element keeps its // canonical tag instead of becoming a plain array of numbers. EXPECT_TRUE(source.contains("std::vector>")) << source.toStdString(); - EXPECT_TRUE(source.contains("logos_gen::Codec>>::to(payloads)")) + EXPECT_TRUE(source.contains("logos::toJson>>(payloads)")) << source.toStdString(); // From #111, still exactly right: Qt-free, and taken by const-ref like the // other composite payloads. @@ -187,11 +187,13 @@ TEST(LidlGenCdylib, ArrayOfBytesMethodParamDecodesPerElement) const QString source = implSourceFor(m); - EXPECT_TRUE(source.contains("logos_gen::Codec>>::from(")) + EXPECT_TRUE(source.contains("logos::fromJson>>(")) << source.toStdString(); - // The scalar param still uses the scalar decoder — deliberately NOT routed - // through the codec, so its documented leniency is unchanged. - EXPECT_TRUE(source.contains("lidlBytesFromJson(")) << source.toStdString(); + // The scalar param decodes leniently too — and now through the SAME + // function as the nested one. It used to be a separate emitted helper, so a + // scalar bstr accepted a plain string while a [bstr] element rejected it: + // echoBytes("hi") worked and echoBytesList(["hi"]) threw, inside one module. + EXPECT_TRUE(source.contains("logos::bytesFromJsonLenient(")) << source.toStdString(); // nlohmann's blanket container decode must not be used for this type: it // refuses a tagged object and would silently accept a raw number array, // skipping the base64 decode entirely. @@ -211,7 +213,7 @@ TEST(LidlGenCdylib, ArrayOfBytesReturnTagsEachElement) ASSERT_TRUE(lidlCdylibSupported(m, &error)) << error.toStdString(); const QString source = implSourceFor(m); - EXPECT_TRUE(source.contains("logos_gen::Codec>>::to(")) + EXPECT_TRUE(source.contains("logos::toJson>>(")) << source.toStdString(); EXPECT_FALSE(source.contains("nlohmann::json(result)")) << source.toStdString(); } @@ -237,7 +239,7 @@ TEST(LidlGenCdylib, NoDedicatedListEncoderAndTheScalarOneStaysGated) const QString listed = eventsSourceFor(withList); // The list rides the codec; no bespoke list encoder is emitted at all. EXPECT_FALSE(listed.contains("lidlBytesListToJson")) << listed.toStdString(); - EXPECT_TRUE(listed.contains("logos_gen::Codec>>::to(")) + EXPECT_TRUE(listed.contains("logos::toJson>>(")) << listed.toStdString(); } @@ -297,9 +299,17 @@ TEST(LidlGenCdylib, OnlyDeclaredRecordsAreRecords) // redefinition. EXPECT_TRUE(types.contains("struct Blob;")) << types.toStdString(); EXPECT_FALSE(types.contains("struct Blob {")) << types.toStdString(); - EXPECT_TRUE(types.contains("template <> struct Codec")) << types.toStdString(); + // Specialized into logos::detail, beside the primary template it specializes, + // and spelled ::Blob because the author's struct is at global scope while + // this is namespace logos::detail. + EXPECT_TRUE(types.contains("template <> struct Codec<::Blob, void>")) << types.toStdString(); + EXPECT_TRUE(types.contains("namespace logos { namespace detail {")) << types.toStdString(); // The bstr field goes through the bytes codec, not nlohmann's array-of-numbers. EXPECT_TRUE(types.contains("Codec>::to(v.payload)")) << types.toStdString(); + // The generic half is NOT emitted any more — it comes from logos_codec.h. + EXPECT_TRUE(types.contains("#include ")) << types.toStdString(); + EXPECT_FALSE(types.contains("namespace logos_gen")) << types.toStdString(); + EXPECT_FALSE(types.contains("struct Codec")) << types.toStdString(); // An undeclared Named type is NOT a record and stays refused. MethodDecl bad; diff --git a/tests/sdk/CMakeLists.txt b/tests/sdk/CMakeLists.txt index 76944e8..26eb5f6 100644 --- a/tests/sdk/CMakeLists.txt +++ b/tests/sdk/CMakeLists.txt @@ -15,4 +15,13 @@ target_link_libraries(sdk_tests PRIVATE GTest::gtest_main ) +# logos_lp_client.h (and through it logos_codec.h / logos_protocol.h) lives in +# logos-protocol. nix/tests.nix already passes LOGOS_PROTOCOL_ROOT; this is what +# plumbs it into the target. +target_include_directories(sdk_tests PRIVATE + ${LOGOS_PROTOCOL_ROOT}/cpp + ${LOGOS_PROTOCOL_ROOT}/include + ${LOGOS_PROTOCOL_ROOT}/include/cpp +) + gtest_discover_tests(sdk_tests) diff --git a/tests/sdk/test_logos_json_bytes.cpp b/tests/sdk/test_logos_json_bytes.cpp index 5935d12..8a7a7c1 100644 --- a/tests/sdk/test_logos_json_bytes.cpp +++ b/tests/sdk/test_logos_json_bytes.cpp @@ -1,4 +1,4 @@ -// Value-level tests for the canonical tagged-bytes codec (logos_json.h). +// Value-level tests for the canonical tagged-bytes codec. // // Binary payloads cross every module boundary as {"_bytes": ""}. // The generated cdylib event sidecar encodes into that form and the generated @@ -9,7 +9,11 @@ #include -#include +// logos_codec.h owns the canonical encode; logos_lp_client.h owns the lenient +// `lp`-path decode. They used to be second copies in logos_json.h, which is now +// aliases-only — see the note at the top of that header. +#include +#include #include #include