diff --git a/API/fleece/FLEncoder.h b/API/fleece/FLEncoder.h index b92d17d1..fe328fd5 100644 --- a/API/fleece/FLEncoder.h +++ b/API/fleece/FLEncoder.h @@ -153,6 +153,13 @@ extern "C" { /** Writes a Fleece Value to an Encoder. */ FLEECE_PUBLIC bool FLEncoder_WriteValue(FLEncoder, FLValue) FLAPI; + /** Writes an Array or Dict to the encoder, as per the format string and printf-style arguments. + For details, see documentation of \ref FLValue_NewWithFormat. */ + FLEECE_PUBLIC bool FLEncoder_WriteFormatted(FLEncoder, const char* format, ...) FLAPI __printflike(2, 3); + + /** Writes an Array or Dict to the encoder, as per the format string and `va_list`. + For details, see documentation of \ref FLValue_NewWithFormat. */ + FLEECE_PUBLIC bool FLEncoder_WriteFormattedArgs(FLEncoder, const char* format, va_list args) FLAPI; /** Begins writing an array value to an encoder. This pushes a new state where each subsequent value written becomes an array item, until FLEncoder_EndArray is called. diff --git a/API/fleece/Fleece.hh b/API/fleece/Fleece.hh index 03e4c938..aac02d65 100644 --- a/API/fleece/Fleece.hh +++ b/API/fleece/Fleece.hh @@ -17,6 +17,7 @@ #include "Fleece.h" #endif #include "slice.hh" +#include #include #include #include @@ -439,6 +440,9 @@ namespace fleece { inline bool writeValue(Value); inline bool convertJSON(slice_NONNULL); + inline bool writeFormatted(const char* format, ...) __printflike(2, 3); + inline bool writeFormattedArgs(const char* format, va_list args); + inline bool beginArray(size_t reserveCount =0); inline bool endArray(); inline bool beginDict(size_t reserveCount =0); @@ -449,6 +453,11 @@ namespace fleece { template inline void write(slice_NONNULL key, T value) {writeKey(key); *this << value;} + template + bool writeArray(FN const& fn) {return beginArray() && (fn(), endArray());} + template + bool writeDict(FN const& fn) {return beginDict() && (fn(), endDict());} + [[nodiscard]] inline Doc finishDoc(FLError* FL_NULLABLE =nullptr); [[nodiscard]] inline alloc_slice finish(FLError* FL_NULLABLE =nullptr); inline void reset(); @@ -604,6 +613,18 @@ namespace fleece { inline FLError Encoder::error() const {return FLEncoder_GetError(_enc);} inline const char* Encoder::errorMessage() const {return FLEncoder_GetErrorMessage(_enc);} + inline bool Encoder::writeFormatted(const char* format, ...) { + va_list args; + va_start(args, format); + bool ok = FLEncoder_WriteFormattedArgs(_enc, format, args); + va_end(args); + return ok; + } + + inline bool Encoder::writeFormattedArgs(const char* format, va_list args) { + return FLEncoder_WriteFormattedArgs(_enc, format, args); + } + // specialization for assigning bool value since there is no Encoder< inline void Encoder::keyref::operator= (bool value) {_enc.writeKey(_key); _enc.writeBool(value);} diff --git a/Fleece.xcodeproj/project.pbxproj b/Fleece.xcodeproj/project.pbxproj index 59cf86be..7844c5ad 100644 --- a/Fleece.xcodeproj/project.pbxproj +++ b/Fleece.xcodeproj/project.pbxproj @@ -86,6 +86,8 @@ 279AC53C1C097941002C80DB /* Value+Dump.cc in Sources */ = {isa = PBXBuildFile; fileRef = 279AC53B1C097941002C80DB /* Value+Dump.cc */; }; 27A0E3DF24DCD86900380563 /* ConcurrentArena.hh in Headers */ = {isa = PBXBuildFile; fileRef = 27A0E3DD24DCD86900380563 /* ConcurrentArena.hh */; }; 27A0E3E024DCD86900380563 /* ConcurrentArena.cc in Sources */ = {isa = PBXBuildFile; fileRef = 27A0E3DE24DCD86900380563 /* ConcurrentArena.cc */; }; + 27A1327B2C700D45008E84FA /* JSLexer.hh in Headers */ = {isa = PBXBuildFile; fileRef = 27A1327A2C700D45008E84FA /* JSLexer.hh */; }; + 27A132812C73BF8C008E84FA /* FLEncoder.cc in Sources */ = {isa = PBXBuildFile; fileRef = 27A132802C73BF8C008E84FA /* FLEncoder.cc */; }; 27A2F73B21248DA50081927B /* FLSlice.h in Headers */ = {isa = PBXBuildFile; fileRef = 27A2F73A21248DA40081927B /* FLSlice.h */; }; 27A924CF1D9C32E800086206 /* Path.cc in Sources */ = {isa = PBXBuildFile; fileRef = 27A924CD1D9C32E800086206 /* Path.cc */; }; 27A924D01D9C32E800086206 /* Path.hh in Headers */ = {isa = PBXBuildFile; fileRef = 27A924CE1D9C32E800086206 /* Path.hh */; }; @@ -410,6 +412,8 @@ 279AC53B1C097941002C80DB /* Value+Dump.cc */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = "Value+Dump.cc"; sourceTree = ""; }; 27A0E3DD24DCD86900380563 /* ConcurrentArena.hh */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = ConcurrentArena.hh; sourceTree = ""; }; 27A0E3DE24DCD86900380563 /* ConcurrentArena.cc */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = ConcurrentArena.cc; sourceTree = ""; }; + 27A1327A2C700D45008E84FA /* JSLexer.hh */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = JSLexer.hh; sourceTree = ""; }; + 27A132802C73BF8C008E84FA /* FLEncoder.cc */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = FLEncoder.cc; sourceTree = ""; }; 27A2F73A21248DA40081927B /* FLSlice.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = FLSlice.h; sourceTree = ""; }; 27A63F38263375B500634F7B /* date.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = date.h; sourceTree = ""; }; 27A924CD1D9C32E800086206 /* Path.cc */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = Path.cc; sourceTree = ""; }; @@ -587,6 +591,7 @@ isa = PBXGroup; children = ( 278163B31CE69CA800B94E32 /* Fleece.cc */, + 27A132802C73BF8C008E84FA /* FLEncoder.cc */, 275B3595234BE12800FE9CF0 /* FLSlice.cc */, 270515551D90596000D62D05 /* Fleece+ImplGlue.hh */, ); @@ -856,6 +861,7 @@ 27298E7F1C04E665000CFBA8 /* Encoder.cc */, 270FA26F1BF53CEA005DCB13 /* Encoder.hh */, 27E3CE0E263B1B0700CA7056 /* Builder.cc */, + 27A1327A2C700D45008E84FA /* JSLexer.hh */, 27E3CE0D263B1B0700CA7056 /* Builder.hh */, 27298E3A1C00F812000CFBA8 /* JSONConverter.cc */, 27298E761C00FB48000CFBA8 /* JSONConverter.hh */, @@ -971,6 +977,8 @@ 278343132A675A7000621050 /* function_ref.hh in Headers */, 270FA2801BF53CEA005DCB13 /* Writer.hh in Headers */, 270FA27D1BF53CEA005DCB13 /* Encoder.hh in Headers */, + 27C8DF072084102900A99BFC /* MutableHashTree.hh in Headers */, + 27A1327B2C700D45008E84FA /* JSLexer.hh in Headers */, 27A924D01D9C32E800086206 /* Path.hh in Headers */, 274D8245209A3A77008BB39F /* HeapDict.hh in Headers */, 274D8253209CF9B3008BB39F /* HeapValue.hh in Headers */, @@ -1288,6 +1296,7 @@ 27CA08431F6B0E9400FF8C71 /* Dict.cc in Sources */, 27867AF2211E27E5007BDA5F /* Doc.cc in Sources */, 27298E801C04E665000CFBA8 /* Encoder.cc in Sources */, + 27A132812C73BF8C008E84FA /* FLEncoder.cc in Sources */, 27298E3C1C00F812000CFBA8 /* JSONConverter.cc in Sources */, 279AC53C1C097941002C80DB /* Value+Dump.cc in Sources */, 27FE87F31E53E43200C5CF3F /* JSONEncoder.cc in Sources */, diff --git a/Fleece/API_Impl/FLEncoder.cc b/Fleece/API_Impl/FLEncoder.cc new file mode 100644 index 00000000..92f0ce5e --- /dev/null +++ b/Fleece/API_Impl/FLEncoder.cc @@ -0,0 +1,191 @@ +// +// FLEncoder.cc +// +// Copyright 2016-Present Couchbase, Inc. +// +// Use of this software is governed by the Business Source License included +// in the file licenses/BSL-Couchbase.txt. As of the Change Date specified +// in that file, in accordance with the Business Source License, use of this +// software will be governed by the Apache License, Version 2.0, included in +// the file licenses/APL2.txt. +// + +#include "Fleece+ImplGlue.hh" +#include "Builder.hh" + +#define ENCODER_DO(E, METHOD) (E)->do_([&](auto& e) {return e.METHOD;}) + +#define ENCODER_TRY(E, METHOD) return (E)->try_([&](auto e) { ENCODER_DO(e, METHOD); return true;}) + +FLEncoder FLEncoder_New(void) FLAPI { + return FLEncoder_NewWithOptions(kFLEncodeFleece, 0, true); +} + +FLEncoder FLEncoder_NewWithOptions(FLEncoderFormat format, + size_t reserveSize, bool uniqueStrings) FLAPI +{ + return new FLEncoderImpl(format, reserveSize, uniqueStrings); +} + +FLEncoder FLEncoder_NewWritingToFile(FILE *outputFile, bool uniqueStrings) FLAPI { + return new FLEncoderImpl(outputFile, uniqueStrings); +} + +void FLEncoder_Reset(FLEncoder e) FLAPI { + e->reset(); +} + +void FLEncoder_Free(FLEncoder FL_NULLABLE e) FLAPI { + delete e; +} + +void FLEncoder_SetSharedKeys(FLEncoder e, FLSharedKeys FL_NULLABLE sk) FLAPI { + if (e->isFleece()) + e->fleeceEncoder()->setSharedKeys(sk); +} + +void FLEncoder_SuppressTrailer(FLEncoder e) FLAPI { + if (e->isFleece()) + e->fleeceEncoder()->suppressTrailer(); +} + +void FLEncoder_Amend(FLEncoder e, FLSlice base, bool reuseStrings, bool externPointers) FLAPI { + if (e->isFleece() && base.size > 0) { + e->fleeceEncoder()->setBase(base, externPointers); + if(reuseStrings) + e->fleeceEncoder()->reuseBaseStrings(); + } +} + +FLSlice FLEncoder_GetBase(FLEncoder e) FLAPI { + if (e->isFleece()) + return e->fleeceEncoder()->base(); + return {}; +} + +size_t FLEncoder_GetNextWritePos(FLEncoder e) FLAPI { + if (e->isFleece()) + return e->fleeceEncoder()->nextWritePos(); + return 0; +} + +size_t FLEncoder_BytesWritten(FLEncoder e) FLAPI { + return ENCODER_DO(e, bytesWritten()); +} + +intptr_t FLEncoder_LastValueWritten(FLEncoder e) FLAPI { + return e->isFleece() ? intptr_t(e->fleeceEncoder()->lastValueWritten()) : kFLNoWrittenValue; +} + +bool FLEncoder_WriteValueAgain(FLEncoder e, intptr_t prewritten) FLAPI { + return e->isFleece() && e->fleeceEncoder()->writeValueAgain(Encoder::PreWrittenValue(prewritten)); +} + +bool FLEncoder_WriteNull(FLEncoder e) FLAPI {ENCODER_TRY(e, writeNull());} +bool FLEncoder_WriteUndefined(FLEncoder e) FLAPI {ENCODER_TRY(e, writeUndefined());} +bool FLEncoder_WriteBool(FLEncoder e, bool b) FLAPI {ENCODER_TRY(e, writeBool(b));} +bool FLEncoder_WriteInt(FLEncoder e, int64_t i) FLAPI {ENCODER_TRY(e, writeInt(i));} +bool FLEncoder_WriteUInt(FLEncoder e, uint64_t u) FLAPI {ENCODER_TRY(e, writeUInt(u));} +bool FLEncoder_WriteFloat(FLEncoder e, float f) FLAPI {ENCODER_TRY(e, writeFloat(f));} +bool FLEncoder_WriteDouble(FLEncoder e, double d) FLAPI {ENCODER_TRY(e, writeDouble(d));} +bool FLEncoder_WriteString(FLEncoder e, FLSlice s) FLAPI {ENCODER_TRY(e, writeString(s));} +bool FLEncoder_WriteDateString(FLEncoder e, FLTimestamp ts, bool asUTC) + FLAPI {ENCODER_TRY(e, writeDateString(ts,asUTC));} +bool FLEncoder_WriteData(FLEncoder e, FLSlice d) FLAPI {ENCODER_TRY(e, writeData(d));} +bool FLEncoder_WriteRaw(FLEncoder e, FLSlice r) FLAPI {ENCODER_TRY(e, writeRaw(r));} +bool FLEncoder_WriteValue(FLEncoder e, FLValue v) FLAPI {ENCODER_TRY(e, writeValue(v));} + +bool FLEncoder_WriteFormatted(FLEncoder e, const char* format, ...) FLAPI { + va_list args; + va_start(args, format); + bool ok = FLEncoder_WriteFormattedArgs(e, format, args); + va_end(args); + return ok; +} + +bool FLEncoder_WriteFormattedArgs(FLEncoder e, const char* format, va_list args) FLAPI { + return e->try_([&](auto impl) { + impl->do_([&](auto& enc) { builder::VEncode(enc, format, args); }); + return true; + }); +} + +bool FLEncoder_BeginArray(FLEncoder e, size_t reserve) FLAPI {ENCODER_TRY(e, beginArray(reserve));} +bool FLEncoder_EndArray(FLEncoder e) FLAPI {ENCODER_TRY(e, endArray());} +bool FLEncoder_BeginDict(FLEncoder e, size_t reserve) FLAPI {ENCODER_TRY(e, beginDictionary(reserve));} +bool FLEncoder_WriteKey(FLEncoder e, FLSlice s) FLAPI {ENCODER_TRY(e, writeKey(s));} +bool FLEncoder_WriteKeyValue(FLEncoder e, FLValue key) FLAPI {ENCODER_TRY(e, writeKey(key));} +bool FLEncoder_EndDict(FLEncoder e) FLAPI {ENCODER_TRY(e, endDictionary());} + +void FLJSONEncoder_NextDocument(FLEncoder e) FLAPI { + if (!e->isFleece()) + e->jsonEncoder()->nextDocument(); +} + +bool FLEncoder_ConvertJSON(FLEncoder e, FLSlice json) FLAPI { + return e->try_([&](auto impl) { return impl->encodeJSON(json); }); +} + +FLError FLEncoder_GetError(FLEncoder e) FLAPI { + return e->errorCode; +} + +const char* FL_NULLABLE FLEncoder_GetErrorMessage(FLEncoder e) FLAPI { + return e->errorMessage.empty() ? nullptr : e->errorMessage.c_str(); +} + +void FLEncoder_SetExtraInfo(FLEncoder e, void* FL_NULLABLE info) FLAPI { + e->extraInfo = info; +} + +void* FL_NULLABLE FLEncoder_GetExtraInfo(FLEncoder e) FLAPI { + return e->extraInfo; +} + +FLSliceResult FLEncoder_Snip(FLEncoder e) FLAPI { + if (e->isFleece()) + return FLSliceResult(e->fleeceEncoder()->snip()); + else + return {}; +} + +size_t FLEncoder_FinishItem(FLEncoder e) FLAPI { + if (e->isFleece()) + return e->fleeceEncoder()->finishItem(); + return 0; +} + +FLDoc FL_NULLABLE FLEncoder_FinishDoc(FLEncoder e, FLError * FL_NULLABLE outError) FLAPI { + if (auto enc = e->fleeceEncoder()) { + if (!e->hasError()) { + try { + return retain(enc->finishDoc()); // finish() can throw + } catch (const std::exception &x) { + e->recordException(x); + } + } + } else { + e->errorCode = kFLUnsupported; // Doc class doesn't support JSON data + } + // Failure: + if (outError) + *outError = e->errorCode; + e->reset(); + return nullptr; +} + + +FLSliceResult FLEncoder_Finish(FLEncoder e, FLError * FL_NULLABLE outError) FLAPI { + if (!e->hasError()) { + try { + return FLSliceResult(ENCODER_DO(e, finish())); // finish() can throw + } catch (const std::exception &x) { + e->recordException(x); + } + } + // Failure: + if (outError) + *outError = e->errorCode; + e->reset(); + return {nullptr, 0}; +} diff --git a/Fleece/API_Impl/Fleece+ImplGlue.hh b/Fleece/API_Impl/Fleece+ImplGlue.hh index 710cfb90..f0aa2eb1 100644 --- a/Fleece/API_Impl/Fleece+ImplGlue.hh +++ b/Fleece/API_Impl/Fleece+ImplGlue.hh @@ -13,15 +13,21 @@ #pragma once #include "FleeceImpl.hh" #include "ValueSlot.hh" +#include "Encoder.hh" #include "JSONEncoder.hh" #include "Path.hh" #include "DeepIterator.hh" #include "Doc.hh" #include "FleeceException.hh" #include +#include -namespace fleece:: impl { - struct FLEncoderImpl; +using namespace fleece; +using namespace fleece::impl; + + +namespace fleece::impl { + class FLEncoderImpl; } // Define the public types as typedefs of the impl types: @@ -52,88 +58,128 @@ namespace fleece::impl { #define catchError(OUTERROR) \ catch (const std::exception &x) { recordError(x, OUTERROR); } - - // Implementation of FLEncoder: a subclass of Encoder that keeps track of its error state. - struct FLEncoderImpl { - FLError errorCode {::kFLNoError}; - const bool ownsFleeceEncoder {true}; - std::string errorMessage; - std::unique_ptr fleeceEncoder; - std::unique_ptr jsonEncoder; - std::unique_ptr jsonConverter; - void* FL_NULLABLE extraInfo {nullptr}; + /** A wrapper around Encoder and JSONEncoder that can write either. Catches exceptions. + The public type `FLEncoder` is a pointer to this. */ + class FLEncoderImpl { + public: FLEncoderImpl(FLEncoderFormat format, size_t reserveSize =0, bool uniqueStrings =true) + :_encoder(mkEncoder(format, reserveSize ? reserveSize : 256)) { - if (reserveSize == 0) - reserveSize = 256; - if (format == kFLEncodeFleece) { - fleeceEncoder.reset(new Encoder(reserveSize)); - fleeceEncoder->uniqueStrings(uniqueStrings); - } else { - jsonEncoder.reset(new JSONEncoder(reserveSize)); - jsonEncoder->setJSON5(format == kFLEncodeJSON5); - } + if (format == kFLEncodeFleece) + fleeceEncoder()->uniqueStrings(uniqueStrings); + else + jsonEncoder()->setJSON5(format == kFLEncodeJSON5); } - FLEncoderImpl(FILE *outputFile, bool uniqueStrings =true) { - fleeceEncoder.reset(new Encoder(outputFile)); - fleeceEncoder->uniqueStrings(uniqueStrings); + explicit FLEncoderImpl(FILE *outputFile, bool uniqueStrings =true) + :_encoder(outputFile) + { + std::get(_encoder).uniqueStrings(uniqueStrings); } - FLEncoderImpl(Encoder *encoder) - :ownsFleeceEncoder(false) - ,fleeceEncoder(encoder) + explicit FLEncoderImpl(Encoder* fleeceEncoder) + :_encoder(fleeceEncoder) { } - ~FLEncoderImpl() { - if (!ownsFleeceEncoder) - fleeceEncoder.release(); + Encoder* FL_NULLABLE fleeceEncoder() { + switch (_encoder.index()) { + case 0: return std::get_if(&_encoder); + case 2: return std::get(_encoder); + default: return nullptr; + } } - bool isFleece() const { - return fleeceEncoder != nullptr; + JSONEncoder* FL_NULLABLE jsonEncoder() { + return std::get_if(&_encoder); } - bool hasError() const { - return errorCode != ::kFLNoError; + bool isFleece() const noexcept {return _encoder.index() == 0;} + bool hasError() const noexcept {return errorCode != ::kFLNoError;} + + bool encodeJSON(slice json) { + if (isFleece()) { + if (_jsonConverter) + _jsonConverter->reset(); + else + _jsonConverter = std::make_unique(*fleeceEncoder()); + if (_jsonConverter->encodeJSON(json)) { // encodeJSON can throw + return true; + } else { + errorCode = (FLError)_jsonConverter->errorCode(); + errorMessage = _jsonConverter->errorMessage(); + _jsonConverter = nullptr; + return false; + } + } else { + jsonEncoder()->writeJSON(json); + return true; + } } - void recordException(const std::exception &x) noexcept { + /// Invokes a callback. If it throws, catches the error, sets my error and returns false. + template + bool try_(CALLBACK const& callback) noexcept { if (!hasError()) { - fleece::impl::recordError(x, &errorCode); - errorMessage = x.what(); + try { + return callback(this); + } catch (const std::exception &x) { + recordException(x); + } } + return false; } - void reset() { - if (fleeceEncoder) - fleeceEncoder->reset(); - if (jsonConverter) - jsonConverter->reset(); - if (jsonEncoder) { - jsonEncoder->reset(); - } + /// Invokes a callback on the underlying encoder. Callback parameter is a ref to the Encoder or JSONEncoder. + template + auto do_(CALLBACK const& callback) { + return std::visit([&](auto& e) { + if constexpr (std::is_pointer_v>) + return callback(*e); + else + return callback(e); + }, _encoder); + } + + /// Combination of `try_` and `do_`. + template + bool try_do_(CALLBACK const& callback) { + return try_([&](auto self) {return self->do_(callback);}); + } + + void reset() noexcept { + do_([](auto& e) {e.reset();}); + if (_jsonConverter) + _jsonConverter->reset(); errorCode = ::kFLNoError; extraInfo = nullptr; } - }; - #define ENCODER_DO(E, METHOD) \ - (E->isFleece() ? E->fleeceEncoder->METHOD : E->jsonEncoder->METHOD) - - // Body of an FLEncoder_WriteXXX function: - #define ENCODER_TRY(E, METHOD) \ - try{ \ - if (!E->hasError()) { \ - ENCODER_DO(E, METHOD); \ - return true; \ - } \ - } catch (const std::exception &x) { \ - E->recordException(x); \ - } \ - return false; + void recordException(const std::exception &x) noexcept { + if (!hasError()) { + errorCode = FLError(FleeceException::getCode(x)); + errorMessage = x.what(); + } + } + + FLError errorCode {::kFLNoError}; + std::string errorMessage; + void* FL_NULLABLE extraInfo {nullptr}; + + private: + using VariEnc = std::variant; + + static VariEnc mkEncoder(FLEncoderFormat format, size_t reserveSize) { + if (format == kFLEncodeFleece) + return VariEnc(std::in_place_type, reserveSize); + else + return VariEnc(std::in_place_type, reserveSize); + } + + VariEnc _encoder; + std::unique_ptr _jsonConverter; + }; class FLPersistentSharedKeys : public PersistentSharedKeys { diff --git a/Fleece/API_Impl/Fleece.cc b/Fleece/API_Impl/Fleece.cc index f7c46f4b..c6c7dcad 100644 --- a/Fleece/API_Impl/Fleece.cc +++ b/Fleece/API_Impl/Fleece.cc @@ -43,12 +43,6 @@ FLEECE_PUBLIC const FLArray kFLEmptyArray = Array::kEmpty; FLEECE_PUBLIC const FLDict kFLEmptyDict = Dict::kEmpty; -static FLSliceResult toSliceResult(alloc_slice &&s) { - s.retain(); - return {(void*)s.buf, s.size}; -} - - FLTimestamp FLTimestamp_Now() FLAPI { return FLTimestamp(std::chrono::duration_cast( std::chrono::system_clock::now().time_since_epoch()).count()); @@ -126,7 +120,7 @@ bool FLValue_IsEqual(FLValue FL_NULLABLE v1, FLValue FL_NULLABLE v2) FLAPI { FLSliceResult FLValue_ToString(FLValue FL_NULLABLE v) FLAPI { if (v) { try { - return toSliceResult(v->toString()); // toString can throw + return FLSliceResult(v->toString()); // toString can throw } catchError(nullptr) } return {nullptr, 0}; @@ -159,7 +153,7 @@ FLSliceResult FLValue_ToJSONX(FLValue FL_NULLABLE v, encoder.setJSON5(json5); encoder.setCanonical(canonical); encoder.writeValue(v); - return toSliceResult(encoder.finish()); + return FLSliceResult(encoder.finish()); } catchError(nullptr) } return {nullptr, 0}; @@ -184,7 +178,7 @@ FLStringResult FLJSON5_ToJSON(FLString json5, size_t errorPos = 0; try { std::string json = ConvertJSON5((std::string((char*)json5.buf, json5.size))); - return toSliceResult(alloc_slice(json)); + return FLSliceResult(alloc_slice(json)); } catch (const json5_error &x) { errorMessage = alloc_slice(x.what()); errorPos = x.inputPos; @@ -195,7 +189,7 @@ FLStringResult FLJSON5_ToJSON(FLString json5, recordError(x, error); } if (outErrorMessage) - *outErrorMessage = toSliceResult(std::move(errorMessage)); + *outErrorMessage = FLSliceResult(std::move(errorMessage)); if (outErrorPos) *outErrorPos = errorPos; return {}; @@ -204,7 +198,7 @@ FLStringResult FLJSON5_ToJSON(FLString json5, FLSliceResult FLData_Dump(FLSlice data) FLAPI { try { - return toSliceResult(alloc_slice(Value::dump(data))); + return FLSliceResult(alloc_slice(Value::dump(data))); } catchError(nullptr) return {nullptr, 0}; } @@ -458,7 +452,7 @@ void FLSharedKeys_Release(FLSharedKeys FL_NULLABLE sk) FLAPI {re unsigned FLSharedKeys_Count(FLSharedKeys sk) FLAPI {return (unsigned)sk->count();} bool FLSharedKeys_LoadStateData(FLSharedKeys sk, FLSlice d)FLAPI {return sk->loadFrom(d);} bool FLSharedKeys_LoadState(FLSharedKeys sk, FLValue s) FLAPI {return sk->loadFrom(s);} -FLSliceResult FLSharedKeys_GetStateData(FLSharedKeys sk) FLAPI {return toSliceResult(sk->stateData());} +FLSliceResult FLSharedKeys_GetStateData(FLSharedKeys sk) FLAPI {return FLSliceResult(sk->stateData());} FLString FLSharedKeys_Decode(FLSharedKeys sk, int key) FLAPI {return sk->decode(key);} void FLSharedKeys_RevertToCount(FLSharedKeys sk, unsigned c) FLAPI {sk->revertToCount(c);} void FLSharedKeys_DisableCaching(FLSharedKeys sk) FLAPI { sk->disableCaching(); } @@ -469,7 +463,7 @@ FLSharedKeys FLSharedKeys_NewWithRead(FLSharedKeysReadCallback callback, void* F void FLSharedKeys_WriteState(FLSharedKeys sk, FLEncoder e) FLAPI { assert_always(e->isFleece()); - sk->writeState(*e->fleeceEncoder); + sk->writeState(*e->fleeceEncoder()); } int FLSharedKeys_Encode(FLSharedKeys sk, FLString keyStr, bool add) FLAPI { @@ -546,11 +540,11 @@ void FLDeepIterator_GetPath(FLDeepIterator i, FLPathComponent* *outPath, size_t } FLSliceResult FLDeepIterator_GetPathString(FLDeepIterator i) FLAPI { - return toSliceResult(alloc_slice(i->pathString())); + return FLSliceResult(alloc_slice(i->pathString())); } FLSliceResult FLDeepIterator_GetJSONPointer(FLDeepIterator i) FLAPI { - return toSliceResult(alloc_slice(i->jsonPointer())); + return FLSliceResult(alloc_slice(i->jsonPointer())); } @@ -580,7 +574,7 @@ FLValue FL_NULLABLE FLKeyPath_EvalOnce(FLSlice specifier, FLValue root, FLError } FLStringResult FLKeyPath_ToString(FLKeyPath path) FLAPI { - return toSliceResult(alloc_slice(std::string(*path))); + return FLSliceResult(alloc_slice(std::string(*path))); } bool FLKeyPath_Equals(FLKeyPath path1, FLKeyPath path2) FLAPI { @@ -629,187 +623,6 @@ void FLKeyPath_DropComponents(FLKeyPath path, size_t n) FLAPI { #pragma mark - ENCODER: -FLEncoder FLEncoder_New(void) FLAPI { - return FLEncoder_NewWithOptions(kFLEncodeFleece, 0, true); -} - -FLEncoder FLEncoder_NewWithOptions(FLEncoderFormat format, - size_t reserveSize, bool uniqueStrings) FLAPI -{ - return new FLEncoderImpl(format, reserveSize, uniqueStrings); -} - -FLEncoder FLEncoder_NewWritingToFile(FILE *outputFile, bool uniqueStrings) FLAPI { - return new FLEncoderImpl(outputFile, uniqueStrings); -} - -void FLEncoder_Reset(FLEncoder e) FLAPI { - e->reset(); -} - -void FLEncoder_Free(FLEncoder FL_NULLABLE e) FLAPI { - delete e; -} - -void FLEncoder_SetSharedKeys(FLEncoder e, FLSharedKeys FL_NULLABLE sk) FLAPI { - if (e->isFleece()) - e->fleeceEncoder->setSharedKeys(sk); -} - -void FLEncoder_SuppressTrailer(FLEncoder e) FLAPI { - if (e->isFleece()) - e->fleeceEncoder->suppressTrailer(); -} - -void FLEncoder_Amend(FLEncoder e, FLSlice base, bool reuseStrings, bool externPointers) FLAPI { - if (e->isFleece() && base.size > 0) { - e->fleeceEncoder->setBase(base, externPointers); - if(reuseStrings) - e->fleeceEncoder->reuseBaseStrings(); - } -} - -FLSlice FLEncoder_GetBase(FLEncoder e) FLAPI { - if (e->isFleece()) - return e->fleeceEncoder->base(); - return {}; -} - -size_t FLEncoder_GetNextWritePos(FLEncoder e) FLAPI { - if (e->isFleece()) - return e->fleeceEncoder->nextWritePos(); - return 0; -} - -size_t FLEncoder_BytesWritten(FLEncoder e) FLAPI { - return ENCODER_DO(e, bytesWritten()); -} - -intptr_t FLEncoder_LastValueWritten(FLEncoder e) FLAPI { - return e->isFleece() ? intptr_t(e->fleeceEncoder->lastValueWritten()) : kFLNoWrittenValue; -} - -bool FLEncoder_WriteValueAgain(FLEncoder e, intptr_t prewritten) FLAPI { - return e->isFleece() && e->fleeceEncoder->writeValueAgain(Encoder::PreWrittenValue(prewritten)); -} - -bool FLEncoder_WriteNull(FLEncoder e) FLAPI {ENCODER_TRY(e, writeNull());} -bool FLEncoder_WriteUndefined(FLEncoder e) FLAPI {ENCODER_TRY(e, writeUndefined());} -bool FLEncoder_WriteBool(FLEncoder e, bool b) FLAPI {ENCODER_TRY(e, writeBool(b));} -bool FLEncoder_WriteInt(FLEncoder e, int64_t i) FLAPI {ENCODER_TRY(e, writeInt(i));} -bool FLEncoder_WriteUInt(FLEncoder e, uint64_t u) FLAPI {ENCODER_TRY(e, writeUInt(u));} -bool FLEncoder_WriteFloat(FLEncoder e, float f) FLAPI {ENCODER_TRY(e, writeFloat(f));} -bool FLEncoder_WriteDouble(FLEncoder e, double d) FLAPI {ENCODER_TRY(e, writeDouble(d));} -bool FLEncoder_WriteString(FLEncoder e, FLSlice s) FLAPI {ENCODER_TRY(e, writeString(s));} -bool FLEncoder_WriteDateString(FLEncoder e, FLTimestamp ts, bool asUTC) - FLAPI {ENCODER_TRY(e, writeDateString(ts,asUTC));} -bool FLEncoder_WriteData(FLEncoder e, FLSlice d) FLAPI {ENCODER_TRY(e, writeData(d));} -bool FLEncoder_WriteRaw(FLEncoder e, FLSlice r) FLAPI {ENCODER_TRY(e, writeRaw(r));} -bool FLEncoder_WriteValue(FLEncoder e, FLValue v) FLAPI {ENCODER_TRY(e, writeValue(v));} - -bool FLEncoder_BeginArray(FLEncoder e, size_t reserve) FLAPI {ENCODER_TRY(e, beginArray(reserve));} -bool FLEncoder_EndArray(FLEncoder e) FLAPI {ENCODER_TRY(e, endArray());} -bool FLEncoder_BeginDict(FLEncoder e, size_t reserve) FLAPI {ENCODER_TRY(e, beginDictionary(reserve));} -bool FLEncoder_WriteKey(FLEncoder e, FLSlice s) FLAPI {ENCODER_TRY(e, writeKey(s));} -bool FLEncoder_WriteKeyValue(FLEncoder e, FLValue key) FLAPI {ENCODER_TRY(e, writeKey(key));} -bool FLEncoder_EndDict(FLEncoder e) FLAPI {ENCODER_TRY(e, endDictionary());} - -void FLJSONEncoder_NextDocument(FLEncoder e) FLAPI { - if (!e->isFleece()) - e->jsonEncoder->nextDocument(); -} - -bool FLEncoder_ConvertJSON(FLEncoder e, FLSlice json) FLAPI { - if (!e->hasError()) { - try { - if (e->isFleece()) { - JSONConverter *jc = e->jsonConverter.get(); - if (jc) { - jc->reset(); - } else { - jc = new JSONConverter(*e->fleeceEncoder); - e->jsonConverter.reset(jc); - } - if (jc->encodeJSON(json)) { // encodeJSON can throw - return true; - } else { - e->errorCode = (FLError)jc->errorCode(); - e->errorMessage = jc->errorMessage(); - } - } else { - e->jsonEncoder->writeJSON(json); - return true; - } - } catch (const std::exception &x) { - e->recordException(x); - } - } - return false; -} - -FLError FLEncoder_GetError(FLEncoder e) FLAPI { - return (FLError)e->errorCode; -} - -const char* FL_NULLABLE FLEncoder_GetErrorMessage(FLEncoder e) FLAPI { - return e->errorMessage.empty() ? nullptr : e->errorMessage.c_str(); -} - -void FLEncoder_SetExtraInfo(FLEncoder e, void* FL_NULLABLE info) FLAPI { - e->extraInfo = info; -} - -void* FL_NULLABLE FLEncoder_GetExtraInfo(FLEncoder e) FLAPI { - return e->extraInfo; -} - -FLSliceResult FLEncoder_Snip(FLEncoder e) FLAPI { - if (e->isFleece()) - return FLSliceResult(e->fleeceEncoder->snip()); - else - return {}; -} - -size_t FLEncoder_FinishItem(FLEncoder e) FLAPI { - if (e->isFleece()) - return e->fleeceEncoder->finishItem(); - return 0; -} - -FLDoc FL_NULLABLE FLEncoder_FinishDoc(FLEncoder e, FLError * FL_NULLABLE outError) FLAPI { - if (e->fleeceEncoder) { - if (!e->hasError()) { - try { - return retain(e->fleeceEncoder->finishDoc()); // finish() can throw - } catch (const std::exception &x) { - e->recordException(x); - } - } - } else { - e->errorCode = kFLUnsupported; // Doc class doesn't support JSON data - } - // Failure: - if (outError) - *outError = e->errorCode; - e->reset(); - return nullptr; -} - - -FLSliceResult FLEncoder_Finish(FLEncoder e, FLError * FL_NULLABLE outError) FLAPI { - if (!e->hasError()) { - try { - return toSliceResult(ENCODER_DO(e, finish())); // finish() can throw - } catch (const std::exception &x) { - e->recordException(x); - } - } - // Failure: - if (outError) - *outError = e->errorCode; - e->reset(); - return {nullptr, 0}; -} #pragma mark - BUILDER @@ -869,7 +682,7 @@ FLValue FL_NULLABLE FLDoc_GetRoot(FLDoc FL_NULLABLE doc) FLAPI {re FLSlice FLDoc_GetData(FLDoc FL_NULLABLE doc) FLAPI {return doc ? doc->data() : slice();} FLSliceResult FLDoc_GetAllocedData(FLDoc FL_NULLABLE doc) FLAPI { - return doc ? toSliceResult(doc->allocedData()) : FLSliceResult{}; + return doc ? FLSliceResult(doc->allocedData()) : FLSliceResult{}; } void* FL_NULLABLE FLDoc_GetAssociated(FLDoc FL_NULLABLE doc, const char *type) FLAPI { @@ -886,7 +699,7 @@ bool FLDoc_SetAssociated(FLDoc FL_NULLABLE doc, void * FL_NULLABLE pointer, cons FLSliceResult FLCreateJSONDelta(FLValue FL_NULLABLE old, FLValue FL_NULLABLE nuu) FLAPI { try { - return toSliceResult(JSONDelta::create(old, nuu)); + return FLSliceResult(JSONDelta::create(old, nuu)); } catch (const std::exception&) { return {}; } @@ -894,7 +707,7 @@ FLSliceResult FLCreateJSONDelta(FLValue FL_NULLABLE old, FLValue FL_NULLABLE nuu bool FLEncodeJSONDelta(FLValue FL_NULLABLE old, FLValue FL_NULLABLE nuu, FLEncoder jsonEncoder) FLAPI { try { - JSONEncoder *enc = jsonEncoder->jsonEncoder.get(); + JSONEncoder *enc = jsonEncoder->jsonEncoder(); precondition(enc); //TODO: Support encoding to Fleece JSONDelta::create(old, nuu, *enc); return true; @@ -907,14 +720,14 @@ bool FLEncodeJSONDelta(FLValue FL_NULLABLE old, FLValue FL_NULLABLE nuu, FLEncod FLSliceResult FLApplyJSONDelta(FLValue FL_NULLABLE old, FLSlice jsonDelta, FLError * FL_NULLABLE outError) FLAPI { try { - return toSliceResult(JSONDelta::apply(old, jsonDelta)); + return FLSliceResult(JSONDelta::apply(old, jsonDelta)); } catchError(outError); return {}; } bool FLEncodeApplyingJSONDelta(FLValue FL_NULLABLE old, FLSlice jsonDelta, FLEncoder encoder) FLAPI { try { - Encoder *enc = encoder->fleeceEncoder.get(); + Encoder *enc = encoder->fleeceEncoder(); if (!enc) FleeceException::_throw(EncodeError, "FLEncodeApplyingJSONDelta cannot encode JSON"); JSONDelta::apply(old, jsonDelta, false, *enc); diff --git a/Fleece/Core/Builder.cc b/Fleece/Core/Builder.cc index c7aaa0f6..12ca47dc 100644 --- a/Fleece/Core/Builder.cc +++ b/Fleece/Core/Builder.cc @@ -18,6 +18,9 @@ #include "Builder.hh" #include "FleeceException.hh" +#include "FleeceImpl.hh" +#include "JSONEncoder.hh" +#include "JSLexer.hh" #include "JSON5.hh" #include "MutableDict.hh" #include "MutableArray.hh" @@ -34,15 +37,11 @@ namespace fleece::impl::builder { using namespace std; - class Builder { + class Builder : private JSLexer { public: - Builder(slice formatString, va_list args) - :_format(formatString) - ,_in(_format) - { - va_copy(_args, args); - } + :JSLexer(formatString, args) + { } // Parses the format, interpolates args, and returns a new mutable Array or Dict. @@ -67,63 +66,55 @@ namespace fleece::impl::builder { void buildInto(MutableDict *dict) { - if (peekToken() != '{') - fail("expected '{'"); + peekToken('{', "expected '{'"); _buildInto(dict); } void buildInto(MutableArray *array) { - if (peekToken() != '[') - fail("expected '['"); + peekToken('[', "expected '['"); _buildInto(array); } - protected: // Parses a Fleece value from the input and stores it in the ValueSlot. // Recognizes a '%' specifier, and calls `putParameter` to read the value from the args. - bool _buildValue(ValueSlot &inSlot) { - switch (peekToken()) { - case '[': { + const bool _buildValue(ValueSlot &inSlot) { + switch (peekValue()) { + case ValueType::array: { Retained array = MutableArray::newArray(); _buildInto(array); inSlot.set(array); break; } - case '{': { + case ValueType::dict: { Retained dict = MutableDict::newDict(); _buildInto(dict); inSlot.set(dict); break; } - case 'n': + case ValueType::null: readIdentifier("null"); inSlot.set(nullValue); break; - case 't': + case ValueType::boolean_true: readIdentifier("true"); inSlot.set(true); break; - case 'f': + case ValueType::boolean_false: readIdentifier("false"); inSlot.set(false); break; - case '-': - case '+': - case '.': - case '0': case '1': case '2': case '3': case '4': - case '5': case '6': case '7': case '8': case '9': - readLiteralNumber(inSlot); + case ValueType::number: + std::visit([&](auto n) {inSlot.set(n);}, readNumber()); break; - case '"': - case '\'': - inSlot.set(readLiteralString()); + case ValueType::string: + inSlot.set(readString()); break; - case '%': - get(); + case ValueType::arg: + getChar(); return putParameter(inSlot); - default: + case ValueType::error: fail("invalid start of value"); } return true; @@ -132,347 +123,199 @@ namespace fleece::impl::builder { // Parses a JSON5 object from the input and adds its entries to `dict`. void _buildInto(MutableDict *dict) { - get(); // skip the opening '{' *without verifying* - char c; - while ('}' != (c = peekToken())) { - // Scan key: - string key; - if (c == '"' || c == '\'') { - // Key string: - key = readLiteralString(); - } else if (isalpha(c) || c == '_' || c == '$') { - // JSON5 unquoted key: - key = string(readIdentifier()); - } else { - fail("expected dict key"); - } - - if (peekToken() != ':') - fail("expected ':' after dict key"); - get(); - - // Value: + getChar(); // skip the opening '{' *without verifying* + while (peekToken() != '}') { + string key = readKey(); if (!_buildValue(dict->setting(key))) dict->remove(key); if (peekToken() == ',') // Note: JSON5 allows trailing `,` before `}` - get(); - else if (peekToken() != '}') - fail("unexpected token after dict item"); + getChar(); + else + peekToken('}', "unexpected token after dict item"); } - get(); // eat close bracket/brace + getChar(); // eat close bracket/brace } // Parses a JSON5 array from the input and adds its entries to `dict`. void _buildInto(MutableArray *array) { - get(); // skip the opening '[' *without verifying* + getChar(); // skip the opening '[' *without verifying* while (peekToken() != ']') { if (!_buildValue(array->appending())) array->remove(array->count() - 1, 1); if (peekToken() == ',') // Note: JSON5 allows trailing `,` before `]` - get(); - else if (peekToken() != ']') - fail("unexpected token after array item"); + getChar(); + else + peekToken(']', "unexpected token after array item"); } - get(); // eat close bracket/brace + getChar(); // eat close bracket/brace } -#pragma mark - PARAMETER SUBSTITUTION: - - // This is where those crazy printf format specs get parsed. bool putParameter(ValueSlot &inSlot) { - char c = get(); - // `-` means to skip this arg if it has a default value: - bool skipDefault = (c == '-'); - if (skipDefault) - c = get(); - - // Size specifier: - char size = ' '; - if (c == 'l' || c == 'q' || c == 'z') { - size = c; - c = get(); - if (size == 'l' && c == 'l') { - size = 'q'; - c = get(); - } + auto p = readArg(); + if (std::holds_alternative(p)) + return false; +#ifdef __APPLE__ + if (std::holds_alternative(p)) { + FLSlot_SetCFValue(FLSlot(&inSlot), CFTypeRef(std::get(p))); + return true; } - - switch (c) { - case 'c': case 'b': { - // Bool: - bool param = va_arg(_args, int) != 0; - if (skipDefault && !param) - return false; - inSlot.set(param); - break; - } - case 'd': case 'i': { - // Signed integers: - int64_t param; - if (size == 'q') - param = va_arg(_args, long long); - else if (size == 'z') - param = va_arg(_args, ptrdiff_t); - else if (size == 'l') - param = va_arg(_args, long); - else - param = va_arg(_args, int); - if (skipDefault && param == 0) - return false; - inSlot.set(param); - break; - } - case 'u': { - // Unsigned integers: - uint64_t param; - if (size == 'q') - param = va_arg(_args, unsigned long long); - else if (size == 'z') - param = va_arg(_args, size_t); - else if (size == 'l') - param = va_arg(_args, unsigned long); - else - param = va_arg(_args, unsigned int); - if (skipDefault && param == 0) - return false; - inSlot.set(param); - break; - } - case 'f': { - // Floats: - double param = va_arg(_args, double); - if (skipDefault && param == 0.0) - return false; - inSlot.set(param); - break; - } - case 's': { - // C string: - slice param(va_arg(_args, const char*)); - if (!param || (skipDefault && param.empty())) - return false; - inSlot.set(param); - break; - } - case '.': { - // Slice ("%.*s") -- takes 2 args: the start and size (see FMTSLICE() macro) - if (get() != '*' || get() != 's') - fail("'.' qualifier only supported in '%.*s'"); - int len = va_arg(_args, int); - auto str = va_arg(_args, void*); - if (!str || (skipDefault && len == 0)) - return false; - inSlot.set(slice(str, len)); - break; - } - case 'p': { - // "%p" is a Fleece value: - auto param = va_arg(_args, const Value*); - if (!param) - return false; - inSlot.set(param); - break; - } -#if __APPLE__ - case '@': { - // "%@" substitutes an Objective-C or CoreFoundation object. - auto param = va_arg(_args, CFTypeRef); - if (!param) - return false; - FLSlot_SetCFValue(FLSlot(&inSlot), param); - return true; - } #endif - default: - fail("unknown '%' format specifier"); - } + std::visit([&](auto val) { + if constexpr (!is_same_v && !is_same_v) + inSlot.set(val); + }, p); return true; } + }; -#pragma mark - LITERALS: - - - // Reads a numeric literal, storing it in the ValueSlot. - void readLiteralNumber(ValueSlot &inSlot) { - // Scan to the end of the number: - // (If the NumConversion.hh API used slice_istream I wouldn't have to do the scan) - auto start = _in.next(); - bool isNegative = (peek() == '-'); - if (isNegative || peek() == '+') - get(); - bool isFloat = false; - - char c; - do { - c = get(); - if (c == '.' || c == 'e' || c == 'E') - isFloat = true; - } while (isdigit(c) || c == '.' || c == 'e' || c == 'E' || c == '-' || c == '+'); - unget(); - auto numStr = string(slice(start, _in.next())); - - if (isFloat) { - double n; - if (ParseDouble(numStr.c_str(), n, false)) { - inSlot.set(n); - return; - } - } else if (isNegative) { - int64_t i; - if (ParseInteger(numStr.c_str(), i, false)) { - inSlot.set(i); - return; - } - } else { - uint64_t u; - if (ParseUnsignedInteger(numStr.c_str(), u, false)) { - inSlot.set(u); - return; - } - } - fail(("Invalid numeric literal " + numStr).c_str()); - } +#pragma mark - ENCODER: - // Reads a string literal in JSON5 format, returning its value - string readLiteralString() { - string out; - const char quote = get(); // single or double-quote - char c; - while (quote != (c = get())) { - if (c == '\\') { - switch ((c = get())) { - case 'n': c = '\n'; break; - case 'r': c = '\r'; break; - case 't': c = '\n'; break; - case 'u': fail("Unicode escapes not supported"); - // default is to leave c alone - } - } else if (c < ' ') { - fail("control character in string literal"); - } - out += c; - } - return out; - } + template + class Buildencoder : private JSLexer { + public: -#pragma mark - LEXER: + Buildencoder(ENCODER& encoder, slice formatString, va_list args) + :JSLexer(formatString, args) + ,_encoder(encoder) + { } - // Reads alphanumeric characters, returning the identifier as a string. - // (The 1st char is accepted even if not alphanumeric, on the assumption the caller already - // peeked at and approved it.) - slice readIdentifier() { - auto start = _in.next(); - get(); // consume the char the caller peeked - while (true) { - char c = peek(); - if (isalnum(c) || c == '_') - get(); - else + // Parses the format, interpolates args, and returns a new mutable Array or Dict. + void buildValue() { + switch (peekToken()) { + case '[': + writeArray(); + break; + case '{': + writeDict(); + break; + default: + writeDictInterior(); break; } - return slice(start, _in.next()); - } - - - // Reads an identifier and fails if it isn't equal to `expected`. - void readIdentifier(slice expected) { - if (readIdentifier() != expected) - fail("unknown identifier"); + finished(); } - - // Reads & ignores a JSON5 comment. - void skipComment() { - char c; - get(); // consume initial '/' - switch (get()) { - case '/': - do { - c = peek(); - if (c) - get(); - } while (c != 0 && c != '\n' && c != '\r'); + protected: + // Parses a Fleece value from the input and writes it, prefixed by the key if one's given. + // Recognizes a '%' specifier, and calls `writeParameter` to read the value from the args. + const bool writeValue(slice key = nullslice) { + auto v = peekValue(); + if (v != ValueType::arg && key) + _encoder.writeKey(key); + switch (v) { + case ValueType::array: + writeArray(); break; - case '*': { - bool star; - c = 0; - do { - star = (c == '*'); - c = get(); - } while (!(star && c=='/')); + case ValueType::dict: + writeDict(); break; - } + case ValueType::null: + readIdentifier("null"); + _encoder.writeNull(); + break; + case ValueType::boolean_true: + readIdentifier("true"); + _encoder.writeBool(true); + break; + case ValueType::boolean_false: + readIdentifier("false"); + _encoder.writeBool(false); + break; + case ValueType::number: + std::visit([&](auto n) {_encoder << n;}, readNumber()); + break; + case ValueType::string: + _encoder.writeString(readString()); + break; + case ValueType::arg: + getChar(); + return writeParameter(key); default: - fail("syntax error"); + fail("invalid start of value"); } + return true; } - // Fails if anything remains in the input but whitespace. - void finished() { - if (peekToken() != 0) - fail("unexpected characters after end of spec"); + // Parses a JSON5 object from the input and adds its entries to `dict`. + void writeDict() { + getChar(); // skip the opening '{' *without verifying* + _encoder.beginDictionary(); + if (peekToken() != '}') + writeDictInterior(); + else + getChar(); + _encoder.endDictionary(); + peekToken('}', "unexpected token after dict item"); + getChar(); // eat close bracket/brace } - // Skips any whitespace and JSON5 comments, then returns a peek at the next character. - char peekToken() { + void writeDictInterior() { while (true) { - char c = peek(); - if (c == 0) { - return c; // EOF - } else if (isspace(c)) { - get(); // skip whitespace - } else if (c == '/') { - skipComment(); - } else { - return c; - } - } - } - + writeValue(readKey()); - // Returns the next character from the input without consuming it, or 0 at EOF. - char peek() { - return _in.peekByte(); - } - - - // Reads the next character from the input. Fails if input is at EOF. - char get() { - if (_in.eof()) - fail("unexpected end"); - return _in.readByte(); + if (peekToken() != ',') // Note: JSON5 allows trailing `,` before `}` + break; + getChar(); + if (char c = peekToken(); c == '}' || c == '\0') + break; + } } - void unget() { - _in.unreadByte(); + // Parses a JSON5 array from the input and adds its entries to `dict`. + void writeArray() { + getChar(); // skip the opening '[' *without verifying* + _encoder.beginArray(); + while (peekToken() != ']') { + writeValue(); + if (peekToken() == ',') // Note: JSON5 allows trailing `,` before `]` + getChar(); + else + peekToken(']', "unexpected token after array item"); + } + getChar(); // eat close bracket/brace + _encoder.endArray(); } - // Throws an exception. - [[noreturn]] - void fail(const char *error) { - slice prefix = _format.upTo(_in.next()), suffix = _format.from(_in.next()); - FleeceException::_throw(InvalidData, "Build(): %s in format: %.*s💥%.*s", - error, FMTSLICE(prefix), FMTSLICE(suffix)); + // This is where those crazy printf format specs get parsed. + // A parameter may be skipped, if the format specifier has a `-` prefix and the value is 0/false/null. + // If a key is given it will be written before the value, unless the parameter is skipped. + bool writeParameter(slice key = nullslice) { + auto p = readArg(); + if (std::holds_alternative(p)) + return false; + if (key) + _encoder.writeKey(key); + std::visit([&](auto val) { + if constexpr (is_same_v) { + // + } else if constexpr (is_same_v) { + _encoder.writeBool(val); + } else if constexpr (is_same_v) { +#ifdef __APPLE__ + _encoder.writeCF(val); +#endif + } else { + _encoder << val; + } + }, p); + return true; } - private: - slice const _format; // The entire format string - slice_istream _in; // Stream for reading _format - va_list _args; // The caller-provided arguments + ENCODER& _encoder; // The Encoder or JSONEncoder I'm writing to }; @@ -488,6 +331,15 @@ namespace fleece::impl::builder { } + void VEncode(Encoder& encoder, slice format, va_list args) { + Buildencoder(encoder, format, args).buildValue(); + } + + void VEncode(JSONEncoder& encoder, slice format, va_list args) { + Buildencoder(encoder, format, args).buildValue(); + } + + RetainedConst Build(const char *format, ...) { va_list args; va_start(args, format); @@ -497,6 +349,14 @@ namespace fleece::impl::builder { } + void Encode(Encoder& encoder, const char *format, ...) { + va_list args; + va_start(args, format); + VEncode(encoder, format, args); + va_end(args); + } + + #ifdef __APPLE__ RetainedConst BuildCF(CFStringRef cfFormat, ...) { va_list args; @@ -505,6 +365,13 @@ namespace fleece::impl::builder { va_end(args); return result; } + + void EncodeCF(Encoder& encoder, CFStringRef cfFormat, ...) { + va_list args; + va_start(args, cfFormat); + VEncode(encoder, nsstring_slice(cfFormat), args); + va_end(args); + } #endif diff --git a/Fleece/Core/Builder.hh b/Fleece/Core/Builder.hh index 188a67a4..6e8d17f0 100644 --- a/Fleece/Core/Builder.hh +++ b/Fleece/Core/Builder.hh @@ -5,10 +5,21 @@ // #pragma once -#include "FleeceImpl.hh" -#include "slice_stream.hh" +#include "Value.hh" +#include "fleece/RefCounted.hh" #include +#ifdef __APPLE__ +#include +#endif + +namespace fleece::impl { + class Encoder; + class JSONEncoder; + class MutableArray; + class MutableDict; +} + namespace fleece::impl::builder { /** Creates a MutableArray or MutableDict by reading the format string and following arguments. @@ -42,7 +53,6 @@ namespace fleece::impl::builder { location where the error occurred. */ RetainedConst Build(const char *format, ...) __printflike(1, 2); - /** Variant of \ref Build that takes a pre-existing `va_list`. */ RetainedConst VBuild(const char *format, va_list args); RetainedConst VBuild(slice format, va_list args); @@ -58,11 +68,29 @@ namespace fleece::impl::builder { /** Variant of \ref Put that takes a pre-existing `va_list`. */ void VPut(Value*, const char *format, va_list args); + + /** Variant of Build that writes the value to an Encoder. + Format string syntax is the same, except that the braces around the outer dictionary may be removed, + which causes its keys and values to be added to the Encoder's current dictionary. */ + void Encode(Encoder&, const char *format, ...) __printflike(2, 3); + void Encode(JSONEncoder&, const char *format, ...) __printflike(2, 3); + + /** Variant of \ref Build that writes to an Encoder and takes a pre-existing `va_list`. */ + void VEncode(Encoder&, slice format, va_list args); + void VEncode(JSONEncoder&, slice format, va_list args); + + #ifdef __APPLE__ /** Variant of Build that allows `%@` for [Core]Foundation values; the corresponding arg must be a `CFStringRef`, `CFNumberRef`, `CFArrayRef` or `CFDictionaryRef`. \note The format string is a `CFStringRef` not a `char*`, because `CF_FORMAT_FUNCTION` requires that. */ RetainedConst BuildCF(CFStringRef format, ...) CF_FORMAT_FUNCTION(1, 2); + + /** Variant of Build to an Encoder that allows `%@` for [Core]Foundation values; the corresponding arg + must be a `CFStringRef`, `CFNumberRef`, `CFArrayRef` or `CFDictionaryRef`. + \note The format string is a `CFStringRef` not a `char*`, because `CF_FORMAT_FUNCTION` + requires that. */ + void EncodeCF(Encoder&, CFStringRef format, ...) CF_FORMAT_FUNCTION(2, 3); #endif } diff --git a/Fleece/Core/Encoder.hh b/Fleece/Core/Encoder.hh index c9e156c6..4788b8ec 100644 --- a/Fleece/Core/Encoder.hh +++ b/Fleece/Core/Encoder.hh @@ -98,6 +98,11 @@ namespace fleece { namespace impl { the callback can invoke the Encoder to write a different Value instead if it likes. */ void writeValue(const Value* NONNULL v, WriteValueFunc fn) {writeValue(v, &fn);} +#ifdef __APPLE__ + /** Writes a CoreFoundation value (CFTypeRef). Supported types are the ones allowed by + NSJSONSerialization, as well as CFData. */ + void writeCF(const void* cfValue); +#endif #ifdef __OBJC__ /** Writes an Objective-C object. Supported classes are the ones allowed by NSJSONSerialization, as well as NSData. */ diff --git a/Fleece/Core/JSLexer.hh b/Fleece/Core/JSLexer.hh new file mode 100644 index 00000000..0c494979 --- /dev/null +++ b/Fleece/Core/JSLexer.hh @@ -0,0 +1,355 @@ +// +// JSLexer.hh +// +// Copyright © 2024 Couchbase. All rights reserved. +// + +#pragma once +#include "NumConversion.hh" +#include "fleece/slice.hh" +#include "slice_stream.hh" +#include +#include +#include + +namespace fleece::impl { + class Value; + + /** Simple lexer for JSON5. */ + class JSLexer { + public: + JSLexer(slice formatString, va_list args) + :_format(formatString) + ,_in(_format) + { + va_copy(_args, args); + } + + // Returns the next character from the input without consuming it, or 0 at EOF. + char peekChar() { + return _in.peekByte(); + } + + + // Reads the next character from the input. Fails if input is at EOF. + char getChar() { + if (_in.eof()) + fail("unexpected end"); + return _in.readByte(); + } + + + void ungetChar() { + _in.unreadByte(); + } + + + // Skips any whitespace and JSON5 comments, then returns a peek at the next character. + char peekToken() { + while (true) { + char c = peekChar(); + if (c == 0) { + return c; // EOF + } else if (isspace(c)) { + getChar(); // skip whitespace + } else if (c == '/') { + skipComment(); + } else { + return c; + } + } + } + + + void peekToken(char c, const char* errorMessage) { + if (char actual = peekToken(); actual != c) { + if (_in.eof()) + fail("unexpected end"); + else + fail(errorMessage); + } + } + + + // Fails if anything remains in the input but whitespace. + void finished() { + if (peekToken() != 0) + fail("unexpected characters after end of spec"); + } + + + // Reads alphanumeric characters, returning the identifier as a string. + // (The 1st char is accepted even if not alphanumeric, on the assumption the caller already + // peeked at and approved it.) + slice readIdentifier() { + auto start = _in.next(); + getChar(); // consume the char the caller peeked + while (true) { + char c = peekChar(); + if (isalnum(c) || c == '_') + getChar(); + else + break; + } + return slice(start, _in.next()); + } + + + // Reads an identifier and fails if it isn't equal to `expected`. + void readIdentifier(slice expected) { + if (readIdentifier() != expected) + fail("unknown identifier"); + } + + + // Reads a dictionary key, including the trailing ':'. + std::string readKey() { + std::string key; + char c = peekToken(); + if (c == '"' || c == '\'') + key = readString(); + else if (isalpha(c) || c == '_' || c == '$') + key = std::string(readIdentifier()); + else + fail("expected dict key"); + if (peekToken() != ':') + fail("expected ':' after dict key"); + getChar(); + return key; + } + + enum class ValueType : uint8_t { + error, array, dict, null, boolean_true, boolean_false, number, string, arg + }; + + /// Returns the type of the next value. + ValueType peekValue() {return kTokenTypes[unsigned(peekToken())];} + + + // Reads a numeric literal. + std::variant readNumber() { + // Scan to the end of the number: + // (If the NumConversion.hh API used slice_istream I wouldn't have to do the scan) + auto start = _in.next(); + bool isNegative = (peekChar() == '-'); + if (isNegative || peekChar() == '+') + getChar(); + bool isFloat = false; + + char c; + do { + c = getChar(); + if (c == '.' || c == 'e' || c == 'E') + isFloat = true; + } while (isdigit(c) || c == '.' || c == 'e' || c == 'E' || c == '-' || c == '+'); + ungetChar(); + auto numStr = std::string(slice(start, _in.next())); + + if (isFloat) { + double n; + if (ParseDouble(numStr.c_str(), n, false)) + return n; + } else if (isNegative) { + int64_t i; + if (ParseInteger(numStr.c_str(), i, false)) + return i; + } else { + uint64_t u; + if (ParseUnsignedInteger(numStr.c_str(), u, false)) + return u; + } + fail(("Invalid numeric literal " + numStr).c_str()); + } + + + // Reads a string literal in JSON5 format, returning its value. + std::string readString() { + std::string out; + const char quote = getChar(); // single or double-quote + char c; + while (quote != (c = getChar())) { + if (c == '\\') { + switch ((c = getChar())) { + case 'n': c = '\n'; break; + case 'r': c = '\r'; break; + case 't': c = '\n'; break; + case 'u': fail("Unicode escapes not supported"); + // default is to leave c alone + } + } else if (c < ' ') { + fail("control character in string literal"); + } + out += c; + } + return out; + } + + enum class ArgType { // these are the indices of the variant returned by readArg() + None, Bool, Int, UInt, Double, Slice, Value, CF + }; + + // Parses those crazy printf format specs and decodes an argument from the va_list. + // Returning type `monostate` means the parameter is suppressed because of the `-` flag. + // Returning type `const void*` denotes a CF type (Apple platforms only.) + std::variant readArg() { + char c = getChar(); + // `-` means to skip this arg if it has a default value: + bool skipDefault = (c == '-'); + if (skipDefault) + c = getChar(); + + // Size specifier: + char size = ' '; + if (c == 'l' || c == 'q' || c == 'z') { + size = c; + c = getChar(); + if (size == 'l' && c == 'l') { + size = 'q'; + c = getChar(); + } + } + + switch (c) { + case 'c': case 'b': { + // Bool: + bool param = va_arg(_args, int) != 0; + if (skipDefault && !param) + return std::monostate{}; + return param; + } + case 'd': case 'i': { + // Signed integers: + int64_t param; + if (size == 'q') + param = va_arg(_args, long long); + else if (size == 'z') + param = va_arg(_args, ptrdiff_t); + else if (size == 'l') + param = va_arg(_args, long); + else + param = va_arg(_args, int); + if (skipDefault && param == 0) + return std::monostate{}; + return param; + } + case 'u': { + // Unsigned integers: + uint64_t param; + if (size == 'q') + param = va_arg(_args, unsigned long long); + else if (size == 'z') + param = va_arg(_args, size_t); + else if (size == 'l') + param = va_arg(_args, unsigned long); + else + param = va_arg(_args, unsigned int); + if (skipDefault && param == 0) + return std::monostate{}; + return param; + } + case 'f': { + // Floats: + double param = va_arg(_args, double); + if (skipDefault && param == 0.0) + return std::monostate{}; + return param; + } + case 's': { + // C string: + slice param(va_arg(_args, const char*)); + if (!param || (skipDefault && param.empty())) + return std::monostate{}; + return param; + } + case '.': { + // Slice ("%.*s") -- takes 2 args: the start and size (see FMTSLICE() macro) + if (getChar() != '*' || getChar() != 's') + fail("'.' qualifier only supported in '%.*s'"); + int len = va_arg(_args, int); + auto str = va_arg(_args, void*); + if (!str || (skipDefault && len == 0)) + return std::monostate{}; + return slice(str, len); + } + case 'p': { + // "%p" is a Fleece value: + const Value* param = va_arg(_args, const Value*); + if (!param) + return std::monostate{}; + return param; + } +#if __APPLE__ + case '@': { + // "%@" substitutes an Objective-C or CoreFoundation object. + auto param = va_arg(_args, const void*); // really CFTypeRef + if (!param) + return std::monostate{}; + return param; + } +#endif + default: + fail("unknown '%' format specifier"); + } + } + + + // Throws an exception. + [[noreturn]] + void fail(const char *error) { + slice prefix = _format.upTo(_in.next()), suffix = _format.from(_in.next()); + FleeceException::_throw(InvalidData, "Build(): %s in format: %.*s💥%.*s", + error, FMTSLICE(prefix), FMTSLICE(suffix)); + } + + private: + + // Reads & ignores a JSON5 comment. + void skipComment() { + char c; + getChar(); // consume initial '/' + switch (getChar()) { + case '/': + do { + c = peekChar(); + if (c) + getChar(); + } while (c != 0 && c != '\n' && c != '\r'); + break; + case '*': { + bool star; + c = 0; + do { + star = (c == '*'); + c = getChar(); + } while (!(star && c=='/')); + break; + } + default: + fail("syntax error"); + } + } + + static constexpr std::array kTokenTypes = []{ + // Build kTokenTypes at compile time: + std::array vals = {}; + vals['['] = ValueType::array; + vals['{'] = ValueType::dict; + vals['n'] = ValueType::null; + vals['t'] = ValueType::boolean_true; + vals['f'] = ValueType::boolean_false; + vals['-'] = vals['+'] = vals['.'] = ValueType::number; + for (int d = '0'; d <= '9'; ++d) + vals[d] = ValueType::number; + vals['"'] = vals['\''] = ValueType::string; + vals['%'] = ValueType::arg; + return vals; + }(); + + slice const _format; // The entire format string + slice_istream _in; // Stream for reading _format + va_list _args; // The caller-provided arguments + }; + + + +} diff --git a/Fleece/Support/Fleece.exp b/Fleece/Support/Fleece.exp index c913f77e..4c95ddff 100644 --- a/Fleece/Support/Fleece.exp +++ b/Fleece/Support/Fleece.exp @@ -59,6 +59,9 @@ _FLValue_ToJSON5 _FLValue_FindDoc _FLValue_Retain _FLValue_Release +_FLValue_NewWithFormat +_FLValue_NewWithFormatV +_FLValue_UpdateWithFormatV _FLData_ConvertJSON _FLJSON5_ToJSON @@ -85,6 +88,7 @@ _FLMutableArray_Remove _FLMutableArray_Resize _FLMutableArray_GetMutableArray _FLMutableArray_GetMutableDict +_FLMutableArray_UpdateWithFormat _FLDict_Count _FLDict_IsEmpty @@ -111,6 +115,7 @@ _FLMutableDict_Remove _FLMutableDict_RemoveAll _FLMutableDict_GetMutableArray _FLMutableDict_GetMutableDict +_FLMutableDict_UpdateWithFormat _FLSlot_SetNull _FLSlot_SetBool @@ -158,6 +163,9 @@ _FLEncoder_FinishItem _FLEncoder_GetBase _FLEncoder_GetNextWritePos _FLEncoder_SuppressTrailer +_FLEncoder_ConvertJSON +_FLEncoder_WriteFormatted +_FLEncoder_WriteFormattedArgs _FLKeyPath_New _FLKeyPath_Free diff --git a/Fleece/Support/JSONEncoder.cc b/Fleece/Support/JSONEncoder.cc index f08b62a1..0fc2ce9a 100644 --- a/Fleece/Support/JSONEncoder.cc +++ b/Fleece/Support/JSONEncoder.cc @@ -17,7 +17,7 @@ #include #include "betterassert.hh" -namespace fleece { namespace impl { +namespace fleece::impl { void JSONEncoder::writeString(slice str) { comma(); @@ -173,4 +173,25 @@ namespace fleece { namespace impl { } } -} } + void JSONEncoder::comma() { + if (_first) + _first = false; + else + _out << ','; + } + + void JSONEncoder::nest() { + ++_nesting; + } + + void JSONEncoder::unnest() { + if (--_nesting < 0) + FleeceException::_throw(JSONError, "Too many endArray/endDictionary calls"); + } + + void JSONEncoder::requireUnnested() { + if (_nesting != 0) + FleeceException::_throw(JSONError, "Unclosed array or dictionary"); + } + +} diff --git a/Fleece/Support/JSONEncoder.hh b/Fleece/Support/JSONEncoder.hh index 774d91ca..65e5d7b6 100644 --- a/Fleece/Support/JSONEncoder.hh +++ b/Fleece/Support/JSONEncoder.hh @@ -36,13 +36,13 @@ namespace fleece { namespace impl { size_t bytesWritten() const {return _out.length();} /** Returns the encoded data. */ - alloc_slice finish() {return _out.finish();} + alloc_slice finish() {requireUnnested(); return _out.finish();} /** Resets the encoder so it can be used again. */ - void reset() {_out.reset(); _first = true;} + void reset() {_out.reset(); _nesting = 0; _first = true;} /** Adds a newline between consecutive top-level values. */ - void nextDocument() {_out << '\n'; _first = true;} + void nextDocument() {requireUnnested(); _out << '\n'; _first = true;} /////// Writing data: @@ -65,20 +65,24 @@ namespace fleece { namespace impl { void writeJSON(slice json) {comma(); _out << json;} void writeRaw(slice raw) {_out << raw;} +#ifdef __APPLE__ + void writeCF(const void*) {FleeceException::_throw(JSONError, + "Encoding CF value to JSON is unimplemented");} +#endif #ifdef __OBJC__ - [[noreturn]] void writeObjC(id) {FleeceException::_throw(JSONError, - "Encoding Obj-C to JSON is unimplemented");} + void writeObjC(id) {FleeceException::_throw(JSONError, + "Encoding Obj-C to JSON is unimplemented");} #endif //////// Writing arrays: - void beginArray() {comma(); _out << '['; _first = true;} - void endArray() {_out << ']'; _first = false;} + void beginArray() {nest(); comma(); _out << '['; _first = true;} + void endArray() {unnest(); _out << ']'; _first = false;} //////// Writing dictionaries: - void beginDictionary() {comma(); _out << '{'; _first = true;} - void endDictionary() {_out << '}'; _first = false;} + void beginDictionary() {nest(); comma(); _out << '{'; _first = true;} + void endDictionary() {unnest(); _out << '}'; _first = false;} void writeKey(slice s); void writeKey(const std::string &s) {writeKey(slice(s));} @@ -108,13 +112,10 @@ namespace fleece { namespace impl { private: void writeDict(const Dict*); - - void comma() { - if (_first) - _first = false; - else - _out << ','; - } + void comma(); + void nest(); + void unnest(); + void requireUnnested(); #pragma GCC diagnostic push #pragma GCC diagnostic ignored "-Wformat-nonliteral" @@ -137,6 +138,7 @@ namespace fleece { namespace impl { } Writer _out; + int _nesting {0}; bool _json5 {false}; bool _canonical {false}; bool _first {true}; diff --git a/ObjC/Encoder+ObjC.mm b/ObjC/Encoder+ObjC.mm index 015ff285..ec6f37a9 100644 --- a/ObjC/Encoder+ObjC.mm +++ b/ObjC/Encoder+ObjC.mm @@ -25,6 +25,10 @@ FLEncoderImpl enc(this); [obj fl_encodeToFLEncoder: &enc]; } + + void Encoder::writeCF(CFTypeRef obj) { + writeObjC((__bridge id)obj); + } } diff --git a/Tests/BuilderTests.cc b/Tests/BuilderTests.cc index ac06671f..cdc851cd 100644 --- a/Tests/BuilderTests.cc +++ b/Tests/BuilderTests.cc @@ -17,6 +17,7 @@ // #include "FleeceTests.hh" +#include "FleeceImpl.hh" #include "Builder.hh" #include @@ -63,7 +64,7 @@ TEST_CASE("Builder String Literals", "[Builder]") { TEST_CASE("Builder Basic Dict", "[Builder]") { - auto v = builder::Build("{name:%s, size:%d, weight:%f}", + auto v = builder::Build("{name:%s, size:%d, weight:%f,}", "Zegpold", 12, 3.14); auto dict = v->asDict(); REQUIRE(dict); @@ -75,7 +76,7 @@ TEST_CASE("Builder Basic Dict", "[Builder]") { TEST_CASE("Builder Basic Array", "[Builder]") { - auto v = builder::Build("[%s, %d, %f]", + auto v = builder::Build("[%s, %d, %f,]", "Zegpold", 12, 3.14); auto array = v->asArray(); REQUIRE(array); @@ -168,3 +169,155 @@ TEST_CASE("Builder CoreFoundation Params", "[Builder]") { CHECK(v->toJSONString() == R"(["Zegpold",12345678])"); } #endif // __APPLE__ + + +#pragma mark - ENCODE: + + +__printflike(1, 2) static RetainedConst buildWithEncoder(const char* format, ...) { + Encoder enc; + va_list args; + va_start(args, format); + builder::VEncode(enc, format, args); + va_end(args); + return enc.finishDoc()->root(); +} + + +TEST_CASE("Builder Encode Literals", "[Builder]") { + auto v = buildWithEncoder("[null, false, true, 0, 1, -12, +123, 123.5, -123.5, +123.5, 123e-4]"); + REQUIRE(v); + CHECK(v->toJSONString() == "[null,false,true,0,1,-12,123,123.5,-123.5,123.5,0.0123]"); +} + + +TEST_CASE("Builder Encode String Literals", "[Builder]") { + auto v = buildWithEncoder(R"({a : 'foo\'', $b : "bar\"rab", _c_ : "", _ : "\r\\"})"); + REQUIRE(v); + std::string expected = R"({"$b":"bar\"rab","_":"\r\\","_c_":"","a":"foo'"})"; + CHECK(v->toJSONString() == expected); +} + + +TEST_CASE("Builder Encode Basic Dict", "[Builder]") { + auto v = buildWithEncoder("{name:%s, size:%d, weight:%f}", + "Zegpold", 12, 3.14); + auto dict = v->asDict(); + REQUIRE(dict); + CHECK(dict->get("name")->asString() == "Zegpold"); + CHECK(dict->get("size")->asInt() == 12); + CHECK(dict->get("weight")->asDouble() == 3.14); + CHECK(v->toJSONString() == R"({"name":"Zegpold","size":12,"weight":3.14})"); +} + + +TEST_CASE("Builder Encode Basic Array", "[Builder]") { + auto v = buildWithEncoder("[%s, %d, %f]", + "Zegpold", 12, 3.14); + auto array = v->asArray(); + REQUIRE(array); + CHECK(array->get(0)->asString() == "Zegpold"); + CHECK(array->get(1)->asInt() == 12); + CHECK(array->get(2)->asDouble() == 3.14); + CHECK(v->toJSONString() == R"(["Zegpold",12,3.14])"); +} + + +TEST_CASE("Builder Encode Nesting", "[Builder]") { + auto v = buildWithEncoder("{name:%s, coords:[%d, %d], info:{nickname:%s}}", + "Zegpold", 4, 5, "Zeggy"); + CHECK(v->toJSONString() == R"({"coords":[4,5],"info":{"nickname":"Zeggy"},"name":"Zegpold"})"); +} + + +TEST_CASE("Builder Encode Bool Params", "[Builder]") { + bool t = true, f = false; + auto v = buildWithEncoder("[%c,%c]", char(t), char(f)); + CHECK(v->toJSONString() == R"([true,false])"); +} + + +TEST_CASE("Builder Encode Integer Params", "[Builder]") { + int i0 = INT_MIN, i1 = INT_MAX; + unsigned u = UINT_MAX; + long l0 = LONG_MIN, l1 = LONG_MAX; + unsigned long ul = ULONG_MAX; + long long ll0 = LLONG_MIN, ll1 = LLONG_MAX; + unsigned long long ull = ULLONG_MAX; + ptrdiff_t p0 = PTRDIFF_MIN, p1 = PTRDIFF_MAX; + size_t z1 = SIZE_MAX; + auto v = buildWithEncoder("[[%d, %d, %u], [%ld,%ld,%lu], [%lld,%lld,%llu], [%zd,%zd,%zu]]", + i0, i1, u, l0, l1, ul, ll0, ll1, ull, p0, p1, z1); + std::string expected32 = "[-2147483648,2147483647,4294967295]"; + std::string expected64 = "[-9223372036854775808,9223372036854775807,18446744073709551615]"; + std::string expected = + "[" + (sizeof(int) == 8 ? expected64 : expected32) + "," + + (sizeof(long) == 8 ? expected64 : expected32) + "," + + (sizeof(long long) == 8 ? expected64 : expected32) + "," + + (sizeof(size_t) == 8 ? expected64 : expected32) + "]"; + CHECK(v->toJSONString() == expected); +} + + +TEST_CASE("Builder Encode Value Params", "[Builder]") { + auto v1 = buildWithEncoder("[%s, %d, %f]", + "Zegpold", 12, 3.14); + auto v2 = buildWithEncoder("{v1: %p, v2: %p}", v1.get(), v1.get()); + CHECK(v2->toJSONString() == R"({"v1":["Zegpold",12,3.14],"v2":["Zegpold",12,3.14]})"); +} + + +TEST_CASE("Builder Encode Empty Strings", "[Builder]") { + const char *str = ""; + slice sl(str); + auto v = buildWithEncoder("{a:%s, b:%.*s, d:[%s, %.*s]}", + str, FMTSLICE(sl), str, FMTSLICE(sl)); + CHECK(v->toJSONString() == R"({"a":"","b":"","d":["",""]})"); +} + + +TEST_CASE("Builder Encode Null Args", "[Builder]") { + const char *str = nullptr; + slice sl = nullslice; + const Value *val = nullptr; + auto v = buildWithEncoder("{a:%s, b:%.*s, c:%p, d:[%s, %.*s, %p]}", + str, FMTSLICE(sl), val, str, FMTSLICE(sl), val); + CHECK(v->toJSONString() == R"({"d":[]})"); +} + + +TEST_CASE("Builder Encode Default Suppression", "[Builder]") { + const char *str = ""; + slice sl(str); + auto v = buildWithEncoder("[%-c, %-d, %-f, %-s, %-.*s]", + char(false), 0, 0.0, str, FMTSLICE(sl)); + CHECK(v->toJSONString() == R"([])"); +} + + +TEST_CASE("Builder Encode Into Existing Dict", "[Builder]") { + Encoder enc; + enc.beginDictionary(); + enc.writeKey("A"); + enc.writeInt(1); + + builder::Encode(enc, "name:%s, size:%d, weight:%f", // Missing braces means encoding into open dict + "Zegpold", 12, 3.14); + + enc.writeKey("Z"); + enc.writeInt(26); + enc.endDictionary(); + + Retained doc = enc.finishDoc(); + auto dict = doc->asDict(); + REQUIRE(dict); + CHECK(dict->get("A")->asInt() == 1); + CHECK(dict->get("name")->asString() == "Zegpold"); + CHECK(dict->get("size")->asInt() == 12); + CHECK(dict->get("weight")->asDouble() == 3.14); + CHECK(dict->get("Z")->asInt() == 26); + CHECK(dict->toJSONString() == R"({"A":1,"Z":26,"name":"Zegpold","size":12,"weight":3.14})"); +} + + + diff --git a/cmake/platform_base.cmake b/cmake/platform_base.cmake index cf1181f9..0b30eed3 100644 --- a/cmake/platform_base.cmake +++ b/cmake/platform_base.cmake @@ -8,6 +8,7 @@ function(set_source_files_base) set( ${BASE_SSS_RESULT} Fleece/API_Impl/Fleece.cc + Fleece/API_Impl/FLEncoder.cc Fleece/API_Impl/FLSlice.cc Fleece/Core/Array.cc Fleece/Core/Builder.cc