#pragma once #include #include #include #include #include #include #include namespace xrpl { namespace detail { /** * @brief Concept that detects whether @c to_string(t) is a valid expression * returning something convertible to @c std::string. * * Because this concept lives inside @c namespace @c xrpl, unqualified lookup * will find every @c xrpl::to_string overload (e.g. for @c Number, * @c Currency, @c AccountID, @c XRPAmount, @c IOUAmount, @c Asset, etc.). * ADL further extends the search to any namespace associated with @p T. */ template concept HasToString = requires(std::remove_cvref_t const& t) { { to_string(t) } -> std::convertible_to; }; /** * @brief Escape a string for safe embedding in a JSON value. * * Escapes backslash, double-quote, and control characters (U+0000-U+001F) * according to RFC 8259 section 7. */ inline void appendEscapedJsonString(std::string& dest, std::string_view sv) { dest.reserve(dest.size() + sv.size() + 2); dest += '"'; for (char const c : sv) { switch (c) { case '"': dest += "\\\""; break; case '\\': dest += "\\\\"; break; case '\b': dest += "\\b"; break; case '\f': dest += "\\f"; break; case '\n': dest += "\\n"; break; case '\r': dest += "\\r"; break; case '\t': dest += "\\t"; break; default: if (static_cast(c) < 0x20) { fmt::format_to( std::back_inserter(dest), "\\u{:04x}", static_cast(c)); } else { dest += c; } break; } } dest += '"'; } /** * @brief Append a value formatted as a JSON fragment to @p dest. * * - @c bool \u2192 unquoted @c true / @c false * - integral (non-bool) \u2192 unquoted number * - floating-point \u2192 unquoted number * - types with @c to_string \u2192 quoted string via @c to_string * - everything else \u2192 quoted string obtained via @c fmt::format * * @tparam T The value type. * @param dest The string to append the JSON fragment to. * @param value The value to format. */ template void appendJsonValue(std::string& dest, T const& value) { if constexpr (std::is_same_v) { dest += value ? "true" : "false"; } else if constexpr (std::is_integral_v) { dest += std::to_string(value); } else if constexpr (std::is_floating_point_v) { dest += std::to_string(value); } else if constexpr (HasToString) { appendEscapedJsonString(dest, to_string(value)); } else { appendEscapedJsonString(dest, fmt::format("{}", value)); } } /** * @brief Append a @c "key":value JSON field to @p dest. * * Inserts a comma separator when @p dest already contains fields. * The value is formatted by @ref appendJsonValue. */ template void appendJsonField(std::string& dest, std::string_view key, T const& value) { if (!dest.empty()) dest += ','; appendEscapedJsonString(dest, key); dest += ':'; appendJsonValue(dest, value); } /** * @brief Append a @c key=value plain-text field to @p dest. * * Inserts a space separator when @p dest already contains fields. */ template void appendTextField(std::string& dest, std::string_view key, T const& value) { if (!dest.empty()) dest += ' '; if constexpr (HasToString) { fmt::format_to(std::back_inserter(dest), "{}={}", key, to_string(value)); } else { fmt::format_to(std::back_inserter(dest), "{}={}", key, value); } } /** @brief Internal builder used by @ref buildJsonPattern. * * Not part of the public API — use @ref buildJsonPattern instead. */ class JsonLoggingPatternBuilder { std::string fields_; static constexpr std::string_view kMESSAGE_FIELD = "\"message\": %v"; static std::string extractFields(std::string_view pattern) { if (!pattern.empty() && pattern.front() == '{') pattern.remove_prefix(1); // Strip the trailing message field: `"message": %v }` or `"message": %v }` if (auto pos = pattern.rfind(kMESSAGE_FIELD); pos != std::string_view::npos) { // Also strip the leading ", " separator if present auto end = pos; if (end >= 2 && pattern.substr(end - 2, 2) == ", ") end -= 2; pattern = pattern.substr(0, end); } return std::string(pattern); } public: JsonLoggingPatternBuilder() = default; explicit JsonLoggingPatternBuilder(std::string_view existingPattern) : fields_(extractFields(existingPattern)) { } /** @brief Add a field. Delegates to @ref appendJsonField. */ template JsonLoggingPatternBuilder& add(std::string_view key, T const& value) { appendJsonField(fields_, key, value); return *this; } [[nodiscard]] std::string build() const { std::string result = "{"; result += fields_; if (!fields_.empty()) result += ", "; result += kMESSAGE_FIELD; result += " }"; return result; } }; } // namespace detail namespace log { /** * @brief Holds a named log parameter with its value. * * Constructed via the @ref param helper function. Designed to be passed * into Logger::Pump (the integration with Pump will be added later). * * @tparam T The value type. */ template class Parameter { std::string name_; T value_; public: Parameter(std::string_view name, T value) : name_(name), value_(std::move(value)) { } /** @brief Get the parameter name. */ [[nodiscard]] std::string_view name() const { return name_; } /** @brief Get the parameter value. */ [[nodiscard]] T const& value() const& { return value_; } /** @brief Get the parameter value. */ [[nodiscard]] T& value() & { return value_; } /** @brief Get the parameter value. */ [[nodiscard]] T&& value() && { return std::move(value_); } }; /** * @brief Create a named log parameter. * * @tparam T The value type (deduced). * @param name The parameter name (JSON key). * @param value The parameter value. * @return A Parameter holding the name and value. */ template [[nodiscard]] Parameter> param(std::string_view name, T&& value) { return Parameter>{name, value}; } } // namespace log /** * @brief Build or extend a JSON log pattern from @ref log::Parameter objects. * * Each parameter's name becomes a JSON key and its value becomes the * corresponding JSON value in the spdlog pattern string. String-like * values are treated as spdlog pattern flags (e.g. @c "%%n", @c "%%l"); * numeric and boolean values are embedded as literals. * * @code * // Build from scratch (empty existing pattern) * auto pattern = buildJsonPattern("", * log::param("channel", std::string_view("%n")), * log::param("level", std::string_view("%l"))); * * // Extend an existing pattern with extra fields * auto extended = buildJsonPattern(pattern, * log::param("trace_id", traceId)); * @endcode * * @tparam Ts Value types of the parameters. * @param existingPattern A pattern previously produced by build() or an * empty string to start fresh. * @param params The parameters to add as JSON fields. * @return The spdlog-compatible JSON pattern string. */ template [[nodiscard]] std::string buildJsonPattern(std::string_view existingPattern, log::Parameter const&... params) { // NOLINTNEXTLINE(misc-const-correctness) detail::JsonLoggingPatternBuilder builder(existingPattern); (builder.add(params.name(), params.value()), ...); return builder.build(); } } // namespace xrpl