using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.Dynamic; using System.Linq; using System.Net.Http; using System.Reflection; using System.Runtime.Serialization; using System.Text; using System.Web.Http; using Newtonsoft.Json; using Newtonsoft.Json.Linq; using Newtonsoft.Json.Serialization; using Newtonsoft.Json.Converters; using System.Net.Http.Formatting; namespace Swashbuckle.Swagger { public class SchemaRegistry { private readonly JsonSerializerSettings _jsonSerializerSettings; private readonly IDictionary> _customSchemaMappings; private readonly IEnumerable _schemaFilters; private readonly IEnumerable _modelFilters; private readonly Func _schemaIdSelector; private readonly bool _ignoreObsoleteProperties; private readonly bool _describeAllEnumsAsStrings; private readonly bool _describeStringEnumsInCamelCase; private readonly bool _applyFiltersToAllSchemas; private readonly IContractResolver _contractResolver; private IDictionary _workItems; private class WorkItem { public string SchemaId; public bool InProgress; public Schema Schema; } public SchemaRegistry( JsonSerializerSettings jsonSerializerSettings, IDictionary> customSchemaMappings, IEnumerable schemaFilters, IEnumerable modelFilters, bool ignoreObsoleteProperties, Func schemaIdSelector, bool describeAllEnumsAsStrings, bool describeStringEnumsInCamelCase, bool applyFiltersToAllSchemas) { _jsonSerializerSettings = jsonSerializerSettings; _customSchemaMappings = customSchemaMappings; _schemaFilters = schemaFilters; _modelFilters = modelFilters; _schemaIdSelector = schemaIdSelector; _ignoreObsoleteProperties = ignoreObsoleteProperties; _describeAllEnumsAsStrings = describeAllEnumsAsStrings; _describeStringEnumsInCamelCase = describeStringEnumsInCamelCase; _applyFiltersToAllSchemas = applyFiltersToAllSchemas; _contractResolver = jsonSerializerSettings.ContractResolver ?? new DefaultContractResolver(); _workItems = new Dictionary(); Definitions = new Dictionary(); } public Schema GetOrRegister(Type type) { var schema = CreateInlineSchema(type); // Iterate outstanding work items (i.e. referenced types) and generate the corresponding definition while (_workItems.Any(entry => entry.Value.Schema == null && !entry.Value.InProgress)) { var typeMapping = _workItems.First(entry => entry.Value.Schema == null && !entry.Value.InProgress); var workItem = typeMapping.Value; workItem.InProgress = true; workItem.Schema = CreateDefinitionSchema(typeMapping.Key); Definitions.Add(workItem.SchemaId, workItem.Schema); workItem.InProgress = false; } return schema; } public IDictionary Definitions { get; private set; } private Schema CreateInlineSchema(Type type) { var jsonContract = _contractResolver.ResolveContract(type); if (_customSchemaMappings.ContainsKey(type)) return FilterSchema(_customSchemaMappings[type](), jsonContract); if (jsonContract is JsonPrimitiveContract) return FilterSchema(CreatePrimitiveSchema((JsonPrimitiveContract)jsonContract), jsonContract); var dictionaryContract = jsonContract as JsonDictionaryContract; if (dictionaryContract != null) return dictionaryContract.IsSelfReferencing() ? CreateRefSchema(type) : FilterSchema(CreateDictionarySchema(dictionaryContract), jsonContract); var arrayContract = jsonContract as JsonArrayContract; if (arrayContract != null) return arrayContract.IsSelfReferencing() ? CreateRefSchema(type) : FilterSchema(CreateArraySchema(arrayContract), jsonContract); var objectContract = jsonContract as JsonObjectContract; if (objectContract != null && !objectContract.IsAmbiguous()) return CreateRefSchema(type); // Fallback to abstract "object" return FilterSchema(new Schema { type = "object" }, jsonContract); } private Schema CreateDefinitionSchema(Type type) { var jsonContract = _contractResolver.ResolveContract(type); if (jsonContract is JsonDictionaryContract) return FilterSchema(CreateDictionarySchema((JsonDictionaryContract)jsonContract), jsonContract); if (jsonContract is JsonArrayContract) return FilterSchema(CreateArraySchema((JsonArrayContract)jsonContract), jsonContract); if (jsonContract is JsonObjectContract) return FilterSchema(CreateObjectSchema((JsonObjectContract)jsonContract), jsonContract); throw new InvalidOperationException( String.Format("Unsupported type - {0} for Defintitions. Must be Dictionary, Array or Object", type)); } private Schema CreatePrimitiveSchema(JsonPrimitiveContract primitiveContract) { var type = Nullable.GetUnderlyingType(primitiveContract.UnderlyingType) ?? primitiveContract.UnderlyingType; if (type.IsEnum) return CreateEnumSchema(primitiveContract, type); switch (type.FullName) { case "System.Boolean": return new Schema { type = "boolean" }; case "System.Byte": case "System.SByte": case "System.Int16": case "System.UInt16": case "System.Int32": case "System.UInt32": return new Schema { type = "integer", format = "int32" }; case "System.Int64": case "System.UInt64": return new Schema { type = "integer", format = "int64" }; case "System.Single": return new Schema { type = "number", format = "float" }; case "System.Double": case "System.Decimal": return new Schema { type = "number", format = "double" }; case "System.Byte[]": return new Schema { type = "string", format = "byte" }; case "System.DateTime": case "System.DateTimeOffset": return new Schema { type = "string", format = "date-time" }; case "System.Guid": return new Schema { type = "string", format = "uuid" }; default: return new Schema { type = "string" }; } } private Schema CreateEnumSchema(JsonPrimitiveContract primitiveContract, Type type) { var stringEnumConverter = primitiveContract.Converter as StringEnumConverter ?? _jsonSerializerSettings.Converters.OfType().FirstOrDefault(); if (_describeAllEnumsAsStrings || stringEnumConverter != null) { var camelCase = _describeStringEnumsInCamelCase || (stringEnumConverter != null && stringEnumConverter.CamelCaseText); return new Schema { type = "string", @enum = camelCase ? type.GetEnumNamesForSerialization().Select(name => name.ToCamelCase()).ToArray() : type.GetEnumNamesForSerialization() }; } return new Schema { type = "integer", format = "int32", @enum = type.GetEnumValues().Cast().ToArray() }; } private Schema CreateDictionarySchema(JsonDictionaryContract dictionaryContract) { var keyType = dictionaryContract.DictionaryKeyType ?? typeof(object); var valueType = dictionaryContract.DictionaryValueType ?? typeof(object); if (keyType.IsEnum) { //return new Schema //{ // type = "object", // properties = Enum.GetNames(keyType).ToDictionary( // (name) => dictionaryContract.PropertyNameResolver(name), // (name) => CreateInlineSchema(valueType) // ) //}; throw new Exception("出现不兼容的情况"); } else { return new Schema { type = "object", additionalProperties = CreateInlineSchema(valueType) }; } } private Schema CreateArraySchema(JsonArrayContract arrayContract) { var itemType = arrayContract.CollectionItemType ?? typeof(object); return new Schema { type = "array", items = CreateInlineSchema(itemType) }; } private Schema CreateObjectSchema(JsonObjectContract jsonContract) { var properties = jsonContract.Properties .Where(p => !p.Ignored) .Where(p => !(_ignoreObsoleteProperties && p.IsObsolete())) .ToDictionary( prop => prop.PropertyName, prop => CreateInlineSchema(prop.PropertyType).WithValidationProperties(prop) ); var required = jsonContract.Properties.Where(prop => prop.IsRequired()) .Select(propInfo => propInfo.PropertyName) .ToList(); return new Schema { required = required.Any() ? required : null, // required can be null but not empty properties = properties, type = "object" }; } private Schema CreateRefSchema(Type type) { if (!_workItems.ContainsKey(type)) { var schemaId = _schemaIdSelector(type); if (_workItems.Any(entry => entry.Value.SchemaId == schemaId)) { var conflictingType = _workItems.First(entry => entry.Value.SchemaId == schemaId).Key; throw new InvalidOperationException(String.Format( "Conflicting schemaIds: Duplicate schemaIds detected for types {0} and {1}. " + "See the config setting - \"UseFullTypeNameInSchemaIds\" for a potential workaround", type.FullName, conflictingType.FullName)); } _workItems.Add(type, new WorkItem { SchemaId = schemaId }); } return new Schema { @ref = "#/definitions/" + _workItems[type].SchemaId }; } private Schema FilterSchema(Schema schema, JsonContract jsonContract) { if (schema.type == "object" || _applyFiltersToAllSchemas) { var jsonObjectContract = jsonContract as JsonObjectContract; if (jsonObjectContract != null) { // NOTE: In next major version, _modelFilters will completely replace _schemaFilters var modelFilterContext = new ModelFilterContext(jsonObjectContract.UnderlyingType, jsonObjectContract, this); foreach (var filter in _modelFilters) { filter.Apply(schema, modelFilterContext); } } foreach (var filter in _schemaFilters) { filter.Apply(schema, this, jsonContract.UnderlyingType); } } return schema; } } }