Authored by Gino Zhang

搜索API测试和markdown说明生成

  1 +package com.yoho.search.service.restapi;
  2 +
  3 +import org.junit.Assert;
  4 +import org.yaml.snakeyaml.Yaml;
  5 +
  6 +import java.io.FileInputStream;
  7 +import java.net.URL;
  8 +import java.util.HashMap;
  9 +import java.util.List;
  10 +import java.util.Map;
  11 +
  12 +/**
  13 + * Created by ginozhang on 2016/11/17.
  14 + */
  15 +public class ApiDefUtils {
  16 +
  17 + private static Map<String, Map<String, Object>> apiDefs = new HashMap<>();
  18 +
  19 + public static Map<String, Object> getApiDef(String api) {
  20 +
  21 + if(apiDefs.containsKey(api))
  22 + {
  23 + return apiDefs.get(api);
  24 + }
  25 +
  26 + synchronized (ApiDefUtils.class) {
  27 + if(apiDefs.containsKey(api))
  28 + {
  29 + return apiDefs.get(api);
  30 + }
  31 +
  32 + String fileName = api.substring(1).replace("/", "_");
  33 + Yaml yaml = new Yaml();
  34 + URL ymlUrl = SearchNewControllerTest.class.getClassLoader().getResource("api_def/" + fileName + ".yml");
  35 + Map<String, Object> def = null;
  36 + try {
  37 + def = (Map<String, Object>) yaml.load(new FileInputStream(ymlUrl.getFile()));
  38 + } catch (Exception e) {
  39 + throw new RuntimeException("cannot find file " + fileName, e);
  40 + }
  41 +
  42 + Assert.assertNotNull(fileName, def);
  43 + apiDefs.put(api, def);
  44 + return def;
  45 + }
  46 + }
  47 +
  48 + public static List<String> getEnumItems(String api, String enumKey)
  49 + {
  50 + List<String> enums = ((Map<String, List<String>>)getApiDef(api).get("_enums")).get(enumKey);
  51 + return enums;
  52 + }
  53 +
  54 +}
