📜 ⬆️ ⬇️

How I stopped worrying and began to give restful API metadata



If you are doing 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.

Supporting such a resource (especially in the conditions when the API is actively developing) is a fairly labor-intensive matter. With changes, you have to synchronize the documentation with the actual implementation and it is annoying. Synchronization consists of:

Automation of the second item is offered by the guys from startup apiary.io , they provide an opportunity to write documentation in a special domain-oriented language (DSL), and then, using a proxy to your API, write down 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 have already described the interface in code.
')
Of course, there is no universal way to extract an interface in the form of a description of requests and responses from code, but if you use a framework in which there are agreements about routing and execution of requests, such information can be obtained. In addition, there is an opinion that such a description is not necessary and the client should understand how to work with the REST API, knowing only the URL of the root resource and the used media types. But I have not seen a single serious public API that uses this approach.

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

There are several options:



I chose the last 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 of time, etc. In addition, I do not really like the idea of ​​publishing metadata and descriptions in natural language in one document (and 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 to an API. Such clients will be the reference implementation, and 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 actions and types, the descriptions of which will fall into the metadata:

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


Now we will make a controller that will return type schemes (something like json schema , but easier), which are available in the 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"); } } } 


It is very unlikely that the types will change in runtime, so we will cache the result.
To get information about 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<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() }; } } 


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

Now it only remains to make a one-page application to display and edit documentation:



The rest of the code can be found on GitHub .

Source: https://habr.com/ru/post/174555/


All Articles