Authored by Gino Zhang

搜索API测试和markdown说明生成

package com.yoho.search.service.restapi;
import org.junit.Assert;
import org.yaml.snakeyaml.Yaml;
import java.io.FileInputStream;
import java.net.URL;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* Created by ginozhang on 2016/11/17.
*/
public class ApiDefUtils {
private static Map<String, Map<String, Object>> apiDefs = new HashMap<>();
public static Map<String, Object> getApiDef(String api) {
if(apiDefs.containsKey(api))
{
return apiDefs.get(api);
}
synchronized (ApiDefUtils.class) {
if(apiDefs.containsKey(api))
{
return apiDefs.get(api);
}
String fileName = api.substring(1).replace("/", "_");
Yaml yaml = new Yaml();
URL ymlUrl = SearchNewControllerTest.class.getClassLoader().getResource("api_def/" + fileName + ".yml");
Map<String, Object> def = null;
try {
def = (Map<String, Object>) yaml.load(new FileInputStream(ymlUrl.getFile()));
} catch (Exception e) {
throw new RuntimeException("cannot find file " + fileName, e);
}
Assert.assertNotNull(fileName, def);
apiDefs.put(api, def);
return def;
}
}
public static List<String> getEnumItems(String api, String enumKey)
{
List<String> enums = ((Map<String, List<String>>)getApiDef(api).get("_enums")).get(enumKey);
return enums;
}
}
... ...
... ... @@ -3,10 +3,8 @@ package com.yoho.search.service.restapi;
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import org.junit.Assert;
import org.yaml.snakeyaml.Yaml;
import java.io.FileInputStream;
import java.net.URL;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.Set;
... ... @@ -16,28 +14,19 @@ import java.util.Set;
*/
public class CheckStructureUtils {
public static void check(String fileName, JSONObject response) {
Yaml yaml = new Yaml();
URL ymlUrl = SearchNewControllerTest.class.getClassLoader().getResource("api_def/" + fileName + ".yml");
Map<String, Object> def = null;
try {
def = (Map<String, Object>) yaml.load(new FileInputStream(ymlUrl.getFile()));
} catch (Exception e) {
throw new RuntimeException("cannot find file " + fileName, e);
}
public static void checkRequest(String api, String url)
{
// TODO:
}
Assert.assertNotNull(fileName, def);
public static void check(Map<String, Object> apiDef, JSONObject response) {
Assert.assertTrue("The response should contain data.", response.containsKey("data") && response.get("data") != null);
// check for repeat
Map<String, Object> responseDef = (Map<String, Object>) def.get("_response");
System.out.println(def);
Map<String, List<String>> enums = (Map<String, List<String>>) def.get("_enums");
Map<String, Object> responseDef = (Map<String, Object>) apiDef.get("_response");
Map<String, List<String>> enums = (Map<String, List<String>>) apiDef.get("_enums");
doCheck(responseDef, response, enums);
}
private static void doCheck(Map<String, Object> responseDef, JSONObject response, Map<String, List<String>> enums) {
if (responseDef == null || response.isEmpty()) {
return;
}
... ... @@ -98,6 +87,13 @@ public class CheckStructureUtils {
Assert.assertTrue("cannot find enum "+enumKey, enums.containsKey(enumKey));
Assert.assertTrue("Invalid value "+sValue+" for enum "+enumKey, enums.get(enumKey).contains(sValue));
}
else if(type.startsWith("connected_enum_"))
{
// 校验枚举类型
String enumKey = type.substring("connected_enum_".length());
Assert.assertTrue("cannot find enum "+enumKey, enums.containsKey(enumKey));
Assert.assertTrue("Invalid value "+sValue+" for enum "+enumKey, enums.get(enumKey).containsAll(Arrays.asList(sValue.split(","))));
}
else if(!"string".equalsIgnoreCase(type))
{
Assert.fail("Unknown property type "+type);
... ...
package com.yoho.search.service.restapi;
import com.yoho.search.base.utils.FileUtils;
import org.apache.commons.lang3.StringUtils;
import java.util.HashMap;
import java.util.Map;
/**
* Created by ginozhang on 2016/11/17.
*/
public class MarkDownUtils {
private final String SEPARATOR = System.getProperty("line.separator");
private Map<String, String> typeDescptions = new HashMap<>();
private String api;
private MarkDownUtils(String api) {
this.api = api;
}
public static MarkDownUtils getInstance(String api) {
return new MarkDownUtils(api);
}
{
typeDescptions.put("connected_int", "int多值参数类型,多个值用逗号分隔。");
typeDescptions.put("ranged_double", "double范围类型,使用逗号分隔下限和上限");
typeDescptions.put("ranged_int", "int范围类型,使用逗号分隔下限和上限");
}
/**
* 根据接口描述文件生成接口描述的MD文件
*/
public void generateApiMd() {
Map<String, Object> apiDef = ApiDefUtils.getApiDef(api);
StringBuffer sb = new StringBuffer(500);
sb.append("# ").append(api).append(" 接口说明").append(SEPARATOR);
// 1. 生成header 秒杀接口的功能和使用场景
sb.append(apiDef.get("_name")).append(SEPARATOR);
// 2. 生成请求参数说明
sb.append(getRequestMdTable(apiDef));
// 3. 生成响应参数说明
sb.append(generateResponseMdTable(apiDef));
FileUtils.writeFile("test.md", sb.toString());
}
private StringBuffer generateResponseMdTable(Map<String, Object> apiDef) {
return generateMdTable4SingleObject(api + " 响应参数说明", (Map<String, Object>) apiDef.get("_response"), 2);
}
private StringBuffer generateMdTable4SingleObject(String tableTitle, Map<String, Object> respnseDef, int level) {
StringBuffer sb = new StringBuffer(100);
sb.append(getLevelChars(level)).append(tableTitle).append(SEPARATOR);
sb.append("|参数名 |参数类型 |参数说明 |").append(SEPARATOR);
sb.append("|------|--------|---------|").append(SEPARATOR);
for (Map.Entry<String, Object> entry : respnseDef.entrySet()) {
String paramName = entry.getKey();
Map<String, Object> details = ((Map<String, Object>) entry.getValue());
String type = getTypeDescription((String) details.get("_type"));
String desc = (String) details.get("_desc");
sb.append("|").append(paramName).append("|").append(type).append("|").append(desc).append("|").append(SEPARATOR);
if ("object".equals(type) || "array".equals(type)) {
// 如果是数组或者对象类型 就新搞一个表格来说明里面的对象
sb.append(SEPARATOR).append(SEPARATOR);
sb.append(generateMdTable4SingleObject("参数" + paramName + "的属性说明", (Map<String, Object>) details.get("_content"), level + 1));
}
}
sb.append(SEPARATOR).append(SEPARATOR);
return sb;
}
private StringBuffer getRequestMdTable(Map<String, Object> apiDef) {
StringBuffer sb = new StringBuffer(100);
sb.append("## ").append(api).append(" 请求参数说明").append(SEPARATOR);
sb.append("|参数名 |参数类型 |参数说明 |").append(SEPARATOR);
sb.append("|------|--------|---------|").append(SEPARATOR);
Map<String, Object> requestDef = (Map<String, Object>) apiDef.get("_request");
for (Map.Entry<String, Object> entry : requestDef.entrySet()) {
String paramName = entry.getKey();
Map<String, String> details = ((Map<String, String>) entry.getValue());
String type = getTypeDescription(details.get("_type"));
String desc = details.get("_desc");
sb.append("|").append(paramName).append("|").append(type).append("|").append(desc).append("|").append(SEPARATOR);
}
sb.append(SEPARATOR).append(SEPARATOR);
return sb;
}
private StringBuffer getLevelChars(int level) {
StringBuffer sb = new StringBuffer(10);
level = level <= 3 ? level : 3;
for (int i = 1; i <= level; i++) {
sb.append("#");
}
sb.append(" ");
return sb;
}
private String getTypeDescription(String type) {
if (StringUtils.isEmpty(type)) {
type = "string";
}
if (typeDescptions.containsKey(type)) {
return typeDescptions.get(type);
}
if (type.startsWith("enum_")) {
String enumKey = type.substring("enum_".length());
return "枚举类型,可选范围:" + ApiDefUtils.getEnumItems(api, enumKey);
} else if (type.startsWith("connected_enum_")) {
String enumKey = type.substring("connected_enum_".length());
return "多值枚举类型,枚举可选范围:" + ApiDefUtils.getEnumItems(api, enumKey) + ", 多个值用逗号分隔。";
} else if (type.startsWith("optional")) {
String specialValue = type.substring("optional(".length(), "optional(".length() + 1);
return "特定值{" + specialValue + "}或不传值";
}
StringBuffer sb = new StringBuffer(type);
return sb.toString();
}
}
... ...
... ... @@ -75,6 +75,8 @@ public class SearchNewControllerTest {
if (appendParams.containsKey(api)) {
url = url + "?" + appendParams.get(api);
}
CheckStructureUtils.checkRequest(api, url);
String jsonResult = HttpClientUtils.getMethod(url);
System.out.println("Request url is: " + url + ". jsonResult: \n" + jsonResult);
JSONObject jsonObject = JSON.parseObject(jsonResult);
... ... @@ -83,10 +85,10 @@ public class SearchNewControllerTest {
Assert.assertEquals(api, "200", jsonObject.getString("code"));
// 检查响应结果是否OK
String fileName = api.substring(1).replace("/", "_");
CheckStructureUtils.check(fileName, jsonObject);
Map<String, Object> def = ApiDefUtils.getApiDef(api);
CheckStructureUtils.check(def, jsonObject);
// 生成markdown接口说明文件
MarkDownUtils.getInstance(api).generateApiMd();
}
}
... ...
... ... @@ -4,15 +4,153 @@
# _desc: 字段描述
# _required: 字段是否必须,默认为false
# _type: 字段类型,可选为int、long、string、double、object、array、enum_<ENUM_NAME>,默认为string
_name: 商品列表搜索接口。
_enums:
YesOrNo: ['Y','N']
Status: ['0','1']
Gender: ['1','2','3']
Outlet: ['0','1', '2']
AgeLevel: ['1','2','3']
Outlet: ['1', '2']
VIPDiscountType: ['0', '1', '2']
Attribute: ['1', '2']
AppType: ['0', '1']
SellChannel: ['0', '1', '2']
_request:
query:
_desc: '搜索关键词'
_type: 'string'
viewNum:
_desc: '分页每页显示商品数量'
_type: 'int'
page:
_desc: '分页页数'
_type: 'int'
product_skn:
_desc: '指定的商品SKN列表'
_type: 'connected_int'
brand:
_desc: '指定的品牌列表'
_type: 'connected_int'
shop:
_desc: '指定的店铺列表'
_type: 'connected_int'
msort:
_desc: '指定的大分类列表'
_type: 'connected_int'
misort:
_desc: '指定的中分类列表'
_type: 'connected_int'
sort:
_desc: '指定的小分类列表'
_type: 'connected_int'
color:
_desc: '指定的颜色列表'
_type: 'connected_int'
style:
_desc: '指定的风格列表'
_type: 'connected_int'
size:
_desc: '指定的尺寸列表'
_type: 'connected_int'
gender:
_desc: '性别(1:男,2:女,3:通用)'
_type: 'connected_enum_Gender'
ageLevel:
_desc: '年龄段(1成人 2大童 3小童)'
_type: 'connected_enum_AgeLevel'
price:
_desc: '价格区间'
_type: 'ranged_int'
specialoffer:
_desc: '是否为促销品[5折以下]'
_type: 'enum_YesOrNo'
isdiscount:
_desc: '是否打折'
_type: 'enum_YesOrNo'
vdt:
_desc: 'VIP折扣类型'
_type: 'enum_VIPDiscountType'
p_d:
_desc: '折扣范围,浮点型,如【p_d_int=0.1,0.3】'
_type: 'ranged_double'
p_d_int:
_desc: '折扣范围,整形,如【p_d_int=1,3】'
_type: 'ranged_int'
isStudentPrice:
_desc: '是否有学生价优惠'
_type: 'enum_YesOrNo'
isStudentRebate:
_desc: '是否有学生返币'
_type: 'enum_YesOrNo'
isInstalment:
_desc: '是否分期'
_type: 'enum_Status'
sales:
_desc: '是否在售'
_type: 'enum_Status'
promotion:
_desc: '是否促销/推广商品 TODO'
_type: 'int'
attribute:
_desc: '包含指定的商品属性(1:正常商品,2:赠品)'
_type: 'enum_Attribute'
attribute:
_desc: '过滤指定的商品属性(1:正常商品,2:赠品)'
_type: 'enum_Attribute'
limited:
_desc: '是否限量商品'
_type: 'enum_YesOrNo'
new:
_desc: '是否新品'
_type: 'enum_YesOrNo'
outlets:
_desc: '是否奥莱商品'
_type: 'enum_Outlet'
status:
_desc: '是否上架'
_type: 'enum_Status'
breaking:
_desc: '传入1只查询断码商品,不传值或其他值该参数不起作用'
_type: 'optional(1)'
app_type:
_desc: 'APP类型'
_type: 'connected_enum_AppType'
sell_channels:
_desc: '销售平台 (网站、APP、现场 TODO)'
_type: 'enum_SellChannel'
stocknumber:
_desc: '商品库存,当传入0时查询库存为0的商品,当传入大于0的数值时,查询库存不小于改数值的商品'
_type: 'int'
folder_id:
_desc: '商品目录'
_type: 'int'
series_id:
_desc: '商品系列'
_type: 'int'
first_shelve_time:
_desc: '首次上架时间间隔'
_type: 'ranged_double'
shelve_time:
_desc: '最近上架日期间隔'
_type: 'ranged_double'
day:
_desc: '上架时间'
_type: 'date(yyyy-MM-dd)'
contain_global:
_desc: '传入Y查询结果包含全球购商品,不传值或其他值该参数不起作用'
_type: 'optional(Y)'
contain_seckill:
_desc: '传入Y查询结果包含秒杀商品,不传值或其他值该参数不起作用'
_type: 'optional(Y)'
act_temp:
_desc: '活动属性-模板id'
_type: 'int'
act_rec:
_desc: '活动属性-是否推荐'
_type: 'enum_Status'
act_status:
_desc: '活动属性-状态'
_type: 'enum_Status'
_response:
code:
_desc: '响应状态码,200为成功,其他为失败'
... ... @@ -159,10 +297,11 @@ _response:
_desc: '是否是奥特莱斯商品'
_type: 'enum_Outlet'
gender:
_desc: '性别'
_desc: '性别(1:男,2:女,3:通用)'
_type: 'enum_Gender'
age_level:
_desc: '商品适合的年龄层'
_desc: '年龄段(1成人 2大童 3小童)'
_type: 'connected_enum_AgeLevel'
default_images:
_desc: '默认的商品图片'
yohood_id:
... ...