refactor: the LIDL codec exists once (#117)

* refactor: the LIDL codec exists once

The cdylib generator emitted its own copy of the codec — ~186 lines of
C++-emitting-C++ mirroring logos-protocol's logos_codec.h by hand. Every codec
fix had to be written twice or it silently only half-applied, which happened
twice in a row recently (routing scalars through the codec + signedness; then
accepting 3.0 while still rejecting 3.7).

It was worse than duplication. The two copies had DRIFTED — the emitted integer
decode gated on is_number() where the canonical one checked is_number_integer()
|| is_number_unsigned() — and logos_json.h's byte helpers were the same mangled
symbols with weak linkage and DIFFERENT bodies as logos_codec.h's, both reaching
one program (module TUs compiled one; liblogos_protocol.a carries TUs that
included the other). Which body won was down to link order.

logos_json.h goes back to its documented charter — "LogosMap/LogosList aliases
for impl classes", per its own CMakeLists — and loses 77 lines. jsonToBytes moves
beside its sibling jsonToStringVec in logos_lp_client.h, rebuilt on the canonical
isTaggedBytes/b64UrlDecode; it keeps its own narrow spelling because every lp
decoder is documented to yield the default-constructed value on a mismatch,
which neither bytesFromJson (throws) nor bytesFromJsonLenient (accepts more) does.

Emptying it rather than making it include logos_codec.h is deliberate: some
thirty alias-only include sites across the module repos get ZERO new includes,
and logos-cpp-sdkConfig's "only dependency is nlohmann_json" stays true.

With the clash gone the generic half is deletable. emitGeneratedCodec becomes
emitRecordCodecs: one logos::detail::Codec<::Rec, void> per declared record, and
nothing else. That residue is irreducible — a LIDL `type` is a per-contract
struct whose fields exist only in that module's header, and C++17 has no field
reflection. Nesting composes for free: Codec<std::vector<Blob>> and deeper come
from the shared half once Codec<::Blob> exists.

One asymmetry dies with it. The scalar bstr decode and the [bstr] element decode
were different functions with different strictness, so echoBytes("hi") succeeded
while echoBytesList(["hi"]) threw — inside one module, for the same type. They
are one function now.

Build wiring: ONE line, in this repo's own test CMake, using a variable
nix/tests.nix already supplies. Nothing in logos-module-builder, logos-qt-sdk, or
any module repo.

verified: cpp-sdk + protocol suites green; test_fullapi_cpp, test_fullapi_ext_cpp
and test_basic_module_cpp build; test-modules 176/176. Conformance delta is
exactly one cell, baselined first in logos-test-modules#31.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* chore: bump logos-protocol to the path-threaded bstr decoder

logos-protocol 4359557 (#33). Required by this branch, not incidental: deleting
the emitted codec swaps its path-carrying bstr decode for the canonical one, and
without #33 the canonical one reported "at value" instead of "[0].payload" —
losing the diagnostic exactly where a malformed bstr is hardest to find.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Dario Lipicar
2026-07-29 16:01:14 -03:00
committed by GitHub
co-authored by Claude Opus 5
parent c364133066
commit 461cfed52d
7 changed files with 108 additions and 232 deletions
+37 -133
View File
@@ -97,13 +97,14 @@ QString jsonArgToStd(const TypeExpr& te, const QString& expr, const QString& pat
const std::set<std::string>& 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<uint8_t> 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<std::string>& 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<std::string>& 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<std::vector<Blob>> and
// deeper come from the shared generic half once Codec<::Blob> exists.
void emitRecordCodecs(QTextStream& s, const ModuleDecl& module,
const std::set<std::string>& recs)
{
s << "namespace logos_gen {\n\n";
s << "// Codec<T>::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 <class T> 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<std::string>()"},
{"double", "number", "is_number()", "get<double>()"},
{"bool", "boolean", "is_boolean()", "get<bool>()"},
};
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<int64_t>() TRUNCATES it — 3.7
// arrived as 3 instead of being rejected.
// is_number() admits a NEGATIVE, and .get<uint64_t>() 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<int64_t> {\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<double>();\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<int64_t>(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>() > uint64_t(std::numeric_limits<int64_t>::max()))\n";
s << " lidlTypeError(\"signed integer in range\", path, j);\n";
s << " return j.get<int64_t>();\n }\n};\n\n";
s << "template <> struct Codec<uint64_t> {\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<double>();\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<uint64_t>(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<int64_t>() < 0)\n";
s << " lidlTypeError(\"unsigned integer\", path, j);\n";
s << " return j.get<uint64_t>();\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<std::vector<uint8_t>> {\n";
s << " static nlohmann::json to(const std::vector<uint8_t>& v) { return logos::bytesToJson(v); }\n";
s << " static std::vector<uint8_t> 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<nlohmann::json> {\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 <class T> struct Codec<std::vector<T>> {\n";
s << " static nlohmann::json to(const std::vector<T>& v) {\n";
s << " nlohmann::json out = nlohmann::json::array();\n";
s << " for (const T& e : v) out.push_back(Codec<T>::to(e));\n";
s << " return out;\n }\n";
s << " static std::vector<T> from(const nlohmann::json& j, const std::string& path) {\n";
s << " if (!j.is_array()) lidlTypeError(\"array\", path, j);\n";
s << " std::vector<T> out;\n out.reserve(j.size());\n";
s << " for (size_t i = 0; i < j.size(); ++i)\n";
s << " out.push_back(Codec<T>::from(j.at(i), path + \"[\" + std::to_string(i) + \"]\"));\n";
s << " return out;\n }\n};\n\n";
s << "template <class T> struct Codec<std::map<std::string, T>> {\n";
s << " static nlohmann::json to(const std::map<std::string, T>& v) {\n";
s << " nlohmann::json out = nlohmann::json::object();\n";
s << " for (const auto& kv : v) out[kv.first] = Codec<T>::to(kv.second);\n";
s << " return out;\n }\n";
s << " static std::map<std::string, T> from(const nlohmann::json& j, const std::string& path) {\n";
s << " if (!j.is_object()) lidlTypeError(\"object\", path, j);\n";
s << " std::map<std::string, T> out;\n";
s << " for (auto it = j.begin(); it != j.end(); ++it)\n";
s << " out.emplace(it.key(), Codec<T>::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<uint8_t> 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 <logos_json.h>\n";
s << "#include <logos_json.h>\n"; // LogosMap / LogosList aliases
s << "#include <logos_codec.h>\n"; // logos::Codec — the ONE definition
s << "#include <cstdint>\n";
s << "#include <cmath>\n"; // the integer codecs accept whole-valued floats
s << "#include <limits>\n"; // ...and range-check
s << "#include <map>\n";
s << "#include <stdexcept>\n";
s << "#include <string>\n";
s << "#include <vector>\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;
}
+22 -83
View File
@@ -1,91 +1,30 @@
#pragma once
#include <nlohmann/json.hpp>
#include <cstddef>
#include <cstdint>
#include <string>
#include <vector>
// 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": "<base64url, unpadded>"}
//
// 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<uint8_t>& 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<uint8_t> 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<uint8_t> 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<uint32_t>(v);
bits += 6;
if (bits >= 8) {
bits -= 8;
out.push_back(static_cast<uint8_t>((buf >> bits) & 0xff));
}
}
return out;
}
// Bytes -> the tagged JSON object.
inline nlohmann::json bytesToJson(const std::vector<uint8_t>& 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<uint8_t> jsonToBytes(const nlohmann::json& j)
{
if (!j.is_object() || !j.contains("_bytes") || !j["_bytes"].is_string())
return {};
return b64UrlDecode(j["_bytes"].get<std::string>());
}
} // namespace logos
+13 -3
View File
@@ -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<std::string> jsonToStringVec(const nlohmann::json& j) {
}
// Binary payloads travel in the canonical tagged form
// {"_bytes": "<base64url, unpadded>"}; logos::bytesToJson / logos::jsonToBytes
// live in logos_json.h and are used by the generated wrappers on both sides.
// {"_bytes": "<base64url, unpadded>"}. 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<uint8_t> jsonToBytes(const nlohmann::json& j) {
if (!isTaggedBytes(j)) return {};
return b64UrlDecode(j["_bytes"].get<std::string>());
}
inline StdLogosResult jsonToStdResult(const nlohmann::json& j) {
StdLogosResult r;
Generated
+3 -3
View File
@@ -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": {
+18 -8
View File
@@ -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<std::vector<uint8_t>>")) << source.toStdString();
EXPECT_TRUE(source.contains("logos_gen::Codec<std::vector<std::vector<uint8_t>>>::to(payloads)"))
EXPECT_TRUE(source.contains("logos::toJson<std::vector<std::vector<uint8_t>>>(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<std::vector<std::vector<uint8_t>>>::from("))
EXPECT_TRUE(source.contains("logos::fromJson<std::vector<std::vector<uint8_t>>>("))
<< 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<std::vector<std::vector<uint8_t>>>::to("))
EXPECT_TRUE(source.contains("logos::toJson<std::vector<std::vector<uint8_t>>>("))
<< 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<std::vector<std::vector<uint8_t>>>::to("))
EXPECT_TRUE(listed.contains("logos::toJson<std::vector<std::vector<uint8_t>>>("))
<< 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<Blob>")) << 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<std::vector<uint8_t>>::to(v.payload)")) << types.toStdString();
// The generic half is NOT emitted any more — it comes from logos_codec.h.
EXPECT_TRUE(types.contains("#include <logos_codec.h>")) << types.toStdString();
EXPECT_FALSE(types.contains("namespace logos_gen")) << types.toStdString();
EXPECT_FALSE(types.contains("struct Codec<int64_t>")) << types.toStdString();
// An undeclared Named type is NOT a record and stays refused.
MethodDecl bad;
+9
View File
@@ -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)
+6 -2
View File
@@ -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": "<base64url>"}.
// The generated cdylib event sidecar encodes into that form and the generated
@@ -9,7 +9,11 @@
#include <gtest/gtest.h>
#include <logos_json.h>
// 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 <logos_codec.h>
#include <logos_lp_client.h>
#include <cstdint>
#include <string>