Как я перестал волноваться и стал отдавать метаданные 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.
    Дневник.ру 36,22
    Компания
    Поделиться публикацией
    Реклама помогает поддерживать и развивать наши сервисы

    Подробнее
    Реклама
    Похожие публикации
    Комментарии 20
    • 0
      Чем не устроило встроенное решение ApiExplorer?
      • 0
        Оно как раз и используется для получения метаданных.
        • +2
          Как то у вас слишком все переусложнено, по сравнению с

          public ActionResult Index()
          {
              var apiExplorer = GlobalConfiguration.Configuration.Services.GetApiExplorer();
              return View(new ApiDocumentationModel(apiExplorer));
          }
          


          View можно скастомизировать по вкусу.
          • 0
            Тут всё тоже самое, только код из конструктора ApiDocumentationModel, для наглядности помещён прямо в контроллер.
      • 0
        Мне кажется, или вы завелосипедили Web API help page?
        • +2
          Да, но с возможностью сделать свой фронтенд. WebApi Help Pagе делает страницы с документацией для людей, а при публикации метаданных в json, можно сгенерировать клиент к API.
          • 0
            Ну в WebAPI очень даже можно сделать свой фронтэнд — там весь UI в обычных разоровских вьюшках, меняй не хочу.

            js-клиент, это, безусловно, плюс, надо будет посмотреть детальнее. В посте про это мельком. Детали реализации это хорошо, но собственно на функционале и отличиях от встроенного механизма акцента не хватает, мне кажется.
        • НЛО прилетело и опубликовало эту надпись здесь
          • 0
            Спасибо за наводку, идея с OPTIONS очень нравится.
          • 0
            Есть еще протокол XHTTP. С его помощью можно легко создавать версионные API и отдавать метаданные.
            • 0
              Для питона есть Sphinx. Он не только строит документацию, включая в нее API доку из исходников, но и может запускать тесты из docstring'ов. Или просто запускать тесты во время билда, кому как нравиться. Главное, что можно не ограничиваться только API докой, можно писать полноценную документацию. Пример и сорцы. Просто пример, как это может быть чуть более кратко.
              • 0
                А почему не использовать WSDL 2.0? Или XML настолько немодный?
                • 0
                  Есть ли Reflection для JS? Объясню, откуда этот вопрос: для PHP я написал я помощью Reflection генератор WSDLей, которые кушает 1ска, причем ты ему скармливаешь имена функций, а дальше он через этот самый Reflection все зависимые классы разворачивает через Reflection и phpdoc. Хотелось бы реализовать что-то подобное для node.js.
                  • 0
                    Не совсем понял вопрос, но скорее всего вам подойдёт вот это — github.com/mashery/iodocs
                    • 0
                      тут, опять же, надо самому делать описание. Задача же — сгенерить описание по коду, желательно на лету. Например:

                      $ws = new WS1c('http://fragster.ru/multithread', 'fragster');
                      
                      $ws->methods[] = new ReflectionFunction('resultsExchange');
                      $ws->methods[] = new ReflectionFunction('newResults');
                      $ws->methods[] = new ReflectionFunction('appendTestResult');
                      $ws->methods[] = new ReflectionFunction('test');
                      
                      header("Content-Type: text/xml;");
                      if (isset($_REQUEST['wsdl'])) {
                          echo $ws->getWSDL();
                      } else {
                          echo  $ws->handleRequest($xml);
                      }
                      


                      и на выходе получаем fragster.ru/perfomanceTest/ws.php?wsdl
                      • 0
                        В JS чтобы получить информацию о содержании объекта, каких-то специальных механизмов не нужно, всегда можно сделать
                        for (member in object) ...
                        Если этого недостаточно, то всегда можно сделать AST из исходников и провести static analysis.
                  • 0
                    Это все круто, но сам классический REST плох тем, что смешивает транспортный уровень (HTTP) и прикладной уровень (собственно ваше API, основанное на обмене объектами). Т.е. например если вы потом захотите завернуть ваше REST API в ВебСокеты, то это будет не слишком легко.
                    • 0
                      У вас есть внутренний API и его аналог, опубликованный через REST. Вам ничего не мешает внутренний API также опубликовать через SOAP или вебсокеты. Другое дело, что с вебсокетами вы меняете сам принцип запрос-ответ на потоковое вещание, что выглядит как минимум странно.
                    • 0
                      Промахнулся по ссылке «ответить», просьба игнориривать.
                      • 0
                        Мы сначала генерировали документацию по результатам тестов, а сейчас зафиксировали её в сервисе apiary.io. (PHP)

                        Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                        Самое читаемое