- PVSM.RU - https://www.pvsm.ru -

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

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

Если вы делаете публичный API, то скорее всего сталкивались с проблемой его документации. Большие [1] компании [2] делают специальные порталы для разработчиков, где можно почитать и обсудить документацию, или скачать библиотеку-клиент для вашего любимого языка программирования.

Поддержка такого ресурса (особенно в условиях, когда API активно развивается) — достаточно трудозатратное дело. При изменениях, приходится синхронизировать документацию с фактической реализацией и это напрягает. Синхронизация состоит из:

  • Проверки, что вся существующая функциональность описана в документации
  • Проверки, что всё описанное работает как заявлено в документации


Автоматизировать второй пункт предлагают ребята из стартапа apiary.io [3], они предоставляют возможность написать документацию на специальном предметно-ориентированном языке (DSL), а потом, при помощи прокси к вашему API, записать запросы, и периодически проверять, что всё, описанное соответствует действительности. Но в данном случае, вам всё ещё придется самим писать всю документацию, и это кажется лишним, потому что интерфейс вы, скорее всего, уже описали в коде.

Конечно же, универсального способа извлечь интерфейс в виде описания запросов и ответов из кода не существует, но если вы используете фреймворк, в котором есть соглашения по поводу маршрутизации и выполнения запросов, то такую информацию можно получить. Кроме того, существует [4] мнение [5], что такое описание не нужно и клиент должен сам понять, как работать с REST API, зная только URL корневого ресурса и используемые media types. Но я не видел ни одного серьёзного публичного API, которое использует такой подход.

Чтобы автоматически сгенерировать документацию, понадобится формат для описания метаданных, что-то вроде WSDL [6], но с описаниями в терминах REST.

Есть несколько вариантов:

  • WADL [7] — требует использования XML для описания, а это давно не модно.
  • Swagger spec [8] — формат метаданных, который используется в фреймворке Swagger [9], основан на json, есть генераторы для нескольких фреймворков и приложение для публикации документации по метаданным.
  • Google API discovery document [10] — формат метаданных, который использует Google для некоторых своих сервисов.
  • IO docs [11] — ещё один формат, очень похожий на гугловый.
  • Свой формат.

Я выбрал последний вариант, потому что он позволяет учесть все особенности вашей реализации, вроде собственной аутентификацииавторизации, ограничений количества запросов в единицу времени, и т.д. Кроме того, мне не очень нравится идея публиковать метаданные и описания на естественном языке в одном документе (а как же локализация?), как это происходит во всех описанных выше решениях.
Помимо генерации документации, метаданные можно использовать для генерации кода клиентов к API. Такие клиенты будут референсной реализацией, и их можно использовать для тестирования API.

Реализация

Дальше будет неинтересно тем, кто далёк от ASP.NET WebAPI [12]. Итак, у вас есть API на этой платформе и вы хотите публиковать метаданные. Для начала нужен атрибут, которым будем помечать экшены и типы, описания которых попадут в метаданные:

    [AttributeUsage(AttributeTargets.Method | AttributeTargets.Class, AllowMultiple = false)]
    public class MetaAttribute : Attribute
    {
        
    }

Теперь сделаем контроллер, который будет отдавать схемы типов (что-то вроде json schema [13], но проще), которые доступны в 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 [14].

    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 файле или в бд.

Теперь осталось только сделать одностраничное приложение, чтобы показывать и редактировать документацию:

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

С остальным кодом можно ознакомиться на GitHub [15].

Автор: Nonick

Источник [16]


Сайт-источник PVSM.RU: https://www.pvsm.ru

Путь до страницы источника: https://www.pvsm.ru/api/30606

Ссылки в тексте:

[1] Большие: http://developers.facebook.com

[2] компании: https://dev.twitter.com

[3] apiary.io: http://apiary.io

[4] существует: http://roy.gbiv.com/untangled/2008/rest-apis-must-be-hypertext-driven

[5] мнение: http://en.wikipedia.org/wiki/HATEOAS

[6] WSDL: http://www.w3.org/TR/wsdl20

[7] WADL: http://www.w3.org/Submission/wadl

[8] Swagger spec: https://github.com/wordnik/swagger-core/wiki

[9] Swagger: https://developers.helloreverb.com/swagger/

[10] Google API discovery document: https://developers.google.com/discovery

[11] IO docs: http://www.mashery.com/product/io-docs

[12] ASP.NET WebAPI: http://www.asp.net/web-api

[13] json schema: http://json-schema.org/

[14] IApiExplorer: http://msdn.microsoft.com/en-us/library/system.web.http.description.iapiexplorer(v=vs.108).aspx

[15] на GitHub: https://github.com/idmitriev/ApiMeta

[16] Источник: http://habrahabr.ru/post/174555/