From 0154b9c7fcf0c26700779232c555a96e2de54b08 Mon Sep 17 00:00:00 2001 From: Congyong Su Date: Fri, 13 Nov 2015 15:55:58 +0800 Subject: [PATCH] OData error with target and details #76 Add writer and reader tests. Update public API baseline. --- .../Json/BufferingJsonReader.cs | 173 ++++++++++++++ .../Json/JsonConstants.cs | 10 + .../Json/ODataJsonWriterUtils.cs | 74 +++++- .../ODataJsonLightErrorDeserializer.cs | 211 +++++++++++++----- .../JsonLight/ODataJsonLightReaderUtils.cs | 6 + .../Microsoft.OData.Core.csproj | 1 + src/Microsoft.OData.Core/ODataError.cs | 11 + src/Microsoft.OData.Core/ODataErrorDetail.cs | 26 +++ .../Common/OData/ODataTestCaseBase.cs | 2 +- .../ObjectModelTests/ODataErrorTests.cs | 32 ++- .../PublicApi/PublicApi.bsl | 10 + .../Microsoft.Test.OData.TDD.Tests.csproj | 2 + .../Reader/BufferingJsonReaderTests.cs | 43 ++++ .../ODataJsonLightErrorDeserializerTests.cs | 79 +++++++ .../ODataJsonLightSerializerTests.cs | 14 ++ .../Writer/ODataJsonWriterUtilsTests.cs | 25 ++- 16 files changed, 651 insertions(+), 68 deletions(-) create mode 100644 src/Microsoft.OData.Core/ODataErrorDetail.cs create mode 100644 test/FunctionalTests/Tests/DataOData/Tests/OData.TDD.Tests/Reader/BufferingJsonReaderTests.cs create mode 100644 test/FunctionalTests/Tests/DataOData/Tests/OData.TDD.Tests/Reader/JsonLight/ODataJsonLightErrorDeserializerTests.cs diff --git a/src/Microsoft.OData.Core/Json/BufferingJsonReader.cs b/src/Microsoft.OData.Core/Json/BufferingJsonReader.cs index 8796f0eebd..d7c4957c6a 100644 --- a/src/Microsoft.OData.Core/Json/BufferingJsonReader.cs +++ b/src/Microsoft.OData.Core/Json/BufferingJsonReader.cs @@ -530,6 +530,43 @@ private bool TryReadInStreamErrorPropertyValue(out ODataError error) break; + case JsonConstants.ODataErrorTargetName: + if (!ODataJsonLightReaderUtils.ErrorPropertyNotFound( + ref propertiesFoundBitmask, + ODataJsonLightReaderUtils.ErrorPropertyBitMask.Target)) + { + return false; + } + + string errorTarget; + if (this.TryReadErrorStringPropertyValue(out errorTarget)) + { + error.Target = errorTarget; + } + else + { + return false; + } + + break; + + case JsonConstants.ODataErrorDetailsName: + if (!ODataJsonLightReaderUtils.ErrorPropertyNotFound( + ref propertiesFoundBitmask, + ODataJsonLightReaderUtils.ErrorPropertyBitMask.Details)) + { + return false; + } + + ICollection details; + if (!this.TryReadErrorDetailsPropertyValue(out details)) + { + return false; + } + + error.Details = details; + break; + case JsonConstants.ODataErrorInnerErrorName: if (!ODataJsonLightReaderUtils.ErrorPropertyNotFound(ref propertiesFoundBitmask, ODataJsonLightReaderUtils.ErrorPropertyBitMask.InnerError)) { @@ -561,6 +598,142 @@ private bool TryReadInStreamErrorPropertyValue(out ODataError error) return propertiesFoundBitmask != ODataJsonLightReaderUtils.ErrorPropertyBitMask.None; } + private bool TryReadErrorDetailsPropertyValue(out ICollection details) + { + Debug.Assert( + this.currentBufferedNode.NodeType == JsonNodeType.Property, + "this.currentBufferedNode.NodeType == JsonNodeType.Property"); + Debug.Assert(this.parsingInStreamError, "this.parsingInStreamError"); + this.AssertBuffering(); + + // move the reader onto the property value + this.ReadInternal(); + + // we expect a start-array node here + if (this.currentBufferedNode.NodeType != JsonNodeType.StartArray) + { + details = null; + return false; + } + + // [ + ReadInternal(); + + details = new List(); + ODataErrorDetail detail; + if (TryReadErrorDetail(out detail)) + { + details.Add(detail); + } + + // ] + ReadInternal(); + + return true; + } + + private bool TryReadErrorDetail(out ODataErrorDetail detail) + { + Debug.Assert( + this.currentBufferedNode.NodeType == JsonNodeType.StartObject, + "this.currentBufferedNode.NodeType == JsonNodeType.StartObject"); + Debug.Assert(this.parsingInStreamError, "this.parsingInStreamError"); + this.AssertBuffering(); + + if (this.currentBufferedNode.NodeType != JsonNodeType.StartObject) + { + detail = null; + return false; + } + + // { + ReadInternal(); + + detail = new ODataErrorDetail(); + // we expect one of the supported properties for the value (or end-object) + var propertiesFoundBitmask = ODataJsonLightReaderUtils.ErrorPropertyBitMask.None; + while (this.currentBufferedNode.NodeType == JsonNodeType.Property) + { + var propertyName = (string)this.currentBufferedNode.Value; + + switch (propertyName) + { + case JsonConstants.ODataErrorCodeName: + if (!ODataJsonLightReaderUtils.ErrorPropertyNotFound( + ref propertiesFoundBitmask, + ODataJsonLightReaderUtils.ErrorPropertyBitMask.Code)) + { + return false; + } + + string code; + if (this.TryReadErrorStringPropertyValue(out code)) + { + detail.ErrorCode = code; + } + else + { + return false; + } + + break; + + case JsonConstants.ODataErrorTargetName: + if (!ODataJsonLightReaderUtils.ErrorPropertyNotFound( + ref propertiesFoundBitmask, + ODataJsonLightReaderUtils.ErrorPropertyBitMask.Target)) + { + return false; + } + + string target; + if (this.TryReadErrorStringPropertyValue(out target)) + { + detail.Target = target; + } + else + { + return false; + } + + break; + + case JsonConstants.ODataErrorMessageName: + if (!ODataJsonLightReaderUtils.ErrorPropertyNotFound( + ref propertiesFoundBitmask, + ODataJsonLightReaderUtils.ErrorPropertyBitMask.MessageValue)) + { + return false; + } + + string message; + if (this.TryReadErrorStringPropertyValue(out message)) + { + detail.Message = message; + } + else + { + return false; + } + + break; + + default: + // if we find a non-supported property in an inner error, we skip it + this.SkipValueInternal(); + break; + } + + this.ReadInternal(); + } + + Debug.Assert( + this.currentBufferedNode.NodeType == JsonNodeType.EndObject, + "this.currentBufferedNode.NodeType == JsonNodeType.EndObject"); + + return true; + } + /// /// Try to read an inner error property value. /// diff --git a/src/Microsoft.OData.Core/Json/JsonConstants.cs b/src/Microsoft.OData.Core/Json/JsonConstants.cs index abac093305..79175aabc1 100644 --- a/src/Microsoft.OData.Core/Json/JsonConstants.cs +++ b/src/Microsoft.OData.Core/Json/JsonConstants.cs @@ -49,6 +49,16 @@ internal static class JsonConstants /// internal const string ODataErrorMessageName = "message"; + /// + /// "target" header for the error message property + /// + internal const string ODataErrorTargetName = "target"; + + /// + /// "details" header for the inner error property + /// + internal const string ODataErrorDetailsName = "details"; + /// /// "innererror" header for the inner error property /// diff --git a/src/Microsoft.OData.Core/Json/ODataJsonWriterUtils.cs b/src/Microsoft.OData.Core/Json/ODataJsonWriterUtils.cs index e40773091a..c869063352 100644 --- a/src/Microsoft.OData.Core/Json/ODataJsonWriterUtils.cs +++ b/src/Microsoft.OData.Core/Json/ODataJsonWriterUtils.cs @@ -28,7 +28,9 @@ internal static class ODataJsonWriterUtils /// A flag indicating whether error details should be written (in debug mode only) or not. /// The maximum number of nested inner errors to allow. /// true if we're writing JSON lite, false if we're writing verbose JSON. - internal static void WriteError(IJsonWriter jsonWriter, Action> writeInstanceAnnotationsDelegate, ODataError error, bool includeDebugInformation, int maxInnerErrorDepth, bool writingJsonLight) + internal static void WriteError(IJsonWriter jsonWriter, + Action> writeInstanceAnnotationsDelegate, ODataError error, + bool includeDebugInformation, int maxInnerErrorDepth, bool writingJsonLight) { Debug.Assert(jsonWriter != null, "jsonWriter != null"); Debug.Assert(error != null, "error != null"); @@ -38,7 +40,17 @@ internal static void WriteError(IJsonWriter jsonWriter, Action @@ -85,7 +97,12 @@ internal static void EndJsonPaddingIfRequired(IJsonWriter jsonWriter, ODataMessa /// Action to write the instance annotations. /// The maximum number of nested inner errors to allow. /// true if we're writing JSON lite, false if we're writing verbose JSON. - private static void WriteError(IJsonWriter jsonWriter, string code, string message, ODataInnerError innerError, IEnumerable instanceAnnotations, Action> writeInstanceAnnotationsDelegate, int maxInnerErrorDepth, bool writingJsonLight) + private static void WriteError(IJsonWriter jsonWriter, string code, string message, string target, + IEnumerable details, + ODataInnerError innerError, + IEnumerable instanceAnnotations, + Action> writeInstanceAnnotationsDelegate, int maxInnerErrorDepth, + bool writingJsonLight) { Debug.Assert(jsonWriter != null, "jsonWriter != null"); Debug.Assert(code != null, "code != null"); @@ -113,6 +130,18 @@ private static void WriteError(IJsonWriter jsonWriter, string code, string messa jsonWriter.WriteName(JsonConstants.ODataErrorMessageName); jsonWriter.WriteValue(message); + // For example, "target": "query", + if (target != null) + { + jsonWriter.WriteName(JsonConstants.ODataErrorTargetName); + jsonWriter.WriteValue(target); + } + + if (details != null) + { + WriteErrorDetails(jsonWriter, details, JsonConstants.ODataErrorDetailsName); + } + if (innerError != null) { WriteInnerError(jsonWriter, innerError, JsonConstants.ODataErrorInnerErrorName, /* recursionDepth */ 0, maxInnerErrorDepth); @@ -129,6 +158,45 @@ private static void WriteError(IJsonWriter jsonWriter, string code, string messa jsonWriter.EndObjectScope(); } + private static void WriteErrorDetails(IJsonWriter jsonWriter, IEnumerable details, + string odataErrorDetailsName) + { + Debug.Assert(jsonWriter != null, "jsonWriter != null"); + Debug.Assert(details != null, "details != null"); + Debug.Assert(odataErrorDetailsName != null, "odataErrorDetailsName != null"); + + // "details": [ + jsonWriter.WriteName(odataErrorDetailsName); + jsonWriter.StartArrayScope(); + + foreach (var detail in details.Where(d => d != null)) + { + // { + jsonWriter.StartObjectScope(); + + // "code": "301", + jsonWriter.WriteName(JsonConstants.ODataErrorCodeName); + jsonWriter.WriteValue(detail.ErrorCode ?? string.Empty); + + if (detail.Target != null) + { + // "target": "$search" + jsonWriter.WriteName(JsonConstants.ODataErrorTargetName); + jsonWriter.WriteValue(detail.Target); + } + + // "message": "$search query option not supported", + jsonWriter.WriteName(JsonConstants.ODataErrorMessageName); + jsonWriter.WriteValue(detail.Message ?? string.Empty); + + // } + jsonWriter.EndObjectScope(); + } + + // ] + jsonWriter.EndArrayScope(); + } + /// /// Write an inner error property and message. /// diff --git a/src/Microsoft.OData.Core/JsonLight/ODataJsonLightErrorDeserializer.cs b/src/Microsoft.OData.Core/JsonLight/ODataJsonLightErrorDeserializer.cs index 2fdd0cea06..b891d94da6 100644 --- a/src/Microsoft.OData.Core/JsonLight/ODataJsonLightErrorDeserializer.cs +++ b/src/Microsoft.OData.Core/JsonLight/ODataJsonLightErrorDeserializer.cs @@ -8,6 +8,7 @@ namespace Microsoft.OData.Core.JsonLight { #region Namespaces using System; + using System.Collections.Generic; using System.Diagnostics; #if ODATALIB_ASYNC using System.Threading.Tasks; @@ -39,7 +40,9 @@ internal ODataJsonLightErrorDeserializer(ODataJsonLightInputContext jsonLightInp /// internal ODataError ReadTopLevelError() { - Debug.Assert(this.JsonReader.NodeType == JsonNodeType.None, "Pre-Condition: expected JsonNodeType.None, the reader must not have been used yet."); + Debug.Assert( + this.JsonReader.NodeType == JsonNodeType.None, + "Pre-Condition: expected JsonNodeType.None, the reader must not have been used yet."); Debug.Assert(!this.JsonReader.DisableInStreamErrorDetection, "!JsonReader.DisableInStreamErrorDetection"); this.JsonReader.AssertNotBuffering(); @@ -61,7 +64,9 @@ internal ODataError ReadTopLevelError() ODataError result = this.ReadTopLevelErrorImplementation(); - Debug.Assert(this.JsonReader.NodeType == JsonNodeType.EndOfInput, "Post-Condition: JsonNodeType.EndOfInput"); + Debug.Assert( + this.JsonReader.NodeType == JsonNodeType.EndOfInput, + "Post-Condition: JsonNodeType.EndOfInput"); this.JsonReader.AssertNotBuffering(); return result; @@ -84,7 +89,9 @@ internal ODataError ReadTopLevelError() /// internal Task ReadTopLevelErrorAsync() { - Debug.Assert(this.JsonReader.NodeType == JsonNodeType.None, "Pre-Condition: expected JsonNodeType.None, the reader must not have been used yet."); + Debug.Assert( + this.JsonReader.NodeType == JsonNodeType.None, + "Pre-Condition: expected JsonNodeType.None, the reader must not have been used yet."); Debug.Assert(!this.JsonReader.DisableInStreamErrorDetection, "!JsonReader.DisableInStreamErrorDetection"); this.JsonReader.AssertNotBuffering(); @@ -100,22 +107,22 @@ internal Task ReadTopLevelErrorAsync() ODataPayloadKind.Error, duplicatePropertyNamesChecker, /*isReadingNestedPayload*/false, - /*allowEmptyPayload*/false) - - .FollowOnSuccessWith(t => - { - ODataError result = this.ReadTopLevelErrorImplementation(); - - Debug.Assert(this.JsonReader.NodeType == JsonNodeType.EndOfInput, "Post-Condition: JsonNodeType.EndOfInput"); - this.JsonReader.AssertNotBuffering(); - - return result; - }) - - .FollowAlwaysWith(t => - { - this.JsonReader.DisableInStreamErrorDetection = false; - }); + /*allowEmptyPayload*/false).FollowOnSuccessWith( + t => + { + ODataError result = this.ReadTopLevelErrorImplementation(); + + Debug.Assert( + this.JsonReader.NodeType == JsonNodeType.EndOfInput, + "Post-Condition: JsonNodeType.EndOfInput"); + this.JsonReader.AssertNotBuffering(); + + return result; + }).FollowAlwaysWith( + t => + { + this.JsonReader.DisableInStreamErrorDetection = false; + }); } #endif @@ -139,12 +146,15 @@ private ODataError ReadTopLevelErrorImplementation() if (string.CompareOrdinal(JsonLightConstants.ODataErrorPropertyName, propertyName) != 0) { // we only allow a single 'error' property for a top-level error object - throw new ODataException(Strings.ODataJsonErrorDeserializer_TopLevelErrorWithInvalidProperty(propertyName)); + throw new ODataException( + Strings.ODataJsonErrorDeserializer_TopLevelErrorWithInvalidProperty(propertyName)); } if (error != null) { - throw new ODataException(OData.Core.Strings.ODataJsonReaderUtils_MultipleErrorPropertiesWithSameName(JsonLightConstants.ODataErrorPropertyName)); + throw new ODataException( + OData.Core.Strings.ODataJsonReaderUtils_MultipleErrorPropertiesWithSameName( + JsonLightConstants.ODataErrorPropertyName)); } error = new ODataError(); @@ -191,26 +201,35 @@ private void ReadJsonObjectInErrorPayload(Action - { - switch (propertyParsingResult) { - case PropertyParsingResult.ODataInstanceAnnotation: - throw new ODataException(Strings.ODataJsonLightErrorDeserializer_InstanceAnnotationNotAllowedInErrorPayload(propertyName)); - case PropertyParsingResult.CustomInstanceAnnotation: - readPropertyWithValue(propertyName, duplicatePropertyNamesChecker); - break; - case PropertyParsingResult.PropertyWithoutValue: - throw new ODataException(Strings.ODataJsonLightErrorDeserializer_PropertyAnnotationWithoutPropertyForError(propertyName)); - case PropertyParsingResult.PropertyWithValue: - readPropertyWithValue(propertyName, duplicatePropertyNamesChecker); - break; - case PropertyParsingResult.EndOfObject: - break; - - case PropertyParsingResult.MetadataReferenceProperty: - throw new ODataException(Strings.ODataJsonLightPropertyAndValueDeserializer_UnexpectedMetadataReferenceProperty(propertyName)); - } - }); + switch (propertyParsingResult) + { + case PropertyParsingResult.ODataInstanceAnnotation: + throw new ODataException( + Strings + .ODataJsonLightErrorDeserializer_InstanceAnnotationNotAllowedInErrorPayload( + propertyName)); + case PropertyParsingResult.CustomInstanceAnnotation: + readPropertyWithValue(propertyName, duplicatePropertyNamesChecker); + break; + case PropertyParsingResult.PropertyWithoutValue: + throw new ODataException( + Strings + .ODataJsonLightErrorDeserializer_PropertyAnnotationWithoutPropertyForError( + propertyName)); + case PropertyParsingResult.PropertyWithValue: + readPropertyWithValue(propertyName, duplicatePropertyNamesChecker); + break; + case PropertyParsingResult.EndOfObject: + break; + + case PropertyParsingResult.MetadataReferenceProperty: + throw new ODataException( + Strings + .ODataJsonLightPropertyAndValueDeserializer_UnexpectedMetadataReferenceProperty + (propertyName)); + } + }); } this.JsonReader.ReadEndObject(); @@ -235,23 +254,30 @@ private object ReadErrorPropertyAnnotationValue(string propertyAnnotationName) { Debug.Assert(!string.IsNullOrEmpty(propertyAnnotationName), "!string.IsNullOrEmpty(propertyAnnotationName)"); Debug.Assert( - propertyAnnotationName.StartsWith(JsonLightConstants.ODataAnnotationNamespacePrefix, StringComparison.Ordinal), + propertyAnnotationName.StartsWith( + JsonLightConstants.ODataAnnotationNamespacePrefix, + StringComparison.Ordinal), "The method should only be called with OData. annotations"); this.AssertJsonCondition(JsonNodeType.PrimitiveValue, JsonNodeType.StartObject, JsonNodeType.StartArray); if (string.CompareOrdinal(propertyAnnotationName, ODataAnnotationNames.ODataType) == 0) { - string typeName = ReaderUtils.AddEdmPrefixOfTypeName(ReaderUtils.RemovePrefixOfTypeName(this.JsonReader.ReadStringValue())); + string typeName = + ReaderUtils.AddEdmPrefixOfTypeName( + ReaderUtils.RemovePrefixOfTypeName(this.JsonReader.ReadStringValue())); if (typeName == null) { - throw new ODataException(Strings.ODataJsonLightPropertyAndValueDeserializer_InvalidTypeName(propertyAnnotationName)); + throw new ODataException( + Strings.ODataJsonLightPropertyAndValueDeserializer_InvalidTypeName(propertyAnnotationName)); } this.AssertJsonCondition(JsonNodeType.Property, JsonNodeType.EndObject); return typeName; } - throw new ODataException(Strings.ODataJsonLightErrorDeserializer_PropertyAnnotationNotAllowedInErrorPayload(propertyAnnotationName)); + throw new ODataException( + Strings.ODataJsonLightErrorDeserializer_PropertyAnnotationNotAllowedInErrorPayload( + propertyAnnotationName)); } /// @@ -265,7 +291,9 @@ private object ReadErrorPropertyAnnotationValue(string propertyAnnotationName) /// private void ReadODataErrorObject(ODataError error) { - this.ReadJsonObjectInErrorPayload((propertyName, duplicationPropertyNameChecker) => this.ReadPropertyValueInODataErrorObject(error, propertyName, duplicationPropertyNameChecker)); + this.ReadJsonObjectInErrorPayload( + (propertyName, duplicationPropertyNameChecker) => + this.ReadPropertyValueInODataErrorObject(error, propertyName, duplicationPropertyNameChecker)); } /// @@ -283,11 +311,15 @@ private ODataInnerError ReadInnerError(int recursionDepth) Debug.Assert(this.JsonReader.DisableInStreamErrorDetection, "JsonReader.DisableInStreamErrorDetection"); this.JsonReader.AssertNotBuffering(); - ValidationUtils.IncreaseAndValidateRecursionDepth(ref recursionDepth, this.MessageReaderSettings.MessageQuotas.MaxNestingDepth); + ValidationUtils.IncreaseAndValidateRecursionDepth( + ref recursionDepth, + this.MessageReaderSettings.MessageQuotas.MaxNestingDepth); ODataInnerError innerError = new ODataInnerError(); - this.ReadJsonObjectInErrorPayload((propertyName, duplicatePropertyNamesChecker) => this.ReadPropertyValueInInnerError(recursionDepth, innerError, propertyName)); + this.ReadJsonObjectInErrorPayload( + (propertyName, duplicatePropertyNamesChecker) => + this.ReadPropertyValueInInnerError(recursionDepth, innerError, propertyName)); this.JsonReader.AssertNotBuffering(); return innerError; @@ -314,11 +346,13 @@ private void ReadPropertyValueInInnerError(int recursionDepth, ODataInnerError i break; case JsonConstants.ODataErrorInnerErrorTypeNameName: - innerError.TypeName = this.JsonReader.ReadStringValue(JsonConstants.ODataErrorInnerErrorTypeNameName); + innerError.TypeName = this.JsonReader.ReadStringValue( + JsonConstants.ODataErrorInnerErrorTypeNameName); break; case JsonConstants.ODataErrorInnerErrorStackTraceName: - innerError.StackTrace = this.JsonReader.ReadStringValue(JsonConstants.ODataErrorInnerErrorStackTraceName); + innerError.StackTrace = + this.JsonReader.ReadStringValue(JsonConstants.ODataErrorInnerErrorStackTraceName); break; case JsonConstants.ODataErrorInnerErrorInnerErrorName: @@ -345,7 +379,10 @@ private void ReadPropertyValueInInnerError(int recursionDepth, ODataInnerError i /// JsonNodeType.EndObject - The end of the "error" object. /// any - Anything else after the property value is an invalid payload (but won't fail in this method). /// - private void ReadPropertyValueInODataErrorObject(ODataError error, string propertyName, DuplicatePropertyNamesChecker duplicationPropertyNameChecker) + private void ReadPropertyValueInODataErrorObject( + ODataError error, + string propertyName, + DuplicatePropertyNamesChecker duplicationPropertyNameChecker) { switch (propertyName) { @@ -357,6 +394,14 @@ private void ReadPropertyValueInODataErrorObject(ODataError error, string proper error.Message = this.JsonReader.ReadStringValue(JsonConstants.ODataErrorMessageName); break; + case JsonConstants.ODataErrorTargetName: + error.Target = this.JsonReader.ReadStringValue(JsonConstants.ODataErrorTargetName); + break; + + case JsonConstants.ODataErrorDetailsName: + error.Details = this.ReadDetails(); + break; + case JsonConstants.ODataErrorInnerErrorName: error.InnerError = this.ReadInnerError(0 /* recursionDepth */); break; @@ -365,7 +410,8 @@ private void ReadPropertyValueInODataErrorObject(ODataError error, string proper // See if it's an instance annotation if (ODataJsonLightReaderUtils.IsAnnotationProperty(propertyName)) { - ODataJsonLightPropertyAndValueDeserializer valueDeserializer = new ODataJsonLightPropertyAndValueDeserializer(this.JsonLightInputContext); + ODataJsonLightPropertyAndValueDeserializer valueDeserializer = + new ODataJsonLightPropertyAndValueDeserializer(this.JsonLightInputContext); object typeName = null; var odataAnnotations = duplicationPropertyNameChecker.GetODataPropertyAnnotations(propertyName); @@ -378,22 +424,75 @@ private void ReadPropertyValueInODataErrorObject(ODataError error, string proper typeName as string, null /*expectedValueTypeReference*/, null /*duplicatePropertiesNamesChecker*/, - null /*collectionValidator*/, - false /*validateNullValue*/, + null /*collectionValidator*/, + false /*validateNullValue*/, false /*isTopLevelPropertyValue*/, false /*insideComplexValue*/, propertyName); - error.GetInstanceAnnotations().Add(new ODataInstanceAnnotation(propertyName, value.ToODataValue())); + error.GetInstanceAnnotations().Add( + new ODataInstanceAnnotation(propertyName, value.ToODataValue())); } else { // we only allow a 'code', 'message' and 'innererror' properties in the value of the 'error' property or custom instance annotations - throw new ODataException(Strings.ODataJsonLightErrorDeserializer_TopLevelErrorValueWithInvalidProperty(propertyName)); + throw new ODataException( + Strings.ODataJsonLightErrorDeserializer_TopLevelErrorValueWithInvalidProperty(propertyName)); } break; } } + + private ICollection ReadDetails() + { + var details = new List(); + // [ + this.JsonReader.ReadStartArray(); + + while (JsonReader.NodeType == JsonNodeType.StartObject) + { + var detail = ReadDetail(); + details.Add(detail); + } + + // ] + this.JsonReader.ReadEndArray(); + + return details; + } + + private ODataErrorDetail ReadDetail() + { + var detail = new ODataErrorDetail(); + + ReadJsonObjectInErrorPayload( + (propertyName, duplicationPropertyNameChecker) => + ReadPropertyValueInODataErrorDetailObject(detail, propertyName)); + + return detail; + } + + private void ReadPropertyValueInODataErrorDetailObject(ODataErrorDetail detail, string propertyName) + { + switch (propertyName) + { + case JsonConstants.ODataErrorCodeName: + detail.ErrorCode = this.JsonReader.ReadStringValue(JsonConstants.ODataErrorCodeName); + break; + + case JsonConstants.ODataErrorMessageName: + detail.Message = this.JsonReader.ReadStringValue(JsonConstants.ODataErrorMessageName); + break; + + case JsonConstants.ODataErrorTargetName: + detail.Target = this.JsonReader.ReadStringValue(JsonConstants.ODataErrorTargetName); + break; + + default: + this.JsonReader.SkipValue(); + break; + } + } } -} +} \ No newline at end of file diff --git a/src/Microsoft.OData.Core/JsonLight/ODataJsonLightReaderUtils.cs b/src/Microsoft.OData.Core/JsonLight/ODataJsonLightReaderUtils.cs index f306cf946e..e22c6b83e0 100644 --- a/src/Microsoft.OData.Core/JsonLight/ODataJsonLightReaderUtils.cs +++ b/src/Microsoft.OData.Core/JsonLight/ODataJsonLightReaderUtils.cs @@ -57,6 +57,12 @@ internal enum ErrorPropertyBitMask /// The "stacktrace" property of an inner error object. StackTrace = 128, + + /// The "target" property. + Target = 256, + + /// The "details" property. + Details = 512 } /// diff --git a/src/Microsoft.OData.Core/Microsoft.OData.Core.csproj b/src/Microsoft.OData.Core/Microsoft.OData.Core.csproj index ac366a07a2..f5b2775acf 100644 --- a/src/Microsoft.OData.Core/Microsoft.OData.Core.csproj +++ b/src/Microsoft.OData.Core/Microsoft.OData.Core.csproj @@ -218,6 +218,7 @@ + diff --git a/src/Microsoft.OData.Core/ODataError.cs b/src/Microsoft.OData.Core/ODataError.cs index 2e96c68596..e581b132e7 100644 --- a/src/Microsoft.OData.Core/ODataError.cs +++ b/src/Microsoft.OData.Core/ODataError.cs @@ -39,6 +39,17 @@ public string Message set; } + /// Gets or sets the target of the particular error. + /// For example, the name of the property in error + public string Target { get; set; } + + /// + /// An array of JSON objects that MUST contain name/value pairs for code and message, and MAY contain + /// a name/value pair for target, as described above. + /// + /// The error details. + public ICollection Details { get; set; } + /// Gets or sets the implementation specific debugging information to help determine the cause of the error. /// The implementation specific debugging information. public ODataInnerError InnerError diff --git a/src/Microsoft.OData.Core/ODataErrorDetail.cs b/src/Microsoft.OData.Core/ODataErrorDetail.cs new file mode 100644 index 0000000000..7f0e332234 --- /dev/null +++ b/src/Microsoft.OData.Core/ODataErrorDetail.cs @@ -0,0 +1,26 @@ +//--------------------------------------------------------------------- +// +// Copyright (C) Microsoft Corporation. All rights reserved. See License.txt in the project root for license information. +// +//--------------------------------------------------------------------- + +namespace Microsoft.OData.Core +{ + /// + /// Class representing an error detail. + /// + public sealed class ODataErrorDetail + { + /// Gets or sets the error code to be used in payloads. + /// The error code to be used in payloads. + public string ErrorCode { get; set; } + + /// Gets or sets the error message. + /// The error message. + public string Message { get; set; } + + /// Gets or sets the target of the particular error. + /// For example, the name of the property in error + public string Target { get; set; } + } +} diff --git a/test/FunctionalTests/Tests/DataOData/Common/OData/ODataTestCaseBase.cs b/test/FunctionalTests/Tests/DataOData/Common/OData/ODataTestCaseBase.cs index 049fe473c1..6ed707242a 100644 --- a/test/FunctionalTests/Tests/DataOData/Common/OData/ODataTestCaseBase.cs +++ b/test/FunctionalTests/Tests/DataOData/Common/OData/ODataTestCaseBase.cs @@ -20,7 +20,7 @@ namespace Microsoft.Test.Taupo.OData using Microsoft.VisualStudio.TestTools.UnitTesting; /// - /// Base classs for ODataLib Tests + /// Base class for ODataLib Tests /// [TestClass] public class ODataTestCaseBase : TestCase diff --git a/test/FunctionalTests/Tests/DataOData/Tests/OData.Common.Tests/ObjectModelTests/ODataErrorTests.cs b/test/FunctionalTests/Tests/DataOData/Tests/OData.Common.Tests/ObjectModelTests/ODataErrorTests.cs index 733952b2b4..9b79f8afe0 100644 --- a/test/FunctionalTests/Tests/DataOData/Tests/OData.Common.Tests/ObjectModelTests/ODataErrorTests.cs +++ b/test/FunctionalTests/Tests/DataOData/Tests/OData.Common.Tests/ObjectModelTests/ODataErrorTests.cs @@ -7,6 +7,7 @@ namespace Microsoft.Test.Taupo.OData.Common.Tests.ObjectModelTests { #region Namespaces + using System.Collections.Generic; using Microsoft.OData.Core; using Microsoft.Test.Taupo.Execution; using Microsoft.VisualStudio.TestTools.UnitTesting; @@ -25,6 +26,8 @@ public void DefaultValuesTest() ODataError error = new ODataError(); this.Assert.IsNull(error.ErrorCode, "Expected null default value for property 'ErrorCode'."); this.Assert.IsNull(error.Message, "Expected null default value for property 'Message'."); + this.Assert.IsNull(error.Target, "Expected null default value for property 'Target'."); + this.Assert.IsNull(error.Details, "Expected null default value for property 'Details'."); this.Assert.IsNull(error.InnerError, "Expected null default value for property 'InnerError'."); } @@ -33,17 +36,26 @@ public void PropertyGettersAndSettersTest() { string errorCode = "500"; string message = "Fehler! Bitte kontaktieren Sie den Administrator!"; + var target = "any target"; + var details = new List + { + new ODataErrorDetail { ErrorCode = "401", Message = "any msg", Target = "another target" } + }; ODataInnerError innerError = new ODataInnerError { Message = "No inner error" }; ODataError error = new ODataError() { ErrorCode = errorCode, Message = message, + Target = target, + Details = details, InnerError = innerError }; this.Assert.AreEqual(errorCode, error.ErrorCode, "Expected equal error code values."); this.Assert.AreEqual(message, error.Message, "Expected equal message values."); + this.Assert.AreEqual(target, error.Target, "Expected equal target values."); + this.Assert.AreSame(details, error.Details, "Expected equal error detail values."); this.Assert.AreSame(innerError, error.InnerError, "Expected equal inner error values."); } @@ -51,18 +63,28 @@ public void PropertyGettersAndSettersTest() public void PropertySettersNullTest() { ODataError error = new ODataError() - { - ErrorCode = "500", - Message = "Fehler! Bitte kontaktieren Sie den Administrator!", - InnerError = new ODataInnerError { Message = "No inner error" }, - }; + { + ErrorCode = "500", + Message = "Fehler! Bitte kontaktieren Sie den Administrator!", + Target = "any target", + Details = + new List + { + new ODataErrorDetail { ErrorCode = "401", Message = "any msg", Target = "another target" } + }, + InnerError = new ODataInnerError { Message = "No inner error" }, + }; error.ErrorCode = null; error.Message = null; + error.Target = null; + error.Details = null; error.InnerError = null; this.Assert.IsNull(error.ErrorCode, "Expected null default value for property 'ErrorCode'."); this.Assert.IsNull(error.Message, "Expected null default value for property 'Message'."); + this.Assert.IsNull(error.Target, "Expected null default value for property 'Target'."); + this.Assert.IsNull(error.Details, "Expected null default value for property 'Details'."); this.Assert.IsNull(error.InnerError, "Expected null default value for property 'InnerError'."); } } diff --git a/test/FunctionalTests/Tests/DataOData/Tests/OData.Common.Tests/PublicApi/PublicApi.bsl b/test/FunctionalTests/Tests/DataOData/Tests/OData.Common.Tests/PublicApi/PublicApi.bsl index b0ce1872bf..fe67325570 100644 --- a/test/FunctionalTests/Tests/DataOData/Tests/OData.Common.Tests/PublicApi/PublicApi.bsl +++ b/test/FunctionalTests/Tests/DataOData/Tests/OData.Common.Tests/PublicApi/PublicApi.bsl @@ -4526,10 +4526,20 @@ DebuggerDisplayAttribute(), public sealed class Microsoft.OData.Core.ODataError : Microsoft.OData.Core.ODataAnnotatable { public ODataError () + System.Collections.Generic.ICollection`1[[Microsoft.OData.Core.ODataErrorDetail]] Details { [CompilerGeneratedAttribute(),]public get; [CompilerGeneratedAttribute(),]public set; } string ErrorCode { [CompilerGeneratedAttribute(),]public get; [CompilerGeneratedAttribute(),]public set; } Microsoft.OData.Core.ODataInnerError InnerError { [CompilerGeneratedAttribute(),]public get; [CompilerGeneratedAttribute(),]public set; } System.Collections.Generic.ICollection`1[[Microsoft.OData.Core.ODataInstanceAnnotation]] InstanceAnnotations { public get; public set; } string Message { [CompilerGeneratedAttribute(),]public get; [CompilerGeneratedAttribute(),]public set; } + string Target { [CompilerGeneratedAttribute(),]public get; [CompilerGeneratedAttribute(),]public set; } +} + +public sealed class Microsoft.OData.Core.ODataErrorDetail { + public ODataErrorDetail () + + string ErrorCode { [CompilerGeneratedAttribute(),]public get; [CompilerGeneratedAttribute(),]public set; } + string Message { [CompilerGeneratedAttribute(),]public get; [CompilerGeneratedAttribute(),]public set; } + string Target { [CompilerGeneratedAttribute(),]public get; [CompilerGeneratedAttribute(),]public set; } } [ diff --git a/test/FunctionalTests/Tests/DataOData/Tests/OData.TDD.Tests/Microsoft.Test.OData.TDD.Tests.csproj b/test/FunctionalTests/Tests/DataOData/Tests/OData.TDD.Tests/Microsoft.Test.OData.TDD.Tests.csproj index 7651d7676a..d75d85fd99 100644 --- a/test/FunctionalTests/Tests/DataOData/Tests/OData.TDD.Tests/Microsoft.Test.OData.TDD.Tests.csproj +++ b/test/FunctionalTests/Tests/DataOData/Tests/OData.TDD.Tests/Microsoft.Test.OData.TDD.Tests.csproj @@ -76,7 +76,9 @@ + + diff --git a/test/FunctionalTests/Tests/DataOData/Tests/OData.TDD.Tests/Reader/BufferingJsonReaderTests.cs b/test/FunctionalTests/Tests/DataOData/Tests/OData.TDD.Tests/Reader/BufferingJsonReaderTests.cs new file mode 100644 index 0000000000..4c7bf4aac1 --- /dev/null +++ b/test/FunctionalTests/Tests/DataOData/Tests/OData.TDD.Tests/Reader/BufferingJsonReaderTests.cs @@ -0,0 +1,43 @@ +//--------------------------------------------------------------------- +// +// Copyright (C) Microsoft Corporation. All rights reserved. See License.txt in the project root for license information. +// +//--------------------------------------------------------------------- + +namespace Microsoft.Test.OData.TDD.Tests.Reader +{ + using System.IO; + using System.Linq; + using Microsoft.OData.Core; + using Microsoft.OData.Core.Json; + using Microsoft.VisualStudio.TestTools.UnitTesting; + + [TestClass] + public class BufferingJsonReaderTests + { + [TestMethod] + public void StartBufferingAndTryToReadInStreamErrorPropertyValue_Works() + { + // Arrange + const string payload = + @"{""code"":"""",""message"":"""",""target"":""any target""," + + @"""details"":[{""code"":""500"",""target"":""another target"",""message"":""any msg""}]}"; + var reader = new StringReader(payload); + var jsonReader = new BufferingJsonReader(reader, "any", 0, ODataFormat.Json, false); + ODataError error; + + // Act + jsonReader.Read(); + var result = jsonReader.StartBufferingAndTryToReadInStreamErrorPropertyValue(out error); + + // Assert + Assert.IsTrue(result); + Assert.AreEqual("any target", error.Target); + Assert.AreEqual(1, error.Details.Count); + var detail = error.Details.Single(); + Assert.AreEqual("500", detail.ErrorCode); + Assert.AreEqual("another target", detail.Target); + Assert.AreEqual("any msg", detail.Message); + } + } +} diff --git a/test/FunctionalTests/Tests/DataOData/Tests/OData.TDD.Tests/Reader/JsonLight/ODataJsonLightErrorDeserializerTests.cs b/test/FunctionalTests/Tests/DataOData/Tests/OData.TDD.Tests/Reader/JsonLight/ODataJsonLightErrorDeserializerTests.cs new file mode 100644 index 0000000000..4ded6d3e3c --- /dev/null +++ b/test/FunctionalTests/Tests/DataOData/Tests/OData.TDD.Tests/Reader/JsonLight/ODataJsonLightErrorDeserializerTests.cs @@ -0,0 +1,79 @@ +//--------------------------------------------------------------------- +// +// Copyright (C) Microsoft Corporation. All rights reserved. See License.txt in the project root for license information. +// +//--------------------------------------------------------------------- + +namespace Microsoft.Test.OData.TDD.Tests.Reader.JsonLight +{ + using System.IO; + using System.Linq; + using System.Text; + using Microsoft.OData.Core; + using Microsoft.OData.Core.JsonLight; + using Microsoft.OData.Edm.Library; + using Microsoft.Test.OData.TDD.Tests.Common.JsonLight; + using Microsoft.VisualStudio.TestTools.UnitTesting; + + [TestClass] + public class ODataJsonLightErrorDeserializerTests + { + [TestMethod] + public void ReadTopLevelError_Works() + { + // Arrange + const string payload = + @"{""error"":{""code"":"""",""message"":"""",""target"":""any target""," + + @"""details"":[{""code"":""500"",""target"":""another target"",""message"":""any msg""}]}}"; + var context = GetInputContext(payload); + var deserializer = new ODataJsonLightErrorDeserializer(context); + + // Act + var error = deserializer.ReadTopLevelError(); + + // Assert + Assert.AreEqual("any target", error.Target); + Assert.AreEqual(1, error.Details.Count); + var detail = error.Details.Single(); + Assert.AreEqual("500", detail.ErrorCode); + Assert.AreEqual("another target", detail.Target); + Assert.AreEqual("any msg", detail.Message); + } + + [TestMethod] + public void ReadTopLevelErrorAsync_Works() + { + // Arrange + const string payload = + @"{""error"":{""code"":"""",""message"":"""",""target"":""any target""," + + @"""details"":[{""code"":""500"",""target"":""another target"",""message"":""any msg""}]}}"; + var context = GetInputContext(payload); + var deserializer = new ODataJsonLightErrorDeserializer(context); + + // Act + var error = deserializer.ReadTopLevelErrorAsync().Result; + + // Assert + Assert.AreEqual("any target", error.Target); + Assert.AreEqual(1, error.Details.Count); + var detail = error.Details.Single(); + Assert.AreEqual("500", detail.ErrorCode); + Assert.AreEqual("another target", detail.Target); + Assert.AreEqual("any msg", detail.Message); + } + + private ODataJsonLightInputContext GetInputContext(string payload) + { + return new ODataJsonLightInputContext( + ODataFormat.Json, + new MemoryStream(Encoding.UTF8.GetBytes(payload)), + JsonLightUtils.JsonLightStreamingMediaType, + Encoding.UTF8, + new ODataMessageReaderSettings(), + /*readingResponse*/ true, + /*synchronous*/ true, + new EdmModel(), + /*urlResolver*/ null); + } + } +} diff --git a/test/FunctionalTests/Tests/DataOData/Tests/OData.TDD.Tests/Writer/JsonLight/ODataJsonLightSerializerTests.cs b/test/FunctionalTests/Tests/DataOData/Tests/OData.TDD.Tests/Writer/JsonLight/ODataJsonLightSerializerTests.cs index 8f34e02ee8..7b3105046f 100644 --- a/test/FunctionalTests/Tests/DataOData/Tests/OData.TDD.Tests/Writer/JsonLight/ODataJsonLightSerializerTests.cs +++ b/test/FunctionalTests/Tests/DataOData/Tests/OData.TDD.Tests/Writer/JsonLight/ODataJsonLightSerializerTests.cs @@ -80,6 +80,18 @@ public void WriteTopLevelErrorUsesProvidedMessage() result.Should().Contain("\"message\":\"error message text\""); } + [TestMethod] + public void WriteTopLevelErrorUsesProvidedTarget() + { + var result = SetupSerializerAndRunTest(null, serializer => + { + ODataError error = new ODataError { Target = "error target text" }; + serializer.WriteTopLevelError(error, includeDebugInformation: false); + }); + + result.Should().Contain("\"target\":\"error target text\""); + } + [TestMethod] public void WriteTopLevelErrorHasCorrectDefaults() { @@ -91,6 +103,8 @@ public void WriteTopLevelErrorHasCorrectDefaults() result.Should().Contain("\"code\":\"\""); result.Should().Contain("\"message\":\"\""); + result.Should().NotContain("\"target\""); + result.Should().NotContain("\"details\""); } [TestMethod] diff --git a/test/FunctionalTests/Tests/DataOData/Tests/OData.TDD.Tests/Writer/ODataJsonWriterUtilsTests.cs b/test/FunctionalTests/Tests/DataOData/Tests/OData.TDD.Tests/Writer/ODataJsonWriterUtilsTests.cs index 3dc03cd23f..f2057ee3ff 100644 --- a/test/FunctionalTests/Tests/DataOData/Tests/OData.TDD.Tests/Writer/ODataJsonWriterUtilsTests.cs +++ b/test/FunctionalTests/Tests/DataOData/Tests/OData.TDD.Tests/Writer/ODataJsonWriterUtilsTests.cs @@ -6,13 +6,10 @@ namespace Microsoft.Test.OData.TDD.Tests.Writer { - using System; using System.IO; - using System.Linq; using FluentAssertions; using Microsoft.OData.Core; using Microsoft.OData.Core.Json; - using Microsoft.OData.Core.JsonLight; using Microsoft.VisualStudio.TestTools.UnitTesting; /// @@ -75,5 +72,27 @@ public void StartAndEndJsonPaddingSuccessTest() ODataJsonWriterUtils.EndJsonPaddingIfRequired(this.jsonWriter, settings); stringWriter.GetStringBuilder().ToString().Should().Be("functionName()"); } + + [TestMethod] + public void WriteError_WritesTargetAndDetails() + { + var error = new ODataError + { + Target = "any target", + Details = + new[] { new ODataErrorDetail { ErrorCode = "500", Target = "any target", Message = "any msg" } } + }; + + ODataJsonWriterUtils.WriteError( + jsonWriter, + enumerable => { }, + error, + includeDebugInformation: false, + maxInnerErrorDepth: 0, + writingJsonLight: false); + var result = stringWriter.GetStringBuilder().ToString(); + result.Should().Be(@"{""error"":{""code"":"""",""message"":"""",""target"":""any target"","+ + @"""details"":[{""code"":""500"",""target"":""any target"",""message"":""any msg""}]}}"); + } } }