/// <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); }
//--- 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."); } } }