浅析如何在Nancy中生成API文档
前言
前后端分离,或许是现如今最为流行开发方式,包括UWP、Android和IOS这样的手机客户端都是需要调用后台的API来进行数据的交互。
但是这样对前端开发和APP开发就会面临这样一个问题:如何知道每个API做什么?
可能,有人会在内部形成一份word文档、pdf;有人会建立一个单独的站点,然后将API的地址,参数等信息列在上面;有人会借助第三方的工具来生成一份文档等。
当然,这基本是取决于不同公司的规范。
说起API文档,就想到前段时间做的微信小程序,由于那个不完善的接口文档,从而导致浪费了很大一部分时间去询问接口相关的内容(用的是老的接口)。
为了处理这个问题,我认为,如果能在写某个API的时候就顺带将这个API的相关信息一并处理了是最好不过!
不过这并不是让我们写好一个接口后,再去打开word等工具去编辑一下这个API的信息,这样明显需要花费更多的时间。
下面就针对这一问题,探讨一下在Nancy中的实现。
如何实现
其实,想在Nancy中生成API文档,是一件十分容易的事,因为作者thecodejunkie已经帮我们在Nancy内部提前做了一些处理
便于我们的后续扩展,这点还是很贴心的。
下面我们先来写点东西,后面才能写相应的API文档。
public class ProductsModule : NancyModule
{
public ProductsModule() : base("/products")
{
Get("/", _ =>
{
return Response.AsText("product list");
}, null, "GetProductList");
Get("/{productid}", _ =>
{
return Response.AsText(_.productid as string);
}, null, "GetProductByProductId");
Post("/", _ =>
{
return Response.AsText("Add product");
}, null, "AddProduct");
//省略部分..
}
}
基本的CURD,没有太多的必要去解释这些内容。当然这里需要指出一点。
正常情况下,我们基本都是只写前面两个参数的,后面两个参数是可选的。由于我们后面需要用到每个路由的名字
所以我们需要用到这里的第4个参数(当前路由的名字),也就意味着我们要在定义的时候写多一点东西!
注: 1.x和2.x的写法是有区别的!示例用的2.x的写法,所以各位要注意这点!
以GET为例,方法定义大致如下
API写好了,下面我们先来简单获取一下这些api的相关信息!
最简单的实现
前面也提到,我们是要把这个api和api文档放到同一个站点下面,免去编辑这一步骤!
世间万物都是相辅相成的,我们不想单独编辑,自然就要在代码里面多做一些处理!
新起一个Module名为DocModule
,将api文档的相关内容放到这个module中来处理。
public class DocMudule : NancyModule
{
private IRouteCacheProvider _routeCacheProvider;
public DocMudule(IRouteCacheProvider routeCacheProvider) : base("/docs")
{
this._routeCacheProvider = routeCacheProvider;
Get("/", _ =>
{
var routeDescriptionList = _routeCacheProvider
.GetCache()
.SelectMany(x => x.Value)
.Select(x => x.Item2)
.Where(x => !string.IsNullOrWhiteSpace(x.Name))
.ToList();
return Response.AsJson(routeDescriptionList);
});
}
}
没错,你没看错,就是这几行代码,就可以帮助我们去生成我们想要的api文档!其实最主要的是IRouteCacheProvider这个接口。
它的具体实现,会在后面的小节讲到,现在先着重于使用!
先调用这个接口的GetCache方法,以拿到缓存的路由信息,这个路由信息有必要来看一下它的定义,因为不看它的定义,我们根本就没有办法继续下去!
后续的查找都是依赖于这些缓存信息!
public interface IRouteCache : IDictionary<Type, List<Tuple<int, RouteDescription>>>, ICollection<KeyValuePair<Type, List<Tuple<int, RouteDescription>>>>, IEnumerable<KeyValuePair<Type, List<Tuple<int, RouteDescription>>>>, IEnumerable
{
bool IsEmpty();
}
看了上面的定义,就可以清楚的知道要用SelectMany去拿到那个元组的内容。再取出元组的RouteDescription。
当然,这个时候我们取到的是所有的路由信息,这些信息都包含了什么内容呢?看看RouteDescription的定义就很清晰了。
public sealed class RouteDescription
{
public RouteDescription(string name, string method, string path, Func<NancyContext, bool> condition);
//The name of the route
public string Name { get; set; }
//The condition that has to be fulfilled inorder for the route to be a valid match.
public Func<NancyContext, bool> Condition { get; }
//The description of what the route is for.
public string Description { get; set; }
//Gets or sets the metadata information for a route.
public RouteMetadata Metadata { get; set; }
//Gets the method of the route.
public string Method { get; }
//Gets the path that the route will be invoked for.
public string Path { get; }
//Gets or set the segments, for the route, that was returned by the Nancy.Routing.IRouteSegmentExtractor.
public IEnumerable<string> Segments { get; set; }
}
在查询之后,我还过滤了那些名字为空的,不让它们显示出来。为什么不显示出来呢?理由也比较简单,像DocModule,我们只定义了一个路由
而且这个路由在严格意义上并不属于我们api的内容,而且这个路由也是没有定义名字的,所以显示出来的意义也不大。
过滤之后,就得到了最终想要的信息!简单起见,这里是先直接 返回一个json对象,便于查看有什么内容,便于在逐步完善后再把它结构化。
下面是最简单实现后的大致效果:
在图中,可以看到GetProductList和GetProductByProductId这两个api的基本信息:请求的method,请求的路径和路由片段。
但是这些信息真的是太少了!连api描述都见不到,拿出来,肯定被人狠狠的骂一顿!!
下面我们要尝试丰富一下我们的接口信息!
丰富一点的实现
要让文档充实,总是需要一个切入点,找到切入点,事情就好办了。仔细观察上面的效果图会发现,里面的metadata是空的。当然这个也就是丰富文档内容的切入点了。
从前面的定义可以看到,这个metadata是一个RouteMetadata的实例
public class RouteMetadata
{
//Creates a new instance of the Nancy.Routing.RouteMetadata class.
public RouteMetadata(IDictionary<Type, object> metadata);
//Gets the raw metadata System.Collections.Generic.IDictionary`2.
public IDictionary<Type, object> Raw { get; }
//Gets a boolean that indicates if the specific type of metadata is stored.
public bool Has<TMetadata>();
//Retrieves metadata of the provided type.
public TMetadata Retrieve<TMetadata>();
}
这里对我们比较重要的是Raw这个属性,因为这个是在返回结果中的一部分,它是一个字典,键是类型,值是这个类型对应的实例。
先定义一个CustomRouteMetadata
,用于返回路由的Metadata信息(可根据具体情况进行相应的定义)。这个CustomRouteMetadata
就是上述字典Type。
public class CustomRouteMetadata
{
// group by the module
public string Group { get; set; }
// description of the api
public string Description { get; set; }
// path of the api
public string Path { get; set; }
// http method of the api
public string Method { get; set; }
// name of the api
public string Name { get; set; }
// segments of the api
public IEnumerable<string> Segments { get; set; }
}
定义好我们要显示的东西后,自然要把这些东西用起来,才能体现它们的价值。
要用起来还涉及到一个MetadataModule,这个命名很像NancyModule,看上去都是一个Module。
先定义一个ProductsMetadataModule
,让它继承MetadataModule<RouteMetadata>
,
具体实现如下:
public class ProductsMetadataModule : MetadataModule<RouteMetadata>
{
public ProductsMetadataModule()
{
Describe["GetProductList"] = desc =>
{
var dic = new Dictionary<System.Type, object>
{
{
typeof(CustomRouteMetadata),
new CustomRouteMetadata
{
Group = "Products",
Description = "Get All Products from Database",
Path = desc.Path,
Method = desc.Method,
Name = desc.Name,
Segments = desc.Segments
}
}
};
return new RouteMetadata(dic);
};
Describe["GetProductByProductId"] = desc =>
{
var dic = new Dictionary<System.Type, object>
{
{
typeof(CustomRouteMetadata),
new CustomRouteMetadata
{
Group = "Products",
Description = "Get a Product by product id",
Path = desc.Path,
Method = desc.Method,
Name = desc.Name,
Segments = desc.Segments
}
}
};
return new RouteMetadata(dic);
};
//省略部分...
}
}
这里的写法就和1.x里写NancyModule的内容是一样的,应该也是比较熟悉的。就不再累赘了。其中的desc是一个委托Func<RouteDescription, TMetadata>
。
默认返回的是一个RouteMetadata实例,而要创建一个这样的实例还需要一个字典,所以大家能看到上面的代码中定义了一个字典。
并且这个字典包含了我们自己定义的信息,其中Group和Description是完全的自定义,其他的是从RouteDescription中拿。
当然,这里已经开了一个口子,想怎么定义都是可以的!
完成上面的代码之后,再来看看我们显示的结果
可以看到我们添加的metadata相关的内容已经出来了!可能这个时候,大家也都发现了,似乎内容有那么点重复的意思!
因为这些重复,就会让人感觉这里比较臃肿,所以我们肯定不需要取出太多重复的东西,目前只需要metadata下面的这些就可以了。
下面来对其进行简化!
简化一点的实现
简化分为两步:
第一步简化:DocModule的简化。
其实,DocModule已经是相当的简单了,但是还能在简洁一点点。这里用到了RetrieveMetadata这个扩展方法来处理。
前面的做法是拿到路由的信息后,用了两个Select来查询,而且查询出来的结果有那么一点臃肿,
而借助扩展方法,可以只取metadata里面的内容,也就是前面自定义的内容,这才是我们真正意义上要用到的。
下面是具体实现的示例:
Get("/", _ =>
{
//01
//var routeDescriptionList = _routeCacheProvider
// .GetCache()
// .SelectMany(x => x.Value)
// .Select(x => x.Item2)
// .Where(x => !string.IsNullOrWhiteSpace(x.Name))
// .ToList();
//return Response.AsJson(routeDescriptionList);
//02
var routeDescriptionList = _routeCacheProvider
.GetCache()
.RetrieveMetadata<RouteMetadata>()
.Where(x => x != null);
return Response.AsJson(routeDescriptionList);
});
经过第一步简化后,已经过滤了不少重复的信息了,效果如下:
第二步简化:Metadata的简化
在返回Metadata的时候,我们是返回了一个默认的RouteMetadata
对象,这个对象相比自定义的CustomRouteMetadata
复杂了不少
而且从上面经过第一步简化后的效果图也可以发现,只有value节点下面的内容才是api文档需要的内容。
所以还要考虑用自定义的这个CustomRouteMetadata去代替原来的。
修改如下:
public class ProductsMetadataModule : MetadataModule<CustomRouteMetadata>
{
public ProductsMetadataModule()
{
Describe["GetProductList"] = desc =>
{
return new CustomRouteMetadata
{
Group = "Products",
Description = "Get All Products from Database",
Path = desc.Path,
Method = desc.Method,
Name = desc.Name,
Segments = desc.Segments
};
};
Describe["GetProductByProductId"] = desc =>
{
return new CustomRouteMetadata
{
Group = "Products",
Description = "Get a Product by product id",
Path = desc.Path,
Method = desc.Method,
Name = desc.Name,
Segments = desc.Segments
};
};
//省略部分..
}
}
由于MetadataModule<TMetadata>
中的TMetadata是自定义的CustomRouteMetadata
,所以在返回的时候直接创建一个简单的实例即可
不需要像RouteMetadata
那样还要定义一个字典。
同时,还要把DocModule
中RetrieveMetadata的TMetadata也要替换成CustomRouteMetadata
var routeDescriptionList = _routeCacheProvider
.GetCache()
//.RetrieveMetadata<RouteMetadata>()
.RetrieveMetadata<CustomRouteMetadata>()
.Where(x => x != null);
经过这两步的简化,现在得到的效果就是我们需要的结果了!
最后,当然要专业一点,不能让人只看json吧!怎么都要添加一个html页面,将这些信息展示出来:
当然,现在看上去还是很丑,文档内容也并不丰富,但是已经把最简单的文档做出来了,想要进一步丰富它就可以自由发挥了。
实现探讨
既然这样简单的代码就能帮助我们去生成api文档,很有必要去研究一下Nancy帮我们做了什么事!
从最开始的IRouteCacheProvider入手,这个接口对应的默认实现DefaultRouteCacheProvider
public class DefaultRouteCacheProvider : IRouteCacheProvider, IDiagnosticsProvider
{
/// <summary>
/// The route cache factory
/// </summary>
protected readonly Func<IRouteCache> RouteCacheFactory;
/// <summary>
/// Initializes a new instance of the DefaultRouteCacheProvider class.
/// </summary>
/// <param name="routeCacheFactory"></param>
public DefaultRouteCacheProvider(Func<IRouteCache> routeCacheFactory)
{
this.RouteCacheFactory = routeCacheFactory;
}
/// <summary>
/// Gets an instance of the route cache.
/// </summary>
/// <returns>An <see cref="IRouteCache"/> instance.</returns>
public IRouteCache GetCache()
{
return this.RouteCacheFactory();
}
//省略部分..
}
里面的GetCache方法是直接调用了定义的委托变量。最终是到了IRouteCache的实现类RouteCache,这个类算是一个重点观察对象!
内容有点多,就只贴出部分核心代码了
它在构造函数里去生成了路由的相关信息。
public RouteCache(
INancyModuleCatalog moduleCatalog,
INancyContextFactory contextFactory,
IRouteSegmentExtractor routeSegmentExtractor,
IRouteDescriptionProvider routeDescriptionProvider,
ICultureService cultureService,
IEnumerable<IRouteMetadataProvider> routeMetadataProviders)
{
this.routeSegmentExtractor = routeSegmentExtractor;
this.routeDescriptionProvider = routeDescriptionProvider;
this.routeMetadataProviders = routeMetadataProviders;
var request = new Request("GET", "/", "http");
using (var context = contextFactory.Create(request))
{
this.BuildCache(moduleCatalog.GetAllModules(context));
}
}
具体的生成方法如下:遍历所有的NancyModule,找到每个Module的RouteDescription集合(一个Module可以包含多个路由)
然后找到每个RouteDescription的描述,路由片段和metadata的信息。最后把这个Module路由信息添加到当前的对象中!
private void BuildCache(IEnumerable<INancyModule> modules)
{
foreach (var module in modules)
{
var moduleType = module.GetType();
var routes =
module.Routes.Select(r => r.Description).ToArray();
foreach (var routeDescription in routes)
{
routeDescription.Description = this.routeDescriptionProvider.GetDescription(module, routeDescription.Path);
routeDescription.Segments = this.routeSegmentExtractor.Extract(routeDescription.Path).ToArray();
routeDescription.Metadata = this.GetRouteMetadata(module, routeDescription);
}
this.AddRoutesToCache(routes, moduleType);
}
}
前面提到RouteDescription的描述,路由片段和metadata的信息都是通过额外的方式拿到的,这里主要是拿metadata来做说明
毕竟在上面最后的一个例子中,用到的是metadata的内容。
先调用定义的私有方法GetRouteMetadata,这个方法里面的内容是不是和前面的MetadataModule有点类似呢,字典和创建RouteMetadata的实例。
private RouteMetadata GetRouteMetadata(INancyModule module, RouteDescription routeDescription)
{
var data = new Dictionary<Type, object>();
foreach (var provider in this.routeMetadataProviders)
{
var type = provider.GetMetadataType(module, routeDescription);
var metadata = provider.GetMetadata(module, routeDescription);
if (type != null && metadata != null)
{
data.Add(type, metadata);
}
}
return new RouteMetadata(data);
}
重点的是provider。这个provider来源来IRouteMetadataProvider,这个接口就两个方法。
Nancy这个项目中还有一个抽象类是继承了这个接口的。但是这个抽象类是没有默认实现的。
public abstract class RouteMetadataProvider<TMetadata> : IRouteMetadataProvider
{
public Type GetMetadataType(INancyModule module, RouteDescription routeDescription)
{
return typeof(TMetadata);
}
public object GetMetadata(INancyModule module, RouteDescription routeDescription)
{
return this.GetRouteMetadata(module, routeDescription);
}
protected abstract TMetadata GetRouteMetadata(INancyModule module, RouteDescription routeDescription);
}
注:前面的原理分析都是基于Nancy这个项目。
这个时候,另外一个项目Nancy.Metadata.Modules就起作用了。我们编写的MetadataModule也是要添加这个的引用才能正常使用的。
从上面编写的MetadataModule可以看出这个项目的起点应该是MetadataModule,而且有关metadata的核心也在这里了。
public abstract class MetadataModule<TMetadata> : IMetadataModule where TMetadata : class
{
private readonly IDictionary<string, Func<RouteDescription, TMetadata>> metadata;
protected MetadataModule()
{
this.metadata = new Dictionary<string, Func<RouteDescription, TMetadata>>();
}
// Gets <see cref="RouteMetadataBuilder"/> for describing routes.
public RouteMetadataBuilder Describe
{
get { return new RouteMetadataBuilder(this); }
}
// Returns metadata for the given RouteDescription.
public object GetMetadata(RouteDescription description)
{
if (this.metadata.ContainsKey(description.Name))
{
return this.metadata[description.Name].Invoke(description);
}
return null;
}
// Helper class for configuring a route metadata handler in a module.
public class RouteMetadataBuilder
{
private readonly MetadataModule<TMetadata> parentModule;
public RouteMetadataBuilder(MetadataModule<TMetadata> metadataModule)
{
this.parentModule = metadataModule;
}
// Describes metadata for a route with the specified name.
public Func<RouteDescription, TMetadata> this[string name]
{
set { this.AddRouteMetadata(name, value); }
}
protected void AddRouteMetadata(string name, Func<RouteDescription, TMetadata> value)
{
this.parentModule.metadata.Add(name, value);
}
}
//省略部分..
}
到这里,已经将GetCache的内内外外都简单分析了一下。至于扩展方法RetrieveMetadata就不在细说了,只是selectmany和select的一层封装。
写在最后
本文粗略讲解了如何在Nancy中生成API文档,以及简单分析了其内部的处理。
下一篇将继续介绍这一块的内容,不过主角是Swagger。
浅析如何在Nancy中生成API文档的更多相关文章
- ASP.NET Core 3.0 WebApi中使用Swagger生成API文档简介
参考地址,官网:https://docs.microsoft.com/zh-cn/aspnet/core/tutorials/getting-started-with-swashbuckle?view ...
- Django restful framework中自动生成API文档
自动生成api文档(不管是函数视图还是类视图都能显示) 1.安装rest_framework_swagger库 pip install django-rest-swagger 2.在项目下的 urls ...
- 【转】Django restful framework中自动生成API文档
转自 https://www.cnblogs.com/sui776265233/p/11350434.html 自动生成api文档(不管是函数视图还是类视图都能显示) 1.安装rest_framewo ...
- 如何在idea中将项目生成API文档(超详细)(Day_32)
1.打开要生成API文档的项目,点击菜单栏中的Tools工具,选择Generate JavaDoc 2.打开如下所示的Specify Generate JavaDoc Scope 界面 3.解释下Ot ...
- eclipse中javadoc给项目生成api文档
步骤 1.打开java代码,编写JavaDoc 注释,只有按照java的规范编写注释,才能很好的生成API文档,javadoc注释与普通注释的区别为多一个*(星号).普通代码注释为/*XXX*/,而j ...
- .Net魔法堂:提取注释生成API文档
一.前言 在多人协作的项目中,除了良好的代码规范外,完整的API文档也相当重要.通过文档我们快速了解系统各模块的实际接口,及其使用场景.使用示例,一定程度上降低沟通成本,和减少后期维护中知识遗失等风险 ...
- 使用bee自动生成api文档
beego中的bee工具可以方便的自动生成api文档,基于数据库字段,自动生成golang版基于beego的crud代码,方法如下: 1.进入到gopath目录的src下执行命令: bee api a ...
- 试试使用 eolinker 扫描 GitLab 代码注释自动生成 API 文档?
前言: 一般写完代码之后,还要将各类参数注解写入API文档,方便后续进行对接和测试,这个过程通常都很麻烦,如果有工具可以读取代码注释直接生成API文档的话,那会十分方便. 此前一直都是在使用eolin ...
- 12 Django Rest Swagger生成api文档
01-简介 Swagger:是一个规范和完整的框架,用于生成.描述.调用和可视化RESTful风格的Web服务.总体目标是使客户端和文件系统源代码作为服务器以同样的速度来更新.当接口有变动时,对应的接 ...
随机推荐
- CoreAnimation 目录
CoreAnimation 目录 CoreAnimation 开篇 CoreAnimation 寄宿图 CoreAnimation 图层几何学 CoreAnimation 视觉效果
- JNDI常见配置方式
JNDI(Java Naming and Directory Interface,Java命名和目录接口)是一组在Java应用中访问命名和目录服务的API.命名服务将名称和对象联系起来,使得我们可以用 ...
- JAVA基础知识(2)--关键字static的使用
在Java类中声明属性.方法和内部类时,可使用关键字static作为修饰符,static标记的属性和方法可以由整个类进行共享,因此static修饰的属性称为类成员或者称为类方法:static修饰的方法 ...
- apply()
apply() 1.apply和call的区别在哪里 2.什么情况下用apply,什么情况下用call 3.apply的其他巧妙用法(一般在什么情况下可以使用apply) apply:方法能劫持另外一 ...
- Build your own linino system 编译你自己的linino系统
懒癌犯了,先简单写过程,之后有时间再补上每一步的理由吧.对着来一遍,有bug请留言,我会尝试回答.(づ ̄ 3 ̄)づ ------------------------------------------ ...
- JS-DOM . 01 简单了解DOM
DOM概述 html加载完毕,渲染引擎会在内存中把html文档生成一个DOM树,getElementById是获取内DOM上的元素,然后操作的时候修改的是该元素的属性 体验事件/事件三要素1.事件源( ...
- 数据库DQL操作(重点)
*****DQL -- 数据查询语言 查询不会修改数据库表记录!一. 基本查询//emp表名1. 字段(列)控制1) 查询所有列 SELECT * FROM 表名; SELECT * FROM em ...
- 任务一:零基础HTML编码练习
任务目的 了解HTML的定义.概念.发展简史 掌握常用HTML标签的含义.用法 能够基于设计稿来合理规划HTML文档结构 理解语义化,合理地使用HTML标签来构建页面 任务描述:完成一个HTML页面代 ...
- 手机自动化测试:appium源码分析之bootstrap十四
手机自动化测试:appium源码分析之bootstrap十四 poptest(www.poptest.cn)是国内唯一一家培养测试开发工程师的培训机构,以学员能胜任自动化测试,性能测试,测试工具开 ...
- Vue.js 学习笔记 一
本文的Demo和源代码已放到GitHub,如果您觉得本篇内容不错,请点个赞,或在GitHub上加个星星! https://github.com/zwl-jasmine95/Vue_test 以下所有知 ...