/// <summary>
        /// Deserialize LocalDate from readable int in ISO yyyymmdd format.
        ///
        /// Null value is handled via [BsonIgnoreIfNull] attribute and is not expected here.
        /// </summary>
        public override LocalDate Deserialize(BsonDeserializationContext context, BsonDeserializationArgs args)
        {
            // LocalDate is serialized as readable int in ISO yyyymmdd format
            int isoDate = context.Reader.ReadInt32();

            // Create LocalDate object by parsing readable int
            var result = LocalDateUtil.FromIsoInt(isoDate);

            return(result);
        }
Exemplo n.º 2
0
        //--- PRIVATE

        /// <summary>
        /// Populate key elements from an array of tokens starting
        /// at the specified token index. Elements that are themselves
        /// keys may use more than one token.
        ///
        /// This method returns the index of the first unused token.
        /// The returned value is the same as the length of the tokens
        /// array if all tokens are used.
        ///
        /// If key AKey has two elements, B and C, where
        ///
        /// * B has type BKey which has two string elements, and
        /// * C has type string,
        ///
        /// the semicolon delimited key has the following format:
        ///
        /// BToken1;BToken2;CToken
        ///
        /// To avoid serialization format uncertainty, key elements
        /// can have any atomic type except Double.
        /// </summary>
        private int PopulateFrom(string[] tokens, int tokenIndex)
        {
            // Get key elements using reflection
            var elementInfoArray = DataTypeInfo.GetOrCreate(this).DataElements;

            // If singleton is detected process it separately, then exit
            if (elementInfoArray.Length == 0)
            {
                // Check that string key is empty
                if (tokens.Length != 1 || tokens[0] != String.Empty)
                {
                    throw new Exception($"Type {GetType()} has key {string.Join(";", tokens)} while " +
                                        $"for a singleton the key must be an empty string (String.Empty). " +
                                        $"Singleton key is a key that has no key elements.");
                }

                // Return the length of empty key which consists of one (empty) token
                return(1);
            }

            // Check that there are enough remaining tokens in the key for each key element
            if (tokens.Length - tokenIndex < elementInfoArray.Length)
            {
                throw new Exception(
                          $"Key of type {GetType().Name} requires at least {elementInfoArray.Length} elements " +
                          $"{String.Join(";", elementInfoArray.Select(p => p.Name).ToArray())} while there are " +
                          $"only {tokens.Length - tokenIndex} remaining key tokens: {string.Join(";", tokens)}.");
            }

            // Iterate over element info elements, advancing tokenIndex by the required
            // number of tokens for each element. In case of embedded keys, the value of
            // tokenIndex is advanced by the recursive call to InitFromTokens method
            // of the embedded key.
            foreach (var elementInfo in elementInfoArray)
            {
                // Get element type
                Type elementType = elementInfo.PropertyType;

                // Convert string token to value depending on elementType
                if (elementType == typeof(string))
                {
                    CheckTokenNotEmpty(tokens, tokenIndex);

                    string token = tokens[tokenIndex++];
                    elementInfo.SetValue(this, token);
                }
                else if (elementType == typeof(double) || elementType == typeof(double?))
                {
                    throw new Exception(
                              $"Key element {elementInfo.Name} has type Double. Elements of this type " +
                              $"cannot be part of key due to serialization format uncertainty.");
                }
                else if (elementType == typeof(bool) || elementType == typeof(bool?))
                {
                    CheckTokenNotEmpty(tokens, tokenIndex);

                    string token      = tokens[tokenIndex++];
                    bool   tokenValue = bool.Parse(token);
                    elementInfo.SetValue(this, tokenValue);
                }
                else if (elementType == typeof(int) || elementType == typeof(int?))
                {
                    CheckTokenNotEmpty(tokens, tokenIndex);

                    string token      = tokens[tokenIndex++];
                    int    tokenValue = int.Parse(token);
                    elementInfo.SetValue(this, tokenValue);
                }
                else if (elementType == typeof(long) || elementType == typeof(long?))
                {
                    CheckTokenNotEmpty(tokens, tokenIndex);

                    string token      = tokens[tokenIndex++];
                    long   tokenValue = long.Parse(token);
                    elementInfo.SetValue(this, tokenValue);
                }
                else if (elementType == typeof(LocalDate) || elementType == typeof(LocalDate?))
                {
                    CheckTokenNotEmpty(tokens, tokenIndex);

                    // Inside the key, LocalDate is represented as readable int in
                    // non-delimited yyyymmdd format, not as delimited ISO string.
                    //
                    // First parse the string to int, then convert int to LocalDate.
                    string token = tokens[tokenIndex++];
                    if (!Int32.TryParse(token, out int isoInt))
                    {
                        throw new Exception(
                                  $"Element {elementInfo.Name} of key type {GetType().Name} has type LocalDate and value {token} " +
                                  $"that cannot be converted to readable int in non-delimited yyyymmdd format.");
                    }

                    LocalDate tokenValue = LocalDateUtil.FromIsoInt(isoInt);
                    elementInfo.SetValue(this, tokenValue);
                }
                else if (elementType == typeof(LocalTime) || elementType == typeof(LocalTime?))
                {
                    CheckTokenNotEmpty(tokens, tokenIndex);

                    // Inside the key, LocalTime is represented as readable int in
                    // non-delimited hhmmssfff format, not as delimited ISO string.
                    //
                    // First parse the string to int, then convert int to LocalTime.
                    string token = tokens[tokenIndex++];
                    if (!Int32.TryParse(token, out int isoInt))
                    {
                        throw new Exception(
                                  $"Element {elementInfo.Name} of key type {GetType().Name} has type LocalTime and value {token} " +
                                  $"that cannot be converted to readable int in non-delimited hhmmssfff format.");
                    }

                    LocalTime tokenValue = LocalTimeUtil.FromIsoInt(isoInt);
                    elementInfo.SetValue(this, tokenValue);
                }
                else if (elementType == typeof(LocalMinute) || elementType == typeof(LocalMinute?))
                {
                    CheckTokenNotEmpty(tokens, tokenIndex);

                    // Inside the key, LocalMinute is represented as readable int in
                    // non-delimited hhmm format, not as delimited ISO string.
                    //
                    // First parse the string to int, then convert int to LocalTime.
                    string token = tokens[tokenIndex++];
                    if (!Int32.TryParse(token, out int isoInt))
                    {
                        throw new Exception(
                                  $"Element {elementInfo.Name} of key type {GetType().Name} has type LocalMinute and value {token} " +
                                  $"that cannot be converted to readable int in non-delimited hhmm format.");
                    }

                    LocalMinute tokenValue = LocalMinuteUtil.FromIsoInt(isoInt);
                    elementInfo.SetValue(this, tokenValue);
                }
                else if (elementType == typeof(LocalDateTime) || elementType == typeof(LocalDateTime?))
                {
                    CheckTokenNotEmpty(tokens, tokenIndex);

                    // Inside the key, LocalDateTime is represented as readable long in
                    // non-delimited yyyymmddhhmmssfff format, not as delimited ISO string.
                    //
                    // First parse the string to long, then convert int to LocalDateTime.
                    string token = tokens[tokenIndex++];
                    if (!Int64.TryParse(token, out long isoLong))
                    {
                        throw new Exception(
                                  $"Element {elementInfo.Name} of key type {GetType().Name} has type LocalDateTime and value {token} " +
                                  $"that cannot be converted to readable long in non-delimited yyyymmddhhmmssfff format.");
                    }

                    LocalDateTime tokenValue = LocalDateTimeUtil.FromIsoLong(isoLong);
                    elementInfo.SetValue(this, tokenValue);
                }
                else if (elementType == typeof(Instant) || elementType == typeof(Instant?))
                {
                    CheckTokenNotEmpty(tokens, tokenIndex);

                    // Inside the key, Instant is represented as readable long in
                    // non-delimited yyyymmddhhmmssfff format, not as delimited ISO string.
                    //
                    // First parse the string to long, then convert int to Instant.
                    string token = tokens[tokenIndex++];
                    if (!Int64.TryParse(token, out long isoLong))
                    {
                        throw new Exception(
                                  $"Element {elementInfo.Name} of key type {GetType().Name} has type Instant and value {token} " +
                                  $"that cannot be converted to readable long in non-delimited yyyymmddhhmmssfff format.");
                    }

                    Instant tokenValue = InstantUtil.FromIsoLong(isoLong);
                    elementInfo.SetValue(this, tokenValue);
                }
                else if (elementType == typeof(TemporalId) || elementType == typeof(TemporalId?))
                {
                    CheckTokenNotEmpty(tokens, tokenIndex);

                    string     token      = tokens[tokenIndex++];
                    TemporalId tokenValue = TemporalId.Parse(token);
                    elementInfo.SetValue(this, tokenValue);
                }
                else if (elementType.BaseType == typeof(Enum)) // TODO Support nullable Enum in key
                {
                    CheckTokenNotEmpty(tokens, tokenIndex);

                    string token      = tokens[tokenIndex++];
                    object tokenValue = Enum.Parse(elementType, token);
                    elementInfo.SetValue(this, tokenValue);
                }
                else if (typeof(Key).IsAssignableFrom(elementType))
                {
                    Key keyElement = (Key)Activator.CreateInstance(elementType);
                    tokenIndex = keyElement.PopulateFrom(tokens, tokenIndex);
                    elementInfo.SetValue(this, keyElement);
                }
                else
                {
                    // Field type is unsupported for a key, error message
                    throw new Exception(
                              $"Element {elementInfo.Name} of key type {GetType().Name} has type {elementType} that " +
                              $"is not one of the supported key element types. Available key element types are " +
                              $"string, bool, int, long, LocalDate, LocalTime, LocalMinute, LocalDateTime, Instant, Enum, or Key.");
                }
            }

            return(tokenIndex);
        }
        /// <summary>
        /// Write atomic value. Value type
        /// will be inferred from object.GetType().
        /// </summary>
        public void WriteValue(object value)
        {
            // Check state transition matrix
            if (currentState_ == TreeWriterState.ValueStarted)
            {
                currentState_ = TreeWriterState.ValueWritten;
            }
            else if (currentState_ == TreeWriterState.ValueArrayItemStarted)
            {
                currentState_ = TreeWriterState.ValueArrayItemWritten;
            }
            else
            {
                throw new Exception(
                          $"A call to WriteEndValue(...) does not follow a matching WriteValue(...) at the same indent level.");
            }

            // Check that we are either inside dictionary or array
            Type elementType = null;

            if (currentArray_ != null)
            {
                elementType = currentArrayItemType_;
            }
            else if (currentDict_ != null)
            {
                elementType = currentElementInfo_.PropertyType;
            }
            else
            {
                throw new Exception($"Cannot WriteValue(...)for element {currentElementName_} " +
                                    $"is called outside dictionary or array.");
            }

            if (value.IsEmpty())
            {
                // Do not record null or empty value into dictionary, but add it to an array
                // Add to dictionary or array, depending on what we are inside of
                if (currentArray_ != null)
                {
                    currentArray_[currentArray_.Count - 1] = null;
                }
                return;
            }

            // Write based on element type
            Type valueType = value.GetType();

            if (elementType == typeof(string) ||
                elementType == typeof(double) || elementType == typeof(double?) ||
                elementType == typeof(bool) || elementType == typeof(bool?) ||
                elementType == typeof(int) || elementType == typeof(int?) ||
                elementType == typeof(long) || elementType == typeof(long?))
            {
                // Check type match
                if (!elementType.IsAssignableFrom(valueType))
                {
                    throw new Exception(
                              $"Attempting to deserialize value of type {valueType.Name} " +
                              $"into element of type {elementType.Name}.");
                }

                // Add to array or dictionary, depending on what we are inside of
                if (currentArray_ != null)
                {
                    currentArray_[currentArray_.Count - 1] = value;
                }
                else if (currentDict_ != null)
                {
                    currentElementInfo_.SetValue(currentDict_, value);
                }
                else
                {
                    throw new Exception($"Value can only be added to a dictionary or array.");
                }
            }
            else if (elementType == typeof(LocalDate) || elementType == typeof(LocalDate?))
            {
                // Check type match
                if (valueType != typeof(int))
                {
                    throw new Exception(
                              $"Attempting to deserialize value of type {valueType.Name} " +
                              $"into LocalDate; type should be int32.");
                }

                // Deserialize LocalDate as ISO int in yyyymmdd format
                LocalDate dateValue = LocalDateUtil.FromIsoInt((int)value);

                // Add to array or dictionary, depending on what we are inside of
                if (currentArray_ != null)
                {
                    currentArray_[currentArray_.Count - 1] = dateValue;
                }
                else if (currentDict_ != null)
                {
                    currentElementInfo_.SetValue(currentDict_, dateValue);
                }
                else
                {
                    throw new Exception($"Value can only be added to a dictionary or array.");
                }
            }
            else if (elementType == typeof(LocalTime) || elementType == typeof(LocalTime?))
            {
                // Check type match
                if (valueType != typeof(int))
                {
                    throw new Exception(
                              $"Attempting to deserialize value of type {valueType.Name} " +
                              $"into LocalTime; type should be int32.");
                }

                // Deserialize LocalTime as ISO int in hhmmssfff format
                LocalTime timeValue = LocalTimeUtil.FromIsoInt((int)value);

                // Add to array or dictionary, depending on what we are inside of
                if (currentArray_ != null)
                {
                    currentArray_[currentArray_.Count - 1] = timeValue;
                }
                else if (currentDict_ != null)
                {
                    currentElementInfo_.SetValue(currentDict_, timeValue);
                }
                else
                {
                    throw new Exception($"Value can only be added to a dictionary or array.");
                }
            }
            else if (elementType == typeof(LocalMinute) || elementType == typeof(LocalMinute?))
            {
                // Check type match
                if (valueType != typeof(int))
                {
                    throw new Exception(
                              $"Attempting to deserialize value of type {valueType.Name} " +
                              $"into LocalMinute; type should be int32.");
                }

                // Deserialize LocalTime as ISO int in hhmmssfff format
                LocalMinute minuteValue = LocalMinuteUtil.FromIsoInt((int)value);

                // Add to array or dictionary, depending on what we are inside of
                if (currentArray_ != null)
                {
                    currentArray_[currentArray_.Count - 1] = minuteValue;
                }
                else if (currentDict_ != null)
                {
                    currentElementInfo_.SetValue(currentDict_, minuteValue);
                }
                else
                {
                    throw new Exception($"Value can only be added to a dictionary or array.");
                }
            }
            else if (elementType == typeof(LocalDateTime) || elementType == typeof(LocalDateTime?))
            {
                // Check type match
                if (valueType != typeof(long))
                {
                    throw new Exception(
                              $"Attempting to deserialize value of type {valueType.Name} " +
                              $"into LocalDateTime; type should be int64.");
                }

                // Deserialize LocalDateTime as ISO long in yyyymmddhhmmssfff format
                LocalDateTime dateTimeValue = LocalDateTimeUtil.FromIsoLong((long)value);

                // Add to array or dictionary, depending on what we are inside of
                if (currentArray_ != null)
                {
                    currentArray_[currentArray_.Count - 1] = dateTimeValue;
                }
                else if (currentDict_ != null)
                {
                    currentElementInfo_.SetValue(currentDict_, dateTimeValue);
                }
                else
                {
                    throw new Exception($"Value can only be added to a dictionary or array.");
                }
            }
            else if (elementType == typeof(Instant) || elementType == typeof(Instant?))
            {
                // Check type match
                if (valueType != typeof(long))
                {
                    throw new Exception(
                              $"Attempting to deserialize value of type {valueType.Name} " +
                              $"into Instant; type should be int64.");
                }

                // Deserialize Instant as ISO long in yyyymmddhhmmssfff format
                Instant instantValue = InstantUtil.FromIsoLong((long)value);

                // Add to array or dictionary, depending on what we are inside of
                if (currentArray_ != null)
                {
                    currentArray_[currentArray_.Count - 1] = instantValue;
                }
                else if (currentDict_ != null)
                {
                    currentElementInfo_.SetValue(currentDict_, instantValue);
                }
                else
                {
                    throw new Exception($"Value can only be added to a dictionary or array.");
                }
            }
            else if (elementType.IsEnum)
            {
                // Check type match
                if (valueType != typeof(string))
                {
                    throw new Exception(
                              $"Attempting to deserialize value of type {valueType.Name} " +
                              $"into enum {elementType.Name}; type should be string.");
                }

                string stringValue = (string)value;

                // Deserialize enum as string
                string enumString = (string)value;
                object enumValue  = Enum.Parse(elementType, enumString);

                // Add to array or dictionary, depending on what we are inside of
                if (currentArray_ != null)
                {
                    currentArray_[currentArray_.Count - 1] = enumValue;
                }
                else if (currentDict_ != null)
                {
                    currentElementInfo_.SetValue(currentDict_, enumValue);
                }
                else
                {
                    throw new Exception($"Value can only be added to a dictionary or array.");
                }
            }
            else
            {
                // We run out of value types at this point, now we can create
                // a reference type and check that it implements Key
                object keyObj = (Key)Activator.CreateInstance(elementType);
                if (keyObj is Key)
                {
                    Key key = (Key)keyObj;

                    // Check type match
                    if (valueType != typeof(string) && valueType != elementType)
                    {
                        throw new Exception(
                                  $"Attempting to deserialize value of type {valueType.Name} " +
                                  $"into key type {elementType.Name}; keys should be serialized into semicolon delimited string.");
                    }

                    // Populate by parsing semicolon delimited string
                    string stringValue = value.AsString();
                    key.PopulateFrom(stringValue);

                    // Add to array or dictionary, depending on what we are inside of
                    if (currentArray_ != null)
                    {
                        currentArray_[currentArray_.Count - 1] = key;
                    }
                    else if (currentDict_ != null)
                    {
                        currentElementInfo_.SetValue(currentDict_, key);
                    }
                    else
                    {
                        throw new Exception($"Value can only be added to a dictionary or array.");
                    }
                }
                else
                {
                    // Argument type is unsupported, error message
                    throw new Exception($"Element type {value.GetType()} is not supported for serialization.");
                }
            }
        }