// baljsn_datumutil.h                                                 -*-C++-*-
#ifndef INCLUDED_BALJSN_DATUMUTIL
#define INCLUDED_BALJSN_DATUMUTIL

#include <bsls_ident.h>
BSLS_IDENT("$Id$ $CSID$")

//@PURPOSE: Provide utilities converting between 'bdld::Datum' and JSON data.
//
//@CLASSES:
//  baljsn::DatumUtil: utilities converting between 'bdld::Datum' and JSON data
//
//@DESCRIPTION: This component provides a struct, 'baljsn::DatumUtil', that is
// a namespace for a suite of functions that convert a 'bdld::Datum' into a
// JSON string, and back.
//
///Mapping Data Types between 'Datum' and JSON
///-------------------------------------------
// While most scalar types supported by 'Datum' can be encoded into a JSON
// string, only the subset of types represented natively in JSON -- number
// (represented in C++ as a 'double'), 'string', 'bool', 'null', array, and map
// types -- will be populated in a 'Datum' decoded from a JSON string.  If one
// were to encode a 'Datum' containing a type not natively supported by JSON,
// if that JSON string were decoded back into a 'Datum' object, the resulting
// 'Datum' would not be equal to the original value.  For example, a 'Datum'
// containing an integer would be encoded into a JSON number, and then decoded
// back into a 'Datum' using 'double' to represent that number.  Note that
// 'DatumUtil' uses a *more permissive* parser for numerical values than the
// strict JSON standard specifies.  In particular, it is possible to parse
// 'NaN', 'Inf', or 'Infinity' into the corresponding singular 'double' values
// even though the JSON standard does not permit this, so applications should
// be ready to handle these kinds of values.  Also note that the 'encode'
// routines do not encode these singular 'double' values in these parseable
// formats.  Singular 'double' values will be rendered as strings (e.g., "+inf"
// or "nan") if the 'strictTypes' encoding configuration is 'false', and will
// result generate an encoding error if 'strictTypes' is 'true'.
//
// Clients wishing to ensure that encoding and then decoding results in a
// 'Datum' equal to the original value should use only 'Datum' types natively
// supported in JSON (see {'Supported Types'} below), and ensure that duplicate
// keys are not present in the source 'Datum' (duplicate keys in a 'Datum' map
// are typically an error, but the interface does allow them to be created).
// Enabling the 'strictTypes' option verifies that the types in encoded JSON
// fields can be decoded back into 'Datum' fields of equal value.  So, for
// example, enabling 'strictTypes' will result in 'encode' producing a positive
// return status if one of the encoded types is an 'int', because decoding the
// resulting JSON will produce a 'double'.  The 'strictTypes' option does not,
// however, verify that a Datum map contains unique keys.  For 'double' fields,
// 'strictTypes' will result in 'encode' returning a positive value if a
// singular 'double' value is encountered, such as a NaN or Infinity.
//
// The order of key/value pairs in objects in textual JSON passed to 'decode'
// is preserved in the decoded 'Datum'.  If multiple entries with the same
// 'key' are present in an object, 'decode' will return the *first* such value.
//
// The order of key/value pairs ('DatumMapEntry') in 'Datum' objects passed to
// 'encode' will be preserved in the resulting 'JSON', and all keys/value pairs
// will be present (including duplicate keys).  Duplicate keys will be rendered
// in an encoded JSON, even if 'strictTypes' checking is enabled.  Note that a
// Datum map containing duplicate keys is typically an error (the result of a
// incorrectly constructed Datum), but the public interface for Datum does not
// disallow creating such a 'Datum' object.
//
///Supported Types
///---------------
// The table below describes the set of types that a 'Datum' may be, whether it
// can be 'encode'd to JSON, and, if so, which JSON type will be 'decode'd if
// the value is read back in.
//
// The 'encode' routines will return a negative (error) status if the input
// 'datum' contains any field that is not 'JSON-able' in this table.
//
// If the 'DatumEncoderOptions' parameter is passed to an 'encode' routine and
// its 'strictTypes' field is 'true', then 'encode' will return a positive
// value if any value is encoded where the 'dataType' and 'decode type' columns
// in this table are different (and therefore the 'strictTypes ok?' column is
// 'no').
//
//..
//  dataType              JSON-able  JSON type   decode type    strictTypes ok?
//  --------              ---------  ---------   -----------    ---------------
//  e_NIL                 yes        null        e_NIL          yes
//  e_INTEGER             yes        number      e_DOUBLE       no
//  e_DOUBLE              yes        number      e_DOUBLE       yes [1]
//  e_STRING              yes        string      e_STRING       yes
//  e_BOOLEAN             yes        bool        e_BOOLEAN      yes
//  e_ERROR               no         N/A         N/A            no
//  e_DATE                yes        string      e_STRING       no
//  e_TIME                yes        string      e_STRING       no
//  e_DATETIME            yes        string      e_STRING       no
//  e_DATETIME_INTERVAL   yes        string      e_STRING       no
//  e_INTEGER64           yes        number      e_DOUBLE       no
//  e_USERDEFINED         no         N/A         N/A            no
//  e_BINARY              no         N/A         N/A            no
//  e_DECIMAL64 [2]       yes        number      e_STRING       no
//
//  dataType              JSON-able  JSON type   decode type    strictTypes ok?
//  --------              ---------  ---------   -----------    ---------------
//  e_ARRAY               yes        array       e_ARRAY        yes
//  e_MAP                 yes        map         e_MAP          yes
//  e_INT_MAP             no         N/A         N/A            no
//
// [1] Singular double values (e.g., inf and nan) are not permitted if
//     strictTypes is 'true', and will be rendered as strings if 'strictTypes'
//     is 'false'.
// [2] If the 'encodeQuotedDecimal64' attribute in the 'DatumEncoderOptions' is
//     'true' (the default), the 'Decimal64' values will be encoded as strings,
//     otherwise they will be encoded as numbers. Encoding a Decimal64 as a
//     JSON number will frequently result in it being later decoded as a binary
//     floating point number, and in the process losing digits of precision
//     that were the point of using the Decimal64 type in the first place.
//     Care should be taken when setting this option to 'false' (though it may
//     be useful when communicating with endpoints that are known to correctly
//     handle high precision JSON numbers).
//..
//: o *dataType* - the 'Datum' type value returned by the 'type()'
//:
//: o *JSON-able* - whether the type can be 'encode'd by this component.
//:
//: o *JSON type* - the JSON type used to 'encode' this value, if supported.
//:
//: o *decode type* - the 'Datum' type this 'encode'd value would be
//:    'decode'd into.
//:
//: o *strictTypes ok?* - 'encode' will return 0 on success even if
//:   'options->strictTypes()' is 'true'.
//
///Usage
///-----
// This section illustrates intended use of this component.
//
///Example 1: Encode (and decode) 'Datum' to (and from) a JSON string.
///- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
// The following example illustrates encoding a 'Datum' as a JSON string and
// then decoding that JSON string back into a 'Datum' object.
//
// First, we create our 'Datum' object, using the 'bdld::DatumMaker' utility:
//..
//  bsls::AlignedBuffer<8 * 1024>      buffer;
//  bdlma::BufferedSequentialAllocator bsa(buffer.buffer(), sizeof(buffer));
//  bdld::DatumMaker                   m(&bsa);
//
//  bdld::Datum books = m.a(m.m("Author", "Ann Leckie",
//                              "Title", "Ancilliary Justice"),
//                          m.m("Author", "John Scalzi",
//                              "Title", "Redshirts"));
//..
// Then, we convert the 'books' 'Datum' to formatted JSON:
//..
//  baljsn::DatumEncoderOptions bookOptions;
//  bookOptions.setEncodingStyle(baljsn::EncodingStyle::e_PRETTY);
//  bookOptions.setSpacesPerLevel(4);
//  bsl::string booksJSON(&bsa);
//
//  int rc = baljsn::DatumUtil::encode(&booksJSON, books, bookOptions);
//  if (0 != rc) {
//      // handle error
//  }
//..
// Next, we compare the result to the JSON we expect:
//..
//  const bsl::string EXPECTED_BOOKS_JSON = R"JSON([
//    {
//        "Author" : "Ann Leckie",
//        "Title" : "Ancilliary Justice"
//    },
//    {
//        "Author" : "John Scalzi",
//        "Title" : "Redshirts"
//    }
//  ])JSON";
//
//  assert(EXPECTED_BOOKS_JSON == booksJSON);
//..
// Finally, we can decode the 'booksJSON' and make sure we got the same value
// back:
//..
//  bdld::ManagedDatum decodedBooks;
//  rc = baljsn::DatumUtil::decode(&decodedBooks, booksJSON);
//  if (0 != rc) {
//      // handle error
//  }
//  assert(*decodedBooks == books);
//..
///Example 2: Converting JSON to 'Datum'
///- - - - - - - - - - - - - - - - - - -
// The following example illustrates decoding a string into a 'Datum' object.
//
// First, we create the JSON source, in both plain and formatted forms:
//..
//  const bsl::string plainFamilyJSON = "["
//                               "{\"firstName\":\"Homer\","
//                               "\"age\":34}"
//                               ",{\"firstName\":\"Marge\","
//                               "\"age\":34}"
//                               ",{\"firstName\":\"Bart\","
//                               "\"age\":10}"
//                               ",{\"firstName\":\"Lisa\","
//                               "\"age\":8}"
//                               ",{\"firstName\":\"Maggie\","
//                               "\"age\":1}"
//                               "]";
//
//  // Note that whitespace formatting is unimportant as long as the result is
//  // legal JSON.  This will generate the same 'Datum' as the single-line form
//  // above.
//  const bsl::string formattedFamilyJSON = R"JSON([
//      {
//          "firstName" : "Homer",
//          "age" : 34
//      },
//      {
//          "firstName" : "Marge",
//          "age" : 34
//      },
//      {
//          "firstName" : "Bart",
//          "age" : 10
//      },
//      {
//          "firstName" : "Lisa",
//          "age" : 8
//      },
//      {
//          "firstName" : "Maggie",
//          "age" : 1
//      }
//  ])JSON";
//..
// Then, we convert the single-line 'string' to a 'Datum':
//..
//  bdld::ManagedDatum family;
//  rc = baljsn::DatumUtil::decode(&family, plainFamilyJSON);
//  if (0 != rc) {
//      // handle error
//  }
//..
// Next, we convert the formatted 'string' to another 'Datum' and make sure
// that the results match:
//..
//  bdld::ManagedDatum family2;
//  rc = baljsn::DatumUtil::decode(&family2, formattedFamilyJSON);
//  if (0 != rc) {
//      // handle error
//  }
//  assert(family == family2);
//..
// Finally, we make sure that the structure of the resulting datum is as we
// expect.
//..
//  assert(family->isArray());
//  assert(5 == family->theArray().length());
//
//  const bdld::Datum &lisa = family->theArray()[3];
//
//  assert(lisa.isMap());
//  assert(2         == lisa.theMap().size());
//  assert("Lisa"    == lisa.theMap().find("firstName")->theString());
//  assert(8         == lisa.theMap().find("age")->theDouble());
//..
// Notice that the 'type' of "age" is 'double', since "age" was encoded as a
// number, and 'double' is the supported representation of a JSON number (see
// {'Supported Types'}).

