How I stopped worrying and began to give metadata to the restful API



    If you are making a public API, then you are most likely faced with the problem of its documentation. Large companies make special portals for developers where you can read and discuss documentation, or download a client library for your favorite programming language.

    Support for such a resource (especially in conditions when the API is actively developing) is a rather labor-consuming matter. With changes, you have to synchronize the documentation with the actual implementation and this is annoying. Synchronization consists of:
    • Checks that all existing functionality is described in the documentation
    • Checks that everything described works as stated in the documentation

    The guys from the startup apiary.io offer to automate the second point , they provide the opportunity to write documentation in a special domain-specific language (DSL), and then, using a proxy to your API, record requests, and periodically check that everything described is true. But in this case, you still have to write all the documentation yourself, and this seems superfluous, because you most likely already described the interface in the code.

    Of course, there is no universal way to extract an interface in the form of a description of requests and responses from the code, but if you use a framework that has agreements on routing and query execution, then such information can be obtained. In addition, there is an opinionthat such a description is not necessary and the client himself must understand how to work with the REST API, knowing only the URL of the root resource and the media types used. But I have not seen a single serious public API that uses this approach.

    To automatically generate documentation, you will need a format for describing metadata, something like WSDL , but with descriptions in terms of REST.

    There are several options:

    • WADL - requires the use of XML for description, and this has not been fashionable for a long time.
    • Swagger spec - the metadata format used in the Swagger framework , based on json, there are generators for several frameworks and an application for publishing documentation on metadata.
    • Google API discovery document is a metadata format that Google uses for some of its services.
    • I \ O docs is another format very similar to Google.
    • Own format.


    I chose the latter option, because it allows you to take into account all the features of your implementation, such as your own authentication / authorization, restrictions on the number of requests per unit time, etc. In addition, I do not really like the idea of ​​publishing metadata and descriptions in a natural language in one document (but what about localization?), As it happens in all the solutions described above.
    In addition to generating documentation, metadata can be used to generate client code for the API. Such clients will be the reference implementation, and they can be used to test the API.

    Implementation


    Further it will be uninteresting to those who are far from ASP.NET WebAPI . So, you have an API on this platform and you want to publish metadata. First, we need an attribute that will mark the actions and types whose descriptions will fall into the metadata:

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


    Now let's make a controller that will return type schemes (something like json schema , but simpler), which are available in the API:

        public class TypeMetadataController : ApiController
        {
            private readonly Assembly typeAssembly;
            public TypeMetadataController(Assembly typeAssembly)
            {
                this.typeAssembly = typeAssembly;
            }
            [OutputCache]
            public IEnumerable 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(name);
                return type;
            }
            ApiType GetApiType(Type type)
            {
                var dataContractAttribute = type.GetCustomAttribute();
                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");
                }
            }
        }
    


    It is very unlikely that types will change in runtime, so we cache the result.
    To get information about the requests that the API can handle, you can use IApiExplorer .

        public class ResourceMetadataController : ApiController
        {
            private readonly IApiExplorer apiExplorer;
            public ResourceMetadataController(IApiExplorer apiExplorer)
            {
                this.apiExplorer = apiExplorer;
            }
            [OutputCache]
            public IEnumerable Get()
            {
                var controllers = this.apiExplorer
                   .ApiDescriptions
                   .Where(x => x.ActionDescriptor.ControllerDescriptor.GetCustomAttributes().Any() || x.ActionDescriptor.GetCustomAttributes().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().Any() || x.ActionDescriptor.ControllerDescriptor.GetCustomAttributes().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 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().Any()
                };
            }
        }
    


    In all returned objects there is a field `DocumentationArticleId` - this is the identifier of the documentation article for elements that are stored separately from metadata, for example, in a json file or in a database.

    Now all that remains is to make a one-page application to display and edit the documentation:



    The rest of the code can be found on GitHub .

    Also popular now: