Если вы делаете публичный API, то скорее всего сталкивались с проблемой его документации. Большие компании делают специальные порталы для разработчиков, где можно почитать и обсудить документацию, или скачать библиотеку-клиент для вашего любимого языка программирования.
Поддержка такого ресурса (особенно в условиях, когда API активное развивается) достаточно трудозатратное дело. При изменениях, приходится синхронизировать документацию с фактической реализацией и это напрягает. Синхронизация состоит из:
- Проверке, что вся существующая функциональность описана в документации
- Проверке, что всё описанное работает как заявлено в документации
Автоматизировать второй пункт предлагают ребята из стартапа apiary.io, они предоставляют возможность написать документацию на специальном предметно-ориентированной языке (DSL), а потом, при помощи прокси к вашему API записать запросы, и периодически проверять, что всё описаное соответствует действительности. Но в данном случае, вам всё ещё придется самим писать всю документацию и это кажется лишним, потому что интерфейс вы скорее всего уже описали в коде.
Конечно же, универсального способа извлечь интерфейс в виде описания запросов и ответов из кода не существует, но если вы используете фреймворк, в котором есть соглашения по поводу маршрутизации и выполнения запросов, то такую информацию можно получить. Кроме того, существует мнение, что такое описание не нужно и клиент должен сам понять как работать с REST API, зная только URL корневого ресурса и используемые media types. Но я не видел ни одного серьёзного публичного API, которое использует такой подход.
Чтобы автоматически сгенерировать документацию, понадобится формат для описания метаданных, что-то вроде WSDL, но с описаниями в терминах REST.
Есть несколько вариантов:
- WADL — требует использования XML для описания, а это давно не модно.
- Swagger spec — формат метаданных, который используется в фреймворке Swagger, основан на json, есть генераторы для нескольких фреймворков и приложение для публикации документации по метаданным.
- Google API discovery document — формат метаданных, который использует Google для некоторых своих сервисов.
- I\O docs — ещё один формат, очень похожий на гугловый.
- Свой формат.
Я выбрал последний вариант, потому что он позволяет учесть все особенности вашей реализации, вроде собственной аутентификации\авторизации, ограничений количества запросов в единицу времени, итд. Кроме того, мне не очень нравится идея публиковать метаданные и описания на естественном языке в одном документе (а как же локализация?), как это происходит во всех описанных выше решениях.
Помимо генерации документации, метаданные можно использовать для генерации кода клиентов к API. Такие клиенты будут референсной реализацией, и их можно использовать для тестирования API.
Реализация
Дальше будет не интересно тем, кто далёк от ASP.NET WebAPI. Итак, у вас есть API на этой платформе и вы хотите публиковать метаданные. Для начала, нужен атрибут, которым будем помечать экшены и типы, описания которых попадут в метаданные:
[AttributeUsage(AttributeTargets.Method | AttributeTargets.Class, AllowMultiple = false)] public class MetaAttribute : Attribute { }
Теперь сделаем контроллер, который будет отдавать схемы типов (что-то вроде json schema, но проще), которые доступны в API:
public class TypeMetadataController : ApiController { private readonly Assembly typeAssembly; public TypeMetadataController(Assembly typeAssembly) { this.typeAssembly = typeAssembly; } [OutputCache] public IEnumerable<ApiType> Get() { return this.typeAssembly .GetTypes() .Where(t => Attribute.IsDefined(t, typeof(MetaAttribute))) .Select(GetApiType); } [OutputCache] public ApiType Get(String name) { var type = this.Get().FirstOrDefault(t => t.Name == name); if (type == null) throw new ResourceNotFoundException<ApiType, String>(name); return type; } ApiType GetApiType(Type type) { var dataContractAttribute = type.GetCustomAttribute<DataContractAttribute>(); return new ApiType { Name = dataContractAttribute != null ? dataContractAttribute.Name : type.Name, DocumentationArticleId = dataContractAttribute != null ? dataContractAttribute.Name : type.Name, Properties = type.GetMembers() .Where(p => p.IsDefined(typeof(DataMemberAttribute), false)) .Select(p => { var dataMemberAttribute = p.GetCustomAttributes(typeof (DataMemberAttribute), false).First() as DataMemberAttribute; return new ApiTypeProperty { Name = dataMemberAttribute != null ? dataMemberAttribute.Name : p.Name, Type = ApiType.GetTypeName(GetMemberUnderlyingType(p)), DocumentationArticleId = String.Format("{0}.{1}", dataContractAttribute != null ? dataContractAttribute.Name : type.Name, dataMemberAttribute != null ? dataMemberAttribute.Name : p.Name) }; } ).ToList() }; } static Type GetMemberUnderlyingType(MemberInfo member) { switch (member.MemberType) { case MemberTypes.Field: return ((FieldInfo)member).FieldType; case MemberTypes.Property: return ((PropertyInfo)member).PropertyType; default: throw new ArgumentException("MemberInfo must be if type FieldInfo or PropertyInfo", "member"); } } }
Очень маловероятно, что типы будут изменяться в рантайме, поэтому закэшируем результат.
Чтобы получить информацию о запросах, которые умеет обрабатывать API, можно воспользоваться IApiExplorer.
public class ResourceMetadataController : ApiController { private readonly IApiExplorer apiExplorer; public ResourceMetadataController(IApiExplorer apiExplorer) { this.apiExplorer = apiExplorer; } [OutputCache] public IEnumerable<ApiResource> Get() { var controllers = this.apiExplorer .ApiDescriptions .Where(x => x.ActionDescriptor.ControllerDescriptor.GetCustomAttributes<MetaAttribute>().Any() || x.ActionDescriptor.GetCustomAttributes<MetaAttribute>().Any()) .GroupBy(x => x.ActionDescriptor.ControllerDescriptor.ControllerName) .Select(x => x.First().ActionDescriptor.ControllerDescriptor.ControllerName) .ToList(); return controllers.Select(GetApiResourceMetadata).ToList(); } ApiResource GetApiResourceMetadata(string controller) { var apis = this.apiExplorer .ApiDescriptions .Where(x => x.ActionDescriptor.ControllerDescriptor.ControllerName == controller && ( x.ActionDescriptor.GetCustomAttributes<MetaAttribute>().Any() || x.ActionDescriptor.ControllerDescriptor.GetCustomAttributes<MetaAttribute>().Any() ) ).GroupBy(x => x.ActionDescriptor); return new ApiResource { Name = controller, Requests = apis.Select(g => this.GetApiRequest(g.First(), g.Select(d => d.RelativePath))).ToList(), DocumentationArticleId = controller }; } ApiRequest GetApiRequest(ApiDescription api, IEnumerable<String> uris) { return new ApiRequest { Name = api.ActionDescriptor.ActionName, Uris = uris.ToArray(), DocumentationArticleId = String.Format("{0}.{1}", api.ActionDescriptor.ControllerDescriptor.ControllerName, api.ActionDescriptor.ActionName), Method = api.HttpMethod.Method, Parameters = api.ParameterDescriptions.Select( parameter => new ApiRequestParameter { Name = parameter.Name, DocumentationArticleId = String.Format("{0}.{1}.{2}", api.ActionDescriptor.ControllerDescriptor.ControllerName, api.ActionDescriptor.ActionName, parameter.Name), Source = parameter.Source.ToString().ToLower().Replace("from",""), Type = ApiType.GetTypeName(parameter.ParameterDescriptor.ParameterType) }).ToList(), ResponseType = ApiType.GetTypeName(api.ActionDescriptor.ReturnType), RequiresAuthorization = api.ActionDescriptor.GetCustomAttributes<RequiresAuthorization>().Any() }; } }
Во всех возвращаемых объектах есть поле `DocumentationArticleId` это идентификатор статьи документации для этого элемента, которые хранятся отдельно от метаданных, например, в json файле или в бд.
Теперь осталось только сделать одностраничное приложение, чтобы показывать и редактировать документацию:
С остальным кодом можно ознакомиться на GitHub.
ссылка на оригинал статьи http://habrahabr.ru/company/dnevnik_ru/blog/174555/
Добавить комментарий