@@ -3,10 +3,8 @@ package com.yoho.search.service.restapi; @@ -3,10 +3,8 @@ package com.yoho.search.service.restapi;
3 import com.alibaba.fastjson.JSONArray; 3 import com.alibaba.fastjson.JSONArray;
4 import com.alibaba.fastjson.JSONObject; 4 import com.alibaba.fastjson.JSONObject;
5 import org.junit.Assert; 5 import org.junit.Assert;
6 -import org.yaml.snakeyaml.Yaml;  
7 6
8 -import java.io.FileInputStream;  
9 -import java.net.URL; 7 +import java.util.Arrays;
10 import java.util.List; 8 import java.util.List;
11 import java.util.Map; 9 import java.util.Map;
12 import java.util.Set; 10 import java.util.Set;
@@ -16,28 +14,19 @@ import java.util.Set; @@ -16,28 +14,19 @@ import java.util.Set;
16 */ 14 */
17 public class CheckStructureUtils { 15 public class CheckStructureUtils {
18 16
19 - public static void check(String fileName, JSONObject response) {  
20 - Yaml yaml = new Yaml();  
21 - URL ymlUrl = SearchNewControllerTest.class.getClassLoader().getResource("api_def/" + fileName + ".yml");  
22 - Map<String, Object> def = null;  
23 - try {  
24 - def = (Map<String, Object>) yaml.load(new FileInputStream(ymlUrl.getFile()));  
25 - } catch (Exception e) {  
26 - throw new RuntimeException("cannot find file " + fileName, e);  
27 - } 17 + public static void checkRequest(String api, String url)
  18 + {
  19 + // TODO:
  20 + }
28 21
29 - Assert.assertNotNull(fileName, def); 22 + public static void check(Map<String, Object> apiDef, JSONObject response) {
30 Assert.assertTrue("The response should contain data.", response.containsKey("data") && response.get("data") != null); 23 Assert.assertTrue("The response should contain data.", response.containsKey("data") && response.get("data") != null);
31 -  
32 - // check for repeat  
33 - Map<String, Object> responseDef = (Map<String, Object>) def.get("_response");  
34 - System.out.println(def);  
35 - Map<String, List<String>> enums = (Map<String, List<String>>) def.get("_enums"); 24 + Map<String, Object> responseDef = (Map<String, Object>) apiDef.get("_response");
  25 + Map<String, List<String>> enums = (Map<String, List<String>>) apiDef.get("_enums");
36 doCheck(responseDef, response, enums); 26 doCheck(responseDef, response, enums);
37 } 27 }
38 28
39 private static void doCheck(Map<String, Object> responseDef, JSONObject response, Map<String, List<String>> enums) { 29 private static void doCheck(Map<String, Object> responseDef, JSONObject response, Map<String, List<String>> enums) {
40 -  
41 if (responseDef == null || response.isEmpty()) { 30 if (responseDef == null || response.isEmpty()) {
42 return; 31 return;
43 } 32 }
@@ -98,6 +87,13 @@ public class CheckStructureUtils { @@ -98,6 +87,13 @@ public class CheckStructureUtils {
98 Assert.assertTrue("cannot find enum "+enumKey, enums.containsKey(enumKey)); 87 Assert.assertTrue("cannot find enum "+enumKey, enums.containsKey(enumKey));
99 Assert.assertTrue("Invalid value "+sValue+" for enum "+enumKey, enums.get(enumKey).contains(sValue)); 88 Assert.assertTrue("Invalid value "+sValue+" for enum "+enumKey, enums.get(enumKey).contains(sValue));
100 } 89 }
  90 + else if(type.startsWith("connected_enum_"))
  91 + {
  92 + // 校验枚举类型
  93 + String enumKey = type.substring("connected_enum_".length());
  94 + Assert.assertTrue("cannot find enum "+enumKey, enums.containsKey(enumKey));
  95 + Assert.assertTrue("Invalid value "+sValue+" for enum "+enumKey, enums.get(enumKey).containsAll(Arrays.asList(sValue.split(","))));
  96 + }
101 else if(!"string".equalsIgnoreCase(type)) 97 else if(!"string".equalsIgnoreCase(type))
102 { 98 {
103 Assert.fail("Unknown property type "+type); 99 Assert.fail("Unknown property type "+type);
  1 +package com.yoho.search.service.restapi;
  2 +
  3 +import com.yoho.search.base.utils.FileUtils;
  4 +import org.apache.commons.lang3.StringUtils;
  5 +
  6 +import java.util.HashMap;
  7 +import java.util.Map;
  8 +
  9 +/**
  10 + * Created by ginozhang on 2016/11/17.
  11 + */
  12 +public class MarkDownUtils {
  13 +
  14 + private final String SEPARATOR = System.getProperty("line.separator");
  15 +
  16 + private Map<String, String> typeDescptions = new HashMap<>();
  17 +
  18 + private String api;
  19 +
  20 + private MarkDownUtils(String api) {
  21 + this.api = api;
  22 + }
  23 +
  24 + public static MarkDownUtils getInstance(String api) {
  25 + return new MarkDownUtils(api);
  26 + }
  27 +
  28 + {
  29 + typeDescptions.put("connected_int", "int多值参数类型,多个值用逗号分隔。");
  30 + typeDescptions.put("ranged_double", "double范围类型,使用逗号分隔下限和上限");
  31 + typeDescptions.put("ranged_int", "int范围类型,使用逗号分隔下限和上限");
  32 + }
  33 +
  34 +
  35 + /**
  36 + * 根据接口描述文件生成接口描述的MD文件
  37 + */
  38 + public void generateApiMd() {
  39 + Map<String, Object> apiDef = ApiDefUtils.getApiDef(api);
  40 + StringBuffer sb = new StringBuffer(500);
  41 + sb.append("# ").append(api).append(" 接口说明").append(SEPARATOR);
  42 +
  43 + // 1. 生成header 秒杀接口的功能和使用场景
  44 + sb.append(apiDef.get("_name")).append(SEPARATOR);
  45 +
  46 + // 2. 生成请求参数说明
  47 + sb.append(getRequestMdTable(apiDef));
  48 +
  49 + // 3. 生成响应参数说明
  50 + sb.append(generateResponseMdTable(apiDef));
  51 +
  52 + FileUtils.writeFile("test.md", sb.toString());
  53 + }
  54 +
  55 + private StringBuffer generateResponseMdTable(Map<String, Object> apiDef) {
  56 + return generateMdTable4SingleObject(api + " 响应参数说明", (Map<String, Object>) apiDef.get("_response"), 2);
  57 + }
  58 +
  59 + private StringBuffer generateMdTable4SingleObject(String tableTitle, Map<String, Object> respnseDef, int level) {
  60 + StringBuffer sb = new StringBuffer(100);
  61 + sb.append(getLevelChars(level)).append(tableTitle).append(SEPARATOR);
  62 + sb.append("|参数名 |参数类型 |参数说明 |").append(SEPARATOR);
  63 + sb.append("|------|--------|---------|").append(SEPARATOR);
  64 + for (Map.Entry<String, Object> entry : respnseDef.entrySet()) {
  65 + String paramName = entry.getKey();
  66 + Map<String, Object> details = ((Map<String, Object>) entry.getValue());
  67 + String type = getTypeDescription((String) details.get("_type"));
  68 + String desc = (String) details.get("_desc");
  69 + sb.append("|").append(paramName).append("|").append(type).append("|").append(desc).append("|").append(SEPARATOR);
  70 +
  71 + if ("object".equals(type) || "array".equals(type)) {
  72 + // 如果是数组或者对象类型 就新搞一个表格来说明里面的对象
  73 + sb.append(SEPARATOR).append(SEPARATOR);
  74 + sb.append(generateMdTable4SingleObject("参数" + paramName + "的属性说明", (Map<String, Object>) details.get("_content"), level + 1));
  75 + }
  76 +
  77 + }
  78 + sb.append(SEPARATOR).append(SEPARATOR);
  79 + return sb;
  80 + }
  81 +
  82 + private StringBuffer getRequestMdTable(Map<String, Object> apiDef) {
  83 + StringBuffer sb = new StringBuffer(100);
  84 + sb.append("## ").append(api).append(" 请求参数说明").append(SEPARATOR);
  85 + sb.append("|参数名 |参数类型 |参数说明 |").append(SEPARATOR);
  86 + sb.append("|------|--------|---------|").append(SEPARATOR);
  87 +
  88 + Map<String, Object> requestDef = (Map<String, Object>) apiDef.get("_request");
  89 + for (Map.Entry<String, Object> entry : requestDef.entrySet()) {
  90 + String paramName = entry.getKey();
  91 + Map<String, String> details = ((Map<String, String>) entry.getValue());
  92 + String type = getTypeDescription(details.get("_type"));
  93 + String desc = details.get("_desc");
  94 + sb.append("|").append(paramName).append("|").append(type).append("|").append(desc).append("|").append(SEPARATOR);
  95 + }
  96 + sb.append(SEPARATOR).append(SEPARATOR);
  97 + return sb;
  98 + }
  99 +
  100 + private StringBuffer getLevelChars(int level) {
  101 + StringBuffer sb = new StringBuffer(10);
  102 + level = level <= 3 ? level : 3;
  103 + for (int i = 1; i <= level; i++) {
  104 + sb.append("#");
  105 + }
  106 +
  107 + sb.append(" ");
  108 + return sb;
  109 + }
  110 +
  111 + private String getTypeDescription(String type) {
  112 +
  113 + if (StringUtils.isEmpty(type)) {
  114 + type = "string";
  115 + }
  116 +
  117 + if (typeDescptions.containsKey(type)) {
  118 + return typeDescptions.get(type);
  119 + }
  120 +
  121 + if (type.startsWith("enum_")) {
  122 + String enumKey = type.substring("enum_".length());
  123 + return "枚举类型,可选范围:" + ApiDefUtils.getEnumItems(api, enumKey);
  124 + } else if (type.startsWith("connected_enum_")) {
  125 + String enumKey = type.substring("connected_enum_".length());
  126 + return "多值枚举类型,枚举可选范围:" + ApiDefUtils.getEnumItems(api, enumKey) + ", 多个值用逗号分隔。";
  127 + } else if (type.startsWith("optional")) {
  128 + String specialValue = type.substring("optional(".length(), "optional(".length() + 1);
  129 + return "特定值{" + specialValue + "}或不传值";
  130 + }
  131 +
  132 + StringBuffer sb = new StringBuffer(type);
  133 + return sb.toString();
  134 + }
  135 +
  136 +}
@@ -75,6 +75,8 @@ public class SearchNewControllerTest { @@ -75,6 +75,8 @@ public class SearchNewControllerTest {
75 if (appendParams.containsKey(api)) { 75 if (appendParams.containsKey(api)) {
76 url = url + "?" + appendParams.get(api); 76 url = url + "?" + appendParams.get(api);
77 } 77 }
  78 +
  79 + CheckStructureUtils.checkRequest(api, url);
78 String jsonResult = HttpClientUtils.getMethod(url); 80 String jsonResult = HttpClientUtils.getMethod(url);
79 System.out.println("Request url is: " + url + ". jsonResult: \n" + jsonResult); 81 System.out.println("Request url is: " + url + ". jsonResult: \n" + jsonResult);
80 JSONObject jsonObject = JSON.parseObject(jsonResult); 82 JSONObject jsonObject = JSON.parseObject(jsonResult);
@@ -83,10 +85,10 @@ public class SearchNewControllerTest { @@ -83,10 +85,10 @@ public class SearchNewControllerTest {
83 Assert.assertEquals(api, "200", jsonObject.getString("code")); 85 Assert.assertEquals(api, "200", jsonObject.getString("code"));
84 86
85 // 检查响应结果是否OK 87 // 检查响应结果是否OK
86 - String fileName = api.substring(1).replace("/", "_");  
87 - CheckStructureUtils.check(fileName, jsonObject); 88 + Map<String, Object> def = ApiDefUtils.getApiDef(api);
  89 + CheckStructureUtils.check(def, jsonObject);
88 90
89 // 生成markdown接口说明文件 91 // 生成markdown接口说明文件
90 - 92 + MarkDownUtils.getInstance(api).generateApiMd();
91 } 93 }
92 } 94 }
@@ -4,15 +4,153 @@ @@ -4,15 +4,153 @@
4 # _desc: 字段描述 4 # _desc: 字段描述
5 # _required: 字段是否必须,默认为false 5 # _required: 字段是否必须,默认为false
6 # _type: 字段类型,可选为int、long、string、double、object、array、enum_<ENUM_NAME>,默认为string 6 # _type: 字段类型,可选为int、long、string、double、object、array、enum_<ENUM_NAME>,默认为string
  7 +_name: 商品列表搜索接口。
7 _enums: 8 _enums:
8 YesOrNo: ['Y','N'] 9 YesOrNo: ['Y','N']
9 Status: ['0','1'] 10 Status: ['0','1']
10 Gender: ['1','2','3'] 11 Gender: ['1','2','3']
11 - Outlet: ['0','1', '2'] 12 + AgeLevel: ['1','2','3']
  13 + Outlet: ['1', '2']
  14 + VIPDiscountType: ['0', '1', '2']
  15 + Attribute: ['1', '2']
  16 + AppType: ['0', '1']
  17 + SellChannel: ['0', '1', '2']
12 _request: 18 _request:
13 query: 19 query:
14 _desc: '搜索关键词' 20 _desc: '搜索关键词'
15 _type: 'string' 21 _type: 'string'
  22 + viewNum:
  23 + _desc: '分页每页显示商品数量'
  24 + _type: 'int'
  25 + page:
  26 + _desc: '分页页数'
  27 + _type: 'int'
  28 + product_skn:
  29 + _desc: '指定的商品SKN列表'
  30 + _type: 'connected_int'
  31 + brand:
  32 + _desc: '指定的品牌列表'
  33 + _type: 'connected_int'
  34 + shop:
  35 + _desc: '指定的店铺列表'
  36 + _type: 'connected_int'
  37 + msort:
  38 + _desc: '指定的大分类列表'
  39 + _type: 'connected_int'
  40 + misort:
  41 + _desc: '指定的中分类列表'
  42 + _type: 'connected_int'
  43 + sort:
  44 + _desc: '指定的小分类列表'
  45 + _type: 'connected_int'
  46 + color:
  47 + _desc: '指定的颜色列表'
  48 + _type: 'connected_int'
  49 + style:
  50 + _desc: '指定的风格列表'
  51 + _type: 'connected_int'
  52 + size:
  53 + _desc: '指定的尺寸列表'
  54 + _type: 'connected_int'
  55 + gender:
  56 + _desc: '性别(1:男,2:女,3:通用)'
  57 + _type: 'connected_enum_Gender'
  58 + ageLevel:
  59 + _desc: '年龄段(1成人 2大童 3小童)'
  60 + _type: 'connected_enum_AgeLevel'
  61 + price:
  62 + _desc: '价格区间'
  63 + _type: 'ranged_int'
  64 + specialoffer:
  65 + _desc: '是否为促销品[5折以下]'
  66 + _type: 'enum_YesOrNo'
  67 + isdiscount:
  68 + _desc: '是否打折'
  69 + _type: 'enum_YesOrNo'
  70 + vdt:
  71 + _desc: 'VIP折扣类型'
  72 + _type: 'enum_VIPDiscountType'
  73 + p_d:
  74 + _desc: '折扣范围,浮点型,如【p_d_int=0.1,0.3】'
  75 + _type: 'ranged_double'
  76 + p_d_int:
  77 + _desc: '折扣范围,整形,如【p_d_int=1,3】'
  78 + _type: 'ranged_int'
  79 + isStudentPrice:
  80 + _desc: '是否有学生价优惠'
  81 + _type: 'enum_YesOrNo'
  82 + isStudentRebate:
  83 + _desc: '是否有学生返币'
  84 + _type: 'enum_YesOrNo'
  85 + isInstalment:
  86 + _desc: '是否分期'
  87 + _type: 'enum_Status'
  88 + sales:
  89 + _desc: '是否在售'
  90 + _type: 'enum_Status'
  91 + promotion:
  92 + _desc: '是否促销/推广商品 TODO'
  93 + _type: 'int'
  94 + attribute:
  95 + _desc: '包含指定的商品属性(1:正常商品,2:赠品)'
  96 + _type: 'enum_Attribute'
  97 + attribute:
  98 + _desc: '过滤指定的商品属性(1:正常商品,2:赠品)'
  99 + _type: 'enum_Attribute'
  100 + limited:
  101 + _desc: '是否限量商品'
  102 + _type: 'enum_YesOrNo'
  103 + new:
  104 + _desc: '是否新品'
  105 + _type: 'enum_YesOrNo'
  106 + outlets:
  107 + _desc: '是否奥莱商品'
  108 + _type: 'enum_Outlet'
  109 + status:
  110 + _desc: '是否上架'
  111 + _type: 'enum_Status'
  112 + breaking:
  113 + _desc: '传入1只查询断码商品,不传值或其他值该参数不起作用'
  114 + _type: 'optional(1)'
  115 + app_type:
  116 + _desc: 'APP类型'
  117 + _type: 'connected_enum_AppType'
  118 + sell_channels:
  119 + _desc: '销售平台 (网站、APP、现场 TODO)'
  120 + _type: 'enum_SellChannel'
  121 + stocknumber:
  122 + _desc: '商品库存,当传入0时查询库存为0的商品,当传入大于0的数值时,查询库存不小于改数值的商品'
  123 + _type: 'int'
  124 + folder_id:
  125 + _desc: '商品目录'
  126 + _type: 'int'
  127 + series_id:
  128 + _desc: '商品系列'
  129 + _type: 'int'
  130 + first_shelve_time:
  131 + _desc: '首次上架时间间隔'
  132 + _type: 'ranged_double'
  133 + shelve_time:
  134 + _desc: '最近上架日期间隔'
  135 + _type: 'ranged_double'
  136 + day:
  137 + _desc: '上架时间'
  138 + _type: 'date(yyyy-MM-dd)'
  139 + contain_global:
  140 + _desc: '传入Y查询结果包含全球购商品,不传值或其他值该参数不起作用'
  141 + _type: 'optional(Y)'
  142 + contain_seckill:
  143 + _desc: '传入Y查询结果包含秒杀商品,不传值或其他值该参数不起作用'
  144 + _type: 'optional(Y)'
  145 + act_temp:
  146 + _desc: '活动属性-模板id'
  147 + _type: 'int'
  148 + act_rec:
  149 + _desc: '活动属性-是否推荐'
  150 + _type: 'enum_Status'
  151 + act_status:
  152 + _desc: '活动属性-状态'
  153 + _type: 'enum_Status'
16 _response: 154 _response:
17 code: 155 code:
18 _desc: '响应状态码,200为成功,其他为失败' 156 _desc: '响应状态码,200为成功,其他为失败'
@@ -159,10 +297,11 @@ _response: @@ -159,10 +297,11 @@ _response:
159 _desc: '是否是奥特莱斯商品' 297 _desc: '是否是奥特莱斯商品'
160 _type: 'enum_Outlet' 298 _type: 'enum_Outlet'
161 gender: 299 gender:
162 - _desc: '性别' 300 + _desc: '性别(1:男,2:女,3:通用)'
163 _type: 'enum_Gender' 301 _type: 'enum_Gender'
164 age_level: 302 age_level:
165 - _desc: '商品适合的年龄层' 303 + _desc: '年龄段(1成人 2大童 3小童)'
  304 + _type: 'connected_enum_AgeLevel'
166 default_images: 305 default_images:
167 _desc: '默认的商品图片' 306 _desc: '默认的商品图片'
168 yohood_id: 307 yohood_id: