├── .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 | ![](http://gtms03.alicdn.com/tps/i3/T1kp4XFNNXXXXaE5nO-688-514.png) 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 | ![](http://work.taobao.net/attachments/download/2929/Midway.png) 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 | --------------------------------------------------------------------------------