#include <balscm_version.h>

#include <baljsn_datumdecoderoptions.h>
#include <baljsn_datumencoderoptions.h>

#include <bdld_datum.h>
#include <bdld_manageddatum.h>
#include <bdlsb_fixedmeminstreambuf.h>

#include <bsls_libraryfeatures.h>

#include <bsl_iosfwd.h>
#include <bsl_streambuf.h>
#include <bsl_string.h>
#include <bsl_string_view.h>

#if defined(BSLS_LIBRARYFEATURES_HAS_CPP17_PMR)
# include <memory_resource>
#endif
#include <string>

namespace BloombergLP {
namespace baljsn {

class SimpleFormatter;

                              // ================
                              // struct DatumUtil
                              // ================

struct DatumUtil {
    // This 'struct' provides a namespace for a suite of functions that convert
    // between a JSON formated string and a 'bdld::Datum'.

    // CLASS METHODS
    static int decode(bdld::ManagedDatum         *result,
                      const bsl::string_view&     json);
    static int decode(bdld::ManagedDatum         *result,
                      const bsl::string_view&     json,
                      const DatumDecoderOptions&  options);
    static int decode(bdld::ManagedDatum         *result,
                      bsl::ostream               *errorStream,
                      const bsl::string_view&     json);
    static int decode(bdld::ManagedDatum         *result,
                      bsl::ostream               *errorStream,
                      const bsl::string_view&     json,
                      const DatumDecoderOptions&  options);
        // Decode the specified 'json' into the specified 'result'.  If the
        // optionally specified 'errorStream' is non-null, a description of any
        // errors that occur during parsing will be output to this stream.  If
        // the optionally specified 'options' argument is not present, treat it
        // as a default-constructed 'DatumDecoderOptions'.  Return 0 on
        // success, and a negative value if 'json' could not be decoded (either
        // because it is ill-formed, or if a constraint imposed by 'option' is
        // violated).  An error status will be returned if 'json' contains
        // arrays or objects that are nested beyond a depth configured by
        // 'options.maxNestedDepth()'.  The mapping of types in JSON to the
        // types supported by 'Datum' is described in {Supported Types}.

