Как я перестал волноваться и стал отдавать метаданные restful API

от автора

Если вы делаете публичный 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/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *