private static PropertyValidationOutcome ValidateSameDataType(ParameterDefinition expected, ParameterDefinition actual, IssueLogger issues, bool relaxStringValidation) { if (expected.Type == actual.Type && actual.Type != ParameterDataType.String) { return(PropertyValidationOutcome.Ok); } else if (expected.Type == actual.Type && actual.Type == ParameterDataType.String) { // Perform extra validation to see if the string is the right format (iso date, enum value, url, or just a string) if (relaxStringValidation) { return(PropertyValidationOutcome.Ok); } return(ValidateStringFormat(expected, actual, issues)); } else if (expected.Type == ParameterDataType.String && expected.Type.IsLessSpecificThan(actual.Type)) { return(PropertyValidationOutcome.Ok); } else if (actual.Type.IsLessSpecificThan(expected.Type)) { if (relaxStringValidation) { issues.Message( $"Expected type {expected.Type} but actual was {actual.Type}, which is less specific than the expected type. Property: {actual.Name}, actual value: '{actual.OriginalValue}'"); return(PropertyValidationOutcome.Ok); } issues.Error( ValidationErrorCode.ExpectedTypeDifferent, $"Expected type {expected.Type} but actual was {actual.Type}, which is less specific than the expected type. Property: {actual.Name}, actual value: '{actual.OriginalValue}'"); return(PropertyValidationOutcome.BadStringValue); } else { // Type of the inputProperty is mismatched from the expected value. issues.Error( ValidationErrorCode.ExpectedTypeDifferent, $"Expected type {expected.Type} but actual was {actual.Type}. Property: {actual.Name}, actual value: '{actual.OriginalValue}'"); return(PropertyValidationOutcome.InvalidType); } }
private static ResourceDefinition ResourceDefinitionFromType(Schema schema, IEnumerable <Schema> otherSchema, ComplexType ct, IssueLogger issues) { var annotation = new CodeBlockAnnotation() { ResourceType = string.Concat(schema.Namespace, ".", ct.Name), BlockType = CodeBlockType.Resource }; var json = BuildJsonExample(ct, otherSchema); ResourceDefinition rd = new JsonResourceDefinition(annotation, json, null, issues); return(rd); }
private void ValidateCollectionObject(JContainer obj, CodeBlockAnnotation annotation, Dictionary <string, JsonSchema> otherSchemas, string collectionPropertyName, IssueLogger issues, ValidationOptions options) { // TODO: also validate additional properties on the collection, like nextDataLink JToken collection = null; if (obj is JArray) { collection = obj; } else { try { collection = obj[collectionPropertyName]; } catch (Exception ex) { issues.Error(ValidationErrorCode.JsonErrorObjectExpected, $"Unable to find collection parameter or array to validate: {ex.Message}"); return; } } if (null == collection) { issues.Error(ValidationErrorCode.MissingCollectionProperty, $"Failed to locate collection property '{collectionPropertyName}' in response."); } else { if (!collection.Any()) { if (!annotation.IsEmpty) { issues.Warning( ValidationErrorCode.CollectionArrayEmpty, $"Property contained an empty array that was not validated: {collectionPropertyName}"); } } else if (annotation.IsEmpty) { issues.Warning( ValidationErrorCode.CollectionArrayNotEmpty, $"Property contained a non-empty array that was expected to be empty: {collectionPropertyName}"); } foreach (var jToken in collection) { var container = jToken as JContainer; if (null != container) { var deeperOptions = new ValidationOptions(options) { AllowTruncatedResponses = annotation.TruncatedResult }; this.ValidateContainerObject( container, deeperOptions, otherSchemas, issues.For("container", onlyKeepUniqueErrors: true)); } } } }
private static IEnumerable <ParameterDefinition> ParseParameterTable(IMarkdownTable table, ParameterLocation location, TableDecoder decoder, IssueLogger issues, bool navigationProperties = false) { var records = table.RowValues.Select(r => new ParameterDefinition { Name = r.ValueForColumn(table, decoder.ParseRule.ColumnNames["name"]), Type = r.ValueForColumn(table, decoder.ParseRule.ColumnNames["type"]).ParseParameterDataType(defaultValue: ParameterDataType.String), Description = r.ValueForColumn(table, decoder.ParseRule.ColumnNames["description"]), Required = r.ValueForColumn(table, decoder.ParseRule.ColumnNames["description"]).IsRequired(), Optional = r.ValueForColumn(table, decoder.ParseRule.ColumnNames["description"]).IsOptional(), Location = location, IsNavigatable = navigationProperties, }).ToList(); var badRows = records.Count(r => string.IsNullOrEmpty(r.Name)); if (badRows > 0) { var tableHeaders = $"|{ string.Join("|", table.ColumnHeaders)}|"; if (badRows == records.Count) { issues.Warning(ValidationErrorCode.MarkdownParserError, $"Failed to parse any rows out of table with headers: {tableHeaders}"); return(Enumerable.Empty <ParameterDefinition>()); } issues.Warning(ValidationErrorCode.ParameterParserError, $"Failed to parse {badRows} row(s) in table with headers: {tableHeaders}"); records = records.Where(r => !string.IsNullOrEmpty(r.Name)).ToList(); } return(records); }
/// <summary> /// Convert resources found in the CSDL schema objects into ResourceDefintion instances /// that can be tested against the documentation. /// </summary> /// <param name="schemas"></param> /// <returns></returns> public static List <ResourceDefinition> GenerateResourcesFromSchemas(IEnumerable <Schema> schemas, IssueLogger issues) { List <ResourceDefinition> resources = new List <ResourceDefinition>(); foreach (var schema in schemas) { resources.AddRange(CreateResourcesFromSchema(schema, schemas, issues)); } return(resources); }
public IODataNavigable NavigateByUriComponent(string component, EntityFramework edmx, IssueLogger issues, bool isLastSegment) { throw new NotImplementedException(); }
/// <summary> /// Evaluate the ApiRequirements file and return any inconsistencies /// as ValidationErrors. /// </summary> public static ValidationResult <bool> IsRequestValid(this HttpRequest request, string sourceFile, ApiRequirements apiRequirements, IssueLogger issues) { if (null == apiRequirements || null == apiRequirements.HttpRequest) { return(new ValidationResult <bool>(true)); } var reqs = apiRequirements.HttpRequest; var requestMimeType = new MultipartMime.MimeContentType(request.ContentType); if (null != reqs.ContentTypes && null != requestMimeType.MimeType && !reqs.ContentTypes.Contains(requestMimeType.MimeType)) { issues.Warning(ValidationErrorCode.InvalidContentType, $"Request content-type header value is not in the supported list of content-types: {request.ContentType}"); } if (reqs.HttpMethods != null && !reqs.HttpMethods.Contains(request.Method)) { issues.Error(ValidationErrorCode.InvalidHttpMethod, $"Request HTTP method is not in the supported list of HTTP methods: {request.Method}"); } if (reqs.MaxUrlLength > 0 && request.Url.Length > reqs.MaxUrlLength) { issues.Error(ValidationErrorCode.UrlLengthExceedsMaximum, $"Request URL is longer than the defined maximum URL length: [{request.Url.Length}] {request.Url}"); } if (reqs.StandardHeaders != null && request.Headers.Count > 0) { foreach (var headerName in request.Headers.AllKeys) { if (!reqs.StandardHeaders.ContainsString(headerName, apiRequirements.CaseSensativeHeaders)) { issues.Warning(ValidationErrorCode.NonStandardHeaderUsed, $"Request includes a non-standard header: {headerName}"); } } } return(new ValidationResult <bool>(issues.IssuesInCurrentScope == 0)); }
public MarkdownValidator(string pathToDocs) { this.pathToDocs = pathToDocs; this.issues = new IssueLogger(); }
public void ClearExistingIssues() { issues = new IssueLogger(); }
/// <summary> /// Check each member of the actualProperty's array to make sure it matches the resource type specified for the property. /// </summary> /// <param name="actualProperty"></param> /// <param name="schemas"></param> /// <param name="detectedErrors"></param> /// <param name="options"></param> private PropertyValidationOutcome ValidateArrayProperty(ParameterDefinition actualProperty, Dictionary <string, JsonSchema> schemas, IssueLogger issues, ValidationOptions options) { JArray actualArray = null; try { actualArray = (JArray)JsonConvert.DeserializeObject(actualProperty.OriginalValue); } catch (InvalidCastException) { throw new InvalidCastException($"Property {actualProperty.Name} expected to be an array, but failed to cast value to an array: {actualProperty.OriginalValue}"); } var expectedPropertyDefinition = this.ExpectedProperties[actualProperty.Name]; JsonSchema memberSchema = null; if (actualProperty.Type.CollectionResourceType == SimpleDataType.Object && expectedPropertyDefinition.Type.CustomTypeName != null) { // We have an ambigious array, but we know what it's supposed to be so let's use that schemas.TryGetValue(expectedPropertyDefinition.Type.CustomTypeName, out memberSchema); } if (memberSchema == null && string.IsNullOrEmpty(actualProperty.Type.CustomTypeName)) { return(this.ValidateSimpleArrayProperty(actualProperty, this.ExpectedProperties[actualProperty.Name], issues)); } else if (memberSchema == null && !schemas.TryGetValue(actualProperty.Type.CustomTypeName, out memberSchema)) { issues.Error(ValidationErrorCode.ResourceTypeNotFound, $"Failed to locate resource definition for: {actualProperty.Type.CustomTypeName}"); return(PropertyValidationOutcome.MissingResourceType); } bool hadErrors = false; var memberIssues = issues.For("member", onlyKeepUniqueErrors: true); for (int i = 0; i < actualArray.Count; i++) { JContainer member = actualArray[i] as JContainer; if (member != null) { memberSchema.ValidateContainerObject(member, options, schemas, memberIssues); // TODO: Filter out non-unique errors hadErrors |= memberIssues.Issues.Count() > 0; } } return(hadErrors ? PropertyValidationOutcome.GenericError : PropertyValidationOutcome.Ok); }
private static ParameterDefinition ParseProperty(JToken token, JsonSchema containerSchema, IssueLogger issues = null) { ParameterDefinition propertyInfo = null; if (token.Type == JTokenType.Property) { JProperty tokenProperty = (JProperty)token; propertyInfo = ParseProperty(tokenProperty.Name, tokenProperty.Value, containerSchema); } else { if (issues != null) { issues.Warning(ValidationErrorCode.JsonParserException, "Unhandled token type: " + token.Type); } else { Console.WriteLine("Unhandled token type: " + token.Type); } } return(propertyInfo); }
private PropertyValidationOutcome ValidateSimpleArrayProperty(ParameterDefinition actualProperty, ParameterDefinition expectedProperty, IssueLogger issues) { if (!actualProperty.Type.IsCollection || !expectedProperty.Type.IsCollection) { throw new SchemaBuildException("Cannot use simple array valiation without array types", null); } if (actualProperty.Type == expectedProperty.Type) { return(PropertyValidationOutcome.Ok); } else if (expectedProperty.Type.IsCollection && actualProperty.Type.IsCollection && actualProperty.OriginalValue == "[]") // Check for the empty array case { return(PropertyValidationOutcome.Ok); } else { issues.Error(ValidationErrorCode.ArrayTypeMismatch, $"Array expected members to be of type {expectedProperty.Type} but found: {actualProperty.Type}"); return(PropertyValidationOutcome.InvalidType); } }
private static PropertyValidationOutcome ValidateStringFormat(ParameterDefinition schemaProperty, ParameterDefinition inputProperty, IssueLogger issues) { switch (schemaProperty.StringFormat()) { case ExpectedStringFormat.Iso8601Date: { DateTimeOffset?output = inputProperty.OriginalValue.TryParseIso8601Date(); if (!output.HasValue) { issues.Error(ValidationErrorCode.InvalidDateTimeString, $"Invalid ISO 8601 date-time string in property: {schemaProperty.Name}: {inputProperty.OriginalValue}"); return(PropertyValidationOutcome.BadStringValue); } return(PropertyValidationOutcome.Ok); } case ExpectedStringFormat.AbsoluteUrl: { try { new Uri(inputProperty.OriginalValue, UriKind.Absolute); return(PropertyValidationOutcome.Ok); } catch (FormatException) { issues.Error(ValidationErrorCode.InvalidUrlString, $"Invalid absolute URL value in property {schemaProperty.Name}: {inputProperty.OriginalValue}"); return(PropertyValidationOutcome.BadStringValue); } } case ExpectedStringFormat.EnumeratedValue: { if (!schemaProperty.IsValidEnumValue(inputProperty.OriginalValue)) { issues.Error(ValidationErrorCode.InvalidEnumeratedValueString, $"Invalid enumerated value in property {schemaProperty.Name}: {inputProperty.OriginalValue}"); return(PropertyValidationOutcome.BadStringValue); } return(PropertyValidationOutcome.Ok); } case ExpectedStringFormat.Generic: return(PropertyValidationOutcome.Ok); default: throw new NotImplementedException(); } }
/// <summary> /// Verify that a property from the json-to-validate matches something in our schema /// </summary> private PropertyValidationOutcome ValidateProperty(ParameterDefinition inputProperty, Dictionary <string, JsonSchema> schemas, IssueLogger issues, ValidationOptions options) { if (this.ExpectedProperties.ContainsKey(inputProperty.Name)) { // The property was expected to be found in this schema! Yay. var schemaPropertyDef = this.ExpectedProperties[inputProperty.Name]; // Check for simple value types first if (this.SimpleValueTypes(schemaPropertyDef.Type, inputProperty.Type) && this.AllFalse(schemaPropertyDef.Type.IsCollection, inputProperty.Type.IsCollection)) { return(ValidateSameDataType(schemaPropertyDef, inputProperty, issues, (null != options) ? options.RelaxedStringValidation : false)); } else if (null == inputProperty.OriginalValue) { if (null != this.NullableProperties && !this.NullableProperties.Contains(schemaPropertyDef.Name)) { issues.Warning(ValidationErrorCode.NullPropertyValue, $"Non-nullable property {schemaPropertyDef.Name} had a null value in the response. Expected {schemaPropertyDef.Type}."); } return(PropertyValidationOutcome.Ok); } else if (schemaPropertyDef.Type.IsCollection || inputProperty.Type.IsCollection) { // Check for an array if (schemaPropertyDef.Type.IsCollection && !inputProperty.Type.IsCollection) { // Expected an array, but didn't get one issues.Error(ValidationErrorCode.ExpectedArrayValue, $"Expected an array but property was not an array: {inputProperty.Name}"); return(PropertyValidationOutcome.InvalidType); } else if (!schemaPropertyDef.Type.IsCollection && inputProperty.Type.IsCollection) { issues.Error(ValidationErrorCode.ExpectedNonArrayValue, $"Expected a value of type {schemaPropertyDef.Type} but property was an array: {inputProperty.Name}"); return(PropertyValidationOutcome.InvalidType); } return(this.ValidateArrayProperty(inputProperty, schemas, issues, options)); } else if (schemaPropertyDef.Type.IsObject && inputProperty.Type.IsObject) { // Compare the ODataType schema to the custom schema if (null == schemaPropertyDef.Type.CustomTypeName || !schemas.ContainsKey(schemaPropertyDef.Type.CustomTypeName)) { issues.Error(ValidationErrorCode.ResourceTypeNotFound, $"Missing resource: resource {schemaPropertyDef.Type.CustomTypeName} was not found (property name '{inputProperty.Name}')."); return(PropertyValidationOutcome.MissingResourceType); } else if (inputProperty.Type.IsObject) { var odataSchema = schemas[schemaPropertyDef.Type.CustomTypeName]; if (null != inputProperty.Type.CustomMembers && !odataSchema.ValidateCustomObject(inputProperty.Type.CustomMembers.ToArray(), issues, schemas, options)) { if (issues.Errors.Any()) { issues.Error(ValidationErrorCode.ConsolidatedError, $"Schema validation failed on property '{inputProperty.Name}' ['{odataSchema.ResourceName}']"); } else { issues.Warning(ValidationErrorCode.ConsolidatedError, $"Schema validation failed on property '{inputProperty.Name}' ['{odataSchema.ResourceName}']"); } return(PropertyValidationOutcome.InvalidType); } else if (null == inputProperty.Type.CustomMembers) { issues.Error(ValidationErrorCode.NoCustomMembersFound, $"Property '{inputProperty.Name}' is of type Custom but has no custom members."); } return(PropertyValidationOutcome.Ok); } else { var odataSchema = schemas[schemaPropertyDef.Type.CustomTypeName]; if (inputProperty.Type.CustomMembers == null) { issues.Error(ValidationErrorCode.MissingCustomMembers, $"Property {inputProperty.Name} is missing custom members and cannot be validated."); return(PropertyValidationOutcome.InvalidType); } else { odataSchema.ValidateObjectProperties(inputProperty.Type.CustomMembers, options, schemas, issues); return(PropertyValidationOutcome.Ok); } } } else if (schemaPropertyDef.Type.IsObject) { issues.Warning(ValidationErrorCode.CustomValidationNotSupported, $"Schemas type was 'Custom' which is not supported. Add a resource type to the definition of property: {inputProperty.Name}"); return(PropertyValidationOutcome.MissingResourceType); } else { issues.Error(ValidationErrorCode.ExpectedTypeDifferent, $"Type mismatch: property '{inputProperty.Name}' [{inputProperty.Type}] doesn't match expected type [{schemaPropertyDef.Type}]."); return(PropertyValidationOutcome.InvalidType); } } else { // Check to see if this property is on the ignorable list string[] ignorableUndocumentedProperties = this.OriginalResource?.SourceFile.Parent.Requirements?.IgnorableProperties; string propertyName = inputProperty.Name; string annotationName = null; var indexOfAtSign = propertyName.IndexOf('@'); if (indexOfAtSign > 0) { // [email protected] is an example of what we're looking for here annotationName = propertyName.Substring(indexOfAtSign); propertyName = propertyName.Substring(0, indexOfAtSign); } if (null != annotationName) { // Check to see if propertyName is known or not. If it isn't known, fail. if (this.Properties.Any(x => x.Name.Equals(propertyName))) { // If the cleaned up propertyName is known, then check to see if the annotation is ignorable if (null != ignorableUndocumentedProperties && ignorableUndocumentedProperties.Contains(annotationName)) { // If we know of both the property and the annotation, we're good. return(PropertyValidationOutcome.Ok); } } } if (null != ignorableUndocumentedProperties && ignorableUndocumentedProperties.Contains(propertyName)) { return(PropertyValidationOutcome.Ok); } if (this.OriginalResource?.OriginalMetadata?.IsOpenType == true) { return(PropertyValidationOutcome.Ok); } // This property isn't documented issues.Warning(new UndocumentedPropertyWarning(null, inputProperty.Name, inputProperty.Type, ResourceName)); return(PropertyValidationOutcome.MissingFromSchema); } }
private static IEnumerable <ParameterDefinition> ParseParameterTable(IMarkdownTable table, ParameterLocation location, TableDecoder decoder, IssueLogger issues, bool navigationProperties = false) { // tables sometimes have column spans to delineate different sections of the table. for instance: // // | Name | Type | Description // |------|--------|-------------- // | one | int | first number // | two | int | second number // | **fancy numbers** // | pi | double | third number // // our markdown parser captures this as a regular row with all the columns, except with for all the blanks. // we try to infer such rows by looking for a **bold** first cell, followed by nbsp in all the other cells. // see below. var records = table.RowValues. Where(r => !r[0].StartsWith("**") || r.Skip(1).Any(c => c != " ")). // see comment above Select(r => new ParameterDefinition { Name = r.ValueForColumn(table, decoder.ParseRule.ColumnNames["name"]), Type = r.ValueForColumn(table, decoder.ParseRule.ColumnNames["type"]).ParseParameterDataType(defaultValue: ParameterDataType.String), Description = r.ValueForColumn(table, decoder.ParseRule.ColumnNames["description"]), Required = r.ValueForColumn(table, decoder.ParseRule.ColumnNames["description"]).IsRequired(), Optional = r.ValueForColumn(table, decoder.ParseRule.ColumnNames["description"]).IsOptional(), Location = location, IsNavigatable = navigationProperties, }).ToList(); var badRows = records.Count(r => string.IsNullOrEmpty(r.Name)); if (badRows > 0) { var tableHeaders = $"|{ string.Join("|", table.ColumnHeaders)}|"; if (badRows == records.Count) { issues.Warning(ValidationErrorCode.MarkdownParserError, $"Failed to parse any rows out of table with headers: {tableHeaders}\nTable was parsed as {decoder.ParseAs}, which requires a name column called one of the following: {{ {String.Join(",", decoder.ParseRule.ColumnNames["name"])} }}"); return(Enumerable.Empty <ParameterDefinition>()); } issues.Warning(ValidationErrorCode.ParameterParserError, $"Failed to parse {badRows} row(s) in table with headers: {tableHeaders}"); records = records.Where(r => !string.IsNullOrEmpty(r.Name)).ToList(); } return(records); }
/// <summary> /// Examines input json string to ensure that it compiles with the JsonSchema definition. Any errors in the /// validation of the schema are returned via the errors out parameter. /// </summary> /// <param name="schema">Schemas definition used as a reference.</param> /// <param name="inputJson">Input json example to be validated</param> /// <param name="issues"></param> /// <param name="expectedJson"></param> /// <returns></returns> public bool ValidateJsonCompilesWithSchema(JsonSchema schema, JsonExample inputJson, IssueLogger issues, JsonExample expectedJson = null, ValidationOptions options = null) { if (null == schema) { throw new ArgumentNullException("schema"); } if (null == inputJson) { throw new ArgumentNullException("inputJson"); } string collectionPropertyName = "value"; if (null != inputJson.Annotation && null != inputJson.Annotation.CollectionPropertyName) { collectionPropertyName = inputJson.Annotation.CollectionPropertyName; } // If we didn't get an options, create a new one with some defaults provided by the annotation options = options ?? new ValidationOptions(); var annotationTruncated = (inputJson.Annotation ?? new CodeBlockAnnotation()).TruncatedResult; options.AllowTruncatedResponses = annotationTruncated || options.AllowTruncatedResponses; options.CollectionPropertyName = collectionPropertyName; return(schema.ValidateJson(inputJson, issues, this.registeredSchema, options, expectedJson)); }
public IODataNavigable NavigateByEntityTypeKey(EntityFramework edmx, IssueLogger issues) { throw new NotImplementedException(); }
/// <summary> /// Validates the value of json according to an implicit schmea defined by expectedJson /// </summary> /// <returns></returns> public bool ValidateJsonExample(CodeBlockAnnotation expectedResponseAnnotation, string actualResponseBodyJson, IssueLogger issues, ValidationOptions options = null) { List <ValidationError> newErrors = new List <ValidationError>(); var resourceType = expectedResponseAnnotation.ResourceType; if (resourceType == "stream") { // No validation since we're streaming data return(true); } else { JsonSchema schema; if (string.IsNullOrEmpty(resourceType)) { schema = JsonSchema.EmptyResponseSchema; } else if (!this.registeredSchema.TryGetValue(resourceType, out schema)) { newErrors.Add(new ValidationWarning(ValidationErrorCode.ResponseResourceTypeMissing, null, "Missing required resource: {0}. Validation limited to basics only.", resourceType)); // Create a new schema based on what's available in the json schema = new JsonSchema(actualResponseBodyJson, new CodeBlockAnnotation { ResourceType = expectedResponseAnnotation.ResourceType }); } this.ValidateJsonCompilesWithSchema(schema, new JsonExample(actualResponseBodyJson, expectedResponseAnnotation), issues, options: options); return(issues.Issues.Count() == 0); } }
protected override async Task PublishFileToDestinationAsync(FileInfo sourceFile, DirectoryInfo destinationRoot, DocFile page, IssueLogger issues) { this.LogMessage(new ValidationMessage(sourceFile.Name, "Publishing file to HTML")); var destinationPath = this.GetPublishedFilePath(sourceFile, destinationRoot, HtmlOutputExtension); // Create a tag processor string tagsInput; if (null == PageParameters || !PageParameters.TryGetValue("tags", out tagsInput)) { tagsInput = string.Empty; } var markdownSource = page.ReadAndPreprocessFileContents(tagsInput, issues); var converter = this.GetMarkdownConverter(); var html = converter.Transform(markdownSource); // Post-process the resulting HTML for any remaining tags TagProcessor tagProcessor = new TagProcessor(tagsInput, page.Parent.SourceFolderPath, LogMessage); html = tagProcessor.PostProcess(html, sourceFile, converter); var pageData = page.Annotation ?? new PageAnnotation(); if (string.IsNullOrEmpty(pageData.Title)) { pageData.Title = (from b in page.OriginalMarkdownBlocks where b.BlockType == BlockType.h1 select b.Content).FirstOrDefault(); } page.Annotation = pageData; await this.WriteHtmlDocumentAsync(html, page, destinationPath, destinationRoot.FullName); }
/// <summary> /// Validates that the actual response body matches the schema defined for the response and any additional constraints /// from the expected request (e.g. properties that are included in the expected response are required in the actual /// response even if the metadata defines that the response is truncated) /// </summary> internal bool ValidateResponseMatchesSchema(MethodDefinition method, HttpResponse actualResponse, HttpResponse expectedResponse, IssueLogger issues, ValidationOptions options = null) { var expectedResourceType = method.ExpectedResponseMetadata.ResourceType; switch (expectedResourceType) { case "stream": case "Stream": case "Edm.stream": case "Edm.Stream": // No validation since we're streaming data return(true); case "string": case "String": case "Edm.String": case " Edm.string": return(true); } // Get a reference of our JsonSchema that we're checking the response with var expectedResponseJson = (null != expectedResponse) ? expectedResponse.Body : null; JsonSchema schema = this.GetJsonSchema(expectedResourceType, issues, expectedResponseJson); if (null == schema) { issues.Error(ValidationErrorCode.ResourceTypeNotFound, $"Unable to locate a definition for resource type: {expectedResourceType}"); } else { this.ValidateJsonCompilesWithSchema(schema, new JsonExample(actualResponse.Body, method.ExpectedResponseMetadata), issues, (null != expectedResponseJson) ? new JsonExample(expectedResponseJson) : null, options); } if (issues.Issues.WereWarningsOrErrors() && method.ExpectedResponseMetadata.ResourceTypeAka != null) { var expectedResourceTypeAka = method.ExpectedResponseMetadata.ResourceTypeAka; schema = this.GetJsonSchema(expectedResourceTypeAka, issues, expectedResponseJson); if (null == schema) { issues.Error(ValidationErrorCode.ResourceTypeNotFound, $"Unable to locate a definition for resource type: {expectedResourceTypeAka}"); } else { this.ValidateJsonCompilesWithSchema(schema, new JsonExample(actualResponse.Body, method.ExpectedResponseMetadata), issues, (null != expectedResponseJson) ? new JsonExample(expectedResponseJson) : null, options); } } return(!issues.Issues.WereWarningsOrErrors()); }
public static ValidationResult <bool> IsResponseValid(this HttpResponse response, string sourceFile, ApiRequirements requirements, IssueLogger issues) { if (null == requirements || null == requirements.HttpResponse) { return(new ValidationResult <bool>(true)); } List <ValidationError> errors = new List <ValidationError>(); var reqs = requirements.HttpResponse; var responseMimeType = new MultipartMime.MimeContentType(response.ContentType); if (reqs.ContentTypes != null && null != responseMimeType.MimeType && !reqs.ContentTypes.Contains(responseMimeType.MimeType)) { issues.Warning(ValidationErrorCode.InvalidContentType, $"Response Content-Type header value is not in the supported list of content-types: {response.ContentType}"); } return(new ValidationResult <bool>(!errors.Any())); }
/// <summary> /// Scans the text content of a file and removes any "internal" comments/references /// </summary> /// <param name="sourceFile">File.</param> /// <param name="destinationRoot"></param> /// <param name="page"></param> protected override async Task PublishFileToDestinationAsync(FileInfo sourceFile, DirectoryInfo destinationRoot, DocFile page, IssueLogger issues) { this.LogMessage(new ValidationMessage(sourceFile.Name, "Scanning text file for internal content.")); var outputPath = this.GetPublishedFilePath(sourceFile, destinationRoot); var writer = new StreamWriter(outputPath, false, Encoding.UTF8) { AutoFlush = true }; StreamReader reader = new StreamReader(sourceFile.OpenRead()); long lineNumber = 0; string nextLine; while ((nextLine = await reader.ReadLineAsync()) != null) { lineNumber++; if (this.IsDoubleBlockQuote(nextLine)) { this.LogMessage(new ValidationMessage(string.Concat(sourceFile, ":", lineNumber), "Removing DoubleBlockQuote: {0}", nextLine)); continue; } await writer.WriteLineAsync(nextLine); } writer.Close(); reader.Close(); }
/// <summary> /// Convert a tablespec block into one of our internal object model representations /// </summary> public TableDefinition ParseTableSpec(Block tableSpecBlock, Stack <Config.DocumentHeader> headerStack, IssueLogger issues) { List <ValidationError> discoveredErrors = new List <ValidationError>(); List <ItemDefinition> items = new List <ItemDefinition>(); var tableShape = tableSpecBlock.Table; TableDecoder decoder = new TableDecoder { Type = TableBlockType.Unknown }; var headerText = headerStack.Peek()?.Title; // Try matching based on header if (headerText != null) { var matchingDecoder = FindDecoderFromHeaderText(headerStack); if (null != matchingDecoder) { decoder = matchingDecoder; } } // Try matching based on shape if (decoder.Type == TableBlockType.Unknown && null != tableSpecBlock.Table) { var matchingDecoder = FindDecoderFromShape(tableShape); if (null != matchingDecoder) { decoder = matchingDecoder; } } switch (decoder.Type) { case TableBlockType.ErrorCodes: items.AddRange(ParseErrorTable(tableShape, decoder)); break; case TableBlockType.PathParameters: items.AddRange(ParseParameterTable(tableShape, ParameterLocation.Path, decoder, issues.For($"{decoder.Type}Table"))); break; case TableBlockType.ResourcePropertyDescriptions: case TableBlockType.RequestObjectProperties: case TableBlockType.ResponseObjectProperties: items.AddRange(ParseParameterTable(tableShape, ParameterLocation.JsonObject, decoder, issues)); break; case TableBlockType.ResourceNavigationPropertyDescriptions: items.AddRange(ParseParameterTable(tableShape, ParameterLocation.JsonObject, decoder, issues, true)); break; case TableBlockType.HttpHeaders: items.AddRange(ParseParameterTable(tableShape, ParameterLocation.Header, decoder, issues)); break; case TableBlockType.QueryStringParameters: items.AddRange(ParseParameterTable(tableShape, ParameterLocation.QueryString, decoder, issues)); break; case TableBlockType.EnumerationValues: items.AddRange(ParseEnumerationTable(tableShape, decoder)); break; case TableBlockType.AuthScopes: items.AddRange(ParseAuthScopeTable(tableShape, decoder)); break; case TableBlockType.Unknown: var headers = tableShape.ColumnHeaders != null?tableShape.ColumnHeaders.ComponentsJoinedByString(",") : "null"; issues.Message($"Ignored unclassified table: headerText='{headerText}', tableHeaders='{headers}'"); break; default: var hdrs = tableShape.ColumnHeaders != null?tableShape.ColumnHeaders.ComponentsJoinedByString(",") : "null"; issues.Message($"Ignored table: classification='{decoder.Type}', headerText='{headerText}', tableHeaders='{hdrs}'"); break; } return(new TableDefinition(decoder.Type, items, headerText)); }
public JsonResourceDefinition(CodeBlockAnnotation sourceAnnotation, string json, DocFile source, IssueLogger issues) : base(sourceAnnotation, json, source, "json") { ParseJsonInput(issues); }
private static IEnumerable <ResourceDefinition> CreateResourcesFromSchema(Schema schema, IEnumerable <Schema> otherSchema, IssueLogger issues) { List <ResourceDefinition> resources = new List <ResourceDefinition>(); resources.AddRange(from ct in schema.ComplexTypes select ResourceDefinitionFromType(schema, otherSchema, ct, issues)); resources.AddRange(from et in schema.EntityTypes select ResourceDefinitionFromType(schema, otherSchema, et, issues)); return(resources); }
public static string RequestUriPathOnly(this MethodDefinition method, string[] baseUrlsToRemove, IssueLogger issues) { if (string.IsNullOrWhiteSpace(method.Request)) { return(string.Empty); } var path = method.Request.FirstLineOnly().TextBetweenCharacters(' ', '?').TrimEnd('/'); if (baseUrlsToRemove != null) { foreach (var baseUrl in baseUrlsToRemove) { if (!string.IsNullOrEmpty(baseUrl) && path.StartsWith(baseUrl, StringComparison.OrdinalIgnoreCase)) { path = path.Substring(baseUrl.Length); } } } // just in case there's a stray example that doesn't match, at least chop the domain part off. foreach (var scheme in new[] { "https://", "http://" }) { if (path.StartsWith(scheme, StringComparison.OrdinalIgnoreCase)) { int pathStartIndex = path.IndexOf('/', scheme.Length); if (pathStartIndex != -1) { path = path.Substring(pathStartIndex); break; } } } // Normalize variables in the request path path = path.ReplaceTextBetweenCharacters('{', '}', "var"); if (method.RequestMetadata.SampleKeys != null) { foreach (var key in method.RequestMetadata.SampleKeys) { path = path.Replace("/" + key, "/{var}"); } } // Normalize function params var substitutions = new Dictionary <string, string>(); var parenIndex = path.IndexOf('('); for (int i = 0; i < path.Length; i++) { if (path[i] == '(') { // this is the start of a function. let's find the closing paren. var close = path.IndexOf(')', i); if (close > -1) { var inner = path.Substring(i + 1, close - i - 1); substitutions[inner] = NormalizeFunctionParameters(inner, issues); i = close; } } } foreach (var sub in substitutions) { if (string.IsNullOrWhiteSpace(sub.Key)) { continue; } path = path.Replace(sub.Key, sub.Value); } // Rewrite path syntax into what it logically means path = path.ReplaceTextBetweenCharacters(':', ':', "/children/{var}", requireSecondChar: false, removeTargetChars: true); return(path); }
/// <summary> /// Validate the input json against the defined scehma when the instance was created. /// </summary> /// <param name="jsonInput">Input json to validate against schema</param> /// <param name="issues"></param> /// <param name="otherSchemas"></param> /// <param name="options"></param> /// <param name="expectedJson"></param> /// <returns>True if validation was successful, otherwise false.</returns> public bool ValidateJson(JsonExample jsonInput, IssueLogger issues, Dictionary <string, JsonSchema> otherSchemas, ValidationOptions options, JsonExample expectedJson = null) { JContainer obj; try { var settings = new JsonSerializerSettings { DateParseHandling = DateParseHandling.None, NullValueHandling = NullValueHandling.Include, DefaultValueHandling = DefaultValueHandling.Include }; obj = (JContainer)JsonConvert.DeserializeObject(jsonInput.JsonData, settings); } catch (Exception ex) { issues.Error(ValidationErrorCode.JsonParserException, $"Failed to parse json string: {jsonInput.JsonData}.", ex); return(false); } var annotation = jsonInput.Annotation ?? new CodeBlockAnnotation(); bool expectErrorObject = (jsonInput.Annotation != null) && jsonInput.Annotation.ExpectError; // Check for an error response try { dynamic errorObject = obj["error"]; if (null != errorObject && !expectErrorObject) { string code = errorObject.code; string message = errorObject.message; issues.Error(ValidationErrorCode.JsonErrorObject, $"Error response received. Code: {code}, Message: {message}"); return(false); } else if (expectErrorObject && null == errorObject) { issues.Error(ValidationErrorCode.JsonErrorObjectExpected, "Expected an error object response, but didn't receive one."); return(false); } } catch (Exception ex) { if (annotation.ExpectError) { issues.Error(ValidationErrorCode.JsonErrorObjectExpected, "Expected an error object, but it doesn't look like we got one.", ex); } } // Check to see if this is a "collection" instance if (null != annotation && annotation.IsCollection) { this.ValidateCollectionObject(obj, annotation, otherSchemas, options.CollectionPropertyName, issues, options); } // otherwise verify the object matches this schema else { options = options ?? new ValidationOptions(annotation); if (null != expectedJson) { var expectedJsonSchema = new JsonSchema(expectedJson.JsonData, expectedJson.Annotation); options.ExpectedJsonSchema = expectedJsonSchema; options.RequiredPropertyNames = expectedJsonSchema.ExpectedProperties.Keys.ToArray(); } this.ValidateContainerObject(obj, options, otherSchemas, issues); } return(issues.Issues.Count() == 0); }
public IODataNavigable NavigateByUriComponent(string component, EntityFramework edmx, IssueLogger issues, bool isLastSegment) { string targetType = null; var entitySet = (from e in EntitySets where e.Name == component select e).FirstOrDefault(); if (null != entitySet) { // EntitySet is always a collection of an item type targetType = "Collection(" + entitySet.EntityType + ")"; } else { var singleton = (from s in Singletons where s.Name == component select s).FirstOrDefault(); if (null != singleton) { targetType = singleton.Type; } } if (targetType != null) { if (targetType.StartsWith("Collection(")) { var innerId = targetType.Substring(11, targetType.Length - 12); return(new ODataCollection(innerId)); } return(edmx.ResourceWithIdentifier <IODataNavigable>(targetType)); } return(null); }
/// <summary> /// Verify that a Json container (object) is valid according to it's resource name (schema). /// </summary> /// <param name="obj"></param> /// <param name="options"></param> /// <param name="otherSchemas"></param> /// <param name="issues"></param> private void ValidateContainerObject(JContainer obj, ValidationOptions options, Dictionary <string, JsonSchema> otherSchemas, IssueLogger issues) { var explicitType = obj["@odata.type"]; if (explicitType != null) { var typeName = explicitType.Value <string>(); if (!string.IsNullOrEmpty(typeName)) { if (typeName.StartsWith("#")) { typeName = typeName.Substring(1); } JsonSchema childType; if (this.childTypes.TryGetValue(typeName, out childType)) { childType.ValidateContainerObject(obj, options, otherSchemas, issues); return; } else if (!typeName.IEquals(this.ResourceName)) { issues.Warning(ValidationErrorCode.ResourceTypeNotFound, $"unrecognized type declaration {typeName}"); } } } var containerProperties = from p in obj select ParseProperty(p, this, issues.For(p.Path)); this.ValidateObjectProperties(containerProperties.Where(x => null != x), options, otherSchemas, issues); }
private void ValidateObjectProperties(IEnumerable <ParameterDefinition> propertiesOnObject, ValidationOptions options, Dictionary <string, JsonSchema> otherSchemas, IssueLogger issues) { List <string> missingProperties = new List <string>(); missingProperties.AddRange(from m in this.ExpectedProperties select m.Key); foreach (var property in propertiesOnObject) { missingProperties.Remove(property.Name); // This detects bad types, extra properties, etc. if (null != options && (property.Type.IsCollection || property.Type.IsObject)) { var propertyOptions = options.CreateForProperty(property.Name); this.ValidateProperty(property, otherSchemas, issues.For(property.Name), propertyOptions); } else { this.ValidateProperty(property, otherSchemas, issues.For(property.Name), options); } } this.CleanMissingProperties(options, missingProperties); if (missingProperties.Count > 0) { issues.Error(ValidationErrorCode.RequiredPropertiesMissing, $"Missing properties: response was missing these required properties: {missingProperties.ComponentsJoinedByString(", ")}"); } }