    static int decode(bdld::ManagedDatum         *result,
                      bsl::streambuf             *jsonBuffer);
    static int decode(bdld::ManagedDatum         *result,
                      bsl::streambuf             *jsonBuffer,
                      const DatumDecoderOptions&  options);
    static int decode(bdld::ManagedDatum         *result,
                      bsl::ostream               *errorStream,
                      bsl::streambuf             *jsonBuffer);
    static int decode(bdld::ManagedDatum         *result,
                      bsl::ostream               *errorStream,
                      bsl::streambuf             *jsonBuffer,
                      const DatumDecoderOptions&  options);
        // Decode the JSON string provided by the specified 'jsonBuffer' into
        // the specified 'result'.  If the optionally specified 'errorStream'
        // is non-null, a description of any errors that occur during parsing
        // will be output to this stream.  If the optionally specified
        // 'options' argument is not present, treat it as a default-constructed
        // 'DatumDecoderOptions'.  Return 0 on success, and a negative value if
        // 'json' could not be decoded (either because it is ill-formed, or if
        // a constraint imposed by 'option' is violated).  An error status will
        // be returned if 'json' contains arrays or objects that are nested
        // beyond a depth configured by 'options.maxNestedDepth()'.  The
        // mapping of types in JSON to the types supported by 'Datum' is
        // described in {Supported Types}.

