├── .gitignore
├── CHANGELOG.md
├── README.md
├── demo
├── README.md
├── basicUsageDemo.js
├── interfaceRules
│ ├── Cart.getCart.rule.json
│ ├── Search.list.rule.json
│ └── test.rule.json
├── interface_demo.json
├── mockserver.js
├── modelproxy-client.html
└── modelproxy-client.js
├── index.js
├── lib
├── constant.js
├── interfacemanager.js
├── modelproxy-client.js
├── modelproxy.js
├── plugins
│ ├── hsf.js
│ ├── http.js
│ └── tms.js
├── proxy.js
└── proxyfactory.js
├── package.json
└── tests
├── README.md
├── httpproxy.test.js
├── interfaceRules
├── Cart.getMyCart.rule.json
├── Search.getNav.rule.json
├── Search.list.rule.json
└── Search.suggest.rule.json
├── interface_test.json
├── interfacemanager.test.js
├── mockserver.js
├── modelproxy.test.js
└── proxyfactory.test.js
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | lib-cov
3 | tests/coverage.html
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | ## v0.3.0-beta
2 | * Proxy 插件化实现
3 |
4 | ## v0.3.0-alpha-2
5 | * 升级river-mock
6 |
7 | ## v0.3.0-alpha
8 | * proxy底层采用继承方式重构
9 | * 支持捕获customized code异常
10 |
11 | ## v0.2.8
12 | * 支持interface配置文件变量引用。
13 | * 重载ModelProxy.init( path, variables )。variables参数为开发者传入的用于解析interface配置文件中出现的变量的变量对象。
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Midway-ModelProxy 轻量级的接口配置建模框架
2 | ===
3 |
4 | ## 目录
5 | - [Why?]()
6 | - [工作原理图]()
7 | - [使用前必读]()
8 | - [快速开始]()
9 | - [用例一 接口文件配置->引入接口配置文件->创建并使用model]()
10 | - [用例二 model多接口配置及合并请求]()
11 | - [用例三 Model混合配置及依赖调用]()
12 | - [用例四 配置mock代理]()
13 | - [用例五 使用ModelProxy拦截请求]()
14 | - [用例六 在浏览器端使用ModelProxy]()
15 | - [用例七 代理带cookie的请求并且回写cookie]()
16 | - [完整实例](demo/)
17 | - [配置文件详解]()
18 | - [interface.json 配置]()
19 | - [http interface 配置]()
20 | - [hsf interface 配置]()
21 | - [配置文件中的全局变量引入]()
22 | - [API]()
23 | - [ModelProxy对象创建方式]()
24 | - [创建ModelProxy对象时指定的profile相关形式]()
25 | - [ModelProxy logger设置]()
26 | - [ModelProxy对象方法]()
27 | - [如何使用ModelProxy的Mock功能]()
28 | - [rule.json文件]()
29 | - [rule.json文件样式]()
30 | - [附一 测试覆盖率]()
31 | - [附二 前后端分离思考与实践]()
32 | - [附三 中途岛整体架构图及modelproxy所处位置]()
33 |
34 | ## Why?
35 | ---
36 | 淘系的技术大背景下,必须依赖Java提供稳定的后端接口服务。在这样环境下,Node Server在实际应用中的一个主要作用即是代理(Proxy)功能。由于淘宝业务复杂,后端接口方式多种多样(MTop, Modulet, HSF...)。然而在使用Node开发web应用时,我们希望有一种统一方式访问这些代理资源的基础框架,为开发者屏蔽接口访问差异,同时提供友好简洁的数据接口使用方式。于是就有了 midway-modelproxy 这个构件。使用midway-modelproxy,可以提供如下好处:
37 |
38 | 1. 不同的开发者对于接口访问代码编写方式统一,含义清晰,降低维护难度。
39 | 2. 框架内部采用工厂+单例模式,实现接口一次配置多次复用。并且开发者可以随意定制组装自己的业务Model(依赖注入)。
40 | 3. 可以非常方便地实现线上,日常,预发环境的切换。
41 | 4. 内置[river-mock](http://gitlab.alibaba-inc.com/river/mock/tree/master)和[mockjs](http://mockjs.com)等mock引擎,提供mock数据非常方便。
42 | 5. 使用接口配置文件,对接口的依赖描述做统一的管理,避免散落在各个代码之中。
43 | 6. 支持浏览器端共享Model,浏览器端可以使用它做前端数据渲染。整个代理过程对浏览器透明。
44 | 7. 接口配置文件本身是结构化的描述文档,可以使用[river](http://gitlab.alibaba-inc.com/river/spec/tree/master)工具集合,自动生成文档。也可使用它做相关自动化接口测试,使整个开发过程形成一个闭环。
45 |
46 | ### ModelProxy工作原理图及相关开发过程图览
47 | ---
48 | 
49 |
50 | ## 使用前必读
51 | ---
52 | 使用ModelProxy之前,您需要在工程根目录下创建名为interface.json的配置文件。该文件定义了工程项目中所有需要使用到的接口集合(详细配置说明见后文)。定义之后,您可以在代码中按照需要引入不同的接口,创建与业务相关的Model对象。接口的定义和model其实是多对多的关系。也即一个接口可以被多个model使用,一个model可以使用多个接口。具体情况由创建model的方式来决定。下面用例中会从易到难交您如何创建这些model。
53 |
54 | ## 快速开始
55 | ---
56 |
57 | ### 用例一 接口文件配置->引入接口配置文件->创建并使用model
58 | * 第一步 配置接口文件命名为:interface_sample.json,并将其放在工程根目录下。
59 | 注意:整个项目有且只有一个接口配置文件,其interfaces字段下定义了多个接口。在本例中,仅仅配置了一个主搜接口。
60 |
61 | ```json
62 | {
63 | "title": "pad淘宝项目数据接口集合定义",
64 | "version": "1.0.0",
65 | "engine": "mockjs",
66 | "rulebase": "interfaceRules",
67 | "status": "online",
68 | "interfaces": [ {
69 | "name": "主搜索接口",
70 | "id": "Search.getItems",
71 | "urls": {
72 | "online": "http://s.m.taobao.com/client/search.do"
73 | }
74 | } ]
75 | }
76 | ```
77 |
78 | * 第二步 在代码中引入ModelProxy模块,并且初始化引入接口配置文件(在实际项目中,引入初始化文件动作应伴随工程项目启动时完成,有且只有一次)
79 |
80 | ```js
81 | // 引入模块
82 | var ModelProxy = require( 'modelproxy' );
83 |
84 | // interface配置文件的绝对路径
85 | var path = require('path').resolve( __dirname, './interface_sample.json' );
86 |
87 | // 初始化引入接口配置文件 (注意:初始化工作有且只有一次)
88 | ModelProxy.init( path );
89 | ```
90 |
91 | * 第三步 使用ModelProxy
92 |
93 | ```js
94 | // 创建model
95 | var searchModel = new ModelProxy( {
96 | searchItems: 'Search.getItems' // 自定义方法名: 配置文件中的定义的接口ID
97 | } );
98 | // 或者这样创建: var searchModel = new ModelProxy( 'Search.getItems' ); 此时getItems 会作为方法名
99 |
100 | // 使用model, 注意: 调用方法所需要的参数即为实际接口所需要的参数。
101 | searchModel.searchItems( { keyword: 'iphone6' } )
102 | // !注意 必须调用 done 方法指定回调函数,来取得上面异步调用searchItems获得的数据!
103 | .done( function( data ) {
104 | console.log( data );
105 | } )
106 | .error( function( err ) {
107 | console.log( err );
108 | } );
109 | ```
110 |
111 | ### 用例二 model多接口配置及合并请求
112 | * 配置
113 |
114 | ```json
115 | { // 头部配置省略...
116 | "interfaces": [ {
117 | "name": "主搜索搜索接口",
118 | "id": "Search.list",
119 | "urls": {
120 | "online": "http://s.m.taobao.com/search.do"
121 | }
122 | }, {
123 | "name": "热词推荐接口",
124 | "id": "Search.suggest",
125 | "urls": {
126 | "online": "http://suggest.taobao.com/sug"
127 | }
128 | }, {
129 | "name": "导航获取接口",
130 | "id": "Search.getNav",
131 | "urls": {
132 | "online": "http://s.m.taobao.com/client/search.do"
133 | }
134 | } ]
135 | }
136 | ```
137 |
138 | * 代码
139 |
140 | ```js
141 | // 更多创建方式,请参考后文API
142 | var model = new ModelProxy( 'Search.*' );
143 |
144 | // 调用自动生成的不同方法
145 | model.list( { keyword: 'iphone6' } )
146 | .done( function( data ) {
147 | console.log( data );
148 | } );
149 |
150 | model.suggest( { q: '女' } )
151 | .done( function( data ) {
152 | console.log( data );
153 | } )
154 | .error( function( err ) {
155 | console.log( err );
156 | } );
157 |
158 | // 合并请求
159 | model.suggest( { q: '女' } )
160 | .list( { keyword: 'iphone6' } )
161 | .getNav( { key: '流行服装' } )
162 | .done( function( data1, data2, data3 ) {
163 | // 参数顺序与方法调用顺序一致
164 | console.log( data1, data2, data3 );
165 | } );
166 | ```
167 |
168 | ### 用例三 Model混合配置及依赖调用
169 |
170 | * 配置
171 |
172 | ```json
173 | { // 头部配置省略...
174 | "interfaces": [ {
175 | "name": "用户信息查询接口",
176 | "id": "Session.getUser",
177 | "urls": {
178 | "online": "http://taobao.com/getUser.do"
179 | }
180 | }, {
181 | "name": "订单获取接口",
182 | "id": "Order.getOrder",
183 | "urls": {
184 | "online": "http://taobao.com/getOrder"
185 | }
186 | } ]
187 | }
188 | ```
189 |
190 | * 代码
191 |
192 | ``` js
193 | var model = new ModelProxy( {
194 | getUser: 'Session.getUser',
195 | getMyOrderList: 'Order.getOrder'
196 | } );
197 | // 先获得用户id,然后再根据id号获得订单列表
198 | model.getUser( { sid: 'fdkaldjfgsakls0322yf8' } )
199 | .done( function( data ) {
200 | var uid = data.uid;
201 | this.getMyOrderList( { id: uid } )
202 | .done( function( data ) {
203 | console.log( data );
204 | } );
205 | } );
206 | ```
207 |
208 | ### 用例四 配置mock代理
209 | * 第一步 在相关接口配置段落中启用mock
210 |
211 | ```json
212 | {
213 | "title": "pad淘宝数据接口定义",
214 | "version": "1.0.0",
215 | "engine": "river-mock", <-- 指定mock引擎
216 | "rulebase": "interfaceRules", <-- 指定存放相关mock规则文件的目录名,约定位置与interface.json文件存放在同一目录,默认为 interfaceRules
217 | "status": "online",
218 | "interfaces": [ {
219 | "name": "主搜索接口",
220 | "id": "Search.getItems",
221 | "ruleFile": "Search.getItems.rule.json", <-- 指定数据mock规则文件名,如果不配置,则将默认设置为 id + '.rule.json'
222 | "urls": {
223 | "online": "http://s.m.taobao.com/client/search.do",
224 | "prep": "http://s.m.taobao.com/client/search.do",
225 | "daily": "http://daily.taobao.net/client/search.do"
226 | },
227 | "status": "mock" <-- 启用mock状态,覆盖全局status
228 | } ]
229 | }
230 | ```
231 |
232 | * 第二步 添加接口对应的规则文件到ruleBase(interfaceRules)指定的文件夹。mock数据规则请参考[river-mock](http://gitlab.alibaba-inc.com/river/mock/tree/master)和[mockjs](http://mockjs.com)。启动程序后,ModelProxy即返回相关mock数据。
233 |
234 | ### 用例五 使用ModelProxy拦截请求
235 |
236 | ```js
237 | var app = require( 'connect' )();
238 | var ModelProxy = require( 'modelproxy' );
239 | var path = require('path').resolve( __dirname, './interface_sample.json' );
240 | ModelProxy.init( path );
241 |
242 | // 指定需要拦截的路径
243 | app.use( '/model', ModelProxy.Interceptor );
244 |
245 | // 此时可直接通过浏览器访问 /model/[interfaceid] 调用相关接口(如果该接口定义中配置了 intercepted = false, 则无法访问)
246 | ```
247 |
248 | ### 用例六 在浏览器端使用ModelProxy
249 | * 第一步 按照用例二配置接口文件
250 |
251 | * 第二步 按照用例五 启用拦截功能
252 |
253 | * 第三步 在浏览器端使用ModelProxy
254 |
255 | ```html
256 |
257 |
258 | ```
259 |
260 | ```html
261 |
284 | ```
285 |
286 | ### 用例七 代理带cookie的请求并且回写cookie (注:请求是否需要带cookie或者回写取决于接口提供者)
287 |
288 | * 关键代码(app 由express创建)
289 |
290 | ```js
291 | app.get( '/getMycart', function( req, res ) {
292 | var cookie = req.headers.cookie;
293 | var cart = ModelProxy.create( 'Cart.*' );
294 | cart.getMyCart()
295 | // 在调用done之前带上cookie
296 | .withCookie( cookie )
297 | // done 回调函数中最后一个参数总是需要回写的cookie,不需要回写时可以忽略
298 | .done( function( data , setCookies ) {
299 | // 回写cookie
300 | res.setHeader( 'Set-Cookie', setCookies );
301 | res.send( data );
302 | }, function( err ) {
303 | res.send( 500, err );
304 | } );
305 | } );
306 | ```
307 |
308 | ### 完整实例请查看 [demo](demo/)
309 |
310 | ## 配置文件详解
311 | ---
312 | ### interface.json 配置结构
313 |
314 | ``` js
315 | {
316 | "title": "pad淘宝项目数据接口集合定义", // [必填][string] 接口文档标题
317 | "version": "1.0.0", // [必填][string] 版本号
318 | "engine": "river-mock", // [选填][string] mock引擎,取值可以是river-mock和mockjs。不需要mock数据时可以不配置
319 | "rulebase": "interfaceRules", // [选填][string] mock规则文件夹名称。不需要mock数据时可以不配置。约定该文件夹与
320 | // interface.json配置文件位于同一文件夹。默认为interfaceRules
321 | "status": "online", // [必填][string] 全局代理状态,取值只能是 interface.urls中出现过的键值或者mock
322 | "interfaces": [ {
323 | // 此处设置每一个interface的具体配置
324 | // ... 不同类型的接口配置方法见下文
325 | } ]
326 | }
327 | ```
328 |
329 | ### http interface 配置
330 |
331 | ``` js
332 | {
333 | "name": "获取购物车信息", // [选填][string] 接口名称
334 | "desc": "接口负责人: 善繁", // [选填][string] 接口描述
335 | "version": "0.0.1", // [选填][string] 接口版本号,发送请求时会带上版本号字段
336 | "type": "http", // [选填][string] 接口类型,取值可以是http或者hsf,默认
337 | "id": "cart.getCart", // [必填][string] 接口ID,必须由英文单词+点号组成
338 | "urls": { // [如果ruleFile不存在, 则必须有一个地址存在][object] 可供切换的url集合
339 | "online": "http://url1", // 线上地址
340 | "prep": "http://url2", // 预发地址
341 | "daily": "http://url3", // 日常地址
342 | },
343 | "ruleFile": "cart.getCart.rule.json",// [选填][string] 对应的数据规则文件,当Proxy Mock状态开启时回返回mock数据
344 | // 不配置时默认为id + ".rule.json"。
345 | "isRuleStatic": true, // [选填][boolean] 数据规则文件是否为静态,即在开启mock状态时,程序会将ruleFile
346 | // 按照静态文件读取, 而非解析该规则文件生成数据,默认为false
347 | "engine": "mockjs" // [选填][string] mock引擎,取值可以是river-mock和mockjs。覆盖全局engine
348 | "status": "online", // [选填][string] 当前代理状态,可以是urls中的某个键值(online, prep, daily)
349 | // 或者mock或mockerr。如果不填,则代理状态依照全局设置的代理状态;如果设置为mock,
350 | // 则返回 ruleFile中定义response内容;如果设置为mockerr,则返回ruleFile中定义
351 | // 的responseError内容。
352 | "method": "post", // [选填][string] 请求方式,取值post|get 默认get
353 | "dataType": "json", // [选填][string] 返回的数据格式, 取值 json|text|jsonp,仅当
354 | // bypassProxyOnClient设置为true时,jsonp才有效,否则由Node端发送的请求按json格
355 | // 式返回数据。默认为json
356 | "isCookieNeeded": true, // [选填][boolean] 是否需要传递cookie默认false
357 | "encoding": "utf8", // [选填][string] 代理的数据源编码类型。取值可以是常用编码类型'utf8', 'gbk',
358 | // 'gb2312' 或者 'raw' 如果设置为raw则直接返回2进制buffer,默认为utf8
359 | // 注意,不论数据源原来为何种编码,代理之后皆以utf8编码输出
360 | "timeout": 5000, // [选填][number] 延时设置,默认10000
361 | "intercepted": true, // [选填][boolean] 是否拦截请求。当设置为true时,如果在Node端启用了ModelProxy拦截器
362 | // (见例六),则浏览器端可以直接通过interface id访问该接口,否则无法访问。默认为true
363 | "bypassProxyOnClient": false, // [选填][boolean] 在浏览器端使用ModelProxy请求数据时是否绕过代理而直接请求原地址。
364 | // 当且仅当status 字段不为mock或者mockerr时有效。默认 false
365 |
366 | }
367 |
368 | ```
369 |
370 | ### hsf interface 配置
371 |
372 | ``` js
373 | {
374 | // 设计中...
375 | }
376 | ```
377 |
378 | ### tms interface 配置
379 |
380 | ``` js
381 | {
382 | // 设计中...
383 | }
384 | ```
385 |
386 | ### 配置文件中的全局变量引入
387 | 需要调用 *ModelProxy.init( path, variables )* 来解析interface.json中引用的变量。其中variables可能从[midway-global](http://gitlab.alibaba-inc.com/midway/midway-global/tree/master)模块读取,也可以是任意指定值。
388 |
389 | * 例
390 |
391 | variables 对象:
392 |
393 | ``` js
394 | {
395 | ...
396 | status: 'online'
397 | hsf: {
398 | onlineArr: '192.168.0.1'
399 | }
400 | }
401 | ```
402 |
403 | interface.json 文件有如下配置片段
404 |
405 | ```js
406 | {
407 | ...
408 | "status": "$status$",
409 | "hsfurl": "$hsf.onlineArr$/getData"
410 | }
411 | ```
412 |
413 | 替换结果为:
414 |
415 | ```js
416 | {
417 | ...
418 | "status": "online",
419 | "hsfurl": "192.168.0.1/getData"
420 | }
421 | ```
422 |
423 | ## API
424 | ---
425 |
426 | ### ModelProxy.init
427 | * ModelProxy.init( path[, variables] ) path为接口配置文件所在的绝对路径, variables 为变量对象,包含interface.
428 | json文件中所引用过的变量。
429 |
430 | ### ModelProxy 对象创建方式
431 |
432 | * 直接new
433 |
434 | ```js
435 | var model = new ModelProxy( profile );
436 |
437 | ```
438 |
439 | * 工厂创建
440 |
441 | ```js
442 | var model = ModelProxy.create( profile );
443 | ```
444 |
445 | ### 创建ModelProxy对象时指定的 *profile* 相关形式
446 | * 接口ID 生成的对象会取ID最后'.'号后面的单词作为方法名
447 |
448 | ```js
449 | ModelProxy.create( 'Search.getItem' );
450 | ```
451 |
452 | * 键值JSON对象 自定义方法名: 接口ID
453 |
454 | ```js
455 | ModelProxy.create( {
456 | getName: 'Session.getUserName',
457 | getMyCarts: 'Cart.getCarts'
458 | } );
459 | ```
460 |
461 | * 数组形式 取最后 . 号后面的单词作为方法名
462 | 下例中生成的方法调用名依次为: Cart_getItem, getItem, suggest, getName
463 |
464 | ```js
465 | ModelProxy.create( [ 'Cart.getItem', 'Search.getItem', 'Search.suggest', 'Session.User.getName' ] );
466 |
467 | ```
468 |
469 | * 前缀形式 (推荐使用)
470 |
471 | ```js
472 | ModelProxy.create( 'Search.*' );
473 | ```
474 |
475 | ### ModelProxy logger设置
476 |
477 | ```js
478 | ModelProxy.setLogger( logger );
479 | ```
480 | 不设置logger的情况下会使用console输出日志
481 |
482 | ### ModelProxy对象方法
483 |
484 | * .method( params )
485 | method为创建model时动态生成,参数 params{Object}, 为请求接口所需要的参数键值对。
486 |
487 | * .done( callback, errCallback )
488 | 接口调用完成函数,callback函数的参数与done之前调用的方法请求结果保持一致.最后一个参数为请求回写的cookie。callback函数中的 this 指向ModelProxy对象本身,方便做进一步调用。errCallback 即出错回调函数(可能会被调用多次)。
489 |
490 | * .withCookie( cookies )
491 | 如果接口需要提供cookie才能返回数据,则调用此方法来设置请求的cookie{String} (如何使用请查看用例七)
492 |
493 | * .error( errCallback )
494 | 指定全局调用出错处理函数, errCallback 的参数为Error对象。
495 |
496 | * .fail( errCallback )
497 | 同 error方法,方便不同的语法使用习惯。
498 |
499 | ## 如何使用ModelProxy的Mock功能
500 | ---
501 | ### rule.json文件
502 | 当mock状态开启时,mock引擎会读取与接口定义相对应的rule.json规则文件,生成相应的数据。该文件应该位于interface.json配置文件中
503 | ruleBase字段所指定的文件夹中。 (建议该文件夹与interface配置文件同级)
504 |
505 | ### rule.json文件样式
506 |
507 | ```js
508 | {
509 | "request": { // 请求参数列表
510 | "参数名1": "规则一", // 具体规则取决于采用何种引擎
511 | "参数名2": "规则二",
512 | ...
513 | },
514 | "response": 响应内容规则, // 响应内容规则取决于采用何种引擎
515 | "responseError": 响应失败规则 // 响应内容规则取决于采用何种引擎
516 | }
517 |
518 | ```
519 | ## 如何为ModelProxy贡献代理插件
520 | 完善中...
521 |
522 | ## [附一] 测试覆盖率
523 | ---
524 |
525 | **Overview: `96%` coverage `272` SLOC**
526 |
527 | [modelproxy.js](tests/modelproxy.test.js) : `98%` coverage `57` SLOC
528 |
529 | [interfacemanager.js](tests/interfacemanager.test.js): `98%` coverage `76` SLOC
530 |
531 | [proxyfactory](tests/proxyfactory.test.js) : `93%` coverage `139` SLOC
532 |
533 |
534 | ## [附二] [前后端分离思考与实践](http://ued.taobao.org/blog/2014/04/modelproxy/)
535 |
536 | ## [附三] 中途岛整体架构图及modelproxy所处位置
537 | 
538 |
539 |
540 | 如有任何问题请联系[@善繁](https://work.alibaba-inc.com/work/u/68162)
--------------------------------------------------------------------------------
/demo/README.md:
--------------------------------------------------------------------------------
1 | # demo 演示说明
2 |
3 | * 第一步
4 |
5 | $ node basicUsageDemo.js
6 |
7 |
8 | * 第二步
9 |
10 | 打开浏览器,访问 http://127.0.0.1:3000/index
11 |
12 | * 第三步
13 |
14 | 打开浏览器控制台 查看输出结果
15 |
16 | ### 查看带cookie的请求及回写
17 |
18 | * 第一步
19 |
20 | sudo vim /etc/hosts
21 | 插入 127.0.0.1 local.taobao.com
22 |
23 | * 第二步
24 |
25 | 访问 http://local.taobao.com:3000/getMyCart
26 |
27 | * 第三步
28 |
29 | 打开浏览器控制台 查看networks选项卡中的 http请求细节(set-cookie)
30 |
31 |
32 | ### 查看post代理效果
33 |
34 | * 第一步
35 |
36 | $ node mockserver.js
37 |
38 | * 第二步
39 |
40 | $ node basicUsageDemo.js
41 |
42 | * 第三步
43 |
44 | 打开浏览器,访问 http://127.0.0.1:3000/index
45 | 填写数据,点击按钮
46 |
47 |
--------------------------------------------------------------------------------
/demo/basicUsageDemo.js:
--------------------------------------------------------------------------------
1 | var app = require( 'express' )();
2 | var ModelProxy = require( '../index' );
3 |
4 | // 初始化modelproxy接口文件
5 | ModelProxy.init( require('path').resolve( __dirname, './interface_demo.json' ) );
6 |
7 | // 配置拦截器,浏览器端可通过访问 127.0.0.1/model/[interfaceId]
8 | app.use( '/model/', ModelProxy.Interceptor );
9 |
10 | app.get( '/index', function( req, res ) {
11 | res.sendfile( 'modelproxy-client.html' );
12 | } );
13 |
14 | var model = ModelProxy.create( 'Test.post' );
15 | model.post( {
16 | a: 'abc',
17 | b: 'bcd',
18 | c: '{"a":"b"}'
19 | } ).done( function( data ) {
20 | console.log( data );
21 | } );
22 |
23 | app.get( '/getCombinedData', function( req, res ) {
24 | var searchModel = ModelProxy.create( 'Search.*' );
25 | searchModel.list( { q: 'ihpone6' } )
26 | .list( { q: '冲锋衣' } )
27 | .suggest( { q: 'i' } )
28 | .getNav( { q: '滑板' } )
29 | .done( function( data1, data2, data3, data4 ) {
30 | res.send( {
31 | "list_ihpone6": data1,
32 | "list_冲锋衣": data2,
33 | "suggest_i": data3,
34 | "getNav_滑板": data4
35 | } );
36 | } ).error( function( err ) {
37 | console.log( err );
38 | res.send( 500, err );
39 | } );
40 | } );
41 |
42 | // 带cookie的代理请求与回写
43 | app.get( '/getMycart', function( req, res ) {
44 | var cookie = req.headers.cookie;
45 | console.log( 'request cookie:\n', cookie );
46 | var cart = ModelProxy.create( 'Cart.*' );
47 | cart.getMyCart()
48 | .withCookie( cookie )
49 | .done( function( data , setCookies ) {
50 | console.log( 'response cookie:\n', setCookies );
51 | res.setHeader( 'Set-Cookie', setCookies );
52 | res.send( data );
53 | }, function( err ) {
54 | console.log( err );
55 | res.send( 500, err );
56 | } );
57 | } );
58 |
59 | // 配置资源路由
60 | app.get( '/modelproxy-client.js', function( req, res ) {
61 | res.sendfile( './modelproxy-client.js' );
62 | } );
63 |
64 | app.listen( 3000 );
65 |
66 | console.log( 'port: 3000' );
--------------------------------------------------------------------------------
/demo/interfaceRules/Cart.getCart.rule.json:
--------------------------------------------------------------------------------
1 | {
2 | "request": {},
3 | "response": {
4 | "success": true,
5 | "globalData": {
6 | "h": "g,ge3telrsgexdknzogizdm",
7 | "tpId": 0,
8 | "totalSize": 7,
9 | "invalidSize": 1,
10 | "isAllCItem": false,
11 | "isAllBItem": false,
12 | "diffTairCount": 0,
13 | "login": false,
14 | "openNoAttenItem": false,
15 | "startTime": "2013-11-08 16:51:10",
16 | "endNotEqualTime": "2014-03-07 11:36:24",
17 | "isNext": true,
18 | "page": 1,
19 | "currentPageSize": 7,
20 | "quickSettlement": false,
21 | "weakenCart": false,
22 | "cartUrl": "http://cart.taobao.com/cart.htm?from=bmini",
23 | "bundRela": {
24 | "list": [
25 | {
26 | "id": 399573899,
27 | "title": "湾儿家",
28 | "bundles": [
29 | "s_399573899"
30 | ]
31 | },
32 | {
33 | "id": 695609583,
34 | "title": "傲世奢侈品交易平",
35 | "bundles": [
36 | "s_695609583"
37 | ]
38 | },
39 | {
40 | "id": 435179831,
41 | "title": "美美——",
42 | "bundles": [
43 | "s_435179831"
44 | ]
45 | },
46 | {
47 | "id": 408561270,
48 | "title": "陈秀羽服饰",
49 | "bundles": [
50 | "s_408561270"
51 | ]
52 | },
53 | {
54 | "id": 94269988,
55 | "title": "我的B2C小店4",
56 | "bundles": [
57 | "s_94269988"
58 | ],
59 | "promo": {
60 | "title": "省10.00元:满就送",
61 | "discount": 1000,
62 | "point": 0,
63 | "usePoint": 0,
64 | "isSelected": true,
65 | "value": "mjs-94269988_132268118"
66 | }
67 | },
68 | {
69 | "id": 592995015,
70 | "title": "妖精づ鞋柜",
71 | "bundles": [
72 | "s_592995015"
73 | ]
74 | },
75 | {
76 | "id": 84815057,
77 | "title": "优木园艺",
78 | "bundles": [
79 | "s_84815057"
80 | ]
81 | },
82 | {
83 | "id": 28011101,
84 | "title": "菲然小屋",
85 | "bundles": [
86 | "s_28011101"
87 | ]
88 | },
89 | {
90 | "id": 682508620,
91 | "title": "淑美玩具",
92 | "bundles": [
93 | "s_682508620"
94 | ]
95 | },
96 | {
97 | "id": 1103141522,
98 | "title": "维依恋旗舰店",
99 | "bundles": [
100 | "s_1103141522"
101 | ],
102 | "promo": {
103 | "title": "省0.00元:3.8生活节",
104 | "discount": 0,
105 | "point": 0,
106 | "usePoint": 0,
107 | "isSelected": true,
108 | "value": "Tmall$tspAll-27484074"
109 | }
110 | },
111 | {
112 | "id": 383999134,
113 | "title": "新星轩精品男童装",
114 | "bundles": [
115 | "s_383999134"
116 | ]
117 | }
118 | ]
119 | },
120 | "showRedEnvelope": false,
121 | "showGodDialogue": false
122 | },
123 | "list": [
124 | {
125 | "id": "s_399573899",
126 | "title": "湾儿家",
127 | "type": "shop",
128 | "url": "http://store.taobao.com/shop/view_shop.htm?user_number_id=399573899",
129 | "seller": "shichongdi",
130 | "host": "C",
131 | "shopId": 61498446,
132 | "sellerId": "399573899",
133 | "isValid": true,
134 | "hasPriceVolume": false,
135 | "extraItem": {
136 | "totalExtraItemNum": 0,
137 | "remainExtraItemNum": 0
138 | },
139 | "bundles": [
140 | {
141 | "id": "s_399573899_0",
142 | "orders": [
143 | {
144 | "id": "51293509168",
145 | "itemId": "19584914202",
146 | "skuId": "45758911856",
147 | "cartId": "51293509168",
148 | "isValid": true,
149 | "url": "http://item.taobao.com/item.htm?id=19584914202",
150 | "pic": "http://img04.taobaocdn.com/bao/uploaded/i4/13899026596314756/T11TybFlXfXXXXXXXX_!!0-item_pic.jpg",
151 | "title": "特价包邮 吸盘浴室 毛巾架 壁挂卫生间 厨房卫浴室 三层架置物架",
152 | "weight": 0,
153 | "skus": {
154 | "颜色分类": "光滑经典吸盘"
155 | },
156 | "shopId": "61498446",
157 | "shopName": "湾儿家",
158 | "shopUrl": "http://store.taobao.com/shop/view_shop.htm?user_number_id=399573899",
159 | "seller": "shichongdi",
160 | "sellerId": 399573899,
161 | "price": {
162 | "now": 6000,
163 | "origin": 12800,
164 | "oriPromo": 6000,
165 | "descend": 0,
166 | "save": 6800,
167 | "sum": 24000,
168 | "actual": 0,
169 | "prepay": 0,
170 | "finalpay": 0,
171 | "extraCharges": 0
172 | },
173 | "amount": {
174 | "now": 4,
175 | "max": 53,
176 | "limit": 9223372036854776000,
177 | "multiple": 1,
178 | "supply": 0,
179 | "demand": 0
180 | },
181 | "itemIcon": {
182 | "CART_EBOOK": [],
183 | "CART_XIAOBAO": [
184 | {
185 | "desc": "如实描述",
186 | "title": "消费者保障服务,卖家承诺商品如实描述",
187 | "link": "http://www.taobao.com/go/act/315/xfzbz_rsms.php?ad_id=&am_id=130011830696bce9eda3&cm_id=&pm_id=",
188 | "img": "http://img03.taobaocdn.com/tps/i3/T1bnR4XEBhXXcQVo..-14-16.png"
189 | },
190 | {
191 | "desc": "7天退换",
192 | "title": "消费者保障服务,卖家承诺7天无理由退换货",
193 | "link": "http://www.taobao.com/go/act/315/xbqt090304.php?ad_id=&am_id=130011831021c2f3caab&cm_id=&pm_id=",
194 | "img": "http://img04.taobaocdn.com/tps/i4/T16lKeXwFcXXadE...-14-15.png"
195 | }
196 | ],
197 | "CART_YULIU": [
198 | {
199 | "title": "支持信用卡支付",
200 | "img": "http://assets.taobaocdn.com/sys/common/icon/trade/xcard.png"
201 | }
202 | ],
203 | "CART_IDENTITY": []
204 | },
205 | "isDouble11": false,
206 | "isDouble11halfDiscount": false,
207 | "isCod": false,
208 | "isAttention": true,
209 | "promos": [
210 | [
211 | {
212 | "style": "tbcard",
213 | "title": "省240元:年末巨惠-...",
214 | "usedPoint": 0
215 | }
216 | ]
217 | ],
218 | "createTime": 1394419698000,
219 | "attr": ";op:12800;cityCode:330100;",
220 | "preference": false,
221 | "isSellerPayPostfee": false,
222 | "leafCategory": 0,
223 | "cumulativeSales": 0,
224 | "skuStatus": 2,
225 | "cartActiveInfo": {
226 | "isDefault": false,
227 | "wantStatus": 0,
228 | "endTime": 0,
229 | "cartBcParams": "buyerCondition~0~~cartCreateTime~1394419698000",
230 | "type": 0
231 | }
232 | }
233 | ],
234 | "type": "shop",
235 | "valid": true
236 | }
237 | ]
238 | },
239 | {
240 | "id": "s_695609583",
241 | "title": "傲世奢侈品交易平",
242 | "type": "shop",
243 | "url": "http://store.taobao.com/shop/view_shop.htm?user_number_id=695609583",
244 | "seller": "edt無道",
245 | "host": "C",
246 | "shopId": 106555610,
247 | "sellerId": "695609583",
248 | "isValid": true,
249 | "hasPriceVolume": false,
250 | "bundles": [
251 | {
252 | "id": "s_695609583_0",
253 | "orders": [
254 | {
255 | "id": "45179047633",
256 | "itemId": "36369001122",
257 | "skuId": "41467959538",
258 | "cartId": "45179047633",
259 | "isValid": true,
260 | "url": "http://item.taobao.com/item.htm?id=36369001122",
261 | "pic": "http://img04.taobaocdn.com/bao/uploaded/i4/T1FP62FkRbXXXXXXXX_!!0-item_pic.jpg",
262 | "title": "愛馬H扣手掌紋皮帶",
263 | "weight": 0,
264 | "skus": {
265 | "颜色分类": "黑色"
266 | },
267 | "shopId": "106555610",
268 | "shopName": "傲世奢侈品交易平臺",
269 | "shopUrl": "http://store.taobao.com/shop/view_shop.htm?user_number_id=695609583",
270 | "seller": "edt無道",
271 | "sellerId": 695609583,
272 | "price": {
273 | "now": 148500,
274 | "origin": 148500,
275 | "oriPromo": 148500,
276 | "descend": 0,
277 | "save": 0,
278 | "sum": 742500,
279 | "actual": 0,
280 | "prepay": 0,
281 | "finalpay": 0,
282 | "extraCharges": 0
283 | },
284 | "amount": {
285 | "now": 5,
286 | "max": 3,
287 | "limit": 9223372036854775807,
288 | "multiple": 1,
289 | "supply": 0,
290 | "demand": 0
291 | },
292 | "itemIcon": {
293 | "CART_EBOOK": [],
294 | "CART_XIAOBAO": [
295 | {
296 | "desc": "如实描述",
297 | "title": "消费者保障服务,卖家承诺商品如实描述",
298 | "link": "http://www.taobao.com/go/act/315/xfzbz_rsms.php?ad_id=&am_id=130011830696bce9eda3&cm_id=&pm_id=",
299 | "img": "http://img03.taobaocdn.com/tps/i3/T1bnR4XEBhXXcQVo..-14-16.png"
300 | }
301 | ],
302 | "CART_YULIU": [],
303 | "CART_IDENTITY": []
304 | },
305 | "isDouble11": false,
306 | "isDouble11halfDiscount": false,
307 | "isCod": false,
308 | "isAttention": true,
309 | "createTime": 1389251633000,
310 | "attr": ";op:148500;cityCode:330100;",
311 | "preference": false,
312 | "isSellerPayPostfee": false,
313 | "leafCategory": 0,
314 | "cumulativeSales": 0,
315 | "skuStatus": 2,
316 | "cartActiveInfo": {
317 | "isDefault": false,
318 | "wantStatus": 0,
319 | "endTime": 0,
320 | "cartBcParams": "buyerCondition~0~~cartCreateTime~1389251633000",
321 | "type": 0
322 | }
323 | },
324 | {
325 | "id": "40865005024",
326 | "itemId": "36367062613",
327 | "skuId": "36240702186",
328 | "cartId": "40865005024",
329 | "isValid": true,
330 | "url": "http://item.taobao.com/item.htm?id=36367062613",
331 | "pic": "http://img04.taobaocdn.com/bao/uploaded/i4/19583033169071904/T1LFMLFXdXXXXXXXXX_!!0-item_pic.jpg",
332 | "title": "全新蘭博基尼LP700-4 含購置稅",
333 | "weight": 0,
334 | "skus": {
335 | "颜色分类": "桔色"
336 | },
337 | "shopId": "106555610",
338 | "shopName": "傲世奢侈品交易平臺",
339 | "shopUrl": "http://store.taobao.com/shop/view_shop.htm?user_number_id=695609583",
340 | "seller": "edt無道",
341 | "sellerId": 695609583,
342 | "price": {
343 | "now": 755000000,
344 | "origin": 770000000,
345 | "oriPromo": 770000000,
346 | "descend": 0,
347 | "save": 15000000,
348 | "sum": 7550000000,
349 | "actual": 0,
350 | "prepay": 0,
351 | "finalpay": 0,
352 | "extraCharges": 0
353 | },
354 | "amount": {
355 | "now": 10,
356 | "max": 10,
357 | "limit": 9223372036854775807,
358 | "multiple": 1,
359 | "supply": 0,
360 | "demand": 0
361 | },
362 | "itemIcon": {
363 | "CART_EBOOK": [],
364 | "CART_XIAOBAO": [
365 | {
366 | "desc": "如实描述",
367 | "title": "消费者保障服务,卖家承诺商品如实描述",
368 | "link": "http://www.taobao.com/go/act/315/xfzbz_rsms.php?ad_id=&am_id=130011830696bce9eda3&cm_id=&pm_id=",
369 | "img": "http://img03.taobaocdn.com/tps/i3/T1bnR4XEBhXXcQVo..-14-16.png"
370 | }
371 | ],
372 | "CART_YULIU": [],
373 | "CART_IDENTITY": []
374 | },
375 | "isDouble11": false,
376 | "isDouble11halfDiscount": false,
377 | "isCod": false,
378 | "isAttention": true,
379 | "createTime": 1386645841000,
380 | "attr": ";op:770000000;cityCode:330100;",
381 | "preference": false,
382 | "isSellerPayPostfee": false,
383 | "leafCategory": 0,
384 | "cumulativeSales": 0,
385 | "skuStatus": 2,
386 | "cartActiveInfo": {
387 | "isDefault": false,
388 | "wantStatus": 0,
389 | "endTime": 0,
390 | "cartBcParams": "buyerCondition~0~~cartCreateTime~1386645841000",
391 | "type": 0
392 | }
393 | },
394 | {
395 | "id": "41100499431",
396 | "itemId": "36321918718",
397 | "skuId": "36104438665",
398 | "cartId": "41100499431",
399 | "isValid": false,
400 | "url": "http://item.taobao.com/item.htm?id=36321918718",
401 | "pic": "http://img04.taobaocdn.com/bao/uploaded/i4/19583031080830054/T1KCzNFadXXXXXXXXX_!!0-item_pic.jpg",
402 | "title": "邁凱輪mp4-12c全新中文版預定",
403 | "weight": 0,
404 | "skus": {
405 | "颜色分类": "红色"
406 | },
407 | "shopId": "106555610",
408 | "shopName": "傲世奢侈品交易平臺",
409 | "shopUrl": "http://store.taobao.com/shop/view_shop.htm?user_number_id=695609583",
410 | "seller": "edt無道",
411 | "sellerId": 695609583,
412 | "price": {
413 | "now": 396000000,
414 | "origin": 396000000,
415 | "oriPromo": 396000000,
416 | "descend": 0,
417 | "save": 0,
418 | "sum": 396000000,
419 | "actual": 0,
420 | "prepay": 0,
421 | "finalpay": 0,
422 | "extraCharges": 0
423 | },
424 | "amount": {
425 | "now": 1,
426 | "max": 2,
427 | "limit": 9223372036854775807,
428 | "multiple": 1,
429 | "supply": 0,
430 | "demand": 0
431 | },
432 | "itemIcon": {
433 | "CART_EBOOK": [],
434 | "CART_XIAOBAO": [
435 | {
436 | "desc": "如实描述",
437 | "title": "消费者保障服务,卖家承诺商品如实描述",
438 | "link": "http://www.taobao.com/go/act/315/xfzbz_rsms.php?ad_id=&am_id=130011830696bce9eda3&cm_id=&pm_id=",
439 | "img": "http://img03.taobaocdn.com/tps/i3/T1bnR4XEBhXXcQVo..-14-16.png"
440 | }
441 | ],
442 | "CART_YULIU": [],
443 | "CART_IDENTITY": []
444 | },
445 | "isDouble11": false,
446 | "isDouble11halfDiscount": false,
447 | "isCod": false,
448 | "isAttention": true,
449 | "createTime": 1386734805000,
450 | "attr": ";op:396000000;cityCode:330100;",
451 | "preference": false,
452 | "isSellerPayPostfee": false,
453 | "leafCategory": 0,
454 | "cumulativeSales": 0,
455 | "skuStatus": 3,
456 | "cartActiveInfo": {
457 | "isDefault": false,
458 | "wantStatus": 0,
459 | "endTime": 0,
460 | "cartBcParams": "buyerCondition~0~~cartCreateTime~1386734805000",
461 | "type": 0
462 | }
463 | },
464 | {
465 | "id": "40865005023",
466 | "itemId": "36250290617",
467 | "skuId": "52649772565",
468 | "cartId": "40865005023",
469 | "isValid": false,
470 | "url": "http://item.taobao.com/item.htm?id=36250290617",
471 | "pic": "http://img04.taobaocdn.com/bao/uploaded/i4/695609583/T2OGnNXfpbXXXXXXXX_!!695609583.jpg",
472 | "title": "兰博基尼LP560-4",
473 | "weight": 0,
474 | "skus": {
475 | "颜色分类": "白色"
476 | },
477 | "shopId": "106555610",
478 | "shopName": "傲世奢侈品交易平臺",
479 | "shopUrl": "http://store.taobao.com/shop/view_shop.htm?user_number_id=695609583",
480 | "seller": "edt無道",
481 | "sellerId": 695609583,
482 | "price": {
483 | "now": 348000000,
484 | "origin": 348000000,
485 | "oriPromo": 348000000,
486 | "descend": 0,
487 | "save": 0,
488 | "sum": 3480000000,
489 | "actual": 0,
490 | "prepay": 0,
491 | "finalpay": 0,
492 | "extraCharges": 0
493 | },
494 | "amount": {
495 | "now": 10,
496 | "max": 10,
497 | "limit": 9223372036854775807,
498 | "multiple": 1,
499 | "supply": 0,
500 | "demand": 0
501 | },
502 | "itemIcon": {
503 | "CART_EBOOK": [],
504 | "CART_XIAOBAO": [
505 | {
506 | "desc": "如实描述",
507 | "title": "消费者保障服务,卖家承诺商品如实描述",
508 | "link": "http://www.taobao.com/go/act/315/xfzbz_rsms.php?ad_id=&am_id=130011830696bce9eda3&cm_id=&pm_id=",
509 | "img": "http://img03.taobaocdn.com/tps/i3/T1bnR4XEBhXXcQVo..-14-16.png"
510 | }
511 | ],
512 | "CART_YULIU": [],
513 | "CART_IDENTITY": []
514 | },
515 | "isDouble11": false,
516 | "isDouble11halfDiscount": false,
517 | "isCod": false,
518 | "isAttention": true,
519 | "createTime": 1386645818000,
520 | "attr": ";op:348000000;cityCode:330100;",
521 | "preference": false,
522 | "isSellerPayPostfee": false,
523 | "leafCategory": 0,
524 | "cumulativeSales": 0,
525 | "skuStatus": 3,
526 | "cartActiveInfo": {
527 | "isDefault": false,
528 | "wantStatus": 0,
529 | "endTime": 0,
530 | "cartBcParams": "buyerCondition~0~~cartCreateTime~1386645818000",
531 | "type": 0
532 | }
533 | }
534 | ],
535 | "type": "shop",
536 | "valid": true
537 | }
538 | ]
539 | },
540 | {
541 | "id": "s_435179831",
542 | "title": "美美——",
543 | "type": "shop",
544 | "url": "http://store.taobao.com/shop/view_shop.htm?user_number_id=435179831",
545 | "seller": "c测试账号136",
546 | "host": "C",
547 | "shopId": 62384964,
548 | "sellerId": "435179831",
549 | "isValid": true,
550 | "scrollPromos": [
551 | "
单笔订单满2件,免邮",
552 | "单笔订单满3件,免邮免邮",
553 | "单笔订单满4件,免邮免邮免邮"
554 | ],
555 | "hasPriceVolume": false,
556 | "bundles": [
557 | {
558 | "id": "s_435179831_0",
559 | "orders": [
560 | {
561 | "id": "45051164095",
562 | "itemId": "35327382733",
563 | "skuId": "33649369706",
564 | "cartId": "45051164095",
565 | "isValid": true,
566 | "url": "http://item.taobao.com/item.htm?id=35327382733",
567 | "pic": "http://img03.taobaocdn.com/bao/uploaded/i3/19831041340172207/T1w1tEFatdXXXXXXXX_!!0-item_pic.jpg",
568 | "title": "测试商品不要折--商品优惠券请不要拍",
569 | "weight": 0,
570 | "skus": {
571 | "颜色分类": "深紫色"
572 | },
573 | "shopId": "62384964",
574 | "shopName": "美美——",
575 | "shopUrl": "http://store.taobao.com/shop/view_shop.htm?user_number_id=435179831",
576 | "seller": "c测试账号136",
577 | "sellerId": 435179831,
578 | "price": {
579 | "now": 1,
580 | "origin": 10000,
581 | "oriPromo": 5000,
582 | "descend": 0,
583 | "save": 9999,
584 | "sum": 15,
585 | "actual": 0,
586 | "prepay": 0,
587 | "finalpay": 0,
588 | "extraCharges": 0
589 | },
590 | "amount": {
591 | "now": 15,
592 | "max": 87,
593 | "limit": 9223372036854775807,
594 | "multiple": 1,
595 | "supply": 0,
596 | "demand": 0
597 | },
598 | "itemIcon": {
599 | "CART_EBOOK": [],
600 | "CART_XIAOBAO": [
601 | {
602 | "desc": "如实描述",
603 | "title": "消费者保障服务,卖家承诺商品如实描述",
604 | "link": "http://www.taobao.com/go/act/315/xfzbz_rsms.php?ad_id=&am_id=130011830696bce9eda3&cm_id=&pm_id=",
605 | "img": "http://img03.taobaocdn.com/tps/i3/T1bnR4XEBhXXcQVo..-14-16.png"
606 | }
607 | ],
608 | "CART_YULIU": [],
609 | "CART_IDENTITY": []
610 | },
611 | "isDouble11": false,
612 | "isDouble11halfDiscount": false,
613 | "isCod": false,
614 | "isAttention": true,
615 | "createTime": 1389238670000,
616 | "attr": ";op:10000;",
617 | "preference": false,
618 | "isSellerPayPostfee": false,
619 | "leafCategory": 0,
620 | "cumulativeSales": 0,
621 | "skuStatus": 2,
622 | "cartActiveInfo": {
623 | "isDefault": false,
624 | "wantStatus": 0,
625 | "endTime": 0,
626 | "cartBcParams": "buyerCondition~0~~cartCreateTime~1389238670000",
627 | "type": 0
628 | }
629 | },
630 | {
631 | "id": "40263066148",
632 | "itemId": "36411580369",
633 | "skuId": "0",
634 | "cartId": "40263066148",
635 | "isValid": true,
636 | "url": "http://item.taobao.com/item.htm?id=36411580369",
637 | "pic": "http://img03.taobaocdn.com/bao/uploaded/i3/T1w1tEFatdXXXXXXXX_!!0-item_pic.jpg",
638 | "title": "[1212预演] 大促无SKU商品----不要拍测试请不要拍",
639 | "weight": 0,
640 | "shopId": "62384964",
641 | "shopName": "美美——",
642 | "shopUrl": "http://store.taobao.com/shop/view_shop.htm?user_number_id=435179831",
643 | "seller": "c测试账号136",
644 | "sellerId": 435179831,
645 | "price": {
646 | "now": 10000,
647 | "origin": 10000,
648 | "oriPromo": 1900,
649 | "descend": 0,
650 | "save": 0,
651 | "sum": 10000,
652 | "actual": 0,
653 | "prepay": 0,
654 | "finalpay": 0,
655 | "extraCharges": 0
656 | },
657 | "amount": {
658 | "now": 1,
659 | "max": 10,
660 | "limit": 9223372036854775807,
661 | "multiple": 1,
662 | "supply": 0,
663 | "demand": 0
664 | },
665 | "itemIcon": {
666 | "CART_EBOOK": [],
667 | "CART_XIAOBAO": [
668 | {
669 | "desc": "如实描述",
670 | "title": "消费者保障服务,卖家承诺商品如实描述",
671 | "link": "http://www.taobao.com/go/act/315/xfzbz_rsms.php?ad_id=&am_id=130011830696bce9eda3&cm_id=&pm_id=",
672 | "img": "http://img03.taobaocdn.com/tps/i3/T1bnR4XEBhXXcQVo..-14-16.png"
673 | }
674 | ],
675 | "CART_YULIU": [],
676 | "CART_IDENTITY": []
677 | },
678 | "isDouble11": false,
679 | "isDouble11halfDiscount": false,
680 | "isCod": false,
681 | "isAttention": true,
682 | "createTime": 1386225437000,
683 | "attr": ";op:10000;",
684 | "preference": false,
685 | "isSellerPayPostfee": false,
686 | "leafCategory": 0,
687 | "cumulativeSales": 0,
688 | "skuStatus": 0,
689 | "cartActiveInfo": {
690 | "isDefault": false,
691 | "wantStatus": 0,
692 | "endTime": 0,
693 | "cartBcParams": "buyerCondition~0~~cartCreateTime~1386225437000",
694 | "type": 0
695 | }
696 | },
697 | {
698 | "id": "40289287003",
699 | "itemId": "36313978495",
700 | "skuId": "36104069226",
701 | "cartId": "40289287003",
702 | "isValid": false,
703 | "url": "http://item.taobao.com/item.htm?id=36313978495",
704 | "pic": "http://img01.taobaocdn.com/bao/uploaded/i1/19831031291397475/T1KEnLFj0XXXXXXXXX_!!2-item_pic.png",
705 | "title": "skuskuku测试数据不要拍请不要拍",
706 | "weight": 0,
707 | "skus": {
708 | "颜色分类": "巧克力色",
709 | "尺码": "大码XXL"
710 | },
711 | "shopId": "62384964",
712 | "shopName": "美美——",
713 | "shopUrl": "http://store.taobao.com/shop/view_shop.htm?user_number_id=435179831",
714 | "seller": "c测试账号136",
715 | "sellerId": 435179831,
716 | "price": {
717 | "now": 8000,
718 | "origin": 8000,
719 | "oriPromo": 7744,
720 | "descend": 0,
721 | "save": 0,
722 | "sum": 8000,
723 | "actual": 0,
724 | "prepay": 0,
725 | "finalpay": 0,
726 | "extraCharges": 0
727 | },
728 | "amount": {
729 | "now": 1,
730 | "max": 2,
731 | "limit": 9223372036854775807,
732 | "multiple": 1,
733 | "supply": 0,
734 | "demand": 0
735 | },
736 | "itemIcon": {
737 | "CART_EBOOK": [],
738 | "CART_XIAOBAO": [
739 | {
740 | "desc": "如实描述",
741 | "title": "消费者保障服务,卖家承诺商品如实描述",
742 | "link": "http://www.taobao.com/go/act/315/xfzbz_rsms.php?ad_id=&am_id=130011830696bce9eda3&cm_id=&pm_id=",
743 | "img": "http://img03.taobaocdn.com/tps/i3/T1bnR4XEBhXXcQVo..-14-16.png"
744 | }
745 | ],
746 | "CART_YULIU": [],
747 | "CART_IDENTITY": []
748 | },
749 | "isDouble11": false,
750 | "isDouble11halfDiscount": false,
751 | "isCod": false,
752 | "isAttention": true,
753 | "createTime": 1386237405000,
754 | "attr": ";op:8000;cityCode:330100;",
755 | "preference": false,
756 | "isSellerPayPostfee": false,
757 | "leafCategory": 0,
758 | "cumulativeSales": 0,
759 | "skuStatus": 3,
760 | "cartActiveInfo": {
761 | "isDefault": false,
762 | "wantStatus": 0,
763 | "endTime": 0,
764 | "cartBcParams": "buyerCondition~0~~cartCreateTime~1386237405000",
765 | "type": 0
766 | }
767 | }
768 | ],
769 | "type": "shop",
770 | "valid": true
771 | }
772 | ]
773 | },
774 | {
775 | "id": "s_408561270",
776 | "title": "陈秀羽服饰",
777 | "type": "shop",
778 | "url": "http://store.taobao.com/shop/view_shop.htm?user_number_id=408561270",
779 | "seller": "陀螺糖",
780 | "host": "C",
781 | "shopId": 61576170,
782 | "sellerId": "408561270",
783 | "isValid": true,
784 | "hasPriceVolume": true,
785 | "bundles": [
786 | {
787 | "id": "s_408561270_0",
788 | "orders": [
789 | {
790 | "id": "44237671039",
791 | "itemId": "36396805073",
792 | "skuId": "36384105920",
793 | "cartId": "44237671039",
794 | "isValid": true,
795 | "url": "http://item.taobao.com/item.htm?id=36396805073",
796 | "pic": "http://img02.taobaocdn.com/bao/uploaded/i2/408561270/T2s8tKXthaXXXXXXXX_!!408561270.jpg",
797 | "title": "慧川卉美 毛呢外套 2013冬季新款 蕾丝拼接 清新优雅中长款女装",
798 | "weight": 0,
799 | "skus": {
800 | "颜色分类": "西瓜红",
801 | "尺码": "S"
802 | },
803 | "shopId": "61576170",
804 | "shopName": "陈秀羽服饰",
805 | "shopUrl": "http://store.taobao.com/shop/view_shop.htm?user_number_id=408561270",
806 | "seller": "陀螺糖",
807 | "sellerId": 408561270,
808 | "price": {
809 | "now": 29500,
810 | "origin": 31200,
811 | "oriPromo": 26500,
812 | "descend": 0,
813 | "save": 1700,
814 | "sum": 29500,
815 | "actual": 0,
816 | "prepay": 0,
817 | "finalpay": 0,
818 | "extraCharges": 0
819 | },
820 | "amount": {
821 | "now": 1,
822 | "max": 10,
823 | "limit": 9223372036854775807,
824 | "multiple": 1,
825 | "supply": 0,
826 | "demand": 0
827 | },
828 | "itemIcon": {
829 | "CART_EBOOK": [],
830 | "CART_XIAOBAO": [
831 | {
832 | "desc": "如实描述",
833 | "title": "消费者保障服务,卖家承诺商品如实描述",
834 | "link": "http://www.taobao.com/go/act/315/xfzbz_rsms.php?ad_id=&am_id=130011830696bce9eda3&cm_id=&pm_id=",
835 | "img": "http://img03.taobaocdn.com/tps/i3/T1bnR4XEBhXXcQVo..-14-16.png"
836 | }
837 | ],
838 | "CART_YULIU": [
839 | {
840 | "title": "支持信用卡支付",
841 | "img": "http://assets.taobaocdn.com/sys/common/icon/trade/xcard.png"
842 | }
843 | ],
844 | "CART_IDENTITY": []
845 | },
846 | "isDouble11": false,
847 | "isDouble11halfDiscount": false,
848 | "isCod": false,
849 | "isAttention": true,
850 | "createTime": 1388657451000,
851 | "attr": ";op:31200;cityCode:330100;",
852 | "preference": false,
853 | "isSellerPayPostfee": false,
854 | "leafCategory": 0,
855 | "cumulativeSales": 0,
856 | "skuStatus": 2,
857 | "cartActiveInfo": {
858 | "isDefault": false,
859 | "wantStatus": 0,
860 | "endTime": 0,
861 | "cartBcParams": "buyerCondition~0~~cartCreateTime~1388657451000",
862 | "type": 0
863 | }
864 | }
865 | ],
866 | "type": "shop",
867 | "valid": true
868 | }
869 | ]
870 | },
871 | {
872 | "id": "s_94269988",
873 | "title": "我的B2C小店4",
874 | "type": "shop",
875 | "url": "http://store.taobao.com/shop/view_shop.htm?user_number_id=94269988",
876 | "seller": "商家测试帐号9",
877 | "host": "B",
878 | "shopId": 57298338,
879 | "sellerId": "94269988",
880 | "isValid": true,
881 | "scrollPromos": [
882 | "\r\n \t\t \t\t\t\t\t\t\t\t\t满100元\r\n\t\t\t\t\t减10元\t\t\t\t\t,免运费\t\t\t\t\t,减后满100元\t\t\t\t\t,送1商城积分\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t"
883 | ],
884 | "hasPriceVolume": false,
885 | "promView": {
886 | "title": "省10.00元:满就送",
887 | "discount": 1000,
888 | "point": 0,
889 | "usePoint": 0,
890 | "isSelected": true,
891 | "value": "mjs-94269988_132268118"
892 | },
893 | "bundles": [
894 | {
895 | "id": "s_94269988_0",
896 | "orders": [
897 | {
898 | "id": "40767079536",
899 | "itemId": "15733416213",
900 | "skuId": "0",
901 | "cartId": "40767079536",
902 | "isValid": true,
903 | "url": "http://detail.tmall.com/item.htm?id=15733416213",
904 | "pic": "http://img01.taobaocdn.com/bao/uploaded/i1/T1F.yJXbRsXXbtr.wW_025126.jpg_sum.jpg",
905 | "title": "【官方测试商品,不要拍】走UMP单品优惠-琅华",
906 | "weight": 0,
907 | "shopId": "57298338",
908 | "shopName": "我的B2C小店4",
909 | "shopUrl": "http://store.taobao.com/shop/view_shop.htm?user_number_id=94269988",
910 | "seller": "商家测试帐号9",
911 | "sellerId": 94269988,
912 | "price": {
913 | "now": 6000000100,
914 | "origin": 6000000100,
915 | "oriPromo": 5999919800,
916 | "descend": 0,
917 | "save": 0,
918 | "sum": 30000000500,
919 | "actual": 0,
920 | "prepay": 0,
921 | "finalpay": 0,
922 | "extraCharges": 0
923 | },
924 | "amount": {
925 | "now": 5,
926 | "max": 99,
927 | "limit": 9223372036854775807,
928 | "multiple": 1,
929 | "supply": 0,
930 | "demand": 0
931 | },
932 | "itemIcon": {
933 | "MALL_CART_IDENTITY": [],
934 | "MALL_CART_YULIU": [],
935 | "MALL_CART_XIAOBAO": [
936 | {
937 | "title": "消费者保障服务,卖家承诺7天退换",
938 | "link": "http://www.taobao.com/go/act/315/xb_20100707.php?ad_id=&am_id=1300268931aef04f0cdc&cm_id=&pm_id=#qitian",
939 | "img": "http://a.tbcdn.cn/tbsp/icon/xiaobao/a_7-day_return_16x16.png",
940 | "name": "七天退换"
941 | },
942 | {
943 | "title": "消费者保障服务,卖家承诺如实描述",
944 | "link": "http://www.taobao.com/go/act/315/xfzbz_rsms.php?ad_id=&am_id=130011830696bce9eda3&cm_id=&pm_id=",
945 | "img": "http://a.tbcdn.cn/tbsp/icon/xiaobao/a_true_description_16x16.png",
946 | "name": "如实描述"
947 | },
948 | {
949 | "title": "消费者保障服务,卖家承诺假一赔三",
950 | "link": "http://www.taobao.com/go/act/315/xfzbz_jyps.php?ad_id=&am_id=1300118304240d56fca9&cm_id=&pm_id=",
951 | "img": "http://a.tbcdn.cn/tbsp/icon/xiaobao/an_authentic_item_16x16.png",
952 | "name": "假一赔三"
953 | }
954 | ]
955 | },
956 | "campaignId": "0",
957 | "isDouble11": false,
958 | "isDouble11halfDiscount": false,
959 | "isCod": false,
960 | "isAttention": true,
961 | "createTime": 1386555127000,
962 | "attr": ";campaignId:0;op:6000000100;cityCode:330100;",
963 | "preference": false,
964 | "isSellerPayPostfee": false,
965 | "leafCategory": 0,
966 | "cumulativeSales": 0,
967 | "skuStatus": 0,
968 | "cartActiveInfo": {
969 | "isDefault": false,
970 | "wantStatus": 0,
971 | "endTime": 0,
972 | "cartBcParams": "buyerCondition~0~~cartCreateTime~1386555127000",
973 | "type": 0
974 | }
975 | }
976 | ],
977 | "type": "shop",
978 | "valid": true
979 | }
980 | ]
981 | },
982 | {
983 | "id": "s_592995015",
984 | "title": "妖精づ鞋柜",
985 | "type": "shop",
986 | "url": "http://store.taobao.com/shop/view_shop.htm?user_number_id=592995015",
987 | "seller": "晨馨眼镜",
988 | "host": "C",
989 | "shopId": 63309206,
990 | "sellerId": "592995015",
991 | "isValid": true,
992 | "hasPriceVolume": false,
993 | "bundles": [
994 | {
995 | "id": "s_592995015_0",
996 | "orders": [
997 | {
998 | "id": "40168833186",
999 | "itemId": "19836221588",
1000 | "skuId": "36834499974",
1001 | "cartId": "40168833186",
1002 | "isValid": true,
1003 | "url": "http://item.taobao.com/item.htm?id=19836221588",
1004 | "pic": "http://img03.taobaocdn.com/bao/uploaded/i3/15015039186552052/T1daGuFjXbXXXXXXXX_!!0-item_pic.jpg",
1005 | "title": "包邮2013秋季新款甜美蕾丝帆布鞋 女 可爱小熊高帮牛筋底学生板鞋",
1006 | "weight": 0,
1007 | "skus": {
1008 | "颜色分类": "浅蓝_小熊款",
1009 | "尺码": "38_标准码_现货"
1010 | },
1011 | "shopId": "63309206",
1012 | "shopName": "妖精づ鞋柜",
1013 | "shopUrl": "http://store.taobao.com/shop/view_shop.htm?user_number_id=592995015",
1014 | "seller": "晨馨眼镜",
1015 | "sellerId": 592995015,
1016 | "price": {
1017 | "now": 3000,
1018 | "origin": 1600,
1019 | "oriPromo": 3000,
1020 | "descend": 0,
1021 | "save": 0,
1022 | "sum": 21000,
1023 | "actual": 0,
1024 | "prepay": 0,
1025 | "finalpay": 0,
1026 | "extraCharges": 0
1027 | },
1028 | "amount": {
1029 | "now": 7,
1030 | "max": 877,
1031 | "limit": 9223372036854775807,
1032 | "multiple": 1,
1033 | "supply": 0,
1034 | "demand": 0
1035 | },
1036 | "itemIcon": {
1037 | "CART_EBOOK": [],
1038 | "CART_XIAOBAO": [
1039 | {
1040 | "desc": "如实描述",
1041 | "title": "消费者保障服务,卖家承诺商品如实描述",
1042 | "link": "http://www.taobao.com/go/act/315/xfzbz_rsms.php?ad_id=&am_id=130011830696bce9eda3&cm_id=&pm_id=",
1043 | "img": "http://img03.taobaocdn.com/tps/i3/T1bnR4XEBhXXcQVo..-14-16.png"
1044 | }
1045 | ],
1046 | "CART_YULIU": [
1047 | {
1048 | "title": "支持信用卡支付",
1049 | "img": "http://assets.taobaocdn.com/sys/common/icon/trade/xcard.png"
1050 | }
1051 | ],
1052 | "CART_IDENTITY": []
1053 | },
1054 | "isDouble11": false,
1055 | "isDouble11halfDiscount": false,
1056 | "isCod": false,
1057 | "isAttention": true,
1058 | "createTime": 1386156674000,
1059 | "attr": ";op:1600;cityCode:330100;",
1060 | "preference": false,
1061 | "isSellerPayPostfee": false,
1062 | "leafCategory": 0,
1063 | "cumulativeSales": 0,
1064 | "skuStatus": 2,
1065 | "cartActiveInfo": {
1066 | "isDefault": false,
1067 | "wantStatus": 0,
1068 | "endTime": 0,
1069 | "cartBcParams": "buyerCondition~0~~cartCreateTime~1386156674000",
1070 | "type": 0
1071 | }
1072 | }
1073 | ],
1074 | "type": "shop",
1075 | "valid": true
1076 | }
1077 | ]
1078 | },
1079 | {
1080 | "id": "s_84815057",
1081 | "title": "优木园艺",
1082 | "type": "shop",
1083 | "url": "http://store.taobao.com/shop/view_shop.htm?user_number_id=84815057",
1084 | "seller": "imalexli",
1085 | "host": "C",
1086 | "shopId": 35851735,
1087 | "sellerId": "84815057",
1088 | "isValid": true,
1089 | "scrollPromos": [
1090 | "\r\n \t\t \t\t\t\t\t\t\t\t\t满49元\r\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t,送多菌灵\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t",
1091 | "\r\n \t\t \t\t\t\t\t\t\t\t\t满99元\r\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t,送多肉植物专用毛刷\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t",
1092 | "\r\n\t\t\t\t\t\t\t\t\t\t\t满200元\r\n\t\t\t\t\t ,送2条10g装美乐棵 ,可在购物车换购商品\t\t\t\t\t\t\t\t\t\t\t\t\t\t",
1093 | "\r\n\t\t\t\t\t\t\t\t\t\t\t满520元\r\n\t\t\t\t\t减10元 ,减后满520元 , 送5元店铺优惠券 ,送美乐棵液体肥 ,送1注彩票 ,可在购物车换购商品\t\t\t\t\t\t\t\t\t\t\t\t\t\t"
1094 | ],
1095 | "hasPriceVolume": false,
1096 | "bundles": [
1097 | {
1098 | "id": "s_84815057_0",
1099 | "orders": [
1100 | {
1101 | "id": "39716263498",
1102 | "itemId": "18201654273",
1103 | "skuId": "0",
1104 | "cartId": "39716263498",
1105 | "isValid": true,
1106 | "url": "http://item.taobao.com/item.htm?id=18201654273",
1107 | "pic": "http://img03.taobaocdn.com/bao/uploaded/i3/T1fXQnFkVdXXXXXXXX_!!0-item_pic.jpg",
1108 | "title": "美乐棵 家庭园艺肥料 通用型 500g 大包装更划算",
1109 | "weight": 0,
1110 | "shopId": "35851735",
1111 | "shopName": "优木园艺",
1112 | "shopUrl": "http://store.taobao.com/shop/view_shop.htm?user_number_id=84815057",
1113 | "seller": "imalexli",
1114 | "sellerId": 84815057,
1115 | "price": {
1116 | "now": 4500,
1117 | "origin": 4500,
1118 | "oriPromo": 4500,
1119 | "descend": 0,
1120 | "save": 0,
1121 | "sum": 4500,
1122 | "actual": 0,
1123 | "prepay": 0,
1124 | "finalpay": 0,
1125 | "extraCharges": 0
1126 | },
1127 | "amount": {
1128 | "now": 1,
1129 | "max": 11,
1130 | "limit": 9223372036854776000,
1131 | "multiple": 1,
1132 | "supply": 0,
1133 | "demand": 0
1134 | },
1135 | "itemIcon": {
1136 | "CART_EBOOK": [],
1137 | "CART_XIAOBAO": [
1138 | {
1139 | "desc": "如实描述",
1140 | "title": "消费者保障服务,卖家承诺商品如实描述",
1141 | "link": "http://www.taobao.com/go/act/315/xfzbz_rsms.php?ad_id=&am_id=130011830696bce9eda3&cm_id=&pm_id=",
1142 | "img": "http://img03.taobaocdn.com/tps/i3/T1bnR4XEBhXXcQVo..-14-16.png"
1143 | }
1144 | ],
1145 | "CART_YULIU": [
1146 | {
1147 | "title": "支持信用卡支付",
1148 | "img": "http://assets.taobaocdn.com/sys/common/icon/trade/xcard.png"
1149 | }
1150 | ],
1151 | "CART_IDENTITY": []
1152 | },
1153 | "isDouble11": false,
1154 | "isDouble11halfDiscount": false,
1155 | "isCod": false,
1156 | "isAttention": true,
1157 | "createTime": 1385604205000,
1158 | "attr": ";op:4500;",
1159 | "preference": false,
1160 | "isSellerPayPostfee": false,
1161 | "leafCategory": 0,
1162 | "cumulativeSales": 0,
1163 | "skuStatus": 0,
1164 | "cartActiveInfo": {
1165 | "isDefault": false,
1166 | "wantStatus": 0,
1167 | "endTime": 0,
1168 | "cartBcParams": "buyerCondition~0~~cartCreateTime~1385604205000",
1169 | "type": 0
1170 | }
1171 | }
1172 | ],
1173 | "type": "shop",
1174 | "valid": true
1175 | }
1176 | ]
1177 | },
1178 | {
1179 | "id": "s_28011101",
1180 | "title": "菲然小屋",
1181 | "type": "shop",
1182 | "url": "http://store.taobao.com/shop/view_shop.htm?user_number_id=28011101",
1183 | "seller": "aoyoucheny",
1184 | "host": "C",
1185 | "shopId": 33385218,
1186 | "sellerId": "28011101",
1187 | "isValid": true,
1188 | "hasPriceVolume": false,
1189 | "bundles": [
1190 | {
1191 | "id": "s_28011101_0",
1192 | "orders": [
1193 | {
1194 | "id": "39840397057",
1195 | "itemId": "20174283014",
1196 | "skuId": "32568327374",
1197 | "cartId": "39840397057",
1198 | "isValid": true,
1199 | "url": "http://item.taobao.com/item.htm?id=20174283014",
1200 | "pic": "http://img01.taobaocdn.com/bao/uploaded/i1/28011101/T21BCmXdxcXXXXXXXX_!!28011101.jpg",
1201 | "title": "夏装2014新款卡其可可童装女童儿童蕾丝网纱裙公主裙礼服裙子童裙",
1202 | "weight": 0,
1203 | "skus": {
1204 | "颜色分类": "81933-蓝色#",
1205 | "参考身高": "100cm"
1206 | },
1207 | "shopId": "33385218",
1208 | "shopName": "菲然小屋",
1209 | "shopUrl": "http://store.taobao.com/shop/view_shop.htm?user_number_id=28011101",
1210 | "seller": "aoyoucheny",
1211 | "sellerId": 28011101,
1212 | "price": {
1213 | "now": 8800,
1214 | "origin": 8800,
1215 | "oriPromo": 7200,
1216 | "descend": 0,
1217 | "save": 0,
1218 | "sum": 8800,
1219 | "actual": 0,
1220 | "prepay": 0,
1221 | "finalpay": 0,
1222 | "extraCharges": 0
1223 | },
1224 | "amount": {
1225 | "now": 1,
1226 | "max": 1,
1227 | "limit": 9223372036854776000,
1228 | "multiple": 1,
1229 | "supply": 1,
1230 | "demand": 0
1231 | },
1232 | "itemIcon": {
1233 | "CART_EBOOK": [],
1234 | "CART_XIAOBAO": [
1235 | {
1236 | "desc": "如实描述",
1237 | "title": "消费者保障服务,卖家承诺商品如实描述",
1238 | "link": "http://www.taobao.com/go/act/315/xfzbz_rsms.php?ad_id=&am_id=130011830696bce9eda3&cm_id=&pm_id=",
1239 | "img": "http://img03.taobaocdn.com/tps/i3/T1bnR4XEBhXXcQVo..-14-16.png"
1240 | },
1241 | {
1242 | "desc": "7天退换",
1243 | "title": "消费者保障服务,卖家承诺7天无理由退换货",
1244 | "link": "http://www.taobao.com/go/act/315/xbqt090304.php?ad_id=&am_id=130011831021c2f3caab&cm_id=&pm_id=",
1245 | "img": "http://img04.taobaocdn.com/tps/i4/T16lKeXwFcXXadE...-14-15.png"
1246 | }
1247 | ],
1248 | "CART_YULIU": [
1249 | {
1250 | "title": "支持信用卡支付",
1251 | "img": "http://assets.taobaocdn.com/sys/common/icon/trade/xcard.png"
1252 | }
1253 | ],
1254 | "CART_IDENTITY": []
1255 | },
1256 | "isDouble11": false,
1257 | "isDouble11halfDiscount": false,
1258 | "isCod": false,
1259 | "isAttention": true,
1260 | "createTime": 1385573556000,
1261 | "attr": ";op:8800;",
1262 | "preference": false,
1263 | "isSellerPayPostfee": false,
1264 | "leafCategory": 0,
1265 | "cumulativeSales": 0,
1266 | "skuStatus": 2,
1267 | "cartActiveInfo": {
1268 | "isDefault": false,
1269 | "wantStatus": 0,
1270 | "endTime": 0,
1271 | "cartBcParams": "buyerCondition~0~~cartCreateTime~1385573556000",
1272 | "type": 0
1273 | }
1274 | }
1275 | ],
1276 | "type": "shop",
1277 | "valid": true
1278 | }
1279 | ]
1280 | },
1281 | {
1282 | "id": "s_682508620",
1283 | "title": "淑美玩具",
1284 | "type": "shop",
1285 | "url": "http://store.taobao.com/shop/view_shop.htm?user_number_id=682508620",
1286 | "seller": "cfwd2011",
1287 | "host": "C",
1288 | "shopId": 65608623,
1289 | "sellerId": "682508620",
1290 | "isValid": true,
1291 | "hasPriceVolume": true,
1292 | "bundles": [
1293 | {
1294 | "id": "s_682508620_0",
1295 | "orders": [
1296 | {
1297 | "id": "39840397058",
1298 | "itemId": "17305114097",
1299 | "skuId": "0",
1300 | "cartId": "39840397058",
1301 | "isValid": true,
1302 | "url": "http://item.taobao.com/item.htm?id=17305114097",
1303 | "pic": "http://img02.taobaocdn.com/bao/uploaded/i2/18620019764649808/T1KZ0eXBXhXXXXXXXX_!!0-item_pic.jpg",
1304 | "title": "志扬玩具 滑翔机 遥控飞机 直升机 固定翼EPP 航模 耐摔",
1305 | "weight": 0,
1306 | "shopId": "65608623",
1307 | "shopName": "淑美玩具",
1308 | "shopUrl": "http://store.taobao.com/shop/view_shop.htm?user_number_id=682508620",
1309 | "seller": "cfwd2011",
1310 | "sellerId": 682508620,
1311 | "price": {
1312 | "now": 11800,
1313 | "origin": 11800,
1314 | "oriPromo": 11800,
1315 | "descend": 0,
1316 | "save": 0,
1317 | "sum": 11800,
1318 | "actual": 0,
1319 | "prepay": 0,
1320 | "finalpay": 0,
1321 | "extraCharges": 0
1322 | },
1323 | "amount": {
1324 | "now": 1,
1325 | "max": 93,
1326 | "limit": 9223372036854776000,
1327 | "multiple": 1,
1328 | "supply": 0,
1329 | "demand": 0
1330 | },
1331 | "itemIcon": {
1332 | "CART_EBOOK": [],
1333 | "CART_XIAOBAO": [
1334 | {
1335 | "desc": "如实描述",
1336 | "title": "消费者保障服务,卖家承诺商品如实描述",
1337 | "link": "http://www.taobao.com/go/act/315/xfzbz_rsms.php?ad_id=&am_id=130011830696bce9eda3&cm_id=&pm_id=",
1338 | "img": "http://img03.taobaocdn.com/tps/i3/T1bnR4XEBhXXcQVo..-14-16.png"
1339 | },
1340 | {
1341 | "desc": "7天退换",
1342 | "title": "消费者保障服务,卖家承诺7天无理由退换货",
1343 | "link": "http://www.taobao.com/go/act/315/xbqt090304.php?ad_id=&am_id=130011831021c2f3caab&cm_id=&pm_id=",
1344 | "img": "http://img04.taobaocdn.com/tps/i4/T16lKeXwFcXXadE...-14-15.png"
1345 | }
1346 | ],
1347 | "CART_YULIU": [
1348 | {
1349 | "title": "支持信用卡支付",
1350 | "img": "http://assets.taobaocdn.com/sys/common/icon/trade/xcard.png"
1351 | }
1352 | ],
1353 | "CART_IDENTITY": []
1354 | },
1355 | "isDouble11": false,
1356 | "isDouble11halfDiscount": false,
1357 | "isCod": false,
1358 | "isAttention": true,
1359 | "createTime": 1385573541000,
1360 | "attr": ";op:11800;",
1361 | "preference": false,
1362 | "isSellerPayPostfee": false,
1363 | "leafCategory": 0,
1364 | "cumulativeSales": 0,
1365 | "skuStatus": 0,
1366 | "cartActiveInfo": {
1367 | "isDefault": false,
1368 | "wantStatus": 0,
1369 | "endTime": 0,
1370 | "cartBcParams": "buyerCondition~0~~cartCreateTime~1385573541000",
1371 | "type": 0
1372 | }
1373 | }
1374 | ],
1375 | "type": "shop",
1376 | "valid": true
1377 | }
1378 | ]
1379 | },
1380 | {
1381 | "id": "s_1103141522",
1382 | "title": "维依恋旗舰店",
1383 | "type": "shop",
1384 | "url": "http://store.taobao.com/shop/view_shop.htm?user_number_id=1103141522",
1385 | "seller": "维依恋旗舰店",
1386 | "host": "B",
1387 | "shopId": 101488084,
1388 | "sellerId": "1103141522",
1389 | "isValid": true,
1390 | "hasPriceVolume": false,
1391 | "promView": {
1392 | "title": "省0.00元:3.8生活节",
1393 | "discount": 0,
1394 | "point": 0,
1395 | "usePoint": 0,
1396 | "isSelected": true,
1397 | "value": "Tmall$tspAll-27484074"
1398 | },
1399 | "bundles": [
1400 | {
1401 | "id": "s_1103141522_1",
1402 | "orders": [
1403 | {
1404 | "id": "38105149506",
1405 | "itemId": "19182186172",
1406 | "skuId": "30291521265",
1407 | "cartId": "38105149506",
1408 | "isValid": true,
1409 | "url": "http://detail.tmall.com/item.htm?id=19182186172",
1410 | "pic": "http://img04.taobaocdn.com/bao/uploaded/i4/1103141522/T2FCMPXmpXXXXXXXXX_!!1103141522.jpg_sum.jpg",
1411 | "title": "维依恋2014春装新款气质女装包臀修身长袖连衣裙春秋针织打底裙子",
1412 | "weight": 0,
1413 | "skus": {
1414 | "颜色分类": "紫色",
1415 | "尺码": "M"
1416 | },
1417 | "shopId": "101488084",
1418 | "shopName": "维依恋旗舰店",
1419 | "shopUrl": "http://store.taobao.com/shop/view_shop.htm?user_number_id=1103141522",
1420 | "seller": "维依恋旗舰店",
1421 | "sellerId": 1103141522,
1422 | "price": {
1423 | "now": 16800,
1424 | "origin": 29800,
1425 | "oriPromo": 16800,
1426 | "descend": 0,
1427 | "save": 13000,
1428 | "sum": 16800,
1429 | "actual": 0,
1430 | "prepay": 0,
1431 | "finalpay": 0,
1432 | "extraCharges": 0
1433 | },
1434 | "amount": {
1435 | "now": 1,
1436 | "max": 21,
1437 | "limit": 9223372036854775807,
1438 | "multiple": 1,
1439 | "supply": 0,
1440 | "demand": 0
1441 | },
1442 | "itemIcon": {
1443 | "MALL_CART_IDENTITY": [],
1444 | "PAYMENT": [
1445 | {
1446 | "title": "支持信用卡支付",
1447 | "link": "",
1448 | "img": "http://a.tbcdn.cn/sys/common/icon/trade/xcard.png"
1449 | }
1450 | ],
1451 | "MALL_CART_YULIU": [],
1452 | "MALL_CART_XIAOBAO": [
1453 | {
1454 | "title": "消费者保障服务,卖家承诺7天退换",
1455 | "link": "http://www.taobao.com/go/act/315/xb_20100707.php?ad_id=&am_id=1300268931aef04f0cdc&cm_id=&pm_id=#qitian",
1456 | "img": "http://a.tbcdn.cn/tbsp/icon/xiaobao/a_7-day_return_16x16.png",
1457 | "name": "七天退换"
1458 | },
1459 | {
1460 | "title": "消费者保障服务,卖家承诺如实描述",
1461 | "link": "http://www.taobao.com/go/act/315/xfzbz_rsms.php?ad_id=&am_id=130011830696bce9eda3&cm_id=&pm_id=",
1462 | "img": "http://a.tbcdn.cn/tbsp/icon/xiaobao/a_true_description_16x16.png",
1463 | "name": "如实描述"
1464 | },
1465 | {
1466 | "title": "消费者保障服务,卖家承诺假一赔三",
1467 | "link": "http://www.taobao.com/go/act/315/xfzbz_jyps.php?ad_id=&am_id=1300118304240d56fca9&cm_id=&pm_id=",
1468 | "img": "http://a.tbcdn.cn/tbsp/icon/xiaobao/an_authentic_item_16x16.png",
1469 | "name": "假一赔三"
1470 | }
1471 | ]
1472 | },
1473 | "campaignId": "0",
1474 | "isDouble11": false,
1475 | "isDouble11halfDiscount": false,
1476 | "isCod": false,
1477 | "isAttention": true,
1478 | "promos": [
1479 | [
1480 | {
1481 | "style": "Tmall$tmallItemPromotion",
1482 | "title": "省130元:三八节大促",
1483 | "usedPoint": 0
1484 | }
1485 | ]
1486 | ],
1487 | "createTime": 1383900681000,
1488 | "attr": ";campaignId:0;op:29800;cityCode:330100;",
1489 | "preference": false,
1490 | "isSellerPayPostfee": false,
1491 | "leafCategory": 0,
1492 | "cumulativeSales": 0,
1493 | "suggestInfo": {
1494 | "tjbSuggestInfo": "1:16500;"
1495 | },
1496 | "skuStatus": 2,
1497 | "cartActiveInfo": {
1498 | "isDefault": false,
1499 | "wantStatus": 0,
1500 | "endTime": 0,
1501 | "cartBcParams": "buyerCondition~0~~cartCreateTime~1383900681000",
1502 | "type": 0
1503 | },
1504 | "logo": [
1505 | {
1506 | "pos": 2,
1507 | "url": "http://img.taobaocdn.com/bao/uploaded/T1CgyXFxhaXXaCwpjX.png"
1508 | }
1509 | ]
1510 | }
1511 | ],
1512 | "type": "act",
1513 | "title": "3.8生活节",
1514 | "url": "http://marketing.tmall.com/home/tsp/page.htm?id=27484074_101488084",
1515 | "scrollPromos": [
1516 | "满1件,享包邮"
1517 | ],
1518 | "gradeMessage": "",
1519 | "promos": [
1520 | "包邮"
1521 | ],
1522 | "valid": true
1523 | }
1524 | ]
1525 | },
1526 | {
1527 | "id": "s_383999134",
1528 | "title": "新星轩精品男童装",
1529 | "type": "shop",
1530 | "url": "http://store.taobao.com/shop/view_shop.htm?user_number_id=383999134",
1531 | "seller": "新星轩",
1532 | "host": "C",
1533 | "shopId": 60795716,
1534 | "sellerId": "383999134",
1535 | "isValid": false,
1536 | "hasPriceVolume": false,
1537 | "bundles": [
1538 | {
1539 | "id": "s_383999134_0",
1540 | "orders": [
1541 | {
1542 | "id": "39748572042",
1543 | "itemId": "20126729337",
1544 | "skuId": "37509411587",
1545 | "cartId": "39748572042",
1546 | "isValid": false,
1547 | "url": "http://item.taobao.com/item.htm?id=20126729337",
1548 | "pic": "http://img01.taobaocdn.com/bao/uploaded/i1/383999134/T2AsXWXvdXXXXXXXXX_!!383999134.jpg",
1549 | "title": "秋款童装 男童风衣外套中长款 儿童皮风衣韩版潮 春装2014新款",
1550 | "weight": 0,
1551 | "skus": {
1552 | "颜色分类": "黑色",
1553 | "参考身高": "110cm【110码】"
1554 | },
1555 | "shopId": "60795716",
1556 | "shopName": "新星轩精品男童装",
1557 | "shopUrl": "http://store.taobao.com/shop/view_shop.htm?user_number_id=383999134",
1558 | "seller": "新星轩",
1559 | "sellerId": 383999134,
1560 | "price": {
1561 | "now": 25800,
1562 | "origin": 25800,
1563 | "oriPromo": 16800,
1564 | "descend": 0,
1565 | "save": 0,
1566 | "sum": 25800,
1567 | "actual": 0,
1568 | "prepay": 0,
1569 | "finalpay": 0,
1570 | "extraCharges": 0
1571 | },
1572 | "amount": {
1573 | "now": 1,
1574 | "max": 0,
1575 | "limit": 9223372036854775807,
1576 | "multiple": 1,
1577 | "supply": 0,
1578 | "demand": 0
1579 | },
1580 | "itemIcon": {
1581 | "CART_EBOOK": [],
1582 | "CART_XIAOBAO": [
1583 | {
1584 | "desc": "如实描述",
1585 | "title": "消费者保障服务,卖家承诺商品如实描述",
1586 | "link": "http://www.taobao.com/go/act/315/xfzbz_rsms.php?ad_id=&am_id=130011830696bce9eda3&cm_id=&pm_id=",
1587 | "img": "http://img03.taobaocdn.com/tps/i3/T1bnR4XEBhXXcQVo..-14-16.png"
1588 | },
1589 | {
1590 | "desc": "7天退换",
1591 | "title": "消费者保障服务,卖家承诺7天无理由退换货",
1592 | "link": "http://www.taobao.com/go/act/315/xbqt090304.php?ad_id=&am_id=130011831021c2f3caab&cm_id=&pm_id=",
1593 | "img": "http://img04.taobaocdn.com/tps/i4/T16lKeXwFcXXadE...-14-15.png"
1594 | }
1595 | ],
1596 | "CART_YULIU": [
1597 | {
1598 | "title": "支持信用卡支付",
1599 | "img": "http://assets.taobaocdn.com/sys/common/icon/trade/xcard.png"
1600 | }
1601 | ],
1602 | "CART_IDENTITY": []
1603 | },
1604 | "isDouble11": false,
1605 | "isDouble11halfDiscount": false,
1606 | "isCod": false,
1607 | "isAttention": true,
1608 | "createTime": 1385627760000,
1609 | "attr": ";op:25800;",
1610 | "preference": false,
1611 | "isSellerPayPostfee": false,
1612 | "leafCategory": 0,
1613 | "cumulativeSales": 0,
1614 | "skuStatus": 3,
1615 | "cartActiveInfo": {
1616 | "isDefault": false,
1617 | "wantStatus": 0,
1618 | "endTime": 0,
1619 | "cartBcParams": "buyerCondition~0~~cartCreateTime~1385627760000",
1620 | "type": 0
1621 | }
1622 | }
1623 | ],
1624 | "type": "shop",
1625 | "valid": true
1626 | }
1627 | ]
1628 | }
1629 | ]
1630 | },
1631 | "responseError": {
1632 |
1633 | }
1634 | }
--------------------------------------------------------------------------------
/demo/interfaceRules/Search.list.rule.json:
--------------------------------------------------------------------------------
1 | {
2 | "request": {
3 | "keywords": "",
4 | "catId|100000-999999": 1
5 | },
6 | "response": {
7 | },
8 | "responseError": {
9 | "CODE": "5301"
10 | }
11 | }
--------------------------------------------------------------------------------
/demo/interfaceRules/test.rule.json:
--------------------------------------------------------------------------------
1 | {
2 | "request": {
3 | "$schema": "http://json-schema.org/draft-04/schema#",
4 | "type": "object",
5 | "required": ["search"],
6 | "properties": {
7 | "search": {
8 | "type": "string",
9 | "description": "搜索关键字"
10 | },
11 | "type": {
12 | "type": "string",
13 | "description": "区分mobile、pc接口"
14 | }
15 | }
16 | },
17 | "response": {
18 | "$schema": "http://json-schema.org/draft-04/schema#",
19 | "type": "object",
20 | "required": ["status"],
21 | "properties": {
22 | "status": {
23 | "type": "integer",
24 | "description": "状态码",
25 | "enum": [200, 400, 500],
26 | "default": 200
27 | },
28 | "data": {
29 | "type": "object",
30 | "description": "服务端返回的的正常数据",
31 | "properties": {
32 | "items": {
33 | "type": "array",
34 | "description": "搜索返回结果列表",
35 | "maxItems": 4,
36 | "minItems": 1,
37 | "items": {
38 | "type": "object",
39 | "properties": {
40 | "itemId": {
41 | "type": "number",
42 | "description": "商品ID"
43 | },
44 | "title": {
45 | "type": "string",
46 | "description": "商品标题",
47 | "maxLength": 26,
48 | "minLength": 18
49 | },
50 | "pic": {
51 | "type": "string",
52 | "description": "商品图片",
53 | "format": "PARAM_PIC_SIZE_344_228"
54 | }
55 | }
56 | }
57 | },
58 | "key": {
59 | "type": "string",
60 | "description": "搜索关键字"
61 | },
62 | "total": {
63 | "type": "number",
64 | "description": "商品总数"
65 | }
66 | }
67 | }
68 | }
69 | },
70 | "response2": {
71 | "$schema": "http://json-schema.org/draft-04/schema#",
72 | "type": "object",
73 | "required": ["status"],
74 | "properties": {
75 | "status": {
76 | "type": "integer"
77 | }
78 | }
79 | },
80 | "responseError": {
81 | "$schema": "http://json-schema.org/draft-04/schema#",
82 | "type": "object",
83 | "required": ["status"],
84 | "properties": {
85 | "status": {
86 | "type": "integer"
87 | }
88 | }
89 | }
90 | }
--------------------------------------------------------------------------------
/demo/interface_demo.json:
--------------------------------------------------------------------------------
1 | {
2 | "title": "midway example interface configuration",
3 | "version": "1.0.0",
4 | "engine": "mockjs",
5 | "rulebase": "interfaceRules",
6 | "status": "online",
7 | "interfaces": [ {
8 | "name": "我的购物车",
9 | "id": "Cart.getMyCart",
10 | "urls": {
11 | "online": "http://cart.taobao.com/json/asyncGetMyCart.do"
12 | },
13 | "encoding": "gbk"
14 | }, {
15 | "name": "主搜索接口",
16 | "id": "Search.list",
17 | "urls": {
18 | "online": "http://api.s.m.taobao.com/search.json"
19 | },
20 | "dataType": "jsonp"
21 | }, {
22 | "name": "热词推荐接口",
23 | "id": "Search.suggest",
24 | "urls": {
25 | "online": "http://suggest.taobao.com/sug"
26 | }
27 | }, {
28 | "name": "导航获取接口",
29 | "id": "Search.getNav",
30 | "urls": {
31 | "online": "http://s.m.taobao.com/client/search.do"
32 | }
33 | }, {
34 | "name": "post测试接口",
35 | "id": "Test.post",
36 | "urls": {
37 | "online": "http://127.0.0.1:3001/post"
38 | },
39 | "method": "post",
40 | "dataType": "text"
41 | } ],
42 | "combo": {
43 | "getMyData": [ "Cart.getCart", "Search.suggest" ]
44 | }
45 | }
--------------------------------------------------------------------------------
/demo/mockserver.js:
--------------------------------------------------------------------------------
1 | var express = require( 'express' );
2 | var app = express();
3 |
4 | app.post( '/post', function( req, res ) {
5 | var d = '';
6 | req.on( 'data', function( chunk ) {
7 | d += chunk;
8 | } );
9 | req.on( 'end', function() {
10 | console.log( d );
11 | } );
12 | res.send( 'this is the msg from mockserver!' );
13 | } );
14 |
15 | app.listen( 3001 );
16 |
17 | // mock test
18 | var mocker = require( 'river-mock' );
19 | var fs = require('fs');
20 | var rule = fs.readFileSync( './interfaceRules/test.rule.json' );
21 |
22 | var schema = JSON.parse( rule );
23 | // console.log( schema );
24 | console.log( mocker.spec2mock( schema, 'response' ) );
--------------------------------------------------------------------------------
/demo/modelproxy-client.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 | id:
10 | content:
11 |
12 |
13 |
14 |
63 |
64 |
--------------------------------------------------------------------------------
/demo/modelproxy-client.js:
--------------------------------------------------------------------------------
1 | KISSY.add( 'modelproxy', function ( S, IO ) {
2 | function Proxy( options ) {
3 | this._opt = options;
4 | }
5 | Proxy.prototype = {
6 | request: function( params, callback, errCallback ) {
7 | IO( {
8 | url: this._opt.bypass
9 | ? this._opt.url
10 | + ( this._opt.url.indexOf( '?' ) === -1 ? '?' : '&' )
11 | + 'version=' + this._opt.version
12 | : Proxy.base + '/' + this._opt.id,
13 | data: params,
14 | type: this._opt.method,
15 | dataType: this._opt.dataType,
16 | success: callback,
17 | error: errCallback
18 | } );
19 | },
20 | getOptions: function() {
21 | return this._opt;
22 | }
23 | };
24 |
25 | Proxy.objects = {};
26 |
27 | Proxy.create = function( id ) {
28 | if ( this.objects[ id ] ) {
29 | return this.objects[ id ];
30 | }
31 | var options = this._interfaces[ id ];
32 | if ( !options ) {
33 | throw new Error( 'No such interface id defined: '
34 | + id + ', please check your interface configuration file' );
35 | }
36 | return this.objects[ id ] = new this( options );
37 | },
38 |
39 | Proxy.configBase = function( base ) {
40 | if ( this.base ) return;
41 | this.base = ( base || '' ).replace( /\/$/, '' );
42 | var self = this;
43 | // load interfaces definition.
44 | IO( {
45 | url: this.base + '/$interfaces',
46 | async: false,
47 | type: 'get',
48 | dataType: 'json',
49 | success: function( interfaces ) {
50 | self.config( interfaces );
51 | },
52 | error: function( err ) {
53 | throw err;
54 | }
55 | } );
56 | };
57 |
58 | Proxy.config = function( interfaces ) {
59 | this._interfaces = interfaces;
60 | };
61 |
62 | Proxy.getInterfaceIdsByPrefix = function( pattern ) {
63 | if ( !pattern ) return [];
64 | var ids = [], map = this._interfaces, len = pattern.length;
65 | for ( var id in map ) {
66 | if ( id.slice( 0, len ) == pattern ) {
67 | ids.push( id );
68 | }
69 | }
70 | return ids;
71 | };
72 |
73 | function ModelProxy( profile ) {
74 | if ( !profile ) return;
75 |
76 | if ( typeof profile === 'string' ) {
77 | if ( /^(\w+\.)+\*$/.test( profile ) ) {
78 | profile = Proxy
79 | .getInterfaceIdsByPrefix( profile.replace( /\*$/, '' ) );
80 |
81 | } else {
82 | profile = [ profile ];
83 | }
84 | }
85 | if ( profile instanceof Array ) {
86 | var prof = {}, methodName;
87 | for ( var i = profile.length - 1; i >= 0; i-- ) {
88 | methodName = profile[ i ];
89 | methodName = methodName
90 | .substring( methodName.lastIndexOf( '.' ) + 1 );
91 | if ( !prof[ methodName ] ) {
92 | prof[ methodName ] = profile[ i ];
93 |
94 | } else {
95 | methodName = profile[ i ].replace( /\./g, '_' );
96 | prof[ methodName ] = profile[ i ];
97 | }
98 | }
99 | profile = prof;
100 | }
101 |
102 | for ( var method in profile ) {
103 | this[ method ] = ( function( methodName, interfaceId ) {
104 | var proxy = Proxy.create( interfaceId );
105 | return function( params ) {
106 | params = params || {};
107 | if ( !this._queue ) {
108 | this._queue = [];
109 | }
110 | this._queue.push( {
111 | params: params,
112 | proxy: proxy
113 | } );
114 | return this;
115 | };
116 | } )( method, profile[ method ] );
117 | }
118 | }
119 |
120 | ModelProxy.prototype = {
121 | done: function( f, ef ) {
122 | if ( typeof f !== 'function' ) return;
123 |
124 | if ( !this._queue ) {
125 | f.apply( this );
126 | return;
127 | }
128 | this._sendRequestsParallel( this._queue, f, ef );
129 |
130 | this._queue = null;
131 | return this;
132 | },
133 | _sendRequestsParallel: function( queue, callback, errCallback ) {
134 | var args = [], self = this;
135 |
136 | var cnt = queue.length;
137 |
138 | for ( var i = 0; i < queue.length; i++ ) {
139 | ( function( reqObj, k ) {
140 | reqObj.proxy.request( reqObj.params, function( data ) {
141 | args[ k ] = data;
142 | --cnt || callback.apply( self, args );
143 | }, function( err ) {
144 | errCallback = errCallback || self._errCallback;
145 | if ( typeof errCallback === 'function' ) {
146 | errCallback( err );
147 |
148 | } else {
149 | console.error( 'Error occured when sending request ='
150 | , reqObj.proxy.getOptions(), '\nCaused by:\n', err );
151 | }
152 | } );
153 | } )( queue[i], i );
154 | }
155 | },
156 | error: function( f ) {
157 | this._errCallback = f;
158 | }
159 | };
160 |
161 | ModelProxy.create = function( profile ) {
162 | return new this( profile );
163 | };
164 |
165 | ModelProxy.configBase = function( path ) {
166 | Proxy.configBase( path );
167 | };
168 |
169 | return ModelProxy;
170 |
171 | }, { requires: ['io'] } );
--------------------------------------------------------------------------------
/index.js:
--------------------------------------------------------------------------------
1 | module.exports = require( './lib/modelproxy' );
--------------------------------------------------------------------------------
/lib/constant.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Constant
3 | */
4 |
5 | var Constant = {
6 | STATUS_MOCK : 'mock',
7 | STATUS_MOCK_ERR : 'mockerr',
8 | RESPONSE : 'response',
9 | RESPONSE_ERROR : 'responseError',
10 | ENCODING_RAW : 'raw',
11 | GET : 'GET',
12 | POST : 'POST',
13 | TEXT : 'text',
14 | JSON : 'json',
15 | JSONP : 'jsonp'
16 | };
17 |
18 | module.exports = Constant;
--------------------------------------------------------------------------------
/lib/interfacemanager.js:
--------------------------------------------------------------------------------
1 | /**
2 | * InterfaceManager
3 | * This Class is provided to parse the interface configuration file so that
4 | * the Proxy class can easily access the structure of the configuration.
5 | * @author ShanFan
6 | * @created 24-3-2014
7 | **/
8 |
9 | var fs = require( 'fs' );
10 | var Constant = require( './constant' );
11 | var util = require( 'midway-parser' );
12 | var logger = console;
13 |
14 | /**
15 | * InterfaceManager
16 | * @param {String|Object} path The file path of inteface configuration or the interface object
17 | * @param {Object} variables
18 | */
19 | function InterfaceManager( path, variables ) {
20 | this._path = path;
21 | variables = variables || {};
22 | // {Object} Interface Mapping, The key is interface id and
23 | // the value is a json profile for this interface.
24 | this._interfaceMap = {};
25 |
26 | // {Object} A interface Mapping for client, the key is interface id and
27 | // the value is a json profile for this interface.
28 | this._clientInterfaces = {};
29 |
30 | // {String} The path of rulebase where the interface rules is stored. This value will be override
31 | // if user specified the path of rulebase in interface.json.
32 | this._rulebase = typeof path === 'string' ? path.replace( /\/[^\/]*$/, '/interfaceRules' ) : '';
33 |
34 | typeof path === 'string'
35 | ? this._loadProfilesFromPath( path, variables )
36 | : this._loadProfiles( path, variables );
37 | }
38 |
39 | // InterfaceManager prototype
40 | InterfaceManager.prototype = {
41 |
42 | // @throws errors
43 | _loadProfilesFromPath: function( path, variables ) {
44 | logger.info( 'Loading interface profiles.\nPath = ', path );
45 |
46 | try {
47 | var profiles = fs.readFileSync( path, { encoding: 'utf8' } );
48 | } catch ( e ) {
49 | throw new Error( 'Fail to load interface profiles.' + e );
50 | }
51 | try {
52 | profiles = JSON.parse( profiles );
53 | } catch( e ) {
54 | throw new Error( 'Interface profiles has syntax error:' + e );
55 | }
56 | this._loadProfiles( profiles, variables );
57 | },
58 |
59 | _loadProfiles: function( profiles, variables ) {
60 | if ( !profiles ) return;
61 | try {
62 | profiles = util.parse( profiles, variables );
63 | } catch ( e ) {
64 | logger.error( 'Error occured when parsing interface file' );
65 | throw e;
66 | }
67 |
68 | logger.info( 'Title:', profiles.title, 'Version:', profiles.version );
69 |
70 | this._rulebase = profiles.rulebase
71 | ? this._rulebase.replace( /\/[^\/]*$/, '/' + profiles.rulebase )
72 | : this._rulebase;
73 | logger.info( 'interface path:' + this._path );
74 | logger.info( 'rulebase path:' + this._rulebase );
75 |
76 | // {String} The mock engine name.
77 | this._engine = profiles.engine || 'mockjs';
78 |
79 | if ( profiles.status === undefined ) {
80 | throw new Error( 'There is no status specified in interface configuration!' );
81 | }
82 |
83 | // {String} The interface status in using.
84 | this._status = profiles.status;
85 |
86 | var interfaces = profiles.interfaces || [];
87 | for ( var i = interfaces.length - 1; i >= 0; i-- ) {
88 | this._addProfile( interfaces[i] )
89 | && logger.info( 'Interface[' + interfaces[i].id + '] is loaded.' );
90 | }
91 | },
92 | getProfile: function( interfaceId ) {
93 | return this._interfaceMap[ interfaceId ];
94 | },
95 | getClientInterfaces: function() {
96 | return this._clientInterfaces;
97 | },
98 | getTypeList: function() {
99 |
100 | },
101 | getEngine: function() {
102 | return this._engine;
103 | },
104 | getStatus: function( name ) {
105 | return this._status;
106 | },
107 | // @return Array
108 | getInterfaceIdsByPrefix: function( pattern ) {
109 | if ( !pattern ) return [];
110 | var ids = [], map = this._interfaceMap, len = pattern.length;
111 | for ( var id in map ) {
112 | if ( id.slice( 0, len ) == pattern ) {
113 | ids.push( id );
114 | }
115 | }
116 | return ids;
117 | },
118 |
119 | isProfileExisted: function( interfaceId ) {
120 | return !!this._interfaceMap[ interfaceId ];
121 | },
122 | _addProfile: function( prof ) {
123 | if ( !prof || !prof.id ) {
124 | logger.error( "Can not add interface profile without id!" );
125 | return false;
126 | }
127 | if ( !/^((\w+\.)*\w+)$/.test( prof.id ) ) {
128 | logger.error( "Invalid id: " + prof.id );
129 | return false;
130 | }
131 | if ( this.isProfileExisted( prof.id ) ) {
132 | logger.error( "Can not repeat to add interface [" + prof.id
133 | + "]! Please check your interface configuration file!" );
134 | return false;
135 | }
136 |
137 |
138 | prof.ruleFile = this._rulebase + '/'
139 | + ( prof.ruleFile || ( prof.id + ".rule.json" ) );
140 |
141 | if ( !this._isUrlsValid( prof.urls )
142 | && !fs.existsSync( prof.ruleFile ) ) {
143 | logger.error( 'Profile is deprecated:\n',
144 | prof, '\nNo urls is configured and No ruleFile is available' );
145 | return false;
146 | }
147 |
148 | if (!(prof.status in prof.urls
149 | || prof.status === Constant.STATUS_MOCK
150 | || prof.status === Constant.STATUS_MOCK_ERR ) ) {
151 | prof.status = this._status;
152 | }
153 |
154 | if ( prof.status === Constant.STATUS_MOCK
155 | || prof.status === Constant.STATUS_MOCK_ERR ) {
156 | try {
157 | prof.rule = require( prof.ruleFile );
158 | } catch ( e ) {
159 | logger.warn( 'Can not read rule file of ' + prof.id
160 | + ' , so deprecated this interface. Caused by:\n', e );
161 | }
162 | }
163 |
164 | prof.type = prof.type || 'http'
165 | prof.engine = prof.engine || this._engine;
166 | prof.method = { POST: 'POST', GET:'GET' }
167 | [ (prof.method || 'GET').toUpperCase() ];
168 | prof.dataType = { json: 'json', text: 'text', jsonp: 'jsonp' }
169 | [ (prof.dataType || 'json').toLowerCase() ];
170 | prof.isRuleStatic = !!prof.isRuleStatic || false;
171 | prof.isCookieNeeded = !!prof.isCookieNeeded || false;
172 | prof.signed = !!prof.signed || false;
173 | prof.timeout = prof.timeout || 2000;
174 | prof.version = prof.version || '';
175 | prof.bypassProxyOnClient = !!prof.bypassProxyOnClient;
176 |
177 | this._interfaceMap[ prof.id ] = prof;
178 |
179 | this._clientInterfaces[ prof.id ] = {
180 | id: prof.id,
181 | method: prof.method,
182 | dataType: prof.dataType,
183 | version: prof.version,
184 | bypass: prof.bypassProxyOnClient,
185 | url: (prof.status !== 'mock' && prof.status !== 'mockerr')
186 | ? prof.urls[ prof.status ] : ''
187 | };
188 |
189 | return true;
190 | },
191 | _isUrlsValid: function( urls ) {
192 | if ( !urls ) return false;
193 | for ( var i in urls ) {
194 | return true;
195 | }
196 | return false;
197 | }
198 | };
199 |
200 | InterfaceManager.setLogger = function( l ) {
201 | logger = l;
202 | };
203 |
204 | module.exports = InterfaceManager;
--------------------------------------------------------------------------------
/lib/modelproxy-client.js:
--------------------------------------------------------------------------------
1 | KISSY.add( 'modelproxy', function ( S, IO ) {
2 | function Proxy( options ) {
3 | this._opt = options;
4 | }
5 | Proxy.prototype = {
6 | request: function( params, callback, errCallback ) {
7 | IO( {
8 | url: this._opt.bypass
9 | ? this._opt.url
10 | + ( this._opt.url.indexOf( '?' ) === -1 ? '?' : '&' )
11 | + 'version=' + this._opt.version
12 | : Proxy.base + '/' + this._opt.id,
13 | data: params,
14 | type: this._opt.method,
15 | dataType: this._opt.dataType,
16 | success: callback,
17 | error: errCallback
18 | } );
19 | },
20 | getOptions: function() {
21 | return this._opt;
22 | }
23 | };
24 |
25 | Proxy.objects = {};
26 |
27 | Proxy.create = function( id ) {
28 | if ( this.objects[ id ] ) {
29 | return this.objects[ id ];
30 | }
31 | var options = this._interfaces[ id ];
32 | if ( !options ) {
33 | throw new Error( 'No such interface id defined: '
34 | + id + ', please check your interface configuration file' );
35 | }
36 | return this.objects[ id ] = new this( options );
37 | },
38 |
39 | Proxy.configBase = function( base ) {
40 | if ( this.base ) return;
41 | this.base = ( base || '' ).replace( /\/$/, '' );
42 | var self = this;
43 | // load interfaces definition.
44 | IO( {
45 | url: this.base + '/$interfaces',
46 | async: false,
47 | type: 'get',
48 | dataType: 'json',
49 | success: function( interfaces ) {
50 | self.config( interfaces );
51 | },
52 | error: function( err ) {
53 | throw err;
54 | }
55 | } );
56 | };
57 |
58 | Proxy.config = function( interfaces ) {
59 | this._interfaces = interfaces;
60 | };
61 |
62 | Proxy.getInterfaceIdsByPrefix = function( pattern ) {
63 | if ( !pattern ) return [];
64 | var ids = [], map = this._interfaces, len = pattern.length;
65 | for ( var id in map ) {
66 | if ( id.slice( 0, len ) == pattern ) {
67 | ids.push( id );
68 | }
69 | }
70 | return ids;
71 | };
72 |
73 | function ModelProxy( profile ) {
74 | if ( !profile ) return;
75 |
76 | if ( typeof profile === 'string' ) {
77 | if ( /^(\w+\.)+\*$/.test( profile ) ) {
78 | profile = Proxy
79 | .getInterfaceIdsByPrefix( profile.replace( /\*$/, '' ) );
80 |
81 | } else {
82 | profile = [ profile ];
83 | }
84 | }
85 | if ( profile instanceof Array ) {
86 | var prof = {}, methodName;
87 | for ( var i = profile.length - 1; i >= 0; i-- ) {
88 | methodName = profile[ i ];
89 | methodName = methodName
90 | .substring( methodName.lastIndexOf( '.' ) + 1 );
91 | if ( !prof[ methodName ] ) {
92 | prof[ methodName ] = profile[ i ];
93 |
94 | } else {
95 | methodName = profile[ i ].replace( /\./g, '_' );
96 | prof[ methodName ] = profile[ i ];
97 | }
98 | }
99 | profile = prof;
100 | }
101 |
102 | for ( var method in profile ) {
103 | this[ method ] = ( function( methodName, interfaceId ) {
104 | var proxy = Proxy.create( interfaceId );
105 | return function( params ) {
106 | params = params || {};
107 | if ( !this._queue ) {
108 | this._queue = [];
109 | }
110 | this._queue.push( {
111 | params: params,
112 | proxy: proxy
113 | } );
114 | return this;
115 | };
116 | } )( method, profile[ method ] );
117 | }
118 | }
119 |
120 | ModelProxy.prototype = {
121 | done: function( f, ef ) {
122 | if ( typeof f !== 'function' ) return;
123 |
124 | if ( !this._queue ) {
125 | f.apply( this );
126 | return;
127 | }
128 | this._sendRequestsParallel( this._queue, f, ef );
129 |
130 | this._queue = null;
131 | return this;
132 | },
133 | _sendRequestsParallel: function( queue, callback, errCallback ) {
134 | var args = [], self = this;
135 |
136 | var cnt = queue.length;
137 |
138 | for ( var i = 0; i < queue.length; i++ ) {
139 | ( function( reqObj, k ) {
140 | reqObj.proxy.request( reqObj.params, function( data ) {
141 | args[ k ] = data;
142 | --cnt || callback.apply( self, args );
143 | }, function( err ) {
144 | errCallback = errCallback || self._errCallback;
145 | if ( typeof errCallback === 'function' ) {
146 | errCallback( err );
147 |
148 | } else {
149 | console.error( 'Error occured when sending request ='
150 | , reqObj.proxy.getOptions(), '\nCaused by:\n', err );
151 | }
152 | } );
153 | } )( queue[i], i );
154 | }
155 | },
156 | error: function( f ) {
157 | this._errCallback = f;
158 | }
159 | };
160 |
161 | ModelProxy.create = function( profile ) {
162 | return new this( profile );
163 | };
164 |
165 | ModelProxy.configBase = function( path ) {
166 | Proxy.configBase( path );
167 | };
168 |
169 | return ModelProxy;
170 |
171 | }, { requires: ['io'] } );
--------------------------------------------------------------------------------
/lib/modelproxy.js:
--------------------------------------------------------------------------------
1 | /**
2 | * ModelProxy
3 | * As named, this class is provided to model the proxy.
4 | * @author ShanFan
5 | * @created 24-3-2014
6 | **/
7 |
8 | // Dependencies
9 | var InterfaceManager = require( './interfacemanager' )
10 | , ProxyFactory = require( './proxyfactory' );
11 |
12 | var logger = console;
13 |
14 | /**
15 | * ModelProxy Constructor
16 | * @param {Object|Array|String} profile. This profile describes what the model looks
17 | * like. eg:
18 | * profile = {
19 | * getItems: 'Search.getItems',
20 | * getCart: 'Cart.getCart'
21 | * }
22 | * profile = ['Search.getItems', 'Cart.getCart']
23 | * profile = 'Search.getItems'
24 | * profile = 'Search.*'
25 | */
26 | function ModelProxy( profile ) {
27 | if ( !profile ) return;
28 |
29 | if ( typeof profile === 'string' ) {
30 |
31 | // Get ids via prefix pattern like 'packageName.*'
32 | if ( /^(\w+\.)+\*$/.test( profile ) ) {
33 | profile = ProxyFactory
34 | .getInterfaceIdsByPrefix( profile.replace( /\*$/, '' ) );
35 |
36 | } else {
37 | profile = [ profile ];
38 | }
39 | }
40 | if ( profile instanceof Array ) {
41 | var prof = {}, methodName;
42 | for ( var i = profile.length - 1; i >= 0; i-- ) {
43 | methodName = profile[ i ];
44 | methodName = methodName
45 | .substring( methodName.lastIndexOf( '.' ) + 1 );
46 | if ( !prof[ methodName ] ) {
47 | prof[ methodName ] = profile[ i ];
48 |
49 | // The method name is duplicated, so the full interface id is set
50 | // as the method name.
51 | } else {
52 | methodName = profile[ i ].replace( /\./g, '_' );
53 | prof[ methodName ] = profile[ i ];
54 | }
55 | }
56 | profile = prof;
57 | }
58 |
59 | // Construct the model following the profile
60 | for ( var method in profile ) {
61 | this[ method ] = ( function( methodName, interfaceId ) {
62 | var proxy = ProxyFactory.create( interfaceId );
63 | return function( params ) {
64 | params = params || {};
65 |
66 | if ( !this._queue ) {
67 | this._queue = [];
68 | }
69 | // Push this method call into request queue. Once the done method
70 | // is called, all requests in this queue will be sent.
71 | this._queue.push( {
72 | params: params,
73 | proxy: proxy
74 | } );
75 | return this;
76 | };
77 | } )( method, profile[ method ] );
78 | // this._addMethod( method, profile[ method ] );
79 | }
80 | }
81 |
82 | ModelProxy.prototype = {
83 | done: function( f, ef ) {
84 | if ( typeof f !== 'function' ) return;
85 |
86 | // No request pushed in _queue, so callback directly and return.
87 | if ( !this._queue ) {
88 | f.apply( this );
89 | return;
90 | }
91 |
92 | // Send requests parallel
93 | this._sendRequests( this._queue, f, ef );
94 |
95 | // Clear queue
96 | this._queue = null;
97 | return this;
98 | },
99 | withCookie: function( cookie ) {
100 | this._cookies = cookie;
101 | return this;
102 | },
103 | _sendRequests: function( queue, callback, errCallback ) {
104 | // The final data array
105 | var args = [], setcookies = [], self = this;
106 |
107 | // Count the number of callback;
108 | var cnt = queue.length;
109 |
110 | // Send each request
111 | for ( var i = 0; i < queue.length; i++ ) {
112 | ( function( reqObj, k, cookie ) {
113 |
114 | reqObj.proxy.request( reqObj.params, function( data, setcookie ) {
115 | // fill data for callback
116 | args[ k ] = data;
117 |
118 | // concat setcookie for cookie rewriting
119 | setcookies = setcookies.concat( setcookie );
120 | args.push( setcookies );
121 |
122 | try {
123 | // push the set-cookies as the last parameter for the callback function.
124 | --cnt || callback.apply( self, args.push( setcookies ) && args );
125 | } catch ( e ) {
126 | errCallback = errCallback || self._errCallback;
127 | if ( typeof errCallback === 'function' ) {
128 | errCallback( err );
129 | } else {
130 | logger.error( err );
131 | }
132 | }
133 |
134 | }, function( err ) {
135 | errCallback = errCallback || self._errCallback;
136 | if ( typeof errCallback === 'function' ) {
137 | errCallback( err );
138 |
139 | } else {
140 | logger.error( 'Error occured when sending request ='
141 | , reqObj.params, '\nCaused by:\n', err );
142 | }
143 | }, cookie ); // request with cookie.
144 |
145 | } )( queue[i], i, self._cookies );
146 | }
147 | // clear cookie of this request.
148 | self._cookies = undefined;
149 | },
150 | error: function( f ) {
151 | this._errCallback = f;
152 | },
153 | fail: function( f ) {
154 | this._errCallback = f;
155 | }
156 | };
157 |
158 | /**
159 | * ModelProxy.init
160 | * @param {String} path The path refers to the interface configuration file
161 | * @param {Object} variables The varibale object which is used to parse the
162 | * reference of variable in interface configuration file.
163 | */
164 | ModelProxy.init = function( path, variables ) {
165 | // if ( typeof path === 'string' ) {
166 | // ProxyFactory.use( new InterfaceManager( path, {} ) );
167 | // } else {
168 | // var globalConf = path;
169 | // try {
170 | // path = globalConf.modelproxy.path;
171 | // } catch ( e ) {
172 | // throw new Error( 'ModelProxy can not be initialized because the path of interface is not configured' );
173 | // }
174 | ProxyFactory.use( new InterfaceManager( path, variables ) );
175 | // }
176 | // if (typeof callback === 'function' ) {
177 | // callback( null, true );
178 | // }
179 | };
180 |
181 | ModelProxy.create = function( profile ) {
182 | return new this( profile );
183 | };
184 |
185 | ModelProxy.Interceptor = function( req, res ) {
186 | // todo: need to handle the case that the request url is multiple
187 | // interfaces combined which configured in interface.json.
188 | ProxyFactory.Interceptor( req, res );
189 | };
190 |
191 | ModelProxy.setLogger = function( l ) {
192 | logger = l;
193 | InterfaceManager.setLogger( l );
194 | ProxyFactory.setLogger( l );
195 | };
196 |
197 | // special code for modelproxy init
198 | // function init() {
199 | // var global = require( 'midway-global' );
200 | // var conf = global.getConfig();
201 | // var path = conf.modelproxy.path;
202 | // ProxyFactory.use( new InterfaceManager( path, conf ) );
203 | // logger = global.getLogger();
204 | // }
205 |
206 | // try {
207 | // init();
208 | // } catch ( e ) {
209 | // logger.warn( 'Can not initialize ModelProxy from global configuration. You must initialize it manually.' );
210 | // }
211 |
212 | module.exports = ModelProxy;
213 |
--------------------------------------------------------------------------------
/lib/plugins/hsf.js:
--------------------------------------------------------------------------------
1 | module.epxorts = function() {
2 |
3 | }
--------------------------------------------------------------------------------
/lib/plugins/http.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Http Proxy Class
3 | */
4 |
5 | var util = require( 'util' )
6 | , url = require( 'url' )
7 | , http = require( 'http' )
8 | , iconv = require( 'iconv-lite' )
9 | , Agent = require( 'agentkeepalive' )
10 | , Constant = require( '../constant' );
11 |
12 | var keepAliveAgent = new Agent( { maxSockets: 1000 } );
13 | var ProxyBase = require( '../proxy' );
14 |
15 | // HttpProxy Constructor
16 | function HttpProxy( options ) {
17 | // ProxyBase.call( this, options );
18 | this._opt = options || {};
19 |
20 | var urls = this._opt.urls || {};
21 |
22 | if ( this._opt.status === Constant.STATUS_MOCK
23 | || this._opt.status === Constant.STATUS_MOCK_ERR ) {
24 | return;
25 | }
26 | var currUrl = urls[ this._opt.status ];
27 |
28 | if ( !currUrl ) {
29 | throw new Error( 'No url can be proxied! InterfaceId = ' + options.id );
30 | }
31 |
32 | var urlObj = url.parse( currUrl );
33 | this._opt.hostname = urlObj.hostname;
34 | this._opt.port = urlObj.port || 80;
35 | this._opt.path = urlObj.path
36 | + ( urlObj.path.indexOf( '?' ) !== -1 ? '&' : '?' )
37 | + 'version=' + this._opt.version;
38 | }
39 |
40 | // Inherits ProxyBase
41 | util.inherits( HttpProxy, ProxyBase );
42 |
43 | // @override request function
44 | HttpProxy.prototype.request = function( params, callback, errCallback, cookie ) {
45 | if ( this._opt.isCookieNeeded === true && cookie === undefined ) {
46 | throw new Error( 'This request is cookie needed, you must set a cookie for'
47 | + ' it before request. id = ' + this._opt.id );
48 | }
49 |
50 | // errCallback = typeof errCallback !== 'function'
51 | // ? function( e ) { logger.error( e ); }
52 | // : errCallback;
53 |
54 | if ( this._opt.status === Constant.STATUS_MOCK
55 | || this._opt.status === Constant.STATUS_MOCK_ERR ) {
56 | this.requestMock( params, callback, errCallback );
57 | return;
58 | }
59 |
60 | var self = this;
61 |
62 | var options = {
63 | hostname: self._opt.hostname,
64 | port: self._opt.port,
65 | path: self._opt.path,
66 | method: self._opt.method,
67 | headers: { 'Cookie': cookie },
68 | agent: keepAliveAgent,
69 | keepAlive: true
70 | };
71 |
72 | var querystring = queryStringify( params );
73 |
74 | if ( self._opt.method === Constant.POST ) {
75 | options.headers[ 'Content-Type' ] = 'application/x-www-form-urlencoded';
76 | options.headers[ 'Content-Length' ] = querystring.length;
77 |
78 | } else if ( self._opt.method === Constant.GET ) {
79 | options.path += querystring;
80 | }
81 |
82 | var timer = setTimeout( function() {
83 | errCallback( new Error( 'timeout' ) );
84 | }, self._opt.timeout || 5000 );
85 |
86 | var req = http.request( options, function( res ) {
87 | var source = [], size = 0;
88 | res.on( 'data', function( chunk ) {
89 | source.push( chunk );
90 | size += chunk.length;
91 | } );
92 | res.on( 'end', function() {
93 | var buffer = Buffer.concat( source, size );
94 | try {
95 | var result = self._opt.encoding === Constant.ENCODING_RAW
96 | ? buffer
97 | : ( self._opt.dataType !== Constant.JSON && self._opt.dataType !== Constant.JSONP
98 | ? iconv.fromEncoding( buffer, self._opt.encoding )
99 | : JSON.parse( iconv.fromEncoding( buffer, self._opt.encoding ) ) );
100 | } catch ( e ) {
101 | clearTimeout( timer );
102 | errCallback( new Error( "The result has syntax error. " + e ) );
103 | return;
104 | }
105 | clearTimeout( timer );
106 | callback( result, res.headers['set-cookie'] );
107 | } );
108 | res.on( 'error', function() {
109 | clearTimeout( timer );
110 | errCallback( e );
111 | } );
112 | } );
113 |
114 | self._opt.method !== Constant.POST || req.write( querystring );
115 | req.on( 'error', function( e ) {
116 | clearTimeout( timer );
117 | errCallback( e );
118 | } );
119 |
120 | req.end();
121 | };
122 |
123 | // @override interceptRequest
124 | HttpProxy.prototype.interceptRequest = function( req, res ) {
125 | if ( this._opt.status === Constant.STATUS_MOCK
126 | || this._opt.status === Constant.STATUS_MOCK_ERR ) {
127 | this.requestMock( {}, function( data ) {
128 | res.end( typeof data === 'string' ? data : JSON.stringify( data ) );
129 | }, function( e ) {
130 | // logger.error( 'Error ocurred when mocking data', e );
131 | res.statusCode = 500;
132 | res.end( 'Error orccured when mocking data' );
133 | } );
134 | return;
135 | }
136 | var self = this;
137 | var options = {
138 | hostname: self._opt.hostname,
139 | port: self._opt.port,
140 | path: self._opt.path + req.url.replace( /^[^\?]*\?/, '' ),
141 | method: self._opt.method,
142 | headers: req.headers,
143 | agent: keepAliveAgent,
144 | keepAlive: true
145 | };
146 |
147 | options.headers.host = self._opt.hostname;
148 | // delete options.headers.referer;
149 | // delete options.headers['x-requested-with'];
150 | // delete options.headers['connection'];
151 | // delete options.headers['accept'];
152 | delete options.headers[ 'accept-encoding' ];
153 |
154 | var req2 = http.request( options, function( res2 ) {
155 | var source = [], size = 0;
156 |
157 | res2.on( 'data', function( chunk ) {
158 | source.push( chunk );
159 | size += chunk.length;
160 | } );
161 | res2.on( 'end', function() {
162 | var buffer = Buffer.concat( source, size );
163 | var result;
164 | try {
165 | result = self._opt.encoding === Constant.ENCODING_RAW
166 | ? buffer
167 | : iconv.fromEncoding( buffer, self._opt.encoding );
168 | } catch ( e ) {
169 | res.statusCode = 500;
170 | res.end( e + '' );
171 | return;
172 | }
173 | res.setHeader( 'Set-Cookie', res2.headers['set-cookie'] );
174 | res.setHeader( 'Content-Type'
175 | , ( self._opt.dataType === Constant.JSON
176 | || self._opt.dataType === Constant.JSONP
177 | ? 'application/json' : 'text/html' )
178 | + ';charset=UTF-8' );
179 |
180 | res.end( result );
181 | } );
182 | res2.on( 'error', function( err ) {
183 | res.statusCode = 500;
184 | res.end( e + '' );
185 | } );
186 | } );
187 |
188 | req2.on( 'error', function( e ) {
189 | res.statusCode = 500;
190 | res.end( e + '' );
191 | } );
192 | req.on( 'data', function( chunck ) {
193 | req2.write( chunck );
194 | } );
195 | req.on( 'end', function() {
196 | req2.end();
197 | } );
198 | };
199 |
200 | function queryStringify( params ) {
201 | if ( !params || typeof params === 'string' ) {
202 | return params || '';
203 | } else if ( params instanceof Array ) {
204 | return params.join( '&' );
205 | }
206 | var qs = [], val;
207 | for ( var i in params ) {
208 | val = typeof params[i] === 'object'
209 | ? JSON.stringify( params[ i ] )
210 | : params[ i ];
211 | qs.push( i + '=' + encodeURIComponent(val) );
212 | }
213 | return qs.join( '&' );
214 | };
215 |
216 | module.exports = HttpProxy;
217 |
--------------------------------------------------------------------------------
/lib/plugins/tms.js:
--------------------------------------------------------------------------------
1 | module.epxorts = function() {
2 |
3 | }
--------------------------------------------------------------------------------
/lib/proxy.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Proxy Base Class
3 | */
4 |
5 | /* Constant */
6 | var Constant = require( './constant' );
7 |
8 | function Proxy( options ) {
9 | this._opt = options || {};
10 | }
11 |
12 | Proxy.prototype = {
13 | // should be overridden
14 | request: function( params, callback, errCallback ) {
15 | callback( 'Hello ModelProxy!' );
16 | },
17 | requestMock: function( params, callback, errCallback ) {
18 | try {
19 | if ( this._opt.isRuleStatic ) {
20 | callback( this._opt.status === Constant.STATUS_MOCK
21 | ? this._opt.rule.response
22 | : this._opt.rule.responseError );
23 | return;
24 | }
25 | var engine = getMockEngine( this._opt.engine );
26 | engine.mock( this._opt.rule
27 | , this._opt.status === Constant.STATUS_MOCK
28 | ? Constant.RESPONSE : Constant.RESPONSE_ERROR );
29 |
30 | } catch ( e ) {
31 | setTimeout( function() {
32 | errCallback( e );
33 | }, 1 );
34 | }
35 | },
36 | getOption: function( name ) {
37 | return this._opt[ name ];
38 | },
39 | // should be overridden
40 | interceptRequest: function( req, res ) {
41 | res.end( 'Hello ModelProxy!' );
42 | }
43 | };
44 |
45 | function getMockEngine( name ) {
46 | var engine = require( name );
47 | return {
48 | mock: function( rule, responseType ) {
49 | if ( name === 'river-mock' ) {
50 | return engine.spec2mock( rule, responseType );
51 |
52 | } else if ( name === 'mockjs' ) {
53 | return engine.mock( rule[responseType] );
54 | }
55 | }
56 | }
57 | }
58 |
59 | module.exports = Proxy;
--------------------------------------------------------------------------------
/lib/proxyfactory.js:
--------------------------------------------------------------------------------
1 | /**
2 | * ProxyFactory, Proxy
3 | * This class is provided to create proxy objects following the configuration
4 | * @author ShanFan
5 | * @created 24-3-2014
6 | */
7 |
8 | // Dependencies
9 | var fs = require( 'fs' );
10 |
11 | // logger
12 | var logger = console;
13 |
14 | // Instance of InterfaceManager, will be intialized when the proxy.use() is called.
15 | var interfaceManager;
16 |
17 | // load proxy plugins.
18 | var ProxyClassSet = {};
19 | var dir = fs.readdirSync( __dirname + '/plugins' );
20 | for ( var i in dir ) {
21 | if ( !/^\w+\.js$/.test( dir[i] ) ) continue;
22 | var type = dir[i].split( '.' )[0];
23 | try {
24 | ProxyClassSet[ type ] = require( './plugins/' + type );
25 | } catch ( e ) {
26 | logger.error( 'Failed to load proxy plugin '
27 | + dir[i] + ', Caused by:\n' + e );
28 | }
29 | }
30 |
31 | var ProxyFactory = {
32 |
33 | // {Object} An object map to store created proxies. The key is interface id
34 | // and the value is the proxy instance.
35 | proxies: {},
36 |
37 | /**
38 | * use
39 | * @param {InterfaceManager} ifmgr
40 | * @throws errors
41 | */
42 | use: function( ifmgr ) {
43 | if ( ifmgr instanceof require( './interfacemanager' ) ) {
44 | interfaceManager = ifmgr;
45 | } else {
46 | throw new Error( 'Proxy can only use instance of InterfacefManager!' );
47 | }
48 | return this;
49 | },
50 |
51 | // Proxy factory
52 | // @throws errors
53 | create: function( interfaceId ) {
54 | if ( this.proxies[ interfaceId ] ) {
55 | return this.proxies[ interfaceId ];
56 | }
57 | var options = interfaceManager.getProfile( interfaceId );
58 | if ( !options ) {
59 | throw new Error( 'Invalid interface id: ' + interfaceId );
60 | }
61 | var ProxyClass = ProxyClassSet[ options.type ];
62 | if ( typeof ProxyClass !== 'function' ) {
63 | throw new Error( 'Invalid proxy type of ' + options.type + ' for interface ' + interfaceId );
64 | }
65 | return this.proxies[ interfaceId ] = new ProxyClass( options );
66 | },
67 |
68 | // setLogger
69 | setLogger: function( l ) {
70 | logger = l;
71 | },
72 | // getInterfaceIdsByPrefix
73 | getInterfaceIdsByPrefix: function( pattern ) {
74 | return interfaceManager.getInterfaceIdsByPrefix( pattern );
75 | },
76 | // Interceptor
77 | Interceptor: function( req, res ) {
78 | var interfaceId = req.url.split( /\?|\// )[ 1 ];
79 | if ( interfaceId === '$interfaces' ) {
80 | var interfaces = interfaceManager.getClientInterfaces();
81 | res.end( this.clientInterfaces
82 | ? this.clientInterfaces
83 | : this.clientInterfaces = JSON.stringify( interfaces ) );
84 |
85 | return;
86 | }
87 |
88 | try {
89 | proxy = this.create( interfaceId );
90 | if ( proxy.getOption( 'intercepted' ) === false ) {
91 | throw new Error( 'This url is not intercepted by proxy.' );
92 | }
93 | } catch ( e ) {
94 | res.statusCode = 404;
95 | res.end( 'Invalid url: ' + req.url + '\n' + e );
96 | return;
97 | }
98 | proxy.interceptRequest( req, res );
99 | }
100 | }
101 |
102 | module.exports = ProxyFactory;
103 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "midway-modelproxy",
3 | "description": "midway modelproxy module",
4 | "version": "0.3.0-beta",
5 | "author": "shanfan ",
6 | "contributors": [
7 | {
8 | "name": "shanfan",
9 | "email": "yingzhu.zyz@alibaba-inc.com"
10 | }
11 | ],
12 | "dependencies": {
13 | "mockjs": "0.1.1",
14 | "river-mock": "*",
15 | "iconv-lite": "0.2.11",
16 | "agentkeepalive": "0.2.2",
17 | "midway-parser": "0.0.1",
18 | "midway-global": "*"
19 | },
20 | "devDependencies": {
21 | "mocha": "1.17.1",
22 | "jscoverage": "0.3.8",
23 | "express": "3.4.8"
24 | },
25 | "repository": {
26 | "type": "git",
27 | "url": "git@gitlab.alibaba-inc.com:tb/midway.git"
28 | }
29 | }
--------------------------------------------------------------------------------
/tests/README.md:
--------------------------------------------------------------------------------
1 | $ node mockserver.js
2 | $ jscoverage lib lib-cov
3 | $ mocha *.test.js -R html-cov > coverage.html
4 |
5 | open coverage.html
--------------------------------------------------------------------------------
/tests/httpproxy.test.js:
--------------------------------------------------------------------------------
1 | describe( 'Proxy', function() {
2 |
3 | it( 'should construct a new Proxy object', function() {
4 | var p = new Proxy( {
5 | id: 'Search.getItems',
6 | urls: {
7 | online: 'http://www.modelproxy.com'
8 | },
9 | status: 'online'
10 | } );
11 | assert( p instanceof Proxy );
12 | console.log( p._opt );
13 | } );
14 |
15 | it( 'should throw error when no url is available', function() {
16 | assert.throws( function() {
17 | var p = new Proxy( {
18 | id: 'Search.getItems',
19 | status: 'online'
20 | } );
21 | }, function( err ) {
22 | return err.toString()
23 | .indexOf( 'No url can be proxied!' ) !== -1;
24 | } );
25 |
26 | assert.throws( function() {
27 | var p = new Proxy( {
28 | id: 'Search.getItems',
29 | urls: {
30 | online: 'http://www.modelproxy.com'
31 | }
32 | } );
33 | }, function( err ) {
34 | return err.toString()
35 | .indexOf( 'No url can be proxied!' ) !== -1;
36 | } );
37 | } );
38 |
39 | describe( '#getOption()', function() {
40 | it( 'should return status of this proxy', function() {
41 | var p = new Proxy( {
42 | id: 'Search.getItems',
43 | status: 'online',
44 | urls: {
45 | online: 'http://www.modelproxy.com'
46 | }
47 | } );
48 | assert.strictEqual( p.getOption( 'id' ), 'Search.getItems' );
49 | } );
50 | } );
51 |
52 | describe( '#_queryStringify()', function() {
53 | var p = new Proxy( {
54 | id: 'Search.getItems',
55 | status: 'online',
56 | urls: {
57 | online: 'http://www.modelproxy.com'
58 | }
59 | } );
60 |
61 | it( 'should return empty string if the params is null undefined', function() {
62 | assert.strictEqual( p._queryStringify( null ), '' );
63 | assert.strictEqual( p._queryStringify( undefined ), '' );
64 | } );
65 |
66 | it( 'should return the params itself if the type of params is string', function() {
67 | assert.strictEqual( p._queryStringify( 'a=b&c=d' ), 'a=b&c=d');
68 | } );
69 |
70 | it( 'should return the joined string with & if the params is instance of Array', function() {
71 | assert.strictEqual( p._queryStringify( [ 'a=b', 'c=d' ] ), 'a=b&c=d' );
72 | assert.strictEqual( p._queryStringify( [] ), '' );
73 | } );
74 |
75 | it( 'should return the joined string with & if the params is a key-value object', function() {
76 | assert.strictEqual( p._queryStringify( {a:'b', c:'d'} ), 'a=b&c=d' );
77 | assert.strictEqual( p._queryStringify( {} ), '' );
78 | assert.strictEqual( p._queryStringify( {a:'b', c:"{'d':'f'}"} )
79 | , "a=b&c=" + encodeURIComponent("{'d':'f'}") );
80 | assert.strictEqual( p._queryStringify( {a:'b', c:"['d','e']"} )
81 | , "a=b&c=" + encodeURIComponent("['d','e']") );
82 | } );
83 | } );
84 |
85 | describe( '#interceptRequest()', function() {
86 | it( 'should response mock data if the proxy status is mock', function( done ) {
87 | var p = new Proxy( {
88 | "name": "我的购物车",
89 | "id": "Cart.getMyCart",
90 | "urls": {
91 | "online": "http://cart.taobao.com/json/asyncGetMyCart.do"
92 | },
93 | "status": "mock"
94 | } );
95 | var res = {
96 | end: function( data ) {
97 | assert.equal( typeof data, 'string' );
98 | done();
99 | }
100 | }
101 | p.interceptRequest( null, res );
102 | } );
103 |
104 | it( 'should response mockerr data if the proxy status is mockerr.', function( done ) {
105 | var p = new Proxy( {
106 | "name": "我的购物车",
107 | "id": "Cart.getMyCart",
108 | "urls": {
109 | "online": "http://cart.taobao.com/json/asyncGetMyCart.do"
110 | },
111 | "ruleFile": "Cart.getCart.rule.json",
112 | "status": "mockerr"
113 | } );
114 | var res = {
115 | end: function( data ) {
116 | assert.notEqual( data.indexOf( 'this is error data' ), -1 );
117 | done();
118 | }
119 | }
120 | p.interceptRequest( null, res );
121 |
122 | } );
123 |
124 | it( 'should response error msg and set the status code as 500 when there is error.'
125 | , function( done ) {
126 | var p = new Proxy( {
127 | "name": "我的购物车1",
128 | "id": "Cart.getCart1",
129 | "urls": {
130 | "online": "http://cart.taobao.com/json/asyncGetMyCart.do"
131 | },
132 | "status": "mock"
133 | } );
134 | var res = {
135 | end: function( data ) {
136 | assert.strictEqual( this.statusCode, 500 );
137 | done();
138 | }
139 | };
140 | p.interceptRequest( null, res );
141 | } );
142 |
143 | it( 'should intercept the request and response data', function( done ) {
144 | var p = ProxyFactory.create( 'Search.suggest' );
145 | var req = {
146 | headers: {
147 | cookie: ''
148 | },
149 | url: 'Search.suggest?q=a',
150 | on: function( eventName, callback ) {
151 | if ( eventName === 'data' ) {
152 | callback( 'mock chunk' );
153 | } else if ( eventName === 'end' ) {
154 | callback();
155 | }
156 | }
157 | };
158 | var res = {
159 | headers: {
160 |
161 | },
162 | end: function( data ) {
163 | assert.notEqual( data.length, 0 );
164 | assert.notEqual( typeof this.headers['Content-Type'], 'undefined' );
165 | done();
166 | },
167 | setHeader: function( key, value ) {
168 | this.headers[key] = value;
169 | }
170 | };
171 | p.interceptRequest( req, res );
172 |
173 | } );
174 |
175 | } );
176 |
177 |
178 | describe( '#request()', function() {
179 | var p = ProxyFactory.create( 'Search.suggest' );
180 | it( 'should get result from the remote', function( done ) {
181 | p.request( {q: 'i'}, function( result ) {
182 | done();
183 | } );
184 | } );
185 |
186 | it( 'should get the result from mock', function( done ) {
187 | ProxyFactory.create( 'Search.getNav' )
188 | .request( {q: 'i'}, function( result ) {
189 | assert.strictEqual( result, 'This is a mock text' );
190 | done();
191 | } );
192 | } );
193 |
194 | it( 'should get the result from mockerr', function( done ) {
195 | new Proxy( {
196 | 'name': '导航获取接口',
197 | 'id': 'Search.getNav',
198 | 'urls': {
199 | 'online': 'http://s.m.taobao.com/client/search.do'
200 | },
201 | 'status': 'mockerr',
202 | 'isRuleStatic': true
203 | } ).request( null, function( result ) {
204 | assert.strictEqual( result, 'This is a mock error' );
205 | done();
206 | } );
207 | } );
208 |
209 | it( 'should get the mocked result by mockEngine', function( done ) {
210 | ProxyFactory.create( 'Search.list' )
211 | .request( {q: 'i'}, function( result ) {
212 | assert.strictEqual( typeof result, 'object' );
213 | done();
214 | } );
215 | } );
216 |
217 | it( 'should get a buffer if the encoding of the result is set as raw', function( done ) {
218 | new Proxy( {
219 | 'name': '热词推荐接口',
220 | 'id': 'Search.suggest',
221 | 'urls': {
222 | 'online': 'http://suggest.taobao.com/sug'
223 | },
224 | 'encoding': 'raw',
225 | 'status': 'online'
226 | } ).request( {q: 'i'}, function( result ) {
227 | assert( typeof result === 'object' );
228 | done();
229 | } );
230 |
231 | } );
232 |
233 | it( 'should get a json if the data type of the result is set as json', function( done ) {
234 | new Proxy( {
235 | 'name': '热词推荐接口',
236 | 'id': 'Search.suggest',
237 | 'urls': {
238 | 'online': 'http://suggest.taobao.com/sug'
239 | },
240 | 'dataType': 'json',
241 | 'status': 'online'
242 | } ).request( {q: 'i'}, function( result ) {
243 | assert( typeof result === 'object' );
244 | done();
245 | } );
246 | } );
247 |
248 | it( 'should get a string if the data type of the result is set as text', function( done ) {
249 | new Proxy( {
250 | 'name': '热词推荐接口',
251 | 'id': 'Search.suggest',
252 | 'urls': {
253 | 'online': 'http://suggest.taobao.com/sug'
254 | },
255 | 'dataType': 'text',
256 | 'status': 'online'
257 | } ).request( {q: 'i'}, function( result ) {
258 | assert( typeof result === 'string' );
259 | done();
260 | } );
261 | } );
262 |
263 | it( 'should get nothing if the request is cookie needed but no cookie is set for this request',
264 | function( done ) {
265 |
266 | new Proxy( {
267 | "name": "我的购物车",
268 | "id": "Cart.getMyCart",
269 | "urls": {
270 | "online": "http://cart.taobao.com/json/asyncGetMyCart.do"
271 | },
272 | "status": "online",
273 | "encoding": "gbk"
274 | } ).request( {q: 'i'}, function( result ) {
275 | assert( result === '' );
276 | done();
277 | }, function( err ) {
278 | console.log( err );
279 | }, cookie );
280 |
281 | new Proxy( {
282 | "name": "我的购物车",
283 | "id": "Cart.getMyCart",
284 | "urls": {
285 | "online": "http://cart.taobao.com/json/asyncGetMyCart.do"
286 | },
287 | "status": "online",
288 | "encoding": "gbk"
289 | } ).request( {q: 'i'}, function( result ) {
290 | console.log( result );
291 | assert( !result );
292 | done();
293 | }, function( err ) {
294 | console.log( err );
295 | } );
296 | } );
297 |
298 | it( 'should throw exception if the isCookieNeeded is set as true but no cookie is set for this request',
299 | function() {
300 | assert.throws( function() {
301 | new Proxy( {
302 | "name": "我的购物车",
303 | "id": "Cart.getMyCart",
304 | "urls": {
305 | "online": "http://cart.taobao.com/json/asyncGetMyCart.do"
306 | },
307 | "status": "online",
308 | "encoding": "gbk",
309 | "isCookieNeeded": true
310 | } ).request( {q: 'i'}, function( result ) {
311 | console.log( result );
312 | done();
313 | }, function( err ) {
314 | console.log( err );
315 | } );
316 | }, function( err ) {
317 | return err.toString().indexOf( 'This request is cookie needed' ) !== -1;
318 | } );
319 | } );
320 | } );
321 |
322 | } );
--------------------------------------------------------------------------------
/tests/interfaceRules/Search.getNav.rule.json:
--------------------------------------------------------------------------------
1 | {
2 | "request": {
3 |
4 | },
5 | "response": "This is a mock text",
6 | "responseError": "This is a mock error"
7 | }
--------------------------------------------------------------------------------
/tests/interfaceRules/Search.list.rule.json:
--------------------------------------------------------------------------------
1 | {
2 | "request": {
3 | "keywords": "",
4 | "catId|100000-999999": 1
5 | },
6 | "response": {
7 | "data|1-10": [ {
8 | "id|+1": 1,
9 | "grade1|1-100": 1,
10 | "grade2|90-100": 1,
11 | "float1|.1-10": 10,
12 | "float2|1-100.1-10": 1,
13 | "float3|999.1-10": 1,
14 | "float4|.3-10": 123.123,
15 | "star|1-5": "★",
16 | "cn|1-5": "汉字",
17 | "repeat|10": "A",
18 | "published|1": false,
19 | "email": "@EMAIL",
20 | "date": "@DATE",
21 | "time": "@TIME",
22 | "datetime": "@DATETIME",
23 | "method|1": ["GET", "POST", "HEAD", "DELETE"],
24 | "size": "@AD_SIZE",
25 | "img1": "@IMG(200x200)",
26 | "img2": "@IMG",
27 | "img3": "@IMG(@size)",
28 | "img4": "@IMG(@AD_SIZE)",
29 | "dummyimage": {
30 | "size": "@AD_SIZE",
31 | "background": "@COLOR",
32 | "foreground": "@COLOR",
33 | "format|1": ["png", "gif", "jpg"],
34 | "text": "@WORD",
35 | "url": "http://dummyimage.com/@size/@background/@foreground.@format&text=@text"
36 | },
37 | "param": "abc=123",
38 | "url1": "@img3?@param",
39 | "url2": "@img4?@ID&id=@id"
40 | } ]
41 | },
42 | "responseError": {
43 | "CODE": "5301"
44 | }
45 | }
--------------------------------------------------------------------------------
/tests/interfaceRules/Search.suggest.rule.json:
--------------------------------------------------------------------------------
1 | error rule
--------------------------------------------------------------------------------
/tests/interface_test.json:
--------------------------------------------------------------------------------
1 | {
2 | "title": "midway example interface configuration",
3 | "version": "1.0.0",
4 | "engine": "mockjs",
5 | "rulebase": "interfaceRules",
6 | "status": "online",
7 | "interfaces": [ {
8 | "name": "我的购物车",
9 | "id": "Cart.getMyCart",
10 | "urls": {
11 | "online": "http://cart.taobao.com/json/asyncGetMyCart.do"
12 | },
13 | "isCookieNeeded": true,
14 | "timeout": 100,
15 | "encoding": "gbk"
16 | }, {
17 | "name": "主搜索接口",
18 | "id": "Search.list",
19 | "urls": {
20 | "online": "http://api.s.m.taobao.com/search.json"
21 | },
22 | "status": "mock"
23 | }, {
24 | "name": "热词推荐接口",
25 | "id": "Search.suggest",
26 | "urls": {
27 | "online": "http://suggest.taobao.com/sug"
28 | }
29 | }, {
30 | "name": "导航获取接口",
31 | "id": "Search.getNav",
32 | "urls": {
33 | "online": "http://s.m.taobao.com/client/search.do"
34 | },
35 | "status": "mock",
36 | "isRuleStatic": true
37 | }, {
38 | "name": "重复的导航获取接口",
39 | "id": "D.getNav",
40 | "urls": {
41 | "online": "http://s.m.taobao.com/client/search.do"
42 | },
43 | "status": "online",
44 | "isRuleStatic": true,
45 | "intercepted": false
46 | }, {
47 | "name": "post测试接口",
48 | "id": "Test.post",
49 | "urls": {
50 | "online": "http://127.0.0.1:3001/post"
51 | },
52 | "method": "post",
53 | "dataType": "text"
54 | },{
55 | "name": "timeout测试接口",
56 | "id": "Test.timeoutUrl",
57 | "urls": {
58 | "online": "http://127.0.0.1:3001/timeoutUrl"
59 | },
60 | "method": "get",
61 | "dataType": "text",
62 | "timeout": 1000
63 | } ],
64 | "combo": {
65 | "getMyData": [ "Cart.getCart", "Search.suggest" ]
66 | }
67 | }
--------------------------------------------------------------------------------
/tests/interfacemanager.test.js:
--------------------------------------------------------------------------------
1 | var assert = require( 'assert' );
2 |
3 | var InterfaceManager = require( '../lib-cov/interfacemanager' );
4 | var interfaceManager;
5 |
6 | describe( 'InterfaceManager', function() {
7 | it( 'can not be initalized by error path', function() {
8 | assert.throws( function() {
9 | new InterfaceManager( 'error/path' );
10 | }, function( err ) {
11 | return err.toString()
12 | .indexOf( 'Fail to load interface profiles.Error' ) !== -1;
13 | } );
14 |
15 | interfaceManager = new InterfaceManager( '../tests/interface_test.json' );
16 | assert.equal( interfaceManager instanceof InterfaceManager, true );
17 | } );
18 |
19 | it( 'can not be initalized when no status is specified', function() {
20 | assert.throws( function() {
21 | new InterfaceManager({});
22 | }, function( err ) {
23 | return err.toString()
24 | .indexOf( 'There is no status specified' ) !== -1;
25 | } );
26 | } );
27 |
28 | it( 'should throw error if the interface configuration is not a json ', function() {
29 | assert.throws( function() {
30 | new InterfaceManager( '../tests/README.md' );
31 | }, function( err ) {
32 | return err.toString()
33 | .indexOf( 'syntax error' ) !== -1;
34 | } );
35 | } );
36 | } );
37 |
38 | describe( 'interfaceManager', function() {
39 | var ifmgr = new InterfaceManager( {
40 | status: 'online'
41 | } );
42 |
43 | describe( '#_addProfile()', function() {
44 | it( 'can not be added without id', function() {
45 | assert.equal( ifmgr._addProfile( {
46 | urls: {
47 | online: 'http://url1'
48 | }
49 | } ), false );
50 | } );
51 |
52 | it( 'can not be added when the interface id does not match ^((\\w+\\.)*\\w+)$', function() {
53 | assert.equal( ifmgr._addProfile( {
54 | id: 'Abc-methodName',
55 | urls: {
56 | online: 'http://url1'
57 | }
58 | } ), false );
59 |
60 | assert.equal( ifmgr._addProfile( {
61 | id: 'Abc.methodName.',
62 | urls: {
63 | online: 'http://url1'
64 | }
65 | } ), false );
66 |
67 | assert.equal( ifmgr._addProfile( {
68 | id: 'Abc.methodName',
69 | urls: {
70 | online: 'http://url1'
71 | }
72 | } ), true );
73 | } );
74 |
75 | it( 'can not add duplicated interface id', function() {
76 | assert.equal( ifmgr._addProfile( {
77 | id: 'Abc.methodName',
78 | urls: {
79 | online: 'htpp://url1'
80 | }
81 | } ), false );
82 |
83 | assert.equal( ifmgr._addProfile( {
84 | id: 'Abc.method1',
85 | urls: {
86 | online: 'htpp://url1'
87 | }
88 | } ), true );
89 | } );
90 |
91 | it( 'must have at least one url in urls or its rulefile is available', function() {
92 | ifmgr._rulebase = '.';
93 |
94 | assert.equal( ifmgr._addProfile( {
95 | id: 'Abc.method2',
96 | urls: {}
97 | } ), false );
98 |
99 | assert.equal( ifmgr._addProfile( {
100 | id: 'Abc.method2',
101 | ruleFile: 'unavailable.rule.json'
102 | } ), false );
103 | } );
104 | } );
105 |
106 | describe( '#getInterfaceIdsByPrefix()', function() {
107 | it( 'should have nothing matched when the prefix is not proper', function() {
108 | assert.equal( interfaceManager.getInterfaceIdsByPrefix( 'Prefix' ).length, 0 );
109 | } );
110 | it ( 'should return an array of interface when the prefix is right', function() {
111 | assert.notEqual( interfaceManager.getInterfaceIdsByPrefix( 'Search.' ).length, 0 );
112 | } );
113 | } );
114 |
115 | describe( '#isProfileExisted()', function() {
116 | it( 'should return false when the interface id does not exist', function() {
117 | assert.equal( interfaceManager.isProfileExisted( 'Search.getItem' ), false );
118 | } );
119 | it( 'should return true when the interface id exists', function() {
120 | assert.equal( interfaceManager.isProfileExisted( 'Search.getNav' ), true );
121 | } );
122 | } );
123 |
124 | describe( '#_isUrlsValid()', function() {
125 | it( 'should return false when the urls is null or an empty object', function() {
126 | assert.equal( interfaceManager._isUrlsValid( null ), false );
127 | assert.equal( interfaceManager._isUrlsValid( {} ), false );
128 | } );
129 | it( 'should return true when the urls is not null and has at least one url', function() {
130 | assert.equal( interfaceManager._isUrlsValid( { daily: 'http://url1' } ), true );
131 | assert.equal( interfaceManager._isUrlsValid( { daily: 'http://url1', online: 'http://url2' } ), true );
132 | } );
133 | } );
134 |
135 | describe( '#getProfile()', function() {
136 | it( 'should return undefined if the given id is not existed', function() {
137 | assert.strictEqual( interfaceManager.getProfile( 'Search.item' ), undefined );
138 | } );
139 |
140 | it( 'should renturn the profile if the given interface id is existed', function() {
141 | assert.equal( typeof interfaceManager.getProfile( 'Cart.getMyCart' ), 'object' );
142 | } );
143 | } );
144 |
145 | describe( '#getRule()', function() {
146 | it( 'should return rule of the given id', function() {
147 | assert.equal( typeof interfaceManager.getRule( 'Search.list' ), 'object' );
148 | } );
149 |
150 | it( 'should throw error when the rule file does not exist', function() {
151 | assert.throws( function() {
152 | interfaceManager.getRule( 'Test.post' );
153 | }, function( err ) {
154 | return err.toString()
155 | .indexOf( 'The rule file is not existed.' ) !== -1;
156 | } );
157 | } );
158 |
159 | it( 'should throw error when the rule file is not a json', function() {
160 | assert.throws( function() {
161 | interfaceManager.getRule( 'Search.suggest' );
162 | }, function( err ) {
163 | return err.toString()
164 | .indexOf( 'Rule file has syntax error' ) !== -1;
165 | } );
166 | } );
167 |
168 | it( 'should throw error when the interface id is not found', function() {
169 | assert.throws( function() {
170 | interfaceManager.getRule( 'Error.id' );
171 | }, function( err ) {
172 | return err.toString()
173 | .indexOf( 'is not found' ) !== -1;
174 | } );
175 | } );
176 | } );
177 |
178 | describe( '#getEngine()', function() {
179 | it( 'should return mockjs', function() {
180 | assert.strictEqual( interfaceManager.getEngine(), 'mockjs' );
181 | } );
182 | } );
183 |
184 | describe( '#getStatus()', function() {
185 | it( 'should return online', function() {
186 | assert.strictEqual( interfaceManager.getStatus(), 'online' );
187 | } );
188 | } );
189 |
190 | it( 'clientInterfaces should be initalized after the object of interfaceManager is created', function() {
191 | var clientInterfaces = interfaceManager.getClientInterfaces();
192 | console.log( clientInterfaces );
193 | assert.notEqual( clientInterfaces, null );
194 | var cnt = 0;
195 | for ( var i in clientInterfaces ) cnt++;
196 | assert.equal( cnt, 7 );
197 | } );
198 |
199 | } );
--------------------------------------------------------------------------------
/tests/mockserver.js:
--------------------------------------------------------------------------------
1 | var express = require( 'express' );
2 | var app = express();
3 |
4 | app.post( '/post', function( req, res ) {
5 | var d = '';
6 | req.on( 'data', function( chunk ) {
7 | d += chunk;
8 | } );
9 | req.on( 'end', function() {
10 | console.log( d );
11 | } );
12 | res.send( 'this is the msg from mockserver!' );
13 | } );
14 |
15 | app.get( '/timeoutUrl', function( req, res ) {
16 | setTimeout( function() {
17 | res.send( 'sorry' );
18 | }, 10000 )
19 | } )
20 | app.listen( 3001 );
--------------------------------------------------------------------------------
/tests/modelproxy.test.js:
--------------------------------------------------------------------------------
1 | var assert = require( 'assert' );
2 |
3 | var ModelProxy = require( '../lib-cov/modelproxy' );
4 |
5 | describe( 'ModelProxy', function() {
6 | describe( '#init()', function() {
7 | it( 'throws exception when the path of the interface is not right', function() {
8 | assert.throws( function() {
9 | ModelProxy.init( 'error/path/interface_test.json' );
10 | }, function( err ) {
11 | return err.toString().indexOf( 'no such file or directory' ) !== -1;
12 | } );
13 | } );
14 | } );
15 |
16 | describe( '#setLogger()', function() {
17 | it( 'should replace the console with the specified logger', function() {
18 | var logger = {
19 | info: function( msg ) {
20 | console.log( 'INFO:', msg );
21 | },
22 | log: function( msg ) {
23 | console.log( 'LOG:', msg );
24 | },
25 | error: function( msg ) {
26 | console.log( 'ERROR:', msg );
27 | },
28 | warn: function( msg ) {
29 | console.log( 'WARN:', msg );
30 | }
31 | };
32 |
33 | ModelProxy.setLogger( logger );
34 |
35 | } );
36 | } );
37 |
38 | ModelProxy.init( '../tests/interface_test.json' );
39 |
40 | describe( '#create()', function() {
41 | it( 'should return an object with methods specified by the profile', function() {
42 | var m = ModelProxy.create( 'Search.suggest' );
43 | assert( typeof m.suggest === 'function' );
44 |
45 | m = ModelProxy.create( [ 'Search.suggest', 'Cart.getMyCart' ] );
46 | assert( typeof m.suggest === 'function' );
47 | assert( typeof m.getMyCart === 'function' );
48 |
49 | m = ModelProxy.create( {
50 | suggest: 'Search.suggest',
51 | getCart: 'Cart.getMyCart'
52 | } );
53 | assert( typeof m.suggest === 'function' );
54 | assert( typeof m.getCart === 'function' );
55 |
56 | m = ModelProxy.create( 'Search.*' );
57 | assert( typeof m.list === 'function' );
58 | assert( typeof m.suggest === 'function' );
59 | assert( typeof m.getNav === 'function' );
60 |
61 | m = ModelProxy.create( [ 'Search.getNav', 'D.getNav' ] );
62 | assert( typeof m.Search_getNav === 'function' );
63 | assert( typeof m.getNav === 'function' );
64 |
65 | } );
66 |
67 | it('should throw exception if the specified interface id does not exist', function() {
68 | assert.throws( function() {
69 | var m = ModelProxy.create( 'Search.getItems' );
70 | }, function( err ) {
71 | return err.toString().indexOf( 'Invalid interface id' ) !== -1;
72 | } );
73 | assert.throws( function() {
74 | var m = ModelProxy.create( ['Search.getItems'] );
75 | }, function( err ) {
76 | return err.toString().indexOf( 'Invalid interface id' ) !== -1;
77 | } );
78 |
79 | assert.throws( function() {
80 | var m = ModelProxy.create( {
81 | getItems: 'Search.getItems',
82 | getMyCart: 'Cart.getMyCart'
83 | } );
84 | }, function( err ) {
85 | return err.toString().indexOf( 'Invalid interface id' ) !== -1;
86 | } );
87 | } );
88 | } );
89 |
90 | } );
91 |
92 | describe( 'modelProxy', function() {
93 |
94 | describe( '#method()', function() {
95 | var m = ModelProxy.create( 'Search.suggest' );
96 | it( 'should return itself', function() {
97 | var m = ModelProxy.create( 'Search.suggest' );
98 | assert( m.suggest( { q: 'i'} ) === m );
99 | } );
100 | } );
101 |
102 | describe( '#done()', function() {
103 | var m = ModelProxy.create( 'Search.*' );
104 | it( 'should send all requests pushed before done and fetch the corresponding data into the callback'
105 | , function( done ) {
106 | m.suggest( {q: 'i'} )
107 | .list( {q: 'i'} )
108 | .done( function( data1, data2 ) {
109 | assert( typeof data1 === 'object' );
110 | assert( typeof data2 === 'object' );
111 | done();
112 | } );
113 | } );
114 |
115 | it( 'should catch error when the request has error', function( done ) {
116 | var m = ModelProxy.create( 'Cart.*' );
117 | m.getMyCart()
118 | .withCookie( 'a=b' )
119 | .done( function( data ) {
120 |
121 | }, function( err ) {
122 | assert( err instanceof Error );
123 | done();
124 | } );
125 | } );
126 |
127 | it( 'should callback directly if no method is called before calling done()', function( done ) {
128 | var m = ModelProxy.create( 'Cart.*' )
129 | m.done( function( nothing ) {
130 | assert( nothing === undefined );
131 | done();
132 | } );
133 | } );
134 |
135 | it( 'should output the error when no errCallback is specified', function() {
136 | var m = ModelProxy.create( 'Cart.*' );
137 | m.getMyCart( {q:'a',key: 'b'} )
138 | .withCookie( 'a=b' )
139 | .done( function( data ) {
140 |
141 | } );
142 | } );
143 |
144 | it( 'should write the data into request body when the method type is POST', function( done ) {
145 | var model = ModelProxy.create( 'Test.post' );
146 | model.post( {
147 | a: 'abc',
148 | b: 'bcd',
149 | c: '{"a":"b"}'
150 | } ).done( function( data ) {
151 | assert.strictEqual( data, 'this is the msg from mockserver!' );
152 | done();
153 | } );
154 | } );
155 |
156 | it( 'should inspire errCallback due to the response is timeout', function( done ) {
157 | var model = ModelProxy.create( {
158 | 'getData': 'Test.timeoutUrl'
159 | } );
160 |
161 | model.getData()
162 | .done( function( data ) {
163 | }, function( err ) {
164 | console.log( err + 'timeout, timeout timeout timeout' );
165 | done();
166 | } )
167 | .error( function( err ) {
168 | console.log( err );
169 | done();
170 | } );
171 |
172 | } );
173 |
174 | } );
175 |
176 | describe( '#withCookie()', function() {
177 | it( 'should be called to set cookie before request the interface which is cookie needed'
178 | , function( done ) {
179 | var m = ModelProxy.create( 'Cart.*' );
180 | var cookie = 'ali_ab=42.120.74.193.1395041649126.7; l=c%E6%B5%8B%E8%AF%95%E8%B4%A6%E5%8F%B7135::1395387929931::11; cna=KcVJCxpk1XkCAX136Nv5aaC4; _tb_token_=DcE1K7Gbq9n; x=e%3D1%26p%3D*%26s%3D0%26c%3D0%26f%3D0%26g%3D0%26t%3D0%26__ll%3D-1%26_ato%3D0; whl=-1%260%260%260; ck1=; lzstat_uv=16278696413116954092|2511607@2511780@2738597@3258521@878758@2735853@2735859@2735862@2735864@2341454@2868200@2898598; lzstat_ss=3744453007_0_1396468901_2868200|970990289_0_1396468901_2898598; uc3=nk2=AKigXc46EgNui%2FwL&id2=Vy%2BbYvqj0fGT&vt3=F8dHqR%2F5HOhOUWkAFIo%3D&lg2=UtASsssmOIJ0bQ%3D%3D; lgc=c%5Cu6D4B%5Cu8BD5%5Cu8D26%5Cu53F7135; tracknick=c%5Cu6D4B%5Cu8BD5%5Cu8D26%5Cu53F7135; _cc_=U%2BGCWk%2F7og%3D%3D; tg=0; mt=ci=3_1&cyk=0_0; cookie2=1c5db2f359099faff00e14d7f39e16f2; t=e8bd0dbbf4bdb8f3704a1974b8a166b5; v=0; uc1=cookie14=UoLVYyvcdJF0aw%3D%3D';
181 | m.getMyCart()
182 | .withCookie( cookie )
183 | .done( function( data ) {
184 | done();
185 | }, function( err ) {
186 | console.log( err );
187 | done();
188 | } );
189 | } );
190 | } );
191 |
192 | describe( '#error()', function() {
193 | it( 'should specify the final error callback if there is no errCallback specified when done() is called'
194 | , function( done ) {
195 | var m = ModelProxy.create( 'Cart.*' );
196 | m.getMyCart()
197 | .withCookie( 'a=b' )
198 | .done( function( data ) {
199 |
200 | } ).error( function( err ) {
201 | done();
202 | } );
203 | } );
204 | } );
205 |
206 | } );
--------------------------------------------------------------------------------
/tests/proxyfactory.test.js:
--------------------------------------------------------------------------------
1 | var assert = require( 'assert' );
2 |
3 | var ProxyFactory = require( '../lib-cov/proxyfactory' );
4 | var InterfaceManager = require( '../lib-cov/interfacemanager' );
5 |
6 | var ifmgr = new InterfaceManager( '../tests/interface_test.json' );
7 | var cookie = 'ali_ab=42.120.74.193.1395041649126.7; l=c%E6%B5%8B%E8%AF%95%E8%B4%A6%E5%8F%B7135::1395387929931::11; cna=KcVJCxpk1XkCAX136Nv5aaC4; _tb_token_=DcE1K7Gbq9n; x=e%3D1%26p%3D*%26s%3D0%26c%3D0%26f%3D0%26g%3D0%26t%3D0%26__ll%3D-1%26_ato%3D0; whl=-1%260%260%260; ck1=; lzstat_uv=16278696413116954092|2511607@2511780@2738597@3258521@878758@2735853@2735859@2735862@2735864@2341454@2868200@2898598; lzstat_ss=3744453007_0_1396468901_2868200|970990289_0_1396468901_2898598; uc3=nk2=AKigXc46EgNui%2FwL&id2=Vy%2BbYvqj0fGT&vt3=F8dHqR%2F5HOhOUWkAFIo%3D&lg2=UtASsssmOIJ0bQ%3D%3D; lgc=c%5Cu6D4B%5Cu8BD5%5Cu8D26%5Cu53F7135; tracknick=c%5Cu6D4B%5Cu8BD5%5Cu8D26%5Cu53F7135; _cc_=U%2BGCWk%2F7og%3D%3D; tg=0; mt=ci=3_1&cyk=0_0; cookie2=1c5db2f359099faff00e14d7f39e16f2; t=e8bd0dbbf4bdb8f3704a1974b8a166b5; v=0; uc1=cookie14=UoLVYyvcdJF0aw%3D%3D';
8 |
9 | describe( 'ProxyFactory', function() {
10 |
11 | it( 'can only use object of InterfaceManager to initial the factory', function() {
12 | assert.throws( function() {
13 | ProxyFactory.use( {} );
14 | }, function( err ) {
15 | return err.toString()
16 | .indexOf( 'Proxy can only use instance of InterfacefManager' ) !== -1
17 | } );
18 | } );
19 | ProxyFactory.use( ifmgr );
20 |
21 | describe( '#getInterfaceIdsByPrefix()', function() {
22 | it( 'should return an id array', function() {
23 | assert.equal( ProxyFactory.getInterfaceIdsByPrefix( 'Search.' ).length, 3 );
24 | } );
25 | } );
26 |
27 | describe( '#create()', function() {
28 | it( 'should throw exception when the interface id is invalid', function() {
29 | assert.throws( function() {
30 | ProxyFactory.create( 'Search.getItems' );
31 | }, function( err ) {
32 | return err.toString()
33 | .indexOf( 'Invalid interface id: Search.getItems' ) !== -1;
34 | } );
35 | } );
36 | } );
37 |
38 | describe( '#Interceptor()', function() {
39 | it( 'should intercept the request which interface id is matched', function( done ) {
40 | var req = {
41 | headers: {
42 | cookie: ''
43 | },
44 | url: '/Search.suggest?q=a',
45 | on: function( eventName, callback ) {
46 | if ( eventName === 'data' ) {
47 | callback( 'mock chunk' );
48 | } else if ( eventName === 'end' ) {
49 | callback();
50 | }
51 | }
52 | };
53 | var res = {
54 | headers: {
55 |
56 | },
57 | end: function( data ) {
58 | assert.notEqual( data.length, 0 );
59 | done();
60 | },
61 | setHeader: function( key, value ) {
62 | this.headers[key] = value;
63 | },
64 | on: function( eventName, callback ) {
65 | if ( eventName === 'data' ) {
66 | callback( 'mock chunk' );
67 | } else if ( eventName === 'end' ) {
68 | callback();
69 | }
70 | }
71 | };
72 | ProxyFactory.Interceptor( req, res );
73 | } );
74 |
75 | it( 'should response 404 when the interface id is not matched', function() {
76 | var req = {
77 | headers: {
78 | cookie: ''
79 | },
80 | url: '/Search.what?q=a'
81 | };
82 | var res = {
83 | headers: {
84 |
85 | },
86 | end: function( data ) {
87 | assert.strictEqual( this.statusCode, 404 );
88 | },
89 | setHeader: function( key, value ) {
90 | this.headers[key] = value;
91 | }
92 | };
93 | ProxyFactory.Interceptor( req, res );
94 | } );
95 |
96 | it( 'should response 404 when the interface id is matched but the intercepted field is configurated as false'
97 | , function() {
98 | var req = {
99 | headers: {
100 | cookie: ''
101 | },
102 | url: '/D.getNav?q=c'
103 | };
104 | var res = {
105 | headers: {
106 |
107 | },
108 | end: function( data ) {
109 | assert.strictEqual( this.statusCode, 404 );
110 | },
111 | setHeader: function( key, value ) {
112 | this.headers[key] = value;
113 | }
114 | };
115 | ProxyFactory.Interceptor( req, res );
116 | } );
117 |
118 | it( 'should response client interfaces', function( done ) {
119 | var req = {
120 | headers: {
121 | cookie: ''
122 | },
123 | url: '/$interfaces',
124 | on: function( eventName, callback ) {
125 | if ( eventName === 'data' ) {
126 | callback( 'mock chunk' );
127 | } else if ( eventName === 'end' ) {
128 | callback();
129 | }
130 | }
131 | };
132 | var res = {
133 | end: function( data ) {
134 | assert.notEqual( data.length, 0 );
135 | done();
136 | }
137 | };
138 | ProxyFactory.Interceptor( req, res );
139 | } )
140 |
141 | } );
142 |
143 | } );
144 |
145 | var Proxy = ProxyFactory;
146 |
147 |
148 |
149 |
150 |
--------------------------------------------------------------------------------