Repository: recruit-tech/agreed Branch: master Commit: dbd99b06d847 Files: 223 Total size: 506.8 KB Directory structure: gitextract_ghiif7wu/ ├── .github/ │ └── workflows/ │ └── nodejs.yml ├── .gitignore ├── README.md ├── media/ │ └── agreed.sketch ├── package.json ├── packages/ │ ├── cli/ │ │ ├── .gitignore │ │ ├── .travis.yml │ │ ├── README.md │ │ ├── bin/ │ │ │ ├── agreed-client.js │ │ │ ├── agreed-server.js │ │ │ ├── agreed-typed.js │ │ │ └── agreed-ui.js │ │ ├── media/ │ │ │ └── agreed.sketch │ │ ├── package.json │ │ ├── renovate.json │ │ └── test/ │ │ ├── agreed.json5 │ │ ├── bin/ │ │ │ ├── agreed-ui.js │ │ │ ├── not_passed.js │ │ │ └── pass.js │ │ └── not_passed.json5 │ ├── client/ │ │ ├── .gitignore │ │ ├── .travis.yml │ │ ├── README.md │ │ ├── bin/ │ │ │ └── agreed-client.js │ │ ├── index.js │ │ ├── lib/ │ │ │ ├── client.js │ │ │ ├── filter.js │ │ │ ├── reason.js │ │ │ ├── reporter.js │ │ │ └── requestPromise.js │ │ ├── package.json │ │ ├── renovate.json │ │ └── test/ │ │ ├── agreed.json5 │ │ ├── agreed_server.json5 │ │ ├── bin/ │ │ │ └── agreed-client.exec.js │ │ ├── client.js │ │ ├── index.js │ │ └── lib/ │ │ └── reason.js │ ├── core/ │ │ ├── .eaterrc │ │ ├── .eslintrc │ │ ├── .gitignore │ │ ├── .travis.yml │ │ ├── README.md │ │ ├── index.js │ │ ├── lib/ │ │ │ ├── agreed.js │ │ │ ├── check/ │ │ │ │ ├── CheckBodyStream.js │ │ │ │ ├── checker.js │ │ │ │ ├── completion.js │ │ │ │ ├── defaultRequest.js │ │ │ │ ├── defaultResponse.js │ │ │ │ ├── diff.js │ │ │ │ ├── extract.js │ │ │ │ ├── isContentJSON.js │ │ │ │ ├── isInclude.js │ │ │ │ └── schemaValidator.js │ │ │ ├── client.js │ │ │ ├── register.js │ │ │ ├── require_hook/ │ │ │ │ ├── compile.js │ │ │ │ ├── hook.js │ │ │ │ ├── json5.js │ │ │ │ ├── requireAgree.js │ │ │ │ ├── requireUncached.js │ │ │ │ ├── typescript.js │ │ │ │ └── yaml.js │ │ │ ├── server.js │ │ │ ├── template/ │ │ │ │ ├── bind.js │ │ │ │ ├── constants.js │ │ │ │ ├── format.js │ │ │ │ ├── hasTemplate.js │ │ │ │ ├── operation/ │ │ │ │ │ ├── index.js │ │ │ │ │ ├── parseInt.js │ │ │ │ │ ├── randomInt.js │ │ │ │ │ ├── randomString.js │ │ │ │ │ └── unixtime.js │ │ │ │ └── toRegexp.js │ │ │ └── utils/ │ │ │ └── logger.js │ │ ├── package.json │ │ ├── register.js │ │ ├── renovate.json │ │ ├── requireUncached.js │ │ └── test/ │ │ ├── agrees/ │ │ │ ├── agrees.js │ │ │ ├── agrees.json5 │ │ │ ├── agrees.ts │ │ │ ├── foo/ │ │ │ │ └── bar.yaml │ │ │ ├── hoge/ │ │ │ │ ├── foo.json │ │ │ │ └── fuga/ │ │ │ │ ├── _agree.json │ │ │ │ └── request.json │ │ │ ├── index.js │ │ │ ├── notify.js │ │ │ ├── qux/ │ │ │ │ ├── request.json │ │ │ │ └── response.json │ │ │ ├── schema/ │ │ │ │ └── hi.json │ │ │ └── sub.js │ │ ├── helper/ │ │ │ ├── espowerLoader.js │ │ │ ├── server.js │ │ │ └── server.notify.js │ │ └── lib/ │ │ ├── check/ │ │ │ ├── checker.checkNullish.js │ │ │ ├── diff.js │ │ │ ├── isInclude.js │ │ │ └── schema.js │ │ ├── client.broken.js │ │ ├── client.js │ │ ├── middleware.js │ │ ├── server.arrayreqs.js │ │ ├── server.arraysWithNull.js │ │ ├── server.bodyHasNull.js │ │ ├── server.checkHeadersCaseInsensitive.js │ │ ├── server.customFuncs.js │ │ ├── server.emptyHeaderValue.js │ │ ├── server.js │ │ ├── server.messages.js │ │ ├── server.nestedbind.js │ │ ├── server.notify.js │ │ ├── server.parseInt.js │ │ ├── server.path.js │ │ ├── server.pathParam.js │ │ ├── server.randomInt.js │ │ ├── server.randomString.js │ │ ├── server.statusTemplate.js │ │ ├── server.strict.js │ │ ├── server.typedcachepath.js │ │ ├── server.unixtime.js │ │ └── template/ │ │ ├── bind.js │ │ ├── format.js │ │ ├── format.operationalKey.js │ │ ├── hasTemplate.js │ │ └── toRegexp.js │ ├── server/ │ │ ├── .gitignore │ │ ├── .travis.yml │ │ ├── README.md │ │ ├── bin/ │ │ │ └── agreed-server.js │ │ ├── index.js │ │ ├── package.json │ │ ├── renovate.json │ │ └── test/ │ │ ├── agreed.json │ │ ├── agreedNotify.json5 │ │ ├── agreedProxy.json │ │ ├── app.js │ │ ├── cors.js │ │ ├── index.js │ │ ├── notify.js │ │ ├── proxy.js │ │ └── www_urlencoded.js │ ├── typed/ │ │ ├── .circleci/ │ │ │ └── config.yml │ │ ├── .gitignore │ │ ├── .npmignore │ │ ├── LICENSE │ │ ├── README.md │ │ ├── package.json │ │ ├── src/ │ │ │ ├── __tests__/ │ │ │ │ ├── data/ │ │ │ │ │ ├── agreed.ts │ │ │ │ │ ├── agrees-get.ts │ │ │ │ │ ├── agrees-get2.ts │ │ │ │ │ ├── agrees-patch.ts │ │ │ │ │ ├── agrees-post.ts │ │ │ │ │ ├── agrees-post2.ts │ │ │ │ │ ├── agrees-put.ts │ │ │ │ │ └── types.ts │ │ │ │ └── server.ts │ │ │ ├── bin/ │ │ │ │ └── agreed-typed.ts │ │ │ ├── commands/ │ │ │ │ ├── __tests__/ │ │ │ │ │ └── gen-swagger.test.ts │ │ │ │ └── gen-swagger.ts │ │ │ ├── generate-schema.ts │ │ │ ├── generate-swagger.ts │ │ │ ├── hook.ts │ │ │ ├── types.ts │ │ │ └── util.ts │ │ ├── tsconfig.json │ │ └── tslint.json │ └── ui/ │ ├── .babelrc │ ├── .eslintrc.json │ ├── .gitignore │ ├── .prettierrc.yml │ ├── LICENSE │ ├── README.md │ ├── bin/ │ │ └── agreed-ui.js │ ├── build/ │ │ ├── index.html │ │ ├── precache-manifest.c7cb397614ac16fe18525a8ed4105b10.js │ │ └── static/ │ │ ├── css/ │ │ │ └── main.e92ec541.chunk.css │ │ └── js/ │ │ ├── 2.b28c1fbe.chunk.js │ │ ├── main.ef43f0f5.chunk.js │ │ └── runtime-main.3336eaa2.js │ ├── package.json │ ├── public/ │ │ └── index.html │ ├── renovate.json │ ├── scripts/ │ │ └── build.js │ ├── server/ │ │ ├── index.js │ │ └── lib/ │ │ └── getAgreements.js │ ├── src/ │ │ ├── components/ │ │ │ ├── Agree/ │ │ │ │ ├── index.js │ │ │ │ └── styles.css │ │ │ ├── Agrees/ │ │ │ │ ├── index.js │ │ │ │ └── styles.css │ │ │ ├── App/ │ │ │ │ ├── App.test.js │ │ │ │ ├── index.js │ │ │ │ └── styles.css │ │ │ ├── Body/ │ │ │ │ ├── index.js │ │ │ │ └── styles.css │ │ │ ├── Definitions/ │ │ │ │ ├── index.js │ │ │ │ └── styles.css │ │ │ ├── JsonSchemaViewer/ │ │ │ │ ├── index.js │ │ │ │ └── styles.css │ │ │ ├── MethodLabel/ │ │ │ │ ├── index.js │ │ │ │ └── styles.css │ │ │ ├── Navigation/ │ │ │ │ ├── index.js │ │ │ │ └── styles.css │ │ │ ├── Request/ │ │ │ │ ├── index.js │ │ │ │ └── styles.css │ │ │ └── Response/ │ │ │ ├── index.js │ │ │ └── styles.css │ │ ├── index.css │ │ └── index.js │ └── test/ │ └── agrees/ │ ├── agrees.js │ ├── agrees.json5 │ ├── foo/ │ │ └── bar.yaml │ ├── hoge/ │ │ ├── foo.json │ │ └── fuga/ │ │ ├── _agree.json │ │ └── request.json │ ├── index.js │ ├── notify.js │ ├── qux/ │ │ ├── request.json │ │ └── response.json │ ├── schema/ │ │ └── hi.json │ └── sub.js └── renovate.json ================================================ FILE CONTENTS ================================================ ================================================ FILE: .github/workflows/nodejs.yml ================================================ # This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node # For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions name: Node.js CI on: push: branches: [ master ] pull_request: branches: [ master ] jobs: build: runs-on: ubuntu-latest strategy: matrix: node-version: [18.x, 20.x] steps: - uses: actions/checkout@v3 - name: Use Node.js ${{ matrix.node-version }} uses: actions/setup-node@v3 with: node-version: ${{ matrix.node-version }} - run: npm ci - run: npm run lint --workspaces --if-present - run: npm test --workspaces --if-present ================================================ FILE: .gitignore ================================================ *.swp node_modules .idea .DS_Store ================================================ FILE: README.md ================================================ # ![Node.js CI](https://github.com/recruit-tech/agreed/workflows/Node.js%20CI/badge.svg) agreed is Consumer Driven Contract tool with JSON mock server. agreed has 3 features. 1. Create contract file as json(json5/yaml/etc) file 1. mock server for frontend development 1. test client for backend development # Install ``` $ npm install @agreed/cli -g ``` # Usage ## Usage as Frontend Mock Server - Create agreed file (this file is used as a contract between frontend and backend) ```javascript // save as agreed.js module.exports = [ { request: { path: '/user/:id', method: 'GET', query: { q: '{:someQueryStrings}', }, values: { id: 'yosuke', someQueryStrings: 'foo' }, }, response: { headers: { 'x-csrf-token': 'csrf-token', }, body: { message: '{:greeting} {:id} {:someQueryStrings}', images: '{:images}', themes: '{:themes}', }, values: { greeting: 'hello', images: [ 'http://example.com/foo.jpg', 'http://example.com/bar.jpg', ], themes: { name: 'green', }, } }, }, ] ``` - Run server ``` $ agreed-server --path ./agreed.js --port 3010 ``` - curl to the mock server ``` $ curl http://localhost:3010/user/yosuke?q=foo { "message": "hello yosuke foo", "images": [ "http://example.com/foo.jpg", "http://example.com/bar.jpg" ], "themes": { "name": "green" } } ``` ## Usage as Backend test client - Run test client for confirm response ``` $ agreed-client --path ./agreed.js --port 3030 --host example.com ``` ## Usage: Agreed Documentation - Run documentation server ``` $ agreed-ui --path ./agreed.js --port 3031 ``` ![ScreenShot](https://raw.githubusercontent.com/recruit-tech/agreed-ui/master/screenshot.png) ================================================ FILE: package.json ================================================ { "name": "root", "private": true, "scripts": {}, "version": "0.1.0", "workspaces": [ "packages/cli", "packages/client", "packages/core", "packages/server", "packages/typed", "packages/ui" ] } ================================================ FILE: packages/cli/.gitignore ================================================ *.swp node_modules .idea .DS_Store ================================================ FILE: packages/cli/.travis.yml ================================================ language: node_js node_js: - "6" - "8" script: npm test sudo: false ================================================ FILE: packages/cli/README.md ================================================ # [![Build Status](https://travis-ci.org/recruit-tech/agreed.svg?branch=master)](https://travis-ci.org/recruit-tech/agreed) agreed is Consumer Driven Contract tool with JSON mock server. agreed has 3 features. 1. Create contract file as json(json5/yaml/etc) file 1. mock server for frontend development. 1. test client for backend development # Install ``` $ npm install agreed -g ``` # Usage ## Usage as Frontend Mock Server - Create agreed file (this file is used as a contract between frontend and backend) ```javascript // save as agreed.js module.exports = [ { request: { path: '/user/:id', method: 'GET', query: { q: '{:someQueryStrings}', }, values: { id: 'yosuke', someQueryStrings: 'foo' }, }, response: { headers: { 'x-csrf-token': 'csrf-token', }, body: { message: '{:greeting} {:id} {:someQueryStrings}', images: '{:images}', themes: '{:themes}', }, values: { greeting: 'hello', images: [ 'http://example.com/foo.jpg', 'http://example.com/bar.jpg', ], themes: { name: 'green', }, } }, }, ] ``` - Run server ``` $ agreed-server --path ./agreed.js --port 3010 ``` - curl to the mock server ``` $ curl http://localhost:3010/user/yosuke?q=foo { "message": "hello yosuke foo", "images": [ "http://example.com/foo.jpg", "http://example.com/bar.jpg" ], "themes": { "name": "green" } } ``` ## Usage as Backend test client - Run test client for confirm response ``` $ agreed-client --path ./agreed.js --port 3030 --host example.com ``` ## Usage: Agreed Documentation - Run documentation server ``` $ agreed-ui --path ./agreed.js --port 3031 ``` ![ScreenShot](https://raw.githubusercontent.com/recruit-tech/agreed-ui/master/screenshot.png) ================================================ FILE: packages/cli/bin/agreed-client.js ================================================ #!/usr/bin/env node require('@agreed/client/bin/agreed-client.js'); ================================================ FILE: packages/cli/bin/agreed-server.js ================================================ #!/usr/bin/env node require('@agreed/server/bin/agreed-server.js'); ================================================ FILE: packages/cli/bin/agreed-typed.js ================================================ #!/usr/bin/env node require('@agreed/typed/lib/bin/agreed-typed.js'); ================================================ FILE: packages/cli/bin/agreed-ui.js ================================================ #!/usr/bin/env node require('@agreed/ui/bin/agreed-ui.js'); ================================================ FILE: packages/cli/package.json ================================================ { "name": "@agreed/cli", "version": "6.0.0", "description": "Agreed is a tool for Consumer Driven Contract with JSON mock server", "main": "index.js", "bin": { "agreed-server": "bin/agreed-server.js", "agreed-client": "bin/agreed-client.js", "agreed-ui": "bin/agreed-ui.js" }, "scripts": { "test": "eater" }, "repository": { "type": "git", "url": "github.com/recruit-tech/agreed" }, "keywords": [ "agreed", "consumer", "driven", "contract", "mock", "json", "server" ], "author": "yosuke-furukawa", "license": "MIT", "devDependencies": { "eater": "4.0.4", "plz-port": "1.0.0" }, "dependencies": { "@agreed/client": "^6.0.0", "@agreed/server": "^6.0.0", "@agreed/typed": "^6.0.0", "@agreed/ui": "^6.0.0", "colo": "^1.0.0", "json5": "^2.0.0", "minimist": "^1.2.0" }, "homepage": "https://github.com/recruit-tech/agreed#readme", "publishConfig": { "access": "public" }, "directories": { "test": "test" } } ================================================ FILE: packages/cli/renovate.json ================================================ { "extends": [ "config:base" ] } ================================================ FILE: packages/cli/test/agreed.json5 ================================================ [ { request: { path: '/foo/ba/:hoge', method: 'GET', query: { id: '{:id}' }, values: { id: '123', hoge: 'hoge' } }, response: { status: 200, body: { message: 'hello', id: '{:id}', path: '{:hoge}', }, values: { name: 'yosuke' } }, }, ] ================================================ FILE: packages/cli/test/bin/agreed-ui.js ================================================ const assert = require('assert'); const plzPort = require('plz-port'); const cp = require('child_process'); const http = require('http'); const path = './test/agreed.json5'; plzPort().then((port) => { const proc = cp.exec(`${process.cwd()}/bin/agreed-ui.js --port ${port} --path ${path}`); setTimeout(() => { http.get(`http://localhost:${port}`, (response) => { let body = '' response.on('data', (d) => body += d); response.on('end', () => { assert(body.includes("Agreed UI")) proc.kill('SIGHUP'); process.exit(0); }); }); }, 10000) }); ================================================ FILE: packages/cli/test/bin/not_passed.js ================================================ const assert = require('assert'); const plzPort = require('plz-port'); const cp = require('child_process'); const pass = './test/agreed.json5'; const not_pass = './test/not_passed.json5'; plzPort().then((port) => { const proc = cp.exec(`${process.cwd()}/bin/agreed-server.js --port ${port} --path ${not_pass}`); setTimeout(() => { const result = cp.execSync(`${process.cwd()}/bin/agreed-client.js --port ${port} --path ${pass}`).toString(); assert(result.indexOf('✗ fail') >= 0); proc.kill(); setTimeout(() => { process.exit(0); }, 500); }, 10000); }); ================================================ FILE: packages/cli/test/bin/pass.js ================================================ const assert = require('assert'); const plzPort = require('plz-port'); const cp = require('child_process'); const path = './test/agreed.json5'; plzPort().then((port) => { const proc = cp.exec(`${process.cwd()}/bin/agreed-server.js --port ${port} --path ${path}`); setTimeout(() => { const result = cp.execSync(`${process.cwd()}/bin/agreed-client.js --port ${port} --path ${path}`).toString(); console.log(result); assert(result.indexOf('✔ pass') >= 0); proc.kill(); setTimeout(() => { process.exit(0); }, 500); }, 10000); }); plzPort().then((port) => { const proc = cp.exec(`${process.cwd()}/bin/agreed-server.js --port ${port} --path ${path} --default-headers " { 'access-control-allow-origin': '*' } "`); setTimeout(() => { const result = cp.execSync(`${process.cwd()}/bin/agreed-client.js --port ${port} --path ${path}`).toString(); console.log(result); assert(result.indexOf('✔ pass') >= 0); proc.kill(); setTimeout(() => { process.exit(0); }, 500); }, 10000); }); ================================================ FILE: packages/cli/test/not_passed.json5 ================================================ [ { request: { path: '/foo/ba/:hoge', method: 'GET', query: { id: '{:id}' }, values: { id: '123', hoge: 'hoge' } }, response: { status: 200, body: { message: 'hi!', path: '{:hoge}', }, values: { name: 'yosuke' } }, }, ] ================================================ FILE: packages/client/.gitignore ================================================ *.swp .idea node_modules .DS_Store ================================================ FILE: packages/client/.travis.yml ================================================ language: node_js node_js: - "6" - "8" script: npm test sudo: false ================================================ FILE: packages/client/README.md ================================================ agreed-client =================== [![Build Status](https://travis-ci.org/recruit-tech/agreed-client.svg?branch=master)](https://travis-ci.org/recruit-tech/agreed-client) An agreed client for check response. # Install ``` $ npm install agreed-client -D ``` # Usage ```javascript const agreedClient = require('agreed-client') const { client, agrees, reporter, } = agreedClient({ path: './test/agreed.json5', // required scheme: 'http', // optional, default is http host: 'localhost', // optional, default is localhost port: 30103, // optional, default is 80 defaultRequestHeaders: { 'x-jwt-token': 'foobarbaz' } }) client.requestPromise(agrees) .then(reporter) .catch((e) => { console.error(e) process.exit(1) }) ``` ================================================ FILE: packages/client/bin/agreed-client.js ================================================ #!/usr/bin/env node const minimist = require('minimist'); const execArgv = minimist(process.execArgv); const argv = minimist(process.argv.slice(2)); const colo = require('colo'); const agreedClient = require('../index.js'); const JSON5 = require('json5'); function showHelp(exitcode) { console.log(` agreed-client [--path agreed path file (required)][--port request server port default 80][--host request server host default localhost][--scheme request server scheme default http][--default-request-headers request default headers] agreed-client --path ./agreed.js --port 4000 agreed-client --path ./agreed.js --port 4000 --host example.com --scheme http --default-request-headers "{ 'x-auth-token': 'fugafuga' }" `); process.exit(exitcode); } if (argv.help) { showHelp(0); } if (argv.version) { const pack = require('../package.json'); console.log(pack.version); process.exit(0); } if (!argv.path) { console.error(colo.red('[agreed-client]: --path option is required')); showHelp(1); } if (argv['default-request-headers']) { argv.defaultRequestHeaders = JSON5.parse(argv['default-request-headers']); } const { client, agrees, reporter } = agreedClient(argv); client.requestPromise(agrees).then(reporter).catch((e) => { console.error(e); process.exit(1); }); ================================================ FILE: packages/client/index.js ================================================ 'use strict'; const Agreed = require('@agreed/core'); const filter = require('./lib/filter'); const requestPromise = require('./lib/requestPromise'); const agreedReporter = require('./lib/reporter'); module.exports = (opts) => { if (!opts) { throw new Error('[agreed-client] option is required.'); } if (!opts.path) { throw new Error('[agreed-client] option.path is required.'); } opts.scheme = opts.scheme || 'http'; opts.host = opts.host || 'localhost'; opts.port = opts.port || 80; const agreed = new Agreed(); const client = agreed.createClient(opts); const agrees = filter(client.getAgreement(), opts.filter); client.requestPromise = requestPromise; const reporter = agreedReporter(agrees); return { client, agrees, reporter, }; }; ================================================ FILE: packages/client/lib/client.js ================================================ 'use strict'; const agreed = require('@agreed/core'); const filter = require('./lib/filter'); ================================================ FILE: packages/client/lib/filter.js ================================================ 'use strict'; module.exports = (agrees, filter) => { var result = agrees; if (!filter) { return result; } if (filter.path) { result = result.filter((agree) => { return agree.request.path.indexOf(filter.path) === 0; }); } if (filter.method) { result = result.filter((agree) => { return agree.request.method === filter.method; }); } return result; }; ================================================ FILE: packages/client/lib/reason.js ================================================ 'use strict'; const colo = require('colo'); const isEmpty = require('is-empty'); module.exports.schemaErrors = (errors) => { if (isEmpty(errors)) { return; } if (Array.isArray(errors)) { errors.forEach((error) => { console.log(`${colo.bold('schema errors are found.')}`); console.log(error.stack); }); } }; module.exports.diff = function reason(diff, depth) { if (depth == null) { depth = 0; } if (Array.isArray(diff) && diff.length === 2) { console.log(''); const agreed = diff[0]; const actual = diff[1]; const exp = explain(agreed, actual); exp && console.log(colo.bold(exp)); console.log(`${colo.bold.cyan('agreed: ')} ${show(agreed)}`); console.log(`${colo.bold.red('actual: ')} ${show(actual)}`); console.log(''); return; } if (depth > 0) { process.stdout.write(colo.bold('.')); } Object.keys(diff).forEach((key) => { process.stdout.write(colo.bold(`${key}`)); reason(diff[key], depth + 1); }); }; function explain(agreed, actual) { if (agreed && actual == null) { return 'actual value is undefined, agreed needs some value.'; } if (typeof agreed !== typeof actual) { return `mismatch type, agreed type is ${typeof agreed}, but actual type is ${typeof actual}`; } if (agreed !== actual) { return `mismatch value, agreed value is ${agreed}, but actual value is ${actual}`; } } function show(value) { if (value && typeof value === 'object') { return JSON.stringify(value); } return value; } ================================================ FILE: packages/client/lib/reporter.js ================================================ 'use strict'; const isEmpty = require('is-empty'); const colo = require('colo'); const reason = require('./reason'); module.exports = (agrees) => (results) => { return results.map((result, i) => { const agree = agrees[i]; const body = result.body; const diff = result.diff; const schemaErrors = result.schemaErrors; const path = `${agree.request.path}`; if ((isEmpty(result) || isEmpty(diff)) && isEmpty(schemaErrors)) { console.log(`${colo.bold.green('✔ pass!')} ${agree.request.method} ${path}`); } else { console.log(`${colo.bold.red('✗ fail!')} ${agree.request.method} ${path}`); body && console.log(`${colo.green('body: ')}`, body); reason.diff(diff); reason.schemaErrors(schemaErrors); } }); }; ================================================ FILE: packages/client/lib/requestPromise.js ================================================ 'use strict'; // wrapper for request executor module.exports = function(agrees) { return new Promise((resolve, reject) => { const requests = this.createRequests(agrees); const results = []; requests.forEach((request, i) => { request.on('response', (response) => { this.checkResponse(response, agrees[i]).on('result', (result) => { results[i] = result; if (results.filter(Boolean).length === requests.length) { return resolve(results); } }); }); request.on('error', reject); request.end(); }); }); }; ================================================ FILE: packages/client/package.json ================================================ { "name": "@agreed/client", "version": "6.0.0", "description": "agreed client, testing utilities", "main": "index.js", "bin": { "client": "bin/agreed-client.js" }, "scripts": { "test": "eater" }, "repository": { "type": "git", "url": "git+https://github.com/recruit-tech/agreed.git" }, "keywords": [ "agreed", "client", "test", "confirmation" ], "author": "yosuke-furukawa", "license": "MIT", "dependencies": { "@agreed/core": "^6.0.0", "colo": "^1.0.0", "is-empty": "^1.0.0" }, "devDependencies": { "@agreed/server": "^6.0.0", "eater": "4.0.4", "plz-port": "1.0.0" }, "publishConfig": { "access": "public" }, "bugs": { "url": "https://github.com/recruit-tech/agreed/issues" }, "homepage": "https://github.com/recruit-tech/agreed#readme", "directories": { "lib": "lib", "test": "test" } } ================================================ FILE: packages/client/renovate.json ================================================ { "extends": [ "config:base" ] } ================================================ FILE: packages/client/test/agreed.json5 ================================================ [ { request: { path: "/users/:id", method: "GET", values: { id: 'foo' } }, response: { body: { message: 'hello {:id}', id: '{:val}', }, values: { id: 2 } } }, { request: { path: "/users/schema/:id", method: "GET", values: { id: 'foo' } }, response: { body: { message: 'hello {:id}', id: '{:val}', }, schema: { type: 'object', properties: { message: { type: 'string', }, id: { type: 'integer', } } }, values: { id: 2 } } } ] ================================================ FILE: packages/client/test/agreed_server.json5 ================================================ [ { request: { path: "/users/:id", method: "GET", values: { id: 'foo' } }, response: { body: { message: 'hi {:id}', id: '{:id}', } } }, { request: { path: "/users/schema/:id", method: "GET", values: { id: 'foo' } }, response: { body: { message: 'hello {:id}', id: '{:val}', }, values: { id: '2' } } } ] ================================================ FILE: packages/client/test/bin/agreed-client.exec.js ================================================ 'use strict'; const assert = require('assert'); const cp = require('child_process'); const agreedServer = require('@agreed/server'); const path = './test/agreed.json5'; const server = agreedServer({ path: './test/agreed.json5', port: 0, }).createServer(); server.on('listening', () => { setTimeout(() => { process.exit(0); }, 500); const exec = `${process.cwd()}/bin/agreed-client.js --port ${server.address().port} --path ${path}`; const proc = cp.exec(exec); let data = ''; proc.on('data', (d) => data += d); proc.on('end', () => { assert(data.indexOf('✔ pass') >= 0); server.close(); proc.kill(); }); }); ================================================ FILE: packages/client/test/client.js ================================================ 'use strict'; const agreedClient = require('../index'); const agreedServer = require('@agreed/server'); const assert = require('assert'); const test = require('eater/runner').test; test('client: getAgreement', () => { const { client } = agreedClient({ path: './test/agreed.json5', port: 0, }); const agrees = client.getAgreement(); assert(agrees); }); test('client: requestPromise', () => { const server = agreedServer({ path: './test/agreed.json5', port: 0, }).createServer(); server.on('listening', () => { const { client } = agreedClient({ path: './test/agreed.json5', port: server.address().port, }); const agrees = client.getAgreement(); client.requestPromise(agrees).then((results) => { assert(results.length === 2); server.close(); }); }); }); ================================================ FILE: packages/client/test/index.js ================================================ 'use strict'; const agreedClient = require('../index'); const agreedServer = require('@agreed/server'); const test = require('eater/runner').test; test('client: smoke', () => { const server = agreedServer({ path: './test/agreed_server.json5', port: 0, }).createServer(); server.on('listening', () => { const { client, agrees, reporter } = agreedClient({ path: './test/agreed.json5', port: server.address().port, }); client.requestPromise(agrees).then(reporter).then(() => { server.close(); }); }); }); ================================================ FILE: packages/client/test/lib/reason.js ================================================ 'use strict'; const reason = require(`${process.cwd()}/lib/reason`); const test = require('eater/runner').test; test('reporter: show reason', () => { const diff = { hoge: [ '{:aaa}', undefined ], ghi: [ 1, 2 ], foo: { b: { fff: [{ a: 'hello {:aa}' }, 'aaa'] } } }; reason.diff(diff); }); ================================================ FILE: packages/core/.eaterrc ================================================ { dir: 'test/lib/', require: [ './test/helper/espowerLoader.js' ] } ================================================ FILE: packages/core/.eslintrc ================================================ { "extends": [ "prettier" ], "env": { "node": true, "jest": true, "es6": true }, "plugins": [ "prettier" ], "rules": { "prettier/prettier": ["error"] } } ================================================ FILE: packages/core/.gitignore ================================================ *.swp node_modules .idea .nyc_output coverage .DS_Store ================================================ FILE: packages/core/.travis.yml ================================================ language: node_js node_js: - "10" - "12" script: - npm run lint - npm test after_success: npm run codecov sudo: false cache: directories: - node_modules ================================================ FILE: packages/core/README.md ================================================ agreed-core ==================== [![Build Status](https://travis-ci.org/recruit-tech/agreed-core.svg?branch=master)](https://travis-ci.org/recruit-tech/agreed-core) [![codecov](https://codecov.io/gh/recruit-tech/agreed-core/branch/master/graph/badge.svg)](https://codecov.io/gh/recruit-tech/agreed-core) agreed is Consumer Driven Contract tool with JSON mock server. agreed has 3 features. 1. Create contract file as json(json5/yaml/etc) file 1. mock server for frontend development. 1. test client for backend development `agreed-core` is a library to create test client and mock server. `agreed-core` provide the following features. 1. json5/yaml require hook, you can write require('foo.json5') / require('bar.yaml') using agreed-core/register. 1. server middleware, agreed-core provides express/pure node http middleware. 1. test client, agreed-core provides response check. # Install ``` $ npm install agreed-core --dev ``` # Usage ## Usage as Frontend Mock Server - Create agreed file (this file is used as a contract between frontend and backend) ```javascript module.exports = [ { request: { path: '/user/:id', method: 'GET', query: { q: '{:someQueryStrings}', index: '{:index}', }, values: { id: 'yosuke', someQueryStrings: 'bye', index: 2, }, }, response: { headers: { 'x-csrf-token': 'csrf-token', }, body: { // hello yosuke bye message: '{:greeting} {:id} {:someQueryStrings}', // http://example.com/baz.jpg image: '{:images[:index]}', themes: [ // { name: 'green' } { name: '{:themes.0.name}' }, // { name: 'blue' }, { name: 'red' } '{:themes.1-last}' ], }, // you can write json schema // schema: { // type: 'object', // properties: { // message: { type: 'string' }, // image: { type: 'string' }, // themes: { // type: 'array', // items: { // type: 'object', // properties: { // name: { type: 'string' } // } // } // } // } // }, values: { greeting: 'hello', images: [ 'http://example.com/foo.jpg', 'http://example.com/bar.jpg', 'http://example.com/baz.jpg', ], themes: [ { name: 'green', }, { name: 'blue', }, { name: 'red', }, ] } }, }, ] ``` - Create server We support express, pure node.js and any other frameworks can use agreed. ```javascript 'use strict'; const express = require('express'); const bodyParser = require('body-parser'); const Agreed = require('agreed-core'); const agreed = new Agreed(); const app = express(); app.use(bodyParser.json()); app.use(agreed.middleware({ path: './agreed/file/agreed.js', })); app.use((err, req, res, next) => { res.statusCode = 500; res.send(`Error is occurred : ${err}`); }); app.listen(3000); ``` ``` $ node server.js ``` - call server from client ``` $ curl http://localhost:3000/user/alice?q=foo { "message": "hello alice foo", "images": [ "http://example.com/foo.jpg", "http://example.com/bar.jpg" ], "themes": { "name": "green", }, } ``` ## Usage as Backend test client agreed can be test client. - Reuse agreed file ```javascript module.exports = [ { request: { path: '/user/:id', method: 'GET', query: { q: '{:someQueryStrings}', }, values: { id: 'yosuke', someQueryStrings: 'foo' }, }, response: { headers: { 'x-csrf-token': 'csrf-token', }, body: { message: '{:greeting} {:id} {:someQueryStrings}', images: '{:images}', themes: '{:themes}', }, values: { greeting: 'hello', images: [ 'http://example.com/foo.jpg', 'http://example.com/bar.jpg', ], themes: { name: 'green', }, } }, }, ] ``` - Create test client ```javascript 'use strinct'; const Agreed = require('agreed-core'); const agreed = new Agreed(); const client = agreed.createClient({ path: './agreed/file/agreed.js', host: 'example.com', port: 12345, }); // Get Agreements as array. const agrees = client.getAgreement(); // request to servers. // in this case, GET example.com:12345/user/yosuke?q=foo const responses = client.executeAgreement(agrees); // Check response object. client.checkResponse(responses, agrees).then((diffs) => { // if the response is mismatched to agreed response, // you can get diff. // but if no difference, you can get empty object {} diffs.forEach((diff) => { if (Object.keys(diff).length > 0) { console.error('your request does not matched: ', diff); } }); }); ``` # APIs ## Agreement ### how to define API specs Agreement file can be written in JSON5/YAML/JavaScript format. You can choose your favorite format. - JSON5 example ```javascript [ { "request": { "path": '/hoge/fuga', "method": 'GET', // you can write query "query": { "q": 'foo', }, }, response: { headers: { 'x-csrf-token': 'csrf-token', }, body: { message: 'hello world', }, }, }, { "request": { // you can write regexp path, // match /users/yosuke "path": '/users/:id', "method": 'GET', }, response: { // embed path :id to your response body // if request path /users/yosuke // return { "message": "hello yosuke" } body: { message: 'hello {:id}', }, }, }, // you can write json file // see test/agrees/hoge/foo.json './hoge/foo.json', // you can write yaml file // see test/agrees/foo/bar.yaml './foo/bar.yaml', // you can separate request/response json { request: './qux/request.json', response: './qux/response.json', }, { request: { path: '/path/:id', method: 'POST', // query embed data, any query is ok. query: { meta: "{:meta}", }, body: { message: "{:message}" }, // value for test client values: { id: 'yosuke', meta: true, message: 'foobarbaz' }, }, response: { headers: { 'x-csrf-token': 'csrf-token', }, body: { // :id is for request value message: 'hello {:id}, {:meta}, {:message}', }, }, }, { request: { path: '/images/:id', method: 'GET', query: { q: '{:someQueryStrings}', }, values: { id: 'yosuke', someQueryStrings: 'foo' }, }, response: { headers: { 'x-csrf-token': 'csrf-token', }, body: { message: '{:greeting} {:id} {:someQueryStrings}', images: '{:images}', themes: '{:themes}', }, values: { greeting: 'hello', images: [ 'http://example.com/foo.jpg', 'http://example.com/bar.jpg', ], themes: { name: 'green', }, } }, }, { request: { path: '/useschema/:index', method: 'GET', values: { index: 1 } }, response: { body: { result : '{:list[:index]}' }, // you can write json schema schema: { type: 'object', properties: { result: { type: 'string' } }, }, values: { list: [ 'hello', 'hi', 'dunke', ] } }, }, ] ``` ================================================ FILE: packages/core/index.js ================================================ module.exports = require("./lib/agreed"); ================================================ FILE: packages/core/lib/agreed.js ================================================ "use strict"; const Server = require("./server"); const Client = require("./client"); class Agreed { middleware(options) { this.server = new Server(options); return (req, res, next) => { this.server.useMiddleware(req, res, next); }; } createClient(options) { this.client = new Client(options); return this.client; } } module.exports = Agreed; ================================================ FILE: packages/core/lib/check/CheckBodyStream.js ================================================ "use strict"; const Writable = require("stream").Writable; const diff = require("./diff"); const schemaValidator = require("./schemaValidator"); class CheckBodyStream extends Writable { constructor(options) { super(options); this.data = ""; this.result = {}; this.on("finish", () => { if (typeof this.expect === "object") { if (this.data.length === 0) { this.result.body = this.data; this.result.error = "empty body"; this.emit("checked", this.result); return; } try { this.data = JSON.parse(this.data); } catch (e) { this.result.body = this.data; this.result.error = "broken json: " + e; this.emit("checked", this.result); return; } this.result.body = this.data; this.result.diff = diff(this.expect, this.data); } else { this.result.body = this.data; this.result.diff = diff(this.expect, this.data) || {}; } if (this.expectSchema) { const validatedResult = schemaValidator(this.data, this.expectSchema); const errors = validatedResult.errors; if (errors.length > 0) { this.result.schemaErrors = errors; } } this.emit("checked", this.result); }); } expect(obj) { this.expect = obj; } schema(obj) { this.expectSchema = obj; } _write(chunk, encoding, callback) { this.data += chunk; callback(); } } module.exports = CheckBodyStream; ================================================ FILE: packages/core/lib/check/checker.js ================================================ "use strict"; const isInclude = require("./isInclude"); const url = require("url"); const logger = require("../utils/logger"); const { hasTemplate } = require("../template/hasTemplate"); const tmplBind = require("../template/bind"); const nullishStrings = ["undefined", "null", ""]; class Checker { static validRequest(result, request, req, debug) { if (!result || !result.error) { return true; } if (debug) { logger.log(result.error); logger.log("agreed request", request); logger.log("actual request", req); } return false; } static request(request, req, options) { const parsedUrl = url.parse(req.url); let similarity = 0; let result = Checker.method(request.method, req.method); similarity += result.similarity; if (!Checker.validRequest(result, request, req, options.debug)) { return { result: false, similarity, error: result.error }; } result = Checker.url(request.pathToRegexp, parsedUrl.pathname, options); similarity += result.similarity; if (!Checker.validRequest(result, request, req, options.debug)) { return { result: false, similarity, error: result.error }; } result = Checker.headers(request.headers, req.headers, options); similarity += result.similarity; if (!Checker.validRequest(result, request, req, options.debug)) { return { result: false, similarity, error: result.error }; } result = Checker.body(request.body, req.body, options); similarity += result.similarity; if (!Checker.validRequest(result, request, req, options.debug)) { return { result: false, similarity, error: result.error }; } // query // The value of the given query character. result = Checker.query(request.query, req.query, options); similarity += result.similarity; if ( !Checker.validRequest(result, request, req, options.debug) || (result.similarity == 0 && options.enablePreferQuery) ) { return { result: false, similarity, error: result.error }; } return { result: true, similarity }; } static method(entryMethod, reqMethod) { const result = { similarity: 1 }; if (entryMethod.toLowerCase() !== reqMethod.toLowerCase()) { result.type = "METHOD"; result.error = `Does not match METHOD, expect ${entryMethod}, but ${reqMethod}.`; result.similarity = 0; } return result; } static url(entryUrl, reqUrl, options) { const result = { similarity: 1 }; const match = entryUrl.exec(reqUrl); const { pathToRegexpKeys, values = {} } = options; if (!match) { result.type = "URL"; result.error = `Does not match URL, expect ${entryUrl}, but ${reqUrl}.`; result.similarity = 0; return result; } const paths = {}; pathToRegexpKeys.forEach((pathKey, index) => { paths[pathKey.name] = match[index + 1]; }); let valuesSimilarity = 1; if (pathToRegexpKeys.length !== 0) { let matched = 0; Object.keys(paths).forEach((k) => { if (paths[k] === values[k] + "") { matched++; } }); valuesSimilarity = matched / pathToRegexpKeys.length; } const nullish = Checker.checkNullish(paths); const nullishError = nullish.error; const nullishSimilarity = nullish.similarity; result.type = "URL"; result.error = nullishError; result.similarity = nullishSimilarity; if (result.similarity >= 1) { result.similarity += valuesSimilarity; } return result; } static headers(entryHeaders, reqHeaders, options) { const result = { similarity: 1 }; if (!entryHeaders) return result; if (!isInclude(entryHeaders, reqHeaders)) { result.type = "HEADERS"; result.error = `Does not include header, expect ${JSON.stringify( entryHeaders )} but ${JSON.stringify(reqHeaders)}`; result.similarity = 0; return result; } if (!options.skipCheckHeaderValueNullable) { const nullish = Checker.checkNullish(reqHeaders); const nullishError = nullish.error; const nullishSimilarity = nullish.similarity; if (nullish) { result.type = "HEADERS"; result.error = nullishError; result.similarity = nullishSimilarity; return result; } } return result; } static body(entryBody, reqBody, options) { const result = { similarity: 1 }; if (!entryBody) return result; if (!isInclude(entryBody, reqBody)) { result.type = "BODY"; result.error = `Does not include body, expect ${JSON.stringify( entryBody )} but ${JSON.stringify(reqBody)}`; result.similarity = 0; } return result; } static query(entryQuery, reqQuery, options) { const result = { similarity: 1 }; if (!entryQuery) return result; if (!isInclude(entryQuery, reqQuery)) { result.type = "QUERY"; result.error = `Does not include query, expect ${JSON.stringify( entryQuery )} but ${JSON.stringify(reqQuery)}`; result.similarity = 0; } if (options.enablePreferQuery) { // e.g.) // entryQuery = { q: "{:someQueryStrings }" } // reqQuery = { q: "bar" } const existEntryQuery = Object.keys(entryQuery).length > 0; if (existEntryQuery) return this.queryWhenCarefulCheckRequired( reqQuery, entryQuery, options ); } return result; } static queryWhenCarefulCheckRequired(reqQuery, entryQuery, options) { const result = { similarity: 1 }; const isMatch = Object.keys(reqQuery).every((key) => { if (entryQuery[key] != undefined) { const tmpl = hasTemplate(entryQuery[key]); const hasTmpl = tmpl !== null && tmpl.length > 0; const reqValue = reqQuery[key]; if (hasTmpl) { // e.g.) {":someQueryStrings"} => someQueryStrings const normalizedKey = entryQuery[key].replace(/\{|\}|\:/g, ""); return this.matchQueryWhenHasTmpl( reqQuery, entryQuery, options, reqValue, normalizedKey ); } else { // e.g.) entryQuery = { foo: "foo", bar: "bar" } // e.g.) entryParameters = { id: "yosuke" } const entryQueryValue = entryQuery[key]; return reqValue === entryQueryValue; } } else { // An undefined query string may have been passed return false; } }); result.similarity = isMatch ? 1 : 0; return result; } static matchQueryWhenHasTmpl( reqQuery, entryQuery, options, reqValue, normalizedKey ) { // e.g.) { "someQueryStrings": "bar" } const calcEntryParameters = tmplBind(entryQuery, reqQuery); const calcEntryQueryValue = calcEntryParameters[normalizedKey]; // e.g.) { id: "yosuke", someQueryStrings: "bar" } // include path parameters const entryParameters = options.values; // Consideration when the query string is given in the path // // e.g.) // path = "/test/arrayreqs/agreed/values?year_months[]=201708&year_months[]=201709&year_months[]=201810" // options = { pathToRegexpKeys: [], values: undefined, debug: undefined } if (entryParameters !== undefined) { const entryQueryValue = entryParameters[normalizedKey]; // By definition, it can be defined as a numerical value, but in reality, only a numerical value as a character string can be passed. return ( calcEntryQueryValue == String(entryQueryValue) && reqValue === String(entryQueryValue) ); } else { return reqValue == calcEntryQueryValue; } } static checkNullish(obj = {}) { const result = { similarity: 1 }; const keys = Object.keys(obj); for (var i = 0; i < keys.length; i++) { const key = keys[i]; const realValue = obj[key]; if (typeof realValue === "object" && realValue) { return Checker.checkNullish(realValue); } if (nullishStrings.indexOf(realValue) >= 0) { result.error = `Request value has nullish strings ${realValue} in ${key}`; result.similarity = 0.5; return result; } } return result; } } module.exports = Checker; ================================================ FILE: packages/core/lib/check/completion.js ================================================ "use strict"; const { pathToRegexp } = require("path-to-regexp"); const path = require("path"); const requireAgree = require("../require_hook/requireAgree"); const DEFAULT_REQUEST = require("./defaultRequest"); const DEFAULT_RESPONSE = require("./defaultResponse"); module.exports = (agree, base, opts = {}) => { if (typeof agree === "string") { agree = normalizedRequire(agree, base, opts.hot); } if (!agree) { throw new Error(`[agreed] agree object is not found`); } if (typeof agree.request === "string") { agree.request = normalizedRequire(agree.request, base, opts.hot); } if (typeof agree.response === "string") { agree.response = normalizedRequire(agree.response, base, opts.hot); } if (!agree.request) { throw new Error( `[agreed] resquest object is required ${JSON.stringify(agree)}` ); } if (!agree.response) { throw new Error( `[agreed] response object is required ${JSON.stringify(agree)}` ); } if (!agree.request.path) { throw new Error( `[agreed] request path is required ${JSON.stringify(agree.request)}` ); } if (!agree.request.method) { agree.request.method = DEFAULT_REQUEST.method; } if (typeof agree.request.path !== "string") { throw new Error( `[agreed] request path must be string ${JSON.stringify(agree.request)}` ); } if (typeof agree.request.method !== "string") { throw new Error( `[agreed] request method must be string ${JSON.stringify(agree.request)}` ); } agree.request.pathToRegexpKeys = []; agree.request.pathToRegexp = pathToRegexp( agree.request.path, agree.request.pathToRegexpKeys ); if (!agree.request.headers) { agree.request.headers = DEFAULT_REQUEST.headers; } else { agree.request.headers = Object.assign( {}, DEFAULT_REQUEST.headers, agree.request.headers ); } if (opts.defaultRequestHeaders) { agree.request.headers = Object.assign( {}, opts.defaultRequestHeaders, agree.request.headers ); } agree.request.headers = toLowerCaseKeys(agree.request.headers); if (!agree.response.status) { agree.response.status = DEFAULT_RESPONSE.status; } if (!agree.response.headers) { agree.response.headers = DEFAULT_RESPONSE.headers; } else { agree.response.headers = Object.assign( {}, DEFAULT_RESPONSE.headers, agree.response.headers ); } if (opts.defaultResponseHeaders) { agree.response.headers = Object.assign( {}, opts.defaultResponseHeaders, agree.response.headers ); } agree.response.body = agree.response.body || DEFAULT_RESPONSE.body; if (typeof agree.response.schema === "string") { agree.response.schema = normalizedRequire( agree.response.schema, base, opts.hot ); } return agree; }; function normalizedRequire(file, base, hot) { return path.isAbsolute(file) ? requireAgree(file, hot) : requireAgree(path.join(base, file), hot); } function toLowerCaseKeys(obj) { const keys = Object.keys(obj); const result = {}; keys.forEach((key) => { result[key.toLowerCase()] = obj[key]; }); return result; } ================================================ FILE: packages/core/lib/check/defaultRequest.js ================================================ module.exports = { method: "GET", body: "", headers: {}, }; ================================================ FILE: packages/core/lib/check/defaultResponse.js ================================================ module.exports = { status: 200, headers: { "Content-Type": "application/json; charset=utf-8", }, body: "", }; ================================================ FILE: packages/core/lib/check/diff.js ================================================ "use strict"; const { hasTemplate, hasTemplateWithAnyString, isRestArrayTemplate, } = require("../template/hasTemplate"); const toRegexp = require("../template/toRegexp"); module.exports = function includeDiff(small, large) { const result = {}; if (small === large) return; if (small instanceof Date && large instanceof Date) { if (small.getTime() !== large.getTime()) { return [small, large]; } } if (typeof small !== "object") { if (hasTemplate(small) && large === null) return; // {:foo} is template if (hasTemplate(small) && large != null) { if (hasTemplateWithAnyString(small)) { const reg = toRegexp(small); if (typeof large === "string" && large.match(reg)) { return; } } else { return; } } // {:foo.1-last} is rest array template if (isRestArrayTemplate(small)) { return; } return [small, large]; } if ((small && !large) || (!small && large)) { return [small, large]; } if (typeof small !== typeof large) { return [small, large]; } const attrs = Object.keys(small); for (const attr of attrs) { const diff = includeDiff(small[attr], large[attr]); if (diff && Object.keys(diff).length > 0) { result[attr] = diff; } } return result; }; ================================================ FILE: packages/core/lib/check/extract.js ================================================ "use strict"; const url = require("url"); const qs = require("querystring"); const { pathToRegexp } = require("path-to-regexp"); const format = require("../template/format"); module.exports.incomingRequest = (req) => { return new Promise((resolve, reject) => { const result = {}; const urlObj = url.parse(req.url); result.method = req.method; result.path = urlObj.pathname; result.query = qs.parse(urlObj.query); result.url = req.url; result.headers = req.headers; if (req.body) { result.body = req.body; return resolve(result); } // has some body contents if (req.headers && req.headers["content-length"]) { var data = ""; req.on("readable", () => { const chunk = req.read(); if (chunk) { data += chunk; } }); req.on("end", () => { result.body = JSON.parse(data); resolve(result); }); req.on("error", reject); } else { return resolve(result); } }); }; module.exports.outgoingRequest = (req, opts) => { const result = {}; result.path = req.path.indexOf(":") >= 0 ? tryCompilePath(req.path, req.values) : req.path; const query = req.query && qs.stringify(format(req.query, req.values)); if (query) { result.path += `?${query}`; } result.method = req.method; result.headers = (req.headers && format(req.headers, req.values)) || {}; result.host = opts.host; result.port = opts.port; return result; }; function tryCompilePath(path, values) { try { return pathToRegexp.compile(path)(values); } catch (e) { return path.replace(/:/g, ""); } } ================================================ FILE: packages/core/lib/check/isContentJSON.js ================================================ "use strict"; module.exports = (obj) => { const isContentJSON = obj.headers && obj.headers["Content-Type"] && obj.headers["Content-Type"].indexOf("application/json") >= 0; return isContentJSON || typeof obj.body === "object"; }; ================================================ FILE: packages/core/lib/check/isInclude.js ================================================ "use strict"; const { isRestArrayTemplate, hasTemplate } = require("../template/hasTemplate"); module.exports = function isInclude(small, large) { if (small === large) return true; if (small instanceof Date && large instanceof Date) { return small.getTime() === large.getTime(); } if (typeof small != "object" || small === null) { if (hasTemplate(small) && large !== undefined) { return true; } // {:foo.1-last} is rest array template if (isRestArrayTemplate(small)) { return true; } return small == large; } if ((small && !large) || (!small && large)) { return false; } const attrs = Object.keys(small); for (const attr of attrs) { if (!isInclude(small[attr], large[attr])) return false; } return true; }; ================================================ FILE: packages/core/lib/check/schemaValidator.js ================================================ const validate = require("jsonschema").validate; module.exports = (target, schema) => { return validate(target, schema); }; ================================================ FILE: packages/core/lib/client.js ================================================ "use strict"; const http = require("http"); const https = require("https"); const path = require("path"); const CheckBodyStream = require("./check/CheckBodyStream"); const completion = require("./check/completion"); const extract = require("./check/extract"); const isContentJSON = require("./check/isContentJSON"); const register = require("./register"); const format = require("./template/format"); const requireUncached = require("./require_hook/requireUncached"); class Client { constructor(options = {}) { this.agrees = options.agrees; if (!options.agrees) { this.agreesPath = require.resolve(path.resolve(options.path)); this.base = path.dirname(this.agreesPath); } this.scheme = options.scheme || "http"; this.host = options.host || "localhost"; this.port = options.port || 80; this.options = options; register(); } getAgreement() { const agrees = this.agrees || [].concat(requireUncached(this.agreesPath)); return this.completion(agrees); } setup(agree) { if (typeof agree !== "object" && agree) { throw new TypeError("agree should be object"); } const options = extract.outgoingRequest(agree.request, this); const hasContentJSON = isContentJSON(agree.request); const content = hasContentJSON && agree.request.body !== null ? JSON.stringify(format(agree.request.body, agree.request.values)) : agree.request.body; const contentLength = content ? Buffer.byteLength(content) : 0; options.headers["Content-Length"] = contentLength; if (agree.request.headers && agree.request.headers["content-type"]) { options.headers["Content-Type"] = agree.request.headers["content-type"]; delete options.headers["content-type"]; } else { options.headers["Content-Type"] = "application/json"; } return { options, content, contentLength }; } createRequest(setting) { if (typeof setting !== "object" && setting) { throw new TypeError("setting should be object"); } const { options, content, contentLength } = setting; const scheme = this.scheme === "https" ? https : http; const request = scheme.request(options); contentLength && request.write(content); request.agreed = { options, content, contentLength }; return request; } createRequests(agrees) { const completedAgrees = this.completion(agrees); const requests = completedAgrees.map(agree => { const setting = this.setup(agree); return this.createRequest(setting); }); return requests; } checkResponse(response, agree) { const completedAgree = completion(agree, this.base); const isSameStatus = completedAgree.response.status == response.statusCode; const checkStream = new CheckBodyStream(); checkStream.expect(completedAgree.response.body); checkStream.schema(completedAgree.response.schema); return response.pipe(checkStream).on("checked", result => { if (!isSameStatus) { result.diff = result.diff || {}; result.diff.status = [ completedAgree.response.status, response.statusCode ]; } checkStream.emit("result", result); }); } completion(agrees) { return agrees.map(agree => completion(agree, this.base, this.options)); } } module.exports = Client; ================================================ FILE: packages/core/lib/register.js ================================================ "use strict"; const json5Hook = require("./require_hook/json5"); const yamlHook = require("./require_hook/yaml"); const tsHook = require("./require_hook/typescript"); module.exports = (option = {}, hot = true) => { json5Hook(); yamlHook(); tsHook(option, hot); }; ================================================ FILE: packages/core/lib/require_hook/compile.js ================================================ module.exports = (module, file, object) => { module._compile("module.exports = " + JSON.stringify(object), file); }; ================================================ FILE: packages/core/lib/require_hook/hook.js ================================================ "use strict"; const fs = require("fs"); const compile = require("./compile"); module.exports = (parse, extensions) => { extensions.forEach((ext) => { delete require.extensions[ext]; require.extensions[ext] = (module, file) => { const agree = parse(fs.readFileSync(file).toString("utf-8")); compile(module, file, agree); }; }); }; ================================================ FILE: packages/core/lib/require_hook/json5.js ================================================ "use strict"; const EXTENSIONS = [".json", ".json5"]; const JSON5 = require("json5"); const hook = require("./hook"); module.exports = () => hook(JSON5.parse, EXTENSIONS); ================================================ FILE: packages/core/lib/require_hook/requireAgree.js ================================================ const requireUncached = require("./requireUncached"); module.exports = (agree, hot = true) => { return hot ? requireUncached(agree) : require(agree); }; ================================================ FILE: packages/core/lib/require_hook/requireUncached.js ================================================ module.exports = (module) => { delete require.cache[require.resolve(module)]; return require(module); }; ================================================ FILE: packages/core/lib/require_hook/typescript.js ================================================ "use strict"; const ts = require("typescript"); const fs = require("fs"); let cache = null; const transpile = (src, options = {}) => { const res = ts.transpileModule( src, options || { compilerOptions: { module: ts.ModuleKind.CommonJS }, } ).outputText; return res; }; const takeCache = (options) => { try { if (!cache && options.typedCachePath) { const data = fs.readFileSync(options.typedCachePath).toString("utf-8"); const cache = JSON.parse(data); return cache; } return {}; } catch (e) { return {}; } }; const getCacheAgree = (file, cache, mtimeMs) => { if (cache && cache[file]) { if (cache[file].mtimeMs === mtimeMs) { return cache[file]; } } return {}; }; const writeCacheOnExit = (options) => { if (options.typedCachePath) { process.on("SIGINT", () => { process.exit(); }); process.on("exit", () => { if (cache) { fs.writeFileSync(options.typedCachePath, JSON.stringify(cache)); } }); } }; module.exports = (options, hot) => { cache = takeCache(options); writeCacheOnExit(options); require.extensions[".ts"] = (module, file) => { if (!options.typedCachePath) { const src = fs.readFileSync(file).toString("utf-8"); const agree = transpile(src, options); module._compile(agree, file); if (hot) { delete require.cache[file]; } return; } const { mtimeMs } = fs.statSync(file); const cached = getCacheAgree(file, cache, mtimeMs); let agree = cached.agree; if (!agree) { const src = fs.readFileSync(file).toString("utf-8"); agree = transpile(src, options); cache[file] = { mtimeMs, agree, }; } // TODO: need to embed cache hit or not module._compile(agree, file); if (hot) { fs.watch(file, () => { delete require.cache[file]; delete cache[file]; }); } }; }; ================================================ FILE: packages/core/lib/require_hook/yaml.js ================================================ "use strict"; const EXTENSIONS = [".yaml", ".yml"]; const YAML = require("yamljs"); const hook = require("./hook"); module.exports = () => hook(YAML.parse, EXTENSIONS); ================================================ FILE: packages/core/lib/server.js ================================================ "use strict"; const path = require("path"); const url = require("url"); const stable = require("stable"); const Checker = require("./check/checker"); const completion = require("./check/completion"); const extract = require("./check/extract"); const isContentJSON = require("./check/isContentJSON"); const register = require("./register"); const format = require("./template/format"); const hasTemplate = require("./template/hasTemplate").hasTemplate; const bind = require("./template/bind"); const requireAgree = require("./require_hook/requireAgree"); const EventEmitter = require("events").EventEmitter; class Server { constructor(options = {}) { this.agrees = options.agrees; if (!options.agrees) { this.agreesPath = require.resolve(path.resolve(options.path)); this.base = path.dirname(this.agreesPath); } this.options = options; this.notifier = new EventEmitter(); const registerOpts = { typedCachePath: options.typedCachePath, ...this.options.register, }; register(registerOpts, this.options.hot); } useMiddleware(req, res, next) { const agrees = ( this.agrees || requireAgree(this.agreesPath, this.options.hot) ).map(agree => completion(agree, this.base, this.options)); extract .incomingRequest(req) .then(req => { const agreesWithResults = stable( agrees.map(agree => { const { result, similarity, error } = Checker.request( agree.request, req, { pathToRegexpKeys: agree.request.pathToRegexpKeys, values: agree.request.values, debug: this.options.debug, enablePreferQuery: this.options.enablePreferQuery, skipCheckHeaderValueNullable: this.options.skipCheckHeaderValueNullable, } ); return { agree, result, similarity, error }; }), (a, b) => b.similarity - a.similarity ); if ( this.options.strict && agreesWithResults.filter(({ result }) => result).length > 1 ) { res.header("Content-Type", "application/json", "charset=utf-8"); res.statusCode = 500; res.end( JSON.stringify( { message: "Ambiguous Request", candidates: agreesWithResults .filter(({ result }) => result) .map(({ agree: { request, response } }) => { delete request.pathToRegexpKeys; delete request.pathToRegexp; delete response.pathToRegexpKeys; delete response.pathToRegexp; return { request, response }; }) }, null, 2 ) ); return; } const result = agreesWithResults.find(({ result }) => result); if (!result || !result.agree) { if (this.options.callNextWhenNotFound) { typeof next === "function" && next(); return; } res.statusCode = 404; const { agree, similarity, error } = agreesWithResults.shift(); if (similarity > 1) { delete agree.request.pathToRegexp; delete agree.request.pathToRegexpKeys; res.end( `Agree Not Found, actual request is ${JSON.stringify( req, null, 2 )}, but similar agree request is ${JSON.stringify( agree.request, null, 2 )}, error: ${error}` ); } else { res.end(`Agree Not Found`); } typeof next === "function" && next(); return; } const { agree } = result; // /foo/:id matched if (agree.request.pathToRegexpKeys.length > 0) { const pathname = url.parse(req.url).pathname; const result = agree.request.pathToRegexp.exec(pathname); const values = {}; agree.request.pathToRegexpKeys.forEach((pathKey, index) => { values[pathKey.name] = result[index + 1]; }); agree.request.values = values; } if ( agree.request.headers && hasTemplate(JSON.stringify(agree.request.headers)) ) { agree.request.values = Object.assign( {}, agree.request.values, bind(agree.request.headers, req.headers) ); } if ( agree.request.query && hasTemplate(JSON.stringify(agree.request.query)) ) { agree.request.values = Object.assign( {}, agree.request.values, bind(agree.request.query, req.query) ); } if ( agree.request.body && hasTemplate(JSON.stringify(agree.request.body)) ) { agree.request.values = Object.assign( {}, agree.request.values, bind(agree.request.body, req.body) ); } res.statusCode = format(agree.response.status, agree.request.values); let messageBody = agree.response.body || ""; if (agree.request.values) { messageBody = format( messageBody, agree.request.values, agree.response.funcs ); } if (agree.response.values) { messageBody = format( messageBody, Object.assign({}, agree.response.values, agree.request.values), agree.response.funcs ); } if (isContentJSON(agree.response)) { messageBody = JSON.stringify(messageBody); } Object.keys(agree.response.headers).forEach(header => { res.setHeader( header, format( agree.response.headers[header], Object.assign( {}, agree.response.values || {}, agree.request.values || {} ), agree.response.funcs ) ); }); if (agree.response.notify) { const notify = format( agree.response.notify.body, Object.assign( {}, agree.response.values || {}, agree.request.values || {} ), agree.response.funcs ); this.notifier.emit(agree.response.notify.event || "message", notify); } res.end(messageBody); if (agree.request.values) { delete agree.request.values; } }) .catch(e => { typeof next === "function" && next(e); process.nextTick(() => { throw e; }); }); } } module.exports = Server; ================================================ FILE: packages/core/lib/template/bind.js ================================================ "use strict"; const constants = require("./constants"); const hasTemplate = require("./hasTemplate").hasTemplate; module.exports = function bind(hasFormatObj, hasValueObj, result, original) { if (!result) result = {}; if (!original) original = hasValueObj; if (typeof hasFormatObj === "object") { Object.keys(hasFormatObj).forEach((key) => { if (hasTemplate(hasFormatObj[key])) { const formatName = hasFormatObj[key].replace( constants.TEMPLATE_REGEXP, "$2" ); const formatParts = formatName.split("."); if (formatParts.length > 1) { formatParts.forEach((part) => { if (hasValueObj[part]) { result[part] = hasValueObj[part]; } else if (original[part]) { result[part] = original[part]; } }); return; } result[formatName] = hasValueObj[key]; } else if (hasFormatObj[key] && typeof hasFormatObj[key] === "object") { bind(hasFormatObj[key], hasValueObj[key], result, original); } }); } return result; }; ================================================ FILE: packages/core/lib/template/constants.js ================================================ "use strict"; const OPERATIONAL_STRINGS = "\\w+"; const REGEXP_STRING = `{(${OPERATIONAL_STRINGS})*:([\\w|\\.|,|:|\\[|\\]|\\-]+)\\}`; const BRACKETS_STRING = "\\[:([\\w]+)\\]"; const REST_ARRAY_STRING = "{:.+\\d+-last}"; const TEMPLATE_REGEXP_GLOBAL = new RegExp(REGEXP_STRING, "g"); const TEMPLATE_REGEXP = new RegExp(REGEXP_STRING); const TEMPLATE_BRACKETS_REGEXP_GLOBAL = new RegExp(BRACKETS_STRING, "g"); const TEMPLATE_BRACKETS_REGEXP = new RegExp(BRACKETS_STRING); const TEMPLATE_REST_ARRAY_STRING_GLOBAL = new RegExp(REST_ARRAY_STRING, "g"); module.exports.TEMPLATE_REGEXP = TEMPLATE_REGEXP; module.exports.TEMPLATE_REGEXP_GLOBAL = TEMPLATE_REGEXP_GLOBAL; module.exports.TEMPLATE_BRACKETS_REGEXP = TEMPLATE_BRACKETS_REGEXP; module.exports.TEMPLATE_BRACKETS_REGEXP_GLOBAL = TEMPLATE_BRACKETS_REGEXP_GLOBAL; module.exports.TEMPLATE_REST_ARRAY_STRING_GLOBAL = TEMPLATE_REST_ARRAY_STRING_GLOBAL; ================================================ FILE: packages/core/lib/template/format.js ================================================ "use strict"; module.exports = format; const constants = require("./constants"); const operation = require("./operation"); function format(template, args, funcs = {}) { var result = template; if (Array.isArray(template)) { return formatArray(template, args, funcs); } if (typeof template === "object") { return formatObject(template, args, funcs); } if (typeof template !== "string") { return result; } if (!args) return result; if (Object.keys(args).length === 0) return result; // matches {:foo} {:foo.bar} {:foo[:aaa]} const matches = template.match(constants.TEMPLATE_REGEXP_GLOBAL); if (!matches) return result; matches.forEach((match) => { var rawKey = match.replace(constants.TEMPLATE_REGEXP, "$2"); // brackets notation like '{:array[:index]}' if (rawKey.indexOf("[:") >= 0) { const matches = rawKey.match(constants.TEMPLATE_BRACKETS_REGEXP_GLOBAL); matches && matches.forEach((match) => { const key = match.replace(constants.TEMPLATE_BRACKETS_REGEXP, "$1"); const value = "." + key.split(".").reduce((o, i) => o && o[i], args); if (value != null) { rawKey = rawKey.replace(match, value); } }); } var value = rawKey.split(",").map((key) => { if (key && args[key]) { return args[key]; } return key.split(".").reduce((val, k) => { if (val) { const a = k.match(/(\d+)-(\d+|last)/); if (a) { const start = a[1]; const end = a[2] === "last" ? val.length : a[2]; const array = val.slice(start, end); array.__spread = true; return array; } return val[k]; } }, args); }); var operationalKey = match.replace(constants.TEMPLATE_REGEXP, "$1"); if (operationalKey) { var operationValue = operation(operationalKey, funcs, ...value); } if (Array.isArray(value) && value.length === 1) { value = value[0]; } if (operationValue) { value = operationValue; } if (value === undefined) { return; } if (match === template) { result = value; } else { result = result.replace(match, value); } }); return result; } function formatObject(obj, args, funcs) { var result = {}; if (!obj) { return obj; } Object.keys(obj).forEach((key) => { const value = obj[key]; result[key] = format(value, args, funcs); }); return result; } function formatArray(array, args, funcs) { var result = []; if (!array) { return array; } array.forEach((item) => { const formattedItem = format(item, args, funcs); if (formattedItem.__spread) { formattedItem.forEach((item) => { result.push(item); }); } else { result.push(formattedItem); } }); return result; } ================================================ FILE: packages/core/lib/template/hasTemplate.js ================================================ "use strict"; const constants = require("./constants"); module.exports.hasTemplate = (str) => { return typeof str === "string" && str.match(constants.TEMPLATE_REGEXP_GLOBAL); }; module.exports.hasTemplateWithAnyString = (str) => { if (typeof str !== "string") { return false; } const removed = str.replace(constants.TEMPLATE_REGEXP_GLOBAL, ""); return removed.length > 0; }; module.exports.isRestArrayTemplate = (str) => { return ( typeof str === "string" && str.match(constants.TEMPLATE_REST_ARRAY_STRING_GLOBAL) ); }; ================================================ FILE: packages/core/lib/template/operation/index.js ================================================ "use strict"; const randomInt = require("./randomInt"); const randomString = require("./randomString"); const parseInt = require("./parseInt"); const unixtime = require("./unixtime"); const path = require("path"); module.exports = (operation, funcs, ...values) => { if (typeof funcs[operation] === "function") { const result = funcs[operation](...values); return result; } else if (typeof funcs[operation] === "string") { const basedir = funcs.basedir || process.cwd(); const funcPath = path.resolve(basedir, funcs[operation]); const result = require(funcPath)(...values); return result; } else if (operation === "randomInt") { const [value] = values; const result = randomInt(value); return result; } else if (operation === "parseInt") { const [value] = values; const result = parseInt(value); return result; } else if (operation === "randomString") { const [value] = values; const result = randomString(value); return result; } else if (operation === "unixtime") { const result = unixtime(); return result; } }; ================================================ FILE: packages/core/lib/template/operation/parseInt.js ================================================ module.exports = Number.parseInt; ================================================ FILE: packages/core/lib/template/operation/randomInt.js ================================================ function getRandom(min = 0, max = Number.MAX_SAFE_INTEGER) { if (min > max) { max = min; min = 0; } return Math.trunc(Math.random() * (max - min) + min); } function randomInt(value) { if (typeof value === "number") { return getRandom(value); } const matched = value.match(/(\d+)-(\d+)/); if (!matched) { return getRandom(); } const min = matched[1]; const max = matched[2]; return getRandom(min, max); } module.exports = randomInt; ================================================ FILE: packages/core/lib/template/operation/randomString.js ================================================ function getRandom(length = 5) { var char = "abcdefghijklmnopqrstuvwxyz0123456789"; var charLength = char.length; var random = ""; for (var i = 0; i < length; i++) { random += char[Math.floor(Math.random() * charLength)]; } return random; } function randomString(value) { if (typeof value === "number") { return getRandom(value); } return getRandom(); } module.exports = randomString; ================================================ FILE: packages/core/lib/template/operation/unixtime.js ================================================ const unixtime = () => parseInt(Date.now() / 1000); module.exports = unixtime; ================================================ FILE: packages/core/lib/template/toRegexp.js ================================================ "use strict"; const constants = require("./constants"); module.exports = (str) => { var rawString = str.replace(constants.TEMPLATE_REGEXP_GLOBAL, ".*"); rawString = "^" + rawString + "$"; return new RegExp(rawString); }; ================================================ FILE: packages/core/lib/utils/logger.js ================================================ const logger = global.logger || console; module.exports = logger; ================================================ FILE: packages/core/package.json ================================================ { "name": "@agreed/core", "version": "6.0.0", "description": "agreed is a mock server and test client, agreed will be helper for Consumer Driven Contract", "main": "index.js", "scripts": { "test": "eater", "lcov": "nyc --reporter=lcov npm test", "codecov": "npm run lcov && codecov", "lint": "run-s lint:*", "lint:lib": "eslint ./lib/**/*.js", "lint:test": "eslint ./test/**/*.js", "lint:root": "eslint ./*.js", "lintfix:lib": "eslint ./lib/**/*.js --fix", "lintfix:test": "eslint ./test/**/*.js --fix", "lintfix:root": "eslint ./*.js --fix", "lintfix": "run-s lintfix:*" }, "repository": { "type": "git", "url": "git+https://github.com/recruit-tech/agreed.git" }, "bugs": { "url": "https://github.com/recruit-tech/agreed-core/issues" }, "homepage": "https://github.com/recruit-tech/agreed-core", "keywords": [ "agreed", "consumer", "driven", "contract", "mock", "test", "server" ], "author": "yosuke-furukawa", "license": "MIT", "dependencies": { "@types/node": "^18.0.0", "json5": "^2.0.0", "jsonschema": "^1.2.4", "path-to-regexp": "^6.0.0", "stable": "^0.1.8", "typescript": "^4.0.0", "yamljs": "^0.3.0" }, "devDependencies": { "assert-stream": "1.1.1", "body-parser": "1.20.0", "codecov": "3.8.3", "eater": "4.0.4", "eslint": "7.32.0", "eslint-config-prettier": "8.7.0", "eslint-plugin-prettier": "4.2.1", "espower-loader": "1.2.2", "express": "4.18.2", "is-empty": "1.2.0", "must-call": "1.0.0", "npm-run-all": "4.1.5", "nyc": "15.1.0", "plz-port": "1.0.0", "power-assert": "1.6.1", "prettier": "2.8.4" }, "publishConfig": { "access": "public" }, "directories": { "lib": "lib", "test": "test" } } ================================================ FILE: packages/core/register.js ================================================ module.exports = require("./lib/register"); ================================================ FILE: packages/core/renovate.json ================================================ { "extends": [ "config:base" ] } ================================================ FILE: packages/core/requireUncached.js ================================================ module.exports = require("./lib/require_hook/requireUncached"); ================================================ FILE: packages/core/test/agrees/agrees.js ================================================ module.exports = [ { request: { path: "/hoge/fuga", method: "GET", query: { q: "foo", }, }, response: { headers: { "x-csrf-token": "csrf-token", }, body: { message: "hello world", }, }, }, "./hoge/foo.json", "./foo/bar.yaml", { request: require("./qux/request.json"), response: require("./qux/response.json"), }, { request: { path: "/path/:id", method: "GET", // value for test client values: { id: "yosuke", }, }, response: { headers: { "x-csrf-token": "csrf-token", }, body: { // :id is for request value message: "hello {:id}", }, }, }, { request: { path: "/path/:id", method: "POST", // query embed data, any query is ok. query: { meta: "{:meta}", }, body: { message: "{:message}", }, // value for test client values: { id: "yosuke", meta: true, message: "foobarbaz", }, }, response: { headers: { "x-csrf-token": "csrf-token", }, body: { // :id is for request value message: "hello {:id}, {:meta}, {:message}", }, }, }, { request: { // if no method then GET path: "/nyan/:id", query: { meta: "{:meta}", }, // value for test client values: { id: "yosuke", meta: false, }, }, response: { headers: { "x-csrf-token": "csrf-token", }, body: { // :id is for request value message: "hello {:id}, {:meta}", }, }, }, { request: { path: "/embed/from/response/:id", method: "POST", query: { meta: "{:meta}", }, body: { message: "{:message}", }, // value for test client values: { id: "yosuke", meta: false, message: "this is a message", }, }, response: { headers: { "x-csrf-token": "csrf-token", }, body: { // embed template from response values image: "{:image}", topics: "{:topics}", message: "hello {:id} {:meta} {:message}", }, values: { image: "http://imgfp.hotp.jp/SYS/cmn/images/front_002/logo_hotopepper_264x45.png", topics: [ { a: "a", }, { b: "b", }, ], }, }, }, { request: { path: "/images/:id", method: "GET", query: { q: "{:someQueryStrings}", }, values: { id: "yosuke", someQueryStrings: "foo", }, }, response: { headers: { "x-csrf-token": "csrf-token", }, body: { message: "{:greeting} {:id} {:someQueryStrings}", images: "{:images}", themes: "{:themes}", }, values: { greeting: "hello", images: ["http://example.com/foo.jpg", "http://example.com/bar.jpg"], themes: { name: "green", }, }, }, }, { request: { path: "/list/:index", method: "GET", values: { index: 1, }, }, response: { body: { result: "{:list[:index]}", }, values: { list: ["hello", "hi", "dunke"], }, }, }, { request: { path: "/useschema/:index", method: "GET", values: { index: 1, }, }, response: { body: { result: "{:list[:index]}", }, schema: { type: "object", properties: { result: { type: "string", }, }, }, values: { list: ["hello", "hi", "dunke"], }, }, }, { request: { path: "/useschema/withstring/:index", method: "GET", values: { index: 1, }, }, response: { body: { result: "{:list[:index]}", }, schema: "./schema/hi.json", values: { list: ["hello", "hi", "dunke"], }, }, }, { request: { path: "/headers/:index", method: "GET", headers: { "x-token": "{:token}", "x-api-key": "{:apiKey}", }, values: { index: 2, token: "nyan", apiKey: "nyaaan", }, }, response: { body: { result: "{:list[:index]} {:token} {:apiKey}", }, values: { list: ["hello", "hi", "dunke"], }, }, }, { request: { path: "/headers/:index", method: "GET", values: { index: 1, }, }, response: { body: { result: "{:list[:index]}", }, values: { list: ["hello", "hi", "dunke"], }, }, }, { request: { path: "/headers/test/:index", method: "GET", headers: { "x-test-token": "{:xTestToken}", }, values: { index: 1, xTestToken: "fdajfdsaoijfdoajofdjaoj", }, }, response: { body: { result: "{:list[:index]}", }, values: { list: ["hello", "hi", "dunke"], }, }, }, ]; ================================================ FILE: packages/core/test/agrees/agrees.json5 ================================================ [ { request: { path: '/hoge/fuga', method: 'GET', query: { q: 'foo', }, }, response: { headers: { 'x-csrf-token': 'csrf-token', }, body: { message: 'hello world', }, }, }, './hoge/foo.json', './foo/bar.yaml', { request: './qux/request.json', response: './qux/response.json', }, { request: { path: '/path/:id', method: 'GET', // value for test client values: { id: 'yosuke', }, }, response: { headers: { 'x-csrf-token': 'csrf-token', }, body: { // :id is for request value message: 'hello {:id}', }, }, }, { request: { path: '/path/:id', method: 'POST', // query embed data, any query is ok. query: { meta: "{:meta}", }, body: { message: "{:message}" }, // value for test client values: { id: 'yosuke', meta: true, message: 'foobarbaz' }, }, response: { headers: { 'x-csrf-token': 'csrf-token', }, body: { // :id is for request value message: 'hello {:id}, {:meta}, {:message}', }, }, }, { request: { path: '/path/header/format', method: 'GET', }, response: { headers: { 'access-control-allow-origin': '{:acao}' }, body: { message: 'hello', }, values: { 'acao': '*' } }, }, { request: { path: '/path/default/header/', method: 'GET', }, response: { body: { message: 'hello', }, }, }, { request: { path: '/path/default/request/header/', method: 'GET', headers: { 'x-forwarded-for': 'forward' }, }, response: { body: { message: 'forward', }, }, }, { request: { path: '/test/case/insensitive/headers', method: 'GET', headers: { 'This-Headers-Should-Be-Lower-Case': 'true' }, }, response: { body: { message: 'hello case insensitive headers', }, }, }, { request: { path: '/test/null/agreed/values', }, response: { body: { messages: [ { message: '{:messages.0.message}' }, '{:messages.1-last}' ], }, values: { messages: [ { message: null }, { message: 'test' }, ] }, }, }, { request: { path: '/test/randomstring/agreed/values', }, response: { body: { random: '{randomString:random}' }, values: { random: 8 }, }, }, { request: { path: '/test/randomint/agreed/values', }, response: { body: { random: '{randomInt:random}' }, values: { random: '1-1000' }, }, }, { request: { path: '/test/parseint/agreed/values/:id', }, response: { body: { id: '{parseInt:id}' }, }, }, { request: { path: '/test/agreed/messages', method: 'POST', body: { messages: [ { message: '{:messages.0.message}' }, '{:messages.1-last}' ] }, values: { messages: [ { message: null }, { message: 'test' }, ] } }, response: { status: 201, body: { results: '{:messages}' }, values: { messages: [ { message: '{:message0}' }, { message: 'test' }, ] } }, }, { request: { path: '/test/agreed/use/null/obj', method: 'POST', body: { test: '{:test}' }, values: { test: null, } }, response: { status: 201, body: { results: '{:test}' }, }, }, { request: { path: '/test/bind/nest/object', method: 'POST', body: { time: { start: '{:time.start}', end: '{:time.end}', break: { start: '{:time.break.start}', end: '{:time.break.end}', } }, members: [ { id: '{:members.0.id}' }, '{:members.1-last}' ] } }, response: { body: { time: { start: '{:time.start}', end: '{:time.end}' }, break: { start: '{:time.break.start}', end: '{:time.break.end}' }, members: [ { id: '{:id}' }, '{:members.1-last}' ] }, }, }, { request: { path: '/test/unixtime/agreed/values', }, response: { body: { unixtime: '{unixtime:time}', }, values: { time: 12344556677, } }, }, ] ================================================ FILE: packages/core/test/agrees/agrees.ts ================================================ module.exports = [{ request: { path: '/ts-messages', method: 'POST', body: { message: '{:message}' }, values: { message: 'test' } }, response: { body: { result: '{:message}' }, values: { message: 'test' }, }, }]; ================================================ FILE: packages/core/test/agrees/foo/bar.yaml ================================================ request: path: '/foo/:bar' method: 'PUT' body: a: 'b' c: 'd' response: headers: Content-Type: 'text' body: 'hello put' ================================================ FILE: packages/core/test/agrees/hoge/foo.json ================================================ { 'request': { method: 'POST', path: '/hoge/:foo', body: { message: 'foobarbaz', }, }, response: { body: { message: 'hello post', }, }, } ================================================ FILE: packages/core/test/agrees/hoge/fuga/_agree.json ================================================ { request: { method: 'GET', }, response: { status: 200, // default is 200 body: { message: 'hello world' } } } ================================================ FILE: packages/core/test/agrees/hoge/fuga/request.json ================================================ { } ================================================ FILE: packages/core/test/agrees/index.js ================================================ module.exports = ["./hoge/foo.json", "./foo/bar.yaml"]; ================================================ FILE: packages/core/test/agrees/notify.js ================================================ module.exports = [ { request: { path: "/messages", method: "POST", body: { message: "{:message}", }, values: { message: "test", }, }, response: { body: { result: "{:message}", }, values: { message: "test", }, notify: { event: "message2", body: { message: "message! {:message}", }, }, }, }, { request: { path: "/messages2", method: "POST", body: { message: "{:message}", }, values: { message: "test", }, }, response: { body: { result: "{:message}", }, values: { message: "test", }, notify: { body: { message: "message2 {:message}", }, }, }, }, ]; ================================================ FILE: packages/core/test/agrees/qux/request.json ================================================ { path: '/qux/fuga', method: 'DELETE', } ================================================ FILE: packages/core/test/agrees/qux/response.json ================================================ { status: 204, } ================================================ FILE: packages/core/test/agrees/schema/hi.json ================================================ { type: 'object', properties: { result: { type: 'string' } }, } ================================================ FILE: packages/core/test/agrees/sub.js ================================================ module.exports = (a, b) => a - b; ================================================ FILE: packages/core/test/helper/espowerLoader.js ================================================ require("espower-loader")({ cwd: process.cwd(), pattern: "test/**/*.js", }); ================================================ FILE: packages/core/test/helper/server.js ================================================ "use strict"; const express = require("express"); const bodyParser = require("body-parser"); const Agreed = require("../../index"); const agreed = new Agreed(); const app = express(); module.exports = (opts) => { app.use(bodyParser.json()); app.use(agreed.middleware(opts)); app.use((err, req, res, next) => { res.statusCode = 500; res.send(`Error is occurred : ${err}`); }); return app.listen(opts.port); }; ================================================ FILE: packages/core/test/helper/server.notify.js ================================================ "use strict"; const express = require("express"); const bodyParser = require("body-parser"); const Agreed = require("../../index"); const agreed = new Agreed(); const app = express(); module.exports = (opts) => { app.use(bodyParser.json()); app.use(agreed.middleware(opts)); app.use((err, req, res, next) => { res.statusCode = 500; res.send(`Error is occurred : ${err}`); }); return { server: app.listen(opts.port), notifier: agreed.server.notifier }; }; ================================================ FILE: packages/core/test/lib/check/checker.checkNullish.js ================================================ "use strict"; const test = require("eater/runner").test; const checker = require(`${process.cwd()}/lib/check/checker`); const assert = require("power-assert"); test('checker[checkNullish]: check type { key: "foo" }, { key: "bar" } doest not have error ', () => { const obj1 = { key: "foo" }; const obj2 = { key: "bar" }; assert(checker.checkNullish(obj1, obj2).error === undefined); }); test('checker[checkNullish]: check type { key: "undefined" }, { key: "foo" } has error because undefined is nullish ', () => { const obj1 = { key: "undefined" }; const obj2 = { key: "foo" }; assert( checker.checkNullish(obj1, obj2).error === "Request value has nullish strings undefined in key" ); }); test('checker[checkNullish]: check type { key: "null" }, { key: "foo" } has error because null is nullish ', () => { const obj1 = { key: "null" }; const obj2 = { key: "foo" }; assert( checker.checkNullish(obj1, obj2).error === "Request value has nullish strings null in key" ); }); test('checker[checkNullish]: check type { key: "123" }, { key: 1 } does not have error', () => { const obj1 = { key: "123" }; const obj2 = { key: 1 }; assert(checker.checkNullish(obj1, obj2).error === undefined); }); test('checker[checkNullish]: check type { key: 1 }, { key: "3" } does not have error ', () => { const obj1 = { key: 1 }; const obj2 = { key: "3" }; assert(checker.checkNullish(obj1, obj2).error === undefined); }); test("checker[checkNullish]: check nested type does not have error ", () => { const obj1 = { key: { foo: "test" } }; const obj2 = { foo: "hoge" }; assert(checker.checkNullish(obj1, obj2).error === undefined); }); test("checker[checkNullish]: check nested type does not have error ", () => { const obj1 = { key: { foo: { bar: "null" } } }; const obj2 = { bar: "hoge" }; assert( checker.checkNullish(obj1, obj2).error === "Request value has nullish strings null in bar" ); }); ================================================ FILE: packages/core/test/lib/check/diff.js ================================================ "use strict"; const test = require("eater/runner").test; const diff = require(`${process.cwd()}/lib/check/diff`); const assert = require("power-assert"); test("diff: check object is equal", () => { const small = { abc: "abc", def: "{:aaa}", ghi: 1 }; const large = { abc: "abc", def: "aaa", ghi: 1, jkl: "aaaaa" }; const d = diff(small, large); assert.deepEqual(d, {}); }); test("diff: check object not equality", () => { const small = { abc: "abc", def: "{:aaa}", ghi: 1 }; const large = { abc: "abc", def: "aaa", ghi: 2, jkl: "aaaaa" }; const d = diff(small, large); assert.deepEqual(d, { ghi: [1, 2] }); }); test("diff: check object not deep equality", () => { const small = { abc: "abc", def: "{:aaa}", hoge: "{:aaa}", fuga: "{:aaa}", ghi: 1, foo: { aa: "123", b: { fff: { a: "hello {:aa}" } } } }; const large = { abc: "abc", def: "aaa", fuga: { abc: "123" }, ghi: 2, jkl: "aaaaa", foo: { aa: "123", b: { fff: "aaa" } } }; const d = diff(small, large); assert.deepEqual(d, { hoge: ["{:aaa}", undefined], ghi: [1, 2], foo: { b: { fff: [ { a: "hello {:aa}" }, "aaa" ] } } }); }); test("diff: check object", () => { const small = { abc: "abc {:test}", def: "{:aaa}", ghi: 1 }; const large = { abc: "aaa test", def: "aaa", ghi: "aaaaa" }; const d = diff(small, large); assert.deepEqual(d, { abc: ["abc {:test}", "aaa test"], ghi: [1, "aaaaa"] }); }); test("diff: check rest array string", () => { const small = { abc: "abc {:test}", def: "{:aaa}", ghi: [ { a: "{:ghi.0.a}", c: "{:ghi.0.c}", e: "{:ghi.0.e}" }, "{:ghi.1-last}" ] }; const large = { abc: "abc test", def: "aaa", ghi: [ { a: "b", c: "d", e: "e" } ] }; const d = diff(small, large); assert.deepEqual(d, {}); }); test("diff: check value included null", () => { const small = { abc: "abc", def: "{:aaa}", ghi: 1 }; const large = { abc: "abc", def: null, ghi: 1 }; const d = diff(small, large); assert.deepEqual(d, {}); }); ================================================ FILE: packages/core/test/lib/check/isInclude.js ================================================ "use strict"; const test = require("eater/runner").test; const isInclude = require(`${process.cwd()}/lib/check/isInclude`); const assert = require("power-assert"); test("isInclude: check object is include", () => { const small = { abc: "abc", def: "{:aaa}", ghi: 1 }; const large = { abc: "abc", def: "aaa", ghi: 1, jkl: "aaaaa" }; const is = isInclude(small, large); assert(is); }); test("isInclude: check nested object is include", () => { const small = { abc: "abc", def: "{:aaa}", ghi: "{:hoo}" }; const large = { abc: "abc", def: { a: "123" }, ghi: 1, jkl: "aaaaa" }; const is = isInclude(small, large); assert(is); }); test("isInclude: check nested array is include", () => { const small = { abc: "abc", def: "{:aaa}", ghi: "{:hoo}", jkl: "{:array}" }; const large = { abc: "abc", def: { a: "123" }, ghi: 1, jkl: [] }; const is = isInclude(small, large); assert(is); }); test("isInclude: check large value is empty string", () => { const small = { abc: "abc", def: "{:aaa}", ghi: "{:hoo}", jkl: "{:array}" }; const large = { abc: "abc", def: { a: "123" }, ghi: 0, jkl: "" }; const is = isInclude(small, large); assert(is); }); test("isInclude: false, check large value is empty string", () => { const small = { abc: "abc", def: "{:aaa}", ghi: "{:hoo}", jkl: "{:array}" }; const large = { abc: "abc", def: { a: "123" }, ghi: null, jkl: undefined }; const is = isInclude(small, large); assert(!is); }); test("isInclude: check small value is null & large value is null", () => { const small = { abc: "abc", def: "{:aaa}", ghi: "{:hoo}", jkl: null }; const large = { abc: "abc", def: { a: "123" }, ghi: "", jkl: null }; const is = isInclude(small, large); assert(is); }); test("isInclude: check small value is null & large value is undefined", () => { const small = { abc: "abc", def: "{:aaa}", ghi: "{:hoo}", jkl: null }; const large = { abc: "abc", def: { a: "123" }, ghi: "" // jkl is undefined }; const is = isInclude(small, large); assert(is); }); test("isInclude: false, check small value is null & large value is empty string", () => { const small = { abc: "abc", def: "{:aaa}", ghi: "{:hoo}", jkl: null }; const large = { abc: "abc", def: { a: "123" }, ghi: "", jkl: "" }; const is = isInclude(small, large); assert(!is); }); ================================================ FILE: packages/core/test/lib/check/schema.js ================================================ "use strict"; const test = require("eater/runner").test; const schemaValidator = require(`${process.cwd()}/lib/check/schemaValidator`); const assert = require("power-assert"); test("schema: check object is satisfied type", () => { const object = { abc: "abc", def: "def", ghi: 1 }; const schema = { type: "object", properties: { abc: { type: "string" }, def: { type: "string" }, ghi: { type: "number" } } }; const result = schemaValidator(object, schema); assert(result.errors.length === 0); }); test("schema: check object is not satisfied type", () => { const object = { abc: "abc", def: "def", ghi: "1" }; const schema = { type: "object", properties: { abc: { type: "string" }, def: { type: "string" }, ghi: { type: "number" } } }; const result = schemaValidator(object, schema); assert(result.errors.length === 1); assert(result.errors[0].stack === "instance.ghi is not of a type(s) number"); }); ================================================ FILE: packages/core/test/lib/client.broken.js ================================================ "use strict"; const agreedServer = require("../helper/server.js"); const Client = require(`${process.cwd()}/lib/client.js`); const test = require("eater/runner").test; const assert = require("power-assert"); const mustCall = require("must-call"); const isEmpty = require("is-empty"); test("feat(client): check broken response - 1", () => { const server = agreedServer({ agrees: [ { request: { path: "/content", }, response: { status: 204, }, }, ], port: 0, }); server.on("listening", () => { const client = new Client({ agrees: [ { request: { path: "/content", }, response: { body: { id: 1, }, }, }, ], port: server.address().port, }); const agrees = client.getAgreement(); const requests = client.createRequests(agrees); let finishedCount = 0; requests.map((request, i) => { request.end(); request.on( "response", mustCall((response) => { client.checkResponse(response, agrees[i]).on( "result", mustCall((result) => { assert(result.error.length > 0); finishedCount++; if (finishedCount === requests.length) { server.close(); } }) ); }) ); }); }); }); test("feat(client): check broken response - 2", () => { const server = agreedServer({ agrees: [ { request: { path: "/content", }, response: { headers: { "Content-Type": "application/octet-stream", }, body: "{broken:'json'", }, }, ], port: 0, }); server.on("listening", () => { const client = new Client({ agrees: [ { request: { path: "/content", }, response: { body: { id: 1, }, }, }, ], port: server.address().port, }); const agrees = client.getAgreement(); const requests = client.createRequests(agrees); let finishedCount = 0; requests.map((request, i) => { request.end(); request.on( "response", mustCall((response) => { client.checkResponse(response, agrees[i]).on( "result", mustCall((result) => { assert(result.error.length > 0); finishedCount++; if (finishedCount === requests.length) { server.close(); } }) ); }) ); }); }); }); ================================================ FILE: packages/core/test/lib/client.js ================================================ "use strict"; const agreedServer = require("../helper/server.js"); const Client = require(`${process.cwd()}/lib/client.js`); const test = require("eater/runner").test; const AssertStream = require("assert-stream"); const assert = require("power-assert"); const mustCall = require("must-call"); const isEmpty = require("is-empty"); const { ClientRequest } = require("http"); test("feat(client): support single agree file", () => { const client = new Client({ path: "test/agrees/hoge/foo.json", }); const agrees = client.getAgreement(); assert(Array.isArray(agrees)); }); test("feat(client): check setup", () => { const client = new Client({ path: "test/agrees/hoge/foo.json", }); const agrees = client.getAgreement(); agrees.forEach((agree) => { const { options, content, contentLength } = client.setup(agree); assert.deepEqual(options, { path: "/hoge/foo", method: "POST", headers: { "Content-Length": 23, "Content-Type": "application/json" }, host: "localhost", port: 80, }); assert.equal(content, '{"message":"foobarbaz"}'); assert.equal(contentLength, 23); }); }); test("feat(client): check createRequest", () => { const client = new Client({ path: "test/agrees/hoge/foo.json", }); const agrees = client.getAgreement(); agrees.forEach((agree) => { const setting = client.setup(agree); const request = client.createRequest(setting); request.abort(); assert(request instanceof ClientRequest); }); }); test("feat(client): check createRequests", () => { const client = new Client({ path: "test/agrees/hoge/foo.json", }); const agrees = client.getAgreement(); const requests = client.createRequests(agrees); requests.forEach((request) => { request.abort(); assert(request instanceof ClientRequest); }); }); test("feat(client): check request to server", () => { const server = agreedServer({ path: "test/agrees/agrees.js", port: 0, }); server.on("listening", () => { const client = new Client({ path: "test/agrees/agrees.js", port: server.address().port, }); const agrees = client.getAgreement(); const requests = client.createRequests(agrees); let finishedCount = 0; requests.map((request, i) => { request.end(); request.on( "response", mustCall((response) => { client.checkResponse(response, agrees[i]).on( "result", mustCall((result) => { if (Object.keys(result.diff).length > 0) { assert(result.body, "Agree Not Found"); assert(result.diff.status, [200, 404]); } else { assert(isEmpty(result.diff)); } finishedCount++; if (finishedCount === requests.length) { server.close(); } }) ); }) ); }); }); }); test("client: add status-diff to result.diff when response-status is different", () => { const agree = (status) => { return { request: { path: "/api/sample/:id", method: "PUT", body: [{ value: "test" }], values: { id: 1, }, }, response: { status, }, }; }; const serverResponseStatus = 204; const invalidClientExpectedStatus = 200; const server = agreedServer({ port: 0, agrees: [agree(serverResponseStatus)], }); server.on("listening", () => { const client = new Client({ port: server.address().port, agrees: [agree(invalidClientExpectedStatus)], }); const agrees = client.getAgreement(); const requests = client.createRequests(agrees); requests.map((request, i) => { request.end(); request.on( "response", mustCall((response) => { client.checkResponse(response, agrees[i]).on( "result", mustCall((result) => { assert(result.diff.status); assert.deepEqual(result.diff.status, [ invalidClientExpectedStatus, serverResponseStatus, ]); server.close(); }) ); }) ); }); }); }); test("feat(client): use specified content-type header", () => { const boundary = "------------------------cafebabe"; const contentType = `multipart/form-data; boundary=${boundary}`; const agree = { request: { path: "/api/csv/import", method: "POST", headers: { "Content-Type": contentType, }, body: "", }, response: { status: 200, }, }; const client = new Client({ agrees: [agree] }); const agrees = client.getAgreement(); const { options } = client.setup(agrees[0]); assert(options.headers); assert.equal( Object.keys(options.headers) .map((v) => v.toLowerCase()) .filter((v) => v === "content-type").length, 1 ); assert.equal(options.headers["Content-Type"], contentType); }); test("feat(client): support single agree ts file", () => { const client = new Client({ path: "test/agrees/agrees.ts", }); const agrees = client.getAgreement(); assert(Array.isArray(agrees)); }); test("fix(client): send null when body is null", () => { const agree = { request: { path: "/foo", method: "GET", body: null, }, response: { status: 200, }, }; const client = new Client({ agrees: [agree] }); const agrees = client.getAgreement(); const { content } = client.setup(agrees[0]); assert.equal(content, null); }); ================================================ FILE: packages/core/test/lib/middleware.js ================================================ "use strict"; const Server = require(`${process.cwd()}/lib/server.js`); const AssertStream = require("assert-stream"); const test = require("eater/runner").test; const http = require("http"); const assert = require("power-assert"); const mustCall = require("must-call"); test("feat(middleware): http GET API", () => { const agreedServer = new Server({ path: "test/agrees/agrees.js" }); const server = http .createServer((req, res) => { agreedServer.useMiddleware(req, res); }) .listen(0); server.on("listening", () => { const port = server.address().port; http.get("http://localhost:" + port + "/hoge/fuga?q=foo", (res) => { const assert = new AssertStream(); assert.expect({ message: "hello world" }); res.pipe(assert); server.close(); }); }); }); test("feat(middleware): http PORT API", (done) => { const agreedServer = new Server({ path: "test/agrees/agrees.js" }); const server = http .createServer((req, res) => { agreedServer.useMiddleware(req, res); }) .listen(0); server.on("listening", () => { const port = server.address().port; const postData = JSON.stringify({ message: "foobarbaz", abc: "foobarbaz", }); const options = { host: "localhost", method: "POST", path: "/hoge/abc", port: port, headers: { "Content-Type": "application/json", "Content-Length": Buffer.byteLength(postData), }, }; const req = http.request(options, (res) => { const assert = new AssertStream(); assert.expect({ message: "hello post" }); res.pipe(assert); server.close(); }); req.write(postData); req.end(); }); }); test("error (middleware): http GET 404 API when nullish", (done) => { const agreedServer = new Server({ path: "test/agrees/agrees.js" }); const server = http .createServer((req, res) => { agreedServer.useMiddleware(req, res); }) .listen(0); server.on("listening", () => { const port = server.address().port; const options = { host: "localhost", method: "GET", path: "/headers/null", port: port, }; const req = http.request(options, (res) => { server.close(); assert(res.statusCode === 404); let content = ""; res.on("data", (chunk) => (content += chunk)); res.on("end", () => console.log(content)); }); req.end(); }); }); test("error (middleware): http GET 404 API when nullish headers", (done) => { const agreedServer = new Server({ path: "test/agrees/agrees.js" }); const server = http .createServer((req, res) => { agreedServer.useMiddleware(req, res); }) .listen(0); server.on("listening", () => { const port = server.address().port; const options = { host: "localhost", method: "GET", path: "/headers/test/1", headers: { "x-test-token": "null", }, port: port, }; const req = http.request(options, (res) => { server.close(); assert(res.statusCode === 404); let content = ""; res.on("data", (chunk) => (content += chunk)); res.on("end", () => console.log(content)); }); req.end(); }); }); test("feat(middleware): http notfound called next", (done) => { const agreedServer = new Server({ path: "test/agrees/agrees.js", callNextWhenNotFound: true, }); const server = http .createServer((req, res) => { agreedServer.useMiddleware( req, res, mustCall(() => { done(); }) ); }) .listen(0); server.on("listening", () => { const port = server.address().port; http.get("http://localhost:" + port + "/foobarbaz"); }); }); ================================================ FILE: packages/core/test/lib/server.arrayreqs.js ================================================ "use strict"; const agreedServer = require("../helper/server.js"); const test = require("eater/runner").test; const http = require("http"); const AssertStream = require("assert-stream"); const assert = require("power-assert"); const mustCall = require("must-call"); test("server: check custom function", () => { const server = agreedServer({ agrees: [ { request: { path: "/test/arrayreqs/agreed/values", query: { "year_months[]": "{:yearMonths}", }, }, response: { body: "{arraify:yearMonths}", funcs: { arraify: (yearMonths) => yearMonths.map((yearMonth) => yearMonth.substring(0, 4)), }, }, }, ], port: 0, }); server.on("listening", () => { const options = { host: "localhost", method: "GET", path: "/test/arrayreqs/agreed/values?year_months[]=201708&year_months[]=201709&year_months[]=201810", port: server.address().port, }; const req = http .request(options, (res) => { let data = ""; res.on("data", (d) => (data += d)); res.on( "end", mustCall(() => { const result = JSON.parse(data); assert.deepStrictEqual(result, ["2017", "2017", "2018"]); }) ); server.close(); }) .on("error", console.error); req.end(); }); }); ================================================ FILE: packages/core/test/lib/server.arraysWithNull.js ================================================ "use strict"; const agreedServer = require("../helper/server.js"); const test = require("eater/runner").test; const http = require("http"); const AssertStream = require("assert-stream"); const assert = require("power-assert"); const mustCall = require("must-call"); test("server: check headers when case insensitive", () => { const server = agreedServer({ path: "test/agrees/agrees.json5", port: 0, }); server.on("listening", () => { const options = { host: "localhost", method: "GET", path: "/test/null/agreed/values", port: server.address().port, }; const req = http .request( options, mustCall((res) => { let data = ""; res.on("data", (d) => (data += d)); res.on( "end", mustCall(() => { const actual = JSON.parse(data); const expected = { messages: [{ message: null }, { message: "test" }], }; assert.deepStrictEqual(actual, expected); }) ); server.close(); }) ) .on("error", console.error); req.end(); }); }); ================================================ FILE: packages/core/test/lib/server.bodyHasNull.js ================================================ "use strict"; const agreedServer = require("../helper/server.js"); const { test } = require("eater/runner"); const http = require("http"); const AssertStream = require("assert-stream"); const assert = require("power-assert"); const mustCall = require("must-call"); test("server: check post to list", () => { const server = agreedServer({ path: "test/agrees/agrees.json5", port: 0, }); server.on("listening", () => { const body = { test: null, }; const bodyString = JSON.stringify(body); const options = { host: "localhost", method: "POST", path: "/test/agreed/use/null/obj", port: server.address().port, headers: { "Content-Type": "application/json", "Content-Length": bodyString.length, }, }; const req = http .request( options, mustCall((res) => { let data = ""; res.on("data", (d) => (data += d)); res.on( "end", mustCall(() => { const actual = JSON.parse(data); const expected = { results: null }; assert.deepStrictEqual(actual, expected); }) ); server.close(); }) ) .on("error", console.error); req.write(bodyString); req.end(); }); }); ================================================ FILE: packages/core/test/lib/server.checkHeadersCaseInsensitive.js ================================================ "use strict"; const agreedServer = require("../helper/server.js"); const test = require("eater/runner").test; const http = require("http"); const AssertStream = require("assert-stream"); const plzPort = require("plz-port"); const assert = require("power-assert"); const mustCall = require("must-call"); test("server: check headers when case insensitive", () => { const server = agreedServer({ path: "test/agrees/agrees.json5", port: 0, }); server.on("listening", () => { const options = { host: "localhost", method: "GET", path: "/test/case/insensitive/headers", port: server.address().port, headers: { "This-Headers-Should-Be-Lower-Case": "true", }, }; const req = http .request(options, (res) => { const assert = new AssertStream(); assert.expect({ message: "hello case insensitive headers" }); res.pipe(assert); server.close(); }) .on("error", console.error); req.end(); }); }); test("server: check headers when case insensitive", () => { const server = agreedServer({ path: "test/agrees/agrees.json5", port: 0, }); server.on("listening", () => { const options = { host: "localhost", method: "GET", path: "/test/case/insensitive/headers", port: server.address().port, headers: { "THIS-HEADERS-SHOULD-BE-LOWER-CASE": "true", }, }; const req = http .request(options, (res) => { const assert = new AssertStream(); assert.expect({ message: "hello case insensitive headers" }); res.pipe(assert); server.close(); }) .on("error", console.error); req.end(); }); }); ================================================ FILE: packages/core/test/lib/server.customFuncs.js ================================================ "use strict"; const agreedServer = require("../helper/server.js"); const test = require("eater/runner").test; const http = require("http"); const AssertStream = require("assert-stream"); const assert = require("power-assert"); const mustCall = require("must-call"); test("server: check custom function", () => { const server = agreedServer({ agrees: [ { request: { path: "/test/custom/agreed/values", query: { a: "{:a}", b: "{:b}", c: "{:c}", }, values: { a: 1, b: 2, c: 3, }, }, response: { body: { sum: "{sum:a,b,c}", }, funcs: { sum: (a, b, c) => parseInt(a) + parseInt(b) + parseInt(c), }, }, }, ], port: 0, }); server.on("listening", () => { const options = { host: "localhost", method: "GET", path: "/test/custom/agreed/values?a=1&b=2&c=3", port: server.address().port, }; const req = http .request(options, (res) => { let data = ""; res.on("data", (d) => (data += d)); res.on( "end", mustCall(() => { const result = JSON.parse(data); assert.strictEqual(result.sum, 6); }) ); server.close(); }) .on("error", console.error); req.end(); }); }); test("server: check custom function with array response", () => { const server = agreedServer({ agrees: [ { request: { path: "/test/custom/agreed/values", query: { a: "{:a}", b: "{:b}", c: "{:c}", }, values: { a: 1, b: 2, c: 3, }, }, response: { body: { sum: "{sum:a,b,c}", }, funcs: { sum: (a, b, c) => { var A = parseInt(a); var B = parseInt(b); var C = parseInt(c); return [A + B + C, A * B * C, A - B - C]; }, }, }, }, ], port: 0, }); server.on("listening", () => { const options = { host: "localhost", method: "GET", path: "/test/custom/agreed/values?a=1&b=2&c=3", port: server.address().port, }; const req = http .request(options, (res) => { let data = ""; res.on("data", (d) => (data += d)); res.on( "end", mustCall(() => { const result = JSON.parse(data); assert.deepStrictEqual(result.sum, [6, 6, -4]); }) ); server.close(); }) .on("error", console.error); req.end(); }); }); ================================================ FILE: packages/core/test/lib/server.emptyHeaderValue.js ================================================ "use strict"; const agreedServer = require("../helper/server.js"); const test = require("eater/runner").test; const http = require("http"); const AssertStream = require("assert-stream"); const assert = require("power-assert"); const mustCall = require("must-call"); test("server: check empty-header-value skip", () => { const server = agreedServer({ agrees: [ { request: { path: "/test/agreed/:id", values: { id: 1, }, }, response: { body: { id: 1, }, }, }, ], port: 0, skipCheckHeaderValueNullable: true, }); server.on("listening", () => { const options = { host: "localhost", method: "GET", path: "/test/agreed/1", port: server.address().port, headers: { "empty-header": "", }, }; const req = http .request(options, (res) => { let data = ""; res.on("data", (d) => (data += d)); res.on( "end", mustCall(() => { try { const result = JSON.parse(data); assert.strictEqual(result.id, 1); } catch (e) { console.error(); } }) ); server.close(); }) .on("error", console.error); req.end(); }); }); test("server: check empty-header-value fail", () => { const server = agreedServer({ agrees: [ { request: { path: "/test/agreed/:id", values: { id: 1, }, }, response: { body: { id: 1, }, }, }, ], port: 0, skipCheckHeaderValueNullable: false, }); server.on("listening", () => { const options = { host: "localhost", method: "GET", path: "/test/agreed/1", port: server.address().port, headers: { "empty-header": "", }, }; const req = http .request(options, (res) => { let data = ""; res.on("data", (d) => (data += d)); res.on( "end", mustCall(() => { try { const result = JSON.parse(data); assert.fail("shoul not be reached here."); } catch (e) { // noop. } }) ); server.close(); }) .on("error", console.error); req.end(); }); }); ================================================ FILE: packages/core/test/lib/server.js ================================================ "use strict"; const agreedServer = require("../helper/server.js"); const test = require("eater/runner").test; const http = require("http"); const AssertStream = require("assert-stream"); const plzPort = require("plz-port"); const assert = require("power-assert"); const mustCall = require("must-call"); const ts = require("typescript"); test("server: POST API", () => { plzPort().then((port) => { const server = agreedServer({ path: "test/agrees/agrees.js", port: port, }); server.on("listening", () => { const postData = JSON.stringify({ message: "foobarbaz", }); const options = { host: "localhost", method: "POST", path: "/hoge/abc", port: port, headers: { "Content-Type": "application/json", "Content-Length": Buffer.byteLength(postData), }, }; const req = http .request(options, (res) => { const assert = new AssertStream(); assert.expect({ message: "hello post" }); res.pipe(assert); server.close(); }) .on("error", console.error); req.write(postData); req.end(); }); }); }); test("server: PUT API", () => { plzPort().then((port) => { const server = agreedServer({ path: "test/agrees/agrees.js", port: port, }); server.on("listening", () => { const postData = JSON.stringify({ a: "b", c: "d", }); const options = { host: "localhost", method: "PUT", path: "/foo/aaa", port: port, headers: { "Content-Type": "application/json", "Content-Length": Buffer.byteLength(postData), }, }; const req = http .request(options, (res) => { const assert = new AssertStream(); assert.expect("hello put"); res.pipe(assert); server.close(); }) .on("error", console.error); req.write(postData); req.end(); }); }); }); test("server: DELETE API", () => { plzPort().then((port) => { const server = agreedServer({ path: "test/agrees/agrees.json5", port: port, }); server.on("listening", () => { const options = { host: "localhost", method: "DELETE", path: "/qux/fuga", port: port, }; const req = http .request(options, (res) => { assert(res.statusCode === 204); server.close(); }) .on("error", console.error); req.end(); }); }); }); test("server: GET with :id ", () => { plzPort().then((port) => { const server = agreedServer({ path: "test/agrees/agrees.js", port: port, }); server.on("listening", () => { const options = { host: "localhost", method: "GET", path: "/path/fuga", port: port, }; const req = http .request(options, (res) => { assert(res.statusCode === 200); const assertStream = new AssertStream(); assertStream.expect({ message: "hello fuga" }); res.pipe(assertStream); server.close(); }) .on("error", console.error); req.end(); }); }); }); test("server: POST with :id ", () => { plzPort().then((port) => { const server = agreedServer({ path: "test/agrees/agrees.js", port: port, }); server.on("listening", () => { const postData = JSON.stringify({ message: "foobarbaz", }); const options = { host: "localhost", method: "POST", path: "/path/fuga?meta=fooo", port: port, headers: { "Content-Type": "application/json", "Content-Length": Buffer.byteLength(postData), }, }; const req = http .request(options, (res) => { assert(res.statusCode === 200); const assertStream = new AssertStream(); assertStream.expect({ message: "hello fuga, fooo, foobarbaz" }); res.pipe(assertStream); server.close(); }) .on("error", console.error); req.write(postData); req.end(); }); }); }); test("server: check response when expect is filled", () => { plzPort().then((port) => { const server = agreedServer({ path: "test/agrees/agrees.js", port: port, }); server.on("listening", () => { const postData = JSON.stringify({ message: "foobarbaz", }); const options = { host: "localhost", method: "POST", path: "/embed/from/response/fuga?meta=true", port: port, headers: { "Content-Type": "application/json", "Content-Length": Buffer.byteLength(postData), }, }; const req = http .request(options, (res) => { assert(res.statusCode === 200); const assertStream = new AssertStream(); assertStream.expect({ message: "hello fuga true foobarbaz", image: "http://imgfp.hotp.jp/SYS/cmn/images/front_002/logo_hotopepper_264x45.png", topics: [ { a: "a", }, { b: "b", }, ], }); res.pipe(assertStream); server.close(); }) .on("error", console.error); req.write(postData); req.end(); }); }); }); test("server: check response when header values are exists", () => { plzPort().then((port) => { const server = agreedServer({ path: "test/agrees/agrees.js", port: port, }); server.on("listening", () => { const options = { host: "localhost", method: "GET", path: "/headers/2/", port: port, headers: { "x-token": "abcdefghi", "x-api-key": "123456789", }, }; const req = http .request(options, (res) => { assert(res.statusCode === 200); const assertStream = new AssertStream(); assertStream.expect({ result: "dunke abcdefghi 123456789", }); res.pipe(assertStream); server.close(); }) .on("error", console.error); req.end(); }); }); }); test("server: response header has format string", () => { plzPort().then((port) => { const server = agreedServer({ path: "test/agrees/agrees.json5", port: port, }); server.on("listening", () => { const options = { host: "localhost", method: "GET", path: "/path/header/format", port: port, }; const req = http .request(options, (res) => { assert(res.statusCode === 200); assert(res.headers["access-control-allow-origin"] === "*"); server.close(); }) .on("error", console.error); req.end(); }); }); }); test("server: response header using default response headers", () => { plzPort().then((port) => { const server = agreedServer({ path: "test/agrees/agrees.json5", port: port, defaultResponseHeaders: { "access-control-allow-origin": "test", }, }); server.on("listening", () => { const options = { host: "localhost", method: "GET", path: "/path/default/header/", port: port, }; const req = http .request( options, mustCall((res) => { assert(res.statusCode === 200); assert(res.headers["access-control-allow-origin"] === "test"); server.close(); }) ) .on("error", console.error); req.end(); }); }); }); test("server: response header using default request headers", () => { plzPort().then((port) => { const server = agreedServer({ path: "test/agrees/agrees.json5", port: port, defaultRequestHeaders: { "x-forwarded-for": "forward", }, }); server.on("listening", () => { const options = { host: "localhost", method: "GET", path: "/path/default/request/header/", port: port, headers: { "x-forwarded-for": "forward", }, }; const req = http .request( options, mustCall((res) => { assert(res.statusCode === 200); const assertStream = new AssertStream(); assertStream.expect({ message: "forward", }); res.pipe(assertStream); server.close(); }) ) .on("error", console.error); req.end(); }); }); }); test("server: POST API with ts agrees", () => { plzPort().then((port) => { const server = agreedServer({ path: "test/agrees/agrees.ts", port: port, }); server.on("listening", () => { const postData = JSON.stringify({ message: "test", }); const options = { host: "localhost", method: "POST", path: "/ts-messages", port: port, headers: { "Content-Type": "application/json", "Content-Length": Buffer.byteLength(postData), }, }; const req = http .request(options, (res) => { const assert = new AssertStream(); assert.expect({ result: "test" }); res.pipe(assert); server.close(); }) .on("error", console.error); req.write(postData); req.end(); }); }); }); test("server: POST with :id (when enable-prefer-query option is true)", () => { plzPort().then((port) => { const server = agreedServer({ path: "test/agrees/agrees.js", port: port, enablePreferQuery: true, }); server.on("listening", () => { const postData = JSON.stringify({ message: "foobarbaz", }); const options = { host: "localhost", method: "POST", path: "/path/fuga?meta=fooo", port: port, headers: { "Content-Type": "application/json", "Content-Length": Buffer.byteLength(postData), }, }; const req = http .request(options, (res) => { assert(res.statusCode === 404); server.close(); }) .on("error", console.error); req.write(postData); req.end(); }); }); }); test("server: check response when expect is filled (when enable-prefer-query option is true)", () => { plzPort().then((port) => { const server = agreedServer({ path: "test/agrees/agrees.js", port: port, enablePreferQuery: true, }); server.on("listening", () => { const postData = JSON.stringify({ message: "foobarbaz", }); const options = { host: "localhost", method: "POST", path: "/embed/from/response/fuga?meta=true", port: port, headers: { "Content-Type": "application/json", "Content-Length": Buffer.byteLength(postData), }, }; const req = http .request(options, (res) => { assert(res.statusCode === 404); server.close(); }) .on("error", console.error); req.write(postData); req.end(); }); }); }); ================================================ FILE: packages/core/test/lib/server.messages.js ================================================ "use strict"; const agreedServer = require("../helper/server.js"); const { test } = require("eater/runner"); const http = require("http"); const AssertStream = require("assert-stream"); const assert = require("power-assert"); const mustCall = require("must-call"); test("server: check post to list", () => { const server = agreedServer({ path: "test/agrees/agrees.json5", port: 0, }); server.on("listening", () => { const body = { messages: [{ message: "hoge" }], }; const bodyString = JSON.stringify(body); const options = { host: "localhost", method: "POST", path: "/test/agreed/messages", port: server.address().port, headers: { "Content-Type": "application/json", "Content-Length": bodyString.length, }, }; const req = http .request( options, mustCall((res) => { res.pipe(process.stdout); let data = ""; res.on("data", (d) => (data += d)); res.on( "end", mustCall(() => { const actual = JSON.parse(data); const expected = { results: body.messages }; assert.deepStrictEqual(actual, expected); }) ); server.close(); }) ) .on("error", console.error); req.write(bodyString); req.end(); }); }); ================================================ FILE: packages/core/test/lib/server.nestedbind.js ================================================ "use strict"; const agreedServer = require("../helper/server.js"); const test = require("eater/runner").test; const http = require("http"); const AssertStream = require("assert-stream"); const assert = require("power-assert"); const mustCall = require("must-call"); test("server: check parseInt", () => { const server = agreedServer({ path: "test/agrees/agrees.json5", port: 0, }); server.on("listening", () => { const body = { time: { start: 1, end: 3, break: { start: 2, end: 5 } }, members: [ { id: 1, }, { id: 2, }, ], }; const bodyStr = JSON.stringify(body); const options = { host: "localhost", method: "POST", path: "/test/bind/nest/object", port: server.address().port, headers: { "Content-Type": "application/json", "Content-Length": bodyStr.length, }, }; const req = http .request(options, (res) => { let data = ""; res.on("data", (d) => (data += d)); res.on( "end", mustCall(() => { const result = JSON.parse(data); const expected = { time: { start: 1, end: 3 }, break: { start: 2, end: 5 }, members: [{ id: 1 }, { id: 2 }], }; assert.deepStrictEqual(result, expected); }) ); server.close(); }) .on("error", console.error); req.write(bodyStr); req.end(); }); }); ================================================ FILE: packages/core/test/lib/server.notify.js ================================================ "use strict"; const agreedServer = require("../helper/server.notify.js"); const { test } = require("eater/runner"); const http = require("http"); const AssertStream = require("assert-stream"); const assert = require("power-assert"); const mustCall = require("must-call"); test("server: check notify", () => { const { server, notifier } = agreedServer({ path: "test/agrees/notify.js", port: 0, }); server.on("listening", () => { const body = { message: "hoge" }; const bodyString = JSON.stringify(body); const options = { host: "localhost", method: "POST", path: "/messages", port: server.address().port, headers: { "Content-Type": "application/json", "Content-Length": bodyString.length, }, }; const req = http .request( options, mustCall((res) => { let data = ""; res.on("data", (d) => (data += d)); res.on( "end", mustCall(() => { const actual = JSON.parse(data); const expected = { result: "hoge" }; assert.deepStrictEqual(actual, expected); }) ); server.close(); }) ) .on("error", console.error); notifier.on( "message2", mustCall((actual) => { const expected = { message: "message! hoge" }; assert.deepStrictEqual(actual, expected); }) ); req.write(bodyString); req.end(); }); }); test("server: check notify default message", () => { const { server, notifier } = agreedServer({ path: "test/agrees/notify.js", port: 0, }); server.on("listening", () => { const body = { message: "hoge" }; const bodyString = JSON.stringify(body); const options = { host: "localhost", method: "POST", path: "/messages2", port: server.address().port, headers: { "Content-Type": "application/json", "Content-Length": bodyString.length, }, }; const req = http .request( options, mustCall((res) => { let data = ""; res.on("data", (d) => (data += d)); res.on( "end", mustCall(() => { const actual = JSON.parse(data); const expected = { result: "hoge" }; assert.deepStrictEqual(actual, expected); }) ); server.close(); }) ) .on("error", console.error); notifier.on( "message", mustCall((actual) => { const expected = { message: "message2 hoge" }; assert.deepStrictEqual(actual, expected); }) ); req.write(bodyString); req.end(); }); }); ================================================ FILE: packages/core/test/lib/server.parseInt.js ================================================ "use strict"; const agreedServer = require("../helper/server.js"); const test = require("eater/runner").test; const http = require("http"); const AssertStream = require("assert-stream"); const assert = require("power-assert"); const mustCall = require("must-call"); test("server: check parseInt", () => { const server = agreedServer({ path: "test/agrees/agrees.json5", port: 0, }); server.on("listening", () => { const options = { host: "localhost", method: "GET", path: "/test/parseint/agreed/values/1000", port: server.address().port, }; const req = http .request(options, (res) => { let data = ""; res.on("data", (d) => (data += d)); res.on( "end", mustCall(() => { const result = JSON.parse(data); assert.deepStrictEqual(result.id, 1000); }) ); server.close(); }) .on("error", console.error); req.end(); }); }); ================================================ FILE: packages/core/test/lib/server.path.js ================================================ "use strict"; const agreedServer = require("../helper/server.js"); const test = require("eater/runner").test; const http = require("http"); const AssertStream = require("assert-stream"); test("server: `path` option can be directory index", () => { const server = agreedServer({ path: "test/agrees", port: 0, }); server.on("listening", () => { const postData = JSON.stringify({ message: "foobarbaz", }); const options = { host: "localhost", method: "POST", path: "/hoge/abc", port: server.address().port, headers: { "Content-Type": "application/json", "Content-Length": Buffer.byteLength(postData), }, }; const req = http .request(options, (res) => { const assert = new AssertStream(); assert.expect({ message: "hello post" }); res.pipe(assert); server.close(); }) .on("error", console.error); req.write(postData); req.end(); }); }); ================================================ FILE: packages/core/test/lib/server.pathParam.js ================================================ "use strict"; const agreedServer = require("../helper/server.js"); const test = require("eater/runner").test; const http = require("http"); const AssertStream = require("assert-stream"); const assert = require("power-assert"); const mustCall = require("must-call"); test("server: check path params priority - 1", () => { const server = agreedServer({ agrees: [ { request: { path: "/test/agreed/:id", values: { id: 1, }, }, response: { body: { id: 1, }, }, }, { request: { path: "/test/agreed/:id", values: { id: 2, }, }, response: { body: { id: 2, }, }, }, { request: { path: "/test/agreed/:id", values: { id: 3, }, }, response: { body: { id: 3, }, }, }, ], port: 0, }); server.on("listening", () => { const options = { host: "localhost", method: "GET", path: "/test/agreed/1", port: server.address().port, }; const req = http .request(options, (res) => { let data = ""; res.on("data", (d) => (data += d)); res.on( "end", mustCall(() => { const result = JSON.parse(data); assert.strictEqual(result.id, 1); }) ); server.close(); }) .on("error", console.error); req.end(); }); }); test("server: check path params priority - 2", () => { const server = agreedServer({ agrees: [ { request: { path: "/test/agreed/:id", values: { id: 1, }, }, response: { body: { id: 1, }, }, }, { request: { path: "/test/agreed/:id", values: { id: 2, }, }, response: { body: { id: 2, }, }, }, { request: { path: "/test/agreed/:id", values: { id: 3, }, }, response: { body: { id: 3, }, }, }, ], port: 0, }); server.on("listening", () => { const options = { host: "localhost", method: "GET", path: "/test/agreed/2", port: server.address().port, }; const req = http .request(options, (res) => { let data = ""; res.on("data", (d) => (data += d)); res.on( "end", mustCall(() => { const result = JSON.parse(data); assert.strictEqual(result.id, 2); }) ); server.close(); }) .on("error", console.error); req.end(); }); }); test("server: check path params priority - 3", () => { const server = agreedServer({ agrees: [ { request: { path: "/test/agreed/:id", values: { id: 1, }, }, response: { body: { id: 1, }, }, }, { request: { path: "/test/agreed/:id", values: { id: 2, }, }, response: { body: { id: 2, }, }, }, { request: { path: "/test/agreed/:id", values: { id: 3, }, }, response: { body: { id: 3, }, }, }, ], port: 0, }); server.on("listening", () => { const options = { host: "localhost", method: "GET", path: "/test/agreed/3", port: server.address().port, }; const req = http .request(options, (res) => { let data = ""; res.on("data", (d) => (data += d)); res.on( "end", mustCall(() => { const result = JSON.parse(data); assert.strictEqual(result.id, 3); }) ); server.close(); }) .on("error", console.error); req.end(); }); }); test("server: check path params priority - 4", () => { const server = agreedServer({ agrees: [ { request: { path: "/test/agreed/:foo/:bar", values: { foo: 1, bar: 2, }, }, response: { body: { id: 1, }, }, }, { request: { path: "/test/agreed/1/3", }, response: { body: { id: 2, }, }, }, ], port: 0, }); server.on("listening", () => { const options = { host: "localhost", method: "GET", path: "/test/agreed/1/2", port: server.address().port, }; const req = http .request(options, (res) => { let data = ""; res.on("data", (d) => (data += d)); res.on( "end", mustCall(() => { const result = JSON.parse(data); assert.strictEqual(result.id, 1); }) ); server.close(); }) .on("error", console.error); req.end(); }); }); test("server: check path params priority - 5", () => { const server = agreedServer({ agrees: [ { request: { path: "/test/agreed/:foo/:bar", values: { foo: 1, bar: 2, }, }, response: { body: { id: 1, }, }, }, { request: { path: "/test/agreed/1/3", }, response: { body: { id: 2, }, }, }, ], port: 0, }); server.on("listening", () => { const options = { host: "localhost", method: "GET", path: "/test/agreed/1/3", port: server.address().port, }; const req = http .request(options, (res) => { let data = ""; res.on("data", (d) => (data += d)); res.on( "end", mustCall(() => { const result = JSON.parse(data); assert.strictEqual(result.id, 2); }) ); server.close(); }) .on("error", console.error); req.end(); }); }); ================================================ FILE: packages/core/test/lib/server.randomInt.js ================================================ "use strict"; const agreedServer = require("../helper/server.js"); const test = require("eater/runner").test; const http = require("http"); const AssertStream = require("assert-stream"); const assert = require("power-assert"); const mustCall = require("must-call"); test("server: check randomInt", () => { const server = agreedServer({ path: "test/agrees/agrees.json5", port: 0, }); server.on("listening", () => { const options = { host: "localhost", method: "GET", path: "/test/randomint/agreed/values", port: server.address().port, }; const req = http .request(options, (res) => { let data = ""; res.on("data", (d) => (data += d)); res.on( "end", mustCall(() => { const result = JSON.parse(data); assert(result.random >= 0); assert(result.random < 1000); }) ); server.close(); }) .on("error", console.error); req.end(); }); }); ================================================ FILE: packages/core/test/lib/server.randomString.js ================================================ "use strict"; const agreedServer = require("../helper/server.js"); const test = require("eater/runner").test; const http = require("http"); const AssertStream = require("assert-stream"); const assert = require("power-assert"); const mustCall = require("must-call"); test("server: check randomString", () => { const server = agreedServer({ path: "test/agrees/agrees.json5", port: 0, }); server.on("listening", () => { const options = { host: "localhost", method: "GET", path: "/test/randomstring/agreed/values", port: server.address().port, }; const req = http .request(options, (res) => { let data = ""; res.on("data", (d) => (data += d)); res.on( "end", mustCall(() => { const result = JSON.parse(data); assert(typeof result.random === "string"); assert(result.random.length === 8); }) ); server.close(); }) .on("error", console.error); req.end(); }); }); ================================================ FILE: packages/core/test/lib/server.statusTemplate.js ================================================ "use strict"; const agreedServer = require("../helper/server.js"); const test = require("eater/runner").test; const http = require("http"); const AssertStream = require("assert-stream"); const assert = require("power-assert"); const mustCall = require("must-call"); test("server: check status template", () => { const server = agreedServer({ agrees: [ { request: { path: "/test/custom/agreed/status", query: { status: "{:status}", }, }, response: { status: "{:status}", body: "OK", }, }, ], port: 0, }); server.on("listening", () => { const options = { host: "localhost", method: "GET", path: "/test/custom/agreed/status?status=404", port: server.address().port, }; const req = http .request(options, (res) => { let data = ""; assert.strictEqual(res.statusCode, 404); res.on("data", (d) => (data += d)); res.on( "end", mustCall(() => { assert.strictEqual(data, '"OK"'); }) ); server.close(); }) .on("error", console.error); req.end(); }); }); ================================================ FILE: packages/core/test/lib/server.strict.js ================================================ "use strict"; const agreedServer = require("../helper/server.js"); const test = require("eater/runner").test; const http = require("http"); const assert = require("power-assert"); const mustCall = require("must-call"); test("server: check strict mode - status code", () => { const server = agreedServer({ agrees: [ { request: { path: "/test/agreed/:id", values: { id: 1, }, }, response: { body: { id: 1, }, }, }, { request: { path: "/test/agreed/:id", values: { id: 2, }, }, response: { body: { id: 2, }, }, }, ], port: 0, strict: true, hot: true, }); server.on("listening", () => { const options = { host: "localhost", method: "GET", path: "/test/agreed/1", port: server.address().port, }; const req = http .request(options, (res) => { assert.strictEqual(res.statusCode, 500); res.resume(); server.close(); }) .on("error", console.error); req.end(); }); }); test("server: check strict mode - message", () => { const server = agreedServer({ agrees: [ { request: { path: "/test/agreed/:id", values: { id: 1, }, }, response: { body: { id: 1, }, }, }, { request: { path: "/test/agreed/:id", values: { id: 2, }, }, response: { body: { id: 2, }, }, }, ], port: 0, strict: true, hot: true, }); server.on("listening", () => { const options = { host: "localhost", method: "GET", path: "/test/agreed/1", port: server.address().port, }; const req = http .request(options, (res) => { let data = ""; res.on("data", (d) => (data += d)); res.on( "end", mustCall(() => { const result = JSON.parse(data); assert.strictEqual(result.message, "Ambiguous Request"); }) ); server.close(); }) .on("error", console.error); req.end(); }); }); test("server: check strict mode - candidates", () => { const server = agreedServer({ agrees: [ { request: { path: "/test/agreed/:id", values: { id: 1, }, }, response: { body: { id: 1, }, }, }, { request: { path: "/test/agreed/:id", values: { id: 2, }, }, response: { body: { id: 2, }, }, }, { request: { path: "/test/agreed/3", }, response: {}, }, ], port: 0, strict: true, hot: true, }); server.on("listening", () => { const options = { host: "localhost", method: "GET", path: "/test/agreed/1", port: server.address().port, }; const req = http .request(options, (res) => { let data = ""; res.on("data", (d) => (data += d)); res.on( "end", mustCall(() => { const result = JSON.parse(data); assert.strictEqual(result.candidates.length, 2); }) ); server.close(); }) .on("error", console.error); req.end(); }); }); ================================================ FILE: packages/core/test/lib/server.typedcachepath.js ================================================ "use strict"; const agreedServer = require("../helper/server.js"); const http = require("http"); const test = require("eater/runner").test; const AssertStream = require("assert-stream"); const plzPort = require("plz-port"); const assert = require("power-assert"); const mustCall = require("must-call"); const os = require("os"); const fs = require("fs"); test("server: POST API with ts agrees using typed cache path", () => { plzPort().then((port) => { const dest = `${os.tmpdir()}/agrees.ts`; const cachePath = `${os.tmpdir()}/.agreed.json`; fs.copyFileSync("test/agrees/agrees.ts", dest); const server = agreedServer({ path: dest, port: port, typedCachePath: cachePath, }); server.on("listening", () => { const postData = JSON.stringify({ message: "test", }); const options = { host: "localhost", method: "POST", path: "/ts-messages", port: port, headers: { "Content-Type": "application/json", "Content-Length": Buffer.byteLength(postData), }, }; const req = http .request(options, (res) => { const assert = new AssertStream(); assert.expect({ result: "test" }); res.pipe(assert); server.close(); process.kill(process.pid, "SIGHUP"); }) .on("error", console.error); req.write(postData); req.end(); }); }); }); test("server: POST API with ts agrees using typed cache path using cache", () => { plzPort().then((port) => { const dest = `${os.tmpdir()}/agrees.ts`; const cachePath = `${os.tmpdir()}/.agreed.json`; const server = agreedServer({ path: dest, port: port, typedCachePath: cachePath, }); server.on("listening", () => { const postData = JSON.stringify({ message: "test", }); const options = { host: "localhost", method: "POST", path: "/ts-messages", port: port, headers: { "Content-Type": "application/json", "Content-Length": Buffer.byteLength(postData), }, }; const req = http .request(options, (res) => { const assert = new AssertStream(); assert.expect({ result: "test" }); res.pipe(assert); server.close(); process.kill(process.pid, "SIGHUP"); }) .on("error", console.error); req.write(postData); req.end(); }); }); }); test("server: POST API with ts agrees using typed cache path using cache", () => { plzPort().then((port) => { const dest = `${os.tmpdir()}/agrees.ts`; const cachePath = `${os.tmpdir()}/.agreed.json`; const content = ` module.exports = [{ request: { path: '/ts-messages2', method: 'POST', body: { message: '{:message}' }, values: { message: 'test' } }, response: { body: { result: '{:message}' }, values: { message: 'test' }, }, }]; `; fs.writeFileSync(dest, content); const server = agreedServer({ path: dest, port: port, typedCachePath: cachePath, }); server.on("listening", () => { const postData = JSON.stringify({ message: "test", }); const options = { host: "localhost", method: "POST", path: "/ts-messages2", port: port, headers: { "Content-Type": "application/json", "Content-Length": Buffer.byteLength(postData), }, }; const req = http .request(options, (res) => { const assert = new AssertStream(); assert.expect({ result: "test" }); res.pipe(assert); server.close(); process.kill(process.pid, "SIGHUP"); }) .on("error", console.error); req.write(postData); req.end(); }); }); }); test("server: use agreed-typed fixtures", () => { plzPort().then((port) => { const cachePath = `${os.tmpdir()}/.agreed.json`; const server = agreedServer({ path: "../typed/src/__tests__/data/agreed.ts", port: port, typedCachePath: cachePath, }); server.on("listening", () => { const options = { host: "localhost", method: "GET", path: "/ping/test", port: port, headers: { "Content-Type": "application/json", }, }; const req = http .request(options, (res) => { const assert = new AssertStream(); assert.expect({ message: "ok test" }); res.pipe(assert); server.close(); process.kill(process.pid, "SIGHUP"); }) .on("error", console.error); req.end(); }); }); }); ================================================ FILE: packages/core/test/lib/server.unixtime.js ================================================ "use strict"; const agreedServer = require("../helper/server.js"); const test = require("eater/runner").test; const http = require("http"); const AssertStream = require("assert-stream"); const assert = require("power-assert"); const mustCall = require("must-call"); test("server: check unixtime", () => { const server = agreedServer({ path: "test/agrees/agrees.json5", port: 0, }); server.on("listening", () => { const options = { host: "localhost", method: "GET", path: "/test/unixtime/agreed/values", port: server.address().port, }; const currentUnix = parseInt(Date.now() / 1000); const req = http .request(options, (res) => { let data = ""; res.on("data", (d) => (data += d)); res.on( "end", mustCall(() => { const result = JSON.parse(data); console.log(currentUnix); console.log(result); assert(result.unixtime >= currentUnix); }) ); server.close(); }) .on("error", console.error); req.end(); }); }); ================================================ FILE: packages/core/test/lib/template/bind.js ================================================ "use strict"; const test = require("eater/runner").test; const bind = require(`${process.cwd()}/lib/template/bind`); const assert = require("power-assert"); test('bind: { key: "{:value}" }, { key: "foo" } => { value: "foo" }', () => { const result = bind({ key: "{:value}" }, { key: "foo" }); assert.deepEqual(result, { value: "foo" }); }); test('bind: { key: { foo: "{:value}" }, { key: { foo: "foo"} } => { value: "foo" }', () => { const result = bind({ key: { foo: "{:value}" } }, { key: { foo: "foo" } }); assert.deepEqual(result, { value: "foo" }); }); test('bind: { key: { foo: "{:value}" }, { key: { foo: null } } => { value: null }', () => { const result = bind({ key: { foo: "{:value}" } }, { key: { foo: null } }); assert.deepEqual(result, { value: null }); }); test('bind: { key: { foo: "{:value.foo}" }, { key: { value: { foo: 12345 } } } => { value: { foo: 12345 } }', () => { const result = bind( { key: { foo: "{:value.foo}" } }, { key: { value: { foo: 12345 } } } ); assert.deepEqual(result, { value: { foo: 12345 } }); }); test('bind: { key: { foo: "{:value.foo.bar.0.baz}" }, { key: { value: { foo: { bar: [ { baz : 12345 } ] } } } } => { value: { foo: { bar: [ { baz: 12345 } ] } } }', () => { const result = bind( { key: { foo: "{:value.foo.bar.0.baz}" } }, { key: { value: { foo: { bar: [{ baz: 12345 }] } } } } ); const expect = { value: { foo: { bar: [{ baz: 12345 }] } } }; assert.deepStrictEqual(result, expect); }); ================================================ FILE: packages/core/test/lib/template/format.js ================================================ "use strict"; const test = require("eater/runner").test; const format = require(`${process.cwd()}/lib/template/format`); const assert = require("power-assert"); test("format: {:id} = {:aa}", () => { const result = format("{:id} = {:aa}", { id: "fooo", aa: "barrr" }); assert(result === "fooo = barrr"); }); test("format: {:id.foo} = {:aa.bar}", () => { const result = format("{:id.foo} = {:aa.bar}", { id: { foo: "fooo" }, aa: { bar: "barrr" } }); assert(result === "fooo = barrr"); }); test("format: {:id.foo.bar} = {:aa.bar.baz}", () => { const result = format("{:id.foo.bar} = {:aa.bar.baz}", { id: { foo: { bar: "fooo" } }, aa: { bar: { baz: "barrr" } } }); assert(result === "fooo = barrr"); }); test("format: object format", () => { const obj = { a: "{:abc}", b: "{:def}", c: "{:ghi}" }; const result = format(obj, { abc: true, def: [1, 2, 3, null], ghi: { aaaa: "bbb" } }); assert.deepStrictEqual(result, { a: true, b: [1, 2, 3, null], c: { aaaa: "bbb" } }); }); test("format: nested json and format is unmatched", () => { const obj = { a: { abc: "{:abc}" }, b: "{:def}", c: "{:ghi}" }; const result = format(obj, { id: "fooo", aa: "barrr", ghi: "123" }); assert.deepStrictEqual(result, { a: { abc: "{:abc}" }, b: "{:def}", c: "123" }); }); test("format: array format", () => { const obj = { a: { abc: "{:abc}" }, b: [1, 2, 3], c: "{:ghi}" }; const result = format(obj, { id: "fooo", aa: "barrr", ghi: "123" }); assert.deepStrictEqual(result, { a: { abc: "{:abc}" }, b: [1, 2, 3], c: "123" }); }); test("format: number format", () => { const obj = { a: { abc: 1 }, c: "{:ghi}" }; const result = format(obj, { id: "fooo", aa: "barrr", ghi: "123" }); assert.deepStrictEqual(result, { a: { abc: 1 }, c: "123" }); }); test("format: brackets notation :ghi[:id]", () => { const obj = { a: { abc: 1 }, c: "{:ghi[:id]}" }; const result = format(obj, { id: "fooo", aa: "barrr", ghi: { fooo: "123", baaa: "234" } }); assert.deepStrictEqual(result, { a: { abc: 1 }, c: "123" }); }); test("format: brackets notation :ghi[:id][:aa]", () => { const obj = { a: { abc: 1 }, c: "{:ghi[:id][:aa]}" }; const result = format(obj, { id: "fooo", aa: "barrr", ghi: { fooo: { barrr: "123" }, baaa: "234" } }); assert.deepStrictEqual(result, { a: { abc: 1 }, c: "123" }); }); test("format: brackets notation to use array", () => { const obj = { a: { abc: 1 }, c: "{:ghi[:id]}" }; const result = format(obj, { id: 1, aa: "barrr", ghi: [1, 2, 3] }); assert.deepStrictEqual(result, { a: { abc: 1 }, c: 2 }); }); test("format: array 1-last notation to spread array", () => { const obj = { arr: [ { name: "{:ghi.0.name}" }, "{:ghi.1-last}" ] }; const result = format(obj, { ghi: [ { name: "foo" }, { name: "bar" }, { name: "baz" } ] }); assert.deepStrictEqual(result, { arr: [ { name: "foo" }, { name: "bar" }, { name: "baz" } ] }); }); test("format: array 1-4 notation to spread array", () => { const obj = { arr: [ { name: "{:ghi.0.name}" }, "{:ghi.1-4}" ] }; const result = format(obj, { ghi: [ { name: "foo" }, { name: "bar" }, { name: "baz" }, { name: "bar" }, { name: "baz" } ] }); assert.deepStrictEqual(result, { arr: [ { name: "foo" }, { name: "bar" }, { name: "baz" }, { name: "bar" } ] }); }); test("format: array 1-last notation to spread array with null", () => { const obj = { arr: [ { name: "{:ghi.0.name}" }, "{:ghi.1-last}" ] }; const result = format(obj, { ghi: [ { name: null }, { name: "bar" }, { name: "baz" }, { name: "bar" }, { name: null } ] }); assert.deepStrictEqual(result, { arr: [ { name: null }, { name: "bar" }, { name: "baz" }, { name: "bar" }, { name: null } ] }); }); ================================================ FILE: packages/core/test/lib/template/format.operationalKey.js ================================================ "use strict"; const test = require("eater/runner").test; const format = require(`${process.cwd()}/lib/template/format`); const assert = require("power-assert"); test("format: {randomInt:id}", () => { const result = format({ id: "{randomInt:id}" }, { id: "1-10000" }); assert(result.id >= 1); assert(result.id <= 10000); }); test("format: {randomInt:id} but no range", () => { const result = format({ id: "{randomInt:id}" }, { id: 1 }); // default value is over 1 assert(result.id > 1); assert(result.id <= Number.MAX_SAFE_INTEGER); }); test("format: {randomInt:id} but range 1000-100", () => { const result = format({ id: "{randomInt:id}" }, { id: "1000-100" }); assert(result.id >= 0); assert(result.id <= 1000); }); test("format: {parseInt:id}", () => { const result = format({ id: "{parseInt:id}" }, { id: "10000" }); assert.deepStrictEqual(result.id, 10000); }); test("format: {unixtime:time}", () => { const unixtime = parseInt(Date.now() / 1000); const result = format({ time: "{unixtime:time}" }, { time: "10000" }); assert(result.time >= unixtime); }); test("format: {sum:a,b}", () => { const sum = (a, b) => a + b; const result = format({ sum: "{sum:a,b}" }, { a: 1, b: 2 }, { sum: sum }); assert.strictEqual(result.sum, 3); }); test("format: {sub:a,b}", () => { const result = format( { sub: "{sub:a,b}" }, { a: 1, b: 2 }, { sub: "./test/agrees/sub.js", basedir: process.cwd() } ); assert.strictEqual(result.sub, -1); }); test("format: {sub:a,b} but response is array", () => { const result = format( { sub: "{sub:a,b}" }, { a: 1, b: 2 }, { sub: (a, b) => [a - b, a + b, a * b, a / b], basedir: process.cwd() } ); assert.deepStrictEqual(result.sub, [-1, 3, 2, 0.5]); }); test("format: {sub:a.0,a.1}", () => { const result = format( { sub: "{sub:a.0,a.1}" }, { a: [0, 4] }, { sub: (a, b) => a - b, basedir: process.cwd() } ); assert.strictEqual(result.sub, -4); }); ================================================ FILE: packages/core/test/lib/template/hasTemplate.js ================================================ "use strict"; const test = require("eater/runner").test; const assert = require("power-assert"); const hasTemplate = require(`${process.cwd()}/lib/template/hasTemplate`) .hasTemplate; const hasTemplateWithAnyString = require(`${process.cwd()}/lib/template/hasTemplate`) .hasTemplateWithAnyString; test("hasTemplate: check template has {:id}", () => { const has = hasTemplate("foo {:id}"); assert(has); }); test("hasTemplate: check template has not {:id}", () => { const has = hasTemplate("foo {id}"); assert(!has); }); test("hasTemplateWithAnyString: check template has not foo {:id}", () => { const has = hasTemplateWithAnyString("foo {:id}"); assert(has); }); test("hasTemplateWithAnyString: check template has not foo {:id}", () => { const has = hasTemplateWithAnyString("{:id}"); assert(!has); }); ================================================ FILE: packages/core/test/lib/template/toRegexp.js ================================================ const test = require("eater/runner").test; const assert = require("power-assert"); const toRegexp = require(`${process.cwd()}/lib/template/toRegexp`); test("toRegexp: template string to Regexp", () => { const regexp = toRegexp("foo {:id}"); assert("foo bar baz".match(regexp)); }); test("toRegexp: template string to Regexp not match", () => { const regexp = toRegexp("bar {:id}"); assert("foo bar baz".match(regexp) === null); }); test("toRegexp: template {:foo.aaa}", () => { const regexp = toRegexp("foo {:id.aaa} baz"); assert("foo bar baz".match(regexp)); }); ================================================ FILE: packages/server/.gitignore ================================================ *.swp node_modules .DS_Store ================================================ FILE: packages/server/.travis.yml ================================================ language: node_js node_js: - "6" - "8" script: npm test sudo: false ================================================ FILE: packages/server/README.md ================================================ # Agreed Server [![Build Status](https://travis-ci.org/recruit-tech/agreed-server.svg?branch=add_travis)](https://travis-ci.org/recruit-tech/agreed-server) [Agreed](https://www.npmjs.com/package/agreed-core) mock server `agreed-server` is a mock server based on `agreed`. This module provides CLI executable command and 2 programmable interface. If you want to use `agreed` as mock, you would be better to install `agreed-server`. # Install ``` $ npm install agreed-server --save-dev ``` # Basic Usage Usage as CLI ``` $ agreed-server --path ./test/agreed.json --port 10101 ``` - if you want to boot agreed server as disable hot replacement. ``` $ agreed-server --path ./test/agreed.json --port 10101 --hot false ``` Usage as programming ```js const agreedServer = require('agreed-server'); const server = agreedServer({ path: 'agreed/agreed.json', port: 3001, static: './static', // serve files from ./static staticPrefixPath: '/public', }).createServer(); ``` # Advanced Usage Usage as Express pure server ```js const agreedServer = require('agreed-server'); const { app, createServer } = agreedServer({ path: 'agreed/agreed.json', port: 3001, static: './static', // serve files from ./static staticPrefixPath: '/public', middlewares: [ logger, perfTool, secureHeaders, ], defaultRequestHeaders: { 'x-forwarded-for': 'nginx' }, defaultResponseHeaders: { 'access-control-allow-origin': '*' }, }); app.use(someGoodMiddleware); app.use((err, req, res, next) => { res.statusCode = 500; res.send(`Error is occurred : ${err}, you should see log`); }); const server = createServer(app); ``` ## notifier Usage as notification event ### agreed file ```js module.exports = [ { request: { path: '/messages', method: 'POST', body: { message: '{:message}' }, values: { message: 'test', }, }, response: { body: { result : '{:message}' }, values: { message: 'test', }, // add notify property for notification notify: { body: { message: 'message! {:message}' } } }, }, ] ``` ```js const agreedServer = require('agreed-server'); const { app, createServer, notifier } = agreedServer({ path: 'agreed/agreed.json', port: 3001, static: './static', // serve files from ./static staticPrefixPath: '/public', middlewares: [ logger, perfTool, secureHeaders, ], defaultRequestHeaders: { 'x-forwarded-for': 'nginx' }, defaultResponseHeaders: { 'access-control-allow-origin': '*' }, }); notifier.on('message', (data) => { console.log(data) // { message: 'message! hoge' } }); app.use(someGoodMiddleware); app.use((err, req, res, next) => { res.statusCode = 500; res.send(`Error is occurred : ${err}, you should see log`); }); const server = createServer(app); ``` ================================================ FILE: packages/server/bin/agreed-server.js ================================================ #!/usr/bin/env node const minimist = require('minimist'); const argv = minimist(process.argv.slice(2), { string: [ 'path', 'port', 'static', 'static-prefix-path', 'default-response-headers', 'default-request-headers', 'proxy', 'proxy-prefix-path', 'typed-cache-path' ], boolean: ['help', 'version', 'logging', 'strict', 'hot', 'cors', 'enable-prefer-query', 'skip-check-header-value-nullable'], alias: { l: 'logging' }, default: { hot: true } }); const path = require('path'); const colo = require('colo'); const JSON5 = require('json5'); const url = require('url'); const agreedServer = require('../'); const usage = ` Usage: agreed-server --path [options] Options: --help Shows the usage and exits. --version Shows version number and exits. --path Agreed file path. Required. --port Server port. Default 3000. --static Static file path. --static-prefix-path Static serve path prefix. --default-response-headers Default response headers object. --default-request-headers Default request headers object. --proxy Proxy host. --proxy-prefix-path Proxy server path prefix. -l, --logging Logs requests in console. --cors Enable CORS. --strict Run strict mode. --hot Hot Replacement agree files. Default true --typed-cache-path Create Cached JSON to improve agreed typed performance. --enable-prefer-query Check for matching query character values --skip-check-header-value-nullable Skip header null check Examples: agreed-server --path ./agreed.js --port 4000 agreed-server --path ./agreed.js --port 4000 \\ --static ./static --stati-prefix-path /public \\ --default-response-headers "{ 'access-control-allow-origin': '*' }" \\ --default-request-headers "{ 'x-jwt-token': 'foobarbaz' }" agreed-server --path ./agreed.js --port 4000 \\ --proxy example.com \\ --proxy-prefix-path /proxy agreed-server --path ./agreed.js --port 4000 \\ --typed-cache-path ./.agreed.json agreed-server --path ./agreed.js --port 4000 --enable-prefer-query `.trim(); function showHelp(exitcode) { console.log(usage); process.exit(exitcode); } if (argv.help) { showHelp(0); } if (argv.version) { const pack = require('../package.json'); console.log(pack.version); process.exit(0); } if (!argv.path) { console.error(colo.red('[agreed-server]: --path option is required')); showHelp(1); } if (argv['default-response-headers']) { argv.defaultResponseHeaders = JSON5.parse(argv['default-response-headers']); } if (argv['default-request-headers']) { argv.defaultRequestHeaders = JSON5.parse(argv['default-request-headers']); } agreedServer(argv).createServer(); ================================================ FILE: packages/server/index.js ================================================ 'use strict'; const express = require('express'); const path = require('path'); const bodyParser = require('body-parser'); const Agreed = require('@agreed/core'); const httpProxy = require('express-http-proxy'); const morgan = require('morgan'); const cors = require('cors'); module.exports = (opts) => { if (!opts) { throw new Error('[agreed-server] option is required.'); } if (!opts.path) { throw new Error('[agreed-server] option.path is required.'); } const app = express(); const port = opts.port || 3000; const stat = opts.static; const staticPrefixPath = opts['static-prefix-path'] || opts.staticPrefixPath; const proxy = opts.proxy; const proxyPrefixPath = opts['proxy-prefix-path'] || opts.proxyPrefixPath; const proxyOpts = opts.proxyOpts || {}; const typedCachePath = opts['typed-cache-path'] || opts.typedCachePath; opts.typedCachePath = typedCachePath; const enablePreferQuery = opts["enable-prefer-query"] || opts.enablePreferQuery; opts.enablePreferQuery = enablePreferQuery; const skipCheckHeaderValueNullable = opts["skip-check-header-value-nullable"] || opts.skipCheckHeaderValueNullable; opts.skipCheckHeaderValueNullable = skipCheckHeaderValueNullable; app.use(bodyParser.json()); app.use(bodyParser.urlencoded({ extended: true })); if (stat) { if (staticPrefixPath) { app.use(staticPrefixPath, express.static(path.join(process.cwd(), stat))); } else { app.use(express.static(path.join(process.cwd(), stat))); } } if (proxy) { if (proxyPrefixPath) { app.use(proxyPrefixPath, httpProxy(proxy, proxyOpts)); } else { app.use(httpProxy(proxy, proxyOpts)); } } if (opts.middlewares) { if (!Array.isArray(opts.middlewares)) { throw new Error('[agreed-server] option.middlewares must be an array.'); } opts.middlewares.forEach((fn) => { app.use(fn); }); } if (opts.logging) { app.use(morgan('tiny')); } if (opts.cors) { app.use(cors()); } const agreed = new Agreed(); app.use(agreed.middleware(opts)); const createServer = (appServer = app) => { appServer.use((err, req, res, next) => { res.statusCode = 500; res.send(`Error is occurred : ${err}`); }); const server = appServer.listen(opts.port); if (opts.closeTime) { setTimeout(server.close, opts.closeTime); } return server; }; const notifier = agreed.server.notifier; return { // low level app, // high level createServer, notifier }; }; ================================================ FILE: packages/server/package.json ================================================ { "name": "@agreed/server", "version": "6.0.0", "description": "agreed server", "main": "index.js", "bin": { "server": "bin/agreed-server.js" }, "scripts": { "test": "eater" }, "repository": { "type": "git", "url": "git+https://github.com/recruit-tech/agreed.git" }, "keywords": [ "agreed", "server", "mock", "port" ], "author": "yosuke-furukawa", "license": "MIT", "bugs": { "url": "https://github.com/recruit-tech/agreed-server/issues" }, "homepage": "https://github.com/recruit-tech/agreed-server#readme", "dependencies": { "@agreed/core": "^6.0.0", "body-parser": "^1.18.3", "cors": "^2.8.5", "express": "^4.16.4", "express-http-proxy": "^1.6.0", "morgan": "^1.9.1" }, "devDependencies": { "assert-stream": "1.1.1", "eater": "4.0.4", "must-call": "1.0.0", "plz-port": "1.0.0" }, "publishConfig": { "access": "public" }, "directories": { "test": "test" } } ================================================ FILE: packages/server/renovate.json ================================================ { "extends": [ "config:base" ] } ================================================ FILE: packages/server/test/agreed.json ================================================ [ { request: { path: "/users/:id" }, response: { body: { message: 'hello {:id}', } } }, { request: { path: "/users/header/:id", values: { jwt: 'testtesttesttest' }, }, response: { body: { message: 'hello {:id}', } } }, { request: { path: "/shops/:id", }, response: { body: { message: 'shop {:id}', } } }, { request: { path: "/urlencoded/test", method: "POST", body: { foo: "bar", bar: "baz", hoge: "fuga" }, headers: { 'content-type': 'application/x-www-form-urlencoded' } }, response: { body: { message: 'hello wwwurlencoded', } } }, { request: { path: "/cors/test", method: "POST" }, response: { body: { message: 'hello cors', } } }, { request: { path: "/bar" }, response: { body: { message: 'hello foo', } } } ] ================================================ FILE: packages/server/test/agreedNotify.json5 ================================================ [ { request: { path: "/users/:id" }, response: { body: { message: 'hello {:id}', }, notify: { body: { message: 'hello {:id}', } } } }, ] ================================================ FILE: packages/server/test/agreedProxy.json ================================================ [ { request: { path: "/test/proxy" }, response: { body: { message: 'hello proxy', } } }, { request: { path: "/foo" }, response: { body: { message: 'hello foo', } } }, ] ================================================ FILE: packages/server/test/app.js ================================================ 'use strict'; const test = require('eater/runner').test; const assert = require('assert'); const agreedServer = require('../'); const AssertStream = require('assert-stream'); const http = require('http'); test('agreed-server: instance app', () => { const { app, createServer } = agreedServer({ path: './test/agreed', port: '0', hot: false }); const server = createServer(app); server.on('listening', () => { const port = server.address().port; const options = { host: 'localhost', method: 'GET', path: '/shops/test', port: port, headers: { 'Content-Type': 'application/json' } }; http.get(options, (res) => { server.close(); const assertStream = new AssertStream(); assertStream.expect({ message: 'shop test' }); res.pipe(assertStream); }); }); }); test('agreed-server: call foobarbaz', () => { const { app, createServer } = agreedServer({ path: './test/agreed', port: '0', callNextWhenNotFound: true, hot: true }); app.use('/foobarbaz', (req, res, next) => { res.send('hello middleware'); }); const server = createServer(app); server.on('listening', () => { const port = server.address().port; const options = { host: 'localhost', method: 'GET', path: '/foobarbaz', port: port }; http.get(options, (res) => { server.close(); const assertStream = new AssertStream(); assertStream.expect('hello middleware'); res.pipe(assertStream); }); }); }); ================================================ FILE: packages/server/test/cors.js ================================================ 'use strict'; const test = require('eater/runner').test; const assert = require('assert'); const agreedServer = require('../'); const http = require('http'); test('cors option', () => { const server = agreedServer({ path: './test/agreed', port: '0', cors: true, }).createServer(); server.on('listening', () => { const port = server.address().port; const url = `http://localhost:${port}/cors/test` const preflight = http.request(url, { method: 'OPTIONS' }, (res) => { assert.strictEqual(res.headers['access-control-allow-origin'], '*'); assert.strictEqual(res.headers['access-control-allow-methods'], 'GET,HEAD,PUT,PATCH,POST,DELETE'); assert.strictEqual(res.statusCode, 204); const post = http.request(url, { method: 'POST' }, (res) => { server.close(); assert.strictEqual(res.headers['access-control-allow-origin'], '*'); }); post.write(''); post.end(); }); preflight.write(''); preflight.end(); }); }); ================================================ FILE: packages/server/test/index.js ================================================ 'use strict'; const test = require('eater/runner').test; const assert = require('assert'); const agreedServer = require('../'); const AssertStream = require('assert-stream'); const http = require('http'); test('agreed-server: call server', () => { const server = agreedServer({ path: './test/agreed', port: '0', }).createServer(); server.on('listening', () => { const port = server.address().port; http.get(`http://localhost:${port}/users/yosuke`, (res) => { server.close(); const assertStream = new AssertStream(); assertStream.expect({ message: 'hello yosuke' }); res.pipe(assertStream); }); }); }); test('agreed-server: static server', () => { const server = agreedServer({ path: './test/agreed', static: './test/static', staticPrefixPath: '/public', port: '0', }).createServer(); server.on('listening', () => { const port = server.address().port; http.get(`http://localhost:${port}/public/test.jpg`, (res) => { server.close(); assert.strictEqual(res.headers['content-type'], 'image/jpeg'); assert.strictEqual(res.statusCode, 200); }); }); }); test('agreed-server: static server without prefix', () => { const server = agreedServer({ path: './test/agreed', static: './test/static', port: '0', }).createServer(); server.on('listening', () => { const port = server.address().port; http.get(`http://localhost:${port}/test.jpg`, (res) => { server.close(); assert.strictEqual(res.headers['content-type'], 'image/jpeg'); assert.strictEqual(res.statusCode, 200); }); }); }); test('pass middlewares option', () => { const server = agreedServer({ path: './test/agreed', port: 0, middlewares: [ (req, res, next) => { res.set({"access-control-allow-origin": "*"}); next(); } ] }).createServer(); server.on('listening', () => { const port = server.address().port; http.get(`http://localhost:${port}/users/yosuke`, (res) => { server.close(); assert.deepEqual(res.headers["access-control-allow-origin"], "*"); }); }); }); test('pass defaultResponseHeaders option', () => { const server = agreedServer({ path: './test/agreed', port: 0, defaultResponseHeaders: { 'access-control-allow-origin': '*' } }).createServer(); server.on('listening', () => { const port = server.address().port; http.get(`http://localhost:${port}/users/yosuke`, (res) => { server.close(); assert.deepEqual(res.headers["access-control-allow-origin"], "*"); }); }); }); test('pass defaultRequestHeaders option', () => { const server = agreedServer({ path: './test/agreed', port: 0, defaultRequestHeaders: { 'x-jwt-token': 'testtesttest' } }).createServer(); server.on('listening', () => { const port = server.address().port; const options = { host: 'localhost', method: 'GET', path: '/users/header/yosuke', port: port, headers: { 'Content-Type': 'application/json', 'x-jwt-token': 'testtesttest' } }; http.get(options, (res) => { server.close(); const assertStream = new AssertStream(); assertStream.expect({ message: 'hello yosuke' }); res.pipe(assertStream); }); }); }); ================================================ FILE: packages/server/test/notify.js ================================================ 'use strict'; const test = require('eater/runner').test; const assert = require('assert'); const agreedServer = require('../'); const AssertStream = require('assert-stream'); const http = require('http'); const mustCall = require('must-call'); test('notify option', () => { const { app, createServer, notifier } = agreedServer({ port: 0, path: './test/agreedNotify.json5' }); const server = createServer(app); server.on('listening', () => { const port = server.address().port; const options = { host: 'localhost', method: 'GET', path: '/users/123', port: port, headers: { 'Content-Type': 'application/json', } }; http.get(options, (res) => { server.close(); const assertStream = new AssertStream(); assertStream.expect({ message: 'hello 123' }); res.pipe(assertStream); }); }); notifier.on('message', mustCall((data) => { assert.deepStrictEqual({ message: 'hello 123' }, data); })); }); ================================================ FILE: packages/server/test/proxy.js ================================================ 'use strict'; const test = require('eater/runner').test; const assert = require('assert'); const agreedServer = require('../'); const AssertStream = require('assert-stream'); const http = require('http'); test('proxy option', () => { const proxyServer = agreedServer({ path: './test/agreedProxy', port: 0, }).createServer(); proxyServer.on('listening', () => { const proxyPort = proxyServer.address().port; const server = agreedServer({ path: './test/agreed', port: 0, proxy: `127.0.0.1:${proxyPort}`, }).createServer(); server.on('listening', () => { const port = server.address().port; const options = { host: 'localhost', method: 'GET', path: '/test/proxy', port: port, }; http.get(options, (res) => { proxyServer.close(); server.close(); const assertStream = new AssertStream(); assertStream.expect({ message: 'hello proxy' }); res.pipe(assertStream); }); }); }); }); test('proxy prefix proxy path option', () => { const proxyServer = agreedServer({ path: './test/agreedProxy', port: 0, }).createServer(); proxyServer.on('listening', () => { const proxyPort = proxyServer.address().port; const server = agreedServer({ path: './test/agreed', port: 0, proxy: `127.0.0.1:${proxyPort}`, proxyPrefixPath: '/proxy' }).createServer(); server.on('listening', () => { const port = server.address().port; const options = { host: 'localhost', method: 'GET', path: '/proxy/test/proxy', port: port, }; http.get(options, (res) => { proxyServer.close(); server.close(); const assertStream = new AssertStream(); assertStream.expect({ message: 'hello proxy' }); res.pipe(assertStream); }); }); }); }); test('proxy prefix proxy path option with proxyMaybeSkipToNextHandler', () => { const proxyServer = agreedServer({ path: './test/agreedProxy', port: 0, }).createServer(); proxyServer.on('listening', () => { const proxyPort = proxyServer.address().port; const server = agreedServer({ path: './test/agreed', port: 0, proxy: `127.0.0.1:${proxyPort}`, proxyOpts: { filter: (req, res) => { return req.originalUrl === '/foo' }, maybeSkipToNextHandler: (proxyRes) => { return proxyRes.statusCode >= 400; }, } }).createServer(); server.on('listening', () => { const port = server.address().port; ['/foo', '/bar'].forEach((path) => { const options = { host: 'localhost', method: 'GET', path: path, port: port, }; http.get(options, (res) => { const assertStream = new AssertStream(); assertStream.expect({ message: 'hello foo' }); res.pipe(assertStream); }); }); setTimeout(() => { proxyServer.close(); server.close(); }, 2000); }); }); }); ================================================ FILE: packages/server/test/www_urlencoded.js ================================================ 'use strict'; const test = require('eater/runner').test; const assert = require('assert'); const agreedServer = require('../'); const AssertStream = require('assert-stream'); const http = require('http'); test('www_urlencoded option', () => { const server = agreedServer({ path: './test/agreed', port: 0, }).createServer(); server.on('listening', () => { const port = server.address().port; const options = { host: 'localhost', method: 'POST', path: '/urlencoded/test', port: port, headers: { 'content-type': 'application/x-www-form-urlencoded' }, }; const request = http.request(options, (res) => { server.close(); const assertStream = new AssertStream(); assertStream.expect({ message: 'hello wwwurlencoded' }); res.pipe(assertStream); }); request.write('foo=bar&bar=baz&hoge=fuga'); request.end(); }); }); ================================================ FILE: packages/typed/.circleci/config.yml ================================================ version: 2 jobs: build: docker: - image: circleci/node:17-browsers working_directory: ~/node steps: - checkout - restore_cache: key: v1-dependencies-{{ checksum "package.json"}} - run: name: Run yarn install command: yarn install - run: name: prepare greenkeeper-lockfile command: | echo 'export PATH=$(yarn global bin):$PATH' >> $BASH_ENV source $BASH_ENV yarn global add greenkeeper-lockfile@1 - save_cache: paths: - node_modules key: v1-dependencies-{{ checksum "package.json" }} - run: name: Run lint command: yarn lint - run: name: tsc command: yarn tsc - run: greenkeeper-lockfile-upload ================================================ FILE: packages/typed/.gitignore ================================================ # Created by https://www.gitignore.io/api/node ### Node ### # Logs logs *.log npm-debug.log* yarn-debug.log* yarn-error.log* # Runtime data pids *.pid *.seed *.pid.lock # Directory for instrumented libs generated by jscoverage/JSCover lib-cov # Coverage directory used by tools like istanbul coverage # nyc test coverage .nyc_output # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) .grunt # Bower dependency directory (https://bower.io/) bower_components # node-waf configuration .lock-wscript # Compiled binary addons (https://nodejs.org/api/addons.html) build/Release # Dependency directories node_modules/ jspm_packages/ # TypeScript v1 declaration files typings/ # Optional npm cache directory .npm # Optional eslint cache .eslintcache # Optional REPL history .node_repl_history # Output of 'npm pack' *.tgz # Yarn Integrity file .yarn-integrity # dotenv environment variables file .env # parcel-bundler cache (https://parceljs.org/) .cache # next.js build output .next # nuxt.js build output .nuxt # vuepress build output .vuepress/dist # Serverless directories .serverless # End of https://www.gitignore.io/api/node lib schema.json schema.yaml ================================================ FILE: packages/typed/.npmignore ================================================ # Created by https://www.gitignore.io/api/node ### Node ### # Logs logs *.log npm-debug.log* yarn-debug.log* yarn-error.log* .idea # Runtime data pids *.pid *.seed *.pid.lock # Directory for instrumented libs generated by jscoverage/JSCover lib-cov # Coverage directory used by tools like istanbul coverage # nyc test coverage .nyc_output # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) .grunt # Bower dependency directory (https://bower.io/) bower_components # node-waf configuration .lock-wscript # Compiled binary addons (https://nodejs.org/api/addons.html) build/Release # Dependency directories node_modules/ jspm_packages/ # TypeScript v1 declaration files typings/ # Optional npm cache directory .npm # Optional eslint cache .eslintcache # Optional REPL history .node_repl_history # Output of 'npm pack' *.tgz # Yarn Integrity file .yarn-integrity # dotenv environment variables file .env # parcel-bundler cache (https://parceljs.org/) .cache # next.js build output .next # nuxt.js build output .nuxt # vuepress build output .vuepress/dist # Serverless directories .serverless # End of https://www.gitignore.io/api/node ================================================ FILE: packages/typed/LICENSE ================================================ Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. ================================================ FILE: packages/typed/README.md ================================================ # agreed-typed [![Greenkeeper badge](https://badges.greenkeeper.io/akito0107/agreed-typed.svg)](https://greenkeeper.io/) - agreed api type definitions (types.ts) - agreed.ts to swagger generator ## install ```shell $ npm install -g agreed-typed ``` ## usage ```shell $ agreed-typed --help Usage: agreed-typed [subcommand] [options] Subcommands: gen-swagger Generate swagger file. Options: --help Shows the usage and exits. --version Shows version number and exits. Examples: agreed-typed gen-swagger --path ./agreed.ts ``` ### gen-swagger ```shell $ agreed-typed gen-swagger --help Usage: agreed-typed gen-swagger [options] Options: --path Agreed file path (required) --title swagger title --description swagger description --version document version --depth aggregate depth (default = 2) --dry-run dry-run mode (outputs on stdout) --output output filename (default schema.json) --host swagger host (default localhost:3030) --format file format [json|yaml] (default json) --help show help Examples: agreed-typed gen-swagger --path ./agreed.ts --output schema # output file = schema.json agreed-typed gen-swagger --path ./agreed.ts --output schema --format yaml # output file = schema.yaml ``` ## Annotations ### Validations http://json-schema.org/latest/json-schema-validation.html - multipleOf - maximum - exclusiveMaximum - minimum - exclusiveMinimum - maxLength - minLength - pattern - maxItems - minItems - uniqueItems - maxProperties - minProperties - additionalProperties - enum - type - examples - ignore - description - format - default - $ref - id ## License This project is licensed under the Apache License 2.0 License - see the [LICENSE](LICENSE) file for details ================================================ FILE: packages/typed/package.json ================================================ { "name": "@agreed/typed", "version": "6.0.2", "main": "./lib/types.js", "types": "./lib/types.d.ts", "license": "MIT", "repository": { "type": "git", "url": "git+https://github.com/recruit-tech/agreed.git" }, "author": "Akito Ito ", "scripts": { "test": "node --import tsx --test src/__tests__/**.ts src/**/__tests__/**.ts", "lint": "tslint ./src/**/*.ts", "lintfix": "tslint ./src/**/*.ts --fix", "tsc": "tsc", "prepublish": "npm run tsc" }, "bin": { "agreed-typed": "lib/bin/agreed-typed.js" }, "devDependencies": { "body-parser": "1.20.0", "cors": "2.8.5", "express": "4.18.2", "prettier": "2.8.4", "ts-node": "^10.9.2", "tslint": "6.1.3", "tslint-config-prettier": "1.18.0", "tslint-plugin-prettier": "2.3.0", "tsx": "^4.7.1", "typescript": "4.7.4" }, "dependencies": { "@agreed/core": "^6.0.0", "@types/estree": "1.0.0", "@types/minimist": "^1.2.0", "@types/node": "^18.0.0", "@typescript-eslint/types": "^7.2.0", "@typescript-eslint/typescript-estree": "^7.2.0", "doctrine": "^3.0.0", "json2yaml": "^1.1.0", "minimist": "^1.2.0", "typescript-json-schema": "^0.55.0" }, "publishConfig": { "access": "public" }, "description": "[![Greenkeeper badge](https://badges.greenkeeper.io/akito0107/agreed-typed.svg)](https://greenkeeper.io/)", "bugs": { "url": "https://github.com/recruit-tech/agreed/issues" }, "homepage": "https://github.com/recruit-tech/agreed#readme", "keywords": [ "typescript", "agreed" ] } ================================================ FILE: packages/typed/src/__tests__/data/agreed.ts ================================================ import { APIDef, Capture, convert, Error404, GET, ResponseDef, Success200 } from "../../types"; import * as getApis from "./agrees-get"; import * as getApi2 from "./agrees-get2"; import * as patchApis from "./agrees-patch"; import * as postApis from "./agrees-post"; import * as postApis2 from "./agrees-post2"; import * as putApis from "./agrees-put"; type HelloAPI = APIDef< GET, // HTTP Method ["user", Capture<":id">], // /user/:id any, // request header { q: string }, // request query undefined, // request body {}, // response header | ResponseDef | ResponseDef // response body and status >; type HelloResponseBody = { message: string }; const hellos: HelloAPI[] = [ { request: { path: ["user", ":id"], method: "GET", query: { q: "{:someQueryStrings}" }, body: undefined }, response: { status: 200, body: { message: "{:id} {:someQueryString}" } } }, { request: { path: ["user", "9999"], method: "GET", query: { q: "{:someQueryStrings}" }, body: undefined }, response: { status: 404, body: { error: "test" } } } ]; const agrees = [ hellos, getApis, getApi2, postApis, putApis, postApis2, patchApis ].map((a: any) => convert(...a)); module.exports = agrees.reduce((acc, val) => acc.concat(val), []); ================================================ FILE: packages/typed/src/__tests__/data/agrees-get.ts ================================================ import { APIDef, Capture, Error404, GET, Integer, ResponseDef, Success200 } from "../../types"; enum QueryEnum { A = 1, B } type Moo = "moo" | "mooo"; /** * Ping GET API * description area * may be acceptable `.md` syntax * @summary PING Get API */ export type PingAPI = APIDef< GET, // HTTP Method ["ping", Capture<":message">], // /ping/:message { apiKey: "x-api-key"; foo?: string }, // request header { q: string; qoo?: string; moo: Moo; query2: QueryEnum; arrayQuery?: Integer[]; }, // request query undefined, // request body { "x-token": "xxx" }, // response header ResponseDef | ResponseDef // response >; type PongBody = { message: string; }; type ErrorPongBody = { errorCode: string; message: string; }; const pingAPIs: PingAPI[] = [ { request: { path: ["ping", "test"], // /ping/test/hoge query: { q: "{:query}", moo: "moo", query2: 1 }, method: "GET", body: undefined }, response: { headers: { "x-token": "xxx" }, status: 200, body: { message: "test" } } }, { request: { path: ["ping", ":message"], // /ping/:message method: "GET", body: undefined }, response: { headers: { "x-token": "xxx" }, status: 200, body: { message: "ok {:message}" } } }, { request: { path: ["ping", "notfound"], // /ping/:message method: "GET", body: undefined }, response: { status: 404, body: { errorCode: "404", message: "invalid id" } } } ]; module.exports = pingAPIs; ================================================ FILE: packages/typed/src/__tests__/data/agrees-get2.ts ================================================ import { APIDef, Capture, GET, Integer, Placeholder, ResponseDef, Success200, Success201 } from "../../types"; export type UserAPI = APIDef< GET, ["user", Capture<":id", Integer>], {}, { q: string; q2?: Integer }, undefined, { "x-csrf-token": "csrf-token" }, | ResponseDef< Success200, { message: string; images: Placeholder; themes: Placeholder; } > | ResponseDef >; type theme = { name: string; }; type resp = { param?: Placeholder<{ name: string; }>; param2: Placeholder<{ nested: Placeholder; }>; }; const api: UserAPI[] = [ { request: { path: ["user", 123], method: "GET", query: { q: "{:someQueryStrings}" }, body: undefined, values: { id: "yosuke", someQueryStrings: "foo" } }, response: { status: 200, headers: { "x-csrf-token": "csrf-token" }, body: { message: "{:greeting} {:id} {:someQueryStrings}", images: "{:images}", themes: "{:themes}" }, values: { greeting: "hello", images: ["http://example.com/foo.jpg", "http://example.com/bar.jpg"], themes: { name: "green" } } } }, { request: { path: ["user", ":id"], method: "GET", query: { q: "{:someQueryStrings}" }, body: undefined, values: { id: "yosuke", someQueryStrings: "foo" } }, response: { status: 201, headers: { "x-csrf-token": "csrf-token" }, body: { param: "{:aaa}", param2: { nested: 123 } }, values: { aaa: "test" } } } ]; module.exports = api; ================================================ FILE: packages/typed/src/__tests__/data/agrees-patch.ts ================================================ import { APIDef, Capture, Error400, PATCH, Placeholder, ResponseDef, Success204 } from "../../types"; import { CreateRequestBody } from "./agrees-post"; /** * @summary patch example */ export type PatchAPI = APIDef< PATCH, // HTTP Method ["ping", Capture<":id">], // /ping/:message { apiKey: string }, // header { q: string }, // query Partial, // Http Request Body {}, | ResponseDef | ResponseDef< Error400, { code: Placeholder; message: string; } > >; const patchAPIs: PatchAPI[] = [ { request: { path: ["ping", "121"], // /ping/test headers: { apiKey: "{:apiKey}" }, method: "PATCH", body: { id: "{:id}", genderId: 2 }, values: { id: 123 } }, response: { status: 204 } }, { request: { path: ["ping", "999"], // /ping/test headers: { apiKey: "{:apiKey}" }, method: "PATCH", body: { email: "hoge@hoge.com{:apiKey}", genderId: 2 }, values: { error: "not a number" } }, response: { status: 400, body: { code: 123, message: "invalid parameter" } } } ]; module.exports = patchAPIs; ================================================ FILE: packages/typed/src/__tests__/data/agrees-post.ts ================================================ import { APIDef, Capture, Placeholder, POST, ResponseDef, Success201 } from "../../types"; export type CreateAPI = APIDef< POST, // HTTP Method ["ping", Capture<":message">], // /ping/:message { apiKey: string }, // header { q: string }, // query CreateRequestBody, // Http Request Body {}, ResponseDef >; enum GenderType { Male = 1, Famale, Other } export type CreateRequestBody = { /** * @pattern [A-Z]+ */ email: string; /** * @maximum 1000 * @minimum 0 */ id: Placeholder; genderId: GenderType; }; type CreateResponseBody = { message: string; }; const createAPIs: CreateAPI[] = [ { request: { path: ["ping", "test"], // /ping/test headers: { apiKey: "{:apiKey}" }, method: "POST", body: { email: "hoge@hoge.com{:apiKey}", id: "{:id}", genderId: 2 }, values: { id: 123 } }, response: { status: 201, body: { message: "test" } } } ]; module.exports = createAPIs; ================================================ FILE: packages/typed/src/__tests__/data/agrees-post2.ts ================================================ import { APIDef, Capture, Placeholder, POST, ResponseDef, Success201 } from "../../types"; import { SubBody } from "./types"; export type CreateAPI2 = APIDef< POST, // HTTP Method ["ping", Capture<":message">, "2"], // /ping/:message { apiKey: string }, // header { q: string }, // query CreateRequestBody, // Http Request Body {}, ResponseDef >; enum GenderType { Male = 1, Famale, Other } type CreateRequestBody = { /** * @pattern [A-Z]+ */ email: string; /** * @maximum 1000 * @minimum 0 */ id: number; genderId: GenderType; }; type CreateResponseBody2 = { // prettier-ignore messages: Array>; }; const createAPIs: CreateAPI2[] = [ { request: { path: ["ping", "test", "2"], // /ping/test headers: { apiKey: "{:apiKey}" }, method: "POST", body: { email: "hoge@hoge.com{:apiKey}", id: 123, genderId: 2 }, values: { id: 123 } }, response: { status: 201, body: { messages: ["{:value}"] } } } ]; module.exports = createAPIs; ================================================ FILE: packages/typed/src/__tests__/data/agrees-put.ts ================================================ import { APIDef, Capture, Error400, Placeholder, PUT, ResponseDef, Success204 } from "../../types"; /** * @summary put example */ export type UpdateAPI = APIDef< PUT, // HTTP Method ["ping", Capture<":id">], // /ping/:message { apiKey: string }, // header { q: string }, // query CreateRequestBody, // Http Request Body {}, | ResponseDef | ResponseDef< Error400, { code: Placeholder; message: string; } > >; enum GenderType { Male = 1, Famale, Other } type CreateRequestBody = { /** * @pattern [A-Z]+ */ email: string; /** * @maximum 1000 * @minimum 0 */ id: Placeholder; genderId: GenderType; }; const createAPIs: UpdateAPI[] = [ { request: { path: ["ping", "121"], // /ping/test headers: { apiKey: "{:apiKey}" }, method: "PUT", body: { email: "hoge@hoge.com{:apiKey}", id: "{:id}", genderId: 2 }, values: { id: 123 } }, response: { status: 204 } }, { request: { path: ["ping", "999"], // /ping/test headers: { apiKey: "{:apiKey}" }, method: "PUT", body: { email: "hoge@hoge.com{:apiKey}", id: "{:error}", genderId: 2 }, values: { error: "not a number" } }, response: { status: 400, body: { code: 123, message: "invalid parameter" } } } ]; module.exports = createAPIs; ================================================ FILE: packages/typed/src/__tests__/data/types.ts ================================================ import { Placeholder } from "../../types"; export type SubBody = { prop1: string; prop2: string; prop3: Placeholder<{ hoge: string; }>; }; ================================================ FILE: packages/typed/src/__tests__/server.ts ================================================ import Agreed from "@agreed/core"; import bodyParser from "body-parser"; import express from "express"; import path from "node:path"; import assert from "node:assert"; import test from "node:test"; const setupServer = (agreed, opts = {}) => { const app = express(); app.use(bodyParser.json()); app.use( agreed.middleware({ ...opts, path: path.resolve(__dirname, "./data/agreed.ts"), }) ); app.use((err, _, res, _next) => { res.status(500); res.send(`Error is occurred : ${err}`); }); return app; }; test("register ts agrees with get", (t, done) => { const app = setupServer(new Agreed()); const server = app.listen(0, async () => { try { const address = server?.address(); if (!address || typeof address === "string") { throw new Error("address is invalid"); } const port = address.port; const response = await fetch(`http://localhost:${port}/ping/hello`); assert.strictEqual(response.status, 200); const data = await response.json(); assert.deepStrictEqual(data, { message: "ok hello" }); server.close(done); } catch (e) { server.close(); done(e); } }); }); test("register ts agrees with get and query", (t, done) => { const app = setupServer(new Agreed()); const server = app.listen(0, async () => { try { const address = server?.address(); if (!address || typeof address === "string") { throw new Error("address is invalid"); } const port = address.port; const response = await fetch( `http://localhost:${port}/ping/test?moo=moo&q=q&query2=1` ); assert.strictEqual(response.status, 200); const data = await response.json(); assert.deepStrictEqual(data, { message: "test" }); server.close(done); } catch (e) { server.close(); done(e); } }); }); test("register ts agrees with post", (t, done) => { const app = setupServer(new Agreed()); const server = app.listen(0, async () => { try { const address = server?.address(); if (!address || typeof address === "string") { throw new Error("address is invalid"); } const port = address.port; const response = await fetch(`http://localhost:${port}/ping/test`, { method: "POST", body: JSON.stringify({ email: "hoge@hoge.comaaa", id: 123, genderId: 2, }), headers: { "Content-Type": "application/json", apiKey: "aaa", }, }); assert.strictEqual(response.status, 201); const data = await response.json(); assert.deepStrictEqual(data, { message: "test" }); server.close(done); } catch (e) { server.close(); done(e); } }); }); test("register ts agrees with post", (t, done) => { const app = setupServer(new Agreed()); const server = app.listen(0, async () => { try { const address = server?.address(); if (!address || typeof address === "string") { throw new Error("address is invalid"); } const port = address.port; const response = await fetch(`http://localhost:${port}/ping/test`, { method: "POST", body: JSON.stringify({ email: "hoge@hoge.comaaa", id: 123, genderId: 2, }), headers: { "Content-Type": "application/json", apiKey: "aaa", }, }); assert.strictEqual(response.status, 201); const data = await response.json(); assert.deepStrictEqual(data, { message: "test" }); server.close(done); } catch (e) { server.close(); done(e); } }); }); test("register ts agrees with get and query (when enable-prefer-query option is true)", (t, done) => { const app = setupServer(new Agreed(), { enablePreferQuery: true }); const server = app.listen(0, async () => { try { const address = server?.address(); if (!address || typeof address === "string") { throw new Error("address is invalid"); } const port = address.port; const response = await fetch( `http://localhost:${port}/ping/test?moo=moo&q=q&query2=1` ); // Agree Not Found when it comes to exact matching of path values const data = await response.json(); assert.deepStrictEqual(data, { message: "ok test" }); server.close(done); } catch (e) { server.close(); done(e); } }); }); ================================================ FILE: packages/typed/src/bin/agreed-typed.ts ================================================ #!/usr/bin/env node import * as minimist from "minimist"; import { generate } from "../commands/gen-swagger"; import "../hook"; import { showHelp } from "../util"; const help = ` Usage: agreed-typed [subcommand] [options] Subcommands: gen-swagger Generate swagger file. Options: --help Shows the usage and exits. --version Shows version number and exits. Examples: agreed-typed gen-swagger --path ./agreed.ts `.trim(); function main() { const argv = minimist(process.argv.slice(2), { stopEarly: true, string: ["help", "version"], }); const commands = { ["gen-swagger"]: generate, }; if (argv.help) { return showHelp(0, help); } if (argv.version) { const pack = require("../../package.json"); process.stdout.write(pack.version); return; } const subcommand = argv._.shift(); const fun = commands[subcommand]; if (!fun) { return showHelp(1, help); } fun(argv._); } main(); ================================================ FILE: packages/typed/src/commands/__tests__/gen-swagger.test.ts ================================================ import * as path from "path"; import "../../hook"; import { run } from "../gen-swagger"; import test from "node:test"; import assert from "node:assert"; test("e2e testing", () => { const agreedPath = path.resolve(__dirname, "../../__tests__/data/agreed.ts"); const swagger = run({ path: agreedPath, depth: 2, title: "testing", description: "test description", version: "test", host: "", disablePathNumber: false }); assert.strictEqual(swagger.info.title, "testing"); }); ================================================ FILE: packages/typed/src/commands/gen-swagger.ts ================================================ import * as doctrine from "doctrine"; import * as minimist from "minimist"; import * as path from "path"; import { generateSchema } from "../generate-schema"; import { generateSwagger } from "../generate-swagger"; import { showHelp } from "../util"; import { AST_NODE_TYPES, parse } from "@typescript-eslint/typescript-estree"; import { ExportNamedDeclaration, Identifier } from "estree"; import * as fs from "fs"; import * as YAML from "json2yaml"; import { Definition } from "typescript-json-schema"; const usage = ` Usage: agreed-typed gen-swagger [options] Options: --path Agreed file path (required) --title swagger title --minify minify json (only affects json default: false) --description swagger description --version document version --depth aggregate depth (default = 2) --dry-run dry-run mode (outputs on stdout) --output output filename (default schema.json) --host swagger host (default localhost:3030) --format file format (default json) --help show help --disable-path-number disable number type for path params (default: false) Examples: agreed-typed gen-swagger --path ./agreed.ts --output schema.json `.trim(); export function generate(arg) { const argv = minimist(arg, { string: [ "path", "title", "description", "version", "depth", "output", "host", "format", ], boolean: ["dry-run", "disable-path-number", "minify"], }); if (argv.help) { showHelp(0, usage); return; } if (!argv.path) { showHelp(1, usage); return; } const depth = argv.depth ? Number(argv.depth) : 2; const swagger = run({ path: argv.path, description: argv.description, version: argv.version, depth, title: argv.title, host: argv.host, disablePathNumber: argv["disable-path-number"], }); write(swagger, { dryRun: argv["dry-run"], format: argv.format, filename: argv.output, minify: argv.minify, }); } function write( obj, { dryRun, format, filename, minify } = { dryRun: true, format: "json", filename: "schema", minify: false, } ) { const output = format === "yaml" ? "# auto generated by agreed-typed\n" + YAML.stringify(obj) : minify ? JSON.stringify(obj) : JSON.stringify(obj, null, 4); if (dryRun) { process.stdout.write(output); return; } fs.writeFileSync( path.resolve(process.cwd(), `${filename || "schema"}.${format || "json"}`), output ); } // testing entry point export function run({ path: pt, depth, title, description, version, host, disablePathNumber, }) { const agreedPath = path.resolve(process.cwd(), pt); require(agreedPath); const currentModule = require.main.children.find( (m) => m.filename === __filename ); const agreedRoot = currentModule.children.find( (m) => m.filename === agreedPath ); const { mods, files } = aggregateModules(agreedRoot, depth); const metaInfos = mods.reduce((p, a) => { p = p.concat(...a.asts.map((m) => m.meta)); return p; }, []); const schemas = generateSchema(files, metaInfos); const defs = schemas .filter((s) => s.schema.definitions) .reduce((p, c) => { return { ...p, ...c.schema.definitions, }; }, {}); const specs = schemas.reduce((prev: ReducedSpec[], current) => { const exist = prev.find((p) => { return isSamePath(p.path, current.path); }); if (exist) { exist.schemas.push(current); return prev; } prev.push({ path: current.path, schemas: [current] }); return prev; }, []); return generateSwagger( specs, title, description, version, host, defs, disablePathNumber ); } export interface ReducedSpec { path: string[]; schemas: { name: string; path: string[]; doc: object; schema: Definition; }[]; } function aggregateModules(mod: NodeModule, lim = 2) { const files = []; const rec = (module: NodeModule, asts, depth, limit) => { if (depth >= limit || files.includes(module.filename)) { return asts; } if ( module.filename.endsWith(".ts") && !module.filename.includes("node_modules") ) { files.push(module.filename); const file = fs.readFileSync(module.filename, "utf-8"); const ast = parse(file, { comment: true, loc: true }); const docs = ast.comments .filter((c) => { return c.type === "Block"; }) .reduce((p, d) => { const comment = `/*${d.value}*/`; const docAST = doctrine.parse(comment, { unwrap: true }); p[d.loc.end.line] = { ast: docAST }; return p; }, {}); const mods = ast.body.reduce((prev, current) => { if (current.type !== AST_NODE_TYPES.ExportNamedDeclaration) { return prev; } const c: ExportNamedDeclaration = current as any; if ( (c.declaration.type as any) !== AST_NODE_TYPES.TSTypeAliasDeclaration ) { return prev; } const declaration = c.declaration as any; if ( declaration.typeAnnotation.type !== AST_NODE_TYPES.TSTypeReference ) { return prev; } const annotation = declaration.typeAnnotation; if ((annotation.typeName as Identifier).name === "APIDef") { const params: any = annotation.typeParameters; const pathArr = params.params[1].elementTypes.map((p) => { if (p.literal) { return p.literal.value; // string } return p.typeParameters.params[0].literal.value; }); const doc = docs[c.declaration.loc.start.line - 1]; prev.push({ meta: { name: declaration.id.name, path: pathArr, doc, }, ast: current, }); } return prev; }, []); if (mods.length > 0) { asts.push({ filename: module.filename, asts: mods }); } } module.children.forEach((m) => { rec(m, asts, depth + 1, limit); }); return asts; }; return { mods: rec(mod, [], 0, lim), files }; } function isSamePath(a: string[], b: string[]): boolean { if (a.length !== b.length) { return false; } let equal = true; a.forEach((r, i) => { const l = b[i]; if (r === l) { return; } if (r.startsWith(":") && l.startsWith(":")) { return; } equal = false; }); return equal; } ================================================ FILE: packages/typed/src/generate-schema.ts ================================================ import * as ts from "typescript"; import * as TJS from "typescript-json-schema"; export interface Spec { name: string; path: string[]; doc: object; schema: TJS.Definition; } export function generateSchema(fileNames, meta): Spec[] { const settings: TJS.PartialArgs = { required: true, ignoreErrors: true }; const compilerOptions: ts.CompilerOptions = { noEmit: true, noEmitOnError: false, emitDecoratorMetadata: true, experimentalDecorators: true, target: ts.ScriptTarget.ES5, module: ts.ModuleKind.CommonJS, allowUnusedLabels: true }; const host = ts.createCompilerHost(compilerOptions); const orgSourceFile = host.getSourceFile; host.getSourceFile = ( fileName: string, languageVersion: ts.ScriptTarget, onError?: (message: string) => void, shouldCreateNewSourceFile?: boolean ): ts.SourceFile | undefined => { const src = orgSourceFile( fileName, languageVersion, onError, shouldCreateNewSourceFile ); const result = ts.transform(src, [ transformPropertySignature, transformTypeReferenceNode, transformCaptureTypeArguments ]); result.dispose(); return result.transformed[0] as ts.SourceFile; }; host.getSourceFileByPath = undefined; const program = TJS.getProgramFromFiles(fileNames, compilerOptions); const generator = TJS.buildGenerator(program as unknown as TJS.Program, settings); return meta.map(m => { return { ...m, schema: generator.getSchemaForSymbol(m.name) }; }); } // PropertyとしてPlaceholderを使う場合 const transformPropertySignature = ( context: ts.TransformationContext ) => (rootNode: T) => { function visit(node: ts.Node): ts.Node { node = ts.visitEachChild(node, visit, context); if (!ts.isPropertySignature(node)) { return node; } const ps = node; if (!ps.type || !ts.isTypeReferenceNode(ps.type)) { return node; } const tr = ps.type; if ((tr.typeName as any).escapedText === "Placeholder") { return ts.factory.updatePropertySignature( ps, ps.modifiers, ps.name, ps.questionToken, tr.typeArguments[0] ); } return node; } return ts.visitNode(rootNode, visit); }; // 型パラメータにPlaceholderを使う場合 const transformTypeReferenceNode = ( context: ts.TransformationContext ) => (rootNode: T) => { function visit(node: ts.Node): ts.Node { node = ts.visitEachChild(node, visit, context); if (!ts.isTypeReferenceNode(node)) { return node; } const tr = node; if (!tr.typeArguments) { return tr; } const args = tr.typeArguments.map(ta => { if (!ts.isTypeReferenceNode(ta)) { return ta; } const tref = ta; if ((tref.typeName as any).escapedText === "Placeholder") { return tref.typeArguments[0]; } return tref; }) as any; return ts.factory.updateTypeReferenceNode(tr, tr.typeName, args); } return ts.visitNode(rootNode, visit); }; const transformCaptureTypeArguments = ( context: ts.TransformationContext ) => (rootNode: T) => { function visit(node: ts.Node): ts.Node { node = ts.visitEachChild(node, visit, context); if (!ts.isTupleTypeNode(node)) { return node; } const tp = node as ts.TupleTypeNode; const et: ts.NodeArray = tp.elements.map(e => { if (!ts.isTypeReferenceNode(e)) { return e; } if ( (e.typeName as ts.Identifier).escapedText === "Capture" && e.typeArguments.length === 2 ) { return e.typeArguments[1]; } return e; }) as any; return ts.factory.updateTupleTypeNode(tp, et); } return ts.visitNode(rootNode, visit); }; ================================================ FILE: packages/typed/src/generate-swagger.ts ================================================ import { ReducedSpec } from "./commands/gen-swagger"; export function generateSwagger( specs: ReducedSpec[], title = "Agreed", description = "Generate via agreed-typed", version = "0.0.1", host = "localhost:3000", definitions, disablePathNumber ) { const swagger = { swagger: "2.0", info: { title, description, version }, host, produces: ["application/json"], consumes: ["application/json"], paths: generatePath(specs, definitions, disablePathNumber), definitions }; return swagger; } function generatePath( specs: ReducedSpec[], definitions: any, disablePathNumber: boolean ) { const genpath = (schema: ReducedSpec) => { const pathParam = schema.path.reduce( (p, c, i) => { if (c.startsWith(":")) { const placeholder = c.slice(1); p.name += `/{${placeholder}}`; p.params.push({ param: placeholder, idx: i }); return p; } p.name += "/" + c; return p; }, { name: "", params: [] } ); const schemas = schema.schemas.reduce((p, c: any) => { const { query, method, headers, body, path } = c.schema.properties.request.properties; if (p[method.enum[0]]) { throw new Error("generator duplicated specs"); } let parameters = [ ...parsePathParam(pathParam.params, path, disablePathNumber), ...parseProperties(query, "query", definitions), ...parseProperties(headers, "header", definitions) ]; if (body && (body.properties || body.$ref)) { parameters = parameters.concat(parseBody(body)); } const responses = parseResponse(c.schema.properties.response); const doc: any = !c.doc ? {} : { description: c.doc.ast.description, ...c.doc.ast.tags.reduce((a, t) => { a[t.title] = t.description; return a; }, {}) }; p[method.enum[0].toLowerCase()] = { parameters, responses, ...doc }; return p; }, {}); return { [pathParam.name]: schemas }; }; return specs.reduce((p, c) => { return { ...p, ...genpath(c) }; }, {}); } function parseBody(body: object): object { return { in: "body", name: "request body", required: true, schema: body }; } function parseResponse(resp: any): object { const responses = resp.anyOf ? resp.anyOf.map(a => a.allOf) : [resp.allOf]; return responses.reduce((p, c) => { const headers = c.find(r => r.properties.headers); const statusCode = c.find(r => r.properties.status); const body = c.find(r => r.properties.body); const headerProps = headers ? headers.properties.headers.properties : {}; const h = Object.keys(headerProps).reduce((m, current) => { return { ...m, [current]: { description: current, type: headerProps[current].type } }; }, {}); const description = statusCode.properties.status.enum[0] > 399 ? "Failure" : "Success"; if (body.properties.body.type === "undefined") { return { ...p, [`${statusCode.properties.status.enum[0]}`]: { description, headers: h } }; } return { ...p, [`${statusCode.properties.status.enum[0]}`]: { description, headers: h, // headers ? parseProperties(headers, "header") : {}, schema: body.properties.body } }; }, {}); } function parsePathParam( paths: Array<{ param: string; idx: number }>, path: any, disablePathNumber: boolean ): object[] { return paths.map(p => { const param = path.items[p.idx]; // workaround if (disablePathNumber && param.type === "number") { param.type = "integer"; } return { in: "path", name: p.param, required: true, ...param }; }); } function parseProperties(query, inname, definitions): object[] { const { properties } = query; return Object.keys(properties).map(k => { if (properties[k].$ref) { const defName = `${properties[k].$ref}`.split("/").slice(-1)[0]; const def = definitions[defName]; return { in: inname, name: k, ...def }; } if (properties[k].enum) { return { in: inname, required: query.required ? query.required.includes(k) : false, type: properties[k].type, name: k, enum: properties[k].enum, default: properties[k].enum[0] }; } return { in: inname, required: query.required ? query.required.includes(k) : false, type: properties[k].type, name: k, format: properties[k].format, ...properties[k] }; }); } ================================================ FILE: packages/typed/src/hook.ts ================================================ import * as fs from "fs"; import * as ts from "typescript"; const transpile = (src, options = {}) => { const res = ts.transpileModule( src, options || { compilerOptions: { module: ts.ModuleKind.CommonJS } } ).outputText; return res; }; require.extensions[".ts"] = (module: any, file) => { const src = fs.readFileSync(file).toString("utf-8"); const agree = transpile(src, {}); module._compile(agree, file); }; ================================================ FILE: packages/typed/src/types.ts ================================================ export type GET = "GET"; export type HEAD = "HEAD"; export type POST = "POST"; export type PUT = "PUT"; export type PATCH = "PATCH"; export type DELETE = "DELETE"; export type HTTPMethods = GET | HEAD | POST | PATCH | PUT | DELETE; export type Capture = P extends string ? (T | string) : (P | string); export type Placeholder = T | string; export type Path = Array>; export type RequestBody = Method extends | POST | PATCH | PUT ? object : undefined; export type Headers = object; export type Query = object; export type RequestDef< P extends Path, H extends Headers, Q extends Query, M extends HTTPMethods, B extends RequestBody > = { path: P; method: M; headers?: H; query?: Q; values?: object; body: B; }; export type Status = { status: C; statusText?: T; }; export type Success200 = Status<200, "OK">; export type Success201 = Status<201, "Created">; export type Success202 = Status<202, "Accepted">; export type Success203 = Status<203, "Non-Authoritative Information">; export type Success204 = Status<204, "No Content">; export type Success205 = Status<205, "Reset Content">; export type Success206 = Status<206, "Parial Content">; export type Success207 = Status<207, "Multi-Status">; export type Success208 = Status<208, "Already Reported">; export type Success226 = Status<226, "IM Used">; export type Redirection300 = Status<300, "Multiple Choices">; export type Redirection301 = Status<301, "Moved Permanently">; export type Redirection302 = Status<302, "Found">; export type Redirection303 = Status<303, "See Other">; export type Redirection304 = Status<304, "Not Modified">; export type Redirection305 = Status<305, "Use Proxy">; export type Redirection307 = Status<307, "Temporary Redirect">; export type Redirection308 = Status<308, "Permanent Redirect">; // 40x client error export type Error400 = Status<400, "Bad Request">; export type Error401 = Status<401, "Unauthorized">; export type Error402 = Status<402, "Payment Required">; export type Error403 = Status<403, "Forbidden">; export type Error404 = Status<404, "Not Found">; export type Error405 = Status<405, "Method Not Allowed">; export type Error406 = Status<406, "Not Acceptable">; export type Error407 = Status<407, "Proxy Authentication Required">; export type Error408 = Status<408, "Request Timeout">; export type Error409 = Status<409, "Conflict">; export type Error410 = Status<410, "Gone">; export type Error411 = Status<411, "Length Required">; export type Error412 = Status<412, "Precondition Failed">; export type Error413 = Status<413, "Payload Too Large">; export type Error414 = Status<414, "URI Too Long">; export type Error415 = Status<415, "Unsupported Media Type">; export type Error416 = Status<416, "Range Not Satisfiable">; export type Error417 = Status<417, "Expectation Failed">; export type Error418 = Status<418, "I'm teapot">; export type Error421 = Status<421, "Misdirected Request">; export type Error422 = Status<422, "Unprocessable Entity">; export type Error423 = Status<423, "Locked">; export type Error424 = Status<424, "Failed Dependency">; export type Error425 = Status<425, "Too Early">; export type Error426 = Status<426, "Upgrade Required">; export type Error451 = Status<451, "Unavailable For Legal Reasons">; // 50x server error export type Error500 = Status<500, "Internal Server Error">; export type Error501 = Status<501, "Not Implemented">; export type Error502 = Status<502, "Bad Gateway">; export type Error503 = Status<503, "Service Unavailable">; export type Error504 = Status<504, "Gateway Timeout">; export type Error505 = Status<505, "HTTP version Not Supported">; export type Error506 = Status<506, "Variant Also Negotiates">; export type Error507 = Status<507, "Insufficient Storage">; export type Error508 = Status<508, "Loop Detected">; export type Error509 = Status<509, "Bandwidth Limit Exceeded">; export type Error510 = Status<510, "Not Extended">; export type Error511 = Status<511, "Network Authentication Required">; export type ResponseBody = object; export type ResponseDef< S extends Status, B extends ResponseBody > = { body?: S extends Success204 ? undefined : B; } & S; export type APIDef< M extends HTTPMethods, P extends Path, ReqHeader extends Headers, Q extends Query, ReqBody extends RequestBody, RespHeader extends Headers, Resp extends ResponseDef, object> > = { title?: string; description?: string; request: RequestDef; response: { headers?: RespHeader; values?: object; } & Resp; }; export function convert(...apis: Array<{ request }>) { return apis.map(a => { const { path } = a.request; if (typeof path === "string") { return a; } a.request.path = "/" + path.join("/"); return a; }); } /** * @TJS-type integer */ export type Integer = number; /** * @TJS-type integer * @minimum 0 * @maximum 4294967295 */ export type UInt32 = number; /** * @TJS-type integer * @minimum 0 * @maximum 18446744073709551615 */ export type UInt64 = number; /** * internal API, only for go-swagger * @param obj */ export function replace(obj) { if (typeof obj !== "object") { return obj; } if (obj == null) { return true; } if (obj.properties) { const required = obj.required || []; const properties = obj.properties || {}; obj.properties = Object.keys(properties).reduce( (p, prop) => { const currentProp = required.includes(prop) ? { ...p[prop] } : { ...p[prop], "x-nullable": true }; return { ...p, [prop]: currentProp }; }, { ...properties } ); } return Array.isArray(obj) ? obj.map(replace) : Object.keys(obj).reduce((p, c) => { p[c] = replace(obj[c]); return p; }, {}); } ================================================ FILE: packages/typed/src/util.ts ================================================ export function showHelp(exitcode, help) { process.stdout.write(help); process.exit(exitcode); } export const flatten = (arr1: any[]) => arr1.reduce((acc, val) => acc.concat(val), []); ================================================ FILE: packages/typed/tsconfig.json ================================================ { "compileOnSave": false, "compilerOptions": { "target": "es2015", "module": "commonjs", "declaration": true, "jsx": "preserve", "moduleResolution": "node", "allowSyntheticDefaultImports": true, "removeComments": false, "preserveConstEnums": true, "sourceMap": true, "skipLibCheck": true, "baseUrl": ".", "lib": [ "dom", "es2016" ], "emitDecoratorMetadata": true, "experimentalDecorators": true, "outDir": "lib" }, "include": ["src/**/*"], "exclude": [ "node_modules", "src/__tests__/**", "src/commands/__tests__/**" ] } ================================================ FILE: packages/typed/tslint.json ================================================ { "extends": [ "tslint:latest", "tslint-config-prettier", "tslint-plugin-prettier" ], "rules": { "prettier": true, "no-submodule-imports": false, "interface-over-type-literal": false, "object-literal-sort-keys": false, "interface-name": [false, "always-prefix"], "no-implicit-dependencies": [false, "dev"], "no-bitwise": false } } ================================================ FILE: packages/ui/.babelrc ================================================ { "presets": [ "@babel/preset-react" ] } ================================================ FILE: packages/ui/.eslintrc.json ================================================ { "env": { "browser": true, "commonjs": true, "es6": true, "node": true, "jest": true }, "parser": "@babel/eslint-parser", "parserOptions": { "ecmaVersion": 2020, "ecmaFeatures": { "jsx": true }, "babelOptions": { "presets": ["@babel/preset-react"] }, "requireConfigFile": false, "sourceType": "module" }, "settings": { "react": { "version": "detect" } }, "extends": [ "eslint:recommended", "plugin:react/recommended", "prettier" ], "plugins": [ "react", "prettier" ], "rules": { "linebreak-style": [ "error", "unix" ], "quotes": [ "error", "single" ], "semi": [ "error", "never" ], "prettier/prettier": "error" } } ================================================ FILE: packages/ui/.gitignore ================================================ # See https://help.github.com/ignore-files/ for more about ignoring files. # dependencies /node_modules # testing /coverage # production /build/service-worker.js /build/asset-manifest.json # misc .DS_Store .env.local .env.development.local .env.test.local .env.production.local npm-debug.log* yarn-debug.log* yarn-error.log* ================================================ FILE: packages/ui/.prettierrc.yml ================================================ --- printWidth: 100 tabWidth: 2 singleQuote: true trailingComma: none jsxBracketSameLine: true semi: false ================================================ FILE: packages/ui/LICENSE ================================================ The MIT License Copyright © 2018 Recruit Technologies Co.,Ltd. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: packages/ui/README.md ================================================ # Agreed UI UI for [Agreed](https://www.npmjs.com/package/agreed-core) ![ScreenShot](https://raw.githubusercontent.com/recruit-tech/agreed-ui/master/screenshot.png) # Install ``` $ npm install agreed-ui --save-dev ``` # Usage ``` $ agreed-ui --path ./test/agreed.json --port 3000 ``` Serve with [Express](https://www.npmjs.com/package/express) Open http://localhost:3000 to view it in the browser. ``` $ agreed-ui build --path ./test/agreed.json --dest ./build ``` Builds the app for static-hosting to the build folder # Features ## Set title and description to contract ``` { title: 'get store information', description: 'get store information', request: { ... }, response: { ... } } ``` title and descripion will be displayed at naviation and each section's title # Development `npm run start:dev -- --path=./test/agrees/agrees.js ` ================================================ FILE: packages/ui/bin/agreed-ui.js ================================================ #!/usr/bin/env node const path = require('path') const spawn = require('child_process').spawn const minimist = require('minimist') const colo = require('colo') const execArgv = minimist(process.execArgv) const argv = minimist(process.argv.slice(2)) function showHelp(exitcode) { console.log(` agreed-ui [--path agreed path file (required)][--port request server port default 3000] agreed-ui build [--path agreed path file (required)][--dest output directory(required)] agreed-ui --path ./agreed.js --port 4000 agreed-ui build --path ./agreed.js --dest ./build `) process.exit(exitcode) } const command = argv['_'][0] || 'start' if (argv.help || command === 'help') { showHelp(0) } if (argv.version || command === 'version') { const pack = require('../package.json') console.log(pack.version) process.exit(0) } if (!argv.path) { console.error(colo.red('[agreed-ui]: --path option is required')) showHelp(1) } if (command === 'build' && !argv.dest) { console.error(colo.red('[agreed-ui]: --dest option is required')) showHelp(1) } const npm = /^win/.test(process.platform) ? 'npm.cmd' : 'npm' const child = spawn( npm, [ 'run', command, '--', `--path=${path.resolve(process.cwd(), argv.path)}`, argv.dest && `--dest=${path.resolve(process.cwd(), argv.dest)}`, argv.port && `--port=${argv.port || 3000}`, ].filter(Boolean), { cwd: path.resolve(__dirname, '../') } ) child.stdout.on('data', function(data) { process.stdout.write(data) }) child.stderr.on('data', function(data) { process.stdout.write(data) }) ================================================ FILE: packages/ui/build/index.html ================================================ Agreed UI
================================================ FILE: packages/ui/build/precache-manifest.c7cb397614ac16fe18525a8ed4105b10.js ================================================ self.__precacheManifest = (self.__precacheManifest || []).concat([ { "revision": "66cb8cbf26c628a82a88f884b999224e", "url": "./index.html" }, { "revision": "b6c3ee666303b94886fc", "url": "./static/css/main.e92ec541.chunk.css" }, { "revision": "400027482ca9a973d5b8", "url": "./static/js/2.b28c1fbe.chunk.js" }, { "revision": "b6c3ee666303b94886fc", "url": "./static/js/main.ef43f0f5.chunk.js" }, { "revision": "f2bf705babb2ec4477fc", "url": "./static/js/runtime-main.3336eaa2.js" } ]); ================================================ FILE: packages/ui/build/static/css/main.e92ec541.chunk.css ================================================ @import url(https://fonts.googleapis.com/css?family=Open+Sans:400,400i|PT+Sans);:root{--color:#1a281f;--color-link:rgba(26,40,31,0.7);--color0:#fff;--color1:rgba(26,40,31,0.6);--color2:#635255;--color3:#ce7b91;--color4:#c0e8f9;--color5:#a4bfbd;--color-green:#66bb6a}*{margin:0;padding:0}a{color:rgba(26,40,31,.7);color:var(--color-link)}a:hover{color:#bbb}h1{font-size:1.5rem;margin:2rem 0}h2{font-size:1.2rem;margin:2rem 0 1rem}body{color:#1a281f;color:var(--color);font-family:-apple-system,BlinkMacSystemFont,Helvetica Neue,Segoe UI,sans-serif;min-height:100vh}.wrap,body{display:flex;flex-direction:column}.wrap{align-items:stretch;height:100vh}header{align-items:center;background:#fff;box-shadow:0 0 2.25rem #9da5ab;display:flex;font-family:PT Sans,sans-serif;justify-content:space-between;padding:20px;position:relative;z-index:1}header h1{margin:0}header p{font-weight:700}.container{align-items:stretch;display:flex;flex:1 1}main{flex-grow:3;order:2;padding:20px 0}aside,main{overflow:auto;position:relative}aside{box-shadow:0 0 .5rem #9da5ab;font-weight:300;flex:1 1;order:1;padding:20px;min-width:300px}.search{margin:1rem 0}.search__input{border:none;border-bottom:2px solid #ddd;font-size:1rem;font-weight:300;padding:.5rem 0;-webkit-transition:border-color .1s ease-out;transition:border-color .1s ease-out;width:80%}.search__input:focus{outline:0;display:block;border-bottom:2px solid var(--color5)}.search__group{display:block;font-size:.9rem;margin-top:5px}nav{position:absolute;padding-bottom:20px}nav p{margin:4px 0}details{padding-bottom:7px}summary{color:var(--color5);cursor:pointer;font-size:.9rem;margin-bottom:4px;outline:none}details>p{padding-left:1em;margin-top:3px}details summary::-webkit-details-marker{-webkit-transform:translateY(1px) scale(.7);transform:translateY(1px) scale(.7);opacity:.5}.count{border-radius:8px;border:1px solid var(--color5);background:var(--color5);margin-left:5px;opacity:.6;padding:0 3px;position:relative;top:-1px}.count,.statusLabel{color:var(--color0);display:inline-block;font-size:.5rem}.statusLabel{border-radius:6px;padding:2px 4px;margin-right:.25rem;opacity:.8}.statusLabel--2{background:var(--color-green)}.statusLabel--3{background:var(--color4)}.statusLabel--4{background:var(--color3)}.statusLabel--5{background:var(--color5)}.method{margin-right:5px;font-size:12px;font-weight:300;font-style:normal;border:1px solid #999;color:#999;border-radius:2px;padding:2px 3px;display:inline-block}.method.get{color:#66bb6a;border-color:#66bb6a}.method.delete{color:#999;border-color:#999}.method.put{color:#9e9d24;border-color:#9e9d24}.method.post{color:#f9a825;border-color:#f9a825}.method.patch{color:#9c27b0;border-color:#9c27b0}.definitions{color:var(--color1);margin:1.5em 0}.definitions h1{font-size:1em;color:var(--color5);margin-top:0;margin-bottom:1em;padding-left:0}.definitions>dl,.definitions>p{margin:.25em 1em}.definitions dl dt{color:var(--color3);font-weight:700;font-style:italic}.definitions dl dt:after{content:": "}.definitions dl>*{display:inline-block;margin-right:.5em}button{background-color:initial;border:none;cursor:pointer;outline:none;padding:0;-webkit-appearance:none;-moz-appearance:none;appearance:none}.body{color:var(--color1);padding:0}.body>.buttonGroup{margin-bottom:1em;padding:0 30px}.body>.code{background:#efefef;font-weight:300;font-size:.9rem;max-height:500px;margin:1em 0;overflow:auto;padding:2em}.body>.code ul{background:#efefef!important}.buttonGroup button{margin-right:1em}.tabButton{box-sizing:border-box;color:var(--color5);opacity:.4;font-size:1rem;font-weight:700;padding:0 1px 3px}.tabButton:hover{opacity:1}.tabButton:disabled{color:var(--color5);cursor:auto;opacity:1;border-bottom:2px solid var(--color5)}.onlyButton.tabButton:disabled{border:none}.contents>.agree{padding-top:2rem;margin-bottom:2rem}.contents>.agree:first-child{padding-top:0}.agree .definitions,.agree .description,.agree>h1,.agree>h2,.agree>section{padding:0 30px}.agree h2{color:var(--color);border-left:2px solid #aaa;padding-left:28px}.title{align-items:center;border-left:2px solid var(--color);display:flex;font-family:Open Sans,sans-serif;font-style:italic;margin:0}.title>*{margin-right:.6rem}.description{color:var(--color1);margin:.25rem 0}.contents section h2{color:var(--color);border-left:2px solid #aaa;padding-left:28px}.contents{position:absolute;width:100%} /*# sourceMappingURL=main.e92ec541.chunk.css.map */ ================================================ FILE: packages/ui/build/static/js/2.b28c1fbe.chunk.js ================================================ (this["webpackJsonp@agreed/ui"]=this["webpackJsonp@agreed/ui"]||[]).push([[2],[function(e,t,n){"use strict";e.exports=n(87)},function(e,t){var n=e.exports={version:"2.6.3"};"number"==typeof __e&&(__e=n)},function(e,t,n){"use strict";var r=n(54),a=n(93),o=Object.prototype.toString;function i(e){return"[object Array]"===o.call(e)}function u(e){return null!==e&&"object"===typeof e}function l(e){return"[object Function]"===o.call(e)}function s(e,t){if(null!==e&&"undefined"!==typeof e)if("object"!==typeof e&&(e=[e]),i(e))for(var n=0,r=e.length;n=0||Object.prototype.hasOwnProperty.call(e,r)&&(n[r]=e[r]);return n}},function(e,t){e.exports={}},function(e,t,n){var r=n(68),a=n(41);e.exports=Object.keys||function(e){return r(e,a)}},function(e,t,n){"use strict";function r(e,t){return function(e){if(Array.isArray(e))return e}(e)||function(e,t){if(Symbol.iterator in Object(e)||"[object Arguments]"===Object.prototype.toString.call(e)){var n=[],r=!0,a=!1,o=void 0;try{for(var i,u=e[Symbol.iterator]();!(r=(i=u.next()).done)&&(n.push(i.value),!t||n.length!==t);r=!0);}catch(l){a=!0,o=l}finally{try{r||null==u.return||u.return()}finally{if(a)throw o}}return n}}(e,t)||function(){throw new TypeError("Invalid attempt to destructure non-iterable instance")}()}n.d(t,"a",(function(){return r}))},function(e,t){e.exports=!0},function(e,t){e.exports=function(e,t){return{enumerable:!(1&e),configurable:!(2&e),writable:!(4&e),value:t}}},function(e,t){var n=0,r=Math.random();e.exports=function(e){return"Symbol(".concat(void 0===e?"":e,")_",(++n+r).toString(36))}},function(e,t){t.f={}.propertyIsEnumerable},function(e,t,n){e.exports={default:n(146),__esModule:!0}},function(e,t,n){"use strict";function r(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}n.d(t,"a",(function(){return r}))},function(e,t,n){"use strict";function r(e,t){for(var n=0;n=t.length?{value:void 0,done:!0}:(e=r(t,n),this._i+=e.length,{value:e,done:!1})}))},function(e,t){var n=Math.ceil,r=Math.floor;e.exports=function(e){return isNaN(e=+e)?0:(e>0?r:n)(e)}},function(e,t){e.exports=function(e){if(void 0==e)throw TypeError("Can't call method on "+e);return e}},function(e,t,n){var r=n(8);e.exports=function(e,t){if(!r(e))return e;var n,a;if(t&&"function"==typeof(n=e.toString)&&!r(a=n.call(e)))return a;if("function"==typeof(n=e.valueOf)&&!r(a=n.call(e)))return a;if(!t&&"function"==typeof(n=e.toString)&&!r(a=n.call(e)))return a;throw TypeError("Can't convert object to primitive value")}},function(e,t,n){var r=n(12),a=n(117),o=n(41),i=n(39)("IE_PROTO"),u=function(){},l=function(){var e,t=n(66)("iframe"),r=o.length;for(t.style.display="none",n(121).appendChild(t),t.src="javascript:",(e=t.contentWindow.document).open(),e.write(" ================================================ FILE: packages/ui/renovate.json ================================================ { "extends": [ "config:base" ] } ================================================ FILE: packages/ui/scripts/build.js ================================================ const fs = require('fs') const fse = require('fs-extra') const path = require('path') const minimist = require('minimist') const serialize = require('serialize-javascript') const template = require('lodash.template') const argv = minimist(process.argv.slice(2)) const getAgreements = require('../server/lib/getAgreements') const agreesPath = argv.path const agrees = getAgreements({ path: agreesPath }) const serialized = serialize(agrees) const srcPath = path.resolve(__dirname, '../build/index.html') const destPath = path.resolve(argv.dest, 'index.html') const html = fs.readFileSync(srcPath, 'utf8') fse.copySync(path.dirname(srcPath), path.dirname(destPath)) fs.writeFileSync( destPath, template(html, { interpolate: /"<%=([\s\S]+?)%>"/g })({ agrees: serialized, title: argv.title, }), ) ================================================ FILE: packages/ui/server/index.js ================================================ const fs = require('fs') const path = require('path') const minimist = require('minimist') const serialize = require('serialize-javascript') const express = require('express') const template = require('lodash.template') const argv = minimist(process.argv.slice(2)) const getAgreements = require('./lib/getAgreements') const port = parseInt(argv.port, 10) || 3000 const agreesPath = argv.path const app = express() app.get('/agrees', (req, res) => { const agrees = getAgreements({ path: agreesPath }) res.send(agrees) }) app.get('/', (req, res) => { const agrees = getAgreements({ path: agreesPath }) const serialized = serialize(agrees) const srcPath = path.resolve(__dirname, '../build/index.html') const html = fs.readFileSync(srcPath, 'utf8') res.send( template(html, { interpolate: /"<%=([\s\S]+?)%>"/g })({ agrees: serialized, title: argv.title }) ) }) app.use(express.static(path.resolve(__dirname, '../build'))) app.listen(port, (err) => { if (err) throw err console.log(`> Ready on http://localhost:${port}`) // eslint-disable-line }) ================================================ FILE: packages/ui/server/lib/getAgreements.js ================================================ 'use strict' const path = require('path') const register = require('@agreed/core/lib/register') const completion = require('@agreed/core/lib/check/completion') const requireUncached = require('@agreed/core/lib/require_hook/requireUncached') const format = require('@agreed/core/lib/template/format') const { parseSchema } = require('json-schema-to-flow-type') module.exports = function (options) { console.log(options) const agreesPath = path.resolve(options.path) const base = path.dirname(agreesPath) register() const agrees = [].concat(requireUncached(agreesPath)) return agrees .map((agree) => completion(agree, base)) .map((agree) => { agree.request.headers_formatted = format(agree.request.headers, agree.request.values) agree.request.formatted = format(agree.request.body, agree.request.values) agree.response.formatted = format(agree.response.body, agree.response.values) if (agree.response.schema) { agree.response.flowtype = parseSchema(agree.response.schema) } return agree }) } ================================================ FILE: packages/ui/src/components/Agree/index.js ================================================ import React from 'react' import PropTypes from 'prop-types' import Request from '../Request' import Response from '../Response' import MethodLabel from '../MethodLabel' import './styles.css' const Agree = ({ agree }) => { const path = agree.request.path const status = agree.response.status return (

{agree.title || path}

{agree.description || 'no description.'}

Request

Response

) } Agree.propTypes = { agree: PropTypes.object.isRequired } export default Agree ================================================ FILE: packages/ui/src/components/Agree/styles.css ================================================ .contents > .agree { padding-top: 2rem; margin-bottom: 2rem; } .contents > .agree:first-child { padding-top: 0; } .agree > h1, .agree > h2, .agree > section, .agree .description, .agree .definitions { padding: 0 30px; } .agree h2 { color: var(--color); border-left: 2px #aaa solid; padding-left: 28px; } .title { align-items: center; border-left: 2px var(--color) solid; display: flex; font-family: 'Open Sans', sans-serif; font-style: italic; margin: 0 0 0 0; } .title > * { margin-right: 0.6rem; } .description { color: var(--color1); margin: 0.25rem 0; } ================================================ FILE: packages/ui/src/components/Agrees/index.js ================================================ import React from 'react' import PropTypes from 'prop-types' import Agree from '../Agree' import './styles.css' function Agrees({ agrees }) { return (
{agrees.map((agree) => ( ))}
) } Agrees.propTypes = { agrees: PropTypes.array.isRequired } export default Agrees ================================================ FILE: packages/ui/src/components/Agrees/styles.css ================================================ .contents section h2 { color: var(--color); border-left: 2px #aaa solid; padding-left: 28px; } .contents { position: absolute; width: 100%; } ================================================ FILE: packages/ui/src/components/App/App.test.js ================================================ import React from 'react' import ReactDOM from 'react-dom' import App from './App' it('renders without crashing', () => { const div = document.createElement('div') ReactDOM.render(, div) ReactDOM.unmountComponentAtNode(div) }) ================================================ FILE: packages/ui/src/components/App/index.js ================================================ import React, { Component } from 'react' import axios from 'axios' import './styles.css' import Navigation from '../Navigation' import Agrees from '../Agrees' const titlePlaceHolder = '"<%= title %>"' const filterAgrees = (search, agrees) => { if (!search) return null const check = shoudDisplay(search) return agrees.filter( (agree) => check(agree.title) || check(agree.request.path) || check(agree.request.method) ) } const shoudDisplay = (search) => (value) => value && value.indexOf(search) > -1 const insertId = (agrees) => agrees.map((agree, i) => ({ ...agree, id: `agree_${i}` })) class App extends Component { constructor(props) { super(props) this.state = { agrees: null, search: '', grouped: false, agreesFiltered: null, title: '' } } defaultTitle = 'Agreed UI' componentDidMount() { const title = window.TITLE === titlePlaceHolder ? '' : window.TITLE || '' const grouped = localStorage.getItem('grouped') === 'true' || this.state.grouped if (title) document.title = title if (Array.isArray(window.AGREES)) { return this.setState({ title, grouped, agrees: insertId(window.AGREES) }) } axios .get('agrees') .then(({ data }) => this.setState({ title, grouped, agrees: insertId(data) })) } onSearchTextChange(value) { this.setState({ search: value, agreesFiltered: filterAgrees(value, this.state.agrees) }) } onFilterChange(value) { localStorage.setItem('grouped', value) this.setState({ grouped: value }) } render() { const { agrees, agreesFiltered, title, search, grouped } = this.state if (!agrees) return null return (

{title || this.defaultTitle}

{title &&

{this.defaultTitle}

}
) } } export default App ================================================ FILE: packages/ui/src/components/App/styles.css ================================================ .wrap { align-items: stretch; display: flex; flex-direction: column; height: 100vh; } header { align-items: center; background: white; box-shadow: 0 0 2.25rem #9da5ab; display: flex; font-family: 'PT Sans', sans-serif; justify-content: space-between; padding: 20px; position: relative; z-index: 1; } header h1 { margin: 0; } header p { font-weight: bold; } .container { align-items: stretch; display: flex; flex: 1; } main { flex-grow: 3; order: 2; padding: 20px 0; overflow: auto; position: relative; } aside { box-shadow: 0 0 0.5rem #9da5ab; font-weight: 300; flex: 1; order: 1; overflow: auto; padding: 20px; position: relative; min-width: 300px; } .search { margin: 1rem 0; } .search__input { border: none; border-bottom: 2px solid #ddd; font-size: 1rem; font-weight: 300; padding: 0.5rem 0; transition: border-color 0.1s ease-out; width: 80%; } .search__input:focus { outline: 0; display: block; border-bottom: 2px solid var(--color5); } .search__group { display: block; font-size: 0.9rem; margin-top: 5px; } ================================================ FILE: packages/ui/src/components/Body/index.js ================================================ import React, { Component } from 'react' import PropTypes from 'prop-types' import { JSONTree } from 'react-json-tree' import classNames from 'classnames' import './styles.css' class Body extends Component { constructor(props) { super(props) this.state = { selected: 'body' } } onClick(selected) { this.setState({ selected }) } render({ formatted, schema, flowtype } = this.props) { const { selected } = this.state return (
{schema && ( )} {flowtype && ( )}
{selected === 'schema' && (
level < 2} />
)} {selected === 'flowtype' &&
{flowtype}
} {selected === 'body' && (
{formatted instanceof Object ? ( level < 2} /> ) : ( formatted )}
)}
) } } Body.propTypes = { formatted: PropTypes.any.isRequired, schema: PropTypes.object, flowtype: PropTypes.any } export default Body ================================================ FILE: packages/ui/src/components/Body/styles.css ================================================ button { background-color: transparent; border: none; cursor: pointer; outline: none; padding: 0; appearance: none; } .body { color: var(--color1); padding: 0; } .body > .buttonGroup { margin-bottom: 1em; padding: 0 30px; } .body > .code { background: #efefef; font-weight: 300; font-size: 0.9rem; max-height: 500px; margin: 1em 0; overflow: auto; padding: 2em 2em; } .body > .code ul { background: #efefef !important; } .buttonGroup button { margin-right: 1em; } .tabButton { box-sizing: border-box; color: var(--color5); opacity: 0.4; font-size: 1rem; font-weight: bold; padding: 0 1px 3px; } .tabButton:hover { opacity: 1; } .tabButton:disabled { color: var(--color5); cursor: initial; opacity: 1; border-bottom: 2px solid var(--color5); } .onlyButton.tabButton:disabled { border: none; } ================================================ FILE: packages/ui/src/components/Definitions/index.js ================================================ import React from 'react' import PropTypes from 'prop-types' import './styles.css' const Definitions = ({ title, description, children }) => { return (

{title}

{description &&

{description}

} {children}
) } Definitions.propTypes = { title: PropTypes.string.isRequired, description: PropTypes.string, children: PropTypes.any } export default Definitions ================================================ FILE: packages/ui/src/components/Definitions/styles.css ================================================ .definitions { color: var(--color1); margin: 1.5em 0; } .definitions h1 { font-size: 1em; color: var(--color5); margin-top: 0; margin-bottom: 1em; padding-left: 0; } .definitions > p, .definitions > dl { margin: 0.25em 1em; } .definitions dl dt { color: var(--color3); font-weight: bold; font-style: italic; } .definitions dl dt:after { content: ': '; } .definitions dl > * { display: inline-block; margin-right: 0.5em; } ================================================ FILE: packages/ui/src/components/JsonSchemaViewer/index.js ================================================ import React, { Component } from 'react' import PropTypes from 'prop-types' import JSONSchemaView from 'json-schema-view-js' import 'json-schema-view-js/dist/style.css' import './styles.css' class JsonSchemaViewer extends Component { mountViewer(el, schema) { el.appendChild(new JSONSchemaView(schema, 1).render()) } render({ schema } = this.props) { return (
{ if (el) this.mountViewer(el, schema) }} /> ) } } JsonSchemaViewer.propTypes = { schema: PropTypes.object } export default JsonSchemaViewer ================================================ FILE: packages/ui/src/components/JsonSchemaViewer/styles.css ================================================ .schemaViewer { opacity: 0.6; } ================================================ FILE: packages/ui/src/components/MethodLabel/index.js ================================================ import React from 'react' import PropTypes from 'prop-types' import './styles.css' function MethodLabel({ method }) { const m = method.toLowerCase() return {m.toUpperCase()} } MethodLabel.propTypes = { method: PropTypes.string.isRequired } export default MethodLabel ================================================ FILE: packages/ui/src/components/MethodLabel/styles.css ================================================ .method { margin-right: 5px; font-size: 12px; font-weight: 300; font-style: normal; border: 1px solid #999; color: #999; border-radius: 2px; padding: 2px 3px; display: inline-block; } .method.get { color: #66BB6A; border-color: #66BB6A; } .method.delete { color: #999; border-color: #999; } .method.put { color: #9E9D24; border-color: #9E9D24; } .method.post { color: #F9A825; border-color: #F9A825; } .method.patch { color: #9C27B0; border-color: #9C27B0; } ================================================ FILE: packages/ui/src/components/Navigation/index.js ================================================ import React from 'react' import PropTypes from 'prop-types' import './styles.css' import MethodLabel from '../MethodLabel' const MARKER = '__AGREED-UI-MARKER__' const groupByRequestPath = (list) => { const ret = {} for (let i = 0, len = list.length; i < len; i++) { const item = list[i] const key = `${item.request.path}${MARKER}${item.request.method}` ret[key] = [...(ret[key] || []), item] } return ret } const StatusLabel = ({ status }) => ( {status} ) StatusLabel.propTypes = { status: PropTypes.number.isRequired } const NaviItem = ({ agree }) => (

{agree.title || agree.request.path}

) NaviItem.propTypes = { agree: PropTypes.object.isRequired } const GroupedItem = ({ agree }) => (

{agree.title || 'no title'}

) GroupedItem.propTypes = { agree: PropTypes.object.isRequired } const Details = ({ path, agrees }) => { const [name, method] = path.split(MARKER) return (
{name} {agrees.length > 1 && {agrees.length}} {agrees.map((agree) => ( ))}
) } Details.propTypes = { path: PropTypes.string.isRequired, agrees: PropTypes.array.isRequired } const Grouped = ({ agrees }) => { const grouped = groupByRequestPath(agrees) const pathList = Object.keys(grouped) return ( {pathList.map((path) => (
))} ) } Grouped.propTypes = { agrees: PropTypes.array.isRequired } function Navigation({ agrees, grouped }) { return ( ) } Navigation.propTypes = { agrees: PropTypes.array.isRequired, grouped: PropTypes.bool.isRequired } export default Navigation ================================================ FILE: packages/ui/src/components/Navigation/styles.css ================================================ nav { position: absolute; padding-bottom: 20px; } nav p { margin: 4px 0; } details { padding-bottom: 7px; } summary { color: var(--color5); cursor: pointer; font-size: 0.9rem; margin-bottom: 4px; outline: none; } details > p { padding-left: 1em; margin-top: 3px; } details summary::-webkit-details-marker { transform: translate(0, 1px) scale(0.7); opacity: 0.5; } .count { border-radius: 8px; border: 1px solid var(--color5); background: var(--color5); color: var(--color0); display: inline-block; font-size: 0.5rem; margin-left: 5px; opacity: 0.6; padding: 0px 3px; position: relative; top: -1px; } .statusLabel { border-radius: 6px; color: var(--color0); display: inline-block; font-size: 0.5rem; padding: 2px 4px; margin-right: 0.25rem; opacity: 0.8; } .statusLabel--2 { background: var(--color-green); } .statusLabel--3 { background: var(--color4); } .statusLabel--4 { background: var(--color3); } .statusLabel--5 { background: var(--color5); } ================================================ FILE: packages/ui/src/components/Request/index.js ================================================ import React from 'react' import PropTypes from 'prop-types' import Definitions from '../Definitions' import Body from '../Body' const Request = ({ data }) => { const { method, path, headers_formatted: headers, query, formatted, schema } = data return (
{query && !!Object.keys(query).length && ( {Object.entries(query).map(([k, v]) => (
{k}
{JSON.stringify(v)}
))}
)} {headers && !!Object.keys(headers).length && ( {Object.entries(headers).map(([k, v]) => (
{k}
{JSON.stringify(v)}
))}
)} {formatted && }
) } Request.propTypes = { data: PropTypes.object.isRequired } export default Request ================================================ FILE: packages/ui/src/components/Request/styles.css ================================================ .definitions { color: var(--color1); margin: 1.5em 0; } .definitions h1 { font-size: 1em; color: var(--color5); margin-top: 0; margin-bottom: 1em; padding-left: 0; } .definitions > p, .definitions > dl { margin: 0.25em 1em; } .definitions dl dt { color: var(--color3); font-weight: bold; font-style: italic; } .definitions dl dt:after { content: ': '; } .definitions dl > * { display: inline-block; margin-right: 0.5em; } ================================================ FILE: packages/ui/src/components/Response/index.js ================================================ import React from 'react' import PropTypes from 'prop-types' import Definitions from '../Definitions' import Body from '../Body' const Response = ({ data }) => { const { status, formatted, schema, flowtype } = data return (
{formatted && }
) } Response.propTypes = { data: PropTypes.object.isRequired } export default Response ================================================ FILE: packages/ui/src/components/Response/styles.css ================================================ .definitions { color: var(--color1); margin: 1.5em 0; } .definitions h1 { font-size: 1em; color: var(--color5); margin-top: 0; margin-bottom: 1em; padding-left: 0; } .definitions > p, .definitions > dl { margin: 0.25em 1em; } .definitions dl dt { color: var(--color3); font-weight: bold; font-style: italic; } .definitions dl dt:after { content: ': '; } .definitions dl > * { display: inline-block; margin-right: 0.5em; } ================================================ FILE: packages/ui/src/index.css ================================================ @import url('https://fonts.googleapis.com/css?family=Open+Sans:400,400i|PT+Sans'); /* RGB */ :root { --color: rgba(26, 40, 31, 1); --color-link: rgba(26, 40, 31, 0.7); --color0: white; --color1: rgba(26, 40, 31, 0.6); --color2: rgba(99, 82, 85, 1); --color3: rgba(206, 123, 145, 1); --color4: rgba(192, 232, 249, 1); --color5: rgba(164, 191, 189, 1); --color-green: #66BB6A; } * { margin: 0; padding: 0; } a { color: var(--color-link); } a:hover { color: #bbb; } h1 { font-size: 1.5rem; margin: 2rem 0; } h2 { font-size: 1.2rem; margin: 2rem 0 1rem; } body { color: var(--color); display: flex; font-family: -apple-system, BlinkMacSystemFont, "Helvetica Neue", "Segoe UI", sans-serif; flex-direction: column; min-height: 100vh; } ================================================ FILE: packages/ui/src/index.js ================================================ import React from 'react' import ReactDOM from 'react-dom' import './index.css' import App from './components/App' ReactDOM.render(, document.getElementById('root')) ================================================ FILE: packages/ui/test/agrees/agrees.js ================================================ module.exports = [ { title: 'getHogeFuga', description: 'hogefuga', request: { path: '/hoge/fuga', method: 'GET', query: { q: 'foo', }, }, response: { headers: { 'x-csrf-token': 'csrf-token', }, body: { message: 'hello world', }, }, }, './hoge/foo.json', './foo/bar.yaml', { request: require('./qux/request.json'), response: require('./qux/response.json'), }, { request: { path: '/path/:id', method: 'GET', // value for test client values: { id: 'yosuke', }, }, response: { headers: { 'x-csrf-token': 'csrf-token', }, body: { // :id is for request value message: 'hello {:id}', }, }, }, { request: { path: '/path/:id', method: 'POST', // query embed data, any query is ok. query: { meta: "{:meta}", }, body: { message: "{:message}" }, // value for test client values: { id: 'yosuke', meta: true, message: 'foobarbaz' }, }, response: { headers: { 'x-csrf-token': 'csrf-token', }, body: { // :id is for request value message: 'hello {:id}, {:meta}, {:message}', }, }, }, { request: { // if no method then GET path: '/nyan/:id', query: { meta: "{:meta}", }, // value for test client values: { id: 'yosuke', meta: false, }, }, response: { headers: { 'x-csrf-token': 'csrf-token', }, body: { // :id is for request value message: 'hello {:id}, {:meta}', }, }, }, { request: { path: '/embed/from/response/:id', method: 'POST', query: { meta: "{:meta}", }, body: { message: '{:message}' }, // value for test client values: { id: 'yosuke', meta: false, message: 'this is a message', }, }, response: { headers: { 'x-csrf-token': 'csrf-token', }, body: { // embed template from response values image: '{:image}', topics: '{:topics}', message: 'hello {:id} {:meta} {:message}', }, values: { image: 'http://imgfp.hotp.jp/SYS/cmn/images/front_002/logo_hotopepper_264x45.png', topics: [ { a: 'a' }, { b: 'b' } ], } }, }, { request: { path: '/images/:id', method: 'GET', query: { q: '{:someQueryStrings}', }, values: { id: 'yosuke', someQueryStrings: 'foo' }, }, response: { headers: { 'x-csrf-token': 'csrf-token', }, body: { message: '{:greeting} {:id} {:someQueryStrings}', images: '{:images}', themes: '{:themes}', }, values: { greeting: 'hello', images: [ 'http://example.com/foo.jpg', 'http://example.com/bar.jpg', ], themes: { name: 'green', }, } }, }, { request: { path: '/list/:index', method: 'GET', values: { index: 1 } }, response: { body: { result : '{:list[:index]}' }, values: { list: [ 'hello', 'hi', 'dunke', ] } }, }, { request: { path: '/useschema/:index', method: 'GET', values: { index: 1 } }, response: { body: { result : '{:list[:index]}' }, schema: { type: 'object', properties: { result: { type: 'string' } }, }, values: { list: [ 'hello', 'hi', 'dunke', ] } }, }, { request: { path: '/useschema/withstring/:index', method: 'GET', values: { index: 1 } }, response: { body: { result : '{:list[:index]}' }, schema: './schema/hi.json', values: { list: [ 'hello', 'hi', 'dunke', ] } }, }, { request: { path: '/headers/:index', method: 'GET', headers: { 'x-token': '{:token}', 'x-api-key': '{:apiKey}', }, values: { index: 2, token: 'nyan', apiKey: 'nyaaan' }, }, response: { body: { result : '{:list[:index]} {:token} {:apiKey}' }, values: { list: [ 'hello', 'hi', 'dunke', ] } }, }, { request: { path: '/headers/:index', method: 'GET', values: { index: 1, }, }, response: { body: { result : '{:list[:index]}' }, values: { list: [ 'hello', 'hi', 'dunke', ] } }, }, { request: { path: '/headers/test/:index', method: 'GET', headers: { 'x-test-token': '{:xTestToken}' }, values: { index: 1, xTestToken: 'fdajfdsaoijfdoajofdjaoj', }, }, response: { body: { result : '{:list[:index]}' }, values: { list: [ 'hello', 'hi', 'dunke', ] } }, }, ] ================================================ FILE: packages/ui/test/agrees/agrees.json5 ================================================ [ { request: { path: '/hoge/fuga', method: 'GET', query: { q: 'foo', }, }, response: { headers: { 'x-csrf-token': 'csrf-token', }, body: { message: 'hello world', }, }, }, './hoge/foo.json', './foo/bar.yaml', { request: './qux/request.json', response: './qux/response.json', }, { request: { path: '/path/:id', method: 'GET', // value for test client values: { id: 'yosuke', }, }, response: { headers: { 'x-csrf-token': 'csrf-token', }, body: { // :id is for request value message: 'hello {:id}', }, }, }, { request: { path: '/path/:id', method: 'POST', // query embed data, any query is ok. query: { meta: "{:meta}", }, body: { message: "{:message}" }, // value for test client values: { id: 'yosuke', meta: true, message: 'foobarbaz' }, }, response: { headers: { 'x-csrf-token': 'csrf-token', }, body: { // :id is for request value message: 'hello {:id}, {:meta}, {:message}', }, }, }, { request: { path: '/path/header/format', method: 'GET', }, response: { headers: { 'access-control-allow-origin': '{:acao}' }, body: { message: 'hello', }, values: { 'acao': '*' } }, }, { request: { path: '/path/default/header/', method: 'GET', }, response: { body: { message: 'hello', }, }, }, { request: { path: '/path/default/request/header/', method: 'GET', headers: { 'x-forwarded-for': 'forward' }, }, response: { body: { message: 'forward', }, }, }, { request: { path: '/test/case/insensitive/headers', method: 'GET', headers: { 'This-Headers-Should-Be-Lower-Case': 'true' }, }, response: { body: { message: 'hello case insensitive headers', }, }, }, { request: { path: '/test/null/agreed/values', }, response: { body: { messages: [ { message: '{:messages.0.message}' }, '{:messages.1-last}' ], }, values: { messages: [ { message: null }, { message: 'test' }, ] }, }, }, { request: { path: '/test/randomstring/agreed/values', }, response: { body: { random: '{randomString:random}' }, values: { random: 8 }, }, }, { request: { path: '/test/randomint/agreed/values', }, response: { body: { random: '{randomInt:random}' }, values: { random: '1-1000' }, }, }, { request: { path: '/test/parseint/agreed/values/:id', }, response: { body: { id: '{parseInt:id}' }, }, }, { request: { path: '/test/agreed/messages', method: 'POST', body: { messages: [ { message: '{:messages.0.message}' }, '{:messages.1-last}' ] }, values: { messages: [ { message: null }, { message: 'test' }, ] } }, response: { status: 201, body: { results: '{:messages}' }, values: { messages: [ { message: '{:message0}' }, { message: 'test' }, ] } }, }, { request: { path: '/test/agreed/use/null/obj', method: 'POST', body: { test: '{:test}' }, values: { test: null, } }, response: { status: 201, body: { results: '{:test}' }, }, }, { request: { path: '/test/bind/nest/object', method: 'POST', body: { time: { start: '{:time.start}', end: '{:time.end}', break: { start: '{:time.break.start}', end: '{:time.break.end}', } }, members: [ { id: '{:members.0.id}' }, '{:members.1-last}' ] } }, response: { body: { time: { start: '{:time.start}', end: '{:time.end}' }, break: { start: '{:time.break.start}', end: '{:time.break.end}' }, members: [ { id: '{:id}' }, '{:members.1-last}' ] }, }, }, { request: { path: '/test/unixtime/agreed/values', }, response: { body: { unixtime: '{unixtime:time}', }, values: { time: 12344556677, } }, }, ] ================================================ FILE: packages/ui/test/agrees/foo/bar.yaml ================================================ request: path: '/foo/:bar' method: 'PUT' body: a: 'b' c: 'd' response: headers: Content-Type: 'text' body: 'hello put' ================================================ FILE: packages/ui/test/agrees/hoge/foo.json ================================================ { 'request': { method: 'POST', path: '/hoge/:foo', body: { message: 'foobarbaz', }, }, response: { body: { message: 'hello post', }, }, } ================================================ FILE: packages/ui/test/agrees/hoge/fuga/_agree.json ================================================ { request: { method: 'GET', }, response: { status: 200, // default is 200 body: { message: 'hello world' } } } ================================================ FILE: packages/ui/test/agrees/hoge/fuga/request.json ================================================ { } ================================================ FILE: packages/ui/test/agrees/index.js ================================================ module.exports = [ './hoge/foo.json', './foo/bar.yaml' ] ================================================ FILE: packages/ui/test/agrees/notify.js ================================================ module.exports = [ { request: { path: "/messages", method: "POST", body: { message: "{:message}" }, values: { message: "test" } }, response: { body: { result: "{:message}" }, values: { message: "test" }, notify: { event: "message2", body: { message: "message! {:message}" } } } }, { request: { path: "/messages2", method: "POST", body: { message: "{:message}" }, values: { message: "test" } }, response: { body: { result: "{:message}" }, values: { message: "test" }, notify: { body: { message: "message2 {:message}" } } } }, { request: { path: "/long_long_long_path/messages3", method: "POST", body: { message: "{:message}" }, values: { message: "test" } }, response: { body: { result: "{:message}" }, values: { message: "test" }, notify: { body: { message: "message2 {:message}" } } } } ]; ================================================ FILE: packages/ui/test/agrees/qux/request.json ================================================ { path: '/qux/fuga', method: 'DELETE', } ================================================ FILE: packages/ui/test/agrees/qux/response.json ================================================ { status: 204, } ================================================ FILE: packages/ui/test/agrees/schema/hi.json ================================================ { type: 'object', properties: { result: { type: 'string' } }, } ================================================ FILE: packages/ui/test/agrees/sub.js ================================================ module.exports = (a, b) => a - b ================================================ FILE: renovate.json ================================================ { "extends": [ "config:base" ] }