    static int encode(bsl::string         *result,
                      const bdld::Datum&   datum);
    static int encode(std::string         *result,
                      const bdld::Datum&   datum);
#if defined(BSLS_LIBRARYFEATURES_HAS_CPP17_PMR)
    static int encode(std::pmr::string   *result,
                      const bdld::Datum&  datum);
#endif
    static int encode(bsl::string                *result,
                      const bdld::Datum&          datum,
                      const DatumEncoderOptions&  options);
    static int encode(std::string                *result,
                      const bdld::Datum&          datum,
                      const DatumEncoderOptions&  options);
#if defined(BSLS_LIBRARYFEATURES_HAS_CPP17_PMR)
    static int encode(std::pmr::string           *result,
                      const bdld::Datum&          datum,
                      const DatumEncoderOptions&  options);
#endif
        // Encode the specified 'datum' as a JSON string, and load the
        // specified 'result' with the encoded JSON string.  Return 0 on
        // success, and a negative value if 'datum' could not be encoded (with
        // no effect on 'result').  If the optionally specified 'options'
        // argument is not present, treat it as a default-constructed
        // 'DatumEncoderOptions'.  If 'options.strictTypes' is 'true' and a
        // type that is not supported by JSON, or a singular double value
        // (e.g., NaN or infinity) is being encoded (see {Supported Types})
        // return a positive value, but also populate 'result' with an encoded
        // JSON string (i.e., the value of 'result' is the same regardless of
        // the 'strictTypes' option, but if 'strictTypes' is 'true' a non-zero
        // positive status will be returned).  The mapping of types supported
        // by 'Datum' to JSON types is described in {Supported Types}.

    static int encode(bsl::ostream&              stream,
                      const bdld::Datum&         datum);
    static int encode(bsl::ostream&              stream,
                      const bdld::Datum&         datum,
                      const DatumEncoderOptions& options);
        // Encode the specified 'datum' as a JSON string, and write it into the
        // specified 'stream'.  Return 0 on success, and a negative value if
        // 'datum' could not be encoded (which may leave a partial JSON
        // sequence on the 'stream').  If the optionally specified 'options'
        // argument is not present, treat it as a default-constructed
        // 'DatumEncoderOptions'.  If 'options.strictTypes' is 'true' and a
        // type that is not supported by JSON, or a singular double value
        // (e.g., NaN or infinity) is being encoded (see {Supported Types})
        // return a positive value, but also populate 'result' with an encoded
        // JSON string (i.e., the value of 'result' is the same regardless of
        // the 'strictTypes' option, but if 'strictTypes' is 'true' a non-zero
        // positive status will be returned).  The mapping of types supported
        // by 'Datum' to JSON types is described in {Supported Types}.
};

// ============================================================================
//                            INLINE DEFINITIONS
// ============================================================================

// CLASS METHODS

inline
int DatumUtil::decode(bdld::ManagedDatum         *result,
                      const bsl::string_view&     json,
                      const DatumDecoderOptions&  options)
{
    bdlsb::FixedMemInStreamBuf buffer(json.data(), json.length());
    return decode(result, 0, &buffer, options);
}

inline
int DatumUtil::decode(bdld::ManagedDatum       *result,
                      const bsl::string_view&   json)
{
    return decode(result, json, DatumDecoderOptions());
}

inline
int DatumUtil::decode(bdld::ManagedDatum         *result,
                      bsl::ostream               *errorStream,
                      const bsl::string_view&     json,
                      const DatumDecoderOptions&  options)
{
    bdlsb::FixedMemInStreamBuf buffer(json.data(), json.length());
    return decode(result, errorStream, &buffer, options);
}

inline
int DatumUtil::decode(bdld::ManagedDatum         *result,
                      bsl::ostream               *errorStream,
                      const bsl::string_view&     json)
{
    return decode(result, errorStream, json, DatumDecoderOptions());
}

inline
int DatumUtil::decode(bdld::ManagedDatum         *result,
                      bsl::streambuf             *jsonBuffer,
                      const DatumDecoderOptions&  options)
{
    return decode(result, 0, jsonBuffer, options);
}

inline
int DatumUtil::decode(bdld::ManagedDatum         *result,
                      bsl::streambuf             *jsonBuffer)
{
    return decode(result, 0, jsonBuffer, DatumDecoderOptions());
}

inline
int DatumUtil::decode(bdld::ManagedDatum         *result,
                      bsl::ostream               *errorStream,
                      bsl::streambuf             *jsonBuffer)
{
    return decode(result, errorStream, jsonBuffer, DatumDecoderOptions());
}

inline
int DatumUtil::encode(bsl::string *result, const bdld::Datum& datum)
{
    return encode(result, datum, DatumEncoderOptions());
}

inline
int DatumUtil::encode(std::string *result, const bdld::Datum& datum)
{
    return encode(result, datum, DatumEncoderOptions());
}

#if defined(BSLS_LIBRARYFEATURES_HAS_CPP17_PMR)
inline
int DatumUtil::encode(std::pmr::string   *result,
                      const bdld::Datum&  datum)
{
    return encode(result, datum, DatumEncoderOptions());
}
#endif

inline
int DatumUtil::encode(bsl::ostream& stream, const bdld::Datum& datum)
{
    return encode(stream, datum, DatumEncoderOptions());
}


}  // close package namespace
}  // close enterprise namespace

#endif

// ----------------------------------------------------------------------------
// Copyright 2020 Bloomberg Finance L.P.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// ----------------------------- END-OF-FILE ----------------------------------